Merge branch 'feature/virology_parameters' into 'master'
ER and P(I) formulas update according to qID parameter Closes #184 See merge request cara/cara!236
This commit is contained in:
commit
e1d8b3a2c8
15 changed files with 153 additions and 113 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
|
||||
|
||||
CARA models the concentration profile of potential infectious viruses in enclosed spaces with clear and intuitive graphs.
|
||||
CARA models the concentration profile of potential virions in enclosed spaces with clear and intuitive graphs.
|
||||
The user can set a number of parameters, including room volume, exposure time, activity type, mask-wearing and ventilation.
|
||||
The report generated indicates how to avoid exceeding critical concentrations and chains of airborne transmission in spaces such as individual offices, meeting rooms and labs.
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ def plot(times, concentrations, model: models.ExposureModel):
|
|||
|
||||
ax.set_xlabel('Time of day')
|
||||
ax.set_ylabel('Mean concentration ($q/m^3$)')
|
||||
ax.set_title('Mean concentration of infectious quanta')
|
||||
ax.set_title('Mean concentration of virions')
|
||||
ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%H:%M"))
|
||||
|
||||
# Plot presence of exposed person
|
||||
|
|
@ -237,7 +237,7 @@ def comparison_plot(scenarios: typing.Dict[str, dict], sample_times: np.ndarray)
|
|||
|
||||
ax.set_xlabel('Time of day')
|
||||
ax.set_ylabel('Mean concentration ($q/m^3$)')
|
||||
ax.set_title('Mean concentration of infectious quanta')
|
||||
ax.set_title('Mean concentration of virions')
|
||||
|
||||
return fig
|
||||
|
||||
|
|
|
|||
|
|
@ -257,7 +257,7 @@
|
|||
|
||||
<p class="data_text"> <strong> Notes for alternative scenarios: </strong><br>
|
||||
<ol>
|
||||
<li>This graph shows the concentration of infectious quanta in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
|
||||
<li>This graph shows the concentration of virions in the air. The filtration of Type I and FFP2 masks, if worn, applies not only to the emission rate but also to the individual exposure (i.e. inhalation).
|
||||
For this reason, scenarios with different types of mask will show the same concentration on the graph but have different absorbed doses and infection probabilities.</li>
|
||||
<li>If you have selected more sophisticated options, such as HEPA filtration or FFP2 masks, alternatives will be indicated in the plot as the "base scenario with/without...", representing a variation on the inputs inserted in the form.<br>
|
||||
The other alternative scenarios shown for comparison will not include either HEPA filtration or FFP2 masks.</li>
|
||||
|
|
@ -288,7 +288,7 @@
|
|||
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
|
||||
</p>
|
||||
<p>
|
||||
CARA models the concentration profile of potential infectious viruses in enclosed spaces with clear and intuitive graphs.
|
||||
CARA models the concentration profile of potential virions in enclosed spaces with clear and intuitive graphs.
|
||||
The user can set a number of parameters, including room volume, exposure time, activity type, mask-wearing and ventilation.
|
||||
The report generated indicates how to avoid exceeding critical concentrations and chains of airborne transmission in spaces such as individual offices, meeting rooms and labs.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ v{{ calculator_version }} <span style="float:right; font-weight:bold">Please sen
|
|||
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
|
||||
</p>
|
||||
<p>
|
||||
CARA models the concentration profile of potential infectious viruses in enclosed spaces with clear and intuitive graphs.
|
||||
CARA models the concentration profile of virions in enclosed spaces with clear and intuitive graphs.
|
||||
The user can set a number of parameters, including room volume, exposure time, activity type, mask-wearing and ventilation.
|
||||
The report generated indicates how to avoid exceeding critical concentrations and chains of airborne transmission in spaces such as individual offices, meeting rooms and labs.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ If you are using the expert version of the tool, you should look at the expert
|
|||
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
|
||||
</p>
|
||||
<p>
|
||||
CARA models the concentration profile of potential infectious viruses in enclosed spaces with clear and intuitive graphs.
|
||||
CARA models the concentration profile of potential virions in enclosed spaces with clear and intuitive graphs.
|
||||
The user can set a number of parameters, including room volume, exposure time, activity type, mask-wearing and ventilation.
|
||||
The report generated indicates how to avoid exceeding critical concentrations and chains of airborne transmission in spaces such as individual offices, meeting rooms and labs.
|
||||
</p>
|
||||
|
|
@ -183,15 +183,15 @@ It is estimated based on the emission rate of virus into the simulated volume, a
|
|||
This probability is valid for the simulation duration - i.e. the start and end time.
|
||||
If you are using the natural ventilation option, the simulation is only valid for the selected month, because the following or preceding month will have a different average temperature profile.
|
||||
The <code>expected number of new cases</code> for the simulation is calculated based on the probability of infection, multiplied by the number of exposed occupants.</p>
|
||||
<p>The graph shows the variation in the concentration of infectious viruses within the simulated volume.
|
||||
<p>The graph shows the variation in the concentration of virions within the simulated volume.
|
||||
It is determined by:</p>
|
||||
<ul>
|
||||
<li>The presence of the infected person, who emits airborne viruses in the volume.</li>
|
||||
<li>The emission rate is related to the type of activity of the infected person (sitting, light exercise), their level of vocalisation (breathing, talking or shouting).</li>
|
||||
<li>The accumulation of infectious quanta in the volume, which is driven, among other factors, by ventilation (if applicable).<ul>
|
||||
<li>The accumulation of virions in the volume, which is driven, among other factors, by ventilation (if applicable).<ul>
|
||||
<li>In a mechanical ventilation scenario, the removal rate is constant, based on fresh airflow supply in and out of the simulated space.</li>
|
||||
<li>Under natural ventilation conditions, the effectiveness of ventilation relies upon the hourly temperature difference between the inside and outside air temperature.</li>
|
||||
<li>A HEPA filter removes infectious virus from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).</li>
|
||||
<li>A HEPA filter removes virions from the air at a constant rate and is modelled in the same way as mechanical ventilation, however air passed through a HEPA filter is recycled (i.e. it is not fresh air).</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ class ExposureModelResult(View):
|
|||
|
||||
ax.set_xlabel('Time (hours)')
|
||||
ax.set_ylabel('Concentration ($q/m^3$)')
|
||||
ax.set_title('Concentration of infectious quanta')
|
||||
ax.set_title('Concentration of virions')
|
||||
else:
|
||||
self.ax.ignore_existing_data_limits = True
|
||||
self.line.set_data(ts, concentration)
|
||||
|
|
@ -154,7 +154,7 @@ class ExposureModelResult(View):
|
|||
def update_textual_result(self, model: models.ExposureModel):
|
||||
lines = []
|
||||
P = model.infection_probability()
|
||||
lines.append(f'Emission rate (quanta/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}')
|
||||
lines.append(f'Emission rate (virus/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}')
|
||||
lines.append(f'Probability of infection: {np.round(P, 0)}%')
|
||||
|
||||
lines.append(f'Number of exposed: {model.exposed.number}')
|
||||
|
|
@ -186,7 +186,7 @@ class ExposureComparissonResult(View):
|
|||
ax.spines['top'].set_visible(False)
|
||||
ax.set_xlabel('Time (hours)')
|
||||
ax.set_ylabel('Concentration ($q/m^3$)')
|
||||
ax.set_title('Concentration of infectious quanta')
|
||||
ax.set_title('Concentration of virions')
|
||||
return ax
|
||||
|
||||
def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _):
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ The model used is based on scientific publications relating to airborne transmis
|
|||
The tool helps assess the potential dose of infectious airborne viruses in indoor gatherings, with people seated, standing, moving around, while breathing, speaking or shouting/singing. The model is based on the Wells-Riley model of aerosol disease transmission, which assumes a fixed value for the average infectious dose. The dose-response models for respiratory diseases is more accurate, although since this parameter for SARS-CoV-2 is not known so far, the Wells-Riley method is recommended in the health science community (see <a href="#references_block">References</a>).
|
||||
The methodology of the model is divided into three parts:
|
||||
<ol>
|
||||
<li>Estimating the emission rate of infectious viruses.</li>
|
||||
<li>Estimating the emission rate of virions.</li>
|
||||
<li>Modeling the concentration evolution of viruses within a given volume and consequent inhalation dose during the exposure time.</li>
|
||||
<li>Estimating the probability of a COVID-19 infection, the expected number of new cases arising from the transmission event and the basic reproduction rate (R0).</li>
|
||||
</ol>
|
||||
|
|
|
|||
|
|
@ -420,8 +420,8 @@ class Virus:
|
|||
#: RNA copies / mL
|
||||
viral_load_in_sputum: _VectorisedFloat
|
||||
|
||||
#: RNA-copies per quantum
|
||||
quantum_infectious_dose: _VectorisedFloat
|
||||
#: Dose to initiate infection, in RNA copies
|
||||
infectious_dose: _VectorisedFloat
|
||||
|
||||
#: Pre-populated examples of Viruses.
|
||||
types: typing.ClassVar[typing.Dict[str, "Virus"]]
|
||||
|
|
@ -458,20 +458,20 @@ Virus.types = {
|
|||
# It is somewhere between 1000 or 10 SARS-CoV viruses,
|
||||
# as per https://www.dhs.gov/publication/st-master-question-list-covid-19
|
||||
# 50 comes from Buonanno et al.
|
||||
quantum_infectious_dose=50.,
|
||||
infectious_dose=50.,
|
||||
),
|
||||
'SARS_CoV_2_B117': SARSCoV2(
|
||||
# also called VOC-202012/01
|
||||
viral_load_in_sputum=1e9,
|
||||
quantum_infectious_dose=30.,
|
||||
infectious_dose=30.,
|
||||
),
|
||||
'SARS_CoV_2_P1': SARSCoV2(
|
||||
viral_load_in_sputum=1e9,
|
||||
quantum_infectious_dose=1/0.045,
|
||||
infectious_dose=1/0.045,
|
||||
),
|
||||
'SARS_CoV_2_B16172': SARSCoV2(
|
||||
viral_load_in_sputum=1e9,
|
||||
quantum_infectious_dose=30/1.6,
|
||||
infectious_dose=30/1.6,
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -677,7 +677,7 @@ class InfectedPopulation(Population):
|
|||
Note that the rate is not currently time-dependent.
|
||||
|
||||
"""
|
||||
# Emission Rate (infectious quantum / h)
|
||||
# Emission Rate (virions / h)
|
||||
# Note on units: exhalation rate is in m^3/h, aerosols in mL/cm^3
|
||||
# and viral load in virus/mL -> 1e6 conversion factor
|
||||
aerosols = self.expiration.aerosols(self.mask)
|
||||
|
|
@ -685,15 +685,14 @@ class InfectedPopulation(Population):
|
|||
ER = (self.virus.viral_load_in_sputum *
|
||||
self.activity.exhalation_rate *
|
||||
10 ** 6 *
|
||||
aerosols /
|
||||
self.virus.quantum_infectious_dose)
|
||||
aerosols)
|
||||
|
||||
# For superspreading event, where ejection_factor is infinite we fix the ER
|
||||
# based on Miller et al. (2020).
|
||||
if isinstance(aerosols, np.ndarray):
|
||||
ER[np.isinf(aerosols)] = 970
|
||||
ER[np.isinf(aerosols)] = 970 * self.virus.infectious_dose
|
||||
elif np.isinf(aerosols):
|
||||
ER = 970
|
||||
ER = 970 * self.virus.infectious_dose
|
||||
|
||||
return ER
|
||||
|
||||
|
|
@ -804,7 +803,7 @@ class ConcentrationModel:
|
|||
|
||||
def concentration(self, time: float) -> _VectorisedFloat:
|
||||
"""
|
||||
Virus quanta concentration, as a function of time.
|
||||
Virus exposure concentration, as a function of time.
|
||||
The formulas used here assume that all parameters (ventilation,
|
||||
emission rate) are constant between two state changes - only
|
||||
the value of these parameters at the next state change, are used.
|
||||
|
|
@ -867,8 +866,8 @@ class ExposureModel:
|
|||
#: The fraction of viruses actually deposited in the respiratory tract
|
||||
fraction_deposited: _VectorisedFloat = 0.6
|
||||
|
||||
def quanta_exposure(self) -> _VectorisedFloat:
|
||||
"""The number of virus quanta per meter^3."""
|
||||
def exposure(self) -> _VectorisedFloat:
|
||||
"""The number of virus per meter^3."""
|
||||
exposure = 0.0
|
||||
|
||||
for start, stop in self.exposed.presence.boundaries():
|
||||
|
|
@ -877,7 +876,7 @@ class ExposureModel:
|
|||
return exposure * self.repeats
|
||||
|
||||
def infection_probability(self) -> _VectorisedFloat:
|
||||
exposure = self.quanta_exposure()
|
||||
exposure = self.exposure()
|
||||
|
||||
inf_aero = (
|
||||
self.exposed.activity.inhalation_rate *
|
||||
|
|
@ -886,7 +885,7 @@ class ExposureModel:
|
|||
)
|
||||
|
||||
# Probability of infection.
|
||||
return (1 - np.exp(-inf_aero)) * 100
|
||||
return (1 - np.exp(-(inf_aero/self.concentration_model.virus.infectious_dose))) * 100
|
||||
|
||||
def expected_new_cases(self) -> _VectorisedFloat:
|
||||
prob = self.infection_probability()
|
||||
|
|
|
|||
|
|
@ -43,18 +43,18 @@ symptomatic_vl_frequencies = LogCustomKernel(
|
|||
virus_distributions = {
|
||||
'SARS_CoV_2': mc.SARSCoV2(
|
||||
viral_load_in_sputum=symptomatic_vl_frequencies,
|
||||
quantum_infectious_dose=100,
|
||||
infectious_dose=100,
|
||||
),
|
||||
'SARS_CoV_2_B117': mc.SARSCoV2(
|
||||
viral_load_in_sputum=symptomatic_vl_frequencies,
|
||||
quantum_infectious_dose=60,
|
||||
infectious_dose=60,
|
||||
),
|
||||
'SARS_CoV_2_P1': mc.SARSCoV2(
|
||||
viral_load_in_sputum=symptomatic_vl_frequencies,
|
||||
quantum_infectious_dose=100/2.25,
|
||||
infectious_dose=100/2.25,
|
||||
),
|
||||
'SARS_CoV_2_B16172': mc.SARSCoV2(
|
||||
viral_load_in_sputum=symptomatic_vl_frequencies,
|
||||
quantum_infectious_dose=60/1.6,
|
||||
infectious_dose=60/1.6,
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from cara import models
|
|||
{'humidity': np.array([0.5, 0.4])},
|
||||
{'air_change': np.array([100, 120])},
|
||||
{'viral_load_in_sputum': np.array([5e8, 1e9])},
|
||||
{'quantum_infectious_dose': np.array([50, 20])},
|
||||
]
|
||||
)
|
||||
def test_concentration_model_vectorisation(override_params):
|
||||
|
|
@ -21,8 +20,7 @@ def test_concentration_model_vectorisation(override_params):
|
|||
'volume': 75,
|
||||
'humidity': 0.5,
|
||||
'air_change': 100,
|
||||
'viral_load_in_sputum': 1e9,
|
||||
'quantum_infectious_dose': 50,
|
||||
'viral_load_in_sputum': 1e9
|
||||
}
|
||||
defaults.update(override_params)
|
||||
|
||||
|
|
@ -43,7 +41,7 @@ def test_concentration_model_vectorisation(override_params):
|
|||
),
|
||||
virus=models.SARSCoV2(
|
||||
viral_load_in_sputum=defaults['viral_load_in_sputum'],
|
||||
quantum_infectious_dose=defaults['quantum_infectious_dose'],
|
||||
infectious_dose=50.,
|
||||
),
|
||||
expiration=models.Expiration((1., 0., 0.)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,26 +3,27 @@ import typing
|
|||
import numpy as np
|
||||
import numpy.testing
|
||||
import pytest
|
||||
from dataclasses import dataclass
|
||||
|
||||
from cara import models
|
||||
from cara.models import ExposureModel
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KnownConcentrations(models.ConcentrationModel):
|
||||
"""
|
||||
A ConcentrationModel which is based on pre-known quanta concentrations and
|
||||
A ConcentrationModel which is based on pre-known exposure concentrations and
|
||||
which therefore doesn't need other components. Useful for testing.
|
||||
|
||||
"""
|
||||
def __init__(self, concentration_function: typing.Callable) -> None:
|
||||
self._func = concentration_function
|
||||
concentration_function: typing.Callable
|
||||
|
||||
def infectious_virus_removal_rate(self, time: float) -> models._VectorisedFloat:
|
||||
# very large decay constant -> same as constant concentration
|
||||
return 1.e50
|
||||
|
||||
def _concentration_limit(self, time: float) -> models._VectorisedFloat:
|
||||
return self._func(time)
|
||||
return self.concentration_function(time)
|
||||
|
||||
def state_change_times(self):
|
||||
return [0, 24]
|
||||
|
|
@ -31,7 +32,7 @@ class KnownConcentrations(models.ConcentrationModel):
|
|||
return 24
|
||||
|
||||
def concentration(self, time: float) -> models._VectorisedFloat: # noqa
|
||||
return self._func(time)
|
||||
return self.concentration_function(time)
|
||||
|
||||
|
||||
halftime = models.PeriodicInterval(120, 60)
|
||||
|
|
@ -49,33 +50,47 @@ populations = [
|
|||
# A population with some array component for inhalation_rate.
|
||||
models.Population(
|
||||
10, halftime, models.Mask.types['Type I'],
|
||||
models.Activity(np.array([0.51,0.57]), 0.57),
|
||||
models.Activity(np.array([0.51, 0.57]), 0.57),
|
||||
),
|
||||
]
|
||||
dummy_room = models.Room(50, 0.5)
|
||||
dummy_ventilation = models._VentilationBase()
|
||||
dummy_infected_population = models.InfectedPopulation(
|
||||
number=1,
|
||||
presence=halftime,
|
||||
mask=models.Mask.types['Type I'],
|
||||
activity=models.Activity.types['Standing'],
|
||||
virus=models.Virus.types['SARS_CoV_2_B117'],
|
||||
expiration=models.Expiration.types['Talking']
|
||||
)
|
||||
|
||||
|
||||
def known_concentrations(func):
|
||||
return KnownConcentrations(dummy_room, dummy_ventilation, dummy_infected_population, func)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"population, cm, f_dep, expected_exposure, expected_probability",[
|
||||
[populations[1], KnownConcentrations(lambda t: 1.2), 1.,
|
||||
np.array([14.4, 14.4]), np.array([99.6803184113, 99.5181053773])],
|
||||
"population, cm, f_dep, expected_exposure, expected_probability", [
|
||||
[populations[1], known_concentrations(lambda t: 36), 1.,
|
||||
np.array([432, 432]), np.array([99.6803184113, 99.5181053773])],
|
||||
|
||||
[populations[2], KnownConcentrations(lambda t: 1.2), 1.,
|
||||
np.array([14.4, 14.4]), np.array([97.4574432074, 98.3493482895])],
|
||||
[populations[2], known_concentrations(lambda t: 36), 1.,
|
||||
np.array([432, 432]), np.array([97.4574432074, 98.3493482895])],
|
||||
|
||||
[populations[0], KnownConcentrations(lambda t: np.array([1.2, 2.4])), 1.,
|
||||
np.array([14.4, 28.8]), np.array([98.3493482895, 99.9727534893])],
|
||||
[populations[0], known_concentrations(lambda t: np.array([36, 72])), 1.,
|
||||
np.array([432, 864]), np.array([98.3493482895, 99.9727534893])],
|
||||
|
||||
[populations[1], KnownConcentrations(lambda t: np.array([1.2, 2.4])), 1.,
|
||||
np.array([14.4, 28.8]), np.array([99.6803184113, 99.9976777757])],
|
||||
[populations[1], known_concentrations(lambda t: np.array([36, 72])), 1.,
|
||||
np.array([432, 864]), np.array([99.6803184113, 99.9976777757])],
|
||||
|
||||
[populations[0], KnownConcentrations(lambda t: 2.4), np.array([0.5, 1.]),
|
||||
28.8, np.array([98.3493482895, 99.9727534893])],
|
||||
[populations[0], known_concentrations(lambda t: 72), np.array([0.5, 1.]),
|
||||
864, np.array([98.3493482895, 99.9727534893])],
|
||||
])
|
||||
def test_exposure_model_ndarray(population, cm, f_dep,
|
||||
expected_exposure, expected_probability):
|
||||
model = ExposureModel(cm, population, fraction_deposited = f_dep)
|
||||
model = ExposureModel(cm, population, fraction_deposited=f_dep)
|
||||
np.testing.assert_almost_equal(
|
||||
model.quanta_exposure(), expected_exposure
|
||||
model.exposure(), expected_exposure
|
||||
)
|
||||
np.testing.assert_almost_equal(
|
||||
model.infection_probability(), expected_probability, decimal=10
|
||||
|
|
@ -89,12 +104,13 @@ def test_exposure_model_ndarray(population, cm, f_dep,
|
|||
|
||||
@pytest.mark.parametrize("population", populations)
|
||||
def test_exposure_model_ndarray_and_float_mix(population):
|
||||
cm = KnownConcentrations(lambda t: 0 if np.floor(t) % 2 else np.array([1.2, 1.2]))
|
||||
cm = known_concentrations(lambda t: 0 if np.floor(t) %
|
||||
2 else np.array([1.2, 1.2]))
|
||||
model = ExposureModel(cm, population)
|
||||
|
||||
expected_exposure = np.array([14.4, 14.4])
|
||||
np.testing.assert_almost_equal(
|
||||
model.quanta_exposure(), expected_exposure
|
||||
model.exposure(), expected_exposure
|
||||
)
|
||||
|
||||
assert isinstance(model.infection_probability(), np.ndarray)
|
||||
|
|
@ -103,22 +119,23 @@ def test_exposure_model_ndarray_and_float_mix(population):
|
|||
|
||||
@pytest.mark.parametrize("population", populations)
|
||||
def test_exposure_model_compare_scalar_vector(population):
|
||||
cm_scalar = KnownConcentrations(lambda t: 1.2)
|
||||
cm_array = KnownConcentrations(lambda t: np.array([1.2, 1.2]))
|
||||
cm_scalar = known_concentrations(lambda t: 1.2)
|
||||
cm_array = known_concentrations(lambda t: np.array([1.2, 1.2]))
|
||||
model_scalar = ExposureModel(cm_scalar, population)
|
||||
model_array = ExposureModel(cm_array, population)
|
||||
expected_exposure = 14.4
|
||||
np.testing.assert_almost_equal(
|
||||
model_scalar.quanta_exposure(), expected_exposure
|
||||
model_scalar.exposure(), expected_exposure
|
||||
)
|
||||
np.testing.assert_almost_equal(
|
||||
model_array.quanta_exposure(), np.array([expected_exposure]*2)
|
||||
model_array.exposure(), np.array([expected_exposure]*2)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conc_model():
|
||||
interesting_times = models.SpecificInterval(([0, 1], [1.01, 1.02], [12, 24]))
|
||||
interesting_times = models.SpecificInterval(
|
||||
([0, 1], [1.01, 1.02], [12, 24]))
|
||||
always = models.SpecificInterval(((0, 24),))
|
||||
return models.ConcentrationModel(
|
||||
models.Room(25),
|
||||
|
|
@ -133,23 +150,51 @@ def conc_model():
|
|||
)
|
||||
)
|
||||
|
||||
# expected quanta were computed with a trapezoidal integration, using
|
||||
# expected exposure were computed with a trapezoidal integration, using
|
||||
# a mesh of 10'000 pts per exposed presence interval.
|
||||
@pytest.mark.parametrize("exposed_time_interval, expected_quanta", [
|
||||
[(0, 1), 5.3334352],
|
||||
[(1, 1.01), 0.061759078],
|
||||
[(1.01, 1.02), 0.060016487],
|
||||
[(12, 12.01), 0.0019012647],
|
||||
[(12, 24), 75.513005],
|
||||
[(0, 24), 81.956988],
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exposed_time_interval, expected_exposure", [
|
||||
[(0, 1), 266.67176],
|
||||
[(1, 1.01), 3.0879539],
|
||||
[(1.01, 1.02), 3.00082435],
|
||||
[(12, 12.01), 0.095063235],
|
||||
[(12, 24), 3775.65025],
|
||||
[(0, 24), 4097.8494],
|
||||
]
|
||||
)
|
||||
def test_exposure_model_integral_accuracy(exposed_time_interval,
|
||||
expected_quanta, conc_model):
|
||||
expected_exposure, conc_model):
|
||||
presence_interval = models.SpecificInterval((exposed_time_interval,))
|
||||
population = models.Population(
|
||||
10, presence_interval, models.Mask.types['Type I'],
|
||||
models.Activity.types['Standing'],
|
||||
)
|
||||
model = ExposureModel(conc_model, population, fraction_deposited=1.)
|
||||
np.testing.assert_allclose(model.quanta_exposure(), expected_quanta)
|
||||
np.testing.assert_allclose(model.exposure(), expected_exposure)
|
||||
|
||||
|
||||
def test_infectious_dose_vectorisation():
|
||||
infected_population = models.InfectedPopulation(
|
||||
number=1,
|
||||
presence=halftime,
|
||||
mask=models.Mask.types['Type I'],
|
||||
activity=models.Activity.types['Standing'],
|
||||
virus=models.SARSCoV2(
|
||||
viral_load_in_sputum=1e9,
|
||||
infectious_dose=np.array([50, 20, 30]),
|
||||
),
|
||||
expiration=models.Expiration.types['Talking']
|
||||
)
|
||||
cm = KnownConcentrations(
|
||||
dummy_room, dummy_ventilation, infected_population, lambda t: 1.2)
|
||||
|
||||
presence_interval = models.SpecificInterval(((0, 1),))
|
||||
population = models.Population(
|
||||
10, presence_interval, models.Mask.types['Type I'],
|
||||
models.Activity.types['Standing'],
|
||||
)
|
||||
model = ExposureModel(cm, population, fraction_deposited=1.0)
|
||||
inf_probability = model.infection_probability()
|
||||
assert isinstance(inf_probability, np.ndarray)
|
||||
assert inf_probability.shape == (3, )
|
||||
|
|
|
|||
|
|
@ -7,14 +7,12 @@ import cara.models
|
|||
@pytest.mark.parametrize(
|
||||
"override_params", [
|
||||
{'viral_load_in_sputum': np.array([5e8, 1e9])},
|
||||
{'quantum_infectious_dose': np.array([50, 20])},
|
||||
{'exhalation_rate': np.array([0.75, 0.81])},
|
||||
]
|
||||
)
|
||||
def test_infected_population_vectorisation(override_params):
|
||||
defaults = {
|
||||
'viral_load_in_sputum': 1e9,
|
||||
'quantum_infectious_dose': 50,
|
||||
'exhalation_rate': 0.75,
|
||||
}
|
||||
defaults.update(override_params)
|
||||
|
|
@ -33,7 +31,7 @@ def test_infected_population_vectorisation(override_params):
|
|||
),
|
||||
virus=cara.models.Virus(
|
||||
viral_load_in_sputum=defaults['viral_load_in_sputum'],
|
||||
quantum_infectious_dose=defaults['quantum_infectious_dose'],
|
||||
infectious_dose=50.,
|
||||
),
|
||||
expiration=cara.models.Expiration((1., 0., 0.)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import cara.data as data
|
|||
|
||||
|
||||
def test_no_mask_superspeading_emission_rate(baseline_model):
|
||||
expected_rate = 970.
|
||||
expected_rate = 48500.
|
||||
npt.assert_allclose(
|
||||
[baseline_model.infected.emission_rate(t) for t in [0, 1, 4, 4.5, 5, 8, 9]],
|
||||
[0, expected_rate, expected_rate, 0, 0, expected_rate, 0],
|
||||
|
|
@ -44,7 +44,7 @@ def test_concentrations(baseline_model):
|
|||
concentrations = [baseline_model.concentration(t) for t in ts]
|
||||
npt.assert_allclose(
|
||||
concentrations,
|
||||
[0.000000e+00, 0.41611256, 1.3205628e-14, 0.41611256, 4.1909001e-28],
|
||||
[0.000000e+00, 20.805628, 6.602814e-13, 20.805628, 2.09545e-26],
|
||||
rtol=1e-6
|
||||
)
|
||||
|
||||
|
|
@ -354,16 +354,16 @@ def build_exposure_model(concentration_model):
|
|||
)
|
||||
|
||||
|
||||
# expected quanta were computed with a trapezoidal integration, using
|
||||
# expected exposure were computed with a trapezoidal integration, using
|
||||
# a mesh of 100'000 pts per exposed presence interval.
|
||||
@pytest.mark.parametrize(
|
||||
"month, expected_quanta",
|
||||
"month, expected_exposure",
|
||||
[
|
||||
['Jan', 9.930854],
|
||||
['Jun', 37.962708],
|
||||
['Jan', 496.5427],
|
||||
['Jun', 1898.1354],
|
||||
],
|
||||
)
|
||||
def test_quanta_hourly_dep(month,expected_quanta):
|
||||
def test_exposure_hourly_dep(month,expected_exposure):
|
||||
m = build_exposure_model(
|
||||
build_hourly_dependent_model(
|
||||
month,
|
||||
|
|
@ -371,20 +371,20 @@ def test_quanta_hourly_dep(month,expected_quanta):
|
|||
intervals_presence_infected=((8, 12), (13, 17))
|
||||
)
|
||||
)
|
||||
quanta = m.quanta_exposure()
|
||||
npt.assert_allclose(quanta, expected_quanta)
|
||||
exposure = m.exposure()
|
||||
npt.assert_allclose(exposure, expected_exposure)
|
||||
|
||||
# expected quanta were computed with a trapezoidal integration, using
|
||||
# expected exposure were computed with a trapezoidal integration, using
|
||||
# a mesh of 100'000 pts per exposed presence interval and 25 pts per hour
|
||||
# for the temperature discretization.
|
||||
@pytest.mark.parametrize(
|
||||
"month, expected_quanta",
|
||||
"month, expected_exposure",
|
||||
[
|
||||
['Jan', 9.993842],
|
||||
['Jun', 40.151985],
|
||||
['Jan', 499.6921],
|
||||
['Jun', 2007.59925],
|
||||
],
|
||||
)
|
||||
def test_quanta_hourly_dep_refined(month,expected_quanta):
|
||||
def test_exposure_hourly_dep_refined(month,expected_exposure):
|
||||
m = build_exposure_model(
|
||||
build_hourly_dependent_model(
|
||||
month,
|
||||
|
|
@ -393,5 +393,5 @@ def test_quanta_hourly_dep_refined(month,expected_quanta):
|
|||
temperatures=data.GenevaTemperatures,
|
||||
)
|
||||
)
|
||||
quanta = m.quanta_exposure()
|
||||
npt.assert_allclose(quanta, expected_quanta, rtol=0.02)
|
||||
exposure = m.exposure()
|
||||
npt.assert_allclose(exposure, expected_exposure, rtol=0.02)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,6 @@ def test_build_concentration_model(baseline_mc_model: cara.monte_carlo.Concentra
|
|||
def test_build_exposure_model(baseline_mc_exposure_model: cara.monte_carlo.ExposureModel):
|
||||
model = baseline_mc_exposure_model.build_model(7)
|
||||
assert isinstance(model, cara.models.ExposureModel)
|
||||
prob = model.quanta_exposure()
|
||||
prob = model.exposure()
|
||||
assert isinstance(prob, np.ndarray)
|
||||
assert prob.shape == (7, )
|
||||
|
|
|
|||
|
|
@ -233,42 +233,42 @@ def skagit_chorale_mc():
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mc_model, expected_pi, expected_new_cases, expected_dose, expected_qR",
|
||||
"mc_model, expected_pi, expected_new_cases, expected_dose, expected_ER",
|
||||
[
|
||||
["shared_office_mc", 10.7, 0.32, 0.954, 10.9],
|
||||
["classroom_mc", 36.1, 6.85, 13.0, 474.4],
|
||||
["ski_cabin_mc", 16.3, 0.49, 0.599, 123.4],
|
||||
["gym_mc", 2.25, 0.63, 0.01307, 16.4],
|
||||
["waiting_room_mc", 9.72, 1.36, 0.571, 58.9],
|
||||
["skagit_chorale_mc",29.9, 17.9, 1.90, 1414],
|
||||
["shared_office_mc", 10.7, 0.32, 57.24, 654],
|
||||
["classroom_mc", 36.1, 6.85, 780.0, 28464],
|
||||
["ski_cabin_mc", 16.3, 0.49, 35.94, 7404],
|
||||
["gym_mc", 2.25, 0.63, 0.7842, 984],
|
||||
["waiting_room_mc", 9.72, 1.36, 34.26, 3534],
|
||||
["skagit_chorale_mc",29.9, 17.9, 190.0, 141400],
|
||||
]
|
||||
)
|
||||
def test_report_models(mc_model, expected_pi, expected_new_cases,
|
||||
expected_dose, expected_qR, request):
|
||||
expected_dose, expected_ER, request):
|
||||
mc_model = request.getfixturevalue(mc_model)
|
||||
exposure_model = mc_model.build_model(size=SAMPLE_SIZE)
|
||||
npt.assert_allclose(exposure_model.infection_probability().mean(),
|
||||
expected_pi, rtol=TOLERANCE)
|
||||
npt.assert_allclose(exposure_model.expected_new_cases().mean(),
|
||||
expected_new_cases, rtol=TOLERANCE)
|
||||
npt.assert_allclose(exposure_model.quanta_exposure().mean(),
|
||||
npt.assert_allclose(exposure_model.exposure().mean(),
|
||||
expected_dose, rtol=TOLERANCE)
|
||||
npt.assert_allclose(
|
||||
exposure_model.concentration_model.infected.emission_rate_when_present().mean(),
|
||||
expected_qR, rtol=TOLERANCE)
|
||||
expected_ER, rtol=TOLERANCE)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mask_type, month, expected_pi, expected_dose, expected_qR",
|
||||
"mask_type, month, expected_pi, expected_dose, expected_ER",
|
||||
[
|
||||
["No mask", "Jul", 30.0, 6.764, 64.9],
|
||||
["Type I", "Jul", 10.2, 1.223, 11.7],
|
||||
["FFP2", "Jul", 4.0, 1.223, 11.7],
|
||||
["Type I", "Feb", 4.25, 0.357, 11.7],
|
||||
["No mask", "Jul", 30.0, 405.84, 3894],
|
||||
["Type I", "Jul", 10.2, 73.38, 702],
|
||||
["FFP2", "Jul", 4.0, 73.38, 702],
|
||||
["Type I", "Feb", 4.25, 21.42, 702],
|
||||
],
|
||||
)
|
||||
def test_small_shared_office_Geneva(mask_type, month, expected_pi,
|
||||
expected_dose, expected_qR):
|
||||
expected_dose, expected_ER):
|
||||
concentration_mc = mc.ConcentrationModel(
|
||||
room=models.Room(volume=33, humidity=0.5),
|
||||
ventilation=models.MultipleVentilation(
|
||||
|
|
@ -309,8 +309,8 @@ def test_small_shared_office_Geneva(mask_type, month, expected_pi,
|
|||
exposure_model = exposure_mc.build_model(size=SAMPLE_SIZE)
|
||||
npt.assert_allclose(exposure_model.infection_probability().mean(),
|
||||
expected_pi, rtol=TOLERANCE)
|
||||
npt.assert_allclose(exposure_model.quanta_exposure().mean(),
|
||||
npt.assert_allclose(exposure_model.exposure().mean(),
|
||||
expected_dose, rtol=TOLERANCE)
|
||||
npt.assert_allclose(
|
||||
exposure_model.concentration_model.infected.emission_rate_when_present().mean(),
|
||||
expected_qR, rtol=TOLERANCE)
|
||||
expected_ER, rtol=TOLERANCE)
|
||||
|
|
|
|||
Loading…
Reference in a new issue