Skip to content

Commit

Permalink
Merge branch 'feature/piecewise_presence' into 'master'
Browse files Browse the repository at this point in the history
Dynamic Number of Occupants (Infected)

Closes #297

See merge request caimira/caimira!433
  • Loading branch information
nmounet committed May 3, 2023
2 parents 7ab8cd0 + dea166b commit 181182f
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 115 deletions.
2 changes: 1 addition & 1 deletion caimira/apps/calculator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CAiMIRA version (found at ``caimira.__version__``).
__version__ = "4.8"
__version__ = "4.9"


class BaseRequestHandler(RequestHandler):
Expand Down
17 changes: 9 additions & 8 deletions caimira/apps/calculator/report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@


def model_start_end(model: models.ExposureModel):
t_start = min(model.exposed.presence.boundaries()[0][0],
model.concentration_model.infected.presence.boundaries()[0][0])
t_end = max(model.exposed.presence.boundaries()[-1][1],
model.concentration_model.infected.presence.boundaries()[-1][1])
t_start = min(model.exposed.presence_interval().boundaries()[0][0],
model.concentration_model.infected.presence_interval().boundaries()[0][0])
t_end = max(model.exposed.presence_interval().boundaries()[-1][1],
model.concentration_model.infected.presence_interval().boundaries()[-1][1])
return t_start, t_end


Expand Down Expand Up @@ -137,15 +137,16 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
prob = np.array(model.infection_probability())
prob_dist_count, prob_dist_bins = np.histogram(prob/100, bins=100, density=True)
prob_probabilistic_exposure = np.array(model.total_probability_rule()).mean()
er = np.array(model.concentration_model.infected.emission_rate_when_present()).mean()
er = np.array(model.concentration_model.infected.emission_rate_per_person_when_present()).mean()
exposed_occupants = model.exposed.number
expected_new_cases = np.array(model.expected_new_cases()).mean()
uncertainties_plot_src = img2base64(_figure2bytes(uncertainties_plot(model))) if form.conditional_probability_plot else None
exposed_presence_intervals = [list(interval) for interval in model.exposed.presence_interval().boundaries()]

return {
"model_repr": repr(model),
"times": list(times),
"exposed_presence_intervals": [list(interval) for interval in model.exposed.presence.boundaries()],
"exposed_presence_intervals": exposed_presence_intervals,
"short_range_intervals": short_range_intervals,
"short_range_expirations": short_range_expirations,
"concentrations": concentrations,
Expand All @@ -154,12 +155,12 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing
"cumulative_doses": list(cumulative_doses),
"long_range_cumulative_doses": list(long_range_cumulative_doses),
"prob_inf": prob.mean(),
"prob_inf_sd": np.std(prob),
"prob_inf_sd": prob.std(),
"prob_dist": list(prob),
"prob_hist_count": list(prob_dist_count),
"prob_hist_bins": list(prob_dist_bins),
"prob_probabilistic_exposure": prob_probabilistic_exposure,
"emission_rate": er,
"emission_rate_per_person": er,
"exposed_occupants": exposed_occupants,
"expected_new_cases": expected_new_cases,
"uncertainties_plot_src": uncertainties_plot_src,
Expand Down
56 changes: 30 additions & 26 deletions caimira/apps/expert.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,9 @@ def update(self, model: models.ExposureModel):

def update_plot(self, model: models.ExposureModel):
resolution = 600
ts = np.linspace(sorted(model.concentration_model.infected.presence.transition_times())[0],
sorted(model.concentration_model.infected.presence.transition_times())[-1], resolution)
infected_presence = model.concentration_model.infected.presence_interval()
ts = np.linspace(sorted(infected_presence.transition_times())[0],
sorted(infected_presence.transition_times())[-1], resolution)
concentration = [model.concentration(t) for t in ts]

cumulative_doses = np.cumsum([
Expand All @@ -164,16 +165,18 @@ def update_plot(self, model: models.ExposureModel):
self.ax.ignore_existing_data_limits = False
self.concentration_line.set_data(ts, concentration)

exposed_presence = model.exposed.presence_interval()

if self.concentration_area is None:
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
where = ((model.exposed.presence.boundaries()[0][0] < ts) & (ts < model.exposed.presence.boundaries()[0][1]) |
(model.exposed.presence.boundaries()[1][0] < ts) & (ts < model.exposed.presence.boundaries()[1][1])))
where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) |
(exposed_presence.boundaries()[1][0] < ts) & (ts < exposed_presence.boundaries()[1][1])))

else:
self.concentration_area.remove()
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
where = ((model.exposed.presence.boundaries()[0][0] < ts) & (ts < model.exposed.presence.boundaries()[0][1]) |
(model.exposed.presence.boundaries()[1][0] < ts) & (ts < model.exposed.presence.boundaries()[1][1])))
where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) |
(exposed_presence.boundaries()[1][0] < ts) & (ts < exposed_presence.boundaries()[1][1])))

