Basics: building blocks and recipes
This page collects small, self-contained recipes for the individual components of the package. Every code block on this page is executed by the test-suite, so they can be copied as is. The blocks of a section build on the previous ones, as in a notebook.
For complete scenarios, see the short term and long term pages.
Installation
The package targets Python 3.9. From a clone of the repository:
git clone https://github.com/duxfrederic/ensi_g14_calculator.git
cd ensi_g14_calculator
python -m pip install .
or directly from GitHub:
python -m pip install git+https://github.com/duxfrederic/ensi_g14_calculator.git
Check that it works:
import ensi_g14_calculator
from ensi_g14_calculator.short_term_scenarios.runner import ScenarioEvaluator
print("import ok")
Units
Every physical quantity passed to the package carries an astropy
unit; results come back with units as well.
The sievert is not available in the astropy version supported (Python 3.9), so the package defines it: import it with
from ensi_g14_calculator.misc import Sv.
import numpy as np
from astropy.units import Bq, m, s, mm, year
from ensi_g14_calculator.misc import Sv
activity = 1e9 * Bq
distance = 500 * m
print(distance.to('km')) # 0.5 km, any length unit is converted internally
dose = 3.2e-6 * Sv
print(dose.value) # 3.2e-06, the bare float
print(dose.to_value(Sv) * 1e3) # 0.0032, in mSv (the prefixed units are not registered by name, scale by hand)
A short term dose at a single point
The ScenarioEvaluator accepts scalars, lines or grids for x and y
(positions in metres, east and north of the chimney base).
from ensi_g14_calculator.orography import Orography
from ensi_g14_calculator.averaging.weather_averager import SourceParams
from ensi_g14_calculator.short_term_scenarios.runner import ScenarioEvaluator
source = SourceParams(
H_a=75 * m, # height of the chimney (total of the stack)
H_G=15 * m, # height of the tallest adjacent buildings or structures
D_i=1.2 * m, # internal diameter of the chimney
D_e=1.2 * m, # external diameter (if unknown, take D_e = D_i as per the directive)
W_0=10.0 * m / s, # exhaust speed of the chimney
# z_m=10 * m, # optional: wind measurement height, default 10 m
)
evaluator = ScenarioEvaluator(source, Orography(flat=True))
nuclides = {'Cs-137': 1e10 * Bq}
results = evaluator.evaluate_short_term_dose(
nuclides_bq=nuclides,
x=800.0 * m, # 800 m east of the chimney ...
y=0.0 * m,
wind_direction=-90, # ... and the wind blows from the west, i.e. towards the point.
)
dose_first_8h = results.sum_doses(weather='D', interval='0-8h', age_group='adult')
print(dose_first_8h)
wind_direction is the direction the wind comes from, in degrees (north = 0, east = 90);
the default -90 is a wind from the west, so a point to the east of the chimney is downwind.
Where does the dose come from?
sum_doses takes optional filters, so you can split the dose by pathway, nuclide, interval or age group.
The pathways are returned by evaluator.short_term_dose; the safest way to list what a result holds is to
look at its keys:
keys = list(results._doses) # DoseKey(weather, nuclide, interval, pathway, age_group, model, is_reference)
pathways = sorted({k.pathway for k in keys})
intervals = sorted({k.interval for k in keys})
print(pathways)
print(intervals)
print(evaluator.age_groups)
for pathway in pathways:
d = results.sum_doses(weather='D', interval='0-8h', age_group='adult', pathway=pathway)
if d is not None:
print(f"{pathway:>16s}: {float(d.to_value(Sv)):.3e} Sv")
sum_doses returns None when no record matches the filters (e.g. a pathway that does not
exist for the requested interval).
Comparing the weather categories and the age groups
for weather in 'ABCDEF':
adult = results.sum_doses(weather=weather, interval='0-8h', age_group='adult')
toddler = results.sum_doses(weather=weather, interval='0-8h', age_group='toddler_1_year')
print(weather, f"adult: {float(adult.to_value(Sv)):.2e} Sv", f"toddler: {float(toddler.to_value(Sv)):.2e} Sv")
The age groups are 'adult', 'child_10_year' and 'toddler_1_year'.
A dose profile along a line
Passing arrays gives a profile; here the first-8h dose along the downwind axis:
import matplotlib
matplotlib.use("Agg") # no display needed
import matplotlib.pyplot as plt
x = np.linspace(200, 5000, 100) * m
y = np.zeros(100) * m
profile = evaluator.evaluate_short_term_dose(nuclides_bq=nuclides, x=x, y=y, wind_direction=-90)
fig, ax = plt.subplots()
for weather in 'ABCDEF':
ax.loglog(x.to_value(m), profile.sum_doses(weather=weather, interval='0-8h', age_group='adult').to_value(Sv),
label=weather)
ax.set_xlabel('distance downwind [m]')
ax.set_ylabel('dose, first 8 h [Sv]')
ax.legend()
fig.savefig('profile.png')
Changing the exposure time and the release date
The exposure time T_exp must be exactly 1 year (default) or 50 years, as in the directive.
The release date is a 'mm-dd' string (default '08-01') and determines which crops are exposed.
results_50y = evaluator.evaluate_short_term_dose(
nuclides_bq=nuclides, x=800.0 * m, y=0.0 * m,
T_exp=50 * year,
release_date='06-15',
)
print(results_50y.sum_doses(weather='D', interval='medium_term', age_group='adult'))
Using the wind speed override
The directive prescribes a wind of 1 m/s for short term scenarios. Should you need to deviate, set it on the evaluator (you are then leaving the scope of the directive):
custom = ScenarioEvaluator(source, Orography(flat=True))
custom.wind_speed = 3.0 * m / s
res = custom.evaluate_short_term_dose(nuclides_bq=nuclides, x=800.0 * m, y=0.0 * m)
print(res.sum_doses(weather='D', interval='0-8h', age_group='adult'))
Geometry: coordinates and elevation
Orography converts between WGS84 coordinates and the local metric frame centred on the chimney,
and interpolates elevation from an ASC file (see the long term page for how to get one).
The block below uses a small elevation map around Bern, shipped in the test data; adapt the path
and coordinates to your own file.
chimney_lat, chimney_lon = 46.93026329420960, 7.420436181732923
oro = Orography(asc_path='data/orography.asc', latitude0=chimney_lat, longitude0=chimney_lon)
group_lat, group_lon = 46.92626527964869, 7.41639267991673
gx, gy = oro.latlon_to_xy(group_lat, group_lon)
print(f"group at x={gx:.0f}, y={gy:.0f}")
print("elevation relative to chimney base:", oro.elevation_xy(gx, gy))
print("inside raster:", oro.contains_latlon(group_lat, group_lon))
Evaluate a short term scenario for that real location. Because the orography is used, the terrain influences the dispersion:
terrain_evaluator = ScenarioEvaluator(source, oro)
at_group = terrain_evaluator.evaluate_short_term_dose(
nuclides_bq=nuclides, x=gx, y=gy, wind_direction=45.0
)
print(at_group.sum_doses(weather='D', interval='0-8h', age_group='adult'))
Elevation profile between two points:
distance_along, elevation = oro.profile_along_line_xy(0 * m, 0 * m, gx, gy, n=50)
print(distance_along[-1], elevation.max())
The dispersion factor on its own
If you only need the atmospheric dispersion \(\chi\) (\(\mathrm{s/m^3}\)), use the WeatherAverager with
a weather statistic given as a table. Here a single bin (category D, 3 m/s, wind from the west):
import pandas as pd
from ensi_g14_calculator.averaging.weather_averager import WeatherAverager, WeatherStats
stats = WeatherStats.from_dataframe(
pd.DataFrame({
'weather_category': ['D'],
'wind_speed': [3.0],
'wind_direction': [270.0],
'probability': [1.0],
}),
randomize_wind_direction_within_bins=False,
)
averager = WeatherAverager(orography=Orography(flat=True), source_params=source)
xg, yg = np.meshgrid(np.linspace(-1000, 1000, 5), np.linspace(-1000, 1000, 5))
chi_k, chi_ks = averager.average_dispersion(xg * m, yg * m, stats)
print(chi_k.unit, chi_k.shape)
print(chi_k.max())
chi_k is the dispersion factor and chi_ks the pseudo-dispersion used for the submersion pathway.
With several weather bins the result is the probability-weighted average, which is what the
long term scenarios use.
Reading the results of a long term run
After a LongTermEvaluator.run (see the long term page), everything is in an sqlite3 file.
For an interactive breakdown of the dose (by chimney, nuclide, pathway, ...), use the dashboards in
ensi_g14_dashboard, see its readme.
To script your own analyses instead, the file can be queried from Python. Here we run a small case with the test data, then query it:
import pandas as pd
from astropy.units import m
from ensi_g14_calculator.long_term_scenarios import LongTermEvaluator
weather = pd.read_csv('data/weather_statistics.csv')
LE = LongTermEvaluator('results.sqlite3', 'data/orography.asc')
LE.run('data/chimneys.csv', 'data/exposed_groups.csv',
weather.drop('rainfall_intensity', axis=1), weather[weather['rainfall_intensity'] > 0.0],
'data/emission.csv', summer_total_precipitation=0.3 * m)
from ensi_g14_calculator.dose.dose_database import DoseDatabase
with DoseDatabase('results.sqlite3') as db:
print(db.get_total_dose('mcdo', 'adult')) # all chimneys / nuclides / pathways
print(db.get_dose_by_pathway('mcdo', 'adult')) # ... split by pathway
print(db.get_dose_by_nuclide('mcdo', 'adult')) # ... by nuclide
print(db.get_dose_by_chimney('mcdo', 'adult')) # ... by chimney
print(db.get_dose_breakdown('mcdo', 'adult', nuclide='H-3_HTO')) # one row per chimney/nuclide/pathway
All the query methods return pandas dataframes, with doses in Sv. Note that the pathway names of the long term
database (e.g. ingestion_milk_dairy) are not identical to the short term ones (ingestion_dairy).
The same table can be read directly with sqlite3 or pandas:
import sqlite3
with sqlite3.connect('results.sqlite3') as conn:
doses = pd.read_sql('SELECT * FROM doses', conn)
print(doses.columns.tolist())
print(doses.groupby(['group_id', 'age_group'])['dose_Sv'].sum())