From fe7dc343363691d700f4b87698d0ca44c213b91b Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Fri, 1 Apr 2022 16:20:32 +0200 Subject: [PATCH 01/28] We put a real png image as a header --- cara/apps/expert/cara.ipynb | 7 ++----- cara/apps/expert/header_image.png | Bin 0 -> 66949 bytes 2 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 cara/apps/expert/header_image.png diff --git a/cara/apps/expert/cara.ipynb b/cara/apps/expert/cara.ipynb index 01ac0106..4326e61c 100644 --- a/cara/apps/expert/cara.ipynb +++ b/cara/apps/expert/cara.ipynb @@ -4,11 +4,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "
\n", - "
\n", - "

\n", - "Airborne Transmission of SARS-CoV-2\n", - "

\n", + "
\n", + "
\n", "

\n", "Please see the CARA homepage for details on the methodology, assumptions and limitations of CARA.

" ] diff --git a/cara/apps/expert/header_image.png b/cara/apps/expert/header_image.png new file mode 100644 index 0000000000000000000000000000000000000000..c08e8991f85e255719109f7e3a788c67a1cc71d6 GIT binary patch literal 66949 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 literal 0 HcmV?d00001 From 1c62b04b3dca7404dc525e07564b47dd5b4a2353 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Tue, 5 Apr 2022 14:21:45 +0200 Subject: [PATCH 02/28] Added options for the mechanical ventilation and changed widgets 'virus', 'exposed' and ' 'infected' sections to be dropdowns. --- cara/apps/expert.py | 111 ++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index bba12b6b..e73da1f7 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -4,13 +4,10 @@ import uuid import ipympl.backend_nbagg import ipywidgets as widgets -import numpy as np import matplotlib import matplotlib.figure - -from cara import models -from cara import state -from cara import data +import numpy as np +from cara import data, models, state def collapsible(widgets_to_collapse: typing.List, title: str, start_collapsed=False): @@ -229,20 +226,21 @@ class ModelWidgets(View): self.widget.children += (self._build_infectivity(node.concentration_model.infected),) def _build_exposed(self, node): - return collapsible([widgets.HBox([ + return collapsible([widgets.VBox([ self._build_mask(node.exposed.mask), self._build_activity(node.exposed.activity), ])], title="Exposed") def _build_infected(self, node): - return collapsible([widgets.HBox([ + return collapsible([widgets.VBox([ self._build_mask(node.mask), self._build_activity(node.activity), self._build_expiration(node.expiration), ])], title="Infected") def _build_room(self, node): - room_volume = widgets.IntSlider(value=node.volume, min=10, max=150) + room_volume = widgets.FloatSlider(value=node.volume, min=10, max=500 + , step=5) def on_value_change(change): node.volume = change['new'] @@ -302,7 +300,7 @@ class ModelWidgets(View): 'Daily variation': self._build_month(node), } - outsidetemp_w = widgets.ToggleButtons( + outsidetemp_w = widgets.Dropdown( options=outsidetemp_widgets.keys(), ) @@ -341,8 +339,8 @@ class ModelWidgets(View): result.add_pairs(sub_group.pairs()) return result - def _build_mechanical(self, node): - q_air_mech = widgets.IntSlider(value=node.q_air_mech, min=0, max=1000, step=5) + def _build_q_air_mech(self, node): + q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=1000, step=5) def q_air_mech_change(change): node.q_air_mech = change['new'] @@ -350,13 +348,48 @@ class ModelWidgets(View): # TODO: Link the state back to the widget, not just the other way around. q_air_mech.observe(q_air_mech_change, names=['value']) - auto_width = widgets.Layout(width='auto') - return widgets.VBox([widget_group([ - [ - widgets.Label('Flow rate (m³/h)', layout=auto_width), - q_air_mech, - ], - ])]) + return widgets.HBox([q_air_mech, widgets.Label('m³/h')]) + + def _build_ach(self, node): + air_exch = widgets.IntSlider(value=node.air_exch, min=0, max=50, step=5) + + def air_exch_change(change): + node.air_exch = change['new'] + + # TODO: Link the state back to the widget, not just the other way around. + air_exch.observe(air_exch_change, names=['value']) + + return widgets.HBox([air_exch, widgets.Label('h⁻¹')]) + + def _build_mechanical(self, node): + mechanical_widgets = { + 'HVACMechanical': self._build_q_air_mech(node._states['HVACMechanical']), + 'AirChange': self._build_ach(node._states['AirChange']), + } + + for name, widget in mechanical_widgets.items(): + widget.layout.visible = False + + mechanival_w = widgets.RadioButtons( + options=list(zip(['Air supply flow rate (m³/h)', 'Air changes per hour (h⁻¹)'], mechanical_widgets.keys())), + # button_style='info', + ) + + def toggle_mechanical(value): + for name, widget in mechanical_widgets.items(): + widget.layout.visible = False + widget.layout.display = 'none' + + node.dcs_select(value) + + widget = mechanical_widgets[value] + widget.layout.visible = True + widget.layout.display = 'flex' + + mechanival_w.observe(lambda event: toggle_mechanical(event['new']), 'value') + toggle_mechanical(mechanival_w.value) + + return widgets.VBox([mechanival_w, widgets.HBox(list(mechanical_widgets.values()))]) def _build_month(self, node) -> WidgetGroup: @@ -377,7 +410,7 @@ class ModelWidgets(View): for name, activity_ in models.Activity.types.items(): if activity == activity_: break - activity = widgets.Select(options=list(models.Activity.types.keys()), value=name) + activity = widgets.Dropdown(options=list(models.Activity.types.keys()), value=name) def on_activity_change(change): act = models.Activity.types[change['new']] @@ -393,7 +426,7 @@ class ModelWidgets(View): for name, mask_ in models.Mask.types.items(): if mask == mask_: break - mask_choice = widgets.Select(options=list(models.Mask.types.keys()), value=name) + mask_choice = widgets.Dropdown(options=list(models.Mask.types.keys()), value=name) def on_mask_change(change): node.dcs_select(change['new']) @@ -408,7 +441,7 @@ class ModelWidgets(View): for name, expiration_ in models.Expiration.types.items(): if expiration == expiration_: break - expiration_choice = widgets.Select(options=list(models.Expiration.types.keys()), value=name) + expiration_choice = widgets.Dropdown(options=list(models.Expiration.types.keys()), value=name) def on_expiration_change(change): expiration = models.Expiration.types[change['new']] @@ -428,13 +461,16 @@ class ModelWidgets(View): ) -> widgets.Widget: ventilation_widgets = { 'Natural': self._build_window(node._states['Natural']).build(), - 'Mechanical': self._build_mechanical(node._states['Mechanical']), + 'HVACMechanical': self._build_mechanical(node), } + + keys=["Natural","HVACMechanical","No ventilation"] + for name, widget in ventilation_widgets.items(): widget.layout.visible = False - ventilation_w = widgets.ToggleButtons( - options=ventilation_widgets.keys(), + ventilation_w = widgets.Dropdown( + options=keys, ) def toggle_ventilation(value): @@ -459,7 +495,7 @@ class ModelWidgets(View): return w def _build_infectivity(self,node): - return collapsible([widgets.HBox([ + return collapsible([widgets.VBox([ self._build_virus(node.virus), ])], title="Virus variant") @@ -468,7 +504,7 @@ class ModelWidgets(View): for name, virus_ in models.Virus.types.items(): if virus == virus_: break - virus_choice = widgets.Select(options=list(models.Virus.types.keys()), value=name) + virus_choice = widgets.Dropdown(options=list(models.Virus.types.keys()), value=name) def on_virus_change(change): node.dcs_select(change['new']) @@ -534,13 +570,28 @@ class CARAStateBuilder(state.StateBuilder): s: state.DataclassStateNamed = state.DataclassStateNamed( states={ 'Natural': self.build_generic(models.WindowOpening), - 'Mechanical': self.build_generic(models.HVACMechanical), + 'No ventilation': self.build_generic(models.AirChange), + 'HVACMechanical': self.build_generic(models.HVACMechanical), + 'AirChange': self.build_generic(models.AirChange), + 'No ventilation': self.build_generic(models.AirChange), + + }, state_builder=self, ) - # Initialise the HVAC state - s._states['Mechanical'].dcs_update_from( - models.HVACMechanical(models.PeriodicInterval(period=24*60, duration=24*60), 500.) + # Initialise the "HVAC" state + s._states['HVACMechanical'].dcs_update_from( + #models.MultipleVentilation(ventilations=[ + #models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.25), + models.HVACMechanical(active=models.PeriodicInterval(period=24*60, duration=24*60), q_air_mech=500.) + #]) + ) + s._states['AirChange'].dcs_update_from( + models.AirChange(models.PeriodicInterval(period=24*60, duration=24*60), 10.) + ) + # Initialize the "No ventilation" state + s._states['No ventilation'].dcs_update_from( + models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.) #will need to add the residual air change of 0.25 ) return s From 63c35139b742e7dd3a52f7ed39170c532b1cf707 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Tue, 5 Apr 2022 14:39:06 +0200 Subject: [PATCH 03/28] Changed the labels for "Mechanical" dropdown widgets --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index e73da1f7..a9c70de0 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -464,7 +464,7 @@ class ModelWidgets(View): 'HVACMechanical': self._build_mechanical(node), } - keys=["Natural","HVACMechanical","No ventilation"] + keys=[("Natural", "Natural"), ("Mechanical", "HVACMechanical"), ("No ventilation", "No ventilation")] for name, widget in ventilation_widgets.items(): widget.layout.visible = False From 6e2f2f34988e08ce20dbe6a239a6107812f5d984 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 6 Apr 2022 17:40:08 +0200 Subject: [PATCH 04/28] Added the options of "Sliding" and "Hinged" windows with associated "open distance" for both and "window width" for the "Hinged window" Added a HEPA filtration distinct type of ventilation --- cara/apps/expert.py | 98 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index a9c70de0..e3a468c7 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -1,4 +1,7 @@ import dataclasses +from msilib.schema import RadioButton +from tkinter import Radiobutton +from turtle import window_height import typing import uuid @@ -276,10 +279,55 @@ class ModelWidgets(View): ), ) + def _build_hinged_window(self, node): + hinged_window = widgets.FloatSlider(value=node.window_width, min=0.1, max=2, step=0.1) + + def hinged_window_change(change): + node.window_width = change['new'] + + # TODO: Link the state back to the widget, not just the other way around. + hinged_window.observe(hinged_window_change, names=['value']) + + return widgets.HBox([widgets.Label('Window width: '),hinged_window, widgets.Label('m')]) + + def _build_sliding_window(self, node): + + return widgets.HBox([]) + def _build_window(self, node) -> WidgetGroup: + window_widgets = { + 'Natural': self._build_sliding_window(node._states['Natural']), + 'Hinged window': self._build_hinged_window(node._states['Hinged window']), + } + + for name, widget in window_widgets.items(): + widget.layout.visible = False + + window_w = widgets.RadioButtons( + options= list(zip(['Sliding window', 'Hinged window'], window_widgets.keys())), + button_style='info', + layout=widgets.Layout(height='45px', width='auto'), + ) + + def toggle_window(value): + for name, widget in window_widgets.items(): + widget.layout.visible = False + widget.layout.display = 'none' + + node.dcs_select(value) + + widget = window_widgets[value] + widget.layout.visible = True + widget.layout.display = 'flex' + + window_w.observe(lambda event: toggle_window(event['new']), 'value') + toggle_window(window_w.value) + 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.) + #window_type = widgets.RadioButtons(options=['Sliding window', 'Hinged window'], disabled=False) + opening_length = widgets.FloatSlider(value=node.opening_length, min=0, max=2, step=0.1) def on_period_change(change): node.active.period = change['new'] @@ -290,10 +338,14 @@ class ModelWidgets(View): def insidetemp_change(change): node.inside_temp.values = (change['new']+273.15,) + def opening_length_change(change): + node.opening_length = change['new'] + # TODO: Link the state back to the widget, not just the other way around. period.observe(on_period_change, names=['value']) interval.observe(on_interval_change, names=['value']) inside_temp.observe(insidetemp_change, names=['value']) + opening_length.observe(opening_length_change, names=['value']) outsidetemp_widgets = { 'Fixed': self._build_outsidetemp(node.outside_temp), @@ -317,6 +369,10 @@ class ModelWidgets(View): auto_width = widgets.Layout(width='auto') result = WidgetGroup( ( + ( + widgets.Label('Opening distance (meters)', layout=auto_width), + opening_length, + ), ( widgets.Label('Interval between openings (minutes)', layout=auto_width), period, @@ -337,7 +393,7 @@ class ModelWidgets(View): ) for sub_group in outsidetemp_widgets.values(): result.add_pairs(sub_group.pairs()) - return result + return widgets.VBox([widgets.VBox([window_w, widgets.HBox(list(window_widgets.values()))]), result.build()]) def _build_q_air_mech(self, node): q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=1000, step=5) @@ -372,7 +428,7 @@ class ModelWidgets(View): mechanival_w = widgets.RadioButtons( options=list(zip(['Air supply flow rate (m³/h)', 'Air changes per hour (h⁻¹)'], mechanical_widgets.keys())), - # button_style='info', + button_style='info', ) def toggle_mechanical(value): @@ -460,11 +516,12 @@ class ModelWidgets(View): ], ) -> widgets.Widget: ventilation_widgets = { - 'Natural': self._build_window(node._states['Natural']).build(), + 'Natural': self._build_window(node), 'HVACMechanical': self._build_mechanical(node), + 'HEPAFilter': self._build_HEPA(node), } - keys=[("Natural", "Natural"), ("Mechanical", "HVACMechanical"), ("No ventilation", "No ventilation")] + keys=[("Natural", "Natural"), ("Mechanical", "HVACMechanical"), ("No ventilation", "No ventilation"), ("HEPA Filter", "HEPAFilter")] for name, widget in ventilation_widgets.items(): widget.layout.visible = False @@ -494,6 +551,20 @@ class ModelWidgets(View): ) return w + def _build_HEPA( + self, + node: state.DataclassStateNamed[models.HEPAFilter], + ) -> widgets.Widget: + + HEPA_w = widgets.FloatSlider(value=node.q_air_mech, min=10, max=500, step=5) + + def on_value_change(change): + node.q_air_mech=change['new'] + + HEPA_w.observe(on_value_change,names= ['value']) + + return widgets.HBox([widgets.Label('HEPA Filtration: '),HEPA_w, widgets.Label('m³/h')]) + def _build_infectivity(self,node): return collapsible([widgets.VBox([ self._build_virus(node.virus), @@ -523,7 +594,7 @@ baseline_model = models.ExposureModel( concentration_model=models.ConcentrationModel( room=models.Room(volume=75), ventilation=models.SlidingWindow( - active=models.PeriodicInterval(period=120, duration=15), + active=models.PeriodicInterval(period= 120, duration= 15), inside_temp=models.PiecewiseConstant((0., 24.), (293.15,)), outside_temp=models.PiecewiseConstant((0., 24.), (283.15,)), window_height=1.6, opening_length=0.6, @@ -573,12 +644,22 @@ class CARAStateBuilder(state.StateBuilder): 'No ventilation': self.build_generic(models.AirChange), 'HVACMechanical': self.build_generic(models.HVACMechanical), 'AirChange': self.build_generic(models.AirChange), - 'No ventilation': self.build_generic(models.AirChange), - + 'Hinged window': self.build_generic(models.WindowOpening), + 'HEPAFilter': self.build_generic(models.HEPAFilter), }, state_builder=self, ) + #Initialise the "Hinged window" state + s._states['Hinged window'].dcs_update_from( + models.HingedWindow(active=models.PeriodicInterval(period=120, duration=15), + inside_temp=models.PiecewiseConstant((0,24.), (293.15,)), + outside_temp=models.PiecewiseConstant((0,24.), (283.15,)), + window_height=1.6, opening_length=0.6, + window_width=10. + ), + + ) # Initialise the "HVAC" state s._states['HVACMechanical'].dcs_update_from( #models.MultipleVentilation(ventilations=[ @@ -593,6 +674,9 @@ class CARAStateBuilder(state.StateBuilder): s._states['No ventilation'].dcs_update_from( models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.) #will need to add the residual air change of 0.25 ) + s._states['HEPAFilter'].dcs_update_from( + models.HEPAFilter(active=models.PeriodicInterval(period=60, duration=60), q_air_mech=500.) + ) return s From 89870a1ff6ae68727f4068406928a81d357ecc89 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 7 Apr 2022 10:24:49 +0200 Subject: [PATCH 05/28] Added a window height parameter --- cara/apps/expert.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index e3a468c7..21229121 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -327,7 +327,8 @@ class ModelWidgets(View): 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.) #window_type = widgets.RadioButtons(options=['Sliding window', 'Hinged window'], disabled=False) - opening_length = widgets.FloatSlider(value=node.opening_length, min=0, max=2, step=0.1) + 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) def on_period_change(change): node.active.period = change['new'] @@ -340,12 +341,16 @@ class ModelWidgets(View): def opening_length_change(change): node.opening_length = change['new'] + + def window_height_change(change): + node.window_height = change['new'] # TODO: Link the state back to the widget, not just the other way around. period.observe(on_period_change, names=['value']) interval.observe(on_interval_change, names=['value']) inside_temp.observe(insidetemp_change, names=['value']) opening_length.observe(opening_length_change, names=['value']) + window_height.observe(window_height_change, names=['value']) outsidetemp_widgets = { 'Fixed': self._build_outsidetemp(node.outside_temp), @@ -373,6 +378,10 @@ class ModelWidgets(View): widgets.Label('Opening distance (meters)', layout=auto_width), opening_length, ), + ( + widgets.Label('Window height (meters)', layout=auto_width), + window_height, + ), ( widgets.Label('Interval between openings (minutes)', layout=auto_width), period, From 61dc7d5f5e05fda2b537d09434363720430ef65a Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 7 Apr 2022 10:34:52 +0200 Subject: [PATCH 06/28] Bug in build_HEPA method --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 21229121..522dd571 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -562,7 +562,7 @@ class ModelWidgets(View): def _build_HEPA( self, - node: state.DataclassStateNamed[models.HEPAFilter], + node, ) -> widgets.Widget: HEPA_w = widgets.FloatSlider(value=node.q_air_mech, min=10, max=500, step=5) From 5e1605d9de4131b7dabbdac2713aaeea042ce509 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 7 Apr 2022 11:12:24 +0200 Subject: [PATCH 07/28] Removing unsused imports --- cara/apps/expert.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 522dd571..4ae50e10 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -1,7 +1,4 @@ import dataclasses -from msilib.schema import RadioButton -from tkinter import Radiobutton -from turtle import window_height import typing import uuid From 94cf375eb729d48ade17eb295ea05ea8b9a18596 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 7 Apr 2022 17:54:07 +0200 Subject: [PATCH 08/28] Aligned the parameters on the UI --- cara/apps/expert.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 4ae50e10..abec55f3 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -7,6 +7,7 @@ import ipywidgets as widgets import matplotlib import matplotlib.figure import numpy as np +from numpy import object_ from cara import data, models, state @@ -209,6 +210,7 @@ class ExposureComparissonResult(View): class ModelWidgets(View): + def __init__(self, model_state: state.DataclassState): #: The widgets that this view produces (inputs and outputs together) self.widget = widgets.VBox([]) @@ -252,11 +254,9 @@ class ModelWidgets(View): node.dcs_observe(on_state_change) widget = collapsible( - [widget_group( - [[widgets.Label('Room volume (m³)'), room_volume]] - )], - title='Specification of workplace', - ) + [widgets.HBox([widgets.Label('Room volume (m³)'), room_volume], layout=widgets.Layout(justify_content='space-between')) + ],title='Specification of workplace', + ) return widget def _build_outsidetemp(self, node) -> WidgetGroup: @@ -284,8 +284,9 @@ class ModelWidgets(View): # TODO: Link the state back to the widget, not just the other way around. hinged_window.observe(hinged_window_change, names=['value']) - - return widgets.HBox([widgets.Label('Window width: '),hinged_window, widgets.Label('m')]) + + auto_width = widgets.Layout(width='auto') + return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between')) def _build_sliding_window(self, node): @@ -456,7 +457,6 @@ class ModelWidgets(View): def _build_month(self, node) -> WidgetGroup: month_choice = widgets.Select(options=list(data.GenevaTemperatures.keys()), value='Jan') - def on_month_change(change): node.outside_temp = data.GenevaTemperatures[change['new']] month_choice.observe(on_month_change, names=['value']) @@ -473,18 +473,17 @@ class ModelWidgets(View): if activity == activity_: break activity = widgets.Dropdown(options=list(models.Activity.types.keys()), value=name) - + def on_activity_change(change): act = models.Activity.types[change['new']] node.dcs_update_from(act) activity.observe(on_activity_change, names=['value']) - return widget_group( - [[widgets.Label("Activity"), activity]] - ) + return widgets.HBox([widgets.Label("Activity"), activity], layout=widgets.Layout(justify_content='space-between')) def _build_mask(self, node): mask = node.dcs_instance() + for name, mask_ in models.Mask.types.items(): if mask == mask_: break @@ -494,9 +493,7 @@ class ModelWidgets(View): node.dcs_select(change['new']) mask_choice.observe(on_mask_change, names=['value']) - return widget_group( - [[widgets.Label("Mask"), mask_choice]] - ) + return widgets.HBox([widgets.Label("Mask"), mask_choice], layout=widgets.Layout(justify_content='space-between')) def _build_expiration(self, node): expiration = node.dcs_instance() @@ -509,10 +506,8 @@ class ModelWidgets(View): expiration = models.Expiration.types[change['new']] node.dcs_update_from(expiration) expiration_choice.observe(on_expiration_change, names=['value']) - - return widget_group( - [[widgets.Label("Expiration"), expiration_choice]] - ) + + return widgets.HBox([widgets.Label("Expiration"), expiration_choice], layout=widgets.Layout(justify_content='space-between')) def _build_ventilation( self, @@ -549,9 +544,9 @@ class ModelWidgets(View): ventilation_w.observe(lambda event: toggle_ventilation(event['new']), 'value') toggle_ventilation(ventilation_w.value) - + w = collapsible( - [widget_group([[widgets.Label('Ventilation type'), ventilation_w]])] + ([widgets.HBox([widgets.Label('Ventilation type'), ventilation_w], layout=widgets.Layout(justify_content='space-between'))]) + list(ventilation_widgets.values()), title='Ventilation scheme', ) @@ -587,9 +582,7 @@ class ModelWidgets(View): node.dcs_select(change['new']) virus_choice.observe(on_virus_change, names=['value']) - return widget_group( - [[widgets.Label("Virus"), virus_choice]] - ) + return widgets.HBox([widgets.Label("Virus"), virus_choice], layout=widgets.Layout(justify_content='space-between')) def present(self): From 17fe8b0a17cb72c6ae467ea519b90afea003ffd9 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Fri, 8 Apr 2022 16:16:58 +0200 Subject: [PATCH 09/28] Added a way to input room volume by specifying surface area and ceiling height --- cara/apps/expert.py | 70 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index abec55f3..09b8a42f 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -4,6 +4,7 @@ import uuid import ipympl.backend_nbagg import ipywidgets as widgets +from ipywidgets import interact import matplotlib import matplotlib.figure import numpy as np @@ -240,7 +241,7 @@ class ModelWidgets(View): self._build_expiration(node.expiration), ])], title="Infected") - def _build_room(self, node): + def _build_room_volume(self, node): room_volume = widgets.FloatSlider(value=node.volume, min=10, max=500 , step=5) @@ -249,14 +250,61 @@ class ModelWidgets(View): # TODO: Link the state back to the widget, not just the other way around. room_volume.observe(on_value_change, names=['value']) - def on_state_change(): - room_volume.value = node.volume - node.dcs_observe(on_state_change) + + return widgets.HBox([widgets.Label('Room volume (m³)'), room_volume], layout=widgets.Layout(justify_content='space-between')) + + + def _build_room_area(self, node): + + room_surface = widgets.FloatSlider(value=1, min=1, max=200, step=5) + room_ceiling_height = widgets.FloatSlider(value=1, min=1, max=20, step=1) + displayed_volume=widgets.Label('1') + + def room_surface_change(change): + node.volume = change['new']*room_ceiling_height.value + displayed_volume.value=str(node.volume) + + def room_ceiling_height_change(change): + node.volume = change['new']*room_surface.value + displayed_volume.value=str(node.volume) + + room_surface.observe(room_surface_change, names=['value']) + room_ceiling_height.observe(room_ceiling_height_change, names=['value']) + + return widgets.VBox([widgets.HBox([widgets.Label('Room surface area '), room_surface, widgets.Label('m²')]), widgets.HBox([widgets.Label('Room ceiling height '), room_ceiling_height, widgets.Label('m')]), widgets.HBox([widgets.Label('Total volume :'), displayed_volume, widgets.Label('m³')], layout=widgets.Layout(width='auto'))]) + + def _build_room(self,node): + room_widgets={ + 'Volume': self._build_room_volume(node), + 'Room area and height': self._build_room_area(node), + } + + for name, widget in room_widgets.items(): + widget.layout.visible = False + widget.layout.display = 'none' + + room_w = widgets.RadioButtons( + options= list(zip(['Volume', 'Room area and height'], room_widgets.keys())), + button_style='info', + layout=widgets.Layout(height='45px', width='auto'), + ) + + def toggle_room(value): + for name, widget in room_widgets.items(): + widget.layout.visible = False + widget.layout.display = 'none' + + widget = room_widgets[value] + widget.layout.visible = True + widget.layout.display = 'flex' + + room_w.observe(lambda event: toggle_room(event['new']), 'value') + toggle_room(room_w.value) widget = collapsible( - [widgets.HBox([widgets.Label('Room volume (m³)'), room_volume], layout=widgets.Layout(justify_content='space-between')) - ],title='Specification of workplace', - ) + [ widgets.VBox([room_w, widgets.VBox(list(room_widgets.values()))])], title="Specification of workspace" + ) + return widget def _build_outsidetemp(self, node) -> WidgetGroup: @@ -285,8 +333,7 @@ class ModelWidgets(View): # TODO: Link the state back to the widget, not just the other way around. hinged_window.observe(hinged_window_change, names=['value']) - auto_width = widgets.Layout(width='auto') - return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between')) + return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between', width='100%')) def _build_sliding_window(self, node): @@ -300,6 +347,7 @@ class ModelWidgets(View): for name, widget in window_widgets.items(): widget.layout.visible = False + widget.layout.display = 'none' window_w = widgets.RadioButtons( options= list(zip(['Sliding window', 'Hinged window'], window_widgets.keys())), @@ -400,7 +448,7 @@ class ModelWidgets(View): ) for sub_group in outsidetemp_widgets.values(): result.add_pairs(sub_group.pairs()) - return widgets.VBox([widgets.VBox([window_w, widgets.HBox(list(window_widgets.values()))]), result.build()]) + return widgets.VBox([window_w, widgets.HBox(list(window_widgets.values())), result.build()]) def _build_q_air_mech(self, node): q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=1000, step=5) @@ -564,7 +612,7 @@ class ModelWidgets(View): HEPA_w.observe(on_value_change,names= ['value']) - return widgets.HBox([widgets.Label('HEPA Filtration: '),HEPA_w, widgets.Label('m³/h')]) + return widgets.HBox([widgets.Label('HEPA Filtration (m³/h) '),HEPA_w], layout=widgets.Layout(justify_content='space-between')) def _build_infectivity(self,node): return collapsible([widgets.VBox([ From fbc6d40cc7b877f3572804041db611bff2bc0f2a Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Mon, 11 Apr 2022 17:28:57 +0200 Subject: [PATCH 10/28] Added the possibility to change the number of windows, number of exposed and infected people in the room Added a text box for the room number A try to change lunch break time but not working yet A try to state wether you have central heating in the room or not but not linked to the graph yet --- cara/apps/expert.py | 84 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 09b8a42f..1dd1d389 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -4,7 +4,6 @@ import uuid import ipympl.backend_nbagg import ipywidgets as widgets -from ipywidgets import interact import matplotlib import matplotlib.figure import numpy as np @@ -59,7 +58,7 @@ class WidgetGroup: return widgets.VBox( [ widgets.HBox( - [labels_w, widgets_w], + [labels_w, widgets_w], layout=widgets.Layout(justify_content='space-between') ), ], ) @@ -224,23 +223,31 @@ 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_working_time(node),) self.widget.children += (self._build_infected(node.concentration_model.infected),) self.widget.children += (self._build_exposed(node),) self.widget.children += (self._build_infectivity(node.concentration_model.infected),) def _build_exposed(self, node): return collapsible([widgets.VBox([ + self._build_exposed_number(node.exposed), self._build_mask(node.exposed.mask), self._build_activity(node.exposed.activity), ])], title="Exposed") def _build_infected(self, 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), ])], title="Infected") + def _build_working_time(self, node): + return collapsible([widgets.VBox([ + self._build_lunch_time(node.concentration_model.infected), + ])], title="Working time") + def _build_room_volume(self, node): room_volume = widgets.FloatSlider(value=node.volume, min=10, max=500 , step=5) @@ -256,7 +263,7 @@ class ModelWidgets(View): def _build_room_area(self, node): - room_surface = widgets.FloatSlider(value=1, min=1, max=200, step=5) + room_surface = widgets.FloatSlider(value=1, min=1, max=200, step=10) room_ceiling_height = widgets.FloatSlider(value=1, min=1, max=20, step=1) displayed_volume=widgets.Label('1') @@ -271,12 +278,13 @@ class ModelWidgets(View): room_surface.observe(room_surface_change, names=['value']) room_ceiling_height.observe(room_ceiling_height_change, names=['value']) - return widgets.VBox([widgets.HBox([widgets.Label('Room surface area '), room_surface, widgets.Label('m²')]), widgets.HBox([widgets.Label('Room ceiling height '), room_ceiling_height, widgets.Label('m')]), widgets.HBox([widgets.Label('Total volume :'), displayed_volume, widgets.Label('m³')], layout=widgets.Layout(width='auto'))]) + return widgets.VBox([widgets.HBox([widgets.Label('Room surface area (m²) '), room_surface], layout=widgets.Layout(justify_content='space-between', width='100%')), widgets.HBox([widgets.Label('Room ceiling height (m)'), room_ceiling_height], layout=widgets.Layout(justify_content='space-between', width='100%')), widgets.HBox([widgets.Label('Total volume :'), displayed_volume, widgets.Label('m³')])]) def _build_room(self,node): + room_number = widgets.Text(value='', placeholder='653/R-004', disabled=False) #not linked to volume yet room_widgets={ 'Volume': self._build_room_volume(node), - 'Room area and height': self._build_room_area(node), + 'Room area and height': self._build_room_area(node) } for name, widget in room_widgets.items(): @@ -301,8 +309,29 @@ class ModelWidgets(View): room_w.observe(lambda event: toggle_room(event['new']), 'value') toggle_room(room_w.value) + heating_w = widgets.RadioButtons( + options= list(['Yes', 'No']), + button_style='info', + layout=widgets.Layout(height='45px', width='auto'), + ) + + def toggle_central_heating(value): + print (node) + for name in (heating_w.options): + print (name) + if name=='Yes': + node.humidity = 0.3 + else: + node.humidiy = 0.5 + print ('oui') + return node.humidity + + heating_w.observe(lambda event: toggle_central_heating(event['new']), 'value') + toggle_central_heating(room_w.value) + + widget = collapsible( - [ widgets.VBox([room_w, widgets.VBox(list(room_widgets.values()))])], title="Specification of workspace" + [ widgets.VBox([widgets.HBox([widgets.Label('Room number '), room_number], layout=widgets.Layout(width='100%', justify_content='space-between')), room_w, widgets.VBox(list(room_widgets.values())), widgets.HBox([widgets.Label('Central heating system in use '), heating_w], layout=widgets.Layout(width='100%', justify_content='space-between'))])], title="Specification of workspace" ) return widget @@ -352,7 +381,7 @@ class ModelWidgets(View): window_w = widgets.RadioButtons( options= list(zip(['Sliding window', 'Hinged window'], window_widgets.keys())), button_style='info', - layout=widgets.Layout(height='45px', width='auto'), + layout=widgets.Layout(height='auto', width='auto'), ) def toggle_window(value): @@ -369,13 +398,16 @@ 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) 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.) - #window_type = widgets.RadioButtons(options=['Sliding window', 'Hinged window'], disabled=False) 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) + def on_value_change(change): + node.number_of_windows = change['new'] + def on_period_change(change): node.active.period = change['new'] @@ -392,6 +424,7 @@ class ModelWidgets(View): node.window_height = change['new'] # TODO: Link the state back to the widget, not just the other way around. + number_of_windows.observe(on_value_change, names=['value']) period.observe(on_period_change, names=['value']) interval.observe(on_interval_change, names=['value']) inside_temp.observe(insidetemp_change, names=['value']) @@ -417,9 +450,13 @@ class ModelWidgets(View): outsidetemp_w.observe(lambda event: toggle_outsidetemp(event['new']), 'value') toggle_outsidetemp(outsidetemp_w.value) - auto_width = widgets.Layout(width='auto') + auto_width = widgets.Layout(width='auto', justify_content='space-between') result = WidgetGroup( ( + ( + widgets.Label('Number of windows ', layout=auto_width), + number_of_windows, + ), ( widgets.Label('Opening distance (meters)', layout=auto_width), opening_length, @@ -543,6 +580,26 @@ class ModelWidgets(View): return widgets.HBox([widgets.Label("Mask"), mask_choice], layout=widgets.Layout(justify_content='space-between')) + def _build_exposed_number(self, node): + number = widgets.IntSlider(value=node.number, min=1, max=200, step=1) + + def exposed_number_change(change): + node.number = change['new'] + # TODO: Link the state back to the widget, not just the other way around. + number.observe(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 _build_infected_number(self, node): + number = widgets.IntSlider(value=node.number, min=1, max=200, step=1) + + def infected_number_change(change): + node.number = change['new'] + # TODO: Link the state back to the widget, not just the other way around. + number.observe(infected_number_change, names=['value']) + + return widgets.HBox([widgets.Label('Number of infected people in the room '), number], layout=widgets.Layout(justify_content='space-between')) + def _build_expiration(self, node): expiration = node.dcs_instance() for name, expiration_ in models.Expiration.types.items(): @@ -556,6 +613,15 @@ 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_lunch_time(self, node): + presence = widgets.FloatRangeSlider(values=node.presence, min=8, max=18, step=1) + #test=widgets.Datetime(description='test:') + def on_lunch_time_change(change): + node.presence = change['new'] + # TODO: Link the state back to the widget, not just the other way around. + presence.observe(on_lunch_time_change, names=['value']) + return widgets.HBox([widgets.Label('Lunch time '), presence], layout=widgets.Layout(justify_content='space-between') ) def _build_ventilation( self, From 053f6bddcf70dc7a4b6d16ced8047374d1989587 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 13 Apr 2022 17:26:25 +0200 Subject: [PATCH 11/28] Created a slider for humidity in the room but still needs revision because of the way models.py handles 0.4<=humidity<0.4 --- cara/apps/expert.py | 61 ++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 1dd1d389..f1d4a4a0 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -223,7 +223,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_working_time(node),) + #self.widget.children += (self._build_working_time(node),) self.widget.children += (self._build_infected(node.concentration_model.infected),) self.widget.children += (self._build_exposed(node),) self.widget.children += (self._build_infectivity(node.concentration_model.infected),) @@ -278,7 +278,11 @@ class ModelWidgets(View): room_surface.observe(room_surface_change, names=['value']) room_ceiling_height.observe(room_ceiling_height_change, names=['value']) - return widgets.VBox([widgets.HBox([widgets.Label('Room surface area (m²) '), room_surface], layout=widgets.Layout(justify_content='space-between', width='100%')), widgets.HBox([widgets.Label('Room ceiling height (m)'), room_ceiling_height], layout=widgets.Layout(justify_content='space-between', width='100%')), widgets.HBox([widgets.Label('Total volume :'), displayed_volume, widgets.Label('m³')])]) + return widgets.VBox([widgets.HBox([widgets.Label('Room surface area (m²) '), room_surface] + , layout=widgets.Layout(justify_content='space-between', width='100%')) + , widgets.HBox([widgets.Label('Room ceiling height (m)'), room_ceiling_height] + , layout=widgets.Layout(justify_content='space-between', width='100%')) + , widgets.HBox([widgets.Label('Total volume :'), displayed_volume, widgets.Label('m³')])]) def _build_room(self,node): room_number = widgets.Text(value='', placeholder='653/R-004', disabled=False) #not linked to volume yet @@ -294,7 +298,7 @@ class ModelWidgets(View): room_w = widgets.RadioButtons( options= list(zip(['Volume', 'Room area and height'], room_widgets.keys())), button_style='info', - layout=widgets.Layout(height='45px', width='auto'), + layout=widgets.Layout(height='auto', width='auto'), ) def toggle_room(value): @@ -309,29 +313,23 @@ class ModelWidgets(View): room_w.observe(lambda event: toggle_room(event['new']), 'value') toggle_room(room_w.value) - heating_w = widgets.RadioButtons( - options= list(['Yes', 'No']), - button_style='info', - layout=widgets.Layout(height='45px', width='auto'), - ) + humidity = widgets.FloatSlider(value = node.humidity, min=0, max=1, step=0.01) - def toggle_central_heating(value): - print (node) - for name in (heating_w.options): - print (name) - if name=='Yes': - node.humidity = 0.3 - else: - node.humidiy = 0.5 - print ('oui') - return node.humidity + def humidity_change(change): + node.humidity = change['new'] - heating_w.observe(lambda event: toggle_central_heating(event['new']), 'value') - toggle_central_heating(room_w.value) - + humidity.observe(humidity_change, names=['value']) widget = collapsible( - [ widgets.VBox([widgets.HBox([widgets.Label('Room number '), room_number], layout=widgets.Layout(width='100%', justify_content='space-between')), room_w, widgets.VBox(list(room_widgets.values())), widgets.HBox([widgets.Label('Central heating system in use '), heating_w], layout=widgets.Layout(width='100%', justify_content='space-between'))])], title="Specification of workspace" + [ widgets.VBox([ + widgets.HBox([ + widgets.Label('Room number '), room_number] + , layout=widgets.Layout(width='100%', justify_content='space-between')) + , room_w, widgets.VBox(list(room_widgets.values())) + , widgets.HBox([widgets.Label('Relative humidity rate in the room depending on the use of a central heating system'),humidity] + , layout=widgets.Layout(width='100%', justify_content='space-between')) + ])] + , title="Specification of workspace" ) return widget @@ -529,7 +527,6 @@ class ModelWidgets(View): widget.layout.display = 'none' node.dcs_select(value) - widget = mechanical_widgets[value] widget.layout.visible = True widget.layout.display = 'flex' @@ -615,13 +612,21 @@ class ModelWidgets(View): return widgets.HBox([widgets.Label("Expiration"), expiration_choice], layout=widgets.Layout(justify_content='space-between')) def _build_lunch_time(self, node): - presence = widgets.FloatRangeSlider(values=node.presence, min=8, max=18, step=1) + presence_start = widgets.FloatRangeSlider(values=node.presence, min=8, max=18, step=1) + presence_finish = widgets.FloatRangeSlider(values=node.presence, min=13, max=18, step=1) + #infected_start = presence_start.value + #infected_finish = presence_finish.value + #node.presence = (((infected_start), (infected_finish))) #test=widgets.Datetime(description='test:') def on_lunch_time_change(change): - node.presence = change['new'] + node.infected_start = change['new'] + node.infected_finish = change['new'] + #node.presence = change['new'] + # TODO: Link the state back to the widget, not just the other way around. - presence.observe(on_lunch_time_change, names=['value']) - return widgets.HBox([widgets.Label('Lunch time '), presence], layout=widgets.Layout(justify_content='space-between') ) + node.infected_start.observe(on_lunch_time_change, names=['value']) + #node.infected_finish.observe(on_lunch_time_change, names=['value']) + return widgets.HBox([widgets.Label('Lunch time '), node.presence], layout=widgets.Layout(justify_content='space-between') ) def _build_ventilation( self, @@ -705,7 +710,7 @@ class ModelWidgets(View): baseline_model = models.ExposureModel( concentration_model=models.ConcentrationModel( - room=models.Room(volume=75), + room=models.Room(volume=75, humidity=0.5), ventilation=models.SlidingWindow( active=models.PeriodicInterval(period= 120, duration= 15), inside_temp=models.PiecewiseConstant((0., 24.), (293.15,)), From 91bc687a58c1f95e0dedc86602eadc651af160ba Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 14 Apr 2022 11:59:35 +0200 Subject: [PATCH 12/28] Added default volume value for surface and ceiling height --- cara/apps/expert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index a1fb112c..9ee90c31 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -263,8 +263,8 @@ class ModelWidgets(View): def _build_room_area(self, node): - room_surface = widgets.FloatSlider(value=1, min=1, max=200, step=10) - room_ceiling_height = widgets.FloatSlider(value=1, min=1, max=20, step=1) + 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) displayed_volume=widgets.Label('1') def room_surface_change(change): From 5509b7efe61c09a1f1390c7586f36f710f66a098 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 14 Apr 2022 12:23:00 +0200 Subject: [PATCH 13/28] Improved code structure --- cara/apps/expert.py | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 9ee90c31..4fecf2ea 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -210,7 +210,6 @@ class ExposureComparissonResult(View): class ModelWidgets(View): - def __init__(self, model_state: state.DataclassState): #: The widgets that this view produces (inputs and outputs together) self.widget = widgets.VBox([]) @@ -223,7 +222,6 @@ 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_working_time(node),) self.widget.children += (self._build_infected(node.concentration_model.infected),) self.widget.children += (self._build_exposed(node),) self.widget.children += (self._build_infectivity(node.concentration_model.infected),) @@ -243,11 +241,6 @@ class ModelWidgets(View): self._build_expiration(node.expiration), ])], title="Infected") - def _build_working_time(self, node): - return collapsible([widgets.VBox([ - self._build_lunch_time(node.concentration_model.infected), - ])], title="Working time") - def _build_room_volume(self, node): room_volume = widgets.FloatSlider(value=node.volume, min=10, max=500 , step=5) @@ -262,7 +255,6 @@ 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) displayed_volume=widgets.Label('1') @@ -363,7 +355,6 @@ class ModelWidgets(View): return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between', width='100%')) def _build_sliding_window(self, node): - return widgets.HBox([]) def _build_window(self, node) -> WidgetGroup: @@ -539,6 +530,7 @@ class ModelWidgets(View): def _build_month(self, node) -> WidgetGroup: month_choice = widgets.Select(options=list(data.GenevaTemperatures.keys()), value='Jan') + def on_month_change(change): node.outside_temp = data.GenevaTemperatures[change['new']] month_choice.observe(on_month_change, names=['value']) @@ -611,23 +603,6 @@ class ModelWidgets(View): return widgets.HBox([widgets.Label("Expiration"), expiration_choice], layout=widgets.Layout(justify_content='space-between')) - def _build_lunch_time(self, node): - presence_start = widgets.FloatRangeSlider(values=node.presence, min=8, max=18, step=1) - presence_finish = widgets.FloatRangeSlider(values=node.presence, min=13, max=18, step=1) - #infected_start = presence_start.value - #infected_finish = presence_finish.value - #node.presence = (((infected_start), (infected_finish))) - #test=widgets.Datetime(description='test:') - def on_lunch_time_change(change): - node.infected_start = change['new'] - node.infected_finish = change['new'] - #node.presence = change['new'] - - # TODO: Link the state back to the widget, not just the other way around. - node.infected_start.observe(on_lunch_time_change, names=['value']) - #node.infected_finish.observe(on_lunch_time_change, names=['value']) - return widgets.HBox([widgets.Label('Lunch time '), node.presence], layout=widgets.Layout(justify_content='space-between') ) - def _build_ventilation( self, node: typing.Union[ @@ -781,10 +756,7 @@ class CARAStateBuilder(state.StateBuilder): ) # Initialise the "HVAC" state s._states['HVACMechanical'].dcs_update_from( - #models.MultipleVentilation(ventilations=[ - #models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.25), models.HVACMechanical(active=models.PeriodicInterval(period=24*60, duration=24*60), q_air_mech=500.) - #]) ) s._states['AirChange'].dcs_update_from( models.AirChange(models.PeriodicInterval(period=24*60, duration=24*60), 10.) From 66d9a6d831f9e68fd3c88a28a19337787ea6e313 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 14 Apr 2022 16:50:45 +0200 Subject: [PATCH 14/28] Changed the name of the relative indoor humidity --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 4fecf2ea..88125435 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -318,7 +318,7 @@ class ModelWidgets(View): widgets.Label('Room number '), room_number] , layout=widgets.Layout(width='100%', justify_content='space-between')) , room_w, widgets.VBox(list(room_widgets.values())) - , widgets.HBox([widgets.Label('Relative humidity rate in the room depending on the use of a central heating system'),humidity] + , widgets.HBox([widgets.Label('Indoor relative humidity '),humidity] , layout=widgets.Layout(width='100%', justify_content='space-between')) ])] , title="Specification of workspace" From a4bbb80727d57220d9bb9958e36d3f5005a850c4 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Tue, 19 Apr 2022 17:23:04 +0200 Subject: [PATCH 15/28] Added a curve for the mean cumulative dose --- cara/apps/expert.py | 55 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 88125435..8fd29abf 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -110,7 +110,9 @@ class ExposureModelResult(View): ipympl_canvas(self.figure) self.html_output = widgets.HTML() self.ax = self.figure.add_subplot(1, 1, 1) - self.line = None + self.ax2 = self.ax.twinx() + self.concentration_line = None + self.cumulative_line = None @property def widget(self): @@ -120,34 +122,61 @@ class ExposureModelResult(View): ]) def update(self, model: models.ExposureModel): - self.update_plot(model.concentration_model) + self.update_plot(model) self.update_textual_result(model) - def update_plot(self, model: models.ConcentrationModel): + def update_plot(self, model: models.ExposureModel): resolution = 600 - ts = np.linspace(sorted(model.infected.presence.transition_times())[0], - sorted(model.infected.presence.transition_times())[-1], resolution) + ts = np.linspace(sorted(model.concentration_model.infected.presence.transition_times())[0], + sorted(model.concentration_model.infected.presence.transition_times())[-1], resolution) concentration = [model.concentration(t) for t in ts] - if self.line is None: - [self.line] = self.ax.plot(ts, concentration) + + cumulative_doses = np.cumsum([ + np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() + for time1, time2 in zip(ts[:-1], ts[1:]) + ]) + + if self.concentration_line is None: + [self.concentration_line] = self.ax.plot(ts, concentration, color='blue', label='Concentration') + ax = self.ax - # ax.text(0.5, 0.9, 'Without masks & window open', transform=ax.transAxes, ha='center') + #ax.text(0.5, 0.9, 'Without masks & window open', transform=ax.transAxes, ha='center') ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.set_xlabel('Time (hours)') - ax.set_ylabel('Concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions') + ax.set_ylabel('Mean concentration ($virions/m^{3}$)') + ax.set_title('Concentration of virions and Cumulative dose') else: self.ax.ignore_existing_data_limits = True - self.line.set_data(ts, concentration) + self.concentration_line.set_data(ts, concentration) + + if self.cumulative_line is None: + [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='red', label='Cumulative dose') + + ax2 = self.ax2 + + ax2.spines['left'].set_visible(False) + ax2.spines['top'].set_visible(False) + + ax2.set_ylabel('Mean cumulative dose (infectious virus)') + + else: + self.ax2.ignore_existing_data_limits = True + self.cumulative_line.set_data(ts[:-1], cumulative_doses) + # Update the top limit based on the concentration if it exceeds 5 # (rare but possible). - top = max([3, max(concentration)]) - self.ax.set_ylim(bottom=0., top=top) + #top = max([3, max(concentration)]) + concentration_top = max([3, max(concentration)]) + self.ax.set_ylim(bottom=0., top=concentration_top) + cumulative_top = max([3, max(cumulative_doses)]) + self.ax2.set_ylim(bottom=0., top=cumulative_top) + self.figure.canvas.draw() + self.figure.legend(loc="upper right", title="Legend", frameon=False) def update_textual_result(self, model: models.ExposureModel): lines = [] From abc61ee5229b6cdd80f62d2d28e55171ea7fb9bf Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Fri, 22 Apr 2022 17:28:13 +0200 Subject: [PATCH 16/28] Moved the plot, change the colors of the curves, added a second axis for cumulative dose, changed the position of the legend --- cara/apps/expert.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 8fd29abf..d24dad53 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -7,6 +7,7 @@ import ipywidgets as widgets import matplotlib import matplotlib.figure import numpy as np +from matplotlib import pyplot as plt from numpy import object_ from cara import data, models, state @@ -109,7 +110,7 @@ class ExposureModelResult(View): self.figure = matplotlib.figure.Figure(figsize=(9, 6)) ipympl_canvas(self.figure) self.html_output = widgets.HTML() - self.ax = self.figure.add_subplot(1, 1, 1) + self.ax = self.figure.add_subplot(2, 1, 2) self.ax2 = self.ax.twinx() self.concentration_line = None self.cumulative_line = None @@ -137,7 +138,7 @@ class ExposureModelResult(View): ]) if self.concentration_line is None: - [self.concentration_line] = self.ax.plot(ts, concentration, color='blue', label='Concentration') + [self.concentration_line] = self.ax.plot(ts, concentration, color='#3530fe', label='Concentration') ax = self.ax @@ -154,7 +155,7 @@ class ExposureModelResult(View): self.concentration_line.set_data(ts, concentration) if self.cumulative_line is None: - [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='red', label='Cumulative dose') + [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', label='Cumulative dose', linestyle='dotted') ax2 = self.ax2 @@ -162,6 +163,7 @@ class ExposureModelResult(View): ax2.spines['top'].set_visible(False) ax2.set_ylabel('Mean cumulative dose (infectious virus)') + ax2.spines['right'].set_linestyle((0,(1,4))) else: self.ax2.ignore_existing_data_limits = True @@ -169,14 +171,15 @@ class ExposureModelResult(View): # Update the top limit based on the concentration if it exceeds 5 # (rare but possible). - #top = max([3, max(concentration)]) concentration_top = max([3, max(concentration)]) self.ax.set_ylim(bottom=0., top=concentration_top) cumulative_top = max([3, 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.figure.canvas.draw() - self.figure.legend(loc="upper right", title="Legend", frameon=False) + self.figure.tight_layout() def update_textual_result(self, model: models.ExposureModel): lines = [] @@ -199,7 +202,11 @@ class ExposureComparissonResult(View): def __init__(self): self.figure = matplotlib.figure.Figure(figsize=(9, 6)) ipympl_canvas(self.figure) - self.ax = self.initialize_axes() + self.html_output = widgets.HTML() + self.ax = self.figure.add_subplot(2, 1, 2) + self.ax2 = self.ax.twinx() + self.concentration_line = None + self.cumulative_line = None @property def widget(self): From 699ef53bce5d86de88b1627f9a7b6bcebb0cb731 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 27 Apr 2022 12:23:14 +0200 Subject: [PATCH 17/28] Changed the label `Daily variation` for `Meteorological data` --- cara/apps/expert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index d24dad53..507daa3a 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -458,7 +458,7 @@ class ModelWidgets(View): outsidetemp_widgets = { 'Fixed': self._build_outsidetemp(node.outside_temp), - 'Daily variation': self._build_month(node), + 'Meteorological data': self._build_month(node), } outsidetemp_w = widgets.Dropdown( @@ -699,7 +699,7 @@ class ModelWidgets(View): def _build_infectivity(self,node): return collapsible([widgets.VBox([ self._build_virus(node.virus), - ])], title="Virus variant") + ])], title="Virus data") def _build_virus(self, node): virus = node.dcs_instance() From 9147b73a90fd127aef4a16c1341d5e97a673a6e4 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Mon, 2 May 2022 17:32:09 +0200 Subject: [PATCH 18/28] 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), --- cara/apps/expert.py | 146 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 28 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 507daa3a..9955efaa 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -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 From 18cce44afaaebed664ffedb23d07760b8b63da0a Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 12 May 2022 16:26:45 +0200 Subject: [PATCH 19/28] Adapted the plot to not have the y axis' legend hidden by the matlpotlib icons, added a better looking legend to the plot, made the comparison results work, added cummulative doses' curves for ExposureComparissonResult and ExposureModelResult, modified the viral load to be displayed as scientific writing on the UI --- cara/apps/expert.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 9955efaa..95f3ae99 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -9,8 +9,9 @@ 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 +import matplotlib.lines as mlines +import matplotlib.patches as patches def collapsible(widgets_to_collapse: typing.List, title: str, start_collapsed=False): collapsed = widgets.Accordion([widgets.VBox(widgets_to_collapse)]) @@ -107,11 +108,10 @@ def ipympl_canvas(figure): class ExposureModelResult(View): def __init__(self): - self.figure = matplotlib.figure.Figure(figsize=(9, 5)) + self.figure = matplotlib.figure.Figure(figsize=(9, 6)) ipympl_canvas(self.figure) self.html_output = widgets.HTML() 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 @@ -124,6 +124,15 @@ class ExposureModelResult(View): self.figure.canvas, ]) + def initialize_axes(self) -> matplotlib.figure.Axes: + ax = self.figure.add_subplot(1, 1, 1) + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + ax.set_xlabel('Time (hours)') + ax.set_ylabel('Concentration ($virions/m^{3}$)') + ax.set_title('Concentration of virions') + return ax + def update(self, model: models.ExposureModel): self.update_plot(model) self.update_textual_result(model) @@ -150,7 +159,7 @@ 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') + ax.set_title('Concentration of virions \nand Cumulative dose') #cursor = SnaptoCursor(self.ax, ts, concentration) @@ -191,15 +200,14 @@ class ExposureModelResult(View): cumulative_top = max([1e-5, max(cumulative_doses)]) self.ax2.set_ylim(bottom=0., top=cumulative_top) - self.ax.set_xlim(left = min(model.concentration_model.infected.presence.present_times[0]), right = max(model.concentration_model.infected.presence.present_times[1])) + self.ax.set_xlim(left = min(min(model.concentration_model.infected.presence.present_times[0]), min(model.exposed.presence.present_times[0])), right = max(max(model.concentration_model.infected.presence.present_times[1]), max(model.exposed.presence.present_times[1]))) + + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), + mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), + patches.Patch(edgecolor="#96cbff", facecolor='#96cbff', label='Presence of exposed person(s)')] + self.figure.legend(handles=figure_legends) - 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() - return legend def update_textual_result(self, model: models.ExposureModel): lines = [] @@ -215,7 +223,7 @@ class ExposureModelResult(View): R0 = np.round(np.array(model.reproduction_number()).mean(), 1) lines.append(f'Reproduction number (R0): {R0}') - self.html_output.value = '
\n'.join(lines) + self.html_output.value = '
\n'.join(lines) class ExposureComparissonResult(View): @@ -223,7 +231,7 @@ class ExposureComparissonResult(View): self.figure = matplotlib.figure.Figure(figsize=(9, 6)) 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.ax2 = self.ax.twinx() self.concentration_line = None self.cumulative_line = None @@ -672,16 +680,17 @@ class ModelWidgets(View): def on_expiration_change(change): expiration = models.Expiration.types[change['new']] node.dcs_update_from(expiration) + 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') + viral_load_in_sputum = widgets.Text(continuous_update=False, value=("{:.2e}".format(node.viral_load_in_sputum))) def viral_load_change(change): - node.viral_load_in_sputum = change['new'] + viral_load_in_sputum.value = "{:.2e}".format(float(change['new'])) + node.viral_load_in_sputum = float(viral_load_in_sputum.value) viral_load_in_sputum.observe(viral_load_change, names=['value']) From 53c82146f661e182394b6381b148e79a3ab3bfb7 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Thu, 12 May 2022 16:30:28 +0200 Subject: [PATCH 20/28] Missing fragment of ExposureComparissonResult from previous commit --- cara/apps/expert.py | 81 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 95f3ae99..5571f07c 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -242,34 +242,75 @@ class ExposureComparissonResult(View): # unless the widget is wrapped in a container (it is seen on all tabs otherwise!). return widgets.HBox([self.figure.canvas]) - def initialize_axes(self) -> matplotlib.figure.Axes: - ax = self.figure.add_subplot(1, 1, 1) - ax.spines['right'].set_visible(False) - ax.spines['top'].set_visible(False) - ax.set_xlabel('Time (hours)') - ax.set_ylabel('Concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions') - return ax - def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _): updated_labels, updated_models = zip(*scenarios) - conc_models = tuple( - model.concentration_model.dcs_instance() for model in updated_models + exp_models = tuple( + model.dcs_instance() for model in updated_models ) - self.update_plot(conc_models, updated_labels) + self.update_plot(exp_models, updated_labels) - def update_plot(self, conc_models: typing.Tuple[models.ConcentrationModel, ...], labels: typing.Tuple[str, ...]): + def update_plot(self, exp_models: typing.Tuple[models.ExposureModel, ...], labels: typing.Tuple[str, ...]): self.ax.lines.clear() - start, finish = models_start_end(conc_models) + self.ax2.lines.clear() + start, finish = models_start_end(exp_models) + colors=['blue', 'red', 'orange', 'yellow', 'pink', 'purple', 'green', 'brown', 'black' ] ts = np.linspace(start, finish, num=250) - concentrations = [[conc_model.concentration(t) for t in ts] for conc_model in conc_models] - for label, concentration in zip(labels, concentrations): - self.ax.plot(ts, concentration, label=label) + concentrations = [[conc_model.concentration_model.concentration(t) for t in ts] for conc_model in exp_models] + for label, concentration, color in zip(labels, concentrations, colors): + self.ax.plot(ts, concentration, label=label, color=color) + + cumulative_doses = [np.cumsum([ + np.array(conc_model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() + for time1, time2 in zip(ts[:-1], ts[1:]) + ]) for conc_model in exp_models] + + for label, cumulative_dose, color in zip(labels, cumulative_doses, colors): + self.ax2.plot(ts[:-1], cumulative_dose, label=label, color=color, linestyle="dotted") - top = max(3., max([max(conc) for conc in concentrations])) - self.ax.set_ylim(bottom=0., top=top) + if self.concentration_line is None: + [self.concentration_line] = self.ax.plot(ts, concentration, '#3530fe', label='Concentration') + + ax = self.ax + + + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + + ax.set_xlabel('Time (hours)') + ax.set_ylabel('Mean concentration ($virions/m^{3}$)') + ax.set_title('Concentration of virions \nand Cumulative dose') + + else: + self.ax.ignore_existing_data_limits = True + self.concentration_line.set_data(ts, concentration) + + if self.cumulative_line is None: + [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_dose, '#1ffd01', label='Cumulative dose', linestyle='dotted') + ax2 = self.ax2 + + ax2.spines['left'].set_visible(False) + ax2.spines['top'].set_visible(False) + + ax2.set_ylabel('Mean cumulative dose (infectious virus)') + ax2.spines['right'].set_linestyle((0,(1,4))) + self.cumulative_line.set_linestyle((0,(1,4))) + + else: + self.ax2.ignore_existing_data_limits = True + self.cumulative_line.set_data(ts[:-1], cumulative_dose) + + # Update the top limit based on the concentration if it exceeds 5 + # (rare but possible). + + concentration_top = max([max(concentration) for concentration in concentrations]) + self.ax.set_ylim(bottom=0., top=concentration_top) + cumulative_top = max([max(cumulative_dose) for cumulative_dose in cumulative_doses]) + self.ax2.set_ylim(bottom=0., top=cumulative_top) + + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), + mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose')] + self.figure.legend(handles=figure_legends) - self.ax.legend() self.figure.canvas.draw() From 587379f20dc8384b6d889f12e62b00d7230128e7 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Fri, 13 May 2022 16:43:24 +0200 Subject: [PATCH 21/28] resolved a bug regarding exposed and infected presence definitions (changed `present_times` for `boundaries()`) --- cara/apps/expert.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 8a4e49df..c8b6fcb3 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -168,14 +168,14 @@ class ExposureModelResult(View): 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]))) - + 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]))) + 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]))) + 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]))) if self.cumulative_line is None: [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', label='Cumulative dose', linestyle='dotted') @@ -198,7 +198,7 @@ class ExposureModelResult(View): cumulative_top = max([1e-5, max(cumulative_doses)]) self.ax2.set_ylim(bottom=0., top=cumulative_top) - self.ax.set_xlim(left = min(min(model.concentration_model.infected.presence.present_times[0]), min(model.exposed.presence.present_times[0])), right = max(max(model.concentration_model.infected.presence.present_times[1]), max(model.exposed.presence.present_times[1]))) + 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]))) figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), From 9a6c7ec3a1810dcd1b33c2f379dbb0ac86807f86 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Mon, 23 May 2022 14:52:32 +0200 Subject: [PATCH 22/28] Changed the relative humidity to be a percentage in a range of 20-80% --- cara/apps/expert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index c8b6fcb3..f0058383 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -410,7 +410,7 @@ class ModelWidgets(View): room_w.observe(lambda event: toggle_room(event['new']), 'value') toggle_room(room_w.value) - humidity = widgets.FloatSlider(value = node.humidity, min=0, max=1, step=0.01) + humidity = widgets.FloatSlider(value = node.humidity, min=20, max=80, step=5) inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.) def on_humidity_change(change): @@ -430,7 +430,7 @@ class ModelWidgets(View): room_w, widgets.VBox(list(room_widgets.values())), widgets.HBox([widgets.Label('Inside temperature (℃)'), inside_temp], layout=widgets.Layout(width='100%', justify_content='space-between')), - widgets.HBox([widgets.Label('Indoor relative humidity'), humidity], + widgets.HBox([widgets.Label('Indoor relative humidity (%)'), humidity], layout=widgets.Layout(width='100%', justify_content='space-between')), ])] , title="Specification of workspace" From 8e77941a29cd54d0d3be4d70a2ba043e7d69f149 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Mon, 23 May 2022 15:21:19 +0200 Subject: [PATCH 23/28] changed relative humidity to be accorder to models humidity --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index f0058383..d9af78fa 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -414,7 +414,7 @@ class ModelWidgets(View): inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.) def on_humidity_change(change): - node.humidity = change['new'] + node.humidity = change['new']/100 def on_insidetemp_change(change): node.inside_temp.values = (change['new']+273.15,) From b64a221c7521e2808ef42f8647bf535d57633b18 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Mon, 23 May 2022 15:28:58 +0200 Subject: [PATCH 24/28] Multiplied the node humidity value by 100 to be displayed as percentage --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index d9af78fa..8df22ce3 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -410,7 +410,7 @@ class ModelWidgets(View): room_w.observe(lambda event: toggle_room(event['new']), 'value') toggle_room(room_w.value) - humidity = widgets.FloatSlider(value = node.humidity, min=20, max=80, step=5) + humidity = widgets.FloatSlider(value = node.humidity*100, min=20, max=80, step=5) inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.) def on_humidity_change(change): From 8ad3b417968e520e0cbe3d87284a2af486232e10 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 24 May 2022 14:45:18 +0200 Subject: [PATCH 25/28] Handled Virus custom choice and cleaned the code --- cara/apps/expert.py | 152 ++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 97 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 8df22ce3..df8655e6 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -6,11 +6,13 @@ import ipympl.backend_nbagg import ipywidgets as widgets import matplotlib import matplotlib.figure -import numpy as np -from matplotlib import pyplot as plt -from cara import data, models, state import matplotlib.lines as mlines import matplotlib.patches as patches +from matplotlib import pyplot as plt +import numpy as np + +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)]) @@ -110,8 +112,7 @@ class ExposureModelResult(View): self.figure = matplotlib.figure.Figure(figsize=(9, 6)) ipympl_canvas(self.figure) self.html_output = widgets.HTML() - self.ax = self.figure.add_subplot(1, 1, 1) - self.ax2 = self.ax.twinx() + self.ax, self.ax2 = self.initialize_axes() self.concentration_line = None self.concentration_area = None self.cumulative_line = None @@ -128,9 +129,16 @@ class ExposureModelResult(View): ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) ax.set_xlabel('Time (hours)') - ax.set_ylabel('Concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions') - return ax + ax.set_ylabel('Mean concentration ($virions/m^{3}$)') + ax.set_title('Concentration of virions \nand Cumulative dose') + + ax2 = ax.twinx() + ax2.spines['left'].set_visible(False) + ax2.spines['top'].set_visible(False) + ax2.set_ylabel('Mean cumulative dose (infectious virus)') + ax2.spines['right'].set_linestyle((0,(1,4))) + + return ax, ax2 def update(self, model: models.ExposureModel): self.update_plot(model) @@ -148,57 +156,37 @@ 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') - - ax.spines['right'].set_visible(False) - ax.spines['top'].set_visible(False) - - ax.set_xlabel('Time (hours)') - ax.set_ylabel('Mean concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions \nand Cumulative dose') - - #cursor = SnaptoCursor(self.ax, ts, concentration) + [self.concentration_line] = self.ax.plot(ts, concentration, color='#3530fe') else: self.ax.ignore_existing_data_limits = False self.concentration_line.set_data(ts, concentration) 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", + 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]))) else: self.concentration_area.remove() - self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", label="Exposed person presence", + 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]))) 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) - - ax2.set_ylabel('Mean cumulative dose (infectious virus)') - ax2.spines['right'].set_linestyle((0,(1,4))) + [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', linestyle='dotted') else: 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([1e-5, max(concentration)]) + concentration_top = max(concentration) self.ax.set_ylim(bottom=0., top=concentration_top) - cumulative_top = max([1e-5, max(cumulative_doses)]) + 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(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]))) figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), @@ -229,10 +217,7 @@ class ExposureComparissonResult(View): self.figure = matplotlib.figure.Figure(figsize=(9, 6)) ipympl_canvas(self.figure) self.html_output = widgets.HTML() - self.ax = self.figure.add_subplot(1, 1, 1) - self.ax2 = self.ax.twinx() - self.concentration_line = None - self.cumulative_line = None + self.ax, self.ax2 = self.initialize_axes() @property def widget(self): @@ -240,6 +225,24 @@ class ExposureComparissonResult(View): # unless the widget is wrapped in a container (it is seen on all tabs otherwise!). return widgets.HBox([self.figure.canvas]) + def initialize_axes(self) -> matplotlib.figure.Axes: + ax = self.figure.add_subplot(1, 1, 1) + ax.spines['right'].set_visible(False) + ax.spines['top'].set_visible(False) + + ax.set_xlabel('Time (hours)') + ax.set_ylabel('Mean concentration ($virions/m^{3}$)') + ax.set_title('Concentration of virions \nand Cumulative dose') + + ax2 = ax.twinx() + ax2.spines['left'].set_visible(False) + ax2.spines['top'].set_visible(False) + ax2.spines['right'].set_linestyle((0,(1,4))) + + ax2.set_ylabel('Mean cumulative dose (infectious virus)') + + return ax, ax2 + def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _): updated_labels, updated_models = zip(*scenarios) exp_models = tuple( @@ -264,51 +267,13 @@ class ExposureComparissonResult(View): for label, cumulative_dose, color in zip(labels, cumulative_doses, colors): self.ax2.plot(ts[:-1], cumulative_dose, label=label, color=color, linestyle="dotted") - - if self.concentration_line is None: - [self.concentration_line] = self.ax.plot(ts, concentration, '#3530fe', label='Concentration') - - ax = self.ax - - - ax.spines['right'].set_visible(False) - ax.spines['top'].set_visible(False) - - ax.set_xlabel('Time (hours)') - ax.set_ylabel('Mean concentration ($virions/m^{3}$)') - ax.set_title('Concentration of virions \nand Cumulative dose') - - else: - self.ax.ignore_existing_data_limits = True - self.concentration_line.set_data(ts, concentration) - - if self.cumulative_line is None: - [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_dose, '#1ffd01', label='Cumulative dose', linestyle='dotted') - ax2 = self.ax2 - - ax2.spines['left'].set_visible(False) - ax2.spines['top'].set_visible(False) - - ax2.set_ylabel('Mean cumulative dose (infectious virus)') - ax2.spines['right'].set_linestyle((0,(1,4))) - self.cumulative_line.set_linestyle((0,(1,4))) - else: - self.ax2.ignore_existing_data_limits = True - self.cumulative_line.set_data(ts[:-1], cumulative_dose) - - # Update the top limit based on the concentration if it exceeds 5 - # (rare but possible). - concentration_top = max([max(concentration) for concentration in concentrations]) self.ax.set_ylim(bottom=0., top=concentration_top) cumulative_top = max([max(cumulative_dose) for cumulative_dose in cumulative_doses]) self.ax2.set_ylim(bottom=0., top=cumulative_top) - figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), - mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose')] - self.figure.legend(handles=figure_legends) - + self.ax.legend() self.figure.canvas.draw() @@ -724,7 +689,6 @@ class ModelWidgets(View): def _build_viral_load(self, node): viral_load_in_sputum = widgets.Text(continuous_update=False, value=("{:.2e}".format(node.viral_load_in_sputum))) - def on_viral_load_change(change): viral_load_in_sputum.value = "{:.2e}".format(float(change['new'])) node.viral_load_in_sputum = float(viral_load_in_sputum.value) @@ -737,7 +701,7 @@ class ModelWidgets(View): 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) @@ -808,15 +772,14 @@ class ModelWidgets(View): return widgets.HBox([widgets.Label('HEPA Filtration (m³/h) '),HEPA_w], layout=widgets.Layout(justify_content='space-between')) - def _build_infectivity(self,node): + def _build_infectivity(self, node): return collapsible([widgets.VBox([ self._build_virus(node.virus), ])], title="Virus data") def _build_virus(self, node): - virus = node.dcs_instance() for name, virus_ in models.Virus.types.items(): - if virus == virus_: + if node.dcs_instance() == 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) @@ -829,17 +792,19 @@ class ModelWidgets(View): infectious_dose.value = virus.infectious_dose def on_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']) + virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, 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 on_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) + virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, 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.options.append("Custom") virus_choice.value = "Custom" virus_choice.observe(on_virus_change, names=['value']) @@ -896,12 +861,6 @@ class CARAStateBuilder(state.StateBuilder): choices=models.Mask.types, ) - def build_type_Virus(self, _: dataclasses.Field): - return state.DataclassStatePredefined( - models.Virus, - choices=models.Virus.types, - ) - def build_type__VentilationBase(self, _: dataclasses.Field): s: state.DataclassStateNamed = state.DataclassStateNamed( states={ @@ -918,11 +877,10 @@ class CARAStateBuilder(state.StateBuilder): #Initialise the "Hinged window" state s._states['Hinged window'].dcs_update_from( models.HingedWindow(active=models.PeriodicInterval(period=120, duration=15), - outside_temp=models.PiecewiseConstant((0,24.), (283.15,)), - window_height=1.6, opening_length=0.6, - window_width=10. + outside_temp=models.PiecewiseConstant((0,24.), (283.15,)), + window_height=1.6, opening_length=0.6, + window_width=10. ), - ) # Initialise the "HVAC" state s._states['HVACMechanical'].dcs_update_from( From a5840b63a8fb5c1ed92c23fedd9ec0a584cdafd6 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 25 May 2022 09:42:06 +0200 Subject: [PATCH 26/28] =?UTF-8?q?Improved=20displayed=20total=20volume,=20?= =?UTF-8?q?raised=20the=20maximum=20value=20of=20the=20air=20supply=20flow?= =?UTF-8?q?=20rate=20to=205000=20m=C2=B3/h,=20lowered=20the=20maximum=20va?= =?UTF-8?q?lue=20of=20air=20exchange=20per=20hour=20to=2020h=E2=81=BB?= =?UTF-8?q?=C2=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cara/apps/expert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index df8655e6..7840de4e 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -327,7 +327,7 @@ class ModelWidgets(View): def _build_room_area(self, node): 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') + displayed_volume=widgets.Label('75') def on_room_surface_change(change): node.volume = change['new']*room_ceiling_height.value @@ -545,7 +545,7 @@ class ModelWidgets(View): return widgets.VBox([window_w, widgets.HBox(list(window_widgets.values())), result.build()]) def _build_q_air_mech(self, node): - q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=1000, step=5) + q_air_mech = widgets.FloatSlider(value=node.q_air_mech, min=0, max=5000, step=25) def on_q_air_mech_change(change): node.q_air_mech = change['new'] @@ -556,7 +556,7 @@ class ModelWidgets(View): return widgets.HBox([q_air_mech, widgets.Label('m³/h')]) def _build_ach(self, node): - air_exch = widgets.IntSlider(value=node.air_exch, min=0, max=50, step=5) + air_exch = widgets.IntSlider(value=node.air_exch, min=0, max=20, step=1) def on_air_exch_change(change): node.air_exch = change['new'] From c231d5a68662ee4d3511da4fa50fb5066988482c Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 25 May 2022 12:12:14 +0200 Subject: [PATCH 27/28] Moved the `Virus data` block to be inside `Infected` --- cara/apps/expert.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index 7840de4e..cee0cd9e 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -292,8 +292,7 @@ class ModelWidgets(View): self.widget.children += (self._build_ventilation(node.concentration_model.ventilation),) 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),) - + def _build_exposed(self, node): return collapsible([widgets.VBox([ self._build_exposed_number(node.exposed), @@ -309,7 +308,8 @@ class ModelWidgets(View): 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) + self._build_infected_presence(node.presence, ventilation_node.active), + ModelWidgets._build_infectivity(self,node) ])], title="Infected") def _build_room_volume(self, node): From f6807619e24bc06f1d2c3a55f495c17818d524a4 Mon Sep 17 00:00:00 2001 From: Germain Personne Date: Wed, 25 May 2022 13:50:00 +0200 Subject: [PATCH 28/28] Improved the `Virus data` call --- cara/apps/expert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cara/apps/expert.py b/cara/apps/expert.py index cee0cd9e..2e2b8b34 100644 --- a/cara/apps/expert.py +++ b/cara/apps/expert.py @@ -309,7 +309,7 @@ class ModelWidgets(View): self._build_expiration(node.expiration), self._build_viral_load(node.virus), self._build_infected_presence(node.presence, ventilation_node.active), - ModelWidgets._build_infectivity(self,node) + self._build_infectivity(node) ])], title="Infected") def _build_room_volume(self, node):