diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index dfd5c581..0d1ea2b2 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -24,7 +24,7 @@ minutes_since_midnight = typing.NewType('minutes_since_midnight', int) # Used to declare when an attribute of a class must have a value provided, and # there should be no default value used. _NO_DEFAULT = object() -_DEFAULT_MC_SAMPLE_SIZE = 50000 +_DEFAULT_MC_SAMPLE_SIZE = 250000 @dataclasses.dataclass diff --git a/cara/models.py b/cara/models.py index b91e2407..7b5d034d 100644 --- a/cara/models.py +++ b/cara/models.py @@ -580,7 +580,7 @@ class MultipleExpiration(_ExpirationBase): Group together different modes of expiration, that represent each the main expiration mode for a certain fraction of time (given by the weights). - Obsolet class that can only be used with single diameters (it cannot + This class can only be used with single diameters (it cannot be used with diameter distributions). """ expirations: typing.Tuple[_ExpirationBase, ...] @@ -900,6 +900,13 @@ class ConcentrationModel: @dataclass(frozen=True) class ExposureModel: + """ + Represents the exposure to a concentration of virions in the air. + NOTE: the infection probability formula assumes that if the diameter + is an array, then none of the ventilation parameters, room volume or virus + decay constant, are arrays as well. + TODO: implement a check this is the case, in __post_init__ + """ #: The virus concentration model which this exposure model should consider. concentration_model: ConcentrationModel @@ -932,7 +939,12 @@ class ExposureModel: corresponds to an integration on diameters). """ emission_rate = self.concentration_model.infected.emission_rate_when_present() - if np.isscalar(self.concentration_model.infected.expiration.diameter): + if (not isinstance(self.concentration_model.infected,InfectedPopulation) + or not isinstance(self.concentration_model.infected.expiration,Expiration) + or np.isscalar(self.concentration_model.infected.expiration.diameter) + ): + # in all these cases, there is no distribution of + # diameters that need to be integrated over return self._normed_exposure() * emission_rate else: # the mean of the diameter-dependent exposure (including diff --git a/cara/tests/apps/calculator/test_model_generator.py b/cara/tests/apps/calculator/test_model_generator.py index 2c61c63e..a01cd3f1 100644 --- a/cara/tests/apps/calculator/test_model_generator.py +++ b/cara/tests/apps/calculator/test_model_generator.py @@ -12,10 +12,9 @@ from cara import models from cara.monte_carlo.data import expiration_distributions # TODO: seed better the random number generators -# this is only for test_blend_expiration np.random.seed(2000) -SAMPLE_SIZE = 500000 -TOLERANCE = 0.01 +SAMPLE_SIZE = 250000 +TOLERANCE = 0.02 def test_model_from_dict(baseline_form_data): form = model_generator.FormData.from_dict(baseline_form_data) diff --git a/cara/tests/test_monte_carlo_full_models.py b/cara/tests/test_monte_carlo_full_models.py index fbf9f105..b13ea0c7 100644 --- a/cara/tests/test_monte_carlo_full_models.py +++ b/cara/tests/test_monte_carlo_full_models.py @@ -5,10 +5,12 @@ import pytest import cara.monte_carlo as mc from cara import models,data from cara.monte_carlo.data import activity_distributions, virus_distributions +from cara.monte_carlo.data import expiration_distribution, expiration_distributions +from cara.apps.calculator.model_generator import build_expiration # TODO: seed better the random number generators np.random.seed(2000) -SAMPLE_SIZE = 50000 +SAMPLE_SIZE = 250000 TOLERANCE = 0.05 # references values for infection_probability and expected new cases @@ -42,10 +44,7 @@ def shared_office_mc(): presence=mc.SpecificInterval(((0., 2.), (2.1, 4.), (5., 7.), (7.1, 9.))), mask=models.Mask(η_inhale=0.3), activity=activity_distributions['Seated'], - expiration=models.MultipleExpiration( - expirations=(models.Expiration.types['Talking'], - models.Expiration.types['Breathing']), - weights=(0.3, 0.7)), + expiration=build_expiration({'Talking': 0.3, 'Breathing': 0.7}), ), ) return mc.ExposureModel( @@ -86,7 +85,7 @@ def classroom_mc(): presence=mc.SpecificInterval(((0., 2.), (2.5, 4.), (5., 7.), (7.5, 9.))), mask=models.Mask.types['No mask'], activity=activity_distributions['Light activity'], - expiration=models.Expiration.types['Talking'], + expiration=expiration_distributions['Talking'], ), ) return mc.ExposureModel( @@ -117,7 +116,7 @@ def ski_cabin_mc(): presence=mc.SpecificInterval(((0., 1/3),)), mask=models.Mask(η_inhale=0.3), activity=activity_distributions['Moderate activity'], - expiration=models.Expiration.types['Talking'], + expiration=expiration_distributions['Talking'], ), ) return mc.ExposureModel( @@ -150,7 +149,7 @@ def gym_mc(): presence=mc.SpecificInterval(((0., 1.),)), mask=models.Mask.types["No mask"], activity=activity_distributions['Heavy exercise'], - expiration=models.Expiration.types['Breathing'], + expiration=expiration_distributions['Breathing'], ), ) return mc.ExposureModel( @@ -182,10 +181,7 @@ def waiting_room_mc(): presence=mc.SpecificInterval(((0., 2.),)), mask=models.Mask.types["No mask"], activity=activity_distributions['Seated'], - expiration=models.MultipleExpiration( - expirations=(models.Expiration.types['Talking'], - models.Expiration.types['Breathing']), - weights=(0.3, 0.7)), + expiration=build_expiration({'Talking': 0.3, 'Breathing': 0.7}) ), ) return mc.ExposureModel( @@ -218,11 +214,7 @@ def skagit_chorale_mc(): presence=mc.SpecificInterval(((0., 2.5),)), mask=models.Mask.types["No mask"], activity=activity_distributions['Light activity'], - expiration=models.Expiration(10.0761), - # The aerosol diameter given (10.0761 microns) is an equivalent - # diameter, chosen in such a way that the aerosol volume is - # the same as the total aerosol volume given by the full BLO model - # with (5, 5, 5) for the B/L/O weights. + expiration=expiration_distribution((5., 5., 5.)), ), ) return mc.ExposureModel( @@ -295,10 +287,7 @@ def test_small_shared_office_Geneva(mask_type, month, expected_pi, presence=mc.SpecificInterval(((9., 10+2/3), (10+5/6, 12.5), (13.5, 15+2/3), (15+5/6, 18.))), mask=models.Mask.types[mask_type], activity=activity_distributions['Seated'], - expiration=models.MultipleExpiration( - expirations=(models.Expiration.types['Talking'], - models.Expiration.types['Breathing']), - weights=(0.33, 0.67)), + expiration=build_expiration({'Talking': 0.33, 'Breathing': 0.67}), ), ) exposure_mc = mc.ExposureModel(