diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py index 2d65089b..fda15b3c 100644 --- a/cara/apps/calculator/model_generator.py +++ b/cara/apps/calculator/model_generator.py @@ -217,7 +217,7 @@ class FormData: return models.SpecificInterval(tuple(present_intervals)) -def model_from_form(form: FormData) -> models.Model: +def model_from_form(form: FormData) -> models.ExposureModel: # Initializes room with volume either given directly or as product of area and height if form.volume_type == 'room_volume': volume = form.room_volume @@ -250,19 +250,25 @@ def model_from_form(form: FormData) -> models.Model: exposed_occupants = form.total_people - infected_occupants # Initializes and returns a model with the attributes defined above - return models.Model( - room=room, - ventilation=form.ventilation(), - infected=models.InfectedPerson( - virus=virus, - presence=form.present_interval(), - mask=mask, - activity=infected_activity, - expiration=infected_expiration + return models.ExposureModel( + concentration_model=models.Model( + room=room, + ventilation=form.ventilation(), + infected=models.InfectedPopulation( + number=infected_occupants, + virus=virus, + presence=form.present_interval(), + mask=mask, + activity=infected_activity, + expiration=infected_expiration + ), ), - infected_occupants=infected_occupants, - exposed_occupants=exposed_occupants, - exposed_activity=exposed_activity + exposed=models.Population( + number=exposed_occupants, + presence=form.present_interval(), + activity=exposed_activity, + mask=mask, + ) ) diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 93d07d01..816b48fb 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -13,20 +13,19 @@ from cara import models from .model_generator import FormData -def calculate_report_data(model: models.Model): +def calculate_report_data(model: models.ExposureModel): resolution = 600 - # TODO: Have this for exposed not infected. - t_start = model.infected.presence.boundaries()[0][0] - t_end = model.infected.presence.boundaries()[-1][1] + t_start = model.exposed.presence.boundaries()[0][0] + t_end = model.exposed.presence.boundaries()[-1][1] times = list(np.linspace(t_start, t_end, resolution)) - concentrations = [model.concentration(time) for time in times] + concentrations = [model.concentration_model.concentration(time) for time in times] highest_const = max(concentrations) prob = model.infection_probability() - er = model.infected.emission_rate(0.1) - exposed_occupants = model.exposed_occupants - r0 = prob * exposed_occupants / 100 + er = model.concentration_model.infected.emission_rate(0.1) + exposed_occupants = model.exposed.number + r0 = model.reproduction_rate() return { "times": times, @@ -78,7 +77,7 @@ def minutes_to_time(minutes: int) -> str: return f"{hour_string}:{minute_string}" -def build_report(model: models.Model, form: FormData): +def build_report(model: models.ExposureModel, form: FormData): now = datetime.now() time = now.strftime("%d/%m/%Y %H:%M:%S") request = {"the": "form", "request": "data"} diff --git a/cara/apps/calculator/templates/report.html.j2 b/cara/apps/calculator/templates/report.html.j2 index aab7f3dc..2d00e61b 100644 --- a/cara/apps/calculator/templates/report.html.j2 +++ b/cara/apps/calculator/templates/report.html.j2 @@ -20,7 +20,7 @@
Input data:
Room Volume: {{ model.room.volume }} m³
Room Volume: {{ model.concentration_model.room.volume }} m³
Ventilation data:
diff --git a/cara/apps/expert.py b/cara/apps/expert.py index c02c56df..9fb47963 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -105,30 +105,29 @@ class WidgetView: pass def update(self): - model = self.model_state.dcs_instance() + model: models.ExposureModel = self.model_state.dcs_instance() for plot in self.plots: - plot.update(model) + plot.update(model.concentration_model) self.out.clear_output() with self.out: P = model.infection_probability() - print(f'Emission rate (quanta/hr): {model.infected.emission_rate(0)}') + print(f'Emission rate (quanta/hr): {model.concentration_model.infected.emission_rate(0.1)}') print(f'Probability of infection: {np.round(P, 0)}%') - print(f'Number of exposed: {model.exposed_occupants}') - R0 = np.round(P / 100 * model.exposed_occupants, 1) + print(f'Number of exposed: {model.exposed.number}') + R0 = np.round(model.reproduction_rate(), 1) print(f'Number of expected new cases (R0): {R0}') - def _build_widget(self, node): - self.widget.children += (self._build_room(node.room),) - self.widget.children += (self._build_ventilation(node.ventilation),) - self.widget.children += (self._build_infected(node.infected),) + self.widget.children += (self._build_room(node.concentration_model.room),) + self.widget.children += (self._build_ventilation(node.concentration_model.ventilation),) + self.widget.children += (self._build_infected(node.concentration_model.infected),) self.widget.children += (self._build_exposed(node),) def _build_exposed(self, node): return collapsible( - [self._build_activity(node.exposed_activity)], + [self._build_activity(node.exposed.activity)], title="Exposed" ) @@ -284,24 +283,30 @@ class WidgetView: return self.widget -baseline_model = models.Model( - room=models.Room(volume=75), - ventilation=models.WindowOpening( - active=models.PeriodicInterval(period=120, duration=120), - inside_temp=models.PiecewiseConstant((0,24),(293,)), - outside_temp=models.PiecewiseConstant((0,24),(283,)), - cd_b=0.6, window_height=1.6, opening_length=0.6, +baseline_model = models.ExposureModel( + concentration_model=models.Model( + room=models.Room(volume=75), + ventilation=models.WindowOpening( + active=models.PeriodicInterval(period=120, duration=120), + inside_temp=models.PiecewiseConstant((0,24),(293,)), + outside_temp=models.PiecewiseConstant((0,24),(283,)), + cd_b=0.6, window_height=1.6, opening_length=0.6, + ), + infected=models.InfectedPopulation( + number=1, + virus=models.Virus.types['SARS_CoV_2'], + 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'], + ), ), - infected=models.InfectedPerson( - virus=models.Virus.types['SARS_CoV_2'], + exposed=models.Population( + number=10, 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'], + mask=models.Mask.types['No mask'], ), - infected_occupants=1, - exposed_occupants=10, - exposed_activity=models.Activity.types['Light exercise'], ) @@ -330,13 +335,13 @@ class CARAStateBuilder(state.StateBuilder): class ExpertApplication: def __init__(self): self.model_state = state.DataclassInstanceState( - models.Model, + models.ExposureModel, state_builder=CARAStateBuilder(), ) self.model_state.dcs_update_from(baseline_model) # For the time-being, we have to initialise the select states. Careful # as values might not correspond to what the baseline model says. - self.model_state.infected.mask.dcs_select('No mask') + self.model_state.concentration_model.infected.mask.dcs_select('No mask') self.view = WidgetView(self.model_state) diff --git a/cara/models.py b/cara/models.py index d765b548..843e5571 100644 --- a/cara/models.py +++ b/cara/models.py @@ -419,6 +419,15 @@ class InfectedPopulation(Population): #: The type of expiration that is being emitted whilst doing the activity. expiration: Expiration + def emission_rate_if_present(self): + """ + The emission rate if the infected population is present. + + Note that the rate is not currently time-dependent. + + """ + + def individual_emission_rate(self, time) -> float: """ The emission rate of a single individual in the population. @@ -426,9 +435,15 @@ class InfectedPopulation(Population): """ # Note: The original model avoids time dependence on the emission rate # at the cost of implementing a piecewise (on time) concentration function. + if not self.person_present(time): return 0 + # Note: It is essential that the value of the emission rate is not + # itself a function of time. Any change in rate must be accompanied + # with a declaration of state change time, as is the case for things + # like Ventilation. + # Emission Rate (infectious quantum / h) aerosols = self.expiration.aerosols(self.mask) if np.isinf(aerosols): diff --git a/cara/tests/apps/test_expert_app.py b/cara/tests/apps/test_expert_app.py index 26851489..af441e99 100644 --- a/cara/tests/apps/test_expert_app.py +++ b/cara/tests/apps/test_expert_app.py @@ -6,4 +6,4 @@ def test_app(): # do anything fancy to verify how it looks etc., we leave that for manual # testing. expert_app = cara.apps.ExpertApplication() - assert expert_app.model_state.room.volume == 75 + assert expert_app.model_state.concentration_model.room.volume == 75