import mplcursors, resized the figure, added a shading for the exposed person presence, added a way to custom the virus properties (viral load, infectious dose, trasmissibility factor),
This commit is contained in:
parent
699ef53bce
commit
9147b73a90
1 changed files with 118 additions and 28 deletions
|
|
@ -7,10 +7,10 @@ import ipywidgets as widgets
|
|||
import matplotlib
|
||||
import matplotlib.figure
|
||||
import numpy as np
|
||||
import mplcursors
|
||||
from matplotlib import pyplot as plt
|
||||
from numpy import object_
|
||||
from cara import data, models, state
|
||||
|
||||
from cara import data, models, state
|
||||
|
||||
def collapsible(widgets_to_collapse: typing.List, title: str, start_collapsed=False):
|
||||
collapsed = widgets.Accordion([widgets.VBox(widgets_to_collapse)])
|
||||
|
|
@ -107,12 +107,14 @@ def ipympl_canvas(figure):
|
|||
|
||||
class ExposureModelResult(View):
|
||||
def __init__(self):
|
||||
self.figure = matplotlib.figure.Figure(figsize=(9, 6))
|
||||
self.figure = matplotlib.figure.Figure(figsize=(9, 5))
|
||||
ipympl_canvas(self.figure)
|
||||
self.html_output = widgets.HTML()
|
||||
self.ax = self.figure.add_subplot(2, 1, 2)
|
||||
self.ax = self.figure.add_subplot(1, 1, 1)
|
||||
self.figure.subplots_adjust(left=8, right=9)
|
||||
self.ax2 = self.ax.twinx()
|
||||
self.concentration_line = None
|
||||
self.concentration_area = None
|
||||
self.cumulative_line = None
|
||||
|
||||
@property
|
||||
|
|
@ -139,7 +141,6 @@ class ExposureModelResult(View):
|
|||
|
||||
if self.concentration_line is None:
|
||||
[self.concentration_line] = self.ax.plot(ts, concentration, color='#3530fe', label='Concentration')
|
||||
|
||||
ax = self.ax
|
||||
|
||||
#ax.text(0.5, 0.9, 'Without masks & window open', transform=ax.transAxes, ha='center')
|
||||
|
|
@ -150,15 +151,29 @@ class ExposureModelResult(View):
|
|||
ax.set_xlabel('Time (hours)')
|
||||
ax.set_ylabel('Mean concentration ($virions/m^{3}$)')
|
||||
ax.set_title('Concentration of virions and Cumulative dose')
|
||||
|
||||
#cursor = SnaptoCursor(self.ax, ts, concentration)
|
||||
|
||||
else:
|
||||
self.ax.ignore_existing_data_limits = True
|
||||
self.ax.ignore_existing_data_limits = False
|
||||
self.concentration_line.set_data(ts, concentration)
|
||||
mplcursors.cursor(self.ax, hover=True)
|
||||
|
||||
if self.concentration_area is None:
|
||||
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", label="Exposed person presence",
|
||||
where = ((model.exposed.presence.present_times[0][0] < ts) & (ts < model.exposed.presence.present_times[0][1]) |
|
||||
(model.exposed.presence.present_times[1][0] < ts) & (ts < model.exposed.presence.present_times[1][1])))
|
||||
|
||||
else:
|
||||
self.concentration_area.remove()
|
||||
self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", label="Exposed person presence",
|
||||
where = ((model.exposed.presence.present_times[0][0] < ts) & (ts < model.exposed.presence.present_times[0][1]) |
|
||||
(model.exposed.presence.present_times[1][0] < ts) & (ts < model.exposed.presence.present_times[1][1])))
|
||||
|
||||
if self.cumulative_line is None:
|
||||
[self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', label='Cumulative dose', linestyle='dotted')
|
||||
|
||||
ax2 = self.ax2
|
||||
|
||||
ax2.spines['left'].set_visible(False)
|
||||
ax2.spines['top'].set_visible(False)
|
||||
|
||||
|
|
@ -166,20 +181,25 @@ class ExposureModelResult(View):
|
|||
ax2.spines['right'].set_linestyle((0,(1,4)))
|
||||
|
||||
else:
|
||||
self.ax2.ignore_existing_data_limits = True
|
||||
self.ax2.ignore_existing_data_limits = False
|
||||
self.cumulative_line.set_data(ts[:-1], cumulative_doses)
|
||||
|
||||
# Update the top limit based on the concentration if it exceeds 5
|
||||
# (rare but possible).
|
||||
concentration_top = max([3, max(concentration)])
|
||||
concentration_top = max([1e-5, max(concentration)])
|
||||
self.ax.set_ylim(bottom=0., top=concentration_top)
|
||||
cumulative_top = max([3, max(cumulative_doses)])
|
||||
cumulative_top = max([1e-5, max(cumulative_doses)])
|
||||
self.ax2.set_ylim(bottom=0., top=cumulative_top)
|
||||
|
||||
self.ax.legend(bbox_to_anchor=(1.4, 1.15), frameon=True)
|
||||
self.ax2.legend(bbox_to_anchor=(1.433, 1), frameon=True)
|
||||
self.ax.set_xlim(left = min(model.concentration_model.infected.presence.present_times[0]), right = max(model.concentration_model.infected.presence.present_times[1]))
|
||||
|
||||
legend = self.ax.legend(bbox_to_anchor=(1.15, 1), frameon=False)
|
||||
self.ax2.legend(bbox_to_anchor=(1.15, 0.89), frameon=False)
|
||||
#sself.marker=plt.connect('motion_notify_event', mouse_move)
|
||||
|
||||
self.figure.canvas.draw()
|
||||
self.figure.tight_layout()
|
||||
self.figure.tight_layout()
|
||||
return legend
|
||||
|
||||
def update_textual_result(self, model: models.ExposureModel):
|
||||
lines = []
|
||||
|
|
@ -258,7 +278,7 @@ class ModelWidgets(View):
|
|||
def _build_widget(self, node):
|
||||
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_infected(node.concentration_model.infected, node.concentration_model.ventilation),)
|
||||
self.widget.children += (self._build_exposed(node),)
|
||||
self.widget.children += (self._build_infectivity(node.concentration_model.infected),)
|
||||
|
||||
|
|
@ -267,18 +287,21 @@ class ModelWidgets(View):
|
|||
self._build_exposed_number(node.exposed),
|
||||
self._build_mask(node.exposed.mask),
|
||||
self._build_activity(node.exposed.activity),
|
||||
self._build_exposed_presence(node.exposed.presence)
|
||||
])], title="Exposed")
|
||||
|
||||
def _build_infected(self, node):
|
||||
def _build_infected(self, node, ventilation_node):
|
||||
return collapsible([widgets.VBox([
|
||||
self._build_infected_number(node),
|
||||
self._build_mask(node.mask),
|
||||
self._build_activity(node.activity),
|
||||
self._build_expiration(node.expiration),
|
||||
self._build_viral_load(node.virus),
|
||||
self._build_infected_presence(node.presence, ventilation_node.active)
|
||||
])], title="Infected")
|
||||
|
||||
def _build_room_volume(self, node):
|
||||
room_volume = widgets.FloatSlider(value=node.volume, min=10, max=500
|
||||
room_volume = widgets.IntText(value=node.volume, min=10, max=500
|
||||
, step=5)
|
||||
|
||||
def on_value_change(change):
|
||||
|
|
@ -291,8 +314,8 @@ class ModelWidgets(View):
|
|||
|
||||
|
||||
def _build_room_area(self, node):
|
||||
room_surface = widgets.FloatSlider(value=25, min=1, max=200, step=10)
|
||||
room_ceiling_height = widgets.FloatSlider(value=3, min=1, max=20, step=1)
|
||||
room_surface = widgets.IntText(value=25, min=1, max=200, step=10)
|
||||
room_ceiling_height = widgets.IntText(value=3, min=1, max=20, step=1)
|
||||
displayed_volume=widgets.Label('1')
|
||||
|
||||
def room_surface_change(change):
|
||||
|
|
@ -423,10 +446,9 @@ class ModelWidgets(View):
|
|||
window_w.observe(lambda event: toggle_window(event['new']), 'value')
|
||||
toggle_window(window_w.value)
|
||||
|
||||
number_of_windows= widgets.IntSlider(value= 1, min= 0, max= 5, step=1)
|
||||
number_of_windows= widgets.IntText(value= 1, min= 0, max= 5, step=1)
|
||||
period = widgets.IntSlider(value=node.active.period, min=0, max=240)
|
||||
interval = widgets.IntSlider(value=node.active.duration, min=0, max=240)
|
||||
inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.)
|
||||
opening_length = widgets.FloatSlider(value=node.opening_length, min=0, max=3, step=0.1)
|
||||
window_height = widgets.FloatSlider(value=node.window_height, min=0, max=3, step=0.1)
|
||||
|
||||
|
|
@ -615,6 +637,21 @@ class ModelWidgets(View):
|
|||
|
||||
return widgets.HBox([widgets.Label('Number of exposed people in the room '), number], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_exposed_presence(self, 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)
|
||||
|
||||
def on_presence_start_change(change):
|
||||
node.present_times = (change['new'], presence_finish.value)
|
||||
|
||||
def on_presence_finish_change(change):
|
||||
node.present_times = (presence_start.value, change['new'])
|
||||
|
||||
presence_start.observe(on_presence_start_change, names=['value'])
|
||||
presence_finish.observe(on_presence_finish_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Exposed presence'), presence_start, presence_finish], layout = widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_infected_number(self, node):
|
||||
number = widgets.IntSlider(value=node.number, min=1, max=200, step=1)
|
||||
|
||||
|
|
@ -638,7 +675,36 @@ class ModelWidgets(View):
|
|||
expiration_choice.observe(on_expiration_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label("Expiration"), expiration_choice], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_viral_load(self, node):
|
||||
|
||||
viral_load_in_sputum = widgets.IntText(value=node.viral_load_in_sputum, PlaceHolder='1e9')
|
||||
|
||||
def viral_load_change(change):
|
||||
node.viral_load_in_sputum = change['new']
|
||||
|
||||
viral_load_in_sputum.observe(viral_load_change, names=['value'])
|
||||
|
||||
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 = 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)
|
||||
#node.present_times = ((presence_start), (presence_stop))
|
||||
def on_presence_start_change(change):
|
||||
node.present_times = (change['new'], presence_finish.value)
|
||||
|
||||
ventilation_node.start = change['new'][0]
|
||||
|
||||
def on_presence_finish_change(change):
|
||||
node.present_times = (presence_start.value, change['new'])
|
||||
|
||||
presence_start.observe(on_presence_start_change, names=['value'])
|
||||
presence_finish.observe(on_presence_finish_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label('Infected presence'), presence_start, presence_finish], layout = widgets.Layout(justify_content='space-between'))
|
||||
|
||||
def _build_ventilation(
|
||||
self,
|
||||
node: typing.Union[
|
||||
|
|
@ -707,23 +773,47 @@ class ModelWidgets(View):
|
|||
if virus == virus_:
|
||||
break
|
||||
virus_choice = widgets.Dropdown(options=list(models.Virus.types.keys()), value=name)
|
||||
transmissibility_factor = widgets.FloatSlider(value=node.transmissibility_factor, min=0, max=1, step=0.1)
|
||||
infectious_dose = widgets.FloatText(value=node.infectious_dose, placeholder='50', disabled=False)
|
||||
|
||||
def on_virus_change(change):
|
||||
node.dcs_select(change['new'])
|
||||
virus = models.Virus.types[change['new']]
|
||||
node.dcs_update_from(virus)
|
||||
transmissibility_factor.value = virus.transmissibility_factor
|
||||
infectious_dose.value = virus.infectious_dose
|
||||
|
||||
def transmissibility_change(change):
|
||||
virus = models.SARSCoV2(viral_load_in_sputum=ModelWidgets._build_viral_load(self, node).children[1].value, infectious_dose=infectious_dose.value, viable_to_RNA_ratio=0.5, transmissibility_factor=change['new'])
|
||||
node.dcs_update_from(virus)
|
||||
if (transmissibility_factor.value != models.Virus.types[virus_choice.value].transmissibility_factor):
|
||||
virus_choice.options = list(models.Virus.types.keys()) + ["Custom"]
|
||||
virus_choice.value = "Custom"
|
||||
|
||||
def infectious_dose_change(change):
|
||||
virus = models.SARSCoV2(viral_load_in_sputum=ModelWidgets._build_viral_load(self, node).children[1].value, infectious_dose=change['new'], viable_to_RNA_ratio=0.5, transmissibility_factor=transmissibility_factor.value)
|
||||
node.dcs_update_from(virus)
|
||||
if (infectious_dose.value != models.Virus.types[virus_choice.value].infectious_dose):
|
||||
virus_choice.options = list(models.Virus.types.keys()) + ["Custom"]
|
||||
virus_choice.value = "Custom"
|
||||
|
||||
virus_choice.observe(on_virus_change, names=['value'])
|
||||
transmissibility_factor.observe(transmissibility_change, names=['value'])
|
||||
infectious_dose.observe(infectious_dose_change, names=['value'])
|
||||
|
||||
return widgets.HBox([widgets.Label("Virus"), virus_choice], layout=widgets.Layout(justify_content='space-between'))
|
||||
|
||||
space_between=widgets.Layout(justify_content='space-between')
|
||||
return widgets.VBox([
|
||||
widgets.HBox([widgets.Label("Virus"), virus_choice], layout=space_between),
|
||||
widgets.HBox([widgets.Label("Tansmissibility factor "), transmissibility_factor], layout=space_between),
|
||||
widgets.HBox([widgets.Label("Infectious dose "), infectious_dose], layout=space_between)])
|
||||
|
||||
def present(self):
|
||||
return self.widget
|
||||
|
||||
|
||||
baseline_model = models.ExposureModel(
|
||||
concentration_model=models.ConcentrationModel(
|
||||
room=models.Room(volume=75, humidity=0.5),
|
||||
ventilation=models.SlidingWindow(
|
||||
active=models.PeriodicInterval(period= 120, duration= 15),
|
||||
active=models.PeriodicInterval(period= 120, duration= 15, start=8.0),
|
||||
inside_temp=models.PiecewiseConstant((0., 24.), (293.15,)),
|
||||
outside_temp=models.PiecewiseConstant((0., 24.), (283.15,)),
|
||||
window_height=1.6, opening_length=0.6,
|
||||
|
|
@ -991,11 +1081,11 @@ class MultiModelView(View):
|
|||
return widgets.VBox(children=(buttons, rename_text_field))
|
||||
|
||||
|
||||
def models_start_end(models: typing.Sequence[models.ConcentrationModel]) -> typing.Tuple[float, float]:
|
||||
def models_start_end(models: typing.Sequence[models.ExposureModel]) -> typing.Tuple[float, float]:
|
||||
"""
|
||||
Returns the earliest start and latest end time of a collection of ConcentrationModel objects
|
||||
|
||||
"""
|
||||
infected_start = min(model.infected.presence.boundaries()[0][0] for model in models)
|
||||
infected_finish = min(model.infected.presence.boundaries()[-1][1] for model in models)
|
||||
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)
|
||||
return infected_start, infected_finish
|
||||
|
|
|
|||
Loading…
Reference in a new issue