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 |
noneexact_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 |
neutralmonin_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_parameteralso 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_roughnessalso 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_samplesalso 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_cloudalso 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_soulhac2011also 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_gridalso 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_rulealso 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_intensityalso 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_BETAalso SCHULTE_BETA |
0.45015815807855303 |
EULER_GAMMA_TRUNCATEDalso GAMMA_E |
0.577 |
EXCHANGE_SIGMA_W_RATIOalso SIRANE_EXCHANGE |
0.22507907903927651 |
GAUSS_CUTOFF_SIGMASalso SEUIL_GAUSS |
4.0 |
KAPPA_040also KAPPA |
0.4 |
KAPPA_041also 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 |