Skip to content

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)
Where 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]
Functions
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.

Functions
__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 (x, y, z) -> Quantity[s / m**3].

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'