backend model changes
This commit is contained in:
parent
70ccfaa978
commit
5b578c15ef
4 changed files with 73 additions and 4 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -847,6 +847,7 @@ baseline_model = models.ExposureModel(
|
|||
mask=models.Mask.types['No mask'],
|
||||
host_immunity=0.,
|
||||
),
|
||||
geographical_data=models.Cases(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue