Skip to content

Commit da625fa

Browse files
Implement Adaptivity interpolation using RBF (#242)
* Default behaviour of Adaptivity still copies results from nearest representatives. * Functional dependencies can be specified. Those will be interpolated using RBF. --------- Co-authored-by: Ishaan Desai <ishaandesai@gmail.com>
1 parent 7de47e2 commit da625fa

13 files changed

Lines changed: 2697 additions & 6 deletions

.github/workflows/check-coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ jobs:
8383
. ../../.venv/bin/activate
8484
mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_adaptivity_parallel
8585
mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_load_balancing
86+
mpirun -n 2 --allow-run-as-root -x PYTHONPATH=. python3 -m coverage run --parallel-mode --source=micro_manager -m unittest test_interpolation
8687
8788
- name: Combine coverage data
8889
working-directory: micro-manager/tests/unit

.github/workflows/run-adaptivity-tests-parallel.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,13 @@ jobs:
113113
. .venv/bin/activate
114114
cd tests/unit
115115
mpiexec -n 4 --oversubscribe --allow-run-as-root python3 -m unittest test_load_balancing.py
116+
117+
- name: Run interpolation unit tests with 2 ranks
118+
timeout-minutes: 3
119+
working-directory: micro-manager
120+
run: |
121+
. .venv/bin/activate
122+
pip install .[sklearn]
123+
pip uninstall -y pyprecice
124+
cd tests/unit
125+
mpiexec -n 2 --allow-run-as-root python3 -m unittest test_interpolation.py

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Micro Manager changelog
22

3+
## latest
4+
5+
- Added RBF interpolation, currently used within adaptivity for output interpolation [#242](https://github.com/precice/micro-manager/pull/242)
6+
37
## v0.10.1
48

59
- Fixed parameter inspection of the `initialize()` routine when micro simulation is pybind11 wrapped [#281](https://github.com/precice/micro-manager/pull/281)

docs/configuration.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,57 @@ To turn on adaptivity, set `"adaptivity": true` in `simulation_params`. Then und
113113
| `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` |
114114
| `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` |
115115
| `load_balancing` | Set to `true` to dynamically balance simulations for parallel runs. See [load balancing settings](#load-balancing) below. | `false` |
116+
| `mappings` | Optional interpolation of results. Set to list of mapping configurations. See below for further details. | `[]` |
117+
118+
Results of inactive simulations can be interpolated from active simulations using radial basis function interpolation. For data in `write_data_names`, a function
119+
can be defined from `read_data_names` to `write_data_names`. When using multiple functions, their interpolation target, i.e., fields
120+
of `write_data_names` must be mutually disjunct. Mappings can be defined as:
121+
122+
```json
123+
"mappings": [
124+
{
125+
"src_fields": ["input1", "input2"],
126+
"dst_fields": ["output1", "output2"],
127+
"n_neighbors": 50,
128+
"rbf_config": {
129+
"basis": {
130+
"type": "c6"
131+
}
132+
},
133+
"domain_config": {
134+
"max_filling": 8,
135+
"coarsening_factor": 2,
136+
"projection": {
137+
"type": "std",
138+
"target_dims": 3
139+
}
140+
}
141+
},
142+
]
143+
```
144+
145+
| Parameter | Description | Default |
146+
|-----------------|-----------------------------------------|---------|
147+
| `src_fields` | List of entries from `read_data_names` | `None` |
148+
| `dst_fields` | List of entries from `write_data_names` | `None` |
149+
| `n_neighbours` | The minimum amount of support points. | `50` |
150+
| `rbf_config` | RBF interpolation configuration. | `None` |
151+
| `domain_config` | Function source domain description. | |
152+
153+
A selection of basis functions is available: `c0`, `c2`, `c4`, `c6`.
154+
The domain must be described/further configured as input data is shared across ranks and must be redistributed for interpolation.
155+
Towards this, spatial discretization techniques are used. For better performance, data can be projected to a lower dimensional space
156+
using the fields with the highest standard deviation.
157+
158+
| Parameter | Description | Default |
159+
|---------------------|---------------------------------------------------------------------------|------------|
160+
| `use_pu` | Enables PU-RBF. (currently not supported) | `False` |
161+
| `pu_overlap` | Controlls overlap radius for PU decomposition. | `0.1` |
162+
| `basis` | RBF basis function: `c0`, `c2`, `c4`, `c6` | `None` |
163+
| `max_filling` | Tunes maximum filling of tree nodes used during decomposition. | `8` |
164+
| `coarsening_factor` | Adjusts the fidelity of the discretized domain. Only integer values >= 1. | `2` |
165+
| `projection` | Either `std` or `identity`. | `identity` |
166+
| `target_dims` | Only if `std` is used. Denotes the target dimension after projection. | `None` |
116167

117168
Example of adaptivity configuration is
118169

micro_manager/adaptivity/adaptivity.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def __init__(
5858

5959
self._max_similarity_dist = 0.0
6060

61+
self._interpolation = None
62+
self._interp_min = -1
63+
self._mappings = []
64+
self._mapping_configs = []
65+
mappings = configurator.get_adaptivity_mapping_configs()
66+
self._load_mappings(mappings)
67+
6168
# is_sim_active: 1D array having state (active or inactive) of each micro simulation
6269
# Start adaptivity calculation with all sims active
6370
# This array is modified in place via the function update_active_sims and update_inactive_sims
@@ -109,6 +116,65 @@ def __init__(
109116

110117
self._metrics_logger.log_info("n|n active|n inactive|assoc ranks")
111118

119+
def _load_mappings(self, mappings: list) -> None:
120+
"""
121+
Translates the mapping information provided from the configuration file into a
122+
interpolation method parseable structure.
123+
124+
This will populate the self._mappings and self._mapping_configs buffers.
125+
Called once during __init__.
126+
127+
Parameters
128+
----------
129+
mappings : list
130+
List of mappings as provided by the configuration file.
131+
"""
132+
for mapping in mappings:
133+
src_fields = mapping["src_fields"]
134+
dst_fields = mapping["dst_fields"]
135+
n_neighbors = mapping["n_neighbors"]
136+
if self._interp_min == -1:
137+
self._interp_min = n_neighbors
138+
else:
139+
self._interp_min = min(n_neighbors, self._interp_min)
140+
141+
self._mappings.append((src_fields, dst_fields))
142+
config = {}
143+
if "use_pu" in mapping["rbf_config"]:
144+
config["use_pu"] = mapping["rbf_config"]["use_pu"]
145+
if "pu_overlap" in mapping["rbf_config"]:
146+
config["pu_overlap"] = mapping["rbf_config"]["pu_overlap"]
147+
config["pu_cluster_size"] = n_neighbors
148+
if "basis" in mapping["rbf_config"]:
149+
if "type" in mapping["rbf_config"]["basis"]:
150+
config["basis"] = mapping["rbf_config"]["basis"]["type"]
151+
if (
152+
config["basis"] == "gauss"
153+
and "eps" in mapping["rbf_config"]["basis"]
154+
):
155+
config["gauss_eps"] = mapping["rbf_config"]["basis"]["eps"]
156+
157+
dom_config = {}
158+
dom_config["n_neighbors"] = n_neighbors
159+
if "max_filling" in mapping["domain_config"]:
160+
dom_config["max_filling"] = mapping["domain_config"]["max_filling"]
161+
if "coarsening_factor" in mapping["domain_config"]:
162+
dom_config["coarsening_factor"] = mapping["domain_config"][
163+
"coarsening_factor"
164+
]
165+
if "projection" in mapping["domain_config"]:
166+
if "type" in mapping["domain_config"]["projection"]:
167+
dom_config["projection_type"] = mapping["domain_config"][
168+
"projection"
169+
]["type"]
170+
if "target_dims" in mapping["domain_config"]["projection"]:
171+
dom_config["projection_std_dims"] = mapping["domain_config"][
172+
"projection"
173+
]["target_dims"]
174+
175+
config["domain_config"] = dom_config
176+
self._mapping_configs.append(config)
177+
112178
def _update_similarity_dists(self, dt: float, data: dict) -> None:
113179
"""
114180
Calculate metric which determines if two micro simulations are similar enough to have one of them deactivated.
@@ -199,6 +265,89 @@ def _check_for_deactivation(self, active_id: int, active_ids: list) -> bool:
199265
return True
200266
return False
201267

268+
def _interpolate_output(self, micro_input, micro_sims_output) -> None:
269+
"""
270+
Interpolates the micro output based on the available inputs and outputs using the selected
271+
interpolation method and desired mappings.
272+
Will compute functions f1 ... fN described in the config.
273+
fi: X -> Y, X and Y must be subsets of the coupled fields.
274+
Every output field may only be used once as interpolation target, meaning there may not be
275+
a function fi and fj with shared Yi and Yj.
276+
277+
This method will edit the output buffer, instead of returning a new buffer.
278+
279+
Parameters
280+
----------
281+
micro_input : list
282+
List of all local micro simulation inputs.
283+
284+
micro_sims_output : list
285+
List of all local micro simulation outputs. (current state)
286+
"""
287+
targets = []
288+
for _, target_args in self._mappings:
289+
targets.extend(target_args)
290+
assert len(targets) == len(set(targets))
291+
292+
# precompute arg sizes
293+
active_lids = self.get_active_sim_local_ids()
294+
inactive_lids = self.get_inactive_sim_local_ids()
295+
arg_sizes = {}
296+
for name, value in micro_input[-1].items():
297+
arg_sizes[name] = (
298+
1 if type(value) != np.ndarray and type(value) != list else len(value)
299+
)
300+
for name, value in micro_sims_output[-1].items():
301+
arg_sizes[name] = (
302+
1 if type(value) != np.ndarray and type(value) != list else len(value)
303+
)
304+
305+
# create interpolation data structures
306+
n_points = len(active_lids)
307+
n_points_inactive = len(inactive_lids)
308+
for m_idx, fun in enumerate(self._mappings):
309+
src_args, dst_args = fun
310+
src_size = np.array([arg_sizes[name] for name in src_args]).sum()
311+
dst_size = np.array([arg_sizes[name] for name in dst_args]).sum()
312+
input_data = np.zeros((n_points, src_size))
313+
output_data = np.zeros((n_points, dst_size))
314+
for idx, lid in enumerate(active_lids):
315+
offset = 0
316+
for src_arg in src_args:
317+
input_data[idx, offset : offset + arg_sizes[src_arg]] = micro_input[
318+
lid
319+
][src_arg]
320+
offset += arg_sizes[src_arg]
321+
offset = 0
322+
for dst_arg in dst_args:
323+
output_data[
324+
idx, offset : offset + arg_sizes[dst_arg]
325+
] = micro_sims_output[lid][dst_arg]
326+
offset += arg_sizes[dst_arg]
327+
input_data_inactive = np.zeros((n_points_inactive, src_size))
328+
for idx, lid in enumerate(inactive_lids):
329+
offset = 0
330+
for src_arg in src_args:
331+
input_data_inactive[
332+
idx, offset : offset + arg_sizes[src_arg]
333+
] = micro_input[lid][src_arg]
334+
offset += arg_sizes[src_arg]
335+
336+
# use interpolant
337+
self._interpolation.configure(self._mappings[m_idx])
338+
self._interpolation.set_local_data(
339+
input_data, input_data_inactive, output_data
340+
)
341+
output_data_inactive = self._interpolation.interpolate()
342+
343+
for idx, lid in enumerate(inactive_lids):
344+
offset = 0
345+
for dst_arg in dst_args:
346+
micro_sims_output[lid][dst_arg] = output_data_inactive[
347+
idx, offset : offset + arg_sizes[dst_arg]
348+
]
349+
offset += arg_sizes[dst_arg]
350+
202351
def _get_similarity_measure(
203352
self, similarity_measure: str
204353
) -> Callable[[np.ndarray], np.ndarray]:

micro_manager/adaptivity/global_adaptivity.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from micro_manager.tools.logging_wrapper import Logger
1616
from micro_manager.micro_simulation import MicroSimulationClass
1717
from micro_manager.model_manager import ModelManager
18+
from micro_manager.interpolation import RBF_PU
1819

1920
from micro_manager.tools.p2p import p2p_comm, get_ranks_of_sims
2021

@@ -68,6 +69,9 @@ def __init__(
6869
self._global_ids = global_ids
6970
self._comm = comm
7071

72+
self._interpolation = RBF_PU(
73+
base_logger, comm, self._rank, self._comm.Get_size()
74+
)
7175
rank_of_sim = get_ranks_of_sims(global_ids, rank, comm, global_number_of_sims)
7276

7377
self._is_sim_on_this_rank = [False] * global_number_of_sims # DECLARATION
@@ -240,12 +244,16 @@ def get_inactive_sim_global_ids(self) -> np.ndarray:
240244

241245
return np.array(inactive_sim_ids)
242246

243-
def get_full_field_micro_output(self, micro_output: list) -> list:
247+
def get_full_field_micro_output(
248+
self, micro_input: list, micro_output: list
249+
) -> list:
244250
"""
245251
Get the full field micro output from active simulations to inactive simulations.
246252
247253
Parameters
248254
----------
255+
micro_input : list
256+
List of dicts containing the input data for each simulation.
249257
micro_output : list
250258
List of dicts having individual output of each simulation. Only the active simulation outputs are entered.
251259
@@ -259,7 +267,17 @@ def get_full_field_micro_output(self, micro_output: list) -> list:
259267
)
260268

261269
micro_sims_output = deepcopy(micro_output)
270+
num_active = np.sum(self._is_sim_active)
271+
if num_active == self._is_sim_active.shape[0]:
272+
self._precice_participant.stop_last_profiling_section()
273+
return micro_sims_output
274+
262275
self._communicate_micro_output(micro_sims_output)
276+
if num_active <= self._interp_min:
277+
self._precice_participant.stop_last_profiling_section()
278+
return micro_sims_output
279+
280+
self._interpolate_output(micro_input, micro_sims_output)
263281

264282
self._precice_participant.stop_last_profiling_section()
265283

micro_manager/adaptivity/local_adaptivity.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from micro_manager.micro_simulation import MicroSimulationClass
1313
from micro_manager.tools.logging_wrapper import Logger
1414
from micro_manager.model_manager import ModelManager
15+
from micro_manager.interpolation import RBF_PU
1516

1617

1718
class LocalAdaptivityCalculator(AdaptivityCalculator):
@@ -49,6 +50,12 @@ def __init__(
4950
configurator, num_sims, micro_problem_cls, model_manager, base_logger, rank
5051
)
5152
self._comm = comm
53+
self._interpolation = RBF_PU(
54+
base_logger,
55+
MPI.COMM_SELF,
56+
MPI.COMM_SELF.Get_rank(),
57+
MPI.COMM_SELF.Get_size(),
58+
)
5259

5360
# similarity_dists: 2D array having similarity distances between each micro simulation pair
5461
# This matrix is modified in place via the function update_similarity_dists
@@ -146,12 +153,16 @@ def get_inactive_sim_global_ids(self) -> np.ndarray:
146153
inactive_sim_ids = self.get_inactive_sim_local_ids()
147154
return inactive_sim_ids
148155

149-
def get_full_field_micro_output(self, micro_output: list) -> list:
156+
def get_full_field_micro_output(
157+
self, micro_input: list, micro_output: list
158+
) -> list:
150159
"""
151160
Get the full field micro output from active simulations to inactive simulations.
152161
153162
Parameters
154163
----------
164+
micro_input : list
165+
List of dicts containing the input data for each simulation.
155166
micro_output : list
156167
List of dicts having individual output of each simulation. Only the active simulation outputs are entered.
157168
@@ -168,6 +179,7 @@ def get_full_field_micro_output(self, micro_output: list) -> list:
168179
micro_sims_output[inactive_id] = deepcopy(
169180
micro_sims_output[self._sim_is_associated_to[inactive_id]]
170181
)
182+
self._interpolate_output(micro_input, micro_sims_output)
171183

172184
return micro_sims_output
173185

0 commit comments

Comments
 (0)