if self.cumulative_line is None:
[self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', linestyle='dotted')
Expand All @@ -187,8 +190,8 @@ def update_plot(self, model: models.ExposureModel):
cumulative_top = max(cumulative_doses)
self.ax2.set_ylim(bottom=0., top=cumulative_top)

self.ax.set_xlim(left = min(min(model.concentration_model.infected.presence.boundaries()[0]), min(model.exposed.presence.boundaries()[0])),
right = max(max(model.concentration_model.infected.presence.boundaries()[1]), max(model.exposed.presence.boundaries()[1])))
self.ax.set_xlim(left = min(min(infected_presence.boundaries()[0]), min(exposed_presence.boundaries()[0])),
right = max(max(infected_presence.boundaries()[1]), max(exposed_presence.boundaries()[1])))

figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'),
mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'),
Expand All @@ -200,7 +203,7 @@ def update_plot(self, model: models.ExposureModel):
def update_textual_result(self, model: models.ExposureModel):
lines = []
P = np.array(model.infection_probability()).mean()
lines.append(f'Emission rate (virus/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}')
lines.append(f'Emission rate per infected person (virus/hr): {np.round(model.concentration_model.infected.emission_rate_per_person_when_present(),0)}')
lines.append(f'Probability of infection: {np.round(P, 0)}%')

lines.append(f'Number of exposed: {model.exposed.number}')
Expand Down Expand Up @@ -649,21 +652,10 @@ def on_exposed_number_change(change):
number.observe(on_exposed_number_change, names=['value'])

return widgets.HBox([widgets.Label('Number of exposed people in the room '), number], layout=widgets.Layout(justify_content='space-between'))

def generate_presence_widget(self, min, max, node):
options = list(pd.date_range(min, max, freq="1min").strftime('%H:%M'))
start_hour = float(node[0])
end_hour = float(node[1])
start_hour_datetime = datetime.time(hour = int(start_hour), minute=int(start_hour%1*60))
end_hour_datetime = datetime.time(hour = int(end_hour), minute=int(end_hour%1*60))
return widgets.SelectionRangeSlider(
options=options,
index=(options.index(str(start_hour_datetime)[:-3]), options.index(str(end_hour_datetime)[:-3])),
)

def _build_exposed_presence(self, node):
presence_start = self.generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0])
presence_finish = self.generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1])
presence_start = generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0])
presence_finish = generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1])

def on_presence_start_change(change):
new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']])
Expand Down Expand Up @@ -719,8 +711,8 @@ def on_viral_load_change(change):
return widgets.HBox([widgets.Label("Viral load (copies/ml)"), viral_load_in_sputum], layout=widgets.Layout(justify_content='space-between'))

def _build_infected_presence(self, node, ventilation_node):
presence_start = self.generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0])
presence_finish = self.generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1])
presence_start = generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0])
presence_finish = generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1])

