diff --git a/config_files/main.toml b/config_files/main.toml index 74140dc..e9252bc 100644 --- a/config_files/main.toml +++ b/config_files/main.toml @@ -181,3 +181,6 @@ protocol_compliance_lockdown_length_reduction = 0.5 Pfizer = 0.913 Moderna = 0.941 AZ = 0.76 + +[logging_info] +level = "INFO" diff --git a/cv19/logger/__init__.py b/cv19/logger/__init__.py new file mode 100644 index 0000000..f4b5ed1 --- /dev/null +++ b/cv19/logger/__init__.py @@ -0,0 +1,3 @@ +from .logger import Logger + +__all__ = ['Logger'] diff --git a/cv19/logger/logger.py b/cv19/logger/logger.py new file mode 100644 index 0000000..e7cc50d --- /dev/null +++ b/cv19/logger/logger.py @@ -0,0 +1,81 @@ +import logging +import sys +from logging import handlers + + +class Logger(object): + """ + Singleton Logger class. This class is only instantiated ONCE. It is to keep a consistent + criteria for the logger throughout the application if need be called upon. + It serves as the criteria for initiating logger for modules. It creates child loggers. + It's important to note these are child loggers as any changes made to the root logger + can be done. + """ + + _instance = None + + def __new__(cls): + """Instantiates the logger or returns same instance. + Returns: + logging handler object : the log file + """ + if cls._instance is None: + cls._instance = super().__new__(cls) + cls.debug_mode = True + cls.formatter = logging.Formatter( + "%(asctime)s — %(name)s — %(levelname)s — %(message)s" + ) + cls.log_file = "log_file.log" + + return cls._instance + + def get_console_handler(self): + """Defines a console handler to come out on the console. + Returns: + logging handler object : the console handler + """ + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(self.formatter) + console_handler.name = "consoleHandler" + return console_handler + + def get_file_handler(self): + """Defines a file handler to come out on the console. + Returns: + logging handler object : the console handler + """ + file_handler = handlers.RotatingFileHandler( + self.log_file, backupCount=1 + ) + file_handler.setFormatter(self.formatter) + file_handler.name = "fileHandler" + return file_handler + + def add_handlers(self, logger, handler_list: list): + """Adds handlers to the logger, checks first if handlers exist to avoid + duplication + Args: + logger: Logger to check handlers + handler_list: list of handlers to add + """ + existing_handler_names = [] + for existing_handler in logger.handlers: + existing_handler_names.append(existing_handler.name) + + for new_handler in handler_list: + if new_handler.name not in existing_handler_names: + logger.addHandler(new_handler) + + def get_logger(self, logger_name: str): + """Generates logger for use in the modules. + Args: + logger_name (string): name of the logger + Returns: + logger: returns logger for module + """ + logger = logging.getLogger(logger_name) + console_handler = self.get_console_handler() + file_handler = self.get_file_handler() + self.add_handlers(logger, [console_handler, file_handler]) + logger.propagate = False + return logger diff --git a/cv19/parallel.py b/cv19/parallel.py index b2a5dee..9805430 100644 --- a/cv19/parallel.py +++ b/cv19/parallel.py @@ -11,7 +11,7 @@ from .simulation import Simulation -def async_simulation(config_file, config_dir="", config_override_data=None, verbose=False): +def async_simulation(config_file, config_dir="", config_override_data=None): """Does a single run of the simulation with the supplied configuration details. Parameters @@ -23,8 +23,6 @@ def async_simulation(config_file, config_dir="", config_override_data=None, verb config_override_data : dict of dict Dictionary containing instances of configuration files that are used to override the default parameters loaded. - verbose : bool, default False - Whether to output information from each day of the simulation. Returns ------- @@ -33,14 +31,13 @@ def async_simulation(config_file, config_dir="", config_override_data=None, verb """ sim = Simulation(config_file=config_file, config_dir=config_dir, - config_override_data=config_override_data, verbose=verbose) + config_override_data=config_override_data) sim.run() return sim.get_arrays() -def run_async(num_runs, config_file, save_name=None, num_cores=-1, config_dir="", config_override_data=None, - verbose=False): +def run_async(num_runs, config_file, save_name=None, num_cores=-1, config_dir="", config_override_data=None): """Runs multiple simulations in parallel using the supplied configuration settings. Parameters @@ -58,8 +55,6 @@ def run_async(num_runs, config_file, save_name=None, num_cores=-1, config_dir="" A dictionary of configuration file instances that can be used to override the files specified in the main configuration file. Designed to allow tabular mode to edit parameters in configuration files other than main. - verbose : bool, default False - Whether to output information from each day of the simulation. Returns ------- @@ -73,7 +68,8 @@ def run_async(num_runs, config_file, save_name=None, num_cores=-1, config_dir="" # Run all of the simulations multiprocessing.freeze_support() with multiprocessing.Pool(processes=num_cores) as pool: - results = pool.starmap(async_simulation, ((config_file, config_dir, config_override_data, verbose) + + results = pool.starmap(async_simulation, ((config_file, config_dir, config_override_data) for _ in range(num_runs))) df = pd.DataFrame(results) @@ -137,7 +133,7 @@ def _config_editor(main_config, disease_config, param_name, value): raise ValueError(f"The supplied param_name {param_name} is not in any configuration file") -def tabular_mode(base_config_file, independent, dependent, num_runs=8, num_cores=8, save_name=None, verbose=False): +def tabular_mode(base_config_file, independent, dependent, num_runs=8, num_cores=8, save_name=None): """Automatically measures the impact of various public health measures on different metrics. Parameters @@ -174,8 +170,6 @@ def tabular_mode(base_config_file, independent, dependent, num_runs=8, num_cores If using a list, then the list must be exactly as long as the number of values for the independent variable, and each scenario will be saved under its corresponding filename. If None, then don't save any results. - verbose : bool, default False - Whether to output information from each day of the simulation. Returns ------- @@ -236,8 +230,9 @@ def tabular_mode(base_config_file, independent, dependent, num_runs=8, num_cores scenario_save_name = save_name[i] elif isinstance(save_name, str): scenario_save_name = save_name + f"{i:02}" + data = run_async(num_runs, temp_main_config, num_cores=num_cores, - save_name=scenario_save_name, config_dir=config_dir, verbose=verbose, + save_name=scenario_save_name, config_dir=config_dir, config_override_data=config_override_data) # Processing the results to get the dependent measurements, add to results @@ -269,7 +264,7 @@ def tabular_mode(base_config_file, independent, dependent, num_runs=8, num_cores return results -def confidence_interval(config, parameterstoplot, num_runs=8, confidence=0.80, num_cores=-1, save_name=None, verbose=False): +def confidence_interval(config, parameterstoplot, num_runs=8, confidence=0.80, num_cores=-1, save_name=None): """Plots the results of multiple simulations with confidence bands to give a better understanding of the trend of a given scenario. Displays a plot of the results. @@ -292,11 +287,9 @@ def confidence_interval(config, parameterstoplot, num_runs=8, confidence=0.80, n save_name: str or None, default None Name to save the results under. Default None, which means don't save the results. - verbose : bool, default False - Whether to output information from each day of the simulation. """ - result = run_async(num_runs, config, num_cores=num_cores, save_name=save_name, verbose=verbose) + result = run_async(num_runs, config, num_cores=num_cores, save_name=save_name) fig_ci, ax_ci = plt.subplots() z_score = st.norm.ppf(confidence) diff --git a/cv19/simulation.py b/cv19/simulation.py index fa94252..473e0e8 100644 --- a/cv19/simulation.py +++ b/cv19/simulation.py @@ -2,6 +2,7 @@ import subprocess from timeit import default_timer as timer from pathlib import Path +import logging import tomli import numpy as np @@ -12,6 +13,7 @@ from .population import Population from .policy import Policy from .interaction_sites import InteractionSites +from .logger import Logger class Simulation(): @@ -23,8 +25,6 @@ class Simulation(): Attributes ---------- - verbose : bool - A variable indicating whether to print updates with simulation information while running. tracking_df : pd.DataFrame A pandas DataFrame object that stores all the tracking arrays for a given simulation. Each array is of length nDays. @@ -36,7 +36,7 @@ class Simulation(): A variable indicating if this object has run a simulaiton yet. """ - def __init__(self, config_file, config_dir="", config_override_data=None, verbose=False): + def __init__(self, config_file, config_dir="", config_override_data=None): """ __init__ method docstring. Parameters @@ -50,8 +50,6 @@ def __init__(self, config_file, config_dir="", config_override_data=None, verbos A dictionary of configuration file instances that can be used to override the files specified in the main configuration file. Designed to allow tabular mode to edit parameters in configuration files other than main. - verbose : bool - A variable indicating whether to print updates with simulation information while running. """ self.config_dir = config_dir @@ -60,8 +58,6 @@ def __init__(self, config_file, config_dir="", config_override_data=None, verbos self.init_classes() # Have to initalize the classes after we have all of the parameters - self.verbose = verbose # Whether or not to print daily simulation information. - self.set_code_version() # Set the version of the code being used to run simulation. self.make_tracking_df() @@ -272,6 +268,20 @@ def run(self, fail_on_rerun=True): run multiple times. """ + log_level_info = {"DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR} + log_level_config = self.parameters["logging_info"]["level"] + log_level = log_level_info.get(log_level_config) + + # Starts logger for file + log = Logger().get_logger(__name__) + logging.root.setLevel(log_level) + logging.captureWarnings(True) + log.info(" %s", '-' * 70) + log.info("Starting simulation...") + # Check whether the simulation has already been run. if fail_on_rerun: information = ("When running again, previous results will be overwritten. " @@ -280,8 +290,7 @@ def run(self, fail_on_rerun=True): "fail_on_rerun argument to False.") self.check_has_run(check=False, information=information, fail=True) - if self.verbose: - print(f"Simulation code version (from git): {self.code_id}\n") + log.info("Simulation code version (from git): %s", self.code_id) # Get current time for measuring elapsed time of simulation. beg_time = timer() @@ -304,23 +313,23 @@ def run(self, fail_on_rerun=True): # UPDATE POLICY mask_mandate = self.policy.update_mask_mandate(day=day) - if mask_mandate != old_mask_mandate and self.verbose: - print(f"Day: {day}, Mask Mandate: {mask_mandate}") + if mask_mandate != old_mask_mandate: + log.info("Day: %i, Mask Mandate: %i", day, mask_mandate) old_mask_mandate = mask_mandate lockdown = self.policy.update_lockdown(day=day) - if lockdown != old_lockdown_mandate and self.verbose: - print(f"Day: {day}, Lockdown: {lockdown}") + if lockdown != old_lockdown_mandate: + log.info("Day: %i, Lockdown: %i", day, lockdown) old_lockdown_mandate = lockdown testing_ON = self.policy.update_testing(day) - if testing_ON != old_testing_mandate and self.verbose: - print(f"Day: {day}, Testing: {testing_ON}") + if testing_ON != old_testing_mandate: + log.info("Day: %i, Testing: %i", day, testing_ON) old_testing_mandate = testing_ON students_go = self.policy.check_students(day=day) - if students_go != old_student_mandate and self.verbose: - print(f"Day: {day}, Uni Mandate: {students_go}") + if students_go != old_student_mandate: + log.info("Day: %i, Uni Mandate: %i", day, students_go) old_student_mandate = students_go # infect random students on the day they come in @@ -406,45 +415,31 @@ def run(self, fail_on_rerun=True): self.tracking_df.at[day, "time"] = timer() - beg_time - if self.verbose: - print((f"Day: {day}, " - f"infected: {self.tracking_df.at[day, 'infected']}, " - f"recovered: {self.tracking_df.at[day, 'recovered']}, " - f"susceptible: {self.tracking_df.at[day, 'susceptible']}, " - f"dead: {self.tracking_df.at[day, 'dead']}, " - f"hospitalized: {self.tracking_df.at[day, 'hospitalized']}, " - f"ICU: {self.tracking_df.at[day, 'ICU']}, " - f"tested: {self.tracking_df.at[day, 'tested']}, " - f"total quarantined: {self.tracking_df.at[day, 'quarantined']}, " - f"infected students: {self.tracking_df.at[day, 'inf_students']}, " - f"vaccinated: {self.tracking_df.at[day, 'vaccinated']}")) - - # Print variants - print("Variants", end=": ") - for key, val in self.track_virus_types.items(): - print(f"{key}:{val[day]}", end=", ") - print("\n") - - if self.verbose: - time_seconds = timer() - beg_time - m, s = divmod(time_seconds, 60) - h, m = divmod(m, 60) - print(f"{'':-<80}") - print("Simulation summary:") - print(f" Time elapsed: {h:02.0f}:{m:02.0f}:{s:02.0f}") - print(f" {self.tracking_df['susceptible'].iloc[-1]} never got it") - print(f" {self.tracking_df['dead'].iloc[-1]} died") - print(f" {self.tracking_df['infected'].max()} had it at the peak") - print(f" {self.tracking_df.at[day, 'tested']} were tested") - print(f" {self.tracking_df['quarantined'].max()} were in quarantine at the peak") - print(f" {self.tracking_df['hospitalized'].max()} at peak hospitalizations") - print(f" {self.tracking_df['dead'].max()} at peak deaths") - print(" The breakdown of the variants is", end=": ") - for key, val in self.track_virus_types.items(): - print(f"{key}-{np.max(val)}", end=", ") - print("") - print(f" {self.tracking_df.at[day, 'vaccinated']} people were vaccinated") - print(f" {self.tracking_df.at[day, 'vaccinated']/self.nPop*100:.2f}% of population was vaccinated.") + log.debug("Day: %i, infected: %i, recovered: %i, susceptible: %i, dead: %i, hospitalized: %i, ICU: %i, tested: %i, total quarantined: %i, infected students: %i, vaccinated: %i", + day, self.tracking_df.at[day, 'infected'], self.tracking_df.at[day, 'recovered'], + self.tracking_df.at[day, 'susceptible'], self.tracking_df.at[day, 'dead'], + self.tracking_df.at[day, 'hospitalized'], self.tracking_df.at[day, 'ICU'], + self.tracking_df.at[day, 'tested'], self.tracking_df.at[day, 'quarantined'], + self.tracking_df.at[day, 'inf_students'], self.tracking_df.at[day, 'vaccinated']) + + time_seconds = timer() - beg_time + m, s = divmod(time_seconds, 60) + h, m = divmod(m, 60) + + log.info("Simulation summary:") + log.info(" Time elapsed : %02.0f:%02.0f:%02.0f", h, m, s) + log.info(" %i never got it", self.tracking_df['susceptible'].iloc[-1]) + log.info(" %i died", self.tracking_df['dead'].iloc[-1]) + log.info(" %i had it at the peak", self.tracking_df['infected'].max()) + log.info(" %i were tested", self.tracking_df.at[day, 'tested']) + log.info(" %i were in quarantine at the peak", self.tracking_df['quarantined'].max()) + log.info(" %i at peak hospitalizations", self.tracking_df['hospitalized'].max()) + log.info(" %i at peak deaths", self.tracking_df['dead'].max()) + log.info("The breakdown of the variants is:") + for key, val in self.track_virus_types.items(): + log.info(" %s - %i", key, np.max(val)) + log.info(" %i people were vaccinated", self.tracking_df.at[day, 'vaccinated']) + log.info(" %0.2f percent of population was vaccinated.", self.tracking_df.at[day, 'vaccinated'] / self.nPop * 100) # Unpack the virus types into the dataframe for virus_type, virus_type_arr in self.track_virus_types.items(): diff --git a/test/test_Simulation.py b/test/test_Simulation.py index 8158f7b..42d7db3 100755 --- a/test/test_Simulation.py +++ b/test/test_Simulation.py @@ -26,11 +26,11 @@ def setUp(self): simulation objects used for testing. """ quarantine_config_file_1 = str(Path(Path(__file__).parent, "testing_config_files/main_quarantine_1.toml").resolve()) - self.quarantine_obj_1 = Simulation(quarantine_config_file_1, verbose=False) + self.quarantine_obj_1 = Simulation(quarantine_config_file_1) self.quarantine_obj_1.run() quarantine_config_file_2 = str(Path(Path(__file__).parent, "testing_config_files/main_quarantine_2.toml").resolve()) - self.quarantine_obj_2 = Simulation(quarantine_config_file_2, verbose=False) + self.quarantine_obj_2 = Simulation(quarantine_config_file_2) self.quarantine_obj_2.run() def tearDown(self): diff --git a/test/testing_config_files/main_quarantine_1.toml b/test/testing_config_files/main_quarantine_1.toml index 06ea9a9..3254c02 100644 --- a/test/testing_config_files/main_quarantine_1.toml +++ b/test/testing_config_files/main_quarantine_1.toml @@ -181,3 +181,6 @@ protocol_compliance_lockdown_length_reduction = 0.5 Pfizer = 0.087 Moderna = 0.059 AZ = 0.24 + +[logging_info] +level = "INFO" diff --git a/test/testing_config_files/main_quarantine_2.toml b/test/testing_config_files/main_quarantine_2.toml index e627a21..57f025a 100644 --- a/test/testing_config_files/main_quarantine_2.toml +++ b/test/testing_config_files/main_quarantine_2.toml @@ -181,3 +181,6 @@ protocol_compliance_lockdown_length_reduction = 0.5 Pfizer = 0.087 Moderna = 0.059 AZ = 0.24 + +[logging_info] +level = "INFO"