Allow the COVID calculator to have a blended Expiration type to represent conversations.

This commit is contained in:
Phil Elson 2020-11-12 20:07:38 +01:00
parent 842b5418f2
commit b4ebad785f
4 changed files with 109 additions and 43 deletions

View file

@ -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,51 +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', 'Conversation'), ('Seated', 'Conversation')),
'callcentre': (('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()
)

View file

@ -67,12 +67,13 @@
<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 occasionally.
{% elif form.activity_type == "callcentre" %}
Call Centre = typical office scenario with all persons seated, all talking continuously.
{% elif form.activity_type == "workshop" %}
{% elif form.activity_type == "workshop" %}
Workshop = assembly workshop environment, all persons doing light exercise, talking.
{% elif form.activity_type == "training" %}
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.

View file

@ -375,10 +375,8 @@ class Expiration:
Expiration.types = {
#added new profile - Conversation = weighting of 1/3rd talking, 2/3 breathing.
'Breathing': Expiration((0.084, 0.009, 0.003, 0.002)),
'Whispering': Expiration((0.11, 0.014, 0.004, 0.002)),
'Conversation': Expiration((0.135, 0.029, 0.004, 0.005)),
'Talking': Expiration((0.236, 0.068, 0.007, 0.011)),
'Unmodulated Vocalization': Expiration((0.751, 0.139, 0.0139, 0.059)),
'Superspreading event': Expiration((np.inf, np.inf, np.inf, np.inf)),

View file

@ -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(