Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/run-dumux-benchmark.yml
Original file line number Diff line number Diff line change
@@ -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: model-validation
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 model-validation -f environment_benchmarks.yml

- 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"
14 changes: 12 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
.snakemake
site
site

# macOS system files
.DS_Store

# Snakemake hidden folders
.snakefile/
.snakemake/

# Sentinel / temporary files
*.done
.simulation_done
63 changes: 63 additions & 0 deletions benchmarks/rotating-cylinders/Snakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# benchmarks/rotating-cylinders/Snakefile
import json
import os

if not os.path.exists("rotating-cylinders_config.json"):
os.system("python3 generate_rc_config.py")

configfile: "rotating-cylinders_config.json"

# Variables
tools = config["tools"]
configs = config["configurations"]
benchmark = config["benchmark"]
benchmark_uri = config["benchmark_uri"]
result_dir = f"snakemake_results/{benchmark}"
shared_dir = os.getcwd()

rule all:
input:
expand(f"{result_dir}/{{tool}}/summary.json", tool=tools)

for tool in tools:
include: f"{tool}/Snakefile"

rule summary:
input:
script = "../common/summarize_results.py",

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 wc: expand(
f"{result_dir}/{wc.tool}/solution_field_data_{{conf}}.zip",
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}
"""
60 changes: 60 additions & 0 deletions benchmarks/rotating-cylinders/dumux/Snakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# benchmarks/rotating-cylinders/dumux/Snakefile
import os

# Setup and Config
tool = "dumux"
container_image = config["container_image"]
dumux_dir = f"{shared_dir}/dumux"

# Rule 1: Input generation
rule generate_dumux_inputs:
input:
grid_t = f"{dumux_dir}/grid_files/grid_template.json",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have thought that parameter.json should be an input to this rule? I see that you regenerate the parameters in input_gen_script.py but I think that is not the intention.

Copy link
Copy Markdown
Collaborator Author

@Sarbani-Roy Sarbani-Roy Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script merges the grid file with other parameters into a single input file, as Dumux requires one parameter file containing all inputs (including grids). As discussed previously, to ensure consistent metric representation across benchmarks, we need to use the summarize_results.py script:
https://github.com/BAMresearch/NFDI4IngModelValidationPlatform/blob/main/benchmarks/common/summarize_results.py

This script requires a separate grid file as input. Therefore, we specified grids in a separate file and merged it with other parameters to create a Dumux-compatible input file. If the grid file argument in summarize_results.py is optional, I could use a single parameter file with all inputs and remove the grid input generation block from the corresponding Snakefile. Or instead of Snakefile I can do this in the workflow file. Which one do you suggest? Or, do you have any other suggestions for handling this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the grid file is just a json file with numbers, why is that a template? Let's discuss that in person, but IMO we should either generate a mesh (if we really want to check the tool implementation assuming everything is identically implemented), or just pass the mesh resolution as a paramter, or completely add that to dumux config.

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}/dumux_input_files/params_{{conf}}.json", conf=configs),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the params file the output of the generate_dumux_inputs? The parameter file(s) are stored in the benchmark definition (currently in git, later they should be stored in a benchmark ROCrate). These should serve as parameterized input to your solver, so e.g. the parameter file should allow to run the benchmark for different fluid velocities and for each velocity do a mesh sensitivity study, resulting in num_meshes * num_velocities parameter files. Dumux ist getting then one parameter file (with a specific velocity and mesh density) and outputs the corresponding metrics (e.g. min mean max pressure).

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 2: Simulation
rule run_dumux_simulation:
input:
# Lambda prevents KeyError before the JSON is generated
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",
f"{dumux_dir}/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 JsonParameterFile={input.params}
"""

