Skip to content

Commit 499675c

Browse files
author
mheie
committed
MultiplePopulations and dynamic occupancy in _ConcentrationModelBase
1 parent 2c4db0a commit 499675c

3 files changed

Lines changed: 195 additions & 3 deletions

File tree

caimira/src/caimira/calculator/models/models.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,9 @@ def people_present(self, time: float):
842842
return self.number * self.person_present(time)
843843
else:
844844
return int(self.number.value(time))
845+
846+
def transition_times(self):
847+
return self.presence_interval().transition_times()
845848

846849

847850
@dataclass(frozen=True)
@@ -989,6 +992,22 @@ def particle(self) -> Particle:
989992
The Particle object representing the aerosol - here the default one
990993
"""
991994
return self.expiration.particle
995+
996+
@dataclass(frozen=True)
997+
class MultiplePopulations:
998+
"""
999+
Store the information of multiple populations to be used for dynamic emitters/infected.
1000+
"""
1001+
populations: list[SimplePopulation]
1002+
1003+
def people_present(self, time: float):
1004+
return np.sum([population.people_present(time) for population in self.populations])
1005+
1006+
def transition_times(self):
1007+
state_change_times = {0.}
1008+
for population in self.populations:
1009+
state_change_times.update(population.transition_times())
1010+
return sorted(state_change_times)
9921011

9931012

9941013
@dataclass(frozen=True)
@@ -1029,7 +1048,7 @@ class _ConcentrationModelBase:
10291048
ventilation: _VentilationBase
10301049

10311050
@property
1032-
def population(self) -> SimplePopulation:
1051+
def population(self) -> typing.Union[MultiplePopulations, SimplePopulation]:
10331052
"""
10341053
Population in the room (the emitters of what we compute the
10351054
concentration of)
@@ -1089,7 +1108,7 @@ def state_change_times(self) -> typing.List[float]:
10891108
the times at which their state changes.
10901109
"""
10911110
state_change_times = {0.}
1092-
state_change_times.update(self.population.presence_interval().transition_times())
1111+
state_change_times.update(self.population.transition_times())
10931112
state_change_times.update(self.ventilation.transition_times(self.room))
10941113
return sorted(state_change_times)
10951114

@@ -1098,7 +1117,10 @@ def _first_presence_time(self) -> float:
10981117
"""
10991118
First presence time. Before that, the concentration is zero.
11001119
"""
1101-
return self.population.presence_interval().boundaries()[0][0]
1120+
if isinstance(self.population, MultiplePopulations):
1121+
return np.min([simple_population.presence_interval().boundaries()[0][0] for simple_population in self.population.populations])
1122+
else:
1123+
return self.population.presence_interval().boundaries()[0][0]
11021124

11031125
def last_state_change(self, time: float) -> float:
11041126
"""
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import re
2+
3+
import numpy as np
4+
import numpy.testing as npt
5+
import pytest
6+
from dataclasses import dataclass
7+
8+
from caimira.calculator.models import models
9+
from caimira.calculator.store.data_registry import DataRegistry
10+
11+
known_min_background_concentration = 440.44
12+
13+
@dataclass(frozen=True)
14+
class KnownConcentrationModelBase(models._ConcentrationModelBase):
15+
"""
16+
A _ConcentrationModelBase class where all the class methods are
17+
redefined with a value taken from new parameters. Useful for testing.
18+
19+
"""
20+
known_multiple_populations: models.MultiplePopulations
21+
22+
known_removal_rate: float
23+
24+
known_min_background_concentration: float
25+
26+
known_normalization_factor: float
27+
28+
@property
29+
def population(self) -> models.MultiplePopulations:
30+
return self.known_multiple_populations
31+
32+
def removal_rate(self, time: float) -> float:
33+
return self.known_removal_rate
34+
35+
def min_background_concentration(self) -> float:
36+
return self.known_min_background_concentration
37+
38+
def normalization_factor(self) -> float:
39+
return self.known_normalization_factor
40+
41+
42+
@pytest.fixture
43+
def dummy_multiple_populations() -> models.MultiplePopulations:
44+
interesting_times1 = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), )
45+
population1 = models.Population(
46+
number=1,
47+
presence=interesting_times1,
48+
mask=models.Mask.types['No mask'],
49+
activity=models.Activity.types['Seated'],
50+
host_immunity=0.,
51+
)
52+
53+
interesting_times2 = models.SpecificInterval(([0.4, 1.], [1.1, 2]), )
54+
population2 = models.Population(
55+
number=10,
56+
presence=interesting_times2,
57+
mask=models.Mask.types['Cloth'],
58+
activity=models.Activity.types['Heavy exercise'],
59+
host_immunity=0.,
60+
)
61+
62+
interesting_times3 = models.SpecificInterval(([5., 6.],), )
63+
population3 = models.Population(
64+
number=5,
65+
presence=interesting_times3,
66+
mask=models.Mask.types['Type I'],
67+
activity=models.Activity.types['Light activity'],
68+
host_immunity=0.5,
69+
)
70+
return models.MultiplePopulations([population1, population2, population3])
71+
72+
@pytest.fixture
73+
def simple_dynamic_conc_model(data_registry, dummy_multiple_populations):
74+
interesting_times = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), )
75+
return KnownConcentrationModelBase(
76+
data_registry,
77+
room = models.Room(75, models.PiecewiseConstant((0., 24.), (293,))),
78+
ventilation = models.AirChange(interesting_times, 100),
79+
known_multiple_populations = dummy_multiple_populations,
80+
known_removal_rate = 10.,
81+
known_min_background_concentration = known_min_background_concentration,
82+
known_normalization_factor = 10.)
83+
84+
def test_multiple_populations(simple_dynamic_conc_model):
85+
assert isinstance(simple_dynamic_conc_model.population, models.MultiplePopulations)
86+
assert np.all([isinstance(population, models.SimplePopulation) for population in simple_dynamic_conc_model.population.populations])
87+
88+
def test_state_change_times(simple_dynamic_conc_model):
89+
expected_transition_times = sorted(set((0.0, 0.4, 0.5, 1., 1.1, 2, 3., 5, 6)))
90+
result = simple_dynamic_conc_model.state_change_times()
91+
npt.assert_array_equal(result, expected_transition_times)
92+
93+
def test_first_presence_time(simple_dynamic_conc_model):
94+
npt.assert_array_equal(simple_dynamic_conc_model._first_presence_time(), 0.4)
95+
96+
def test_concentration(simple_dynamic_conc_model):
97+
assert isinstance(simple_dynamic_conc_model.concentration(0), float)
98+
npt.assert_almost_equal(simple_dynamic_conc_model.concentration(0),known_min_background_concentration)
99+
assert simple_dynamic_conc_model.concentration(0.5) > known_min_background_concentration
100+
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import re
2+
3+
import numpy as np
4+
import numpy.testing as npt
5+
import pytest
6+
from dataclasses import dataclass
7+
8+
from caimira.calculator.models import models
9+
from caimira.calculator.store.data_registry import DataRegistry
10+
11+
@pytest.fixture
12+
def dummy_multiple_populations() -> models.MultiplePopulations:
13+
interesting_times1 = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), )
14+
population1 = models.Population(
15+
number=1,
16+
presence=interesting_times1,
17+
mask=models.Mask.types['No mask'],
18+
activity=models.Activity.types['Seated'],
19+
host_immunity=0.,
20+
)
21+
22+
interesting_times2 = models.SpecificInterval(([0.4, 1.], [1.1, 2]), )
23+
population2 = models.Population(
24+
number=10,
25+
presence=interesting_times2,
26+
mask=models.Mask.types['Cloth'],
27+
activity=models.Activity.types['Heavy exercise'],
28+
host_immunity=0.,
29+
)
30+
31+
interesting_times3 = models.SpecificInterval(([5., 6.],), )
32+
population3 = models.Population(
33+
number=5,
34+
presence=interesting_times3,
35+
mask=models.Mask.types['Type I'],
36+
activity=models.Activity.types['Light activity'],
37+
host_immunity=0.5,
38+
)
39+
return models.MultiplePopulations([population1, population2, population3])
40+
41+
42+
def test_multiple_populations(dummy_multiple_populations):
43+
assert isinstance(dummy_multiple_populations, models.MultiplePopulations)
44+
assert isinstance(dummy_multiple_populations.populations, list)
45+
assert np.all([isinstance(population, models.SimplePopulation) for population in dummy_multiple_populations.populations])
46+
47+
48+
@pytest.mark.parametrize(
49+
"time, expected_people_present", [
50+
[0.5, 10], # Out of range goes to the first state.
51+
[1., 11],
52+
[1.1, 0],
53+
[2., 11],
54+
[2, 11],
55+
[2.5, 1],
56+
[5., 0],
57+
[6., 5],
58+
[4., 0],
59+
[7., 0],
60+
[0.1, 0],
61+
]
62+
)
63+
def test_people_present(time, expected_people_present, dummy_multiple_populations):
64+
result = dummy_multiple_populations.people_present(time)
65+
npt.assert_array_equal(result, expected_people_present)
66+
67+
def test_transition_times(dummy_multiple_populations):
68+
expected_transition_times = sorted(set((0.0, 0.4, 0.5, 1., 1.1, 2, 3., 5, 6)))
69+
result = dummy_multiple_populations.transition_times()
70+
npt.assert_array_equal(result, expected_transition_times)

0 commit comments

Comments
 (0)