Averaging: putting everything together
The averaging submodule brings all the components together to compute long term and short term dispersion. Here, short term is a special case of long term (long term with only one weather category). So, we use the averaging infrastructure as the base API for dispersion / deposition calculation.
Advanced PKI Submersion
Photon point-kernel integration uses the weather-averaged physical
chi_k value at arbitrary 3-D quadrature points. Use
WeatherAverager.average_dispersion_at_points for this path; it preserves the
paired broadcast shape of the east, north, and up coordinates and returns
s/m^3. It bypasses the use of the regulatory chi_ks submersion factor.
Coordinates are relative to the chimney base. The z coordinate is absolute
up in this chimney-centered frame, so local terrain is subtracted before the
plume is evaluated. Points below terrain are masked. PKI directly samples the
plume at quadrature points, but does block photon rays at terrain
intersections.
Submersion Surrogate based on PKI
The G-14 polynomial submersion model, chi_ks, is based on a limited set of
PKI simulations and remains the regulatory default. This package also
implements a dense neural surrogate trained on exact local-flat PKI labels.
It predicts an equivalent semi-infinite-cloud dispersion, accepts signed
downwind positions (including the source plane and upwind observers), and
takes photon energy into account.
Users can select this method globally by setting this attribute in the WeatherAverager class before running scenarios:
from ensi_g14_calculator.averaging.weather_averager import WeatherAverager
WeatherAverager.submersion_model = "pki_surrogate"
The class attribute then reaches WeatherAverager instances constructed
internally by scenario runners.
Or you can set the attribute locally on each instantiated weather averager.
When a nuclide has a packaged PKI photon spectrum, scenario dose calculations
evaluate every photon line at its own energy and sum the line contributions
using their yields and ISO fluence-to-dose coefficients. The spectrum-summed
surrogate path is photon-only; it does not reuse the nuclide-level regulatory
submersion coefficient or include beta contributions. The
submersion_surrogate_energy value (default 0.662 MeV) is used only when
requesting the generic equivalent-dispersion grid from average_dispersion.
The surrogate evaluates whole observer arrays and all lines in a spectrum at
once. It evaluates elevated and ground plume components separately,
reconstructs them with U_Ha and U0, and combines them exactly using G_t,
as does the G-14 model.
Unlike the G-14 branch, this model covers signed downwind observer positions and does not apply the regulatory 200 m submersion mask. V8 validates finite geometry, positive plume widths and velocity, and the packaged energy domain; its dense sampled phase-space range is not enforced as a hard cutoff, so extrapolated results still require validation.
ensi_g14_calculator.averaging
Modules
weather_averager
This file implements appendix A2.1.2 of ENSI-G14 2025, and in the process brings together all the pieces defined in the other files.
The WeatherAverager class is effectively the disperion / deposition computer for any coordinates. For computing short-term scenarios (single weather situation), simply pass a weather statistics with only one weather situation of probability 1.
Classes
SourceParams
dataclass
Just a container for the parameters of the chimney / the source.
WeatherStats
dataclass
A class holding the weather information,
its instances are to be passed to the averager methods of the WeatherAverager class.
Instantiate this like this:
weather = WeatherStats.from_dataframe(pandas_dataframe)
pandas_dataframe must have the following columns:
'category': 1..6 or A..F'speed': wind speed at measurement height (m/s)'angle': wind angle in your polar convention (deg)'probability': bin probability Optional for washout averaging:'rainfall_intensity': precipitation intensity (mm/h) [name configurable]
from_dataframe(df, rain_col='rainfall_intensity', check_deg=True, randomize_wind_direction_within_bins=True)
staticmethod
Instantiates a WeatherStats object from a pandas dataframe.
--> Reads a dataframe df, with columns {"weather_category", "wind_speed", "wind_direction", "probability"},
and additional a rain column (default "rainfall_intensity"), returns a WeatherStats instance to be used
with the averager.
More about the columns in the dataframe:
- weather_category: string, 'A' ... 'F'
- wind_speed: float, in m/s (without astropy units, just the value)
- wind_direction: float, in degrees, with 0° being North and 90° being East. Denotes the direction the wind is coming from.
- probability: probability of the specific weather scenario.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
pandas dataframe containing the weather situations, see above for required format |
required | |
rain_col
|
string, name of the column containing the rain information. default 'rainfall_intensity' |
'rainfall_intensity'
|
|
check_deg
|
bool, if True (default) throws a warning if all the angles are small (might indicate radians) |
True
|
|
randomize_wind_direction_within_bins
|
bool, default True, adds a small angle scatter within each wind direction bin. |
True
|
Returns:
| Type | Description |
|---|---|
|
WeatherStats instance |
WeatherAverager
Computes \(\chi_K\) / \(\chi_{KS}\) / washout \(W\), averaging over weather bins. This class puts together all the components:
- orography for elevation and coordinates.
- effective release height calculation.
- wind profile calculation.
- dispersion calculation (Vogt factors and gaussian plume).
- deposition, including reading nuclide-dependent deposition parameters.
This is why we use it both for long term (a probability-weighted average over weather situations) and short term (one weather situation with probability 1).
Some implemented guardrails:
- D (on-axis distance) \(\leq\) 0 => contribution 0 (wind blowing away / receptor upwind)
- prob \(\leq\) 0 or NaN => skipped
- wind speed \(\leq\) 0 => skipped
See main documentation for usage examples.
__init__(orography, source_params, recorder=NULL_RECORDER)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
orography
|
Orography
|
Orography instance |
required |
source_params
|
SourceParams
|
SourceParams, dataclass defined in this file for storing source params. |
required |
average_submersion_surrogate_dose_coefficient(x, y, weather, nuclide, normalise_probabilities=True, time_weight=None)
Return a spectrum-summed surrogate coefficient in Sv/Bq.
time_weight (optional) is applied at the receptor, see average_dispersion.
average_dispersion(x, y, weather, normalise_probabilities=True, time_weight=None, height=None)
Implementation of A2.1.2 ENSI-G14 2025, an average of dispersions across weather situations weighted by their probabilities. This method computes both the actual dispersion and the pseudo-dispersion used for submersion, and returns both as a tuple.
Note that this function does more than just averaging a list of pre-computed dispersions, it orchestrates their computation using the different pieces implemented in this package. Namely, it
- sets up the geometry (x and y are cartesian coordinates relative to the chimney, here we switch to polar and more importantly distances "on-axis" and "off-axis" relative to the wind for each weather situation),
- masks any upwind point in x and y as to avoid spurious values,
- computes U_Ha, H_eff_oro, G_t
- computes the gaussian plume parameters,
- implement the bounds of the ENSI G14 (e.g. cap distance to 5km even for more distant points)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array or scalar with astropy units, length |
required | |
y
|
array or scalar with astropy units, length -- must be the same shape as x |
required | |
weather
|
WeatherStats
|
WeatherStats instance, see docstring of WeatherStats -- 3-parameter weather statistic. |
required |
normalise_probabilities
|
whether to normalise the probabilities to 1 in the weather statistics. |
True
|
|
time_weight
|
optional callable t -> dimensionless factor, evaluated at the travel time t = x / U of each plume component, e.g. radioactive decay during transport (see dispersion/plume_decay.py). None: no weighting (G-14). For chi_K,S and the surrogate it is applied at the receptor. |
None
|
|
height
|
optional observer height above local ground (scalar or array like x), with units (length). Outside G-14; None means at ground level. chi_K is the plume at that height; for chi_K,S the vertical offset to the plume axis is used. Not supported by the PKI surrogate (trained for observers at ground level): use the exact PKI (average_dispersion_at_points). |
None
|
Returns:
| Type | Description |
|---|---|
|
tuple of arrays of the same shape as x and y, \(\chi_K\) and \(chi_{KS}\) (dispersion and pseudo-dispersio for submersion), unit s/m³. |
average_dispersion_at_points(x, y, z, weather, normalise_probabilities=True, time_weight=None)
Return weather-averaged physical dispersion at paired 3-D points. not used in the rest of this class, used for PKI submersion evaluations only. (as dispersion_evaluator, required by the quadrature)
x, y, and z are global chimney-centered coordinates. Their
broadcast shape is preserved in the result. Terrain is subtracted from
z to obtain local height, and points below terrain or upwind of a
weather bin contribute zero. Unlike :meth:average_dispersion, this
method computes only physical chi_k and obviously not the ensi submersion model one, chi_k_s.
time_weight (optional): callable t -> factor at the travel time t = x / U of each plume component,
e.g. radioactive decay during transport (see dispersion/plume_decay.py).
physical_dispersion_field(weather, normalise_probabilities=True, time_weight=None)
Return a callable for physical dispersion at cloud source points.
The returned callable accepts chimney-centred global (x, y, z)
coordinates of source-volume points in the east, north, up frame. It
evaluates only physical chi_k: source points upwind of the chimney
and points below local terrain are masked, but an observer's position
is never considered. This distinction is important for irradiation
behind the plume, where an upwind observer can still see downwind
cloud volume.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weather
|
WeatherStats
|
WeatherStats instance used for probability averaging. |
required |
normalise_probabilities
|
whether to normalise weather probabilities before averaging. |
True
|
|
time_weight
|
optional decay during transport, see average_dispersion_at_points. |
None
|
Returns:
| Type | Description |
|---|---|
|
callable |
average_washout(x, y, weather4d, nuclide, normalize_probabilities=False, rain_unit=u.mm / u.h, time_weight=None)
Same as average_dispersion, but for washout! Implementation of A2.2.2.2 ENSI-G14. Again this function does more than just averaging, it sets up the geometry (see average_dispersion doc string).
here, we most likely do NOT want to normalize the cumulated probability: indeed, ain't raining all the time bro.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array or scalar with astropy units, length. |
required | |
y
|
array or scalar with astropy units, length -- must be the same shape as x. |
required | |
weather4d
|
WeatherStats
|
WeatherStats instance, see docstring of WeatherStats -- 4-parameter weather statistic. |
required |
nuclide
|
string, e.g. 'Cs-137'. |
required | |
normalize_probabilities
|
whether to normalize the probabilities to 1 in the weather statistics. |
False
|
|
rain_unit
|
astropy unit to be attached to the rain values, default mm/h. |
mm / h
|
|
time_weight
|
optional decay during transport, see average_dispersion. |
None
|
Returns:
| Type | Description |
|---|---|
|
dict {"avg_W": avg_W, "p_sum": p_sum} |
weather_measurement_clustering
For cases when you get a huge weather file, say raw measurements over several years. Example in tests/exemple_data: ~45k rows, 5 minute intervals of weather data.
It just isn't smart to sum over everything with equal probability. Much cheaper to cluster these weather situations into representative bins. This is what this file is for: it takes in a database of weather measurements (typically with rain!), and spits out another database with weather bins, and the probability of each.
Functions
bin_weather_scenarios(df, n_clusters=400, wind_dir_unit='deg', random_state=0)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
dataframe, with columns "weather_category", "wind_direction" (of the wind), "wind_speed" (of the wind), and "rainfall_intensity". |
required | |
n_clusters
|
int, number of bins onto which we aim to represent the input weather measurements |
400
|
|
wind_dir_unit
|
string, deg or rad |
'deg'
|
|
random_state
|
int, seed, default 0. |
0
|
Returns:
| Type | Description |
|---|---|
|
tuple with the following elements: reduced_df: one row per cluster with representative original row + cluster weight. labels: cluster id per original row. model: fitted clustering model. |
plot_weather_pairs(df, reps_df, direction_unit='deg')
just a diagnostic tool to make sure our clustering "looks" represntative compared to the raw measurements.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
original dataframe (typically, measurements, many many rows.) |
required | |
reps_df
|
reduced representatives dataframe (one row per cluster) with same columns |
required | |
direction_unit
|
"deg" or "rad" (only affects axis label) |
'deg'
|