From 332f2414ad16591b97b692d111e7ca82756d040e Mon Sep 17 00:00:00 2001 From: Phil Elson Date: Tue, 27 Oct 2020 14:47:45 +0100 Subject: [PATCH] Abstract the presence time as an interval. --- cara/apps.py | 6 +-- cara/models.py | 80 ++++++++++++----------------- cara/tests/test_known_quantities.py | 4 +- 3 files changed, 39 insertions(+), 51 deletions(-) diff --git a/cara/apps.py b/cara/apps.py index 3caa3fb1..dbca65fb 100644 --- a/cara/apps.py +++ b/cara/apps.py @@ -34,8 +34,8 @@ class ConcentrationFigure: self.line = None def update(self, model: models.Model): - resolution = 500 - ts = np.linspace(0, 9.09, resolution) + resolution = 600 + ts = np.linspace(0, 10, resolution) concentration = [model.concentration(t) for t in ts] if self.line is None: [self.line] = self.ax.plot(ts, concentration) @@ -298,7 +298,7 @@ baseline_model = models.Model( ), infected=models.InfectedPerson( virus=models.Virus.types['SARS_CoV_2'], - present_times=((0, 4), (5, 8)), + presence=models.SpecificInterval(((0, 4), (5, 8))), mask=models.Mask.types['No mask'], activity=models.Activity.types['Light exercise'], expiration=models.Expiration.types['Unmodulated Vocalization'], diff --git a/cara/models.py b/cara/models.py index 643d33ea..07b7f382 100644 --- a/cara/models.py +++ b/cara/models.py @@ -21,6 +21,11 @@ class Interval: The "thing" may be when an action is taken, such as opening a window, or entering a room. + Note that all intervals are closed at the start, and open at the end. So a + simple start, stop interval follows:: + + start < t <= end + """ def triggered(self, time: float) -> bool: """Whether the given time falls inside this interval.""" @@ -31,6 +36,27 @@ class Interval: return set() +@dataclass(frozen=True) +class SpecificInterval(Interval): + #: A sequence of times (start, stop), in hours, that the infected person + #: is present. The flattened list of times must be strictly monotonically + #: increasing. + present_times: typing.Tuple[typing.Tuple[float, float], ...] + + def triggered(self, time: float) -> bool: + for start, end in self.present_times: + if start < time <= end: + return True + return False + + def boundaries(self) -> typing.Set[float]: + state_changes = set() + for start, end in self.present_times: + state_changes.add(start) + state_changes.add(end) + return state_changes + + @dataclass(frozen=True) class PeriodicInterval(Interval): #: How often does the interval occur (minutes). @@ -53,16 +79,6 @@ class PeriodicInterval(Interval): return True return False - if self.duration >= self.period: - return True - if self.duration == 0: - return False - - period = self.period / 60. - duration = self.duration / 60. - t = time % period - return 0 <= t < duration - def boundaries(self) -> typing.Set[float]: state_changes = set() for start, end in self.states(): @@ -70,19 +86,6 @@ class PeriodicInterval(Interval): state_changes.add(end) return state_changes - if self.duration >= self.period: - return set() - if self.duration == 0: - return set() - state_changes = set() - period_h = self.period / 60 - duration_h = self.duration / 60 - # Take as many steps as we need to get to a full day. - for i in np.arange(0, 24, period_h): - state_changes.add(i) - state_changes.add(i + duration_h) - return state_changes - @dataclass(frozen=True) class Ventilation: @@ -273,30 +276,14 @@ Activity.types = { @dataclass(frozen=True) class InfectedPerson: virus: Virus - #: A sequence of times (start, stop), in hours, that the infected person - #: is present. The flattened list of times must be strictly monotonically - #: increasing. - present_times: typing.Tuple[typing.Tuple[float, float], ...] + #: The times in which the person is in the room. + presence: Interval mask: Mask activity: Activity expiration: Expiration def person_present(self, time): - for start, end in self.present_times: - if start < time <= end: - return True - return False - - # def start_end_of_presence(self, time) -> typing.Tuple[float, float]: - # """ - # Find the most recent start (and associated) end-time (even if the - # given time is after the end-point) given a time. - # - # """ - # for start, end in self.present_times[::-1]: - # if time > start: - # return start, end - # return start, end + return self.presence.triggered(time) @functools.lru_cache() def emission_rate(self, time) -> float: @@ -350,9 +337,10 @@ class Model: """ state_change_times = set() - for start, end in self.infected.present_times: - state_change_times.add(start) - state_change_times.add(end) + # for start, end in self.infected.present_times: + # state_change_times.add(start) + # state_change_times.add(end) + state_change_times.update(self.infected.presence.boundaries()) state_change_times.update(self.ventilation.times_of_state_change()) return sorted(state_change_times) @@ -394,7 +382,7 @@ class Model: return np.trapz([fn(v) for v in values], values) # TODO: Have this for exposed not infected. - for start, stop in self.infected.present_times: + for start, stop in self.infected.presence.present_times: exposure += (integrate(self.concentration, start, stop)) inf_aero = ( diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py index 82393d4c..5a11e4ae 100644 --- a/cara/tests/test_known_quantities.py +++ b/cara/tests/test_known_quantities.py @@ -34,7 +34,7 @@ def baseline_model(): ), infected=models.InfectedPerson( virus=models.Virus.types['SARS_CoV_2'], - present_times=((0, 4), (5, 8)), + presence=models.SpecificInterval(((0, 4), (5, 8))), mask=models.Mask.types['No mask'], activity=models.Activity.types['Light exercise'], expiration=models.Expiration.types['Unmodulated Vocalization'], @@ -97,7 +97,7 @@ def build_model(interval_duration): ), infected=models.InfectedPerson( virus=models.Virus.types['SARS_CoV_2'], - present_times=((0, 4), (5, 8)), + presence=models.SpecificInterval(((0, 4), (5, 8))), mask=models.Mask.types['No mask'], activity=models.Activity.types['Light exercise'], expiration=models.Expiration.types['Unmodulated Vocalization'],