From b1aa3af33217cd751dce3ae7459a505dd626e818 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 16:20:44 +0200 Subject: [PATCH 01/10] defined global store with configuration --- caimira/store/configuration.py | 319 +++++++++++++++++++++++++++++++++ caimira/store/global_store.py | 38 ++++ 2 files changed, 357 insertions(+) create mode 100644 caimira/store/configuration.py create mode 100644 caimira/store/global_store.py diff --git a/caimira/store/configuration.py b/caimira/store/configuration.py new file mode 100644 index 00000000..cc791140 --- /dev/null +++ b/caimira/store/configuration.py @@ -0,0 +1,319 @@ +import asyncio +import os + +from caimira.store.global_store import GlobalStore + + +class Configuration: + ''' + Configuration to handle data. Contains the default values used in the model. + Might suffer update from the Data Service. + ''' + + def __init__(self): + self.data_fetched = False + self.BLOmodel = { + 'cn': {'B': 0.06, + 'L': 0.2, + 'O': 0.0010008, + }, + 'mu': { + 'B': 0.989541, + 'L': 1.38629, + 'O': 4.97673, + }, + 'sigma': { + 'B': 0.262364, + 'L': 0.506818, + 'O': 0.585005, + }, + } + self.activity_distributions = { + 'Seated': { + 'inhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108}, + }, + 'exhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108}, + }, + }, + 'Standing': { + 'inhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223}, + }, + 'exhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223}, + }, + }, + 'Light activity': { + 'inhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601}, + }, + 'exhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601}, + }, + }, + 'Moderate activity': { + 'inhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137}, + }, + 'exhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137}, + }, + }, + 'Heavy exercise': { + 'inhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565}, + }, + 'exhalation_rate': { + 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', + 'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565}, + }, + }, + } + self.symptomatic_vl_frequencies = { + 'log_variable': [2.46032, 2.67431, 2.85434, 3.06155, 3.25856, 3.47256, 3.66957, 3.85979, 4.09927, 4.27081, + 4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549, + 6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053, + 8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552], + 'frequencies': [0.001206885, 0.007851618, 0.008078144, 0.01502491, 0.013258014, 0.018528495, 0.020053765, + 0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217, + 0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991, + 0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754, + 0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788, + 0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662], + 'kernel_bandwidth': 0.1, + } + self.covid_overal_vl_data = { + 'shape_factor': 3.47, + 'scale_factor': 7.01, + 'start': 0.01, + 'stop': 0.99, + 'num': 30.0, + 'min_bound': 2, + 'max_bound': 10, + 'interpolation_fp_left': 0, + 'interpolation_fp_right': 0, + 'max_function': 0.2, + } + self.viable_to_RNA_ratio_distribution = { + 'low': 0.01, + 'high': 0.6, + } + self.infectious_dose_distribution = { + 'low': 10, + 'high': 100, + } + self.virus_distributions = { + 'SARS_CoV_2': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 1, + 'infectiousness_days': 14, + }, + 'SARS_CoV_2_ALPHA': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.78, + 'infectiousness_days': 14, + }, + 'SARS_CoV_2_BETA': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.8, + 'infectiousness_days': 14 + }, + 'SARS_CoV_2_GAMMA': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.72, + 'infectiousness_days': 14 + }, + 'SARS_CoV_2_DELTA': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.51, + 'infectiousness_days': 14 + }, + 'SARS_CoV_2_OMICRON': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.2, + 'infectiousness_days': 14 + }, + 'SARS_CoV_2_Other': { + 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', + 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', + 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', + 'transmissibility_factor': 0.1, + 'infectiousness_days': 14, + }, + } + self.mask_distributions = { + 'Type I': { + 'η_inhale': { + 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', + 'parameters': { + 'low': 0.25, + 'high': 0.80, + }, + }, + 'Known filtration efficiency of masks when exhaling?': 'No', + 'factor_exhale': 1, + }, + 'FFP2': { + 'η_inhale': { + 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', + 'parameters': { + 'low': 0.83, + 'high': 0.91, + }, + }, + 'Known filtration efficiency of masks when exhaling?': 'No', + 'factor_exhale': 1, + }, + 'Cloth': { + 'η_inhale': { + 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', + 'parameters': { + 'low': 0.05, + 'high': 0.40, + }, + }, + 'Known filtration efficiency of masks when exhaling?': 'Yes', + 'η_exhale': { + 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', + 'parameters': { + 'low': 0.20, + 'high': 0.50, + }, + }, + 'factor_exhale': 1, + }, + } + self.expiration_BLO_factors = { + 'Breathing': {'B': 1., 'L': 0., 'O': 0., }, + 'Speaking': {'B': 1., 'L': 1., 'O': 1., }, + 'Singing': {'B': 1., 'L': 5., 'O': 5., }, + 'Shouting': {'B': 1., 'L': 5., 'O': 5., }, + } + self.long_range_expiration_distributions = { + 'minimum_diameter': 0.1, + 'maximum_diameter': 30, + } + self.short_range_expiration_distributions = { + 'minimum_diameter': 0.1, + 'maximum_diameter': 100, + } + self.short_range_distances = { + 'minimum_distance': 0.5, + 'maximum_distance': 2., + } + + #################################### + + self.room = { + 'defaults': { + 'inside_temp': 293, + 'humidity_with_heating': 0.3, + 'humidity_without_heating': 0.5, + }, + } + self.ventilation = { + 'natural': { + 'discharge_factor': { + 'sliding': 0.6, + }, + }, + 'infiltration_ventilation': 0.25, + } + self.particle = { + 'evaporation_factor': 0.3, + } + self.population_with_virus = { + 'fraction_of_infectious_virus': 1, + } + self.concentration_model = { + 'min_background_concentration': 0., + 'CO2_concentration_model': { + 'CO2_atmosphere_concentration': 440.44, + 'CO2_fraction_exhaled': 0.042, + }, + } + self.short_range_model = { + 'dilution_factor': { + 'mouth_diameter': 0.02, + 'exhalation_coefficient': 2, + 'tstar': 2, + 'penetration_coefficients': { + '𝛽r1': 0.18, '𝛽r2': .2, '𝛽x1': 2.4, + }, + }, + } + self.exposure_model = { + 'repeats': 1, + } + self.conditional_prob_inf_given_viral_load = { + 'lower_percentile': 0.05, + 'upper_percentile': 0.95, + 'min_vl': 2, + 'max_vl': 10, + } + self.monte_carlo_sample_size = 250000 + self.population_scenario_activity = { + 'office': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 2}}, + 'smallmeeting': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': None}}, + 'largemeeting': {'activity': 'Standing', 'expiration': {'Speaking': 1, 'Breathing': 2}}, + 'callcenter': {'activity': 'Seated', 'expiration': {'Speaking': 1}}, + 'controlroom-day': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 1}}, + 'controlroom-night': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 9}}, + 'library': {'activity': 'Seated', 'expiration': {'Breathing': 1}}, + 'lab': {'activity': 'Light activity', 'expiration': {'Speaking': 1, 'Breathing': 1}}, + 'workshop': {'activity': 'Moderate activity', 'expiration': {'Speaking': 1, 'Breathing': 1}}, + 'training': {'activity': 'Standing', 'expiration': {'Speaking': 1}}, + 'training_attendee': {'activity': 'Seated', 'expiration': {'Breathing': 1}}, + 'gym': {'activity': 'Heavy exercise', 'expiration': {'Breathing': 1}}, + 'household-day': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}}, + 'household-night': {'activity': 'Seated', 'expiration': {'Breathing': 7, 'Speaking': 3}}, + 'primary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}}, + 'secondary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 7, 'Speaking': 3}}, + 'university': {'activity': 'Seated', 'expiration': {'Breathing': 9, 'Speaking': 1}}, + 'restaurant': {'activity': 'Seated', 'expiration': {'Breathing': 1, 'Speaking': 9}}, + 'precise': {'activity': None, 'expiration': None}, + } + + async def populate_data(self): + """ + Fetches data from the API and populates the configuration object. + """ + if not self.data_fetched and os.environ.get('DATA_SERVICE_ENABLED', 'False').lower() == 'true': + # Fetch and populate data from API + await GlobalStore.populate_from_api() + data = GlobalStore.get_data()['data'] + + # Dynamically set attributes based on the data fetched from the API + for attr_name, value in data.items(): + setattr(self, attr_name, value) + + self.data_fetched = True + return + + +config = Configuration() + +asyncio.run(config.populate_data()) diff --git a/caimira/store/global_store.py b/caimira/store/global_store.py new file mode 100644 index 00000000..5d30a021 --- /dev/null +++ b/caimira/store/global_store.py @@ -0,0 +1,38 @@ +import os + +from caimira.store.data_service import DataService + + +class GlobalStore: + ''' + Singleton pattern - ensure that there's only one instance of + GlobalStore throughout the application + ''' + + _instance = None + + def __new__(self): + if self._instance is None: + self._instance = super().__new__(self) + self._instance = {} + + return self._instance + + @classmethod + async def populate_from_api(self): + data_service_credentials = { + 'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None), + 'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None), + } + data_service = None + data_service_enabled = os.environ.get( + 'DATA_SERVICE_ENABLED', 'False').lower() == 'true' + if data_service_enabled: + data_service = DataService(data_service_credentials) + self._instance = await data_service.fetch() + else: + print('Data service not enabled.') + + @classmethod + def get_data(self): + return self._instance From ef212f94559a6be940eac2ea7126fa18890ed278 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 16:27:27 +0200 Subject: [PATCH 02/10] moved data service to separate module and removed logic from caimira calculator module --- caimira/apps/calculator/__init__.py | 24 ------------------- .../calculator => store}/data_service.py | 10 ++++---- caimira/store/global_store.py | 15 ++++++++---- caimira/tests/test_data_service.py | 2 +- 4 files changed, 16 insertions(+), 35 deletions(-) rename caimira/{apps/calculator => store}/data_service.py (92%) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 7dc383a7..80476955 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -28,7 +28,6 @@ import tornado.log from . import markdown_tools from . import model_generator from .report_generator import ReportGenerator, calculate_report_data -from .data_service import DataService from .user import AuthenticatedUser, AnonymousUser # The calculator version is based on a combination of the model version and the @@ -105,17 +104,6 @@ class ConcentrationModel(BaseRequestHandler): from pprint import pprint pprint(requested_model_config) start = datetime.datetime.now() - - # Data Service API Integration - fetched_service_data = None - data_service: DataService = self.settings["data_service"] - if self.settings["data_service"]: - try: - fetched_service_data = await data_service.fetch() - except Exception as err: - error_message = f"Something went wrong with the data service: {str(err)}" - LOG.error(error_message, exc_info=True) - self.send_error(500, reason=error_message) try: form = model_generator.FormData.from_dict(requested_model_config) @@ -429,15 +417,6 @@ def make_app( ) template_environment.globals['get_url']=get_root_url template_environment.globals['get_calculator_url']=get_root_calculator_url - - data_service_credentials = { - 'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None), - 'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None), - } - data_service = None - data_service_enabled = os.environ.get('DATA_SERVICE_ENABLED', 'False').lower() == 'true' - if data_service_enabled: - data_service = DataService(data_service_credentials) if debug: tornado.log.enable_pretty_logging() @@ -456,9 +435,6 @@ def make_app( arve_client_secret=os.environ.get('ARVE_CLIENT_SECRET', None), arve_api_key=os.environ.get('ARVE_API_KEY', None), - # Data Service Integration - data_service=data_service, - # Process parallelism controls. There is a balance between serving a single report # requests quickly or serving multiple requests concurrently. # The defaults are: handle one report at a time, and allow parallelism diff --git a/caimira/apps/calculator/data_service.py b/caimira/store/data_service.py similarity index 92% rename from caimira/apps/calculator/data_service.py rename to caimira/store/data_service.py index f3705970..703884b8 100644 --- a/caimira/apps/calculator/data_service.py +++ b/caimira/store/data_service.py @@ -41,10 +41,11 @@ class DataService(): if (client_email == None or client_password == None): # If the credentials are not defined, an exception is raised. raise Exception("DataService credentials not set") - + http_client = AsyncHTTPClient() headers = {'Content-type': 'application/json'} - json_body = { "email": f"{client_email}", "password": f"{client_password}"} + json_body = {"email": f"{client_email}", + "password": f"{client_password}"} response = await http_client.fetch(HTTPRequest( url=self.host + '/login', @@ -52,7 +53,7 @@ class DataService(): headers=headers, body=json.dumps(json_body), ), - raise_error=True) + raise_error=True) self._access_token = json.loads(response.body)['access_token'] return self._access_token @@ -68,7 +69,6 @@ class DataService(): method='GET', headers=headers, ), - raise_error=True) + raise_error=True) return json.loads(response.body) - \ No newline at end of file diff --git a/caimira/store/global_store.py b/caimira/store/global_store.py index 5d30a021..164af849 100644 --- a/caimira/store/global_store.py +++ b/caimira/store/global_store.py @@ -1,14 +1,17 @@ import os +import logging from caimira.store.data_service import DataService +LOG = logging.getLogger(__name__) + class GlobalStore: ''' Singleton pattern - ensure that there's only one instance of GlobalStore throughout the application ''' - + _instance = None def __new__(self): @@ -28,10 +31,12 @@ class GlobalStore: data_service_enabled = os.environ.get( 'DATA_SERVICE_ENABLED', 'False').lower() == 'true' if data_service_enabled: - data_service = DataService(data_service_credentials) - self._instance = await data_service.fetch() - else: - print('Data service not enabled.') + try: + data_service = DataService(data_service_credentials) + self._instance = await data_service.fetch() + except Exception as err: + error_message = f"Something went wrong with the data service: {str(err)}" + LOG.error(error_message, exc_info=True) @classmethod def get_data(self): diff --git a/caimira/tests/test_data_service.py b/caimira/tests/test_data_service.py index 01cf176c..29c8b9e6 100644 --- a/caimira/tests/test_data_service.py +++ b/caimira/tests/test_data_service.py @@ -4,7 +4,7 @@ import unittest from unittest.mock import patch, MagicMock from tornado.httpclient import HTTPError -from caimira.apps.calculator.data_service import DataService +from caimira.store.data_service import DataService @dataclass class MockResponse: From ac050ee49cb5919dd5e9fabc8afda7cfbbc51d46 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 16:27:56 +0200 Subject: [PATCH 03/10] adapted defaults from configuration file --- caimira/apps/calculator/defaults.py | 53 ++++------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/caimira/apps/calculator/defaults.py b/caimira/apps/calculator/defaults.py index 22b157aa..8bae7e23 100644 --- a/caimira/apps/calculator/defaults.py +++ b/caimira/apps/calculator/defaults.py @@ -1,11 +1,13 @@ import typing +from caimira.store.configuration import config + # ------------------ Default form values ---------------------- # Used to declare when an attribute of a class must have a value provided, and # there should be no default value used. NO_DEFAULT = object() -DEFAULT_MC_SAMPLE_SIZE = 250_000 +DEFAULT_MC_SAMPLE_SIZE = config.monte_carlo_sample_size #: The default values for undefined fields. Note that the defaults here #: and the defaults in the html form must not be contradictory. @@ -79,57 +81,17 @@ DEFAULTS = { # ------------------ Activities ---------------------- -ACTIVITIES: typing.List[typing.Dict[str, typing.Any]] = [ - # Mostly silent in the office, but 1/3rd of time speaking. - {'name': 'office', 'activity': 'Seated', - 'expiration': {'Speaking': 1, 'Breathing': 2}}, - {'name': 'smallmeeting', 'activity': 'Seated', - 'expiration': {'Speaking': 1, 'Breathing': None}}, - # Each infected person spends 1/3 of time speaking. - {'name': 'largemeeting', 'activity': 'Standing', - 'expiration': {'Speaking': 1, 'Breathing': 2}}, - {'name': 'callcentre', 'activity': 'Seated', 'expiration': 'Speaking'}, - # Daytime control room shift, 50% speaking. - {'name': 'controlroom-day', 'activity': 'Seated', - 'expiration': {'Speaking': 1, 'Breathing': 1}}, - # Nightshift control room, 10% speaking. - {'name': 'controlroom-night', 'activity': 'Seated', - 'expiration': {'Speaking': 1, 'Breathing': 9}}, - {'name': 'library', 'activity': 'Seated', 'expiration': 'Breathing'}, - # Model 1/2 of time spent speaking in a lab. - {'name': 'lab', 'activity': 'Light activity', - 'expiration': {'Speaking': 1, 'Breathing': 1}}, - # Model 1/2 of time spent speaking in a workshop. - {'name': 'workshop', 'activity': 'Moderate activity', - 'expiration': {'Speaking': 1, 'Breathing': 1}}, - {'name': 'training', 'activity': 'Standing', 'expiration': 'Speaking'}, - {'name': 'training_attendee', 'activity': 'Seated', 'expiration': 'Breathing'}, - {'name': 'gym', 'activity': 'Heavy exercise', 'expiration': 'Breathing'}, - {'name': 'household-day', 'activity': 'Light activity', - 'expiration': {'Breathing': 5, 'Speaking': 5}}, - {'name': 'household-night', 'activity': 'Seated', - 'expiration': {'Breathing': 7, 'Speaking': 3}}, - {'name': 'primary-school', 'activity': 'Light activity', - 'expiration': {'Breathing': 5, 'Speaking': 5}}, - {'name': 'secondary-school', 'activity': 'Light activity', - 'expiration': {'Breathing': 7, 'Speaking': 3}}, - {'name': 'university', 'activity': 'Seated', - 'expiration': {'Breathing': 9, 'Speaking': 1}}, - {'name': 'restaurant', 'activity': 'Seated', - 'expiration': {'Breathing': 1, 'Speaking': 9}}, - {'name': 'precise', 'activity': None, 'expiration': None}, -] +ACTIVITIES: typing.Dict[str, typing.Dict] = config.population_scenario_activity # ------------------ Validation ---------------------- - -ACTIVITY_TYPES = [activity['name'] for activity in ACTIVITIES] +ACTIVITY_TYPES: typing.List[str] = list(ACTIVITIES.keys()) 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} MECHANICAL_VENTILATION_TYPES = { 'mech_type_air_changes', 'mech_type_air_supply', 'not-applicable'} -MASK_TYPES = {'Type I', 'FFP2', 'Cloth'} +MASK_TYPES: typing.List[str] = list(config.mask_distributions.keys()) MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', @@ -143,8 +105,7 @@ VACCINE_TYPE = ['Ad26.COV2.S_(Janssen)', 'Any_mRNA_-_heterologous', 'AZD1222_(As 'mRNA-1273_(Moderna)', 'Sputnik_V_(Gamaleya)', 'CoronaVac_(Sinovac)_and_BNT162b2_(Pfizer)'] VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'} -VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_ALPHA', 'SARS_CoV_2_BETA', - 'SARS_CoV_2_GAMMA', 'SARS_CoV_2_DELTA', 'SARS_CoV_2_OMICRON'} +VIRUS_TYPES: typing.List[str] = list(config.virus_distributions) VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'} WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'} From 3cc90593c218aa6d5c983822cf27ca88c41a4108 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 16:28:40 +0200 Subject: [PATCH 04/10] infected data in monte carlo data file with helper methods --- caimira/monte_carlo/data.py | 385 ++++++++++++++++++++++++++++-------- 1 file changed, 304 insertions(+), 81 deletions(-) diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index e0eaa09a..211ff19f 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -6,12 +6,115 @@ from scipy import special as sp from scipy.stats import weibull_min import caimira.monte_carlo as mc -from caimira.monte_carlo.sampleable import LogCustom, LogNormal,LogCustomKernel,CustomKernel,Uniform, Custom +from caimira.monte_carlo.sampleable import LogCustom, LogNormal, Normal, LogCustomKernel, CustomKernel, Uniform, Custom +from caimira.store.configuration import config sqrt2pi = np.sqrt(2.*np.pi) sqrt2 = np.sqrt(2.) +def custom_distribution_lookup(dict: dict, key_part: str) -> typing.Any: + """ + Look up a custom distribution based on a partial key. + + Args: + dict (dict): The root to search. + key_part (str): The distribution key to match. + + Returns: + str: The associated distribution. + """ + try: + for key, value in dict.items(): + if (key_part in key): + return value['associated_distribution'] + except KeyError: + return f"Key '{key_part}' not found." + + +def evaluate_reference(reference_variable: str) -> typing.Any: + """ + Evaluate a reference variable. + + Args: + reference_variable (str): The variable to evaluate. + + Returns: + Any: The evaluated value or an error message if the variable is not defined. + + """ + try: + return eval(reference_variable) + except NameError: + return f"Variable '{reference_variable}' is not defined." + + +def evaluate_custom_distribution(dist: str, params: typing.Dict) -> typing.Any: + """ + Evaluate a custom distribution. + + Args: + dist (str): The type of distribution. + params (Dict): The parameters for the distribution. + + Returns: + Any: The generated distribution. + + Raises: + ValueError: If the distribution type is not recognized. + + """ + if dist == 'Numpy Linear Space (linspace)': + return np.linspace(params['start'], params['stop'], params['num']) + elif dist == 'Numpy Normal Distribution (random.normal)': + return Normal(params['mean_gaussian'], params['standard_deviation_gaussian']) + elif dist == 'Numpy Log-normal Distribution (random.lognormal)': + return LogNormal(params['mean_gaussian'], params['standard_deviation_gaussian']) + elif dist == 'Numpy Uniform Distribution (random.uniform)': + return Uniform(params['low'], params['high']) + else: + raise ValueError('Bad request - distribution not found.') + + +def param_evaluation(root: typing.Dict, param: typing.Union[str, typing.Any]) -> typing.Any: + """ + Evaluate a parameter from a nested dictionary. + + Args: + root (dict): The root dictionary. + param (Union[str, Any]): The parameter to evaluate. + + Returns: + Any: The evaluated value. + + Raises: + TypeError: If the type of the parameter is not defined. + + """ + value = root.get(param) + + if isinstance(value, str): + if value.startswith('Ref:'): + reference_variable = value.split(' - ')[1].strip() + return evaluate_reference(reference_variable) + elif value == 'Custom': + custom_distribution: typing.Dict = custom_distribution_lookup( + root, 'custom distribution') + for d, p in custom_distribution.items(): + return evaluate_custom_distribution(d, p) + + elif isinstance(value, dict): + dist: str = root[param]['associated_distribution'] + params: typing.Dict = root[param]['parameters'] + return evaluate_custom_distribution(dist, params) + + elif isinstance(value, float) or isinstance(value, int): + return value + + else: + raise TypeError('Bad request - type not allowed.') + + @dataclass(frozen=True) class BLOmodel: """ @@ -34,25 +137,37 @@ class BLOmodel: #: cn (cm^-3) for resp. the B, L and O modes. Corresponds to the # total concentration of aerosols for each mode. - cn: typing.Tuple[float, float, float] = (0.06, 0.2, 0.0010008) + cn: typing.Tuple[float, float, float] = ( + config.BLOmodel['cn']['B'], + config.BLOmodel['cn']['L'], + config.BLOmodel['cn']['O'] + ) # Mean of the underlying normal distributions (represents the log of a # diameter in microns), for resp. the B, L and O modes. - mu: typing.Tuple[float, float, float] = (0.989541, 1.38629, 4.97673) + mu: typing.Tuple[float, float, float] = ( + config.BLOmodel['mu']['B'], + config.BLOmodel['mu']['L'], + config.BLOmodel['mu']['O'] + ) # Std deviation of the underlying normal distribution, for resp. # the B, L and O modes. - sigma: typing.Tuple[float, float, float] = (0.262364, 0.506818, 0.585005) + sigma: typing.Tuple[float, float, float] = ( + config.BLOmodel['sigma']['B'], + config.BLOmodel['sigma']['L'], + config.BLOmodel['sigma']['O'] + ) def distribution(self, d): """ Returns the raw value of the probability distribution for a given diameter d (microns). """ - return sum( (1 / d) * (A * cn / (sqrt2pi * sigma)) * - np.exp(-(np.log(d) - mu) ** 2 / (2 * sigma ** 2)) - for A,cn,mu,sigma in zip(self.BLO_factors, self.cn, - self.mu, self.sigma) ) + return sum((1 / d) * (A * cn / (sqrt2pi * sigma)) * + np.exp(-(np.log(d) - mu) ** 2 / (2 * sigma ** 2)) + for A, cn, mu, sigma in zip(self.BLO_factors, self.cn, + self.mu, self.sigma)) def integrate(self, dmin, dmax): """ @@ -60,7 +175,7 @@ class BLOmodel: probability distribution. """ result = 0. - for A,cn,mu,sigma in zip(self.BLO_factors, self.cn, self.mu, self.sigma): + for A, cn, mu, sigma in zip(self.BLO_factors, self.cn, self.mu, self.sigma): ymin = (np.log(dmin)-mu)/(sqrt2*sigma) ymax = (np.log(dmax)-mu)/(sqrt2*sigma) result += A * cn * (sp.erf(ymax)-sp.erf(ymin)) / 2. @@ -69,35 +184,55 @@ class BLOmodel: # From https://doi.org/10.1101/2021.10.14.21264988 and references therein activity_distributions = { - 'Seated': mc.Activity(LogNormal(-0.6872121723362303, 0.10498338229297108), - LogNormal(-0.6872121723362303, 0.10498338229297108)), + 'Seated': mc.Activity( + inhalation_rate=param_evaluation( + config.activity_distributions['Seated'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + config.activity_distributions['Seated'], 'exhalation_rate'), + ), - 'Standing': mc.Activity(LogNormal(-0.5742377578494785, 0.09373162411398223), - LogNormal(-0.5742377578494785, 0.09373162411398223)), + 'Standing': mc.Activity( + inhalation_rate=param_evaluation( + config.activity_distributions['Standing'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + config.activity_distributions['Standing'], 'exhalation_rate'), + ), - 'Light activity': mc.Activity(LogNormal(0.21380242785625422,0.09435378091059601), - LogNormal(0.21380242785625422,0.09435378091059601)), + 'Light activity': mc.Activity( + inhalation_rate=param_evaluation( + config.activity_distributions['Light activity'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + config.activity_distributions['Light activity'], 'exhalation_rate'), + ), - 'Moderate activity': mc.Activity(LogNormal(0.551771330362601, 0.1894616357138137), - LogNormal(0.551771330362601, 0.1894616357138137)), + 'Moderate activity': mc.Activity( + inhalation_rate=param_evaluation( + config.activity_distributions['Moderate activity'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + config.activity_distributions['Moderate activity'], 'exhalation_rate'), + ), - 'Heavy exercise': mc.Activity(LogNormal(1.1644665696723049, 0.21744554768657565), - LogNormal(1.1644665696723049, 0.21744554768657565)), + 'Heavy exercise': mc.Activity( + inhalation_rate=param_evaluation( + config.activity_distributions['Heavy exercise'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + config.activity_distributions['Heavy exercise'], 'exhalation_rate'), + ), } # From https://doi.org/10.1101/2021.10.14.21264988 and references therein symptomatic_vl_frequencies = LogCustomKernel( np.array((2.46032, 2.67431, 2.85434, 3.06155, 3.25856, 3.47256, 3.66957, 3.85979, 4.09927, 4.27081, - 4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549, - 6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053, - 8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552)), + 4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549, + 6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053, + 8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552)), np.array((0.001206885, 0.007851618, 0.008078144, 0.01502491, 0.013258014, 0.018528495, 0.020053765, - 0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217, - 0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991, - 0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754, - 0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788, - 0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662)), + 0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217, + 0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991, + 0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754, + 0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788, + 0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662)), kernel_bandwidth=0.1 ) @@ -105,61 +240,103 @@ symptomatic_vl_frequencies = LogCustomKernel( # Weibull distribution with a shape factor of 3.47 and a scale factor of 7.01. # From https://elifesciences.org/articles/65774 and first line of the figure in # https://iiif.elifesciences.org/lax:65774%2Felife-65774-fig4-figsupp3-v2.tif/full/1500,/0/default.jpg -viral_load = np.linspace(weibull_min.ppf(0.01, c=3.47, scale=7.01), - weibull_min.ppf(0.99, c=3.47, scale=7.01), 30) -frequencies_pdf = weibull_min.pdf(viral_load, c=3.47, scale=7.01) -covid_overal_vl_data = LogCustom(bounds=(2, 10), - function=lambda d: np.interp(d, viral_load, frequencies_pdf, left=0., right=0.), - max_function=0.2) +viral_load = np.linspace( + weibull_min.ppf( + config.covid_overal_vl_data['start'], + c=config.covid_overal_vl_data['shape_factor'], + scale=config.covid_overal_vl_data['scale_factor'] + ), + weibull_min.ppf( + config.covid_overal_vl_data['stop'], + c=config.covid_overal_vl_data['shape_factor'], + scale=config.covid_overal_vl_data['scale_factor'] + ), + int(config.covid_overal_vl_data['num']) +) +frequencies_pdf = weibull_min.pdf( + viral_load, + c=config.covid_overal_vl_data['shape_factor'], + scale=config.covid_overal_vl_data['scale_factor'] +) +covid_overal_vl_data = LogCustom(bounds=(config.covid_overal_vl_data['min_bound'], config.covid_overal_vl_data['max_bound']), + function=lambda d: np.interp(d, viral_load, frequencies_pdf, config.covid_overal_vl_data[ + 'interpolation_fp_left'], config.covid_overal_vl_data['interpolation_fp_right']), + max_function=config.covid_overal_vl_data['max_function']) # Derived from data in doi.org/10.1016/j.ijid.2020.09.025 and # https://iosh.com/media/8432/aerosol-infection-risk-hospital-patient-care-full-report.pdf (page 60) -viable_to_RNA_ratio_distribution = Uniform(0.01, 0.6) +viable_to_RNA_ratio_distribution = Uniform( + config.viable_to_RNA_ratio_distribution['low'], config.viable_to_RNA_ratio_distribution['high']) # From discussion with virologists -infectious_dose_distribution = Uniform(10., 100.) +infectious_dose_distribution = Uniform( + config.infectious_dose_distribution['low'], config.infectious_dose_distribution['high']) # From https://doi.org/10.1101/2021.10.14.21264988 and refererences therein virus_distributions = { 'SARS_CoV_2': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=1., - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2'], 'transmissibility_factor'), + ), 'SARS_CoV_2_ALPHA': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=0.78, - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2_ALPHA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2_ALPHA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2_ALPHA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2_ALPHA'], 'transmissibility_factor'), + ), 'SARS_CoV_2_BETA': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=0.8, - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2_BETA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2_BETA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2_BETA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2_BETA'], 'transmissibility_factor'), + ), 'SARS_CoV_2_GAMMA': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=0.72, - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2_GAMMA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2_GAMMA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2_GAMMA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2_GAMMA'], 'transmissibility_factor'), + ), 'SARS_CoV_2_DELTA': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=0.51, - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2_DELTA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2_DELTA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2_DELTA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2_DELTA'], 'transmissibility_factor'), + ), 'SARS_CoV_2_OMICRON': mc.SARSCoV2( - viral_load_in_sputum=covid_overal_vl_data, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, - transmissibility_factor=0.2, - ), + viral_load_in_sputum=param_evaluation( + config.virus_distributions['SARS_CoV_2_OMICRON'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + config.virus_distributions['SARS_CoV_2_OMICRON'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + config.virus_distributions['SARS_CoV_2_OMICRON'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + config.virus_distributions['SARS_CoV_2_OMICRON'], 'transmissibility_factor'), + ), } @@ -169,14 +346,33 @@ virus_distributions = { # https://doi.org/10.4209/aaqr.2020.08.0531 # https://doi.org/10.1080/02786826.2021.1890687 mask_distributions = { - 'Type I': mc.Mask(η_inhale=Uniform(0.25, 0.80)), - 'FFP2': mc.Mask(η_inhale=Uniform(0.83, 0.91)), - 'Cloth': mc.Mask(η_inhale=Uniform(0.05, 0.40), η_exhale=Uniform(0.20, 0.50)), + 'Type I': mc.Mask( + η_inhale=param_evaluation( + config.mask_distributions['Type I'], 'η_inhale'), + η_exhale=param_evaluation( + config.mask_distributions['Type I'], 'η_exhale') + if config.mask_distributions['Type I']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), + 'FFP2': mc.Mask( + η_inhale=param_evaluation( + config.mask_distributions['FFP2'], 'η_inhale'), + η_exhale=param_evaluation( + config.mask_distributions['FFP2'], 'η_exhale') + if config.mask_distributions['FFP2']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), + 'Cloth': mc.Mask( + η_inhale=param_evaluation( + config.mask_distributions['Cloth'], 'η_inhale'), + η_exhale=param_evaluation( + config.mask_distributions['Cloth'], 'η_exhale') + if config.mask_distributions['Cloth']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), } def expiration_distribution( BLO_factors, + d_min=0.1, d_max=30., ) -> mc.Expiration: """ @@ -187,40 +383,67 @@ def expiration_distribution( an historical choice based on previous implementations of the model (it limits the influence of the O-mode). """ - dscan = np.linspace(0.1, d_max, 3000) + dscan = np.linspace(d_min, d_max, 3000) return mc.Expiration( CustomKernel( dscan, BLOmodel(BLO_factors).distribution(dscan), kernel_bandwidth=0.1, ), - cn=BLOmodel(BLO_factors).integrate(0.1, d_max), + cn=BLOmodel(BLO_factors).integrate(d_min, d_max), ) expiration_BLO_factors = { - 'Breathing': (1., 0., 0.), - 'Speaking': (1., 1., 1.), - 'Singing': (1., 5., 5.), - 'Shouting': (1., 5., 5.), + 'Breathing': ( + param_evaluation(config.expiration_BLO_factors['Breathing'], 'B'), + param_evaluation(config.expiration_BLO_factors['Breathing'], 'L'), + param_evaluation(config.expiration_BLO_factors['Breathing'], 'O') + ), + 'Speaking': ( + param_evaluation(config.expiration_BLO_factors['Speaking'], 'B'), + param_evaluation(config.expiration_BLO_factors['Speaking'], 'L'), + param_evaluation(config.expiration_BLO_factors['Speaking'], 'O') + ), + 'Singing': ( + param_evaluation(config.expiration_BLO_factors['Singing'], 'B'), + param_evaluation(config.expiration_BLO_factors['Singing'], 'L'), + param_evaluation(config.expiration_BLO_factors['Singing'], 'O') + ), + 'Shouting': ( + param_evaluation(config.expiration_BLO_factors['Shouting'], 'B'), + param_evaluation(config.expiration_BLO_factors['Shouting'], 'L'), + param_evaluation(config.expiration_BLO_factors['Shouting'], 'O') + ), } expiration_distributions = { - exp_type: expiration_distribution(BLO_factors) + exp_type: expiration_distribution(BLO_factors, + d_min=param_evaluation( + config.long_range_expiration_distributions, 'minimum_diameter'), + d_max=param_evaluation(config.long_range_expiration_distributions, 'maximum_diameter')) for exp_type, BLO_factors in expiration_BLO_factors.items() } short_range_expiration_distributions = { - exp_type: expiration_distribution(BLO_factors, d_max=100) + exp_type: expiration_distribution( + BLO_factors, + d_min=param_evaluation( + config.short_range_expiration_distributions, 'minimum_diameter'), + d_max=param_evaluation(config.short_range_expiration_distributions, 'maximum_diameter')) for exp_type, BLO_factors in expiration_BLO_factors.items() } # Derived from Fig 8 a) "stand-stand" in https://www.mdpi.com/1660-4601/17/4/1445/htm -distances = np.array((0.5,0.6,0.7,0.8,0.9,1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,2)) -frequencies = np.array((0.0598036,0.0946154,0.1299152,0.1064905,0.1099066,0.0998209, 0.0845298,0.0479286,0.0406084,0.039795,0.0205997,0.0152316,0.0118155,0.0118155,0.018485,0.0205997)) -short_range_distances = Custom(bounds=(0.5,2.), - function=lambda x: np.interp(x,distances,frequencies,left=0.,right=0.), - max_function=0.13) \ No newline at end of file +distances = np.array((0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.2, + 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2)) +frequencies = np.array((0.0598036, 0.0946154, 0.1299152, 0.1064905, 0.1099066, 0.0998209, 0.0845298, + 0.0479286, 0.0406084, 0.039795, 0.0205997, 0.0152316, 0.0118155, 0.0118155, 0.018485, 0.0205997)) +short_range_distances = Custom(bounds=(param_evaluation(config.short_range_distances, 'minimum_distance'), + param_evaluation(config.short_range_distances, 'maximum_distance')), + function=lambda x: np.interp( + x, distances, frequencies, left=0., right=0.), + max_function=0.13) From 3f75fc77cd1cbb6a01929b4a6fbdafad756cb325 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 16:29:20 +0200 Subject: [PATCH 05/10] injected parameters in models, model and report generator --- caimira/apps/calculator/model_generator.py | 43 ++++++--------------- caimira/apps/calculator/report_generator.py | 9 +++-- caimira/models.py | 27 ++++++------- 3 files changed, 32 insertions(+), 47 deletions(-) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index a5fce869..b844aa68 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -19,6 +19,7 @@ from caimira.monte_carlo.data import expiration_distribution, expiration_BLO_fac from .defaults import (NO_DEFAULT, DEFAULT_MC_SAMPLE_SIZE, DEFAULTS, ACTIVITIES, ACTIVITY_TYPES, COFFEE_OPTIONS_INT, CONFIDENCE_LEVEL_OPTIONS, MECHANICAL_VENTILATION_TYPES, MASK_TYPES, MASK_WEARING_OPTIONS, MONTH_NAMES, VACCINE_BOOSTER_TYPE, VACCINE_TYPE, VENTILATION_TYPES, VIRUS_TYPES, VOLUME_TYPES, WINDOWS_OPENING_REGIMES, WINDOWS_TYPES) +from caimira.store.configuration import config LOG = logging.getLogger(__name__) @@ -314,10 +315,10 @@ class FormData: if self.arve_sensors_option == False: if self.room_heating_option: - humidity = 0.3 + humidity = config.room['defaults']['humidity_with_heating'] else: - humidity = 0.5 - inside_temp = 293. + humidity = config.room['defaults']['humidity_without_heating'] + inside_temp = config.room['defaults']['inside_temp'] else: humidity = float(self.humidity) inside_temp = self.inside_temp @@ -373,7 +374,7 @@ class FormData: if (self.activity_type == 'precise'): activity_defn, _ = self.generate_precise_activity_expiration() else: - activity_defn = ACTIVITIES[ACTIVITY_TYPES.index(self.activity_type)]['activity'] + activity_defn = activity_defn = ACTIVITIES[self.activity_type]['activity'] population = mc.SimplePopulation( number=models.IntPiecewiseConstant(transition_times=tuple(transition_times), values=tuple(total_people)), @@ -476,7 +477,8 @@ class FormData: # This is a minimal, always present source of ventilation, due # to the air infiltration from the outside. # See CERN-OPEN-2021-004, p. 12. - infiltration_ventilation = models.AirChange(active=always_on, air_exch=0.25) + residual_vent: float = config.ventilation['infiltration_ventilation'] # type: ignore + infiltration_ventilation = models.AirChange(active=always_on, air_exch=residual_vent) if self.hepa_option: hepa = models.HEPAFilter(active=always_on, q_air_mech=self.hepa_amount) return models.MultipleVentilation((ventilation, hepa, infiltration_ventilation)) @@ -511,9 +513,8 @@ class FormData: # Initializes the virus virus = virus_distributions[self.virus_type] - activity_index = ACTIVITY_TYPES.index(self.activity_type) - activity_defn = ACTIVITIES[activity_index]['activity'] - expiration_defn = ACTIVITIES[activity_index]['expiration'] + activity_defn = ACTIVITIES[self.activity_type]['activity'] + expiration_defn = ACTIVITIES[self.activity_type]['expiration'] if (self.activity_type == 'smallmeeting'): # Conversation of N people is approximately 1/N% of the time speaking. @@ -538,29 +539,9 @@ class FormData: return infected def exposed_population(self) -> mc.Population: - scenario_activity = { - 'office': 'Seated', - 'controlroom-day': 'Seated', - 'controlroom-night': 'Seated', - 'smallmeeting': 'Seated', - 'largemeeting': 'Standing', - 'callcentre': 'Seated', - 'library': 'Seated', - 'training': 'Standing', - 'training_attendee': 'Seated', - 'lab':'Light activity', - 'workshop': 'Moderate activity', - 'gym':'Heavy exercise', - 'household-day': 'Light activity', - 'household-night': 'Seated', - 'primary-school': 'Light activity', - 'secondary-school': 'Light activity', - 'university': 'Seated', - 'restaurant': 'Seated', - 'precise': self.precise_activity['physical_activity'] if self.activity_type == 'precise' else None, - } - - activity_defn = scenario_activity[self.activity_type] + activity_defn = (self.precise_activity['physical_activity'] + if self.activity_type == 'precise' + else str(config.population_scenario_activity[self.activity_type]['activity'])) activity = activity_distributions[activity_defn] infected_occupants = self.infected_people diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 6efb9177..12c2eddc 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -17,6 +17,7 @@ from caimira.apps.calculator import markdown_tools from ... import monte_carlo as mc from .model_generator import FormData, DEFAULT_MC_SAMPLE_SIZE from ... import dataclass_utils +from caimira.store.configuration import config def model_start_end(model: models.ExposureModel): @@ -201,8 +202,8 @@ def conditional_prob_inf_given_vl_dist(infection_probability: models._Vectorised for vl_log in viral_loads: specific_prob = infection_probability[np.where((vl_log-step/2-specific_vl)*(vl_log+step/2-specific_vl)<0)[0]] #type: ignore pi_means.append(specific_prob.mean()) - lower_percentiles.append(np.quantile(specific_prob, 0.05)) - upper_percentiles.append(np.quantile(specific_prob, 0.95)) + lower_percentiles.append(np.quantile(specific_prob, config.conditional_prob_inf_given_viral_load['lower_percentile'])) + upper_percentiles.append(np.quantile(specific_prob, config.conditional_prob_inf_given_viral_load['upper_percentile'])) return pi_means, lower_percentiles, upper_percentiles @@ -210,7 +211,9 @@ def conditional_prob_inf_given_vl_dist(infection_probability: models._Vectorised def manufacture_conditional_probability_data(exposure_model: models.ExposureModel, infection_probability: models._VectorisedFloat): - min_vl, max_vl, step = 2, 10, 8/100 + min_vl = config.conditional_prob_inf_given_viral_load['min_vl'] + max_vl = config.conditional_prob_inf_given_viral_load['max_vl'] + step = (max_vl - min_vl)/100 viral_loads = np.arange(min_vl, max_vl, step) specific_vl = np.log10(exposure_model.concentration_model.virus.viral_load_in_sputum) pi_means, lower_percentiles, upper_percentiles = conditional_prob_inf_given_vl_dist(infection_probability, viral_loads, diff --git a/caimira/models.py b/caimira/models.py index 24ef62e2..d1dc1a87 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -49,6 +49,7 @@ else: from .utils import method_cache from .dataclass_utils import nested_replace +from caimira.store.configuration import config oneoverln2 = 1 / np.log(2) # Define types for items supporting vectorisation. In the future this may be replaced @@ -347,7 +348,7 @@ class SlidingWindow(WindowOpening): Average measured value of discharge coefficient for sliding or side-hung windows. """ - return 0.6 + return config.ventilation['natural']['discharge_factor']['sliding'] @dataclass(frozen=True) @@ -860,7 +861,7 @@ class _PopulationWithVirus(Population): The fraction of infectious virus. """ - return 1. + return config.population_with_virus['fraction_of_infectious_virus'] def aerosols(self): """ @@ -1032,7 +1033,7 @@ class _ConcentrationModelBase: (in the same unit as the concentration). Its the value towards which the concentration will decay to. """ - return 0. + return config.concentration_model['min_background_concentration'] def normalization_factor(self) -> _VectorisedFloat: """ @@ -1220,7 +1221,7 @@ class ConcentrationModel(_ConcentrationModelBase): #: evaporation factor: the particles' diameter is multiplied by this # factor as soon as they are in the air (but AFTER going out of the, # mask, if any). - evaporation_factor: float = 0.3 + evaporation_factor: float = config.particle['evaporation_factor'] @property def population(self) -> InfectedPopulation: @@ -1260,10 +1261,10 @@ class CO2ConcentrationModel(_ConcentrationModelBase): CO2_emitters: SimplePopulation #: CO2 concentration in the atmosphere (in ppm) - CO2_atmosphere_concentration: float = 440.44 + CO2_atmosphere_concentration: float = config.concentration_model['CO2_concentration_model']['CO2_atmosphere_concentration'] #: CO2 fraction in the exhaled air - CO2_fraction_exhaled: float = 0.042 + CO2_fraction_exhaled: float = config.concentration_model['CO2_concentration_model']['CO2_fraction_exhaled'] @property def population(self) -> SimplePopulation: @@ -1309,14 +1310,14 @@ class ShortRangeModel: The dilution factor for the respective expiratory activity type. ''' # Average mouth opening diameter (m) - mouth_diameter = 0.02 + mouth_diameter: float = config.short_range_model['dilution_factor']['mouth_diameter'] # Breathing rate, from m3/h to m3/s BR = np.array(self.activity.exhalation_rate/3600.) # Exhalation coefficient. Ratio between the duration of a breathing cycle and the duration of # the exhalation. - φ = 2 + φ: float = config.short_range_model['dilution_factor']['exhalation_coefficient'] # Exhalation airflow, as per Jia et al. (2022) Q_exh: _VectorisedFloat = φ * BR @@ -1328,12 +1329,12 @@ class ShortRangeModel: u0 = np.array(Q_exh/Am) # Duration of the expiration period(s), assuming a 4s breath-cycle - tstar = 2.0 + tstar: float = config.short_range_model['dilution_factor']['tstar'] # Streamwise and radial penetration coefficients - 𝛽r1 = 0.18 - 𝛽r2 = 0.2 - 𝛽x1 = 2.4 + 𝛽r1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r1'] + 𝛽r2: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r2'] + 𝛽x1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽x1'] # Parameters in the jet-like stage # Position of virtual origin @@ -1489,7 +1490,7 @@ class ExposureModel: geographical_data: Cases #: The number of times the exposure event is repeated (default 1). - repeats: int = 1 + repeats: int = config.exposure_model['repeats'] def __post_init__(self): """ From e0714d6aad488c890e5fe8398997da8794461dd5 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 26 Oct 2023 17:11:16 +0200 Subject: [PATCH 06/10] added module init --- caimira/store/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 caimira/store/__init__.py diff --git a/caimira/store/__init__.py b/caimira/store/__init__.py new file mode 100644 index 00000000..e69de29b From 035190564c5e8ea7a0e1270def832c763bcd0f58 Mon Sep 17 00:00:00 2001 From: Nicola Tarocco Date: Thu, 9 Nov 2023 15:39:58 +0100 Subject: [PATCH 07/10] refactor data service --- caimira/store/configuration.py | 718 +++++++++++++++++------------ caimira/store/data_service.py | 111 +++-- caimira/store/global_store.py | 43 -- caimira/tests/test_data_service.py | 84 ++-- 4 files changed, 530 insertions(+), 426 deletions(-) delete mode 100644 caimira/store/global_store.py diff --git a/caimira/store/configuration.py b/caimira/store/configuration.py index cc791140..e1272c5c 100644 --- a/caimira/store/configuration.py +++ b/caimira/store/configuration.py @@ -1,319 +1,451 @@ -import asyncio -import os - -from caimira.store.global_store import GlobalStore - - class Configuration: - ''' - Configuration to handle data. Contains the default values used in the model. - Might suffer update from the Data Service. - ''' + """Configuration singleton to cache data values.""" - def __init__(self): - self.data_fetched = False - self.BLOmodel = { - 'cn': {'B': 0.06, - 'L': 0.2, - 'O': 0.0010008, - }, - 'mu': { - 'B': 0.989541, - 'L': 1.38629, - 'O': 4.97673, - }, - 'sigma': { - 'B': 0.262364, - 'L': 0.506818, - 'O': 0.585005, - }, - } - self.activity_distributions = { - 'Seated': { - 'inhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108}, - }, - 'exhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': -0.6872121723362303, 'standard_deviation_gaussian': 0.10498338229297108}, + BLOmodel = { + "cn": { + "B": 0.06, + "L": 0.2, + "O": 0.0010008, + }, + "mu": { + "B": 0.989541, + "L": 1.38629, + "O": 4.97673, + }, + "sigma": { + "B": 0.262364, + "L": 0.506818, + "O": 0.585005, + }, + } + activity_distributions = { + "Seated": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": -0.6872121723362303, + "standard_deviation_gaussian": 0.10498338229297108, }, }, - 'Standing': { - 'inhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223}, - }, - 'exhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': -0.5742377578494785, 'standard_deviation_gaussian': 0.09373162411398223}, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": -0.6872121723362303, + "standard_deviation_gaussian": 0.10498338229297108, }, }, - 'Light activity': { - 'inhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601}, - }, - 'exhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 0.21380242785625422, 'standard_deviation_gaussian': 0.09435378091059601}, + }, + "Standing": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": -0.5742377578494785, + "standard_deviation_gaussian": 0.09373162411398223, }, }, - 'Moderate activity': { - 'inhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137}, - }, - 'exhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 0.551771330362601, 'standard_deviation_gaussian': 0.1894616357138137}, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": -0.5742377578494785, + "standard_deviation_gaussian": 0.09373162411398223, }, }, - 'Heavy exercise': { - 'inhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565}, - }, - 'exhalation_rate': { - 'associated_distribution': 'Numpy Log-normal Distribution (random.lognormal)', - 'parameters': {'mean_gaussian': 1.1644665696723049, 'standard_deviation_gaussian': 0.21744554768657565}, + }, + "Light activity": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 0.21380242785625422, + "standard_deviation_gaussian": 0.09435378091059601, }, }, - } - self.symptomatic_vl_frequencies = { - 'log_variable': [2.46032, 2.67431, 2.85434, 3.06155, 3.25856, 3.47256, 3.66957, 3.85979, 4.09927, 4.27081, - 4.47631, 4.66653, 4.87204, 5.10302, 5.27456, 5.46478, 5.6533, 5.88428, 6.07281, 6.30549, - 6.48552, 6.64856, 6.85407, 7.10373, 7.30075, 7.47229, 7.66081, 7.85782, 8.05653, 8.27053, - 8.48453, 8.65607, 8.90573, 9.06878, 9.27429, 9.473, 9.66152, 9.87552], - 'frequencies': [0.001206885, 0.007851618, 0.008078144, 0.01502491, 0.013258014, 0.018528495, 0.020053765, - 0.021896167, 0.022047184, 0.018604005, 0.01547796, 0.018075445, 0.021503523, 0.022349217, - 0.025097721, 0.032875078, 0.030594727, 0.032573045, 0.034717482, 0.034792991, - 0.033267721, 0.042887485, 0.036846816, 0.03876473, 0.045016819, 0.040063473, 0.04883754, - 0.043944602, 0.048142864, 0.041588741, 0.048762031, 0.027921732, 0.033871788, - 0.022122693, 0.016927718, 0.008833228, 0.00478598, 0.002807662], - 'kernel_bandwidth': 0.1, - } - self.covid_overal_vl_data = { - 'shape_factor': 3.47, - 'scale_factor': 7.01, - 'start': 0.01, - 'stop': 0.99, - 'num': 30.0, - 'min_bound': 2, - 'max_bound': 10, - 'interpolation_fp_left': 0, - 'interpolation_fp_right': 0, - 'max_function': 0.2, - } - self.viable_to_RNA_ratio_distribution = { - 'low': 0.01, - 'high': 0.6, - } - self.infectious_dose_distribution = { - 'low': 10, - 'high': 100, - } - self.virus_distributions = { - 'SARS_CoV_2': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 1, - 'infectiousness_days': 14, - }, - 'SARS_CoV_2_ALPHA': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.78, - 'infectiousness_days': 14, - }, - 'SARS_CoV_2_BETA': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.8, - 'infectiousness_days': 14 - }, - 'SARS_CoV_2_GAMMA': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.72, - 'infectiousness_days': 14 - }, - 'SARS_CoV_2_DELTA': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.51, - 'infectiousness_days': 14 - }, - 'SARS_CoV_2_OMICRON': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.2, - 'infectiousness_days': 14 - }, - 'SARS_CoV_2_Other': { - 'viral_load_in_sputum': 'Ref: Viral load - covid_overal_vl_data', - 'infectious_dose': 'Ref: Infectious dose - infectious_dose_distribution', - 'viable_to_RNA_ratio': 'Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution', - 'transmissibility_factor': 0.1, - 'infectiousness_days': 14, - }, - } - self.mask_distributions = { - 'Type I': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.25, - 'high': 0.80, - }, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 0.21380242785625422, + "standard_deviation_gaussian": 0.09435378091059601, }, - 'Known filtration efficiency of masks when exhaling?': 'No', - 'factor_exhale': 1, }, - 'FFP2': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.83, - 'high': 0.91, - }, + }, + "Moderate activity": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 0.551771330362601, + "standard_deviation_gaussian": 0.1894616357138137, }, - 'Known filtration efficiency of masks when exhaling?': 'No', - 'factor_exhale': 1, }, - 'Cloth': { - 'η_inhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.05, - 'high': 0.40, - }, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 0.551771330362601, + "standard_deviation_gaussian": 0.1894616357138137, }, - 'Known filtration efficiency of masks when exhaling?': 'Yes', - 'η_exhale': { - 'associated_distribution': 'Numpy Uniform Distribution (random.uniform)', - 'parameters': { - 'low': 0.20, - 'high': 0.50, - }, - }, - 'factor_exhale': 1, }, - } - self.expiration_BLO_factors = { - 'Breathing': {'B': 1., 'L': 0., 'O': 0., }, - 'Speaking': {'B': 1., 'L': 1., 'O': 1., }, - 'Singing': {'B': 1., 'L': 5., 'O': 5., }, - 'Shouting': {'B': 1., 'L': 5., 'O': 5., }, - } - self.long_range_expiration_distributions = { - 'minimum_diameter': 0.1, - 'maximum_diameter': 30, - } - self.short_range_expiration_distributions = { - 'minimum_diameter': 0.1, - 'maximum_diameter': 100, - } - self.short_range_distances = { - 'minimum_distance': 0.5, - 'maximum_distance': 2., - } + }, + "Heavy exercise": { + "inhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 1.1644665696723049, + "standard_deviation_gaussian": 0.21744554768657565, + }, + }, + "exhalation_rate": { + "associated_distribution": "Numpy Log-normal Distribution (random.lognormal)", + "parameters": { + "mean_gaussian": 1.1644665696723049, + "standard_deviation_gaussian": 0.21744554768657565, + }, + }, + }, + } + symptomatic_vl_frequencies = { + "log_variable": [ + 2.46032, + 2.67431, + 2.85434, + 3.06155, + 3.25856, + 3.47256, + 3.66957, + 3.85979, + 4.09927, + 4.27081, + 4.47631, + 4.66653, + 4.87204, + 5.10302, + 5.27456, + 5.46478, + 5.6533, + 5.88428, + 6.07281, + 6.30549, + 6.48552, + 6.64856, + 6.85407, + 7.10373, + 7.30075, + 7.47229, + 7.66081, + 7.85782, + 8.05653, + 8.27053, + 8.48453, + 8.65607, + 8.90573, + 9.06878, + 9.27429, + 9.473, + 9.66152, + 9.87552, + ], + "frequencies": [ + 0.001206885, + 0.007851618, + 0.008078144, + 0.01502491, + 0.013258014, + 0.018528495, + 0.020053765, + 0.021896167, + 0.022047184, + 0.018604005, + 0.01547796, + 0.018075445, + 0.021503523, + 0.022349217, + 0.025097721, + 0.032875078, + 0.030594727, + 0.032573045, + 0.034717482, + 0.034792991, + 0.033267721, + 0.042887485, + 0.036846816, + 0.03876473, + 0.045016819, + 0.040063473, + 0.04883754, + 0.043944602, + 0.048142864, + 0.041588741, + 0.048762031, + 0.027921732, + 0.033871788, + 0.022122693, + 0.016927718, + 0.008833228, + 0.00478598, + 0.002807662, + ], + "kernel_bandwidth": 0.1, + } + covid_overal_vl_data = { + "shape_factor": 3.47, + "scale_factor": 7.01, + "start": 0.01, + "stop": 0.99, + "num": 30.0, + "min_bound": 2, + "max_bound": 10, + "interpolation_fp_left": 0, + "interpolation_fp_right": 0, + "max_function": 0.2, + } + viable_to_RNA_ratio_distribution = { + "low": 0.01, + "high": 0.6, + } + infectious_dose_distribution = { + "low": 10, + "high": 100, + } + virus_distributions = { + "SARS_CoV_2": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 1, + "infectiousness_days": 14, + }, + "SARS_CoV_2_ALPHA": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.78, + "infectiousness_days": 14, + }, + "SARS_CoV_2_BETA": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.8, + "infectiousness_days": 14, + }, + "SARS_CoV_2_GAMMA": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.72, + "infectiousness_days": 14, + }, + "SARS_CoV_2_DELTA": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.51, + "infectiousness_days": 14, + }, + "SARS_CoV_2_OMICRON": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.2, + "infectiousness_days": 14, + }, + "SARS_CoV_2_Other": { + "viral_load_in_sputum": "Ref: Viral load - covid_overal_vl_data", + "infectious_dose": "Ref: Infectious dose - infectious_dose_distribution", + "viable_to_RNA_ratio": "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution", + "transmissibility_factor": 0.1, + "infectiousness_days": 14, + }, + } + mask_distributions = { + "Type I": { + "η_inhale": { + "associated_distribution": "Numpy Uniform Distribution (random.uniform)", + "parameters": { + "low": 0.25, + "high": 0.80, + }, + }, + "Known filtration efficiency of masks when exhaling?": "No", + "factor_exhale": 1, + }, + "FFP2": { + "η_inhale": { + "associated_distribution": "Numpy Uniform Distribution (random.uniform)", + "parameters": { + "low": 0.83, + "high": 0.91, + }, + }, + "Known filtration efficiency of masks when exhaling?": "No", + "factor_exhale": 1, + }, + "Cloth": { + "η_inhale": { + "associated_distribution": "Numpy Uniform Distribution (random.uniform)", + "parameters": { + "low": 0.05, + "high": 0.40, + }, + }, + "Known filtration efficiency of masks when exhaling?": "Yes", + "η_exhale": { + "associated_distribution": "Numpy Uniform Distribution (random.uniform)", + "parameters": { + "low": 0.20, + "high": 0.50, + }, + }, + "factor_exhale": 1, + }, + } + expiration_BLO_factors = { + "Breathing": { + "B": 1.0, + "L": 0.0, + "O": 0.0, + }, + "Speaking": { + "B": 1.0, + "L": 1.0, + "O": 1.0, + }, + "Singing": { + "B": 1.0, + "L": 5.0, + "O": 5.0, + }, + "Shouting": { + "B": 1.0, + "L": 5.0, + "O": 5.0, + }, + } + long_range_expiration_distributions = { + "minimum_diameter": 0.1, + "maximum_diameter": 30, + } + short_range_expiration_distributions = { + "minimum_diameter": 0.1, + "maximum_diameter": 100, + } + short_range_distances = { + "minimum_distance": 0.5, + "maximum_distance": 2.0, + } - #################################### + #################################### - self.room = { - 'defaults': { - 'inside_temp': 293, - 'humidity_with_heating': 0.3, - 'humidity_without_heating': 0.5, + room = { + "defaults": { + "inside_temp": 293, + "humidity_with_heating": 0.3, + "humidity_without_heating": 0.5, + }, + } + ventilation = { + "natural": { + "discharge_factor": { + "sliding": 0.6, }, - } - self.ventilation = { - 'natural': { - 'discharge_factor': { - 'sliding': 0.6, - }, + }, + "infiltration_ventilation": 0.25, + } + particle = { + "evaporation_factor": 0.3, + } + population_with_virus = { + "fraction_of_infectious_virus": 1, + } + concentration_model = { + "min_background_concentration": 0.0, + "CO2_concentration_model": { + "CO2_atmosphere_concentration": 440.44, + "CO2_fraction_exhaled": 0.042, + }, + } + short_range_model = { + "dilution_factor": { + "mouth_diameter": 0.02, + "exhalation_coefficient": 2, + "tstar": 2, + "penetration_coefficients": { + "𝛽r1": 0.18, + "𝛽r2": 0.2, + "𝛽x1": 2.4, }, - 'infiltration_ventilation': 0.25, - } - self.particle = { - 'evaporation_factor': 0.3, - } - self.population_with_virus = { - 'fraction_of_infectious_virus': 1, - } - self.concentration_model = { - 'min_background_concentration': 0., - 'CO2_concentration_model': { - 'CO2_atmosphere_concentration': 440.44, - 'CO2_fraction_exhaled': 0.042, - }, - } - self.short_range_model = { - 'dilution_factor': { - 'mouth_diameter': 0.02, - 'exhalation_coefficient': 2, - 'tstar': 2, - 'penetration_coefficients': { - '𝛽r1': 0.18, '𝛽r2': .2, '𝛽x1': 2.4, - }, - }, - } - self.exposure_model = { - 'repeats': 1, - } - self.conditional_prob_inf_given_viral_load = { - 'lower_percentile': 0.05, - 'upper_percentile': 0.95, - 'min_vl': 2, - 'max_vl': 10, - } - self.monte_carlo_sample_size = 250000 - self.population_scenario_activity = { - 'office': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 2}}, - 'smallmeeting': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': None}}, - 'largemeeting': {'activity': 'Standing', 'expiration': {'Speaking': 1, 'Breathing': 2}}, - 'callcenter': {'activity': 'Seated', 'expiration': {'Speaking': 1}}, - 'controlroom-day': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 1}}, - 'controlroom-night': {'activity': 'Seated', 'expiration': {'Speaking': 1, 'Breathing': 9}}, - 'library': {'activity': 'Seated', 'expiration': {'Breathing': 1}}, - 'lab': {'activity': 'Light activity', 'expiration': {'Speaking': 1, 'Breathing': 1}}, - 'workshop': {'activity': 'Moderate activity', 'expiration': {'Speaking': 1, 'Breathing': 1}}, - 'training': {'activity': 'Standing', 'expiration': {'Speaking': 1}}, - 'training_attendee': {'activity': 'Seated', 'expiration': {'Breathing': 1}}, - 'gym': {'activity': 'Heavy exercise', 'expiration': {'Breathing': 1}}, - 'household-day': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}}, - 'household-night': {'activity': 'Seated', 'expiration': {'Breathing': 7, 'Speaking': 3}}, - 'primary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 5, 'Speaking': 5}}, - 'secondary-school': {'activity': 'Light activity', 'expiration': {'Breathing': 7, 'Speaking': 3}}, - 'university': {'activity': 'Seated', 'expiration': {'Breathing': 9, 'Speaking': 1}}, - 'restaurant': {'activity': 'Seated', 'expiration': {'Breathing': 1, 'Speaking': 9}}, - 'precise': {'activity': None, 'expiration': None}, - } + }, + } + exposure_model = { + "repeats": 1, + } + conditional_prob_inf_given_viral_load = { + "lower_percentile": 0.05, + "upper_percentile": 0.95, + "min_vl": 2, + "max_vl": 10, + } + monte_carlo_sample_size = 250000 + population_scenario_activity = { + "office": {"activity": "Seated", "expiration": {"Speaking": 1, "Breathing": 2}}, + "smallmeeting": { + "activity": "Seated", + "expiration": {"Speaking": 1, "Breathing": None}, + }, + "largemeeting": { + "activity": "Standing", + "expiration": {"Speaking": 1, "Breathing": 2}, + }, + "callcenter": {"activity": "Seated", "expiration": {"Speaking": 1}}, + "controlroom-day": { + "activity": "Seated", + "expiration": {"Speaking": 1, "Breathing": 1}, + }, + "controlroom-night": { + "activity": "Seated", + "expiration": {"Speaking": 1, "Breathing": 9}, + }, + "library": {"activity": "Seated", "expiration": {"Breathing": 1}}, + "lab": { + "activity": "Light activity", + "expiration": {"Speaking": 1, "Breathing": 1}, + }, + "workshop": { + "activity": "Moderate activity", + "expiration": {"Speaking": 1, "Breathing": 1}, + }, + "training": {"activity": "Standing", "expiration": {"Speaking": 1}}, + "training_attendee": {"activity": "Seated", "expiration": {"Breathing": 1}}, + "gym": {"activity": "Heavy exercise", "expiration": {"Breathing": 1}}, + "household-day": { + "activity": "Light activity", + "expiration": {"Breathing": 5, "Speaking": 5}, + }, + "household-night": { + "activity": "Seated", + "expiration": {"Breathing": 7, "Speaking": 3}, + }, + "primary-school": { + "activity": "Light activity", + "expiration": {"Breathing": 5, "Speaking": 5}, + }, + "secondary-school": { + "activity": "Light activity", + "expiration": {"Breathing": 7, "Speaking": 3}, + }, + "university": { + "activity": "Seated", + "expiration": {"Breathing": 9, "Speaking": 1}, + }, + "restaurant": { + "activity": "Seated", + "expiration": {"Breathing": 1, "Speaking": 9}, + }, + "precise": {"activity": None, "expiration": None}, + } - async def populate_data(self): - """ - Fetches data from the API and populates the configuration object. - """ - if not self.data_fetched and os.environ.get('DATA_SERVICE_ENABLED', 'False').lower() == 'true': - # Fetch and populate data from API - await GlobalStore.populate_from_api() - data = GlobalStore.get_data()['data'] - - # Dynamically set attributes based on the data fetched from the API - for attr_name, value in data.items(): - setattr(self, attr_name, value) - - self.data_fetched = True - return + def update(self, data): + """Update local cache with data provided as argument.""" + for attr_name, value in data.items(): + setattr(self, attr_name, value) +# module-level variable as a form of singleton config = Configuration() - -asyncio.run(config.populate_data()) diff --git a/caimira/store/data_service.py b/caimira/store/data_service.py index 703884b8..e203be48 100644 --- a/caimira/store/data_service.py +++ b/caimira/store/data_service.py @@ -1,74 +1,95 @@ -import dataclasses -import json import logging +import os import typing -from tornado.httpclient import AsyncHTTPClient, HTTPRequest +import requests -LOG = logging.getLogger(__name__) +from .configuration import config + +logger = logging.getLogger(__name__) -@dataclasses.dataclass -class DataService(): - ''' - Responsible for establishing a connection to a - database through a REST API by handling authentication - and fetching data. It utilizes the Tornado web framework - for asynchronous HTTP requests. - ''' - # Credentials used for authentication - credentials: dict - - # Host URL for the CAiMIRA Data Service API - host: str = 'https://caimira-data-api.app.cern.ch' +class DataService: + """Responsible for fetching data from the data service endpoint.""" # Cached access token _access_token: typing.Optional[str] = None + def __init__( + self, + credentials: typing.Dict[str, str], + host: str = "https://caimira-data-api.app.cern.ch", + ): + self._credentials = credentials + self._host = host + def _is_valid(self, access_token): # decode access_token # check validity return False - async def _login(self): + def _login(self): if self._is_valid(self._access_token): return self._access_token # invalid access_token, fetch it again - client_email = self.credentials["data_service_client_email"] - client_password = self.credentials['data_service_client_password'] + client_email = self._credentials["email"] + client_password = self._credentials["password"] - if (client_email == None or client_password == None): + if client_email == None or client_password == None: # If the credentials are not defined, an exception is raised. raise Exception("DataService credentials not set") - http_client = AsyncHTTPClient() - headers = {'Content-type': 'application/json'} - json_body = {"email": f"{client_email}", - "password": f"{client_password}"} + url = f"{self._host}/login" + headers = {"Content-Type": "application/json"} + json_body = dict(email=client_email, password=client_password) - response = await http_client.fetch(HTTPRequest( - url=self.host + '/login', - method='POST', - headers=headers, - body=json.dumps(json_body), - ), - raise_error=True) + try: + response = requests.post(url, json=json_body, headers=headers) + response.raise_for_status() + if response.status_code == 200: + self._access_token = response.json()["access_token"] + return self._access_token + else: + logger.error( + f"Unexpected error on login. Response status code: {response.status_code}, body: f{response.text}" + ) + except requests.exceptions.RequestException as e: + logger.exception(e) - self._access_token = json.loads(response.body)['access_token'] - return self._access_token + def fetch(self): + access_token = self._login() - async def fetch(self): - access_token = await self._login() + headers = { + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + } + url = f"{self._host}/data" - http_client = AsyncHTTPClient() - headers = {'Authorization': f'Bearer {access_token}'} + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + if response.status_code == 200: + return response.json() + else: + logger.error( + f"Unexpected error when fetching data. Response status code: {response.status_code}, body: f{response.text}" + ) + except requests.exceptions.RequestException as e: + logger.exception(e) - response = await http_client.fetch(HTTPRequest( - url=self.host + '/data', - method='GET', - headers=headers, - ), - raise_error=True) - return json.loads(response.body) +def update_configuration(): + data_service_enabled = os.environ.get("DATA_SERVICE_ENABLED", "False") + is_enabled = data_service_enabled.lower() == "true" + if is_enabled: + credentials = { + "email": os.environ.get("DATA_SERVICE_CLIENT_EMAIL", None), + "password": os.environ.get("DATA_SERVICE_CLIENT_PASSWORD", None), + } + data_service = DataService(credentials) + data = data_service.fetch() + if data: + config.update(data["data"]) + else: + logger.error("Could not fetch fresh data from the data service.") diff --git a/caimira/store/global_store.py b/caimira/store/global_store.py deleted file mode 100644 index 164af849..00000000 --- a/caimira/store/global_store.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import logging - -from caimira.store.data_service import DataService - -LOG = logging.getLogger(__name__) - - -class GlobalStore: - ''' - Singleton pattern - ensure that there's only one instance of - GlobalStore throughout the application - ''' - - _instance = None - - def __new__(self): - if self._instance is None: - self._instance = super().__new__(self) - self._instance = {} - - return self._instance - - @classmethod - async def populate_from_api(self): - data_service_credentials = { - 'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None), - 'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None), - } - data_service = None - data_service_enabled = os.environ.get( - 'DATA_SERVICE_ENABLED', 'False').lower() == 'true' - if data_service_enabled: - try: - data_service = DataService(data_service_credentials) - self._instance = await data_service.fetch() - except Exception as err: - error_message = f"Something went wrong with the data service: {str(err)}" - LOG.error(error_message, exc_info=True) - - @classmethod - def get_data(self): - return self._instance diff --git a/caimira/tests/test_data_service.py b/caimira/tests/test_data_service.py index 29c8b9e6..9e0189e7 100644 --- a/caimira/tests/test_data_service.py +++ b/caimira/tests/test_data_service.py @@ -1,87 +1,81 @@ -from dataclasses import dataclass - import unittest -from unittest.mock import patch, MagicMock -from tornado.httpclient import HTTPError +from unittest.mock import Mock, patch from caimira.store.data_service import DataService -@dataclass -class MockResponse: - body: str class DataServiceTests(unittest.TestCase): def setUp(self): # Set up any necessary test data or configurations - self.credentials = { - "data_service_client_email": "test@example.com", - "data_service_client_password": "password123" - } + self.credentials = {"email": "test@example.com", "password": "password123"} self.data_service = DataService(self.credentials) - @patch('caimira.apps.calculator.data_service.AsyncHTTPClient') - async def test_login_successful(self, mock_http_client): + @patch("requests.post") + def test_login_successful(self, mock_post): # Mock successful login response - mock_response = MockResponse('{"access_token": "dummy_token"}') - mock_fetch = MagicMock(return_value=mock_response) - mock_http_client.return_value.fetch = mock_fetch + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"access_token": "dummy_token"} + mock_post.return_value = mock_response # Call the login method - access_token = await self.data_service._login() + access_token = self.data_service._login() # Assert that the access token is returned correctly self.assertEqual(access_token, "dummy_token") # Verify that the fetch method was called with the expected arguments - mock_fetch.assert_called_once_with( - url='https://caimira-data-api.app.cern.ch/login', - method='POST', - headers={'Content-type': 'application/json'}, - body='{"email": "test@example.com", "password": "password123"}' + mock_post.assert_called_once_with( + "https://caimira-data-api.app.cern.ch/login", + json=dict(email="test@example.com", password="password123"), + headers={"Content-Type": "application/json"}, ) - @patch('caimira.apps.calculator.data_service.AsyncHTTPClient') - async def test_login_error(self, mock_http_client): + @patch("requests.post") + def test_login_error(self, mock_post): # Mock login error response - mock_fetch = MagicMock(side_effect=HTTPError(500)) - mock_http_client.return_value.fetch = mock_fetch + mock_post.return_value = Mock() + mock_post.return_value.status_code = 500 # Call the login method - access_token = await self.data_service.login() + access_token = self.data_service._login() # Assert that the login method returns None in case of an error self.assertIsNone(access_token) - @patch('caimira.apps.calculator.data_service.AsyncHTTPClient') - async def test_fetch_successful(self, mock_http_client): + @patch("requests.get") + @patch.object(DataService, "_login") + def test_fetch_successful(self, mock_login, mock_get): # Mock successful fetch response - mock_response = MockResponse('{"data": "dummy_data"}') - mock_fetch = MagicMock(return_value=mock_response) - mock_http_client.return_value.fetch = mock_fetch - + mock_get.return_value = Mock() + mock_get.return_value.status_code = 200 + mock_get.return_value.json.return_value = {"data": "dummy_data"} # Call the fetch method with a mock access token - self.data_service._access_token = "dummy_token" - data = await self.data_service.fetch() + mock_login.return_value = "dummy_token" + data = self.data_service.fetch() # Assert that the data is returned correctly self.assertEqual(data, {"data": "dummy_data"}) # Verify that the fetch method was called with the expected arguments - mock_fetch.assert_called_once_with( - url='https://caimira-data-api.app.cern.ch/data', - method='GET', - headers={'Authorization': 'Bearer dummy_token'} + mock_get.assert_called_once_with( + "https://caimira-data-api.app.cern.ch/data", + headers={ + "Authorization": "Bearer dummy_token", + "Content-Type": "application/json", + }, ) - @patch('caimira.apps.calculator.data_service.AsyncHTTPClient') - async def test_fetch_error(self, mock_http_client): + @patch("requests.get") + @patch.object(DataService, "_login") + def test_fetch_error(self, mock_login, mock_get): # Mock fetch error response - mock_fetch = MagicMock(side_effect=HTTPError(404)) - mock_http_client.return_value.fetch = mock_fetch + mock_get.return_value = Mock() + mock_get.return_value.status_code = 500 # Call the fetch method with a mock access token - self.data_service._access_token = "dummy_token" - data = await self.data_service.fetch() + mock_login.return_value = "dummy_token" + data = self.data_service.fetch() # Assert that the fetch method returns None in case of an error self.assertIsNone(data) From 34449788cc4f508a5e9ff589989548e7893fd9ae Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 17 Nov 2023 09:16:33 +0100 Subject: [PATCH 08/10] added # type: ignore annotations and modified default precise activity --- caimira/models.py | 26 +++++++++++++------------- caimira/store/configuration.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/caimira/models.py b/caimira/models.py index d1dc1a87..3d8d5a3f 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -348,7 +348,7 @@ class SlidingWindow(WindowOpening): Average measured value of discharge coefficient for sliding or side-hung windows. """ - return config.ventilation['natural']['discharge_factor']['sliding'] + return config.ventilation['natural']['discharge_factor']['sliding'] # type: ignore @dataclass(frozen=True) @@ -861,7 +861,7 @@ class _PopulationWithVirus(Population): The fraction of infectious virus. """ - return config.population_with_virus['fraction_of_infectious_virus'] + return config.population_with_virus['fraction_of_infectious_virus'] # type: ignore def aerosols(self): """ @@ -1033,7 +1033,7 @@ class _ConcentrationModelBase: (in the same unit as the concentration). Its the value towards which the concentration will decay to. """ - return config.concentration_model['min_background_concentration'] + return config.concentration_model['min_background_concentration'] # type: ignore def normalization_factor(self) -> _VectorisedFloat: """ @@ -1221,7 +1221,7 @@ class ConcentrationModel(_ConcentrationModelBase): #: evaporation factor: the particles' diameter is multiplied by this # factor as soon as they are in the air (but AFTER going out of the, # mask, if any). - evaporation_factor: float = config.particle['evaporation_factor'] + evaporation_factor: float = config.particle['evaporation_factor'] # type: ignore @property def population(self) -> InfectedPopulation: @@ -1261,10 +1261,10 @@ class CO2ConcentrationModel(_ConcentrationModelBase): CO2_emitters: SimplePopulation #: CO2 concentration in the atmosphere (in ppm) - CO2_atmosphere_concentration: float = config.concentration_model['CO2_concentration_model']['CO2_atmosphere_concentration'] + CO2_atmosphere_concentration: float = config.concentration_model['CO2_concentration_model']['CO2_atmosphere_concentration'] # type: ignore #: CO2 fraction in the exhaled air - CO2_fraction_exhaled: float = config.concentration_model['CO2_concentration_model']['CO2_fraction_exhaled'] + CO2_fraction_exhaled: float = config.concentration_model['CO2_concentration_model']['CO2_fraction_exhaled'] # type: ignore @property def population(self) -> SimplePopulation: @@ -1310,14 +1310,14 @@ class ShortRangeModel: The dilution factor for the respective expiratory activity type. ''' # Average mouth opening diameter (m) - mouth_diameter: float = config.short_range_model['dilution_factor']['mouth_diameter'] + mouth_diameter: float = config.short_range_model['dilution_factor']['mouth_diameter'] # type: ignore # Breathing rate, from m3/h to m3/s BR = np.array(self.activity.exhalation_rate/3600.) # Exhalation coefficient. Ratio between the duration of a breathing cycle and the duration of # the exhalation. - φ: float = config.short_range_model['dilution_factor']['exhalation_coefficient'] + φ: float = config.short_range_model['dilution_factor']['exhalation_coefficient'] # type: ignore # Exhalation airflow, as per Jia et al. (2022) Q_exh: _VectorisedFloat = φ * BR @@ -1329,12 +1329,12 @@ class ShortRangeModel: u0 = np.array(Q_exh/Am) # Duration of the expiration period(s), assuming a 4s breath-cycle - tstar: float = config.short_range_model['dilution_factor']['tstar'] + tstar: float = config.short_range_model['dilution_factor']['tstar'] # type: ignore # Streamwise and radial penetration coefficients - 𝛽r1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r1'] - 𝛽r2: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r2'] - 𝛽x1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽x1'] + 𝛽r1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r1'] # type: ignore + 𝛽r2: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽r2'] # type: ignore + 𝛽x1: float = config.short_range_model['dilution_factor']['penetration_coefficients']['𝛽x1'] # type: ignore # Parameters in the jet-like stage # Position of virtual origin @@ -1490,7 +1490,7 @@ class ExposureModel: geographical_data: Cases #: The number of times the exposure event is repeated (default 1). - repeats: int = config.exposure_model['repeats'] + repeats: int = config.exposure_model['repeats'] # type: ignore def __post_init__(self): """ diff --git a/caimira/store/configuration.py b/caimira/store/configuration.py index e1272c5c..155d80b2 100644 --- a/caimira/store/configuration.py +++ b/caimira/store/configuration.py @@ -438,7 +438,7 @@ class Configuration: "activity": "Seated", "expiration": {"Breathing": 1, "Speaking": 9}, }, - "precise": {"activity": None, "expiration": None}, + "precise": {"activity": '', "expiration": {}}, } def update(self, data): From fcf0de7115d52985026c38ce7421e894e969e1da Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 17 Nov 2023 09:25:28 +0100 Subject: [PATCH 09/10] added types requests spec --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f9cb7dba..41b69c1b 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ REQUIREMENTS: dict = { 'numpy-stubs @ git+https://github.com/numpy/numpy-stubs.git', 'types-dataclasses', 'types-python-dateutil', + 'types-requests', ], 'dev': [ 'jupyterlab', From 36d105b358366212212102faebc9b269118086bc Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Fri, 17 Nov 2023 11:38:15 +0100 Subject: [PATCH 10/10] updated version --- caimira/apps/calculator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 80476955..452df4ef 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -37,7 +37,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.12.1" +__version__ = "4.13.0" LOG = logging.getLogger(__name__)