diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index b1fc09fc..4d2a9210 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -96,14 +96,6 @@ def interesting_times(model: models.ExposureModel, approx_n_pts=100) -> typing.L return nice_times -def short_range_interesting_times(model: models.ExposureModel, times: typing.List[float]) -> typing.List[float]: - short_range_times : typing.List[float] = [] - for period in model.concentration_model.infected.short_range_presence: - start, finish = tuple(period.boundaries()) - short_range_times = short_range_times + [time for time in times if time >= start and time <= finish] - return short_range_times - - def calculate_report_data(model: models.ExposureModel): times = interesting_times(model) short_range_intervals = [] diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 31f589dc..33065bbe 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -503,7 +503,11 @@ baseline_model = models.ExposureModel( ), evaporation_factor=0.3, ), - short_range=[], + short_range=models.ShortRangeModel( + presence=[], + expirations=[], + dilutions=[], + ), exposed=models.Population( number=10, presence=models.SpecificInterval(((8., 12.), (13., 17.))), diff --git a/cara/models.py b/cara/models.py index f95dc1d8..db23fafb 100644 --- a/cara/models.py +++ b/cara/models.py @@ -635,6 +635,9 @@ class _ExpirationBase: raise NotImplementedError("Subclass must implement") def jet_origin_concentration(self): + """ + concentration of viruses at the jet origin (mL/m3). + """ raise NotImplementedError("Subclass must implement") @@ -896,7 +899,7 @@ class InfectedPopulation(_PopulationWithVirus): class ConcentrationModel: room: Room ventilation: _VentilationBase - infected: _PopulationWithVirus + infected: InfectedPopulation #: evaporation factor: the particles' diameter is multiplied by this # factor as soon as they are in the air (but AFTER going out of the, @@ -1078,7 +1081,7 @@ class ShortRangeModel: expirations: typing.List[Expiration] #: The dilution factors for each of the expiratory activity in the short range interactions - dilutions: _VectorisedFloat + dilutions: typing.List[_VectorisedFloat] def _normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: # normalized only by the viral load diff --git a/cara/tests/conftest.py b/cara/tests/conftest.py index 27ce9f1d..e5d94fe6 100644 --- a/cara/tests/conftest.py +++ b/cara/tests/conftest.py @@ -6,7 +6,7 @@ import pytest @pytest.fixture -def baseline_model(): +def baseline_concentration_model(): model = models.ConcentrationModel( room=models.Room(volume=75), ventilation=models.AirChange( @@ -30,14 +30,24 @@ def baseline_model(): @pytest.fixture -def baseline_exposure_model(baseline_model): +def baseline_sr_model(): + return models.ShortRangeModel( + presence=[], + expirations=[], + dilutions=[], + ) + + +@pytest.fixture +def baseline_exposure_model(baseline_concentration_model, baseline_sr_model): return models.ExposureModel( - baseline_model, + baseline_concentration_model, + baseline_sr_model, exposed=models.Population( number=1000, - presence=baseline_model.infected.presence, - activity=baseline_model.infected.activity, - mask=baseline_model.infected.mask, + presence=baseline_concentration_model.infected.presence, + activity=baseline_concentration_model.infected.activity, + mask=baseline_concentration_model.infected.mask, host_immunity=0., ), ) diff --git a/cara/tests/models/test_exposure_model.py b/cara/tests/models/test_exposure_model.py index 238e2316..ae185852 100644 --- a/cara/tests/models/test_exposure_model.py +++ b/cara/tests/models/test_exposure_model.py @@ -90,8 +90,8 @@ def known_concentrations(func): np.array([40.91708675, 91.46172332]), np.array([51.6749232285, 80.3196524031])], ]) def test_exposure_model_ndarray(population, cm, - expected_exposure, expected_probability): - model = ExposureModel(cm, population) + expected_exposure, expected_probability, sr_model): + model = ExposureModel(cm, sr_model, population) np.testing.assert_almost_equal( model.deposited_exposure(), expected_exposure ) @@ -110,10 +110,10 @@ def test_exposure_model_ndarray(population, cm, [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure): +def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure, sr_model): cm = known_concentrations( lambda t: 0. if np.floor(t) % 2 else np.array([1.2, 1.2])) - model = ExposureModel(cm, population) + model = ExposureModel(cm, sr_model, population) np.testing.assert_almost_equal( model.deposited_exposure(), expected_deposited_exposure @@ -128,17 +128,17 @@ def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exp [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_vector(population, expected_deposited_exposure): +def test_exposure_model_vector(population, expected_deposited_exposure, sr_model): cm_array = known_concentrations(lambda t: np.array([1.2, 1.2])) - model_array = ExposureModel(cm_array, population) + model_array = ExposureModel(cm_array, sr_model, population) np.testing.assert_almost_equal( model_array.deposited_exposure(), np.array(expected_deposited_exposure) ) -def test_exposure_model_scalar(): +def test_exposure_model_scalar(sr_model): cm_scalar = known_concentrations(lambda t: 1.2) - model_scalar = ExposureModel(cm_scalar, populations[0]) + model_scalar = ExposureModel(cm_scalar, sr_model, populations[0]) expected_deposited_exposure = 1.52436206 np.testing.assert_almost_equal( model_scalar.deposited_exposure(), expected_deposited_exposure @@ -169,6 +169,14 @@ def conc_model(): ) +@pytest.fixture +def sr_model(): + return models.ShortRangeModel( + presence=[], + expirations=[], + dilutions=[], + ) + # Expected deposited exposure were computed with a trapezoidal integration, using # a mesh of 10'000 pts per exposed presence interval. @pytest.mark.parametrize( @@ -183,17 +191,17 @@ def conc_model(): ] ) def test_exposure_model_integral_accuracy(exposed_time_interval, - expected_deposited_exposure, conc_model): + expected_deposited_exposure, conc_model, sr_model): presence_interval = models.SpecificInterval((exposed_time_interval,)) population = models.Population( 10, presence_interval, models.Mask.types['Type I'], models.Activity.types['Standing'], 0., ) - model = ExposureModel(conc_model, population) + model = ExposureModel(conc_model, sr_model, population) np.testing.assert_allclose(model.deposited_exposure(), expected_deposited_exposure) -def test_infectious_dose_vectorisation(): +def test_infectious_dose_vectorisation(sr_model): infected_population = models.InfectedPopulation( number=1, presence=halftime, @@ -216,7 +224,7 @@ def test_infectious_dose_vectorisation(): 10, presence_interval, models.Mask.types['Type I'], models.Activity.types['Standing'], 0., ) - model = ExposureModel(cm, population) + model = ExposureModel(cm, sr_model, population) inf_probability = model.infection_probability() assert isinstance(inf_probability, np.ndarray) assert inf_probability.shape == (3, ) diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 2ab80fc8..78c4541f 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -6,10 +6,10 @@ import cara.models as models import cara.data as data -def test_no_mask_superspeading_emission_rate(baseline_model): +def test_no_mask_superspeading_emission_rate(baseline_concentration_model): expected_rate = 48500. npt.assert_allclose( - [baseline_model.infected.emission_rate(float(t)) for t in [0, 1, 4, 4.5, 5, 8, 9]], + [baseline_concentration_model.infected.emission_rate(float(t)) for t in [0, 1, 4, 4.5, 5, 8, 9]], [0, expected_rate, expected_rate, 0, 0, expected_rate, 0], rtol=1e-12 ) @@ -38,10 +38,10 @@ def baseline_periodic_hepa(): ) -def test_concentrations(baseline_model): +def test_concentrations(baseline_concentration_model): # expected concentrations were computed analytically ts = [0, 4, 5, 7, 10] - concentrations = [baseline_model.concentration(float(t)) for t in ts] + concentrations = [baseline_concentration_model.concentration(float(t)) for t in ts] npt.assert_allclose( concentrations, [0.000000e+00, 20.805628, 6.602814e-13, 20.805628, 2.09545e-26], @@ -49,13 +49,13 @@ def test_concentrations(baseline_model): ) -def test_smooth_concentrations(baseline_model): +def test_smooth_concentrations(baseline_concentration_model): # We don't care about the actual concentrations in this test, but rather # that the curve itself is smooth. dx = 0.002 dy_limit = 0.2 # Anything more than this (in relative) is a bit steep. ts = np.arange(0, 10, dx) - concentrations = [baseline_model.concentration(float(t)) for t in ts] + concentrations = [baseline_concentration_model.concentration(float(t)) for t in ts] assert np.abs(np.diff(concentrations)).max()/np.mean(concentrations) < dy_limit @@ -367,10 +367,11 @@ def test_concentrations_refine_times(time): npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-8) -def build_exposure_model(concentration_model): +def build_exposure_model(concentration_model, short_range_model): infected = concentration_model.infected return models.ExposureModel( concentration_model=concentration_model, + short_range=short_range_model, exposed=models.Population( number=10, presence=infected.presence, @@ -390,13 +391,13 @@ def build_exposure_model(concentration_model): ['Jun', 1721.03336729], ], ) -def test_exposure_hourly_dep(month,expected_deposited_exposure): +def test_exposure_hourly_dep(month,expected_deposited_exposure, baseline_sr_model): m = build_exposure_model( build_hourly_dependent_model( month, intervals_open=((0., 24.), ), intervals_presence_infected=((8., 12.), (13., 17.)) - ) + ), baseline_sr_model ) deposited_exposure = m.deposited_exposure() npt.assert_allclose(deposited_exposure, expected_deposited_exposure) @@ -411,14 +412,14 @@ def test_exposure_hourly_dep(month,expected_deposited_exposure): ['Jun', 1799.17597184], ], ) -def test_exposure_hourly_dep_refined(month,expected_deposited_exposure): +def test_exposure_hourly_dep_refined(month,expected_deposited_exposure, baseline_sr_model): m = build_exposure_model( build_hourly_dependent_model( month, intervals_open=((0., 24.),), intervals_presence_infected=((8., 12.), (13., 17.)), temperatures=data.GenevaTemperatures, - ) + ), baseline_sr_model ) deposited_exposure = m.deposited_exposure() npt.assert_allclose(deposited_exposure, expected_deposited_exposure, rtol=0.02) diff --git a/cara/tests/test_monte_carlo.py b/cara/tests/test_monte_carlo.py index 8a8b0271..ba6a3de6 100644 --- a/cara/tests/test_monte_carlo.py +++ b/cara/tests/test_monte_carlo.py @@ -38,7 +38,7 @@ def test_type_annotations(): @pytest.fixture -def baseline_mc_model() -> cara.monte_carlo.ConcentrationModel: +def baseline_mc_concentration_model() -> cara.monte_carlo.ConcentrationModel: mc_model = cara.monte_carlo.ConcentrationModel( room=cara.monte_carlo.Room(volume=cara.monte_carlo.sampleable.Normal(75, 20)), ventilation=cara.monte_carlo.SlidingWindow( @@ -62,21 +62,31 @@ def baseline_mc_model() -> cara.monte_carlo.ConcentrationModel: @pytest.fixture -def baseline_mc_exposure_model(baseline_mc_model) -> cara.monte_carlo.ExposureModel: +def baseline_mc_sr_model() -> cara.monte_carlo.ShortRangeModel: + return cara.monte_carlo.ShortRangeModel( + presence=[], + expirations=[], + dilutions=[], + ) + + +@pytest.fixture +def baseline_mc_exposure_model(baseline_mc_concentration_model, baseline_mc_sr_model) -> cara.monte_carlo.ExposureModel: return cara.monte_carlo.ExposureModel( - baseline_mc_model, + baseline_mc_concentration_model, + baseline_mc_sr_model, exposed=cara.models.Population( number=10, - presence=baseline_mc_model.infected.presence, - activity=baseline_mc_model.infected.activity, - mask=baseline_mc_model.infected.mask, + presence=baseline_mc_concentration_model.infected.presence, + activity=baseline_mc_concentration_model.infected.activity, + mask=baseline_mc_concentration_model.infected.mask, host_immunity=0., ) ) -def test_build_concentration_model(baseline_mc_model: cara.monte_carlo.ConcentrationModel): - model = baseline_mc_model.build_model(7) +def test_build_concentration_model(baseline_mc_concentration_model: cara.monte_carlo.ConcentrationModel): + model = baseline_mc_concentration_model.build_model(7) assert isinstance(model, cara.models.ConcentrationModel) assert isinstance(model.concentration(time=0.), float) conc = model.concentration(time=1.) diff --git a/cara/tests/test_monte_carlo_full_models.py b/cara/tests/test_monte_carlo_full_models.py index b4aa247d..70b0e498 100644 --- a/cara/tests/test_monte_carlo_full_models.py +++ b/cara/tests/test_monte_carlo_full_models.py @@ -41,7 +41,15 @@ TorontoTemperatures = { # in the following tests, were obtained from the feature/mc branch @pytest.fixture -def shared_office_mc(): +def sr_model_mc() -> mc.ShortRangeModel: + return mc.ShortRangeModel( + presence=[], + expirations=[], + dilutions=[], + ) + +@pytest.fixture +def shared_office_mc(sr_model_mc): """ Corresponds to the 1st line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ @@ -72,6 +80,7 @@ def shared_office_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=3, presence=mc.SpecificInterval(present_times=((0, 3.5), (4.5, 9))), @@ -83,7 +92,7 @@ def shared_office_mc(): @pytest.fixture -def classroom_mc(): +def classroom_mc(sr_model_mc): """ Corresponds to the 2nd line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ @@ -114,6 +123,7 @@ def classroom_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=19, presence=models.SpecificInterval(((0, 2), (2.5, 4), (5, 7), (7.5, 9))), @@ -125,7 +135,7 @@ def classroom_mc(): @pytest.fixture -def ski_cabin_mc(): +def ski_cabin_mc(sr_model_mc): """ Corresponds to the 3rd line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ @@ -147,6 +157,7 @@ def ski_cabin_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=3, presence=models.SpecificInterval(((0, 20/60),)), @@ -158,7 +169,7 @@ def ski_cabin_mc(): @pytest.fixture -def skagit_chorale_mc(): +def skagit_chorale_mc(sr_model_mc): """ Corresponds to the 4th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, assuming viral is 10**9 instead of a LogCustomKernel distribution. @@ -186,6 +197,7 @@ def skagit_chorale_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=60, presence=models.SpecificInterval(((0, 2.5), )), @@ -197,7 +209,7 @@ def skagit_chorale_mc(): @pytest.fixture -def bus_ride_mc(): +def bus_ride_mc(sr_model_mc): """ Corresponds to the 5th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, assuming viral is 5*10**8 instead of a LogCustomKernel distribution. @@ -225,6 +237,7 @@ def bus_ride_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=67, presence=models.SpecificInterval(((0, 1.67), )), @@ -236,7 +249,7 @@ def bus_ride_mc(): @pytest.fixture -def gym_mc(): +def gym_mc(sr_model_mc): """ Gym model for testing """ @@ -259,6 +272,7 @@ def gym_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=28, presence=concentration_mc.infected.presence, @@ -270,7 +284,7 @@ def gym_mc(): @pytest.fixture -def waiting_room_mc(): +def waiting_room_mc(sr_model_mc): """ Waiting room model for testing """ @@ -293,6 +307,7 @@ def waiting_room_mc(): ) return mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=14, presence=concentration_mc.infected.presence, @@ -340,7 +355,7 @@ def test_report_models(mc_model, expected_pi, expected_new_cases, ], ) def test_small_shared_office_Geneva(mask_type, month, expected_pi, - expected_dose, expected_ER): + expected_dose, expected_ER, sr_model_mc): concentration_mc = mc.ConcentrationModel( room=models.Room(volume=33, humidity=0.5), ventilation=models.MultipleVentilation( @@ -370,6 +385,7 @@ def test_small_shared_office_Geneva(mask_type, month, expected_pi, ) exposure_mc = mc.ExposureModel( concentration_model=concentration_mc, + short_range=sr_model_mc, exposed=mc.Population( number=1, presence=concentration_mc.infected.presence,