From 9ed65a2b044448483dfc6b3de34047705cb0e3b2 Mon Sep 17 00:00:00 2001
From: Nicolas Mounet
Date: Mon, 13 Sep 2021 12:00:06 +0200
Subject: [PATCH 02/12] Model optimisation: avoid computing before first
presence time
---
cara/models.py | 14 +++++++++++++-
cara/tests/models/test_concentration_model.py | 4 ++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/cara/models.py b/cara/models.py
index 53cf52bc..86d2acb9 100644
--- a/cara/models.py
+++ b/cara/models.py
@@ -776,6 +776,14 @@ class ConcentrationModel:
state_change_times.update(self.ventilation.transition_times())
return sorted(state_change_times)
+ @method_cache
+ def _first_presence_time(self) -> float:
+ """
+ First presence time. Before that, the concentration is zero.
+
+ """
+ return self.infected.presence.boundaries()[0][0]
+
def last_state_change(self, time: float) -> float:
"""
Find the most recent/previous state change.
@@ -823,7 +831,9 @@ class ConcentrationModel:
Note that time is not vectorised. You can only pass a single float
to this method.
"""
- if time == 0:
+ # The model always starts at t=0, but we avoid running concentration calculations
+ # before the first presence as an optimisation.
+ if time <= self._first_presence_time():
return 0.0
next_state_change_time = self._next_state_change(time)
IVRR = self.infectious_virus_removal_rate(next_state_change_time)
@@ -841,6 +851,8 @@ class ConcentrationModel:
"""
Get the integrated concentration dose between the times start and stop.
"""
+ if stop <= self._first_presence_time():
+ return 0.0
state_change_times = self.state_change_times()
req_start, req_stop = start, stop
total_concentration = 0.
diff --git a/cara/tests/models/test_concentration_model.py b/cara/tests/models/test_concentration_model.py
index 46e7e88e..9373d4a9 100644
--- a/cara/tests/models/test_concentration_model.py
+++ b/cara/tests/models/test_concentration_model.py
@@ -121,6 +121,10 @@ def test_next_state_change_time_out_of_range(simple_conc_model: models.Concentra
simple_conc_model._next_state_change(3.1)
+def test_first_presence_time(simple_conc_model):
+ assert simple_conc_model._first_presence_time() == 0.5
+
+
def test_integrated_concentration(simple_conc_model):
c1 = simple_conc_model.integrated_concentration(0, 2)
c2 = simple_conc_model.integrated_concentration(0, 1)
From fdca7da53a0b8cd5866d688307681e8a11007356 Mon Sep 17 00:00:00 2001
From: jdevine
Date: Mon, 13 Sep 2021 13:50:12 +0200
Subject: [PATCH 03/12] Changed default strain in calculator to Delta. Also
applies retroactively to QR codes missing delta. The rational for the
retrospective change is that old simulations (wild type) re-run need to be
re-run with the current dominant strain.
---
cara/apps/calculator/model_generator.py | 2 +-
cara/apps/calculator/templates/calculator.form.html.j2 | 6 +++---
cara/apps/calculator/templates/userguide.html.j2 | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py
index 06c29229..61f4e9fd 100644
--- a/cara/apps/calculator/model_generator.py
+++ b/cara/apps/calculator/model_generator.py
@@ -117,7 +117,7 @@ class FormData:
'simulation_name': _NO_DEFAULT,
'total_people': _NO_DEFAULT,
'ventilation_type': 'no_ventilation',
- 'virus_type': 'SARS_CoV_2',
+ 'virus_type': 'SARS_CoV_2_B16172',
'volume_type': _NO_DEFAULT,
'window_type': 'window_sliding',
'window_height': 0.,
diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2
index c119d4e5..97209c98 100644
--- a/cara/apps/calculator/templates/calculator.form.html.j2
+++ b/cara/apps/calculator/templates/calculator.form.html.j2
@@ -62,10 +62,10 @@ v{{ calculator_version }} Please sen
@@ -344,13 +344,13 @@ v{{ calculator_version }} Please sen
Quick Guide:
This tool simulates the long range airborne spread SARS-CoV-2 virus in a finite volume and estimates the risk of COVID-19 infection. It is based on current scientific data and can be used to compare the effectiveness of different mitigation measures. Virus data:
- SARS-CoV-2 covers typical strains of the virus and three variants of concern (VOC):
+ SARS-CoV-2 covers the original wild strain of the virus and three variants of concern (VOC):
Alpha (also known as B.1.1.7, first identified in UK, Dec 2020),
Gamma (also known as P.1, first identified in Brazil/Japan, Jan 2021).
Delta (also known as B.1.617.2, first identified in India, Oct 2020).
- Choose variant according to local area prevalence, e.g. for Geneva
+ Modify the default (Delta) as necessary, according to local area prevalence e.g. for Geneva
or Ain (France). Ventilation data:
diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/calculator/templates/userguide.html.j2
index ede8c8e3..ac0e69ca 100644
--- a/cara/apps/calculator/templates/userguide.html.j2
+++ b/cara/apps/calculator/templates/userguide.html.j2
@@ -56,12 +56,12 @@ Changing this setting alters the properties of the virus which are used for the
This has a significant effect on the probability of infection.
The choices are:
-
SARS-CoV-2 (nominal strain), covering typical strains and varaints which are not of concern from an epidemiologic point of view of the virus;
+
SARS-CoV-2 (nominal strain), covering typical strains and variants which are not of concern from an epidemiologic point of view of the virus;
SARS-CoV-2 (Alpha VOC), first identified in the UK at the end of 2020 which is found to be approximately 1.5x more transmissible compared to the non-VOCs;
SARS-CoV-2 (Gamma VOC), first identified in Brazil in January 2021 which is found to be approximately 2.2x more transmissible compared to the non-VOCs.
SARS-CoV-2 (Delta VOC), first identified in India towards the end of 2020 which is found to be approximately 60% more transmissible compared to the ALPHA VOC.
-
The user can base their choice according to the prevalence of the different variants in the local area. Access to this information can be found here:
+
The user can modify the selected variant from the default (Delta VOC), according to the prevalence of the different variants in the local area. Access to this information can be found here:
From 8fbe7856b98cd908934ee9798b1ac06039fb40ae Mon Sep 17 00:00:00 2001
From: jdevine
Date: Tue, 14 Sep 2021 14:57:48 +0200
Subject: [PATCH 04/12] Addressed comments from Andre.
---
cara/apps/calculator/__init__.py | 2 +-
cara/apps/calculator/templates/calculator.form.html.j2 | 6 +++---
cara/apps/calculator/templates/userguide.html.j2 | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py
index 1688d494..a60d5f74 100644
--- a/cara/apps/calculator/__init__.py
+++ b/cara/apps/calculator/__init__.py
@@ -33,7 +33,7 @@ from .user import AuthenticatedUser, AnonymousUser
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CARA version (found at ``cara.__version__``).
-__version__ = "3.0.0"
+__version__ = "3.0.1"
class BaseRequestHandler(RequestHandler):
diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2
index 97209c98..65391454 100644
--- a/cara/apps/calculator/templates/calculator.form.html.j2
+++ b/cara/apps/calculator/templates/calculator.form.html.j2
@@ -62,10 +62,10 @@ v{{ calculator_version }} Please sen
@@ -344,13 +344,13 @@ v{{ calculator_version }} Please sen
Quick Guide:
This tool simulates the long range airborne spread SARS-CoV-2 virus in a finite volume and estimates the risk of COVID-19 infection. It is based on current scientific data and can be used to compare the effectiveness of different mitigation measures. Virus data:
- SARS-CoV-2 covers the original wild strain of the virus and three variants of concern (VOC):
+ SARS-CoV-2 covers the original "wild type" strain of the virus and three variants of concern (VOC):
Alpha (also known as B.1.1.7, first identified in UK, Dec 2020),
Gamma (also known as P.1, first identified in Brazil/Japan, Jan 2021).
Delta (also known as B.1.617.2, first identified in India, Oct 2020).
- Modify the default (Delta) as necessary, according to local area prevalence e.g. for Geneva
+ Modify the default as necessary, according to local area prevalence e.g. for Geneva
or Ain (France). Ventilation data:
diff --git a/cara/apps/calculator/templates/userguide.html.j2 b/cara/apps/calculator/templates/userguide.html.j2
index ac0e69ca..f51cba61 100644
--- a/cara/apps/calculator/templates/userguide.html.j2
+++ b/cara/apps/calculator/templates/userguide.html.j2
@@ -61,7 +61,7 @@ The choices are:
SARS-CoV-2 (Gamma VOC), first identified in Brazil in January 2021 which is found to be approximately 2.2x more transmissible compared to the non-VOCs.
SARS-CoV-2 (Delta VOC), first identified in India towards the end of 2020 which is found to be approximately 60% more transmissible compared to the ALPHA VOC.
-
The user can modify the selected variant from the default (Delta VOC), according to the prevalence of the different variants in the local area. Access to this information can be found here:
+
The user can modify the selected variant from the default, according to the prevalence of the different variants in the local area. Access to this information can be found here:
From 92f9bce7e5c2de2de65ad13ea3d96139bb32c2b7 Mon Sep 17 00:00:00 2001
From: Nicolas Mounet
Date: Tue, 14 Sep 2021 16:11:44 +0200
Subject: [PATCH 05/12] Normalization of the computation of the concentration
---
cara/models.py | 94 ++++++++++++++--------
cara/tests/models/test_exposure_model.py | 16 ++--
cara/tests/test_monte_carlo_full_models.py | 2 +-
3 files changed, 70 insertions(+), 42 deletions(-)
diff --git a/cara/models.py b/cara/models.py
index 86d2acb9..98245997 100644
--- a/cara/models.py
+++ b/cara/models.py
@@ -701,11 +701,11 @@ class InfectedPopulation(Population):
elif np.isinf(aerosols):
ER = 970 * self.virus.infectious_dose
- return ER
+ return ER * self.number
- def individual_emission_rate(self, time) -> _VectorisedFloat:
+ def emission_rate(self, time) -> _VectorisedFloat:
"""
- The emission rate of a single individual in the population.
+ The emission rate of the population.
"""
# Note: The original model avoids time dependence on the emission rate
@@ -721,13 +721,6 @@ class InfectedPopulation(Population):
return self.emission_rate_when_present()
- def emission_rate(self, time) -> _VectorisedFloat:
- """
- The emission rate of the entire population.
-
- """
- return self.individual_emission_rate(time) * self.number
-
@dataclass(frozen=True)
class ConcentrationModel:
@@ -753,16 +746,22 @@ class ConcentrationModel:
)
@method_cache
- def _concentration_limit(self, time: float) -> _VectorisedFloat:
+ def _normed_concentration_limit(self, time: float) -> _VectorisedFloat:
"""
Provides a constant that represents the theoretical asymptotic
value reached by the concentration when time goes to infinity,
if all parameters were to stay time-independent.
+ This is normalized by the emission rate, the latter acting as a
+ multiplicative constant factor for the concentration model that
+ can be put back in front of the concentration after the time
+ dependence has been solved for.
"""
+ if not self.infected.person_present(time):
+ return 0.
V = self.room.volume
IVRR = self.infectious_virus_removal_rate(time)
- return (self.infected.emission_rate(time)) / (IVRR * V)
+ return 1. / (IVRR * V)
@method_cache
def state_change_times(self) -> typing.List[float]:
@@ -815,15 +814,16 @@ class ConcentrationModel:
)
@method_cache
- def _concentration_cached(self, time: float) -> _VectorisedFloat:
- # A cached version of the concentration method. Use this method if you
- # expect that there may be multiple concentration calculations for the
- # same time (e.g. at state change times).
- return self.concentration(time)
+ def _normed_concentration_cached(self, time: float) -> _VectorisedFloat:
+ # A cached version of the _normed_concentration method. Use this
+ # method if you expect that there may be multiple concentration
+ # calculations for the same time (e.g. at state change times).
+ return self._normed_concentration(time)
- def concentration(self, time: float) -> _VectorisedFloat:
+ def _normed_concentration(self, time: float) -> _VectorisedFloat:
"""
- Virus exposure concentration, as a function of time.
+ Virus exposure concentration, as a function of time, and
+ normalized by the emission rate.
The formulas used here assume that all parameters (ventilation,
emission rate) are constant between two state changes - only
the value of these parameters at the next state change, are used.
@@ -837,25 +837,36 @@ class ConcentrationModel:
return 0.0
next_state_change_time = self._next_state_change(time)
IVRR = self.infectious_virus_removal_rate(next_state_change_time)
- concentration_limit = self._concentration_limit(next_state_change_time)
+ conc_limit = self._normed_concentration_limit(next_state_change_time)
t_last_state_change = self.last_state_change(time)
- concentration_at_last_state_change = self._concentration_cached(t_last_state_change)
+ conc_at_last_state_change = self._normed_concentration_cached(t_last_state_change)
delta_time = time - t_last_state_change
fac = np.exp(-IVRR * delta_time)
- return concentration_limit * (1 - fac) + concentration_at_last_state_change * fac
+ return conc_limit * (1 - fac) + conc_at_last_state_change * fac
+
+ def concentration(self, time: float) -> _VectorisedFloat:
+ """
+ Virus exposure concentration, as a function of time.
+
+ Note that time is not vectorised. You can only pass a single float
+ to this method.
+ """
+ return (self._normed_concentration(time) *
+ self.infected.emission_rate_when_present())
@method_cache
- def integrated_concentration(self, start: float, stop: float) -> _VectorisedFloat:
+ def normed_integrated_concentration(self, start: float, stop: float) -> _VectorisedFloat:
"""
- Get the integrated concentration dose between the times start and stop.
+ Get the integrated concentration dose between the times start and stop,
+ normalized by the emission rate.
"""
if stop <= self._first_presence_time():
return 0.0
state_change_times = self.state_change_times()
req_start, req_stop = start, stop
- total_concentration = 0.
+ total_normed_concentration = 0.
for interval_start, interval_stop in zip(state_change_times[:-1], state_change_times[1:]):
if req_start > interval_stop or req_stop < interval_start:
continue
@@ -863,17 +874,24 @@ class ConcentrationModel:
start = max([interval_start, req_start])
stop = min([interval_stop, req_stop])
- conc_start = self._concentration_cached(start)
+ conc_start = self._normed_concentration_cached(start)
next_conc_state = self._next_state_change(stop)
- conc_limit = self._concentration_limit(next_conc_state)
+ conc_limit = self._normed_concentration_limit(next_conc_state)
IVRR = self.infectious_virus_removal_rate(next_conc_state)
delta_time = stop - start
- total_concentration += (
+ total_normed_concentration += (
conc_limit * delta_time +
(conc_limit - conc_start) * (np.exp(-IVRR*delta_time)-1) / IVRR
)
- return total_concentration
+ return total_normed_concentration
+
+ def integrated_concentration(self, start: float, stop: float) -> _VectorisedFloat:
+ """
+ Get the integrated concentration dose between the times start and stop.
+ """
+ return (self.normed_integrated_concentration(start, stop) *
+ self.infected.emission_rate_when_present())
@dataclass(frozen=True)
@@ -890,14 +908,22 @@ class ExposureModel:
#: The fraction of viruses actually deposited in the respiratory tract
fraction_deposited: _VectorisedFloat = 0.6
- def exposure(self) -> _VectorisedFloat:
- """The number of virus per meter^3."""
- exposure = 0.0
+ def _normed_exposure(self) -> _VectorisedFloat:
+ """
+ The number of virus per meter^3, normalized by the emission rate
+ of the infected population.
+ """
+ normed_exposure = 0.0
for start, stop in self.exposed.presence.boundaries():
- exposure += self.concentration_model.integrated_concentration(start, stop)
+ normed_exposure += self.concentration_model.normed_integrated_concentration(start, stop)
- return exposure * self.repeats
+ return normed_exposure * self.repeats
+
+ def exposure(self) -> _VectorisedFloat:
+ """The number of virus per meter^3."""
+ return (self._normed_exposure() *
+ self.concentration_model.infected.emission_rate_when_present())
def infection_probability(self) -> _VectorisedFloat:
exposure = self.exposure()
diff --git a/cara/tests/models/test_exposure_model.py b/cara/tests/models/test_exposure_model.py
index cf8cfb6e..11a135a1 100644
--- a/cara/tests/models/test_exposure_model.py
+++ b/cara/tests/models/test_exposure_model.py
@@ -11,20 +11,20 @@ from cara.dataclass_utils import replace
@dataclass(frozen=True)
-class KnownConcentrations(models.ConcentrationModel):
+class KnownNormedconcentration(models.ConcentrationModel):
"""
A ConcentrationModel which is based on pre-known exposure concentrations and
which therefore doesn't need other components. Useful for testing.
"""
- concentration_function: typing.Callable
+ normed_concentration_function: typing.Callable
def infectious_virus_removal_rate(self, time: float) -> models._VectorisedFloat:
# very large decay constant -> same as constant concentration
return 1.e50
- def _concentration_limit(self, time: float) -> models._VectorisedFloat:
- return self.concentration_function(time)
+ def _normed_concentration_limit(self, time: float) -> models._VectorisedFloat:
+ return self.normed_concentration_function(time)
def state_change_times(self):
return [0., 24.]
@@ -32,8 +32,8 @@ class KnownConcentrations(models.ConcentrationModel):
def _next_state_change(self, time: float):
return 24.
- def concentration(self, time: float) -> models._VectorisedFloat: # noqa
- return self.concentration_function(time)
+ def _normed_concentration(self, time: float) -> models._VectorisedFloat: # noqa
+ return self.normed_concentration_function(time)
halftime = models.PeriodicInterval(120, 60)
@@ -67,7 +67,9 @@ def known_concentrations(func):
virus=models.Virus.types['SARS_CoV_2_B117'],
expiration=models.Expiration.types['Talking']
)
- return KnownConcentrations(dummy_room, dummy_ventilation, dummy_infected_population, func)
+ normed_func = lambda x: func(x) / dummy_infected_population.emission_rate_when_present()
+ return KnownNormedconcentration(dummy_room, dummy_ventilation,
+ dummy_infected_population, normed_func)
@pytest.mark.parametrize(
diff --git a/cara/tests/test_monte_carlo_full_models.py b/cara/tests/test_monte_carlo_full_models.py
index 5cf88198..08bd2dad 100644
--- a/cara/tests/test_monte_carlo_full_models.py
+++ b/cara/tests/test_monte_carlo_full_models.py
@@ -238,7 +238,7 @@ def skagit_chorale_mc():
["shared_office_mc", 10.7, 0.32, 57.24, 654],
["classroom_mc", 36.1, 6.85, 780.0, 28464],
["ski_cabin_mc", 16.3, 0.49, 35.94, 7404],
- ["gym_mc", 2.25, 0.63, 0.7842, 984],
+ ["gym_mc", 2.25, 0.63, 0.7842, 1968],
["waiting_room_mc", 9.72, 1.36, 34.26, 3534],
["skagit_chorale_mc",29.9, 17.9, 190.0, 141400],
]
From 5d4dc4b91d7b3c22bc30ded340e7a65f3cd2f3aa Mon Sep 17 00:00:00 2001
From: jdevine
Date: Tue, 14 Sep 2021 16:22:49 +0200
Subject: [PATCH 06/12] reverted model_generator default virus type
---
cara/apps/calculator/model_generator.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cara/apps/calculator/model_generator.py b/cara/apps/calculator/model_generator.py
index 61f4e9fd..06c29229 100644
--- a/cara/apps/calculator/model_generator.py
+++ b/cara/apps/calculator/model_generator.py
@@ -117,7 +117,7 @@ class FormData:
'simulation_name': _NO_DEFAULT,
'total_people': _NO_DEFAULT,
'ventilation_type': 'no_ventilation',
- 'virus_type': 'SARS_CoV_2_B16172',
+ 'virus_type': 'SARS_CoV_2',
'volume_type': _NO_DEFAULT,
'window_type': 'window_sliding',
'window_height': 0.,
From af3ded7935f09eaa958cd624e12ad42a173281e9 Mon Sep 17 00:00:00 2001
From: Nicolas Mounet
Date: Thu, 16 Sep 2021 13:55:04 +0200
Subject: [PATCH 07/12] Remove suprespreading event from the expiration types
---
cara/models.py | 74 ++++++++++++++----------
cara/tests/conftest.py | 6 +-
cara/tests/models/test_exposure_model.py | 6 +-
cara/tests/test_known_quantities.py | 18 +++---
4 files changed, 62 insertions(+), 42 deletions(-)
diff --git a/cara/models.py b/cara/models.py
index 98245997..296c260c 100644
--- a/cara/models.py
+++ b/cara/models.py
@@ -620,7 +620,6 @@ _ExpirationBase.types = {
'Talking': Expiration((1., 1., 1.)),
'Shouting': Expiration((1., 5., 5.)),
'Singing': Expiration((1., 5., 5.)),
- 'Superspreading event': Expiration((np.inf, 0., 0.)),
}
@@ -669,44 +668,21 @@ class Population:
@dataclass(frozen=True)
-class InfectedPopulation(Population):
+class _PopulationWithVirus(Population):
#: The virus with which the population is infected.
virus: Virus
- #: The type of expiration that is being emitted whilst doing the activity.
- expiration: _ExpirationBase
-
@method_cache
def emission_rate_when_present(self) -> _VectorisedFloat:
"""
- The emission rate if the infected population is present.
-
- Note that the rate is not currently time-dependent.
-
+ The emission rate if the infected population is present
+ (in virions / h). It should not be a function of time.
"""
- # Emission Rate (virions / h)
- # Note on units: exhalation rate is in m^3/h, aerosols in mL/cm^3
- # and viral load in virus/mL -> 1e6 conversion factor
- aerosols = self.expiration.aerosols(self.mask)
-
- ER = (self.virus.viral_load_in_sputum *
- self.activity.exhalation_rate *
- 10 ** 6 *
- aerosols)
-
- # For superspreading event, where ejection_factor is infinite we fix the ER
- # based on Miller et al. (2020).
- if isinstance(aerosols, np.ndarray):
- ER[np.isinf(aerosols)] = 970 * self.virus.infectious_dose
- elif np.isinf(aerosols):
- ER = 970 * self.virus.infectious_dose
-
- return ER * self.number
+ raise NotImplementedError("Subclass must implement")
def emission_rate(self, time) -> _VectorisedFloat:
"""
- The emission rate of the population.
-
+ The emission rate of the population vs time.
"""
# Note: The original model avoids time dependence on the emission rate
# at the cost of implementing a piecewise (on time) concentration function.
@@ -722,11 +698,49 @@ class InfectedPopulation(Population):
return self.emission_rate_when_present()
+@dataclass(frozen=True)
+class EmittingPopulation(_PopulationWithVirus):
+ #: The emission rate of a single individual, in virions / h.
+ known_individual_emission_rate: float
+
+ @method_cache
+ def emission_rate_when_present(self) -> _VectorisedFloat:
+ """
+ The emission rate if the infected population is present.
+ """
+ return self.known_individual_emission_rate * self.number
+
+
+@dataclass(frozen=True)
+class InfectedPopulation(_PopulationWithVirus):
+ #: The type of expiration that is being emitted whilst doing the activity.
+ expiration: _ExpirationBase
+
+ @method_cache
+ def emission_rate_when_present(self) -> _VectorisedFloat:
+ """
+ The emission rate if the infected population is present.
+ Note that the rate is not currently time-dependent.
+ """
+ # Emission Rate (virions / h)
+ # Note on units: exhalation rate is in m^3/h, aerosols in mL/cm^3
+ # and viral load in virus/mL -> 1e6 conversion factor
+
+ aerosols = self.expiration.aerosols(self.mask)
+
+ ER = (self.virus.viral_load_in_sputum *
+ self.activity.exhalation_rate *
+ 10 ** 6 *
+ aerosols)
+
+ return ER * self.number
+
+
@dataclass(frozen=True)
class ConcentrationModel:
room: Room
ventilation: _VentilationBase
- infected: InfectedPopulation
+ infected: _PopulationWithVirus
@property
def virus(self):
diff --git a/cara/tests/conftest.py b/cara/tests/conftest.py
index 9499dfcf..e828e0cd 100644
--- a/cara/tests/conftest.py
+++ b/cara/tests/conftest.py
@@ -13,13 +13,15 @@ def baseline_model():
active=models.SpecificInterval(((0., 24.), )),
air_exch=30.,
),
- infected=models.InfectedPopulation(
+ infected=models.EmittingPopulation(
number=1,
virus=models.Virus.types['SARS_CoV_2'],
presence=models.SpecificInterval(((0., 4.), (5., 8.))),
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Light activity'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
+ # superspreading event, where ejection factor is fixed based
+ # on Miller et al. (2020) - 50 represents the infectious dose.
),
)
return model
diff --git a/cara/tests/models/test_exposure_model.py b/cara/tests/models/test_exposure_model.py
index 11a135a1..ac82dae3 100644
--- a/cara/tests/models/test_exposure_model.py
+++ b/cara/tests/models/test_exposure_model.py
@@ -144,13 +144,15 @@ def conc_model():
return models.ConcentrationModel(
models.Room(25),
models.AirChange(always, 5),
- models.InfectedPopulation(
+ models.EmittingPopulation(
number=1,
presence=interesting_times,
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Seated'],
virus=models.Virus.types['SARS_CoV_2'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
+ # superspreading event, where ejection factor is fixed based
+ # on Miller et al. (2020) - 50 represents the infectious dose.
)
)
diff --git a/cara/tests/test_known_quantities.py b/cara/tests/test_known_quantities.py
index 89c0268d..d1db42c2 100644
--- a/cara/tests/test_known_quantities.py
+++ b/cara/tests/test_known_quantities.py
@@ -66,13 +66,15 @@ def build_model(interval_duration):
active=models.PeriodicInterval(period=120, duration=interval_duration),
q_air_mech=500.,
),
- infected=models.InfectedPopulation(
+ infected=models.EmittingPopulation(
number=1,
virus=models.Virus.types['SARS_CoV_2'],
presence=models.SpecificInterval(((0., 4.), (5., 8.))),
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Light activity'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
+ # superspreading event, where ejection factor is fixed based
+ # on Miller et al. (2020) - 50 represents the infectious dose.
),
)
return model
@@ -226,13 +228,13 @@ def build_hourly_dependent_model(
outside_temp=outside_temp,
window_height=1.6, opening_length=0.6,
),
- infected=models.InfectedPopulation(
+ infected=models.EmittingPopulation(
number=1,
virus=models.Virus.types['SARS_CoV_2'],
presence=models.SpecificInterval(intervals_presence_infected),
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Light activity'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
),
)
return model
@@ -247,13 +249,13 @@ def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)):
outside_temp=models.PiecewiseConstant((0., 24.), (outside_temp,)),
window_height=1.6, opening_length=0.6,
),
- infected=models.InfectedPopulation(
+ infected=models.EmittingPopulation(
number=1,
virus=models.Virus.types['SARS_CoV_2'],
presence=models.SpecificInterval(((0., 4.), (5., 7.5))),
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Light activity'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
),
)
return model
@@ -275,13 +277,13 @@ def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5
model = models.ConcentrationModel(
room=models.Room(volume=75),
ventilation=vent,
- infected=models.InfectedPopulation(
+ infected=models.EmittingPopulation(
number=1,
virus=models.Virus.types['SARS_CoV_2'],
presence=models.SpecificInterval(((0., 4.), (5., 7.5))),
mask=models.Mask.types['No mask'],
activity=models.Activity.types['Light activity'],
- expiration=models.Expiration.types['Superspreading event'],
+ known_individual_emission_rate=970 * 50,
),
)
return model
From 480e5237c976dff48f628e12e3950a66c4eee4c5 Mon Sep 17 00:00:00 2001
From: Luis Aleixo
Date: Tue, 28 Sep 2021 09:48:26 +0200
Subject: [PATCH 08/12] ipympl (Matplotlib Jupyter Extension) version
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 9e5eb596..6be0d0f9 100644
--- a/setup.py
+++ b/setup.py
@@ -20,7 +20,7 @@ REQUIREMENTS: dict = {
'core': [
'dataclasses; python_version < "3.7"',
'ipykernel',
- 'ipympl',
+ 'ipympl != 0.8.0',
'ipywidgets',
'Jinja2',
'loky',
From dfc66991f4efe2bfec0b8564c3de431fd8ec6dcf Mon Sep 17 00:00:00 2001
From: Phil Elson
Date: Tue, 10 Aug 2021 17:34:37 +0200
Subject: [PATCH 09/12] Move QR generation to the browser, saving some time and
effort on the server.
---
cara/apps/calculator/report_generator.py | 23 ++++-----------
cara/apps/calculator/static/js/pdf.js | 29 -------------------
.../templates/base/calculator.report.html.j2 | 16 ++++++----
cara/tests/apps/calculator/test_webapp.py | 16 +++++-----
requirements.txt | 1 -
setup.py | 1 -
6 files changed, 24 insertions(+), 62 deletions(-)
delete mode 100644 cara/apps/calculator/static/js/pdf.js
diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py
index 773481f3..2f55e3eb 100644
--- a/cara/apps/calculator/report_generator.py
+++ b/cara/apps/calculator/report_generator.py
@@ -1,17 +1,15 @@
import concurrent.futures
import base64
import dataclasses
-from datetime import datetime, timedelta
+from datetime import datetime
import io
+import json
import typing
import urllib
import zlib
-import loky
import jinja2
import numpy as np
-import qrcode
-import json
from cara import models
from ... import monte_carlo as mc
@@ -123,7 +121,7 @@ def calculate_report_data(model: models.ExposureModel):
}
-def generate_qr_code(base_url, calculator_prefix, form: FormData):
+def generate_permalink(base_url, calculator_prefix, form: FormData):
form_dict = FormData.to_dict(form, strip_defaults=True)
# Generate the calculator URL arguments that would be needed to re-create this
@@ -136,20 +134,9 @@ def generate_qr_code(base_url, calculator_prefix, form: FormData):
qr_url = f"{base_url}/_c/{compressed_args}"
url = f"{base_url}{calculator_prefix}?{args}"
- qr = qrcode.QRCode(
- version=1,
- error_correction=qrcode.constants.ERROR_CORRECT_H,
- box_size=10,
- border=4,
- )
- qr.add_data(qr_url)
- qr.make(fit=True)
- img = qr.make_image(fill_color="black", back_color="white").convert('RGB')
-
return {
- 'image': img2base64(_img2bytes(img)),
'link': url,
- 'qr_url': qr_url,
+ 'shortened': qr_url,
}
@@ -313,7 +300,7 @@ class ReportGenerator:
context['alternative_scenarios'] = comparison_report(
alternative_scenarios, scenario_sample_times, executor_factory=executor_factory,
)
- context['qr_code'] = generate_qr_code(base_url, self.calculator_prefix, form)
+ context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form)
context['calculator_prefix'] = self.calculator_prefix
context['scale_warning'] = {
'level': 'yellow-2',
diff --git a/cara/apps/calculator/static/js/pdf.js b/cara/apps/calculator/static/js/pdf.js
deleted file mode 100644
index 6afb4fea..00000000
--- a/cara/apps/calculator/static/js/pdf.js
+++ /dev/null
@@ -1,29 +0,0 @@
-function generate_pdf_version(qr_link) {
- const pdf_version = this.document.getElementById("body");
-
- // PDF styling
- var opt = {
- filename: 'myfile.pdf',
- image: { type: 'jpeg', quality: 0.98 },
- html2canvas: { scale: 2, width: 1200, windowWidth: 1200 },
- enableLinks: false,
- jsPDF: {
- unit: 'pt',
- format: 'letter',
- orientation: 'portrait',
- },
- pagebreak: { mode: '', avoid: '.break-avoid' },
- };
- html2pdf().set(opt).from(pdf_version).toPdf().get('pdf').then(function(pdf) {
- var totalPages = pdf.internal.getNumberOfPages();
- pdf.setPage(1);
- pdf.link(530, 25, 60, 60, { url: qr_link }); //Hyperlink to reproduce results
-
- for (i = 1; i <= totalPages; i++) {
- pdf.setPage(i);
- pdf.setFontSize(10);
- pdf.setTextColor(150);
- pdf.text('Page ' + i + ' of ' + totalPages, (pdf.internal.pageSize.getWidth() / 2.25), (pdf.internal.pageSize.getHeight() - 10));
- }
- }).save();
-};
\ No newline at end of file
diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2
index 146478dc..128dd21d 100644
--- a/cara/apps/calculator/templates/base/calculator.report.html.j2
+++ b/cara/apps/calculator/templates/base/calculator.report.html.j2
@@ -24,8 +24,6 @@
Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}
- {# To be replaced by "Generate PDF" #}
-
{% endblock report_header %}
@@ -162,10 +160,10 @@
-
+
- Click the QR code to regenerate the report and get a shareable link. Alternatively, scan to regenerate the report. Mobile-friendly app coming soon!
+ Click the QR code to regenerate the report and get a shareable link. Alternatively, scan to regenerate the report.