Skip to content
Merged
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
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
add_subdirectory(poisson)
add_subdirectory(linelast)
add_subdirectory(stokes)
add_subdirectory(steady_ns)
7 changes: 7 additions & 0 deletions examples/steady_ns/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
basis/
configs/
logs/
meshes/
results/
rom/
snapshots/
17 changes: 17 additions & 0 deletions examples/steady_ns/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2026 Lawrence Livermore National Security, LLC.
# SPDX-License-Identifier: MIT

set(STEADY_NS_EXAMPLE_FILES
README.md
generate_configs.py
prepare_meshes.sh
report_results.py
steady_ns.yml
submit_workflow.sh)

foreach(example_file IN LISTS STEADY_NS_EXAMPLE_FILES)
configure_file(${example_file} ${example_file} COPYONLY)
endforeach()

file(COPY jobs DESTINATION ${CMAKE_CURRENT_BINARY_DIR})

15 changes: 15 additions & 0 deletions examples/steady_ns/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Steady Navier--Stokes paper workflow

This reproduces the paper's representative 16x16, Re=25 tensorial-ROM case. It uses five component types, 2,000 random 2x2 training arrays (five jobs of 400), and 40 velocity, pressure, and supremizer modes per component. Meshes and HDF5 configurations are generated at run time and are not stored in the repository.

Install and build on Dane, then run from the configured build example:

```bash
./script/install_dane.bash
cd build/examples/steady_ns
./submit_workflow.sh
```

The submission chain runs mesh/configuration generation, sampling, `train_rom` POD and supremizer training, tensorial operator construction, a 16x16 FOM solve, and the dependent ROM comparison. If `train_rom` writes the POD bases but reaches its wall-time limit during supremizer enrichment, resume that step with `sbatch jobs/03_auxiliary.sbatch`; this fallback job is not part of the normal chain. Jobs use the paper/legacy Dane settings (`pbatch`, account `sosu`; one 2 GB single-thread task for online runs; 5 nodes and 36 MPI ranks for POD training). Override Slurm options when needed, for example `./submit_workflow.sh --account=my_account`.

Outputs are under `snapshots/`, `basis/`, `rom/`, and `results/`; logs are under `logs/`. After completion, `python3 report_results.py` prints FOM/ROM times and velocity/pressure relative errors. Each `jobs/*.sbatch` file may also be submitted separately.
123 changes: 123 additions & 0 deletions examples/steady_ns/generate_configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Generate the component catalog, training meshes, and paper 16x16 mesh."""

import argparse
import os
import sys
from pathlib import Path


def find_utilities(start: Path) -> Path:
configured_root = os.environ.get("SCALEUPROM_ROOT")
if configured_root:
candidate = Path(configured_root) / "utils" / "python"
if (candidate / "box_channel_config.py").is_file():
return candidate
for parent in (start, *start.parents):
candidate = parent / "utils" / "python"
if (candidate / "box_channel_config.py").is_file():
return candidate
raise FileNotFoundError("cannot locate scaleupROM/utils/python")


HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(find_utilities(HERE)))

from box_channel_config import BoxChannelConfig # noqa: E402
from config import Empty, ObjectInSpace # noqa: E402


def components():
return [
Empty(),
ObjectInSpace("square-circle"),
ObjectInSpace("square-square"),
ObjectInSpace("square-triangle"),
ObjectInSpace("square-star"),
]


def add_components(config):
for component in components():
config.addComponent(component)


def write_component_catalog(path: Path):
catalog = BoxChannelConfig(1, 5)
add_components(catalog)

# Register every oriented component/face pairing. BuildROM uses this
# compact configuration to construct all reusable component operators.
for component1 in catalog.comps:
for displacement, face1 in component1.face_map.items():
opposite = tuple(-value for value in displacement)
for component2 in catalog.comps:
if opposite in component2.face_map:
catalog.appendRefPort(
component1.name,
component2.name,
face1,
component2.face_map[opposite],
)

for component_index in range(len(catalog.comps)):
catalog.addMesh(component_index, 0)
catalog.close()
catalog.save(str(path))


# Component ordering extracted from the 16x16 configuration used by the paper.
# 0=empty, 1=circle, 2=square, 3=triangle, 4=star.
PAPER_16X16_COMPONENTS = [
1, 1, 3, 0, 2, 1, 4, 1, 2, 4, 0, 2, 4, 1, 4, 2,
0, 4, 1, 2, 4, 0, 3, 3, 0, 1, 3, 0, 0, 1, 3, 1,
0, 3, 4, 0, 1, 1, 1, 2, 1, 4, 0, 4, 1, 4, 4, 3,
1, 4, 0, 0, 3, 3, 0, 3, 4, 0, 0, 4, 2, 1, 0, 2,
0, 1, 4, 1, 0, 3, 3, 1, 3, 4, 4, 3, 4, 3, 1, 0,
3, 3, 0, 2, 0, 2, 0, 0, 2, 1, 0, 2, 3, 3, 1, 1,
3, 4, 2, 2, 4, 2, 0, 2, 3, 2, 0, 2, 0, 0, 3, 0,
2, 4, 3, 1, 4, 1, 2, 3, 3, 1, 1, 4, 1, 1, 4, 3,
3, 2, 3, 4, 4, 0, 0, 1, 2, 1, 2, 3, 1, 2, 1, 0,
1, 1, 1, 0, 0, 2, 0, 4, 4, 4, 3, 2, 4, 3, 0, 2,
0, 0, 1, 1, 0, 4, 0, 3, 0, 3, 3, 3, 4, 0, 2, 4,
3, 4, 4, 3, 4, 4, 3, 1, 4, 2, 1, 0, 4, 3, 4, 0,
1, 0, 3, 4, 2, 2, 2, 4, 4, 2, 3, 3, 1, 0, 2, 0,
3, 1, 2, 0, 0, 4, 1, 0, 1, 0, 4, 1, 1, 1, 0, 4,
1, 4, 0, 0, 1, 3, 3, 4, 2, 2, 3, 0, 0, 2, 1, 3,
4, 3, 1, 4, 4, 1, 2, 0, 2, 4, 4, 1, 0, 3, 4, 3,
]


def write_paper_target(path: Path):
target = BoxChannelConfig(16, 16)
add_components(target)
for component_index in PAPER_16X16_COMPONENTS:
target.addMesh(component_index, 0)
target.close()
target.save(str(path))


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=Path, default=HERE / "configs")
args = parser.parse_args()

output = args.output.resolve()
training = output / "training"
training.mkdir(parents=True, exist_ok=True)

write_component_catalog(output / "component_catalog.h5")

samples = BoxChannelConfig(2, 2)
add_components(samples)
samples.prefix = str(training / "box-channel.config")
count = samples.GenerateAllConfigs(0)
if count != 625:
raise RuntimeError(f"expected 625 training configurations, generated {count}")

write_paper_target(output / "paper.16x16.h5")
print(f"Generated {count} training configurations and {output / 'paper.16x16.h5'}")


if __name__ == "__main__":
main()
17 changes: 17 additions & 0 deletions examples/steady_ns/jobs/00_mesh.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
#SBATCH --job-name=ns-mesh
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=00:30:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

set -euo pipefail
example_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "${example_dir}"
mkdir -p logs
./prepare_meshes.sh

27 changes: 27 additions & 0 deletions examples/steady_ns/jobs/01_samples.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/bash
#SBATCH --job-name=ns-samples
#SBATCH --array=1-5
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=24:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%A_%a.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

batch_id=${SLURM_ARRAY_TASK_ID:-${1:-}}
if [[ -z ${batch_id} || ${batch_id} -lt 1 || ${batch_id} -gt 5 ]]; then
echo "Batch index must be in [1, 5]." >&2
exit 2
fi

mkdir -p "snapshots/batch-${batch_id}"
# RandomSampleGenerator seeds from wall time. Offset simultaneous array tasks
# so that independent batches cannot receive the same one-second seed.
sleep $((5 * batch_id))
"${MAIN_BIN}" -i steady_ns.yml -f \
"main/mode=sample_generation:main/use_rom=false:sample_generation/file_path/prefix=snapshots/batch-${batch_id}/steady_ns"
16 changes: 16 additions & 0 deletions examples/steady_ns/jobs/02_basis.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
#SBATCH --job-name=ns-basis
#SBATCH --nodes=5
#SBATCH --ntasks=36
#SBATCH --cpus-per-task=1
#SBATCH --mem-per-cpu=2G
#SBATCH --time=24:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

srun --ntasks=36 "${MAIN_BIN}" -i steady_ns.yml -f \
"main/mode=train_rom:main/use_rom=true"
17 changes: 17 additions & 0 deletions examples/steady_ns/jobs/03_auxiliary.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
# Manual recovery only: train_rom normally calls aux_train_rom itself.
#SBATCH --job-name=ns-supremizer
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=02:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

"${MAIN_BIN}" -i steady_ns.yml -f \
"main/mode=aux_train_rom:main/use_rom=true:basis/number_of_basis=40:basis/number_of_supremizer=40"
16 changes: 16 additions & 0 deletions examples/steady_ns/jobs/04_build_rom.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/bash
#SBATCH --job-name=ns-build
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=02:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

"${MAIN_BIN}" -i steady_ns.yml -f \
"main/mode=build_rom:main/use_rom=true:basis/number_of_basis=40:basis/number_of_supremizer=40:model_reduction/nonlinear_handling=tensor:model_reduction/save_operator/prefix=rom/steady_ns_nb40"
17 changes: 17 additions & 0 deletions examples/steady_ns/jobs/05_fom_16x16.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
#SBATCH --job-name=ns-fom-16
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=02:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

mkdir -p results/paraview/fom
"${MAIN_BIN}" -i steady_ns.yml -o results/fom_timings.h5 -f \
"main/mode=single_run:main/use_rom=false:mesh/component-wise/global_config=configs/paper.16x16.h5:model_reduction/compare_solution/enabled=false:save_solution/enabled=true:save_solution/file_path/prefix=results/fom_solution:visualization/enabled=true:visualization/file_path/prefix=results/paraview/fom/steady_ns"
19 changes: 19 additions & 0 deletions examples/steady_ns/jobs/06_rom_compare_16x16.sbatch
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
#SBATCH --job-name=ns-rom-16
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=1
#SBATCH --mem=2G
#SBATCH --time=02:00:00
#SBATCH --partition=pbatch
#SBATCH --account=genddfem
#SBATCH --output=logs/%x-%j.out

# shellcheck source=jobs/common.bash
source "$(dirname -- "${BASH_SOURCE[0]}")/common.bash"

mkdir -p results/paraview/rom
"${MAIN_BIN}" -i steady_ns.yml -o results/rom_vs_fom.h5 -f \
"main/mode=single_run:main/use_rom=true:mesh/component-wise/global_config=configs/paper.16x16.h5:basis/number_of_basis=40:basis/number_of_supremizer=40:model_reduction/nonlinear_handling=tensor:model_reduction/save_operator/prefix=rom/steady_ns_nb40:model_reduction/compare_solution/enabled=true:model_reduction/compare_solution/load_solution=true:model_reduction/compare_solution/fom_solution_file=results/fom_solution.h5:save_solution/enabled=false:visualization/enabled=true:visualization/file_path/prefix=results/paraview/rom/steady_ns"

python3 report_results.py
19 changes: 19 additions & 0 deletions examples/steady_ns/jobs/common.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
set -euo pipefail

EXAMPLE_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "${EXAMPLE_DIR}"

if [[ -n ${SCALEUPROM_MAIN:-} ]]; then
MAIN_BIN=${SCALEUPROM_MAIN}
elif [[ -x ../../bin/main ]]; then
MAIN_BIN=../../bin/main
elif [[ -x ../../build/bin/main ]]; then
MAIN_BIN=../../build/bin/main
else
echo "Cannot find the scaleupROM main executable; set SCALEUPROM_MAIN." >&2
exit 1
fi

export OMP_NUM_THREADS=1
mkdir -p logs results rom basis snapshots

43 changes: 43 additions & 0 deletions examples/steady_ns/prepare_meshes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
set -euo pipefail

example_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)

repo_root=${SCALEUPROM_ROOT:-}
if [[ -z ${repo_root} ]]; then
search_dir=${example_dir}
while [[ ${search_dir} != / ]]; do
if [[ -d ${search_dir}/utils/python && -d ${search_dir}/examples/stokes/meshes ]]; then
repo_root=${search_dir}
break
fi
search_dir=$(dirname -- "${search_dir}")
done
fi

if [[ -z ${repo_root} ]]; then
echo "Could not locate the scaleupROM source root." >&2
exit 1
fi

converter=${SCALEUPROM_GMSH2MFEM:-${repo_root}/build/utils/gmsh2mfem}
if [[ ! -x ${converter} ]]; then
echo "Missing ${converter}; build scaleupROM before preparing this example." >&2
exit 1
fi

cd "${example_dir}"
mkdir -p meshes configs

for mesh in square-circle.msh.mfem square-square.msh.mfem \
square-triangle.msh.mfem square-star.msh.mfem; do
cp "${repo_root}/examples/stokes/meshes/${mesh}" "meshes/${mesh}"
done

# Promote the linear triangular empty-cell mesh to the exact cubic mesh used
# by the paper. gmsh2mfem appends ".mfem" to its input name.
cp "${repo_root}/test/meshes/square.tri.mesh" meshes/square.tri.mesh
"${converter}" -m meshes/square.tri.mesh -o 3
mv meshes/square.tri.mesh.mfem meshes/square.tri-elem.mesh

python3 generate_configs.py --output configs
Loading
Loading