Skip to content

Commit eba8566

Browse files
author
mheie
committed
Merge https://gitlab.cern.ch/caimira/caimira into feature/multiple_concentrationmodels
2 parents dc13962 + d674fcc commit eba8566

4 files changed

Lines changed: 111 additions & 57 deletions

File tree

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

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,18 +1116,6 @@ def last_state_change(self, time: float) -> float:
11161116
t_index = max([t_index - 1, 0])
11171117
return times[t_index]
11181118

1119-
def _next_state_change(self, time: float) -> float:
1120-
"""
1121-
Find the nearest future state change.
1122-
"""
1123-
for change_time in self.state_change_times():
1124-
if change_time >= time:
1125-
return change_time
1126-
raise ValueError(
1127-
f"The requested time ({time}) is greater than last available "
1128-
f"state change time ({change_time})"
1129-
)
1130-
11311119
@method_cache
11321120
def _normed_concentration_cached(self, time: float) -> _VectorisedFloat:
11331121
"""
@@ -1153,8 +1141,7 @@ def _normed_concentration(self, time: float) -> _VectorisedFloat:
11531141
if time <= self._first_presence_time():
11541142
return self.min_background_concentration()/self.normalization_factor()
11551143

1156-
next_state_change_time = self._next_state_change(time)
1157-
RR = self.removal_rate(next_state_change_time)
1144+
RR = self.removal_rate(time)
11581145

11591146
t_last_state_change = self.last_state_change(time)
11601147
conc_at_last_state_change = self._normed_concentration_cached(t_last_state_change)
@@ -1165,12 +1152,12 @@ def _normed_concentration(self, time: float) -> _VectorisedFloat:
11651152
curr_conc_state = np.empty(RR.shape, dtype=np.float64)
11661153
curr_conc_state[RR == 0.] = delta_time * self.population.people_present(time) / (
11671154
self.room.volume[RR == 0.] if isinstance(self.room.volume,np.ndarray) else self.room.volume)
1168-
curr_conc_state[RR != 0.] = self._normed_concentration_limit(next_state_change_time)[RR != 0.] * (1 - fac[RR != 0.])
1155+
curr_conc_state[RR != 0.] = self._normed_concentration_limit(time)[RR != 0.] * (1 - fac[RR != 0.])
11691156
else:
11701157
if RR == 0.:
11711158
curr_conc_state = delta_time * self.population.people_present(time) / self.room.volume
11721159
else:
1173-
curr_conc_state = self._normed_concentration_limit(next_state_change_time) * (1 - fac)
1160+
curr_conc_state = self._normed_concentration_limit(time) * (1 - fac)
11741161

11751162
return curr_conc_state + conc_at_last_state_change * fac
11761163

@@ -1193,21 +1180,21 @@ def normed_integrated_concentration(self, start: float, stop: float) -> _Vectori
11931180
"""
11941181
if stop <= self._first_presence_time():
11951182
return (stop - start)*self.min_background_concentration()/self.normalization_factor()
1196-
state_change_times = self.state_change_times()
1183+
change_times = self.state_change_times()
1184+
if stop > change_times[-1]:
1185+
change_times.append(stop)
11971186
req_start, req_stop = start, stop
11981187
total_normed_concentration = 0.
1199-
for interval_start, interval_stop in zip(state_change_times[:-1], state_change_times[1:]):
1188+
for interval_start, interval_stop in zip(change_times[:-1], change_times[1:]):
12001189
if req_start > interval_stop or req_stop < interval_start:
12011190
continue
12021191
# Clip the current interval to the requested range.
12031192
start = max([interval_start, req_start])
12041193
stop = min([interval_stop, req_stop])
12051194

12061195
conc_start = self._normed_concentration_cached(start)
1207-
1208-
next_conc_state = self._next_state_change(stop)
1209-
conc_limit = self._normed_concentration_limit(next_conc_state)
1210-
RR = self.removal_rate(next_conc_state)
1196+
conc_limit = self._normed_concentration_limit(stop)
1197+
RR = self.removal_rate(stop)
12111198
delta_time = stop - start
12121199
total_normed_concentration += (
12131200
conc_limit * delta_time +

caimira/tests/models/test_co2_concentration_model.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ def simple_co2_conc_model(data_registry):
2020
),
2121
)
2222

23+
@pytest.fixture
24+
def simple_co2_conc_model_extended_presence(data_registry):
25+
return models.CO2ConcentrationModel(
26+
data_registry=data_registry,
27+
room=models.Room(200, models.PiecewiseConstant((0., 24.), (293,))),
28+
ventilation=models.AirChange(models.PeriodicInterval(period=120, duration=120), 0.25),
29+
CO2_emitters=models.SimplePopulation(
30+
number=5,
31+
presence=models.SpecificInterval((([0., 4.], [20., 20.1]))),
32+
activity=models.Activity.types['Seated'],
33+
),
34+
)
35+
2336

2437
@pytest.mark.parametrize(
2538
"time, expected_co2_concentration", [
@@ -114,4 +127,30 @@ def root_mean_square_error_percentage(actual, predicted) -> float:
114127

115128
acceptable_rmsep = 10 # Threshold of 10% for the accepted error margin
116129
assert rmsep <= acceptable_rmsep, f"RMSEP {rmsep} exceeds acceptable threshold {acceptable_rmsep}"
130+
131+
@pytest.mark.parametrize("time", [4.1,10])
132+
def test_concentration_limit_last_state_change(simple_co2_conc_model, time):
133+
npt.assert_almost_equal(simple_co2_conc_model._normed_concentration_limit(time), simple_co2_conc_model.min_background_concentration()/simple_co2_conc_model.normalization_factor())
134+
135+
@pytest.mark.parametrize([
136+
"start",
137+
"stop"],
138+
[
139+
[10, 19],
140+
[4, 19],
141+
[0., 19],
142+
[0., 20],
143+
]
144+
)
145+
def test_concentration_after_last_state_change(simple_co2_conc_model, simple_co2_conc_model_extended_presence, start, stop):
146+
"""
147+
Test that the concentration results for a model with no more presence of emitters
148+
equals the concentration results of a model where the emitter will reenter at a later point.
149+
"""
150+
time = (start+stop)/2
151+
npt.assert_almost_equal(simple_co2_conc_model.removal_rate(time), simple_co2_conc_model_extended_presence.removal_rate(time))
152+
npt.assert_almost_equal(simple_co2_conc_model.concentration(time), simple_co2_conc_model_extended_presence.concentration(time))
153+
npt.assert_almost_equal(simple_co2_conc_model._normed_concentration(time), simple_co2_conc_model_extended_presence._normed_concentration(time))
154+
npt.assert_almost_equal(simple_co2_conc_model.normed_integrated_concentration(start, stop), simple_co2_conc_model_extended_presence.normed_integrated_concentration(start, stop))
155+
npt.assert_almost_equal(simple_co2_conc_model.integrated_concentration(start, stop), simple_co2_conc_model_extended_presence.integrated_concentration(start, stop))
117156

caimira/tests/models/test_concentration_model.py

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,26 @@ def simple_conc_model(data_registry):
107107
evaporation_factor=0.3,
108108
)
109109

110+
@pytest.fixture
111+
def simple_conc_model_extended_presence(data_registry):
112+
ventilation_times = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), )
113+
return models.ConcentrationModel(
114+
data_registry=data_registry,
115+
room = models.Room(75, models.PiecewiseConstant((0., 24.), (293,))),
116+
ventilation = models.AirChange(ventilation_times, 100),
117+
infected = models.InfectedPopulation(
118+
data_registry=data_registry,
119+
number=1,
120+
presence=models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.], [20., 20.001]), ),
121+
mask=models.Mask.types['Type I'],
122+
activity=models.Activity.types['Seated'],
123+
virus=models.Virus.types['SARS_CoV_2'],
124+
expiration=models.Expiration.types['Breathing'],
125+
host_immunity=0.,
126+
),
127+
evaporation_factor=0.3,
128+
)
129+
110130

111131
@pytest.fixture
112132
def dummy_population(simple_conc_model) -> models.Population:
@@ -143,35 +163,6 @@ def test_last_state_change_time(
143163
assert simple_conc_model.last_state_change(float(time)) == expected_last_state_change
144164

145165

146-
@pytest.mark.parametrize(
147-
"time, expected_next_state_change", [
148-
[0.0, 0.0],
149-
[0.5, 0.5],
150-
[1, 1],
151-
[1.05, 1.1],
152-
[1.1, 1.1],
153-
[1.11, 2],
154-
[2, 2],
155-
[2.1, 3],
156-
[3, 3],
157-
]
158-
)
159-
def test_next_state_change_time(
160-
simple_conc_model: models.ConcentrationModel,
161-
time,
162-
expected_next_state_change,
163-
):
164-
assert simple_conc_model._next_state_change(float(time)) == expected_next_state_change
165-
166-
167-
def test_next_state_change_time_out_of_range(simple_conc_model: models.ConcentrationModel):
168-
with pytest.raises(
169-
ValueError,
170-
match=re.escape("The requested time (3.1) is greater than last available state change time (3.0)")
171-
):
172-
simple_conc_model._next_state_change(3.1)
173-
174-
175166
def test_first_presence_time(simple_conc_model):
176167
assert simple_conc_model._first_presence_time() == 0.5
177168

@@ -286,3 +277,29 @@ def test_zero_ventilation_rate(
286277

287278
normed_concentration = known_conc_model.concentration(1)
288279
assert normed_concentration == pytest.approx(expected_concentration, abs=1e-6)
280+
281+
@pytest.mark.parametrize("time", [3.1,10])
282+
def test_concentration_limit_last_state_change(simple_conc_model, time):
283+
npt.assert_almost_equal(simple_conc_model._normed_concentration_limit(time), simple_conc_model.min_background_concentration()/simple_conc_model.normalization_factor())
284+
285+
@pytest.mark.parametrize([
286+
"start",
287+
"stop"],
288+
[
289+
[10, 19],
290+
[3, 19],
291+
[0., 19],
292+
[0., 20],
293+
]
294+
)
295+
def test_concentration_after_last_state_change(simple_conc_model, simple_conc_model_extended_presence, start, stop):
296+
"""
297+
Test that the concentration results for a model with no more presence of emitters
298+
equals the concentration results of a model where the emitter will reenter at a later point.
299+
"""
300+
time = (start+stop)/2
301+
npt.assert_almost_equal(simple_conc_model.removal_rate(time), simple_conc_model_extended_presence.removal_rate(time))
302+
npt.assert_almost_equal(simple_conc_model.concentration(time), simple_conc_model_extended_presence.concentration(time))
303+
npt.assert_almost_equal(simple_conc_model._normed_concentration(time), simple_conc_model_extended_presence._normed_concentration(time))
304+
npt.assert_almost_equal(simple_conc_model.normed_integrated_concentration(start, stop), simple_conc_model_extended_presence.normed_integrated_concentration(start, stop))
305+
npt.assert_almost_equal(simple_conc_model.integrated_concentration(start, stop), simple_conc_model_extended_presence.integrated_concentration(start, stop))

caimira/tests/models/test_exposure_model.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@ class KnownNormedconcentration(models.ConcentrationModel):
1919
2020
"""
2121
normed_concentration_function: typing.Callable = lambda x: 0
22+
normed_concentration_limit_function: typing.Callable = normed_concentration_function
2223

2324
def removal_rate(self, time: float) -> models._VectorisedFloat:
2425
# Very large decay constant -> same as constant concentration
2526
return 1.e50
2627

2728
def _normed_concentration_limit(self, time: float) -> models._VectorisedFloat:
28-
return self.normed_concentration_function(time) * self.infected.number
29+
return self.normed_concentration_limit_function(time) * self.infected.number
2930

3031
def state_change_times(self):
3132
return [0., 24.]
3233

33-
def _next_state_change(self, time: float):
34+
def _next_state_change(self, time: float): # Outdated method, see merge_requests/539
3435
return 24.
3536

3637
def _normed_concentration(self, time: float) -> models._VectorisedFloat: # noqa
@@ -56,7 +57,7 @@ def _normed_concentration(self, time: float) -> models._VectorisedFloat: # noqa
5657
),
5758
]
5859

59-
def known_concentrations(func, data_registry=DataRegistry()):
60+
def known_concentrations(func, func_lim=None, data_registry=DataRegistry()):
6061
dummy_room = models.Room(50, 0.5)
6162
dummy_ventilation = models._VentilationBase()
6263
dummy_infected_population = models.InfectedPopulation(
@@ -71,8 +72,13 @@ def known_concentrations(func, data_registry=DataRegistry()):
7172
)
7273
normed_func = lambda x: (func(x) /
7374
dummy_infected_population.emission_rate_per_person_when_present())
75+
if func_lim:
76+
normed_func_lim = lambda x: (func_lim(x) /
77+
dummy_infected_population.emission_rate_per_person_when_present())
78+
else:
79+
normed_func_lim = normed_func
7480
return KnownNormedconcentration(data_registry, dummy_room, dummy_ventilation,
75-
dummy_infected_population, 0.3, normed_func)
81+
dummy_infected_population, 0.3, normed_func, normed_func_lim)
7682

7783

7884
@pytest.mark.parametrize(
@@ -114,8 +120,13 @@ def test_exposure_model_ndarray(data_registry, population, cm,
114120
[populations[2], np.array([1.36390289, 1.52436206])],
115121
])
116122
def test_exposure_model_ndarray_and_float_mix(data_registry, population, expected_deposited_exposure, sr_model, cases_model):
117-
cm = known_concentrations(
118-
lambda t: 0. if np.floor(t) % 2 else np.array([0.6, 0.6]))
123+
func = lambda t: 0. if np.floor(t) % 2 else np.array([0.6, 0.6])
124+
125+
# After merge_requests/539 normed_integrated_concentration computes normed_concentration_limit(t).
126+
# However, the expected_deposited_exposure values assume normed_integrated_concentration computes
127+
# normed_concentration_limit(_next_state_change(t)) = np.array([0.6, 0.6]) for all t
128+
func_lim = lambda t: func(24)
129+
cm = known_concentrations(func,func_lim)
119130
model = ExposureModel(data_registry, cm, sr_model, population, cases_model)
120131

121132
np.testing.assert_almost_equal(

0 commit comments

Comments
 (0)