screen3

Python functions for using SCREEN3.

  • screen3.run – wrapper to quickly create a run without needing to go through the prompt
  • screen3.read – load the output into a pandas DataFrame
  • screen3.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.

screen3 lives on GitHub.

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.1"
 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    import datetime
392
393    inputs = locals()  # collect inputs for saving in the df
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)
def run( *, RUN_NAME='A point-source SCREEN3 run', Q=100.0, HS=30.0, DS=10.0, VS=2.0, TS=310.0, TA=293.0, ZR=1.0, X=array([ 1., 50., 100., 150., 200., 250., 300., 350., 400., 450., 500.]), IMETEO=3, ISTAB=4, WS=5.0, U_or_R='R', DOWNWASH_YN='N', HB=30.0, HL=10.0, HW=20.0, exe='./src/SCREEN3.exe'):
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    import datetime
393
394    inputs = locals()  # collect inputs for saving in the df
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}):
    1. Full (supplying stability class + WS for each grid point), or SCREEN runs for each STAB option??...
    2. Single stability class
    3. 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 or pathlib.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 by read.
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.

def read(fp, *, t_run=None, run_inputs=None):
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, though SCREEN.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 the run function used to generated this dataset (if applicable)
def plot( df, *, labels=None, yvals=None, yvar='', yvar_units='', plot_type='line', ax=None, **pyplot_kwargs):
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 dfs. If you pass a list of dfs, 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) using read.
  • 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 if labels 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.
def download(*, src='./src'):
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 for build, run, or load_example.
def build(*, src='./src'):
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 using screen3.download.
def load_example(s, *, src='./src'):
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")
SCREEN_OUT_COL_UNITS_DICT = {'DIST': 'm', 'CONC': 'μg/m$^3$', 'STAB': '', 'U10M': 'm/s', 'USTK': 'm/s', 'MIX_HT': 'm', 'PLUME_HT': 'm', 'SIGMA_Y': 'm', 'SIGMA_Z': 'm', 'DWASH': ''}

Dict of units for the outputs to be used in the plots. For example, 'DIST': 'm'.

DEFAULT_SRC_DIR = './src'

Default directory in which to place the SCREEN3 source code from EPA.