def on_presence_start_change(change):
new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']])
Expand Down Expand Up @@ -1119,6 +1111,18 @@ def models_start_end(models: typing.Sequence[models.ExposureModel]) -> typing.Tu
Returns the earliest start and latest end time of a collection of ConcentrationModel objects
"""
infected_start = min(model.concentration_model.infected.presence.boundaries()[0][0] for model in models)
infected_finish = min(model.concentration_model.infected.presence.boundaries()[-1][1] for model in models)
infected_start = min(model.concentration_model.infected.presence_interval().boundaries()[0][0] for model in models)
infected_finish = min(model.concentration_model.infected.presence_interval().boundaries()[-1][1] for model in models)
return infected_start, infected_finish


def generate_presence_widget(min, max, node):
options = list(pd.date_range(min, max, freq="1min").strftime('%H:%M'))
start_hour = float(node[0])
end_hour = float(node[1])
start_hour_datetime = datetime.time(hour = int(start_hour), minute=int(start_hour%1*60))
end_hour_datetime = datetime.time(hour = int(end_hour), minute=int(end_hour%1*60))
return widgets.SelectionRangeSlider(
options=options,
index=(options.index(str(start_hour_datetime)[:-3]), options.index(str(end_hour_datetime)[:-3])),
)
45 changes: 27 additions & 18 deletions caimira/apps/expert_co2.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import matplotlib.figure
import matplotlib.lines as mlines
import matplotlib.patches as patches
from .expert import collapsible, ipympl_canvas, WidgetGroup, CAIMIRAStateBuilder
from .expert import generate_presence_widget, collapsible, ipympl_canvas, WidgetGroup, CAIMIRAStateBuilder


baseline_model = models.CO2ConcentrationModel(
Expand Down Expand Up @@ -86,8 +86,8 @@ def update(self, model: models.CO2ConcentrationModel):

def update_plot(self, model: models.CO2ConcentrationModel):
resolution = 600
ts = np.linspace(sorted(model.CO2_emitters.presence.transition_times())[0],
sorted(model.CO2_emitters.presence.transition_times())[-1], resolution)
ts = np.linspace(sorted(model.CO2_emitters.presence_interval().transition_times())[0],
sorted(model.CO2_emitters.presence_interval().transition_times())[-1], resolution)
concentration = [model.concentration(t) for t in ts]

if self.concentration_line is None:
Expand All @@ -99,19 +99,19 @@ def update_plot(self, model: models.CO2ConcentrationModel):

if self.concentration_area is None:
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
where = ((model.CO2_emitters.presence.boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[0][1]) |
(model.CO2_emitters.presence.boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[1][1])))
where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) |
(model.CO2_emitters.presence_interval().boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[1][1])))

else:
self.concentration_area.remove()
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff",
where = ((model.CO2_emitters.presence.boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[0][1]) |
(model.CO2_emitters.presence.boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence.boundaries()[1][1])))
where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) |
(model.CO2_emitters.presence_interval().boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[1][1])))

concentration_top = max(np.array(concentration))
self.ax.set_ylim(bottom=model.CO2_atmosphere_concentration * 0.9, top=concentration_top*1.1)
self.ax.set_xlim(left = min(model.CO2_emitters.presence.boundaries()[0])*0.95,
right = max(model.CO2_emitters.presence.boundaries()[1])*1.05)
self.ax.set_xlim(left = min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95,
right = max(model.CO2_emitters.presence_interval().boundaries()[1])*1.05)

figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='CO₂ concentration'),
mlines.Line2D([], [], color='salmon', markersize=15, label='Insufficient level', linestyle='--'),
Expand All @@ -122,7 +122,10 @@ def update_plot(self, model: models.CO2ConcentrationModel):
self.ax.set_ylim(top=concentration_top*1.1)
else:
self.ax.set_ylim(top=1550)
self.ax.hlines([800, 1500], xmin=min(model.CO2_emitters.presence.boundaries()[0])*0.95, xmax=max(model.CO2_emitters.presence.boundaries()[1])*1.05, colors=['limegreen', 'salmon'], linestyles='dashed')
self.ax.hlines([800, 1500], xmin=min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95,
xmax=max(model.CO2_emitters.presence_interval().boundaries()[1])*1.05,
colors=['limegreen', 'salmon'],
linestyles='dashed')
self.figure.canvas.draw()


Expand Down Expand Up @@ -368,20 +371,26 @@ def on_population_number_change(change):
return widgets.HBox([widgets.Label('Number of people in the room '), number], layout=widgets.Layout(justify_content='space-between'))

def _build_population_presence(self, node, ventilation_node):
presence_start = widgets.FloatRangeSlider(value = node.present_times[0], min = 8., max=13., step=0.1)
presence_finish = widgets.FloatRangeSlider(value = node.present_times[1], min = 13., max=18., step=0.1)
presence_start = generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0])
presence_finish = generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1])

def on_presence_start_change(change):
ventilation_node.active.start = change['new'][0] - ventilation_node.active.duration / 60
node.present_times = (change['new'], presence_finish.value)
new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']])
ventilation_node.active.start = new_value[0] - ventilation_node.active.duration / 60
node.present_times = (new_value, node.present_times[1])

def on_presence_finish_change(change):
node.present_times = (presence_start.value, change['new'])
new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']])
node.present_times = (node.present_times[0], new_value)

presence_start.observe(on_presence_start_change, names=['value'])
presence_finish.observe(on_presence_finish_change, names=['value'])

return widgets.HBox([widgets.Label('Population presence'), presence_start, presence_finish], layout = widgets.Layout(justify_content='space-between'))
return widgets.VBox([
widgets.Label('Exposed presence:'),
widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]),
widgets.HBox([widgets.Label('Afternoon:', layout=widgets.Layout(width='15%')), presence_finish])
])

def present(self):
return self.widget
Expand Down Expand Up @@ -782,6 +791,6 @@ def models_start_end(models: typing.Sequence[models.CO2ConcentrationModel]) -> t
Returns the earliest start and latest end time of a collection of v objects
"""
emitters_start = min(model.CO2_emitters.presence.boundaries()[0][0] for model in models)
emitters_finish = min(model.CO2_emitters.presence.boundaries()[-1][1] for model in models)
emitters_start = min(model.CO2_emitters.presence_interval().boundaries()[0][0] for model in models)
emitters_finish = min(model.CO2_emitters.presence_interval().boundaries()[-1][1] for model in models)
return emitters_start, emitters_finish
Loading

0 comments on commit 181182f

Please sign in to comment.