# Rule 3: Post-processing
rule postprocess_dumux:
input:
sim_data = [
f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00000.vtu",
f"{dumux_dir}/test_rotatingcylinders_{{configuration}}-00001.vtu"
],
postprocess_script = f"{dumux_dir}/run_dumux_postprocessing.py"
output:
metrics = f"{result_dir}/{tool}/solution_metrics_{{configuration}}.json",
fields = f"{result_dir}/{tool}/solution_field_data_{{configuration}}.zip"
shell:
"""
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}
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"Problem": {
"EnableGravity": false,
"EnableInertiaTerms": true
},
"FreeFlow": {
"EnableUnsymmetrizedVelocityGradient": true
},
"Flux": {
"UpwindWeight": 0.5
},
"Mass.Assembly.NumericDifference": {
"PriVarMagnitude": "1e-2",
"BaseEpsilon": 0.01
},
"Momentum.Assembly.NumericDifference": {
"PriVarMagnitude": "0.2 0.2",
"BaseEpsilon": 0.01
},
"LinearSolver": {
"Type": "gmres",
"MaxIterations": 500,
"ResidualReduction": "1e-10",
"SymmetrizeDirichlet": true,
"DirectSolverForVelocity": false,
"GMResRestart": 500,
"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-6"
}
}
123 changes: 123 additions & 0 deletions benchmarks/rotating-cylinders/dumux/dumux_input_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import json
import argparse
from pathlib import Path

def generate_grid_files(grid_template_path, grid_dir, base_cells0, base_cells1, num_files, prob_data):
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)

# Extract geometry from problem file
r_in = prob_data["geometry"]["radius_inner"]
r_out = prob_data["geometry"]["radius_outer"]
r_mid = (r_in + r_out) / 2.0
ang = prob_data["geometry"]["angle_range"]

generated_configs = []

for i in range(num_files):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this loop should be done outside. On the outer level (for all tools, not only dumux), we define how to generate c0 and c1, and those are then added as parameters. These parameter are then passed as input to your dumux specific part.

scale = 2 ** i
c0 = base_cells0 * scale
c1 = base_cells1 * scale
config_id = f"{c0}_{c1}"

current_grid = json.loads(json.dumps(grid_template))

# Inject tool-specific resolution
current_grid["Grid"]["Cells0"] = f"{c0} {c0}"
current_grid["Grid"]["Cells1"] = c1

# Inject problem-specific geometry
current_grid["Grid"]["Radial0"] = f"{r_in} {r_mid} {r_out}"
current_grid["Grid"]["Angular1"] = f"{ang[0]}.0 {ang[1]}.0"

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 write_dumux_inputs_json(grid_template, dumux_template, grid_out, input_out, prob_path):
problem_name_base = "/dumux/shared/dumux/test_rotatingcylinders"
base_cells0, base_cells1 = 10, 80
num_files = 3

# 1. Load Problem Specific Params (from parent/current folder)
if not prob_path.exists():
print(f"Error: problem file not found at {prob_path}")
return

with open(prob_path, "r") as f:
prob_data = json.load(f)

# 2. Load Tool 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)

# 3. Generate Grids (Injected with problem geometry)
grid_configs = generate_grid_files(
grid_template, grid_out, base_cells0, base_cells1, num_files, prob_data
)

input_out.mkdir(parents=True, exist_ok=True)

# 4. Generate Final Params
for config_id, grid_data in grid_configs:
full_config = json.loads(json.dumps(dumux_config))

# Merge problem-specific physics and identity
if "Problem" not in full_config: full_config["Problem"] = {}
full_config["Problem"]["Name"] = f"{problem_name_base}_{config_id}"
full_config["Problem"]["Omega1"] = prob_data["physics"]["omega_inner"]
full_config["Problem"]["Omega2"] = prob_data["physics"]["omega_outer"]

# Merge fluid properties
full_config["Component"] = {
"LiquidDensity": prob_data["physics"]["LiquidDensity"],
"LiquidDynamicViscosity": prob_data["physics"]["LiquidDynamicViscosity"]
}

# Merge grid data
full_config.update(grid_data)

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 JSON input files.")

parser.add_argument("--grid_template", type=str, default="./dumux/grid_files/grid_template.json")
parser.add_argument("--dumux_template", type=str, default="./dumux/dumux_input_files/dumux_config.json")
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()

# Locate problem file in the same directory as this script
script_dir = Path(__file__).parent.parent.resolve()
problem_file = script_dir / "param_rot_cylin.json"

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(),
input_out=Path(args.input_dir).resolve(),
prob_path=problem_file
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Grid": {
"Grading0": "1.1 -1.1",
"Grading1": "1.0"
}
}
Loading
Loading