From 3668e8409fb2cefd6382d8e5876f4c31e1375136 Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Fri, 15 May 2026 16:51:47 +0200 Subject: [PATCH 1/6] impl new config --- micro_manager/adaptivity/adaptivity.py | 20 +- .../adaptivity/adaptivity_selection.py | 2 +- micro_manager/adaptivity/global_adaptivity.py | 6 +- micro_manager/adaptivity/local_adaptivity.py | 6 +- micro_manager/adaptivity/model_adaptivity.py | 10 +- micro_manager/config.py | 1546 +++++++++-------- micro_manager/domain_decomposition.py | 8 +- micro_manager/load_balancing.py | 10 +- micro_manager/micro_manager.py | 64 +- micro_manager/snapshot/snapshot.py | 20 +- tests/unit/test_adaptivity_parallel.py | 18 +- tests/unit/test_adaptivity_serial.py | 42 +- tests/unit/test_domain_decomposition.py | 62 +- tests/unit/test_load_balancing.py | 40 +- tests/unit/test_micro_manager.py | 26 +- tests/unit/test_snapshot_computation.py | 12 +- 16 files changed, 1038 insertions(+), 854 deletions(-) diff --git a/micro_manager/adaptivity/adaptivity.py b/micro_manager/adaptivity/adaptivity.py index edfb8897..46b759f6 100644 --- a/micro_manager/adaptivity/adaptivity.py +++ b/micro_manager/adaptivity/adaptivity.py @@ -15,7 +15,7 @@ class AdaptivityCalculator: def __init__( self, - configurator: Config, + config: Config, nsims: int, micro_problem_cls: MicroSimulationClass, model_manager: ModelManager, @@ -27,7 +27,7 @@ def __init__( Parameters ---------- - configurator : object of class Config + config : object of class Config Object which has getter functions to get parameters defined in the configuration file. nsims : int Number of micro simulations. @@ -40,12 +40,12 @@ def __init__( rank : int Rank of the MPI communicator. """ - self._refine_const = configurator.get_adaptivity_refining_const() - self._coarse_const = configurator.get_adaptivity_coarsening_const() - self._hist_param = configurator.get_adaptivity_hist_param() - self._adaptivity_data_names = configurator.get_data_for_adaptivity() - self._adaptivity_type = configurator.get_adaptivity_type() - self._adaptivity_output_type = configurator.get_adaptivity_output_type() + self._refine_const = config.adaptivity_refining_constant() + self._coarse_const = config.adaptivity_coarsening_constant() + self._hist_param = config.adaptivity_history_param() + self._adaptivity_data_names = config.data_for_adaptivity() + self._adaptivity_type = config.adaptivity_type() + self._adaptivity_output_type = config.adaptivity_output_type() self._micro_problem_cls = micro_problem_cls self._model_manager = model_manager @@ -71,10 +71,10 @@ def __init__( self._just_deactivated: list[int] = [] self._similarity_measure = self._get_similarity_measure( - configurator.get_adaptivity_similarity_measure() + config.adaptivity_similarity_measure() ) - output_dir = configurator.get_output_dir() + output_dir = config.output_dir() if output_dir is not None: metrics_output_dir = output_dir + "/adaptivity-metrics" diff --git a/micro_manager/adaptivity/adaptivity_selection.py b/micro_manager/adaptivity/adaptivity_selection.py index a772483c..8b92a016 100644 --- a/micro_manager/adaptivity/adaptivity_selection.py +++ b/micro_manager/adaptivity/adaptivity_selection.py @@ -15,7 +15,7 @@ def create_adaptivity_calculator( micro_problem_cls, model_manager, ) -> AdaptivityCalculator: - adaptivity_type = config.get_adaptivity_type() + adaptivity_type = config.adaptivity_type() if adaptivity_type == "local": return LocalAdaptivityCalculator( diff --git a/micro_manager/adaptivity/global_adaptivity.py b/micro_manager/adaptivity/global_adaptivity.py index 0657443d..3e6a0c73 100644 --- a/micro_manager/adaptivity/global_adaptivity.py +++ b/micro_manager/adaptivity/global_adaptivity.py @@ -22,7 +22,7 @@ class GlobalAdaptivityCalculator(AdaptivityCalculator): def __init__( self, - configurator: Config, + config: Config, global_number_of_sims: int, global_ids: list, participant, @@ -37,7 +37,7 @@ def __init__( Parameters ---------- - configurator : object of class Config + config : object of class Config Object which has getter functions to get parameters defined in the configuration file. global_number_of_sims : int Total number of simulations in the macro-micro coupled problem. @@ -57,7 +57,7 @@ def __init__( Handles instantiation of the micro simulation. """ super().__init__( - configurator, + config, global_number_of_sims, micro_problem_cls, model_manager, diff --git a/micro_manager/adaptivity/local_adaptivity.py b/micro_manager/adaptivity/local_adaptivity.py index bbd9d614..1b626826 100644 --- a/micro_manager/adaptivity/local_adaptivity.py +++ b/micro_manager/adaptivity/local_adaptivity.py @@ -17,7 +17,7 @@ class LocalAdaptivityCalculator(AdaptivityCalculator): def __init__( self, - configurator: Config, + config: Config, num_sims: int, base_logger: Logger, rank: int, @@ -30,7 +30,7 @@ def __init__( Parameters ---------- - configurator : object of class Config + config : object of class Config Object which has getter functions to get parameters defined in the configuration file. num_sims : int Number of micro simulations. @@ -46,7 +46,7 @@ def __init__( Handles instantiation of micro simulation. """ super().__init__( - configurator, num_sims, micro_problem_cls, model_manager, base_logger, rank + config, num_sims, micro_problem_cls, model_manager, base_logger, rank ) self._comm = comm diff --git a/micro_manager/adaptivity/model_adaptivity.py b/micro_manager/adaptivity/model_adaptivity.py index 304a67dc..a31ddc27 100644 --- a/micro_manager/adaptivity/model_adaptivity.py +++ b/micro_manager/adaptivity/model_adaptivity.py @@ -23,7 +23,7 @@ class ModelAdaptivity: def __init__( self, model_manager: ModelManager, - configurator: Config, + config: Config, comm: MPI.Comm, rank: int, log_file: str, @@ -37,7 +37,7 @@ def __init__( ---------- model_manager: ModelManager ModelManager instance - configurator : object of class Config + config : object of class Config Object which has getter functions to get parameters defined in the configuration file. comm: MPI.Comm MPI communicator @@ -54,12 +54,12 @@ def __init__( self._comm = comm self._model_manager = model_manager - self._model_files = configurator.get_model_adaptivity_file_names() + self._model_files = config.model_adaptivity_file_names() self._switching_func_name = ( - configurator.get_model_adaptivity_switching_function() + config.model_adaptivity_switching_function() ) - stateless_flags = configurator.get_model_adaptivity_micro_stateless() + stateless_flags = config.model_adaptivity_micro_stateless() self._model_classes = [] pos = 0 for model_file in self._model_files: diff --git a/micro_manager/config.py b/micro_manager/config.py index 7d6b634b..648bb3c6 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -5,6 +5,379 @@ import json import os import importlib.metadata +import string +from collections import defaultdict +from typing import Optional, Type, List, Dict, Any, Callable +import inspect +from tools.logging_wrapper import Logger + + +class ConfigDSL: + """ + Provides a standardized context to read data from JSON. + Data retrieval can be set up to yield optionals, with default values or raise Errors. + """ + def __init__(self, data: Dict, log: Logger): + """ + Constructs a context for the JSON data. + + Parameters + ---------- + data : Dict + JSON data + log : Logger + Logging object + """ + self._data: Dict = data + self._log: Logger = log + self._access_queue: List[str] = [] + + def __getitem__(self, path: str): + """ + Adds an access request for the given path. + Used for nested JSON objects as well as element access. + + Parameters + ---------- + path : str + Path + + Returns + ------- + context : ConfigDSL + same object, for chained requests + """ + self._access_queue.append(path) + return self + + def exists(self): + """ + Checks if the requested path exists. + + Returns + ------- + exists : bool + True if exists, False otherwise + """ + current_element = self._data + try: + for key in self._access_queue: + current_element = current_element[key] + except BaseException as e: + return False + return True + + def get_or_none( + self, + fmt_success: Optional[str]=None, + fmt_error: Optional[str]=None, + dtype: Optional[Type]=None, + options: Optional[List]=None, + **kwargs + ) -> Optional[Any]: + """ + Resolves the value specified by the set path. Returns the result if available, otherwise None. + Format strings can be provided for success or failure. {data} and {ex} are reserved keywords. + {data} is the retrieved data, {ex} is the raised exception. + In addition, a target data type and a list of possible options can be specified. + + Parameters + ---------- + fmt_success : Optional[str] + Format string containing message on success. + fmt_error : Optional[str] + Format string containing message on error. + dtype: Optional[Type] + Target data type + options: Optional[List] + Options of which the retrieved value must be an element. + kwargs: ... + Keyword arguments that should be passed to the format strings. + + Returns + ------- + result: Optional[Any] + None if path not available or other failure, else value of JSON. + """ + return self.get_with_default(None, fmt_success, fmt_error, dtype, options, **kwargs) + + def get_with_default( + self, + default: Any, + fmt_success: Optional[str] = None, + fmt_error: Optional[str] = None, + dtype: Optional[Type] = None, + options: Optional[List] = None, + **kwargs + ) -> Any: + """ + Resolves the value specified by the set path. Returns the result if available, otherwise the specified default. + Format strings can be provided for success or failure. {data} and {ex} are reserved keywords. + {data} is the retrieved data, {ex} is the raised exception. + In addition, a target data type and a list of possible options can be specified. + + Parameters + ---------- + default : Any + Default value. + fmt_success : Optional[str] + Format string containing message on success. + fmt_error : Optional[str] + Format string containing message on error. + dtype: Optional[Type] + Target data type + options: Optional[List] + Options of which the retrieved value must be an element. + kwargs: ... + Keyword arguments that should be passed to the format strings. + + Returns + ------- + result: Any + Default value if path not available or other failure, else value of JSON. + """ + current_element = self._data + try: + for key in self._access_queue: + current_element = current_element[key] + + if dtype is not None and type(current_element) != dtype: + raise RuntimeError("Wrong data type") + if options is not None and current_element not in options: + raise RuntimeError("Wrong option") + except BaseException as e: + self.handle_fmt_error(fmt_error, e, kwargs) + return default + + self.handle_fmt_success(fmt_success, current_element, kwargs) + return current_element + + def get_or_raise( + self, + fmt_success: Optional[str] = None, + fmt_error: Optional[str] = None, + dtype: Optional[Type] = None, + options: Optional[List] = None, + **kwargs + ): + """ + Resolves the value specified by the set path. Returns the result if available, otherwise throws. + Format strings can be provided for success or failure. {data} and {ex} are reserved keywords. + {data} is the retrieved data, {ex} is the raised exception. + In addition, a target data type and a list of possible options can be specified. + + Parameters + ---------- + fmt_success : Optional[str] + Format string containing message on success. + fmt_error : Optional[str] + Format string containing message on error. + dtype: Optional[Type] + Target data type + options: Optional[List] + Options of which the retrieved value must be an element. + kwargs: ... + Keyword arguments that should be passed to the format strings. + + Returns + ------- + result: Any + Throws if path not available or other failure, else value of JSON. + """ + current_element = self._data + try: + for key in self._access_queue: + current_element = current_element[key] + + if dtype is not None and type(current_element) != dtype: + raise RuntimeError("Wrong data type") + if options is not None and current_element not in options: + raise RuntimeError("Wrong option") + except BaseException as e: + self.handle_fmt_error(fmt_error, e, kwargs) + raise e + + self.handle_fmt_success(fmt_success, current_element, kwargs) + return current_element + + def handle_fmt( + self, + fmt: Optional[str], + kwargs: Optional[Dict[str, Any]], + ) -> None: + """ + Processes the message and prints to the logger. + + Parameters + ---------- + fmt : Optional[str] + Format string. + kwargs: Optional[Dict[str, Any]] + Additional keyword arguments that should be passed to the format string. + """ + if fmt is None: + return + if kwargs is None: + kwargs = {} + + fmt_args = {} + for key, value in kwargs.items(): + has_key = ConfigDSL.fmt_check_key(fmt, key) + if has_key: + fmt_args[key] = value + + msg = fmt.format(**fmt_args) + self._log.log_info_rank_zero(msg) + + def handle_fmt_error( + self, + fmt_error: str, + ex: BaseException, + kwargs: Dict[str, Any] + ) -> None: + """ + Processes the error message and prints to the logger. + + Parameters + ---------- + fmt_error : str + Format string containing message on success. + ex : BaseException + Raised Exception. + kwargs: Dict[str, Any] + Additional keyword arguments that should be passed to the format string. + """ + kwargs["ex"] = ex + self.handle_fmt(fmt_error, kwargs) + + def handle_fmt_success( + self, + fmt_success: str, + data: Any, + kwargs: Dict[str, Any], + ) -> None: + """ + Processes the success message and prints to the logger. + + Parameters + ---------- + fmt_success : str + Format string containing message on success. + data : Any + Retrieved data. + kwargs: Dict[str, Any] + Additional keyword arguments that should be passed to the format string. + """ + kwargs["data"] = data + self.handle_fmt(fmt_success, kwargs) + + _formatter = string.Formatter() + @staticmethod + def fmt_check_key(fmt: str, key: str) -> bool: + """ + Checks if the provided format string contains the specified key. + + Parameters + ---------- + fmt : str + Format string to check. + key : str + Key of question. + + Returns + ------- + contains_key : bool + True if fmt contains the key, else False. + + """ + for _, field_name, _, _ in ConfigDSL._formatter.parse(fmt): + if field_name == key: + return True + return False + + +class ConfigEntryProxy: + """ + Enables the usage of the following syntax: + # DEFINITION + @config_entry + def member_name(self): ... + + # WRITE ACCESS + self.member_name.set = value + + # KEY ACCESS + self.member_name.key + + Hereby, config entries do not need to define individual backing fields and can instead use + the dict self._fields within the config object. Data is stored using the respective keys. + The additional write access should simplify writing to the dict. + + """ + def __init__(self, func: Callable): + self._func : Callable = func + self._instance : Optional[Config] = None + + def attach_instance(self, instance:"Config"): + self._instance = instance + + @property + def key(self) -> str: + """ + Gets the key of the config entry. + + Returns + ------- + key : str + Key of the config entry. + """ + return self._func.__name__ + + def __call__(self) -> Any: + """ + Gets the stored value for the config entry. + + Returns + ------- + value : Any + Value of the config entry. + """ + return self._instance._fields[self.key] + + def __setattr__(self, key: str, value: Any) -> None: + """ + Stores the provided value for the config entry, when attempting to write to the self.set member. + + Parameters + ---------- + key : str + Name of written to member. (provided by Python RT) + value : Any + Value of the config entry to be stored. + """ + if key in ["_func", "_instance"]: + super().__setattr__(key, value) + return + if key == "set": + self._instance._fields[self.key] = value + return + + +def config_entry(func: Callable) -> Callable: + """ + Decorator for Config entries. See ConfigEntryProxy for details. + + Parameters + ---------- + func : Callable + config entry method + + Returns + ------- + func : Callable + config entry method, enriched by ConfigEntryProxy. + """ + return ConfigEntryProxy(func) class Config: @@ -13,83 +386,27 @@ class Config: the config class in https://github.com/precice/fenics-adapter/tree/develop/fenicsadapter """ - def __init__(self, config_file_name): + def __init__(self, config_file_name: str): """ Constructor of the Config class. Parameters ---------- config_file_name : string - Name of the JSON configuration file - """ - self._config_file_name = config_file_name - self._logger = None - self._micro_file_name = None - self._micro_stateless = False - - self._precice_config_file_name = None - self._macro_mesh_name = None - self._read_data_names = None - self._write_data_names = None - self._micro_dt = None - - # Domain decomposition information - self._macro_domain_bounds = None - self._ranks_per_axis = None - self._decomposition_type = "uniform" - self._minimum_access_region_size: list = [] - - self._micro_output_n = 1 - self._diagnostics_data_names = None - - self._mem_usage_output_type = "" - self._mem_usage_output_n = 1 - - self._interpolate_crash = False - - self._adaptivity = False - self._adaptivity_type = "" - self._data_for_adaptivity = dict() - self._adaptivity_n = 1 - self._adaptivity_history_param = 0.5 - self._adaptivity_coarsening_constant = 0.5 - self._adaptivity_refining_constant = 0.5 - self._adaptivity_every_implicit_iteration = False - self._adaptivity_similarity_measure = "L2rel" - self._adaptivity_output_type = "" - self._adaptivity_output_n = 1 - - self._load_balancing = False - self._load_balancing_type = "time" - self._load_balancing_n = 1 - self._load_balancing_partitioning = "lpt" - self._load_balancing_threshold = 0 - self._load_balancing_balance_inactive_sims = False - - # Snapshot information - self._parameter_file_name = None - self._postprocessing_file_name = None - self._initialize_once = False - self._output_file_name = "snapshot_data" - - self._output_dir = None - - self._lazy_initialization = False - - # Model Adaptivity information - self._m_adap = False - self._m_adap_micro_file_names = None - self._m_adap_micro_stateless = None - self._m_adap_switching_function = None - - # Tasking - self._task_is_slurm = False - self._task_backend = "socket" - self._task_num_workers = 1 - self._task_mpi_impl = "open" - self._task_pinning_hostfile = "./hosts.micro" - - def set_logger(self, logger): + Path to the JSON configuration file + """ + self._config_file_name: str = config_file_name + self._base_dir: str = os.path.dirname(os.path.join(os.getcwd(), config_file_name)) + self._logger: Optional[Logger] = None + self._data: Optional[Dict[str, Any]] = None + self._fields: Dict[str, Any] = defaultdict(lambda: None) + + # attach external backing field to all config entries + config_entries = inspect.getmembers(self, lambda f: hasattr(f, "attach_instance")) + for _, e in config_entries: + e.attach_instance(self) + + def set_logger(self, logger: Logger): """ Set the logger for the Config class. @@ -100,135 +417,114 @@ def set_logger(self, logger): """ self._logger = logger - def _read_json(self, config_file_name): + @property + def json(self) -> ConfigDSL: + """ + Returns and Object to load JSON values dynamically. + """ + assert self._data is not None and self._logger is not None + return ConfigDSL(self._data, self._logger) + + def _read_json_base(self, config_file_name: str): """ Reads JSON configuration file. Parameters ---------- config_file_name : string - Name of the JSON configuration file + Path to the JSON configuration file """ - self._logger.log_info_rank_zero( - "Micro Manager version: " - + importlib.metadata.version("micro-manager-precice") - ) - - self._folder = os.path.dirname(os.path.join(os.getcwd(), config_file_name)) - path = os.path.join(self._folder, os.path.basename(config_file_name)) + assert self._logger is not None + self._logger.log_info_rank_zero(f"Micro Manager version: {importlib.metadata.version('micro-manager-precice')}") + path = os.path.join(self._base_dir, os.path.basename(config_file_name)) with open(path, "r") as read_file: self._data = json.load(read_file) - self._logger.log_info_rank_zero("Reading JSON configuration file: " + path) + self._logger.log_info_rank_zero(f"Reading JSON configuration file: {path}") + + # ====================================================== + # Micro Manager Base + # ====================================================== # convert paths to python-importable paths - self._micro_file_name = ( - self._data["micro_file_name"] - .replace("/", ".") - .replace("\\", ".") - .replace(".py", "") + self.micro_file_name.set = (self.json["micro_file_name"].get_or_raise( + "Micro simulation file name: {data}", + "'micro_file_name' must be specified!", + str + ).replace("/", ".") + .replace("\\", ".") + .replace(".py", "")) + + self.enable_micro_stateless.set = self.json["micro_stateless"].get_with_default( + False, + "Only creating one full instance of Micro Model.", + "Creating full instance of Micro Model per mesh vertex.", + bool ) - try: - self._micro_stateless = self._data["micro_stateless"] - self._logger.log_info_rank_zero( - "Only creating one full instance of MicroSimulation." - ) - except: - self._micro_stateless = False - self._logger.log_info_rank_zero( - "Creating an instance of MicroSimulation for each mesh vertex." - ) + self.output_dir.set = self.json["output_directory"].get_or_none( + "Logging and metrics output directory: {data}", + "No output directory provided. Output (including logging) will be saved in the current working directory." + ) - self._logger.log_info_rank_zero( - "Micro simulation file name: " + self._data["micro_file_name"] + self.memory_usage_output_type.set = self.json["memory_usage_output_type"].get_with_default( + "", + "Memory usage output type: {data}", + "Micro Manager will not output memory usage.", + options=["all", "local", "global"], ) - try: - self._output_dir = self._data["output_directory"] - self._logger.log_info_rank_zero( - "Logging and metrics output directory: " + self._output_dir - ) - except BaseException: - self._logger.log_info_rank_zero( - "No output directory provided. Output (including logging) will be saved in the current working directory." - ) + self.memory_usage_output_n.set = self.json["memory_usage_output_n"].get_with_default( + 1, + "Memory usage will be output every {data} time windows.", + "No output interval for memory usage output provided. Memory usage will be output every time window." + ) - try: - self._mem_usage_output_type = self._data["memory_usage_output_type"] - if self._mem_usage_output_type not in ["all", "local", "global"]: - raise Exception( - "Memory usage output can be either 'all', 'local' or 'global'." - ) - self._logger.log_info_rank_zero( - "Memory usage output type: " + self._mem_usage_output_type - ) - except BaseException: - self._logger.log_info_rank_zero( - "Micro Manager will not output memory usage." - ) + self.write_data_names.set = self.json["coupling_params"]["write_data_names"].get_or_none( + "Micro Manager is writing the following data: {data}", + "No write data names provided. Micro manager will only read data from preCICE.", + list + ) - try: - self._mem_usage_output_n = self._data["memory_usage_output_n"] - self._logger.log_info_rank_zero( - "Memory usage will be output every " - + str(self._mem_usage_output_n) - + " time windows." - ) - except BaseException: - self._logger.log_info_rank_zero( - "No output interval for memory usage output provided. Memory usage will be output every time window." - ) + self.read_data_names.set = self.json["coupling_params"]["read_data_names"].get_or_none( + "Micro Manager is reading the following data: {data}", + "No read data names provided. Micro manager will only write data to preCICE.", + list + ) - try: - self._write_data_names = self._data["coupling_params"]["write_data_names"] - if not isinstance(self._write_data_names, list): - raise Exception("Write data entry is not a list") - self._logger.log_info_rank_zero( - "Micro Manager is writing the following data: " - + str(self._write_data_names) + self.micro_dt.set = self.json["simulation_params"]["micro_dt"].get_or_raise() + + # ====================================================== + # Tasking + # ====================================================== + + if self.json["tasking"].exists(): + self.tasking_backend.set = self.json["tasking"]["backend"].get_with_default( + "socket", + "Tasking backend: {data}", + "No tasking backed defined. Falling back to sockets.", + options=["mpi", "socket"] ) - except BaseException: - self._logger.log_info_rank_zero( - "No write data names provided. Micro manager will only read data from preCICE." + self.enable_tasking_slurm.set = self.json["tasking"]["is_slurm"].get_with_default( + False, + "Tasking using slurm: {data}", + "No tasking slurm flag defined. Assuming non-slurm system.", ) - - try: - self._read_data_names = self._data["coupling_params"]["read_data_names"] - if not isinstance(self._read_data_names, list): - raise Exception("Read data entry is not a list") - self._logger.log_info_rank_zero( - "Micro Manager is reading the following data: " - + str(self._read_data_names) + self.tasking_num_workers.set = self.json["tasking"]["num_workers"].get_with_default( + 1, + "Tasking will use {data} workers", + "No tasking worker count defined. Using 1 worker per rank.", ) - except BaseException: - self._logger.log_info_rank_zero( - "No read data names provided. Micro manager will only write data to preCICE." + self.mpi_impl.set = self.json["tasking"]["mpi_impl"].get_with_default( + "open", + "Tasking using mpi implementation: {data}", + "No tasking mpi implementation defined. Assuming open mpi.", + options=["open", "mpi"], ) - - self._micro_dt = self._data["simulation_params"]["micro_dt"] - - try: - if self._data["tasking"]: - backend = self._data["tasking"]["backend"] - if backend not in ["mpi", "socket"]: - raise Exception("Backend must be either 'mpi' or 'socket'.") - self._task_backend = backend - if "is_slurm" in self._data["tasking"]: - self._task_is_slurm = self._data["tasking"]["is_slurm"] - if "num_workers" in self._data["tasking"]: - self._task_num_workers = self._data["tasking"]["num_workers"] - if self._task_is_slurm and backend == "mpi": - raise Exception("MPI backend not supported on SLURM systems.") - if "mpi_impl" in self._data["tasking"]: - self._task_mpi_impl = self._data["tasking"]["mpi_impl"] - if self._task_mpi_impl not in ["open", "intel"]: - raise Exception("mpi_impl must be either 'open' or 'intel'.") - if "hostfile" in self._data["tasking"]: - self._task_pinning_hostfile = self._data["tasking"]["hostfile"] - except BaseException: - self._logger.log_info_rank_zero( - "No or incorrect tasking information provided. Micro manager will not create workers and instead solve micro simulations locally." + self.tasking_hostfile.set = self.json["tasking"]["hostfile"].get_with_default( + "./hosts.micro", + "Tasking will use nodes from hostlist file: {data}", + "No hostfile for tasking defined. Using hosts.micro as default." ) def read_json_micro_manager(self): @@ -236,368 +532,223 @@ def read_json_micro_manager(self): Reads Micro Manager relevant information from JSON configuration file and saves the data to the respective instance attributes. """ - self._read_json(self._config_file_name) # Read base information + assert self._logger is not None + self._read_json_base(self._config_file_name) - self._precice_config_file_name = os.path.join( - self._folder, self._data["coupling_params"]["precice_config_file_name"] + self.precice_config_file_name.set = os.path.join( + self._base_dir, self._data["coupling_params"]["precice_config_file_name"] ) - self._logger.log_info_rank_zero( - "preCICE configuration file name: " + self._precice_config_file_name + self._logger.log_info_rank_zero(f"preCICE configuration file name: {self.precice_config_file_name()}") + + # ====================================================== + # Mesh and Decomposition + # ====================================================== + + self.macro_mesh_name.set = self.json["coupling_params"]["macro_mesh_name"].get_or_raise( + "Macro mesh name: {data}" ) - self._macro_mesh_name = self._data["coupling_params"]["macro_mesh_name"] - self._logger.log_info_rank_zero("Macro mesh name: " + self._macro_mesh_name) + self.macro_domain_bounds.set = self.json["simulation_params"]["macro_domain_bounds"].get_or_raise( + "Macro domain bounds: {data}" + ) - self._macro_domain_bounds = self._data["simulation_params"][ - "macro_domain_bounds" - ] - self._logger.log_info_rank_zero( - "Macro domain bounds: " + str(self._macro_domain_bounds) + self.ranks_per_axis.set = self.json["simulation_params"]["decomposition"].get_with_default( + [1, 1, 1], + "Axis-wise domain decomposition: {data}", + "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial.", + list, ) - try: - self._ranks_per_axis = self._data["simulation_params"]["decomposition"] - if not isinstance(self._ranks_per_axis, list): - raise Exception("Ranks per axis entry is not a list") - self._logger.log_info_rank_zero( - "Axis-wise domain decomposition: " + str(self._ranks_per_axis) - ) - if self._data["simulation_params"]["decomposition_type"]: - self._decomposition_type = self._data["simulation_params"][ - "decomposition_type" - ] - if self._decomposition_type not in ["uniform", "nonuniform"]: - raise Exception( - "Decomposition type can be either 'uniform' or 'nonuniform'." - ) - if ( - self._data["simulation_params"]["decomposition_type"] - == "nonuniform" - ): - if self._data["simulation_params"]["minimum_access_region_size"]: - self._minimum_access_region_size = self._data[ - "simulation_params" - ]["minimum_access_region_size"] - else: - self._logger.log_info_rank_zero( - "Minimum access region size is not specified. Calculating it as 1 / (2^ranks_per_axis - 1) of the macro domain size in each axis." - ) + self.decomposition_type.set = self.json["simulation_params"]["decomposition_type"].get_with_default( + "uniform", + "Domain decomposition type: {data}", + "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial.", + str, + ["uniform", "nonuniform"] + ) + self.minimum_access_region_size.set = [] + if self.decomposition_type() == "nonuniform": + self.minimum_access_region_size.set = self.json["simulation_params"]["minimum_access_region_size"].get_with_default( + [], + None, + "Minimum access region size is not specified. Calculating it as 1 / (2^ranks_per_axis - 1) of the macro domain size in each axis." - self._logger.log_info_rank_zero( - "Domain decomposition type: " + self._decomposition_type - ) - except BaseException: - self._logger.log_info_rank_zero( - "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial." ) - try: - if self._data["simulation_params"]["adaptivity"]: - self._adaptivity = True - self._logger.log_info_rank_zero( - "Micro Manager will adaptively run micro simulations." - ) - if not self._data["simulation_params"]["adaptivity_settings"]: - raise Exception( - "Adaptivity is turned on but no adaptivity settings are provided." - ) - else: - self._adaptivity = False - if self._data["simulation_params"]["adaptivity_settings"]: - raise Exception( - "Adaptivity settings are provided but adaptivity is turned off." - ) - except BaseException: + # ====================================================== + # Adaptivity + # ====================================================== + + self.enable_adaptivity.set = self.json["simulation_params"]["adaptivity"].get_with_default( + False, + "Micro Manager will adaptively run micro simulations.", + None, + bool, + ) + adaptivity_settings_avail = self.json["simulation_params"]["adaptivity_settings"].exists() + if self.enable_adaptivity() and not adaptivity_settings_avail: + self.enable_adaptivity.set = False + self._logger.log_info_rank_zero("Adaptivity is turned on but no adaptivity settings are provided.") self._logger.log_info_rank_zero( "Micro Manager will not adaptively run micro simulations, but instead will run all micro simulations." ) - - if self._adaptivity: - if ( - self._data["simulation_params"]["adaptivity_settings"]["type"] - == "local" - ): - self._adaptivity_type = "local" - elif ( - self._data["simulation_params"]["adaptivity_settings"]["type"] - == "global" - ): - self._adaptivity_type = "global" - else: - raise Exception("Adaptivity type can be either local or global.") - - self._logger.log_info_rank_zero("Adaptivity type: " + self._adaptivity_type) - - if self._data["simulation_params"]["adaptivity_settings"].get( - "lazy_initialization" - ): - self._lazy_initialization = True - - self._logger.log_info_rank_zero( - "Micro simulations will be created only when they are required to be active for the very first time." + if not self.enable_adaptivity() and adaptivity_settings_avail: + self._logger.log_info_rank_zero("Adaptivity settings are provided but adaptivity is turned off.") + + if self.enable_adaptivity(): + self.adaptivity_type.set = self.json["simulation_params"]["adaptivity_settings"]["type"].get_with_default( + "local", + "Adaptivity type: {data}", + "Adaptivity type can be either local or global.", + options=["local", "global"], ) - - self._data_for_adaptivity = self._data["simulation_params"][ - "adaptivity_settings" - ]["data"] - - self._logger.log_info_rank_zero( - "Data used for adaptivity: " + str(self._data_for_adaptivity) + self.adaptivity_mapping_configs.set = self.json["simulation_params"]["adaptivity_settings"]["mappings"].get_with_default( + [], + None, + "Adaptivity will not interpolate outputs, only use representatives." ) - - if self._data_for_adaptivity == self._write_data_names: + self.enable_adaptivity_lazy_init.set = self.json["simulation_params"]["adaptivity_settings"]["lazy_initialization"].get_with_default(False) + if self.enable_adaptivity_lazy_init(): + self._logger.log_info_rank_zero( + "Micro simulations will be created only when they are required to be active for the very first time." + ) + self.data_for_adaptivity.set = self.json["simulation_params"]["adaptivity_settings"]["data"].get_or_raise( + "Data used for adaptivity: {data}", + "Adaptivity Data must be provided." + ) + if self.data_for_adaptivity() == self.write_data_names(): self._logger.log_info_rank_zero( "Only micro simulation data is used for similarity computation in adaptivity. This would lead to the" " same set of active and inactive simulations for the entire simulation time. If this is not intended," " please include macro data as well." ) - - try: - self._adaptivity_n = self._data["simulation_params"][ - "adaptivity_settings" - ]["adaptivity_every_n_time_windows"] - self._logger.log_info_rank_zero( - "Adaptivity will be computed every " - + str(self._adaptivity_n) - + " time windows." - ) - except BaseException: - self._logger.log_info_rank_zero( - "No interval for adaptivity computation provided. Adaptivity will be computed in every time window." - ) - - try: - self._adaptivity_output_type = self._data["simulation_params"][ - "adaptivity_settings" - ]["output_type"] - if self._adaptivity_output_type not in ["all", "local", "global"]: - raise Exception( - "Adaptivity output type can be either 'all', 'local' or 'global'." - ) - self._logger.log_info_rank_zero( - "Adaptivity output type: " + self._adaptivity_output_type - ) - except BaseException: - self._logger.log_info_rank_zero( - "No adaptivity output type provided. No metrics will be output." - ) - - try: - self._adaptivity_output_n = self._data["simulation_params"][ - "adaptivity_settings" - ]["output_n"] - self._logger.log_info_rank_zero( - "Adaptivity metrics will be output every " - + str(self._adaptivity_output_n) - + " time windows." - ) - except BaseException: - self._logger.log_info_rank_zero( - "No output interval for adaptivity provided. Adaptivity metrics will be output every time window." - ) - - self._adaptivity_history_param = self._data["simulation_params"][ - "adaptivity_settings" - ]["history_param"] - self._logger.log_info_rank_zero( - "Adaptivity history parameter: " + str(self._adaptivity_history_param) + self.adaptivity_n.set = self.json["simulation_params"]["adaptivity_settings"]["adaptivity_every_n_time_windows"].get_with_default( + 1, + "Adaptivity will be computed every {data} time windows.", + "No interval for adaptivity computation provided. Adaptivity will be computed in every time window." ) - - self._adaptivity_coarsening_constant = self._data["simulation_params"][ - "adaptivity_settings" - ]["coarsening_constant"] - self._logger.log_info_rank_zero( - "Adaptivity coarsening constant: " - + str(self._adaptivity_coarsening_constant) + self.adaptivity_output_type.set = self.json["simulation_params"]["adaptivity_settings"]["output_type"].get_with_default( + "", + "Adaptivity output type: {data}", + "Adaptivity output type can be either 'all', 'local' or 'global'. No metrics will be output.", + options=["all", "local", "global"], ) - - self._adaptivity_refining_constant = self._data["simulation_params"][ - "adaptivity_settings" - ]["refining_constant"] - self._logger.log_info_rank_zero( - "Adaptivity refining constant: " - + str(self._adaptivity_refining_constant) + self.adaptivity_output_n.set = self.json["simulation_params"]["adaptivity_settings"]["output_n"].get_with_default( + 1, + "Adaptivity will be computed every {data} time windows.", + "No output interval for adaptivity provided. Adaptivity metrics will be output every time window.", + ) + self.adaptivity_history_param.set = self.json["simulation_params"]["adaptivity_settings"]["history_param"].get_or_raise( + "Adaptivity history parameter: {data}" + ) + self.adaptivity_coarsening_constant.set = self.json["simulation_params"]["adaptivity_settings"]["coarsening_constant"].get_or_raise( + "Adaptivity coarsening constant: {data}" + ) + self.adaptivity_refining_constant.set = self.json["simulation_params"]["adaptivity_settings"]["refining_constant"].get_or_raise( + "Adaptivity refining constant: {data}" ) + self.adaptivity_similarity_measure.set = self.json["simulation_params"]["adaptivity_settings"]["similarity_measure"].get_with_default( + "L1", + "Adaptivity similarity measure: {data}", + "No similarity measure provided, using L1 norm as default." + ) + self.enable_adaptivity_each_implicit_iteration.set = self.json["simulation_params"]["adaptivity_settings"]["every_implicit_iteration"].get_with_default( + False, - if ( - "similarity_measure" - in self._data["simulation_params"]["adaptivity_settings"] - ): - self._adaptivity_similarity_measure = self._data["simulation_params"][ - "adaptivity_settings" - ]["similarity_measure"] + "Micro Manager will compute adaptivity once at the start of every time window." + ) + if self.enable_adaptivity_each_implicit_iteration(): self._logger.log_info_rank_zero( - "Adaptivity similarity measure: " - + str(self._adaptivity_similarity_measure) + "Micro Manager will compute adaptivity in every implicit iteration, if implicit coupling is done." ) else: self._logger.log_info_rank_zero( - "No similarity measure provided, using L1 norm as default." + "Micro Manager will compute adaptivity once at the start of every time window." ) - self._adaptivity_similarity_measure = "L1" - try: - adaptivity_every_implicit_iteration = self._data["simulation_params"][ - "adaptivity_settings" - ]["every_implicit_iteration"] + self.write_data_names().append("Active-State") + self.write_data_names().append("Active-Steps") - if adaptivity_every_implicit_iteration: - self._adaptivity_every_implicit_iteration = True - self._logger.log_info_rank_zero( - "Micro Manager will compute adaptivity in every implicit iteration, if implicit coupling is done." - ) - - elif not adaptivity_every_implicit_iteration: - self._adaptivity_every_implicit_iteration = False - self._logger.log_info_rank_zero( - "Micro Manager will compute adaptivity once at the start of every time window." - ) - except: - self._logger.log_info_rank_zero( - "Micro Manager will compute adaptivity once at the start of every time window." - ) - self._adaptivity_every_implicit_iteration = False + # ====================================================== + # Load Balancing + # ====================================================== - self._write_data_names.append("Active-State") - self._write_data_names.append("Active-Steps") + self.enable_load_balancing.set = self.json["simulation_params"]["load_balancing"].get_with_default(False) + if self.enable_load_balancing(): + self._logger.log_info_rank_zero( + "Micro Manager will dynamically balance micro simulations based on compute times." + ) - try: - self._load_balancing = self._data["simulation_params"]["load_balancing"] - if self._load_balancing: + if self.enable_adaptivity() and not self.adaptivity_type() == "global": + self.enable_load_balancing.set = False self._logger.log_info_rank_zero( - "Micro Manager will dynamically balance micro simulations." + "Attempting to load balancing with adaptivity other than global adaptivity. " + "Disabling load balancing. To use load balancing either disable adaptivity or run with global adaptiviy." ) - self._write_data_names.append("rank_of_sim") - if self._adaptivity and not self._adaptivity_type == "global": - raise Exception( - "Load balancing can be done only with global adaptivity." - ) - except BaseException: - self._logger.log_info_rank_zero( - "Micro Manager will not dynamically balance micro simulations." - ) + else: + self.write_data_names().append("rank_of_sim") - if self._load_balancing: - self._load_balancing_n = self._data["simulation_params"][ - "load_balancing_settings" - ]["every_n_time_windows"] - self._logger.log_info_rank_zero( - "Load balancing will be done every " - + str(self._load_balancing_n) - + " time windows." + self.load_balancing_n.set = self.json["simulation_params"]["load_balancing_settings"]["every_n_time_windows"].get_with_default( + 1, + "Load balancing will be computed every {data} time windows.", + "Load balancing will be computed in every time window." ) - - try: - self._load_balancing_type = self._data["simulation_params"][ - "load_balancing_settings" - ]["type"] - except BaseException: - self._load_balancing_type = "time" - self._logger.log_info_rank_zero( - f"Load balancing will use {self._load_balancing_type} based balancing." + self.load_balancing_type.set = self.json["simulation_params"]["load_balancing_settings"]["type"].get_with_default( + "time", + "Load balancing type: {data}", + "Load balancing will use time based balancing.", + options=["time", "active"] ) + if self.load_balancing_type() == "active": + self.enable_load_balancing_inactive.set = self.json["simulation_params"]["load_balancing_settings"]["balance_inactive_simulations"].get_with_default( + False, + "Load balancing enable inactive balancing: {data}", + "Load balancing will not balance inactive micro simulations.", + dtype=bool + ) - if self._load_balancing_type == "active": - try: - self._load_balancing_threshold = self._data["simulation_params"][ - "load_balancing_settings" - ]["threshold"] - except BaseException: - self._load_balancing_threshold = 0 - self._logger.log_info_rank_zero( - "Load balancing will use 0 threshold." - ) - - try: - self._load_balancing_balance_inactive_sims = self._data[ - "simulation_params" - ]["load_balancing_settings"]["balance_inactive_sims"] - except BaseException: - self._load_balancing_balance_inactive_sims = False - self._logger.log_info_rank_zero( - "Load balancing will not consider inactive simulations." - ) - else: - if ( - "threshold" - in self._data["simulation_params"]["load_balancing_settings"] - ): - self._logger.log_info_rank_zero( - 'Load balancing is not using active simulation balancing. Field "threshold" will be ignored.' - ) - if ( - "balance_inactive_sims" - in self._data["simulation_params"]["load_balancing_settings"] - ): - self._logger.log_info_rank_zero( - 'Load balancing is not using active simulation balancing. Field "balance_inactive_sims" will be ignored.' - ) + self.load_balancing_threshold.set = self.json["simulation_params"]["load_balancing_settings"]["threshold"].get_with_default( + 0, + "Load balancing threshold: {data}", + "Load balancing will use 0 threshold.", + ) + if self.load_balancing_type() == "time": + self.load_balancing_partitioning.set = self.json["simulation_params"]["load_balancing_settings"]["partitioning"].get_with_default( + "lpt", + "Load balancing partitioning: {data}", + "No partitioning provided, using LPT as default.", + options=["lpt"], + ) - if self._load_balancing_type == "time": - try: - self._load_balancing_partitioning = self._data["simulation_params"][ - "load_balancing_settings" - ]["partitioning"] - except BaseException: - self._logger.log_info_rank_zero( - "Partitioning type must be provided for time based load balancing. Defaulting to 'lpt'." - ) - self._load_balancing_partitioning = "lpt" + # ====================================================== + # Model Adaptivity + # ====================================================== - try: - if self._data["simulation_params"]["model_adaptivity"]: - self._m_adap = True - self._logger.log_info_rank_zero( - "Micro Manager will use Model Adaptivity." - ) - if not self._data["simulation_params"]["model_adaptivity_settings"]: - raise Exception( - "Model Adaptivity is turned on but no model adaptivity settings are provided." - ) - else: - self._m_adap = False - if self._data["simulation_params"]["model_adaptivity_settings"]: - raise Exception( - "Model Adaptivity settings are provided but model adaptivity is turned off." - ) - except BaseException: - self._logger.log_info_rank_zero( - "Micro Manager will not adaptively switch simulation models." - ) + self.enable_model_adaptivity.set = self.json["simulation_params"]["model_adaptivity"].get_with_default(False) + if self.enable_model_adaptivity() and not self.json["simulation_params"]["model_adaptivity_settings"].exists(): + self.enable_model_adaptivity.set = False + self._logger.log_info_rank_zero("Model Adaptivity is turned on but no model adaptivity settings are provided.") - if self._m_adap: - self._m_adap_micro_file_names = [ + if self.enable_model_adaptivity(): + file_names = self.json["simulation_params"]["model_adaptivity_settings"]["micro_file_names"].get_or_raise() + self.model_adaptivity_file_names.set = [ name.replace("/", ".").replace("\\", ".").replace(".py", "") - for name in self._data["simulation_params"][ - "model_adaptivity_settings" - ]["micro_file_names"] + for name in file_names ] - - if len(self._m_adap_micro_file_names) < 2: + if len(file_names) < 2: self._logger.log_info_rank_zero( "Not enough Micro Models provided for Model Adaptivity. Need min 2." ) self._logger.log_info_rank_zero("Disabling Model Adaptivity.") - self._m_adap = False - - self._m_adap_switching_function = self._data["simulation_params"][ - "model_adaptivity_settings" - ]["switching_function"] - - if ( - "micro_stateless" - in self._data["simulation_params"]["model_adaptivity_settings"] - ): - self._m_adap_micro_stateless = self._data["simulation_params"][ - "model_adaptivity_settings" - ]["micro_stateless"] - else: - self._m_adap_micro_stateless = [False] * len( - self._m_adap_micro_file_names - ) + self.enable_model_adaptivity.set = False + + self.model_adaptivity_switching_function.set = self.json["simulation_params"]["model_adaptivity_settings"]["switching_function"].get_or_raise() + self.model_adaptivity_micro_stateless.set = self.json["simulation_params"]["model_adaptivity_settings"]["micro_stateless"].get_with_default( + [False] * len(file_names), + ) - for i in range(len(self._m_adap_micro_file_names)): - if self._m_adap_micro_stateless[i]: + for i in range(len(file_names)): + if self.model_adaptivity_micro_stateless()[i]: self._logger.log_info_rank_zero( f"Only creating one full instance of Micro Model {i}." ) @@ -606,100 +757,75 @@ def read_json_micro_manager(self): f"Creating full instance of Micro Model {i} per mesh vertex." ) - if "interpolate_crash" in self._data["simulation_params"]: - if self._data["simulation_params"]["interpolate_crash"]: - self._interpolate_crash = True - self._logger.log_info_rank_zero( - "Micro Manager will interpolate output of crashed micro simulations from its neighbors." - ) + # ====================================================== + # Crash Interpolation and Diagnostics + # ====================================================== - try: - diagnostics_data_names = self._data["diagnostics"]["data_from_micro_sims"] - if not isinstance(diagnostics_data_names, list): - raise Exception("Diagnostics data entry is not a list") - except BaseException: - self._logger.log_info_rank_zero( - "No diagnostics data is defined. Micro Manager will not output any diagnostics data." - ) + self.enable_crashed_sim_interpolation.set = self.json["simulation_params"]["interpolate_crash"].get_with_default(False) + if self.enable_crashed_sim_interpolation(): + self._logger.log_info_rank_zero("Micro Manager will interpolate output of crashed micro simulations from its neighbors.") - try: - self._micro_output_n = self._data["diagnostics"]["micro_output_n"] - except BaseException: - self._logger.log_info_rank_zero( - "Output interval of micro simulations not specified, if output is available then it will be called " - "in every time window." - ) + # TODO what? this is not being saved nor used + diagnostics_data_names = self.json["diagnostics"]["data_from_micro_sims"].get_or_none( + None, + "No diagnostics data is defined. Micro Manager will not output any diagnostics data.", + list, + ) + + self.micro_output_n.set = self.json["diagnostics"]["micro_output_n"].get_with_default( + 1, + "Micro Manager will compute micro output every {data} time windows.", + "Output interval of micro simulations not specified, if output is available then it will be called " + "in every time window.", + ) def read_json_snapshot(self): """ Reads Snapshot relevant information from JSON configuration file """ - self._read_json(self._config_file_name) # Read base information - - self._logger.log_info_rank_zero( - "Reading JSON configuration file: " + self._config_file_name - ) - + assert self._logger is not None + self._read_json_base(self._config_file_name) # Read base information + self._logger.log_info_rank_zero(f"Reading JSON configuration file: {self._config_file_name}") self._logger.log_info_rank_zero("Micro Manager is running in snapshot mode.") - - self._parameter_file_name = os.path.join( - self._folder, self._data["coupling_params"]["parameter_file_name"] - ) - self._logger.log_info_rank_zero( - "Parameter file name: " + self._parameter_file_name + self.parameter_file_name.set = os.path.join( + self._base_dir, self.json["coupling_params"]["parameter_file_name"].get_or_raise() ) + self._logger.log_info_rank_zero(f"Parameter file name: {self.parameter_file_name()}") - try: - self._output_file_name = self._data["snapshot_params"]["output_file_name"] - self._logger.log_info_rank_zero( - "Output file name: " + self._output_file_name - ) - except BaseException: - self._logger.log_info_rank_zero( - "No snapshot output file name provided. Defaulting to 'snapshot_data'." - ) - self._output_file_name = "snapshot_data" + self.output_file_name.set = self.json["snapshot_params"]["output_file_name"].get_with_default( + "snapshot_data", + "Output file name: {data}", + "No snapshot output file name provided. Defaulting to 'snapshot_data'." + ) - try: - self._postprocessing_file_name = ( - self._data["snapshot_params"]["post_processing_file_name"] + post_proc_file = self.json["snapshot_params"]["post_processing_file_name"].get_or_none( + "Post-processing file name {data}", + "No post-processing file name provided. Snapshot computation will not perform any post-processing." + ) + if post_proc_file is not None: + post_proc_file = ( + post_proc_file .replace("/", ".") .replace("\\", ".") .replace(".py", "") ) - self._logger.log_info_rank_zero( - "Post-processing file name: " + self._postprocessing_file_name - ) - except BaseException: - self._logger.log_info_rank_zero( - "No post-processing file name provided. Snapshot computation will not perform any post-processing." - ) - self._postprocessing_file_name = None + self.postprocessing_file_name.set = post_proc_file - try: - diagnostics_data_names = self._data["diagnostics"]["data_from_micro_sims"] - if not isinstance(diagnostics_data_names, list): - raise Exception("Diagnostics data entry is not a list") - self._logger.log_info_rank_zero( - "Diagnostics data: " + str(diagnostics_data_names) - ) - except BaseException: - self._logger.log_info_rank_zero( - "No diagnostics data is defined. Snapshot computation will not output any diagnostics data." - ) + # TODO what? this is not being saved nor used + diagnostics_data_names = self.json["diagnostics"]["data_from_micro_sims"].get_or_none( + "Diagnostics data: {data}", + "No diagnostics data is defined. Micro Manager will not output any diagnostics data.", + list, + ) - try: - if self._data["snapshot_params"]["initialize_once"]: - self._initialize_once = True - self._logger.log_info_rank_zero( - "Micro Manager will initialize only one micro simulations object for snapshot computation." - ) - except BaseException: - self._logger.log_info_rank_zero( - "For each snapshot a new micro simulation object will be created." - ) + self.enable_single_sim_object.set = self.json["snapshot_params"]["initialize_once"].get_with_default( + False, + "Micro Manager will initialize only one micro simulations object for snapshot computation.", + "For each snapshot a new micro simulation object will be created." + ) - def get_precice_config_file_name(self): + @config_entry + def precice_config_file_name(self) -> str: """ Get the name of the preCICE XML configuration file. @@ -708,9 +834,10 @@ def get_precice_config_file_name(self): config_file_name : string Name of the preCICE XML configuration file. """ - return self._precice_config_file_name + pass - def get_macro_mesh_name(self): + @config_entry + def macro_mesh_name(self) -> str: """ Get the name of the macro mesh. This name is expected to be the same as the one defined in the preCICE configuration file. @@ -721,57 +848,62 @@ def get_macro_mesh_name(self): Name of the macro mesh as stated in the JSON configuration file. """ - return self._macro_mesh_name + pass - def get_read_data_names(self): + @config_entry + def read_data_names(self) -> List[str]: """ Get the user defined dictionary carrying information of the data to be read from preCICE. Returns ------- - read_data_names: dict_like + read_data_names: List[str] A dictionary containing the names of the data to be read from preCICE as keys and information on whether the data are scalar or vector as values. """ - return self._read_data_names + pass - def get_write_data_names(self): + @config_entry + def write_data_names(self) -> List[str]: """ Get the user defined dictionary carrying information of the data to be written to preCICE. Returns ------- - write_data_names: dict_like + write_data_names: List[str] A dictionary containing the names of the data to be written to preCICE as keys and information on whether the data are scalar or vector as values. """ - return self._write_data_names + pass - def get_macro_domain_bounds(self): + @config_entry + def macro_domain_bounds(self) -> List[List[float]]: """ Get the upper and lower bounds of the macro domain. Returns ------- - macro_domain_bounds : list + macro_domain_bounds : List[List[float]] List containing upper and lower bounds of the macro domain. Format in 2D is [x_min, x_max, y_min, y_max] Format in 2D is [x_min, x_max, y_min, y_max, z_min, z_max] """ - return self._macro_domain_bounds + pass - def get_ranks_per_axis(self): + @config_entry + def ranks_per_axis(self) -> List[int]: """ Get the ranks per axis for a parallel simulation Returns ------- - ranks_per_axis : list + ranks_per_axis : List[int] List containing ranks in the x, y and z axis respectively. """ - return self._ranks_per_axis + pass - def get_decomposition_type(self): + @config_entry + def decomposition_type(self) -> str: """ Get the type of domain decomposition. @@ -780,31 +912,34 @@ def get_decomposition_type(self): decomposition_type : str Type of domain decomposition, can be either "uniform" or "non-uniform". """ - return self._decomposition_type + pass - def get_minimum_access_region_size(self): + @config_entry + def minimum_access_region_size(self) -> List: """ Get the minimum access region size for non-uniform domain decomposition. Returns ------- - minimum_access_region_size : list + minimum_access_region_size : List List containing the minimum access region size in each axis for non-uniform domain decomposition. """ - return self._minimum_access_region_size + pass - def get_micro_file_name(self): + @config_entry + def micro_file_name(self) -> str: """ Get the path to the Python script of the micro-simulation. Returns ------- - micro_file_name : string + micro_file_name : str String carrying the path to the Python script of the micro-simulation. """ - return self._micro_file_name + pass - def turn_on_micro_stateless(self): + @config_entry + def enable_micro_stateless(self) -> bool: """ Boolean stating whether micro model is stateless or not. @@ -813,9 +948,10 @@ def turn_on_micro_stateless(self): stateless : bool True if micro model is stateless, False otherwise. """ - return self._micro_stateless + pass - def get_micro_output_n(self): + @config_entry + def micro_output_n(self) -> int: """ Get the micro output frequency @@ -824,9 +960,10 @@ def get_micro_output_n(self): micro_output_n : int Output frequency of micro simulations, so output every N timesteps """ - return self._micro_output_n + pass - def turn_on_adaptivity(self): + @config_entry + def enable_adaptivity(self) -> bool: """ Boolean stating whether adaptivity is ot or not. @@ -836,9 +973,10 @@ def turn_on_adaptivity(self): True is adaptivity settings are done, False otherwise. """ - return self._adaptivity + pass - def get_adaptivity_type(self): + @config_entry + def adaptivity_type(self) -> str: """ String stating type of adaptivity computation, either "local" or "global". @@ -847,21 +985,35 @@ def get_adaptivity_type(self): adaptivity_type : str Either "local" or "global" depending on the type of adaptivity computation """ - return self._adaptivity_type + pass + + @config_entry + def adaptivity_mapping_configs(self): + """ + Get the mapping configurations for the adaptivity interpolation scheme. + + Returns + ------- + adaptivity_mapping_configs : list + List of adaptivity mapping configurations. + """ + pass - def get_data_for_adaptivity(self): + @config_entry + def data_for_adaptivity(self) -> List[str]: """ Get names of data to be used for similarity distance calculation in adaptivity Returns ------- - data_for_adaptivity : dict_like - A dictionary containing the names of the data to be used in adaptivity as keys and information on whether + data_for_adaptivity : List[str] + A list containing the names of the data to be used in adaptivity as keys and information on whether the data are scalar or vector as values. """ - return self._data_for_adaptivity + pass - def get_adaptivity_n(self): + @config_entry + def adaptivity_n(self) -> int: """ Get the frequency of adaptivity computation. @@ -870,9 +1022,10 @@ def get_adaptivity_n(self): adaptivity_n : int Frequency of adaptivity computation, as a multiple of time windows. """ - return self._adaptivity_n + pass - def get_adaptivity_output_type(self): + @config_entry + def adaptivity_output_type(self) -> str: """ Get the type of adaptivity output. @@ -881,9 +1034,10 @@ def get_adaptivity_output_type(self): adaptivity_output_type : str Type of adaptivity output, can be "all", "local" or "global". """ - return self._adaptivity_output_type + pass - def get_adaptivity_output_n(self): + @config_entry + def adaptivity_output_n(self) -> int: """ Get the output frequency of adaptivity metrics. @@ -892,9 +1046,10 @@ def get_adaptivity_output_n(self): adaptivity_output_n : int Output frequency of adaptivity metrics, so output every N timesteps """ - return self._adaptivity_output_n + pass - def get_adaptivity_hist_param(self): + @config_entry + def adaptivity_history_param(self) -> float: """ Get adaptivity history parameter. More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity @@ -904,9 +1059,10 @@ def get_adaptivity_hist_param(self): adaptivity_hist_param : float Adaptivity history parameter """ - return self._adaptivity_history_param + pass - def get_adaptivity_coarsening_const(self): + @config_entry + def adaptivity_coarsening_constant(self) -> float: """ Get adaptivity coarsening constant. More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity @@ -916,9 +1072,10 @@ def get_adaptivity_coarsening_const(self): adaptivity_coarsening_constant : float Adaptivity coarsening constant """ - return self._adaptivity_coarsening_constant + pass - def get_adaptivity_refining_const(self): + @config_entry + def adaptivity_refining_constant(self) -> float: """ Get adaptivity refining constant. More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity @@ -928,9 +1085,10 @@ def get_adaptivity_refining_const(self): adaptivity_refining_constant : float Adaptivity refining constant """ - return self._adaptivity_refining_constant + pass - def get_adaptivity_similarity_measure(self): + @config_entry + def adaptivity_similarity_measure(self) -> str: """ Get measure to be used to calculate similarity between pairs of simulations. More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity @@ -940,9 +1098,10 @@ def get_adaptivity_similarity_measure(self): adaptivity_similarity_measure : str String of measure to be used in calculating similarity between pairs of simulations. """ - return self._adaptivity_similarity_measure + pass - def is_adaptivity_required_in_every_implicit_iteration(self): + @config_entry + def enable_adaptivity_each_implicit_iteration(self) -> bool: """ Check if adaptivity needs to be calculated in every time iteration or every time window. @@ -951,9 +1110,10 @@ def is_adaptivity_required_in_every_implicit_iteration(self): adaptivity_every_implicit_iteration : bool True if adaptivity needs to be calculated in every time iteration, False otherwise. """ - return self._adaptivity_every_implicit_iteration + pass - def turn_on_load_balancing(self): + @config_entry + def enable_load_balancing(self) -> bool: """ Check if load balancing should be performed. @@ -962,20 +1122,10 @@ def turn_on_load_balancing(self): load_balancing : bool True if load balancing needs to be done, False otherwise. """ - return self._load_balancing - - def get_load_balancing_n(self): - """ - Get the load balancing frequency. - - Returns - ------- - load_balancing_n : int - Load balancing frequency - """ - return self._load_balancing_n + pass - def get_load_balancing_type(self): + @config_entry + def load_balancing_type(self) -> str: """ Get load balancing type. @@ -984,9 +1134,10 @@ def get_load_balancing_type(self): type : str Load balancing type. """ - return self._load_balancing_type + pass - def get_load_balancing_threshold(self): + @config_entry + def load_balancing_threshold(self) -> float: """ Get load balancing threshold. @@ -995,9 +1146,10 @@ def get_load_balancing_threshold(self): load_balancing_threshold : float Load balancing threshold """ - return self._load_balancing_threshold + pass - def turn_on_load_balancing_inactive(self): + @config_entry + def enable_load_balancing_inactive(self) -> bool: """ Check if load balancing should be performed on inactive micro simulations. @@ -1006,9 +1158,22 @@ def turn_on_load_balancing_inactive(self): balancing_inactive : bool True if load balancing should consider inactive simulations, False otherwise. """ - return self._load_balancing_balance_inactive_sims + pass - def get_load_balancing_partitioning(self): + @config_entry + def load_balancing_n(self) -> int: + """ + Get the load balancing frequency. + + Returns + ------- + load_balancing_n : int + Load balancing frequency + """ + pass + + @config_entry + def load_balancing_partitioning(self) -> str: """ Get the load balancing partitioning type @@ -1017,21 +1182,23 @@ def get_load_balancing_partitioning(self): load_balancing_partitioning : str Load balancing partitioning type """ - return self._load_balancing_partitioning + pass - def initialize_sims_lazily(self): + @config_entry + def enable_adaptivity_lazy_init(self) -> bool: """ Check if simulations are to be created only when they are required to be active for the very first time. Returns ------- - adaptivity : bool + enable_adaptivity_lazy_init : bool True if micro simulations are created only when needed, False otherwise. """ - return self._lazy_initialization + pass - def get_micro_dt(self): + @config_entry + def micro_dt(self) -> float: """ Get the size of the micro time window. @@ -1040,32 +1207,35 @@ def get_micro_dt(self): micro_time_window : float Size of the micro time window. """ - return self._micro_dt + pass - def get_parameter_file_name(self): + @config_entry + def parameter_file_name(self) -> str: """ Get the name of the parameter file. Returns ------- - parameter_file_name : string + parameter_file_name : str Name of the hdf5 file containing the macro parameters. """ - return self._parameter_file_name + pass - def get_output_file_name(self): + @config_entry + def output_file_name(self) -> str: """ Get the name of the output file. Returns ------- - output_file_name : string + output_file_name : str Name of the hdf5 file containing the snapshot data. """ - return self._output_file_name + pass - def get_postprocessing_file_name(self): + @config_entry + def postprocessing_file_name(self) -> str: """ Depending on user input, snapshot computation will perform post-processing for every micro simulation before writing output to a file. @@ -1074,9 +1244,10 @@ def get_postprocessing_file_name(self): postprocessing : str Name of post-processing script. """ - return self._postprocessing_file_name + pass - def interpolate_crashed_micro_sim(self): + @config_entry + def enable_crashed_sim_interpolation(self) -> bool: """ Check if user wants crashed micro simulations to be interpolated. @@ -1085,9 +1256,10 @@ def interpolate_crashed_micro_sim(self): interpolate_crash : bool True if crashed micro simulations need to be interpolated, False otherwise. """ - return self._interpolate_crash + pass - def create_single_sim_object(self): + @config_entry + def enable_single_sim_object(self) -> bool: """ Check if multiple snapshots can be computed on a single micro simulation object. @@ -1096,20 +1268,22 @@ def create_single_sim_object(self): initialize_once : bool True if initialization is done only once, False otherwise. """ - return self._initialize_once + pass - def get_output_dir(self): + @config_entry + def output_dir(self) -> str: """ Get the name of the output directory. Returns ------- - output_dir : string + output_dir : str Name of the output folder. """ - return self._output_dir + pass - def get_memory_usage_output_type(self): + @config_entry + def memory_usage_output_type(self) -> str: """ Get the type of memory usage output. @@ -1118,9 +1292,10 @@ def get_memory_usage_output_type(self): mem_usage_output_type : str Type of adaptivity output, can be "all", "local" or "global". """ - return self._mem_usage_output_type + pass - def get_memory_usage_output_n(self): + @config_entry + def memory_usage_output_n(self) -> int: """ Get the output frequency of memory usage. @@ -1129,9 +1304,10 @@ def get_memory_usage_output_n(self): mem_usage_output_n : int Output frequency of memory usage, so output every N timesteps """ - return self._mem_usage_output_n + pass - def turn_on_model_adaptivity(self): + @config_entry + def enable_model_adaptivity(self) -> bool: """ Boolean stating whether adaptivity is ot or not. @@ -1141,31 +1317,34 @@ def turn_on_model_adaptivity(self): True is model adaptivity settings are done, False otherwise. """ - return self._m_adap + pass - def get_model_adaptivity_file_names(self): + @config_entry + def model_adaptivity_file_names(self) -> List[str]: """ Get the paths to the Python scripts of the model-adaptive-micro-simulations. Returns ------- - micro_file_names : string + micro_file_names : List[str] String carrying the path to the Python script of the micro-simulation. """ - return self._m_adap_micro_file_names + pass - def get_model_adaptivity_micro_stateless(self): + @config_entry + def model_adaptivity_micro_stateless(self) -> List[bool]: """ List of boolean stating whether the respective micro model is stateless or not. Returns ------- - stateless : list + stateless : List[bool] True if micro model is stateless, False otherwise. """ - return self._m_adap_micro_stateless + pass - def get_model_adaptivity_switching_function(self): + @config_entry + def model_adaptivity_switching_function(self) -> str: """ Get path to switching function file @@ -1174,9 +1353,10 @@ def get_model_adaptivity_switching_function(self): switching_function : str String containing the path to the switching function file """ - return self._m_adap_switching_function + pass - def get_tasking_num_workers(self): + @config_entry + def tasking_num_workers(self) -> int: """ Get number of workers @@ -1185,9 +1365,10 @@ def get_tasking_num_workers(self): num_workers : int Number of workers """ - return self._task_num_workers + pass - def get_tasking_backend(self): + @config_entry + def tasking_backend(self) -> str: """ Get backend type @@ -1196,9 +1377,10 @@ def get_tasking_backend(self): backend : str either socket or mpi """ - return self._task_backend + pass - def get_tasking_use_slurm(self): + @config_entry + def enable_tasking_slurm(self) -> bool: """ Get flag whether slurm is used @@ -1207,9 +1389,10 @@ def get_tasking_use_slurm(self): use_slurm : bool use slurm or not """ - return self._task_is_slurm + pass - def get_tasking_hostfile(self): + @config_entry + def tasking_hostfile(self) -> str: """ Get hostfile path for workers @@ -1218,9 +1401,10 @@ def get_tasking_hostfile(self): hostfile : str Hostfile path for workers """ - return self._task_pinning_hostfile + pass - def get_mpi_impl(self): + @config_entry + def mpi_impl(self) -> str: """ Get mpi implementation type @@ -1229,4 +1413,4 @@ def get_mpi_impl(self): mpi_impl : str mpi implementation type """ - return self._task_mpi_impl + pass \ No newline at end of file diff --git a/micro_manager/domain_decomposition.py b/micro_manager/domain_decomposition.py index e9bd7015..820594ee 100644 --- a/micro_manager/domain_decomposition.py +++ b/micro_manager/domain_decomposition.py @@ -26,21 +26,21 @@ def __init__(self, configurator: Config, rank: int, size: int) -> None: self._size = size self._ranks_per_axis = ( - configurator.get_ranks_per_axis() + configurator.ranks_per_axis() ) # Check if ranks per axis is provided in the configuration file for parallel runs self._dims = len(self._ranks_per_axis) self._is_minimum_access_region_size_specified = False - self._decomposition_type = configurator.get_decomposition_type() + self._decomposition_type = configurator.decomposition_type() - self._macro_bounds = configurator.get_macro_domain_bounds() + self._macro_bounds = configurator.macro_domain_bounds() self._get_local_mesh_bounds = self._get_local_mesh_bounds_variant() self._minimum_access_region_size: list = ( - configurator.get_minimum_access_region_size() + configurator.minimum_access_region_size() ) if self._minimum_access_region_size: # if list is not empty self._is_minimum_access_region_size_specified = True diff --git a/micro_manager/load_balancing.py b/micro_manager/load_balancing.py index cdd51a80..2cd273d8 100644 --- a/micro_manager/load_balancing.py +++ b/micro_manager/load_balancing.py @@ -60,7 +60,7 @@ def __init__( rank: int local rank """ - self._enabled = config.turn_on_load_balancing() + self._enabled = config.enable_load_balancing() self._precice_participant = precice_participant self._model_manager = model_manager self._adaptivity_controller = adaptivity_controller @@ -78,7 +78,7 @@ def __init__( self._balance_metric_local = dict() self._balance_metric_global = np.zeros(global_number_of_sims) self._partition_impl = self.get_partition_impl( - config.get_load_balancing_partitioning() + config.load_balancing_partitioning() ) if ( @@ -440,8 +440,8 @@ def __init__( rank, ) self._partition_impl = lambda a, b: (None, None) - self._threshold = config.get_load_balancing_threshold() - self._balance_inactive_sims = config.turn_on_load_balancing_inactive() + self._threshold = config.load_balancing_threshold() + self._balance_inactive_sims = config.enable_load_balancing_inactive() self._bypass_skip = False # used for testing self._bypass_active = False # used for testing @@ -718,7 +718,7 @@ def create_load_balancer( comm: MPI.Comm, rank: int, ) -> LoadBalancer: - lb_type = config.get_load_balancing_type() + lb_type = config.load_balancing_type() if lb_type == "time": lb_cls = LoadBalancer diff --git a/micro_manager/micro_manager.py b/micro_manager/micro_manager.py index e5aa1d69..ff29391f 100644 --- a/micro_manager/micro_manager.py +++ b/micro_manager/micro_manager.py @@ -61,11 +61,11 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._config.set_logger(self._logger) self._config.read_json_micro_manager() - self._memory_usage_output_type = self._config.get_memory_usage_output_type() + self._memory_usage_output_type = self._config.memory_usage_output_type() - self._memory_usage_output_n = self._config.get_memory_usage_output_n() + self._memory_usage_output_n = self._config.memory_usage_output_n() - self._output_dir = self._config.get_output_dir() + self._output_dir = self._config.output_dir() if self._output_dir is not None: self._output_dir = os.path.abspath(self._output_dir) + "/" @@ -74,17 +74,17 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._output_dir = os.path.abspath(os.getcwd()) + "/" # Data names of data to output to the snapshot database - self._write_data_names = self._config.get_write_data_names() + self._write_data_names = self._config.write_data_names() # Data names of data to read as input parameter to the simulations - self._read_data_names = self._config.get_read_data_names() + self._read_data_names = self._config.read_data_names() - self._micro_dt = self._config.get_micro_dt() + self._micro_dt = self._config.micro_dt() - self._macro_mesh_name = self._config.get_macro_mesh_name() + self._macro_mesh_name = self._config.macro_mesh_name() # Parameter for interpolation in case of a simulation crash - self._interpolate_crashed_sims = self._config.interpolate_crashed_micro_sim() + self._interpolate_crashed_sims = self._config.enable_crashed_sim_interpolation() if self._interpolate_crashed_sims: if Interpolation is None: self._logger.log_info_rank_zero( @@ -96,16 +96,16 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._crash_threshold = 0.2 self._number_of_nearest_neighbors = 4 - self._micro_n_out = self._config.get_micro_output_n() + self._micro_n_out = self._config.micro_output_n() - self._lazy_init = self._config.initialize_sims_lazily() + self._lazy_init = self._config.enable_adaptivity_lazy_init() - self._is_adaptivity_on = self._config.turn_on_adaptivity() + self._is_adaptivity_on = self._config.enable_adaptivity() if self._is_adaptivity_on: self._data_for_adaptivity: dict[str, list] = dict() - self._adaptivity_data_names = self._config.get_data_for_adaptivity() + self._adaptivity_data_names = self._config.data_for_adaptivity() # Names of macro data to be used for adaptivity computation self._adaptivity_macro_data_names: list = [] @@ -119,21 +119,21 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._adaptivity_micro_data_names.append(name) self._adaptivity_in_every_implicit_step = ( - self._config.is_adaptivity_required_in_every_implicit_iteration() + self._config.enable_adaptivity_each_implicit_iteration() ) - self._adaptivity_n = self._config.get_adaptivity_n() + self._adaptivity_n = self._config.adaptivity_n() - self._adaptivity_output_type = self._config.get_adaptivity_output_type() + self._adaptivity_output_type = self._config.adaptivity_output_type() - self._adaptivity_output_n = self._config.get_adaptivity_output_n() + self._adaptivity_output_n = self._config.adaptivity_output_n() - self._is_model_adaptivity_on = self._config.turn_on_model_adaptivity() + self._is_model_adaptivity_on = self._config.enable_model_adaptivity() # Define the preCICE Participant self._participant = precice.Participant( "Micro-Manager", - self._config.get_precice_config_file_name(), + self._config.precice_config_file_name(), self._rank, self._size, ) @@ -149,9 +149,9 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self.state_loader = lambda sim: sim.attachments self.state_setter = lambda sim, state: sim.attachments.update(state) self._is_load_balancing = ( - self._config.turn_on_load_balancing() and self._is_parallel + self._config.enable_load_balancing() and self._is_parallel ) - self._load_balancing_n = self._config.get_load_balancing_n() + self._load_balancing_n = self._config.load_balancing_n() self.load_balancing = None # ************** @@ -430,13 +430,13 @@ def initialize(self) -> None: # Decompose the macro-domain and set the mesh access region for each partition in preCICE if not len( - self._config.get_macro_domain_bounds() + self._config.macro_domain_bounds() ) / 2 == self._participant.get_mesh_dimensions(self._macro_mesh_name): raise Exception("Provided macro mesh bounds are of incorrect dimension") if self._is_parallel: if not len( - self._config.get_ranks_per_axis() + self._config.ranks_per_axis() ) == self._participant.get_mesh_dimensions(self._macro_mesh_name): raise Exception( "Provided ranks combination is of incorrect dimension" @@ -448,7 +448,7 @@ def initialize(self) -> None: if self._is_parallel and not self._is_load_balancing: coupling_mesh_bounds = domain_decomposer.get_local_mesh_bounds() else: # When serial or load balancing, the whole macro domain is assigned to one/each rank - coupling_mesh_bounds = self._config.get_macro_domain_bounds() + coupling_mesh_bounds = self._config.macro_domain_bounds() self._participant.set_mesh_access_region( self._macro_mesh_name, coupling_mesh_bounds @@ -572,14 +572,14 @@ def initialize(self) -> None: # Setup remote workers base_dir = os.path.dirname(os.path.abspath(__file__)) worker_exec = os.path.join(base_dir, "tasking", "worker_main.py") - num_ranks = self._config.get_tasking_num_workers() + num_ranks = self._config.tasking_num_workers() self._conn = spawn_local_workers( worker_exec, num_ranks, - self._config.get_tasking_backend(), - self._config.get_tasking_use_slurm(), - self._config.get_mpi_impl(), - self._config.get_tasking_hostfile(), + self._config.tasking_backend(), + self._config.enable_tasking_slurm(), + self._config.mpi_impl(), + self._config.tasking_hostfile(), ) # load micro sim @@ -598,17 +598,17 @@ def initialize(self) -> None: self._model_adaptivity_controller.get_resolution_sim_class(0) ) else: - micro_problem_base = load_backend_class(self._config.get_micro_file_name()) + micro_problem_base = load_backend_class(self._config.micro_file_name()) micro_problem_cls = create_simulation_class( self._logger, micro_problem_base, - self._config.get_micro_file_name(), - self._config.get_tasking_num_workers(), + self._config.micro_file_name(), + self._config.tasking_num_workers(), self._conn, "MicroSimulationDefault", ) self._model_manager.register( - micro_problem_cls, self._config.turn_on_micro_stateless() + micro_problem_cls, self._config.enable_micro_stateless() ) # Create micro simulation objects diff --git a/micro_manager/snapshot/snapshot.py b/micro_manager/snapshot/snapshot.py index 618e5987..bd261afc 100644 --- a/micro_manager/snapshot/snapshot.py +++ b/micro_manager/snapshot/snapshot.py @@ -41,7 +41,7 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._config.set_logger(self._logger) self._config.read_json_snapshot() - self._output_dir = self._config.get_output_dir() + self._output_dir = self._config.output_dir() if self._output_dir is not None: self._output_dir = os.path.abspath(self._output_dir) + "/" @@ -49,24 +49,24 @@ def __init__(self, config_file: str, log_file: str = "") -> None: else: self._output_dir = os.path.abspath(os.getcwd()) + "/" - self._output_file_name = self._config.get_output_file_name() + self._output_file_name = self._config.output_file_name() # Data names of data to output to the snapshot database - self._write_data_names = self._config.get_write_data_names() + self._write_data_names = self._config.write_data_names() # Data names of data to read as input parameter to the simulations - self._read_data_names = self._config.get_read_data_names() + self._read_data_names = self._config.read_data_names() - self._micro_dt = self._config.get_micro_dt() + self._micro_dt = self._config.micro_dt() # Path to the parameter file containing input parameters for micro simulations - self._parameter_file = self._config.get_parameter_file_name() + self._parameter_file = self._config.parameter_file_name() # Get name of pos-processing script - self._post_processing_file_name = self._config.get_postprocessing_file_name() + self._post_processing_file_name = self._config.postprocessing_file_name() # Check if simulation object can be re-used. - self._initialize_once = self._config.create_single_sim_object() + self._initialize_once = self._config.enable_single_sim_object() # Collect crashed indices self._crashed_snapshots: list[int] = [] # Declaration @@ -87,7 +87,7 @@ def solve(self) -> None: micro_problem_cls = create_simulation_class( self._logger, self._micro_problem, - self._config.get_micro_file_name(), + self._config.micro_file_name(), 1, None, ) @@ -262,7 +262,7 @@ def initialize(self) -> None: for i in range(self._local_number_of_sims): self._global_ids_of_local_sims.append(sim_id) sim_id += 1 - self._micro_problem = load_backend_class(self._config.get_micro_file_name()) + self._micro_problem = load_backend_class(self._config.micro_file_name()) self._micro_sims_have_output = False if hasattr(self._micro_problem, "output") and callable( diff --git a/tests/unit/test_adaptivity_parallel.py b/tests/unit/test_adaptivity_parallel.py index fd16182a..a06b55fd 100644 --- a/tests/unit/test_adaptivity_parallel.py +++ b/tests/unit/test_adaptivity_parallel.py @@ -39,8 +39,8 @@ def setUp(self): self._size = self._comm.Get_size() self._configurator = MagicMock() - self._configurator.get_output_dir = MagicMock(return_value="output_dir") - self._configurator.get_micro_file_name = MagicMock( + self._configurator.output_dir = MagicMock(return_value="output_dir") + self._configurator.micro_file_name = MagicMock( return_value="test_adaptivity_parallel" ) @@ -57,7 +57,7 @@ def test_update_inactive_sims_global_adaptivity(self): expected_is_sim_active = np.array([True, False, True, True, True]) expected_sim_is_associated_to = [-2, 3, -2, -2, -2] - self._configurator.get_adaptivity_similarity_measure = MagicMock( + self._configurator.adaptivity_similarity_measure = MagicMock( return_value="L1" ) @@ -137,10 +137,10 @@ def test_update_all_active_sims_global_adaptivity(self): expected_is_sim_active = np.array([False, False, True, False, True]) expected_sim_is_associated_to = [4, 2, -2, 2, -2] - self._configurator.get_adaptivity_hist_param = MagicMock(return_value=0.1) - self._configurator.get_adaptivity_refining_const = MagicMock(return_value=0.5) - self._configurator.get_adaptivity_coarsening_const = MagicMock(return_value=0.3) - self._configurator.get_adaptivity_similarity_measure = MagicMock( + self._configurator.adaptivity_history_param = MagicMock(return_value=0.1) + self._configurator.adaptivity_refining_constant = MagicMock(return_value=0.5) + self._configurator.adaptivity_coarsening_constant = MagicMock(return_value=0.3) + self._configurator.adaptivity_similarity_measure = MagicMock( return_value="L2rel" ) @@ -204,7 +204,7 @@ def test_communicate_micro_output(self): sim_output = [output_1, None] expected_sim_output = [output_1, output_0] - self._configurator.get_adaptivity_similarity_measure = MagicMock( + self._configurator.adaptivity_similarity_measure = MagicMock( return_value="L1" ) @@ -245,7 +245,7 @@ def test_get_ranks_of_sims(self): The first three simulations are on rank 0, and the last two on rank 1. The expected ranks of simulations are [0, 0, 0, 1, 1]. """ - self._configurator.get_adaptivity_similarity_measure = MagicMock( + self._configurator.adaptivity_similarity_measure = MagicMock( return_value="L1" ) diff --git a/tests/unit/test_adaptivity_serial.py b/tests/unit/test_adaptivity_serial.py index cee62f58..f6bba432 100644 --- a/tests/unit/test_adaptivity_serial.py +++ b/tests/unit/test_adaptivity_serial.py @@ -92,9 +92,9 @@ def test_update_similarity_dists(self): Test functionality of calculating the similarity distance matrix in class AdaptivityCalculator. """ configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) @@ -142,9 +142,9 @@ def test_update_active_sims(self): Test functionality of updating active simulations in class LocalAdaptivityCalculator. """ configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) @@ -180,9 +180,9 @@ def test_adaptivity_norms(self): Test functionality for calculating similarity criteria between pairs of simulations using different norms in class AdaptivityCalculator. """ configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) @@ -282,9 +282,9 @@ def test_adaptivity_norms_with_zeros_no_warning(self): import warnings configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L2rel") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L2rel") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) adaptivity_l2rel = AdaptivityCalculator( @@ -297,11 +297,11 @@ def test_adaptivity_norms_with_zeros_no_warning(self): ) configurator_l1 = MagicMock() - configurator_l1.get_adaptivity_similarity_measure = MagicMock( + configurator_l1.adaptivity_similarity_measure = MagicMock( return_value="L1rel" ) - configurator_l1.get_output_dir = MagicMock(return_value="output_dir") - configurator_l1.get_micro_file_name = MagicMock( + configurator_l1.output_dir = MagicMock(return_value="output_dir") + configurator_l1.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) adaptivity_l1rel = AdaptivityCalculator( @@ -338,9 +338,9 @@ def test_associate_active_to_inactive(self): Test functionality to associate inactive sims to active ones, in the class AdaptivityCalculator. """ configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) @@ -381,9 +381,9 @@ def test_update_inactive_sims_local_adaptivity(self): Test functionality to update inactive simulations in a particular setting, for a local adaptivity setting. """ configurator = MagicMock() - configurator.get_adaptivity_similarity_measure = MagicMock(return_value="L1") - configurator.get_output_dir = MagicMock(return_value="output_dir") - configurator.get_micro_file_name = MagicMock( + configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") + configurator.output_dir = MagicMock(return_value="output_dir") + configurator.micro_file_name = MagicMock( return_value="test_adaptivity_serial" ) diff --git a/tests/unit/test_domain_decomposition.py b/tests/unit/test_domain_decomposition.py index a3d1e14b..094c1b79 100644 --- a/tests/unit/test_domain_decomposition.py +++ b/tests/unit/test_domain_decomposition.py @@ -28,11 +28,11 @@ def test_rank2_out_of_4_2d(self): """ Check bounds for rank 2 in a setting of axis-wise ranks: [2, 2] """ - self._configuration_mock.get_decomposition_type.return_value = "uniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "uniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_2d ) - self._configuration_mock.get_ranks_per_axis.return_value = [2, 2] + self._configuration_mock.ranks_per_axis.return_value = [2, 2] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=2, size=4) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -43,11 +43,11 @@ def test_rank1_out_of_4_3d(self): """ Check bounds for rank 1 in a setting of axis-wise ranks: [2, 2, 1] """ - self._configuration_mock.get_decomposition_type.return_value = "uniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "uniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_3d ) - self._configuration_mock.get_ranks_per_axis.return_value = [2, 2, 1] + self._configuration_mock.ranks_per_axis.return_value = [2, 2, 1] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=1, size=4) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -58,11 +58,11 @@ def test_rank5_outof_10_3d(self): """ Test domain decomposition for rank 5 in a setting of axis-wise ranks: [1, 2, 5] """ - self._configuration_mock.get_decomposition_type.return_value = "uniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "uniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_3d ) - self._configuration_mock.get_ranks_per_axis.return_value = [1, 2, 5] + self._configuration_mock.ranks_per_axis.return_value = [1, 2, 5] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=5, size=10) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -73,11 +73,11 @@ def test_rank10_out_of_32_3d(self): """ Test domain decomposition for rank 10 in a setting of axis-wise ranks: [4, 1, 8] """ - self._configuration_mock.get_decomposition_type.return_value = "uniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "uniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_3d ) - self._configuration_mock.get_ranks_per_axis.return_value = [4, 1, 8] + self._configuration_mock.ranks_per_axis.return_value = [4, 1, 8] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=10, size=32) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -88,11 +88,11 @@ def test_rank7_out_of_16_3d(self): """ Test domain decomposition for rank 7 in a setting of axis-wise ranks: [8, 2, 1] """ - self._configuration_mock.get_decomposition_type.return_value = "uniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "uniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_3d ) - self._configuration_mock.get_ranks_per_axis.return_value = [8, 2, 1] + self._configuration_mock.ranks_per_axis.return_value = [8, 2, 1] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=7, size=16) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -124,12 +124,12 @@ def test_nonuniform_rank2_out_of_4_2d(self): Check non-uniform bounds for rank 2 in a setting of axis-wise ranks: [2, 2]. Along each axis, the local width doubles from one rank to the next. """ - self._configuration_mock.get_decomposition_type.return_value = "nonuniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "nonuniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_2d ) - self._configuration_mock.get_ranks_per_axis.return_value = [2, 2] - self._configuration_mock.get_minimum_access_region_size.return_value = [] + self._configuration_mock.ranks_per_axis.return_value = [2, 2] + self._configuration_mock.minimum_access_region_size.return_value = [] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=2, size=4) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -141,10 +141,10 @@ def test_nonuniform_rank15_out_of_128_2d(self): Check non-uniform bounds for rank 15 in a setting of axis-wise ranks: [16, 8]. Along each axis, the local width doubles from one rank to the next. """ - self._configuration_mock.get_decomposition_type.return_value = "nonuniform" - self._configuration_mock.get_macro_domain_bounds.return_value = [0, 1, 0, 0.5] - self._configuration_mock.get_ranks_per_axis.return_value = [16, 8] - self._configuration_mock.get_minimum_access_region_size.return_value = [ + self._configuration_mock.decomposition_type.return_value = "nonuniform" + self._configuration_mock.macro_domain_bounds.return_value = [0, 1, 0, 0.5] + self._configuration_mock.ranks_per_axis.return_value = [16, 8] + self._configuration_mock.minimum_access_region_size.return_value = [ 1.0 / 256.0, 1.0 / 128.0, ] @@ -173,12 +173,12 @@ def test_nonuniform_rank1_out_of_4_3d(self): """ Check non-uniform bounds for rank 1 in a setting of axis-wise ranks: [2, 2, 1]. """ - self._configuration_mock.get_decomposition_type.return_value = "nonuniform" - self._configuration_mock.get_macro_domain_bounds.return_value = ( + self._configuration_mock.decomposition_type.return_value = "nonuniform" + self._configuration_mock.macro_domain_bounds.return_value = ( self._macro_bounds_3d ) - self._configuration_mock.get_ranks_per_axis.return_value = [2, 2, 1] - self._configuration_mock.get_minimum_access_region_size.return_value = [] + self._configuration_mock.ranks_per_axis.return_value = [2, 2, 1] + self._configuration_mock.minimum_access_region_size.return_value = [] domain_decomposer = DomainDecomposer(self._configuration_mock, rank=1, size=4) mesh_bounds = domain_decomposer.get_local_mesh_bounds() @@ -191,7 +191,7 @@ def test_nonuniform_invalid_processor_count_raises(self): """ A mismatch between `ranks_per_axis` and communicator size should raise a ValueError. """ - self._configuration_mock.get_decomposition_type.return_value = "nonuniform" + self._configuration_mock.decomposition_type.return_value = "nonuniform" domain_decomposer = DomainDecomposer(self._configuration_mock, rank=0, size=4) @@ -218,7 +218,7 @@ def test_no_duplicates(self): ] all_ids = [[0, 1], [2, 3]] - self._configuration_mock.get_decomposition_type.return_value = "uniform" + self._configuration_mock.decomposition_type.return_value = "uniform" coords, ids = DomainDecomposer( self._configuration_mock, rank=0, size=2 @@ -242,7 +242,7 @@ def test_duplicate_on_boundary_rank0_keeps(self): ] all_ids = [[0, 1], [1, 2]] - self._configuration_mock.get_decomposition_type.return_value = "uniform" + self._configuration_mock.decomposition_type.return_value = "uniform" # Rank 0 should keep both its coords coords0, ids0 = DomainDecomposer( @@ -270,7 +270,7 @@ def test_duplicate_on_boundary_three_ranks(self): ] all_ids = [[0, 1], [0, 2], [0, 3]] - self._configuration_mock.get_decomposition_type.return_value = "uniform" + self._configuration_mock.decomposition_type.return_value = "uniform" coords0, _ = DomainDecomposer( self._configuration_mock, rank=0, size=3 diff --git a/tests/unit/test_load_balancing.py b/tests/unit/test_load_balancing.py index e294c261..69f1193e 100644 --- a/tests/unit/test_load_balancing.py +++ b/tests/unit/test_load_balancing.py @@ -81,8 +81,8 @@ def test_redistribute_no_time_two_ranks(self): micro_sims.append(sim_cls(i)) config = MagicMock() - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") load_balancer = LoadBalancer( MagicMock(), ModelManager(sim_cls), @@ -157,8 +157,8 @@ def set_is_on_rank(self, *args, **kwargs): micro_sims.append(sim_cls(i)) config = MagicMock() - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") load_balancer = LoadBalancer( MagicMock(), ModelManager(sim_cls), @@ -217,10 +217,10 @@ def test_redistribute_active_sims_two_ranks(self): Run this test in parallel using MPI with 2 ranks. """ config = MagicMock() - config.get_load_balancing_threshold = MagicMock(return_value=0) - config.turn_on_load_balancing_inactive = MagicMock(return_value=False) - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.load_balancing_threshold = MagicMock(return_value=0) + config.enable_load_balancing_inactive = MagicMock(return_value=False) + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") global_number_of_sims = 8 @@ -292,10 +292,10 @@ def test_redistribute_inactive_sims_two_ranks(self): Run this test in parallel using MPI with 2 ranks. """ config = MagicMock() - config.get_load_balancing_threshold = MagicMock(return_value=0) - config.turn_on_load_balancing_inactive = MagicMock(return_value=True) - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.load_balancing_threshold = MagicMock(return_value=0) + config.enable_load_balancing_inactive = MagicMock(return_value=True) + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") global_number_of_sims = 5 if self._rank == 0: @@ -366,10 +366,10 @@ def test_redistribute_active_sims_four_ranks(self): Run this test in parallel using MPI with 4 ranks. """ config = MagicMock() - config.get_load_balancing_threshold = MagicMock(return_value=0) - config.turn_on_load_balancing_inactive = MagicMock(return_value=False) - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.load_balancing_threshold = MagicMock(return_value=0) + config.enable_load_balancing_inactive = MagicMock(return_value=False) + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") global_number_of_sims = 15 @@ -470,10 +470,10 @@ def test_redistribute_inactive_sims_four_ranks(self): Run this test in parallel using MPI with 4 ranks. """ config = MagicMock() - config.get_load_balancing_threshold = MagicMock(return_value=0) - config.turn_on_load_balancing_inactive = MagicMock(return_value=True) - config.turn_on_load_balancing = MagicMock(return_value=True) - config.get_load_balancing_partitioning = MagicMock(return_value="lpt") + config.load_balancing_threshold = MagicMock(return_value=0) + config.enable_load_balancing_inactive = MagicMock(return_value=True) + config.enable_load_balancing = MagicMock(return_value=True) + config.load_balancing_partitioning = MagicMock(return_value="lpt") global_number_of_sims = 15 # global_active_gids = [0, 1, 2, 8, 12, 13, 14] diff --git a/tests/unit/test_micro_manager.py b/tests/unit/test_micro_manager.py index 35ae8ea4..f347129e 100644 --- a/tests/unit/test_micro_manager.py +++ b/tests/unit/test_micro_manager.py @@ -128,20 +128,20 @@ def test_config(self): config.set_logger(MagicMock()) config.read_json_micro_manager() self.assertEqual( - config._precice_config_file_name.split("/")[-1], "dummy-config.xml" + config.precice_config_file_name().split("/")[-1], "dummy-config.xml" ) - self.assertEqual(config._micro_file_name, "test_micro_manager") - self.assertEqual(config._macro_mesh_name, "Macro-Mesh") - self.assertEqual(config._micro_output_n, 10) - self.assertListEqual(config._read_data_names, self.fake_read_data_names) - self.assertListEqual(self.fake_write_data_names, config._write_data_names) - self.assertEqual(config._adaptivity, True) - self.assertListEqual(config._data_for_adaptivity, self.fake_read_data_names) - self.assertEqual(config._adaptivity_type, "local") - self.assertEqual(config._adaptivity_history_param, 0.5) - self.assertEqual(config._adaptivity_coarsening_constant, 0.3) - self.assertEqual(config._adaptivity_refining_constant, 0.4) - self.assertEqual(config._adaptivity_every_implicit_iteration, False) + self.assertEqual(config.micro_file_name(), "test_micro_manager") + self.assertEqual(config.macro_mesh_name(), "Macro-Mesh") + self.assertEqual(config.micro_output_n(), 10) + self.assertListEqual(config.read_data_names(), self.fake_read_data_names) + self.assertListEqual(self.fake_write_data_names, config.write_data_names()) + self.assertEqual(config.enable_adaptivity(), True) + self.assertListEqual(config.data_for_adaptivity(), self.fake_read_data_names) + self.assertEqual(config.adaptivity_type(), "local") + self.assertEqual(config.adaptivity_history_param(), 0.5) + self.assertEqual(config.adaptivity_coarsening_constant(), 0.3) + self.assertEqual(config.adaptivity_refining_constant(), 0.4) + self.assertEqual(config.enable_adaptivity_each_implicit_iteration(), False) if __name__ == "__main__": diff --git a/tests/unit/test_snapshot_computation.py b/tests/unit/test_snapshot_computation.py index 55e7d617..d90a3a1d 100644 --- a/tests/unit/test_snapshot_computation.py +++ b/tests/unit/test_snapshot_computation.py @@ -148,13 +148,13 @@ def test_config(self): config.read_json_snapshot() self.assertEqual( - config._parameter_file_name.split("/")[-1], "test_parameter.hdf5" + config.parameter_file_name().split("/")[-1], "test_parameter.hdf5" ) - self.assertEqual(config._micro_file_name, "test_snapshot_computation") - self.assertListEqual(config._read_data_names, self.fake_read_data_names) - self.assertListEqual(config._write_data_names, self.fake_write_data_names) - self.assertEqual(config._postprocessing_file_name, "snapshot_post_processing") - self.assertTrue(config._initialize_once) + self.assertEqual(config.micro_file_name(), "test_snapshot_computation") + self.assertListEqual(config.read_data_names(), self.fake_read_data_names) + self.assertListEqual(config.write_data_names(), self.fake_write_data_names) + self.assertEqual(config.postprocessing_file_name(), "snapshot_post_processing") + self.assertTrue(config.enable_single_sim_object()) if __name__ == "__main__": From 2acaac02c66d97abb446fb832144b8610f9a871a Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Fri, 15 May 2026 17:00:29 +0200 Subject: [PATCH 2/6] apply format --- micro_manager/adaptivity/model_adaptivity.py | 4 +- micro_manager/config.py | 370 ++++++++++++------- tests/unit/test_adaptivity_parallel.py | 12 +- tests/unit/test_adaptivity_serial.py | 28 +- 4 files changed, 248 insertions(+), 166 deletions(-) diff --git a/micro_manager/adaptivity/model_adaptivity.py b/micro_manager/adaptivity/model_adaptivity.py index a31ddc27..93f34387 100644 --- a/micro_manager/adaptivity/model_adaptivity.py +++ b/micro_manager/adaptivity/model_adaptivity.py @@ -55,9 +55,7 @@ def __init__( self._comm = comm self._model_manager = model_manager self._model_files = config.model_adaptivity_file_names() - self._switching_func_name = ( - config.model_adaptivity_switching_function() - ) + self._switching_func_name = config.model_adaptivity_switching_function() stateless_flags = config.model_adaptivity_micro_stateless() self._model_classes = [] diff --git a/micro_manager/config.py b/micro_manager/config.py index 648bb3c6..c5653794 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -17,6 +17,7 @@ class ConfigDSL: Provides a standardized context to read data from JSON. Data retrieval can be set up to yield optionals, with default values or raise Errors. """ + def __init__(self, data: Dict, log: Logger): """ Constructs a context for the JSON data. @@ -68,12 +69,12 @@ def exists(self): return True def get_or_none( - self, - fmt_success: Optional[str]=None, - fmt_error: Optional[str]=None, - dtype: Optional[Type]=None, - options: Optional[List]=None, - **kwargs + self, + fmt_success: Optional[str] = None, + fmt_error: Optional[str] = None, + dtype: Optional[Type] = None, + options: Optional[List] = None, + **kwargs, ) -> Optional[Any]: """ Resolves the value specified by the set path. Returns the result if available, otherwise None. @@ -99,16 +100,18 @@ def get_or_none( result: Optional[Any] None if path not available or other failure, else value of JSON. """ - return self.get_with_default(None, fmt_success, fmt_error, dtype, options, **kwargs) + return self.get_with_default( + None, fmt_success, fmt_error, dtype, options, **kwargs + ) def get_with_default( - self, - default: Any, - fmt_success: Optional[str] = None, - fmt_error: Optional[str] = None, - dtype: Optional[Type] = None, - options: Optional[List] = None, - **kwargs + self, + default: Any, + fmt_success: Optional[str] = None, + fmt_error: Optional[str] = None, + dtype: Optional[Type] = None, + options: Optional[List] = None, + **kwargs, ) -> Any: """ Resolves the value specified by the set path. Returns the result if available, otherwise the specified default. @@ -153,12 +156,12 @@ def get_with_default( return current_element def get_or_raise( - self, - fmt_success: Optional[str] = None, - fmt_error: Optional[str] = None, - dtype: Optional[Type] = None, - options: Optional[List] = None, - **kwargs + self, + fmt_success: Optional[str] = None, + fmt_error: Optional[str] = None, + dtype: Optional[Type] = None, + options: Optional[List] = None, + **kwargs, ): """ Resolves the value specified by the set path. Returns the result if available, otherwise throws. @@ -201,9 +204,9 @@ def get_or_raise( return current_element def handle_fmt( - self, - fmt: Optional[str], - kwargs: Optional[Dict[str, Any]], + self, + fmt: Optional[str], + kwargs: Optional[Dict[str, Any]], ) -> None: """ Processes the message and prints to the logger. @@ -230,10 +233,7 @@ def handle_fmt( self._log.log_info_rank_zero(msg) def handle_fmt_error( - self, - fmt_error: str, - ex: BaseException, - kwargs: Dict[str, Any] + self, fmt_error: str, ex: BaseException, kwargs: Dict[str, Any] ) -> None: """ Processes the error message and prints to the logger. @@ -251,10 +251,10 @@ def handle_fmt_error( self.handle_fmt(fmt_error, kwargs) def handle_fmt_success( - self, - fmt_success: str, - data: Any, - kwargs: Dict[str, Any], + self, + fmt_success: str, + data: Any, + kwargs: Dict[str, Any], ) -> None: """ Processes the success message and prints to the logger. @@ -272,6 +272,7 @@ def handle_fmt_success( self.handle_fmt(fmt_success, kwargs) _formatter = string.Formatter() + @staticmethod def fmt_check_key(fmt: str, key: str) -> bool: """ @@ -314,11 +315,12 @@ def member_name(self): ... The additional write access should simplify writing to the dict. """ + def __init__(self, func: Callable): - self._func : Callable = func - self._instance : Optional[Config] = None + self._func: Callable = func + self._instance: Optional[Config] = None - def attach_instance(self, instance:"Config"): + def attach_instance(self, instance: "Config"): self._instance = instance @property @@ -396,13 +398,17 @@ def __init__(self, config_file_name: str): Path to the JSON configuration file """ self._config_file_name: str = config_file_name - self._base_dir: str = os.path.dirname(os.path.join(os.getcwd(), config_file_name)) + self._base_dir: str = os.path.dirname( + os.path.join(os.getcwd(), config_file_name) + ) self._logger: Optional[Logger] = None self._data: Optional[Dict[str, Any]] = None self._fields: Dict[str, Any] = defaultdict(lambda: None) # attach external backing field to all config entries - config_entries = inspect.getmembers(self, lambda f: hasattr(f, "attach_instance")) + config_entries = inspect.getmembers( + self, lambda f: hasattr(f, "attach_instance") + ) for _, e in config_entries: e.attach_instance(self) @@ -435,7 +441,9 @@ def _read_json_base(self, config_file_name: str): Path to the JSON configuration file """ assert self._logger is not None - self._logger.log_info_rank_zero(f"Micro Manager version: {importlib.metadata.version('micro-manager-precice')}") + self._logger.log_info_rank_zero( + f"Micro Manager version: {importlib.metadata.version('micro-manager-precice')}" + ) path = os.path.join(self._base_dir, os.path.basename(config_file_name)) with open(path, "r") as read_file: self._data = json.load(read_file) @@ -447,49 +455,61 @@ def _read_json_base(self, config_file_name: str): # ====================================================== # convert paths to python-importable paths - self.micro_file_name.set = (self.json["micro_file_name"].get_or_raise( - "Micro simulation file name: {data}", - "'micro_file_name' must be specified!", - str - ).replace("/", ".") - .replace("\\", ".") - .replace(".py", "")) + self.micro_file_name.set = ( + self.json["micro_file_name"] + .get_or_raise( + "Micro simulation file name: {data}", + "'micro_file_name' must be specified!", + str, + ) + .replace("/", ".") + .replace("\\", ".") + .replace(".py", "") + ) self.enable_micro_stateless.set = self.json["micro_stateless"].get_with_default( False, "Only creating one full instance of Micro Model.", "Creating full instance of Micro Model per mesh vertex.", - bool + bool, ) self.output_dir.set = self.json["output_directory"].get_or_none( "Logging and metrics output directory: {data}", - "No output directory provided. Output (including logging) will be saved in the current working directory." + "No output directory provided. Output (including logging) will be saved in the current working directory.", ) - self.memory_usage_output_type.set = self.json["memory_usage_output_type"].get_with_default( + self.memory_usage_output_type.set = self.json[ + "memory_usage_output_type" + ].get_with_default( "", "Memory usage output type: {data}", "Micro Manager will not output memory usage.", options=["all", "local", "global"], ) - self.memory_usage_output_n.set = self.json["memory_usage_output_n"].get_with_default( + self.memory_usage_output_n.set = self.json[ + "memory_usage_output_n" + ].get_with_default( 1, "Memory usage will be output every {data} time windows.", - "No output interval for memory usage output provided. Memory usage will be output every time window." + "No output interval for memory usage output provided. Memory usage will be output every time window.", ) - self.write_data_names.set = self.json["coupling_params"]["write_data_names"].get_or_none( + self.write_data_names.set = self.json["coupling_params"][ + "write_data_names" + ].get_or_none( "Micro Manager is writing the following data: {data}", "No write data names provided. Micro manager will only read data from preCICE.", - list + list, ) - self.read_data_names.set = self.json["coupling_params"]["read_data_names"].get_or_none( + self.read_data_names.set = self.json["coupling_params"][ + "read_data_names" + ].get_or_none( "Micro Manager is reading the following data: {data}", "No read data names provided. Micro manager will only write data to preCICE.", - list + list, ) self.micro_dt.set = self.json["simulation_params"]["micro_dt"].get_or_raise() @@ -503,14 +523,18 @@ def _read_json_base(self, config_file_name: str): "socket", "Tasking backend: {data}", "No tasking backed defined. Falling back to sockets.", - options=["mpi", "socket"] + options=["mpi", "socket"], ) - self.enable_tasking_slurm.set = self.json["tasking"]["is_slurm"].get_with_default( + self.enable_tasking_slurm.set = self.json["tasking"][ + "is_slurm" + ].get_with_default( False, "Tasking using slurm: {data}", "No tasking slurm flag defined. Assuming non-slurm system.", ) - self.tasking_num_workers.set = self.json["tasking"]["num_workers"].get_with_default( + self.tasking_num_workers.set = self.json["tasking"][ + "num_workers" + ].get_with_default( 1, "Tasking will use {data} workers", "No tasking worker count defined. Using 1 worker per rank.", @@ -521,10 +545,12 @@ def _read_json_base(self, config_file_name: str): "No tasking mpi implementation defined. Assuming open mpi.", options=["open", "mpi"], ) - self.tasking_hostfile.set = self.json["tasking"]["hostfile"].get_with_default( + self.tasking_hostfile.set = self.json["tasking"][ + "hostfile" + ].get_with_default( "./hosts.micro", "Tasking will use nodes from hostlist file: {data}", - "No hostfile for tasking defined. Using hosts.micro as default." + "No hostfile for tasking defined. Using hosts.micro as default.", ) def read_json_micro_manager(self): @@ -538,83 +564,105 @@ def read_json_micro_manager(self): self.precice_config_file_name.set = os.path.join( self._base_dir, self._data["coupling_params"]["precice_config_file_name"] ) - self._logger.log_info_rank_zero(f"preCICE configuration file name: {self.precice_config_file_name()}") + self._logger.log_info_rank_zero( + f"preCICE configuration file name: {self.precice_config_file_name()}" + ) # ====================================================== # Mesh and Decomposition # ====================================================== - self.macro_mesh_name.set = self.json["coupling_params"]["macro_mesh_name"].get_or_raise( - "Macro mesh name: {data}" - ) + self.macro_mesh_name.set = self.json["coupling_params"][ + "macro_mesh_name" + ].get_or_raise("Macro mesh name: {data}") - self.macro_domain_bounds.set = self.json["simulation_params"]["macro_domain_bounds"].get_or_raise( - "Macro domain bounds: {data}" - ) + self.macro_domain_bounds.set = self.json["simulation_params"][ + "macro_domain_bounds" + ].get_or_raise("Macro domain bounds: {data}") - self.ranks_per_axis.set = self.json["simulation_params"]["decomposition"].get_with_default( + self.ranks_per_axis.set = self.json["simulation_params"][ + "decomposition" + ].get_with_default( [1, 1, 1], "Axis-wise domain decomposition: {data}", "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial.", list, ) - self.decomposition_type.set = self.json["simulation_params"]["decomposition_type"].get_with_default( + self.decomposition_type.set = self.json["simulation_params"][ + "decomposition_type" + ].get_with_default( "uniform", "Domain decomposition type: {data}", "Domain decomposition is not specified, so the Micro Manager will expect to be run in serial.", str, - ["uniform", "nonuniform"] + ["uniform", "nonuniform"], ) self.minimum_access_region_size.set = [] if self.decomposition_type() == "nonuniform": - self.minimum_access_region_size.set = self.json["simulation_params"]["minimum_access_region_size"].get_with_default( + self.minimum_access_region_size.set = self.json["simulation_params"][ + "minimum_access_region_size" + ].get_with_default( [], None, - "Minimum access region size is not specified. Calculating it as 1 / (2^ranks_per_axis - 1) of the macro domain size in each axis." - + "Minimum access region size is not specified. Calculating it as 1 / (2^ranks_per_axis - 1) of the macro domain size in each axis.", ) # ====================================================== # Adaptivity # ====================================================== - self.enable_adaptivity.set = self.json["simulation_params"]["adaptivity"].get_with_default( + self.enable_adaptivity.set = self.json["simulation_params"][ + "adaptivity" + ].get_with_default( False, "Micro Manager will adaptively run micro simulations.", None, bool, ) - adaptivity_settings_avail = self.json["simulation_params"]["adaptivity_settings"].exists() + adaptivity_settings_avail = self.json["simulation_params"][ + "adaptivity_settings" + ].exists() if self.enable_adaptivity() and not adaptivity_settings_avail: self.enable_adaptivity.set = False - self._logger.log_info_rank_zero("Adaptivity is turned on but no adaptivity settings are provided.") + self._logger.log_info_rank_zero( + "Adaptivity is turned on but no adaptivity settings are provided." + ) self._logger.log_info_rank_zero( "Micro Manager will not adaptively run micro simulations, but instead will run all micro simulations." ) if not self.enable_adaptivity() and adaptivity_settings_avail: - self._logger.log_info_rank_zero("Adaptivity settings are provided but adaptivity is turned off.") + self._logger.log_info_rank_zero( + "Adaptivity settings are provided but adaptivity is turned off." + ) if self.enable_adaptivity(): - self.adaptivity_type.set = self.json["simulation_params"]["adaptivity_settings"]["type"].get_with_default( + self.adaptivity_type.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["type"].get_with_default( "local", "Adaptivity type: {data}", "Adaptivity type can be either local or global.", options=["local", "global"], ) - self.adaptivity_mapping_configs.set = self.json["simulation_params"]["adaptivity_settings"]["mappings"].get_with_default( + self.adaptivity_mapping_configs.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["mappings"].get_with_default( [], None, - "Adaptivity will not interpolate outputs, only use representatives." + "Adaptivity will not interpolate outputs, only use representatives.", ) - self.enable_adaptivity_lazy_init.set = self.json["simulation_params"]["adaptivity_settings"]["lazy_initialization"].get_with_default(False) + self.enable_adaptivity_lazy_init.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["lazy_initialization"].get_with_default(False) if self.enable_adaptivity_lazy_init(): self._logger.log_info_rank_zero( "Micro simulations will be created only when they are required to be active for the very first time." ) - self.data_for_adaptivity.set = self.json["simulation_params"]["adaptivity_settings"]["data"].get_or_raise( - "Data used for adaptivity: {data}", - "Adaptivity Data must be provided." + self.data_for_adaptivity.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["data"].get_or_raise( + "Data used for adaptivity: {data}", "Adaptivity Data must be provided." ) if self.data_for_adaptivity() == self.write_data_names(): self._logger.log_info_rank_zero( @@ -622,44 +670,55 @@ def read_json_micro_manager(self): " same set of active and inactive simulations for the entire simulation time. If this is not intended," " please include macro data as well." ) - self.adaptivity_n.set = self.json["simulation_params"]["adaptivity_settings"]["adaptivity_every_n_time_windows"].get_with_default( + self.adaptivity_n.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["adaptivity_every_n_time_windows"].get_with_default( 1, "Adaptivity will be computed every {data} time windows.", - "No interval for adaptivity computation provided. Adaptivity will be computed in every time window." + "No interval for adaptivity computation provided. Adaptivity will be computed in every time window.", ) - self.adaptivity_output_type.set = self.json["simulation_params"]["adaptivity_settings"]["output_type"].get_with_default( + self.adaptivity_output_type.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["output_type"].get_with_default( "", "Adaptivity output type: {data}", "Adaptivity output type can be either 'all', 'local' or 'global'. No metrics will be output.", options=["all", "local", "global"], ) - self.adaptivity_output_n.set = self.json["simulation_params"]["adaptivity_settings"]["output_n"].get_with_default( + self.adaptivity_output_n.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["output_n"].get_with_default( 1, "Adaptivity will be computed every {data} time windows.", "No output interval for adaptivity provided. Adaptivity metrics will be output every time window.", ) - self.adaptivity_history_param.set = self.json["simulation_params"]["adaptivity_settings"]["history_param"].get_or_raise( - "Adaptivity history parameter: {data}" - ) - self.adaptivity_coarsening_constant.set = self.json["simulation_params"]["adaptivity_settings"]["coarsening_constant"].get_or_raise( + self.adaptivity_history_param.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["history_param"].get_or_raise("Adaptivity history parameter: {data}") + self.adaptivity_coarsening_constant.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["coarsening_constant"].get_or_raise( "Adaptivity coarsening constant: {data}" ) - self.adaptivity_refining_constant.set = self.json["simulation_params"]["adaptivity_settings"]["refining_constant"].get_or_raise( - "Adaptivity refining constant: {data}" - ) - self.adaptivity_similarity_measure.set = self.json["simulation_params"]["adaptivity_settings"]["similarity_measure"].get_with_default( + self.adaptivity_refining_constant.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["refining_constant"].get_or_raise("Adaptivity refining constant: {data}") + self.adaptivity_similarity_measure.set = self.json["simulation_params"][ + "adaptivity_settings" + ]["similarity_measure"].get_with_default( "L1", "Adaptivity similarity measure: {data}", - "No similarity measure provided, using L1 norm as default." + "No similarity measure provided, using L1 norm as default.", ) - self.enable_adaptivity_each_implicit_iteration.set = self.json["simulation_params"]["adaptivity_settings"]["every_implicit_iteration"].get_with_default( + self.enable_adaptivity_each_implicit_iteration.set = self.json[ + "simulation_params" + ]["adaptivity_settings"]["every_implicit_iteration"].get_with_default( False, - - "Micro Manager will compute adaptivity once at the start of every time window." + "Micro Manager will compute adaptivity once at the start of every time window.", ) if self.enable_adaptivity_each_implicit_iteration(): self._logger.log_info_rank_zero( - "Micro Manager will compute adaptivity in every implicit iteration, if implicit coupling is done." + "Micro Manager will compute adaptivity in every implicit iteration, if implicit coupling is done." ) else: self._logger.log_info_rank_zero( @@ -673,7 +732,9 @@ def read_json_micro_manager(self): # Load Balancing # ====================================================== - self.enable_load_balancing.set = self.json["simulation_params"]["load_balancing"].get_with_default(False) + self.enable_load_balancing.set = self.json["simulation_params"][ + "load_balancing" + ].get_with_default(False) if self.enable_load_balancing(): self._logger.log_info_rank_zero( "Micro Manager will dynamically balance micro simulations based on compute times." @@ -688,32 +749,44 @@ def read_json_micro_manager(self): else: self.write_data_names().append("rank_of_sim") - self.load_balancing_n.set = self.json["simulation_params"]["load_balancing_settings"]["every_n_time_windows"].get_with_default( + self.load_balancing_n.set = self.json["simulation_params"][ + "load_balancing_settings" + ]["every_n_time_windows"].get_with_default( 1, "Load balancing will be computed every {data} time windows.", - "Load balancing will be computed in every time window." + "Load balancing will be computed in every time window.", ) - self.load_balancing_type.set = self.json["simulation_params"]["load_balancing_settings"]["type"].get_with_default( + self.load_balancing_type.set = self.json["simulation_params"][ + "load_balancing_settings" + ]["type"].get_with_default( "time", "Load balancing type: {data}", "Load balancing will use time based balancing.", - options=["time", "active"] + options=["time", "active"], ) if self.load_balancing_type() == "active": - self.enable_load_balancing_inactive.set = self.json["simulation_params"]["load_balancing_settings"]["balance_inactive_simulations"].get_with_default( + self.enable_load_balancing_inactive.set = self.json[ + "simulation_params" + ]["load_balancing_settings"][ + "balance_inactive_simulations" + ].get_with_default( False, "Load balancing enable inactive balancing: {data}", "Load balancing will not balance inactive micro simulations.", - dtype=bool + dtype=bool, ) - self.load_balancing_threshold.set = self.json["simulation_params"]["load_balancing_settings"]["threshold"].get_with_default( + self.load_balancing_threshold.set = self.json["simulation_params"][ + "load_balancing_settings" + ]["threshold"].get_with_default( 0, "Load balancing threshold: {data}", "Load balancing will use 0 threshold.", ) if self.load_balancing_type() == "time": - self.load_balancing_partitioning.set = self.json["simulation_params"]["load_balancing_settings"]["partitioning"].get_with_default( + self.load_balancing_partitioning.set = self.json["simulation_params"][ + "load_balancing_settings" + ]["partitioning"].get_with_default( "lpt", "Load balancing partitioning: {data}", "No partitioning provided, using LPT as default.", @@ -724,13 +797,22 @@ def read_json_micro_manager(self): # Model Adaptivity # ====================================================== - self.enable_model_adaptivity.set = self.json["simulation_params"]["model_adaptivity"].get_with_default(False) - if self.enable_model_adaptivity() and not self.json["simulation_params"]["model_adaptivity_settings"].exists(): + self.enable_model_adaptivity.set = self.json["simulation_params"][ + "model_adaptivity" + ].get_with_default(False) + if ( + self.enable_model_adaptivity() + and not self.json["simulation_params"]["model_adaptivity_settings"].exists() + ): self.enable_model_adaptivity.set = False - self._logger.log_info_rank_zero("Model Adaptivity is turned on but no model adaptivity settings are provided.") + self._logger.log_info_rank_zero( + "Model Adaptivity is turned on but no model adaptivity settings are provided." + ) if self.enable_model_adaptivity(): - file_names = self.json["simulation_params"]["model_adaptivity_settings"]["micro_file_names"].get_or_raise() + file_names = self.json["simulation_params"]["model_adaptivity_settings"][ + "micro_file_names" + ].get_or_raise() self.model_adaptivity_file_names.set = [ name.replace("/", ".").replace("\\", ".").replace(".py", "") for name in file_names @@ -742,8 +824,12 @@ def read_json_micro_manager(self): self._logger.log_info_rank_zero("Disabling Model Adaptivity.") self.enable_model_adaptivity.set = False - self.model_adaptivity_switching_function.set = self.json["simulation_params"]["model_adaptivity_settings"]["switching_function"].get_or_raise() - self.model_adaptivity_micro_stateless.set = self.json["simulation_params"]["model_adaptivity_settings"]["micro_stateless"].get_with_default( + self.model_adaptivity_switching_function.set = self.json[ + "simulation_params" + ]["model_adaptivity_settings"]["switching_function"].get_or_raise() + self.model_adaptivity_micro_stateless.set = self.json["simulation_params"][ + "model_adaptivity_settings" + ]["micro_stateless"].get_with_default( [False] * len(file_names), ) @@ -761,18 +847,26 @@ def read_json_micro_manager(self): # Crash Interpolation and Diagnostics # ====================================================== - self.enable_crashed_sim_interpolation.set = self.json["simulation_params"]["interpolate_crash"].get_with_default(False) + self.enable_crashed_sim_interpolation.set = self.json["simulation_params"][ + "interpolate_crash" + ].get_with_default(False) if self.enable_crashed_sim_interpolation(): - self._logger.log_info_rank_zero("Micro Manager will interpolate output of crashed micro simulations from its neighbors.") + self._logger.log_info_rank_zero( + "Micro Manager will interpolate output of crashed micro simulations from its neighbors." + ) # TODO what? this is not being saved nor used - diagnostics_data_names = self.json["diagnostics"]["data_from_micro_sims"].get_or_none( + diagnostics_data_names = self.json["diagnostics"][ + "data_from_micro_sims" + ].get_or_none( None, "No diagnostics data is defined. Micro Manager will not output any diagnostics data.", list, ) - self.micro_output_n.set = self.json["diagnostics"]["micro_output_n"].get_with_default( + self.micro_output_n.set = self.json["diagnostics"][ + "micro_output_n" + ].get_with_default( 1, "Micro Manager will compute micro output every {data} time windows.", "Output interval of micro simulations not specified, if output is available then it will be called " @@ -785,43 +879,53 @@ def read_json_snapshot(self): """ assert self._logger is not None self._read_json_base(self._config_file_name) # Read base information - self._logger.log_info_rank_zero(f"Reading JSON configuration file: {self._config_file_name}") + self._logger.log_info_rank_zero( + f"Reading JSON configuration file: {self._config_file_name}" + ) self._logger.log_info_rank_zero("Micro Manager is running in snapshot mode.") self.parameter_file_name.set = os.path.join( - self._base_dir, self.json["coupling_params"]["parameter_file_name"].get_or_raise() + self._base_dir, + self.json["coupling_params"]["parameter_file_name"].get_or_raise(), + ) + self._logger.log_info_rank_zero( + f"Parameter file name: {self.parameter_file_name()}" ) - self._logger.log_info_rank_zero(f"Parameter file name: {self.parameter_file_name()}") - self.output_file_name.set = self.json["snapshot_params"]["output_file_name"].get_with_default( + self.output_file_name.set = self.json["snapshot_params"][ + "output_file_name" + ].get_with_default( "snapshot_data", "Output file name: {data}", - "No snapshot output file name provided. Defaulting to 'snapshot_data'." + "No snapshot output file name provided. Defaulting to 'snapshot_data'.", ) - post_proc_file = self.json["snapshot_params"]["post_processing_file_name"].get_or_none( + post_proc_file = self.json["snapshot_params"][ + "post_processing_file_name" + ].get_or_none( "Post-processing file name {data}", - "No post-processing file name provided. Snapshot computation will not perform any post-processing." + "No post-processing file name provided. Snapshot computation will not perform any post-processing.", ) if post_proc_file is not None: post_proc_file = ( - post_proc_file - .replace("/", ".") - .replace("\\", ".") - .replace(".py", "") + post_proc_file.replace("/", ".").replace("\\", ".").replace(".py", "") ) self.postprocessing_file_name.set = post_proc_file # TODO what? this is not being saved nor used - diagnostics_data_names = self.json["diagnostics"]["data_from_micro_sims"].get_or_none( + diagnostics_data_names = self.json["diagnostics"][ + "data_from_micro_sims" + ].get_or_none( "Diagnostics data: {data}", "No diagnostics data is defined. Micro Manager will not output any diagnostics data.", list, ) - self.enable_single_sim_object.set = self.json["snapshot_params"]["initialize_once"].get_with_default( + self.enable_single_sim_object.set = self.json["snapshot_params"][ + "initialize_once" + ].get_with_default( False, "Micro Manager will initialize only one micro simulations object for snapshot computation.", - "For each snapshot a new micro simulation object will be created." + "For each snapshot a new micro simulation object will be created.", ) @config_entry @@ -1413,4 +1517,4 @@ def mpi_impl(self) -> str: mpi_impl : str mpi implementation type """ - pass \ No newline at end of file + pass diff --git a/tests/unit/test_adaptivity_parallel.py b/tests/unit/test_adaptivity_parallel.py index a06b55fd..947d764a 100644 --- a/tests/unit/test_adaptivity_parallel.py +++ b/tests/unit/test_adaptivity_parallel.py @@ -57,9 +57,7 @@ def test_update_inactive_sims_global_adaptivity(self): expected_is_sim_active = np.array([True, False, True, True, True]) expected_sim_is_associated_to = [-2, 3, -2, -2, -2] - self._configurator.adaptivity_similarity_measure = MagicMock( - return_value="L1" - ) + self._configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") sim_cls = create_simulation_class( MagicMock(), @@ -204,9 +202,7 @@ def test_communicate_micro_output(self): sim_output = [output_1, None] expected_sim_output = [output_1, output_0] - self._configurator.adaptivity_similarity_measure = MagicMock( - return_value="L1" - ) + self._configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") sim_cls = create_simulation_class( MagicMock(), @@ -245,9 +241,7 @@ def test_get_ranks_of_sims(self): The first three simulations are on rank 0, and the last two on rank 1. The expected ranks of simulations are [0, 0, 0, 1, 1]. """ - self._configurator.adaptivity_similarity_measure = MagicMock( - return_value="L1" - ) + self._configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") if self._rank == 0: global_ids = [0, 1, 2] diff --git a/tests/unit/test_adaptivity_serial.py b/tests/unit/test_adaptivity_serial.py index f6bba432..214ed864 100644 --- a/tests/unit/test_adaptivity_serial.py +++ b/tests/unit/test_adaptivity_serial.py @@ -94,9 +94,7 @@ def test_update_similarity_dists(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_controller = AdaptivityCalculator( configurator, @@ -144,9 +142,7 @@ def test_update_active_sims(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_controller = LocalAdaptivityCalculator( configurator, @@ -182,9 +178,7 @@ def test_adaptivity_norms(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_controller = AdaptivityCalculator( configurator, @@ -284,9 +278,7 @@ def test_adaptivity_norms_with_zeros_no_warning(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L2rel") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_l2rel = AdaptivityCalculator( configurator, nsims=3, @@ -297,9 +289,7 @@ def test_adaptivity_norms_with_zeros_no_warning(self): ) configurator_l1 = MagicMock() - configurator_l1.adaptivity_similarity_measure = MagicMock( - return_value="L1rel" - ) + configurator_l1.adaptivity_similarity_measure = MagicMock(return_value="L1rel") configurator_l1.output_dir = MagicMock(return_value="output_dir") configurator_l1.micro_file_name = MagicMock( return_value="test_adaptivity_serial" @@ -340,9 +330,7 @@ def test_associate_active_to_inactive(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_controller = AdaptivityCalculator( configurator, @@ -383,9 +371,7 @@ def test_update_inactive_sims_local_adaptivity(self): configurator = MagicMock() configurator.adaptivity_similarity_measure = MagicMock(return_value="L1") configurator.output_dir = MagicMock(return_value="output_dir") - configurator.micro_file_name = MagicMock( - return_value="test_adaptivity_serial" - ) + configurator.micro_file_name = MagicMock(return_value="test_adaptivity_serial") adaptivity_controller = LocalAdaptivityCalculator( configurator, From a82d4af73382783fa70f123dc393ad4c4b8d4f69 Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Fri, 15 May 2026 18:08:48 +0200 Subject: [PATCH 3/6] add fixed to load default args --- micro_manager/config.py | 99 +++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/micro_manager/config.py b/micro_manager/config.py index c5653794..47ee533f 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -3,13 +3,14 @@ """ import json +import logging import os import importlib.metadata import string from collections import defaultdict from typing import Optional, Type, List, Dict, Any, Callable import inspect -from tools.logging_wrapper import Logger +from .tools.logging_wrapper import Logger class ConfigDSL: @@ -149,10 +150,10 @@ def get_with_default( if options is not None and current_element not in options: raise RuntimeError("Wrong option") except BaseException as e: - self.handle_fmt_error(fmt_error, e, kwargs) + self.handle_fmt_error(fmt_error or "", e, kwargs) return default - self.handle_fmt_success(fmt_success, current_element, kwargs) + self.handle_fmt_success(fmt_success or "", current_element, kwargs) return current_element def get_or_raise( @@ -197,10 +198,10 @@ def get_or_raise( if options is not None and current_element not in options: raise RuntimeError("Wrong option") except BaseException as e: - self.handle_fmt_error(fmt_error, e, kwargs) + self.handle_fmt_error(fmt_error or "", e, kwargs) raise e - self.handle_fmt_success(fmt_success, current_element, kwargs) + self.handle_fmt_success(fmt_success or "", current_element, kwargs) return current_element def handle_fmt( @@ -382,6 +383,55 @@ def config_entry(func: Callable) -> Callable: return ConfigEntryProxy(func) +class ConfigLogContext: + """ + Provides a context in which the loggers of the config can be optionally switched. + If one log uses a high log level, this can disable log-outputs for the subsequent code block. + """ + + def __init__(self, config: "Config", should_swap: bool): + """ + Constructs a ConfigLogContext object. + + Parameters + ---------- + config : Config + Config object. + should_swap : bool + Should the loggers be swapped. + """ + self._config: Config = config + self._should_swap: bool = should_swap + + def swap(self): + """ + Swaps the loggers used by the config object. + """ + tmp = self._config._logger + self._config._logger = self._config._logger_null + self._config._logger_null = tmp + + def __enter__(self) -> "ConfigLogContext": + """ + Called upon entering a with ... statement. + + Returns + ------- + ctx : ConfigLogContext + Logging context. + """ + if self._should_swap: + self.swap() + return self + + def __exit__(self, *args) -> None: + """ + Called upon exiting a with ... statement. + """ + if self._should_swap: + self.swap() + + class Config: """ Handles the reading of parameters in the JSON configuration file provided by the user. This class is based on @@ -401,16 +451,20 @@ def __init__(self, config_file_name: str): self._base_dir: str = os.path.dirname( os.path.join(os.getcwd(), config_file_name) ) - self._logger: Optional[Logger] = None + self._logger_null: Logger = Logger(Config.__name__, level=logging.CRITICAL) + self._logger: Logger = self._logger_null self._data: Optional[Dict[str, Any]] = None self._fields: Dict[str, Any] = defaultdict(lambda: None) # attach external backing field to all config entries + # inspection triggers assertions: need to temporarily set to value other than None + self._data = {} config_entries = inspect.getmembers( self, lambda f: hasattr(f, "attach_instance") ) for _, e in config_entries: e.attach_instance(self) + self._data = None def set_logger(self, logger: Logger): """ @@ -423,12 +477,34 @@ def set_logger(self, logger: Logger): """ self._logger = logger + def show_log_if(self, cond: bool) -> ConfigLogContext: + """ + Redirects log requests in the resulting context if cond is True. + + Usage: + with self.show_log_if(cond): + ... load fields ... + + Hereby default arguments can be loaded for inactive modules without emitting log output. + + Parameters + ---------- + cond : bool + Should redirect? + + Returns + ------- + ctx : ConfigLogContext + Logging context + """ + return ConfigLogContext(self, not cond) + @property def json(self) -> ConfigDSL: """ Returns and Object to load JSON values dynamically. """ - assert self._data is not None and self._logger is not None + assert self._data is not None return ConfigDSL(self._data, self._logger) def _read_json_base(self, config_file_name: str): @@ -440,7 +516,6 @@ def _read_json_base(self, config_file_name: str): config_file_name : string Path to the JSON configuration file """ - assert self._logger is not None self._logger.log_info_rank_zero( f"Micro Manager version: {importlib.metadata.version('micro-manager-precice')}" ) @@ -517,8 +592,7 @@ def _read_json_base(self, config_file_name: str): # ====================================================== # Tasking # ====================================================== - - if self.json["tasking"].exists(): + with self.show_log_if(self.json["tasking"].exists()): self.tasking_backend.set = self.json["tasking"]["backend"].get_with_default( "socket", "Tasking backend: {data}", @@ -558,7 +632,6 @@ def read_json_micro_manager(self): Reads Micro Manager relevant information from JSON configuration file and saves the data to the respective instance attributes. """ - assert self._logger is not None self._read_json_base(self._config_file_name) self.precice_config_file_name.set = os.path.join( @@ -636,7 +709,7 @@ def read_json_micro_manager(self): "Adaptivity settings are provided but adaptivity is turned off." ) - if self.enable_adaptivity(): + with self.show_log_if(self.enable_adaptivity()): self.adaptivity_type.set = self.json["simulation_params"][ "adaptivity_settings" ]["type"].get_with_default( @@ -749,6 +822,7 @@ def read_json_micro_manager(self): else: self.write_data_names().append("rank_of_sim") + with self.show_log_if(self.enable_load_balancing()): self.load_balancing_n.set = self.json["simulation_params"][ "load_balancing_settings" ]["every_n_time_windows"].get_with_default( @@ -877,7 +951,6 @@ def read_json_snapshot(self): """ Reads Snapshot relevant information from JSON configuration file """ - assert self._logger is not None self._read_json_base(self._config_file_name) # Read base information self._logger.log_info_rank_zero( f"Reading JSON configuration file: {self._config_file_name}" From 373e665fd1bf788ac365cf426ea5609d9364166d Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Fri, 15 May 2026 18:24:30 +0200 Subject: [PATCH 4/6] small fix --- micro_manager/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micro_manager/config.py b/micro_manager/config.py index 47ee533f..2ec7cf0c 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -709,7 +709,7 @@ def read_json_micro_manager(self): "Adaptivity settings are provided but adaptivity is turned off." ) - with self.show_log_if(self.enable_adaptivity()): + if self.enable_adaptivity(): self.adaptivity_type.set = self.json["simulation_params"][ "adaptivity_settings" ]["type"].get_with_default( From 3b53cbf14f52809bca5b631b97ea4b10bbbcf928 Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Wed, 20 May 2026 11:18:19 +0200 Subject: [PATCH 5/6] apply requested change Co-authored-by: Ishaan Desai --- micro_manager/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/micro_manager/config.py b/micro_manager/config.py index 2ec7cf0c..902c4b24 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -779,7 +779,7 @@ def read_json_micro_manager(self): self.adaptivity_similarity_measure.set = self.json["simulation_params"][ "adaptivity_settings" ]["similarity_measure"].get_with_default( - "L1", + "L2rel", "Adaptivity similarity measure: {data}", "No similarity measure provided, using L1 norm as default.", ) From 7d9f902802d220b476f692193c19870af9e42a3c Mon Sep 17 00:00:00 2001 From: Alex Hocks Date: Fri, 22 May 2026 17:23:14 +0200 Subject: [PATCH 6/6] add changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cee45e7..48451008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## latest +- Redesigned configuration loading for enhanced clarity and standardization [#264](https://github.com/precice/micro-manager/pull/264) - Allow `initialize()` to return data that is not used by the adaptivity [#261](https://github.com/precice/micro-manager/pull/261) - Fixed `MicroSimulation` initialization requiring positional parameters [#255](https://github.com/precice/micro-manager/pull/255) - Fixed model adaptivity convergence at resolution boundaries to prevent infinite loops for out-of-range switching requests [#252](https://github.com/precice/micro-manager/pull/252)