Skip to content

PyAERMOD Quick Start Guide

Installation

pip install pyaermod

For visualization, geospatial export, or the GUI, install extras:

pip install pyaermod[all]  # everything

5-Minute Example

Create a complete AERMOD input file in just a few lines:

from pyaermod import (
    AERMODProject,
    ControlPathway,
    SourcePathway,
    PointSource,
    ReceptorPathway,
    CartesianGrid,
    MeteorologyPathway,
    OutputPathway,
    PollutantType,
)

# 1. Define the control parameters
control = ControlPathway(
    title_one="My First AERMOD Run",
    pollutant_id=PollutantType.PM25,
    averaging_periods=["ANNUAL", "24"],
    terrain_type="FLAT",
)

# 2. Add your emission source(s)
sources = SourcePathway()
sources.add_source(PointSource(
    source_id="STACK1",
    x_coord=500.0,      # meters (UTM)
    y_coord=500.0,      # meters (UTM)
    base_elevation=10.0,
    stack_height=50.0,   # meters
    stack_temp=400.0,    # Kelvin
    exit_velocity=15.0,  # m/s
    stack_diameter=2.0,  # meters
    emission_rate=1.5,   # g/s
))

# 3. Define receptor grid
receptors = ReceptorPathway()
receptors.add_cartesian_grid(
    CartesianGrid.from_bounds(
        x_min=0, x_max=2000,
        y_min=0, y_max=2000,
        spacing=100,  # 100m grid spacing
    )
)

# 4. Specify meteorology files
meteorology = MeteorologyPathway(
    surface_file="met_data.sfc",
    profile_file="met_data.pfl",
)

# 5. Configure output
output = OutputPathway(
    receptor_table=True,
    max_table=True,
    summary_file="results.sum",
)

# 6. Create project and write input file
project = AERMODProject(
    control=control,
    sources=sources,
    receptors=receptors,
    meteorology=meteorology,
    output=output,
)

project.write("my_first_run.inp")

That's it! You now have a valid AERMOD input file.

Running AERMOD from Python

from pyaermod.runner import run_aermod

result = run_aermod("my_first_run.inp")
print(f"Success: {result.success}")
print(f"Runtime: {result.runtime_seconds:.1f}s")
print(f"Output: {result.output_file}")

Note

You need the AERMOD executable installed separately. Download it from EPA SCRAM.

Parsing Output

from pyaermod.output_parser import parse_aermod_output

results = parse_aermod_output(result.output_file)
df = results.get_concentrations("ANNUAL")
print(f"Max concentration: {df['concentration'].max():.4g}")

Visualization

from pyaermod.visualization import AERMODVisualizer

viz = AERMODVisualizer(results)
fig = viz.plot_contours(averaging_period="ANNUAL")

Common Patterns

Multiple Sources

from pyaermod import SourcePathway, PointSource

sources = SourcePathway()

stack_data = [
    (100.0, 200.0, 1.0),
    (300.0, 400.0, 2.5),
    (500.0, 600.0, 0.8),
]

for i, (x, y, rate) in enumerate(stack_data):
    sources.add_source(PointSource(
        source_id=f"STACK{i + 1}",
        x_coord=x,
        y_coord=y,
        stack_height=50.0,
        stack_temp=400.0,
        exit_velocity=15.0,
        stack_diameter=2.0,
        emission_rate=rate,
    ))

Polar Grid Around Facility

from pyaermod import PolarGrid, ReceptorPathway

receptors = ReceptorPathway()
receptors.add_polar_grid(PolarGrid(
    x_origin=500.0,
    y_origin=500.0,
    dist_init=100.0,      # start 100m from origin
    dist_num=20,          # 20 distance rings
    dist_delta=100.0,     # 100m spacing
    dir_init=0.0,         # start at north
    dir_num=36,           # 36 directions (every 10 degrees)
    dir_delta=10.0,
))

Building Downwash

from pyaermod import PointSource

stack = PointSource(
    source_id="STACK1",
    x_coord=0.0,
    y_coord=0.0,
    stack_height=30.0,
    stack_temp=400.0,
    exit_velocity=15.0,
    stack_diameter=2.0,
    emission_rate=1.5,
    building_height=25.0,
    building_width=40.0,
    building_length=60.0,
    building_x_offset=0.0,
    building_y_offset=20.0,
)

Building downwash (PRIME) is also supported on AreaSource and VolumeSource with the same five fields. For direction-dependent building downwash, use the BPIP module or the GUI's built-in BPIP calculator.

Background Concentrations

Add ambient background levels to account for existing pollution:

from pyaermod import (
    SourcePathway, BackgroundConcentration, BackgroundSector,
)

sources = SourcePathway()
# ... add sources ...

# Uniform background — one value applied to every hour
sources.background = BackgroundConcentration(uniform_value=12.0)  # ug/m3

# Or per-averaging-period
sources.background = BackgroundConcentration(
    period_values={"ANNUAL": 12.0, "24": 35.0},
)

# Or sector-dependent — define starting directions + per-sector values
sources.background = BackgroundConcentration(
    sectors=[
        BackgroundSector(sector_id=1, start_direction=0.0),
        BackgroundSector(sector_id=2, start_direction=90.0),
    ],
    sector_values={(1, "ANNUAL"): 12.0, (1, "24"): 35.0,
                   (2, "ANNUAL"): 8.0, (2, "24"): 25.0},
)

Deposition Modeling

Enable dry or wet deposition for any source type:

from pyaermod import (
    PointSource, DepositionMethod, GasDepositionParams,
    ParticleDepositionParams,
)

stack = PointSource(
    source_id="STACK1",
    x_coord=500.0, y_coord=500.0,
    stack_height=50.0, stack_temp=400.0,
    exit_velocity=15.0, stack_diameter=2.0,
    emission_rate=1.5,
    deposition_method=DepositionMethod.GASDEPVD,
    gas_deposition=GasDepositionParams(
        diffusivity=0.126,     # cm^2/s  (SO2 reference value)
        alpha_r=10.0,          # dimensionless
        reactivity=8.0,        # dimensionless
        henry_constant=1.2e-3, # M/atm (alternative to dry_dep_velocity)
    ),
)

Set OutputPathway(output_type="DEPOS") to get deposition flux instead of concentration in the output.

NO2 Chemistry Options

For NO2 modeling, configure Tier 2/3 chemistry:

from pyaermod import (
    ControlPathway, PollutantType, ChemistryMethod,
    ChemistryOptions, OzoneData,
)

control = ControlPathway(
    title_one="NO2 Tier 3 Analysis",
    pollutant_id=PollutantType.NO2,
    averaging_periods=["1", "ANNUAL"],
    chemistry=ChemistryOptions(
        method=ChemistryMethod.PVMRM,
        default_no2_ratio=0.5,
        ozone_data=OzoneData(uniform_value=40.0),  # ppb
    ),
)

Source Groups

Define custom source groups for separate impact analysis:

from pyaermod import SourcePathway, SourceGroupDefinition

sources = SourcePathway()
# ... add sources STACK1, STACK2, STACK3 ...

sources.group_definitions = [
    SourceGroupDefinition(
        group_name="BOILERS",
        member_source_ids=["STACK1", "STACK2"],
        description="Boiler stacks",
    ),
    SourceGroupDefinition(
        group_name="PROCESS",
        member_source_ids=["STACK3"],
        description="Process vents",
    ),
]

EVENT Processing

Run AERMOD in event mode for specific date/receptor combinations:

from pyaermod import EventPathway, EventPeriod

events = EventPathway(events=[
    EventPeriod(
        event_name="MAXDAY",
        source_group="ALL",
        averaging_period="24",
        start_date="24031501",
        end_date="24031524",
    ),
])

project = AERMODProject(control, sources, receptors, meteorology, output)
project.write("facility.inp", event_filename="facility.evn")

Parameter Sweep

from pyaermod import *

for rate in [1.0, 2.0, 3.0, 4.0, 5.0]:
    control = ControlPathway(
        title_one=f"Emission Rate = {rate} g/s",
        pollutant_id=PollutantType.SO2,
        averaging_periods=["1", "24", "ANNUAL"],
    )

    sources = SourcePathway()
    sources.add_source(PointSource(
        source_id="STACK1",
        x_coord=500.0,
        y_coord=500.0,
        stack_height=50.0,
        stack_temp=400.0,
        exit_velocity=15.0,
        stack_diameter=2.0,
        emission_rate=rate,
    ))

    project = AERMODProject(control, sources, receptors, meteorology, output)
    project.write(f"scenario_rate_{rate}.inp")

POSTFILE Parsing

from pyaermod.postfile import read_postfile

# Auto-detects text (PLOT) or binary (UNFORM) format
result = read_postfile("postfile.pst")
print(f"Max concentration: {result.max_concentration:.4g}")
print(f"Location: {result.max_location}")

