Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d1ea98
first basic model switching impl
Snapex2409 Nov 3, 2025
6f6505b
added file based switching and full docstrings
Snapex2409 Nov 4, 2025
b49ab5b
fix style issues
Snapex2409 Nov 5, 2025
cc6aedf
add missing dep in setup description
Snapex2409 Nov 5, 2025
ac2e847
add input to switching fun
Snapex2409 Nov 7, 2025
29bafff
integrate adaptivity with new micro sim loading
Snapex2409 Nov 7, 2025
50b609f
use pre-commit fix
Snapex2409 Nov 7, 2025
75708a1
fix tests
Snapex2409 Nov 7, 2025
762b3a3
use pre-commit fix
Snapex2409 Nov 7, 2025
0d061b0
clean up and documentation
Snapex2409 Nov 12, 2025
01d7c55
Merge branch 'develop' into model-adaptivity
Snapex2409 Nov 12, 2025
0b9cadd
fix linting
Snapex2409 Nov 12, 2025
38bc30b
Update docs/configuration.md
Snapex2409 Nov 14, 2025
0a95e74
Update docs/configuration.md
Snapex2409 Nov 14, 2025
cc8ee34
Update docs/model-adaptivity.md
Snapex2409 Nov 14, 2025
2b030d1
Update docs/model-adaptivity.md
Snapex2409 Nov 14, 2025
0e66284
Update docs/model-adaptivity.md
Snapex2409 Nov 14, 2025
f60a7f1
Update model_adaptivity.py
Snapex2409 Nov 14, 2025
456df3d
Merge branch 'develop' into model-adaptivity
Snapex2409 Dec 1, 2025
a2d59b6
switch to per micro-sim switching func evaluation
Snapex2409 Dec 1, 2025
a3f48f4
Apply documentation changes
IshaanDesai Dec 2, 2025
65aa0a2
Apply suggested changes
Snapex2409 Dec 9, 2025
f5a4be5
apply linting fixes
Snapex2409 Dec 9, 2025
23e5302
Merge remote-tracking branch 'origin/develop' into model-adaptivity
Snapex2409 Dec 9, 2025
94e0cfa
Remove package name from proj toml
Snapex2409 Dec 9, 2025
825b530
Make changelog entry more descriptive
IshaanDesai Dec 10, 2025
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)
Comment thread
IshaanDesai marked this conversation as resolved.
Outdated
Comment thread
IshaanDesai marked this conversation as resolved.
Outdated
- 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.
Comment thread
Snapex2409 marked this conversation as resolved.
Outdated
`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
Comment thread
Snapex2409 marked this conversation as resolved.
Outdated

```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.
Comment thread
Snapex2409 marked this conversation as resolved.
Outdated
---

## Main Concept

For scenarios such as FE2 simulations computing all or perhaps only active micro simulations
Comment thread
Snapex2409 marked this conversation as resolved.
Outdated
may be still computationally infeasible. The alternative here is to reduce model complexity via reduced order models (ROMs) or similar.
Comment thread
Snapex2409 marked this conversation as resolved.
Outdated
Towards this the model adaptivity feature allows for the definition of multiple
Comment thread
IshaanDesai marked this conversation as resolved.
Outdated
model fidelities and the switching between those at run-time.
Comment thread
IshaanDesai marked this conversation as resolved.
Outdated

### 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 thread
IshaanDesai marked this conversation as resolved.
Outdated

**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 thread
IshaanDesai marked this conversation as resolved.
Outdated

```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)
Comment thread
IshaanDesai marked this conversation as resolved.
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 thread
IshaanDesai marked this conversation as resolved.
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