From bb51eaa36050962f72caf74ac12ff2a76ca3d60c Mon Sep 17 00:00:00 2001 From: Nicola Tarocco Date: Wed, 29 Nov 2023 08:54:58 +0100 Subject: [PATCH 1/7] config: inject data registry instance instead of static config * closes #365 --- .../auth-service/auth_service/__init__.py | 2 +- caimira/apps/calculator/__init__.py | 113 +++-- caimira/apps/calculator/__main__.py | 10 +- .../apps/calculator/co2_model_generator.py | 24 +- caimira/apps/calculator/defaults.py | 10 +- caimira/apps/calculator/form_data.py | 23 +- caimira/apps/calculator/model_generator.py | 101 ++-- caimira/apps/calculator/report_generator.py | 79 +-- caimira/apps/expert.py | 168 +++---- caimira/apps/expert/caimira.ipynb | 2 +- caimira/apps/expert_co2.py | 125 ++--- .../templates/base/calculator.report.html.j2 | 56 +-- caimira/models.py | 179 ++++--- caimira/monte_carlo/data.py | 454 +++++++++--------- .../{configuration.py => data_registry.py} | 13 +- caimira/store/data_service.py | 42 +- caimira/tests/apps/calculator/conftest.py | 4 +- .../apps/calculator/test_model_generator.py | 74 +-- .../test_specific_model_generator.py | 17 +- caimira/tests/conftest.py | 18 +- .../models/test_co2_concentration_model.py | 3 +- .../tests/models/test_concentration_model.py | 47 +- .../tests/models/test_dynamic_population.py | 27 +- caimira/tests/models/test_exposure_model.py | 67 +-- .../tests/models/test_short_range_model.py | 33 +- caimira/tests/test_conditional_probability.py | 21 +- caimira/tests/test_data_service.py | 10 +- caimira/tests/test_expiration.py | 4 +- caimira/tests/test_full_algorithm.py | 76 +-- caimira/tests/test_infected_population.py | 3 +- caimira/tests/test_known_quantities.py | 46 +- caimira/tests/test_monte_carlo.py | 7 +- caimira/tests/test_monte_carlo_full_models.py | 97 ++-- .../tests/test_predefined_distributions.py | 9 +- caimira/tests/test_ventilation.py | 3 +- 35 files changed, 1093 insertions(+), 874 deletions(-) rename caimira/store/{configuration.py => data_registry.py} (98%) diff --git a/app-config/auth-service/auth_service/__init__.py b/app-config/auth-service/auth_service/__init__.py index 9cf81049..c02e9e7f 100644 --- a/app-config/auth-service/auth_service/__init__.py +++ b/app-config/auth-service/auth_service/__init__.py @@ -13,7 +13,7 @@ from tornado.web import Application, RequestHandler import tornado.log -LOG = logging.getLogger(__name__) +LOG = logging.getLogger("AUTH") class BaseHandler(RequestHandler): diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index e7ba083c..6ab1b920 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -11,6 +11,7 @@ import html import json import pandas as pd +from pprint import pformat from io import StringIO import os from pathlib import Path @@ -24,6 +25,9 @@ from tornado.web import Application, RequestHandler, StaticFileHandler from tornado.httpclient import AsyncHTTPClient, HTTPRequest import tornado.log +from caimira.store.data_registry import DataRegistry + +from caimira.store.data_service import DataService from . import markdown_tools from . import model_generator, co2_model_generator @@ -39,11 +43,11 @@ # increase the overall CAiMIRA version (found at ``caimira.__version__``). __version__ = "4.14.2" -LOG = logging.getLogger(__name__) - +LOG = logging.getLogger("APP") + class BaseRequestHandler(RequestHandler): - + async def prepare(self): """Called at the beginning of a request before `get`/`post`/etc.""" @@ -97,20 +101,22 @@ async def prepare(self): class ConcentrationModel(BaseRequestHandler): async def post(self) -> None: + debug = self.settings.get("debug", False) + + data_registry = self.settings.get("data_registry") + data_service = self.settings.get("data_service") + if data_service: + data_service.update_registry(data_registry) + requested_model_config = { name: self.get_argument(name) for name in self.request.arguments } - if self.settings.get("debug", False): - from pprint import pprint - pprint(requested_model_config) - start = datetime.datetime.now() - + LOG.debug(pformat(requested_model_config)) + try: - form = model_generator.VirusFormData.from_dict(requested_model_config) + form = model_generator.VirusFormData.from_dict(requested_model_config, data_registry) except Exception as err: - if self.settings.get("debug", False): - import traceback - print(traceback.format_exc()) + LOG.exception(err) response_json = {'code': 400, 'error': f'Your request was invalid {html.escape(str(err))}'} self.set_status(400) self.finish(json.dumps(response_json)) @@ -126,7 +132,7 @@ async def post(self) -> None: if self.get_cookie('conditional_plot'): form.conditional_probability_plot = True if self.get_cookie('conditional_plot') == '1' else False self.clear_cookie('conditional_plot') # Clears cookie after changing the form value. - + report_task = executor.submit( report_generator.build_report, base_url, form, executor_factory=functools.partial( @@ -151,17 +157,20 @@ async def post(self) -> None: Expects algorithm input in HTTP POST request body in JSON format. Returns report data (algorithm output) in HTTP POST response body in JSON format. """ + debug = self.settings.get("debug", False) + + data_registry = self.settings.get("data_registry") + data_service = self.settings.get("data_service") + if data_service: + data_service.update_configuration(data_registry) + requested_model_config = json.loads(self.request.body) - if self.settings.get("debug", False): - from pprint import pprint - pprint(requested_model_config) + LOG.debug(pformat(requested_model_config)) try: - form = model_generator.VirusFormData.from_dict(requested_model_config) + form = model_generator.VirusFormData.from_dict(requested_model_config, data_registry) except Exception as err: - if self.settings.get("debug", False): - import traceback - print(traceback.format_exc()) + LOG.exception(err) response_json = {'code': 400, 'error': f'Your request was invalid {html.escape(str(err))}'} self.set_status(400) await self.finish(json.dumps(response_json)) @@ -171,14 +180,22 @@ async def post(self) -> None: max_workers=self.settings['handler_worker_pool_size'], timeout=300, ) - report_data_task = executor.submit(calculate_report_data, form, form.build_model()) + model = form.build_model() + report_data_task = executor.submit(calculate_report_data, form, model) report_data: dict = await asyncio.wrap_future(report_data_task) await self.finish(report_data) class StaticModel(BaseRequestHandler): async def get(self) -> None: - form = model_generator.VirusFormData.from_dict(model_generator.baseline_raw_form_data()) + debug = self.settings.get("debug", False) + + data_registry = self.settings.get("data_registry") + data_service = self.settings.get("data_service") + if data_service: + data_service.update_configuration(data_registry) + + form = model_generator.VirusFormData.from_dict(model_generator.baseline_raw_form_data(), data_registry) base_url = self.request.protocol + "://" + self.request.host report_generator: ReportGenerator = self.settings['report_generator'] executor = loky.get_reusable_executor(max_workers=self.settings['handler_worker_pool_size']) @@ -245,14 +262,14 @@ async def get(self, hotel_id, floor_id): if (client_id == None or client_secret == None or arve_api_key == None): # If the credentials are not defined, we skip the ARVE API connection return self.send_error(401) - + http_client = AsyncHTTPClient() URL = 'https://arveapi.auth.eu-central-1.amazoncognito.com/oauth2/token' headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": b"Basic " + base64.b64encode(f'{client_id}:{client_secret}'.encode()) } - + try: response = await http_client.fetch(HTTPRequest( url=URL, @@ -263,9 +280,9 @@ async def get(self, hotel_id, floor_id): raise_error=True) except Exception as e: print("Something went wrong: %s" % e) - + access_token = json.loads(response.body)['access_token'] - + URL = f'https://api.arve.swiss/v1/{hotel_id}/{floor_id}' headers = { "x-api-key": arve_api_key, @@ -280,11 +297,11 @@ async def get(self, hotel_id, floor_id): raise_error=True) except Exception as e: print("Something went wrong: %s" % e) - + self.set_header("Content-Type", 'application/json') return self.finish(response.body) - + class CasesData(BaseRequestHandler): async def get(self, country): http_client = AsyncHTTPClient() @@ -300,7 +317,7 @@ async def get(self, country): print("Something went wrong: %s" % e) country_name = json.loads(response.body)['name']['common'] - + # Get global incident rates URL = 'https://covid19.who.int/WHO-COVID-19-global-data.csv' try: @@ -321,7 +338,7 @@ async def get(self, country): # If any of the 'New_cases' is 0, it means the data is not updated. if (cases.loc[eight_days_ago:current_date]['New_cases'] == 0).any(): return self.finish('') return self.finish(str(round(cases.loc[eight_days_ago:current_date]['New_cases'].mean()))) - + class GenericExtraPage(BaseRequestHandler): @@ -340,7 +357,7 @@ def get(self): active_page=self.active_page, text_blocks=template_environment.globals["common_text"] )) - + class CO2ModelResponse(BaseRequestHandler): def check_xsrf_cookie(self): @@ -349,11 +366,16 @@ def check_xsrf_cookie(self): Thus, XSRF cookies are disabled by overriding base class implementation of this method with a pass statement. """ pass - + async def post(self, endpoint: str) -> None: + data_registry = self.settings.get("data_registry") + data_service = self.settings.get("data_service") + if data_service: + data_service.update_configuration(data_registry) + requested_model_config = tornado.escape.json_decode(self.request.body) try: - form = co2_model_generator.CO2FormData.from_dict(requested_model_config) + form = co2_model_generator.CO2FormData.from_dict(requested_model_config, data_registry) except Exception as err: if self.settings.get("debug", False): import traceback @@ -376,17 +398,17 @@ async def post(self, endpoint: str) -> None: co2_model_generator.CO2FormData.build_model, form, ) report = await asyncio.wrap_future(report_task) - + result = dict(report.CO2_fit_params()) ventilation_transition_times = report.ventilation_transition_times result['fitting_ventilation_type'] = form.fitting_ventilation_type result['transition_times'] = ventilation_transition_times - result['CO2_plot'] = co2_model_generator.CO2FormData.generate_ventilation_plot(CO2_data=form.CO2_data, - transition_times=ventilation_transition_times[:-1], + result['CO2_plot'] = co2_model_generator.CO2FormData.generate_ventilation_plot(CO2_data=form.CO2_data, + transition_times=ventilation_transition_times[:-1], predictive_CO2=result['predictive_CO2']) self.finish(result) - + def get_url(app_root: str, relative_path: str = '/'): return app_root.rstrip('/') + relative_path.rstrip('/') @@ -413,7 +435,7 @@ def make_app( (get_root_calculator_url(r'/report'), ConcentrationModel), (get_root_url(r'/static/(.*)'), StaticFileHandler, {'path': static_dir}), (get_root_calculator_url(r'/static/(.*)'), StaticFileHandler, {'path': calculator_static_dir}), - ] + ] urls: typing.List = base_urls + [ (get_root_url(r'/_c/(.*)'), CompressedCalculatorFormInputs), @@ -429,7 +451,7 @@ def make_app( 'active_page': 'calculator/user-guide', 'filename': 'userguide.html.j2'}), ] - + interface: str = os.environ.get('CAIMIRA_THEME', '') if interface != '' and (interface != '' and 'cern' not in interface): urls = list(filter(lambda i: i in base_urls, urls)) @@ -468,9 +490,22 @@ def make_app( if debug: tornado.log.enable_pretty_logging() + data_registry = DataRegistry() + data_service = None + 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.create(credentials) + return Application( urls, debug=debug, + data_registry=data_registry, + data_service=data_service, template_environment=template_environment, default_handler_class=Missing404Handler, report_generator=ReportGenerator(loader, get_root_url, get_root_calculator_url), diff --git a/caimira/apps/calculator/__main__.py b/caimira/apps/calculator/__main__.py index d9c46bd5..ab147c7d 100644 --- a/caimira/apps/calculator/__main__.py +++ b/caimira/apps/calculator/__main__.py @@ -1,4 +1,5 @@ import argparse +import logging from pathlib import Path from tornado.ioloop import IOLoop @@ -37,13 +38,18 @@ def configure_parser(parser) -> argparse.ArgumentParser: def main(): parser = configure_parser(argparse.ArgumentParser()) args = parser.parse_args() + + debug = args.no_debug + logging.getLogger().setLevel(logging.DEBUG if debug else logging.WARNING) + theme_dir = args.theme if theme_dir is not None: theme_dir = Path(theme_dir).absolute() assert theme_dir.exists() - app = make_app(debug=args.no_debug, APPLICATION_ROOT=args.app_root, calculator_prefix=args.prefix, theme_dir=theme_dir) + + app = make_app(debug=debug, APPLICATION_ROOT=args.app_root, calculator_prefix=args.prefix, theme_dir=theme_dir) app.listen(args.port) - IOLoop.instance().start() + IOLoop.current().start() if __name__ == '__main__': diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index 3d90fb99..cd21eafc 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -2,13 +2,14 @@ import logging import typing import numpy as np +from caimira.store.data_registry import DataRegistry import ruptures as rpt import matplotlib.pyplot as plt import re from caimira import models from .form_data import FormData, cast_class_fields -from .defaults import DEFAULT_MC_SAMPLE_SIZE, NO_DEFAULT +from .defaults import NO_DEFAULT from .report_generator import img2base64, _figure2bytes minutes_since_midnight = typing.NewType('minutes_since_midnight', int) @@ -49,7 +50,7 @@ class CO2FormData(FormData): 'total_people': NO_DEFAULT, } - def __init__(self, **kwargs): + def __init__(self, **kwargs): # Set default values defined in CO2FormData for key, value in self._DEFAULTS.items(): setattr(self, key, kwargs.get(key, value)) @@ -62,7 +63,7 @@ def validate(self): if self.specific_breaks != {}: if type(self.specific_breaks) is not dict: raise TypeError('The specific breaks should be in a dictionary.') - + dict_keys = list(self.specific_breaks.keys()) if "exposed_breaks" not in dict_keys: raise TypeError(f'Unable to fetch "exposed_breaks" key. Got "{dict_keys[0]}".') @@ -119,10 +120,10 @@ def find_change_points_with_pelt(self, CO2_data: dict): result_set.add(times[CO2_values.index(min(CO2_np[segment]))]) result_set.add(times[CO2_values.index(max(CO2_np[segment]))]) return list(result_set) - + @classmethod - def generate_ventilation_plot(self, CO2_data: dict, - transition_times: typing.Optional[list] = None, + def generate_ventilation_plot(self, CO2_data: dict, + transition_times: typing.Optional[list] = None, predictive_CO2: typing.Optional[list] = None): times_values = CO2_data['times'] CO2_values = CO2_data['CO2'] @@ -139,7 +140,7 @@ def generate_ventilation_plot(self, CO2_data: dict, plt.ylabel('Concentration (ppm)') plt.legend() return img2base64(_figure2bytes(fig)) - + def population_present_changes(self, infected_presence: models.Interval, exposed_presence: models.Interval) -> typing.List[float]: state_change_times = set(infected_presence.transition_times()) state_change_times.update(exposed_presence.transition_times()) @@ -154,10 +155,11 @@ def ventilation_transition_times(self) -> typing.Tuple[float, ...]: else: return tuple((self.CO2_data['times'][0], self.CO2_data['times'][-1])) - def build_model(self, size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2DataModel: # type: ignore + def build_model(self, size=None) -> models.CO2DataModel: # type: ignore + size = size or self.data_registry.monte_carlo_sample_size # Build a simple infected and exposed population for the case when presence # intervals and number of people are dynamic. Activity type is not needed. - infected_presence = self.infected_present_interval() + infected_presence = self.infected_present_interval() infected_population = models.SimplePopulation( number=self.infected_people, presence=infected_presence, @@ -173,7 +175,7 @@ def build_model(self, size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2DataModel: # typ all_state_changes=self.population_present_changes(infected_presence, exposed_presence) total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) for _, stop in zip(all_state_changes[:-1], all_state_changes[1:])] - + return models.CO2DataModel( room_volume=self.room_volume, number=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), @@ -182,5 +184,5 @@ def build_model(self, size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2DataModel: # typ times=self.CO2_data['times'], CO2_concentrations=self.CO2_data['CO2'], ) - + cast_class_fields(CO2FormData) diff --git a/caimira/apps/calculator/defaults.py b/caimira/apps/calculator/defaults.py index 82f1cf4a..661918d1 100644 --- a/caimira/apps/calculator/defaults.py +++ b/caimira/apps/calculator/defaults.py @@ -1,13 +1,10 @@ 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 = 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. @@ -82,17 +79,13 @@ # ------------------ Activities ---------------------- -ACTIVITIES: typing.Dict[str, typing.Dict] = config.population_scenario_activity - # ------------------ Validation ---------------------- -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: typing.List[str] = list(config.mask_distributions.keys()) MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', @@ -104,9 +97,8 @@ VACCINE_TYPE = ['Ad26.COV2.S_(Janssen)', 'Any_mRNA_-_heterologous', 'AZD1222_(AstraZeneca)', 'AZD1222_(AstraZeneca)_and_any_mRNA_-_heterologous', 'AZD1222_(AstraZeneca)_and_BNT162b2_(Pfizer)', 'BBIBP-CorV_(Beijing_CNBG)', 'BNT162b2_(Pfizer)', 'BNT162b2_(Pfizer)_and_mRNA-1273_(Moderna)', 'CoronaVac_(Sinovac)', 'CoronaVac_(Sinovac)_and_AZD1222_(AstraZeneca)', 'Covishield', 'mRNA-1273_(Moderna)', 'Sputnik_V_(Gamaleya)', 'CoronaVac_(Sinovac)_and_BNT162b2_(Pfizer)'] -VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', +VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation', 'from_fitting'} -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'} diff --git a/caimira/apps/calculator/form_data.py b/caimira/apps/calculator/form_data.py index bdf61d8b..ecb98e13 100644 --- a/caimira/apps/calculator/form_data.py +++ b/caimira/apps/calculator/form_data.py @@ -9,7 +9,8 @@ import numpy as np from caimira import models -from .defaults import DEFAULTS, NO_DEFAULT, COFFEE_OPTIONS_INT, DEFAULT_MC_SAMPLE_SIZE +from caimira.store.data_registry import DataRegistry +from .defaults import DEFAULTS, NO_DEFAULT, COFFEE_OPTIONS_INT LOG = logging.getLogger(__name__) @@ -38,10 +39,12 @@ class FormData: room_volume: float total_people: int + data_registry: DataRegistry + _DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = DEFAULTS @classmethod - def from_dict(cls, form_data: typing.Dict): + def from_dict(cls, form_data: typing.Dict, data_registry: DataRegistry): # Take a copy of the form data so that we can mutate it. form_data = form_data.copy() form_data.pop('_xsrf', None) @@ -64,7 +67,7 @@ def from_dict(cls, form_data: typing.Dict): if key not in cls._DEFAULTS: raise ValueError(f'Invalid argument "{html.escape(key)}" given') - instance = cls(**form_data) + instance = cls(**form_data, data_registry=data_registry) instance.validate() return instance @@ -87,7 +90,7 @@ def to_dict(cls, form: "FormData", strip_defaults: bool = False) -> dict: if default is not NO_DEFAULT and value in [default, 'not-applicable']: form_dict.pop(attr) return form_dict - + def validate_population_parameters(self): # Validate number of infected <= number of total people if self.infected_people >= self.total_people: @@ -113,7 +116,7 @@ def validate_population_parameters(self): def validate_lunch(start, finish): lunch_start = getattr(self, f'{population}_lunch_start') lunch_finish = getattr(self, f'{population}_lunch_finish') - return (start <= lunch_start <= finish and + return (start <= lunch_start <= finish and start <= lunch_finish <= finish) def get_lunch_mins(population): @@ -121,7 +124,7 @@ def get_lunch_mins(population): if getattr(self, f'{population}_lunch_option'): lunch_mins = getattr(self, f'{population}_lunch_finish') - getattr(self, f'{population}_lunch_start') return lunch_mins - + def get_coffee_mins(population): coffee_mins = 0 if getattr(self, f'{population}_coffee_break_option') != 'coffee_break_0': @@ -146,8 +149,8 @@ def get_activity_mins(population): raise ValueError( f"Length of breaks >= Length of {population} presence." ) - - for attr_name, valid_set in [('exposed_coffee_break_option', COFFEE_OPTIONS_INT), + + for attr_name, valid_set in [('exposed_coffee_break_option', COFFEE_OPTIONS_INT), ('infected_coffee_break_option', COFFEE_OPTIONS_INT)]: if getattr(self, attr_name) not in valid_set: raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}") @@ -155,7 +158,7 @@ def get_activity_mins(population): def validate(self): raise NotImplementedError("Subclass must implement") - def build_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE): + def build_model(self, sample_size=None): raise NotImplementedError("Subclass must implement") def _compute_breaks_in_interval(self, start, finish, n_breaks, duration) -> models.BoundarySequence_t: @@ -342,7 +345,7 @@ def infected_present_interval(self) -> models.Interval: self.infected_start, self.infected_finish, breaks=breaks, ) - + def population_present_interval(self) -> models.Interval: state_change_times = set(self.infected_present_interval().transition_times()) state_change_times.update(self.exposed_present_interval().transition_times()) diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 19419b96..03bfba17 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -14,12 +14,11 @@ from .form_data import FormData, cast_class_fields, time_string_to_minutes from caimira.monte_carlo.data import activity_distributions, virus_distributions, mask_distributions, short_range_distances from caimira.monte_carlo.data import expiration_distribution, expiration_BLO_factors, expiration_distributions, short_range_expiration_distributions -from .defaults import (DEFAULT_MC_SAMPLE_SIZE, DEFAULTS, ACTIVITIES, ACTIVITY_TYPES, 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 +from .defaults import (DEFAULTS, CONFIDENCE_LEVEL_OPTIONS, + MECHANICAL_VENTILATION_TYPES, MASK_WEARING_OPTIONS, MONTH_NAMES, VACCINE_BOOSTER_TYPE, VACCINE_TYPE, + VENTILATION_TYPES, VOLUME_TYPES, WINDOWS_OPENING_REGIMES, WINDOWS_TYPES) -LOG = logging.getLogger(__name__) +LOG = logging.getLogger("MODEL") minutes_since_midnight = typing.NewType('minutes_since_midnight', int) @@ -75,17 +74,17 @@ class VirusFormData(FormData): short_range_interactions: list _DEFAULTS: typing.ClassVar[typing.Dict[str, typing.Any]] = DEFAULTS - + def validate(self): # Validate population parameters self.validate_population_parameters() - validation_tuples = [('activity_type', ACTIVITY_TYPES), + validation_tuples = [('activity_type', self.data_registry.population_scenario_activity.keys()), ('mechanical_ventilation_type', MECHANICAL_VENTILATION_TYPES), - ('mask_type', MASK_TYPES), + ('mask_type', list(mask_distributions(self.data_registry).keys())), ('mask_wearing_option', MASK_WEARING_OPTIONS), ('ventilation_type', VENTILATION_TYPES), - ('virus_type', VIRUS_TYPES), + ('virus_type', list(virus_distributions(self.data_registry).keys())), ('volume_type', VOLUME_TYPES), ('window_opening_regime', WINDOWS_OPENING_REGIMES), ('window_type', WINDOWS_TYPES), @@ -93,11 +92,11 @@ def validate(self): ('ascertainment_bias', CONFIDENCE_LEVEL_OPTIONS), ('vaccine_type', VACCINE_TYPE), ('vaccine_booster_type', VACCINE_BOOSTER_TYPE),] - + for attr_name, valid_set in validation_tuples: if getattr(self, attr_name) not in valid_set: raise ValueError(f"{getattr(self, attr_name)} is not a valid value for {attr_name}") - + # Validate number of infected people == 1 when activity is Conference/Training. if self.activity_type == 'training' and self.infected_people > 1: raise ValueError('Conference/Training activities are limited to 1 infected.') @@ -129,7 +128,7 @@ def validate(self): if self.specific_breaks != {}: if type(self.specific_breaks) is not dict: raise TypeError('The specific breaks should be in a dictionary.') - + dict_keys = list(self.specific_breaks.keys()) if "exposed_breaks" not in dict_keys: raise TypeError(f'Unable to fetch "exposed_breaks" key. Got "{dict_keys[0]}".') @@ -163,7 +162,7 @@ def validate(self): raise TypeError(f'Unable to fetch "physical_activity" key. Got "{dict_keys[0]}".') if "respiratory_activity" not in dict_keys: raise TypeError(f'Unable to fetch "respiratory_activity" key. Got "{dict_keys[1]}".') - + if type(self.precise_activity['physical_activity']) is not str: raise TypeError('The physical activities should be a single string.') @@ -180,7 +179,7 @@ def validate(self): if "percentage" not in dict_keys: raise TypeError(f'Unable to fetch "percentage" key. Got "{dict_keys[1]}".') total_percentage += respiratory_activity['percentage'] - + if total_percentage != 100: raise ValueError(f'The sum of all respiratory activities should be 100. Got {total_percentage}.') @@ -193,10 +192,10 @@ def initialize_room(self) -> models.Room: if self.arve_sensors_option == False: if self.room_heating_option: - humidity = config.room['defaults']['humidity_with_heating'] + humidity = self.data_registry.room['defaults']['humidity_with_heating'] else: - humidity = config.room['defaults']['humidity_without_heating'] - inside_temp = config.room['defaults']['inside_temp'] + humidity = self.data_registry.room['defaults']['humidity_without_heating'] + inside_temp = self.data_registry.room['defaults']['inside_temp'] else: humidity = float(self.humidity) inside_temp = self.inside_temp @@ -204,14 +203,15 @@ def initialize_room(self) -> models.Room: return models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0, 24), (inside_temp,)), humidity=humidity) def build_mc_model(self) -> mc.ExposureModel: - room = self.initialize_room() + room = self.initialize_room() ventilation: models._VentilationBase = self.ventilation() infected_population = self.infected_population() - + short_range = [] if self.short_range_option == "short_range_yes": for interaction in self.short_range_interactions: short_range.append(mc.ShortRangeModel( + data_registry=self.data_registry, expiration=short_range_expiration_distributions[interaction['expiration']], activity=infected_population.activity, presence=self.short_range_interval(interaction), @@ -219,7 +219,9 @@ def build_mc_model(self) -> mc.ExposureModel: )) return mc.ExposureModel( + data_registry=self.data_registry, concentration_model=mc.ConcentrationModel( + data_registry=self.data_registry, room=room, ventilation=ventilation, infected=infected_population, @@ -234,10 +236,12 @@ def build_mc_model(self) -> mc.ExposureModel: ), ) - def build_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE) -> models.ExposureModel: + def build_model(self, sample_size=None) -> models.ExposureModel: + sample_size = sample_size or self.data_registry.monte_carlo_sample_size return self.build_mc_model().build_model(size=sample_size) - def build_CO2_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2ConcentrationModel: + def build_CO2_model(self, sample_size=None) -> models.CO2ConcentrationModel: + sample_size = sample_size or self.data_registry.monte_carlo_sample_size infected_population: models.InfectedPopulation = self.infected_population().build_model(sample_size) exposed_population: models.Population = self.exposed_population().build_model(sample_size) @@ -245,22 +249,23 @@ def build_CO2_model(self, sample_size=DEFAULT_MC_SAMPLE_SIZE) -> models.CO2Conce state_change_times.update(exposed_population.presence_interval().transition_times()) transition_times = sorted(state_change_times) - total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) + total_people = [infected_population.people_present(stop) + exposed_population.people_present(stop) for _, stop in zip(transition_times[:-1], transition_times[1:])] - + if (self.activity_type == 'precise'): activity_defn, _ = self.generate_precise_activity_expiration() else: - activity_defn = activity_defn = ACTIVITIES[self.activity_type]['activity'] + activity_defn = self.data_registry.population_scenario_activity[self.activity_type]['activity'] population = mc.SimplePopulation( number=models.IntPiecewiseConstant(transition_times=tuple(transition_times), values=tuple(total_people)), presence=None, - activity=activity_distributions[activity_defn], + activity=activity_distributions(self.data_registry)[activity_defn], ) - + # Builds a CO2 concentration model based on model inputs return mc.CO2ConcentrationModel( + data_registry=self.data_registry, room=self.initialize_room(), ventilation=self.ventilation(), CO2_emitters=population, @@ -314,14 +319,14 @@ def outside_temp(self) -> models.PiecewiseConstant: def ventilation(self) -> models._VentilationBase: always_on = models.PeriodicInterval(period=120, duration=120) - periodic_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, + periodic_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, min(self.infected_start, self.exposed_start)/60) if self.ventilation_type == 'from_fitting': ventilations = [] if self.CO2_fitting_result['fitting_ventilation_type'] == 'fitting_natural_ventilation': transition_times = self.CO2_fitting_result['transition_times'] for index, (start, stop) in enumerate(zip(transition_times[:-1], transition_times[1:])): - ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), + ventilations.append(models.AirChange(active=models.SpecificInterval(present_times=((start, stop), )), air_exch=self.CO2_fitting_result['ventilation_values'][index])) else: ventilations.append(models.AirChange(active=always_on, air_exch=self.CO2_fitting_result['ventilation_values'][0])) @@ -339,6 +344,7 @@ def ventilation(self) -> models._VentilationBase: ventilation: models.Ventilation if self.window_type == 'window_sliding': ventilation = models.SlidingWindow( + data_registry=self.data_registry, active=window_interval, outside_temp=outside_temp, window_height=self.window_height, @@ -367,7 +373,7 @@ def ventilation(self) -> models._VentilationBase: # This is a minimal, always present source of ventilation, due # to the air infiltration from the outside. # See CERN-OPEN-2021-004, p. 12. - residual_vent: float = config.ventilation['infiltration_ventilation'] # type: ignore + residual_vent: float = self.data_registry.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) @@ -385,7 +391,7 @@ def mask(self) -> models.Mask: # Initializes the mask type if mask wearing is "continuous", otherwise instantiates the mask attribute as # the "No mask"-mask if self.mask_wearing_option == 'mask_on': - mask = mask_distributions[self.mask_type] + mask = mask_distributions(self.data_registry)[self.mask_type] else: mask = models.Mask.types['No mask'] return mask @@ -396,28 +402,29 @@ def generate_precise_activity_expiration(self) -> typing.Tuple[typing.Any, ...]: respiratory_dict = {} for respiratory_activity in self.precise_activity['respiratory_activity']: respiratory_dict[respiratory_activity['type']] = respiratory_activity['percentage'] - + return (self.precise_activity['physical_activity'], respiratory_dict) def infected_population(self) -> mc.InfectedPopulation: # Initializes the virus - virus = virus_distributions[self.virus_type] + virus = virus_distributions(self.data_registry)[self.virus_type] - activity_defn = ACTIVITIES[self.activity_type]['activity'] - expiration_defn = ACTIVITIES[self.activity_type]['expiration'] + activity_defn = self.data_registry.population_scenario_activity[self.activity_type]['activity'] + expiration_defn = self.data_registry.population_scenario_activity[self.activity_type]['expiration'] if (self.activity_type == 'smallmeeting'): # Conversation of N people is approximately 1/N% of the time speaking. expiration_defn = {'Speaking': 1, 'Breathing': self.total_people - 1} elif (self.activity_type == 'precise'): - activity_defn, expiration_defn = self.generate_precise_activity_expiration() + activity_defn, expiration_defn = self.generate_precise_activity_expiration() - activity = activity_distributions[activity_defn] - expiration = build_expiration(expiration_defn) + activity = activity_distributions(self.data_registry)[activity_defn] + expiration = build_expiration(self.data_registry, expiration_defn) infected_occupants = self.infected_people infected = mc.InfectedPopulation( + data_registry=self.data_registry, number=infected_occupants, virus=virus, presence=self.infected_present_interval(), @@ -429,10 +436,10 @@ def infected_population(self) -> mc.InfectedPopulation: return infected def exposed_population(self) -> mc.Population: - 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] + activity_defn = (self.precise_activity['physical_activity'] + if self.activity_type == 'precise' + else str(self.data_registry.population_scenario_activity[self.activity_type]['activity'])) + activity = activity_distributions(self.data_registry)[activity_defn] infected_occupants = self.infected_people # The number of exposed occupants is the total number of occupants @@ -441,8 +448,8 @@ def exposed_population(self) -> mc.Population: if (self.vaccine_option): if (self.vaccine_booster_option and self.vaccine_booster_type != 'Other'): - host_immunity = [vaccine['VE'] for vaccine in data.vaccine_booster_host_immunity if - vaccine['primary series vaccine'] == self.vaccine_type and + host_immunity = [vaccine['VE'] for vaccine in data.vaccine_booster_host_immunity if + vaccine['primary series vaccine'] == self.vaccine_type and vaccine['booster vaccine'] == self.vaccine_booster_type][0] else: host_immunity = data.vaccine_primary_host_immunity[self.vaccine_type] @@ -464,16 +471,16 @@ def short_range_interval(self, interaction) -> models.SpecificInterval: return models.SpecificInterval(present_times=((start_time/60, (start_time + duration)/60),)) -def build_expiration(expiration_definition) -> mc._ExpirationBase: +def build_expiration(data_registry, expiration_definition) -> mc._ExpirationBase: if isinstance(expiration_definition, str): return expiration_distributions[expiration_definition] elif isinstance(expiration_definition, dict): total_weight = sum(expiration_definition.values()) BLO_factors = np.sum([ - np.array(expiration_BLO_factors[exp_type]) * weight/total_weight + np.array(expiration_BLO_factors(data_registry)[exp_type]) * weight/total_weight for exp_type, weight in expiration_definition.items() ], axis=0) - return expiration_distribution(BLO_factors=tuple(BLO_factors)) + return expiration_distribution(data_registry=data_registry, BLO_factors=tuple(BLO_factors)) def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]: @@ -525,7 +532,7 @@ def baseline_raw_form_data() -> typing.Dict[str, typing.Union[str, float]]: 'total_people': '10', 'vaccine_option': '0', 'vaccine_booster_option': '0', - 'vaccine_type': 'Ad26.COV2.S_(Janssen)', + 'vaccine_type': 'Ad26.COV2.S_(Janssen)', 'vaccine_booster_type': 'AZD1222_(AstraZeneca)', 'ventilation_type': 'natural_ventilation', 'virus_type': 'SARS_CoV_2', diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index e3757229..9ff0358e 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -14,10 +14,10 @@ from caimira import models from caimira.apps.calculator import markdown_tools +from caimira.store.data_registry import DataRegistry from ... import monte_carlo as mc -from .model_generator import VirusFormData, DEFAULT_MC_SAMPLE_SIZE +from .model_generator import VirusFormData from ... import dataclass_utils -from caimira.store.configuration import config def model_start_end(model: models.ExposureModel): @@ -84,7 +84,7 @@ def walk_model(model, name=""): def interesting_times(model: models.ExposureModel, approx_n_pts: typing.Optional[int] = None) -> typing.List[float]: """ Pick approximately ``approx_n_pts`` time points which are interesting for the - given model. If not provided by argument, ``approx_n_pts`` is set to be 15 times + given model. If not provided by argument, ``approx_n_pts`` is set to be 15 times the number of hours of the simulation. Initially the times are seeded by important state change times (excluding @@ -118,13 +118,13 @@ def calculate_report_data(form: VirusFormData, model: models.ExposureModel) -> t times = interesting_times(model) short_range_intervals = [interaction.presence.boundaries()[0] for interaction in model.short_range] short_range_expirations = [interaction['expiration'] for interaction in form.short_range_interactions] if form.short_range_option == "short_range_yes" else [] - + concentrations = [ np.array(model.concentration(float(time))).mean() for time in times - ] + ] lower_concentrations = concentrations_with_sr_breathing(form, model, times, short_range_intervals) - + cumulative_doses = np.cumsum([ np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() for time1, time2 in zip(times[:-1], times[1:]) @@ -146,8 +146,8 @@ def calculate_report_data(form: VirusFormData, model: models.ExposureModel) -> t expected_new_cases = np.array(model.expected_new_cases()).mean() uncertainties_plot_src = img2base64(_figure2bytes(uncertainties_plot(model, prob))) if form.conditional_probability_plot else None exposed_presence_intervals = [list(interval) for interval in model.exposed.presence_interval().boundaries()] - conditional_probability_data = {key: value for key, value in - zip(('viral_loads', 'pi_means', 'lower_percentiles', 'upper_percentiles'), + conditional_probability_data = {key: value for key, value in + zip(('viral_loads', 'pi_means', 'lower_percentiles', 'upper_percentiles'), manufacture_conditional_probability_data(model, prob))} @@ -194,45 +194,54 @@ def generate_permalink(base_url, get_root_url, get_root_calculator_url, form: V } -def conditional_prob_inf_given_vl_dist(infection_probability: models._VectorisedFloat, - viral_loads: np.ndarray, specific_vl: float, step: models._VectorisedFloat): +def conditional_prob_inf_given_vl_dist( + data_registry: DataRegistry, + infection_probability: models._VectorisedFloat, + viral_loads: np.ndarray, + specific_vl: float, + step: models._VectorisedFloat + ): + pi_means = [] lower_percentiles = [] upper_percentiles = [] - + 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, 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'])) - + lower_percentiles.append(np.quantile(specific_prob, data_registry.conditional_prob_inf_given_viral_load['lower_percentile'])) + upper_percentiles.append(np.quantile(specific_prob, data_registry.conditional_prob_inf_given_viral_load['upper_percentile'])) + return pi_means, lower_percentiles, upper_percentiles -def manufacture_conditional_probability_data(exposure_model: models.ExposureModel, - infection_probability: models._VectorisedFloat): - - min_vl = config.conditional_prob_inf_given_viral_load['min_vl'] - max_vl = config.conditional_prob_inf_given_viral_load['max_vl'] +def manufacture_conditional_probability_data( + data_registry: DataRegistry, + exposure_model: models.ExposureModel, + infection_probability: models._VectorisedFloat +): + + min_vl = data_registry.conditional_prob_inf_given_viral_load['min_vl'] + max_vl = data_registry.conditional_prob_inf_given_viral_load['max_vl'] step = (max_vl - min_vl)/100 - viral_loads = np.arange(min_vl, max_vl, step) + 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, + pi_means, lower_percentiles, upper_percentiles = conditional_prob_inf_given_vl_dist(infection_probability, viral_loads, specific_vl, step) - + return list(viral_loads), list(pi_means), list(lower_percentiles), list(upper_percentiles) def uncertainties_plot(exposure_model: models.ExposureModel, prob: models._VectorisedFloat): fig = plt.figure(figsize=(4, 7), dpi=110) - + infection_probability = prob / 100 viral_loads, pi_means, lower_percentiles, upper_percentiles = manufacture_conditional_probability_data(exposure_model, infection_probability) - fig, axs = plt.subplots(2, 3, + fig, axs = plt.subplots(2, 3, gridspec_kw={'width_ratios': [5, 0.5] + [1], 'height_ratios': [3, 1], 'wspace': 0}, - sharey='row', + sharey='row', sharex='col') for y, x in [(0, 1)] + [(1, i + 1) for i in range(2)]: @@ -263,14 +272,14 @@ def uncertainties_plot(exposure_model: models.ExposureModel, prob: models._Vecto axs[1, 0].set_xlim(2, 10) axs[1, 0].set_xlabel('Viral load\n(RNA copies)', fontsize=12) axs[0, 0].set_ylabel('Conditional Probability\nof Infection', fontsize=12) - + axs[0, 0].text(9.5, -0.01, '$(i)$') axs[1, 0].text(9.5, axs[1, 0].get_ylim()[1] * 0.8, '$(ii)$') axs[0, 2].set_title('$(iii)$', fontsize=10) axs[0, 0].legend() return fig - + def _img2bytes(figure): # Draw the image @@ -346,7 +355,7 @@ def manufacture_viral_load_scenarios_percentiles(model: mc.ExposureModel) -> typ scenarios = {} for percentil in (0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99): vl = np.quantile(viral_load, percentil) - specific_vl_scenario = dataclass_utils.nested_replace(model, + specific_vl_scenario = dataclass_utils.nested_replace(model, {'concentration_model.infected.virus.viral_load_in_sputum': vl} ) scenarios[str(vl)] = np.mean(specific_vl_scenario.infection_probability()) @@ -396,7 +405,7 @@ def manufacture_alternative_scenarios(form: VirusFormData) -> typing.Dict[str, m scenarios['No ventilation with Type I masks'] = with_mask_no_vent.build_mc_model() if not (form.mask_wearing_option == 'mask_off' and form.ventilation_type == 'no_ventilation'): scenarios['Neither ventilation nor masks'] = without_mask_or_vent.build_mc_model() - + else: no_short_range_alternative = dataclass_utils.replace(form, short_range_interactions=[]) scenarios['Base scenario without short-range interactions'] = no_short_range_alternative.build_mc_model() @@ -404,8 +413,13 @@ def manufacture_alternative_scenarios(form: VirusFormData) -> typing.Dict[str, m return scenarios -def scenario_statistics(mc_model: mc.ExposureModel, sample_times: typing.List[float], compute_prob_exposure: bool): - model = mc_model.build_model(size=DEFAULT_MC_SAMPLE_SIZE) +def scenario_statistics( + data_registry: DataRegistry, + mc_model: mc.ExposureModel, + sample_times: typing.List[float], + compute_prob_exposure: bool +): + model = mc_model.build_model(size=data_registry.monte_carlo_sample_size) if (compute_prob_exposure): # It means we have data to calculate the total_probability_rule prob_probabilistic_exposure = model.total_probability_rule() @@ -440,7 +454,7 @@ def comparison_report( } else: statistics = {} - + if (form.short_range_option == "short_range_yes" and form.exposure_option == "p_probabilistic_exposure"): compute_prob_exposure = True else: @@ -493,6 +507,7 @@ def prepare_context( 'model': model, 'form': form, 'creation_date': time, + 'data_registry_version': model.data_registry.version, } scenario_sample_times = interesting_times(model) diff --git a/caimira/apps/expert.py b/caimira/apps/expert.py index 240e90cb..7082e4b9 100644 --- a/caimira/apps/expert.py +++ b/caimira/apps/expert.py @@ -13,7 +13,8 @@ import datetime import pandas as pd -from caimira import data, models, state +from caimira import data, models, state +from caimira.store.data_registry import DataRegistry def collapsible(widgets_to_collapse: typing.List, title: str, start_collapsed=False): @@ -134,12 +135,12 @@ def initialize_axes(self) -> typing.Tuple[matplotlib.axes.Axes, matplotlib.axes. ax.set_ylabel('Mean concentration ($IRP/m^{3}$)') ax.set_title('Concentration and Cumulative\ndose of Infectious Respiratory Particles') - ax2 = ax.twinx() + ax2 = ax.twinx() ax2.spines['left'].set_visible(False) ax2.spines['top'].set_visible(False) ax2.set_ylabel('Mean cumulative dose (IRP)') ax2.spines['right'].set_linestyle((0,(1,4))) - + return ax, ax2 def update(self, model: models.ExposureModel): @@ -152,7 +153,7 @@ def update_plot(self, model: models.ExposureModel): ts = np.linspace(sorted(infected_presence.transition_times())[0], sorted(infected_presence.transition_times())[-1], resolution) concentration = [model.concentration(t) for t in ts] - + cumulative_doses = np.cumsum([ np.array(model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() for time1, time2 in zip(ts[:-1], ts[1:]) @@ -164,23 +165,23 @@ def update_plot(self, model: models.ExposureModel): else: self.ax.ignore_existing_data_limits = False self.concentration_line.set_data(ts, concentration) - + exposed_presence = model.exposed.presence_interval() if self.concentration_area is None: self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", - where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) | + where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) | (exposed_presence.boundaries()[1][0] < ts) & (ts < exposed_presence.boundaries()[1][1]))) - + else: - self.concentration_area.remove() + self.concentration_area.remove() self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", - where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) | + where = ((exposed_presence.boundaries()[0][0] < ts) & (ts < exposed_presence.boundaries()[0][1]) | (exposed_presence.boundaries()[1][0] < ts) & (ts < exposed_presence.boundaries()[1][1]))) if self.cumulative_line is None: [self.cumulative_line] = self.ax2.plot(ts[:-1], cumulative_doses, color='#0000c8', linestyle='dotted') - + else: self.ax2.ignore_existing_data_limits = False self.cumulative_line.set_data(ts[:-1], cumulative_doses) @@ -190,9 +191,9 @@ def update_plot(self, model: models.ExposureModel): cumulative_top = max(cumulative_doses) self.ax2.set_ylim(bottom=0., top=cumulative_top) - self.ax.set_xlim(left = min(min(infected_presence.boundaries()[0]), min(exposed_presence.boundaries()[0])), + self.ax.set_xlim(left = min(min(infected_presence.boundaries()[0]), min(exposed_presence.boundaries()[0])), right = max(max(infected_presence.boundaries()[1]), max(exposed_presence.boundaries()[1]))) - + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='Mean concentration'), mlines.Line2D([], [], color='#0000c8', markersize=15, ls="dotted", label='Cumulative dose'), patches.Patch(edgecolor="#96cbff", facecolor='#96cbff', label='Presence of exposed person(s)')] @@ -214,7 +215,7 @@ def update_textual_result(self, model: models.ExposureModel): R0 = np.round(np.array(model.reproduction_number()).mean(), 1) lines.append(f'Reproduction number (R0): {R0}') - self.html_output.value = '
\n'.join(lines) + self.html_output.value = '
\n'.join(lines) class ExposureComparisonResult(View): @@ -234,7 +235,7 @@ def initialize_axes(self) -> typing.Tuple[matplotlib.axes.Axes, matplotlib.axes. ax = self.figure.add_subplot(1, 1, 1) ax.spines['right'].set_visible(False) ax.spines['top'].set_visible(False) - + ax.set_xlabel('Time (hours)') ax.set_ylabel('Mean concentration ($IRP/m^{3}$)') ax.set_title('Concentration and Cumulative\ndose of Infectious Respiratory Particles') @@ -258,22 +259,22 @@ def scenarios_updated(self, scenarios: typing.Sequence[ScenarioType], _): def update_plot(self, exp_models: typing.Tuple[models.ExposureModel, ...], labels: typing.Tuple[str, ...]): [line.remove() for line in self.ax.lines] [line.remove() for line in self.ax2.lines] - + start, finish = models_start_end(exp_models) colors=['blue', 'red', 'orange', 'yellow', 'pink', 'purple', 'green', 'brown', 'black' ] ts = np.linspace(start, finish, num=250) concentrations = [[conc_model.concentration_model.concentration(t) for t in ts] for conc_model in exp_models] for label, concentration, color in zip(labels, concentrations, colors): self.ax.plot(ts, concentration, label=label, color=color) - + cumulative_doses = [np.cumsum([ np.array(conc_model.deposited_exposure_between_bounds(float(time1), float(time2))).mean() for time1, time2 in zip(ts[:-1], ts[1:]) ]) for conc_model in exp_models] - + for label, cumulative_dose, color in zip(labels, cumulative_doses, colors): self.ax2.plot(ts[:-1], cumulative_dose, label=label, color=color, linestyle="dotted") - + concentration_top = max([max(np.array(concentration)) for concentration in concentrations]) self.ax.set_ylim(bottom=0., top=concentration_top) cumulative_top = max([max(cumulative_dose) for cumulative_dose in cumulative_doses]) @@ -282,7 +283,7 @@ def update_plot(self, exp_models: typing.Tuple[models.ExposureModel, ...], label handles, labels = self.figure.gca().get_legend_handles_labels() by_label = dict(zip(labels, handles)) self.ax.legend(by_label.values(), by_label.keys()) - + self.figure.canvas.draw() @@ -301,7 +302,7 @@ def _build_widget(self, node): self.widget.children += (self._build_ventilation(node.concentration_model.ventilation),) self.widget.children += (self._build_infected(node.concentration_model.infected, node.concentration_model.ventilation),) self.widget.children += (self._build_exposed(node),) - + def _build_exposed(self, node): return collapsible([widgets.VBox([ self._build_exposed_number(node.exposed), @@ -329,7 +330,7 @@ def on_volume_change(change): # TODO: Link the state back to the widget, not just the other way around. room_volume.observe(on_volume_change, names=['value']) - + return widgets.HBox([widgets.Label('Room volume (m³)'), room_volume], layout=widgets.Layout(justify_content='space-between')) @@ -339,15 +340,15 @@ def _build_room_area(self, node): displayed_volume=widgets.Label('75') def on_room_surface_change(change): - node.volume = change['new']*room_ceiling_height.value + node.volume = change['new']*room_ceiling_height.value displayed_volume.value=str(node.volume) def on_room_ceiling_height_change(change): - node.volume = change['new']*room_surface.value + node.volume = change['new']*room_surface.value displayed_volume.value=str(node.volume) room_surface.observe(on_room_surface_change, names=['value']) - room_ceiling_height.observe(on_room_ceiling_height_change, names=['value']) + room_ceiling_height.observe(on_room_ceiling_height_change, names=['value']) return widgets.VBox([widgets.HBox([widgets.Label('Room surface area (m²) '), room_surface], layout=widgets.Layout(justify_content='space-between', width='100%')), @@ -436,16 +437,16 @@ def on_hinged_window_change(change): # TODO: Link the state back to the widget, not just the other way around. hinged_window.observe(on_hinged_window_change, names=['value']) - + return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between', width='100%')) def _build_sliding_window(self, node): - return widgets.HBox([]) + return widgets.HBox([]) def _build_window(self, node) -> WidgetGroup: window_widgets = { 'Natural': self._build_sliding_window(node._states['Natural']), - 'Hinged window': self._build_hinged_window(node._states['Hinged window']), + 'Hinged window': self._build_hinged_window(node._states['Hinged window']), } for name, widget in window_widgets.items(): @@ -479,7 +480,7 @@ def toggle_window(value): def on_value_change(change): node.number_of_windows = change['new'] - + def on_period_change(change): node.active.period = change['new'] @@ -488,7 +489,7 @@ def on_interval_change(change): def on_opening_length_change(change): node.opening_length = change['new'] - + def on_window_height_change(change): node.window_height = change['new'] @@ -522,9 +523,9 @@ def toggle_outsidetemp(value): result = WidgetGroup( ( ( - widgets.Label('Number of windows ', layout=auto_width), + widgets.Label('Number of windows ', layout=auto_width), number_of_windows, - ), + ), ( widgets.Label('Opening distance (meters)', layout=auto_width), opening_length, @@ -621,7 +622,7 @@ def _build_activity(self, node): if activity == activity_: break activity = widgets.Dropdown(options=list(models.Activity.types.keys()), value=name) - + def on_activity_change(change): act = models.Activity.types[change['new']] node.dcs_update_from(act) @@ -652,7 +653,7 @@ def on_exposed_number_change(change): number.observe(on_exposed_number_change, names=['value']) return widgets.HBox([widgets.Label('Number of exposed people in the room '), number], layout=widgets.Layout(justify_content='space-between')) - + def _build_exposed_presence(self, node): presence_start = generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0]) presence_finish = generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1]) @@ -664,13 +665,13 @@ def on_presence_start_change(change): def on_presence_finish_change(change): new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']]) node.present_times = (node.present_times[0], new_value) - + presence_start.observe(on_presence_start_change, names=['value']) presence_finish.observe(on_presence_finish_change, names=['value']) return widgets.VBox([ - widgets.Label('Exposed presence:'), - widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), + widgets.Label('Exposed presence:'), + widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), widgets.HBox([widgets.Label('Afternoon:', layout=widgets.Layout(width='15%')), presence_finish]) ]) @@ -695,9 +696,9 @@ def _build_expiration(self, node): def on_expiration_change(change): expiration = models.Expiration.types[change['new']] node.dcs_update_from(expiration) - + expiration_choice.observe(on_expiration_change, names=['value']) - + return widgets.HBox([widgets.Label("Expiration"), expiration_choice], layout=widgets.Layout(justify_content='space-between')) def _build_viral_load(self, node): @@ -707,9 +708,9 @@ def on_viral_load_change(change): node.viral_load_in_sputum = float(viral_load_in_sputum.value) viral_load_in_sputum.observe(on_viral_load_change, names=['value']) - + return widgets.HBox([widgets.Label("Viral load (copies/ml)"), viral_load_in_sputum], layout=widgets.Layout(justify_content='space-between')) - + def _build_infected_presence(self, node, ventilation_node): presence_start = generate_presence_widget(min='00:00', max='13:00', node=node.present_times[0]) presence_finish = generate_presence_widget(min='13:00', max='23:59', node=node.present_times[1]) @@ -722,13 +723,13 @@ def on_presence_start_change(change): def on_presence_finish_change(change): new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']]) node.present_times = (node.present_times[0], new_value) - + presence_start.observe(on_presence_start_change, names=['value']) presence_finish.observe(on_presence_finish_change, names=['value']) return widgets.VBox([ - widgets.Label('Infected presence:'), - widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), + widgets.Label('Infected presence:'), + widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), widgets.HBox([widgets.Label('Afternoon:', layout=widgets.Layout(width='15%')), presence_finish]) ]) @@ -767,7 +768,7 @@ def toggle_ventilation(value): ventilation_w.observe(lambda event: toggle_ventilation(event['new']), 'value') toggle_ventilation(ventilation_w.value) - + w = collapsible( ([widgets.HBox([widgets.Label('Ventilation type'), ventilation_w], layout=widgets.Layout(justify_content='space-between'))]) + list(ventilation_widgets.values()), @@ -779,7 +780,7 @@ def _build_HEPA( self, node, ) -> widgets.Widget: - + HEPA_w = widgets.FloatSlider(value=node.q_air_mech, min=10, max=500, step=5) def on_value_change(change): @@ -807,17 +808,17 @@ def on_virus_change(change): node.dcs_update_from(virus) transmissibility_factor.value = virus.transmissibility_factor infectious_dose.value = virus.infectious_dose - + def on_transmissibility_change(change): - virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, infectious_dose=infectious_dose.value, + virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, infectious_dose=infectious_dose.value, viable_to_RNA_ratio=0.5, transmissibility_factor=change['new']) node.dcs_update_from(virus) if (transmissibility_factor.value != models.Virus.types[virus_choice.value].transmissibility_factor): virus_choice.options = list(models.Virus.types.keys()) + ["Custom"] virus_choice.value = "Custom" - + def on_infectious_dose_change(change): - virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, infectious_dose=change['new'], + virus = models.SARSCoV2(viral_load_in_sputum=node.dcs_instance().viral_load_in_sputum, infectious_dose=change['new'], viable_to_RNA_ratio=0.5, transmissibility_factor=transmissibility_factor.value) node.dcs_update_from(virus) if (infectious_dose.value != models.Virus.types[virus_choice.value].infectious_dose): @@ -830,50 +831,54 @@ def on_infectious_dose_change(change): space_between=widgets.Layout(justify_content='space-between') return widgets.VBox([ - widgets.HBox([widgets.Label("Virus"), virus_choice], layout=space_between), - widgets.HBox([widgets.Label("Tansmissibility factor "), transmissibility_factor], layout=space_between), + widgets.HBox([widgets.Label("Virus"), virus_choice], layout=space_between), + widgets.HBox([widgets.Label("Tansmissibility factor "), transmissibility_factor], layout=space_between), widgets.HBox([widgets.Label("Infectious dose "), infectious_dose], layout=space_between)]) def present(self): return self.widget -baseline_model = models.ExposureModel( - concentration_model=models.ConcentrationModel( - room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))), - ventilation=models.SlidingWindow( - active=models.PeriodicInterval(period=120, duration=15), - outside_temp=models.PiecewiseConstant((0., 24.), (283.15,)), - window_height=1.6, opening_length=0.6, +def baseline_model(data_registry: DataRegistry): + return models.ExposureModel( + concentration_model=models.ConcentrationModel( + data_registry=data_registry, + room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))), + ventilation=models.SlidingWindow( + active=models.PeriodicInterval(period=120, duration=15), + outside_temp=models.PiecewiseConstant((0., 24.), (283.15,)), + window_height=1.6, opening_length=0.6, + ), + infected=models.InfectedPopulation( + data_registry=data_registry, + number=1, + virus=models.Virus.types['SARS_CoV_2'], + presence=models.SpecificInterval(((8.5, 12.5), (13.5, 17.5))), + mask=models.Mask.types['No mask'], + activity=models.Activity.types['Seated'], + expiration=models.Expiration.types['Speaking'], + host_immunity=0., + ), + evaporation_factor=0.3, ), - infected=models.InfectedPopulation( - number=1, - virus=models.Virus.types['SARS_CoV_2'], + short_range=(), + exposed=models.Population( + number=10, presence=models.SpecificInterval(((8.5, 12.5), (13.5, 17.5))), - mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], - expiration=models.Expiration.types['Speaking'], + mask=models.Mask.types['No mask'], host_immunity=0., ), - evaporation_factor=0.3, - ), - short_range=(), - exposed=models.Population( - number=10, - presence=models.SpecificInterval(((8.5, 12.5), (13.5, 17.5))), - activity=models.Activity.types['Seated'], - mask=models.Mask.types['No mask'], - host_immunity=0., - ), - geographical_data=models.Cases(), -) + geographical_data=models.Cases(), + ) class CAIMIRAStateBuilder(state.StateBuilder): # Note: The methods in this class must correspond to the *type* of the data classes. # For example, build_type__VentilationBase is called when dealing with ConcentrationModel # types as it has a ventilation: _VentilationBase field. - - def __init__(self, selected_ventilation: str): + + def __init__(self, data_registry: DataRegistry, selected_ventilation: str): + self.data_registry = data_registry self.selected_ventilation = selected_ventilation def build_type_Mask(self, _: dataclasses.Field): @@ -923,6 +928,7 @@ def build_type__VentilationBase(self, _: dataclasses.Field): class ExpertApplication(Controller): def __init__(self) -> None: + self._data_registry = DataRegistry() #: A list of scenario name and ModelState instances. This is intended to be #: mutated. Any mutation should notify the appropriate Views for handling. self._model_scenarios: typing.List[ScenarioType] = [] @@ -948,9 +954,9 @@ def __init__(self) -> None: def build_new_model(self, vent: str) -> state.DataclassInstanceState[models.ExposureModel]: default_model = state.DataclassInstanceState( models.ExposureModel, - state_builder=CAIMIRAStateBuilder(selected_ventilation=vent), + state_builder=CAIMIRAStateBuilder(data_registry=self._data_registry, selected_ventilation=vent), ) - default_model.dcs_update_from(baseline_model) + default_model.dcs_update_from((baseline_model(self._data_registry))) # For the time-being, we have to initialise the select states. Careful # as values might not correspond to what the baseline model says. default_model.concentration_model.infected.mask.dcs_select('No mask') @@ -962,7 +968,7 @@ def add_scenario(self, name, copy_from_model: typing.Optional[state.DataclassIns model.dcs_update_from(copy_from_model.dcs_instance()) else: model = self.build_new_model(vent='Natural') # Default - model.dcs_update_from(baseline_model) + model.dcs_update_from(baseline_model(self._data_registry)) self._model_scenarios.append((name, model)) self._active_scenario = len(self._model_scenarios) - 1 @@ -1067,7 +1073,7 @@ def remove_tab(self, tab_index): self._tab_model_ids.pop(tab_index) self._tab_widgets.pop(tab_index) self._tab_model_views.pop(tab_index) - + self.update_tab_widget() def update_tab_widget(self): diff --git a/caimira/apps/expert/caimira.ipynb b/caimira/apps/expert/caimira.ipynb index 9200ce2a..7153e257 100644 --- a/caimira/apps/expert/caimira.ipynb +++ b/caimira/apps/expert/caimira.ipynb @@ -40,7 +40,7 @@ "import caimira.apps\n", "\n", "app = caimira.apps.ExpertApplication()\n", - "app.widget" + "app.widget\n" ] } ], diff --git a/caimira/apps/expert_co2.py b/caimira/apps/expert_co2.py index 5c3437bf..6ce8b81f 100644 --- a/caimira/apps/expert_co2.py +++ b/caimira/apps/expert_co2.py @@ -4,6 +4,7 @@ import numpy as np from caimira import data, models, state +from caimira.store.data_registry import DataRegistry import matplotlib import matplotlib.figure import matplotlib.lines as mlines @@ -11,17 +12,19 @@ from .expert import generate_presence_widget, collapsible, ipympl_canvas, WidgetGroup, CAIMIRAStateBuilder -baseline_model = models.CO2ConcentrationModel( - room=models.Room(volume=120, humidity=0.5, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))), - ventilation=models.HVACMechanical(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=500), - CO2_emitters=models.SimplePopulation( - number=10, - presence=models.SpecificInterval(((8., 12.), (13., 17.))), - activity=models.Activity.types['Seated'], - ), - CO2_atmosphere_concentration=440.44, - CO2_fraction_exhaled=0.042, -) +def baseline_model(data_registry: DataRegistry): + return models.CO2ConcentrationModel( + data_registry=data_registry, + room=models.Room(volume=120, humidity=0.5, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))), + ventilation=models.HVACMechanical(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=500), + CO2_emitters=models.SimplePopulation( + number=10, + presence=models.SpecificInterval(((8., 12.), (13., 17.))), + activity=models.Activity.types['Seated'], + ), + CO2_atmosphere_concentration=440.44, + CO2_fraction_exhaled=0.042, + ) class Controller: @@ -94,23 +97,23 @@ def update_plot(self, model: models.CO2ConcentrationModel): else: self.ax.ignore_existing_data_limits = False self.concentration_line.set_data(ts, concentration) - + if self.concentration_area is None: self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", - where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) | + where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) | (model.CO2_emitters.presence_interval().boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[1][1]))) - + else: - self.concentration_area.remove() + self.concentration_area.remove() self.concentration_area = self.ax.fill_between(x = ts, y1=0, y2=concentration, color="#96cbff", - where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) | + where = ((model.CO2_emitters.presence_interval().boundaries()[0][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[0][1]) | (model.CO2_emitters.presence_interval().boundaries()[1][0] < ts) & (ts < model.CO2_emitters.presence_interval().boundaries()[1][1]))) concentration_top = max(np.array(concentration)) self.ax.set_ylim(bottom=model.CO2_atmosphere_concentration * 0.9, top=concentration_top*1.1) - self.ax.set_xlim(left = min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95, + self.ax.set_xlim(left = min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95, right = max(model.CO2_emitters.presence_interval().boundaries()[1])*1.05) - + figure_legends = [mlines.Line2D([], [], color='#3530fe', markersize=15, label='CO₂ concentration'), mlines.Line2D([], [], color='salmon', markersize=15, label='Insufficient level', linestyle='--'), mlines.Line2D([], [], color='limegreen', markersize=15, label='Acceptable level', linestyle='--'), @@ -120,10 +123,10 @@ def update_plot(self, model: models.CO2ConcentrationModel): self.ax.set_ylim(top=concentration_top*1.1) else: self.ax.set_ylim(top=1550) - self.ax.hlines([800, 1500], xmin=min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95, - xmax=max(model.CO2_emitters.presence_interval().boundaries()[1])*1.05, - colors=['limegreen', 'salmon'], - linestyles='dashed') + self.ax.hlines([800, 1500], xmin=min(model.CO2_emitters.presence_interval().boundaries()[0])*0.95, + xmax=max(model.CO2_emitters.presence_interval().boundaries()[1])*1.05, + colors=['limegreen', 'salmon'], + linestyles='dashed') self.figure.canvas.draw() @@ -143,7 +146,7 @@ def widget(self): def initialize_axes(self) -> matplotlib.axes.Axes: ax = self.figure.add_subplot(1, 1, 1) ax.spines[['right', 'top']].set_visible(False) - + ax.set_xlabel('Time (hours)') ax.set_ylabel('CO₂ concentration (ppm)') ax.set_title('CO₂ Concentration') @@ -167,18 +170,18 @@ def update_plot(self, CO2_models: typing.Tuple[models.CO2ConcentrationModel, ... concentrations = [[conc_model.concentration(t) for t in ts] for conc_model in CO2_models] for label, concentration, color in zip(labels, concentrations, colors): self.ax.plot(ts, concentration, label=label, color=color) - + concentration_top = max([max(np.array(concentration)) for concentration in concentrations]) concentration_min = min([model.CO2_atmosphere_concentration for model in CO2_models]) - + self.ax.set_ylim(bottom=concentration_min * 0.9, top=concentration_top*1.1) - self.ax.set_xlim(left = start*0.95, + self.ax.set_xlim(left = start*0.95, right = finish*1.05) if 1500 < concentration_top: self.ax.set_ylim(top=concentration_top*1.1) else: self.ax.set_ylim(top=1550) - self.ax.hlines([800, 1500], xmin=start*0.95, xmax=finish*1.05, colors=['limegreen', 'salmon'], linestyles='dashed') + self.ax.hlines([800, 1500], xmin=start*0.95, xmax=finish*1.05, colors=['limegreen', 'salmon'], linestyles='dashed') self.ax.legend() self.figure.canvas.draw_idle() @@ -187,6 +190,7 @@ def update_plot(self, CO2_models: typing.Tuple[models.CO2ConcentrationModel, ... class CO2Application(Controller): def __init__(self) -> None: + self._data_registry = DataRegistry() # self._debug_output = widgets.Output() #: A list of scenario name and ModelState instances. This is intended to be @@ -215,7 +219,7 @@ def __init__(self) -> None: def build_new_model(self, vent: str) -> state.DataclassInstanceState[models.CO2ConcentrationModel]: new_model = state.DataclassInstanceState( models.CO2ConcentrationModel, - state_builder=CAIMIRACO2StateBuilder(selected_ventilation=vent) + state_builder=CAIMIRACO2StateBuilder(data_registry=self._data_registry, selected_ventilation=vent) ) return new_model @@ -225,8 +229,8 @@ def add_scenario(self, name, copy_from_model: typing.Optional[state.DataclassIns model.dcs_update_from(copy_from_model.dcs_instance()) else: model = self.build_new_model(vent='HVACMechanical') # Default - model.dcs_update_from(baseline_model) - + model.dcs_update_from(baseline_model(self._data_registry)) + self._model_scenarios.append((name, model)) self._active_scenario = len(self._model_scenarios) - 1 model.dcs_observe(self.notify_model_values_changed) @@ -238,7 +242,7 @@ def _find_model_id(self, model_id): return index, name, model else: raise ValueError("Model not found") - + def rename_scenario(self, model_id, new_name): index, _, model = self._find_model_id(model_id) self._model_scenarios[index] = (new_name, model) @@ -295,20 +299,20 @@ def _build_atmospheric_concentration(self, node): return collapsible([widgets.VBox([ self._build_co2_concentration(node), ])], title="Carbon Dioxide") - + def _build_population(self, node, ventilation_node): return collapsible([widgets.VBox([ self._build_population_number(node), self._build_activity(node.activity), self._build_population_presence(node.presence, ventilation_node) ])], title="Population") - + def _build_co2_concentration(self, node): concentration = widgets.IntSlider(value=node.CO2_atmosphere_concentration, min=300, max=1000, step=10) def on_atmospheric_concentration_change(change): node.CO2_atmosphere_concentration = change['new'] - + concentration.observe(on_atmospheric_concentration_change, names=['value']) return widgets.HBox([widgets.Label('Atmospheric Concentration (ppm) '), concentration], layout=widgets.Layout(justify_content='space-between')) @@ -319,7 +323,7 @@ def _build_room(self,node): inside_temp = widgets.IntSlider(value=node.inside_temp.values[0]-273.15, min=15., max=25.) def on_volume_change(change): - node.volume = change['new'] + node.volume = change['new'] def on_humidity_change(change): node.humidity = change['new']/100 @@ -350,7 +354,7 @@ def _build_activity(self, node): if activity == activity_: break activity = widgets.Dropdown(options=list(models.Activity.types.keys()), value=name) - + def on_activity_change(change): act = models.Activity.types[change['new']] node.dcs_update_from(act) @@ -363,7 +367,7 @@ def _build_population_number(self, node): def on_population_number_change(change): node.number = change['new'] - + number.observe(on_population_number_change, names=['value']) return widgets.HBox([widgets.Label('Number of people in the room '), number], layout=widgets.Layout(justify_content='space-between')) @@ -380,13 +384,13 @@ def on_presence_start_change(change): def on_presence_finish_change(change): new_value = tuple([int(time[:-3])+float(time[3:])/60 for time in change['new']]) node.present_times = (node.present_times[0], new_value) - + presence_start.observe(on_presence_start_change, names=['value']) presence_finish.observe(on_presence_finish_change, names=['value']) return widgets.VBox([ - widgets.Label('Exposed presence:'), - widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), + widgets.Label('Exposed presence:'), + widgets.HBox([widgets.Label('Morning:', layout=widgets.Layout(width='15%')), presence_start]), widgets.HBox([widgets.Label('Afternoon:', layout=widgets.Layout(width='15%')), presence_finish]) ]) @@ -429,7 +433,7 @@ def toggle_ventilation(value): ventilation_w.observe(lambda event: toggle_ventilation(event['new']), 'value') toggle_ventilation(ventilation_w.value) - + w = collapsible( ([widgets.HBox([widgets.Label('Ventilation type'), ventilation_w], layout=widgets.Layout(justify_content='space-between'))]) + list(ventilation_widgets.values()), @@ -475,16 +479,16 @@ def on_hinged_window_change(change): node.window_width = change['new'] hinged_window.observe(on_hinged_window_change, names=['value']) - + return widgets.HBox([widgets.Label('Window width (meters) '), hinged_window], layout=widgets.Layout(justify_content='space-between', width='100%')) def _build_sliding_window(self, node): - return widgets.HBox([]) + return widgets.HBox([]) def _build_window(self, node, emitters_node) -> WidgetGroup: window_widgets = { 'Sliding window': self._build_sliding_window(node._states['Sliding window']), - 'Hinged window': self._build_hinged_window(node._states['Hinged window']), + 'Hinged window': self._build_hinged_window(node._states['Hinged window']), } for name, widget in window_widgets.items(): @@ -519,7 +523,7 @@ def toggle_window(value): def on_value_change(change): node.number_of_windows = change['new'] - + def on_period_change(change): node.active.period = change['new'] duration.max = change['new'] @@ -530,7 +534,7 @@ def on_duration_change(change): def on_opening_length_change(change): node.opening_length = change['new'] - + def on_window_height_change(change): node.window_height = change['new'] @@ -563,9 +567,9 @@ def toggle_outsidetemp(value): result = WidgetGroup( ( ( - widgets.Label('Number of windows ', layout=auto_width), + widgets.Label('Number of windows ', layout=auto_width), number_of_windows, - ), + ), ( widgets.Label('Opening distance (meters)', layout=auto_width), opening_length, @@ -644,7 +648,7 @@ def toggle_mechanical(value): def _build_no_ventilation(self, node): return widgets.HBox([]) - + class MultiModelView(View): def __init__(self, controller: CO2Application): self._controller = controller @@ -682,7 +686,7 @@ def scenarios_updated( assert self._tab_model_ids == model_scenario_ids self.widget.selected_index = active_scenario_index - + def add_tab(self, name, model): self._tab_model_views.append(ModelWidgets(model)) @@ -703,7 +707,7 @@ def remove_tab(self, tab_index): self._tab_model_ids.pop(tab_index) self._tab_widgets.pop(tab_index) self._tab_model_views.pop(tab_index) - + self.update_tab_widget() def update_tab_widget(self): @@ -735,16 +739,17 @@ def on_duplicate_click(b): delete_button.on_click(on_delete_click) duplicate_button.on_click(on_duplicate_click) rename_text_field.observe(on_rename_text_field, 'value') - + buttons_w_delete = widgets.HBox(children=(duplicate_button, delete_button)) buttons = duplicate_button if len(self._tab_model_ids) < 2 else buttons_w_delete - + return widgets.VBox(children=(buttons, rename_text_field)) - + class CAIMIRACO2StateBuilder(CAIMIRAStateBuilder): - - def __init__(self, selected_ventilation: str): + + def __init__(self, data_registry: DataRegistry, selected_ventilation: str): + self._data_registry = data_registry self.selected_ventilation = selected_ventilation def build_type__VentilationBase(self, _: dataclasses.Field): @@ -759,10 +764,12 @@ def build_type__VentilationBase(self, _: dataclasses.Field): base_type=self.selected_ventilation, state_builder=self, ) - s._states['HVACMechanical'].dcs_update_from(baseline_model.ventilation) + s._states['HVACMechanical'].dcs_update_from(baseline_model(self._data_registry).ventilation) #Initialise the "Sliding window" state s._states['Sliding window'].dcs_update_from( - models.SlidingWindow(active=models.PeriodicInterval(period=120, duration=15, start=8-(15/60)), + models.SlidingWindow( + data_registry=self._data_registry, + active=models.PeriodicInterval(period=120, duration=15, start=8-(15/60)), outside_temp=models.PiecewiseConstant((0,24.), (283.15,)), window_height=1.6, opening_length=0.6, ), @@ -780,7 +787,7 @@ def build_type__VentilationBase(self, _: dataclasses.Field): ) # Initialize the "No ventilation" state s._states['No ventilation'].dcs_update_from( - models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.) + models.AirChange(active=models.PeriodicInterval(period=60, duration=60), air_exch=0.) ) return s diff --git a/caimira/apps/templates/base/calculator.report.html.j2 b/caimira/apps/templates/base/calculator.report.html.j2 index 6f37e7e2..819071ea 100644 --- a/caimira/apps/templates/base/calculator.report.html.j2 +++ b/caimira/apps/templates/base/calculator.report.html.j2 @@ -29,7 +29,7 @@

