screen3
Python functions for using SCREEN3.
screen3.run
– wrapper to quickly create a run without needing to go through the promptscreen3.read
– load the output into a pandas DataFramescreen3.plot
– some plotting routines using the output DataFrame
Most of the variables related to SCREEN3 are the same as the ones in the SCREEN3 input and output files, including that they are in uppercase.
Python 3.6+ is required
1# -*- coding: utf-8 -*- 2""" 3Python functions for using 4[SCREEN3](https://www.epa.gov/scram/air-quality-dispersion-modeling-screening-models#screen3). 5* `screen3.run` – wrapper to quickly create a run without needing to go through the prompt 6* `screen3.read` – load the output into a pandas DataFrame 7* `screen3.plot` – some plotting routines using the output DataFrame 8 9Most of the variables related to SCREEN3 are the same as the ones in the SCREEN3 input and output files, 10including that they are in uppercase. 11 12`screen3` lives [on GitHub](https://github.com/zmoon/screen3). 13 14.. note:: 15 Python 3.6+ is required 16""" 17# TODO: type annotations for the main user-facing fns? 18 19import os 20from pathlib import Path 21import platform 22import subprocess 23import sys 24import warnings 25 26import numpy as np 27 28__version__ = "0.1.2" 29 30__all__ = ( 31 "run", 32 "read", 33 "plot", 34 "download", 35 "build", 36 "load_example", 37 "SCREEN_OUT_COL_UNITS_DICT", 38 "DEFAULT_SRC_DIR", 39) 40 41 42# Check for Python 3.6+ 43# Note: `sys.version_info` became a named tuple in 3.1 44if not (sys.version_info.major >= 3 and sys.version_info.minor >= 6): 45 raise Exception(f"Python version must be 3.6+.\nYours is {sys.version}") 46 47 48_THIS_DIR = Path(__file__).parent 49 50 51# DEFAULT_SRC_DIR = Path.home() / ".local/screen3/src" 52DEFAULT_SRC_DIR = "./src" 53"""Default directory in which to place the SCREEN3 source code from EPA.""" 54 55_DEFAULT_EXE_PATH = f"{DEFAULT_SRC_DIR}/SCREEN3.exe" 56 57 58def download(*, src=DEFAULT_SRC_DIR): 59 """Download the SCREEN3 zip from EPA and extract to directory `src`. 60 61 If it fails, you can always download it yourself some other way. 62 63 <https://gaftp.epa.gov/Air/aqmg/SCRAM/models/screening/screen3/screen3.zip> 64 65 Parameters 66 ---------- 67 src : path-like 68 Where to extract the files to. 69 .. note:: 70 As long as this isn't modified, the `src`/`exe` keyword doesn't need to be changed 71 for `build`, `run`, or `load_example`. 72 """ 73 import io 74 import zipfile 75 from pathlib import Path 76 77 import requests 78 79 url = "https://gaftp.epa.gov/Air/aqmg/SCRAM/models/screening/screen3/screen3.zip" 80 81 extract_to = Path(src) 82 extract_to.mkdir(exist_ok=True, parents=True) 83 84 r = requests.get(url, verify=False) # TODO: get it working without having to disable certificate verification 85 with zipfile.ZipFile(io.BytesIO(r.content)) as z: 86 for info in z.infolist(): 87 with z.open(info) as zf, open(extract_to / info.filename, "wb") as f: 88 f.write(zf.read()) 89 90 91_SCREEN3A_PATCH = """ 92386,397c386,401 93< $IF DEFINED (LAHEY) 94< CALL DATE(RUNDAT) 95< CALL TIME(RUNTIM) 96< $ELSE 97< CALL GETDAT(IPTYR, IPTMON, IPTDAY) 98< CALL GETTIM(IPTHR, IPTMIN, IPTSEC, IPTHUN) 99< C Convert Year to Two Digits 100< IPTYR = IPTYR - 100*INT(IPTYR/100) 101< C Write Date and Time to Character Variables, RUNDAT & RUNTIM 102< WRITE(RUNDAT,'(2(I2.2,1H/),I2.2)') IPTMON, IPTDAY, IPTYR 103< WRITE(RUNTIM,'(2(I2.2,1H:),I2.2)') IPTHR, IPTMIN, IPTSEC 104< $ENDIF 105--- 106> !$IF DEFINED (LAHEY) 107> ! CALL DATE(RUNDAT) 108> ! CALL TIME(RUNTIM) 109> !$ELSE 110> ! CALL GETDAT(IPTYR, IPTMON, IPTDAY) 111> ! CALL GETTIM(IPTHR, IPTMIN, IPTSEC, IPTHUN) 112> !C Convert Year to Two Digits 113> ! IPTYR = IPTYR - 100*INT(IPTYR/100) 114> !C Write Date and Time to Character Variables, RUNDAT & RUNTIM 115> ! WRITE(RUNDAT,'(2(I2.2,1H/),I2.2)') IPTMON, IPTDAY, IPTYR 116> ! WRITE(RUNTIM,'(2(I2.2,1H:),I2.2)') IPTHR, IPTMIN, IPTSEC 117> !$ENDIF 118> 119> ! here for GNU (zm) 120> call DATE_AND_TIME(DATE=RUNDAT, TIME=RUNTIM) 121> 122""".lstrip() 123 124 125_DEPVAR_PATCH = """ 12635c35 127< 128\ No newline at end of file 129--- 130> 131""".lstrip() 132 133 134def _dos2unix(fp): 135 """Convert CRLF line endings (if any) to LF.""" 136 with open(fp, "r") as f: 137 s = f.read() 138 with open(fp, "w", newline="\n") as f: 139 f.write(s) 140 141 142def build(*, src=DEFAULT_SRC_DIR): 143 """Build the SCREEN3 executable by pre-processing the sources and compiling with GNU Fortran. 144 145 .. note:: 146 Requires `patch` and `gfortran` on PATH. 147 148 Parameters 149 ---------- 150 src : path-like 151 Source directory, containing `SCREEN3A.FOR` etc., e.g., downloaded using `screen3.download`. 152 """ 153 cwd = Path.cwd() 154 bld = Path(src) 155 156 os.chdir(bld) 157 158 srcs = ['SCREEN3A.FOR', 'SCREEN3B.FOR', 'SCREEN3C.FOR'] 159 160 # Fix line endings 161 if platform.system() != "Windows": 162 # subprocess.run(['dos2unix'] + srcs) 163 for src in srcs: 164 _dos2unix(src) 165 166 # Patch code 167 with open("SCREEN3A.FOR.patch", "w") as f: 168 f.write(_SCREEN3A_PATCH) 169 with open("DEPVAR.INC.patch", "w") as f: 170 f.write(_DEPVAR_PATCH) 171 subprocess.run(['patch', 'SCREEN3A.FOR', 'SCREEN3A.FOR.patch']) 172 subprocess.run(['patch', 'DEPVAR.INC', 'DEPVAR.INC.patch']) 173 174 # Compile 175 subprocess.run(['gfortran', '-cpp'] + srcs + ['-o', 'SCREEN3.exe']) 176 177 os.chdir(cwd) 178 179 180# note that U10M becomes UHANE in non-regulatory mode 181SCREEN_OUT_COL_NAMES = 'DIST CONC STAB U10M USTK MIX_HT PLUME_HT SIGMA_Y SIGMA_Z DWASH'.split() 182SCREEN_OUT_COL_UNITS = ['m', 'μg/m$^3$', '', 'm/s', 'm/s', 'm', 'm', 'm', 'm', ''] 183SCREEN_OUT_COL_UNITS_DICT = dict(zip(SCREEN_OUT_COL_NAMES, SCREEN_OUT_COL_UNITS)) 184"""Dict of units for the outputs to be used in the plots. For example, `'DIST': 'm'`.""" 185 186 187def read( 188 fp, 189 *, 190 t_run=None, 191 run_inputs=None, 192): 193 """Read and extract data from a SCREEN3 run (i.e., a `SCREEN.OUT` file). 194 195 Parameters 196 ---------- 197 fp : str, pathlib.Path 198 File path to the `SCREEN.OUT` file, 199 e.g., `'./screen3/SCREEN.OUT'`. 200 t_run : datetime.datetime 201 Time of the run. 202 If the output file (`SCREEN.OUT`) is older than this, the run did not complete successfully. 203 run_inputs : dict, optional 204 `run` passes this so that we can store what the inputs to the Python function were for this run, 205 though `SCREEN.OUT` should have much of the same info. 206 207 Returns 208 ------- 209 df : pd.DataFrame 210 SCREEN3 output dataset. 211 212 If the pandas version has [`attrs`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.attrs.html), 213 `df.attrs` will include the following: 214 * `'units'`: (dict) *keys*: the variables names, *values*: unit strings (for use in plots) 215 * `'SCREEN_OUT'`: (str) the SCREEN3 output file as a string 216 * `'SCREEN_DAT'`: (str) the SCREEN3 DAT (copied inputs) file as a string 217 * `'run_inputs'`: (dict) copy of the inputs to the the `run` function used to generated this dataset (if applicable) 218 """ 219 import datetime 220 221 import pandas as pd 222 223 if run_inputs is None: 224 run_inputs = {} 225 226 # If we have `t_run`, first check that the file has indeed been modified 227 p = Path(fp) 228 t_out_modified = datetime.datetime.fromtimestamp(p.stat().st_mtime) 229 if t_run is not None and p.is_file(): # i.e., t_run has been specified 230 if (t_run - t_out_modified) > datetime.timedelta(seconds=1): 231 raise ValueError( 232 "`SCREEN.OUT` is older than time of the run." 233 " This probably means that SCREEN failed to run because it didn't like the value of one or more inputs." 234 " Or some other reason.") 235 236 # Read lines 237 with open(fp, 'r') as f: 238 lines_raw = f.readlines() 239 lines_stripped = [line.strip() for line in lines_raw] 240 241 # TODO: could also read other info from the file like the run settings at the top 242 243 iheader = [i for i, s in enumerate(lines_stripped) if s.split()[:2] == ['DIST', 'CONC']] 244 if not iheader: 245 raise ValueError( 246 "Data table not found in output. " 247 "This probably means that SCREEN failed to complete the run because " 248 "it didn't like the value of one or more inputs. " 249 "Or some other reason." 250 ) 251 252 if len(iheader) > 1: 253 print(f"Multiple sets of data found. Only loading the first one.") 254 iheader = iheader[0] 255 256 # Find first blank after iheader 257 iblankrel = lines_stripped[iheader:].index("") 258 iblank = iheader + iblankrel 259 260 # Could (technically) read these from file instead 261 col_names = SCREEN_OUT_COL_NAMES 262 col_units = SCREEN_OUT_COL_UNITS 263 264 # Load into pandas 265 # Note: at some point `pd.read_table` was deprecated but seems like that was undone? 266 n_X = iblank - iheader - 2 267 df = pd.read_table(fp, 268 sep=None, skipinitialspace=True, engine='python', 269 keep_default_na=False, 270 skiprows=iheader+2, nrows=n_X) 271 df.columns = col_names 272 273 units_dict = dict(zip(col_names, col_units)) 274 275 p_dat = p.parents[0] / f'{p.stem}.DAT' 276 try: 277 with open(p_dat, 'r') as f: 278 s_screen_dat = f.read() 279 except FileNotFoundError: 280 s_screen_dat = None 281 282 # Assign attrs to df (pandas 1.0+) 283 if hasattr(df, "attrs"): 284 df.attrs.update({ 285 'SCREEN_DAT': s_screen_dat, 286 'SCREEN_OUT': ''.join(lines_raw), 287 'units': units_dict, 288 'run_inputs': run_inputs 289 }) 290 291 return df 292 293 294def run( 295 *, 296 RUN_NAME='A point-source SCREEN3 run', 297 Q=100.0, 298 HS=30.0, 299 DS=10.0, 300 VS=2.0, 301 TS=310.0, 302 TA=293.0, 303 ZR=1.0, 304 # 305 X=np.append(np.r_[1.0], np.arange(50.0, 500.0+50.0, 50.0)), 306 # 307 IMETEO=3, 308 ISTAB=4, 309 WS=5.0, # there is a max! 310 # 311 U_or_R='R', 312 # 313 DOWNWASH_YN='N', 314 HB=30.0, # BUILDING HEIGHT (M) 315 HL=10.0, # MINIMUM HORIZ. BUILDING DIMENSION (M) 316 HW=20.0, # MAXIMUM HORIZ. BUILDING DIMENSION (M) 317 # 318 exe=_DEFAULT_EXE_PATH, 319): 320 """Create SCREEN3 input file, feed it to the executable, and load the result. 321 322 .. note:: 323 Inputs must be specified as keyword arguments, but can be entered in any order (non-positional). 324 325 Parameters 326 ---------- 327 Q : float 328 Emission rate (g/s). 329 HS : float 330 Stack height (m). 331 DS : float 332 Stack inside diameter (m). 333 VS : float 334 Stack gas exit velocity (m/s). 335 TS : float 336 Stack gas temperature (K). 337 TA : float 338 Ambient air temperature (K). 339 ZR : float 340 Receptor height above ground (flagpole receptor) (M). 341 X : array_like 342 Array of downwind distances at which to compute the outputs (m). 343 IMETEO : int, {1, 2, 3} 344 1. Full (supplying stability class + WS for each grid point), 345 or SCREEN runs for each STAB option??... 346 2. Single stability class 347 3. Stability class + WS 348 ISTAB : int 349 Stability class. 350 1 (= A) to 6 (= F). 351 WS : float 352 Mean wind speed at 10 m (m/s). 353 .. warning:: 354 The run will fail if you exceed the maximum that SCREEN3 allows! 355 U_or_R : str {'U', 'R'} 356 Urban (U) or rural (R). 357 DOWNWASH_YN : str {'Y', 'N'} 358 whether to apply building downwash calculations 359 the building dimension parameters do nothing if it is 'N' 360 HB : float 361 Building height (m). 362 HL : float 363 Minimum horizontal building dimension (m). 364 HW : float 365 Maximum horizontal building dimension (m). 366 exe : path-like 367 Path to the executable to use, e.g., as a `str` or `pathlib.Path`. 368 369 Examples 370 -------- 371 Change parameters. 372 ```python 373 screen3.run(TA=310, WS=2) 374 ``` 375 Specify executable to use if yours isn't in the default place (`./src/SCREEN3.exe`). 376 ```python 377 screen3.run(exe="/path/to/executable") 378 ``` 379 380 Returns 381 ------- 382 df : pd.DataFrame 383 Results dataset, read from the `SCREEN.OUT` by `read`. 384 385 Notes 386 ----- 387 The SCREEN3 program parses the inputs and makes a copy called `SCREEN.DAT`. 388 Upon running, it produces an output file called `SCREEN.OUT`. 389 Both of these will be in the source directory, where the executable resides. 390 """ 391 inputs = locals() # collect inputs for saving in the df 392 393 import datetime 394 395 # TODO: should validate wind speed? 396 397 # Check exe is file 398 exe = Path(exe) 399 if not exe.is_file(): 400 raise ValueError( 401 f"{exe.absolute()!r} does not exist or is not a file. " 402 "Use the `exe` keyword argument of this function to specify the correct path." 403 ) 404 405 # Check for H changes without downwad 406 H_defaults = (30.0, 10.0, 20.0) # keep in sync with fn defaults 407 if any(x0 != x for x0, x in zip(H_defaults, [HB, HL, HW])) and DOWNWASH_YN == 'N': 408 print("Note: You have modified a building parameter, but downwash is not enabled.") 409 410 # Check x positions 411 X = np.asarray(X) 412 if X.size > 50: 413 raise ValueError('SCREEN3 does not support inputting more than 50 distance values') 414 415 # ------------------------------------------------------ 416 # Other parameters 417 # For now the user cannot set these 418 # although they are required inputs to the model 419 420 I_SOURCE_TYPE = 'P' # point source 421 422 # Complex terrain also currently not used 423 # (assumed flat) 424 TERRAIN_1_YN = 'N' # complex terrain screen above stack height 425 TERRAIN_2_YN = 'N' # simple terrain above base 426 # HTER = 10. # MAXIMUM TERRAIN HEIGHT ABOVE STACK BASE (M) 427 # ^ not needed if not using the terrain settings 428 # at least for simple terrain, seems to just run the x positions for different heights 429 430 # Currently user input of distances is required 431 # i.e., automated option not used 432 AUTOMATED_DIST_YN = 'N' 433 DISCRETE_DIST_YN = 'Y' 434 435 # Final questions 436 FUMIG_YN = 'N' 437 PRINT_YN = 'N' 438 439 # ------------------------------------------------------ 440 # Create the input file 441 442 def s_METEO(): 443 if IMETEO == 1: # A range (depending on U/R, type of source, etc.) of WS and STABs are examined to find maximum impact 444 # print(f"* Note that IMETEO={IMETEO!r} isn't really implemented presently..." 445 # "\nExamine the WS and STAB in the model output to see what happened.") 446 return '1' 447 elif IMETEO == 2: 448 return f'{IMETEO}\n{ISTAB}' 449 elif IMETEO == 3: 450 return f'{IMETEO}\n{ISTAB}\n{WS}' 451 else: 452 raise ValueError(f'invalid `IMETEO` {IMETEO!r}. Must be in {{1, 2, 3}}') 453 454 def s_X(): 455 vals = X.astype(str).tolist() 456 return '\n'.join(vals + ['0.0']) 457 458 def s_building_props(): 459 if DOWNWASH_YN == 'Y': 460 return f'{HB}\n{HL}\n{HW}' 461 else: 462 return "" 463 464 dat_text = f""" 465{RUN_NAME} 466{I_SOURCE_TYPE} 467{Q} 468{HS} 469{DS} 470{VS} 471{TS} 472{TA} 473{ZR} 474{U_or_R} 475{DOWNWASH_YN} 476{s_building_props()} 477{TERRAIN_1_YN} 478{TERRAIN_2_YN} 479{s_METEO()} 480{AUTOMATED_DIST_YN} 481{DISCRETE_DIST_YN} 482{s_X()} 483{FUMIG_YN} 484{PRINT_YN} 485 """.strip() + "\n" 486 # ^ need newline at end or Fortran complains when passing text on the cl 487 488 # TODO: optionally write the input text file to disk in cwd 489 # ifn = 'screen3_input.txt' # input filename 490 # ifp = cwd / ifn 491 # with open(ifn, 'w') as f: 492 # f.write(dat_text) 493 494 # ------------------------------------------------------ 495 # Run the SCREEN executable 496 497 t_utc_run = datetime.datetime.now() 498 499 s_exe = str(exe.absolute()) 500 cwd = Path.cwd() 501 src_dir = exe.parent 502 503 # Move to src location so that the output file will be saved there 504 os.chdir(src_dir) 505 506 # Invoke executable 507 subprocess.run( 508 args=[s_exe], 509 input=dat_text, 510 universal_newlines=True, # equivalent to `text=True`, but that was added in 3.7 511 check=True, 512 shell=True, 513 stdout=subprocess.DEVNULL, 514 stderr=subprocess.STDOUT, 515 ) 516 517 # Return to cwd 518 os.chdir(cwd) 519 520 # Read and parse the output 521 df = read(src_dir/'SCREEN.OUT', t_run=t_utc_run, run_inputs=inputs) 522 523 return df 524 525 526def _add_units(x_units, y_units, *, ax=None): 527 """Add units and make room.""" 528 import matplotlib.pyplot as plt 529 530 if ax is None: 531 ax = plt.gca() 532 xl = ax.get_xlabel() 533 yl = ax.get_ylabel() 534 sxu = f'({x_units})' if x_units else '' 535 syu = f'({y_units})' if y_units else '' 536 ax.set_xlabel(f'{xl} {sxu}' if xl else sxu) 537 ax.set_ylabel(f'{yl} {syu}' if xl else syu) 538 539 540def plot( 541 df, 542 *, 543 labels=None, 544 yvals=None, 545 yvar="", 546 yvar_units="", 547 plot_type='line', 548 ax=None, 549 **pyplot_kwargs): 550 """Plot concentration. 551 552 The first argument (positional) can be one `df` or a list of `df`s. 553 If you pass a list of `df`s, you can also pass `labels`. 554 555 The default plot type is `'line'`, but 2-D plots `'pcolor'` and `'contourf'` are also options. 556 Unless you additionally pass `yvals`, they will be plotted as if the labels were equally spaced. 557 558 Parameters 559 ---------- 560 df : pd.DataFrame or list of pd.DataFrame 561 Data extracted from the `SCREEN.OUT` of the run(s) using `read`. 562 labels : array_like 563 Labels for the separate cases. 564 Used if input `df` is a list instead of just one dataset. 565 Currently we assume that the first part of the label is the variable name that is being varied. 566 yvals : array_like 567 Optional positions of labels. 568 yvar : str 569 *y* variable name. 570 Only used if labels not provided. 571 yvar_units : str 572 Units to use when labeling `yvar`. 573 Only used if `labels` not provided. 574 plot_type : str {'line', 'pcolor', 'contourf'} 575 The type of plot to do. 576 ax : plt.Axes 577 If an existing ax is not passed, we create a new figure (and ax). 578 Else, we plot on the existing ax. 579 **pyplot_kwargs 580 Passed on to the relevant pyplot plotting function, 581 e.g., `ax.plot`. 582 """ 583 import matplotlib as mpl 584 import matplotlib.pyplot as plt 585 import pandas as pd 586 587 if labels is None: 588 labels = [] 589 if yvals is None: 590 yvals = [] 591 592 units = SCREEN_OUT_COL_UNITS_DICT 593 # or `df.attrs['units']` 594 595 if not isinstance(df, (pd.DataFrame, list)): 596 raise TypeError(f"df is not a `pd.DataFrame` or `list`, it is type `{type(df).__name__}`") 597 598 if plot_type not in ['line', 'pcolor', 'contourf']: 599 raise ValueError(f"`plot_type` {plot_type!r} not allowed") 600 601 if ax is None or not isinstance(ax, plt.Axes): 602 fig, ax = plt.subplots() 603 else: 604 fig = plt.gcf() 605 606 # determine number of points 607 if isinstance(df, list): 608 n = df[0].index.size 609 else: 610 n = df.index.size 611 612 # some line styling 613 if n > 20: 614 line_style = '.-' 615 ms = 6 616 else: 617 line_style = 'o-' 618 ms = 5 619 pyplot_kwargs.update({ 620 'markersize': ms 621 }) 622 623 if isinstance(df, list): 624 if not list(labels): 625 labels = list(range(1, len(df)+1)) 626 627 if plot_type == 'line': 628 with mpl.rc_context({'legend.fontsize': 'small'}): 629 for i, dfi in enumerate(df): 630 dfi.plot(x='DIST', y='CONC', style=line_style, ax=ax, label=labels[i], **pyplot_kwargs) 631 632 else: # 2-D plot options 633 ax.set_xlabel('DIST') 634 yvals = np.asarray(yvals) 635 if yvals.any(): 636 Y = yvals 637 638 # yvar_guess = labels[0].split()[0] 639 # try: 640 # yvar_units = units[yvar_guess] 641 # except KeyError: 642 # raise ValueError(f"Y variable guess {yvar_guess!r} based on labels passed not found in allowed list." 643 # " Make sure that the first part of each label is the variable, followed by a space.") 644 # ax.set_ylabel(yvar_guess) 645 646 if labels: 647 ax.set_yticks(yvals) 648 ax.set_yticklabels(labels) 649 650 ax.set_ylabel(yvar) 651 _add_units(units['DIST'], yvar_units, ax=ax) 652 653 else: 654 Y = labels 655 _add_units(units['DIST'], '', ax=ax) 656 657 X = df[0].DIST 658 # Y = yvals if yvals.any() else labels 659 Z = np.stack([dfi['CONC'] for dfi in df]) 660 661 if plot_type == 'pcolor': 662 im = ax.pcolormesh(X, Y, Z, **pyplot_kwargs) 663 664 elif plot_type == 'contourf': 665 im = ax.contourf(X, Y, Z, **pyplot_kwargs) 666 667 cb = fig.colorbar(im) 668 669 cb.set_label(f"CONC ({units['CONC']})") 670 671 else: # just one to plot 672 if plot_type != 'line': 673 raise ValueError("With only one run, only `plot_type` 'line' can be used") 674 df.plot(x='DIST', y='CONC', style=line_style, ax=ax, **pyplot_kwargs) 675 676 if plot_type == 'line': 677 ax.set_ylabel('CONC') 678 _add_units(units['DIST'], units['CONC'], ax=ax) # use the units dict to look up the units 679 680 # ax.set_xlim(xmin=0, xmax=) 681 ax.autoscale(axis='x', tight=True) 682 683 fig.tight_layout() 684 685 return fig 686 687 688def load_example(s, *, src=DEFAULT_SRC_DIR): 689 """Load one of the examples included with the `screen3.zip` download, 690 such as `'EXAMPLE.OUT'` from SCREEN3 source directory `src`. 691 692 Examples 693 -------- 694 ```python 695 screen3.load_example("EXAMPLE.OUT") 696 ``` 697 """ 698 valid_examples = [ 699 "EXAMPLE.OUT", "examplenew.out", "examplnrnew.out", 700 "EXAMPNR.OUT", "exampnrnew.out", 701 ] 702 if s not in valid_examples: 703 raise ValueError(f"invalid example file name. Valid options are: {valid_examples}") 704 705 return read(Path(src) / s)
295def run( 296 *, 297 RUN_NAME='A point-source SCREEN3 run', 298 Q=100.0, 299 HS=30.0, 300 DS=10.0, 301 VS=2.0, 302 TS=310.0, 303 TA=293.0, 304 ZR=1.0, 305 # 306 X=np.append(np.r_[1.0], np.arange(50.0, 500.0+50.0, 50.0)), 307 # 308 IMETEO=3, 309 ISTAB=4, 310 WS=5.0, # there is a max! 311 # 312 U_or_R='R', 313 # 314 DOWNWASH_YN='N', 315 HB=30.0, # BUILDING HEIGHT (M) 316 HL=10.0, # MINIMUM HORIZ. BUILDING DIMENSION (M) 317 HW=20.0, # MAXIMUM HORIZ. BUILDING DIMENSION (M) 318 # 319 exe=_DEFAULT_EXE_PATH, 320): 321 """Create SCREEN3 input file, feed it to the executable, and load the result. 322 323 .. note:: 324 Inputs must be specified as keyword arguments, but can be entered in any order (non-positional). 325 326 Parameters 327 ---------- 328 Q : float 329 Emission rate (g/s). 330 HS : float 331 Stack height (m). 332 DS : float 333 Stack inside diameter (m). 334 VS : float 335 Stack gas exit velocity (m/s). 336 TS : float 337 Stack gas temperature (K). 338 TA : float 339 Ambient air temperature (K). 340 ZR : float 341 Receptor height above ground (flagpole receptor) (M). 342 X : array_like 343 Array of downwind distances at which to compute the outputs (m). 344 IMETEO : int, {1, 2, 3} 345 1. Full (supplying stability class + WS for each grid point), 346 or SCREEN runs for each STAB option??... 347 2. Single stability class 348 3. Stability class + WS 349 ISTAB : int 350 Stability class. 351 1 (= A) to 6 (= F). 352 WS : float 353 Mean wind speed at 10 m (m/s). 354 .. warning:: 355 The run will fail if you exceed the maximum that SCREEN3 allows! 356 U_or_R : str {'U', 'R'} 357 Urban (U) or rural (R). 358 DOWNWASH_YN : str {'Y', 'N'} 359 whether to apply building downwash calculations 360 the building dimension parameters do nothing if it is 'N' 361 HB : float 362 Building height (m). 363 HL : float 364 Minimum horizontal building dimension (m). 365 HW : float 366 Maximum horizontal building dimension (m). 367 exe : path-like 368 Path to the executable to use, e.g., as a `str` or `pathlib.Path`. 369 370 Examples 371 -------- 372 Change parameters. 373 ```python 374 screen3.run(TA=310, WS=2) 375 ``` 376 Specify executable to use if yours isn't in the default place (`./src/SCREEN3.exe`). 377 ```python 378 screen3.run(exe="/path/to/executable") 379 ``` 380 381 Returns 382 ------- 383 df : pd.DataFrame 384 Results dataset, read from the `SCREEN.OUT` by `read`. 385 386 Notes 387 ----- 388 The SCREEN3 program parses the inputs and makes a copy called `SCREEN.DAT`. 389 Upon running, it produces an output file called `SCREEN.OUT`. 390 Both of these will be in the source directory, where the executable resides. 391 """ 392 inputs = locals() # collect inputs for saving in the df 393 394 import datetime 395 396 # TODO: should validate wind speed? 397 398 # Check exe is file 399 exe = Path(exe) 400 if not exe.is_file(): 401 raise ValueError( 402 f"{exe.absolute()!r} does not exist or is not a file. " 403 "Use the `exe` keyword argument of this function to specify the correct path." 404 ) 405 406 # Check for H changes without downwad 407 H_defaults = (30.0, 10.0, 20.0) # keep in sync with fn defaults 408 if any(x0 != x for x0, x in zip(H_defaults, [HB, HL, HW])) and DOWNWASH_YN == 'N': 409 print("Note: You have modified a building parameter, but downwash is not enabled.") 410 411 # Check x positions 412 X = np.asarray(X) 413 if X.size > 50: 414 raise ValueError('SCREEN3 does not support inputting more than 50 distance values') 415 416 # ------------------------------------------------------ 417 # Other parameters 418 # For now the user cannot set these 419 # although they are required inputs to the model 420 421 I_SOURCE_TYPE = 'P' # point source 422 423 # Complex terrain also currently not used 424 # (assumed flat) 425 TERRAIN_1_YN = 'N' # complex terrain screen above stack height 426 TERRAIN_2_YN = 'N' # simple terrain above base 427 # HTER = 10. # MAXIMUM TERRAIN HEIGHT ABOVE STACK BASE (M) 428 # ^ not needed if not using the terrain settings 429 # at least for simple terrain, seems to just run the x positions for different heights 430 431 # Currently user input of distances is required 432 # i.e., automated option not used 433 AUTOMATED_DIST_YN = 'N' 434 DISCRETE_DIST_YN = 'Y' 435 436 # Final questions 437 FUMIG_YN = 'N' 438 PRINT_YN = 'N' 439 440 # ------------------------------------------------------ 441 # Create the input file 442 443 def s_METEO(): 444 if IMETEO == 1: # A range (depending on U/R, type of source, etc.) of WS and STABs are examined to find maximum impact 445 # print(f"* Note that IMETEO={IMETEO!r} isn't really implemented presently..." 446 # "\nExamine the WS and STAB in the model output to see what happened.") 447 return '1' 448 elif IMETEO == 2: 449 return f'{IMETEO}\n{ISTAB}' 450 elif IMETEO == 3: 451 return f'{IMETEO}\n{ISTAB}\n{WS}' 452 else: 453 raise ValueError(f'invalid `IMETEO` {IMETEO!r}. Must be in {{1, 2, 3}}') 454 455 def s_X(): 456 vals = X.astype(str).tolist() 457 return '\n'.join(vals + ['0.0']) 458 459 def s_building_props(): 460 if DOWNWASH_YN == 'Y': 461 return f'{HB}\n{HL}\n{HW}' 462 else: 463 return "" 464 465 dat_text = f""" 466{RUN_NAME} 467{I_SOURCE_TYPE} 468{Q} 469{HS} 470{DS} 471{VS} 472{TS} 473{TA} 474{ZR} 475{U_or_R} 476{DOWNWASH_YN} 477{s_building_props()} 478{TERRAIN_1_YN} 479{TERRAIN_2_YN} 480{s_METEO()} 481{AUTOMATED_DIST_YN} 482{DISCRETE_DIST_YN} 483{s_X()} 484{FUMIG_YN} 485{PRINT_YN} 486 """.strip() + "\n" 487 # ^ need newline at end or Fortran complains when passing text on the cl 488 489 # TODO: optionally write the input text file to disk in cwd 490 # ifn = 'screen3_input.txt' # input filename 491 # ifp = cwd / ifn 492 # with open(ifn, 'w') as f: 493 # f.write(dat_text) 494 495 # ------------------------------------------------------ 496 # Run the SCREEN executable 497 498 t_utc_run = datetime.datetime.now() 499 500 s_exe = str(exe.absolute()) 501 cwd = Path.cwd() 502 src_dir = exe.parent 503 504 # Move to src location so that the output file will be saved there 505 os.chdir(src_dir) 506 507 # Invoke executable 508 subprocess.run( 509 args=[s_exe], 510 input=dat_text, 511 universal_newlines=True, # equivalent to `text=True`, but that was added in 3.7 512 check=True, 513 shell=True, 514 stdout=subprocess.DEVNULL, 515 stderr=subprocess.STDOUT, 516 ) 517 518 # Return to cwd 519 os.chdir(cwd) 520 521 # Read and parse the output 522 df = read(src_dir/'SCREEN.OUT', t_run=t_utc_run, run_inputs=inputs) 523 524 return df
Create SCREEN3 input file, feed it to the executable, and load the result.
Inputs must be specified as keyword arguments, but can be entered in any order (non-positional).
Parameters
- Q (float): Emission rate (g/s).
- HS (float): Stack height (m).
- DS (float): Stack inside diameter (m).
- VS (float): Stack gas exit velocity (m/s).
- TS (float): Stack gas temperature (K).
- TA (float): Ambient air temperature (K).
- ZR (float): Receptor height above ground (flagpole receptor) (M).
- X (array_like): Array of downwind distances at which to compute the outputs (m).
- IMETEO (int, {1, 2, 3}):
- Full (supplying stability class + WS for each grid point), or SCREEN runs for each STAB option??...
- Single stability class
- Stability class + WS
- ISTAB (int): Stability class. 1 (= A) to 6 (= F).
WS (float): Mean wind speed at 10 m (m/s).
The run will fail if you exceed the maximum that SCREEN3 allows!
U_or_R (str {'U', 'R'}): Urban (U) or rural (R).
- DOWNWASH_YN (str {'Y', 'N'}): whether to apply building downwash calculations the building dimension parameters do nothing if it is 'N'
- HB (float): Building height (m).
- HL (float): Minimum horizontal building dimension (m).
- HW (float): Maximum horizontal building dimension (m).
- exe (path-like):
Path to the executable to use, e.g., as a
str
orpathlib.Path
.
Examples
Change parameters.
screen3.run(TA=310, WS=2)
Specify executable to use if yours isn't in the default place (./src/SCREEN3.exe
).
screen3.run(exe="/path/to/executable")
Returns
- df (pd.DataFrame):
Results dataset, read from the
SCREEN.OUT
byread
.
Notes
The SCREEN3 program parses the inputs and makes a copy called SCREEN.DAT
.
Upon running, it produces an output file called SCREEN.OUT
.
Both of these will be in the source directory, where the executable resides.
188def read( 189 fp, 190 *, 191 t_run=None, 192 run_inputs=None, 193): 194 """Read and extract data from a SCREEN3 run (i.e., a `SCREEN.OUT` file). 195 196 Parameters 197 ---------- 198 fp : str, pathlib.Path 199 File path to the `SCREEN.OUT` file, 200 e.g., `'./screen3/SCREEN.OUT'`. 201 t_run : datetime.datetime 202 Time of the run. 203 If the output file (`SCREEN.OUT`) is older than this, the run did not complete successfully. 204 run_inputs : dict, optional 205 `run` passes this so that we can store what the inputs to the Python function were for this run, 206 though `SCREEN.OUT` should have much of the same info. 207 208 Returns 209 ------- 210 df : pd.DataFrame 211 SCREEN3 output dataset. 212 213 If the pandas version has [`attrs`](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.attrs.html), 214 `df.attrs` will include the following: 215 * `'units'`: (dict) *keys*: the variables names, *values*: unit strings (for use in plots) 216 * `'SCREEN_OUT'`: (str) the SCREEN3 output file as a string 217 * `'SCREEN_DAT'`: (str) the SCREEN3 DAT (copied inputs) file as a string 218 * `'run_inputs'`: (dict) copy of the inputs to the the `run` function used to generated this dataset (if applicable) 219 """ 220 import datetime 221 222 import pandas as pd 223 224 if run_inputs is None: 225 run_inputs = {} 226 227 # If we have `t_run`, first check that the file has indeed been modified 228 p = Path(fp) 229 t_out_modified = datetime.datetime.fromtimestamp(p.stat().st_mtime) 230 if t_run is not None and p.is_file(): # i.e., t_run has been specified 231 if (t_run - t_out_modified) > datetime.timedelta(seconds=1): 232 raise ValueError( 233 "`SCREEN.OUT` is older than time of the run." 234 " This probably means that SCREEN failed to run because it didn't like the value of one or more inputs." 235 " Or some other reason.") 236 237 # Read lines 238 with open(fp, 'r') as f: 239 lines_raw = f.readlines() 240 lines_stripped = [line.strip() for line in lines_raw] 241 242 # TODO: could also read other info from the file like the run settings at the top 243 244 iheader = [i for i, s in enumerate(lines_stripped) if s.split()[:2] == ['DIST', 'CONC']] 245 if not iheader: 246 raise ValueError( 247 "Data table not found in output. " 248 "This probably means that SCREEN failed to complete the run because " 249 "it didn't like the value of one or more inputs. " 250 "Or some other reason." 251 ) 252 253 if len(iheader) > 1: 254 print(f"Multiple sets of data found. Only loading the first one.") 255 iheader = iheader[0] 256 257 # Find first blank after iheader 258 iblankrel = lines_stripped[iheader:].index("") 259 iblank = iheader + iblankrel 260 261 # Could (technically) read these from file instead 262 col_names = SCREEN_OUT_COL_NAMES 263 col_units = SCREEN_OUT_COL_UNITS 264 265 # Load into pandas 266 # Note: at some point `pd.read_table` was deprecated but seems like that was undone? 267 n_X = iblank - iheader - 2 268 df = pd.read_table(fp, 269 sep=None, skipinitialspace=True, engine='python', 270 keep_default_na=False, 271 skiprows=iheader+2, nrows=n_X) 272 df.columns = col_names 273 274 units_dict = dict(zip(col_names, col_units)) 275 276 p_dat = p.parents[0] / f'{p.stem}.DAT' 277 try: 278 with open(p_dat, 'r') as f: 279 s_screen_dat = f.read() 280 except FileNotFoundError: 281 s_screen_dat = None 282 283 # Assign attrs to df (pandas 1.0+) 284 if hasattr(df, "attrs"): 285 df.attrs.update({ 286 'SCREEN_DAT': s_screen_dat, 287 'SCREEN_OUT': ''.join(lines_raw), 288 'units': units_dict, 289 'run_inputs': run_inputs 290 }) 291 292 return df
Read and extract data from a SCREEN3 run (i.e., a SCREEN.OUT
file).
Parameters
- fp (str, pathlib.Path):
File path to the
SCREEN.OUT
file, e.g.,'./screen3/SCREEN.OUT'
. - t_run (datetime.datetime):
Time of the run.
If the output file (
SCREEN.OUT
) is older than this, the run did not complete successfully. - run_inputs (dict, optional):
run
passes this so that we can store what the inputs to the Python function were for this run, thoughSCREEN.OUT
should have much of the same info.
Returns
df (pd.DataFrame): SCREEN3 output dataset.
If the pandas version has
attrs
,df.attrs
will include the following:'units'
: (dict) keys: the variables names, values: unit strings (for use in plots)'SCREEN_OUT'
: (str) the SCREEN3 output file as a string'SCREEN_DAT'
: (str) the SCREEN3 DAT (copied inputs) file as a string'run_inputs'
: (dict) copy of the inputs to the therun
function used to generated this dataset (if applicable)
541def plot( 542 df, 543 *, 544 labels=None, 545 yvals=None, 546 yvar="", 547 yvar_units="", 548 plot_type='line', 549 ax=None, 550 **pyplot_kwargs): 551 """Plot concentration. 552 553 The first argument (positional) can be one `df` or a list of `df`s. 554 If you pass a list of `df`s, you can also pass `labels`. 555 556 The default plot type is `'line'`, but 2-D plots `'pcolor'` and `'contourf'` are also options. 557 Unless you additionally pass `yvals`, they will be plotted as if the labels were equally spaced. 558 559 Parameters 560 ---------- 561 df : pd.DataFrame or list of pd.DataFrame 562 Data extracted from the `SCREEN.OUT` of the run(s) using `read`. 563 labels : array_like 564 Labels for the separate cases. 565 Used if input `df` is a list instead of just one dataset. 566 Currently we assume that the first part of the label is the variable name that is being varied. 567 yvals : array_like 568 Optional positions of labels. 569 yvar : str 570 *y* variable name. 571 Only used if labels not provided. 572 yvar_units : str 573 Units to use when labeling `yvar`. 574 Only used if `labels` not provided. 575 plot_type : str {'line', 'pcolor', 'contourf'} 576 The type of plot to do. 577 ax : plt.Axes 578 If an existing ax is not passed, we create a new figure (and ax). 579 Else, we plot on the existing ax. 580 **pyplot_kwargs 581 Passed on to the relevant pyplot plotting function, 582 e.g., `ax.plot`. 583 """ 584 import matplotlib as mpl 585 import matplotlib.pyplot as plt 586 import pandas as pd 587 588 if labels is None: 589 labels = [] 590 if yvals is None: 591 yvals = [] 592 593 units = SCREEN_OUT_COL_UNITS_DICT 594 # or `df.attrs['units']` 595 596 if not isinstance(df, (pd.DataFrame, list)): 597 raise TypeError(f"df is not a `pd.DataFrame` or `list`, it is type `{type(df).__name__}`") 598 599 if plot_type not in ['line', 'pcolor', 'contourf']: 600 raise ValueError(f"`plot_type` {plot_type!r} not allowed") 601 602 if ax is None or not isinstance(ax, plt.Axes): 603 fig, ax = plt.subplots() 604 else: 605 fig = plt.gcf() 606 607 # determine number of points 608 if isinstance(df, list): 609 n = df[0].index.size 610 else: 611 n = df.index.size 612 613 # some line styling 614 if n > 20: 615 line_style = '.-' 616 ms = 6 617 else: 618 line_style = 'o-' 619 ms = 5 620 pyplot_kwargs.update({ 621 'markersize': ms 622 }) 623 624 if isinstance(df, list): 625 if not list(labels): 626 labels = list(range(1, len(df)+1)) 627 628 if plot_type == 'line': 629 with mpl.rc_context({'legend.fontsize': 'small'}): 630 for i, dfi in enumerate(df): 631 dfi.plot(x='DIST', y='CONC', style=line_style, ax=ax, label=labels[i], **pyplot_kwargs) 632 633 else: # 2-D plot options 634 ax.set_xlabel('DIST') 635 yvals = np.asarray(yvals) 636 if yvals.any(): 637 Y = yvals 638 639 # yvar_guess = labels[0].split()[0] 640 # try: 641 # yvar_units = units[yvar_guess] 642 # except KeyError: 643 # raise ValueError(f"Y variable guess {yvar_guess!r} based on labels passed not found in allowed list." 644 # " Make sure that the first part of each label is the variable, followed by a space.") 645 # ax.set_ylabel(yvar_guess) 646 647 if labels: 648 ax.set_yticks(yvals) 649 ax.set_yticklabels(labels) 650 651 ax.set_ylabel(yvar) 652 _add_units(units['DIST'], yvar_units, ax=ax) 653 654 else: 655 Y = labels 656 _add_units(units['DIST'], '', ax=ax) 657 658 X = df[0].DIST 659 # Y = yvals if yvals.any() else labels 660 Z = np.stack([dfi['CONC'] for dfi in df]) 661 662 if plot_type == 'pcolor': 663 im = ax.pcolormesh(X, Y, Z, **pyplot_kwargs) 664 665 elif plot_type == 'contourf': 666 im = ax.contourf(X, Y, Z, **pyplot_kwargs) 667 668 cb = fig.colorbar(im) 669 670 cb.set_label(f"CONC ({units['CONC']})") 671 672 else: # just one to plot 673 if plot_type != 'line': 674 raise ValueError("With only one run, only `plot_type` 'line' can be used") 675 df.plot(x='DIST', y='CONC', style=line_style, ax=ax, **pyplot_kwargs) 676 677 if plot_type == 'line': 678 ax.set_ylabel('CONC') 679 _add_units(units['DIST'], units['CONC'], ax=ax) # use the units dict to look up the units 680 681 # ax.set_xlim(xmin=0, xmax=) 682 ax.autoscale(axis='x', tight=True) 683 684 fig.tight_layout() 685 686 return fig
Plot concentration.
The first argument (positional) can be one df
or a list of df
s.
If you pass a list of df
s, you can also pass labels
.
The default plot type is 'line'
, but 2-D plots 'pcolor'
and 'contourf'
are also options.
Unless you additionally pass yvals
, they will be plotted as if the labels were equally spaced.
Parameters
- df (pd.DataFrame or list of pd.DataFrame):
Data extracted from the
SCREEN.OUT
of the run(s) usingread
. - labels (array_like):
Labels for the separate cases.
Used if input
df
is a list instead of just one dataset. Currently we assume that the first part of the label is the variable name that is being varied. - yvals (array_like): Optional positions of labels.
- yvar (str): y variable name. Only used if labels not provided.
- yvar_units (str):
Units to use when labeling
yvar
. Only used iflabels
not provided. - plot_type (str {'line', 'pcolor', 'contourf'}): The type of plot to do.
- ax (plt.Axes): If an existing ax is not passed, we create a new figure (and ax). Else, we plot on the existing ax.
- **pyplot_kwargs: Passed on to the relevant pyplot plotting function,
e.g.,
ax.plot
.
59def download(*, src=DEFAULT_SRC_DIR): 60 """Download the SCREEN3 zip from EPA and extract to directory `src`. 61 62 If it fails, you can always download it yourself some other way. 63 64 <https://gaftp.epa.gov/Air/aqmg/SCRAM/models/screening/screen3/screen3.zip> 65 66 Parameters 67 ---------- 68 src : path-like 69 Where to extract the files to. 70 .. note:: 71 As long as this isn't modified, the `src`/`exe` keyword doesn't need to be changed 72 for `build`, `run`, or `load_example`. 73 """ 74 import io 75 import zipfile 76 from pathlib import Path 77 78 import requests 79 80 url = "https://gaftp.epa.gov/Air/aqmg/SCRAM/models/screening/screen3/screen3.zip" 81 82 extract_to = Path(src) 83 extract_to.mkdir(exist_ok=True, parents=True) 84 85 r = requests.get(url, verify=False) # TODO: get it working without having to disable certificate verification 86 with zipfile.ZipFile(io.BytesIO(r.content)) as z: 87 for info in z.infolist(): 88 with z.open(info) as zf, open(extract_to / info.filename, "wb") as f: 89 f.write(zf.read())
Download the SCREEN3 zip from EPA and extract to directory src
.
If it fails, you can always download it yourself some other way.
https://gaftp.epa.gov/Air/aqmg/SCRAM/models/screening/screen3/screen3.zip
Parameters
src (path-like): Where to extract the files to.
As long as this isn't modified, the
src
/exe
keyword doesn't need to be changed forbuild
,run
, orload_example
.
143def build(*, src=DEFAULT_SRC_DIR): 144 """Build the SCREEN3 executable by pre-processing the sources and compiling with GNU Fortran. 145 146 .. note:: 147 Requires `patch` and `gfortran` on PATH. 148 149 Parameters 150 ---------- 151 src : path-like 152 Source directory, containing `SCREEN3A.FOR` etc., e.g., downloaded using `screen3.download`. 153 """ 154 cwd = Path.cwd() 155 bld = Path(src) 156 157 os.chdir(bld) 158 159 srcs = ['SCREEN3A.FOR', 'SCREEN3B.FOR', 'SCREEN3C.FOR'] 160 161 # Fix line endings 162 if platform.system() != "Windows": 163 # subprocess.run(['dos2unix'] + srcs) 164 for src in srcs: 165 _dos2unix(src) 166 167 # Patch code 168 with open("SCREEN3A.FOR.patch", "w") as f: 169 f.write(_SCREEN3A_PATCH) 170 with open("DEPVAR.INC.patch", "w") as f: 171 f.write(_DEPVAR_PATCH) 172 subprocess.run(['patch', 'SCREEN3A.FOR', 'SCREEN3A.FOR.patch']) 173 subprocess.run(['patch', 'DEPVAR.INC', 'DEPVAR.INC.patch']) 174 175 # Compile 176 subprocess.run(['gfortran', '-cpp'] + srcs + ['-o', 'SCREEN3.exe']) 177 178 os.chdir(cwd)
Build the SCREEN3 executable by pre-processing the sources and compiling with GNU Fortran.
Requires patch
and gfortran
on PATH.
Parameters
- src (path-like):
Source directory, containing
SCREEN3A.FOR
etc., e.g., downloaded usingscreen3.download
.
689def load_example(s, *, src=DEFAULT_SRC_DIR): 690 """Load one of the examples included with the `screen3.zip` download, 691 such as `'EXAMPLE.OUT'` from SCREEN3 source directory `src`. 692 693 Examples 694 -------- 695 ```python 696 screen3.load_example("EXAMPLE.OUT") 697 ``` 698 """ 699 valid_examples = [ 700 "EXAMPLE.OUT", "examplenew.out", "examplnrnew.out", 701 "EXAMPNR.OUT", "exampnrnew.out", 702 ] 703 if s not in valid_examples: 704 raise ValueError(f"invalid example file name. Valid options are: {valid_examples}") 705 706 return read(Path(src) / s)
Load one of the examples included with the screen3.zip
download,
such as 'EXAMPLE.OUT'
from SCREEN3 source directory src
.
Examples
screen3.load_example("EXAMPLE.OUT")
Dict of units for the outputs to be used in the plots. For example, 'DIST': 'm'
.
Default directory in which to place the SCREEN3 source code from EPA.