From 7cb762ec1f871202b85ab40e16775955239e9111 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 7 Jun 2024 18:19:38 +0200 Subject: [PATCH 01/17] fixed error with f_inf (short-range) --- caimira/models.py | 9 +++++---- caimira/tests/models/test_short_range_model.py | 6 +++--- caimira/tests/test_full_algorithm.py | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index e671194b..89f55915 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1428,7 +1428,8 @@ class ShortRangeModel: Virus short-range exposure concentration, as a function of time. """ return (self._normed_concentration(concentration_model, time) * - concentration_model.virus.viral_load_in_sputum) + concentration_model.virus.viral_load_in_sputum * + concentration_model.virus.viable_to_RNA_ratio) @method_cache def _normed_short_range_concentration_cached(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: @@ -1693,7 +1694,6 @@ class ExposureModel: self.exposed.activity.inhalation_rate * (1 - self.exposed.mask.inhale_efficiency())) - # In the end we multiply the final results by the fraction of infectious virus of the vD equation. return deposited_exposure def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _VectorisedFloat: @@ -1744,8 +1744,9 @@ class ExposureModel: # Then we multiply by diameter-independent quantities: viral load # and fraction of infected virions deposited_exposure *= ( - self.concentration_model.virus.viral_load_in_sputum - * (1 - self.exposed.mask.inhale_efficiency())) + self.concentration_model.virus.viral_load_in_sputum * + self.concentration_model.virus.viable_to_RNA_ratio * + (1 - self.exposed.mask.inhale_efficiency())) # Long-range concentration deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) diff --git a/caimira/tests/models/test_short_range_model.py b/caimira/tests/models/test_short_range_model.py index 5e20d03b..f86da130 100644 --- a/caimira/tests/models/test_short_range_model.py +++ b/caimira/tests/models/test_short_range_model.py @@ -106,9 +106,9 @@ def test_extract_between_bounds(short_range_model, time1, time2, @pytest.mark.parametrize( "time, expected_short_range_concentration", [ [8.5, 0.], - [10.5, 11.266605], - [10.6, 11.266605], - [11.0, 11.266605], + [10.5, 5.6333025], + [10.6, 5.6333025], + [11.0, 5.6333025], [12.0, 0.], ] ) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index 3f6c7fa5..70a3e3a8 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -263,6 +263,7 @@ class SimpleShortRangeModel: we perform the integral of Np(d)*V(d) over diameter analytically """ vl = conc_model.viral_load + viable_to_RNA = conc_model.viable_to_RNA dmin = self.diameter_min dmax = self.diameter_max result = 0. @@ -273,7 +274,7 @@ class SimpleShortRangeModel: ymax = (np.log(dmax)-mu)/(sqrt2*sigma)-3.*sigma/sqrt2 result += ( (cn * famp * d0**3)/2. * np.exp(9*sigma**2/2.) * (erf(ymax) - erf(ymin)) ) - return vl * 1e-6 * result * np.pi/6. + return vl * viable_to_RNA * 1e-6 * result * np.pi/6. def concentration(self, conc_model: SimpleConcentrationModel, time: float) -> _VectorisedFloat: """ @@ -430,7 +431,7 @@ class SimpleExposureModel(SimpleConcentrationModel): res = (quad(integrand, sr_model.diameter_min,sr_model.diameter_max, epsabs=0.,limit=500)[0] - * self.viral_load * 1e-6 * (t2-t1) ) + * self.viral_load * self.viable_to_RNA * 1e-6 * (t2-t1) ) result += sr_model.breathing_rate * ( res-self.integrated_longrange_concentration(t1,t2,evaporation) )/sr_model.dilution_factor() From c78d8bf7e7f4d559e8d5a4c6c0a95d12565f707e Mon Sep 17 00:00:00 2001 From: lrdossan Date: Sat, 8 Jun 2024 12:18:25 +0200 Subject: [PATCH 02/17] refined methods for short_range emission rate --- caimira/models.py | 93 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 21 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 89f55915..fb30aa48 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -687,13 +687,15 @@ class _ExpirationBase: def aerosols(self, mask: Mask): """ - Total volume of aerosols expired per volume of exhaled air (mL/cm^3). + Total volume of aerosols expired per volume of exhaled air + considering the outward mask efficiency (mL/cm^3). """ raise NotImplementedError("Subclass must implement") - def jet_origin_concentration(self): + def aerosols_without_mask(self): """ - Concentration of viruses at the jet origin (mL/m3). + Total volume of aerosols expired per volume of exhaled air + without considering the mask (mL.m^-3). """ raise NotImplementedError("Subclass must implement") @@ -723,8 +725,9 @@ class Expiration(_ExpirationBase): @cached() def aerosols(self, mask: Mask): """ - Total volume of aerosols expired per volume of exhaled air. - Result is in mL.cm^-3 + Total volume of aerosols expired per volume + of exhaled air considering the outward mask + efficiency. Result is in mL.cm^-3. """ def volume(d): return (np.pi * d**3) / 6. @@ -734,7 +737,11 @@ class Expiration(_ExpirationBase): (1 - mask.exhale_efficiency(self.diameter))) * 1e-12 @cached() - def jet_origin_concentration(self): + def aerosols_without_mask(self): + """ + Total volume of aerosols expired per volume of exhaled air + without considering the mask. Result is in mL.m^-3. + """ def volume(d): return (np.pi * d**3) / 6. @@ -886,6 +893,13 @@ class _PopulationWithVirus(Population): Total volume of aerosols expired per volume of exhaled air (mL/cm^3). """ raise NotImplementedError("Subclass must implement") + + def aerosols_without_mask(self): + """ + Total volume of aerosols expired per volume of exhaled air + without considering the mask (mL.m^-3). + """ + raise NotImplementedError("Subclass must implement") def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ @@ -895,6 +909,24 @@ class _PopulationWithVirus(Population): It should not be a function of time. """ raise NotImplementedError("Subclass must implement") + + @method_cache + def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: + """ + This method includes only the diameter-independent variables within the emission rate + of the short-range model. + It should not be a function of time. + """ + raise NotImplementedError("Subclass must implement") + + @method_cache + def short_range_emission_rate_per_person_when_present(self) -> _VectorisedFloat: + """ + The emission rate if the infected population is present, per person + (in virions / h). + """ + return (self.short_range_emission_rate_per_aerosol() * + self.aerosols_without_mask()) @method_cache def emission_rate_per_person_when_present(self) -> _VectorisedFloat: @@ -905,7 +937,7 @@ class _PopulationWithVirus(Population): return (self.emission_rate_per_aerosol_per_person_when_present() * self.aerosols()) - def emission_rate(self, time) -> _VectorisedFloat: + def emission_rate(self, time: float) -> _VectorisedFloat: """ The emission rate of the population vs time. """ @@ -951,7 +983,16 @@ class EmittingPopulation(_PopulationWithVirus): It should not be a function of time. """ return self.known_individual_emission_rate - + + @method_cache + def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: + """ + This method includes only the diameter-independent variables within the emission rate + of the short-range model. + It should not be a function of time. + """ + return self.known_individual_emission_rate + @dataclass(frozen=True) class InfectedPopulation(_PopulationWithVirus): @@ -986,6 +1027,16 @@ class InfectedPopulation(_PopulationWithVirus): self.fraction_of_infectious_virus() * 10 ** 6) return ER + + @method_cache + def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: + """ + This method includes only the diameter-independent variables within the emission rate + of the short-range model. + It should not be a function of time. + """ + return (self.virus.viral_load_in_sputum * + self.fraction_of_infectious_virus()) @property def particle(self) -> Particle: @@ -1400,14 +1451,15 @@ class ShortRangeModel: Virus short-range exposure concentration, as a function of time. If the given time falls within a short-range interval it returns the - short-range concentration normalized by the virus viral load. Otherwise - it returns 0. + short-range concentration normalized by the virus viral load and f_inf. + Otherwise it returns 0. """ start, stop = self.presence.boundaries()[0] # Verifies if the given time falls within a short-range interaction if start <= time <= stop: dilution = self.dilution_factor() - jet_origin_concentration = self.expiration.jet_origin_concentration() + # Jet origin concentration normalized by the viral load and f_inf + normed_jet_origin_concentration = self.expiration.aerosols_without_mask() # Long-range concentration normalized by the virus viral load long_range_normed_concentration = self._long_range_normed_concentration(concentration_model, time) @@ -1420,16 +1472,16 @@ class ShortRangeModel: # Short-range concentration formula. The long-range concentration is added in the concentration method (ExposureModel). # based on continuum model proposed by Jia et al (2022) - https://doi.org/10.1016/j.buildenv.2022.109166 - return ((1/dilution)*(jet_origin_concentration - long_range_normed_concentration_interpolated)) + return ((1/dilution)*(normed_jet_origin_concentration - long_range_normed_concentration_interpolated)) return 0. def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. + Factor of normalization from the emission rate applied here. """ return (self._normed_concentration(concentration_model, time) * - concentration_model.virus.viral_load_in_sputum * - concentration_model.virus.viable_to_RNA_ratio) + concentration_model.infected.short_range_emission_rate_per_aerosol()) @method_cache def _normed_short_range_concentration_cached(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: @@ -1472,7 +1524,7 @@ class ShortRangeModel: without dilution. """ start, stop = self.extract_between_bounds(time1, time2) - jet_origin = self.expiration.jet_origin_concentration() + jet_origin = self.expiration.aerosols_without_mask() return jet_origin * (stop - start) def _normed_interpolated_longrange_exposure_between_bounds( @@ -1707,6 +1759,7 @@ class ExposureModel: initial deposited exposure. """ deposited_exposure: _VectorisedFloat = 0. + short_range_emission_rate_per_aerosol = self.concentration_model.infected.short_range_emission_rate_per_aerosol() for interaction in self.short_range: start, stop = interaction.extract_between_bounds(time1, time2) short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds( @@ -1741,12 +1794,10 @@ class ExposureModel: interaction.activity.inhalation_rate /dilution) - # Then we multiply by diameter-independent quantities: viral load - # and fraction of infected virions - deposited_exposure *= ( - self.concentration_model.virus.viral_load_in_sputum * - self.concentration_model.virus.viable_to_RNA_ratio * - (1 - self.exposed.mask.inhale_efficiency())) + # Then we multiply by the normalization factor: short_range_emission_rate_per_aerosol + # and parameters of the vD equation (i.e. n_in). + deposited_exposure *= (short_range_emission_rate_per_aerosol * + (1 - self.exposed.mask.inhale_efficiency())) # Long-range concentration deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) From 0d2eaa99f8262367368300a29346e69d1959fbc6 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 10 Jun 2024 12:09:23 +0200 Subject: [PATCH 03/17] added f_inf to the normalization factor of short-range --- caimira/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index fb30aa48..374ba6a8 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1441,10 +1441,11 @@ class ShortRangeModel: def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus long-range exposure concentration normalized by the - virus viral load, as function of time. + virus viral load and fraction of infectious virus, as function of time. """ - return (concentration_model.concentration(time) / - concentration_model.virus.viral_load_in_sputum) + return (concentration_model.concentration(time) / ( + concentration_model.virus.viral_load_in_sputum * + concentration_model.infected.fraction_of_infectious_virus())) def _normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ From f59d56ed162d0a4eceb4dabb6b3b74ddd055d473 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 14 Jun 2024 11:41:39 +0200 Subject: [PATCH 04/17] adjusted the normalization factor for short-range --- caimira/models.py | 68 +++++++++++++++-------------------------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 374ba6a8..10777814 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -909,24 +909,6 @@ class _PopulationWithVirus(Population): It should not be a function of time. """ raise NotImplementedError("Subclass must implement") - - @method_cache - def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: - """ - This method includes only the diameter-independent variables within the emission rate - of the short-range model. - It should not be a function of time. - """ - raise NotImplementedError("Subclass must implement") - - @method_cache - def short_range_emission_rate_per_person_when_present(self) -> _VectorisedFloat: - """ - The emission rate if the infected population is present, per person - (in virions / h). - """ - return (self.short_range_emission_rate_per_aerosol() * - self.aerosols_without_mask()) @method_cache def emission_rate_per_person_when_present(self) -> _VectorisedFloat: @@ -984,15 +966,6 @@ class EmittingPopulation(_PopulationWithVirus): """ return self.known_individual_emission_rate - @method_cache - def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: - """ - This method includes only the diameter-independent variables within the emission rate - of the short-range model. - It should not be a function of time. - """ - return self.known_individual_emission_rate - @dataclass(frozen=True) class InfectedPopulation(_PopulationWithVirus): @@ -1027,16 +1000,6 @@ class InfectedPopulation(_PopulationWithVirus): self.fraction_of_infectious_virus() * 10 ** 6) return ER - - @method_cache - def short_range_emission_rate_per_aerosol(self) -> _VectorisedFloat: - """ - This method includes only the diameter-independent variables within the emission rate - of the short-range model. - It should not be a function of time. - """ - return (self.virus.viral_load_in_sputum * - self.fraction_of_infectious_virus()) @property def particle(self) -> Particle: @@ -1437,6 +1400,9 @@ class ShortRangeModel: xstar[distances >= xstar])/𝛽r1/(xstar[distances >= xstar] + x0))**3 return factors + + def _normed_jet_origin_concentration(self) -> _VectorisedFloat: + return self.expiration.aerosols_without_mask() def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ @@ -1460,8 +1426,8 @@ class ShortRangeModel: if start <= time <= stop: dilution = self.dilution_factor() # Jet origin concentration normalized by the viral load and f_inf - normed_jet_origin_concentration = self.expiration.aerosols_without_mask() - # Long-range concentration normalized by the virus viral load + normed_jet_origin_concentration = self._normed_jet_origin_concentration() + # Long-range concentration normalized by the virus viral load and f_inf long_range_normed_concentration = self._long_range_normed_concentration(concentration_model, time) # The long-range concentration values are then approximated using interpolation: @@ -1475,14 +1441,23 @@ class ShortRangeModel: # based on continuum model proposed by Jia et al (2022) - https://doi.org/10.1016/j.buildenv.2022.109166 return ((1/dilution)*(normed_jet_origin_concentration - long_range_normed_concentration_interpolated)) return 0. - + + def normalization_factor(self, infected: InfectedPopulation) -> _VectorisedFloat: + # The normalization factor does not consider the BR contribution, and therefore the conversion factor. + return infected.emission_rate_per_aerosol_per_person_when_present() / ( + infected.activity.exhalation_rate * 10 ** 6 + ) + + def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: + return self._normed_jet_origin_concentration() * self.normalization_factor(infected) + def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. Factor of normalization from the emission rate applied here. """ - return (self._normed_concentration(concentration_model, time) * - concentration_model.infected.short_range_emission_rate_per_aerosol()) + return (self._normed_concentration(concentration_model, time) * + self.normalization_factor(concentration_model.infected)) @method_cache def _normed_short_range_concentration_cached(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: @@ -1760,7 +1735,6 @@ class ExposureModel: initial deposited exposure. """ deposited_exposure: _VectorisedFloat = 0. - short_range_emission_rate_per_aerosol = self.concentration_model.infected.short_range_emission_rate_per_aerosol() for interaction in self.short_range: start, stop = interaction.extract_between_bounds(time1, time2) short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds( @@ -1795,10 +1769,12 @@ class ExposureModel: interaction.activity.inhalation_rate /dilution) - # Then we multiply by the normalization factor: short_range_emission_rate_per_aerosol + # Then we multiply by the emission rate without the BR contribution (and conversion factor), # and parameters of the vD equation (i.e. n_in). - deposited_exposure *= (short_range_emission_rate_per_aerosol * - (1 - self.exposed.mask.inhale_efficiency())) + deposited_exposure *= ( + (self.concentration_model.infected.emission_rate_per_aerosol_per_person_when_present() / + (self.concentration_model.infected.activity.exhalation_rate * 10 ** 6)) * + (1 - self.exposed.mask.inhale_efficiency())) # Long-range concentration deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) From fa38282186e2211af47a4308dd27e40733cd07c2 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 17 Jun 2024 12:12:00 +0200 Subject: [PATCH 05/17] removed aerosols_without_mask in order to always use aerosols method with the correction conversion factor --- caimira/models.py | 63 +++++-------------- .../tests/models/test_short_range_model.py | 2 +- 2 files changed, 18 insertions(+), 47 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 10777814..856613a1 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -692,13 +692,6 @@ class _ExpirationBase: """ raise NotImplementedError("Subclass must implement") - def aerosols_without_mask(self): - """ - Total volume of aerosols expired per volume of exhaled air - without considering the mask (mL.m^-3). - """ - raise NotImplementedError("Subclass must implement") - @dataclass(frozen=True) class Expiration(_ExpirationBase): @@ -736,18 +729,6 @@ class Expiration(_ExpirationBase): return self.cn * (volume(self.diameter) * (1 - mask.exhale_efficiency(self.diameter))) * 1e-12 - @cached() - def aerosols_without_mask(self): - """ - Total volume of aerosols expired per volume of exhaled air - without considering the mask. Result is in mL.m^-3. - """ - def volume(d): - return (np.pi * d**3) / 6. - - # Final result converted from microns^3/cm3 to mL/m3 - return self.cn * volume(self.diameter) * 1e-6 - @dataclass(frozen=True) class MultipleExpiration(_ExpirationBase): @@ -893,13 +874,6 @@ class _PopulationWithVirus(Population): Total volume of aerosols expired per volume of exhaled air (mL/cm^3). """ raise NotImplementedError("Subclass must implement") - - def aerosols_without_mask(self): - """ - Total volume of aerosols expired per volume of exhaled air - without considering the mask (mL.m^-3). - """ - raise NotImplementedError("Subclass must implement") def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ @@ -1402,16 +1376,15 @@ class ShortRangeModel: return factors def _normed_jet_origin_concentration(self) -> _VectorisedFloat: - return self.expiration.aerosols_without_mask() + # The short range origin concentration does not consider the mask contribution. + return self.expiration.aerosols(mask=Mask.types['No mask']) def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus long-range exposure concentration normalized by the virus viral load and fraction of infectious virus, as function of time. """ - return (concentration_model.concentration(time) / ( - concentration_model.virus.viral_load_in_sputum * - concentration_model.infected.fraction_of_infectious_virus())) + return (concentration_model.concentration(time) / self.normalization_factor(concentration_model.infected)) def _normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ @@ -1425,9 +1398,9 @@ class ShortRangeModel: # Verifies if the given time falls within a short-range interaction if start <= time <= stop: dilution = self.dilution_factor() - # Jet origin concentration normalized by the viral load and f_inf + # Jet origin concentration normalized by the emission rate (except the BR) normed_jet_origin_concentration = self._normed_jet_origin_concentration() - # Long-range concentration normalized by the virus viral load and f_inf + # Long-range concentration normalized by the emission rate (except the BR) long_range_normed_concentration = self._long_range_normed_concentration(concentration_model, time) # The long-range concentration values are then approximated using interpolation: @@ -1443,10 +1416,8 @@ class ShortRangeModel: return 0. def normalization_factor(self, infected: InfectedPopulation) -> _VectorisedFloat: - # The normalization factor does not consider the BR contribution, and therefore the conversion factor. - return infected.emission_rate_per_aerosol_per_person_when_present() / ( - infected.activity.exhalation_rate * 10 ** 6 - ) + # The normalization factor does not consider the BR contribution + return infected.emission_rate_per_aerosol_per_person_when_present() / infected.activity.exhalation_rate def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: return self._normed_jet_origin_concentration() * self.normalization_factor(infected) @@ -1491,16 +1462,16 @@ class ShortRangeModel: return start, stop def _normed_jet_exposure_between_bounds(self, - concentration_model: ConcentrationModel, time1: float, time2: float): """ Get the part of the integrated short-range concentration of viruses in the air, between the times start and stop, coming - from the jet concentration, normalized by the viral load, and - without dilution. + from the jet concentration, normalized by the viral load and + f_inf, and without dilution. """ start, stop = self.extract_between_bounds(time1, time2) - jet_origin = self.expiration.aerosols_without_mask() + # Note the conversion factor mL/cm^3 -> mL/m3 + jet_origin = self.expiration.aerosols(mask=Mask.types['No mask']) * 10**6 return jet_origin * (stop - start) def _normed_interpolated_longrange_exposure_between_bounds( @@ -1508,8 +1479,8 @@ class ShortRangeModel: time1: float, time2: float): """ Get the part of the integrated short-range concentration due - to the background concentration, normalized by the viral load - and the breathing rate, and without dilution. + to the background concentration, normalized by the viral load, + f_inf, and the breathing rate, and without dilution. One needs to interpolate the integrated long-range concentration for the particle diameters defined here. TODO: make sure any potential extrapolation has a @@ -1522,6 +1493,7 @@ class ShortRangeModel: normed_int_concentration = ( concentration_model.integrated_concentration(start, stop) /concentration_model.virus.viral_load_in_sputum + /concentration_model.infected.fraction_of_infectious_virus() /concentration_model.infected.activity.exhalation_rate ) normed_int_concentration_interpolated = np.interp( @@ -1737,8 +1709,7 @@ class ExposureModel: deposited_exposure: _VectorisedFloat = 0. for interaction in self.short_range: start, stop = interaction.extract_between_bounds(time1, time2) - short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds( - self.concentration_model, start, stop) + short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds(start, stop) short_range_lr_exposure = interaction._normed_interpolated_longrange_exposure_between_bounds( self.concentration_model, start, stop) dilution = interaction.dilution_factor() @@ -1772,8 +1743,8 @@ class ExposureModel: # Then we multiply by the emission rate without the BR contribution (and conversion factor), # and parameters of the vD equation (i.e. n_in). deposited_exposure *= ( - (self.concentration_model.infected.emission_rate_per_aerosol_per_person_when_present() / - (self.concentration_model.infected.activity.exhalation_rate * 10 ** 6)) * + (self.concentration_model.infected.emission_rate_per_aerosol_per_person_when_present() / ( + self.concentration_model.infected.activity.exhalation_rate * 10**6)) * (1 - self.exposed.mask.inhale_efficiency())) # Long-range concentration deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) diff --git a/caimira/tests/models/test_short_range_model.py b/caimira/tests/models/test_short_range_model.py index f86da130..7bf5f429 100644 --- a/caimira/tests/models/test_short_range_model.py +++ b/caimira/tests/models/test_short_range_model.py @@ -49,7 +49,7 @@ def test_short_range_model_ndarray(concentration_model, short_range_model): model = short_range_model.build_model(SAMPLE_SIZE) assert isinstance(model._normed_concentration(concentration_model, 10.75), np.ndarray) assert isinstance(model.short_range_concentration(concentration_model, 10.75), np.ndarray) - assert isinstance(model._normed_jet_exposure_between_bounds(concentration_model, 10.75, 10.85), np.ndarray) + assert isinstance(model._normed_jet_exposure_between_bounds(10.75, 10.85), np.ndarray) assert isinstance(model._normed_interpolated_longrange_exposure_between_bounds(concentration_model, 10.75, 10.85), np.ndarray) assert isinstance(model.short_range_concentration(concentration_model, 14.0), float) From 7924af041f76df21c052d6539d9de80da87aae0f Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 20 Jun 2024 15:51:03 +0200 Subject: [PATCH 06/17] adapted comments and docstrings of some methods --- caimira/models.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 856613a1..3032bc62 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -964,15 +964,17 @@ class InfectedPopulation(_PopulationWithVirus): """ The emission rate of virions in the expired air per mL of respiratory fluid, if the infected population is present, in (virion.cm^3)/(mL.h). - This method includes only the diameter-independent variables within the emission rate. + This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ - # Note on units: exhalation rate is in m^3/h -> 1e6 conversion factor - # Returns the emission rate times the number of infected hosts in the room + # Conversion factor explanation: + # The exhalation rate is in m^3/h, therefore the 1e6 conversion factor + # is to convert m^3/h into cm^3/h to return (virions.cm^3)/(mL.h), + # so that we can then multiply by aerosols (mL/cm^3). ER = (self.virus.viral_load_in_sputum * self.activity.exhalation_rate * self.fraction_of_infectious_virus() * - 10 ** 6) + 10**6) return ER @property @@ -1376,6 +1378,10 @@ class ShortRangeModel: return factors def _normed_jet_origin_concentration(self) -> _VectorisedFloat: + ''' + The initial emission concentration at the source origin (mouth/nose) + normalized by the viral load and f_inf factors. Results in mL.cm^3. + ''' # The short range origin concentration does not consider the mask contribution. return self.expiration.aerosols(mask=Mask.types['No mask']) @@ -1416,10 +1422,18 @@ class ShortRangeModel: return 0. def normalization_factor(self, infected: InfectedPopulation) -> _VectorisedFloat: - # The normalization factor does not consider the BR contribution + """ + The normalization factor applied to the short-range results. It refers + to the emission rate per aerosol without accounting for the exhalation rate. + Result in virions/mL. + """ + # Re-use the emission rate method divided by the BR contribution. return infected.emission_rate_per_aerosol_per_person_when_present() / infected.activity.exhalation_rate def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: + """ + The initial emission concentration at the source origin (mouth/nose). + """ return self._normed_jet_origin_concentration() * self.normalization_factor(infected) def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: @@ -1490,6 +1504,9 @@ class ShortRangeModel: if stop<=start: return 0. + # Note that the emission_rate_per_aerosol_per_person_when_present method + # is not used here due to the influence that the conversion factor (10**6) + # could have in the interpolation. normed_int_concentration = ( concentration_model.integrated_concentration(start, stop) /concentration_model.virus.viral_load_in_sputum From ddaa77b1c911920331b2ad8363596ccaff9f72ed Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 20 Jun 2024 15:51:17 +0200 Subject: [PATCH 07/17] added the fraction of infectious virus method in the test full algorithm file --- caimira/tests/test_full_algorithm.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index 70a3e3a8..77aa3646 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -55,7 +55,7 @@ class SimpleConcentrationModel: #: Number of infected people num_infected: int = 1 - #: Fraction of infected viruses (viable to RNA ratio) + #: Viable to RNA ratio viable_to_RNA: _VectorisedFloat = 0.5 #: Host immunity factor (0. for not immune) @@ -97,6 +97,12 @@ class SimpleConcentrationModel: return (self.lambda_ventilation + ln2/(np.where(hl_calc <= 0, 6.43, np.minimum(6.43, hl_calc)))) + def fraction_of_infectious_virus(self) -> _VectorisedFloat: + """ + The fraction of infectious virus. + """ + return self.viable_to_RNA * (1 - self.HI) + @method_cache def deposition_removal_coefficient(self) -> float: """ @@ -181,8 +187,8 @@ class SimpleConcentrationModel: return ( ( (0 if not self.infected_presence.triggered(t) else self.f(lambda_rate,0)) + result * np.exp(-lambda_rate*(t-ti)) ) - * self.num_infected * self.viable_to_RNA - * (1. - self.HI) / self.room_volume) + * self.num_infected * self.fraction_of_infectious_virus() + / self.room_volume) @dataclass(frozen=True) @@ -411,8 +417,8 @@ class SimpleExposureModel(SimpleConcentrationModel): else self.f_with_fdep(lambda_rate,0,evaporation)*(t2-t1)) + (primitive(t2) * np.exp(-lambda_rate*(t2-ti)) - primitive(t1) * np.exp(-lambda_rate*(t1-ti)) ) ) - * self.num_infected * self.viable_to_RNA - * (1. - self.HI) / self.room_volume) + * self.num_infected * self.fraction_of_infectious_virus() + / self.room_volume) @method_cache def integrated_shortrange_concentration(self) -> _VectorisedFloat: @@ -431,7 +437,7 @@ class SimpleExposureModel(SimpleConcentrationModel): res = (quad(integrand, sr_model.diameter_min,sr_model.diameter_max, epsabs=0.,limit=500)[0] - * self.viral_load * self.viable_to_RNA * 1e-6 * (t2-t1) ) + * self.viral_load * self.fraction_of_infectious_virus() * 1e-6 * (t2-t1) ) result += sr_model.breathing_rate * ( res-self.integrated_longrange_concentration(t1,t2,evaporation) )/sr_model.dilution_factor() From 9e3782268cb45938075ff5751fe6a9dbe61e1b2c Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 20 Jun 2024 17:39:52 +0200 Subject: [PATCH 08/17] minor update in docstring --- caimira/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 3032bc62..bd2efa92 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1379,8 +1379,9 @@ class ShortRangeModel: def _normed_jet_origin_concentration(self) -> _VectorisedFloat: ''' - The initial emission concentration at the source origin (mouth/nose) - normalized by the viral load and f_inf factors. Results in mL.cm^3. + The initial jet concentration at the source origin (mouth/nose) + normalized by the diameter-independent variables (viral load and f_inf). + Results in mL.cm^3. ''' # The short range origin concentration does not consider the mask contribution. return self.expiration.aerosols(mask=Mask.types['No mask']) @@ -1432,7 +1433,8 @@ class ShortRangeModel: def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: """ - The initial emission concentration at the source origin (mouth/nose). + The initial jet concentration at the source origin (mouth/nose). + Returns the full result with the diameter dependent and independent varibles, in virions/m^3. """ return self._normed_jet_origin_concentration() * self.normalization_factor(infected) From e07fd2b8726125796f39f0eca981cc7ae0fa23df Mon Sep 17 00:00:00 2001 From: lrdossan Date: Tue, 2 Jul 2024 16:49:46 +0100 Subject: [PATCH 09/17] applied changes to docstrings --- caimira/models.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index bd2efa92..24806bfc 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1378,18 +1378,18 @@ class ShortRangeModel: return factors def _normed_jet_origin_concentration(self) -> _VectorisedFloat: - ''' - The initial jet concentration at the source origin (mouth/nose) - normalized by the diameter-independent variables (viral load and f_inf). - Results in mL.cm^3. - ''' + """ + The initial jet concentration at the source origin (mouth/nose), normalized by + normalization_factor (corresponding to the diameter-independent variables). + Results in mL.cm^-3. + """ # The short range origin concentration does not consider the mask contribution. return self.expiration.aerosols(mask=Mask.types['No mask']) def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ - Virus long-range exposure concentration normalized by the - virus viral load and fraction of infectious virus, as function of time. + Virus long-range exposure concentration normalized by normalization_factor, + as function of time. Results in mL.cm^-3. """ return (concentration_model.concentration(time) / self.normalization_factor(concentration_model.infected)) @@ -1398,8 +1398,8 @@ class ShortRangeModel: Virus short-range exposure concentration, as a function of time. If the given time falls within a short-range interval it returns the - short-range concentration normalized by the virus viral load and f_inf. - Otherwise it returns 0. + short-range concentration normalized by normalization_factor. + Otherwise it returns 0. Results in mL.cm^-3. """ start, stop = self.presence.boundaries()[0] # Verifies if the given time falls within a short-range interaction @@ -1424,9 +1424,9 @@ class ShortRangeModel: def normalization_factor(self, infected: InfectedPopulation) -> _VectorisedFloat: """ - The normalization factor applied to the short-range results. It refers - to the emission rate per aerosol without accounting for the exhalation rate. - Result in virions/mL. + The normalization factor applied to the short-range results. It refers to the emission + rate per aerosol without accounting for the exhalation rate (viral load and f_inf). + Result in (virions.cm^3)/(mL.m^3). """ # Re-use the emission rate method divided by the BR contribution. return infected.emission_rate_per_aerosol_per_person_when_present() / infected.activity.exhalation_rate @@ -1434,14 +1434,14 @@ class ShortRangeModel: def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: """ The initial jet concentration at the source origin (mouth/nose). - Returns the full result with the diameter dependent and independent varibles, in virions/m^3. + Returns the full result with the diameter dependent and independent variables, in virions/m^3. """ return self._normed_jet_origin_concentration() * self.normalization_factor(infected) def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. - Factor of normalization from the emission rate applied here. + Factor of normalization applied back here. Results in virions/m^3. """ return (self._normed_concentration(concentration_model, time) * self.normalization_factor(concentration_model.infected)) @@ -1482,12 +1482,12 @@ class ShortRangeModel: """ Get the part of the integrated short-range concentration of viruses in the air, between the times start and stop, coming - from the jet concentration, normalized by the viral load and - f_inf, and without dilution. + from the jet concentration, normalized by normalization_factor, + and without dilution. """ start, stop = self.extract_between_bounds(time1, time2) - # Note the conversion factor mL/cm^3 -> mL/m3 - jet_origin = self.expiration.aerosols(mask=Mask.types['No mask']) * 10**6 + # Note the conversion factor mL.cm^-3 -> mL.m^-3 + jet_origin = self._normed_jet_origin_concentration() * 10**6 return jet_origin * (stop - start) def _normed_interpolated_longrange_exposure_between_bounds( @@ -1495,8 +1495,8 @@ class ShortRangeModel: time1: float, time2: float): """ Get the part of the integrated short-range concentration due - to the background concentration, normalized by the viral load, - f_inf, and the breathing rate, and without dilution. + to the background concentration, normalized by normalization_factor + together with breathing rate, and without dilution. One needs to interpolate the integrated long-range concentration for the particle diameters defined here. TODO: make sure any potential extrapolation has a From d3034ffe07c47865173212aac5dc7752dbd4cbcf Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 4 Jul 2024 10:49:47 +0200 Subject: [PATCH 10/17] added reference to the normalization factor in the short-range class --- caimira/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 24806bfc..37f4af55 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1380,16 +1380,16 @@ class ShortRangeModel: def _normed_jet_origin_concentration(self) -> _VectorisedFloat: """ The initial jet concentration at the source origin (mouth/nose), normalized by - normalization_factor (corresponding to the diameter-independent variables). - Results in mL.cm^-3. + normalization_factor in the ShortRange class (corresponding to the diameter-independent + variables). Results in mL.cm^-3. """ # The short range origin concentration does not consider the mask contribution. return self.expiration.aerosols(mask=Mask.types['No mask']) def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ - Virus long-range exposure concentration normalized by normalization_factor, - as function of time. Results in mL.cm^-3. + Virus long-range exposure concentration normalized by normalization_factor in the + ShortRange class, as function of time. Results in mL.cm^-3. """ return (concentration_model.concentration(time) / self.normalization_factor(concentration_model.infected)) @@ -1398,8 +1398,8 @@ class ShortRangeModel: Virus short-range exposure concentration, as a function of time. If the given time falls within a short-range interval it returns the - short-range concentration normalized by normalization_factor. - Otherwise it returns 0. Results in mL.cm^-3. + short-range concentration normalized by normalization_factor in the + Short-range class. Otherwise it returns 0. Results in mL.cm^-3. """ start, stop = self.presence.boundaries()[0] # Verifies if the given time falls within a short-range interaction From e0fa8fd0d2e3b9b5e02c7dbb0cbc9231048e1c74 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 5 Jul 2024 12:17:35 +0200 Subject: [PATCH 11/17] updated version --- caimira/apps/calculator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index abe9bda7..98f75762 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -42,7 +42,7 @@ from .user import AuthenticatedUser, AnonymousUser # calculator version. If the calculator needs to make breaking changes (e.g. change # form attributes) then it can also increase its MAJOR version without needing to # increase the overall CAiMIRA version (found at ``caimira.__version__``). -__version__ = "4.16.0" +__version__ = "4.16.1" LOG = logging.getLogger("Calculator") From 56550c44060c957e72550b5a53afc4d7be90c92e Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 11 Jul 2024 16:23:53 +0200 Subject: [PATCH 12/17] updated docstrings for virions vs. IRP --- caimira/models.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index 37f4af55..ebcd0b50 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -877,9 +877,9 @@ class _PopulationWithVirus(Population): def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, - per person, if the infected population is present, in (virion.cm^3)/(mL.h). - This method includes only the diameter-independent variables within the emission rate. + The emission rate of infectious respiratory particles (IRP) in the expired air per + mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). + This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ raise NotImplementedError("Subclass must implement") @@ -888,7 +888,7 @@ class _PopulationWithVirus(Population): def emission_rate_per_person_when_present(self) -> _VectorisedFloat: """ The emission rate if the infected population is present, per person - (in virions / h). + (in IRP/h). """ return (self.emission_rate_per_aerosol_per_person_when_present() * self.aerosols()) @@ -920,7 +920,7 @@ class _PopulationWithVirus(Population): @dataclass(frozen=True) class EmittingPopulation(_PopulationWithVirus): - #: The emission rate of a single individual, in virions / h. + #: The emission rate of a single individual, in IRP / h. known_individual_emission_rate: float def aerosols(self): @@ -933,9 +933,9 @@ class EmittingPopulation(_PopulationWithVirus): @method_cache def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, - per person, if the infected population is present, in (virion.cm^3)/(mL.h). - This method includes only the diameter-independent variables within the emission rate. + The emission rate of infectious respiratory particles (IRP) in the expired air per + mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). + This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ return self.known_individual_emission_rate @@ -962,14 +962,14 @@ class InfectedPopulation(_PopulationWithVirus): @method_cache def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, - if the infected population is present, in (virion.cm^3)/(mL.h). + The emission rate of infectious respiratory particles (IRP) in the expired air per + mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ # Conversion factor explanation: # The exhalation rate is in m^3/h, therefore the 1e6 conversion factor - # is to convert m^3/h into cm^3/h to return (virions.cm^3)/(mL.h), + # is to convert m^3/h into cm^3/h to return (IRP.cm^3)/(mL.h), # so that we can then multiply by aerosols (mL/cm^3). ER = (self.virus.viral_load_in_sputum * self.activity.exhalation_rate * @@ -1426,7 +1426,7 @@ class ShortRangeModel: """ The normalization factor applied to the short-range results. It refers to the emission rate per aerosol without accounting for the exhalation rate (viral load and f_inf). - Result in (virions.cm^3)/(mL.m^3). + Result in (IRP.cm^3)/(mL.m^3). """ # Re-use the emission rate method divided by the BR contribution. return infected.emission_rate_per_aerosol_per_person_when_present() / infected.activity.exhalation_rate @@ -1434,14 +1434,14 @@ class ShortRangeModel: def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: """ The initial jet concentration at the source origin (mouth/nose). - Returns the full result with the diameter dependent and independent variables, in virions/m^3. + Returns the full result with the diameter dependent and independent variables, in IRP/m^3. """ return self._normed_jet_origin_concentration() * self.normalization_factor(infected) def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. - Factor of normalization applied back here. Results in virions/m^3. + Factor of normalization applied back here. Results in IRP/m^3. """ return (self._normed_concentration(concentration_model, time) * self.normalization_factor(concentration_model.infected)) @@ -1499,16 +1499,16 @@ class ShortRangeModel: together with breathing rate, and without dilution. One needs to interpolate the integrated long-range concentration for the particle diameters defined here. - TODO: make sure any potential extrapolation has a - negligible effect. """ start, stop = self.extract_between_bounds(time1, time2) if stop<=start: return 0. - # Note that the emission_rate_per_aerosol_per_person_when_present method - # is not used here due to the influence that the conversion factor (10**6) - # could have in the interpolation. + # Note that for the correct interpolation one needs to isolate those parameters + # that are diameter-dependent from those that are diameter independent. + # Therefore, the diameter-independent parameters (viral load, f_ind and BR) + # are removed for the interpolation, and added back once the integration over + # the new aerosol diameters (done with the mean) is completed. normed_int_concentration = ( concentration_model.integrated_concentration(start, stop) /concentration_model.virus.viral_load_in_sputum @@ -1586,7 +1586,8 @@ class CO2DataModel: @dataclass(frozen=True) class ExposureModel: """ - Represents the exposure to a concentration of virions in the air. + Represents the exposure to a concentration of + infectious respiratory particles (IRP) in the air. """ data_registry: DataRegistry @@ -1726,6 +1727,7 @@ class ExposureModel: initial deposited exposure. """ deposited_exposure: _VectorisedFloat = 0. + emission_rate_per_aerosol_per_person = self.concentration_model.normalization_factor() for interaction in self.short_range: start, stop = interaction.extract_between_bounds(time1, time2) short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds(start, stop) From d4455aaa27818b2c511fdc57214326d2c662d66f Mon Sep 17 00:00:00 2001 From: lrdossan Date: Thu, 11 Jul 2024 17:34:35 +0200 Subject: [PATCH 13/17] added type hint for axes (matplotlib update) --- caimira/apps/calculator/report_generator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index d0288b03..587c3b64 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -279,11 +279,14 @@ def uncertainties_plot(infection_probability: models._VectorisedFloat, lower_percentiles: models._VectorisedFloat, upper_percentiles: models._VectorisedFloat): - fig, axs = plt.subplots(2, 3, + fig, axes = plt.subplots(2, 3, gridspec_kw={'width_ratios': [5, 0.5] + [1], 'height_ratios': [3, 1], 'wspace': 0}, sharey='row', sharex='col') + + # Type hint for axs + axs: np.ndarray = np.array(axes) for y, x in [(0, 1)] + [(1, i + 1) for i in range(2)]: axs[y, x].axis('off') From cab9a85703807783a9b85f25b234ffcb892f6f90 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 12 Jul 2024 12:10:29 +0200 Subject: [PATCH 14/17] changed docstring (interpolation: integration) --- caimira/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caimira/models.py b/caimira/models.py index ebcd0b50..b600aa39 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -1504,7 +1504,7 @@ class ShortRangeModel: if stop<=start: return 0. - # Note that for the correct interpolation one needs to isolate those parameters + # Note that for the correct integration one needs to isolate those parameters # that are diameter-dependent from those that are diameter independent. # Therefore, the diameter-independent parameters (viral load, f_ind and BR) # are removed for the interpolation, and added back once the integration over From 42570887b519380c4c24fcc17dac9146f4eab48b Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 12 Jul 2024 14:06:22 +0200 Subject: [PATCH 15/17] added test for scale with f_inf --- caimira/tests/test_full_algorithm.py | 118 +++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index 77aa3646..0ce0fa0d 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -15,6 +15,8 @@ from caimira.models import _VectorisedFloat,Interval,SpecificInterval from caimira.monte_carlo.data import (expiration_distributions, expiration_BLO_factors,short_range_expiration_distributions, short_range_distances,virus_distributions,activity_distributions) +import caimira.dataclass_utils as dc_utils + SAMPLE_SIZE = 1_000_000 TOLERANCE = 0.04 @@ -849,3 +851,119 @@ def test_exposure_with_shortrange_and_distributions(expo_sr_model_distr, rtol=0.03 ) + +def exposure_model_from_parameter(data_registry, short_range_models, f_inf=0.5, viral_load=1e9, BR=1.25): + virus: models.SARSCoV2 = models.SARSCoV2( + viral_load_in_sputum=viral_load, + infectious_dose=50, + viable_to_RNA_ratio=f_inf, + transmissibility_factor=0.51, + ) + c_model = mc.ConcentrationModel( + data_registry=data_registry, + room=models.Room(volume=50, humidity=0.3), + ventilation=models.AirChange(active=models.PeriodicInterval(period=120, duration=120), + air_exch=10_000_000), + infected=mc.InfectedPopulation( + data_registry=data_registry, + number=1, + presence=models.SpecificInterval(present_times=((8.5, 12.5), (13.5, 17.5))), + virus=virus, + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + expiration=expiration_distributions(data_registry)['Breathing'], + host_immunity=0., + ), + evaporation_factor=0.3, + ) + return mc.ExposureModel( + data_registry=data_registry, + concentration_model=c_model, + short_range=short_range_models, + exposed=mc.Population( + number=1, + presence=models.SpecificInterval(present_times=((8.5, 12.5), (13.5, 17.5))), + mask=models.Mask.types['No mask'], + activity=models.Activity(inhalation_rate=BR, exhalation_rate=1.25), + host_immunity=0., + ), + geographical_data=models.Cases(), + ).build_model(SAMPLE_SIZE) + + +@retry(tries=10) +def test_exposure_scale_with_f_inf(data_registry, sr_models): + """ + Exposure scaling test for the fraction of infectious virus. + """ + e_model_1: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, f_inf=0.5) + e_model_2: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, f_inf=1) + np.testing.assert_allclose( + 2*e_model_1.deposited_exposure().mean(), + e_model_2.deposited_exposure().mean(), rtol=0.02 + ) + + +@retry(tries=10) +def test_exposure_scale_with_viral_load(data_registry, sr_models): + """ + Exposure scaling test for the viral load. + """ + e_model_1: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, viral_load=1e9) + e_model_2: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, viral_load=2e9) + np.testing.assert_allclose( + 2*e_model_1.deposited_exposure().mean(), + e_model_2.deposited_exposure().mean(), rtol=0.02 + ) + + +@retry(tries=10) +def test_lr_exposure_scale_with_breathing_rate(data_registry): + """ + Exposure scaling test for the breathing rate when there are only long-range + interactions defined. Only the inhalation rate of the infected takes place + at the deposited exposure level. + """ + e_model_1: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=(), BR=1.25) + e_model_2: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=(), BR=2.5) + np.testing.assert_allclose( + 2*e_model_1.deposited_exposure().mean(), + e_model_2.deposited_exposure().mean(), rtol=0.02 + ) + + +@retry(tries=10) +def test_exposure_scale_with_breathing_rate(data_registry, sr_models): + """ + Exposure scaling test for the breathing rate when long- and short-range + interactions are defined. We need to apply the multiplication factor + to the inhalation rate of the infected (long-range), but also for + each short-range interaction + """ + e_model_1: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, BR=1.25) + + seated_act = models.Activity.types['Seated'] + heavy_exercise_act = models.Activity.types['Heavy exercise'] + sr_models_activity = ( + mc.ShortRangeModel( + data_registry = data_registry, + expiration = short_range_expiration_distributions(data_registry)['Speaking'], + activity = models.Activity(inhalation_rate=seated_act.inhalation_rate * 2, + exhalation_rate=seated_act.exhalation_rate), + presence = interaction_intervals[0], + distance = 0.854, + ), + mc.ShortRangeModel( + data_registry = data_registry, + expiration = short_range_expiration_distributions(data_registry)['Breathing'], + activity = models.Activity(inhalation_rate=heavy_exercise_act.inhalation_rate * 2, + exhalation_rate=heavy_exercise_act.inhalation_rate), + presence = interaction_intervals[1], + distance = 0.854, + ), + ) + e_model_2: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models_activity, BR=2.5) + np.testing.assert_allclose( + 2*e_model_1.deposited_exposure().mean(), + e_model_2.deposited_exposure().mean(), rtol=0.02 + ) From 810b3015116ee208dd4228105ef193e6920bfff3 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Fri, 12 Jul 2024 16:52:10 +0200 Subject: [PATCH 16/17] final adjustment for docstrings --- caimira/models.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index b600aa39..88f9ffb1 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -878,7 +878,7 @@ class _PopulationWithVirus(Population): def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ The emission rate of infectious respiratory particles (IRP) in the expired air per - mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). + mL of respiratory fluid, if the infected population is present, in (virions.cm^3)/(mL.h). This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ @@ -888,7 +888,7 @@ class _PopulationWithVirus(Population): def emission_rate_per_person_when_present(self) -> _VectorisedFloat: """ The emission rate if the infected population is present, per person - (in IRP/h). + (in virions/h). """ return (self.emission_rate_per_aerosol_per_person_when_present() * self.aerosols()) @@ -920,7 +920,7 @@ class _PopulationWithVirus(Population): @dataclass(frozen=True) class EmittingPopulation(_PopulationWithVirus): - #: The emission rate of a single individual, in IRP / h. + #: The emission rate of a single individual, in virions / h. known_individual_emission_rate: float def aerosols(self): @@ -934,7 +934,7 @@ class EmittingPopulation(_PopulationWithVirus): def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ The emission rate of infectious respiratory particles (IRP) in the expired air per - mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). + mL of respiratory fluid, if the infected population is present, in (virions.cm^3)/(mL.h). This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ @@ -963,13 +963,13 @@ class InfectedPopulation(_PopulationWithVirus): def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ The emission rate of infectious respiratory particles (IRP) in the expired air per - mL of respiratory fluid, if the infected population is present, in (IRP.cm^3)/(mL.h). + mL of respiratory fluid, if the infected population is present, in (virions.cm^3)/(mL.h). This method returns only the diameter-independent variables within the emission rate. It should not be a function of time. """ # Conversion factor explanation: # The exhalation rate is in m^3/h, therefore the 1e6 conversion factor - # is to convert m^3/h into cm^3/h to return (IRP.cm^3)/(mL.h), + # is to convert m^3/h into cm^3/h to return (virions.cm^3)/(mL.h), # so that we can then multiply by aerosols (mL/cm^3). ER = (self.virus.viral_load_in_sputum * self.activity.exhalation_rate * @@ -1426,7 +1426,7 @@ class ShortRangeModel: """ The normalization factor applied to the short-range results. It refers to the emission rate per aerosol without accounting for the exhalation rate (viral load and f_inf). - Result in (IRP.cm^3)/(mL.m^3). + Result in (virions.cm^3)/(mL.m^3). """ # Re-use the emission rate method divided by the BR contribution. return infected.emission_rate_per_aerosol_per_person_when_present() / infected.activity.exhalation_rate @@ -1434,14 +1434,14 @@ class ShortRangeModel: def jet_origin_concentration(self, infected: InfectedPopulation) -> _VectorisedFloat: """ The initial jet concentration at the source origin (mouth/nose). - Returns the full result with the diameter dependent and independent variables, in IRP/m^3. + Returns the full result with the diameter dependent and independent variables, in virions/m^3. """ return self._normed_jet_origin_concentration() * self.normalization_factor(infected) def short_range_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. - Factor of normalization applied back here. Results in IRP/m^3. + Factor of normalization applied back here. Results in virions/m^3. """ return (self._normed_concentration(concentration_model, time) * self.normalization_factor(concentration_model.infected)) @@ -1727,7 +1727,6 @@ class ExposureModel: initial deposited exposure. """ deposited_exposure: _VectorisedFloat = 0. - emission_rate_per_aerosol_per_person = self.concentration_model.normalization_factor() for interaction in self.short_range: start, stop = interaction.extract_between_bounds(time1, time2) short_range_jet_exposure = interaction._normed_jet_exposure_between_bounds(start, stop) From bbb82d2927536f364bf62077967f997ac26488b6 Mon Sep 17 00:00:00 2001 From: lrdossan Date: Mon, 15 Jul 2024 09:03:42 +0200 Subject: [PATCH 17/17] removed unused import and fixed docstring --- caimira/tests/test_full_algorithm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index 0ce0fa0d..112b1082 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -15,7 +15,6 @@ from caimira.models import _VectorisedFloat,Interval,SpecificInterval from caimira.monte_carlo.data import (expiration_distributions, expiration_BLO_factors,short_range_expiration_distributions, short_range_distances,virus_distributions,activity_distributions) -import caimira.dataclass_utils as dc_utils SAMPLE_SIZE = 1_000_000 @@ -938,7 +937,7 @@ def test_exposure_scale_with_breathing_rate(data_registry, sr_models): Exposure scaling test for the breathing rate when long- and short-range interactions are defined. We need to apply the multiplication factor to the inhalation rate of the infected (long-range), but also for - each short-range interaction + each short-range interaction. """ e_model_1: models.ExposureModel = exposure_model_from_parameter(data_registry=data_registry, short_range_models=sr_models, BR=1.25)