REPORT - {{ form.simulation_name }}

-

Created {{ creation_date }} using CAiMIRA calculator version v{{ form.calculator_version }}

+

Created {{ creation_date }} using CAiMIRA calculator version v{{ form.calculator_version }}. Data version {{ data_registry_version or '`default`' }}.

@@ -52,17 +52,17 @@
- +
- - {% if form.short_range_option == "short_range_yes" %} + + {% if form.short_range_option == "short_range_yes" %} {% set scenario = alternative_scenarios.stats.values() | first %} {% set long_range_prob_inf = scenario.probability_of_infection %} {% set long_range_prob_probabilistic_exposure = scenario.prob_probabilistic_exposure if form.exposure_option == 'p_probabilistic_exposure' %} {% else %} {% set long_range_prob_inf = prob_inf %} {% endif %} - + {% block report_results %}
Results @@ -81,7 +81,7 @@
Probability of infection (%)
{% if form.short_range_option == "short_range_yes" %} - Without short-range interactions + Without short-range interactions {% endif %}

@@ -136,11 +136,11 @@ {% if form.exposure_option == "p_probabilistic_exposure" %}
@@ -232,7 +232,7 @@ -
+
@@ -244,12 +244,12 @@ "Mean concentration (ppm)", h_lines = [ {'label': 'Acceptable level', - 'y': 800, + 'y': 800, 'color': 'forestgreen', 'style': 'dashed' }, {'label': 'Insufficient level', - 'y': 1500, + 'y': 1500, 'color': 'firebrick', 'style': 'dashed' }, @@ -269,7 +269,7 @@ -
+
@@ -341,7 +341,7 @@
{# If short-range interactions are set, we don't display Alternative Scenarios, and there is no need to arrange items in a list #} - {% if form.short_range_option == "short_range_no" %} + {% if form.short_range_option == "short_range_no" %}
  • Current Scenario @@ -372,7 +372,7 @@
{% if form.short_range_option == "short_range_no" %} - +
  • Alternative Scenarios
    @@ -383,7 +383,7 @@
  • {% endif %} - +
    - +
    Vaccination data:
    @@ -576,7 +576,7 @@ {% elif form.activity_type == "smallmeeting" %} Small meeting – typical scenario with all persons seated, one person speaking at a time. {% elif form.activity_type == "largemeeting" %} - Large meeting – infected occupant(s) is standing and speaking 1/3rd of the time, while the other occupants are seated. + Large meeting – infected occupant(s) is standing and speaking 1/3rd of the time, while the other occupants are seated. {% elif form.activity_type == "callcentre" %} Call Centre - typical office-like scenario with all persons seated, all speaking continuously. {% elif form.activity_type == "controlroom-day" %} @@ -590,7 +590,7 @@ {% elif form.activity_type == "training" %} Conference/Training (speaker infected) – one person (the speaker/trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the speaker/trainer is the infected person, for the worst case scenario. {% elif form.activity_type == "training_attendee" %} - Conference/Training (attendee infected) – the infected person(s) are in the audience. All persons seated and breathing. + Conference/Training (attendee infected) – the infected person(s) are in the audience. All persons seated and breathing. {% elif form.activity_type == "lab" %} Laboratory - Lab or technical environment, all persons doing light physical activity, speaking 50% of the time. {% elif form.activity_type == "gym" %} @@ -658,7 +658,7 @@ {% if form.infected_dont_have_breaks_with_exposed %}

    Infected occupant(s):

    - +
    • Lunch break: {% if form.infected_lunch_option%} @@ -690,7 +690,7 @@

      Mask wearing:
      -
      +
      • Masks worn at workstations? {{ 'Yes' if form.mask_wearing_option == "mask_on" else 'No' }}

      • {% if form.mask_wearing_option == "mask_on" %} @@ -700,10 +700,10 @@

      - {% endblock simulation_overview %} + {% endblock simulation_overview %}
      {% block report_preamble %} - {% endblock report_preamble %} + {% endblock report_preamble %}
    diff --git a/caimira/models.py b/caimira/models.py index fd5121cb..80b2f215 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -40,6 +40,8 @@ import scipy.stats as sct from scipy.optimize import minimize +from caimira.store.data_registry import DataRegistry + if not typing.TYPE_CHECKING: from memoization import cached else: @@ -50,7 +52,6 @@ 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 @@ -343,13 +344,15 @@ class SlidingWindow(WindowOpening): Sliding window, or side-hung window (with the hinge perpendicular to the horizontal plane). """ + data_registry: DataRegistry = None + @property def discharge_coefficient(self) -> _VectorisedFloat: """ Average measured value of discharge coefficient for sliding or side-hung windows. """ - return config.ventilation['natural']['discharge_factor']['sliding'] # type: ignore + return self.data_registry.ventilation['natural']['discharge_factor']['sliding'] # type: ignore @dataclass(frozen=True) @@ -448,7 +451,7 @@ class CustomVentilation(_VentilationBase): def transition_times(self, room: Room) -> typing.Set[float]: return set(self.ventilation_value.transition_times) - + def air_exchange(self, room: Room, time: float) -> _VectorisedFloat: return self.ventilation_value.value(time) @@ -474,7 +477,7 @@ class Virus: infectiousness_days: int def halflife(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFloat) -> _VectorisedFloat: - # Biological decay (inactivation of the virus in air) - virus + # Biological decay (inactivation of the virus in air) - virus # dependent and function of humidity raise NotImplementedError @@ -487,7 +490,7 @@ def decay_constant(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFlo class SARSCoV2(Virus): #: Number of days the infector is contagious infectiousness_days: int = 14 - + def halflife(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFloat) -> _VectorisedFloat: """ Half-life changes with humidity level. Here is implemented a simple @@ -495,23 +498,23 @@ def halflife(self, humidity: _VectorisedFloat, inside_temp: _VectorisedFloat) -> CERN-OPEN-2021-004, DOI: 10.17181/CERN.1GDQ.5Y75) """ # Updated to use the formula from Dabish et al. with correction https://doi.org/10.1080/02786826.2020.1829536 - # with a maximum at hl = 6.43 (compensate for the negative decay values in the paper). + # with a maximum at hl = 6.43 (compensate for the negative decay values in the paper). # Note that humidity is in percentage and inside_temp in °C. # factor np.log(2) -> decay rate to half-life; factor 60 -> minutes to hours hl_calc = ((np.log(2)/((0.16030 + 0.04018*(((inside_temp-273.15)-20.615)/10.585) +0.02176*(((humidity*100)-45.235)/28.665) -0.14369 -0.02636*((inside_temp-273.15)-20.615)/10.585)))/60) - + return np.where(hl_calc <= 0, 6.43, np.minimum(6.43, hl_calc)) - + # Example of Viruses only used for the Expert app and tests. Virus.types = { 'SARS_CoV_2': SARSCoV2( viral_load_in_sputum=1e9, # No data on coefficient for SARS-CoV-2 yet. - # It is somewhere between 1000 or 10 SARS-CoV viruses, + # It is somewhere between 1000 or 10 SARS-CoV viruses, # as per https://www.dhs.gov/publication/st-master-question-list-covid-19 # 50 comes from Buonanno et al. infectious_dose=50., @@ -578,7 +581,7 @@ def exhale_efficiency(self, diameter: _VectorisedFloat) -> _VectorisedFloat: if self.η_exhale is not None: # When η_exhale is specified, return it directly return self.η_exhale - + d = np.array(diameter) intermediate_range1 = np.bitwise_and(0.5 <= d, d < 0.94614) intermediate_range2 = np.bitwise_and(0.94614 <= d, d < 3.) @@ -719,9 +722,9 @@ def particle(self) -> Particle: @cached() def aerosols(self, mask: Mask): - """ + """ Total volume of aerosols expired per volume of exhaled air. - Result is in mL.cm^-3 + Result is in mL.cm^-3 """ def volume(d): return (np.pi * d**3) / 6. @@ -734,7 +737,7 @@ def volume(d): def jet_origin_concentration(self): def volume(d): return (np.pi * d**3) / 6. - + # Final result converted from microns^3/cm3 to mL/m3 return self.cn * volume(self.diameter) * 1e-6 @@ -825,11 +828,11 @@ def __post_init__(self): else: if self.presence is not None: raise TypeError(f'The presence argument must be None for a IntPiecewiseConstant number') - + def presence_interval(self): - if isinstance(self.presence, Interval): + if isinstance(self.presence, Interval): return self.presence - elif isinstance(self.number, IntPiecewiseConstant): + elif isinstance(self.number, IntPiecewiseConstant): return self.number.interval() def person_present(self, time: float): @@ -858,13 +861,15 @@ class Population(SimplePopulation): mask: Mask #: The ratio of virions that are inactivated by the person's immunity. - # This parameter considers the potential antibodies in the person, + # This parameter considers the potential antibodies in the person, # which might render inactive some RNA copies (virions). host_immunity: float @dataclass(frozen=True) class _PopulationWithVirus(Population): + data_registry: DataRegistry + #: The virus with which the population is infected. virus: Virus @@ -874,7 +879,7 @@ def fraction_of_infectious_virus(self) -> _VectorisedFloat: The fraction of infectious virus. """ - return config.population_with_virus['fraction_of_infectious_virus'] # type: ignore + return self.data_registry.population_with_virus['fraction_of_infectious_virus'] # type: ignore def aerosols(self): """ @@ -884,7 +889,7 @@ def aerosols(self): def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, + The emission rate of virions in the expired air per mL of respiratory fluid, per person, if the infected population is present, in (virion.cm^3)/(mL.h). This method includes only the diameter-independent variables within the emission rate. It should not be a function of time. @@ -940,7 +945,7 @@ def aerosols(self): @method_cache def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, + The emission rate of virions in the expired air per mL of respiratory fluid, per person, if the infected population is present, in (virion.cm^3)/(mL.h). This method includes only the diameter-independent variables within the emission rate. It should not be a function of time. @@ -969,14 +974,13 @@ def aerosols(self): @method_cache def emission_rate_per_aerosol_per_person_when_present(self) -> _VectorisedFloat: """ - The emission rate of virions in the expired air per mL of respiratory fluid, + The emission rate of virions in the expired air per mL of respiratory fluid, if the infected population is present, in (virion.cm^3)/(mL.h). This method includes only the diameter-independent variables within the emission rate. It should not be a function of time. """ # Note on units: exhalation rate is in m^3/h -> 1e6 conversion factor # Returns the emission rate times the number of infected hosts in the room - ER = (self.virus.viral_load_in_sputum * self.activity.exhalation_rate * self.fraction_of_infectious_virus() * @@ -1024,6 +1028,7 @@ class _ConcentrationModelBase: A generic superclass that contains the methods to calculate the concentration (e.g. viral concentration or CO2 concentration). """ + data_registry: DataRegistry room: Room ventilation: _VentilationBase @@ -1044,10 +1049,10 @@ def removal_rate(self, time: float) -> _VectorisedFloat: def min_background_concentration(self) -> _VectorisedFloat: """ Minimum background concentration in the room for a given scenario - (in the same unit as the concentration). Its the value towards which + (in the same unit as the concentration). Its the value towards which the concentration will decay to. """ - return config.concentration_model['min_background_concentration'] # type: ignore + return self.data_registry.concentration_model['min_background_concentration'] # type: ignore def normalization_factor(self) -> _VectorisedFloat: """ @@ -1060,7 +1065,7 @@ def normalization_factor(self) -> _VectorisedFloat: @method_cache def _normed_concentration_limit(self, time: float) -> _VectorisedFloat: """ - Provides a constant that represents the theoretical asymptotic + Provides a constant that represents the theoretical asymptotic value reached by the concentration when time goes to infinity, if all parameters were to stay time-independent. This is normalized by the normalization factor, the latter acting as a @@ -1097,8 +1102,8 @@ def _first_presence_time(self) -> float: """ First presence time. Before that, the concentration is zero. """ - return self.population.presence_interval().boundaries()[0][0] - + return self.population.presence_interval().boundaries()[0][0] + def last_state_change(self, time: float) -> float: """ Find the most recent/previous state change. @@ -1151,7 +1156,7 @@ def _normed_concentration(self, time: float) -> _VectorisedFloat: # before the first presence as an optimisation. if time <= self._first_presence_time(): return self.min_background_concentration()/self.normalization_factor() - + next_state_change_time = self._next_state_change(time) RR = self.removal_rate(next_state_change_time) @@ -1172,7 +1177,7 @@ def _normed_concentration(self, time: float) -> _VectorisedFloat: curr_conc_state = self._normed_concentration_limit(next_state_change_time) * (1 - fac) return curr_conc_state + conc_at_last_state_change * fac - + def concentration(self, time: float) -> _VectorisedFloat: """ Total concentration as a function of time. The normalization @@ -1181,7 +1186,7 @@ def concentration(self, time: float) -> _VectorisedFloat: Note that time is not vectorised. You can only pass a single float to this method. """ - return (self._normed_concentration_cached(time) * + return (self._normed_concentration_cached(time) * self.normalization_factor()) @method_cache @@ -1227,14 +1232,17 @@ class ConcentrationModel(_ConcentrationModelBase): """ Class used for the computation of the long-range virus concentration. """ - #: Infected population in the room, emitting virions infected: InfectedPopulation #: 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'] # type: ignore + evaporation_factor: float + + def __post_init__(self): + if self.evaporation_factor is None: + self.evaporation_factor = self.data_registry.particle['evaporation_factor'] @property def population(self) -> InfectedPopulation: @@ -1274,10 +1282,14 @@ 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'] # type: ignore + @property + def CO2_atmosphere_concentration(self) -> float: + return self.data_registry.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'] # type: ignore + @property + def CO2_fraction_exhaled(self) -> float: + return self.data_registry.concentration_model['CO2_concentration_model']['CO2_fraction_exhaled'] # type: ignore @property def population(self) -> SimplePopulation: @@ -1302,10 +1314,11 @@ def normalization_factor(self) -> _VectorisedFloat: @dataclass(frozen=True) class ShortRangeModel: ''' - Based on the two-stage (jet/puff) expiratory jet model by + Based on the two-stage (jet/puff) expiratory jet model by Jia et al (2022) - https://doi.org/10.1016/j.buildenv.2022.109166 ''' - + data_registry: DataRegistry + #: Expiration type expiration: _ExpirationBase @@ -1322,32 +1335,34 @@ def dilution_factor(self) -> _VectorisedFloat: ''' The dilution factor for the respective expiratory activity type. ''' + _dilution_factor = self.data_registry.short_range_model['dilution_factor'] # Average mouth opening diameter (m) - mouth_diameter: float = config.short_range_model['dilution_factor']['mouth_diameter'] # type: ignore + mouth_diameter: float = _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 + # 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'] # type: ignore + φ: float = _dilution_factor['exhalation_coefficient'] # type: ignore # Exhalation airflow, as per Jia et al. (2022) Q_exh: _VectorisedFloat = φ * BR # Area of the mouth assuming a perfect circle (m2) - Am = np.pi*(mouth_diameter**2)/4 + Am = np.pi*(mouth_diameter**2)/4 # Initial velocity of the exhalation airflow (m/s) 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'] # type: ignore - + tstar: float = _dilution_factor['tstar'] # type: ignore + # Streamwise and radial penetration coefficients - 𝛽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 + _df_pc = _dilution_factor['penetration_coefficients'] + 𝛽r1: float = _df_pc['𝛽r1'] # type: ignore + 𝛽r2: float = _df_pc['𝛽r2'] # type: ignore + 𝛽x1: float = _df_pc['𝛽x1'] # type: ignore # Parameters in the jet-like stage # Position of virtual origin @@ -1371,20 +1386,20 @@ def dilution_factor(self) -> _VectorisedFloat: def _long_range_normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ - Virus long-range exposure concentration normalized by the + Virus long-range exposure concentration normalized by the virus viral load, as function of time. """ - return (concentration_model.concentration(time) / + return (concentration_model.concentration(time) / concentration_model.virus.viral_load_in_sputum) def _normed_concentration(self, concentration_model: ConcentrationModel, time: float) -> _VectorisedFloat: """ Virus short-range exposure concentration, as a function of time. - If the given time falls within a short-range interval it returns the + If the given time falls within a short-range interval it returns the short-range concentration normalized by the virus viral load. Otherwise it returns 0. - """ + """ start, stop = self.presence.boundaries()[0] # Verifies if the given time falls within a short-range interaction if start <= time <= stop: @@ -1392,14 +1407,14 @@ def _normed_concentration(self, concentration_model: ConcentrationModel, time: f jet_origin_concentration = self.expiration.jet_origin_concentration() # Long-range concentration normalized by the virus viral load long_range_normed_concentration = self._long_range_normed_concentration(concentration_model, time) - + # The long-range concentration values are then approximated using interpolation: - # The set of points where we want the interpolated values are the short-range particle diameters (given the current expiration); + # The set of points where we want the interpolated values are the short-range particle diameters (given the current expiration); # The set of points with a known value are the long-range particle diameters (given the initial expiration); # The set of known values are the long-range concentration values normalized by the viral load. - long_range_normed_concentration_interpolated=np.interp(np.array(self.expiration.particle.diameter), + long_range_normed_concentration_interpolated=np.interp(np.array(self.expiration.particle.diameter), np.array(concentration_model.infected.particle.diameter), long_range_normed_concentration) - + # Short-range concentration formula. The long-range concentration is added in the concentration method (ExposureModel). # based on continuum model proposed by Jia et al (2022) - https://doi.org/10.1016/j.buildenv.2022.109166 return ((1/dilution)*(jet_origin_concentration - long_range_normed_concentration_interpolated)) @@ -1409,7 +1424,7 @@ def short_range_concentration(self, concentration_model: ConcentrationModel, tim """ Virus short-range exposure concentration, as a function of time. """ - return (self._normed_concentration(concentration_model, time) * + return (self._normed_concentration(concentration_model, time) * concentration_model.virus.viral_load_in_sputum) @method_cache @@ -1530,11 +1545,11 @@ def fun(x): exhalation_rate=exhalation_rate, ventilation_values=ventilation_values ) - return np.sqrt(np.sum((np.array(self.CO2_concentrations) - + return np.sqrt(np.sum((np.array(self.CO2_concentrations) - np.array(the_concentrations))**2)) # The goal is to minimize the difference between the two different curves (known concentrations vs. predicted concentrations) res_dict = minimize(fun=fun, x0=np.ones(len(self.ventilation_transition_times)), method='powell', - bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))], + bounds=[(0, None) for _ in range(len(self.ventilation_transition_times))], options={'xtol': 1e-3}) exhalation_rate = res_dict['x'][0] @@ -1548,6 +1563,8 @@ class ExposureModel: """ Represents the exposure to a concentration of virions in the air. """ + data_registry: DataRegistry + #: The virus concentration model which this exposure model should consider. concentration_model: ConcentrationModel @@ -1561,7 +1578,9 @@ class ExposureModel: geographical_data: Cases #: The number of times the exposure event is repeated (default 1). - repeats: int = config.exposure_model['repeats'] # type: ignore + @property + def repeats(self) -> int: + return self.data_registry.exposure_model['repeats'] # type: ignore def __post_init__(self): """ @@ -1572,17 +1591,17 @@ def __post_init__(self): In other words, the air exchange rate from the ventilation, and the virus decay constant, must not be given as arrays. - """ + """ c_model = self.concentration_model # Check if the diameter is vectorised. if (isinstance(c_model.infected, InfectedPopulation) and not np.isscalar(c_model.infected.expiration.diameter) # Check if the diameter-independent elements of the infectious_virus_removal_rate method are vectorised. and not ( - all(np.isscalar(c_model.virus.decay_constant(c_model.room.humidity, c_model.room.inside_temp.value(time)) + + all(np.isscalar(c_model.virus.decay_constant(c_model.room.humidity, c_model.room.inside_temp.value(time)) + c_model.ventilation.air_exchange(c_model.room, time)) for time in c_model.state_change_times()))): raise ValueError("If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time.") - + @method_cache def population_state_change_times(self) -> typing.List[float]: """ @@ -1591,7 +1610,7 @@ def population_state_change_times(self) -> typing.List[float]: """ state_change_times = set(self.concentration_model.infected.presence_interval().transition_times()) state_change_times.update(self.exposed.presence_interval().transition_times()) - + return sorted(state_change_times) def long_range_fraction_deposited(self) -> _VectorisedFloat: @@ -1605,7 +1624,7 @@ def long_range_fraction_deposited(self) -> _VectorisedFloat: def _long_range_normed_exposure_between_bounds(self, time1: float, time2: float) -> _VectorisedFloat: """ - The number of virions per meter^3 between any two times, normalized + The number of virions per meter^3 between any two times, normalized by the emission rate of the infected population """ exposure = 0. @@ -1677,7 +1696,7 @@ def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _Vect Considers a contribution between the short-range and long-range exposures: It calculates the deposited exposure given a short-range interaction (if any). Then, the deposited exposure given the long-range interactions is added to the - initial deposited exposure. + initial deposited exposure. """ deposited_exposure: _VectorisedFloat = 0. for interaction in self.short_range: @@ -1690,7 +1709,7 @@ def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _Vect fdep = interaction.expiration.particle.fraction_deposited(evaporation_factor=1.0) diameter = interaction.expiration.particle.diameter - + # Aerosols not considered given the formula for the initial # concentration at mouth/nose. if diameter is not None and not np.isscalar(diameter): @@ -1723,7 +1742,7 @@ def deposited_exposure_between_bounds(self, time1: float, time2: float) -> _Vect deposited_exposure += self.long_range_deposited_exposure_between_bounds(time1, time2) return deposited_exposure - + def _deposited_exposure_list(self): """ The number of virus per m^3 deposited on the respiratory tract. @@ -1733,15 +1752,15 @@ def _deposited_exposure_list(self): deposited_exposure = [] for start, stop in zip(population_change_times[:-1], population_change_times[1:]): deposited_exposure.append(self.deposited_exposure_between_bounds(start, stop)) - + return deposited_exposure - + def deposited_exposure(self) -> _VectorisedFloat: """ The number of virus per m^3 deposited on the respiratory tract. """ return np.sum(self._deposited_exposure_list(), axis=0) * self.repeats - + def _infection_probability_list(self): # Viral dose (vD) vD_list = self._deposited_exposure_list() @@ -1750,26 +1769,26 @@ def _infection_probability_list(self): infectious_dose = oneoverln2 * self.concentration_model.virus.infectious_dose # Probability of infection. - return [(1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose * + return [(1 - np.exp(-((vD * (1 - self.exposed.host_immunity))/(infectious_dose * self.concentration_model.virus.transmissibility_factor)))) for vD in vD_list] - + @method_cache def infection_probability(self) -> _VectorisedFloat: - return (1 - np.prod([1 - prob for prob in self._infection_probability_list()], axis = 0)) * 100 - + return (1 - np.prod([1 - prob for prob in self._infection_probability_list()], axis = 0)) * 100 + def total_probability_rule(self) -> _VectorisedFloat: - if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or + if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or isinstance(self.exposed.number, IntPiecewiseConstant)): raise NotImplementedError("Cannot compute total probability " "(including incidence rate) with dynamic occupancy") - - if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0): + + if (self.geographical_data.geographic_population != 0 and self.geographical_data.geographic_cases != 0): sum_probability = 0.0 # Create an equivalent exposure model but changing the number of infected cases. total_people = self.concentration_model.infected.number + self.exposed.number max_num_infected = (total_people if total_people < 10 else 10) - # The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability. + # The influence of a higher number of simultainious infected people (> 4 - 5) yields an almost negligible contirbution to the total probability. # To be on the safe side, a hard coded limit with a safety margin of 2x was set. # Therefore we decided a hard limit of 10 infected people. for num_infected in range(1, max_num_infected + 1): @@ -1780,18 +1799,18 @@ def total_probability_rule(self) -> _VectorisedFloat: n = total_people - num_infected # By means of the total probability rule prob_at_least_one_infected = 1 - (1 - prob_ind)**n - sum_probability += (prob_at_least_one_infected * + sum_probability += (prob_at_least_one_infected * self.geographical_data.probability_meet_infected_person(self.concentration_model.infected.virus, num_infected, total_people)) return sum_probability * 100 else: return 0 def expected_new_cases(self) -> _VectorisedFloat: - if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or + if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or isinstance(self.exposed.number, IntPiecewiseConstant)): raise NotImplementedError("Cannot compute expected new cases " "with dynamic occupancy") - + return self.infection_probability() * self.exposed.number / 100 def reproduction_number(self) -> _VectorisedFloat: @@ -1800,7 +1819,7 @@ def reproduction_number(self) -> _VectorisedFloat: cases directly generated by one infected case in a population. """ - if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or + if (isinstance(self.concentration_model.infected.number, IntPiecewiseConstant) or isinstance(self.exposed.number, IntPiecewiseConstant)): raise NotImplementedError("Cannot compute reproduction number " "with dynamic occupancy") diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index 211ff19f..101508c5 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -7,7 +7,7 @@ import caimira.monte_carlo as mc from caimira.monte_carlo.sampleable import LogCustom, LogNormal, Normal, LogCustomKernel, CustomKernel, Uniform, Custom -from caimira.store.configuration import config +from caimira.store.data_registry import DataRegistry sqrt2pi = np.sqrt(2.*np.pi) sqrt2 = np.sqrt(2.) @@ -128,6 +128,7 @@ class BLOmodel: vol. 42, no. 12, pp. 839 – 851, 2011, https://doi.org/10.1016/j.jaerosci.2011.07.009). """ + data_registry: DataRegistry #: Factors assigned to resp. the B, L and O modes. They are # charateristics of the kind of expiratory activity (e.g. breathing, # speaking, singing, or shouting). These are applied on top of the @@ -137,27 +138,24 @@ 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] = ( - config.BLOmodel['cn']['B'], - config.BLOmodel['cn']['L'], - config.BLOmodel['cn']['O'] - ) + @property + def cn(self) -> typing.Tuple[float, float, float]: + _cn = self.data_registry.BLOmodel['cn'] + return (_cn['B'],_cn['L'],_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] = ( - config.BLOmodel['mu']['B'], - config.BLOmodel['mu']['L'], - config.BLOmodel['mu']['O'] - ) + @property + def mu(self) -> typing.Tuple[float, float, float]: + _mu = self.data_registry.BLOmodel['mu'] + return (_mu['B'], _mu['L'], _mu['O']) # Std deviation of the underlying normal distribution, for resp. # the B, L and O modes. - sigma: typing.Tuple[float, float, float] = ( - config.BLOmodel['sigma']['B'], - config.BLOmodel['sigma']['L'], - config.BLOmodel['sigma']['O'] - ) + @property + def sigma(self) -> typing.Tuple[float, float, float]: + _sigma = self.data_registry.BLOmodel['sigma'] + return (_sigma['B'],_sigma['L'],_sigma['O']) def distribution(self, d): """ @@ -170,8 +168,8 @@ def distribution(self, d): self.mu, self.sigma)) def integrate(self, dmin, dmax): - """ - Returns the integral between dmin and dmax (in microns) of the + """ + Returns the integral between dmin and dmax (in microns) of the probability distribution. """ result = 0. @@ -183,42 +181,43 @@ def integrate(self, dmin, dmax): # From https://doi.org/10.1101/2021.10.14.21264988 and references therein -activity_distributions = { - '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( - inhalation_rate=param_evaluation( - config.activity_distributions['Standing'], 'inhalation_rate'), - exhalation_rate=param_evaluation( - config.activity_distributions['Standing'], 'exhalation_rate'), - ), - - '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( - 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( - inhalation_rate=param_evaluation( - config.activity_distributions['Heavy exercise'], 'inhalation_rate'), - exhalation_rate=param_evaluation( - config.activity_distributions['Heavy exercise'], 'exhalation_rate'), - ), -} +def activity_distributions(data_registry): + return { + 'Seated': mc.Activity( + inhalation_rate=param_evaluation( + data_registry.activity_distributions['Seated'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + data_registry.activity_distributions['Seated'], 'exhalation_rate'), + ), + + 'Standing': mc.Activity( + inhalation_rate=param_evaluation( + data_registry.activity_distributions['Standing'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + data_registry.activity_distributions['Standing'], 'exhalation_rate'), + ), + + 'Light activity': mc.Activity( + inhalation_rate=param_evaluation( + data_registry.activity_distributions['Light activity'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + data_registry.activity_distributions['Light activity'], 'exhalation_rate'), + ), + + 'Moderate activity': mc.Activity( + inhalation_rate=param_evaluation( + data_registry.activity_distributions['Moderate activity'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + data_registry.activity_distributions['Moderate activity'], 'exhalation_rate'), + ), + + 'Heavy exercise': mc.Activity( + inhalation_rate=param_evaluation( + data_registry.activity_distributions['Heavy exercise'], 'inhalation_rate'), + exhalation_rate=param_evaluation( + data_registry.activity_distributions['Heavy exercise'], 'exhalation_rate'), + ), + } # From https://doi.org/10.1101/2021.10.14.21264988 and references therein @@ -240,104 +239,115 @@ def integrate(self, dmin, dmax): # 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( - 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'] +def viral_load(data_registry): + return np.linspace( + weibull_min.ppf( + data_registry.covid_overal_vl_data['start'], + c=data_registry.covid_overal_vl_data['shape_factor'], + scale=data_registry.covid_overal_vl_data['scale_factor'] + ), + weibull_min.ppf( + data_registry.covid_overal_vl_data['stop'], + c=data_registry.covid_overal_vl_data['shape_factor'], + scale=data_registry.covid_overal_vl_data['scale_factor'] + ), + int(data_registry.covid_overal_vl_data['num']) ) -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']) +def frequencies_pdf(data_registry): + return weibull_min.pdf( + viral_load(data_registry), + c=data_registry.covid_overal_vl_data['shape_factor'], + scale=data_registry.covid_overal_vl_data['scale_factor'] + ) +def covid_overal_vl_data(data_registry): + return LogCustom( + bounds=(data_registry.covid_overal_vl_data['min_bound'], data_registry.covid_overal_vl_data['max_bound']), + function=lambda d: np.interp( + d, + viral_load(data_registry), + frequencies_pdf, + data_registry.covid_overal_vl_data['interpolation_fp_left'], + data_registry.covid_overal_vl_data['interpolation_fp_right'] + ), + max_function=data_registry.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( - config.viable_to_RNA_ratio_distribution['low'], config.viable_to_RNA_ratio_distribution['high']) +def viable_to_RNA_ratio_distribution(data_registry): + return Uniform(data_registry.viable_to_RNA_ratio_distribution['low'], data_registry.viable_to_RNA_ratio_distribution['high']) # From discussion with virologists -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=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=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=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=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=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=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'), - ), -} +def infectious_dose_distribution(data_registry): + return Uniform(data_registry.infectious_dose_distribution['low'], data_registry.infectious_dose_distribution['high']) + + +# From https://doi.org/10.1101/2021.10.14.21264988 and references therein +def virus_distributions(data_registry): + return { + 'SARS_CoV_2': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2'], 'transmissibility_factor'), + ), + 'SARS_CoV_2_ALPHA': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'transmissibility_factor'), + ), + 'SARS_CoV_2_BETA': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_BETA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_BETA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_BETA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_BETA'], 'transmissibility_factor'), + ), + 'SARS_CoV_2_GAMMA': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'transmissibility_factor'), + ), + 'SARS_CoV_2_DELTA': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'transmissibility_factor'), + ), + 'SARS_CoV_2_OMICRON': mc.SARSCoV2( + viral_load_in_sputum=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'viral_load_in_sputum'), + infectious_dose=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'infectious_dose'), + viable_to_RNA_ratio=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'viable_to_RNA_ratio'), + transmissibility_factor=param_evaluation( + data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'transmissibility_factor'), + ), + } # From: @@ -345,32 +355,34 @@ def integrate(self, dmin, dmax): # https://doi.org/10.1016/j.jhin.2013.02.007 # https://doi.org/10.4209/aaqr.2020.08.0531 # https://doi.org/10.1080/02786826.2021.1890687 -mask_distributions = { - '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 mask_distributions(data_registry): + return { + 'Type I': mc.Mask( + η_inhale=param_evaluation( + data_registry.mask_distributions['Type I'], 'η_inhale'), + η_exhale=param_evaluation( + data_registry.mask_distributions['Type I'], 'η_exhale') + if data_registry.mask_distributions['Type I']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), + 'FFP2': mc.Mask( + η_inhale=param_evaluation( + data_registry.mask_distributions['FFP2'], 'η_inhale'), + η_exhale=param_evaluation( + data_registry.mask_distributions['FFP2'], 'η_exhale') + if data_registry.mask_distributions['FFP2']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), + 'Cloth': mc.Mask( + η_inhale=param_evaluation( + data_registry.mask_distributions['Cloth'], 'η_inhale'), + η_exhale=param_evaluation( + data_registry.mask_distributions['Cloth'], 'η_exhale') + if data_registry.mask_distributions['Cloth']['Known filtration efficiency of masks when exhaling?'] == 'Yes' else None, + ), + } def expiration_distribution( + data_registry, BLO_factors, d_min=0.1, d_max=30., @@ -387,54 +399,62 @@ def expiration_distribution( return mc.Expiration( CustomKernel( dscan, - BLOmodel(BLO_factors).distribution(dscan), + BLOmodel(data_registry, BLO_factors).distribution(dscan), kernel_bandwidth=0.1, ), - cn=BLOmodel(BLO_factors).integrate(d_min, d_max), + cn=BLOmodel(data_registry, BLO_factors).integrate(d_min, d_max), ) -expiration_BLO_factors = { - '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, - 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_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() -} +def expiration_BLO_factors(data_registry): + breathing = data_registry.expiration_BLO_factors['Breathing'] + speaking = data_registry.expiration_BLO_factors['Speaking'] + singing = data_registry.expiration_BLO_factors['Singing'] + shouting = data_registry.expiration_BLO_factors['Shouting'] + return { + 'Breathing': ( + param_evaluation(breathing, 'B'), + param_evaluation(breathing, 'L'), + param_evaluation(breathing, 'O') + ), + 'Speaking': ( + param_evaluation(speaking, 'B'), + param_evaluation(speaking, 'L'), + param_evaluation(speaking, 'O') + ), + 'Singing': ( + param_evaluation(singing, 'B'), + param_evaluation(singing, 'L'), + param_evaluation(singing, 'O') + ), + 'Shouting': ( + param_evaluation(shouting, 'B'), + param_evaluation(shouting, 'L'), + param_evaluation(shouting, 'O') + ), + } + + +def expiration_distributions(data_registry): + return { + exp_type: expiration_distribution( + BLO_factors, + d_min=param_evaluation(data_registry.long_range_expiration_distributions, 'minimum_diameter'), + d_max=param_evaluation(data_registry.long_range_expiration_distributions, 'maximum_diameter') + ) + for exp_type, BLO_factors in expiration_BLO_factors.items() + } + + +def short_range_expiration_distributions(data_registry): + return { + exp_type: expiration_distribution( + BLO_factors, + d_min=param_evaluation(data_registry.short_range_expiration_distributions, 'minimum_diameter'), + d_max=param_evaluation(data_registry.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 @@ -442,8 +462,12 @@ def expiration_distribution( 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) +def short_range_distances(data_registry): + return Custom( + bounds=( + param_evaluation(data_registry.short_range_distances, 'minimum_distance'), + param_evaluation(data_registry.short_range_distances, 'maximum_distance') + ), + function=lambda x: np.interp(x, distances, frequencies, left=0., right=0.), + max_function=0.13 + ) diff --git a/caimira/store/configuration.py b/caimira/store/data_registry.py similarity index 98% rename from caimira/store/configuration.py rename to caimira/store/data_registry.py index bf7390ab..69d6d702 100644 --- a/caimira/store/configuration.py +++ b/caimira/store/data_registry.py @@ -1,5 +1,7 @@ -class Configuration: - """Configuration singleton to cache data values.""" +class DataRegistry: + """Registry to hold data values.""" + + version = None BLOmodel = { "cn": { @@ -441,11 +443,10 @@ class Configuration: "precise": {"activity": "", "expiration": {}}, } - def update(self, data): + def update(self, data, version=None): """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() + if version: + self.version = version diff --git a/caimira/store/data_service.py b/caimira/store/data_service.py index 04fb5353..b9163848 100644 --- a/caimira/store/data_service.py +++ b/caimira/store/data_service.py @@ -1,14 +1,13 @@ import logging -import os import typing from datetime import datetime, timedelta, timezone import jwt import requests -from .configuration import config +from caimira.store.data_registry import DataRegistry -logger = logging.getLogger(__name__) +logger = logging.getLogger("DATA") class DataService: @@ -20,12 +19,18 @@ class DataService: def __init__( self, credentials: typing.Dict[str, str], - host: str = "https://caimira-data-api.app.cern.ch", + host: str, ): self._credentials = credentials self._host = host + @classmethod + def create(cls, credentials: typing.Dict[str, str], host: str = "https://caimira-data-api.app.cern.ch"): + """Factory.""" + return cls(credentials, host) + def _is_valid(self, access_token): + """Return True if the expiration token is still valid.""" try: decoded = jwt.decode( access_token, algorithms=["HS256"], options={"verify_signature": False} @@ -35,9 +40,13 @@ def _is_valid(self, access_token): tzinfo=timezone.utc ) now = datetime.now(timezone.utc) - return now < expiration - timedelta( + is_valid = now < expiration - timedelta( seconds=5 ) # 5 seconds time delta to avoid timing issues + + logger.debug(f"Access token expiration: {expiration_timestamp}. Is valid? {is_valid}") + + return is_valid except jwt.ExpiredSignatureError: logger.warning("JWT token expired.") except jwt.InvalidTokenError: @@ -45,6 +54,8 @@ def _is_valid(self, access_token): return False def _login(self): + logger.debug(f"Access token: {self._access_token}") + if self._access_token and self._is_valid(self._access_token): return self._access_token @@ -65,6 +76,7 @@ def _login(self): response.raise_for_status() if response.status_code == 200: self._access_token = response.json()["access_token"] + logger.debug(f"Obtained new access token: {self._access_token}") return self._access_token else: logger.error( @@ -73,7 +85,7 @@ def _login(self): except requests.exceptions.RequestException as e: logger.exception(e) - def fetch(self): + def _fetch(self): access_token = self._login() headers = { @@ -86,7 +98,9 @@ def fetch(self): response = requests.get(url, headers=headers) response.raise_for_status() if response.status_code == 200: - return response.json() + json_body = response.json() + logger.debug(f"Data service call: {url}. Response: {json_body}") + return json_body else: logger.error( f"Unexpected error when fetching data. Response status code: {response.status_code}, body: f{response.text}" @@ -95,17 +109,9 @@ def fetch(self): logger.exception(e) -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() + def update_registry(self, registry: DataRegistry): + data = self._fetch() if data: - config.update(data["data"]) + registry.update(data["data"], version=data["version"]) else: logger.error("Could not fetch fresh data from the data service.") diff --git a/caimira/tests/apps/calculator/conftest.py b/caimira/tests/apps/calculator/conftest.py index e4eedb11..38c19e97 100644 --- a/caimira/tests/apps/calculator/conftest.py +++ b/caimira/tests/apps/calculator/conftest.py @@ -9,5 +9,5 @@ def baseline_form_data(): @pytest.fixture -def baseline_form(baseline_form_data): - return model_generator.VirusFormData.from_dict(baseline_form_data) +def baseline_form(baseline_form_data, data_registry): + return model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) diff --git a/caimira/tests/apps/calculator/test_model_generator.py b/caimira/tests/apps/calculator/test_model_generator.py index 697e962f..c6f6c89f 100644 --- a/caimira/tests/apps/calculator/test_model_generator.py +++ b/caimira/tests/apps/calculator/test_model_generator.py @@ -4,7 +4,7 @@ import numpy as np import numpy.testing as npt import pytest -from retry import retry +from retry import retry from caimira.apps.calculator import model_generator from caimira.apps.calculator.form_data import (_hours2timestring, minutes_since_midnight, @@ -12,17 +12,18 @@ from caimira import models from caimira.monte_carlo.data import expiration_distributions from caimira.apps.calculator.defaults import NO_DEFAULT +from caimira.store.data_registry import DataRegistry -def test_model_from_dict(baseline_form_data): - form = model_generator.VirusFormData.from_dict(baseline_form_data) +def test_model_from_dict(baseline_form_data, data_registry): + form = model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) assert isinstance(form.build_model(), models.ExposureModel) -def test_model_from_dict_invalid(baseline_form_data): +def test_model_from_dict_invalid(baseline_form_data, data_registry): baseline_form_data['invalid_item'] = 'foobar' with pytest.raises(ValueError, match='Invalid argument "invalid_item" given'): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) @retry(tries=10) @@ -45,7 +46,7 @@ def test_blend_expiration(mask_type): npt.assert_allclose(r.aerosols(mask).mean(), expected, rtol=TOLERANCE) -def test_ventilation_slidingwindow(baseline_form: model_generator.VirusFormData): +def test_ventilation_slidingwindow(data_registry: DataRegistry, baseline_form: model_generator.VirusFormData): baseline_form.ventilation_type = 'natural_ventilation' baseline_form.windows_duration = 10 baseline_form.windows_frequency = 120 @@ -61,6 +62,7 @@ def test_ventilation_slidingwindow(baseline_form: model_generator.VirusFormData) assert isinstance(baseline_window, models.SlidingWindow) window = models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(period=120, duration=10, start=9), outside_temp=baseline_window.outside_temp, window_height=1.6, opening_length=0.6, @@ -135,7 +137,7 @@ def test_ventilation_airchanges(baseline_form: model_generator.VirusFormData): np.array([baseline_form.ventilation().air_exchange(room, t) for t in ts])) -def test_ventilation_window_hepa(baseline_form: model_generator.VirusFormData): +def test_ventilation_window_hepa(data_registry: DataRegistry, baseline_form: model_generator.VirusFormData): baseline_form.ventilation_type = 'natural_ventilation' baseline_form.windows_duration = 10 baseline_form.windows_frequency = 120 @@ -152,6 +154,7 @@ def test_ventilation_window_hepa(baseline_form: model_generator.VirusFormData): # Now build the equivalent ventilation instance directly, and compare. window = models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(period=120, duration=10, start=9), outside_temp=baseline_window.outside_temp, window_height=1.6, opening_length=0.6, @@ -178,12 +181,13 @@ def test_ventilation_window_hepa(baseline_form: model_generator.VirusFormData): ] ) def test_infected_less_than_total_people(activity, total_people, infected_people, error, - baseline_form: model_generator.VirusFormData): + baseline_form: model_generator.VirusFormData, + data_registry: DataRegistry): baseline_form.activity_type = activity baseline_form.total_people = total_people baseline_form.infected_people = infected_people with pytest.raises(ValueError, match=error): - baseline_form.validate() + baseline_form.validate(data_registry) def present_times(interval: models.Interval) -> models.BoundarySequence_t: @@ -266,12 +270,12 @@ def test_exposed_present_intervals_ending_with_lunch(baseline_form: model_genera assert present_times(baseline_form.exposed_present_interval()) == correct -def test_exposed_present_lunch_end_before_beginning(baseline_form: model_generator.VirusFormData): +def test_exposed_present_lunch_end_before_beginning(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.exposed_coffee_break_option = 'coffee_break_0' baseline_form.exposed_lunch_start = minutes_since_midnight(14 * 60) baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60) with pytest.raises(ValueError): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.mark.parametrize( @@ -283,11 +287,11 @@ def test_exposed_present_lunch_end_before_beginning(baseline_form: model_generat [9, 20], # lunch_finish after the presence finishing ], ) -def test_exposed_presence_lunch_break(baseline_form: model_generator.VirusFormData, exposed_lunch_start, exposed_lunch_finish): +def test_exposed_presence_lunch_break(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry, exposed_lunch_start, exposed_lunch_finish): baseline_form.exposed_lunch_start = minutes_since_midnight(exposed_lunch_start * 60) baseline_form.exposed_lunch_finish = minutes_since_midnight(exposed_lunch_finish * 60) with pytest.raises(ValueError, match='exposed lunch break must be within presence times.'): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.mark.parametrize( @@ -299,24 +303,24 @@ def test_exposed_presence_lunch_break(baseline_form: model_generator.VirusFormDa [9, 20], # lunch_finish after the presence finishing ], ) -def test_infected_presence_lunch_break(baseline_form: model_generator.VirusFormData, infected_lunch_start, infected_lunch_finish): +def test_infected_presence_lunch_break(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry, infected_lunch_start, infected_lunch_finish): baseline_form.infected_lunch_start = minutes_since_midnight(infected_lunch_start * 60) baseline_form.infected_lunch_finish = minutes_since_midnight(infected_lunch_finish * 60) with pytest.raises(ValueError, match='infected lunch break must be within presence times.'): - baseline_form.validate() + baseline_form.validate(data_registry) -def test_exposed_breaks_length(baseline_form: model_generator.VirusFormData): +def test_exposed_breaks_length(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.exposed_coffee_break_option = 'coffee_break_4' baseline_form.exposed_coffee_duration = 30 baseline_form.exposed_start = minutes_since_midnight(10 * 60) baseline_form.exposed_finish = minutes_since_midnight(11 * 60) baseline_form.exposed_lunch_option = False with pytest.raises(ValueError, match='Length of breaks >= Length of exposed presence.'): - baseline_form.validate() + baseline_form.validate(data_registry) -def test_infected_breaks_length(baseline_form: model_generator.VirusFormData): +def test_infected_breaks_length(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.infected_start = minutes_since_midnight(9 * 60) baseline_form.infected_finish = minutes_since_midnight(12 * 60) baseline_form.infected_lunch_start = minutes_since_midnight(10 * 60) @@ -324,7 +328,7 @@ def test_infected_breaks_length(baseline_form: model_generator.VirusFormData): baseline_form.infected_coffee_break_option = 'coffee_break_4' baseline_form.infected_coffee_duration = 30 with pytest.raises(ValueError, match='Length of breaks >= Length of infected presence.'): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.fixture @@ -431,12 +435,12 @@ def test_present_only_during_second_break(breaks_every_25_mins_for_20_mins): assert_boundaries(interval, []) -def test_valid_no_lunch(baseline_form: model_generator.VirusFormData): +def test_valid_no_lunch(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): # Check that it is valid to have a 0 length lunch if no lunch is selected. baseline_form.exposed_lunch_option = False baseline_form.exposed_lunch_start = minutes_since_midnight(0) baseline_form.exposed_lunch_finish = minutes_since_midnight(0) - assert baseline_form.validate() is None + assert baseline_form.validate(data_registry) is None def test_no_breaks(baseline_form: model_generator.VirusFormData): @@ -487,45 +491,45 @@ def test_coffee_breaks(baseline_form: model_generator.VirusFormData): np.testing.assert_allclose(present_times(baseline_form.exposed_present_interval()), correct, rtol=1e-14) -def test_key_validation(baseline_form_data): +def test_key_validation(baseline_form_data, data_registry): baseline_form_data['activity_type'] = 'invalid key' with pytest.raises(ValueError): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) -def test_key_validation_natural_ventilation_window_type_na(baseline_form_data): +def test_key_validation_natural_ventilation_window_type_na(baseline_form_data, data_registry): baseline_form_data['ventilation_type'] = 'natural_ventilation' baseline_form_data['window_type'] = 'not-applicable' with pytest.raises(ValueError, match='window_type cannot be \'not-applicable\''): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) -def test_key_validation_natural_ventilation_window_opening_regime_na(baseline_form_data): +def test_key_validation_natural_ventilation_window_opening_regime_na(baseline_form_data, data_registry): baseline_form_data['ventilation_type'] = 'natural_ventilation' baseline_form_data['window_opening_regime'] = 'not-applicable' with pytest.raises(ValueError, match='window_opening_regime cannot be \'not-applicable\''): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) -def test_natural_ventilation_window_opening_periodically(baseline_form: model_generator.VirusFormData): +def test_natural_ventilation_window_opening_periodically(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.window_opening_regime = 'windows_open_periodically' baseline_form.windows_duration = 20 baseline_form.windows_frequency = 10 with pytest.raises(ValueError, match='Duration cannot be bigger than frequency.'): - baseline_form.validate() + baseline_form.validate(data_registry) -def test_key_validation_mech_ventilation_type_na(baseline_form_data): +def test_key_validation_mech_ventilation_type_na(baseline_form_data, data_registry): baseline_form_data['ventilation_type'] = 'mechanical_ventilation' baseline_form_data['mechanical_ventilation_type'] = 'not-applicable' with pytest.raises(ValueError, match='mechanical_ventilation_type cannot be \'not-applicable\''): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) -def test_key_validation_event_month(baseline_form_data): +def test_key_validation_event_month(baseline_form_data, data_registry): baseline_form_data['event_month'] = 'invalid month' with pytest.raises(ValueError, match='invalid month is not a valid value for event_month'): - model_generator.VirusFormData.from_dict(baseline_form_data) + model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) def test_default_types(): @@ -574,11 +578,11 @@ def test_form_to_dict(baseline_form): [-176.433333, -44.033333, 'August', '+1245', 12.75], # Chatham Islands ] ) -def test_form_timezone(baseline_form_data, longitude, latitude, month, expected_tz_name, expected_offset): +def test_form_timezone(baseline_form_data, data_registry, longitude, latitude, month, expected_tz_name, expected_offset): baseline_form_data['location_latitude'] = latitude baseline_form_data['location_longitude'] = longitude baseline_form_data['event_month'] = month - form = model_generator.VirusFormData.from_dict(baseline_form_data) + form = model_generator.VirusFormData.from_dict(baseline_form_data, data_registry) name, offset = form.tz_name_and_utc_offset() assert name == expected_tz_name assert offset == expected_offset diff --git a/caimira/tests/apps/calculator/test_specific_model_generator.py b/caimira/tests/apps/calculator/test_specific_model_generator.py index baeb2242..b25c9f02 100644 --- a/caimira/tests/apps/calculator/test_specific_model_generator.py +++ b/caimira/tests/apps/calculator/test_specific_model_generator.py @@ -3,6 +3,7 @@ import pytest from caimira.apps.calculator import model_generator +from caimira.store.data_registry import DataRegistry @pytest.mark.parametrize( @@ -13,10 +14,10 @@ [{"exposed_breaks": [], "ifected_breaks": []}, 'Unable to fetch "infected_breaks" key. Got "ifected_breaks".'], ] ) -def test_specific_break_structure(break_input, error, baseline_form: model_generator.VirusFormData): +def test_specific_break_structure(break_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.specific_breaks = break_input with pytest.raises(TypeError, match=error): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.mark.parametrize( @@ -30,10 +31,10 @@ def test_specific_break_structure(break_input, error, baseline_form: model_gener [[{"start_time": "10:00", "finish_time": "11"}], 'Wrong time format - "HH:MM". Got "11".'], ] ) -def test_specific_population_break_data_structure(population_break_input, error, baseline_form: model_generator.VirusFormData): +def test_specific_population_break_data_structure(population_break_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.specific_breaks = {'exposed_breaks': population_break_input, 'infected_breaks': population_break_input} with pytest.raises(TypeError, match=error): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.mark.parametrize( @@ -64,10 +65,10 @@ def test_specific_break_time(break_input, error, baseline_form: model_generator. [{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentag": 50}, {"type": "Speaking", "percentage": 50}]}, 'Unable to fetch "percentage" key. Got "percentag".'], ] ) -def test_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.VirusFormData): +def test_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.precise_activity = precise_activity_input with pytest.raises(TypeError, match=error): - baseline_form.validate() + baseline_form.validate(data_registry) @pytest.mark.parametrize( @@ -79,7 +80,7 @@ def test_precise_activity_structure(precise_activity_input, error, baseline_form [{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 50.'], ] ) -def test_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.VirusFormData): +def test_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.precise_activity = precise_activity_input with pytest.raises(ValueError, match=error): - baseline_form.validate() \ No newline at end of file + baseline_form.validate(data_registry) diff --git a/caimira/tests/conftest.py b/caimira/tests/conftest.py index c1bdb8aa..f2965b6c 100644 --- a/caimira/tests/conftest.py +++ b/caimira/tests/conftest.py @@ -4,16 +4,24 @@ import pytest +from caimira.store.data_registry import DataRegistry + + +@pytest.fixture +def data_registry(): + return DataRegistry() @pytest.fixture -def baseline_concentration_model(): +def baseline_concentration_model(data_registry): model = models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))), ventilation=models.AirChange( active=models.SpecificInterval(((0., 24.), )), air_exch=30., ), infected=models.EmittingPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(((0., 4.), (5., 8.))), @@ -35,10 +43,11 @@ def baseline_sr_model(): @pytest.fixture -def baseline_exposure_model(baseline_concentration_model, baseline_sr_model): +def baseline_exposure_model(data_registry, baseline_concentration_model, baseline_sr_model): return models.ExposureModel( - baseline_concentration_model, - baseline_sr_model, + data_registry=data_registry, + concentration_model=baseline_concentration_model, + short_range=baseline_sr_model, exposed=models.Population( number=1000, presence=baseline_concentration_model.infected.presence, @@ -55,6 +64,7 @@ def exposure_model_w_outside_temp_changes(baseline_exposure_model: models.Exposu exp_model = caimira.dataclass_utils.nested_replace( baseline_exposure_model, { 'concentration_model.ventilation': models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(2.2 * 60, 1.8 * 60), outside_temp=caimira.data.GenevaTemperatures['Jan'], window_height=1.6, diff --git a/caimira/tests/models/test_co2_concentration_model.py b/caimira/tests/models/test_co2_concentration_model.py index ab5562db..ce57110b 100644 --- a/caimira/tests/models/test_co2_concentration_model.py +++ b/caimira/tests/models/test_co2_concentration_model.py @@ -5,8 +5,9 @@ @pytest.fixture -def simple_co2_conc_model(): +def simple_co2_conc_model(data_registry): return models.CO2ConcentrationModel( + data_registry=data_registry, room=models.Room(200, models.PiecewiseConstant((0., 24.), (293,))), ventilation=models.AirChange(models.PeriodicInterval(period=120, duration=120), 0.25), CO2_emitters=models.SimplePopulation( diff --git a/caimira/tests/models/test_concentration_model.py b/caimira/tests/models/test_concentration_model.py index 27703a2f..7c126843 100644 --- a/caimira/tests/models/test_concentration_model.py +++ b/caimira/tests/models/test_concentration_model.py @@ -45,7 +45,7 @@ def normalization_factor(self) -> float: {'viral_load_in_sputum': np.array([5e8, 1e9])}, ] ) -def test_concentration_model_vectorisation(override_params): +def test_concentration_model_vectorisation(override_params, data_registry): defaults = { 'volume': 75, 'humidity': 0.5, @@ -56,9 +56,11 @@ def test_concentration_model_vectorisation(override_params): always = models.PeriodicInterval(240, 240) # TODO: This should be a thing on an interval. c_model = models.ConcentrationModel( + data_registry, models.Room(defaults['volume'], models.PiecewiseConstant((0., 24.), (293,)), defaults['humidity']), models.AirChange(always, defaults['air_change']), models.InfectedPopulation( + data_registry=data_registry, number=1, presence=always, mask=models.Mask( @@ -86,12 +88,14 @@ def test_concentration_model_vectorisation(override_params): @pytest.fixture -def simple_conc_model(): +def simple_conc_model(data_registry): interesting_times = models.SpecificInterval(([0.5, 1.], [1.1, 2], [2., 3.]), ) return models.ConcentrationModel( + data_registry=data_registry, room = models.Room(75, models.PiecewiseConstant((0., 24.), (293,))), ventilation = models.AirChange(interesting_times, 100), infected = models.InfectedPopulation( + data_registry=data_registry, number=1, presence=interesting_times, mask=models.Mask.types['Type I'], @@ -180,11 +184,11 @@ def test_integrated_concentration(simple_conc_model): npt.assert_almost_equal(c1, c2 + c3, decimal=15) -# The expected numbers were obtained via the quad integration of the +# The expected numbers were obtained via the quad integration of the # normed_integrated_concentration method with 0 (start) and 2 (stop) as limits. @pytest.mark.parametrize([ - "known_min_background_concentration", - "expected_normed_integrated_concentration"], + "known_min_background_concentration", + "expected_normed_integrated_concentration"], [ [0.0, 0.00018533333708996207], [240.0, 48.000185340695275], @@ -196,26 +200,26 @@ def test_integrated_concentration(simple_conc_model): def test_normed_integrated_concentration_with_background_concentration( simple_conc_model: models.ConcentrationModel, dummy_population: models.Population, - known_min_background_concentration: float, + known_min_background_concentration: float, expected_normed_integrated_concentration: float): known_conc_model = KnownConcentrationModelBase( - room = simple_conc_model.room, - ventilation = simple_conc_model.ventilation, + room = simple_conc_model.room, + ventilation = simple_conc_model.ventilation, known_population = dummy_population, known_removal_rate = 100., known_min_background_concentration = known_min_background_concentration, - known_normalization_factor = 10.) + known_normalization_factor = 10.) npt.assert_almost_equal(known_conc_model.normed_integrated_concentration(0, 2), expected_normed_integrated_concentration) -# The expected numbers were obtained via the quad integration of the +# The expected numbers were obtained via the quad integration of the # normed_integrated_concentration method with 0 (start) and 2 (stop) as limits. @pytest.mark.parametrize([ "known_removal_rate", - "known_min_background_concentration", + "known_min_background_concentration", "known_normalization_factor", - "expected_normed_integrated_concentration"], + "expected_normed_integrated_concentration"], [ [np.array([0.25, 10]), 0.0, 10., np.array([0.012161005755130391, 0.0017333437605308818])], [100, np.array([0, 240.0]), 10., np.array([0.00018533333708996207, 48.000185340695275])], @@ -228,18 +232,18 @@ def test_normed_integrated_concentration_vectorisation( simple_conc_model: models.ConcentrationModel, dummy_population: models.Population, known_removal_rate: float, - known_min_background_concentration: float, + known_min_background_concentration: float, known_normalization_factor: float, expected_normed_integrated_concentration: float): known_conc_model = KnownConcentrationModelBase( - room = simple_conc_model.room, - ventilation = simple_conc_model.ventilation, + room = simple_conc_model.room, + ventilation = simple_conc_model.ventilation, known_population = dummy_population, known_removal_rate = known_removal_rate, known_min_background_concentration = known_min_background_concentration, known_normalization_factor = known_normalization_factor) - + integrated_concentration = known_conc_model.normed_integrated_concentration(0, 2) assert isinstance(integrated_concentration, np.ndarray) @@ -249,8 +253,8 @@ def test_normed_integrated_concentration_vectorisation( @pytest.mark.parametrize([ "known_removal_rate", - "known_min_background_concentration", - "expected_concentration"], + "known_min_background_concentration", + "expected_concentration"], [ [0., 240., 240. + 0.5/75], [0.0001, 240.0, 240. + 0.5/75], @@ -267,13 +271,12 @@ def test_zero_ventilation_rate( expected_concentration: float): known_conc_model = KnownConcentrationModelBase( - room = simple_conc_model.room, - ventilation = simple_conc_model.ventilation, + room = simple_conc_model.room, + ventilation = simple_conc_model.ventilation, known_population = dummy_population, known_removal_rate = known_removal_rate, known_normalization_factor=1., known_min_background_concentration = known_min_background_concentration) - + normed_concentration = known_conc_model.concentration(1) assert normed_concentration == pytest.approx(expected_concentration, abs=1e-6) - \ No newline at end of file diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index 8fe6b891..7b7607f4 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -8,13 +8,15 @@ import caimira.dataclass_utils as dc_utils @pytest.fixture -def full_exposure_model(): +def full_exposure_model(data_registry): return models.ExposureModel( concentration_model=models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=100), ventilation=models.AirChange( active=models.PeriodicInterval(120, 120), air_exch=0.25), infected=models.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(((8, 12), (13, 17), )), mask=models.Mask.types['No mask'], @@ -27,7 +29,7 @@ def full_exposure_model(): short_range=(), exposed=models.Population( number=10, - presence=models.SpecificInterval(((8, 12), (13, 17), )), + presence=models.SpecificInterval(((8, 12), (13, 17), )), mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], host_immunity=0. @@ -37,8 +39,9 @@ def full_exposure_model(): @pytest.fixture -def baseline_infected_population_number(): +def baseline_infected_population_number(data_registry): return models.InfectedPopulation( + data_registry=data_registry, number=models.IntPiecewiseConstant( (8, 12, 13, 17), (1, 0, 1)), presence=None, @@ -77,7 +80,7 @@ def dynamic_exposed_single_exposure_model(full_exposure_model, baseline_exposed_ @pytest.fixture def dynamic_population_exposure_model(full_exposure_model, baseline_infected_population_number ,baseline_exposed_population_number): return dc_utils.nested_replace(full_exposure_model, { - 'concentration_model.infected': baseline_infected_population_number, + 'concentration_model.infected': baseline_infected_population_number, 'exposed': baseline_exposed_population_number, }) @@ -107,7 +110,7 @@ def test_population_number(full_exposure_model: models.ExposureModel, dc_utils.nested_replace( piecewise_population_number, {'presence': models.SpecificInterval(((8, 12), ))} ) - + assert int_population_number.person_present(time) == piecewise_population_number.person_present(time) assert int_population_number.people_present(time) == piecewise_population_number.people_present(time) @@ -129,8 +132,8 @@ def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureM dynamic_infected_single_exposure_model: models.ExposureModel, time: float, number_of_infected: int): - - + + static_multiple_exposure_model: models.ExposureModel = dc_utils.nested_replace( full_exposure_model, { @@ -190,7 +193,7 @@ def test_dynamic_dose(full_exposure_model: models.ExposureModel, time: float): dynamic_concentration = dynamic_infected.concentration(time) dynamic_exposure = dynamic_infected.deposited_exposure() - static_concentration, static_exposure = zip(*[(model.concentration(time), model.deposited_exposure()) + static_concentration, static_exposure = zip(*[(model.concentration(time), model.deposited_exposure()) for model in (single_infected, two_infected, three_infected)]) npt.assert_almost_equal(dynamic_concentration, np.sum(static_concentration)) @@ -202,7 +205,7 @@ def test_infection_probability( dynamic_infected_single_exposure_model: models.ExposureModel, dynamic_exposed_single_exposure_model: models.ExposureModel, dynamic_population_exposure_model: models.ExposureModel): - + base_infection_probability = full_exposure_model.infection_probability() npt.assert_almost_equal(base_infection_probability, dynamic_infected_single_exposure_model.infection_probability()) npt.assert_almost_equal(base_infection_probability, dynamic_exposed_single_exposure_model.infection_probability()) @@ -223,12 +226,12 @@ def test_dynamic_total_probability_rule( with pytest.raises(NotImplementedError, match=re.escape("Cannot compute total probability " "(including incidence rate) with dynamic occupancy")): dynamic_population_exposure_model.total_probability_rule() - + def test_dynamic_expected_new_cases( dynamic_infected_single_exposure_model: models.ExposureModel, dynamic_exposed_single_exposure_model: models.ExposureModel, dynamic_population_exposure_model: models.ExposureModel): - + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute expected new cases " "with dynamic occupancy")): dynamic_infected_single_exposure_model.expected_new_cases() @@ -243,7 +246,7 @@ def test_dynamic_reproduction_number( dynamic_infected_single_exposure_model: models.ExposureModel, dynamic_exposed_single_exposure_model: models.ExposureModel, dynamic_population_exposure_model: models.ExposureModel): - + with pytest.raises(NotImplementedError, match=re.escape("Cannot compute reproduction number " "with dynamic occupancy")): dynamic_infected_single_exposure_model.reproduction_number() diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index 07722dd6..3bd6640e 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -9,6 +9,7 @@ from caimira.models import ExposureModel from caimira.dataclass_utils import replace from caimira.monte_carlo.data import expiration_distributions +from caimira.store.data_registry import DataRegistry @dataclass(frozen=True) class KnownNormedconcentration(models.ConcentrationModel): @@ -55,10 +56,11 @@ def _normed_concentration(self, time: float) -> models._VectorisedFloat: # noqa ), ] -def known_concentrations(func): +def known_concentrations(func, data_registry=DataRegistry()): dummy_room = models.Room(50, 0.5) dummy_ventilation = models._VentilationBase() dummy_infected_population = models.InfectedPopulation( + data_registry=data_registry, number=1, presence=halftime, mask=models.Mask.types['Type I'], @@ -147,15 +149,17 @@ def test_exposure_model_scalar(sr_model, cases_model): @pytest.fixture -def conc_model(): +def conc_model(data_registry): interesting_times = models.SpecificInterval( ([0., 1.], [1.01, 1.02], [12., 24.]), ) always = models.SpecificInterval(((0., 24.), )) return models.ConcentrationModel( + data_registry, models.Room(25, models.PiecewiseConstant((0., 24.), (293,))), models.AirChange(always, 5), models.EmittingPopulation( + data_registry=data_registry, number=1, presence=interesting_times, mask=models.Mask.types['No mask'], @@ -171,10 +175,11 @@ def conc_model(): @pytest.fixture -def diameter_dependent_model(conc_model) -> models.InfectedPopulation: +def diameter_dependent_model(conc_model, data_registry) -> models.InfectedPopulation: # Generate a diameter dependent model return replace(conc_model, infected = models.InfectedPopulation( + data_registry=data_registry, number=1, presence=halftime, virus=models.Virus.types['SARS_CoV_2_DELTA'], @@ -193,7 +198,7 @@ def sr_model(): @pytest.fixture def cases_model(): return () - + # Expected deposited exposure were computed with a trapezoidal integration, using # a mesh of 10'000 pts per exposed presence interval. @@ -219,8 +224,9 @@ def test_exposure_model_integral_accuracy(exposed_time_interval, np.testing.assert_allclose(model.deposited_exposure(), expected_deposited_exposure) -def test_infectious_dose_vectorisation(sr_model, cases_model): +def test_infectious_dose_vectorisation(sr_model, cases_model, data_registry): infected_population = models.InfectedPopulation( + data_registry=data_registry, number=1, presence=halftime, mask=models.Mask.types['Type I'], @@ -258,7 +264,7 @@ def test_infectious_dose_vectorisation(sr_model, cases_model): ] ) def test_probability_random_individual(pop, cases, infectiousness_days, AB, prob_random_individual): - cases = models.Cases(geographic_population=pop, geographic_cases=cases, + cases = models.Cases(geographic_population=pop, geographic_cases=cases, ascertainment_bias=AB) virus=models.SARSCoV2( viral_load_in_sputum=1e9, @@ -280,7 +286,7 @@ def test_probability_random_individual(pop, cases, infectiousness_days, AB, prob ] ) def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_infected_person): - cases = models.Cases(geographic_population=pop, geographic_cases=cases, + cases = models.Cases(geographic_population=pop, geographic_cases=cases, ascertainment_bias=AB) virus = models.Virus.types['SARS_CoV_2'] np.testing.assert_allclose(cases.probability_meet_infected_person(virus, infected, exposed+infected), @@ -302,7 +308,7 @@ def test_probabilistic_exposure_probability(sr_model, exposed_population, cm, pop, AB, cases, probabilistic_exposure_probability): population = models.Population( - exposed_population, models.PeriodicInterval(120, 60), models.Activity.types['Standing'], + exposed_population, models.PeriodicInterval(120, 60), models.Activity.types['Standing'], models.Mask.types['Type I'], host_immunity=0.,) model = ExposureModel(cm, sr_model, population, models.Cases(geographic_population=pop, geographic_cases=cases, ascertainment_bias=AB),) @@ -314,32 +320,35 @@ def test_probabilistic_exposure_probability(sr_model, exposed_population, cm, @pytest.mark.parametrize( "volume, outside_temp, window_height, opening_length", [ [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293.,)), 1., 1.,], # Verify (room) volume vectorisation. - [50, models.PiecewiseConstant((0., 12, 24.), + [50, models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]),)), 1., 1.,], # Verify (ventilation) outside_temp vectorisation. - [50, models.PiecewiseConstant((0., 24.), (293.,)), + [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([1., 0.5]), 1.], # Verify (ventilation) window_height vectorisation. [50, models.PiecewiseConstant((0., 24.), (293.,)), 1., np.array([1., 0.5])], # Verify (ventilation) opening_length vectorisation. ] ) -def test_diameter_vectorisation_window_opening(diameter_dependent_model, sr_model, volume, outside_temp, +def test_diameter_vectorisation_window_opening(data_registry, diameter_dependent_model, sr_model, volume, outside_temp, window_height, opening_length, cases_model): - concentration = replace(diameter_dependent_model, + concentration = replace(diameter_dependent_model, room = models.Room(volume=volume, inside_temp=models.PiecewiseConstant((0., 24.), (293.,)), humidity=0.3), - ventilation=models.SlidingWindow(active=models.PeriodicInterval(period=120, duration=120), - outside_temp=outside_temp, - window_height=window_height, - opening_length=opening_length), + ventilation=models.SlidingWindow( + data_registry=data_registry, + active=models.PeriodicInterval(period=120, duration=120), + outside_temp=outside_temp, + window_height=window_height, + opening_length=opening_length + ), ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time."): models.ExposureModel(concentration, sr_model, populations[0], cases_model) - + def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) window_width vectorisation. - concentration = replace(diameter_dependent_model, - ventilation = models.HingedWindow(active=models.PeriodicInterval(period=120, duration=120), + concentration = replace(diameter_dependent_model, + ventilation = models.HingedWindow(active=models.PeriodicInterval(period=120, duration=120), outside_temp=models.PiecewiseConstant((0., 24.), (293.,)), window_height=1., opening_length=1., @@ -352,8 +361,8 @@ def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) q_air_mech vectorisation. - concentration = replace(diameter_dependent_model, - ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), + concentration = replace(diameter_dependent_model, + ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), q_air_mech=np.array([0.5, 1.])) ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " @@ -363,8 +372,8 @@ def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) air_exch vectorisation. - concentration = replace(diameter_dependent_model, - ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), + concentration = replace(diameter_dependent_model, + ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=np.array([0.5, 1.])) ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " @@ -377,21 +386,21 @@ def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, c [np.array([50, 100]), models.PiecewiseConstant((0., 24.), (293.,)), 0.3, "If the diameter is an array, none of the ventilation parameters or virus decay constant " "can be arrays at the same time."], # Verify room volume vectorisation - [50, models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]))), 0.3, + [50, models.PiecewiseConstant((0., 12, 24.), (np.array([293., 300.]), np.array([305., 310.]))), 0.3, "If the diameter is an array, none of the ventilation parameters or virus decay constant " "can be arrays at the same time."], # Verify room inside_temp vectorisation - [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), + [50, models.PiecewiseConstant((0., 24.), (293.,)), np.array([0.3, 0.5]), "If the diameter is an array, none of the ventilation parameters or virus decay constant " "can be arrays at the same time."], # Verify room humidity vectorisation ] ) def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error_message): - concentration = replace(diameter_dependent_model, + concentration = replace(diameter_dependent_model, room = models.Room(volume=volume, inside_temp=inside_temp, humidity=humidity), - ventilation = models.HVACMechanical(active=models.SpecificInterval(((0., 24.), )), q_air_mech=100.)) + ventilation = models.HVACMechanical(active=models.SpecificInterval(((0., 24.), )), q_air_mech=100.)) with pytest.raises(ValueError, match=error_message): models.ExposureModel(concentration, sr_model, populations[0], cases_model) - + @pytest.mark.parametrize( ["cm", "host_immunity", "expected_probability"], @@ -402,7 +411,7 @@ def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_m ) def test_host_immunity_vectorisation(sr_model, cases_model, cm, host_immunity, expected_probability): population = models.Population( - 10, halftime, models.Activity.types['Standing'], + 10, halftime, models.Activity.types['Standing'], models.Mask(np.array([0.3, 0.35])), host_immunity=host_immunity ) model = ExposureModel(cm, sr_model, population, cases_model) diff --git a/caimira/tests/models/test_short_range_model.py b/caimira/tests/models/test_short_range_model.py index 84ced3c2..4e02f8e6 100644 --- a/caimira/tests/models/test_short_range_model.py +++ b/caimira/tests/models/test_short_range_model.py @@ -13,20 +13,22 @@ @pytest.fixture -def concentration_model() -> mc_models.ConcentrationModel: +def concentration_model(data_registry) -> mc_models.ConcentrationModel: return mc_models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75), ventilation=models.AirChange( active=models.SpecificInterval(present_times=((8.5, 12.5), (13.5, 17.5))), air_exch=10_000_000., ), infected=mc_models.InfectedPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(present_times=((8.5, 12.5), (13.5, 17.5))), mask=models.Mask.types['No mask'], activity=models.Activity.types['Light activity'], - expiration=build_expiration({'Speaking': 0.33, 'Breathing': 0.67}), + expiration=build_expiration(data_registry, {'Speaking': 0.33, 'Breathing': 0.67}), host_immunity=0., ), evaporation_factor=0.3, @@ -34,9 +36,10 @@ def concentration_model() -> mc_models.ConcentrationModel: @pytest.fixture -def short_range_model(): - return mc_models.ShortRangeModel(expiration=short_range_expiration_distributions['Breathing'], - activity=activity_distributions['Seated'], +def short_range_model(data_registry): + return mc_models.ShortRangeModel(data_registry=data_registry, + expiration=short_range_expiration_distributions['Breathing'], + activity=activity_distributions(data_registry)['Seated'], presence=models.SpecificInterval(present_times=((10.5, 11.0),)), distance=short_range_distances) @@ -60,11 +63,14 @@ def test_short_range_model_ndarray(concentration_model, short_range_model): ["Heavy exercise", 16.372], ] ) -def test_dilution_factor(activity, expected_dilution): - model = mc_models.ShortRangeModel(expiration=short_range_expiration_distributions['Breathing'], - activity=models.Activity.types[activity], - presence=models.SpecificInterval(present_times=((10.5, 11.0),)), - distance=0.854).build_model(SAMPLE_SIZE) +def test_dilution_factor(data_registry, activity, expected_dilution): + model = mc_models.ShortRangeModel( + data_registry=data_registry, + expiration=short_range_expiration_distributions['Breathing'], + activity=models.Activity.types[activity], + presence=models.SpecificInterval(present_times=((10.5, 11.0),)), + distance=0.854 + ).build_model(SAMPLE_SIZE) assert isinstance(model.dilution_factor(), np.ndarray) np.testing.assert_almost_equal( model.dilution_factor(), expected_dilution @@ -116,12 +122,14 @@ def test_short_range_concentration(time, expected_short_range_concentration, ) -def test_short_range_exposure_with_ndarray_mask(): +def test_short_range_exposure_with_ndarray_mask(data_registry): c_model = mc_models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=50, humidity=0.3), ventilation=models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=10_000_000,), infected=mc_models.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(present_times=((8.5, 12.5), (13.5, 17.5))), virus=models.Virus.types['SARS_CoV_2_DELTA'], @@ -132,7 +140,8 @@ def test_short_range_exposure_with_ndarray_mask(): ), evaporation_factor=0.3, ) - sr_model = mc_models.ShortRangeModel(expiration=short_range_expiration_distributions['Shouting'], + sr_model = mc_models.ShortRangeModel(data_registry=data_registry, + expiration=short_range_expiration_distributions['Shouting'], activity=models.Activity.types['Heavy exercise'], presence=models.SpecificInterval(present_times=((10.5, 11.0),)), distance=0.854) diff --git a/caimira/tests/test_conditional_probability.py b/caimira/tests/test_conditional_probability.py index 85f1c666..6e0156d3 100644 --- a/caimira/tests/test_conditional_probability.py +++ b/caimira/tests/test_conditional_probability.py @@ -10,8 +10,9 @@ @pytest.fixture -def baseline_exposure_model(): +def baseline_exposure_model(data_registry): concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=50, inside_temp=models.PiecewiseConstant((0., 24.), (298,)), humidity=0.5), ventilation=models.MultipleVentilation( ventilations=( @@ -19,23 +20,25 @@ def baseline_exposure_model(): ) ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=mc.SpecificInterval(present_times=((0, 3.5), (4.5, 9))), - virus=virus_distributions['SARS_CoV_2_DELTA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], - activity=activity_distributions['Seated'], - expiration=expiration_distributions['Breathing'], + activity=activity_distributions(data_registry)['Seated'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., ), evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( number=3, presence=mc.SpecificInterval(present_times=((0, 3.5), (4.5, 9))), - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], mask=models.Mask.types['No mask'], host_immunity=0., ), @@ -45,7 +48,7 @@ def baseline_exposure_model(): @retry(tries=3) def test_conditional_prob_inf_given_vl_dist(baseline_exposure_model): - + viral_loads = np.array([3., 5., 7., 9.,]) mc_model: models.ExposureModel = baseline_exposure_model.build_model(2_000_000) @@ -64,14 +67,14 @@ def test_conditional_prob_inf_given_vl_dist(baseline_exposure_model): expected_pi_means.append(np.mean(pi)) expected_lower_percentiles.append(np.quantile(pi, 0.05)) expected_upper_percentiles.append(np.quantile(pi, 0.95)) - - infection_probability = mc_model.infection_probability() / 100 + + infection_probability = mc_model.infection_probability() / 100 specific_vl = np.log10(mc_model.concentration_model.infected.virus.viral_load_in_sputum) step = 8/100 actual_pi_means, actual_lower_percentiles, actual_upper_percentiles = ( report_generator.conditional_prob_inf_given_vl_dist(infection_probability, viral_loads, specific_vl, step) ) - + assert np.allclose(actual_pi_means, expected_pi_means, atol=0.002) assert np.allclose(actual_lower_percentiles, expected_lower_percentiles, atol=0.002) assert np.allclose(actual_upper_percentiles, expected_upper_percentiles, atol=0.002) diff --git a/caimira/tests/test_data_service.py b/caimira/tests/test_data_service.py index e5740873..5cd02f8e 100644 --- a/caimira/tests/test_data_service.py +++ b/caimira/tests/test_data_service.py @@ -11,7 +11,7 @@ class DataServiceTests(unittest.TestCase): def setUp(self): # Set up any necessary test data or configurations self.credentials = {"email": "test@example.com", "password": "password123"} - self.data_service = DataService(self.credentials) + self.data_service = DataService.create(self.credentials, host="https://dataservice.example.com") def test_jwt_expiration(self): is_valid = self.data_service._is_valid(None) @@ -47,7 +47,7 @@ def test_login_successful(self, mock_post): # Verify that the fetch method was called with the expected arguments mock_post.assert_called_once_with( - "https://caimira-data-api.app.cern.ch/login", + "https://dataservice.example.com/login", json=dict(email="test@example.com", password="password123"), headers={"Content-Type": "application/json"}, ) @@ -73,14 +73,14 @@ def test_fetch_successful(self, mock_login, mock_get): mock_get.return_value.json.return_value = {"data": "dummy_data"} # Call the fetch method with a mock access token mock_login.return_value = "dummy_token" - data = self.data_service.fetch() + 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_get.assert_called_once_with( - "https://caimira-data-api.app.cern.ch/data", + "https://dataservice.example.com/data", headers={ "Authorization": "Bearer dummy_token", "Content-Type": "application/json", @@ -96,7 +96,7 @@ def test_fetch_error(self, mock_login, mock_get): # Call the fetch method with a mock access token mock_login.return_value = "dummy_token" - data = self.data_service.fetch() + data = self.data_service._fetch() # Assert that the fetch method returns None in case of an error self.assertIsNone(data) diff --git a/caimira/tests/test_expiration.py b/caimira/tests/test_expiration.py index 4d498775..0cda581b 100644 --- a/caimira/tests/test_expiration.py +++ b/caimira/tests/test_expiration.py @@ -52,8 +52,8 @@ def test_multiple(): [(1.,5.,5.), 1.06701e-10], ], ) -def test_expiration_aerosols(BLO_weights, expected_aerosols): +def test_expiration_aerosols(data_registry, BLO_weights, expected_aerosols): mask = models.Mask.types['No mask'] - e = expiration_distribution(BLO_weights) + e = expiration_distribution(data_registry, BLO_weights) npt.assert_allclose(e.build_model(100000).aerosols(mask).mean(), expected_aerosols, rtol=1e-2) diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index ff5f3843..cde51c13 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -73,7 +73,7 @@ class SimpleConcentrationModel: #: Evaporation factor evaporation: float = 0.3 - + #: 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) @@ -123,7 +123,7 @@ def Np(self,diameter: float, result = 0. for cn,mu,sigma,famp in zip(self.cn,self.mu,self.sigma, BLO_factors): - result += ( (cn * famp)/sigma * + result += ( (cn * famp)/sigma * np.exp(-(np.log(diameter)-mu)**2/(2*sigma**2))) return result/(diameter*sqrt2pi) @@ -194,19 +194,19 @@ class SimpleShortRangeModel: For independent, end-to-end testing purposes. This assumes no mask wearing. """ - + #: Time intervals in which a short-range interaction occurs interaction_interval: SpecificInterval #: Tuple with interpersonal distanced from infected person (m) distance : _VectorisedFloat = 0.854 - + #: Breathing rate (m^3/h) breathing_rate: _VectorisedFloat = 0.51 #: Exhalation coefficient φ = 2 - + #: Tuple with BLO factors BLO_factors: typing.Tuple[float, float, float] = (1,0,0) @@ -215,18 +215,18 @@ class SimpleShortRangeModel: #: Maximum diameter for integration (short-range only) (microns) diameter_max: float = 100. - + #: Average mouth opening diameter (m) mouth_diameter: float = 0.02 - + #: Duration of the expiration period(s), assuming a 4s breath-cycle tstar: float = 2. - + #: Streamwise and radial penetration coefficients 𝛽r1: float = 0.18 𝛽r2: float = 0.2 𝛽x1: float = 2.4 - + @method_cache def dilution_factor(self) -> _VectorisedFloat: """ @@ -467,11 +467,13 @@ def probability_infection(self): @pytest.fixture -def c_model() -> mc.ConcentrationModel: +def c_model(data_registry) -> mc.ConcentrationModel: return mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=50, inside_temp=models.PiecewiseConstant((0., 24.), (293,)), humidity=0.3), ventilation=models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=1.), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=presence, virus=models.Virus.types['SARS_CoV_2_DELTA'], @@ -485,18 +487,20 @@ def c_model() -> mc.ConcentrationModel: @pytest.fixture -def c_model_distr() -> mc.ConcentrationModel: +def c_model_distr(data_registry) -> mc.ConcentrationModel: return mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=50, humidity=0.3), ventilation=models.AirChange(active=models.PeriodicInterval( period=120, duration=120), air_exch=1.), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=presence, - virus=virus_distributions['SARS_CoV_2_DELTA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], - activity=activity_distributions['Seated'], - expiration=expiration_distributions['Breathing'], + activity=activity_distributions(data_registry)['Seated'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., ), evaporation_factor=0.3, @@ -504,15 +508,17 @@ def c_model_distr() -> mc.ConcentrationModel: @pytest.fixture -def sr_models() -> typing.Tuple[mc.ShortRangeModel, ...]: +def sr_models(data_registry) -> typing.Tuple[mc.ShortRangeModel, ...]: return ( mc.ShortRangeModel( + data_registry = data_registry, expiration = short_range_expiration_distributions['Speaking'], activity = models.Activity.types['Seated'], presence = interaction_intervals[0], distance = 0.854, ), mc.ShortRangeModel( + data_registry = data_registry, expiration = short_range_expiration_distributions['Breathing'], activity = models.Activity.types['Heavy exercise'], presence = interaction_intervals[1], @@ -587,19 +593,21 @@ def simple_expo_sr_model(simple_sr_models) -> SimpleExposureModel: @pytest.fixture -def expo_sr_model_distr(c_model_distr) -> mc.ExposureModel: +def expo_sr_model_distr(data_registry, c_model_distr) -> mc.ExposureModel: return mc.ExposureModel( concentration_model=c_model_distr, short_range=( mc.ShortRangeModel( + data_registry = data_registry, expiration = short_range_expiration_distributions['Breathing'], - activity = activity_distributions['Seated'], + activity = activity_distributions(data_registry)['Seated'], presence = interaction_intervals[0], distance = short_range_distances, ), mc.ShortRangeModel( + data_registry = data_registry, expiration = short_range_expiration_distributions['Speaking'], - activity = activity_distributions['Seated'], + activity = activity_distributions(data_registry)['Seated'], presence = interaction_intervals[1], distance = short_range_distances, ), @@ -616,35 +624,35 @@ def expo_sr_model_distr(c_model_distr) -> mc.ExposureModel: @pytest.fixture -def simple_expo_sr_model_distr() -> SimpleExposureModel: +def simple_expo_sr_model_distr(data_registry) -> SimpleExposureModel: return SimpleExposureModel( infected_presence = presence, - viral_load = virus_distributions['SARS_CoV_2_DELTA' + viral_load = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viral_load_in_sputum, - breathing_rate = activity_distributions['Seated'].build_model( + breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' + viable_to_RNA = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., - ID50 = virus_distributions['SARS_CoV_2_DELTA' + ID50 = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).infectious_dose, - transmissibility = virus_distributions['SARS_CoV_2_DELTA' + transmissibility = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].transmissibility_factor, sr_models = ( SimpleShortRangeModel( interaction_interval = interaction_intervals[0], distance = short_range_distances.generate_samples(SAMPLE_SIZE), - breathing_rate = activity_distributions['Seated'].build_model( + breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, BLO_factors = expiration_BLO_factors['Breathing'], ), SimpleShortRangeModel( interaction_interval = interaction_intervals[1], distance = short_range_distances.generate_samples(SAMPLE_SIZE), - breathing_rate = activity_distributions['Seated'].build_model( + breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, BLO_factors = expiration_BLO_factors['Speaking'], ) @@ -718,12 +726,12 @@ def test_longrange_exposure(c_model): @pytest.mark.parametrize( "time", [11., 12.5, 17.] ) -def test_longrange_concentration_with_distributions(c_model_distr,time): +def test_longrange_concentration_with_distributions(c_model_distr, time, data_registry): simple_expo_model = SimpleConcentrationModel( infected_presence = presence, - viral_load = virus_distributions['SARS_CoV_2_DELTA' + viral_load = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viral_load_in_sputum, - breathing_rate = activity_distributions['Seated'].build_model( + breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., @@ -741,19 +749,19 @@ def test_longrange_concentration_with_distributions(c_model_distr,time): def test_longrange_exposure_with_distributions(c_model_distr): simple_expo_model = SimpleExposureModel( infected_presence = presence, - viral_load = virus_distributions['SARS_CoV_2_DELTA' + viral_load = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viral_load_in_sputum, - breathing_rate = activity_distributions['Seated'].build_model( + breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., BLO_factors = expiration_BLO_factors['Breathing'], - viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' + viable_to_RNA = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., - ID50 = virus_distributions['SARS_CoV_2_DELTA' + ID50 = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).infectious_dose, - transmissibility = virus_distributions['SARS_CoV_2_DELTA' + transmissibility = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].transmissibility_factor, sr_models = (), ) diff --git a/caimira/tests/test_infected_population.py b/caimira/tests/test_infected_population.py index 77f92697..192441e4 100644 --- a/caimira/tests/test_infected_population.py +++ b/caimira/tests/test_infected_population.py @@ -10,7 +10,7 @@ {'exhalation_rate': np.array([0.75, 0.81])}, ] ) -def test_infected_population_vectorisation(override_params): +def test_infected_population_vectorisation(override_params, data_registry): defaults = { 'viral_load_in_sputum': 1e9, 'exhalation_rate': 0.75, @@ -19,6 +19,7 @@ def test_infected_population_vectorisation(override_params): office_hours = caimira.models.SpecificInterval(present_times=[(8,17)]) infected = caimira.models.InfectedPopulation( + data_registry=data_registry, number=1, presence=office_hours, mask=caimira.models.Mask( diff --git a/caimira/tests/test_known_quantities.py b/caimira/tests/test_known_quantities.py index 86fb49a0..98a2fde2 100644 --- a/caimira/tests/test_known_quantities.py +++ b/caimira/tests/test_known_quantities.py @@ -16,8 +16,9 @@ def test_no_mask_superspeading_emission_rate(baseline_concentration_model): @pytest.fixture -def baseline_periodic_window(): +def baseline_periodic_window(data_registry): return models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(period=120, duration=15), outside_temp=models.PiecewiseConstant((0., 24.), (283,)), window_height=1.6, opening_length=0.6, @@ -58,14 +59,16 @@ def test_smooth_concentrations(baseline_concentration_model): assert np.abs(np.diff(concentrations)).max()/np.mean(concentrations) < dy_limit -def build_model(interval_duration): +def build_model(data_registry, interval_duration): model = models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75), ventilation=models.HEPAFilter( active=models.PeriodicInterval(period=120, duration=interval_duration), q_air_mech=500., ), infected=models.EmittingPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(((0., 4.), (5., 8.))), @@ -129,22 +132,28 @@ def test_periodic_hepa(baseline_periodic_hepa, baseline_room): [2.5, 3.7393925], ], ) -def test_multiple_ventilation_HEPA_window(baseline_periodic_hepa, time, expected_value): +def test_multiple_ventilation_HEPA_window(data_registry, baseline_periodic_hepa, time, expected_value): room = models.Room(volume=68., inside_temp=models.PiecewiseConstant((0., 24.),(293.15,))) tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15)) - window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]), - outside_temp=tempOutside, - window_height=1.,opening_length=0.6) + window = models.SlidingWindow( + data_registry=data_registry, + active=models.SpecificInterval([(1 / 60, 24.)]), + outside_temp=tempOutside, + window_height=1.,opening_length=0.6 + ) vent = models.MultipleVentilation([window, baseline_periodic_hepa]) npt.assert_allclose(vent.air_exchange(room,time), expected_value, rtol=1e-5) -def test_multiple_ventilation_HEPA_window_transitions(baseline_periodic_hepa): +def test_multiple_ventilation_HEPA_window_transitions(data_registry, baseline_periodic_hepa): tempOutside = models.PiecewiseConstant((0., 1., 2.5),(273.15, 283.15)) room = models.Room(68, models.PiecewiseConstant((0., 24.),(293.15,))) - window = models.SlidingWindow(active=models.SpecificInterval([(1 / 60, 24.)]), - outside_temp=tempOutside, - window_height=1.,opening_length=0.6) + window = models.SlidingWindow( + data_registry=data_registry, + active=models.SpecificInterval([(1 / 60, 24.)]), + outside_temp=tempOutside, + window_height=1.,opening_length=0.6 + ) vent = models.MultipleVentilation([window, baseline_periodic_hepa]) assert set(vent.transition_times(room)) == set([0.0, 1/60, 0.25, 1.0, 2.0, 2.25, 2.5, 4.0, 4.25, 6.0, 6.25, 8.0, 8.25, 10.0, 10.25, 12.0, 12.25, @@ -184,9 +193,10 @@ def test_multiple_ventilation_HEPA_HVAC_AirChange(volume, expected_value): [16., 3.7393925], ], ) -def test_windowopening(time, expected_value): +def test_windowopening(data_registry, time, expected_value): tempOutside = models.PiecewiseConstant((0., 10., 24.),(273.15, 283.15)) w = models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval([(0., 24.)]), outside_temp=tempOutside, window_height=1., opening_length=0.6, @@ -197,6 +207,7 @@ def test_windowopening(time, expected_value): def build_hourly_dependent_model( + data_registry, month, intervals_open=((7.5, 8.5),), intervals_presence_infected=((0., 4.), (5., 7.5)), @@ -220,13 +231,16 @@ def build_hourly_dependent_model( outside_temp = temperatures[month] model = models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293, ))), ventilation=models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval(intervals_open), outside_temp=outside_temp, window_height=1.6, opening_length=0.6, ), infected=models.EmittingPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(intervals_presence_infected), @@ -240,15 +254,18 @@ def build_hourly_dependent_model( return model -def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)): +def build_constant_temp_model(data_registry, outside_temp, intervals_open=((7.5, 8.5),)): model = models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))), ventilation=models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval(intervals_open), outside_temp=models.PiecewiseConstant((0., 24.), (outside_temp,)), window_height=1.6, opening_length=0.6, ), infected=models.EmittingPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(((0., 4.), (5., 7.5))), @@ -262,9 +279,10 @@ def build_constant_temp_model(outside_temp, intervals_open=((7.5, 8.5),)): return model -def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5, 8.5),)): +def build_hourly_dependent_model_multipleventilation(data_registry, month, intervals_open=((7.5, 8.5),)): vent = models.MultipleVentilation(( models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval(intervals_open), outside_temp=data.GenevaTemperatures[month], window_height=1.6, opening_length=0.6, @@ -275,9 +293,11 @@ def build_hourly_dependent_model_multipleventilation(month, intervals_open=((7.5 ), )) model = models.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))), ventilation=vent, infected=models.EmittingPopulation( + data_registry=data_registry, number=1, virus=models.Virus.types['SARS_CoV_2'], presence=models.SpecificInterval(((0., 4.), (5., 7.5))), diff --git a/caimira/tests/test_monte_carlo.py b/caimira/tests/test_monte_carlo.py index 76a0610f..98d91824 100644 --- a/caimira/tests/test_monte_carlo.py +++ b/caimira/tests/test_monte_carlo.py @@ -37,16 +37,19 @@ def test_type_annotations(): @pytest.fixture -def baseline_mc_concentration_model() -> caimira.monte_carlo.ConcentrationModel: +def baseline_mc_concentration_model(data_registry) -> caimira.monte_carlo.ConcentrationModel: mc_model = caimira.monte_carlo.ConcentrationModel( - room=caimira.monte_carlo.Room(volume=caimira.monte_carlo.sampleable.Normal(75, 20), + data_registry=data_registry, + room=caimira.monte_carlo.Room(volume=caimira.monte_carlo.sampleable.Normal(75, 20), inside_temp=caimira.models.PiecewiseConstant((0., 24.), (293,))), ventilation=caimira.monte_carlo.SlidingWindow( + data_registry=data_registry, active=caimira.models.PeriodicInterval(period=120, duration=120), outside_temp=caimira.models.PiecewiseConstant((0., 24.), (283,)), window_height=1.6, opening_length=0.6, ), infected=caimira.models.InfectedPopulation( + data_registry=data_registry, number=1, virus=caimira.models.Virus.types['SARS_CoV_2'], presence=caimira.models.SpecificInterval(((0., 4.), (5., 8.))), diff --git a/caimira/tests/test_monte_carlo_full_models.py b/caimira/tests/test_monte_carlo_full_models.py index 60cdf477..3acb379e 100644 --- a/caimira/tests/test_monte_carlo_full_models.py +++ b/caimira/tests/test_monte_carlo_full_models.py @@ -37,15 +37,17 @@ # References values for infection_probability and expected new cases # in the following tests, were obtained from the feature/mc branch @pytest.fixture -def shared_office_mc(): +def shared_office_mc(data_registry): """ Corresponds to the 1st line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=50, inside_temp=models.PiecewiseConstant((0., 24.), (298,)), humidity=0.5), ventilation=models.MultipleVentilation( ventilations=( models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(period=120, duration=120), outside_temp=data.GenevaTemperatures['Jun'], window_height=1.6, @@ -55,12 +57,13 @@ def shared_office_mc(): ) ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=mc.SpecificInterval(present_times=((0, 3.5), (4.5, 9))), - virus=virus_distributions['SARS_CoV_2_DELTA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], - activity=activity_distributions['Seated'], - expiration=build_expiration({'Speaking': 0.33, 'Breathing': 0.67}), + activity=activity_distributions(data_registry)['Seated'], + expiration=build_expiration(data_registry, {'Speaking': 0.33, 'Breathing': 0.67}), host_immunity=0., ), evaporation_factor=0.3, @@ -71,7 +74,7 @@ def shared_office_mc(): exposed=mc.Population( number=3, presence=mc.SpecificInterval(present_times=((0, 3.5), (4.5, 9))), - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], mask=models.Mask.types['No mask'], host_immunity=0., ), @@ -80,15 +83,17 @@ def shared_office_mc(): @pytest.fixture -def classroom_mc(): +def classroom_mc(data_registry): """ Corresponds to the 2nd line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=160, inside_temp=models.PiecewiseConstant((0., 24.), (293,)), humidity=0.3), ventilation=models.MultipleVentilation( ventilations=( models.SlidingWindow( + data_registry=data_registry, active=models.PeriodicInterval(period=120, duration=120), outside_temp=TorontoTemperatures['Dec'], window_height=1.6, @@ -98,12 +103,13 @@ def classroom_mc(): ) ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(((0, 2), (2.5, 4), (5, 7), (7.5, 9))), - virus=virus_distributions['SARS_CoV_2_ALPHA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_ALPHA'], mask=models.Mask.types["No mask"], - activity=activity_distributions['Light activity'], - expiration=build_expiration('Speaking'), + activity=activity_distributions(data_registry)['Light activity'], + expiration=build_expiration(data_registry, 'Speaking'), host_immunity=0., ), evaporation_factor=0.3, @@ -114,7 +120,7 @@ def classroom_mc(): exposed=mc.Population( number=19, presence=models.SpecificInterval(((0, 2), (2.5, 4), (5, 7), (7.5, 9))), - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], mask=models.Mask.types["No mask"], host_immunity=0., ), @@ -123,22 +129,24 @@ def classroom_mc(): @pytest.fixture -def ski_cabin_mc(): +def ski_cabin_mc(data_registry): """ Corresponds to the 3rd line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988 """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=10, humidity=0.3), ventilation=models.MultipleVentilation( (models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=0.0), models.AirChange(active=models.PeriodicInterval(period=120, duration=120), air_exch=0.25))), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(((0, 20/60),)), - virus=virus_distributions['SARS_CoV_2_DELTA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], - activity=activity_distributions['Moderate activity'], - expiration=build_expiration('Speaking'), + activity=activity_distributions(data_registry)['Moderate activity'], + expiration=build_expiration(data_registry, 'Speaking'), host_immunity=0., ), evaporation_factor=0.3, @@ -149,7 +157,7 @@ def ski_cabin_mc(): exposed=mc.Population( number=3, presence=models.SpecificInterval(((0, 20/60),)), - activity=activity_distributions['Moderate activity'], + activity=activity_distributions(data_registry)['Moderate activity'], mask=models.Mask.types['No mask'], host_immunity=0., ), @@ -158,17 +166,19 @@ def ski_cabin_mc(): @pytest.fixture -def skagit_chorale_mc(): +def skagit_chorale_mc(data_registry): """ - Corresponds to the 4th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, - assuming viral is 10**9 instead of a LogCustomKernel distribution. + Corresponds to the 4th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, + assuming viral is 10**9 instead of a LogCustomKernel distribution. """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=810, humidity=0.5), ventilation=models.AirChange( active=models.PeriodicInterval(period=120, duration=120), air_exch=0.7), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(((0, 2.5), )), virus=mc.SARSCoV2( @@ -178,8 +188,8 @@ def skagit_chorale_mc(): transmissibility_factor=1., ), mask=models.Mask.types['No mask'], - activity=activity_distributions['Moderate activity'], - expiration=build_expiration('Shouting'), + activity=activity_distributions(data_registry)['Moderate activity'], + expiration=build_expiration(data_registry, 'Shouting'), host_immunity=0., ), evaporation_factor=0.3, @@ -190,7 +200,7 @@ def skagit_chorale_mc(): exposed=mc.Population( number=60, presence=models.SpecificInterval(((0, 2.5), )), - activity=activity_distributions['Moderate activity'], + activity=activity_distributions(data_registry)['Moderate activity'], mask=models.Mask.types['No mask'], host_immunity=0., ), @@ -199,17 +209,19 @@ def skagit_chorale_mc(): @pytest.fixture -def bus_ride_mc(): +def bus_ride_mc(data_registry): """ - Corresponds to the 5th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, - assuming viral is 5*10**8 instead of a LogCustomKernel distribution. + Corresponds to the 5th line of Table 4 in https://doi.org/10.1101/2021.10.14.21264988, + assuming viral is 5*10**8 instead of a LogCustomKernel distribution. """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=45, humidity=0.5), ventilation=models.AirChange( active=models.PeriodicInterval(period=120, duration=120), air_exch=1.25), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, presence=models.SpecificInterval(((0, 1.67), )), virus=mc.SARSCoV2( @@ -219,8 +231,8 @@ def bus_ride_mc(): transmissibility_factor=1., ), mask=models.Mask.types['No mask'], - activity=activity_distributions['Seated'], - expiration=build_expiration('Speaking'), + activity=activity_distributions(data_registry)['Seated'], + expiration=build_expiration(data_registry, 'Speaking'), host_immunity=0., ), evaporation_factor=0.3, @@ -231,7 +243,7 @@ def bus_ride_mc(): exposed=mc.Population( number=67, presence=models.SpecificInterval(((0, 1.67), )), - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], mask=models.Mask.types['No mask'], host_immunity=0., ), @@ -240,22 +252,24 @@ def bus_ride_mc(): @pytest.fixture -def gym_mc(): +def gym_mc(data_registry): """ Gym model for testing """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=300, humidity=0.5), ventilation=models.AirChange( active=models.SpecificInterval(((0., 24.),)), air_exch=6, ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=2, - virus=virus_distributions['SARS_CoV_2_ALPHA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_ALPHA'], presence=mc.SpecificInterval(((0., 1.),)), mask=models.Mask.types["No mask"], - activity=activity_distributions['Heavy exercise'], + activity=activity_distributions(data_registry)['Heavy exercise'], expiration=expiration_distributions['Breathing'], host_immunity=0., ), @@ -276,23 +290,25 @@ def gym_mc(): @pytest.fixture -def waiting_room_mc(): +def waiting_room_mc(data_registry): """ Waiting room model for testing """ concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=100, humidity=0.5), ventilation=models.AirChange( active=models.SpecificInterval(((0., 24.),)), air_exch=0.25, ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, - virus=virus_distributions['SARS_CoV_2_ALPHA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_ALPHA'], presence=mc.SpecificInterval(((0., 2.),)), mask=models.Mask.types["No mask"], - activity=activity_distributions['Seated'], - expiration=build_expiration({'Speaking': 0.3, 'Breathing': 0.7}), + activity=activity_distributions(data_registry)['Seated'], + expiration=build_expiration(data_registry, {'Speaking': 0.3, 'Breathing': 0.7}), host_immunity=0., ), evaporation_factor=0.3, @@ -356,13 +372,15 @@ def test_report_models(mc_model, expected_pi, expected_new_cases, ["Cloth", "Jul", 5.322, 5.064, 673.10*0.305], ], ) -def test_small_shared_office_Geneva(mask_type, month, expected_pi, +def test_small_shared_office_Geneva(data_registry, mask_type, month, expected_pi, expected_dose, expected_ER): concentration_mc = mc.ConcentrationModel( + data_registry=data_registry, room=models.Room(volume=33, inside_temp=models.PiecewiseConstant((0., 24.), (293,)), humidity=0.5), ventilation=models.MultipleVentilation( ( models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval(((0., 24.),)), outside_temp=data.GenevaTemperatures[month], window_height=1.5, opening_length=0.2, @@ -374,12 +392,13 @@ def test_small_shared_office_Geneva(mask_type, month, expected_pi, ), ), infected=mc.InfectedPopulation( + data_registry=data_registry, number=1, - virus=virus_distributions['SARS_CoV_2_ALPHA'], + virus=virus_distributions(data_registry)['SARS_CoV_2_ALPHA'], presence=mc.SpecificInterval(((9., 10+2/3), (10+5/6, 12.5), (13.5, 15+2/3), (15+5/6, 18.))), mask=models.Mask.types[mask_type], - activity=activity_distributions['Seated'], - expiration=build_expiration({'Speaking': 0.33, 'Breathing': 0.67}), + activity=activity_distributions(data_registry)['Seated'], + expiration=build_expiration(data_registry, {'Speaking': 0.33, 'Breathing': 0.67}), host_immunity=0., ), evaporation_factor=0.3, @@ -390,7 +409,7 @@ def test_small_shared_office_Geneva(mask_type, month, expected_pi, exposed=mc.Population( number=1, presence=concentration_mc.infected.presence, - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], mask=concentration_mc.infected.mask, host_immunity=0., ), diff --git a/caimira/tests/test_predefined_distributions.py b/caimira/tests/test_predefined_distributions.py index e2c029da..1ed8d240 100644 --- a/caimira/tests/test_predefined_distributions.py +++ b/caimira/tests/test_predefined_distributions.py @@ -3,6 +3,7 @@ import pytest from caimira.monte_carlo.data import activity_distributions, virus_distributions +from caimira.store import data_registry # Mean & std deviations from https://doi.org/10.1101/2021.10.14.21264988 (Table 3) @@ -21,13 +22,13 @@ ['Heavy exercise', 3.28, 0.72,], ] ) -def test_activity_distributions(distribution, mean, std): - activity = activity_distributions[distribution].build_model(size=1000000) +def test_activity_distributions(data_registry, distribution, mean, std): + activity = activity_distributions(data_registry)[distribution].build_model(size=1000000) npt.assert_allclose(activity.inhalation_rate.mean(), mean, atol=0.01) npt.assert_allclose(activity.inhalation_rate.std(), std, atol=0.01) -# Mean & std deviations from https://doi.org/10.1101/2021.10.14.21264988 (Table 3) +# Mean & std deviations from https://doi.org/10.1101/2021.10.14.21264988 (Table 3) # - with a refined precision on the values @pytest.mark.parametrize( "distribution, mean, std",[ @@ -39,6 +40,6 @@ def test_activity_distributions(distribution, mean, std): ] ) def test_viral_load_logdistribution(distribution, mean, std): - virus = virus_distributions[distribution].build_model(size=1000000) + virus = virus_distributions(data_registry)[distribution].build_model(size=1000000) npt.assert_allclose(np.log10(virus.viral_load_in_sputum).mean(), mean, atol=0.01) npt.assert_allclose(np.log10(virus.viral_load_in_sputum).std(), std, atol=0.01) diff --git a/caimira/tests/test_ventilation.py b/caimira/tests/test_ventilation.py index 4a09782f..4d322cfa 100644 --- a/caimira/tests/test_ventilation.py +++ b/caimira/tests/test_ventilation.py @@ -8,8 +8,9 @@ @pytest.fixture -def baseline_slidingwindow(): +def baseline_slidingwindow(data_registry): return models.SlidingWindow( + data_registry=data_registry, active=models.SpecificInterval(((0, 4), (5, 9))), outside_temp=models.PiecewiseConstant((0, 24), (283,)), window_height=1.6, opening_length=0.6, From 2abdd130cffd623fd5d13e8f474d9bbc43c425d1 Mon Sep 17 00:00:00 2001 From: Nicola Tarocco Date: Fri, 8 Dec 2023 13:56:55 +0100 Subject: [PATCH 2/7] config: remove Ref: config parsing --- caimira/enums.py | 13 ++++ caimira/monte_carlo/data.py | 109 +++++++++++++++--------------- caimira/monte_carlo/sampleable.py | 6 +- caimira/store/data_registry.py | 45 ++++++------ 4 files changed, 96 insertions(+), 77 deletions(-) create mode 100644 caimira/enums.py diff --git a/caimira/enums.py b/caimira/enums.py new file mode 100644 index 00000000..8b5418bd --- /dev/null +++ b/caimira/enums.py @@ -0,0 +1,13 @@ +from enum import Enum + +class ViralLoads(Enum): + COVID_OVERALL = "COVID overall" + SYMPTOMATIC_FREQUENCIES = "Symptomatic frequencies" + + +class InfectiousDoses(Enum): + DISTRIBUTION = "Distribution" + + +class ViableToRNARatios(Enum): + DISTRIBUTION = "Distribution" diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index 101508c5..d6fca11c 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass import typing @@ -5,10 +7,37 @@ from scipy import special as sp from scipy.stats import weibull_min -import caimira.monte_carlo as mc +from caimira.enums import ViralLoads, InfectiousDoses, ViableToRNARatios + +import caimira.monte_carlo.models as mc from caimira.monte_carlo.sampleable import LogCustom, LogNormal, Normal, LogCustomKernel, CustomKernel, Uniform, Custom from caimira.store.data_registry import DataRegistry + +def evaluate_vl(value, data_registry: DataRegistry): + if value == ViralLoads.COVID_OVERALL: + return covid_overal_vl_data(data_registry) + elif value == ViralLoads.COVID_OVERALL: + return symptomatic_vl_frequencies + else: + raise ValueError(f"Invalid ViralLoads value {value}") + + +def evaluate_infectd(value, data_registry: DataRegistry): + if value == InfectiousDoses.DISTRIBUTION: + return infectious_dose_distribution(data_registry) + else: + raise ValueError(f"Invalid InfectiousDoses value {value}") + + +def evaluate_vtrr(value, data_registry: DataRegistry): + """.""" + if value == ViableToRNARatios.DISTRIBUTION: + return viable_to_RNA_ratio_distribution(data_registry) + else: + raise ValueError(f"Invalid ViableToRNARatios value {value}") + + sqrt2pi = np.sqrt(2.*np.pi) sqrt2 = np.sqrt(2.) @@ -94,10 +123,7 @@ def param_evaluation(root: typing.Dict, param: typing.Union[str, typing.Any]) -> 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': + if value == 'Custom': custom_distribution: typing.Dict = custom_distribution_lookup( root, 'custom distribution') for d, p in custom_distribution.items(): @@ -286,66 +312,43 @@ def infectious_dose_distribution(data_registry): # From https://doi.org/10.1101/2021.10.14.21264988 and references therein def virus_distributions(data_registry): + vd = data_registry.virus_distributions return { 'SARS_CoV_2': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2']['transmissibility_factor'], ), 'SARS_CoV_2_ALPHA': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_ALPHA'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2_ALPHA']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2_ALPHA']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2_ALPHA']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2_ALPHA']['transmissibility_factor'], ), 'SARS_CoV_2_BETA': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_BETA'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_BETA'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_BETA'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_BETA'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2_BETA']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2_BETA']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2_BETA']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2_BETA']['transmissibility_factor'], ), 'SARS_CoV_2_GAMMA': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_GAMMA'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2_GAMMA']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2_GAMMA']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2_GAMMA']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2_GAMMA']['transmissibility_factor'], ), 'SARS_CoV_2_DELTA': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_DELTA'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2_DELTA']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2_DELTA']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2_DELTA']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2_DELTA']['transmissibility_factor'], ), 'SARS_CoV_2_OMICRON': mc.SARSCoV2( - viral_load_in_sputum=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'viral_load_in_sputum'), - infectious_dose=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'infectious_dose'), - viable_to_RNA_ratio=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'viable_to_RNA_ratio'), - transmissibility_factor=param_evaluation( - data_registry.virus_distributions['SARS_CoV_2_OMICRON'], 'transmissibility_factor'), + viral_load_in_sputum=evaluate_vl(vd['SARS_CoV_2_OMICRON']['viral_load_in_sputum'], data_registry), + infectious_dose=evaluate_infectd(vd['SARS_CoV_2_OMICRON']['infectious_dose'], data_registry), + viable_to_RNA_ratio=evaluate_vtrr(vd['SARS_CoV_2_OMICRON']['viable_to_RNA_ratio'], data_registry), + transmissibility_factor=vd['SARS_CoV_2_OMICRON']['transmissibility_factor'], ), } diff --git a/caimira/monte_carlo/sampleable.py b/caimira/monte_carlo/sampleable.py index 479b91cf..4bbc4c35 100644 --- a/caimira/monte_carlo/sampleable.py +++ b/caimira/monte_carlo/sampleable.py @@ -62,7 +62,7 @@ class Custom(SampleableDistribution): Defines a distribution which follows a custom curve vs. the random variable. Uses a simple algorithm. This is appropriate for a smooth distribution function. - Note: in max_function, a value slightly above the maximum of the distribution + Note: in max_function, a value slightly above the maximum of the distribution function should be provided. """ def __init__(self, bounds: typing.Tuple[float, float], @@ -87,8 +87,8 @@ class LogCustom(SampleableDistribution): """ Defines a distribution which follows a custom curve vs. the log (in base 10) of the random variable. Uses a simple algorithm. This is appropriate for a smooth - distribution function. - Note: in max_function, a value slightly above the maximum of the distribution + distribution function. + Note: in max_function, a value slightly above the maximum of the distribution function should be provided. """ def __init__(self, bounds: typing.Tuple[float, float], diff --git a/caimira/store/data_registry.py b/caimira/store/data_registry.py index 69d6d702..e0c0cfb8 100644 --- a/caimira/store/data_registry.py +++ b/caimira/store/data_registry.py @@ -1,3 +1,6 @@ +from caimira.enums import ViralLoads, InfectiousDoses, ViableToRNARatios + + class DataRegistry: """Registry to hold data values.""" @@ -207,51 +210,51 @@ class DataRegistry: } 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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.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", + "viral_load_in_sputum": ViralLoads.COVID_OVERALL, + "infectious_dose": InfectiousDoses.DISTRIBUTION, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, "transmissibility_factor": 0.1, "infectiousness_days": 14, }, From 20e8bf1df7771302409cf64395e2500839eeb519 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Mon, 11 Dec 2023 18:00:31 +0100 Subject: [PATCH 3/7] added data_service attr where needed --- caimira/apps/calculator/__init__.py | 22 +++---- caimira/apps/calculator/form_data.py | 1 - caimira/apps/calculator/report_generator.py | 9 ++- caimira/monte_carlo/data.py | 12 ++-- caimira/store/data_service.py | 4 +- .../apps/calculator/test_model_generator.py | 16 ++--- .../test_specific_model_generator.py | 10 +-- .../tests/models/test_concentration_model.py | 8 ++- .../tests/models/test_dynamic_population.py | 5 +- caimira/tests/models/test_exposure_model.py | 52 ++++++++-------- .../tests/models/test_fitting_algorithm.py | 3 +- .../tests/models/test_short_range_model.py | 11 ++-- caimira/tests/test_full_algorithm.py | 61 ++++++++++--------- caimira/tests/test_known_quantities.py | 42 +++++++------ caimira/tests/test_monte_carlo_full_models.py | 9 ++- 15 files changed, 146 insertions(+), 119 deletions(-) diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 6ab1b920..9004f940 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -103,8 +103,8 @@ class ConcentrationModel(BaseRequestHandler): async def post(self) -> None: debug = self.settings.get("debug", False) - data_registry = self.settings.get("data_registry") - data_service = self.settings.get("data_service") + data_registry: DataRegistry = self.settings["data_registry"] + data_service: typing.Optional[DataService] = self.settings.get("data_service", None) if data_service: data_service.update_registry(data_registry) @@ -159,10 +159,10 @@ async def post(self) -> None: """ debug = self.settings.get("debug", False) - data_registry = self.settings.get("data_registry") - data_service = self.settings.get("data_service") + data_registry: DataRegistry = self.settings["data_registry"] + data_service: typing.Optional[DataService] = self.settings.get("data_service", None) if data_service: - data_service.update_configuration(data_registry) + data_service.update_registry(data_registry) requested_model_config = json.loads(self.request.body) LOG.debug(pformat(requested_model_config)) @@ -190,10 +190,10 @@ class StaticModel(BaseRequestHandler): async def get(self) -> None: debug = self.settings.get("debug", False) - data_registry = self.settings.get("data_registry") - data_service = self.settings.get("data_service") + data_registry: DataRegistry = self.settings["data_registry"] + data_service: typing.Optional[DataService] = self.settings.get("data_service", None) if data_service: - data_service.update_configuration(data_registry) + data_service.update_registry(data_registry) form = model_generator.VirusFormData.from_dict(model_generator.baseline_raw_form_data(), data_registry) base_url = self.request.protocol + "://" + self.request.host @@ -368,10 +368,10 @@ def check_xsrf_cookie(self): pass async def post(self, endpoint: str) -> None: - data_registry = self.settings.get("data_registry") - data_service = self.settings.get("data_service") + data_registry: DataRegistry = self.settings["data_registry"] + data_service: typing.Optional[DataService] = self.settings.get("data_service", None) if data_service: - data_service.update_configuration(data_registry) + data_service.update_registry(data_registry) requested_model_config = tornado.escape.json_decode(self.request.body) try: diff --git a/caimira/apps/calculator/form_data.py b/caimira/apps/calculator/form_data.py index ecb98e13..a0a96b67 100644 --- a/caimira/apps/calculator/form_data.py +++ b/caimira/apps/calculator/form_data.py @@ -1,5 +1,4 @@ import dataclasses -import datetime import html import logging import typing diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 9ff0358e..4a4bac12 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -216,17 +216,17 @@ def conditional_prob_inf_given_vl_dist( def manufacture_conditional_probability_data( - data_registry: DataRegistry, exposure_model: models.ExposureModel, infection_probability: models._VectorisedFloat ): - + data_registry: DataRegistry = exposure_model.data_registry + min_vl = data_registry.conditional_prob_inf_given_viral_load['min_vl'] max_vl = data_registry.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, + pi_means, lower_percentiles, upper_percentiles = conditional_prob_inf_given_vl_dist(data_registry, infection_probability, viral_loads, specific_vl, step) return list(viral_loads), list(pi_means), list(lower_percentiles), list(upper_percentiles) @@ -414,12 +414,11 @@ def manufacture_alternative_scenarios(form: VirusFormData) -> typing.Dict[str, m def scenario_statistics( - data_registry: DataRegistry, mc_model: mc.ExposureModel, sample_times: typing.List[float], compute_prob_exposure: bool ): - model = mc_model.build_model(size=data_registry.monte_carlo_sample_size) + model = mc_model.build_model(size=mc_model.data_registry.monte_carlo_sample_size) if (compute_prob_exposure): # It means we have data to calculate the total_probability_rule prob_probabilistic_exposure = model.total_probability_rule() diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index d6fca11c..17171a77 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -291,7 +291,7 @@ def covid_overal_vl_data(data_registry): function=lambda d: np.interp( d, viral_load(data_registry), - frequencies_pdf, + frequencies_pdf(data_registry), data_registry.covid_overal_vl_data['interpolation_fp_left'], data_registry.covid_overal_vl_data['interpolation_fp_right'] ), @@ -441,22 +441,24 @@ def expiration_BLO_factors(data_registry): def expiration_distributions(data_registry): return { exp_type: expiration_distribution( - BLO_factors, + data_registry=data_registry, + BLO_factors=BLO_factors, d_min=param_evaluation(data_registry.long_range_expiration_distributions, 'minimum_diameter'), d_max=param_evaluation(data_registry.long_range_expiration_distributions, 'maximum_diameter') ) - for exp_type, BLO_factors in expiration_BLO_factors.items() + for exp_type, BLO_factors in expiration_BLO_factors(data_registry).items() } def short_range_expiration_distributions(data_registry): return { exp_type: expiration_distribution( - BLO_factors, + data_registry=data_registry, + BLO_factors=BLO_factors, d_min=param_evaluation(data_registry.short_range_expiration_distributions, 'minimum_diameter'), d_max=param_evaluation(data_registry.short_range_expiration_distributions, 'maximum_diameter') ) - for exp_type, BLO_factors in expiration_BLO_factors.items() + for exp_type, BLO_factors in expiration_BLO_factors(data_registry).items() } diff --git a/caimira/store/data_service.py b/caimira/store/data_service.py index b9163848..a8964031 100644 --- a/caimira/store/data_service.py +++ b/caimira/store/data_service.py @@ -18,14 +18,14 @@ class DataService: def __init__( self, - credentials: typing.Dict[str, str], + credentials: typing.Dict[str, typing.Optional[str]], host: str, ): self._credentials = credentials self._host = host @classmethod - def create(cls, credentials: typing.Dict[str, str], host: str = "https://caimira-data-api.app.cern.ch"): + def create(cls, credentials: typing.Dict[str, typing.Optional[str]], host: str = "https://caimira-data-api.app.cern.ch"): """Factory.""" return cls(credentials, host) diff --git a/caimira/tests/apps/calculator/test_model_generator.py b/caimira/tests/apps/calculator/test_model_generator.py index c6f6c89f..0372eb82 100644 --- a/caimira/tests/apps/calculator/test_model_generator.py +++ b/caimira/tests/apps/calculator/test_model_generator.py @@ -187,7 +187,7 @@ def test_infected_less_than_total_people(activity, total_people, infected_people baseline_form.total_people = total_people baseline_form.infected_people = infected_people with pytest.raises(ValueError, match=error): - baseline_form.validate(data_registry) + baseline_form.validate() def present_times(interval: models.Interval) -> models.BoundarySequence_t: @@ -275,7 +275,7 @@ def test_exposed_present_lunch_end_before_beginning(baseline_form: model_generat baseline_form.exposed_lunch_start = minutes_since_midnight(14 * 60) baseline_form.exposed_lunch_finish = minutes_since_midnight(13 * 60) with pytest.raises(ValueError): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.mark.parametrize( @@ -291,7 +291,7 @@ def test_exposed_presence_lunch_break(baseline_form: model_generator.VirusFormDa baseline_form.exposed_lunch_start = minutes_since_midnight(exposed_lunch_start * 60) baseline_form.exposed_lunch_finish = minutes_since_midnight(exposed_lunch_finish * 60) with pytest.raises(ValueError, match='exposed lunch break must be within presence times.'): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.mark.parametrize( @@ -307,7 +307,7 @@ def test_infected_presence_lunch_break(baseline_form: model_generator.VirusFormD baseline_form.infected_lunch_start = minutes_since_midnight(infected_lunch_start * 60) baseline_form.infected_lunch_finish = minutes_since_midnight(infected_lunch_finish * 60) with pytest.raises(ValueError, match='infected lunch break must be within presence times.'): - baseline_form.validate(data_registry) + baseline_form.validate() def test_exposed_breaks_length(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): @@ -317,7 +317,7 @@ def test_exposed_breaks_length(baseline_form: model_generator.VirusFormData, dat baseline_form.exposed_finish = minutes_since_midnight(11 * 60) baseline_form.exposed_lunch_option = False with pytest.raises(ValueError, match='Length of breaks >= Length of exposed presence.'): - baseline_form.validate(data_registry) + baseline_form.validate() def test_infected_breaks_length(baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): @@ -328,7 +328,7 @@ def test_infected_breaks_length(baseline_form: model_generator.VirusFormData, da baseline_form.infected_coffee_break_option = 'coffee_break_4' baseline_form.infected_coffee_duration = 30 with pytest.raises(ValueError, match='Length of breaks >= Length of infected presence.'): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.fixture @@ -440,7 +440,7 @@ def test_valid_no_lunch(baseline_form: model_generator.VirusFormData, data_regis baseline_form.exposed_lunch_option = False baseline_form.exposed_lunch_start = minutes_since_midnight(0) baseline_form.exposed_lunch_finish = minutes_since_midnight(0) - assert baseline_form.validate(data_registry) is None + assert baseline_form.validate() is None def test_no_breaks(baseline_form: model_generator.VirusFormData): @@ -516,7 +516,7 @@ def test_natural_ventilation_window_opening_periodically(baseline_form: model_ge baseline_form.windows_duration = 20 baseline_form.windows_frequency = 10 with pytest.raises(ValueError, match='Duration cannot be bigger than frequency.'): - baseline_form.validate(data_registry) + baseline_form.validate() def test_key_validation_mech_ventilation_type_na(baseline_form_data, data_registry): diff --git a/caimira/tests/apps/calculator/test_specific_model_generator.py b/caimira/tests/apps/calculator/test_specific_model_generator.py index b25c9f02..e8a6b977 100644 --- a/caimira/tests/apps/calculator/test_specific_model_generator.py +++ b/caimira/tests/apps/calculator/test_specific_model_generator.py @@ -17,7 +17,7 @@ def test_specific_break_structure(break_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.specific_breaks = break_input with pytest.raises(TypeError, match=error): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.mark.parametrize( @@ -34,7 +34,7 @@ def test_specific_break_structure(break_input, error, baseline_form: model_gener def test_specific_population_break_data_structure(population_break_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.specific_breaks = {'exposed_breaks': population_break_input, 'infected_breaks': population_break_input} with pytest.raises(TypeError, match=error): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.mark.parametrize( @@ -68,7 +68,7 @@ def test_specific_break_time(break_input, error, baseline_form: model_generator. def test_precise_activity_structure(precise_activity_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): baseline_form.precise_activity = precise_activity_input with pytest.raises(TypeError, match=error): - baseline_form.validate(data_registry) + baseline_form.validate() @pytest.mark.parametrize( @@ -80,7 +80,7 @@ def test_precise_activity_structure(precise_activity_input, error, baseline_form [{"physical_activity": "Light activity", "respiratory_activity": [{"type": "Breathing", "percentage": 50}]}, 'The sum of all respiratory activities should be 100. Got 50.'], ] ) -def test_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.VirusFormData, data_registry: DataRegistry): +def test_sum_precise_activity(precise_activity_input, error, baseline_form: model_generator.VirusFormData): baseline_form.precise_activity = precise_activity_input with pytest.raises(ValueError, match=error): - baseline_form.validate(data_registry) + baseline_form.validate() diff --git a/caimira/tests/models/test_concentration_model.py b/caimira/tests/models/test_concentration_model.py index 7c126843..88d796d7 100644 --- a/caimira/tests/models/test_concentration_model.py +++ b/caimira/tests/models/test_concentration_model.py @@ -4,9 +4,9 @@ import numpy.testing as npt import pytest from dataclasses import dataclass -import typing from caimira import models +from caimira.store.data_registry import DataRegistry @dataclass(frozen=True) class KnownConcentrationModelBase(models._ConcentrationModelBase): @@ -198,12 +198,14 @@ def test_integrated_concentration(simple_conc_model): ] ) def test_normed_integrated_concentration_with_background_concentration( + data_registry: DataRegistry, simple_conc_model: models.ConcentrationModel, dummy_population: models.Population, known_min_background_concentration: float, expected_normed_integrated_concentration: float): known_conc_model = KnownConcentrationModelBase( + data_registry, room = simple_conc_model.room, ventilation = simple_conc_model.ventilation, known_population = dummy_population, @@ -229,6 +231,7 @@ def test_normed_integrated_concentration_with_background_concentration( ] ) def test_normed_integrated_concentration_vectorisation( + data_registry: DataRegistry, simple_conc_model: models.ConcentrationModel, dummy_population: models.Population, known_removal_rate: float, @@ -237,6 +240,7 @@ def test_normed_integrated_concentration_vectorisation( expected_normed_integrated_concentration: float): known_conc_model = KnownConcentrationModelBase( + data_registry = data_registry, room = simple_conc_model.room, ventilation = simple_conc_model.ventilation, known_population = dummy_population, @@ -264,6 +268,7 @@ def test_normed_integrated_concentration_vectorisation( ] ) def test_zero_ventilation_rate( + data_registry: DataRegistry, simple_conc_model: models.ConcentrationModel, dummy_population: models.Population, known_removal_rate: float, @@ -271,6 +276,7 @@ def test_zero_ventilation_rate( expected_concentration: float): known_conc_model = KnownConcentrationModelBase( + data_registry = data_registry, room = simple_conc_model.room, ventilation = simple_conc_model.ventilation, known_population = dummy_population, diff --git a/caimira/tests/models/test_dynamic_population.py b/caimira/tests/models/test_dynamic_population.py index 7b7607f4..79c89980 100644 --- a/caimira/tests/models/test_dynamic_population.py +++ b/caimira/tests/models/test_dynamic_population.py @@ -10,6 +10,7 @@ @pytest.fixture def full_exposure_model(data_registry): return models.ExposureModel( + data_registry=data_registry, concentration_model=models.ConcentrationModel( data_registry=data_registry, room=models.Room(volume=100), @@ -25,6 +26,7 @@ def full_exposure_model(data_registry): virus=models.Virus.types['SARS_CoV_2'], host_immunity=0. ), + evaporation_factor=0.3, ), short_range=(), exposed=models.Population( @@ -148,12 +150,13 @@ def test_linearity_with_number_of_infected(full_exposure_model: models.ExposureM @pytest.mark.parametrize( "time", (8., 9., 10., 11., 12., 13., 14.), ) -def test_dynamic_dose(full_exposure_model: models.ExposureModel, time: float): +def test_dynamic_dose(data_registry, full_exposure_model: models.ExposureModel, time: float): dynamic_infected: models.ExposureModel = dc_utils.nested_replace( full_exposure_model, { 'concentration_model.infected': models.InfectedPopulation( + data_registry=data_registry, number=models.IntPiecewiseConstant( (8, 10, 12, 13, 17), (1, 2, 0, 3)), presence=None, diff --git a/caimira/tests/models/test_exposure_model.py b/caimira/tests/models/test_exposure_model.py index 3bd6640e..99e7ca1e 100644 --- a/caimira/tests/models/test_exposure_model.py +++ b/caimira/tests/models/test_exposure_model.py @@ -71,7 +71,7 @@ def known_concentrations(func, data_registry=DataRegistry()): ) normed_func = lambda x: (func(x) / dummy_infected_population.emission_rate_per_person_when_present()) - return KnownNormedconcentration(dummy_room, dummy_ventilation, + return KnownNormedconcentration(data_registry, dummy_room, dummy_ventilation, dummy_infected_population, 0.3, normed_func) @@ -92,9 +92,9 @@ def known_concentrations(func, data_registry=DataRegistry()): [populations[2], known_concentrations(lambda t: np.array([18., 36.])), np.array([40.91708675, 91.46172332]), np.array([51.6749232285, 80.3196524031])], ]) -def test_exposure_model_ndarray(population, cm, +def test_exposure_model_ndarray(data_registry, population, cm, expected_exposure, expected_probability, sr_model, cases_model): - model = ExposureModel(cm, sr_model, population, cases_model) + model = ExposureModel(data_registry, cm, sr_model, population, cases_model) np.testing.assert_almost_equal( model.deposited_exposure(), expected_exposure ) @@ -113,10 +113,10 @@ def test_exposure_model_ndarray(population, cm, [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exposure, sr_model, cases_model): +def test_exposure_model_ndarray_and_float_mix(data_registry, population, expected_deposited_exposure, sr_model, cases_model): cm = known_concentrations( lambda t: 0. if np.floor(t) % 2 else np.array([0.6, 0.6])) - model = ExposureModel(cm, sr_model, population, cases_model) + model = ExposureModel(data_registry, cm, sr_model, population, cases_model) np.testing.assert_almost_equal( model.deposited_exposure(), expected_deposited_exposure @@ -131,17 +131,17 @@ def test_exposure_model_ndarray_and_float_mix(population, expected_deposited_exp [populations[1], np.array([2.13410688, 1.98167067])], [populations[2], np.array([1.36390289, 1.52436206])], ]) -def test_exposure_model_vector(population, expected_deposited_exposure, sr_model, cases_model): +def test_exposure_model_vector(data_registry, population, expected_deposited_exposure, sr_model, cases_model): cm_array = known_concentrations(lambda t: np.array([0.6, 0.6])) - model_array = ExposureModel(cm_array, sr_model, population, cases_model) + model_array = ExposureModel(data_registry, cm_array, sr_model, population, cases_model) np.testing.assert_almost_equal( model_array.deposited_exposure(), np.array(expected_deposited_exposure) ) -def test_exposure_model_scalar(sr_model, cases_model): +def test_exposure_model_scalar(data_registry, sr_model, cases_model): cm_scalar = known_concentrations(lambda t: 0.6) - model_scalar = ExposureModel(cm_scalar, sr_model, populations[0], cases_model) + model_scalar = ExposureModel(data_registry, cm_scalar, sr_model, populations[0], cases_model) expected_deposited_exposure = 1.52436206 np.testing.assert_almost_equal( model_scalar.deposited_exposure(), expected_deposited_exposure @@ -185,7 +185,7 @@ def diameter_dependent_model(conc_model, data_registry) -> models.InfectedPopula virus=models.Virus.types['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], - expiration=expiration_distributions['Breathing'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., )) @@ -213,14 +213,14 @@ def cases_model(): [(0., 24.), 645.8401125684933], ] ) -def test_exposure_model_integral_accuracy(exposed_time_interval, +def test_exposure_model_integral_accuracy(data_registry, exposed_time_interval, expected_deposited_exposure, conc_model, sr_model, cases_model): presence_interval = models.SpecificInterval((exposed_time_interval,)) population = models.Population( 10, presence_interval, models.Activity.types['Standing'], models.Mask.types['Type I'], 0., ) - model = ExposureModel(conc_model, sr_model, population, cases_model) + model = ExposureModel(data_registry, conc_model, sr_model, population, cases_model) np.testing.assert_allclose(model.deposited_exposure(), expected_deposited_exposure) @@ -248,7 +248,7 @@ def test_infectious_dose_vectorisation(sr_model, cases_model, data_registry): 10, presence_interval, models.Activity.types['Standing'], models.Mask.types['Type I'], 0., ) - model = ExposureModel(cm, sr_model, population, cases_model) + model = ExposureModel(data_registry, cm, sr_model, population, cases_model) inf_probability = model.infection_probability() assert isinstance(inf_probability, np.ndarray) assert inf_probability.shape == (3, ) @@ -304,13 +304,13 @@ def test_prob_meet_infected_person(pop, cases, AB, exposed, infected, prob_meet_ [30, known_concentrations(lambda t: 0.6), 100000, 68, 5, 55.93154502], ]) -def test_probabilistic_exposure_probability(sr_model, exposed_population, cm, +def test_probabilistic_exposure_probability(data_registry, sr_model, exposed_population, cm, pop, AB, cases, probabilistic_exposure_probability): population = models.Population( exposed_population, models.PeriodicInterval(120, 60), models.Activity.types['Standing'], models.Mask.types['Type I'], host_immunity=0.,) - model = ExposureModel(cm, sr_model, population, models.Cases(geographic_population=pop, + model = ExposureModel(data_registry, cm, sr_model, population, models.Cases(geographic_population=pop, geographic_cases=cases, ascertainment_bias=AB),) np.testing.assert_allclose( model.total_probability_rule(), probabilistic_exposure_probability, rtol=0.05 @@ -342,10 +342,10 @@ def test_diameter_vectorisation_window_opening(data_registry, diameter_dependent ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time."): - models.ExposureModel(concentration, sr_model, populations[0], cases_model) + models.ExposureModel(data_registry, concentration, sr_model, populations[0], cases_model) -def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model, cases_model): +def test_diameter_vectorisation_hinged_window(data_registry, diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) window_width vectorisation. concentration = replace(diameter_dependent_model, ventilation = models.HingedWindow(active=models.PeriodicInterval(period=120, duration=120), @@ -356,10 +356,10 @@ def test_diameter_vectorisation_hinged_window(diameter_dependent_model, sr_model ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time."): - models.ExposureModel(concentration, sr_model, populations[0], cases_model) + models.ExposureModel(data_registry, concentration, sr_model, populations[0], cases_model) -def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, cases_model): +def test_diameter_vectorisation_HEPA_filter(data_registry, diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) q_air_mech vectorisation. concentration = replace(diameter_dependent_model, ventilation = models.HEPAFilter(active=models.PeriodicInterval(period=120, duration=120), @@ -367,10 +367,10 @@ def test_diameter_vectorisation_HEPA_filter(diameter_dependent_model, sr_model, ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time."): - models.ExposureModel(concentration, sr_model, populations[1], cases_model) + models.ExposureModel(data_registry, concentration, sr_model, populations[1], cases_model) -def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, cases_model): +def test_diameter_vectorisation_air_change(data_registry, diameter_dependent_model, sr_model, cases_model): # Verify (ventilation) air_exch vectorisation. concentration = replace(diameter_dependent_model, ventilation = models.AirChange(active=models.PeriodicInterval(period=120, duration=120), @@ -378,7 +378,7 @@ def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, c ) with pytest.raises(ValueError, match="If the diameter is an array, none of the ventilation parameters " "or virus decay constant can be arrays at the same time."): - models.ExposureModel(concentration, sr_model, populations[2], cases_model) + models.ExposureModel(data_registry, concentration, sr_model, populations[2], cases_model) @pytest.mark.parametrize( @@ -394,12 +394,12 @@ def test_diameter_vectorisation_air_change(diameter_dependent_model, sr_model, c "can be arrays at the same time."], # Verify room humidity vectorisation ] ) -def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error_message): +def test_diameter_vectorisation_room(data_registry, diameter_dependent_model, sr_model, cases_model, volume, inside_temp, humidity, error_message): concentration = replace(diameter_dependent_model, room = models.Room(volume=volume, inside_temp=inside_temp, humidity=humidity), ventilation = models.HVACMechanical(active=models.SpecificInterval(((0., 24.), )), q_air_mech=100.)) with pytest.raises(ValueError, match=error_message): - models.ExposureModel(concentration, sr_model, populations[0], cases_model) + models.ExposureModel(data_registry, concentration, sr_model, populations[0], cases_model) @pytest.mark.parametrize( @@ -409,12 +409,12 @@ def test_diameter_vectorisation_room(diameter_dependent_model, sr_model, cases_m [known_concentrations(lambda t: 18.), np.array([0., 1.]), np.array([67.95037626, 0.])], ] ) -def test_host_immunity_vectorisation(sr_model, cases_model, cm, host_immunity, expected_probability): +def test_host_immunity_vectorisation(data_registry, sr_model, cases_model, cm, host_immunity, expected_probability): population = models.Population( 10, halftime, models.Activity.types['Standing'], models.Mask(np.array([0.3, 0.35])), host_immunity=host_immunity ) - model = ExposureModel(cm, sr_model, population, cases_model) + model = ExposureModel(data_registry, cm, sr_model, population, cases_model) inf_probability = model.infection_probability() np.testing.assert_almost_equal( diff --git a/caimira/tests/models/test_fitting_algorithm.py b/caimira/tests/models/test_fitting_algorithm.py index 4e1c98fb..c4bafa4d 100644 --- a/caimira/tests/models/test_fitting_algorithm.py +++ b/caimira/tests/models/test_fitting_algorithm.py @@ -16,8 +16,9 @@ ['Standing', [8, 17], [2.45]], ] ) -def test_fitting_algorithm(activity_type, ventilation_active, air_exch): +def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air_exch): conc_model = models.CO2ConcentrationModel( + data_registry = data_registry, room=models.Room( volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293,))), ventilation=models.CustomVentilation(models.PiecewiseConstant( diff --git a/caimira/tests/models/test_short_range_model.py b/caimira/tests/models/test_short_range_model.py index 4e02f8e6..5e20d03b 100644 --- a/caimira/tests/models/test_short_range_model.py +++ b/caimira/tests/models/test_short_range_model.py @@ -38,10 +38,10 @@ def concentration_model(data_registry) -> mc_models.ConcentrationModel: @pytest.fixture def short_range_model(data_registry): return mc_models.ShortRangeModel(data_registry=data_registry, - expiration=short_range_expiration_distributions['Breathing'], + expiration=short_range_expiration_distributions(data_registry)['Breathing'], activity=activity_distributions(data_registry)['Seated'], presence=models.SpecificInterval(present_times=((10.5, 11.0),)), - distance=short_range_distances) + distance=short_range_distances(data_registry)) def test_short_range_model_ndarray(concentration_model, short_range_model): @@ -66,7 +66,7 @@ def test_short_range_model_ndarray(concentration_model, short_range_model): def test_dilution_factor(data_registry, activity, expected_dilution): model = mc_models.ShortRangeModel( data_registry=data_registry, - expiration=short_range_expiration_distributions['Breathing'], + expiration=short_range_expiration_distributions(data_registry)['Breathing'], activity=models.Activity.types[activity], presence=models.SpecificInterval(present_times=((10.5, 11.0),)), distance=0.854 @@ -135,17 +135,18 @@ def test_short_range_exposure_with_ndarray_mask(data_registry): virus=models.Virus.types['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], - expiration=expiration_distributions['Breathing'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., ), evaporation_factor=0.3, ) sr_model = mc_models.ShortRangeModel(data_registry=data_registry, - expiration=short_range_expiration_distributions['Shouting'], + expiration=short_range_expiration_distributions(data_registry)['Shouting'], activity=models.Activity.types['Heavy exercise'], presence=models.SpecificInterval(present_times=((10.5, 11.0),)), distance=0.854) e_model = mc_models.ExposureModel( + data_registry = data_registry, concentration_model = c_model, short_range = (sr_model,), exposed = mc_models.Population( diff --git a/caimira/tests/test_full_algorithm.py b/caimira/tests/test_full_algorithm.py index cde51c13..3f6c7fa5 100644 --- a/caimira/tests/test_full_algorithm.py +++ b/caimira/tests/test_full_algorithm.py @@ -9,10 +9,9 @@ from retry import retry import caimira.monte_carlo as mc -from caimira import models,data +from caimira import models from caimira.utils import method_cache from caimira.models import _VectorisedFloat,Interval,SpecificInterval -from caimira.monte_carlo.sampleable import LogNormal from caimira.monte_carlo.data import (expiration_distributions, expiration_BLO_factors,short_range_expiration_distributions, short_range_distances,virus_distributions,activity_distributions) @@ -479,7 +478,7 @@ def c_model(data_registry) -> mc.ConcentrationModel: virus=models.Virus.types['SARS_CoV_2_DELTA'], mask=models.Mask.types['No mask'], activity=models.Activity.types['Seated'], - expiration=expiration_distributions['Breathing'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., ), evaporation_factor=0.3, @@ -512,14 +511,14 @@ def sr_models(data_registry) -> typing.Tuple[mc.ShortRangeModel, ...]: return ( mc.ShortRangeModel( data_registry = data_registry, - expiration = short_range_expiration_distributions['Speaking'], + expiration = short_range_expiration_distributions(data_registry)['Speaking'], activity = models.Activity.types['Seated'], presence = interaction_intervals[0], distance = 0.854, ), mc.ShortRangeModel( data_registry = data_registry, - expiration = short_range_expiration_distributions['Breathing'], + expiration = short_range_expiration_distributions(data_registry)['Breathing'], activity = models.Activity.types['Heavy exercise'], presence = interaction_intervals[1], distance = 0.854, @@ -528,40 +527,41 @@ def sr_models(data_registry) -> typing.Tuple[mc.ShortRangeModel, ...]: @pytest.fixture -def simple_c_model() -> SimpleConcentrationModel: +def simple_c_model(data_registry) -> SimpleConcentrationModel: return SimpleConcentrationModel( infected_presence = presence, viral_load = models.Virus.types['SARS_CoV_2_DELTA'].viral_load_in_sputum, breathing_rate = models.Activity.types['Seated'].exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ) @pytest.fixture -def simple_sr_models() -> typing.Tuple[SimpleShortRangeModel, ...]: +def simple_sr_models(data_registry) -> typing.Tuple[SimpleShortRangeModel, ...]: return ( SimpleShortRangeModel( interaction_interval = interaction_intervals[0], distance = 0.854, breathing_rate = models.Activity.types['Seated'].exhalation_rate, - BLO_factors = expiration_BLO_factors['Speaking'], + BLO_factors = expiration_BLO_factors(data_registry)['Speaking'], ), SimpleShortRangeModel( interaction_interval = interaction_intervals[1], distance = 0.854, breathing_rate = models.Activity.types['Heavy exercise'].exhalation_rate, - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], ), ) @pytest.fixture -def expo_sr_model(c_model,sr_models) -> mc.ExposureModel: +def expo_sr_model(data_registry, c_model,sr_models) -> mc.ExposureModel: return mc.ExposureModel( + data_registry=data_registry, concentration_model=c_model, short_range=sr_models, exposed=mc.Population( @@ -576,14 +576,14 @@ def expo_sr_model(c_model,sr_models) -> mc.ExposureModel: @pytest.fixture -def simple_expo_sr_model(simple_sr_models) -> SimpleExposureModel: +def simple_expo_sr_model(data_registry, simple_sr_models) -> SimpleExposureModel: return SimpleExposureModel( infected_presence = presence, viral_load = models.Virus.types['SARS_CoV_2_DELTA'].viral_load_in_sputum, breathing_rate = models.Activity.types['Seated'].exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ID50 = models.Virus.types['SARS_CoV_2_DELTA'].infectious_dose, @@ -595,21 +595,22 @@ def simple_expo_sr_model(simple_sr_models) -> SimpleExposureModel: @pytest.fixture def expo_sr_model_distr(data_registry, c_model_distr) -> mc.ExposureModel: return mc.ExposureModel( + data_registry=data_registry, concentration_model=c_model_distr, short_range=( mc.ShortRangeModel( data_registry = data_registry, - expiration = short_range_expiration_distributions['Breathing'], + expiration = short_range_expiration_distributions(data_registry)['Breathing'], activity = activity_distributions(data_registry)['Seated'], presence = interaction_intervals[0], - distance = short_range_distances, + distance = short_range_distances(data_registry), ), mc.ShortRangeModel( data_registry = data_registry, - expiration = short_range_expiration_distributions['Speaking'], + expiration = short_range_expiration_distributions(data_registry)['Speaking'], activity = activity_distributions(data_registry)['Seated'], presence = interaction_intervals[1], - distance = short_range_distances, + distance = short_range_distances(data_registry), ), ), exposed=mc.Population( @@ -633,7 +634,7 @@ def simple_expo_sr_model_distr(data_registry) -> SimpleExposureModel: SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], viable_to_RNA = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., @@ -644,17 +645,17 @@ def simple_expo_sr_model_distr(data_registry) -> SimpleExposureModel: sr_models = ( SimpleShortRangeModel( interaction_interval = interaction_intervals[0], - distance = short_range_distances.generate_samples(SAMPLE_SIZE), + distance = short_range_distances(data_registry).generate_samples(SAMPLE_SIZE), breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], ), SimpleShortRangeModel( interaction_interval = interaction_intervals[1], - distance = short_range_distances.generate_samples(SAMPLE_SIZE), + distance = short_range_distances(data_registry).generate_samples(SAMPLE_SIZE), breathing_rate = activity_distributions(data_registry)['Seated'].build_model( SAMPLE_SIZE).exhalation_rate, - BLO_factors = expiration_BLO_factors['Speaking'], + BLO_factors = expiration_BLO_factors(data_registry)['Speaking'], ) ), ) @@ -687,14 +688,14 @@ def test_shortrange_concentration(time,c_model,simple_c_model, ) -def test_longrange_exposure(c_model): +def test_longrange_exposure(data_registry, c_model): simple_expo_model = SimpleExposureModel( infected_presence = presence, viral_load = models.Virus.types['SARS_CoV_2_DELTA'].viral_load_in_sputum, breathing_rate = models.Activity.types['Seated'].exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], viable_to_RNA = models.Virus.types['SARS_CoV_2_DELTA'].viable_to_RNA_ratio, HI = 0., ID50 = models.Virus.types['SARS_CoV_2_DELTA'].infectious_dose, @@ -702,6 +703,7 @@ def test_longrange_exposure(c_model): sr_models = (), ) expo_model = mc.ExposureModel( + data_registry=data_registry, concentration_model=c_model.build_model(SAMPLE_SIZE), short_range=(), exposed=mc.Population( @@ -735,8 +737,8 @@ def test_longrange_concentration_with_distributions(c_model_distr, time, data_re SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], - viable_to_RNA = virus_distributions['SARS_CoV_2_DELTA' + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], + viable_to_RNA = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., ) @@ -746,7 +748,7 @@ def test_longrange_concentration_with_distributions(c_model_distr, time, data_re ) -def test_longrange_exposure_with_distributions(c_model_distr): +def test_longrange_exposure_with_distributions(data_registry, c_model_distr): simple_expo_model = SimpleExposureModel( infected_presence = presence, viral_load = virus_distributions(data_registry)['SARS_CoV_2_DELTA' @@ -755,7 +757,7 @@ def test_longrange_exposure_with_distributions(c_model_distr): SAMPLE_SIZE).exhalation_rate, room_volume = 50., lambda_ventilation= 1., - BLO_factors = expiration_BLO_factors['Breathing'], + BLO_factors = expiration_BLO_factors(data_registry)['Breathing'], viable_to_RNA = virus_distributions(data_registry)['SARS_CoV_2_DELTA' ].build_model(SAMPLE_SIZE).viable_to_RNA_ratio, HI = 0., @@ -766,13 +768,14 @@ def test_longrange_exposure_with_distributions(c_model_distr): sr_models = (), ) expo_model = mc.ExposureModel( + data_registry=data_registry, concentration_model=c_model_distr.build_model(SAMPLE_SIZE), short_range=(), exposed=mc.Population( number=1, presence=presence, mask=models.Mask.types['No mask'], - activity=activity_distributions['Seated'], + activity=activity_distributions(data_registry)['Seated'], host_immunity=0., ), geographical_data=models.Cases(), diff --git a/caimira/tests/test_known_quantities.py b/caimira/tests/test_known_quantities.py index 98a2fde2..ba7e451a 100644 --- a/caimira/tests/test_known_quantities.py +++ b/caimira/tests/test_known_quantities.py @@ -84,11 +84,11 @@ def build_model(data_registry, interval_duration): return model -def test_concentrations_startup(): +def test_concentrations_startup(data_registry): # The concentrations should be the same until the beginning of the # first time that the ventilation is disabled. - m1 = build_model(interval_duration=120) - m2 = build_model(interval_duration=65) + m1 = build_model(data_registry, interval_duration=120) + m2 = build_model(data_registry, interval_duration=65) assert m1.concentration(1.) == m2.concentration(1.) @@ -319,11 +319,11 @@ def build_hourly_dependent_model_multipleventilation(data_registry, month, inter "time", [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8.], ) -def test_concentrations_hourly_dep_temp_vs_constant(month, temperatures, time): +def test_concentrations_hourly_dep_temp_vs_constant(data_registry, month, temperatures, time): # The concentrations should be the same up to 8 AM (time when the # temperature changes DURING the window opening). - m1 = build_hourly_dependent_model(month) - m2 = build_constant_temp_model(temperatures[7] + 273.15) + m1 = build_hourly_dependent_model(data_registry, month) + m2 = build_constant_temp_model(data_registry, temperatures[7] + 273.15) npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5) @pytest.mark.parametrize( @@ -334,10 +334,11 @@ def test_concentrations_hourly_dep_temp_vs_constant(month, temperatures, time): "time", [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8.], ) -def test_concentrations_hourly_dep_temp_startup(month, temperatures, time): +def test_concentrations_hourly_dep_temp_startup(data_registry, month, temperatures, time): # The concentrations should be the zero up to the first presence time # of an infected person. m = build_hourly_dependent_model( + data_registry, month, ((0., 0.5), (1., 1.5), (4., 4.5), (7.5, 8), ), ((8., 12.), ), @@ -345,8 +346,8 @@ def test_concentrations_hourly_dep_temp_startup(month, temperatures, time): assert m.concentration(time) == 0. -def test_concentrations_hourly_dep_multipleventilation(): - m = build_hourly_dependent_model_multipleventilation('Jan') +def test_concentrations_hourly_dep_multipleventilation(data_registry): + m = build_hourly_dependent_model_multipleventilation(data_registry, 'Jan') m.concentration(12.) @@ -358,11 +359,11 @@ def test_concentrations_hourly_dep_multipleventilation(): "time", [0.5, 1.2, 2., 3.5, 5., 6.5, 7.5, 7.9, 8., 8.5, 9., 12.], ) -def test_concentrations_hourly_dep_adding_artificial_transitions(month_temp_item, time): +def test_concentrations_hourly_dep_adding_artificial_transitions(data_registry, month_temp_item, time): month, temperatures = month_temp_item # Adding a second opening inside the first one should not change anything - m1 = build_hourly_dependent_model(month, intervals_open=((7.5, 8.5), )) - m2 = build_hourly_dependent_model(month, intervals_open=((7.5, 8.5), (8., 8.1), )) + m1 = build_hourly_dependent_model(data_registry, month, intervals_open=((7.5, 8.5), )) + m2 = build_hourly_dependent_model(data_registry, month, intervals_open=((7.5, 8.5), (8., 8.1), )) npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-5) @@ -373,17 +374,18 @@ def test_concentrations_hourly_dep_adding_artificial_transitions(month_temp_item + [float(t) for t in np.arange(0, 24.5, 0.5)] ), ) -def test_concentrations_refine_times(time): +def test_concentrations_refine_times(data_registry, time): month = 'Jan' - m1 = build_hourly_dependent_model(month, intervals_open=((0., 24.),)) - m2 = build_hourly_dependent_model(month, intervals_open=((0., 24.),), + m1 = build_hourly_dependent_model(data_registry, month, intervals_open=((0., 24.),)) + m2 = build_hourly_dependent_model(data_registry, month, intervals_open=((0., 24.),), artificial_refinement=True) npt.assert_allclose(m1.concentration(time), m2.concentration(time), rtol=1e-8) -def build_exposure_model(concentration_model, short_range_model): +def build_exposure_model(data_registry, concentration_model, short_range_model): infected = concentration_model.infected return models.ExposureModel( + data_registry=data_registry, concentration_model=concentration_model, short_range=short_range_model, exposed=models.Population( @@ -406,9 +408,11 @@ def build_exposure_model(concentration_model, short_range_model): ['Jun', 1385.917562], ], ) -def test_exposure_hourly_dep(month,expected_deposited_exposure, baseline_sr_model): +def test_exposure_hourly_dep(data_registry, month, expected_deposited_exposure, baseline_sr_model): m = build_exposure_model( + data_registry, build_hourly_dependent_model( + data_registry, month, intervals_open=((0., 24.), ), intervals_presence_infected=((8., 12.), (13., 17.)) @@ -427,9 +431,11 @@ def test_exposure_hourly_dep(month,expected_deposited_exposure, baseline_sr_mode ['Jun', 1439.267381], ], ) -def test_exposure_hourly_dep_refined(month,expected_deposited_exposure, baseline_sr_model): +def test_exposure_hourly_dep_refined(data_registry, month, expected_deposited_exposure, baseline_sr_model): m = build_exposure_model( + data_registry, build_hourly_dependent_model( + data_registry, month, intervals_open=((0., 24.),), intervals_presence_infected=((8., 12.), (13., 17.)), diff --git a/caimira/tests/test_monte_carlo_full_models.py b/caimira/tests/test_monte_carlo_full_models.py index 3acb379e..1c329bad 100644 --- a/caimira/tests/test_monte_carlo_full_models.py +++ b/caimira/tests/test_monte_carlo_full_models.py @@ -69,6 +69,7 @@ def shared_office_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -115,6 +116,7 @@ def classroom_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -152,6 +154,7 @@ def ski_cabin_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -195,6 +198,7 @@ def skagit_chorale_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -238,6 +242,7 @@ def bus_ride_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -270,12 +275,13 @@ def gym_mc(data_registry): presence=mc.SpecificInterval(((0., 1.),)), mask=models.Mask.types["No mask"], activity=activity_distributions(data_registry)['Heavy exercise'], - expiration=expiration_distributions['Breathing'], + expiration=expiration_distributions(data_registry)['Breathing'], host_immunity=0., ), evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( @@ -314,6 +320,7 @@ def waiting_room_mc(data_registry): evaporation_factor=0.3, ) return mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( From d79ef934cb410d501bea1ae8c3cc8e913b55a4a0 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Tue, 12 Dec 2023 11:58:19 +0100 Subject: [PATCH 4/7] finished injection of data_service param --- caimira/apps/calculator/co2_model_generator.py | 2 +- caimira/apps/calculator/model_generator.py | 6 +++--- caimira/apps/expert.py | 1 + caimira/apps/expert_co2.py | 2 -- caimira/models.py | 4 +++- caimira/monte_carlo/data.py | 2 +- caimira/tests/apps/calculator/test_model_generator.py | 10 ++++++---- caimira/tests/conftest.py | 2 +- caimira/tests/models/test_fitting_algorithm.py | 1 + caimira/tests/test_conditional_probability.py | 4 ++-- caimira/tests/test_monte_carlo.py | 3 ++- caimira/tests/test_monte_carlo_full_models.py | 9 +++++---- caimira/tests/test_predefined_distributions.py | 2 +- 13 files changed, 27 insertions(+), 21 deletions(-) diff --git a/caimira/apps/calculator/co2_model_generator.py b/caimira/apps/calculator/co2_model_generator.py index cd21eafc..a16c33e3 100644 --- a/caimira/apps/calculator/co2_model_generator.py +++ b/caimira/apps/calculator/co2_model_generator.py @@ -2,7 +2,6 @@ import logging import typing import numpy as np -from caimira.store.data_registry import DataRegistry import ruptures as rpt import matplotlib.pyplot as plt import re @@ -177,6 +176,7 @@ def build_model(self, size=None) -> models.CO2DataModel: # type: ignore for _, stop in zip(all_state_changes[:-1], all_state_changes[1:])] return models.CO2DataModel( + data_registry=self.data_registry, room_volume=self.room_volume, number=models.IntPiecewiseConstant(transition_times=tuple(all_state_changes), values=tuple(total_people)), presence=None, diff --git a/caimira/apps/calculator/model_generator.py b/caimira/apps/calculator/model_generator.py index 03bfba17..26b11244 100644 --- a/caimira/apps/calculator/model_generator.py +++ b/caimira/apps/calculator/model_generator.py @@ -212,10 +212,10 @@ def build_mc_model(self) -> mc.ExposureModel: for interaction in self.short_range_interactions: short_range.append(mc.ShortRangeModel( data_registry=self.data_registry, - expiration=short_range_expiration_distributions[interaction['expiration']], + expiration=short_range_expiration_distributions(self.data_registry)[interaction['expiration']], activity=infected_population.activity, presence=self.short_range_interval(interaction), - distance=short_range_distances, + distance=short_range_distances(self.data_registry), )) return mc.ExposureModel( @@ -473,7 +473,7 @@ def short_range_interval(self, interaction) -> models.SpecificInterval: def build_expiration(data_registry, expiration_definition) -> mc._ExpirationBase: if isinstance(expiration_definition, str): - return expiration_distributions[expiration_definition] + return expiration_distributions(data_registry)[expiration_definition] elif isinstance(expiration_definition, dict): total_weight = sum(expiration_definition.values()) BLO_factors = np.sum([ diff --git a/caimira/apps/expert.py b/caimira/apps/expert.py index 7082e4b9..111505d2 100644 --- a/caimira/apps/expert.py +++ b/caimira/apps/expert.py @@ -840,6 +840,7 @@ def present(self): def baseline_model(data_registry: DataRegistry): return models.ExposureModel( + data_registry=data_registry, concentration_model=models.ConcentrationModel( data_registry=data_registry, room=models.Room(volume=75, inside_temp=models.PiecewiseConstant((0., 24.), (293.15,))), diff --git a/caimira/apps/expert_co2.py b/caimira/apps/expert_co2.py index 6ce8b81f..d083d20b 100644 --- a/caimira/apps/expert_co2.py +++ b/caimira/apps/expert_co2.py @@ -22,8 +22,6 @@ def baseline_model(data_registry: DataRegistry): presence=models.SpecificInterval(((8., 12.), (13., 17.))), activity=models.Activity.types['Seated'], ), - CO2_atmosphere_concentration=440.44, - CO2_fraction_exhaled=0.042, ) diff --git a/caimira/models.py b/caimira/models.py index 80b2f215..4963b80f 100644 --- a/caimira/models.py +++ b/caimira/models.py @@ -344,7 +344,7 @@ class SlidingWindow(WindowOpening): Sliding window, or side-hung window (with the hinge perpendicular to the horizontal plane). """ - data_registry: DataRegistry = None + data_registry: DataRegistry = DataRegistry() @property def discharge_coefficient(self) -> _VectorisedFloat: @@ -1507,6 +1507,7 @@ class CO2DataModel: It uses optimization techniques to fit the model's parameters and estimate the exhalation rate and ventilation values that best match the measured CO2 concentrations. ''' + data_registry: DataRegistry room_volume: float number: typing.Union[int, IntPiecewiseConstant] presence: typing.Optional[Interval] @@ -1518,6 +1519,7 @@ def CO2_concentrations_from_params(self, exhalation_rate: float, ventilation_values: typing.Tuple[float, ...]) -> typing.List[_VectorisedFloat]: CO2_concentrations = CO2ConcentrationModel( + data_registry=self.data_registry, room=Room(volume=self.room_volume), ventilation=CustomVentilation(PiecewiseConstant( self.ventilation_transition_times, ventilation_values)), diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index 17171a77..1a403084 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -389,7 +389,7 @@ def expiration_distribution( BLO_factors, d_min=0.1, d_max=30., -) -> mc.Expiration: +): """ Returns an Expiration with an aerosol diameter distribution, defined by the BLO factors (a length-3 tuple). diff --git a/caimira/tests/apps/calculator/test_model_generator.py b/caimira/tests/apps/calculator/test_model_generator.py index 0372eb82..bcd9e064 100644 --- a/caimira/tests/apps/calculator/test_model_generator.py +++ b/caimira/tests/apps/calculator/test_model_generator.py @@ -35,14 +35,14 @@ def test_model_from_dict_invalid(baseline_form_data, data_registry): ["Cloth"], ] ) -def test_blend_expiration(mask_type): +def test_blend_expiration(data_registry, mask_type): SAMPLE_SIZE = 250000 TOLERANCE = 0.02 blend = {'Breathing': 2, 'Speaking': 1} - r = model_generator.build_expiration(blend).build_model(SAMPLE_SIZE) + r = model_generator.build_expiration(data_registry, blend).build_model(SAMPLE_SIZE) mask = models.Mask.types[mask_type] - expected = (expiration_distributions['Breathing'].build_model(SAMPLE_SIZE).aerosols(mask).mean()*2/3. + - expiration_distributions['Speaking'].build_model(SAMPLE_SIZE).aerosols(mask).mean()/3.) + expected = (expiration_distributions(data_registry)['Breathing'].build_model(SAMPLE_SIZE).aerosols(mask).mean()*2/3. + + expiration_distributions(data_registry)['Speaking'].build_model(SAMPLE_SIZE).aerosols(mask).mean()/3.) npt.assert_allclose(r.aerosols(mask).mean(), expected, rtol=TOLERANCE) @@ -555,6 +555,8 @@ def test_default_types(): raise TypeError(f'{field} has type {field_type}, got {type(value)}') for field in fields.values(): + if field.name == "data_registry": + continue # Skip the assertion for the "data_registry" field assert field.name in model_generator.VirusFormData._DEFAULTS, f"No default set for field name {field.name}" diff --git a/caimira/tests/conftest.py b/caimira/tests/conftest.py index f2965b6c..4142ff79 100644 --- a/caimira/tests/conftest.py +++ b/caimira/tests/conftest.py @@ -60,7 +60,7 @@ def baseline_exposure_model(data_registry, baseline_concentration_model, baselin @pytest.fixture -def exposure_model_w_outside_temp_changes(baseline_exposure_model: models.ExposureModel): +def exposure_model_w_outside_temp_changes(data_registry, baseline_exposure_model: models.ExposureModel): exp_model = caimira.dataclass_utils.nested_replace( baseline_exposure_model, { 'concentration_model.ventilation': models.SlidingWindow( diff --git a/caimira/tests/models/test_fitting_algorithm.py b/caimira/tests/models/test_fitting_algorithm.py index c4bafa4d..65b6f447 100644 --- a/caimira/tests/models/test_fitting_algorithm.py +++ b/caimira/tests/models/test_fitting_algorithm.py @@ -39,6 +39,7 @@ def test_fitting_algorithm(data_registry, activity_type, ventilation_active, air # Generate CO2DataModel data_model = models.CO2DataModel( + data_registry=data_registry, room_volume=75, number=models.IntPiecewiseConstant(transition_times=tuple( [8, 12, 13, 17]), values=tuple([2, 1, 2])), diff --git a/caimira/tests/test_conditional_probability.py b/caimira/tests/test_conditional_probability.py index 6e0156d3..0e616ae7 100644 --- a/caimira/tests/test_conditional_probability.py +++ b/caimira/tests/test_conditional_probability.py @@ -47,7 +47,7 @@ def baseline_exposure_model(data_registry): @retry(tries=3) -def test_conditional_prob_inf_given_vl_dist(baseline_exposure_model): +def test_conditional_prob_inf_given_vl_dist(data_registry, baseline_exposure_model): viral_loads = np.array([3., 5., 7., 9.,]) mc_model: models.ExposureModel = baseline_exposure_model.build_model(2_000_000) @@ -72,7 +72,7 @@ def test_conditional_prob_inf_given_vl_dist(baseline_exposure_model): specific_vl = np.log10(mc_model.concentration_model.infected.virus.viral_load_in_sputum) step = 8/100 actual_pi_means, actual_lower_percentiles, actual_upper_percentiles = ( - report_generator.conditional_prob_inf_given_vl_dist(infection_probability, viral_loads, specific_vl, step) + report_generator.conditional_prob_inf_given_vl_dist(data_registry, infection_probability, viral_loads, specific_vl, step) ) assert np.allclose(actual_pi_means, expected_pi_means, atol=0.002) diff --git a/caimira/tests/test_monte_carlo.py b/caimira/tests/test_monte_carlo.py index 98d91824..656450ea 100644 --- a/caimira/tests/test_monte_carlo.py +++ b/caimira/tests/test_monte_carlo.py @@ -69,8 +69,9 @@ def baseline_mc_sr_model() -> caimira.monte_carlo.ShortRangeModel: @pytest.fixture -def baseline_mc_exposure_model(baseline_mc_concentration_model, baseline_mc_sr_model) -> caimira.monte_carlo.ExposureModel: +def baseline_mc_exposure_model(data_registry, baseline_mc_concentration_model, baseline_mc_sr_model) -> caimira.monte_carlo.ExposureModel: return caimira.monte_carlo.ExposureModel( + data_registry, baseline_mc_concentration_model, baseline_mc_sr_model, exposed=caimira.models.Population( diff --git a/caimira/tests/test_monte_carlo_full_models.py b/caimira/tests/test_monte_carlo_full_models.py index 1c329bad..f5b6bc74 100644 --- a/caimira/tests/test_monte_carlo_full_models.py +++ b/caimira/tests/test_monte_carlo_full_models.py @@ -186,8 +186,8 @@ def skagit_chorale_mc(data_registry): presence=models.SpecificInterval(((0, 2.5), )), virus=mc.SARSCoV2( viral_load_in_sputum=10**9, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, + infectious_dose=infectious_dose_distribution(data_registry), + viable_to_RNA_ratio=viable_to_RNA_ratio_distribution(data_registry), transmissibility_factor=1., ), mask=models.Mask.types['No mask'], @@ -230,8 +230,8 @@ def bus_ride_mc(data_registry): presence=models.SpecificInterval(((0, 1.67), )), virus=mc.SARSCoV2( viral_load_in_sputum=5*10**8, - infectious_dose=infectious_dose_distribution, - viable_to_RNA_ratio=viable_to_RNA_ratio_distribution, + infectious_dose=infectious_dose_distribution(data_registry), + viable_to_RNA_ratio=viable_to_RNA_ratio_distribution(data_registry), transmissibility_factor=1., ), mask=models.Mask.types['No mask'], @@ -411,6 +411,7 @@ def test_small_shared_office_Geneva(data_registry, mask_type, month, expected_pi evaporation_factor=0.3, ) exposure_mc = mc.ExposureModel( + data_registry=data_registry, concentration_model=concentration_mc, short_range=(), exposed=mc.Population( diff --git a/caimira/tests/test_predefined_distributions.py b/caimira/tests/test_predefined_distributions.py index 1ed8d240..b75e4ce5 100644 --- a/caimira/tests/test_predefined_distributions.py +++ b/caimira/tests/test_predefined_distributions.py @@ -39,7 +39,7 @@ def test_activity_distributions(data_registry, distribution, mean, std): ['SARS_CoV_2_GAMMA', 6.22, 1.80], ] ) -def test_viral_load_logdistribution(distribution, mean, std): +def test_viral_load_logdistribution(data_registry, distribution, mean, std): virus = virus_distributions(data_registry)[distribution].build_model(size=1000000) npt.assert_allclose(np.log10(virus.viral_load_in_sputum).mean(), mean, atol=0.01) npt.assert_allclose(np.log10(virus.viral_load_in_sputum).std(), std, atol=0.01) From 00ff1af71d6dfe1385d2606fdbbc9505153e97aa Mon Sep 17 00:00:00 2001 From: Nicola Tarocco Date: Tue, 12 Dec 2023 19:04:21 +0100 Subject: [PATCH 5/7] fix enums for data service --- caimira/apps/calculator/__main__.py | 13 ++++++- caimira/apps/calculator/report_generator.py | 5 ++- caimira/monte_carlo/data.py | 26 ++++++++++--- caimira/store/data_registry.py | 42 ++++++++++----------- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/caimira/apps/calculator/__main__.py b/caimira/apps/calculator/__main__.py index ab147c7d..6bf16d30 100644 --- a/caimira/apps/calculator/__main__.py +++ b/caimira/apps/calculator/__main__.py @@ -35,12 +35,23 @@ def configure_parser(parser) -> argparse.ArgumentParser: return parser +def _init_logging(debug=False): + # Set the logging level for urllib3 and requests to WARNING + logging.getLogger("urllib3").setLevel(logging.WARNING) + logging.getLogger("requests").setLevel(logging.WARNING) + + # set app root log level + logger = logging.getLogger() + root_log_level = logging.DEBUG if debug else logging.WARNING + logger.setLevel(root_log_level) + + def main(): parser = configure_parser(argparse.ArgumentParser()) args = parser.parse_args() debug = args.no_debug - logging.getLogger().setLevel(logging.DEBUG if debug else logging.WARNING) + _init_logging(debug) theme_dir = args.theme if theme_dir is not None: diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 4a4bac12..f4ab702d 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -220,7 +220,7 @@ def manufacture_conditional_probability_data( infection_probability: models._VectorisedFloat ): data_registry: DataRegistry = exposure_model.data_registry - + min_vl = data_registry.conditional_prob_inf_given_viral_load['min_vl'] max_vl = data_registry.conditional_prob_inf_given_viral_load['max_vl'] step = (max_vl - min_vl)/100 @@ -502,11 +502,12 @@ def prepare_context( now = datetime.utcnow().astimezone() time = now.strftime("%Y-%m-%d %H:%M:%S UTC") + data_registry_version = f"v{model.data_registry.version}" if model.data_registry.version else None context = { 'model': model, 'form': form, 'creation_date': time, - 'data_registry_version': model.data_registry.version, + 'data_registry_version': data_registry_version, } scenario_sample_times = interesting_times(model) diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index 1a403084..eaf1496e 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -15,24 +15,40 @@ def evaluate_vl(value, data_registry: DataRegistry): - if value == ViralLoads.COVID_OVERALL: + # FIXME: temp fix until data service values is updated + # remove old Ref value + if "covid_overal_vl_data" in value: + value = ViralLoads.COVID_OVERALL.value + elif "symptomatic_vl_frequencies" in value: + value = ViralLoads.SYMPTOMATIC_FREQUENCIES.value + + if value == ViralLoads.COVID_OVERALL.value: return covid_overal_vl_data(data_registry) - elif value == ViralLoads.COVID_OVERALL: + elif value == ViralLoads.SYMPTOMATIC_FREQUENCIES.value: return symptomatic_vl_frequencies else: raise ValueError(f"Invalid ViralLoads value {value}") def evaluate_infectd(value, data_registry: DataRegistry): - if value == InfectiousDoses.DISTRIBUTION: + # FIXME: temp fix until data service values is updated + # remove old Ref value + if "infectious_dose_distribution" in value: + value = InfectiousDoses.DISTRIBUTION.value + + if value == InfectiousDoses.DISTRIBUTION.value: return infectious_dose_distribution(data_registry) else: raise ValueError(f"Invalid InfectiousDoses value {value}") def evaluate_vtrr(value, data_registry: DataRegistry): - """.""" - if value == ViableToRNARatios.DISTRIBUTION: + # FIXME: temp fix until data service values is updated + # remove old Ref value + if "viable_to_RNA_ratio_distribution" in value: + value = ViableToRNARatios.DISTRIBUTION.value + + if value == ViableToRNARatios.DISTRIBUTION.value: return viable_to_RNA_ratio_distribution(data_registry) else: raise ValueError(f"Invalid ViableToRNARatios value {value}") diff --git a/caimira/store/data_registry.py b/caimira/store/data_registry.py index e0c0cfb8..ab9d74ab 100644 --- a/caimira/store/data_registry.py +++ b/caimira/store/data_registry.py @@ -210,51 +210,51 @@ class DataRegistry: } virus_distributions = { "SARS_CoV_2": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 1, "infectiousness_days": 14, }, "SARS_CoV_2_ALPHA": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.78, "infectiousness_days": 14, }, "SARS_CoV_2_BETA": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.8, "infectiousness_days": 14, }, "SARS_CoV_2_GAMMA": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.72, "infectiousness_days": 14, }, "SARS_CoV_2_DELTA": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.51, "infectiousness_days": 14, }, "SARS_CoV_2_OMICRON": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.2, "infectiousness_days": 14, }, "SARS_CoV_2_Other": { - "viral_load_in_sputum": ViralLoads.COVID_OVERALL, - "infectious_dose": InfectiousDoses.DISTRIBUTION, - "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION, + "viral_load_in_sputum": ViralLoads.COVID_OVERALL.value, + "infectious_dose": InfectiousDoses.DISTRIBUTION.value, + "viable_to_RNA_ratio": ViableToRNARatios.DISTRIBUTION.value, "transmissibility_factor": 0.1, "infectiousness_days": 14, }, From d3daca23d3693180fcf60214d04d681a881e5667 Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 14 Dec 2023 15:44:39 +0100 Subject: [PATCH 6/7] updated enum values and removed fixme references --- caimira/enums.py | 8 ++++---- caimira/monte_carlo/data.py | 17 ----------------- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/caimira/enums.py b/caimira/enums.py index 8b5418bd..6a776e2c 100644 --- a/caimira/enums.py +++ b/caimira/enums.py @@ -1,13 +1,13 @@ from enum import Enum class ViralLoads(Enum): - COVID_OVERALL = "COVID overall" - SYMPTOMATIC_FREQUENCIES = "Symptomatic frequencies" + COVID_OVERALL = "Ref: Viral load - covid_overal_vl_data" + SYMPTOMATIC_FREQUENCIES = "Ref: Viral load - symptomatic_vl_frequencies" class InfectiousDoses(Enum): - DISTRIBUTION = "Distribution" + DISTRIBUTION = "Ref: Infectious dose - infectious_dose_distribution" class ViableToRNARatios(Enum): - DISTRIBUTION = "Distribution" + DISTRIBUTION = "Ref: Viable to RNA ratio - viable_to_RNA_ratio_distribution" diff --git a/caimira/monte_carlo/data.py b/caimira/monte_carlo/data.py index eaf1496e..e4cc7e57 100644 --- a/caimira/monte_carlo/data.py +++ b/caimira/monte_carlo/data.py @@ -15,13 +15,6 @@ def evaluate_vl(value, data_registry: DataRegistry): - # FIXME: temp fix until data service values is updated - # remove old Ref value - if "covid_overal_vl_data" in value: - value = ViralLoads.COVID_OVERALL.value - elif "symptomatic_vl_frequencies" in value: - value = ViralLoads.SYMPTOMATIC_FREQUENCIES.value - if value == ViralLoads.COVID_OVERALL.value: return covid_overal_vl_data(data_registry) elif value == ViralLoads.SYMPTOMATIC_FREQUENCIES.value: @@ -31,11 +24,6 @@ def evaluate_vl(value, data_registry: DataRegistry): def evaluate_infectd(value, data_registry: DataRegistry): - # FIXME: temp fix until data service values is updated - # remove old Ref value - if "infectious_dose_distribution" in value: - value = InfectiousDoses.DISTRIBUTION.value - if value == InfectiousDoses.DISTRIBUTION.value: return infectious_dose_distribution(data_registry) else: @@ -43,11 +31,6 @@ def evaluate_infectd(value, data_registry: DataRegistry): def evaluate_vtrr(value, data_registry: DataRegistry): - # FIXME: temp fix until data service values is updated - # remove old Ref value - if "viable_to_RNA_ratio_distribution" in value: - value = ViableToRNARatios.DISTRIBUTION.value - if value == ViableToRNARatios.DISTRIBUTION.value: return viable_to_RNA_ratio_distribution(data_registry) else: From ddecd91a8592e3c21bfe51dd3373f67f02d34abd Mon Sep 17 00:00:00 2001 From: Luis Aleixo Date: Thu, 14 Dec 2023 15:45:21 +0100 Subject: [PATCH 7/7] increased 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 9004f940..842f2de6 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -41,7 +41,7 @@ # 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.14.2" +__version__ = "4.14.3" LOG = logging.getLogger("APP")