Skip to content

Street air quality: catalogue

Street-canyon networks: canyon exchange, junction routing, above-roof plumes, chemistry and exposure (SIRANE and MUNICH physics, case readers and writers). How the pieces fit together: Street air quality. Every application is set up the same way; see Using noodl.

Import from noodl.apps.street_aq. The full reference, with every parameter, is in the API reference.

Model options

Keyword arguments of build_model choosing the physics; a preset (preset=, one of sirane, munich) sets them all, and any given explicitly overrides it. A value marked with a preset's name is that preset's choice. Defined in noodl.apps.street_aq.closures.

Option Values What it chooses
canyon_wind bessel_profile (sirane)
exponential_profile (munich)
the along-canyon velocity. bessel_profile is the Soulhac, Perkins and Salizzoni (2008) closed form on the friction velocity; exponential_profile is the single-regime exponential profile on the roof-level wind (Kim et al. 2022, Eq. B14).
roof_wind bessel_canyon_mean (sirane, munich)
canopy_log_law
the roof-level wind u_H the exponential profile needs. bessel_canyon_mean is the canyon mean of the Bessel profile (Kim et al. 2022, Eq. B12); canopy_log_law is a log law with Macdonald's (1998) displacement height and roughness.
roof_exchange turbulent_velocity (sirane)
aspect_ratio_scaled (munich)
the roof exchange velocity. turbulent_velocity is u_d = sigma_w / (sqrt(2) pi); aspect_ratio_scaled is Schulte's u_d = beta sigma_w / (1 + H/W).
junction_routing non_crossing_streamlines (sirane, munich)
perfect_mixing
how a junction's inflows are shared among its outflows. non_crossing_streamlines fills the outflows in angular order without crossing; perfect_mixing shares every inflow in proportion to the outflows.
direction_averaging none
exact_gaussian (sirane)
rectangle_rule (munich)
gauss_hermite
the average of the junction routing over the wind-direction spread. none routes at the mean direction; exact_gaussian is the exact Gaussian average of the piecewise-constant routing; rectangle_rule is uniform sampling on [-2 sigma, 2 sigma] with unnormalised weights; gauss_hermite is an n_theta-point Gauss-Hermite quadrature.
direction_spread driver (sirane)
turbulence_intensity (munich)
where the spread sigma_theta comes from. driver takes the sigma_theta driver, else the constructor value, else 0; turbulence_intensity is sigma_theta = min(sigma_v / U_ref, 10 deg).
stability neutral
monin_obukhov (sirane, munich)
the turbulence profiles. neutral is sigma_w = 1.3 u* (1 - 0.8 z/h); monin_obukhov switches between unstable, neutral and stable branches on the Obukhov length, and is the neutral form wherever no lmo driver is given.
sigma_w_height canopy_height (sirane)
street_height (munich)
the height at which the sigma_w of the roof exchange velocity is evaluated. canopy_height is one network-wide height above ground, h_canopy (SIRANE's H_R), the same for every street; street_height is each street's own height H.
shape_constant exact_root (sirane)
grid_search (munich)
how the shape parameter c of the Bessel canyon profile is evaluated. exact_root solves the shape equation to machine precision; grid_search takes the point of the grid c = 0.01, ..., 1.00 with the smallest residual, as MUNICH does (forward value quantised to 0.01, gradient of the exact root).

Numeric settings each preset fixes (each also a build_model keyword):

Setting sirane munich
kappa 0.4 0.41
canyon_wind_min 0.0 0.1
u_d_min 0.0 0.001
sigma_w_min 0.3 0.0
sigma_v_min 0.5 0.0
h_canopy 20.0 20.0

Set-up

Build the model and its starting state and inputs.

Name What it does How to call it
build_model (Model, initial state, driver template) for one street network. build_model(net, *, preset='sirane', canyon_wind=None, roof_wind=None, roof_exchange=None, junction_routing=None, direction_averaging=None, direction_spread=None, stability=None, shape_constant=None, n_theta=None, sigma_theta=None, species=('nox',), chemistry=None, scheme='implicit', kappa=None, canyon_wind_min=None, u_d_min=None, sigma_w_min=None, sigma_v_min=None, sigma_w_height=None, h_canopy=None, z0_s=0.01, z_ref=30.0, pblh_floor=True, meteo='uniform', background='uniform', atmosphere='atmosphere', layer_name='street')
initial_drivers build_model's driver template (copied), then values by name (see noodl.refs.drivers_from), e.g. by street and species name. initial_drivers(model, *, values=None)
initial_state Zero concentration everywhere, in each transport layer's own shape; then values by street and species name (see noodl.refs.state_from): initial_state(model, values={"street": {"s1": {"nox": 1e-7}}}). initial_state(model, *, values=None)

Network components

Descriptions of the physical objects a network is built from.

Name What it does How to call it
PlumeTable One plume trajectory per hour, resampled onto a distance grid (plume_table). PlumeTable(batch, sigma_y, p_z, u, step, x_max)
Street One street segment between junctions u and v (the azimuth points u -> v): its length, width and building height (m), the roughness length of its building walls z0_b (m, 0.15 by default) and emission_scale, a dimensionless factor applied to its emissions. Street(name, u, v, length, width, height, z0_b=0.15, emission_scale=1.0)
StreetCase One street-network case: the network plus every array a Model built on it needs to run, independent of which source model (source) it came from. StreetCase(source, network, times, street_ids, junction_ids, species, meteo, meteo_junction, emissions, background, native, start=None)
StreetResults One street model's own results, read format-neutral by read_results from a SIRANE result directory or a MUNICH results/ directory. StreetResults(source, times, street_ids, species, c_in, c_above, u_canyon, sigma_w_roof, u_exchange, meteo)
StreetGeometry Everything StreetFlows needs about the streets, as plain tensors. StreetGeometry(names, u, v, length, width, height, z0_b, azimuth)
StreetNetwork Streets plus the x/y coordinates (metres) of every junction they name. StreetNetwork(streets, x, y, street_direction='planar', lon=None, lat=None, _azimuth=<factory>)

Layers and model

The conservation layers and the model that steps them.

Name What it does How to call it
BoundaryLayer Friction velocity and the turbulence it sets, batched over forcing instances. BoundaryLayer(u_star, h_abl, z_ref, d, z0, kappa=0.4)

Closures

Functions of the state that compute drivers each step (densities, flows, levels).

Name What it does How to call it
StreetFlows The closure that turns the wind drivers into every prescribed edge flow. StreetFlows(net, layer, geometry, *, preset='sirane', canyon_wind=None, roof_wind=None, roof_exchange=None, junction_routing=None, direction_averaging=None, direction_spread=None, stability=None, shape_constant=None, n_theta=None, sigma_theta=None, kappa=None, canyon_wind_min=None, u_d_min=None, sigma_w_min=None, sigma_v_min=None, sigma_w_height=None, h_canopy=None, z0_s=0.01, z_ref=30.0, pblh_floor=True, meteo='uniform', layer_name='street')

Files

Readers and writers of other tools' file formats.

Name What it does How to call it
read_case StreetCase from path: a directory holding munich.cfg is read as a MUNICH case; a .dat file is read as a SIRANE master file (Donnees_*.dat) with the deck it names (see _sirane_files.read_sirane_case for what is read and what is refused). Anything else raises ValueError naming both expectations. read_case(path)
read_results StreetResults from path: a SIRANE result directory (holding RUES_PAR_HEURE/ and METEO/Resul_Meteo.dat) or a MUNICH results/ directory of <species>.bin files (see _sirane_files.read_sirane_results, _munich_files.read_munich_results for what each reads). read_results(path, *, case=None, hours='case')
write_case Writes case under out_dir in format ("munich" or "sirane"), and returns out_dir. case.start must be set: both formats date every input. write_case(out_dir, case, *, format='munich', options=None)
write_network_concentration Write a network_concentration_<year>.nc of per-street results, as classic CDF. write_network_concentration(path, *, time_hours, feature_index, osmid, background, canyon_velocity, concentration, year=None, solver_background=0.0)
write_sweep Writes a sweep of SIRANE decks under out_dir -- one per (direction, speed, source street, variant) -- plus runs.csv, a manifest of each deck's parameters; returns out_dir. write_sweep(out_dir, base_case, *, directions_deg, speeds, sources='unit_impulse', species='NO2', variants=None, chapman=0)

Other classes

Supporting types.

Name What it does How to call it
StreetSources Every street's roof flux as sub-sources (source_points). StreetSources(xy, owner, weight, width, clamp)

Functions

Calculations and helpers.

Name What it does How to call it
bessel_shape_parameter
also soulhac_shape
The shape parameter c of the Bessel canyon profile: the root of bessel_shape_residual, batched and differentiable in ratio. bessel_shape_parameter(ratio)
boundary_layer Friction velocity and the boundary-layer height from the network-mean geometry and reference wind, batched and differentiable. boundary_layer(h_mean, u_ref, h_abl, *, z_ref=30.0, kappa=0.4, pblh_floor=None)
canopy_displacement_roughness
also macdonald_profile
Macdonald (1998) displacement height and roughness length, (d_c, z0c), in metres. canopy_displacement_roughness(h_mean, w_mean, *, lambda_p=0.4, big_delta=4.43, small_delta=1.0, c_db=1.2, kappa=0.41)
canyon_velocity The SIGNED along-canyon velocity, m/s. Positive means from u to v. canyon_velocity(W, H, phi, *, u_star=None, u_h=None, canyon_wind=None, z0_b=0.15, z0_s=0.01, kappa=0.4, canyon_wind_min=0.0, shape_constant='exact_root')
direction_offsets Wind-direction samples and their weights, (offsets (..., m), weights (..., m)). direction_offsets(direction_averaging=None, sigma_theta=None, *, n_theta=None)
downwind_cutoff SIRANE's downwind cut-off, m: meteo_cell_dx / \|cos theta_w\|, the x-size of the meteo-grid cell over the cosine of the wind's angle to the x axis (an empirical rule: 900 m cells give 910 m, 700 m cells 700 m whatever the cells' y-size, and a wind at 45 degrees to the axis ~1020 m; \|cos\| is floored at 1e-6). It holds for cells of about 700 m and larger: with smaller cells SIRANE stops the Gaussian plume much earlier (about 168 m for 350 m cells) and continues with a cell-based far field, which this kernel does not model. downwind_cutoff(theta_w, meteo_cell_dx)
drivers_at The driver mapping at time index k for model, built from case. drivers_at(case, model, k, *, species=None)
exact_gaussian_direction_samples
also sirane_direction_samples
The EXACT Gaussian direction average of the junction routing, as samples and weights (offsets (..., n_j, m), weights (..., n_j, m)) -- SIRANE's P^_ij(phi0) = int f(phi - phi0) P_ij(phi) dphi (Soulhac et al. 2011, Atmos. Environ. 45:7379, Eq. 7) with f the normal density of standard deviation sigma_theta. exact_gaussian_direction_samples(theta, sigma_theta, slot_angle, slot_active)
exchange_velocity The roof exchange velocity u_d, m/s. exchange_velocity(sigma_w, H, W, *, roof_exchange=None, u_d_min=0.0, sigma_w_min=0.0)
grid_shape_parameter The shape parameter c the way MUNICH evaluates it: the point of the grid c = 0.01, 0.02, ..., 1.00 that minimises \|2/c exp((pi/2) Y1(c)/J1(c) - gamma_E) - z0/di\| (ATM ComputeSiraneC, MeteorologyStreet.cxx:114-152; the first minimum wins on a tie, as MUNICH's strict > makes it). grid_shape_parameter(ratio)
exposure_reduction_adjoint Exposure reduction from cutting each listed street's emission by delta_fraction of their mean, to first order, from ONE steady solve and one backward pass (the adjoint); compare exposure_reduction_forward. exposure_reduction_adjoint(model, state, drivers, *, population, streets, delta_fraction=0.1)
exposure_reduction_forward The same exposure reductions as exposure_reduction_adjoint, by brute force: one steady solve per street with that street's emission cut (len(streets) + 1 solves, no linearisation). exposure_reduction_forward(model, state, drivers, *, population, streets, delta_fraction=0.1)
from_test_network A fixed four-junction, three-street test geometry. from_test_network()
from_ug_m3 ug/m3 -> kg/m3. from_ug_m3(x)
j_no2_elevation_cloud
also j_no2_sirane
SIRANE's NO2 photolysis rate k1 (1/s) at solar elevation elevation_deg under cloud_octas of cloud (0 to 8). j_no2_elevation_cloud(elevation_deg, cloud_octas=0.0)
j_no2_zenith_table Clear-sky J_NO2 [1/s] at zenith_deg, times attenuation. j_no2_zenith_table(zenith_deg, attenuation=1.0)
junction_kernel batch + (n_streets, n_junctions): C_ext above street i (at its midpoint) per unit vertical-flux rate out of junction j, s/m3 -- SIRANE's intersection plume (Soulhac et al. 2011, Sect. 5.2: C_ext sums the contributions of upwind streets AND intersections), each junction one point source of junction_sources' width and clamp. table, theta_w, cutoff_sigma and meteo_cell_dx are plume_kernel's. junction_kernel(net, table, *, theta_w, cutoff_sigma=4.0, meteo_cell_dx=None)
junction_sources (xy (J, 2), width (J,), clamp (J,)): every junction as one point source of width W_j and clamp 1/H_j, with W_j, H_j the mean width and height of the streets that meet at the junction. The mean is a choice: on SIRANE's South Kensington fluxes, at the deck's downwind cut-off, it gives SIRANE's C_ext to a median 0.32 % and p90 1.38 %, against 0.35 % and 1.38 % with the largest width and height, and 0.53 % and 1.48 % with the smallest. junction_sources(net)
k_no_o3_jpl2003 k(NO + O3) = 3.0e-12 exp(-1500/T) cm3 molecule^-1 s^-1, returned in m3 mol^-1 s^-1. k_no_o3_jpl2003(temperature)
k_no_o3_soulhac2011
also k_no_o3_sirane
k(NO + O3) = 1.325e6 exp(-1430/T) m3 mol^-1 s^-1 at temperature (K), SIRANE's rate (Soulhac et al. 2011, with the prefactor that reproduces SIRANE v2.1 output). k_no_o3_soulhac2011(temperature)
twelve_street_grid
also munich_idealised
The 12-street network of Kim et al. 2022 Fig. 1, p. 7374. twelve_street_grid(*, L=100.0, W=20.0, H=20.0)
n_theta_rectangle_rule
also n_theta_munich
The sample count of the rectangle_rule direction average: ntheta = floor(sigma_theta in DEGREES) (MUNICH, SRC :3568), as a long tensor, clamped to [1, 10]. MUNICH skips the averaging entirely at 1, i.e. below two degrees. n_theta_rectangle_rule(sigma_theta)
node_closure MUNICH's roof closure at an intersection, before any routing (SRC :2980-3014). node_closure(flux)
photostationary_for_streets The Leighton reaction wired to the "no", "no2" and "o3" columns of species. photostationary_for_streets(species, *, preset=None, no_o3_rate=None, floor_ppb=None, j_key='J_NO2', temperature_key='temperature', molar_volume_key='molar_volume')
plume_kernel Concentration at each receptor per unit rate at each source, s/m3: batch + (R, P). plume_kernel(receptor_xy, source_xy, table, *, theta_w, width=0.0, clamp=inf, cutoff_sigma=4.0, meteo_cell_dx=None)
plume_table SIRANE's plume trajectory for each hour, resampled onto a 10 m grid (module docstring: the table). plume_table(*, u_star, h_abl, lmo, h_canopy, z0, d, sigma_theta, theta_star=None, temperature=288.15, sigma_v_min=0.5, sigma_w_min=0.3, tau_y=None, tau_z=None, centre_c=10.0, centre_k=0.675, x_max=700.0, kappa=0.4, neutral_abs_lmo=inf)
roof_wind Wind speed at roof level u_H, from the friction velocity. roof_wind(u_star, H, W, *, roof_wind=None, z0_s=0.01, kappa=0.41, h_mean=None, w_mean=None, n_levels=100, shape_constant='exact_root')
routing_matrix The street-to-street flux matrix F[..., p, r] from ORDERED, closed marginals. routing_matrix(flux_in, flux_out, *, junction_routing=None)
shape_parameter The Bessel shape parameter c, by the shape_constant closure option: "exact_root" is bessel_shape_parameter (the continuous root), "grid_search" is grid_shape_parameter (MUNICH's argmin on a 0.01 grid). shape_parameter(ratio, shape_constant='exact_root')
sigma_theta_turbulence_intensity
also sigma_theta_munich
sigma_theta = min(sigma_v / U, 10 deg) (SRC :3567; Blackadar 1997, Soulhac 2009), the direction_spread="turbulence_intensity" spread. sigma_theta_turbulence_intensity(sigma_v, u_ref)
solar_elevation The solar elevation (degrees) at latitude_deg, day day_of_year (1 January = 1) and clock time hour (fractional hours), in SIRANE's convention. solar_elevation(latitude_deg, day_of_year, hour)
source_points Each street's roof flux as SIRANE's sub-sources: a street u -> v of coordinate length L is cut into n = floor(L / SUBSOURCE_LENGTH_M) + 1 equal segments with one sub-source at the centre of each, weight 1/n, width (L/n)\|sin phi\| and clamp min(CLAMP_LENGTH_M / W, 1/(H (1 - \|sin phi\|))), where phi is the angle between the street and the wind theta_w (radians counter-clockwise from east, the direction the wind blows TOWARD; any batch shape) and W, H the street's width and height. The coordinate length is used, not Street.length, because the points lie on the drawn segment. source_points(net, *, theta_w)
street_geometry The plain-tensor view of net that StreetFlows consumes. street_geometry(net)
street_index Street name -> its row of "<layer>.x". street_index(model, *, layer_name='street')
street_kernel batch + (n_streets, n_streets): K[..., i, j] is C_ext above street i (at its midpoint) per unit roof-flux rate of street j, s/m3. street_kernel(net, table, *, theta_w, self_contribution=False, cutoff_sigma=4.0, meteo_cell_dx=None)
street_population p_i = n_total * H_i L_i / sum_j H_j L_j (their eq. 6 with a common building depth and storey height, which cancel). street_population(net, *, n_total)
street_steady The steady state of transport AND chemistry, by an explicit fixed point. street_steady(model, state, drivers, *, reaction=None, tol=1e-14, max_iter=50, layer_name='street', **solve_kwargs)
street_steady_with_plume The steady state of the street network with C_ext from the above-roof plumes. street_steady_with_plume(model, state, drivers, *, kernel, junction_kernel, junction_source='upward', background=None, tol=1e-12, max_iter=100, relaxation=1.0, adjoint_rtol=1e-12, adjoint_restart=20, diagnostics=None, layer_name='street', **solve_kwargs)
to_ug_m3 kg/m3 -> ug/m3. to_ug_m3(x)
total_exposure Population-weighted inhaled dose rate, sum_i q p_i c_i: breathing rate q times each street's population p_i times its concentration c_i. total_exposure(concentration, population, *, q=0.0001586111111111111)

Constants and tables

Named values and option tables.

Name Value
ASPECT_RATIO_EXCHANGE_BETA
also SCHULTE_BETA
0.45015815807855303
EULER_GAMMA_TRUNCATED
also GAMMA_E
0.577
EXCHANGE_SIGMA_W_RATIO
also SIRANE_EXCHANGE
0.22507907903927651
GAUSS_CUTOFF_SIGMAS
also SEUIL_GAUSS
4.0
KAPPA_040
also KAPPA
0.4
KAPPA_041
also KAPPA_MUNICH
0.41
OPTIONS table of 9 entries: canyon_wind, roof_wind, roof_exchange, junction_routing, direction_averaging, direction_spread, stability, sigma_w_height, ...
PRESETS table of 2 entries: sirane, munich
Q_INHALATION 0.0001586111111111111
Z0_B_DEFAULT 0.15
Z0_S_DEFAULT 0.01