diff --git a/examples/micro-manager-cpp-adaptivity-config.json b/examples/micro-manager-cpp-adaptivity-config.json index 9a898e01..606fff2d 100644 --- a/examples/micro-manager-cpp-adaptivity-config.json +++ b/examples/micro-manager-cpp-adaptivity-config.json @@ -16,6 +16,7 @@ "history_param": 0.5, "coarsening_constant": 0.3, "refining_constant": 0.4, + "adaptive_coarsening_constant": true, "every_implicit_iteration": true, "output_cpu_time": true } diff --git a/micro_manager/adaptivity/adaptivity.py b/micro_manager/adaptivity/adaptivity.py index a129803f..0cb6b965 100644 --- a/micro_manager/adaptivity/adaptivity.py +++ b/micro_manager/adaptivity/adaptivity.py @@ -1,15 +1,18 @@ """ Functionality for adaptive initialization and control of micro simulations """ +from copy import deepcopy import sys +import os +import numpy as np from math import exp from typing import Callable +import re +import xml.etree.ElementTree as ET from warnings import warn import importlib from micro_manager.tools.logging_wrapper import Logger -import numpy as np - class AdaptivityCalculator: def __init__(self, configurator, rank, nsims) -> None: @@ -32,6 +35,15 @@ def __init__(self, configurator, rank, nsims) -> None: self._adaptivity_type = configurator.get_adaptivity_type() self._adaptivity_output_type = configurator.get_adaptivity_output_type() + self._dynamic_adaptivity = configurator.get_dynamic_adaptivity() + self._dynamic_refine_const = self._refine_const + self._precice_config_file_name = configurator.get_precice_config_file_name() + self._convergence_measure = [] + self._min_addition = 1.0 + self._logger = Logger( + "adaptivity-logger", "adaptivity-" + str(rank) + ".log", rank + ) + self._micro_problem = getattr( importlib.import_module( configurator.get_micro_file_name(), "MicroSimulation" @@ -50,6 +62,9 @@ def __init__(self, configurator, rank, nsims) -> None: # Start adaptivity calculation with all sims active # This array is modified in place via the function update_active_sims and update_inactive_sims self._is_sim_active = np.array([True] * nsims, dtype=np.bool_) + self._is_sim_active_ref = self._is_sim_active.copy() + self._is_sim_to_solve = self._is_sim_active.copy() + self._global_data_last = None # sim_is_associated_to: 1D array with values of associated simulations of inactive simulations. Active simulations have None # Active sims do not have an associated sim @@ -57,6 +72,7 @@ def __init__(self, configurator, rank, nsims) -> None: self._sim_is_associated_to = np.full((nsims), -2, dtype=np.intc) self._just_deactivated: list[int] = [] + self._just_deactivated_ref: list[int] = [] self._similarity_measure = self._get_similarity_measure( configurator.get_adaptivity_similarity_measure() @@ -95,6 +111,60 @@ def __init__(self, configurator, rank, nsims) -> None: csv_logger=True, ) + if self._dynamic_adaptivity: + # Read convergence measures from preCICE configuration file + self._data_values, self._limit_values = self.read_convergence_measures() + + def read_convergence_measures(self): + """ + Reads convergence measures from a preCICE configuration file. + + Parameters: + ----------- + config_file_name : str + Path to the preCICE configuration file. + + Returns: + -------- + data_values : list + List of data names involved in the convergence measurement + limit_values : list + List of limit attributes for corresponding data names + """ + # Read the XML configuration file + with open(self._precice_config_file_name, "r") as xml_file: + xml_data = xml_file.read() + + unique_names = [ + "absolute-convergence-measure", + "relative-convergence-measure", + "residual-relative-convergence-measure", + ] + + # Initialize lists to store the found attributes + data_values = [] + limit_values = [] + + for unique_name in unique_names: + pattern = f'<{unique_name} limit="([^"]+)" data="([^"]+)" mesh="([^"]+)"' + matches = re.finditer(pattern, xml_data) + for match in matches: + data_values.append(match.group(2)) + limit_values.append(match.group(1)) + + # Check if any matches were found + if data_values and limit_values: + for i, (data_value, limit_value) in enumerate( + zip(data_values, limit_values), start=1 + ): + print(f"Match {i}:") + print(f"Data: {data_value}") + print(f"Limit: {limit_value}") + else: + print(f"No attributes found for unique names: {unique_names}") + + return data_values, limit_values + def _update_similarity_dists(self, dt: float, data: dict) -> None: """ Calculate metric which determines if two micro simulations are similar enough to have one of them deactivated. @@ -119,11 +189,138 @@ def _update_similarity_dists(self, dt: float, data: dict) -> None: self._similarity_dists += dt * self._similarity_measure(data_vals) - def _update_active_sims(self) -> None: + def _reset_hist(self) -> None: + self._global_data_last = None + + def _get_addition(self) -> float: + """ + Get adapted refining constant based on limit values in preCICE configuration file and convergence measurements in preCICE + + Returns + ------- + adapted_similarity_const : float + """ + + # read convergence value from precice-Mysolver-convergence.log file + convergence_values = [] # last iteration + addition = 0.0 + + file_path = None + file_name_suffix = "-convergence.log" + + for root, _, files in os.walk(os.getcwd()): + for file_name in files: + if file_name.endswith(file_name_suffix): + file_path = os.path.join(root, file_name) + break + with open(file_path, "r") as file: + lines = file.readlines() + + if len(lines) > 1 and len(lines) > len(self._convergence_measure): + if len(lines) == 2: + self._convergence_measure.append(lines[0].strip().split()) + self._convergence_measure.append(lines[-1].strip().split()) + header_line = self._convergence_measure[0] + last_line = self._convergence_measure[-1] + + if int(last_line[0]) == 1: + self._logger.log_info("first time window") + addition = 0.0 + else: + if int(last_line[1]) == 1: + self._min_addition = 1.0 + else: + if self._min_addition == 0.0: + addition = 0.0 + else: + for data in self._data_values: + for element in header_line: + if data in element: + index = header_line.index(element) + if last_line[index] == "inf": + convergence_values.append(1e20) + else: + index_config = self._data_values.index(data) + convergence_values.append( + max( + float(last_line[index]), + float(self._limit_values[index_config]), + ) + ) + min_convergence = np.log10( + np.prod( + np.array(self._limit_values, dtype=float) + / np.array(convergence_values, dtype=float) + ) + ) + + self._logger.log_info( + "min Convergence: {} ".format(min_convergence) + ) + + alpha = 3.0 + addition = min( + self._min_addition, + min( + (1 + 1.0 / (min_convergence - 1.0)) ** alpha, + float(last_line[2]) + / self._max_similarity_dist + / self._coarse_const, + ), + ) + self._min_addition = addition + + return addition + + def _get_dynamic_adaptivity_refine_const(self) -> float: + """ + Get dynamic adaptivity refine constant. + + Returns + ------- + dynamic_adaptivity_refine_const : float + Dynamic adaptivity refine constant. + """ + return self._dynamic_refine_const + + def _update_active_sims(self, is_sim_active, just_deactivated) -> None: + """ + Update set of active micro simulations. + """ + self._is_sim_active = is_sim_active + self._just_deactivated = just_deactivated + self._is_sim_to_solve = self._is_sim_active.copy() + self._is_sim_active_ref = self._is_sim_active.copy() + + def _update_active_sims_ref(self, is_sim_active_ref, just_deactivated_ref) -> None: + """ + Update set of active micro simulations. + """ + self._is_sim_active_ref = is_sim_active_ref + self._just_deactivated_ref = just_deactivated_ref + + def _compute_active_sims(self, use_dyn) -> tuple: """ - Update set of active micro simulations. Active micro simulations are compared to each other + Campute the set of active micro simulations. Active micro simulations are compared to each other and if found similar, one of them is deactivated. """ + is_sim_active = self._is_sim_active_ref.copy() + just_deactivated = self._just_deactivated.copy() + + if use_dyn and self._dynamic_adaptivity: + addition = self._get_addition() * (1 - self._refine_const) + # self._min_addition = min(self._min_addition, addition) + # addition = self._min_addition + if addition > 0.0: + self._dynamic_refine_const = addition + self._refine_const + else: + self._dynamic_refine_const = self._refine_const + self._logger.log_info( + "Adaptive refine constant: {}".format(self._dynamic_refine_const) + ) + else: + self._dynamic_refine_const = self._refine_const + if self._max_similarity_dist == 0.0: warn( "All similarity distances are zero, probably because all the data for adaptivity is the same. Coarsening tolerance will be manually set to minimum float number." @@ -131,15 +328,58 @@ def _update_active_sims(self) -> None: self._coarse_tol = sys.float_info.min else: self._coarse_tol = ( - self._coarse_const * self._refine_const * self._max_similarity_dist + self._coarse_const + * self._dynamic_refine_const + * self._max_similarity_dist ) + self._logger.log_info("Coarsening tolerance: {}".format(self._coarse_tol)) # Update the set of active micro sims for i in range(self._is_sim_active.size): - if self._is_sim_active[i]: # if sim is active - if self._check_for_deactivation(i, self._is_sim_active): - self._is_sim_active[i] = False - self._just_deactivated.append(i) + if is_sim_active[i]: # if sim is active + if self._check_for_deactivation(i, is_sim_active): + is_sim_active[i] = False + just_deactivated.append(i) + return is_sim_active, just_deactivated + + def _compute_sims_to_solve(self, global_data: dict) -> None: + """ + Compute which simulations to solve. + """ + self._is_sim_to_solve = self._is_sim_active.copy() + hist_dist = 0.0 + + if self._global_data_last is None: + self._global_data_last = deepcopy(global_data) + return + else: + tol_u = ( + self._dynamic_refine_const + * self._max_similarity_dist + / sum(self._is_sim_active) + ) + self._logger.log_info("tol_u: {}".format(tol_u)) + if sum(self._is_sim_active) > 1: + for i in range(self._is_sim_active.size): + if self._is_sim_active[i]: + for name in global_data.keys(): + hist_dist = np.abs( + self._global_data_last[name][i] - global_data[name][i] + ) + self._logger.log_info( + "_global_data_last: {}, global_data: {}, hist_dist: {} for cell {}".format( + self._global_data_last[name][i], + global_data[name][i], + hist_dist, + i, + ) + ) + if hist_dist >= tol_u: + self._global_data_last[name][i] = global_data[name][i] + else: + self._is_sim_to_solve[i] = False + if sum(self._is_sim_to_solve) == 0: + self._is_sim_to_solve = self._is_sim_active.copy() def _associate_inactive_to_active(self) -> None: """ @@ -156,6 +396,7 @@ def _associate_inactive_to_active(self) -> None: for inactive_id in inactive_ids: # Begin with a large distance to trigger the search for the most similar active sim dist_min = dist_min_start_value + for active_id in active_ids: # Find most similar active sim for every inactive sim if self._similarity_dists[inactive_id, active_id] < dist_min: diff --git a/micro_manager/adaptivity/global_adaptivity.py b/micro_manager/adaptivity/global_adaptivity.py index b867e95c..bdc859d0 100644 --- a/micro_manager/adaptivity/global_adaptivity.py +++ b/micro_manager/adaptivity/global_adaptivity.py @@ -155,14 +155,60 @@ def compute_adaptivity( self._max_similarity_dist = np.amax(self._similarity_dists) - self._update_active_sims() + _is_sim_active_sta, _just_deactivated_sta = self._compute_active_sims(False) + ( + _is_sim_active_sta, + _sim_is_associated_to_sta, + _to_be_activated_ids_sta, + ) = self._compute_inactive_sims( + self._refine_const, _is_sim_active_sta, _just_deactivated_sta + ) + + self._update_active_sims_ref(_is_sim_active_sta, _just_deactivated_sta) + + self._logger.log_info( + "active sims (static): {}".format(np.sum(_is_sim_active_sta)) + ) + + _is_sim_active_dyn, _just_deactivated_dyn = self._compute_active_sims(True) + ( + _is_sim_active_dyn, + _sim_is_associated_to_dyn, + _to_be_activated_ids_dyn, + ) = self._compute_inactive_sims( + self._dynamic_refine_const, _is_sim_active_dyn, _just_deactivated_dyn + ) - self._update_inactive_sims(micro_sims) + self._logger.log_info( + "active sims (dynamic): {}".format(np.sum(_is_sim_active_dyn)) + ) + + self._update_active_sims(_is_sim_active_dyn, _just_deactivated_dyn) + + self._update_inactive_sims( + micro_sims, + _is_sim_active_dyn, + _sim_is_associated_to_dyn, + _to_be_activated_ids_sta, + ) + self._compute_sims_to_solve(global_data_for_adaptivity) + self._logger.log_info("sims to solve: {}".format(np.sum(self._is_sim_to_solve))) self._associate_inactive_to_active() self._precice_participant.stop_last_profiling_section() + if np.array_equal(_is_sim_active_dyn, _is_sim_active_sta) and np.array_equal( + _sim_is_associated_to_dyn, _sim_is_associated_to_sta + ): + self._dynamic_refine_const = self._refine_const + + self._logger.log_info( + "send refine const {} ---------------------------".format( + self._dynamic_refine_const + ) + ) + def get_active_sim_ids(self) -> np.ndarray: """ Get the ids of active simulations. @@ -176,6 +222,19 @@ def get_active_sim_ids(self) -> np.ndarray: self._is_sim_active[self._global_ids[0] : self._global_ids[-1] + 1] )[0] + def get_active_sim_ids_to_solve(self) -> np.ndarray: + """ + Get the ids of active simulations. + + Returns + ------- + numpy array + 1D array of active simulation ids + """ + return np.where( + self._is_sim_to_solve[self._global_ids[0] : self._global_ids[-1] + 1] + )[0] + def get_inactive_sim_ids(self) -> np.ndarray: """ Get the ids of inactive simulations. @@ -340,7 +399,33 @@ def _communicate_micro_output( for local_id in active_to_inactive_map[assoc_active_ids[count]]: micro_output[local_id] = deepcopy(output) - def _update_inactive_sims(self, micro_sims: list) -> None: + def _compute_inactive_sims( + self, _refine_const, is_sim_active, just_deactivated + ) -> tuple: + self._ref_tol = _refine_const * self._max_similarity_dist + + _sim_is_associated_to_updated = np.copy(self._sim_is_associated_to) + + # Check inactive simulations for activation and collect IDs of those to be activated + to_be_activated_ids = [] # Global IDs to be activated + for i in range(is_sim_active.size): + if not is_sim_active[i]: # if id is inactive + if self._check_for_activation(i, is_sim_active): + is_sim_active[i] = True + _sim_is_associated_to_updated[ + i + ] = -2 # Active sim cannot have an associated sim + if self._is_sim_on_this_rank[i] and i not in just_deactivated: + to_be_activated_ids.append(i) + return is_sim_active, _sim_is_associated_to_updated, to_be_activated_ids + + def _update_inactive_sims( + self, + micro_sims: list, + _is_sim_active, + _sim_is_associated_to_updated, + to_be_activated_ids, + ) -> None: """ Update set of inactive micro simulations. Each inactive micro simulation is compared to all active ones and if it is not similar to any of them, it is activated. @@ -352,23 +437,14 @@ def _update_inactive_sims(self, micro_sims: list) -> None: micro_sims : list List of objects of class MicroProblem, which are the micro simulations """ - self._ref_tol = self._refine_const * self._max_similarity_dist - - _sim_is_associated_to_updated = np.copy(self._sim_is_associated_to) - - # Check inactive simulations for activation and collect IDs of those to be activated - to_be_activated_ids = [] # Global IDs to be activated - for i in range(self._is_sim_active.size): - if not self._is_sim_active[i]: # if id is inactive - if self._check_for_activation(i, self._is_sim_active): - self._is_sim_active[i] = True - _sim_is_associated_to_updated[ - i - ] = -2 # Active sim cannot have an associated sim - if self._is_sim_on_this_rank[i] and i not in self._just_deactivated: - to_be_activated_ids.append(i) + self._is_sim_active = np.copy(_is_sim_active) + to_be_activated_ids_active = [] + for i in range(len(to_be_activated_ids)): + if self._is_sim_active[to_be_activated_ids[i]]: + to_be_activated_ids_active.append(to_be_activated_ids[i]) self._just_deactivated.clear() # Clear the list of sims deactivated in this step + self._just_deactivated_ref.clear() local_sim_is_associated_to = self._sim_is_associated_to[ self._global_ids[0] : self._global_ids[-1] + 1 @@ -378,7 +454,7 @@ def _update_inactive_sims(self, micro_sims: list) -> None: # global IDs of inactive sims associated to the active sims which are on this rank to_be_activated_map: Dict[int, list] = dict() - for i in to_be_activated_ids: + for i in to_be_activated_ids_active: # Only handle activation of simulations on this rank -- LOCAL SCOPE HERE ON if self._is_sim_on_this_rank[i]: to_be_activated_local_id = self._global_ids.index(i) diff --git a/micro_manager/adaptivity/local_adaptivity.py b/micro_manager/adaptivity/local_adaptivity.py index 45722bd8..cdd3b057 100644 --- a/micro_manager/adaptivity/local_adaptivity.py +++ b/micro_manager/adaptivity/local_adaptivity.py @@ -79,14 +79,37 @@ def compute_adaptivity( self._max_similarity_dist = np.amax(self._similarity_dists) - self._update_active_sims() - - self._update_inactive_sims(micro_sims) + _is_sim_active_sta, _just_deactivated_sta = self._compute_active_sims(False) + ( + _is_sim_active_sta, + _sim_is_associated_to_sta, + refine_const_sta, + ) = self._compute_inactive_sims(self._refine_const) + _is_sim_active_dyn, _just_deactivated_dyn = self._compute_active_sims(True) + ( + _is_sim_active_dyn, + _sim_is_associated_to_dyn, + _to_be_activated_ids_dyn, + ) = self._compute_inactive_sims(self._dynamic_refine_const) + + self._update_active_sims(_is_sim_active_dyn, _just_deactivated_dyn) + + self._update_inactive_sims( + micro_sims, + _is_sim_active_dyn, + _sim_is_associated_to_dyn, + _to_be_activated_ids_dyn, + ) self._associate_inactive_to_active() self._precice_participant.stop_last_profiling_section() + if np.array_equal(_is_sim_active_dyn, _is_sim_active_sta) and np.array_equal( + _sim_is_associated_to_dyn, _sim_is_associated_to_sta + ): + self._dynamic_refine_const = self._refine_const + def get_active_sim_ids(self) -> np.ndarray: """ Get the ids of active simulations. @@ -198,7 +221,26 @@ def log_metrics(self, n: int) -> None: ) ) - def _update_inactive_sims(self, micro_sims: list) -> None: + def _compute_inactive_sims(self, _refine_const) -> tuple: + self._ref_tol = _refine_const * self._max_similarity_dist + + _sim_is_associated_to_updated = np.copy(self._sim_is_associated_to) + is_sim_active = np.copy(self._is_sim_active) + + # Check inactive simulations for activation and collect IDs of those to be activated + to_be_activated_ids = [] # Global IDs to be activated + for i in range(is_sim_active.size): + if not is_sim_active[i]: # if id is inactive + if self._check_for_activation(i, is_sim_active): + is_sim_active[i] = True + _sim_is_associated_to_updated[ + i + ] = -2 # Active sim cannot have an associated sim + if self._is_sim_on_this_rank[i] and i not in self._just_deactivated: + to_be_activated_ids.append(i) + return is_sim_active, _sim_is_associated_to_updated, to_be_activated_ids + + def _update_inactive_sims(self, micro_sims: list, _is_sim_active) -> None: """ Update set of inactive micro simulations. Each inactive micro simulation is compared to all active ones and if it is not similar to any of them, it is activated. @@ -211,28 +253,10 @@ def _update_inactive_sims(self, micro_sims: list) -> None: micro_sims : list List containing micro simulation objects. """ - self._ref_tol = self._refine_const * self._max_similarity_dist - - to_be_activated_ids = [] - # Update the set of inactive micro sims - for i in range(self._is_sim_active.size): - if not self._is_sim_active[i]: # if id is inactive - if self._check_for_activation(i, self._is_sim_active): - self._is_sim_active[i] = True - if i not in self._just_deactivated: - to_be_activated_ids.append(i) + self._is_sim_active = np.copy(_is_sim_active) self._just_deactivated.clear() # Clear the list of sims deactivated in this step - # 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].set_state(micro_sims[associated_active_id].get_state()) - self._sim_is_associated_to[ - i - ] = -2 # Active sim cannot have an associated sim - # Delete the inactive micro simulations which have not been activated for i in range(self._is_sim_active.size): if not self._is_sim_active[i]: diff --git a/micro_manager/config.py b/micro_manager/config.py index 8d73ccac..5a20600a 100644 --- a/micro_manager/config.py +++ b/micro_manager/config.py @@ -51,6 +51,7 @@ def __init__(self, config_file_name): self._adaptivity_coarsening_constant = 0.5 self._adaptivity_refining_constant = 0.5 self._adaptivity_every_implicit_iteration = False + self._dynamic_adaptivity = False self._adaptivity_similarity_measure = "L1" self._adaptivity_output_type = "" self._adaptivity_output_n = 1 @@ -374,6 +375,21 @@ def read_json_micro_manager(self): "Micro Manager will compute adaptivity in every implicit iteration, if implicit coupling is done." ) + try: + adaptivity_for_refining_constant = self._data[ + "simulation_params" + ]["adaptivity_settings"]["adaptive_refining_constant"] + if adaptivity_for_refining_constant: + self._dynamic_adaptivity = True + self._logger.log_info_rank_zero( + "The adaptivity for refining constant is {}.".format( + self._dynamic_adaptivity + ) + ) + except: + self._logger.log_info_rank_zero( + "No dynamic adaptivity for refining constant provided, defaulting to False." + ) elif not adaptivity_every_implicit_iteration: self._adaptivity_every_implicit_iteration = False self._logger.log_info_rank_zero( @@ -385,6 +401,7 @@ def read_json_micro_manager(self): ) self._adaptivity_every_implicit_iteration = False + self._write_data_names.append("refine_const") self._write_data_names.append("active_state") self._write_data_names.append("active_steps") @@ -679,6 +696,18 @@ def get_adaptivity_refining_const(self): """ return self._adaptivity_refining_constant + def get_dynamic_adaptivity(self): + """ + Get adaptivity for refining constant. + More details: https://precice.org/tooling-micro-manager-configuration.html#adaptivity + + Returns + ------- + adaptivity_for_refining_constant : bool + Adaptivity for refining constant + """ + return self._dynamic_adaptivity + def get_adaptivity_similarity_measure(self): """ Get measure to be used to calculate similarity between pairs of simulations. diff --git a/micro_manager/micro_manager.py b/micro_manager/micro_manager.py index 61d418ef..ad39e4e5 100644 --- a/micro_manager/micro_manager.py +++ b/micro_manager/micro_manager.py @@ -142,6 +142,9 @@ def __init__(self, config_file: str, log_file: str = "") -> None: self._size, ) + self._micro_sims_input_last = None # DECLARATION + self._micro_sims_output_last = None # DECLARATION + # ************** # Public methods # ************** @@ -202,6 +205,9 @@ def solve(self) -> None: micro_sims_output = micro_sim_solve(micro_sims_input, dt) + self._micro_sims_input_last = micro_sims_input + self._micro_sims_output_last = micro_sims_output + # Check if more than a certain percentage of the micro simulations have crashed and terminate if threshold is exceeded if self._interpolate_crashed_sims: crashed_sims_on_all_ranks = np.zeros(self._size, dtype=np.int64) @@ -260,6 +266,10 @@ def solve(self) -> None: mem_usage_n.append(n) self._logger.log_info_rank_zero("Time window {} converged.".format(n)) + self._micro_sims_input_last = None + self._micro_sims_output_last = None + if self._is_adaptivity_on: + self._adaptivity_controller._reset_hist() # Reset first iteration flag for the next time window first_iteration = True @@ -823,10 +833,17 @@ def _solve_micro_simulations_with_adaptivity( List of dicts in which keys are names of data and the values are the data which are required outputs of """ active_sim_ids = self._adaptivity_controller.get_active_sim_ids() + active_sim_ids_to_solve = ( + self._adaptivity_controller.get_active_sim_ids_to_solve() + ) - micro_sims_output = [0] * self._local_number_of_sims + if self._micro_sims_input_last is None: + micro_sims_output = [0] * self._local_number_of_sims + else: + micro_sims_output = self._micro_sims_output_last # Solve all active micro simulations + # for active_id in active_sim_ids_to_solve: # to use hist for active_id in active_sim_ids: # If micro simulation has not crashed in a previous iteration, attempt to solve it if not self._has_sim_crashed[active_id]: @@ -912,6 +929,13 @@ def _solve_micro_simulations_with_adaptivity( for name in self._adaptivity_micro_data_names: self._data_for_adaptivity[name][i] = micro_sims_output[i][name] + # Add similarity constants to the output + ref_const = self._adaptivity_controller._get_dynamic_adaptivity_refine_const() + for inactive_id in inactive_sim_ids: + micro_sims_output[inactive_id]["refine_const"] = ref_const + for active_id in active_sim_ids: + micro_sims_output[active_id]["refine_const"] = ref_const + return micro_sims_output def _get_solve_variant(self) -> Callable[[list, float], list]: