Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions docs/adaptivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. <br> 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` |

Expand All @@ -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"}
}
}
```
Expand Down
23 changes: 23 additions & 0 deletions micro_manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
)
Expand Down Expand Up @@ -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.
Expand Down
65 changes: 50 additions & 15 deletions micro_manager/micro_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,21 @@ 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()
)

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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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."
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
46 changes: 46 additions & 0 deletions tests/unit/micro-manager-config-local-adaptivity-data.json
Original file line number Diff line number Diff line change
@@ -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
}
}
44 changes: 44 additions & 0 deletions tests/unit/test_micro_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading