diff --git a/changelog-entries/684.md b/changelog-entries/684.md new file mode 100644 index 000000000..dc4f8adf8 --- /dev/null +++ b/changelog-entries/684.md @@ -0,0 +1 @@ +- Added PCE-base surrogate for micro-dumux in two-scale heat conduction [#684](https://github.com/precice/tutorials/pull/684) diff --git a/tools/cleaning-tools.sh b/tools/cleaning-tools.sh index eaa4758fd..b8003eee7 100755 --- a/tools/cleaning-tools.sh +++ b/tools/cleaning-tools.sh @@ -199,6 +199,7 @@ clean_dumux() { echo "- Cleaning up DuMuX case in $(pwd)" rm -fv ./*.vtu rm -fv ./*.pvd + rm -fv ./*.hdf5 clean_precice_logs . clean_case_logs . ) diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/clean.sh b/two-scale-heat-conduction/micro-dumux-surrogate/clean.sh new file mode 100755 index 000000000..3e9fd4e51 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/clean.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +set -e -u + +. ../../tools/cleaning-tools.sh + +clean_dumux . diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-model-switching-config.json b/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-model-switching-config.json new file mode 100644 index 000000000..860327c02 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-model-switching-config.json @@ -0,0 +1,22 @@ +{ + "micro_file_names": ["micro_sim_sur", "micro_sim"], + "coupling_params": { + "precice_config_file_name": "../precice-config.xml", + "macro_mesh_name": "Macro-Mesh", + "write_data_names": ["K00", "K11", "Porosity"], + "read_data_names": ["Concentration"] + }, + "simulation_params": { + "micro_dt": 0.01, + "macro_domain_bounds": [0.0, 1.0, 0.0, 0.5], + "decomposition": [2, 1], + "adaptivity": false, + "model_adaptivity": true, + "model_adaptivity_settings": { + "switching_function": "switch-model" + } + }, + "diagnostics": { + "data_from_micro_sims": ["grain_size"] + } +} diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-snapshot-config.json b/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-snapshot-config.json new file mode 100644 index 000000000..3a527daad --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/micro-manager-snapshot-config.json @@ -0,0 +1,15 @@ +{ + "micro_file_names": ["micro_sim"], + "coupling_params": { + "parameter_file_name": "macro-concentration-samples.hdf5", + "write_data_names": ["K00", "K11", "Porosity"], + "read_data_names": ["Concentration"] + }, + "simulation_params": { + "micro_dt": 0.01 + }, + "snapshot_params": { + "output_file_name": "micro-dumux-snapshots", + "initialize_once": false + } +} diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim.pc.in b/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim.pc.in new file mode 100644 index 000000000..566aba5e4 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim.pc.in @@ -0,0 +1,15 @@ +prefix=@prefix@ +exec_prefix=@exec_prefix@ +libdir=@libdir@ +includedir=@includedir@ +CXX=@CXX@ +CC=@CC@ +DEPENDENCIES=@REQUIRES@ + +Name: @PACKAGE_NAME@ +Version: @VERSION@ +Description: micro_sim module +URL: http://dune-project.org/ +Requires: dumux-phasefield dumux-precice +Libs: -L${libdir} +Cflags: -I${includedir} diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim_sur.py b/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim_sur.py new file mode 100644 index 000000000..0a6a6d19e --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/micro_sim_sur.py @@ -0,0 +1,86 @@ +""" +Micro simulation Surrogate, requrie previous computation of surrogate model +""" +import joblib +import numpy as np +import math + + +class MicroSimulation: + + def __init__(self, sim_id): + """ + Get the micro-scale model from the BayesValidRox surrogate model. + + Parameters + ---------- + sim_id : int + The simulation ID for the micro-scale simulation. + """ + self._sim_id = sim_id + self._state = None + + self._model = None + with open('micro-dumux-surrogate.pkl', 'rb') as input: + self._model = joblib.load(input) + if self._model is None: + raise RuntimeError("Failed to load model.") + + def initialize(self): + output_data = dict() + output_data["K00"] = 0.4912490635619572 + output_data["K11"] = 0.4912490635989945 + output_data["Porosity"] = 0.4933482661391027 + + if self._sim_id == 0: + output_data["K00"] = 0.4912490640081466 + output_data["K11"] = 0.4912490640081367 + + return output_data + + def get_state(self): + """ + Get the current state of the micro-scale simulation. + + Returns + ------- + state : dict + The current state of the micro-scale simulation. + """ + return self._state + + def set_state(self, state): + """ + Set the current state of the micro-scale simulation. + + Parameters + ---------- + state : dict + The state to set for the micro-scale simulation. + """ + self._state = state + + def solve(self, macro_data, dt): + """ + Solve the micro-scale simulation using the surrogate model. + + Parameters + ---------- + macro_data : dict + The macro-scale data required for the micro-scale simulation. + dt : float + The time step for the micro-scale simulation. + + Returns + ------- + output_data : dict + The output data from the micro-scale simulation. + """ + model_eval, _ = self._model.eval_metamodel(np.array([macro_data["Concentration"]])[:, np.newaxis]) + output_data = dict() + output_data["K00"] = model_eval["K00"][0][0] + output_data["K11"] = model_eval["K11"][0][0] + output_data["Porosity"] = model_eval["Porosity"][0][0] + output_data["grain_size"] = math.sqrt((1 - model_eval["Porosity"][0][0]) / math.pi) + + return output_data diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/model.py b/two-scale-heat-conduction/micro-dumux-surrogate/model.py new file mode 100644 index 000000000..5284f7485 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/model.py @@ -0,0 +1,5 @@ +# Dummy model function for the micro-dumux surrogate. +# We do not wrap the original DuMuX model because we will directly provide +# snapshots (computed by the Micro Manager) to BayesValidRox. +def model(samples): + return None diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/params.input b/two-scale-heat-conduction/micro-dumux-surrogate/params.input new file mode 100644 index 000000000..964a6896d --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/params.input @@ -0,0 +1,25 @@ +[Assembly] +Multithreading = false + +[TimeLoop] +TEnd = 0.25 # end time of the simulation +DtInitial = 0.01 # initial time step size +MaxTimeStepSize = 0.01 # maximal time step size + +[Grid] +LowerLeft = 0.0 0.0 # lower left (front) corner of the domain (keep this fixed at 0 0!) +UpperRight = 1.0 1.0 # upper right (back) corner of the domain +Cells = 80 80 # grid resolution in each coordinate direction +Periodic = 1 1 # Periodic Boundary conditions in both dimensions + +[Problem] +xi = 0.08 # phasefield parameter (lambda, set to around 4/Ncells) +omega = 0.01 # phasefield diffusivity/surface tension parameter (gamma) +kt = 1.0 # constant deciding speed of expansion/contraction +eqconc = 0.5 # equilibrium concentration +ks = 1.0 # conductivity of sand material +kg = 0.0 # conductivity of void material +Name = cell_phase # base name for VTK output files +Radius = 0.4 # initial radius of the grain +PhasefieldICScaling = 4.0 # factor in initial phasefield function +MaxPorosity = 0.9686 # porosity cap diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/requirements.txt b/two-scale-heat-conduction/micro-dumux-surrogate/requirements.txt new file mode 100644 index 000000000..8970d34da --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/requirements.txt @@ -0,0 +1,4 @@ +numpy +bayesvalidrox +pyprecice +micro-manager-precice diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/run-surrogate-workflow.sh b/two-scale-heat-conduction/micro-dumux-surrogate/run-surrogate-workflow.sh new file mode 100755 index 000000000..ddf636773 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/run-surrogate-workflow.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -e -u + +python3 -m venv .venv +. .venv/bin/activate + +pip install -r requirements.txt + +python surrogate_workflow.py diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/run.sh b/two-scale-heat-conduction/micro-dumux-surrogate/run.sh new file mode 100755 index 000000000..7736ee581 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -e -u + +. ../../tools/log.sh +exec > >(tee --append "$LOGFILE") 2>&1 + +usage() { echo "Usage: cmd [-s] [-p n]" 1>&2; exit 1; } + +# Check if no input argument was provided +if [ -z "$*" ] ; then + echo "No input argument provided. Micro Manager is launched in serial" + micro-manager-precice micro-manager-model-switching-config.json +fi + +while getopts ":sp" opt; do + case ${opt} in + s) + micro-manager-precice micro-manager-model-switching-config.json + ;; + p) + mpiexec -n "$2" micro-manager-precice micro-manager-model-switching-config.json + ;; + *) + usage + ;; + esac +done + +close_log diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/surrogate_workflow.py b/two-scale-heat-conduction/micro-dumux-surrogate/surrogate_workflow.py new file mode 100644 index 000000000..69581a747 --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/surrogate_workflow.py @@ -0,0 +1,194 @@ +import os +import subprocess +from bayesvalidrox import PyLinkForwardModel, Input, PCE, ExpDesigns, Engine +import h5py +import joblib +import numpy as np +import matplotlib.pyplot as plt + + +def create_snapshots() -> None: + """ + Create snapshots of the DuMuX model in micro-dumux/ using the Micro Manager. + """ + # What effect do uniformly distributed concentration samples have on the quality of the surrogate model? + concentration_samples = np.linspace(0.0, 0.9, 500) + + # The name of the HDF5 file should be the same as the parameter_file_name in micro-manager-snapshot-config.json + with h5py.File("macro-concentration-samples.hdf5", "w") as f: + f.create_dataset("Concentration", data=concentration_samples) + + # Run the Micro Manager to create snapshot database + subprocess.run( + ["mpiexec", "-n", "2", "micro-manager-precice", "--snapshot", "micro-manager-snapshot-config.json"], + check=True, + ) + + +def read_snapshots() -> tuple: + """ + Read the snapshots created by the Micro Manager and return the inputs and outputs. + The inputs are the concentration samples, and the outputs are the porosity and conductivity matrix values. + + Returns + ------- + tuple + A tuple containing the inputs (concentration samples) and outputs (porosity and conductivity values). + """ + with h5py.File("micro-dumux-snapshots.hdf5", "r") as f: + concentration_data = f["Concentration"][:] + porosity_data = f["Porosity"][:] + k_00_data = f["K00"][:] + k_11_data = f["K11"][:] + + concentration = np.swapaxes(np.array([concentration_data]), 0, 1) + porosity = np.swapaxes(np.array([porosity_data]), 0, 1) + k_00 = np.swapaxes(np.array([k_00_data]), 0, 1) + k_11 = np.swapaxes(np.array([k_11_data]), 0, 1) + + outputs = {"Porosity": porosity, "K00": k_00, "K11": k_11, "x_values": np.array([0])} + return concentration, outputs + + +def split_samples(X, y, n_valid): + """ + Split the samples and evaluations into training and validation/test data. + The split is performed randomly. + + Parameters + ---------- + X : np.ndarray + Samples, shape (#samples, #parameters) + y : dict + Corresponding model evaluations. Expected to match BVR output format. + n_valid : int + Number of samples to keep for validation. + + Returns + ------- + X_train, y_train, + X_valid, y_valid + """ + n_samples = X.shape[0] + if n_valid >= n_samples: + raise AttributeError('The set number of validation points is invalid.') + + # Random split + n_train = n_samples - n_valid + choice = np.random.choice( + range(n_samples), size=(n_train,), replace=False + ) + ind = np.zeros(n_samples, dtype=bool) + ind[choice] = True + + # Split samples + X_train = X[ind] + X_valid = X[~ind] + + # Split outputs + y_train = {} + y_valid = {} + for key in y: + if key != "x_values": + y_train[key] = y[key][ind] + y_valid[key] = y[key][~ind] + + return X_train, y_train, X_valid, y_valid + + +def create_surrogate() -> tuple: + """ + Create a surrogate model from the Micro Manager snapshots. + + Returns + ------- + tuple + A tuple containing the validation samples and corresponding model evaluations. + """ + # We create a fake model from model.py because we directly provide the + # input and outputs from the Micro Manager snapshots. + model = PyLinkForwardModel() + model.py_file = "model" + model.name = "micro-dumux-surrogate" + model.link_type = "function" + model.output_names = ["Porosity", "K00", "K11"] + + x, y = read_snapshots() + + # Split the samples into training and validation sets + n_valid = 200 + x_train, y_train, x_valid, y_valid = split_samples(x, y, n_valid) + + inputs = Input() + inputs.add_marginals(name="Concentration", dist_type="unif", parameters=[0, 0.5]) + + exp_design = ExpDesigns(inputs) + exp_design.x = x_train + exp_design.y = y_train + + # Create the surrogate model + meta_model = PCE(inputs) + meta_model.meta_model_type = "aPCE" + meta_model.pce_reg_method = "FastARD" + meta_model.pce_deg = 5 + + # Train the surrogate model + engine = Engine(meta_model, model, exp_design) + engine.train_normal() + + with open(f'{model.name}.pkl', 'wb') as output: + joblib.dump(engine, output, 2) + + return x_valid, y_valid + + +def validate_surrogate(x_valid, y_valid, model_name="micro-dumux-surrogate.pkl") -> None: + """ + Validate the surrogate model using the validation samples and outputs. + + Parameters + ---------- + x_valid : np.ndarray + Validation samples. + y_valid : dict + Corresponding model evaluations for validation samples. + model_name : str + Name of the surrogate model file. + """ + with open(model_name, 'rb') as input: + engine = joblib.load(input) + + y_metamod, _ = engine.eval_metamodel(x_valid) + + # engine.plot_adapt(y_valid, y_metamod, y_metamod_std, x_valid) + + # Compare predictions with true values + plt.figure() + plt.scatter(y_valid["Porosity"], y_metamod["Porosity"]) + plt.xlabel("True Values") + plt.ylabel("Predictions") + plt.title(f"Validation: porosity") + plt.plot([0.4, 1.1], [0.4, 1.1], "k--") + plt.xlim(0.4, 1.1) + plt.ylim(0.4, 1.1) + plt.savefig("surrogate-validation-porosity.pdf", bbox_inches="tight") + + plt.figure() + plt.scatter(x_valid, y_valid["Porosity"]) + plt.xlabel("Validation Concentration") + plt.ylabel("Validation Porosity") + plt.title("IO: Concentration to Porosity") + plt.savefig("surrogate-validation-concentration-porosity.pdf", bbox_inches="tight") + + +def main(): + """ + Create snaphots of the DuMuX model, create a surrogate model from the snapshots, and validate the surrogate model. + """ + create_snapshots() + x_valid, y_valid = create_surrogate() + validate_surrogate(x_valid, y_valid) + + +if __name__ == "__main__": + main() diff --git a/two-scale-heat-conduction/micro-dumux-surrogate/switch-model.py b/two-scale-heat-conduction/micro-dumux-surrogate/switch-model.py new file mode 100644 index 000000000..eb2d99b0d --- /dev/null +++ b/two-scale-heat-conduction/micro-dumux-surrogate/switch-model.py @@ -0,0 +1,48 @@ +import numpy as np +from typing import Dict + + +def switching_function(model_res: int, location: np.ndarray, t: float, input: Dict, prev_output: Dict): + """ + Determine which model to use based on the resolution, location, time, input, and previous output. + + Parameters + ---------- + model_res : int + The resolution level of the model (0 for full-order model, >0 for reduced-order model). + location : np.ndarray + The macro-scale coordinates corresponding to the micro-scale simulation. + t : float + The current time step. + input : Dict + The input data for the model, including concentration. + prev_output : dict + The output from the previous time step. + + Returns + ------- + model_res_offset : int + The offset to change the model used from the provided hierarchy of models. + """ + model_res_offset = 0 + + # Only use the full-order model for the first time step + if t == 0.0: + if model_res > 0: + model_res_offset = -1 + else: + model_res_offset = 0 + + # After the first time step, the model is selected dynamically based on + # the concentration value and the model resolution. + else: + concentration = input["Concentration"] + is_valid_range = 0.2 < concentration < 0.3 + is_fom = model_res == 0 + + if is_fom and is_valid_range: + model_res_offset = 1 + if not is_fom and not is_valid_range: + model_res_offset = -1 + + return model_res_offset diff --git a/two-scale-heat-conduction/precice-config.xml b/two-scale-heat-conduction/precice-config.xml index 028d0bf31..8e85953c2 100644 --- a/two-scale-heat-conduction/precice-config.xml +++ b/two-scale-heat-conduction/precice-config.xml @@ -16,6 +16,7 @@ + @@ -25,6 +26,7 @@ + @@ -44,6 +46,7 @@ +