From 307cf13bfaf922353abc05cd301fdc5a9912473d Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Mon, 3 May 2021 12:30:00 +0200 Subject: [PATCH 01/13] Adding a test to check that infectious virus removal rate is computed only at the time of a state change --- cara/tests/models/test_concentration_model.py | 96 +++++++++++++++++-- 1 file changed, 86 insertions(+), 10 deletions(-) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index 9be1cbbf..d7022646 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -1,7 +1,10 @@ +from dataclasses import dataclass + import numpy as np +import numpy.testing as npt import pytest -import cara.models +from cara import models @pytest.mark.parametrize( @@ -27,28 +30,28 @@ def test_concentration_model_vectorisation(override_params): } defaults.update(override_params) - always = cara.models.PeriodicInterval(240, 240) # TODO: This should be a thing on an interval. - c_model = cara.models.ConcentrationModel( - cara.models.Room(defaults['volume']), - cara.models.AirChange(always, defaults['air_change']), - cara.models.InfectedPopulation( + always = models.PeriodicInterval(240, 240) # TODO: This should be a thing on an interval. + c_model = models.ConcentrationModel( + models.Room(defaults['volume']), + models.AirChange(always, defaults['air_change']), + models.InfectedPopulation( number=1, presence=always, - mask=cara.models.Mask( + mask=models.Mask( η_exhale=defaults['η_exhale'], η_leaks=defaults['η_leaks'], η_inhale=0.3, ), - activity=cara.models.Activity( + activity=models.Activity( 0.51, 0.75, ), - virus=cara.models.Virus( + virus=models.Virus( halflife=defaults['virus_halflife'], viral_load_in_sputum=defaults['viral_load_in_sputum'], coefficient_of_infectivity=defaults['coefficient_of_infectivity'], ), - expiration=cara.models.Expiration( + expiration=models.Expiration( ejection_factor=(0.084, 0.009, 0.003, 0.002), ), ) @@ -56,3 +59,76 @@ def test_concentration_model_vectorisation(override_params): concentrations = c_model.concentration(10) assert isinstance(concentrations, np.ndarray) assert concentrations.shape == (2, ) + + +@dataclass(frozen=True) +class DummyVentilation(models.Ventilation): + # Dummy ventilation where air_exchange depends on time explicitly + + #: The interval in which the ventilation is operating. + active: models.Interval + + def air_exchange(self, room: models.Room, time: float) -> models._VectorisedFloat: + if not self.active.triggered(time): + return 0. + return time*0.5 + + +@dataclass(frozen=True) +class DummyPopulation(models.Population): + # Dummy infected population where emission rate depends on time + # explicitly + virus: models.Virus + expiration: models.Expiration + + def emission_rate_when_present(self) -> models._VectorisedFloat: + return 50. + + def individual_emission_rate(self, time) -> models._VectorisedFloat: + """ + The emission rate of a single individual in the population. + """ + + if not self.person_present(time): + return 0. + + return self.emission_rate_when_present()*time + + def emission_rate(self, time) -> models._VectorisedFloat: + """ + The emission rate of the entire population. + """ + return self.individual_emission_rate(time) * self.number + + +def test_concentration_model_constant_parameters(): + always = models.SpecificInterval(present_times=[(0,24)]) + c_model = models.ConcentrationModel( + models.Room(75), + DummyVentilation(always), + DummyPopulation( + number=1, + presence=always, + mask=models.Mask( + η_exhale=0.95, + η_leaks=0.15, + η_inhale=0.3, + ), + activity=models.Activity( + 0.51, + 0.75, + ), + virus=models.Virus( + halflife=1.1, + viral_load_in_sputum=1e9, + coefficient_of_infectivity=0.02, + ), + expiration=models.Expiration( + ejection_factor=(0.084, 0.009, 0.003, 0.002), + ), + ) + ) + times = [0.1, 10, 20, 24] + IVRRs = np.array([c_model.infectious_virus_removal_rate(t) + for t in times]) + assert np.all(IVRRs==IVRRs[-1]) From b62228320654238e4966b63f6b0490e55fcb5717 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Mon, 3 May 2021 12:35:10 +0200 Subject: [PATCH 02/13] In concentration model, compute infectious virus removal rate only at time of next state change, to make sure it stays constant --- cara/models.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cara/models.py b/cara/models.py index 0c04eac0..c232c23c 100644 --- a/cara/models.py +++ b/cara/models.py @@ -641,7 +641,8 @@ class ConcentrationModel: # Deposition rate (h^-1) k = (vg * 3600) / h - return k + self.virus.decay_constant + self.ventilation.air_exchange(self.room, time) + return k + self.virus.decay_constant + self.ventilation.air_exchange( + self.room, self.next_state_change(time)) @cached() def state_change_times(self): @@ -666,6 +667,16 @@ class ConcentrationModel: return change_time return 0 + def next_state_change(self, time: float): + """ + Find the nearest future state change. + + """ + for change_time in self.state_change_times(): + if change_time >= time: + return change_time + return 0 + @cached() def concentration(self, time: float) -> _VectorisedFloat: # Note that time is not vectorised. You can only pass a single float From d0ef2f13c949a6194a2a0b8d41e6595d7dfaa477 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Mon, 3 May 2021 15:01:02 +0200 Subject: [PATCH 03/13] Adding test on concentration limit in concentration model --- cara/tests/models/test_concentration_model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index d7022646..90bc86e4 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -129,6 +129,9 @@ def test_concentration_model_constant_parameters(): ) ) times = [0.1, 10, 20, 24] + concentration_limits = np.array([c_model.concentration_limit(t) + for t in times]) IVRRs = np.array([c_model.infectious_virus_removal_rate(t) for t in times]) + assert np.all(concentration_limits==concentration_limits[-1]) assert np.all(IVRRs==IVRRs[-1]) From 8b1571dcf15ded5ca9a211ca04b9bf854b27b053 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Mon, 3 May 2021 15:01:42 +0200 Subject: [PATCH 04/13] Exposing concentration_limit in ConcentrationModel --- cara/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cara/models.py b/cara/models.py index c232c23c..d37cb0a1 100644 --- a/cara/models.py +++ b/cara/models.py @@ -644,6 +644,13 @@ class ConcentrationModel: return k + self.virus.decay_constant + self.ventilation.air_exchange( self.room, self.next_state_change(time)) + @cached() + def concentration_limit(self, time: float) -> _VectorisedFloat: + V = self.room.volume + IVRR = self.infectious_virus_removal_rate(time) + + return (self.infected.emission_rate(self.next_state_change(time))) / (IVRR * V) + @cached() def state_change_times(self): """ From 94d01104d01c2347a1a2f03b7142ec7bd75f6dd9 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Tue, 4 May 2021 06:43:27 +0200 Subject: [PATCH 05/13] Using concentration_limit method in concentration (ConcentrationModel) --- cara/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cara/models.py b/cara/models.py index 8323a165..8d0386bf 100644 --- a/cara/models.py +++ b/cara/models.py @@ -694,14 +694,13 @@ class ConcentrationModel: if time == 0: return 0.0 IVRR = self.infectious_virus_removal_rate(time) - V = self.room.volume t_last_state_change = self.last_state_change(time) concentration_at_last_state_change = self.concentration(t_last_state_change) delta_time = time - t_last_state_change fac = np.exp(-IVRR * delta_time) - concentration_limit = (self.infected.emission_rate(time)) / (IVRR * V) + concentration_limit = self.concentration_limit(time) return concentration_limit * (1 - fac) + concentration_at_last_state_change * fac From f8b565ea5bc3354651288491d99980f952646260 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Tue, 4 May 2021 07:27:58 +0200 Subject: [PATCH 06/13] Avoid next_state_change to be public in ConcentrationModel; raise an error if time is higher than largest transition time --- cara/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cara/models.py b/cara/models.py index 8d0386bf..553dd8bd 100644 --- a/cara/models.py +++ b/cara/models.py @@ -644,14 +644,14 @@ class ConcentrationModel: k = (vg * 3600) / h return k + self.virus.decay_constant + self.ventilation.air_exchange( - self.room, self.next_state_change(time)) + self.room, self._next_state_change(time)) @cached() def concentration_limit(self, time: float) -> _VectorisedFloat: V = self.room.volume IVRR = self.infectious_virus_removal_rate(time) - return (self.infected.emission_rate(self.next_state_change(time))) / (IVRR * V) + return (self.infected.emission_rate(self._next_state_change(time))) / (IVRR * V) @cached() def state_change_times(self): @@ -676,7 +676,7 @@ class ConcentrationModel: return change_time return 0 - def next_state_change(self, time: float): + def _next_state_change(self, time: float): """ Find the nearest future state change. @@ -684,7 +684,7 @@ class ConcentrationModel: for change_time in self.state_change_times(): if change_time >= time: return change_time - return 0 + raise ValueError("Time higher than largest state change") @cached() def concentration(self, time: float) -> _VectorisedFloat: From 236a7139f54ef1651116791ab5398eccb10f9a02 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Tue, 4 May 2021 08:49:02 +0200 Subject: [PATCH 07/13] Adding a test for next_state_change in ConcentrationModel --- cara/models.py | 2 +- cara/tests/models/test_concentration_model.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/cara/models.py b/cara/models.py index 553dd8bd..609da9a3 100644 --- a/cara/models.py +++ b/cara/models.py @@ -684,7 +684,7 @@ class ConcentrationModel: for change_time in self.state_change_times(): if change_time >= time: return change_time - raise ValueError("Time higher than largest state change") + raise ValueError("Time larger than highest state change") @cached() def concentration(self, time: float) -> _VectorisedFloat: diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index 90bc86e4..10cf6a8c 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -61,6 +61,33 @@ def test_concentration_model_vectorisation(override_params): assert concentrations.shape == (2, ) +@pytest.mark.parametrize( + "time, expected_next_state_change", [ + [0, 0], + [1, 4], + [4, 4], + [24, 24], + ] +) +def test_concentration_model_next_state_change(time,expected_next_state_change): + always = models.PeriodicInterval(240, 240) + c_model = models.ConcentrationModel( + models.Room(75), + models.AirChange(always, 100), + models.InfectedPopulation( + number=1, + presence=always, + mask=models.Mask.types['Type I'], + activity=models.Activity.types['Seated'], + virus=models.Virus.types['SARS_CoV_2'], + expiration=models.Expiration.types['Breathing'], + ) + ) + assert c_model._next_state_change(time) == expected_next_state_change + with pytest.raises(ValueError, match="Time larger than highest state change"): + c_model._next_state_change(24.1) + + @dataclass(frozen=True) class DummyVentilation(models.Ventilation): # Dummy ventilation where air_exchange depends on time explicitly From 717390377e4075b7a6891de8b199d00bbe557560 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Tue, 4 May 2021 08:57:40 +0200 Subject: [PATCH 08/13] concentration_limit method not public anymore; providing a docstring --- cara/models.py | 11 ++++++++--- cara/tests/models/test_concentration_model.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/cara/models.py b/cara/models.py index 609da9a3..521c40c2 100644 --- a/cara/models.py +++ b/cara/models.py @@ -647,7 +647,13 @@ class ConcentrationModel: self.room, self._next_state_change(time)) @cached() - def concentration_limit(self, time: float) -> _VectorisedFloat: + def _concentration_limit(self, time: float) -> _VectorisedFloat: + """ + Provides a constant that represents the theoretical asymptotic + value reached by the concentration when time goes to infinity. + It's a piecewise constant function - at any time it takes the + value it would have at the end of the current interval. + """ V = self.room.volume IVRR = self.infectious_virus_removal_rate(time) @@ -700,8 +706,7 @@ class ConcentrationModel: delta_time = time - t_last_state_change fac = np.exp(-IVRR * delta_time) - concentration_limit = self.concentration_limit(time) - return concentration_limit * (1 - fac) + concentration_at_last_state_change * fac + return self._concentration_limit(time) * (1 - fac) + concentration_at_last_state_change * fac @dataclass(frozen=True) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index 10cf6a8c..f14735d8 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -156,7 +156,7 @@ def test_concentration_model_constant_parameters(): ) ) times = [0.1, 10, 20, 24] - concentration_limits = np.array([c_model.concentration_limit(t) + concentration_limits = np.array([c_model._concentration_limit(t) for t in times]) IVRRs = np.array([c_model.infectious_virus_removal_rate(t) for t in times]) From ac866491dfe62c5ae544838cb7859fa5bb0d470a Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Tue, 4 May 2021 09:03:45 +0200 Subject: [PATCH 09/13] Adding docstrings in DummyPopulation and DummyVentilation (test_concentration_model) --- cara/tests/models/test_concentration_model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index f14735d8..9cc90e6c 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -96,6 +96,10 @@ class DummyVentilation(models.Ventilation): active: models.Interval def air_exchange(self, room: models.Room, time: float) -> models._VectorisedFloat: + """ + Here we put an explicit function of time, hence breaking the rules + set in cara.models. For testing purposes only. + """ if not self.active.triggered(time): return 0. return time*0.5 @@ -114,6 +118,8 @@ class DummyPopulation(models.Population): def individual_emission_rate(self, time) -> models._VectorisedFloat: """ The emission rate of a single individual in the population. + Here we put an explicit function of time, hence breaking the rules + set in cara.models. For testing purposes only. """ if not self.person_present(time): From 817ca8f3bfbf002c15a0aadd3f4a92dc01ece197 Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Wed, 5 May 2021 12:45:02 +0200 Subject: [PATCH 10/13] ConcentrationModel: _next_state_change used only in concentration function, now; improving docstrings and error handling --- cara/models.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/cara/models.py b/cara/models.py index 521c40c2..8f2a8c42 100644 --- a/cara/models.py +++ b/cara/models.py @@ -644,20 +644,19 @@ class ConcentrationModel: k = (vg * 3600) / h return k + self.virus.decay_constant + self.ventilation.air_exchange( - self.room, self._next_state_change(time)) + self.room, time) @cached() def _concentration_limit(self, time: float) -> _VectorisedFloat: """ Provides a constant that represents the theoretical asymptotic - value reached by the concentration when time goes to infinity. - It's a piecewise constant function - at any time it takes the - value it would have at the end of the current interval. + value reached by the concentration when time goes to infinity, + if all parameters were to stay time-independent. """ V = self.room.volume IVRR = self.infectious_virus_removal_rate(time) - return (self.infected.emission_rate(self._next_state_change(time))) / (IVRR * V) + return (self.infected.emission_rate(time)) / (IVRR * V) @cached() def state_change_times(self): @@ -694,19 +693,32 @@ class ConcentrationModel: @cached() def concentration(self, time: float) -> _VectorisedFloat: - # Note that time is not vectorised. You can only pass a single float - # to this method. + """ + Virus quanta 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. + + Note that time is not vectorised. You can only pass a single float + to this method. + """ if time == 0: return 0.0 - IVRR = self.infectious_virus_removal_rate(time) + try: + IVRR = self.infectious_virus_removal_rate(self._next_state_change(time)) + concentration_limit = self._concentration_limit(self._next_state_change(time)) + except ValueError: + raise ValueError("Concentration cannot be computed at a time" + " larger than last state change (parameters" + " are not defined)") t_last_state_change = self.last_state_change(time) concentration_at_last_state_change = self.concentration(t_last_state_change) delta_time = time - t_last_state_change fac = np.exp(-IVRR * delta_time) - return self._concentration_limit(time) * (1 - fac) + concentration_at_last_state_change * fac + return concentration_limit * (1 - fac) + concentration_at_last_state_change * fac @dataclass(frozen=True) From f613ff80bad7a23f09f53c6e4db2e77d7637824c Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Wed, 5 May 2021 12:46:27 +0200 Subject: [PATCH 11/13] Supressing now useless (wrong) tests on time-independence of parameters in ConcentrationModel; adding a test on error handling if time used for concentration computation is beyond the last state change time --- cara/tests/models/test_concentration_model.py | 86 ++----------------- 1 file changed, 5 insertions(+), 81 deletions(-) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index 9cc90e6c..0fd81271 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import re import numpy as np import numpy.testing as npt @@ -86,85 +87,8 @@ def test_concentration_model_next_state_change(time,expected_next_state_change): assert c_model._next_state_change(time) == expected_next_state_change with pytest.raises(ValueError, match="Time larger than highest state change"): c_model._next_state_change(24.1) + with pytest.raises(ValueError, match=re.escape("Concentration cannot " + "be computed at a time larger than last state change " + "(parameters are not defined)")): + c_model.concentration(24.1) - -@dataclass(frozen=True) -class DummyVentilation(models.Ventilation): - # Dummy ventilation where air_exchange depends on time explicitly - - #: The interval in which the ventilation is operating. - active: models.Interval - - def air_exchange(self, room: models.Room, time: float) -> models._VectorisedFloat: - """ - Here we put an explicit function of time, hence breaking the rules - set in cara.models. For testing purposes only. - """ - if not self.active.triggered(time): - return 0. - return time*0.5 - - -@dataclass(frozen=True) -class DummyPopulation(models.Population): - # Dummy infected population where emission rate depends on time - # explicitly - virus: models.Virus - expiration: models.Expiration - - def emission_rate_when_present(self) -> models._VectorisedFloat: - return 50. - - def individual_emission_rate(self, time) -> models._VectorisedFloat: - """ - The emission rate of a single individual in the population. - Here we put an explicit function of time, hence breaking the rules - set in cara.models. For testing purposes only. - """ - - if not self.person_present(time): - return 0. - - return self.emission_rate_when_present()*time - - def emission_rate(self, time) -> models._VectorisedFloat: - """ - The emission rate of the entire population. - """ - return self.individual_emission_rate(time) * self.number - - -def test_concentration_model_constant_parameters(): - always = models.SpecificInterval(present_times=[(0,24)]) - c_model = models.ConcentrationModel( - models.Room(75), - DummyVentilation(always), - DummyPopulation( - number=1, - presence=always, - mask=models.Mask( - η_exhale=0.95, - η_leaks=0.15, - η_inhale=0.3, - ), - activity=models.Activity( - 0.51, - 0.75, - ), - virus=models.Virus( - halflife=1.1, - viral_load_in_sputum=1e9, - coefficient_of_infectivity=0.02, - ), - expiration=models.Expiration( - ejection_factor=(0.084, 0.009, 0.003, 0.002), - ), - ) - ) - times = [0.1, 10, 20, 24] - concentration_limits = np.array([c_model._concentration_limit(t) - for t in times]) - IVRRs = np.array([c_model.infectious_virus_removal_rate(t) - for t in times]) - assert np.all(concentration_limits==concentration_limits[-1]) - assert np.all(IVRRs==IVRRs[-1]) From 31a5954a4a19c877ec74f1805fb1f4a979d78720 Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Wed, 5 May 2021 21:03:36 +0200 Subject: [PATCH 12/13] Review actions on time handling. --- cara/models.py | 18 +++---- cara/tests/models/test_concentration_model.py | 52 +++++++++++-------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/cara/models.py b/cara/models.py index 8f2a8c42..9195ef76 100644 --- a/cara/models.py +++ b/cara/models.py @@ -644,7 +644,8 @@ class ConcentrationModel: k = (vg * 3600) / h return k + self.virus.decay_constant + self.ventilation.air_exchange( - self.room, time) + self.room, time + ) @cached() def _concentration_limit(self, time: float) -> _VectorisedFloat: @@ -689,7 +690,10 @@ class ConcentrationModel: for change_time in self.state_change_times(): if change_time >= time: return change_time - raise ValueError("Time larger than highest state change") + raise ValueError( + f"The requested time ({time}) is greater than last available " + f"state change time ({change_time})" + ) @cached() def concentration(self, time: float) -> _VectorisedFloat: @@ -705,13 +709,9 @@ class ConcentrationModel: if time == 0: return 0.0 - try: - IVRR = self.infectious_virus_removal_rate(self._next_state_change(time)) - concentration_limit = self._concentration_limit(self._next_state_change(time)) - except ValueError: - raise ValueError("Concentration cannot be computed at a time" - " larger than last state change (parameters" - " are not defined)") + next_state_change_time = self._next_state_change(time) + IVRR = self.infectious_virus_removal_rate(next_state_change_time) + concentration_limit = self._concentration_limit(next_state_change_time) t_last_state_change = self.last_state_change(time) concentration_at_last_state_change = self.concentration(t_last_state_change) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index 0fd81271..be58ddd3 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -1,8 +1,6 @@ -from dataclasses import dataclass import re import numpy as np -import numpy.testing as npt import pytest from cara import models @@ -62,33 +60,43 @@ def test_concentration_model_vectorisation(override_params): assert concentrations.shape == (2, ) -@pytest.mark.parametrize( - "time, expected_next_state_change", [ - [0, 0], - [1, 4], - [4, 4], - [24, 24], - ] -) -def test_concentration_model_next_state_change(time,expected_next_state_change): - always = models.PeriodicInterval(240, 240) - c_model = models.ConcentrationModel( +@pytest.fixture +def simple_conc_model(): + interesting_times = models.SpecificInterval(([0, 1], [1.1, 1.999], [2, 3]), ) + return models.ConcentrationModel( models.Room(75), - models.AirChange(always, 100), + models.AirChange(interesting_times, 100), models.InfectedPopulation( number=1, - presence=always, + presence=interesting_times, mask=models.Mask.types['Type I'], activity=models.Activity.types['Seated'], virus=models.Virus.types['SARS_CoV_2'], expiration=models.Expiration.types['Breathing'], ) ) - assert c_model._next_state_change(time) == expected_next_state_change - with pytest.raises(ValueError, match="Time larger than highest state change"): - c_model._next_state_change(24.1) - with pytest.raises(ValueError, match=re.escape("Concentration cannot " - "be computed at a time larger than last state change " - "(parameters are not defined)")): - c_model.concentration(24.1) + +@pytest.mark.parametrize( + "time, expected_next_state_change", [ + [0, 0], + [1, 1], + [1.1, 1.999], + [2, 3], + [3, 3], + ] +) +def test_next_state_change_time( + simple_conc_model: models.ConcentrationModel, + time, + expected_next_state_change, +): + simple_conc_model._next_state_change(time) == expected_next_state_change + + +def test_next_state_change_time_out_of_range(simple_conc_model: models.ConcentrationModel): + with pytest.raises( + ValueError, + match=re.escape("The requested time (3.1) is greater than last available state change time (3)") + ): + simple_conc_model._next_state_change(3.1) From 7d4284e5f9f8cc8e72b460ab1a238acf131462eb Mon Sep 17 00:00:00 2001 From: Nicolas Mounet Date: Thu, 6 May 2021 06:07:33 +0200 Subject: [PATCH 13/13] Correcting test_next_state_change_time --- cara/tests/models/test_concentration_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py index be58ddd3..5f42a6d0 100644 --- a/cara/tests/models/test_concentration_model.py +++ b/cara/tests/models/test_concentration_model.py @@ -81,8 +81,10 @@ def simple_conc_model(): "time, expected_next_state_change", [ [0, 0], [1, 1], - [1.1, 1.999], - [2, 3], + [1.1, 1.1], + [1.11, 1.999], + [2, 2], + [2.1, 3], [3, 3], ] ) @@ -91,7 +93,7 @@ def test_next_state_change_time( time, expected_next_state_change, ): - simple_conc_model._next_state_change(time) == expected_next_state_change + assert simple_conc_model._next_state_change(time) == expected_next_state_change def test_next_state_change_time_out_of_range(simple_conc_model: models.ConcentrationModel):