Merge branch 'conversation_profile' into 'master'
Added new expiration profile "Conversation", changed office activity type + new activity "call centre" Closes #93 and #96 See merge request cara/cara!86
This commit is contained in:
commit
22c93b05f6
6 changed files with 120 additions and 45 deletions
|
|
@ -97,7 +97,9 @@ As an example, for a shared office with 4 people, where one person is infected,
|
|||
|
||||
There are three predefined activities in the tool at present.
|
||||
|
||||
**Office / Meeting ** = All persons seated, talking. Everyone (occupants and infected occupants) is treated the same in this model.
|
||||
**Office / Meeting ** = All persons seated, talking occasionally (1/3rd of the time, with normal breathing the other 2/3rds of the time). Everyone (occupants and infected occupants) is treated the same in this model.
|
||||
|
||||
**Call Centre** = All persons seated, all talking simultaneously, all the time. This is a conservative profile (i.e. will show in increased ``P(i)`` compared to office/meeting) if used for office activity.
|
||||
|
||||
**Workshop** = Based on a mechanical assembly workshop, all persons are doing light exercise (standing, moving around, using tools) and talking. Everyone (occupants and infected occupants) is treated the same in this model.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ from dataclasses import dataclass
|
|||
import html
|
||||
import typing
|
||||
|
||||
import numpy as np
|
||||
|
||||
from cara import models
|
||||
from cara import data
|
||||
|
||||
|
|
@ -161,6 +163,67 @@ class FormData:
|
|||
else:
|
||||
return ventilation
|
||||
|
||||
def mask(self) -> models.Mask:
|
||||
# Initializes the mask type if mask wearing is "continuous", otherwise instantiates the mask attribute as
|
||||
# the "No mask"-mask
|
||||
mask = models.Mask.types[self.mask_type if self.mask_wearing == "continuous" else 'No mask']
|
||||
return mask
|
||||
|
||||
def infected_population(self) -> models.InfectedPopulation:
|
||||
# Initializes the virus as SARS_Cov_2
|
||||
virus = models.Virus.types['SARS_CoV_2']
|
||||
|
||||
scenario_activity_and_expiration = {
|
||||
'office': (
|
||||
'Seated',
|
||||
# Mostly silent in the office, but 1/3rd of time talking.
|
||||
{'Talking': 1, 'Breathing': 2}
|
||||
),
|
||||
'callcentre': ('Seated', 'Talking'),
|
||||
'training': ('Light exercise', 'Talking'),
|
||||
'workshop': ('Light exercise', 'Talking'),
|
||||
}
|
||||
|
||||
[activity_defn, expiration_defn] = scenario_activity_and_expiration[self.activity_type]
|
||||
activity = models.Activity.types[activity_defn]
|
||||
expiration = build_expiration(expiration_defn)
|
||||
|
||||
infected_occupants = self.infected_people
|
||||
|
||||
infected = models.InfectedPopulation(
|
||||
number=infected_occupants,
|
||||
virus=virus,
|
||||
presence=self.infected_present_interval(),
|
||||
mask=self.mask(),
|
||||
activity=activity,
|
||||
expiration=expiration
|
||||
)
|
||||
return infected
|
||||
|
||||
def exposed_population(self) -> models.Population:
|
||||
scenario_activity = {
|
||||
'office': 'Seated',
|
||||
'callcentre': 'Seated',
|
||||
'training': 'Light exercise',
|
||||
'workshop': 'Light exercise',
|
||||
}
|
||||
|
||||
activity_defn = scenario_activity[self.activity_type]
|
||||
activity = models.Activity.types[activity_defn]
|
||||
|
||||
infected_occupants = self.infected_people
|
||||
# The number of exposed occupants is the total number of occupants
|
||||
# minus the number of infected occupants.
|
||||
exposed_occupants = self.total_people - infected_occupants
|
||||
|
||||
exposed = models.Population(
|
||||
number=exposed_occupants,
|
||||
presence=self.exposed_present_interval(),
|
||||
activity=activity,
|
||||
mask=self.mask(),
|
||||
)
|
||||
return exposed
|
||||
|
||||
def coffee_break_times(self) -> typing.Tuple[typing.Tuple[int, int]]:
|
||||
if not self.coffee_breaks:
|
||||
return ()
|
||||
|
|
@ -228,6 +291,38 @@ class FormData:
|
|||
return self.present_interval(self.activity_start, self.activity_finish)
|
||||
|
||||
|
||||
def build_expiration(expiration_definition) -> models.Expiration:
|
||||
if isinstance(expiration_definition, str):
|
||||
return models.Expiration.types[expiration_definition]
|
||||
elif isinstance(expiration_definition, dict):
|
||||
return expiration_blend({
|
||||
build_expiration(exp): amount
|
||||
for exp, amount in expiration_definition.items()
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def expiration_blend(expiration_weights: typing.Dict[models.Expiration, int]) -> models.Expiration:
|
||||
"""
|
||||
Combine together multiple types of Expiration, using a weighted mean to
|
||||
compute their ejection factor and particle sizes.
|
||||
|
||||
"""
|
||||
ejection_factor = np.zeros(4)
|
||||
particle_sizes = np.zeros(4)
|
||||
|
||||
total_weight = 0
|
||||
for expiration, weight in expiration_weights.items():
|
||||
total_weight += weight
|
||||
ejection_factor += np.array(expiration.ejection_factor) * weight
|
||||
particle_sizes += np.array(expiration.particle_sizes) * weight
|
||||
|
||||
return models.Expiration(
|
||||
ejection_factor=tuple(ejection_factor/total_weight),
|
||||
particle_sizes=tuple(particle_sizes/total_weight),
|
||||
)
|
||||
|
||||
|
||||
def model_from_form(form: FormData) -> models.ExposureModel:
|
||||
# Initializes room with volume either given directly or as product of area and height
|
||||
if form.volume_type == 'room_volume':
|
||||
|
|
@ -236,50 +331,14 @@ def model_from_form(form: FormData) -> models.ExposureModel:
|
|||
volume = form.floor_area * form.ceiling_height
|
||||
room = models.Room(volume=volume)
|
||||
|
||||
# Initializes the virus as SARS_Cov_2
|
||||
virus = models.Virus.types['SARS_CoV_2']
|
||||
|
||||
# Initializes the mask type if mask wearing is "continuous", otherwise instantiates the mask attribute as
|
||||
# the "No mask"-mask
|
||||
mask = models.Mask.types[form.mask_type if form.mask_wearing == "continuous" else 'No mask']
|
||||
|
||||
# A dictionary containing the mapping of activities listed in the UI to the activity level and expiration level
|
||||
# of the infected and exposed occupants respectively.
|
||||
# I.e. (infected_activity, infected_expiration), (exposed_activity, exposed_expiration)
|
||||
|
||||
activity_dict = {'office': (('Seated', 'Talking'), ('Seated', 'Talking')),
|
||||
'training': (('Light exercise', 'Talking'), ('Seated', 'Whispering')),
|
||||
'workshop': (('Light exercise', 'Talking'), ('Light exercise', 'Talking'))}
|
||||
|
||||
(infected_activity, infected_expiration), (exposed_activity, exposed_expiration) = activity_dict[form.activity_type]
|
||||
# Converts these strings to Activity and Expiration instances
|
||||
infected_activity, exposed_activity = models.Activity.types[infected_activity], models.Activity.types[exposed_activity]
|
||||
infected_expiration, exposed_expiration = models.Expiration.types[infected_expiration], models.Expiration.types[exposed_expiration]
|
||||
|
||||
infected_occupants = form.infected_people
|
||||
# Defines the number of exposed occupants as the total number of occupants minus the number of infected occupants
|
||||
exposed_occupants = form.total_people - infected_occupants
|
||||
|
||||
# Initializes and returns a model with the attributes defined above
|
||||
return models.ExposureModel(
|
||||
concentration_model=models.ConcentrationModel(
|
||||
room=room,
|
||||
ventilation=form.ventilation(),
|
||||
infected=models.InfectedPopulation(
|
||||
number=infected_occupants,
|
||||
virus=virus,
|
||||
presence=form.infected_present_interval(),
|
||||
mask=mask,
|
||||
activity=infected_activity,
|
||||
expiration=infected_expiration
|
||||
),
|
||||
infected=form.infected_population(),
|
||||
),
|
||||
exposed=models.Population(
|
||||
number=exposed_occupants,
|
||||
presence=form.exposed_present_interval(),
|
||||
activity=exposed_activity,
|
||||
mask=mask,
|
||||
)
|
||||
exposed=form.exposed_population()
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -323,7 +382,7 @@ def baseline_raw_form_data():
|
|||
}
|
||||
|
||||
|
||||
ACTIVITY_TYPES = {'office', 'training', 'workshop'}
|
||||
ACTIVITY_TYPES = {'office', 'training', 'callcentre', 'workshop'}
|
||||
EVENT_TYPES = {'single_event', 'recurrent_event'}
|
||||
MECHANICAL_VENTILATION_TYPES = {'air_changes', 'air_supply', 'not-applicable'}
|
||||
MASK_TYPES = {'Type I', 'FFP2'}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@
|
|||
|
||||
Activity type: <select id="activity_type" name="activity_type">
|
||||
<option value="office">Office/Meeting</option>
|
||||
<option value="callcentre">Call Centre</option>
|
||||
<option value="workshop">Workshop</option>
|
||||
<option value="training">Training</option>
|
||||
</select><br>
|
||||
|
|
@ -206,8 +207,9 @@
|
|||
<b>Activity types:</b><br>
|
||||
The type of activity that includes both the infected and exposed persons:
|
||||
<ul>
|
||||
<li>Office/Meeting = typical scenario all persons seated, talking.</li>
|
||||
<li>Office/Meeting = typical scenario all persons seated, in conversation (talking 33% of the time, otherwise breathing normally).</li>
|
||||
<li>Workshop = assembly workshop environment, all persons doing light exercise, talking.</li>
|
||||
<li>Call Centre = A conservative assumption for office spaces, assumes all occupants are seated and talking continuously.</li>
|
||||
<li>Training = one person (the trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the trainer is the infected person, for the worst case scenario.</li>
|
||||
</ul>
|
||||
<b>Activity breaks:</b><br>
|
||||
|
|
|
|||
|
|
@ -67,9 +67,12 @@
|
|||
<li><p class="data_text">Number of attendees and infected people: {{ form.total_people }} in attendance, of whom {{ form.infected_people }}
|
||||
{{ "is" if form.infected_people == 1 else "are" }}
|
||||
infected.</p></li>
|
||||
<li><p class="data_text">Activity type:
|
||||
<li><p class="data_text">
|
||||
Activity type:
|
||||
{% if form.activity_type == "office" %}
|
||||
Office/Meeting – typical scenario with all persons seated, talking.
|
||||
Office/Meeting – typical scenario with all persons seated, talking occasionally.
|
||||
{% elif form.activity_type == "callcentre" %}
|
||||
Call Centre = typical office scenario with all persons seated, all talking continuously.
|
||||
{% elif form.activity_type == "workshop" %}
|
||||
Workshop = assembly workshop environment, all persons doing light exercise, talking.
|
||||
{% elif form.activity_type == "training" %}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ class MultipleVentilation:
|
|||
@abstractmethod
|
||||
def air_exchange(self, room: Room, time: float) -> float:
|
||||
"""
|
||||
Returns the rate at which air is being exchanged in the given room
|
||||
Returns the rate at which air is being exchanged in the given room
|
||||
at a given time (in hours).
|
||||
"""
|
||||
return sum([ventilation.air_exchange(room,time)
|
||||
|
|
@ -271,7 +271,7 @@ class AirChange(Ventilation):
|
|||
#: The interval in which the ventilation is operating.
|
||||
active: Interval
|
||||
|
||||
#: The rate (in h^-1) at which the ventilation exchanges all the air
|
||||
#: The rate (in h^-1) at which the ventilation exchanges all the air
|
||||
# of the room (when switched on)
|
||||
air_exch: float
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,15 @@ def test_model_from_dict(baseline_form_data):
|
|||
# assert model.ventilation == cara.models.Ventilation()
|
||||
|
||||
|
||||
def test_blend_expiration():
|
||||
blend = {'Breathing': 2, 'Talking': 1}
|
||||
r = model_generator.build_expiration(blend)
|
||||
expected = models.Expiration(
|
||||
(0.13466666666666668, 0.02866666666666667, 0.004333333333333334, 0.005)
|
||||
)
|
||||
assert r == expected
|
||||
|
||||
|
||||
def test_ventilation_window(baseline_form):
|
||||
room = models.Room(75)
|
||||
window = models.WindowOpening(
|
||||
|
|
|
|||
Loading…
Reference in a new issue