-
Notifications
You must be signed in to change notification settings - Fork 5
Run DuMuX simulation inside container via Snakemake #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ab25875
9d2e868
91fd400
ef52760
edc0793
ad3298d
443a91a
eb7dc63
3fc7ecb
0dfbe98
8dcd787
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| 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 |
| 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} | ||
| """ |
| 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", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
joergfunger marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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: | ||
srosenbu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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" | ||
| } | ||
| } |
| 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): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.