# Step through timesteps
for date in result.data["date"].unique():
    ts = result.get_timestep(date)
    print(f"{date}: max={ts['concentration'].max():.4g}")

Binary POSTFILE with Deposition

Binary (UNFORM) POSTFILEs from deposition runs store concentration, dry deposition, and wet deposition as contiguous blocks of N floats each (3N total per record).

from pyaermod.postfile import read_postfile

# Explicit deposition flag
result = read_postfile("depo_post.pst", has_deposition=True)
df = result.to_dataframe()
print(df[["x", "y", "concentration", "dry_depo", "wet_depo"]])

# Auto-detect: provide num_receptors, parser checks if 3N floats
result = read_postfile("post.pst", num_receptors=50)
# If 150 floats found, deposition is auto-detected

Validation

The input generator includes built-in validation:

  • Required parameters are enforced
  • Parameter types are checked (floats, ints, strings)
  • Enum values are validated (pollutant types, terrain types)
  • Coordinate systems are consistent
from pyaermod.validator import Validator

validator = Validator()
errors = validator.validate(project)
for error in errors:
    print(error)

What's Available Now

Feature Module
10 source types (POINT, AREA, AREACIRC, AREAPOLY, VOLUME, LINE, RLINE, RLINEXT, BUOYLINE, OPENPIT) input_generator
Background concentrations (uniform, period, sector) input_generator
Deposition modeling (dry, wet, combined) input_generator
NO2/SO2 chemistry (OLM, PVMRM, ARM2, GRSM) input_generator
Source group management and per-group output input_generator
EVENT processing for date/receptor analysis input_generator
Input file validation validator
AERMOD execution runner
Output parsing to DataFrames output_parser
POSTFILE parsing (text and binary, with deposition) postfile
Contour plots, Folium maps visualization
3D surfaces, wind roses, animations advanced_viz
AERMET meteorological preprocessing aermet
AERMAP terrain preprocessing aermap
DEM download and terrain pipeline terrain
Coordinate transforms, GIS export geospatial
Building downwash (BPIP) for POINT, AREA, VOLUME bpip
Interactive 7-page web GUI gui

AERMOD Keywords Supported

Based on AERMOD version 24142:

Control Pathway (CO)

STARTING, FINISHED, TITLEONE, TITLETWO, MODELOPT, AVERTIME, POLLUTID, RUNORNOT, ELEVUNIT, FLAGPOLE, HALFLIFE, DCAYCOEF, URBANOPT, LOW_WIND, EVENTFIL, O3VALUES, OZONEFIL, NO2RATIO, NOXFIL

Source Pathway (SO)

LOCATION (POINT, AREA, AREACIRC, AREAPOLY, VOLUME, LINE, RLINE, RLINEXT, BUOYLINE, OPENPIT), SRCPARAM, BUILDHGT, BUILDWID, BUILDLEN, XBADJ, YBADJ, SRCGROUP, URBANSRC, APTS_CAP, BLPINPUT, BLPGROUP, RBARRIER, RDEPRESS, BACKGRND, BGSECTOR, GASDEPOS, PARTDIAM, MASSFRAX, PARTDENS, METHOD_2, DEPRESS

Receptor Pathway (RE)

GRIDCART (with ELEV/HILL terrain support), GRIDPOLR (with ELEV/HILL terrain support), DISCCART, ELEVUNIT

Meteorology Pathway (ME)

SURFFILE, PROFFILE, SURFDATA, UAIRDATA, STARTEND, WDROTATE

Output Pathway (OU)

RECTABLE, MAXTABLE, DAYTABLE, SUMMFILE, MAXIFILE, PLOTFILE, POSTFILE

Event Pathway (EV)

EVENTPER, EVENTLOC

Testing & Validation

PyAERMOD includes comprehensive testing validated against official EPA data:

  • 1600+ unit and integration tests with 97%+ code coverage
  • 315 EPA test cases parsed from official AERMOD v24142 output files (LOVETT, FLATELEV, TESTPART, etc.)
  • End-to-end pipeline tests chaining input generation through visualization

Next Steps

  • Try the GUI User Guide for a no-code modeling experience
  • Browse the API Reference for detailed module documentation
  • Check the examples/ directory for runnable scripts and Jupyter notebooks

Getting Help

  1. Check the examples/ directory for more usage patterns
  2. Review the Architecture document for design details
  3. Browse the API Reference for module documentation
  4. Read AERMOD documentation from EPA SCRAM

License

MIT