diff --git a/caimira/docs/UML-CAiMIRA.png b/caimira/docs/UML-CAiMIRA.png index 35fd4fb3..57c6d3e4 100644 Binary files a/caimira/docs/UML-CAiMIRA.png and b/caimira/docs/UML-CAiMIRA.png differ diff --git a/caimira/docs/full_diameter_dependence.rst b/caimira/docs/full_diameter_dependence.rst index 3fbea5ba..bbe5cbef 100644 --- a/caimira/docs/full_diameter_dependence.rst +++ b/caimira/docs/full_diameter_dependence.rst @@ -69,14 +69,16 @@ In the code, for a given Expiration, we use different methods to perform the cal Note that the diameter-dependence is kept at this stage. Since other parameters downstream in code are also diameter-dependent, the Monte-Carlo integration over the aerosol sizes is computed at the level of the dose :math:`\mathrm{vD^{total}}`. In case one would like to have intermediate results for emission rate, perform the Monte-Carlo integration of :math:`E_{c, j}^{\mathrm{total}}` and compute :math:`\mathrm{vR^{total}} =\mathrm{vl_{in}} \cdot E_{c, j}^{\mathrm{total}} \cdot \mathrm{BR_k}`. -Concentration - C(t, D) -======================= +Virus Concentration - C(t, D) +============================= The estimate of the concentration of virus-laden particles in a given room is based on a two-box exposure model: * **Box 1** - long-range exposure: also known as the *background* concentration, corresponds to the exposure of airborne virions where the susceptible (exposed) host is more than 2 m away from the infected host(s), considering the result of a mass balance equation between the emission rate of the infected host(s) and the removal rates from the environmental/virological characteristics. * **Box 2** - short-range exposure: also known as the *exhaled jet* concentration in close-proximity, corresponds to the exposure of airborne virions where the susceptible (exposed) host is distanced between 0.5 and 2 m from an infected host, considering the result of a two-stage exhaled jet model. +Note that most of the methods used to calculate the concentration are defined in the superclass :meth:`caimira.models._ConcentrationModelBase`, while the specific methods for the long-range virus concentration are part of the subclass :meth:`caimira.models.ConcentrationModel`. + Long-range approach ******************* @@ -84,8 +86,8 @@ The long-range concentration of virus-laden aerosols of a given size :math:`D`, :math:`C_{\mathrm{LR}}(t, D)=\frac{\mathrm{vR}(D) \cdot N_{\mathrm{inf}}}{\lambda_{\mathrm{vRR}}(D) \cdot V_r}-\left (\frac{\mathrm{vR}(D) \cdot N_{\mathrm{inf}}}{\lambda_{\mathrm{vRR}}(D) \cdot V_r}-C_0(D) \right )e^{-\lambda_{\mathrm{vRR}}(D)t}` , -and computed, as a function of the exposure time and particle diameter, in the :meth:`caimira.models.ConcentrationModel.concentration` method. -The long-range concentration, integrated over the exposure time (in piecewise constant steps), :math:`C(D)`, is given by :meth:`caimira.models.ConcentrationModel.integrated_concentration`. +and computed, as a function of the exposure time and particle diameter, in the :meth:`caimira.models._ConcentrationModelBase.concentration` method. +The long-range concentration, integrated over the exposure time (in piecewise constant steps), :math:`C(D)`, is given by :meth:`caimira.models._ConcentrationModelBase.integrated_concentration`. In the :math:`C_{\mathrm{LR}}(t, D)` equation above, the **emission rate** - :math:`\mathrm{vR}(D)` - and the **viral removal rate** - :math:`\lambda_{\mathrm{vRR}}(D)`, :meth:`caimira.models.ConcentrationModel.infectious_virus_removal_rate` - are both diameter-dependent. One can show that the resulting concentration is always proportional to the emission rate :math:`\mathrm{vR}(D)`. Hence, for computational speed-up purposes @@ -93,8 +95,8 @@ the code computes first a normalized version of the concentration, i.e. divided To summarize, we can split the concentration in two different formulations: -* Normalized concentration :meth:`caimira.models.ConcentrationModel._normed_concentration`: :math:`\mathrm{C_\mathrm{LR, normed}}(t, D)` that computes the concentration without including the emission rate. -* Concentration :meth:`caimira.models.ConcentrationModel.concentration` : :math:`C_{\mathrm{LR}}(t, D) = \mathrm{C_\mathrm{LR, normed}}(t, D) \cdot \mathrm{vR}(D)`, where :math:`\mathrm{vR}(D)` is the result of the :meth:`caimira.models._PopulationWithVirus.emission_rate_when_present` method. +* Normalized concentration :meth:`caimira.models._ConcentrationModelBase._normed_concentration`: :math:`\mathrm{C_\mathrm{LR, normed}}(t, D)` that computes the concentration without including the emission rate. +* Concentration :meth:`caimira.models._ConcentrationModelBase.concentration` : :math:`C_{\mathrm{LR}}(t, D) = \mathrm{C_\mathrm{LR, normed}}(t, D) \cdot \mathrm{vR}(D)`, where :math:`\mathrm{vR}(D)` is the result of the :meth:`caimira.models._PopulationWithVirus.emission_rate_when_present` method. Note that in order to get the total concentration value in this stage, the final result should be averaged over the particle diameters (i.e. Monte-Carlo integration over diameters, see above). For the calculator app report, the total concentration (MC integral over the diameter) is performed only when generating the plot. @@ -102,8 +104,8 @@ Otherwise, the diameter-dependence continues until we compute the inhaled dose i The following methods calculate the integrated concentration between two times. They are mostly used when calculating the **dose**: -* :meth:`caimira.models.ConcentrationModel.normed_integrated_concentration`, :math:`\mathrm{C_\mathrm{normed}}(D)` that returns the integrated long-range concentration of viruses in the air, between any two times, normalized by the emission rate. Note that this method performs the integral between any two times of the previously mentioned :meth:`caimira.models.ConcentrationModel._normed_concentration` method. -* :meth:`caimira.models.ConcentrationModel.integrated_concentration`, :math:`C(D)`, that returns the same result as the previous one, but multiplied by the emission rate. +* :meth:`caimira.models._ConcentrationModelBase.normed_integrated_concentration`, :math:`\mathrm{C_\mathrm{normed}}(D)` that returns the integrated long-range concentration of viruses in the air, between any two times, normalized by the emission rate. Note that this method performs the integral between any two times of the previously mentioned :meth:`caimira.models._ConcentrationModelBase._normed_concentration` method. +* :meth:`caimira.models._ConcentrationModelBase.integrated_concentration`, :math:`C(D)`, that returns the same result as the previous one, but multiplied by the emission rate. The integral over the exposure times is calculated directly in the class (integrated methods). @@ -115,7 +117,7 @@ The short-range concentration is the result of a two-stage exhaled jet model dev :math:`C_{\mathrm{SR}}(t, D) = C_{\mathrm{LR}} (t, D) + \frac{1}{S({x})} \cdot (C_{0, \mathrm{SR}}(D) - C_{\mathrm{LR}, 100μm}(t, D))` , where :math:`S(x)` is the dilution factor due to jet dynamics, as a function of the interpersonal distance :math:`x` and :math:`C_{0, \mathrm{SR}}(D)` corresponds to the initial concentration of virions at the mouth/nose outlet during exhalation. -:math:`C_{\mathrm{LR}, 100μm}(t, D)` is the long-range concentration, calculated in :meth:`caimira.models.ConcentrationModel.concentration` method but **interpolated** to the diameter range used for close-proximity (from 0 to 100μm). +:math:`C_{\mathrm{LR}, 100μm}(t, D)` is the long-range concentration, calculated in :meth:`caimira.models._ConcentrationModelBase.concentration` method but **interpolated** to the diameter range used for close-proximity (from 0 to 100μm). Note that :math:`C_{0, \mathrm{SR}}(D)` is constant over time, hence only dependent on the particle diameter distribution. For code simplification, we split the :math:`C_{\mathrm{SR}}(t, D)` equation into two components: @@ -182,9 +184,9 @@ To summarize, in the code, :math:`C_{\mathrm{SR}}(t, D)` is computed as follows: * calculate the `dilution_factor` - :math:`S({x})` - in the method :meth:`caimira.models.ShortRangeModel.dilution_factor`, with the distance :math:`x` as a random variable (log normal distribution in :meth:`caimira.monte_carlo.data.short_range_distances`) * compute :math:`\frac{1}{S({x})} \cdot (C_{0, \mathrm{SR}}(D) - C_{\mathrm{LR}, 100\mathrm{μm}}(t, D))` in method :meth:`caimira.models.ShortRangeModel.normed_concentration`, * multiply by the diameter-independent parameter, viral load, in method :meth:`caimira.models.ShortRangeModel.short_range_concentration` -* complete the equation of :math:`C_{\mathrm{SR}}(t, D)` by adding the long-range concentration from the :meth:`caimira.models.ConcentrationModel.concentration` (all integrated over :math:`D`), returning the final short-range concentration value for a given time and expiration activity. This is done at the level of the Exposure Model (:meth:`caimira.models.ExposureModel.concentration`). +* complete the equation of :math:`C_{\mathrm{SR}}(t, D)` by adding the long-range concentration from the :meth:`caimira.models._ConcentrationModelBase.concentration` (all integrated over :math:`D`), returning the final short-range concentration value for a given time and expiration activity. This is done at the level of the Exposure Model (:meth:`caimira.models.ExposureModel.concentration`). -Note that :meth:`caimira.models.ShortRangeModel._normed_concentration` method is different from :meth:`caimira.models.ConcentrationModel._normed_concentration` and :meth:`caimira.models.ConcentrationModel.concentration` differs from :meth:`caimira.models.ExposureModel.concentration`. +Note that :meth:`caimira.models.ShortRangeModel._normed_concentration` method is different from :meth:`caimira.models._ConcentrationModelBase._normed_concentration` and :meth:`caimira.models._ConcentrationModelBase.concentration` differs from :meth:`caimira.models.ExposureModel.concentration`. Unless one is computing the mean concentration values (e.g. for the plots in the report), the diameter-dependence is kept at this stage. Since other parameters downstream in the code are also diameter-dependent, the Monte-Carlo integration over the particle sizes is computed at the level of the dose :math:`\mathrm{vD^{total}}`. In case one would like to have intermediate results for the initial short-range concentration, this is done at the :class:`caimira.models.ExposureModel` class level. @@ -217,7 +219,7 @@ Long-range approach ******************* Regarding the concentration part of the long-range exposure (concentration integrated over time, :math:`\int_{t1}^{t2}C_{\mathrm{LR}}(t, D)\;\mathrm{d}t`), the respective method is :meth:`caimira.models.ExposureModel._long_range_normed_exposure_between_bounds`, -which uses the long-range exposure (concentration) between two bounds (time1 and time2), normalized by the emission rate of the infected population, calculated from :meth:`caimira.models.ConcentrationModel.normed_integrated_concentration`. +which uses the long-range exposure (concentration) between two bounds (time1 and time2), normalized by the emission rate of the infected population, calculated from :meth:`caimira.models._ConcentrationModelBase.normed_integrated_concentration`. The former method filters out the given bounds considering the breaks through the day (i.e. the time intervals during which there is no exposition to the virus) and retrieves the integrated long-range concentration of viruses in the air between any two times. After the calculations of the integrated concentration over the time, in order to calculate the final dose, we have to compute the remaining factors in the above equation. @@ -304,6 +306,15 @@ If short-range interactions exist: the long-range component is added to the alre If the are no short-range interactions: the short-range component (`deposited_exposure`) is zero, hence the result is equal solely to the long-range component :math:`C_{\mathrm{LR}}`. +:math:`\mathrm{CO}_{2}` Concentration +===================================== + +The estimate of the concentration of :math:`\mathrm{CO}_{2}` in a given room to indicate the air quality is given by the same equation of the virus concentration for the long-range approach, :math:`C_{\mathrm{LR}}(t, D)`, where :math:`C_0(D)` is the background :math:`\mathrm{CO}_{2}` concentration in the atmosphere (initially defined as `440.44 ppm`). +Note that in order to calculate the :math:`\mathrm{CO}_{2}` concentration one should use the concentration method defined in the superclass - :meth:`caimira.models._ConcentrationModelBase.concentration` - for a dedicated :class:`caimira.models.CO2ConcentrationModel` scenario. +A fraction of 4.2% of the exhalation rate of the defined activity was considered as the :math:`\mathrm{CO}_{2}` supplied to the room. + +Since the :math:`\mathrm{CO}_{2}` concentration differs from the virus concentration, the specific removal rate, atmospheric concentration and normalization factors are respectively defined in :meth:`caimira.models.CO2ConcentrationModel.removal_rate`, :meth:`caimira.models.CO2ConcentrationModel.atmosphere_concentration` and :meth:`caimira.models.CO2ConcentrationModel.normalization_factor`. + .. _caimira-uml-diagram: CAiMIRA UML Diagram diff --git a/caimira/models.py b/caimira/models.py index 43cab84f..b6875d53 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1107,7 +1107,7 @@ class _ConcentrationModelBase: normalized by normalization_factor. """ if stop <= self._first_presence_time(): - return 0.0 + return (stop - start)*self.atmosphere_concentration()/self.normalization_factor() state_change_times = self.state_change_times() req_start, req_stop = start, stop total_normed_concentration = 0. diff --git a/caimira/tests/models/test_co2_concentration_model.py b/caimira/tests/models/test_co2_concentration_model.py new file mode 100644 index 00000000..ad4a348c --- /dev/null +++ b/caimira/tests/models/test_co2_concentration_model.py @@ -0,0 +1,44 @@ +import numpy.testing as npt +import pytest + +from caimira import models + + +@pytest.fixture +def simple_co2_conc_model(): + return models.CO2ConcentrationModel( + room=models.Room(200, models.PiecewiseConstant((0., 24.), (293,))), + ventilation=models.AirChange(models.PeriodicInterval(period=120, duration=120), 0.25), + CO2_emitters=models.Population( + number=5, + presence=models.SpecificInterval((([0., 4.], ))), + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + host_immunity=0., + ), + ) + + +@pytest.mark.parametrize( + "time, expected_co2_concentration", [ + [0., 440.44], + [1., 914.2487227], + [2., 1283.251327], + [3., 1570.630844], + [4., 1794.442237], + ] +) +def test_co2_concentration( + simple_co2_conc_model: models.CO2ConcentrationModel, + time: float, + expected_co2_concentration: float, +): + npt.assert_almost_equal(simple_co2_conc_model.concentration(time), expected_co2_concentration) + + +def test_integrated_concentration(simple_co2_conc_model): + c1 = simple_co2_conc_model.integrated_concentration(0, 2) + c2 = simple_co2_conc_model.integrated_concentration(0, 1) + c3 = simple_co2_conc_model.integrated_concentration(1, 2) + assert c1 != 0 + npt.assert_almost_equal(c1, c2 + c3) diff --git a/caimira/tests/models/test_concentration_model.py b/caimira/tests/models/test_concentration_model.py index 0f6acb37..4141e6c9 100644 --- a/caimira/tests/models/test_concentration_model.py +++ b/caimira/tests/models/test_concentration_model.py @@ -3,9 +3,34 @@ import re import numpy as np import numpy.testing as npt import pytest +from dataclasses import dataclass from caimira import models +@dataclass(frozen=True) +class KnownConcentrationModelBase(models._ConcentrationModelBase): + """ + A ConcentrationModel where the atmosphere_concentration method is + redefined with a value taken from a new parameter. Useful for testing. + + """ + known_population: models.Population + + known_atmosphere_concentration: float = 0.0 + + @property + def population(self) -> models.Population: + return self.known_population + + def removal_rate(self, time: float): + return 10. + + def atmosphere_concentration(self): + return self.known_atmosphere_concentration + + def normalization_factor(self): + return 1e2 + @pytest.mark.parametrize( "override_params", [ @@ -59,9 +84,9 @@ def test_concentration_model_vectorisation(override_params): def simple_conc_model(): interesting_times = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), ) return models.ConcentrationModel( - models.Room(75, models.PiecewiseConstant((0., 24.), (293,))), - models.AirChange(interesting_times, 100), - models.InfectedPopulation( + room = models.Room(75, models.PiecewiseConstant((0., 24.), (293,))), + ventilation = models.AirChange(interesting_times, 100), + infected = models.InfectedPopulation( number=1, presence=interesting_times, mask=models.Mask.types['Type I'], @@ -137,3 +162,33 @@ def test_integrated_concentration(simple_conc_model): c3 = simple_conc_model.integrated_concentration(1, 2) assert c1 != 0 npt.assert_almost_equal(c1, c2 + c3, decimal=15) + + +@pytest.mark.parametrize( + "known_atmosphere_concentration, expected_normed_integrated_concentration", [ + [0.0, 0.0017333437605308818], + [240.0, 4.801733343835203], + [440.0, 8.801733343835203], + [600., 12.001733343835202], + [1000., 20.00173334429238], + ] +) +def test_normed_integrated_concentration( + simple_conc_model: models.ConcentrationModel, + known_atmosphere_concentration: float, + expected_normed_integrated_concentration: float): + + dummy_population = models.Population( + number=10, + presence=simple_conc_model.infected.presence, + mask=models.Mask.types['Type I'], + activity=models.Activity.types['Seated'], + host_immunity=0., + ) + + known_conc_model = KnownConcentrationModelBase( + simple_conc_model.room, + simple_conc_model.ventilation, + dummy_population, + known_atmosphere_concentration) + npt.assert_almost_equal(known_conc_model.normed_integrated_concentration(0, 2), expected_normed_integrated_concentration)