From ab25875b7f57b55cb45ed95671c89093d69f6fc0 Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Wed, 7 Jan 2026 18:31:20 +0100 Subject: [PATCH 1/7] Run DuMuX simulation inside container via Snakemake --- benchmarks/rotating-cylinders/dumux/.DS_Store | Bin 0 -> 6148 bytes .../rotating-cylinders/dumux/.simulation_done | 0 benchmarks/rotating-cylinders/dumux/Snakefile | 26 ++++++++++ .../dumux/docker_rotatingCylinders.sh | 49 ++++++++++++++++++ .../rotating-cylinders/dumux/environment.yaml | 10 ++++ 5 files changed, 85 insertions(+) create mode 100644 benchmarks/rotating-cylinders/dumux/.DS_Store create mode 100644 benchmarks/rotating-cylinders/dumux/.simulation_done create mode 100644 benchmarks/rotating-cylinders/dumux/Snakefile create mode 100644 benchmarks/rotating-cylinders/dumux/docker_rotatingCylinders.sh create mode 100644 benchmarks/rotating-cylinders/dumux/environment.yaml diff --git a/benchmarks/rotating-cylinders/dumux/.DS_Store b/benchmarks/rotating-cylinders/dumux/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 [options]" + echo "" + echo " docker_rotatingCylinders.sh open [image] - run a container from the image." + echo " docker_rotatingCylinders.sh help - display this message." + echo "" + echo "Optionally supply a Docker image name to the open command." + echo "" +} + +# start a container. Only argument is the Docker image. +open() +{ + IMAGE="$1" + + if docker ps -a --format '{{.Names}}' | grep -Eq "^dumux_rotatingCylinders\$"; then + echo "Removing existing container 'dumux_rotatingCylinders'" + docker rm -f dumux_rotatingCylinders + fi + + docker run --rm \ + -e HOST_UID=$(id -u) \ + -e HOST_GID=$(id -g) \ + -v "$SHARED_DIR_HOST:$SHARED_DIR_CONTAINER" \ + --name dumux_rotatingCylinders \ + "$IMAGE" +} + +# Check if user specified valid command otherwise print help message +if [ "$1" == "open" ]; then + IMAGE="$2" : ${IMAGE:="$IMAGE_NAME"} + open $IMAGE +else + help + exit 1 +fi diff --git a/benchmarks/rotating-cylinders/dumux/environment.yaml b/benchmarks/rotating-cylinders/dumux/environment.yaml new file mode 100644 index 0000000..43aa3e1 --- /dev/null +++ b/benchmarks/rotating-cylinders/dumux/environment.yaml @@ -0,0 +1,10 @@ +name: rc-dumux +channels: + - conda-forge +dependencies: + - python=3.11 + - bash + - coreutils + - pip + - pip: + - snakemake From 9d2e868db119d416df66bde09d2326e27f114a26 Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Wed, 7 Jan 2026 18:59:46 +0100 Subject: [PATCH 2/7] Update .gitignore and remove unnecessary files (.DS_Store, .simulation_done) --- .gitignore | 14 ++++++++++++-- benchmarks/rotating-cylinders/dumux/.DS_Store | Bin 6148 -> 0 bytes .../rotating-cylinders/dumux/.simulation_done | 0 3 files changed, 12 insertions(+), 2 deletions(-) delete mode 100644 benchmarks/rotating-cylinders/dumux/.DS_Store delete mode 100644 benchmarks/rotating-cylinders/dumux/.simulation_done diff --git a/.gitignore b/.gitignore index f684468..bfbec91 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ -.snakemake -site \ No newline at end of file +site + +# macOS system files +.DS_Store + +# Snakemake hidden folders +.snakefile/ +.snakemake/ + +# Sentinel / temporary files +*.done +.simulation_done diff --git a/benchmarks/rotating-cylinders/dumux/.DS_Store b/benchmarks/rotating-cylinders/dumux/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Thu, 22 Jan 2026 10:52:47 +0100 Subject: [PATCH 3/7] dumux simulation can run via snakemake rule --- benchmarks/rotating-cylinders/Snakefile | 70 ++++++++++++++ .../rotating-cylinders/config_template.json | 8 ++ .../create_dumux_summary.py | 53 +++++++++++ benchmarks/rotating-cylinders/dumux/Snakefile | 58 +++++++++--- .../dumux/docker_rotatingCylinders.sh | 49 ---------- .../dumux/run_dumux_postprocessing.py | 93 +++++++++++++++++++ .../{dumux => }/environment.yaml | 1 + .../generate_inputs_and_config.py | 65 +++++++++++++ benchmarks/rotating-cylinders/params.template | 55 +++++++++++ 9 files changed, 388 insertions(+), 64 deletions(-) create mode 100644 benchmarks/rotating-cylinders/Snakefile create mode 100644 benchmarks/rotating-cylinders/config_template.json create mode 100644 benchmarks/rotating-cylinders/create_dumux_summary.py delete mode 100644 benchmarks/rotating-cylinders/dumux/docker_rotatingCylinders.sh create mode 100644 benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py rename benchmarks/rotating-cylinders/{dumux => }/environment.yaml (89%) create mode 100644 benchmarks/rotating-cylinders/generate_inputs_and_config.py create mode 100644 benchmarks/rotating-cylinders/params.template diff --git a/benchmarks/rotating-cylinders/Snakefile b/benchmarks/rotating-cylinders/Snakefile new file mode 100644 index 0000000..c7f71ed --- /dev/null +++ b/benchmarks/rotating-cylinders/Snakefile @@ -0,0 +1,70 @@ +# benchmarks/rotating-cylinders/Snakefile + +import json +import os + +# -------------------------- +# Load workflow configuration +# -------------------------- +with open("rotating-cylinders_config.json") as f: + config = json.load(f) + +tools = config["tools"] +benchmark = config["benchmark"] +benchmark_uri = config["benchmark_uri"] +configurations = config["configurations"] +config_to_param = config["configuration_to_parameter_file"] +config_to_vtu_files = config.get("configuration_to_solution_vtu_files", {}) + +# -------------------------- +# Helper functions +# -------------------------- +def parameter_file(cfg): + return config_to_param[cfg] + +def vtu_files(cfg): + return config_to_vtu_files.get(cfg, []) + +def summary_file(tool_name): + return f"{tool_name}_summary.json" + +# -------------------------- +# Include tool Snakefiles +# -------------------------- +for tool in tools: + include: f"{tool}/Snakefile" + +# -------------------------- +# Global result directory +# -------------------------- +result_dir = f"snakemake_results/rotating-cylinders" + +# -------------------------- +# Rule: Aggregate all outputs +# -------------------------- +rule all: + input: + expand(f"{result_dir}/dumux/solution_metrics_{{configuration}}.json", configuration=configurations), + expand(f"{result_dir}/dumux/solution_field_data_{{configuration}}.zip", configuration=configurations) + +# -------------------------- +# Rule: Create summary +# -------------------------- +rule create_summary: + input: + parameter_files=[parameter_file(cfg) for cfg in configurations], + solution_vtu_files=sum([vtu_files(cfg) for cfg in configurations], []) # flatten list of lists + output: + summary=summary_file("dumux") + params: + configs=configurations + shell: + """ + python create_dumux_summary.py \ + --input_configuration {params.configs} \ + --input_parameter_file {input.parameter_files} \ + --input_solution_vtu {input.solution_vtu_files} \ + --input_benchmark "{benchmark}" \ + --input_benchmark_uri "{benchmark_uri}" \ + --output_summary_json {output.summary} + """ \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/config_template.json b/benchmarks/rotating-cylinders/config_template.json new file mode 100644 index 0000000..05171a7 --- /dev/null +++ b/benchmarks/rotating-cylinders/config_template.json @@ -0,0 +1,8 @@ +{ + "benchmark": "rotating-cylinders", + "benchmark_uri": "https://www.openfoam.com/documentation/guides/latest/doc/verification-validation-rotating-cylinders-2d.html", + "tools": ["dumux"], + "configurations": [], + "configuration_to_parameter_file": {}, + "configuration_to_solution_vtu_files": {} +} diff --git a/benchmarks/rotating-cylinders/create_dumux_summary.py b/benchmarks/rotating-cylinders/create_dumux_summary.py new file mode 100644 index 0000000..03f075a --- /dev/null +++ b/benchmarks/rotating-cylinders/create_dumux_summary.py @@ -0,0 +1,53 @@ +import json +import argparse +import meshio + +def create_dumux_summary(configurations, parameter_files, solution_vtu_files, benchmark, benchmark_uri, summary_json): + summaries = [] + for cfg, param_file, vtu_file in zip(configurations, parameter_files, solution_vtu_files): + summary = { + "benchmark": benchmark, + "benchmark_uri": benchmark_uri, + "configuration": cfg + } + # Load parameters + with open(param_file) as f: + summary["parameters"] = json.load(f) + + summary["solution_vtu"] = vtu_file + + # Extract field summaries + vtu = meshio.read(vtu_file) + field_summaries = {} + for key, data in vtu.point_data.items(): + field_summaries[key] = { + "min": float(data.min()), + "max": float(data.max()), + "mean": float(data.mean()) + } + summary["solution_fields_summary"] = field_summaries + + summaries.append(summary) + + with open(summary_json, "w") as f: + json.dump(summaries, f, indent=4) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input_configuration", nargs="+", required=True) + parser.add_argument("--input_parameter_file", nargs="+", required=True) + parser.add_argument("--input_solution_vtu", nargs="+", required=True) + parser.add_argument("--input_benchmark", required=True) + parser.add_argument("--input_benchmark_uri", required=True) + parser.add_argument("--output_summary_json", required=True) + args = parser.parse_args() + + create_dumux_summary( + args.input_configuration, + args.input_parameter_file, + args.input_solution_vtu, + args.input_benchmark, + args.input_benchmark_uri, + args.output_summary_json + ) diff --git a/benchmarks/rotating-cylinders/dumux/Snakefile b/benchmarks/rotating-cylinders/dumux/Snakefile index c325902..4ab8b12 100644 --- a/benchmarks/rotating-cylinders/dumux/Snakefile +++ b/benchmarks/rotating-cylinders/dumux/Snakefile @@ -1,26 +1,54 @@ -# Snakefile — rotating cylinders benchmark - import os -import glob +# 1. Setup and Config tool = "dumux" +result_dir = f"snakemake_results/rotating-cylinders/{tool}" +configurations = config["configurations"] +configuration_to_parameter_file = config["configuration_to_parameter_file"] +vtu_mapping = config["configuration_to_solution_vtu_files"] -# Directory where simulation writes VTU files -output_dir = "." +container_image = "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0" +shared_dir = os.getcwd() +# dumux_input = f"{shared_dir}/{tool}/input_files" +dumux_dir = f"{shared_dir}/{tool}" -# Dynamically find all VTU files starting with "test_rotatingcylinders" -vtu_files = glob.glob(os.path.join(output_dir, "test_rotatingcylinders*.vtu")) +# Rule 1: Only runs the simulation, outputs the VTU files +rule run_dumux_simulation: + input: + params=lambda wildcards: f"{dumux_dir}/input_files/{configuration_to_parameter_file[wildcards.configuration]}" + output: + # Use a list of the actual files you expect + vtu_files = [ + f"{tool}/test_rotatingcylinders_{{configuration}}-00000.vtu", + f"{tool}/test_rotatingcylinders_{{configuration}}-00001.vtu" + ] + resources: + serial_run=1 + singularity: + f"docker://{container_image}" + shell: + """ + set -euo pipefail + cd /dumux/rotating-cylinders/build-cmake/test/freeflow/navierstokes/rotatingcylinders + ./test_ff_navierstokes_rotatingcylinders {input.params} -IO.OutputPath {dumux_dir} + """ -rule run_rotating_cylinders_simulation: +# Rule 2: Only runs post-processing, depends on the VTU files +rule postprocess_dumux: input: - script = "docker_rotatingCylinders.sh" + sim_data = [ + f"{tool}/test_rotatingcylinders_{{configuration}}-00000.vtu", + f"{tool}/test_rotatingcylinders_{{configuration}}-00001.vtu" + ], + postprocess_script=f"{tool}/run_dumux_postprocessing.py" output: - # We can use a sentinel file since the container produces multiple outputs - os.path.join(output_dir, ".simulation_done") - conda: - "environment.yaml", + metrics=f"{result_dir}/solution_metrics_{{configuration}}.json", + fields=f"{result_dir}/solution_field_data_{{configuration}}.zip" shell: """ - bash {input.script} open - touch {output} + python3 {input.postprocess_script} \ + --input_dumux_output_dir {dumux_dir} \ + --input_configuration {wildcards.configuration} \ + --output_solution_file_zip {output.fields} \ + --output_metrics_file {output.metrics} """ \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/dumux/docker_rotatingCylinders.sh b/benchmarks/rotating-cylinders/dumux/docker_rotatingCylinders.sh deleted file mode 100644 index 46cf71f..0000000 --- a/benchmarks/rotating-cylinders/dumux/docker_rotatingCylinders.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright © DuMux Project contributors, see AUTHORS.md in root folder -# SPDX-License-Identifier: GPL-3.0-or-later - -IMAGE_NAME=git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:1.0 - -# the host directory ... -SHARED_DIR_HOST="$(pwd)" -# ... that is mounted into this container directory: -SHARED_DIR_CONTAINER="/dumux/shared" - -help () -{ - echo "" - echo "Usage: docker_rotatingCylinders.sh [options]" - echo "" - echo " docker_rotatingCylinders.sh open [image] - run a container from the image." - echo " docker_rotatingCylinders.sh help - display this message." - echo "" - echo "Optionally supply a Docker image name to the open command." - echo "" -} - -# start a container. Only argument is the Docker image. -open() -{ - IMAGE="$1" - - if docker ps -a --format '{{.Names}}' | grep -Eq "^dumux_rotatingCylinders\$"; then - echo "Removing existing container 'dumux_rotatingCylinders'" - docker rm -f dumux_rotatingCylinders - fi - - docker run --rm \ - -e HOST_UID=$(id -u) \ - -e HOST_GID=$(id -g) \ - -v "$SHARED_DIR_HOST:$SHARED_DIR_CONTAINER" \ - --name dumux_rotatingCylinders \ - "$IMAGE" -} - -# Check if user specified valid command otherwise print help message -if [ "$1" == "open" ]; then - IMAGE="$2" : ${IMAGE:="$IMAGE_NAME"} - open $IMAGE -else - help - exit 1 -fi diff --git a/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py b/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py new file mode 100644 index 0000000..1dbe5bc --- /dev/null +++ b/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py @@ -0,0 +1,93 @@ +import json +import zipfile +from argparse import ArgumentParser +from pathlib import Path + +import meshio +import numpy as np + + +def run_dumux_postprocessing( + dumux_output_dir: str, + configuration: str, + metrics_file: str, + solution_file_zip: str, +) -> None: + dumux_output_dir = Path(dumux_output_dir) + + # ---- read all VTU / PVTU files ---- + vtk_files = list(dumux_output_dir.glob("*.vtu")) + list( + dumux_output_dir.glob("*.pvtu") + ) + if not vtk_files: + raise RuntimeError("No VTU/PVTU files found in DuMuX output directory") + + # Read first file for metrics (extend if needed) + mesh = meshio.read(vtk_files[0]) + + metrics = {} + + # ---- example metrics (adapt to benchmark definition) ---- + if "p" in mesh.cell_data_dict: + p = mesh.cell_data_dict["p"] + # flatten possible block structure + p_vals = np.concatenate([np.asarray(v).ravel() for v in p.values()]) + metrics["min_pressure"] = float(np.min(p_vals)) + metrics["max_pressure"] = float(np.max(p_vals)) + metrics["mean_pressure"] = float(np.mean(p_vals)) + + if "velocity_liq (m/s)" in mesh.cell_data_dict: + v = mesh.cell_data_dict["velocity_liq (m/s)"] + v_vals = np.concatenate([np.asarray(vv) for vv in v.values()]) + speed = np.linalg.norm(v_vals, axis=1) + metrics["max_velocity_magnitude"] = float(np.max(speed)) + metrics["mean_velocity_magnitude"] = float(np.mean(speed)) + + # ---- write metrics JSON ---- + with open(metrics_file, "w") as f: + json.dump(metrics, f, indent=4) + + # ---- zip solution field data ---- + with zipfile.ZipFile(solution_file_zip, "w", zipfile.ZIP_DEFLATED) as zipf: + for file in vtk_files: + zipf.write(file, arcname=file.name) + + # ---- cleanup: delete original VTU/PVTU files ---- + for ext in ["*.vtu", "*.pvtu", "*.pvd"]: + for file in dumux_output_dir.glob(ext): + file.unlink() + + +if __name__ == "__main__": + parser = ArgumentParser( + description="Post-process DuMuX results into benchmark artifacts" + ) + parser.add_argument( + "--input_dumux_output_dir", + required=True, + help="Directory containing DuMuX VTU/PVTU files", + ) + parser.add_argument( + "--input_configuration", + required=True, + help="Configuration name", + ) + parser.add_argument( + "--output_metrics_file", + required=True, + help="Path to solution_metrics_{configuration}.json", + ) + parser.add_argument( + "--output_solution_file_zip", + required=True, + help="Path to solution_field_data_{configuration}.zip", + ) + + args = parser.parse_args() + + run_dumux_postprocessing( + args.input_dumux_output_dir, + args.input_configuration, + args.output_metrics_file, + args.output_solution_file_zip, + ) diff --git a/benchmarks/rotating-cylinders/dumux/environment.yaml b/benchmarks/rotating-cylinders/environment.yaml similarity index 89% rename from benchmarks/rotating-cylinders/dumux/environment.yaml rename to benchmarks/rotating-cylinders/environment.yaml index 43aa3e1..6f26cb9 100644 --- a/benchmarks/rotating-cylinders/dumux/environment.yaml +++ b/benchmarks/rotating-cylinders/environment.yaml @@ -8,3 +8,4 @@ dependencies: - pip - pip: - snakemake + - meshio diff --git a/benchmarks/rotating-cylinders/generate_inputs_and_config.py b/benchmarks/rotating-cylinders/generate_inputs_and_config.py new file mode 100644 index 0000000..5403700 --- /dev/null +++ b/benchmarks/rotating-cylinders/generate_inputs_and_config.py @@ -0,0 +1,65 @@ +import json +from pathlib import Path + +# Paths +TEMPLATE_FILE = "params.template" +JSON_OUTPUT_FILE = "rotating-cylinders_config.json" + +def generate_input(cells0, cells1, problem_name, output_file, output_file_name): + template = Path(TEMPLATE_FILE).read_text() + content = template.format( + cells0_x=cells0, + cells0_y=cells0, + cells1=cells1, + problem_name=problem_name + ) + # Ensure the parent directory exists + Path(output_file).parent.mkdir(parents=True, exist_ok=True) + + # Write file + Path(output_file).write_text(content) + print(f"Written: {output_file_name}") + +if __name__ == "__main__": + base_cells0 = 10 + base_cells1 = 80 + num_files = 3 + name_prefix = "/dumux/shared/dumux/test_rotatingcylinders" + + # Lists to collect JSON entries + configurations = [] + configuration_to_parameter_file = {} + configuration_to_solution_vtu_files = {} + + for i in range(num_files): + cells0 = base_cells0 * (2 ** i) + cells1 = base_cells1 * (2 ** i) + config_name = f"{cells0}_{cells1}" + problem_name = f"{name_prefix}_{cells0}_{cells1}" + input_file_name = f"params_{cells0}_{cells1}.input" + input_file = f"./dumux/input_files/{input_file_name}" + + # Generate input file + generate_input(cells0, cells1, problem_name, input_file, input_file_name) + + # Update JSON lists/dicts + configurations.append(config_name) + configuration_to_parameter_file[config_name] = input_file_name + configuration_to_solution_vtu_files[config_name] = [ + f"dumux/test_rotatingcylinders_{cells0}_{cells1}-00000.vtu", + f"dumux/test_rotatingcylinders_{cells0}_{cells1}-00001.vtu" + ] + + # Read template +template_path = Path("config_template.json") +benchmark_json = json.loads(template_path.read_text()) + +# Update only the dynamic parts +benchmark_json["configurations"] = configurations +benchmark_json["configuration_to_parameter_file"] = configuration_to_parameter_file +benchmark_json["configuration_to_solution_vtu_files"] = configuration_to_solution_vtu_files + +# Write final JSON +with open(JSON_OUTPUT_FILE, "w") as f: + json.dump(benchmark_json, f, indent=4) +print(f"Benchmark JSON written to {JSON_OUTPUT_FILE}") diff --git a/benchmarks/rotating-cylinders/params.template b/benchmarks/rotating-cylinders/params.template new file mode 100644 index 0000000..45b3311 --- /dev/null +++ b/benchmarks/rotating-cylinders/params.template @@ -0,0 +1,55 @@ +[Grid] +Cells0 = {cells0_x} {cells0_y} +Cells1 = {cells1} +Grading0 = 1.1 -1.1 +Grading1 = 1.0 +Radial0 = 1.0 1.5 2.0 +Angular1 = 0.0 360.0 + +[Problem] +Name = {problem_name} +EnableGravity = false +EnableInertiaTerms = true + +[FreeFlow] +EnableUnsymmetrizedVelocityGradient = true + +[Flux] +UpwindWeight = 0.5 + +[Component] +LiquidDensity = 1 +LiquidDynamicViscosity = 1 + +[Mass.Assembly.NumericDifference] +PriVarMagnitude = 1e-2 +BaseEpsilon = 0.01 + +[Momentum.Assembly.NumericDifference] +PriVarMagnitude = 0.2 0.2 +BaseEpsilon = 0.01 + +[LinearSolver] +MaxIterations = 500 +ResidualReduction = 1e-10 +SymmetrizeDirichlet = true +DirectSolverForVelocity = false +GMResRestart = 500 +Type = gmres +Verbosity = 1 + +[LinearSolver.Preconditioner] +Mode = Triangular +Iterations = 5 +AmgSmootherIterations = 2 +AmgDefaultAggregationDimension = 2 +AmgMinAggregateSize = 2 +AmgMaxAggregateSize = 2 +AmgAdditive = false +AmgGamma = 1 +AmgCriterionSymmetric = true + +[Newton] +MinSteps = 1 +EnableAbsoluteResidualCriterion = true +MaxAbsoluteResidual = 4e-6 From ef527603a890af7411af1c8e0af53b77d882e159 Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Fri, 23 Jan 2026 12:04:12 +0100 Subject: [PATCH 4/7] executing dumux simulation with three different input files --- .github/workflows/run-benchmark.yml | 108 ----------------- benchmarks/rotating-cylinders/Snakefile | 111 +++++++++--------- .../rotating-cylinders/config_template.json | 8 -- benchmarks/rotating-cylinders/dumux/Snakefile | 37 +++--- .../dumux/run_dumux_postprocessing.py | 77 ++++++------ .../generate_inputs_and_config.py | 65 ---------- .../rotating-cylinders/generate_rc_config.py | 40 +++++++ 7 files changed, 146 insertions(+), 300 deletions(-) delete mode 100644 .github/workflows/run-benchmark.yml delete mode 100644 benchmarks/rotating-cylinders/config_template.json delete mode 100644 benchmarks/rotating-cylinders/generate_inputs_and_config.py create mode 100644 benchmarks/rotating-cylinders/generate_rc_config.py diff --git a/.github/workflows/run-benchmark.yml b/.github/workflows/run-benchmark.yml deleted file mode 100644 index 04ea390..0000000 --- a/.github/workflows/run-benchmark.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: CI -on: - push: - - pull_request: - branches: [ main ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - - # Runs the workflow once per day at 3:15am - schedule: - - cron: '3 16 * * *' - -env: - CACHE_NUMBER: 1 # increase to reset cache manually - -jobs: - tests: - runs-on: ubuntu-latest - - - - steps: - - name: checkout repo content - uses: actions/checkout@v2 - - - name: Setup Mambaforge - uses: conda-incubator/setup-miniconda@v3 - with: - miniforge-version: latest - activate-environment: model-validation - use-mamba: true - - - name: Set strict channel priority - run: conda config --set channel_priority strict - - - name: Update environment - run: mamba env update -n model-validation -f environment_benchmarks.yml - - - name: generate-config-files - shell: bash -l {0} - run: | - cd $GITHUB_WORKSPACE/benchmarks/linear-elastic-plate-with-hole/ - python generate_config.py - - - name: run_linear-elastic-plate-with-hole-benchmarks_snakemake - shell: bash -l {0} - run: | - cd $GITHUB_WORKSPACE/benchmarks/linear-elastic-plate-with-hole/ - snakemake --use-conda --force --cores 'all' - snakemake --use-conda --force --cores all \ - --reporter metadata4ing \ - --report-metadata4ing-paramscript ../common/parameter_extractor.py \ - --report-metadata4ing-filename snakemake_provenance - unzip snakemake_provenance -d snakemake_provenance - - - name: run_linear-elastic-plate-with-hole-benchmarks_nextflow - shell: bash -l {0} - run: | - cd $GITHUB_WORKSPACE/benchmarks/linear-elastic-plate-with-hole/ - nextflow run main.nf -params-file workflow_config.json -c ../common/nextflow.config -plugins nf-prov@1.4.0 - - - name: Archive Linear Elastic plate with a hole benchmark data for snakemake - uses: actions/upload-artifact@v4 - with: - name: snakemake_results_linear-elastic-plate-with-hole - path: | - benchmarks/linear-elastic-plate-with-hole/snakemake_provenance/ - - - name: Archive Linear Elastic plate with a hole benchmark data for nextflow - uses: actions/upload-artifact@v4 - with: - name: nextflow_results_linear-elastic-plate-with-hole - path: | - benchmarks/linear-elastic-plate-with-hole/nextflow_results/ - - process-artifacts: - runs-on: ubuntu-latest - needs: tests - steps: - - name: Checkout repo content - uses: actions/checkout@v2 - - - name: Download artifact - uses: actions/download-artifact@v4 - with: - name: snakemake_results_linear-elastic-plate-with-hole - path: ./snakemake_provenance - - - name: Setup Mambaforge with postprocessing env - uses: conda-incubator/setup-miniconda@v3 - with: - miniforge-version: latest - activate-environment: postprocessing - use-mamba: true - environment-file: benchmarks/linear-elastic-plate-with-hole/environment_postprocessing.yml - - - name: Run plotting script - shell: bash -l {0} - run: | - python benchmarks/linear-elastic-plate-with-hole/plot_metrics.py ./snakemake_provenance - - - name: Upload PDF plot as artifact - uses: actions/upload-artifact@v4 - with: - name: element-size-vs-stress-plot - path: element_size_vs_stress.pdf \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/Snakefile b/benchmarks/rotating-cylinders/Snakefile index c7f71ed..e88663d 100644 --- a/benchmarks/rotating-cylinders/Snakefile +++ b/benchmarks/rotating-cylinders/Snakefile @@ -1,70 +1,65 @@ # benchmarks/rotating-cylinders/Snakefile - import json import os -# -------------------------- -# Load workflow configuration -# -------------------------- -with open("rotating-cylinders_config.json") as f: - config = json.load(f) - -tools = config["tools"] -benchmark = config["benchmark"] -benchmark_uri = config["benchmark_uri"] -configurations = config["configurations"] -config_to_param = config["configuration_to_parameter_file"] -config_to_vtu_files = config.get("configuration_to_solution_vtu_files", {}) - -# -------------------------- -# Helper functions -# -------------------------- -def parameter_file(cfg): - return config_to_param[cfg] +# Ensure the config exists before loading +if not os.path.exists("rotating-cylinders_config.json"): + # Run the generator script if the file is missing + os.system("python3 generate_rc_config.py") -def vtu_files(cfg): - return config_to_vtu_files.get(cfg, []) +configfile: "rotating-cylinders_config.json" -def summary_file(tool_name): - return f"{tool_name}_summary.json" - -# -------------------------- -# Include tool Snakefiles -# -------------------------- -for tool in tools: - include: f"{tool}/Snakefile" - -# -------------------------- -# Global result directory -# -------------------------- -result_dir = f"snakemake_results/rotating-cylinders" +# Variables for clarity +tools = config["tools"] +CONFIGS = config["configurations"] +RESULT_DIR = f"snakemake_results/{config['benchmark']}" +shared_dir = os.getcwd() +dumux_dir = f"{shared_dir}/dumux" -# -------------------------- -# Rule: Aggregate all outputs -# -------------------------- rule all: input: - expand(f"{result_dir}/dumux/solution_metrics_{{configuration}}.json", configuration=configurations), - expand(f"{result_dir}/dumux/solution_field_data_{{configuration}}.zip", configuration=configurations) + # This will create paths for every tool in every configuration + expand( + f"{RESULT_DIR}/{{tool}}/solution_metrics_{{configuration}}.json", + tool=tools, + configuration=CONFIGS + ) -# -------------------------- -# Rule: Create summary -# -------------------------- -rule create_summary: +# Divide and Conquer: This rule handles the templating for EACH configuration +rule generate_dumux_input: input: - parameter_files=[parameter_file(cfg) for cfg in configurations], - solution_vtu_files=sum([vtu_files(cfg) for cfg in configurations], []) # flatten list of lists + template = "params.template" output: - summary=summary_file("dumux") - params: - configs=configurations - shell: - """ - python create_dumux_summary.py \ - --input_configuration {params.configs} \ - --input_parameter_file {input.parameter_files} \ - --input_solution_vtu {input.solution_vtu_files} \ - --input_benchmark "{benchmark}" \ - --input_benchmark_uri "{benchmark_uri}" \ - --output_summary_json {output.summary} - """ \ No newline at end of file + # f-string uses double braces for Snakemake wildcards to escape Python evaluation + param_file = f"{dumux_dir}/input_files/params_{{configuration}}.input" + run: + # Split "10_80" into c0="10" and c1="80" + c0, c1 = wildcards.configuration.split("_") + + with open(input.template, "r") as f: + content = f.read() + + formatted = content.format( + cells0_x=c0, + cells0_y=c0, + cells1=c1, + problem_name=f"/dumux/shared/dumux/test_rotatingcylinders_{c0}_{c1}" + ) + + os.makedirs(os.path.dirname(output.param_file), exist_ok=True) + with open(output.param_file, "w") as f: + f.write(formatted) + +# Include tool specific rules +for tool in tools: + include: f"{tool}/Snakefile" + +# # Global Summary Rule +# rule summary: +# input: +# # Cross-reference the expand to ensure all metrics exist +# metrics = expand(f"{RESULT_DIR}/dumux/solution_metrics_{{configuration}}.json", configuration=CONFIGS) +# output: +# json = f"{RESULT_DIR}/dumux/summary.json" +# shell: +# "python3 summarize.py --input {input.metrics} --output {output.json}" diff --git a/benchmarks/rotating-cylinders/config_template.json b/benchmarks/rotating-cylinders/config_template.json deleted file mode 100644 index 05171a7..0000000 --- a/benchmarks/rotating-cylinders/config_template.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "benchmark": "rotating-cylinders", - "benchmark_uri": "https://www.openfoam.com/documentation/guides/latest/doc/verification-validation-rotating-cylinders-2d.html", - "tools": ["dumux"], - "configurations": [], - "configuration_to_parameter_file": {}, - "configuration_to_solution_vtu_files": {} -} diff --git a/benchmarks/rotating-cylinders/dumux/Snakefile b/benchmarks/rotating-cylinders/dumux/Snakefile index 4ab8b12..933b1dd 100644 --- a/benchmarks/rotating-cylinders/dumux/Snakefile +++ b/benchmarks/rotating-cylinders/dumux/Snakefile @@ -2,25 +2,24 @@ import os # 1. Setup and Config tool = "dumux" -result_dir = f"snakemake_results/rotating-cylinders/{tool}" -configurations = config["configurations"] -configuration_to_parameter_file = config["configuration_to_parameter_file"] -vtu_mapping = config["configuration_to_solution_vtu_files"] -container_image = "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0" +# Use .get() to prevent crashes if config isn't fully loaded yet +benchmark_name = config.get("benchmark", "rotating-cylinders") +result_dir = f"snakemake_results/{benchmark_name}/{tool}" + +container_image = config.get("container_image", "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0") shared_dir = os.getcwd() -# dumux_input = f"{shared_dir}/{tool}/input_files" -dumux_dir = f"{shared_dir}/{tool}" +dumux_dir = f"{shared_dir}/{tool}" -# Rule 1: Only runs the simulation, outputs the VTU files +# Rule 1: Simulation rule run_dumux_simulation: input: - params=lambda wildcards: f"{dumux_dir}/input_files/{configuration_to_parameter_file[wildcards.configuration]}" + # Lambda prevents KeyError before the JSON is generated + params = lambda wildcards: f"{dumux_dir}/input_files/{config['configuration_to_parameter_file'][wildcards.configuration]}" output: - # Use a list of the actual files you expect vtu_files = [ - f"{tool}/test_rotatingcylinders_{{configuration}}-00000.vtu", - f"{tool}/test_rotatingcylinders_{{configuration}}-00001.vtu" + f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00000.vtu", + f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00001.vtu" ] resources: serial_run=1 @@ -30,20 +29,20 @@ rule run_dumux_simulation: """ set -euo pipefail cd /dumux/rotating-cylinders/build-cmake/test/freeflow/navierstokes/rotatingcylinders - ./test_ff_navierstokes_rotatingcylinders {input.params} -IO.OutputPath {dumux_dir} + ./test_ff_navierstokes_rotatingcylinders {input.params} """ -# Rule 2: Only runs post-processing, depends on the VTU files +# Rule 2: Post-processing rule postprocess_dumux: input: sim_data = [ - f"{tool}/test_rotatingcylinders_{{configuration}}-00000.vtu", - f"{tool}/test_rotatingcylinders_{{configuration}}-00001.vtu" + f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00000.vtu", + f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00001.vtu" ], - postprocess_script=f"{tool}/run_dumux_postprocessing.py" + postprocess_script = f"{dumux_dir}/run_dumux_postprocessing.py" output: - metrics=f"{result_dir}/solution_metrics_{{configuration}}.json", - fields=f"{result_dir}/solution_field_data_{{configuration}}.zip" + metrics = f"{result_dir}/solution_metrics_{{configuration}}.json", + fields = f"{result_dir}/solution_field_data_{{configuration}}.zip" shell: """ python3 {input.postprocess_script} \ diff --git a/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py b/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py index 1dbe5bc..e3c06ff 100644 --- a/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py +++ b/benchmarks/rotating-cylinders/dumux/run_dumux_postprocessing.py @@ -2,11 +2,9 @@ import zipfile from argparse import ArgumentParser from pathlib import Path - import meshio import numpy as np - def run_dumux_postprocessing( dumux_output_dir: str, configuration: str, @@ -15,22 +13,28 @@ def run_dumux_postprocessing( ) -> None: dumux_output_dir = Path(dumux_output_dir) - # ---- read all VTU / PVTU files ---- - vtk_files = list(dumux_output_dir.glob("*.vtu")) + list( - dumux_output_dir.glob("*.pvtu") - ) + # ---- Targeted Discovery ---- + # Only find files that belong to THIS configuration to avoid race conditions + # DuMuX files usually look like: test_rotatingcylinders_10_80-00001.vtu + vtk_files = list(dumux_output_dir.glob(f"*{configuration}*.vtu")) + \ + list(dumux_output_dir.glob(f"*{configuration}*.pvtu")) + + pvd_files = list(dumux_output_dir.glob(f"*{configuration}*.pvd")) + if not vtk_files: - raise RuntimeError("No VTU/PVTU files found in DuMuX output directory") + raise RuntimeError(f"No VTU/PVTU files found for configuration: {configuration}") - # Read first file for metrics (extend if needed) - mesh = meshio.read(vtk_files[0]) + # Sort files to ensure we process the last timestep (usually the highest index) + vtk_files.sort() + + # Read the last VTU file for metrics (represents the final state) + mesh = meshio.read(vtk_files[-1]) metrics = {} - # ---- example metrics (adapt to benchmark definition) ---- + # ---- Example metrics (keep your original logic) ---- if "p" in mesh.cell_data_dict: p = mesh.cell_data_dict["p"] - # flatten possible block structure p_vals = np.concatenate([np.asarray(v).ravel() for v in p.values()]) metrics["min_pressure"] = float(np.min(p_vals)) metrics["max_pressure"] = float(np.max(p_vals)) @@ -43,45 +47,34 @@ def run_dumux_postprocessing( metrics["max_velocity_magnitude"] = float(np.max(speed)) metrics["mean_velocity_magnitude"] = float(np.mean(speed)) - # ---- write metrics JSON ---- - with open(metrics_file, "w") as f: + # ---- Write metrics JSON ---- + metrics_path = Path(metrics_file) + metrics_path.parent.mkdir(parents=True, exist_ok=True) + with open(metrics_path, "w") as f: json.dump(metrics, f, indent=4) - # ---- zip solution field data ---- + # ---- Zip solution field data ---- + # We include VTUs and the PVD specifically for this configuration with zipfile.ZipFile(solution_file_zip, "w", zipfile.ZIP_DEFLATED) as zipf: - for file in vtk_files: + for file in vtk_files + pvd_files: zipf.write(file, arcname=file.name) - # ---- cleanup: delete original VTU/PVTU files ---- - for ext in ["*.vtu", "*.pvtu", "*.pvd"]: - for file in dumux_output_dir.glob(ext): + # ---- Targeted Cleanup ---- + # Only delete files we just zipped. This allows other concurrent + # configurations to finish safely. + for file in vtk_files + pvd_files: + try: file.unlink() + except FileNotFoundError: + pass # Already removed or handled if __name__ == "__main__": - parser = ArgumentParser( - description="Post-process DuMuX results into benchmark artifacts" - ) - parser.add_argument( - "--input_dumux_output_dir", - required=True, - help="Directory containing DuMuX VTU/PVTU files", - ) - parser.add_argument( - "--input_configuration", - required=True, - help="Configuration name", - ) - parser.add_argument( - "--output_metrics_file", - required=True, - help="Path to solution_metrics_{configuration}.json", - ) - parser.add_argument( - "--output_solution_file_zip", - required=True, - help="Path to solution_field_data_{configuration}.zip", - ) + parser = ArgumentParser(description="Post-process DuMuX results") + parser.add_argument("--input_dumux_output_dir", required=True) + parser.add_argument("--input_configuration", required=True) + parser.add_argument("--output_metrics_file", required=True) + parser.add_argument("--output_solution_file_zip", required=True) args = parser.parse_args() @@ -90,4 +83,4 @@ def run_dumux_postprocessing( args.input_configuration, args.output_metrics_file, args.output_solution_file_zip, - ) + ) \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/generate_inputs_and_config.py b/benchmarks/rotating-cylinders/generate_inputs_and_config.py deleted file mode 100644 index 5403700..0000000 --- a/benchmarks/rotating-cylinders/generate_inputs_and_config.py +++ /dev/null @@ -1,65 +0,0 @@ -import json -from pathlib import Path - -# Paths -TEMPLATE_FILE = "params.template" -JSON_OUTPUT_FILE = "rotating-cylinders_config.json" - -def generate_input(cells0, cells1, problem_name, output_file, output_file_name): - template = Path(TEMPLATE_FILE).read_text() - content = template.format( - cells0_x=cells0, - cells0_y=cells0, - cells1=cells1, - problem_name=problem_name - ) - # Ensure the parent directory exists - Path(output_file).parent.mkdir(parents=True, exist_ok=True) - - # Write file - Path(output_file).write_text(content) - print(f"Written: {output_file_name}") - -if __name__ == "__main__": - base_cells0 = 10 - base_cells1 = 80 - num_files = 3 - name_prefix = "/dumux/shared/dumux/test_rotatingcylinders" - - # Lists to collect JSON entries - configurations = [] - configuration_to_parameter_file = {} - configuration_to_solution_vtu_files = {} - - for i in range(num_files): - cells0 = base_cells0 * (2 ** i) - cells1 = base_cells1 * (2 ** i) - config_name = f"{cells0}_{cells1}" - problem_name = f"{name_prefix}_{cells0}_{cells1}" - input_file_name = f"params_{cells0}_{cells1}.input" - input_file = f"./dumux/input_files/{input_file_name}" - - # Generate input file - generate_input(cells0, cells1, problem_name, input_file, input_file_name) - - # Update JSON lists/dicts - configurations.append(config_name) - configuration_to_parameter_file[config_name] = input_file_name - configuration_to_solution_vtu_files[config_name] = [ - f"dumux/test_rotatingcylinders_{cells0}_{cells1}-00000.vtu", - f"dumux/test_rotatingcylinders_{cells0}_{cells1}-00001.vtu" - ] - - # Read template -template_path = Path("config_template.json") -benchmark_json = json.loads(template_path.read_text()) - -# Update only the dynamic parts -benchmark_json["configurations"] = configurations -benchmark_json["configuration_to_parameter_file"] = configuration_to_parameter_file -benchmark_json["configuration_to_solution_vtu_files"] = configuration_to_solution_vtu_files - -# Write final JSON -with open(JSON_OUTPUT_FILE, "w") as f: - json.dump(benchmark_json, f, indent=4) -print(f"Benchmark JSON written to {JSON_OUTPUT_FILE}") diff --git a/benchmarks/rotating-cylinders/generate_rc_config.py b/benchmarks/rotating-cylinders/generate_rc_config.py new file mode 100644 index 0000000..9f922ed --- /dev/null +++ b/benchmarks/rotating-cylinders/generate_rc_config.py @@ -0,0 +1,40 @@ +import json +from pathlib import Path + +def write_benchmark_config(): + base_cells0, base_cells1 = 10, 80 + num_files = 3 + + configurations = [] + config_to_param = {} + config_to_vtu = {} + + for i in range(num_files): + cells0 = base_cells0 * (2 ** i) + cells1 = base_cells1 * (2 ** i) + config_name = f"{cells0}_{cells1}" + + configurations.append(config_name) + config_to_param[config_name] = f"params_{config_name}.input" + config_to_vtu[config_name] = [ + f"dumux/test_rotatingcylinders_{config_name}-00000.vtu", + f"dumux/test_rotatingcylinders_{config_name}-00001.vtu" + ] + + # Your hardcoded dictionary + benchmark_json = { + "benchmark": "rotating-cylinders", + "benchmark_uri": "https://www.openfoam.com/documentation/guides/latest/doc/verification-validation-rotating-cylinders-2d.html", + "tools": ["dumux"], + "configurations": configurations, + "configuration_to_parameter_file": config_to_param, + "configuration_to_solution_vtu_files": config_to_vtu, + "container_image": "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0" + } + + with open("rotating-cylinders_config.json", "w") as f: + json.dump(benchmark_json, f, indent=4) + print("rotating-cylinders_config.json generated.") + +if __name__ == "__main__": + write_benchmark_config() \ No newline at end of file From ad3298d21ccc75a9d75c2279e422055d1ee44975 Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Tue, 27 Jan 2026 14:08:55 +0100 Subject: [PATCH 5/7] dumux benchmark implementation working --- .github/workflows/run-dumux-benchmark.yml | 55 ++++++++ benchmarks/rotating-cylinders/Snakefile | 84 ++++++------ .../create_dumux_summary.py | 53 ------- benchmarks/rotating-cylinders/dumux/Snakefile | 8 +- .../dumux/grid_files/grid_template.json | 13 ++ .../dumux/input_files/dumux_config.json | 50 +++++++ .../rotating-cylinders/dumux_input_gen.py | 129 ++++++++++++++++++ ...nvironment.yaml => environment_dumux.yaml} | 9 ++ benchmarks/rotating-cylinders/params.template | 55 -------- 9 files changed, 299 insertions(+), 157 deletions(-) create mode 100644 .github/workflows/run-dumux-benchmark.yml delete mode 100644 benchmarks/rotating-cylinders/create_dumux_summary.py create mode 100644 benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json create mode 100644 benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json create mode 100644 benchmarks/rotating-cylinders/dumux_input_gen.py rename benchmarks/rotating-cylinders/{environment.yaml => environment_dumux.yaml} (54%) delete mode 100644 benchmarks/rotating-cylinders/params.template diff --git a/.github/workflows/run-dumux-benchmark.yml b/.github/workflows/run-dumux-benchmark.yml new file mode 100644 index 0000000..0b8a6d9 --- /dev/null +++ b/.github/workflows/run-dumux-benchmark.yml @@ -0,0 +1,55 @@ +name: DuMux-CI +on: + push: + + pull_request: + branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + + # Runs the workflow once per day at 3:15am + schedule: + - cron: '3 16 * * *' + +env: + CACHE_NUMBER: 1 # increase to reset cache manually + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - name: checkout repo content + uses: actions/checkout@v2 + + - name: Setup Mambaforge + uses: conda-incubator/setup-miniconda@v3 + with: + miniforge-version: latest + activate-environment: rc-dumux + use-mamba: true + + - name: Set strict channel priority + run: conda config --set channel_priority strict + + - name: Setup Apptainer + uses: eWaterCycle/setup-apptainer@v2 + with: + apptainer-version: 1.4.5 + + - name: Update environment + run: mamba env update -n rc-dumux -f $GITHUB_WORKSPACE/benchmarks/rotating-cylinders/environment_dumux.yaml + + - name: generate-rc-config + shell: bash -l {0} + run: | + cd $GITHUB_WORKSPACE/benchmarks/rotating-cylinders + python3 generate_rc_config.py + + - name: run-rotating-cylinders-snakemake + shell: bash -l {0} + run: | + cd $GITHUB_WORKSPACE/benchmarks/rotating-cylinders + # Note: --use-singularity is aliased to --use-apptainer in modern Snakemake + snakemake --use-apptainer --cores all --resources serial_run=1 --apptainer-args "--bind $(pwd):/dumux/shared" \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/Snakefile b/benchmarks/rotating-cylinders/Snakefile index e88663d..7d79551 100644 --- a/benchmarks/rotating-cylinders/Snakefile +++ b/benchmarks/rotating-cylinders/Snakefile @@ -2,64 +2,62 @@ import json import os -# Ensure the config exists before loading if not os.path.exists("rotating-cylinders_config.json"): - # Run the generator script if the file is missing os.system("python3 generate_rc_config.py") configfile: "rotating-cylinders_config.json" -# Variables for clarity +# Variables tools = config["tools"] -CONFIGS = config["configurations"] -RESULT_DIR = f"snakemake_results/{config['benchmark']}" +configs = config["configurations"] +benchmark = config["benchmark"] +benchmark_uri = config.get("benchmark_uri", "https://nfdi-benchmark.org/rc") +result_dir = f"snakemake_results/{benchmark}" shared_dir = os.getcwd() dumux_dir = f"{shared_dir}/dumux" rule all: input: - # This will create paths for every tool in every configuration - expand( - f"{RESULT_DIR}/{{tool}}/solution_metrics_{{configuration}}.json", - tool=tools, - configuration=CONFIGS - ) + expand(f"{result_dir}/{{tool}}/summary.json", tool=tools) -# Divide and Conquer: This rule handles the templating for EACH configuration -rule generate_dumux_input: +rule generate_dumux_inputs: input: - template = "params.template" + grid_t = f"{dumux_dir}/grid_files/grid_template.json", + dumux_t = f"{dumux_dir}/input_files/dumux_config.json" output: - # f-string uses double braces for Snakemake wildcards to escape Python evaluation - param_file = f"{dumux_dir}/input_files/params_{{configuration}}.input" - run: - # Split "10_80" into c0="10" and c1="80" - c0, c1 = wildcards.configuration.split("_") - - with open(input.template, "r") as f: - content = f.read() - - formatted = content.format( - cells0_x=c0, - cells0_y=c0, - cells1=c1, - problem_name=f"/dumux/shared/dumux/test_rotatingcylinders_{c0}_{c1}" - ) - - os.makedirs(os.path.dirname(output.param_file), exist_ok=True) - with open(output.param_file, "w") as f: - f.write(formatted) + params = expand(f"{dumux_dir}/input_files/params_{{conf}}.input", conf=configs), + grids = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs) + shell: + "python3 dumux_input_gen.py --grid_template {input.grid_t} --dumux_template {input.dumux_t}" -# Include tool specific rules for tool in tools: include: f"{tool}/Snakefile" -# # Global Summary Rule -# rule summary: -# input: -# # Cross-reference the expand to ensure all metrics exist -# metrics = expand(f"{RESULT_DIR}/dumux/solution_metrics_{{configuration}}.json", configuration=CONFIGS) -# output: -# json = f"{RESULT_DIR}/dumux/summary.json" -# shell: -# "python3 summarize.py --input {input.metrics} --output {output.json}" +rule summary: + input: + script = "../common/summarize_results.py", + # We use the grids generated by the generator rule + parameters = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs), + mesh = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs), + metrics = lambda wildcards: expand( + f"{result_dir}/{{tool}}/solution_metrics_{{conf}}.json", + tool=[wildcards.tool], conf=configs + ), + solution_field_data = lambda wildcards: expand( + f"{result_dir}/{{tool}}/solution_field_data_{{conf}}.zip", + tool=[wildcards.tool], conf=configs + ) + output: + summary_json = f"{result_dir}/{{tool}}/summary.json" + shell: + """ + python3 {input.script} \ + --input_configuration {configs} \ + --input_parameter_file {input.parameters} \ + --input_mesh_file {input.mesh} \ + --input_solution_metrics {input.metrics} \ + --input_solution_field_data {input.solution_field_data} \ + --input_benchmark {benchmark} \ + --input_benchmark_uri {benchmark_uri} \ + --output_summary_json {output.summary_json} + """ \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/create_dumux_summary.py b/benchmarks/rotating-cylinders/create_dumux_summary.py deleted file mode 100644 index 03f075a..0000000 --- a/benchmarks/rotating-cylinders/create_dumux_summary.py +++ /dev/null @@ -1,53 +0,0 @@ -import json -import argparse -import meshio - -def create_dumux_summary(configurations, parameter_files, solution_vtu_files, benchmark, benchmark_uri, summary_json): - summaries = [] - for cfg, param_file, vtu_file in zip(configurations, parameter_files, solution_vtu_files): - summary = { - "benchmark": benchmark, - "benchmark_uri": benchmark_uri, - "configuration": cfg - } - # Load parameters - with open(param_file) as f: - summary["parameters"] = json.load(f) - - summary["solution_vtu"] = vtu_file - - # Extract field summaries - vtu = meshio.read(vtu_file) - field_summaries = {} - for key, data in vtu.point_data.items(): - field_summaries[key] = { - "min": float(data.min()), - "max": float(data.max()), - "mean": float(data.mean()) - } - summary["solution_fields_summary"] = field_summaries - - summaries.append(summary) - - with open(summary_json, "w") as f: - json.dump(summaries, f, indent=4) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--input_configuration", nargs="+", required=True) - parser.add_argument("--input_parameter_file", nargs="+", required=True) - parser.add_argument("--input_solution_vtu", nargs="+", required=True) - parser.add_argument("--input_benchmark", required=True) - parser.add_argument("--input_benchmark_uri", required=True) - parser.add_argument("--output_summary_json", required=True) - args = parser.parse_args() - - create_dumux_summary( - args.input_configuration, - args.input_parameter_file, - args.input_solution_vtu, - args.input_benchmark, - args.input_benchmark_uri, - args.output_summary_json - ) diff --git a/benchmarks/rotating-cylinders/dumux/Snakefile b/benchmarks/rotating-cylinders/dumux/Snakefile index 933b1dd..d9045e9 100644 --- a/benchmarks/rotating-cylinders/dumux/Snakefile +++ b/benchmarks/rotating-cylinders/dumux/Snakefile @@ -5,11 +5,7 @@ tool = "dumux" # Use .get() to prevent crashes if config isn't fully loaded yet benchmark_name = config.get("benchmark", "rotating-cylinders") -result_dir = f"snakemake_results/{benchmark_name}/{tool}" - container_image = config.get("container_image", "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0") -shared_dir = os.getcwd() -dumux_dir = f"{shared_dir}/{tool}" # Rule 1: Simulation rule run_dumux_simulation: @@ -41,8 +37,8 @@ rule postprocess_dumux: ], postprocess_script = f"{dumux_dir}/run_dumux_postprocessing.py" output: - metrics = f"{result_dir}/solution_metrics_{{configuration}}.json", - fields = f"{result_dir}/solution_field_data_{{configuration}}.zip" + metrics = f"{result_dir}/{tool}/solution_metrics_{{configuration}}.json", + fields = f"{result_dir}/{tool}/solution_field_data_{{configuration}}.zip" shell: """ python3 {input.postprocess_script} \ diff --git a/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json b/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json new file mode 100644 index 0000000..d99e09c --- /dev/null +++ b/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json @@ -0,0 +1,13 @@ +{ + "Grid": { + "Cells0": { + "x": "{cells0_x}", + "y": "{cells0_y}" + }, + "Cells1": "{cells1}", + "Grading0": [1.1, -1.1], + "Grading1": 1.0, + "Radial0": [1.0, 1.5, 2.0], + "Angular1": [0.0, 360.0] + } +} \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json b/benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json new file mode 100644 index 0000000..3035543 --- /dev/null +++ b/benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json @@ -0,0 +1,50 @@ +{ + "Problem": { + "Name": "{problem_name}", + "EnableGravity": false, + "EnableInertiaTerms": true + }, + "FreeFlow": { + "EnableUnsymmetrizedVelocityGradient": true + }, + "Flux": { + "UpwindWeight": 0.5 + }, + "Component": { + "LiquidDensity": 1, + "LiquidDynamicViscosity": 1 + }, + "Mass_Assembly_NumericDifference": { + "PriVarMagnitude": 0.01, + "BaseEpsilon": 0.01 + }, + "Momentum_Assembly_NumericDifference": { + "PriVarMagnitude": [0.2, 0.2], + "BaseEpsilon": 0.01 + }, + "LinearSolver": { + "MaxIterations": 500, + "ResidualReduction": 1e-10, + "SymmetrizeDirichlet": true, + "DirectSolverForVelocity": false, + "GMResRestart": 500, + "Type": "gmres", + "Verbosity": 1, + "Preconditioner": { + "Mode": "Triangular", + "Iterations": 5, + "AmgSmootherIterations": 2, + "AmgDefaultAggregationDimension": 2, + "AmgMinAggregateSize": 2, + "AmgMaxAggregateSize": 2, + "AmgAdditive": false, + "AmgGamma": 1, + "AmgCriterionSymmetric": true + } + }, + "Newton": { + "MinSteps": 1, + "EnableAbsoluteResidualCriterion": true, + "MaxAbsoluteResidual": 4e-05 + } +} \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/dumux_input_gen.py b/benchmarks/rotating-cylinders/dumux_input_gen.py new file mode 100644 index 0000000..94389aa --- /dev/null +++ b/benchmarks/rotating-cylinders/dumux_input_gen.py @@ -0,0 +1,129 @@ +import json +import argparse +from pathlib import Path + +def generate_grid_files(grid_template_path, grid_dir, base_cells0, base_cells1, num_files): + """ + Generates standalone grid JSON files and returns data for merging. + """ + # Ensure the output directory exists + grid_dir.mkdir(parents=True, exist_ok=True) + + if not grid_template_path.exists(): + print(f"Error: {grid_template_path} not found.") + return [] + + with open(grid_template_path, "r") as f: + grid_template = json.load(f) + + generated_configs = [] + + for i in range(num_files): + scale = 2 ** i + c0 = base_cells0 * scale + c1 = base_cells1 * scale + config_id = f"{c0}_{c1}" + + current_grid = json.loads(json.dumps(grid_template)) + current_grid["Grid"]["Cells0"] = f"{c0} {c0}" + current_grid["Grid"]["Cells1"] = c1 + + # Write individual grid JSON files to grid_dir + grid_file_path = grid_dir / f"grid_{config_id}.json" + with open(grid_file_path, "w") as f: + json.dump(current_grid, f, indent=4) + + generated_configs.append((config_id, current_grid)) + print(f"Generated Grid JSON: {grid_file_path}") + + return generated_configs + +def dict_to_dumux_format(data): + """ + Converts a nested dictionary into DuMuX .input format. + Handles underscore-to-dot conversion for sections and recursive nesting. + """ + lines = [] + + def process_section(section_name, content): + # Convert underscores to dots for DuMuX section naming convention + formatted_section = section_name.replace("_", ".") + lines.append(f"[{formatted_section}]") + + sub_sections = {} + + for key, value in content.items(): + if isinstance(value, dict): + # Store nested dicts to process as [Section.SubSection] later + sub_sections[f"{formatted_section}.{key}"] = value + else: + # Format values (handle lists as space-separated, bools as lowercase) + if isinstance(value, list): + val_str = " ".join(map(str, value)) + elif isinstance(value, bool): + val_str = str(value).lower() + else: + val_str = str(value) + lines.append(f"{key} = {val_str}") + + lines.append("") # Newline after section + + # Recursively process nested dictionaries + for sub_name, sub_content in sub_sections.items(): + process_section(sub_name, sub_content) + + for section, params in data.items(): + process_section(section, params) + + return "\n".join(lines) + +def write_dumux_inputs(grid_template, dumux_template, grid_out, input_out): + # Configuration constants + problem_name_base = "/dumux/shared/dumux/test_rotatingcylinders" + base_cells0, base_cells1 = 10, 80 + num_files = 3 + + # 1. Load the dumux Template + if not dumux_template.exists(): + print(f"Error: dumux template not found at {dumux_template}") + return + + with open(dumux_template, "r") as f: + dumux_config = json.load(f) + + # 2. Get Grid Data and generate grid files + grid_configs = generate_grid_files(grid_template, grid_out, base_cells0, base_cells1, num_files) + + # 3. Merge and Generate .input files + input_out.mkdir(parents=True, exist_ok=True) + for config_id, grid_data in grid_configs: + full_config = json.loads(json.dumps(dumux_config)) + full_config["Problem"]["Name"] = f"{problem_name_base}_{config_id}" + full_config.update(grid_data) + + dumux_content = dict_to_dumux_format(full_config) + input_filename = input_out / f"params_{config_id}.input" + + with open(input_filename, "w") as f: + f.write(dumux_content) + + print(f"Generated DuMuX Input: {input_filename}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate DuMuX input files from JSON templates.") + + # Define arguments + parser.add_argument("--grid_template", type=str, required=True, help="Path to grid_template.json") + parser.add_argument("--dumux_template", type=str, required=True, help="Path to dumux_config.json") + parser.add_argument("--grid_dir", type=str, default="./dumux/grid_files", help="Output directory for grid JSONs") + parser.add_argument("--input_dir", type=str, default="./dumux/input_files", help="Output directory for .input files") + + args = parser.parse_args() + + # Execute with absolute paths + write_dumux_inputs( + grid_template=Path(args.grid_template).resolve(), + dumux_template=Path(args.dumux_template).resolve(), + grid_out=Path(args.grid_dir).resolve(), + input_out=Path(args.input_dir).resolve() + ) \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/environment.yaml b/benchmarks/rotating-cylinders/environment_dumux.yaml similarity index 54% rename from benchmarks/rotating-cylinders/environment.yaml rename to benchmarks/rotating-cylinders/environment_dumux.yaml index 6f26cb9..6f09c4f 100644 --- a/benchmarks/rotating-cylinders/environment.yaml +++ b/benchmarks/rotating-cylinders/environment_dumux.yaml @@ -1,11 +1,20 @@ name: rc-dumux channels: - conda-forge + - defaults dependencies: - python=3.11 - bash - coreutils + - pint + - pyvista + - rdflib + - matplotlib + - pandas + - numpy - pip - pip: - snakemake - meshio + - roc-validator + - rohub diff --git a/benchmarks/rotating-cylinders/params.template b/benchmarks/rotating-cylinders/params.template deleted file mode 100644 index 45b3311..0000000 --- a/benchmarks/rotating-cylinders/params.template +++ /dev/null @@ -1,55 +0,0 @@ -[Grid] -Cells0 = {cells0_x} {cells0_y} -Cells1 = {cells1} -Grading0 = 1.1 -1.1 -Grading1 = 1.0 -Radial0 = 1.0 1.5 2.0 -Angular1 = 0.0 360.0 - -[Problem] -Name = {problem_name} -EnableGravity = false -EnableInertiaTerms = true - -[FreeFlow] -EnableUnsymmetrizedVelocityGradient = true - -[Flux] -UpwindWeight = 0.5 - -[Component] -LiquidDensity = 1 -LiquidDynamicViscosity = 1 - -[Mass.Assembly.NumericDifference] -PriVarMagnitude = 1e-2 -BaseEpsilon = 0.01 - -[Momentum.Assembly.NumericDifference] -PriVarMagnitude = 0.2 0.2 -BaseEpsilon = 0.01 - -[LinearSolver] -MaxIterations = 500 -ResidualReduction = 1e-10 -SymmetrizeDirichlet = true -DirectSolverForVelocity = false -GMResRestart = 500 -Type = gmres -Verbosity = 1 - -[LinearSolver.Preconditioner] -Mode = Triangular -Iterations = 5 -AmgSmootherIterations = 2 -AmgDefaultAggregationDimension = 2 -AmgMinAggregateSize = 2 -AmgMaxAggregateSize = 2 -AmgAdditive = false -AmgGamma = 1 -AmgCriterionSymmetric = true - -[Newton] -MinSteps = 1 -EnableAbsoluteResidualCriterion = true -MaxAbsoluteResidual = 4e-6 From 443a91af141161282f56b0ee312125136d49eda7 Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Wed, 18 Mar 2026 21:59:34 +0100 Subject: [PATCH 6/7] Moved DuMuX-specific rules into a dedicated Snakefile and use environment_benchmarks in the DuMuX workflow file. --- .github/workflows/run-dumux-benchmark.yml | 4 +- benchmarks/rotating-cylinders/Snakefile | 44 +++++++++---------- benchmarks/rotating-cylinders/dumux/Snakefile | 23 +++++++--- .../{ => dumux}/dumux_input_gen.py | 0 .../rotating-cylinders/environment_dumux.yaml | 20 --------- .../rotating-cylinders/generate_rc_config.py | 3 +- 6 files changed, 42 insertions(+), 52 deletions(-) rename benchmarks/rotating-cylinders/{ => dumux}/dumux_input_gen.py (100%) delete mode 100644 benchmarks/rotating-cylinders/environment_dumux.yaml diff --git a/.github/workflows/run-dumux-benchmark.yml b/.github/workflows/run-dumux-benchmark.yml index 0b8a6d9..3c00e6f 100644 --- a/.github/workflows/run-dumux-benchmark.yml +++ b/.github/workflows/run-dumux-benchmark.yml @@ -27,7 +27,7 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: miniforge-version: latest - activate-environment: rc-dumux + activate-environment: model-validation use-mamba: true - name: Set strict channel priority @@ -39,7 +39,7 @@ jobs: apptainer-version: 1.4.5 - name: Update environment - run: mamba env update -n rc-dumux -f $GITHUB_WORKSPACE/benchmarks/rotating-cylinders/environment_dumux.yaml + run: mamba env update -n model-validation -f environment_benchmarks.yml - name: generate-rc-config shell: bash -l {0} diff --git a/benchmarks/rotating-cylinders/Snakefile b/benchmarks/rotating-cylinders/Snakefile index 7d79551..63d7e95 100644 --- a/benchmarks/rotating-cylinders/Snakefile +++ b/benchmarks/rotating-cylinders/Snakefile @@ -11,44 +11,44 @@ configfile: "rotating-cylinders_config.json" tools = config["tools"] configs = config["configurations"] benchmark = config["benchmark"] -benchmark_uri = config.get("benchmark_uri", "https://nfdi-benchmark.org/rc") +benchmark_uri = config["benchmark_uri"] result_dir = f"snakemake_results/{benchmark}" shared_dir = os.getcwd() -dumux_dir = f"{shared_dir}/dumux" rule all: input: expand(f"{result_dir}/{{tool}}/summary.json", tool=tools) -rule generate_dumux_inputs: - input: - grid_t = f"{dumux_dir}/grid_files/grid_template.json", - dumux_t = f"{dumux_dir}/input_files/dumux_config.json" - output: - params = expand(f"{dumux_dir}/input_files/params_{{conf}}.input", conf=configs), - grids = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs) - shell: - "python3 dumux_input_gen.py --grid_template {input.grid_t} --dumux_template {input.dumux_t}" - for tool in tools: include: f"{tool}/Snakefile" rule summary: input: script = "../common/summarize_results.py", - # We use the grids generated by the generator rule - parameters = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs), - mesh = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs), - metrics = lambda wildcards: expand( - f"{result_dir}/{{tool}}/solution_metrics_{{conf}}.json", - tool=[wildcards.tool], conf=configs + + parameters=lambda wc: expand( + f"{shared_dir}/{wc.tool}/grid_files/grid_{{conf}}.json", + conf=configs + ), + + mesh=lambda wc: expand( + f"{shared_dir}/{wc.tool}/grid_files/grid_{{conf}}.json", + conf=configs + ), + + metrics=lambda wc: expand( + f"{result_dir}/{wc.tool}/solution_metrics_{{conf}}.json", + conf=configs ), - solution_field_data = lambda wildcards: expand( - f"{result_dir}/{{tool}}/solution_field_data_{{conf}}.zip", - tool=[wildcards.tool], conf=configs + + solution_field_data=lambda wc: expand( + f"{result_dir}/{wc.tool}/solution_field_data_{{conf}}.zip", + conf=configs ) + output: - summary_json = f"{result_dir}/{{tool}}/summary.json" + summary_json=f"{result_dir}/{{tool}}/summary.json" + shell: """ python3 {input.script} \ diff --git a/benchmarks/rotating-cylinders/dumux/Snakefile b/benchmarks/rotating-cylinders/dumux/Snakefile index d9045e9..f3e67d5 100644 --- a/benchmarks/rotating-cylinders/dumux/Snakefile +++ b/benchmarks/rotating-cylinders/dumux/Snakefile @@ -1,13 +1,24 @@ +# benchmarks/rotating-cylinders/dumux/Snakefile import os -# 1. Setup and Config +# Setup and Config tool = "dumux" +container_image = config["container_image"] +dumux_dir = f"{shared_dir}/dumux" -# Use .get() to prevent crashes if config isn't fully loaded yet -benchmark_name = config.get("benchmark", "rotating-cylinders") -container_image = config.get("container_image", "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0") +# Rule 1: Input generation +rule generate_dumux_inputs: + input: + grid_t = f"{dumux_dir}/grid_files/grid_template.json", + dumux_t = f"{dumux_dir}/input_files/dumux_config.json", + input_gen_script = f"{dumux_dir}/dumux_input_gen.py" + output: + params = expand(f"{dumux_dir}/input_files/params_{{conf}}.input", conf=configs), + grids = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs) + shell: + "python3 {input.input_gen_script} --grid_template {input.grid_t} --dumux_template {input.dumux_t}" -# Rule 1: Simulation +# Rule 2: Simulation rule run_dumux_simulation: input: # Lambda prevents KeyError before the JSON is generated @@ -28,7 +39,7 @@ rule run_dumux_simulation: ./test_ff_navierstokes_rotatingcylinders {input.params} """ -# Rule 2: Post-processing +# Rule 3: Post-processing rule postprocess_dumux: input: sim_data = [ diff --git a/benchmarks/rotating-cylinders/dumux_input_gen.py b/benchmarks/rotating-cylinders/dumux/dumux_input_gen.py similarity index 100% rename from benchmarks/rotating-cylinders/dumux_input_gen.py rename to benchmarks/rotating-cylinders/dumux/dumux_input_gen.py diff --git a/benchmarks/rotating-cylinders/environment_dumux.yaml b/benchmarks/rotating-cylinders/environment_dumux.yaml deleted file mode 100644 index 6f09c4f..0000000 --- a/benchmarks/rotating-cylinders/environment_dumux.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: rc-dumux -channels: - - conda-forge - - defaults -dependencies: - - python=3.11 - - bash - - coreutils - - pint - - pyvista - - rdflib - - matplotlib - - pandas - - numpy - - pip - - pip: - - snakemake - - meshio - - roc-validator - - rohub diff --git a/benchmarks/rotating-cylinders/generate_rc_config.py b/benchmarks/rotating-cylinders/generate_rc_config.py index 9f922ed..08a0b1a 100644 --- a/benchmarks/rotating-cylinders/generate_rc_config.py +++ b/benchmarks/rotating-cylinders/generate_rc_config.py @@ -26,9 +26,8 @@ def write_benchmark_config(): "benchmark": "rotating-cylinders", "benchmark_uri": "https://www.openfoam.com/documentation/guides/latest/doc/verification-validation-rotating-cylinders-2d.html", "tools": ["dumux"], - "configurations": configurations, "configuration_to_parameter_file": config_to_param, - "configuration_to_solution_vtu_files": config_to_vtu, + "configurations": configurations, "container_image": "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0" } From eb7dc63669d9988d266c93df6436bff006b0adba Mon Sep 17 00:00:00 2001 From: Sarbani-Roy Date: Thu, 26 Mar 2026 11:57:45 +0100 Subject: [PATCH 7/7] Parameterize angular velocities and switch input files to JSON format --- benchmarks/rotating-cylinders/dumux/Snakefile | 8 +- .../dumux_config.json | 28 ++++-- .../dumux/dumux_input_gen.py | 89 +++++-------------- .../dumux/grid_files/grid_template.json | 15 ++-- .../rotating-cylinders/generate_rc_config.py | 10 +-- 5 files changed, 54 insertions(+), 96 deletions(-) rename benchmarks/rotating-cylinders/dumux/{input_files => dumux_input_files}/dumux_config.json (67%) diff --git a/benchmarks/rotating-cylinders/dumux/Snakefile b/benchmarks/rotating-cylinders/dumux/Snakefile index f3e67d5..cb03796 100644 --- a/benchmarks/rotating-cylinders/dumux/Snakefile +++ b/benchmarks/rotating-cylinders/dumux/Snakefile @@ -10,10 +10,10 @@ dumux_dir = f"{shared_dir}/dumux" rule generate_dumux_inputs: input: grid_t = f"{dumux_dir}/grid_files/grid_template.json", - dumux_t = f"{dumux_dir}/input_files/dumux_config.json", + dumux_t = f"{dumux_dir}/dumux_input_files/dumux_config.json", input_gen_script = f"{dumux_dir}/dumux_input_gen.py" output: - params = expand(f"{dumux_dir}/input_files/params_{{conf}}.input", conf=configs), + params = expand(f"{dumux_dir}/dumux_input_files/params_{{conf}}.json", conf=configs), grids = expand(f"{dumux_dir}/grid_files/grid_{{conf}}.json", conf=configs) shell: "python3 {input.input_gen_script} --grid_template {input.grid_t} --dumux_template {input.dumux_t}" @@ -22,7 +22,7 @@ rule generate_dumux_inputs: rule run_dumux_simulation: input: # Lambda prevents KeyError before the JSON is generated - params = lambda wildcards: f"{dumux_dir}/input_files/{config['configuration_to_parameter_file'][wildcards.configuration]}" + params = lambda wildcards: f"{dumux_dir}/dumux_input_files/{config['configuration_to_parameter_file'][wildcards.configuration]}" output: vtu_files = [ f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00000.vtu", @@ -36,7 +36,7 @@ rule run_dumux_simulation: """ set -euo pipefail cd /dumux/rotating-cylinders/build-cmake/test/freeflow/navierstokes/rotatingcylinders - ./test_ff_navierstokes_rotatingcylinders {input.params} + ./test_ff_navierstokes_rotatingcylinders JsonParameterFile={input.params} """ # Rule 3: Post-processing diff --git a/benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json b/benchmarks/rotating-cylinders/dumux/dumux_input_files/dumux_config.json similarity index 67% rename from benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json rename to benchmarks/rotating-cylinders/dumux/dumux_input_files/dumux_config.json index 3035543..ea30c5a 100644 --- a/benchmarks/rotating-cylinders/dumux/input_files/dumux_config.json +++ b/benchmarks/rotating-cylinders/dumux/dumux_input_files/dumux_config.json @@ -1,6 +1,8 @@ { "Problem": { - "Name": "{problem_name}", + "Name": "test_rotatingcylinders", + "Omega1": 1e2, + "Omega2": 0, "EnableGravity": false, "EnableInertiaTerms": true }, @@ -14,17 +16,25 @@ "LiquidDensity": 1, "LiquidDynamicViscosity": 1 }, - "Mass_Assembly_NumericDifference": { - "PriVarMagnitude": 0.01, - "BaseEpsilon": 0.01 + "Mass": { + "Assembly": { + "NumericDifference": { + "PriVarMagnitude": "1e-2", + "BaseEpsilon": 0.01 + } + } }, - "Momentum_Assembly_NumericDifference": { - "PriVarMagnitude": [0.2, 0.2], - "BaseEpsilon": 0.01 + "Momentum": { + "Assembly": { + "NumericDifference": { + "PriVarMagnitude": "0.2 0.2", + "BaseEpsilon": 0.01 + } + } }, "LinearSolver": { "MaxIterations": 500, - "ResidualReduction": 1e-10, + "ResidualReduction": "1e-10", "SymmetrizeDirichlet": true, "DirectSolverForVelocity": false, "GMResRestart": 500, @@ -45,6 +55,6 @@ "Newton": { "MinSteps": 1, "EnableAbsoluteResidualCriterion": true, - "MaxAbsoluteResidual": 4e-05 + "MaxAbsoluteResidual": "4e-6" } } \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/dumux/dumux_input_gen.py b/benchmarks/rotating-cylinders/dumux/dumux_input_gen.py index 94389aa..70a18f7 100644 --- a/benchmarks/rotating-cylinders/dumux/dumux_input_gen.py +++ b/benchmarks/rotating-cylinders/dumux/dumux_input_gen.py @@ -3,10 +3,6 @@ from pathlib import Path def generate_grid_files(grid_template_path, grid_dir, base_cells0, base_cells1, num_files): - """ - Generates standalone grid JSON files and returns data for merging. - """ - # Ensure the output directory exists grid_dir.mkdir(parents=True, exist_ok=True) if not grid_template_path.exists(): @@ -25,10 +21,9 @@ def generate_grid_files(grid_template_path, grid_dir, base_cells0, base_cells1, config_id = f"{c0}_{c1}" current_grid = json.loads(json.dumps(grid_template)) - current_grid["Grid"]["Cells0"] = f"{c0} {c0}" + current_grid["Grid"]["Cells0"] = f"{c0} {c0}" current_grid["Grid"]["Cells1"] = c1 - # Write individual grid JSON files to grid_dir grid_file_path = grid_dir / f"grid_{config_id}.json" with open(grid_file_path, "w") as f: json.dump(current_grid, f, indent=4) @@ -37,53 +32,13 @@ def generate_grid_files(grid_template_path, grid_dir, base_cells0, base_cells1, print(f"Generated Grid JSON: {grid_file_path}") return generated_configs - -def dict_to_dumux_format(data): - """ - Converts a nested dictionary into DuMuX .input format. - Handles underscore-to-dot conversion for sections and recursive nesting. - """ - lines = [] - - def process_section(section_name, content): - # Convert underscores to dots for DuMuX section naming convention - formatted_section = section_name.replace("_", ".") - lines.append(f"[{formatted_section}]") - - sub_sections = {} - - for key, value in content.items(): - if isinstance(value, dict): - # Store nested dicts to process as [Section.SubSection] later - sub_sections[f"{formatted_section}.{key}"] = value - else: - # Format values (handle lists as space-separated, bools as lowercase) - if isinstance(value, list): - val_str = " ".join(map(str, value)) - elif isinstance(value, bool): - val_str = str(value).lower() - else: - val_str = str(value) - lines.append(f"{key} = {val_str}") - - lines.append("") # Newline after section - - # Recursively process nested dictionaries - for sub_name, sub_content in sub_sections.items(): - process_section(sub_name, sub_content) - for section, params in data.items(): - process_section(section, params) - - return "\n".join(lines) -def write_dumux_inputs(grid_template, dumux_template, grid_out, input_out): - # Configuration constants +def write_dumux_inputs_json(grid_template, dumux_template, grid_out, input_out): problem_name_base = "/dumux/shared/dumux/test_rotatingcylinders" base_cells0, base_cells1 = 10, 80 num_files = 3 - # 1. Load the dumux Template if not dumux_template.exists(): print(f"Error: dumux template not found at {dumux_template}") return @@ -91,37 +46,39 @@ def write_dumux_inputs(grid_template, dumux_template, grid_out, input_out): with open(dumux_template, "r") as f: dumux_config = json.load(f) - # 2. Get Grid Data and generate grid files - grid_configs = generate_grid_files(grid_template, grid_out, base_cells0, base_cells1, num_files) + grid_configs = generate_grid_files( + grid_template, grid_out, base_cells0, base_cells1, num_files + ) - # 3. Merge and Generate .input files input_out.mkdir(parents=True, exist_ok=True) + for config_id, grid_data in grid_configs: full_config = json.loads(json.dumps(dumux_config)) + full_config["Problem"]["Name"] = f"{problem_name_base}_{config_id}" full_config.update(grid_data) - dumux_content = dict_to_dumux_format(full_config) - input_filename = input_out / f"params_{config_id}.input" - - with open(input_filename, "w") as f: - f.write(dumux_content) - - print(f"Generated DuMuX Input: {input_filename}") + output_file = input_out / f"params_{config_id}.json" + + with open(output_file, "w") as f: + json.dump(full_config, f, indent=4) + + print(f"Generated JSON Input: {output_file}") + if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Generate DuMuX input files from JSON templates.") - - # Define arguments - parser.add_argument("--grid_template", type=str, required=True, help="Path to grid_template.json") - parser.add_argument("--dumux_template", type=str, required=True, help="Path to dumux_config.json") - parser.add_argument("--grid_dir", type=str, default="./dumux/grid_files", help="Output directory for grid JSONs") - parser.add_argument("--input_dir", type=str, default="./dumux/input_files", help="Output directory for .input files") + parser = argparse.ArgumentParser( + description="Generate DuMuX JSON input files from templates." + ) + + parser.add_argument("--grid_template", type=str, required=True) + parser.add_argument("--dumux_template", type=str, required=True) + parser.add_argument("--grid_dir", type=str, default="./dumux/grid_files") + parser.add_argument("--input_dir", type=str, default="./dumux/dumux_input_files") args = parser.parse_args() - # Execute with absolute paths - write_dumux_inputs( + write_dumux_inputs_json( grid_template=Path(args.grid_template).resolve(), dumux_template=Path(args.dumux_template).resolve(), grid_out=Path(args.grid_dir).resolve(), diff --git a/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json b/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json index d99e09c..7c5021f 100644 --- a/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json +++ b/benchmarks/rotating-cylinders/dumux/grid_files/grid_template.json @@ -1,13 +1,10 @@ { - "Grid": { - "Cells0": { - "x": "{cells0_x}", - "y": "{cells0_y}" - }, + "Grid": { + "Cells0": "{cells0_x} {cells0_y}", "Cells1": "{cells1}", - "Grading0": [1.1, -1.1], - "Grading1": 1.0, - "Radial0": [1.0, 1.5, 2.0], - "Angular1": [0.0, 360.0] + "Grading0": "1.1 -1.1", + "Grading1": "1.0", + "Radial0": "1.0 1.5 2.0", + "Angular1": "0.0 360.0" } } \ No newline at end of file diff --git a/benchmarks/rotating-cylinders/generate_rc_config.py b/benchmarks/rotating-cylinders/generate_rc_config.py index 08a0b1a..b64b861 100644 --- a/benchmarks/rotating-cylinders/generate_rc_config.py +++ b/benchmarks/rotating-cylinders/generate_rc_config.py @@ -7,7 +7,6 @@ def write_benchmark_config(): configurations = [] config_to_param = {} - config_to_vtu = {} for i in range(num_files): cells0 = base_cells0 * (2 ** i) @@ -15,20 +14,15 @@ def write_benchmark_config(): config_name = f"{cells0}_{cells1}" configurations.append(config_name) - config_to_param[config_name] = f"params_{config_name}.input" - config_to_vtu[config_name] = [ - f"dumux/test_rotatingcylinders_{config_name}-00000.vtu", - f"dumux/test_rotatingcylinders_{config_name}-00001.vtu" - ] + config_to_param[config_name] = f"params_{config_name}.json" - # Your hardcoded dictionary benchmark_json = { "benchmark": "rotating-cylinders", "benchmark_uri": "https://www.openfoam.com/documentation/guides/latest/doc/verification-validation-rotating-cylinders-2d.html", "tools": ["dumux"], "configuration_to_parameter_file": config_to_param, "configurations": configurations, - "container_image": "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.0" + "container_image": "git.iws.uni-stuttgart.de:4567/benchmarks/rotating-cylinders:3.1" } with open("rotating-cylinders_config.json", "w") as f: