diff --git a/CHANGELOG.md b/CHANGELOG.md index d48682d3..3fe92079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Added option to use compute instances to reduce memory consumption [#226](https://github.com/precice/micro-manager/pull/226) - Added support to run micro simulations in separate processes with workers [#219](https://github.com/precice/micro-manager/pull/219) - Added abstraction layers to micro simulations to support more features [#218](https://github.com/precice/micro-manager/pull/218) - +- Micro simulations can now pass local data exclusively for the adaptivity similarity distance calculation, without sending it to the macro simulation. Configure via `local_data` in `adaptivity_settings`. ([#25](https://github.com/precice/micro-manager/issues/25)) ## v0.8.0 - Conformed to naming standard in precice/tutorials [#215](https://github.com/precice/micro-manager/pull/215) diff --git a/docs/adaptivity.md b/docs/adaptivity.md index 9bdffe86..4cee453d 100644 --- a/docs/adaptivity.md +++ b/docs/adaptivity.md @@ -67,6 +67,21 @@ The primary tuning parameters for adaptivity are the history parameter $$ \Lambd See the [adaptivity configuration](tooling-micro-manager-configuration.html#adaptivity) documentation on how to configure the parameters $$ C_c $$, $$ C_r $$, and more. +## Local data for adaptivity + +In some scenarios, a micro simulation may have internal state data that is well-suited for characterizing similarity, but should not be sent to the macro simulation. For example, an internal phase indicator or a convergence metric. Such data can be passed to the Micro Manager via the `local_data` key in `adaptivity_settings`. The Micro Manager collects this data for the similarity distance calculation but never writes it to preCICE. The micro simulation simply includes the local data key in the dict returned by its `solve()` (and optionally `initialize()`) method: + +```python +def solve(self, macro_data, dt): + # ... solve ... + return { + "macro-output-data": ..., # sent to macro simulation + "internal-state": 42.0, # used only for adaptivity, not sent to macro + } +``` + +See the [adaptivity configuration](tooling-micro-manager-configuration.html#adaptivity) for how to declare `local_data` in the configuration file. + ## Adaptivity variants If the Micro Manager is run in parallel, micro simulations are distributed over MPI ranks. This opens the door different ways in which the adaptivity can be computed. There are two principle ways to go about it. diff --git a/docs/configuration.md b/docs/configuration.md index f3260f1d..3a0e8771 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -83,6 +83,7 @@ To turn on adaptivity, set `"adaptivity": true` in `simulation_params`. Then und | `refining_constant` | Refining constant $$ C_r $$, set as $$ 0 =< C_r < 1 $$. | 0.5 | | `every_implicit_iteration` | If `true`, adaptivity is calculated in every implicit iteration.
If False, adaptivity is calculated once at the start of the time window and then reused in every implicit time iteration. | `false` | | `similarity_measure` | Similarity measure to be used for adaptivity. Can be either `L1`, `L2`, `L1rel` or `L2rel`. By default, `L1` is used. The `rel` variants calculate the respective relative norms. This parameter is *optional*. | `L2rel` | +| `local_data` | Dictionary of names of micro simulation data which are used **only** for the adaptivity similarity distance calculation and are **not** sent to the macro simulation. For example `{"internal-state": "scalar"}`. This is useful for internal micro simulation state data that characterizes similarity but should not be communicated to the macro side. This parameter is *optional*. | `{}` | | `lazy_initialization` | Set to `true` to lazily create and initialize micro simulations. If selected, micro simulation objects are created only when the micro simulation is activated for the first time. | `false` | | `load_balancing` | Set to `true` to dynamically balance simulations for parallel runs. See [load balancing settings](#load-balancing) below. | `false` | @@ -100,7 +101,8 @@ Example of adaptivity configuration is "coarsening_constant": 0.3, "refining_constant": 0.4, "every_implicit_iteration": false, - "lazy_initialization": true + "lazy_initialization": true, + "local_data": {"internal-state": "scalar"} } } ``` diff --git a/micro_manager/config.py b/micro_manager/config.py index 37623cd9..eba4c548 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -46,6 +46,7 @@ def __init__(self, config_file_name): self._adaptivity = False self._adaptivity_type = "" self._data_for_adaptivity = dict() + self._local_data_for_adaptivity = dict() self._adaptivity_n = 1 self._adaptivity_history_param = 0.5 self._adaptivity_coarsening_constant = 0.5 @@ -311,6 +312,15 @@ def read_json_micro_manager(self): "adaptivity_settings" ]["data"] + self._local_data_for_adaptivity = self._data["simulation_params"][ + "adaptivity_settings" + ].get("local_data", {}) + if self._local_data_for_adaptivity: + self._logger.log_info_rank_zero( + "Local data used only for adaptivity (not sent to macro): " + + str(self._local_data_for_adaptivity) + ) + self._logger.log_info_rank_zero( "Data used for adaptivity: " + str(self._data_for_adaptivity) ) @@ -779,6 +789,19 @@ def get_data_for_adaptivity(self): """ return self._data_for_adaptivity + def get_local_data_for_adaptivity(self): + """ + Get names of micro simulation local data to be used only for similarity distance calculation in adaptivity. + This data is not sent to the macro simulation. + + Returns + ------- + local_data_for_adaptivity : dict_like + A dictionary containing the names of the local adaptivity data as keys and information on whether + the data are scalar or vector as values. + """ + return self._local_data_for_adaptivity + def get_adaptivity_n(self): """ Get the frequency of adaptivity computation. diff --git a/micro_manager/micro_manager.py b/micro_manager/micro_manager.py index 5fe973a6..bec4c836 100644 --- a/micro_manager/micro_manager.py +++ b/micro_manager/micro_manager.py @@ -128,6 +128,11 @@ def __init__(self, config_file: str, log_file: str = "") -> None: if name in self._write_data_names: self._adaptivity_micro_data_names.append(name) + # Names of micro simulation local data used only for adaptivity (not sent to macro) + self._adaptivity_local_data_names: list = list( + self._config.get_local_data_for_adaptivity().keys() + ) + self._adaptivity_in_every_implicit_step = ( self._config.is_adaptivity_required_in_every_implicit_iteration() ) @@ -135,6 +140,9 @@ def __init__(self, config_file: str, log_file: str = "") -> None: if self._is_adaptivity_with_load_balancing: self._load_balancing_n = self._config.get_load_balancing_n() + if not self._is_adaptivity_on: + self._adaptivity_local_data_names: list = [] + self._adaptivity_n = self._config.get_adaptivity_n() self._adaptivity_output_type = self._config.get_adaptivity_output_type() @@ -553,6 +561,8 @@ def initialize(self) -> None: if self._is_adaptivity_on: for name in self._adaptivity_data_names: self._data_for_adaptivity[name] = [0] * self._local_number_of_sims + for name in self._adaptivity_local_data_names: + self._data_for_adaptivity[name] = [0] * self._local_number_of_sims # Create lists of global IDs self._global_ids_of_local_sims = [] # DECLARATION @@ -738,9 +748,9 @@ def initialize(self) -> None: # Save initial data from first micro simulation as we anyway have it for name in initial_micro_output.keys(): if name in self._data_for_adaptivity: - self._data_for_adaptivity[name][ - first_id - ] = initial_micro_output[name] + self._data_for_adaptivity[name][first_id] = ( + initial_micro_output[name] + ) else: raise Exception( "The initialize() method needs to return data which is required for the adaptivity calculation." @@ -753,18 +763,34 @@ def initialize(self) -> None: initial_data[i] ) for name in self._adaptivity_micro_data_names: - self._data_for_adaptivity[name][ - i - ] = initial_micro_output[name] + self._data_for_adaptivity[name][i] = ( + initial_micro_output[name] + ) initial_micro_data[name][i] = initial_micro_output[name] + for name in self._adaptivity_local_data_names: + if name in initial_micro_output: + self._data_for_adaptivity[name][i] = ( + initial_micro_output[name] + ) + initial_micro_data[name][i] = initial_micro_output[ + name + ] else: for i in micro_sims_to_init: initial_micro_output = self._micro_sims[i].initialize() for name in self._adaptivity_micro_data_names: - self._data_for_adaptivity[name][ - i - ] = initial_micro_output[name] + self._data_for_adaptivity[name][i] = ( + initial_micro_output[name] + ) initial_micro_data[name][i] = initial_micro_output[name] + for name in self._adaptivity_local_data_names: + if name in initial_micro_output: + self._data_for_adaptivity[name][i] = ( + initial_micro_output[name] + ) + initial_micro_data[name][i] = initial_micro_output[ + name + ] # If lazy initialization is on, initial states of inactive simulations need to be determined if self._lazy_init: @@ -776,6 +802,11 @@ def initialize(self) -> None: self._data_for_adaptivity[name][i] = initial_micro_data[ name ][i] + for name in self._adaptivity_local_data_names: + if name in initial_micro_data: + self._data_for_adaptivity[name][i] = ( + initial_micro_data[name][i] + ) del initial_micro_data # Once the initial data is fed into the adaptivity data, it is no longer required else: @@ -975,9 +1006,9 @@ def _solve_micro_simulations_with_adaptivity( # Mark the micro sim as active for export micro_sims_output[lid]["Active-State"] = 1 gid = self._global_ids_of_local_sims[lid] - micro_sims_output[lid][ - "Active-Steps" - ] = self._micro_sims_active_steps[gid] + micro_sims_output[lid]["Active-Steps"] = ( + self._micro_sims_active_steps[gid] + ) # If simulation crashes, log the error and keep the output constant at the previous iteration's output except Exception as error_message: @@ -1031,14 +1062,18 @@ def _solve_micro_simulations_with_adaptivity( for inactive_lid in inactive_sim_lids: micro_sims_output[inactive_lid]["Active-State"] = 0 gid = self._global_ids_of_local_sims[inactive_lid] - micro_sims_output[inactive_lid][ - "Active-Steps" - ] = self._micro_sims_active_steps[gid] + micro_sims_output[inactive_lid]["Active-Steps"] = ( + self._micro_sims_active_steps[gid] + ) # Collect micro sim output for adaptivity calculation for i in range(self._local_number_of_sims): for name in self._adaptivity_micro_data_names: self._data_for_adaptivity[name][i] = micro_sims_output[i][name] + # Collect local data provided by micro sims only for adaptivity (not sent to macro) + for name in self._adaptivity_local_data_names: + if name in micro_sims_output[i]: + self._data_for_adaptivity[name][i] = micro_sims_output[i].pop(name) return micro_sims_output diff --git a/tests/unit/micro-manager-config-local-adaptivity-data.json b/tests/unit/micro-manager-config-local-adaptivity-data.json new file mode 100644 index 00000000..f6bf30ae --- /dev/null +++ b/tests/unit/micro-manager-config-local-adaptivity-data.json @@ -0,0 +1,46 @@ +{ + "micro_file_name": "test_micro_manager", + "coupling_params": { + "precice_config_file_name": "dummy-config.xml", + "macro_mesh_name": "Macro-Mesh", + "read_data_names": [ + "Macro-Scalar-Data", + "Macro-Vector-Data" + ], + "write_data_names": [ + "Micro-Scalar-Data", + "Micro-Vector-Data" + ] + }, + "simulation_params": { + "micro_dt": 0.1, + "macro_domain_bounds": [ + 0.0, + 25.0, + 0.0, + 25.0, + 0.0, + 25.0 + ], + "adaptivity": true, + "adaptivity_settings": { + "type": "local", + "data": [ + "Macro-Scalar-Data", + "Macro-Vector-Data" + ], + "history_param": 0.5, + "coarsening_constant": 0.3, + "refining_constant": 0.4, + "every_implicit_iteration": false, + "similarity_measure": "L1", + "local_data": { + "Micro-Local-Data": "scalar" + } + } + }, + "diagnostics": { + "output_micro_sim_solve_time": true, + "micro_output_n": 10 + } +} \ No newline at end of file diff --git a/tests/unit/test_micro_manager.py b/tests/unit/test_micro_manager.py index b8ef7f39..a8213863 100644 --- a/tests/unit/test_micro_manager.py +++ b/tests/unit/test_micro_manager.py @@ -150,3 +150,47 @@ def test_config(self): import unittest unittest.main() + + +class MicroSimulationWithLocalAdaptivityData: + def __init__(self, sim_id): + self.very_important_value = 0 + + def initialize(self): + pass + + def solve(self, macro_data, dt): + return { + "Micro-Scalar-Data": macro_data["Macro-Scalar-Data"] + 1, + "Micro-Vector-Data": macro_data["Macro-Vector-Data"] + 1, + "Micro-Local-Data": 42.0, # local data only for adaptivity, not sent to macro + } + + +class TestLocalAdaptivityData(TestCase): + def test_local_adaptivity_data_not_written_to_precice(self): + """ + Test that data listed under 'local_data' in adaptivity_settings is used for + adaptivity calculation but is NOT passed to preCICE write_data. + """ + manager = micro_manager.MicroManagerCoupling( + "micro-manager-config-local-adaptivity-data.json" + ) + manager.initialize() + + # Verify local data name is registered + self.assertIn("Micro-Local-Data", manager._adaptivity_local_data_names) + + # Simulate a solve step on one active sim + micro_sims_input = [ + {"Macro-Scalar-Data": 1, "Macro-Vector-Data": np.array([0, 1, 2])} + ] * manager._local_number_of_sims + + output = manager._solve_micro_simulations_with_adaptivity(micro_sims_input, 0.1) + + # Local data must be collected in _data_for_adaptivity + self.assertIn("Micro-Local-Data", manager._data_for_adaptivity) + + # Local data must NOT appear in the output sent to preCICE + for sim_output in output: + self.assertNotIn("Micro-Local-Data", sim_output)