diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 6ce337d..2536b8e 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -1,5 +1,5 @@ --- -name: Build and push Docker image +name: Docker on: push: @@ -31,7 +31,7 @@ env: jobs: build_and_push: - name: 📦 + name: 📦 (build and push docker image) runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index eed20cd..17620ab 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -1,5 +1,5 @@ --- -name: Governance checks +name: Governance permissions: contents: read @@ -13,7 +13,7 @@ concurrency: jobs: governance: - name: 🩺 + name: 🩺 (run governance checks) runs-on: ubuntu-latest timeout-minutes: 5 defaults: diff --git a/.github/workflows/test_bash_infra.yml b/.github/workflows/test_bash_infra.yml index bf4e64f..e481720 100644 --- a/.github/workflows/test_bash_infra.yml +++ b/.github/workflows/test_bash_infra.yml @@ -1,5 +1,5 @@ --- -name: Test Bash infrastructure +name: Bash on: push: @@ -18,7 +18,7 @@ concurrency: jobs: run_natively: - name: 🛠 (natively) + name: 🛠 (native bash infra tests) runs-on: ubuntu-latest timeout-minutes: 25 # something is off if we need more than this defaults: @@ -60,7 +60,7 @@ jobs: CI=1 bash 02_generate_output_from_code.bash run_dockerised: - name: 🐳 (dockerised) + name: 🐳 (dockerised bash infra tests) runs-on: ubuntu-latest timeout-minutes: 25 # something is off if we need more than this defaults: diff --git a/.github/workflows/test_figure_generation.yml b/.github/workflows/test_figure_generation.yml index 9527083..00b7f7f 100644 --- a/.github/workflows/test_figure_generation.yml +++ b/.github/workflows/test_figure_generation.yml @@ -1,5 +1,5 @@ --- -name: Test figure generation +name: Figures on: push: @@ -18,7 +18,7 @@ concurrency: jobs: run_natively: - name: 🛠 (natively) + name: 🛠 (native figure generation tests) runs-on: ubuntu-latest timeout-minutes: 20 # something is off if we need more than this defaults: @@ -114,7 +114,7 @@ jobs: delete-on-status: success run_dockerised: - name: 🐳 (dockerised) + name: 🐳 (dockerised figure generation tests) runs-on: ubuntu-latest timeout-minutes: 20 # something is off if we need more than this defaults: diff --git a/.github/workflows/test_python_code.yml b/.github/workflows/test_python_code.yml index 04c018e..40b4b49 100644 --- a/.github/workflows/test_python_code.yml +++ b/.github/workflows/test_python_code.yml @@ -1,5 +1,5 @@ --- -name: Run Python Tests +name: Code on: push: @@ -18,7 +18,7 @@ concurrency: jobs: run_natively: - name: 🛠 (natively) + name: 🛠 (native Python tests) runs-on: ubuntu-latest timeout-minutes: 25 # something is off if we need more than this defaults: @@ -76,7 +76,7 @@ jobs: delete-on-status: success run_dockerised: - name: 🐳 (dockerised) + name: 🐳 (dockerised Python tests) runs-on: ubuntu-latest timeout-minutes: 25 # something is off if we need more than this defaults: diff --git a/.github/workflows/zenodo-release.yml b/.github/workflows/zenodo-release.yml index f359437..bc63acb 100644 --- a/.github/workflows/zenodo-release.yml +++ b/.github/workflows/zenodo-release.yml @@ -1,5 +1,5 @@ --- -name: Zenodo release +name: Zenodo # Publish a release of this repository to Zenodo. # @@ -68,7 +68,7 @@ concurrency: jobs: release: - name: Archive and deposit to Zenodo + name: 🏛 (archive and deposit to Zenodo) runs-on: ubuntu-latest timeout-minutes: 15 defaults: diff --git a/.gitignore b/.gitignore index 8da71b1..82cbafb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__ *.ipynb .ipynb_checkpoints .secrets +02_generate_output_from_code.log diff --git a/.linkspector.yml b/.linkspector.yml index 4adca9d..58dc51c 100644 --- a/.linkspector.yml +++ b/.linkspector.yml @@ -7,7 +7,6 @@ useGitIgnore: true ignorePatterns: - pattern: '^mailto:' - pattern: '^meta/index' -- pattern: '^https://github\.com/pymor/2025_pymor_paper/.*' # Site not publicly available -- pattern: '^https://github\.com/pymor/2026_paper_code.*' # Site not publicly available +- pattern: '^https://handle\.test\.datacite\.org/10.5072/zenodo\.568476' # gives a 404 timeout: 10000 retryCount: 3 diff --git a/02_generate_output_from_code.bash b/02_generate_output_from_code.bash index 613f3e6..7734dee 100755 --- a/02_generate_output_from_code.bash +++ b/02_generate_output_from_code.bash @@ -5,15 +5,38 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "${SCRIPT_DIR}" +# Full output of every experiment script is always captured here, so a second +# terminal can `tail -f 02_generate_output_from_code.log` regardless of VERBOSE. +LOGFILE="02_generate_output_from_code.log" +LOGFILE_PATH="${SCRIPT_DIR}/${LOGFILE}" +: > "${LOGFILE_PATH}" + +# VERBOSE!=0 also streams each script's output to this console. +# VERBOSE=0 (default) keeps the console clean and instead prints a +# per-script wall-time and peak-memory summary; the full output is still in the +# LOGFILE_PATH. +VERBOSE="${VERBOSE:-0}" + if [[ -t 1 ]]; then - _green=$'\033[32m' _bold=$'\033[1m' _red=$'\033[31m' _reset=$'\033[0m' + _green=$'\033[32m' _bold=$'\033[1m' _red=$'\033[31m' _blue=$'\033[34m' _reset=$'\033[0m' else - _green="" _bold="" _red="" _reset="" + _green="" _bold="" _red="" _blue="" _reset="" fi log() { echo "${_green}:: $*${_reset}"; } raise() { echo "${_red}:: [ERROR] $*${_reset}" >&2;} +if [[ "${VERBOSE}" == "0" ]]; then + log "Suppressing experiment output: tail -f ${LOGFILE} to follow, or run with VERBOSE=1." + step() { echo -n "${_blue}:: $* ... ${_reset}"; } +else + step() { echo "${_blue}:: $*:${_reset}"; } +fi + +if [[ "${QUICKTEST:-0}" == "1" ]]; then + log "QUICKTEST=1 detected: only running coarse experiments." +fi + PYTHON_PATH="$(command -v python 2>/dev/null || true)" EXPECTED_SUFFIX=".conda_env/bin/python" @@ -27,6 +50,52 @@ else exit 1 fi +# Memory is measured via Python's resource.getrusage, which is portable and needs +# no /usr/bin/time. +run_experiment() { + local exp="$1" + VERBOSE="${VERBOSE}" LOGFILE_PATH="${LOGFILE_PATH}" python - "${exp}" <<'PY' +import os, resource, subprocess, sys, time + +script = sys.argv[1] +verbose = os.environ.get('VERBOSE', '0') != '0' +LOGFILE_PATH = os.environ['LOGFILE_PATH'] + +start = time.perf_counter() +with open(LOGFILE_PATH, 'a') as log: + log.write(f'\n===== {script} =====\n') + log.flush() + if verbose: + # Stream to both console and LOGFILE_PATH. + proc = subprocess.Popen([sys.executable, script], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1) + for line in proc.stdout: + sys.stdout.write(line) + log.write(line) + proc.wait() + else: + # LOGFILE_PATH only; console stays clean. + proc = subprocess.run([sys.executable, script], stdout=log, stderr=log) +wall = time.perf_counter() - start + +# ru_maxrss is reported in KiB on Linux (the container platform). +maxrss_kib = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss +if sys.platform == 'darwin': # macOS reports bytes + maxrss_kib /= 1024 +mem_mib = maxrss_kib / 1024 +h, rem = divmod(int(wall), 3600) +m, s = divmod(rem, 60) +time_str=f"{s:02d}s" +if m > 0: + time_str = f"{m:02d}m{time_str}" +if h > 0: + time_str = f"{h:d}h{time_str}" +print(f" (took {time_str} and {mem_mib:.0f}MiB)") +sys.exit(proc.returncode) +PY +} + log "Setting environment variables for reproducability ..." export OMP_NUM_THREADS=1 export OPENBLAS_NUM_THREADS=1 @@ -34,23 +103,21 @@ export MKL_NUM_THREADS=1 export MKL_CBWR=COMPATIBLE export MPLBACKEND=Agg -log "Running Mass-spring-damper experiments..." +log "Running Mass-spring-damper experiments:" cd "${SCRIPT_DIR}/code/msd_chain" for exp in run_*.py do - log "Running ${exp}" - python ./"${exp}" - echo "" + step "Running msd_chain/${exp}" + run_experiment "${exp}" done cd "${SCRIPT_DIR}" -log "Running Navier-Stokes experiments..." +log "Running Navier-Stokes experiments:" cd "${SCRIPT_DIR}/code/navier_stokes" for exp in run_*.py do - log "Running ${exp}" - python ./"${exp}" - echo "" + step "Running navier_stokes/${exp}" + run_experiment "${exp}" done cd "${SCRIPT_DIR}" diff --git a/03_generate_figures_from_output.bash b/03_generate_figures_from_output.bash index cb3ea52..d125354 100755 --- a/03_generate_figures_from_output.bash +++ b/03_generate_figures_from_output.bash @@ -36,6 +36,10 @@ fi FIGURES_DIR="${SCRIPT_DIR}/figures" cd "${FIGURES_DIR}" +if [[ "${QUICKTEST:-0}" == "1" ]]; then + log "QUICKTEST=1 detected: figures will use the QUICKTEST output." +fi + # Collect the default figure targets. This mirrors @default_files in # .latexmkrc: all fig_*.tex plus architecture_diagram.tex. log "Collecting figure targets..." diff --git a/README.md b/README.md index 53c2714..ba167d3 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ by - Stephan Rave () - Felix Schindler () +[![DOI](https://sandbox.zenodo.org/badge/DOI/10.5072/zenodo.568476.svg)](https://handle.test.datacite.org/10.5072/zenodo.568476) [![REUSE status](https://api.reuse.software/badge/github.com/pymor/2026_paper_code)](https://api.reuse.software/info/github.com/pymor/2026_paper_code) + ## Reproducing results from the paper ### Generating the figures and tables @@ -46,7 +48,8 @@ bash 02_generate_output_from_code.bash to re-generate new [output/](output/) data. > Note, that we're running all experiments sequentially with MKL compatibility flags for numerical reproducability (see the [02_generate_output_from_code.bash](02_generate_output_from_code.bash) script). -> Running them required around 51GB of memory and 150h of time on our target hardware (see for instance [figures/NOTES.md](figures/NOTES.md))! +> Running them required around 52GB of memory and 150h of time on our target hardware (see for instance [figures/NOTES.md](figures/NOTES.md))! +> See [the section on quick tests](#quick-tests) on how to run coarse experiments with lower requirements. ## Getting started @@ -147,6 +150,40 @@ docker compose -f docker/docker-compose.yml run --rm dev COMMAND This will mount the projects directory inside the container, source the env and run `COMMAND`. Sourcing the `01_activate_env.bash` is thus _not_ required in this execution mode. +## Quick tests + +We support running coarse experiments with `QUICKTEST=1` to roughly reproduce the behaviour of the `code/` with lower compute requirements. + +> Note: this required around 2GB of memory and 1.5h of time on a consumer Notebook with a 12th Gen Intel(R) Core(TM) i7-12800H CPU. + +- In [native mode](#native-execution): + + ```bash + source 01_activate_env.bash + QUICKTEST=1 bash 02_generate_output_from_code.bash + QUICKTEST=1 bash 03_generate_figures_from_output.bash + ``` + +- In [dockerised mode](#dockerised-execution): + + ```bash + QUICKTEST=1 docker compose -f docker/docker-compose.yml run --rm dev bash 02_generate_output_from_code.bash + QUICKTEST=1 docker compose -f docker/docker-compose.yml run --rm dev bash 03_generate_figures_from_output.bash + ``` + +This should leave you with `output/` and `figures/` that display a similar behaviour to the published ones. + +> Note that there are significant differences in some figures: +> +> - figures/fig_dmd_evals_errors.pdf might not reproduce the eigenvalues +> - figures/fig_solution_outputs.pdf suffers from the shorter `T=0.5` (as opposed to `T=8`) +> +> Also note that running the quick tests will change the existing `output/` and `figures/` and add new files (due to changed filenames). +> To discard these changes and revert back to the reference state, use: +> +> - `git restore output figures` +> - `git clean -xdf output figures` + ## License Different parts of this repository are released under different licenses, diff --git a/code/msd_chain/config.py b/code/msd_chain/config.py index f76e5e5..7f5f9f8 100644 --- a/code/msd_chain/config.py +++ b/code/msd_chain/config.py @@ -1,6 +1,7 @@ import os CI = os.environ.get('CI', '0') == '1' +QUICKTEST = os.environ.get('QUICKTEST', '0') == '1' if CI: print("CI=1 detected: only running basic test cases!") @@ -11,6 +12,15 @@ PARAM_GRID = 25 +elif QUICKTEST: + print("QUICKTEST=1 detected: only running coarse experiments!") + + DATA_AMOUNT_STEP = 20 + ERA_NT_FACTOR = 51 + ORDER_STEP = 1 + PARAM_GRID = 100 + + else: DATA_AMOUNT_STEP = 1 ERA_NT_FACTOR = 1020 diff --git a/code/navier_stokes/config.py b/code/navier_stokes/config.py index 5adb98c..5873b6a 100644 --- a/code/navier_stokes/config.py +++ b/code/navier_stokes/config.py @@ -3,6 +3,7 @@ from pymor.parameters.base import Parameters CI = os.environ.get('CI', '0') == '1' +QUICKTEST = os.environ.get('QUICKTEST', '0') == '1' if CI: print("CI=1 detected: only running basic test cases!") @@ -29,6 +30,31 @@ ML_ONLY_FINAL = True ML_NN_EPOCHS = 10 +elif QUICKTEST: + print("QUICKTEST=1 detected: only running coarse experiments!") + + T = 0.5 + REFINE = False + DT = 0.0005 + + PARAMETER_RANGE = (0.01, 1.) + + MAX_NUM_PODDEIM_TRAINING_MUS = 11 + MAX_NUM_ML_TRAINING_MUS = 30 + NUM_DETERMINISTIC_TRAINING_MUS = 6 + NUM_RANDOM_TRAINING_MUS = MAX_NUM_ML_TRAINING_MUS - NUM_DETERMINISTIC_TRAINING_MUS + NUM_TEST_MUS = 20 + + POD_TOL = 1e-4 + HAPOD_SLICES = 2 + + ML_INPUT_SCALING = True + ML_OUTPUT_SCALING = True + ML_NUM_ROM_TRAINING_MUS = 200 + ML_TIME_SUBSAMPLE = 10 + ML_ONLY_FINAL = False + ML_NN_EPOCHS = 10 + else: T = 8 REFINE = True diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9e6a891..7a9ae5b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,6 +5,11 @@ services: hostname: 2026_paper_code_docker environment: - CI=${CI:-0} + - QUICKTEST=${QUICKTEST:-0} + - VERBOSE=${VERBOSE:-0} + # Force VTK/pyvista offscreen software rendering so headless PNG generation + # does not probe GPU/EGL/X11 and flood the output with "bad X server"/EGL warnings. + - VTK_DEFAULT_OPENGL_WINDOW=vtkOSOpenGLRenderWindow build: context: .. dockerfile: docker/Dockerfile