backend model changes

This commit is contained in:
Luis Aleixo 2022-09-20 16:00:25 +02:00
parent 70ccfaa978
commit 5b578c15ef
4 changed files with 73 additions and 4 deletions

View file

@ -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 CAiMIRA version (found at ``caimira.__version__``).
__version__ = "4.3"
__version__ = "4.4"
class BaseRequestHandler(RequestHandler):

View file

@ -57,6 +57,10 @@ class FormData:
location_name: str
location_latitude: float
location_longitude: float
geographic_population: int
geographic_cases: int
ascertainment_bias: str
p_recurrent_option: str
mask_type: str
mask_wearing_option: str
mechanical_ventilation_type: str
@ -116,6 +120,10 @@ class FormData:
'location_latitude': _NO_DEFAULT,
'location_longitude': _NO_DEFAULT,
'location_name': _NO_DEFAULT,
'geographic_population': 0,
'geographic_cases': 0,
'ascertainment_bias': 'confidence_low',
'p_recurrent_option': 'p_recurrent_event',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': 'not-applicable',
@ -261,7 +269,9 @@ class FormData:
('volume_type', VOLUME_TYPES),
('window_opening_regime', WINDOWS_OPENING_REGIMES),
('window_type', WINDOWS_TYPES),
('event_month', MONTH_NAMES)]
('event_month', MONTH_NAMES),
('ascertainment_bias', CONFIDENCE_LEVEL_OPTIONS),]
for attr_name, valid_set in validation_tuples:
if getattr(self, attr_name) not in valid_set:
raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}")
@ -329,6 +339,11 @@ class FormData:
),
short_range = tuple(short_range),
exposed=self.exposed_population(),
geographical_data=mc.Cases(
geographic_population=self.geographic_population,
geographic_cases=self.geographic_cases,
ascertainment_bias=CONFIDENCE_LEVEL_OPTIONS[self.ascertainment_bias],
),
)
def build_model(self, sample_size=_DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel:
@ -759,6 +774,9 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]:
'location_latitude': 46.20833,
'location_longitude': 6.14275,
'location_name': 'Geneva',
'geographic_population': 0,
'geographic_cases': 0,
'ascertainment_bias': 'confidence_low',
'mask_type': 'Type I',
'mask_wearing_option': 'mask_off',
'mechanical_ventilation_type': '',
@ -794,9 +812,8 @@ VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_ALPHA', 'SARS_CoV_2_BETA','SARS_CoV_2_G
VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'}
WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'}
WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'}
COFFEE_OPTIONS_INT = {'coffee_break_0': 0, 'coffee_break_1': 1, 'coffee_break_2': 2, 'coffee_break_4': 4}
CONFIDENCE_LEVEL_OPTIONS = {'confidence_low': 10, 'confidence_medium': 5, 'confidence_high': 2}
MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December',

View file

@ -847,6 +847,7 @@ baseline_model = models.ExposureModel(
mask=models.Mask.types['No mask'],
host_immunity=0.,
),
geographical_data=models.Cases(),
)

View file

@ -37,6 +37,7 @@ import typing
import numpy as np
from scipy.interpolate import interp1d
import scipy.stats as sct
if not typing.TYPE_CHECKING:
from memoization import cached
@ -911,6 +912,33 @@ class InfectedPopulation(_PopulationWithVirus):
return self.expiration.particle
@dataclass(frozen=True)
class Cases:
"""
The geographical data to calculate the probability of having at least 1
new infection in a specific event.
"""
#: Geographic location population
geographic_population: int = 0
#: Geographic location new cases
geographic_cases: int = 0
#: Number of new cases confidence level
ascertainment_bias: int = 0
def probability_random_individual(self) -> _VectorisedFloat:
"""Probability that a randomly selected individual in a focal population is infected."""
return self.geographic_cases*self.ascertainment_bias/self.geographic_population
def probability_meet_infected_person(self, event, x) -> _VectorisedFloat:
"""
Probability to meet x infected persons in an event.
From https://doi.org/10.1038/s41562-020-01000-9.
"""
return sct.binom.pmf(x, event, self.probability_random_individual())
@dataclass(frozen=True)
class ConcentrationModel:
room: Room
@ -1280,6 +1308,9 @@ class ExposureModel:
#: The population of non-infected people to be used in the model.
exposed: Population
#: Geographical data
geographical_data: Cases
#: The number of times the exposure event is repeated (default 1).
repeats: int = 1
@ -1435,6 +1466,26 @@ class ExposureModel:
return (1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose *
self.concentration_model.virus.transmissibility_factor)))) * 100
def total_probability_rule(self) -> _VectorisedFloat:
if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0):
sum_probability = 0.0
# Create an equivalent exposure model but with i infected cases
total_people = self.concentration_model.infected.number + self.exposed.number
X = (total_people if total_people < 10 else 10)
# The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability.
# To be on the safe side, a hard coded limit with a safety margin of 2x was set.
# Therefore we decided a hard limit of 10 infected people.
for x in range(1, X):
exposure_model = nested_replace(
self, {'concentration_model.infected.number': x}
)
prob_exposed_occupant = exposure_model.infection_probability().mean() / 100
# By means of a Binomial Distribution
sum_probability += (prob_exposed_occupant)*self.geographical_data.probability_meet_infected_person(self.exposed.number, x)
return sum_probability * 100
else:
return 0
def expected_new_cases(self) -> _VectorisedFloat:
# Create an equivalent exposure model without short-range interactions, if any.
if (len(self.short_range) == 0):