- extract, isolate and package it in a completely independent Python module, versioned and in a way that allows releases on PyPI.org - fixed error in placeholder for secondary school (data registry defaults) - added restriction in pytest version to install - expected number of new cases fix - data registry update (schema v2.1.1) - github update - deprecate ExpertApplication and CO2Application - changes to reflect schema update 2.0.2 - version update - Fixed error with f_inf (short-range) - new folder layout - Conditional probability data update - General fixes - Fitting results in L/S/person - CO2 fitting algorithm refinement
261 lines
11 KiB
Python
261 lines
11 KiB
Python
import re
|
|
|
|
import numpy as np
|
|
import numpy.testing as npt
|
|
import pytest
|
|
|
|
from caimira.calculator.models import models
|
|
from caimira.calculator.models import dataclass_utils as dc_utils
|
|
|
|
@pytest.fixture
|
|
def full_exposure_model(data_registry):
|
|
return models.ExposureModel(
|
|
data_registry=data_registry,
|
|
concentration_model=models.ConcentrationModel(
|
|
data_registry=data_registry,
|
|
room=models.Room(volume=100),
|
|
ventilation=models.AirChange(
|
|
active=models.PeriodicInterval(120, 120), air_exch=0.25),
|
|
infected=models.InfectedPopulation(
|
|
data_registry=data_registry,
|
|
number=1,
|
|
presence=models.SpecificInterval(((8, 12), (13, 17), )),
|
|
mask=models.Mask.types['No mask'],
|
|
activity=models.Activity.types['Seated'],
|
|
expiration=models.Expiration.types['Breathing'],
|
|
virus=models.Virus.types['SARS_CoV_2'],
|
|
host_immunity=0.
|
|
),
|
|
evaporation_factor=0.3,
|
|
),
|
|
short_range=(),
|
|
exposed=models.Population(
|
|
number=10,
|
|
presence=models.SpecificInterval(((8, 12), (13, 17), )),
|
|
mask=models.Mask.types['No mask'],
|
|
activity=models.Activity.types['Seated'],
|
|
host_immunity=0.
|
|
),
|
|
geographical_data=(),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def baseline_infected_population_number(data_registry):
|
|
return models.InfectedPopulation(
|
|
data_registry=data_registry,
|
|
number=models.IntPiecewiseConstant(
|
|
(8, 12, 13, 17), (1, 0, 1)),
|
|
presence=None,
|
|
mask=models.Mask.types['No mask'],
|
|
activity=models.Activity.types['Seated'],
|
|
virus=models.Virus.types['SARS_CoV_2'],
|
|
expiration=models.Expiration.types['Breathing'],
|
|
host_immunity=0.,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def baseline_exposed_population_number():
|
|
return models.Population(
|
|
number=models.IntPiecewiseConstant(
|
|
(8, 12, 13, 17), (10, 0, 10)),
|
|
presence=None,
|
|
mask=models.Mask.types['No mask'],
|
|
activity=models.Activity.types['Seated'],
|
|
host_immunity=0.,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def dynamic_infected_single_exposure_model(full_exposure_model, baseline_infected_population_number):
|
|
return dc_utils.nested_replace(full_exposure_model,
|
|
{'concentration_model.infected': baseline_infected_population_number, })
|
|
|
|
|
|
@pytest.fixture
|
|
def dynamic_exposed_single_exposure_model(full_exposure_model, baseline_exposed_population_number):
|
|
return dc_utils.nested_replace(full_exposure_model,
|
|
{'exposed': baseline_exposed_population_number, })
|
|
|
|
|
|
@pytest.fixture
|
|
def dynamic_population_exposure_model(full_exposure_model, baseline_infected_population_number ,baseline_exposed_population_number):
|
|
return dc_utils.nested_replace(full_exposure_model, {
|
|
'concentration_model.infected': baseline_infected_population_number,
|
|
'exposed': baseline_exposed_population_number,
|
|
})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"time",
|
|
[4., 8., 10., 12., 13., 14., 16., 20., 24.],
|
|
)
|
|
def test_population_number(full_exposure_model: models.ExposureModel,
|
|
baseline_infected_population_number: models.InfectedPopulation, time: float):
|
|
|
|
int_population_number: models.InfectedPopulation = full_exposure_model.concentration_model.infected
|
|
piecewise_population_number: models.InfectedPopulation = baseline_infected_population_number
|
|
|
|
with pytest.raises(
|
|
TypeError,
|
|
match=f'The presence argument must be an "Interval". Got {type(None)}'
|
|
):
|
|
dc_utils.nested_replace(
|
|
int_population_number, {'presence': None}
|
|
)
|
|
|
|
with pytest.raises(
|
|
TypeError,
|
|
match="The presence argument must be None for a IntPiecewiseConstant number"
|
|
):
|
|
dc_utils.nested_replace(
|
|
piecewise_population_number, {'presence': models.SpecificInterval(((8, 12), ))}
|
|
)
|
|
|
|
assert int_population_number.person_present(time) == piecewise_population_number.person_present(time)
|
|
assert int_population_number.people_present(time) == piecewise_population_number.people_present(time)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"time",
|
|
[4., 8., 10., 12., 13., 14., 16., 20., 24.],
|
|
)
|
|
def test_concentration_model_dynamic_population(full_exposure_model: models.ExposureModel,
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
time: float):
|
|
|
|
assert full_exposure_model.concentration(time) == dynamic_infected_single_exposure_model.concentration(time)
|
|
|
|
|
|
@pytest.mark.parametrize("number_of_infected",[1, 2, 3, 4, 5])
|
|
@pytest.mark.parametrize("time",[9., 12.5, 16.])
|
|
def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureModel,
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
time: float,
|
|
number_of_infected: int):
|
|
|
|
|
|
static_multiple_exposure_model: models.ExposureModel = dc_utils.nested_replace(
|
|
full_exposure_model,
|
|
{
|
|
'concentration_model.infected.number': number_of_infected,
|
|
}
|
|
)
|
|
|
|
npt.assert_almost_equal(static_multiple_exposure_model.concentration(time), dynamic_infected_single_exposure_model.concentration(time) * number_of_infected)
|
|
npt.assert_almost_equal(static_multiple_exposure_model.deposited_exposure(), dynamic_infected_single_exposure_model.deposited_exposure() * number_of_infected)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"time", (8., 9., 10., 11., 12., 13., 14.),
|
|
)
|
|
def test_dynamic_dose(data_registry, full_exposure_model: models.ExposureModel, time: float):
|
|
|
|
dynamic_infected: models.ExposureModel = dc_utils.nested_replace(
|
|
full_exposure_model,
|
|
{
|
|
'concentration_model.infected': models.InfectedPopulation(
|
|
data_registry=data_registry,
|
|
number=models.IntPiecewiseConstant(
|
|
(8, 10, 12, 13, 17), (1, 2, 0, 3)),
|
|
presence=None,
|
|
mask=models.Mask.types['No mask'],
|
|
activity=models.Activity.types['Seated'],
|
|
virus=models.Virus.types['SARS_CoV_2'],
|
|
expiration=models.Expiration.types['Breathing'],
|
|
host_immunity=0.,
|
|
),
|
|
}
|
|
)
|
|
|
|
single_infected: models.ExposureModel = dc_utils.nested_replace(
|
|
full_exposure_model,
|
|
{
|
|
'concentration_model.infected.number': 1,
|
|
'concentration_model.infected.presence': models.SpecificInterval(((8, 10), )),
|
|
}
|
|
)
|
|
|
|
two_infected: models.ExposureModel = dc_utils.nested_replace(
|
|
full_exposure_model,
|
|
{
|
|
'concentration_model.infected.number': 2,
|
|
'concentration_model.infected.presence': models.SpecificInterval(((10, 12), )),
|
|
}
|
|
)
|
|
|
|
three_infected: models.ExposureModel = dc_utils.nested_replace(
|
|
full_exposure_model,
|
|
{
|
|
'concentration_model.infected.number': 3,
|
|
'concentration_model.infected.presence': models.SpecificInterval(((13, 17), )),
|
|
}
|
|
)
|
|
|
|
dynamic_concentration = dynamic_infected.concentration(time)
|
|
dynamic_exposure = dynamic_infected.deposited_exposure()
|
|
|
|
static_concentration, static_exposure = zip(*[(model.concentration(time), model.deposited_exposure())
|
|
for model in (single_infected, two_infected, three_infected)])
|
|
|
|
npt.assert_almost_equal(dynamic_concentration, np.sum(static_concentration))
|
|
npt.assert_almost_equal(dynamic_exposure, np.sum(static_exposure))
|
|
|
|
|
|
def test_infection_probability(
|
|
full_exposure_model: models.ExposureModel,
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
dynamic_exposed_single_exposure_model: models.ExposureModel,
|
|
dynamic_population_exposure_model: models.ExposureModel):
|
|
|
|
base_infection_probability = full_exposure_model.infection_probability()
|
|
npt.assert_almost_equal(base_infection_probability, dynamic_infected_single_exposure_model.infection_probability())
|
|
npt.assert_almost_equal(base_infection_probability, dynamic_exposed_single_exposure_model.infection_probability())
|
|
npt.assert_almost_equal(base_infection_probability, dynamic_population_exposure_model.infection_probability())
|
|
|
|
|
|
def test_dynamic_total_probability_rule(
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
dynamic_exposed_single_exposure_model: models.ExposureModel,
|
|
dynamic_population_exposure_model: models.ExposureModel):
|
|
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability "
|
|
"(including incidence rate) with dynamic occupancy")):
|
|
dynamic_infected_single_exposure_model.total_probability_rule()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability "
|
|
"(including incidence rate) with dynamic occupancy")):
|
|
dynamic_exposed_single_exposure_model.total_probability_rule()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability "
|
|
"(including incidence rate) with dynamic occupancy")):
|
|
dynamic_population_exposure_model.total_probability_rule()
|
|
|
|
def test_dynamic_expected_new_cases(
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
dynamic_exposed_single_exposure_model: models.ExposureModel,
|
|
dynamic_population_exposure_model: models.ExposureModel):
|
|
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases "
|
|
"with dynamic occupancy")):
|
|
dynamic_infected_single_exposure_model.expected_new_cases()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases "
|
|
"with dynamic occupancy")):
|
|
dynamic_exposed_single_exposure_model.expected_new_cases()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases "
|
|
"with dynamic occupancy")):
|
|
dynamic_population_exposure_model.expected_new_cases()
|
|
|
|
def test_dynamic_reproduction_number(
|
|
dynamic_infected_single_exposure_model: models.ExposureModel,
|
|
dynamic_exposed_single_exposure_model: models.ExposureModel,
|
|
dynamic_population_exposure_model: models.ExposureModel):
|
|
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number "
|
|
"with dynamic occupancy")):
|
|
dynamic_infected_single_exposure_model.reproduction_number()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number "
|
|
"with dynamic occupancy")):
|
|
dynamic_exposed_single_exposure_model.reproduction_number()
|
|
with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number "
|
|
"with dynamic occupancy")):
|
|
dynamic_population_exposure_model.reproduction_number()
|