Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## latest

- Add basic Model-Adaptivity [#198](https://github.com/precice/micro-manager/pull/198)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Add basic Model-Adaptivity [#198](https://github.com/precice/micro-manager/pull/198)
- Add functionality to adaptively switch micro-scale models [#198](https://github.com/precice/micro-manager/pull/198)

- Fix bug in load balancing when a rank has exactly as many active simulation as the global average [#200](https://github.com/precice/micro-manager/pull/200)
- Use global maximum similarity distance in local adaptivity [#197](https://github.com/precice/micro-manager/pull/197)
- Log adaptivity metrics at t=0 [#194](https://github.com/precice/micro-manager/pull/194)
Expand Down
25 changes: 25 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,31 @@ Example of adaptivity configuration is
}
```

## Model Adaptivity

See the [model adaptivity](tooling-micro-manager-model-adaptivity.html) documentation for a detailed explanation about the interface.

To turn on model adaptivity, set `"model_adaptivity": true` in `simulation_params`. Then under `model_adaptivity_settings` set the following variables:

Parameter | Description
--- | ---
`micro_file_names` | List of paths to the files containing the Python importable micro simulation classes. If the files are not in the working directory, give the relative path from the directory where the Micro Manager is executed. At least 2 files.
`switching_function` | Path to the file containing the Python importable switching function. If the file is not in the working directory, give the relative path from the directory where the Micro Manager is executed.

Example of adaptivity configuration is

```json
"simulation_params": {
"micro_dt": 1.0,
"macro_domain_bounds": [0.0, 25.0, 0.0, 25.0, 0.0, 25.0],
"model_adaptivity": true,
"model_adaptivity_settings": {
"micro_file_names": ["python-dummy/micro_dummy", "python-dummy/micro_dummy", "python-dummy/micro_dummy"],
"switching_function": "mada_switcher"
}
}
```

### Adding adaptivity in the preCICE XML configuration

If adaptivity is used, the Micro Manager will attempt to write two scalar data per micro simulation to preCICE, called `active_state` and `active_steps`.
Expand Down
163 changes: 163 additions & 0 deletions docs/model-adaptivity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
title: Adaptive switching of simulation models
permalink: tooling-micro-manager-model-adaptivity.html
keywords: tooling, macro-micro, two-scale, model-adaptivity
summary: Micro Manager can switch micro models adaptively.
---

## Main Concept

For scenarios such as FE2 simulations computing all or perhaps only active micro simulations
may be still computationally infeasible. The alternative here is to reduce model complexity via reduced order models (ROMs) or similar.
Towards this the model adaptivity feature allows for the definition of multiple
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Towards this the model adaptivity feature allows for the definition of multiple
The model adaptivity functionality allows for the definition of multiple

model fidelities and the switching between those at run-time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
model fidelities and the switching between those at run-time.
model fidelities and the switching between them at run-time.


### Iterative Process

**Without** model adaptivity the call to `micro_sim_solve(micro_sims_input, dt)` within the micro_manager would just
provide each micro simulation with its input, solve it and return the output.
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Without** model adaptivity the call to `micro_sim_solve(micro_sims_input, dt)` within the micro_manager would just
provide each micro simulation with its input, solve it and return the output.
**Without** model adaptivity, the Micro Manager calls the `solve(micro_sims_input, dt)` routine of all active simulations
and copies their output to their closest similar inactive counterparts.


**With** model adaptivity this becomes an iterative process, as a model may not be sufficiently accurate (given the current input).
Thus, the call to `micro_sim_solve(micro_sims_input, dt)` with model adaptivity results in the following:
Comment on lines +20 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**With** model adaptivity this becomes an iterative process, as a model may not be sufficiently accurate (given the current input).
Thus, the call to `micro_sim_solve(micro_sims_input, dt)` with model adaptivity results in the following:
**With** model adaptivity, there is an iterative process, because a model may not be sufficiently accurate (given the current input).
The call to `solve(micro_sims_input, dt)` leads to the following logic:


```python
self._model_adaptivity_controller.initialise_solve()

active_sim_ids = None
if self._is_adaptivity_on:
active_sim_ids = self._adaptivity_controller.get_active_sim_local_ids()
output = None

while self._model_adaptivity_controller.should_iterate():
self._model_adaptivity_controller.switch_models(
self._mesh_vertex_coords,
self._t,
micro_sims_input,
output,
self._micro_sims,
active_sim_ids,
)
output = solve_variant(micro_sims_input, dt)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this solve_variant call be of self._micro_sims?

self._model_adaptivity_controller.check_convergence(
self._mesh_vertex_coords,
self._t,
micro_sims_input,
output,
self._micro_sims,
active_sim_ids,
)

self._model_adaptivity_controller.finalise_solve()
return output
```

Here, after initialization and active sim acquisition, models will be switched, evaluated and checked for convergence
as long as the `switching_function` contains values other than 0.
Model evaluation - in the call `solve_variant(micro_sims_input, dt)` - is delegated to the regular
(non-model-adaptive) `micro_sim_solve(micro_sims_input, dt)` method.

### Interfaces

```python
class MicroSimulation: # Name is fixed
def __init__(self, sim_id):
"""
Constructor of class MicroSimulation.
Parameters
----------
sim_id : int
ID of the simulation instance, that the Micro Manager has set for it.
"""

def initialize(self) -> dict:
"""
Initialize the micro simulation and return initial data which will be used in computing adaptivity before the first time step.
Defining this function is OPTIONAL.
Returns
-------
initial_data : dict
Dictionary with names of initial data as keys and the initial data itself as values.
"""

def solve(self, macro_data: dict, dt: float) -> dict:
"""
Solve one time step of the micro simulation for transient problems or solve until steady state for steady-state problems.
Parameters
----------
macro_data : dict
Dictionary with names of macro data as keys and the data as values.
dt : float
Current time step size.
Returns
-------
micro_data : dict
Dictionary with names of micro data as keys and the updated micro data a values.
"""

def set_state(self, state):
"""
Set the state of the micro simulation.
"""

def get_state(self):
"""
Return the state of the micro simulation.
"""

def output(self):
"""
This function writes output of the micro simulation in some form.
It will be called with frequency set by configuration option `simulation_params: micro_output_n`
This function is *optional*.
"""
```

For this the default MicroSimulation still serves as the model interface, while the `(set)|(get)_state()` methods
are called to transfer internal model parameters from one to another.
The list of provided models is interpreted in decreasing fidelity order. In other words, the first one
is likely to be the full order model, while subsequent ones are ROMs.

```python
def switching_function(
resolutions: np.ndarray,
locations: np.ndarray,
t: float,
inputs: list[dict],
prev_output: dict,
active: np.ndarray,
) -> np.ndarray:
"""
Switching interface function, use as reference
Parameters
----------
resolutions : np.array - shape(N,)
Array with resolution information as get_sim_class_resolution would return for a sim obj.
locations : np.array - shape(N,D)
Array with gaussian points for all sims. D is the mesh dimension.
Comment on lines +141 to +142
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about having macro locations as part of the switching function inputs. I could imagine the switching being solely based on the macro- and micro-scale data available at a particular point.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it might be useful to enforce FOM at certain locations, where the user can already expect high fluctuations in input/output or where maximal accuracy should be guaranteed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it might be useful to enforce FOM at certain locations, where the user can already expect high fluctuations in input/output or where maximal accuracy should be guaranteed.

Yes, that should be possible and required. But my comment was still about why the macro-scale coordinates need to be passed to the switching function. I look at the switching function as a function to be called individually at each location. If the switching mechanism requires a macro-scale location, it can be passed on a per-micro-scale simulation basis. I think having a single call-type switching mechanism is beneficial for the user.

t : float
Current time in simulation.
inputs : list[dict]
List of input objects.
prev_output : [None, dict-like]
Contains the outputs of the previous model evaluation.
active : np.array - shape(N,)
Bool array indicating whether the model is active or not.
"""
return np.zeros_like(resolutions)
```

The switching of models is governed by the `switching_function`.
The output is expected to be a np.ndarray of shape (N,) and is interpreted in the following manner:

Value | Action
--- | ---
0 | No resolution change
-1 | Increase model fidelity by one (go back one in list)
1 | Decrease model fidelity by one (go one ahead in list)
9 changes: 9 additions & 0 deletions examples/mada_switcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import numpy as np


def switching_function(resolutions, locations, t, inputs, prev_output, active):
output = np.zeros_like(resolutions)
mask_loc = locations[:, 0] % 2 == 0
mask_res = resolutions == 0
output[mask_loc * mask_res] = 1
return output
21 changes: 21 additions & 0 deletions examples/micro-manager-python-mada-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"micro_file_name": "python-dummy/micro_dummy",
"coupling_params": {
"precice_config_file_name": "precice-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": 1.0,
"macro_domain_bounds": [0.0, 25.0, 0.0, 25.0, 0.0, 25.0],
"model_adaptivity": true,
"model_adaptivity_settings": {
"micro_file_names": ["python-dummy/micro_dummy", "python-dummy/micro_dummy", "python-dummy/micro_dummy"],
"switching_function": "mada_switcher"
}
},
"diagnostics": {
"output_micro_sim_solve_time": true
}
}
11 changes: 4 additions & 7 deletions micro_manager/adaptivity/adaptivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


class AdaptivityCalculator:
def __init__(self, configurator, rank, nsims) -> None:
def __init__(self, configurator, rank, nsims, micro_problem_cls) -> None:
"""
Class constructor.

Expand All @@ -24,6 +24,8 @@ def __init__(self, configurator, rank, nsims) -> None:
Rank of the MPI communicator.
nsims : int
Number of micro simulations.
micro_problem_cls : callable
Class of micro problem.
"""
self._refine_const = configurator.get_adaptivity_refining_const()
self._coarse_const = configurator.get_adaptivity_coarsening_const()
Expand All @@ -32,12 +34,7 @@ def __init__(self, configurator, rank, nsims) -> None:
self._adaptivity_type = configurator.get_adaptivity_type()
self._adaptivity_output_type = configurator.get_adaptivity_output_type()

self._micro_problem = getattr(
importlib.import_module(
configurator.get_micro_file_name(), "MicroSimulation"
),
"MicroSimulation",
)
self._micro_problem_cls = micro_problem_cls

self._coarse_tol = 0.0
self._ref_tol = 0.0
Expand Down
13 changes: 6 additions & 7 deletions micro_manager/adaptivity/global_adaptivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(
participant,
rank: int,
comm_world,
micro_problem_cls,
) -> None:
"""
Class constructor.
Expand All @@ -42,8 +43,10 @@ def __init__(
MPI rank.
comm_world : MPI.COMM_WORLD
Base global communicator of MPI.
micro_problem_cls : callable
Class of micro problem.
"""
super().__init__(configurator, rank, global_number_of_sims)
super().__init__(configurator, rank, global_number_of_sims, micro_problem_cls)
self._global_number_of_sims = global_number_of_sims
self._global_ids = global_ids
self._comm_world = comm_world
Expand Down Expand Up @@ -419,9 +422,7 @@ def _update_inactive_sims(self, micro_sims: list) -> None:
# Only handle activation of simulations on this rank
for gid in to_be_activated_gids:
to_be_activated_lid = self._global_ids.index(gid)
micro_sims[to_be_activated_lid] = create_simulation_class(
self._micro_problem
)(gid)
micro_sims[to_be_activated_lid] = self._micro_problem_cls(gid)
assoc_active_gid = self._sim_is_associated_to[gid]

if self._is_sim_on_this_rank[
Expand Down Expand Up @@ -454,9 +455,7 @@ def _update_inactive_sims(self, micro_sims: list) -> None:
local_ids = to_be_activated_map[gid]
for lid in local_ids:
# Create the micro simulation object and set its state
micro_sims[lid] = create_simulation_class(self._micro_problem)(
self._global_ids[lid]
)
micro_sims[lid] = self._micro_problem_cls(self._global_ids[lid])
micro_sims[lid].set_state(state)

# Delete the micro simulation object if it is inactive
Expand Down
13 changes: 5 additions & 8 deletions micro_manager/adaptivity/global_adaptivity_lb.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
logger,
rank: int,
comm,
micro_problem_cls: callable,
) -> None:
"""
Class constructor.
Expand All @@ -45,6 +46,8 @@ def __init__(
MPI rank.
comm : MPI.COMM_WORLD
Global communicator of MPI.
micro_problem_cls : callable
Class of micro problem.
"""
super().__init__(
configurator,
Expand All @@ -53,13 +56,7 @@ def __init__(
participant,
rank,
comm,
)

self._micro_problem = getattr(
importlib.import_module(
configurator.get_micro_file_name(), "MicroSimulation"
),
"MicroSimulation",
micro_problem_cls,
)

self._base_logger = logger
Expand Down Expand Up @@ -368,7 +365,7 @@ def _move_active_sims(
# Create simulations and set them to the received states
for req in recv_reqs:
output, gid = req.wait()
micro_sims.append(create_simulation_class(self._micro_problem)(gid))
micro_sims.append(self._micro_problem_cls(gid))
micro_sims[-1].set_state(output)
self._global_ids.append(gid)
self._is_sim_on_this_rank[gid] = True
Expand Down
10 changes: 7 additions & 3 deletions micro_manager/adaptivity/local_adaptivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@


class LocalAdaptivityCalculator(AdaptivityCalculator):
def __init__(self, configurator, num_sims, participant, rank, comm_world) -> None:
def __init__(
self, configurator, num_sims, participant, rank, comm_world, micro_problem_cls
) -> None:
"""
Class constructor.

Expand All @@ -28,8 +30,10 @@ def __init__(self, configurator, num_sims, participant, rank, comm_world) -> Non
Rank of the current MPI process.
comm_world : MPI.COMM_WORLD
Global communicator of MPI.
micro_problem_cls : callable
Class of micro problem.
"""
super().__init__(configurator, rank, num_sims)
super().__init__(configurator, rank, num_sims, micro_problem_cls)
self._comm_world = comm_world

if (
Expand Down Expand Up @@ -260,7 +264,7 @@ def _update_inactive_sims(self, micro_sims: list) -> None:
# Update the set of inactive micro sims
for i in to_be_activated_ids:
associated_active_id = self._sim_is_associated_to[i]
micro_sims[i] = create_simulation_class(self._micro_problem)(i)
micro_sims[i] = self._micro_problem_cls(i)
micro_sims[i].set_state(micro_sims[associated_active_id].get_state())
self._sim_is_associated_to[
i
Expand Down
Loading