diff --git a/.github/AGENT_GUIDE.md b/.github/AGENT_GUIDE.md index 79ce4de965..a1041058f4 100644 --- a/.github/AGENT_GUIDE.md +++ b/.github/AGENT_GUIDE.md @@ -208,7 +208,7 @@ Key constraints: - **Spin-up restart**: cold-start `init.nc` has zero hydrometeors. Ensemble generation runs 24h unperturbed first, then perturbs from the restart. - **PyCECT minimum members**: ensemble size must be >= number of output variables (~48). Default: 200. - **Time slice**: `run-perturb-mpas` invokes `trim_history.py` with `--tslice (Time.size - 1)`, producing files with exactly **one** time dimension (`Time=1`). PyCECT (`pyCECT.py` and `pyEnsSumMPAS.py`) is then called with `--tslice 0` — the only valid index in a single-slice file. There is no `ECT_TSLICE` knob in `ci-config.env`; changing the effective slice would require editing `run-perturb-mpas` (not config). Slice 0 in a cold-start (untrimmed) run is the unintegrated initial state, which is why we trim before validation. -- ECT parameters and paths in `.github/ci-config.env` (`ECT_*`, `ECT_EXCLUDED_VARS`, `PYCECT_TAG`); the ECT release tag itself is derived at runtime from `Registry.xml` via the `mpas-version` composite action. +- Per-case ECT parameters (archive, durations, perturbation, summary/restart names, excluded variables) are in `.github/ect-cases.yml`; PyCECT tag/commit stay in `.github/ci-config.env` (`PYCECT_*`); the ECT release tag itself is derived at runtime from `Registry.xml`. ## Shell Scripting Notes @@ -233,3 +233,11 @@ Workflows accept `mpas-repository` and `mpas-ref` inputs for testing upstream MP - **NVHPC+OpenMPI**: model exits 134 (SIGABRT) on GA runners with 4 ranks. MPICH works. Caller workflows and reusable CPU/GPU jobs mark this combination `continue-on-error` until resolved. - **NVHPC/Intel MPI F08 bindings**: broken with hpcdev MPI libraries. Both use `MPAS_MPI_F08=0` workaround. + +### ECT cases (`.github/ect-cases.yml`) + +One table lists the ECT cases: `default` (used when a workflow's `ect-case` input is empty), `defaults` (shared values) and `cases` +(per-case fields such as `summary-file`, `restart-file`, `resolution`, durations, perturbation; quote durations and magnitudes, YAML reads +unquoted `1_00:00:00` as a number). Each workflow resolves its case once in its config job (`resolve-ect-case` action) and passes the +values on as inputs. Run a case by typing its name into the `ect-case` input of `ect-test.yml` (or `ect-ensemble-gen.yml` to build its +reference). A new case needs its own summary and restart assets in the ECT release. Results record the case in an `ect-case` column. diff --git a/.github/actions/resolve-ect-case/action.yml b/.github/actions/resolve-ect-case/action.yml new file mode 100644 index 0000000000..3c3f1fd14f --- /dev/null +++ b/.github/actions/resolve-ect-case/action.yml @@ -0,0 +1,54 @@ +name: 'Resolve ECT case' +description: > + Resolve one ECT case from .github/ect-cases.yml (defaults merged with the case) into explicit outputs. Run it in a workflow's + config job (the repository must be checked out) and pass the outputs on as inputs; nothing is read from the environment. + +inputs: + ect-case: + description: 'ECT case name (a key under `cases` in .github/ect-cases.yml). Empty = the table default.' + required: false + default: '' + +outputs: + case: + description: 'Resolved case name' + value: ${{ steps.resolve.outputs.case }} + resolution: + description: 'Test archive name (.tar.gz, RELEASE_TESTDATA_ in ci-config.env)' + value: ${{ steps.resolve.outputs.resolution }} + summary-file: + description: 'Ensemble summary release asset name' + value: ${{ steps.resolve.outputs.summary-file }} + restart-file: + description: 'Spin-up restart release asset name (stored gzipped)' + value: ${{ steps.resolve.outputs.restart-file }} + spinup-duration: + description: 'Spin-up duration (config_run_duration format)' + value: ${{ steps.resolve.outputs.spinup-duration }} + run-duration: + description: 'Duration of each ensemble/test member (config_run_duration format)' + value: ${{ steps.resolve.outputs.run-duration }} + ensemble-size: + description: 'Reference ensemble size' + value: ${{ steps.resolve.outputs.ensemble-size }} + perturb-variable: + description: 'Perturbed initial-condition variable' + value: ${{ steps.resolve.outputs.perturb-variable }} + perturb-magnitude: + description: 'Perturbation magnitude' + value: ${{ steps.resolve.outputs.perturb-magnitude }} + excluded-vars: + description: 'Path (relative to the repo root) of the excluded-variables file' + value: ${{ steps.resolve.outputs.excluded-vars }} + +runs: + using: 'composite' + steps: + - name: Resolve ECT case + id: resolve + shell: bash + env: + ECT_CASE_INPUT: ${{ inputs.ect-case }} + run: | + python3 -c "import yaml" 2>/dev/null || python3 -m pip install --quiet pyyaml + python3 "${GITHUB_WORKSPACE}/.github/scripts/resolve-ect-case.py" --case "${ECT_CASE_INPUT}" | tee -a "${GITHUB_OUTPUT}" diff --git a/.github/actions/run-perturb-mpas/action.yml b/.github/actions/run-perturb-mpas/action.yml index df5952a1f0..1e5bdfed7a 100644 --- a/.github/actions/run-perturb-mpas/action.yml +++ b/.github/actions/run-perturb-mpas/action.yml @@ -33,6 +33,12 @@ inputs: description: 'MPI implementation (openmpi, mpich)' required: false default: 'openmpi' + excluded-vars: + description: 'Path (relative to the repo root) of the file listing history variables to drop before PyCECT (from resolve-ect-case)' + required: true + perturb-magnitude: + description: 'Magnitude of the initial-condition perturbation (from resolve-ect-case)' + required: true gpu-aware-mpi: description: > Use GPU-aware MPI halo exchanges (config_gpu_aware_mpi = .true. in each member's namelist plus the MPI @@ -124,8 +130,14 @@ runs: fi EXCLUDE_FILE="" - if [ -n "${ECT_EXCLUDED_VARS}" ]; then - EXCLUDE_FILE="${GITHUB_WORKSPACE}/${ECT_EXCLUDED_VARS}" + EXCLUDED_VARS="${{ inputs.excluded-vars }}" + PERTURB_MAGNITUDE="${{ inputs.perturb-magnitude }}" + if [ -z "${PERTURB_MAGNITUDE}" ]; then + echo "::error::perturb-magnitude is required (resolve it with the resolve-ect-case action)" + exit 1 + fi + if [ -n "${EXCLUDED_VARS}" ]; then + EXCLUDE_FILE="${GITHUB_WORKSPACE}/${EXCLUDED_VARS}" fi mkdir -p "${OUTDIR}" @@ -173,7 +185,7 @@ runs: echo "[$(date +%H:%M:%S)] Perturbing theta (seed=${MEMBER})..." python3 ${GITHUB_WORKSPACE}/.github/actions/run-perturb-mpas/perturb_theta.py \ - "${PERTURB_FILE}" --seed ${MEMBER} --magnitude ${ECT_PERTURB_MAGNITUDE} + "${PERTURB_FILE}" --seed ${MEMBER} --magnitude ${PERTURB_MAGNITUDE} if [ "${VERBOSE}" = "true" ]; then echo " Restart MD5: $(md5sum "${PERTURB_FILE}" | cut -d' ' -f1)" diff --git a/.github/actions/run-perturb-mpas/trim_history.py b/.github/actions/run-perturb-mpas/trim_history.py index 706b4268ef..81b3c21825 100644 --- a/.github/actions/run-perturb-mpas/trim_history.py +++ b/.github/actions/run-perturb-mpas/trim_history.py @@ -9,7 +9,7 @@ areaTriangle) to compute global means. These static variables are preserved even though they lack a Time dimension. -The ECT exclusion list path is configured via ECT_EXCLUDED_VARS in ci-config.env +The ECT exclusion list path comes from the ECT case (excluded-vars in .github/ect-cases.yml) and passed via --exclude-file. Usage: diff --git a/.github/actions/validate-ect/action.yml b/.github/actions/validate-ect/action.yml index 62da7d62cd..dc7591df15 100644 --- a/.github/actions/validate-ect/action.yml +++ b/.github/actions/validate-ect/action.yml @@ -15,6 +15,13 @@ inputs: ect-tag: description: 'ECT data release tag (from the mpas-version action, e.g. ect-v8.4)' required: true + summary-file: + description: 'Ensemble summary release asset name for the ECT case (from resolve-ect-case)' + required: true + ect-case: + description: "Resolved ECT case name (from resolve-ect-case). Recorded as the 'ect-case' column of the result." + required: false + default: '' dimensions: description: 'Multi-line key=value pairs describing this test combination (written into result file)' required: false @@ -44,7 +51,11 @@ runs: # ECT release tag is passed in, not stored in ci-config.env. # See .github/actions/mpas-version. - echo "summary-file=${ECT_SUMMARY_FILE}" >> $GITHUB_OUTPUT + if [ -z "${{ inputs.summary-file }}" ]; then + echo "::error::summary-file is required (resolve it with the resolve-ect-case action)" + exit 1 + fi + echo "summary-file=${{ inputs.summary-file }}" >> $GITHUB_OUTPUT echo "pycect-tag=${PYCECT_TAG}" >> $GITHUB_OUTPUT echo "pycect-commit=${PYCECT_COMMIT}" >> $GITHUB_OUTPUT echo "release-tag=${{ inputs.ect-tag }}" >> $GITHUB_OUTPUT @@ -207,4 +218,7 @@ runs: if [ -n "${DIMS}" ]; then echo "${DIMS}" fi + if [ -n "${{ inputs.ect-case }}" ]; then + echo "ect-case=${{ inputs.ect-case }}" + fi } > ect-result.txt diff --git a/.github/ci-config.env b/.github/ci-config.env index e54eab1b90..ba8e26d9da 100644 --- a/.github/ci-config.env +++ b/.github/ci-config.env @@ -102,17 +102,11 @@ RELEASE_TESTDATA_120KM=testdata-120km-v2 # ── ECT configuration ──────────────────────────── -# Parameters for the Ensemble Consistency Test (PyCECT). -# These are CI-specific settings not carried in the model namelist. +# Per-case ECT settings (test archive, physics namelist overrides, run lengths, perturbation, summary/restart asset names, +# excluded variables) live in ONE table, .github/ect-cases.yml, resolved by the .github/actions/resolve-ect-case action. +# Only tool-level settings stay here. # Reference: Price-Broncucia et al. (2025), doi:10.5194/gmd-18-2349-2025 -ECT_RESOLUTION=120km -ECT_ENSEMBLE_SIZE=200 -ECT_PERTURB_MAGNITUDE=1e-14 -ECT_PERTURB_VARIABLE=theta -ECT_SUMMARY_FILE=mpas_ect_summary_120km.nc -ECT_RESTART_FILE=120km-spinup-restart.nc -ECT_EXCLUDED_VARS=.github/data/ect_excluded_vars.txt PYCECT_TAG=3.3.1 PYCECT_COMMIT=b3c36a9d72ee211f396d1bc7078f6d5466916b0a diff --git a/.github/ect-cases.yml b/.github/ect-cases.yml new file mode 100644 index 0000000000..f38737434e --- /dev/null +++ b/.github/ect-cases.yml @@ -0,0 +1,35 @@ +# ECT cases: one table, read by .github/actions/resolve-ect-case. +# +# A case = one ECT configuration (test archive, run lengths, perturbation, and the names of its +# ensemble-summary and spin-up-restart release assets). Fields not set on a case come from `defaults`. Nothing here overrides +# ci-config.env: containers, compilers, MPI flags, PyCECT version and test-data release tags stay in ci-config.env. +# Workflows take an `ect-case` input (empty = `default`), resolve it once in their config job, and pass the resolved values on +# as explicit inputs/outputs. +# +# Fields +# description free text +# resolution test-archive name; must have RELEASE_TESTDATA_ in ci-config.env (the archive is .tar.gz) +# summary-file ensemble summary asset in the ECT release (ect-v{MAJOR.MINOR}); give each case its own name +# restart-file spin-up restart asset in the ECT release (stored gzipped as .gz) +# spinup-duration (QUOTE durations and magnitudes) unperturbed spin-up before the ensemble is perturbed (config_run_duration format) +# run-duration length of each (perturbed) member +# ensemble-size reference ensemble size (must be >= number of analysed variables; ~48 minimum) +# perturb-variable initial-condition variable perturbed (O(1e-14), needs double precision) +# perturb-magnitude +# excluded-vars file listing history variables dropped before PyCECT +default: mesoscale_reference + +defaults: + resolution: 120km + spinup-duration: "1_00:00:00" # quote durations: unquoted 1_00:00:00 would be read as a base-60 number + run-duration: "0_02:36:00" + ensemble-size: 200 + perturb-variable: theta + perturb-magnitude: "1e-14" + excluded-vars: .github/data/ect_excluded_vars.txt + +cases: + mesoscale_reference: + description: 120km global mesh, mesoscale_reference physics suite as shipped in the archive's namelist + summary-file: mpas_ect_summary_120km.nc + restart-file: 120km-spinup-restart.nc diff --git a/.github/scripts/resolve-ect-case.py b/.github/scripts/resolve-ect-case.py new file mode 100755 index 0000000000..654d2d2146 --- /dev/null +++ b/.github/scripts/resolve-ect-case.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Resolve one ECT case from .github/ect-cases.yml: defaults merged with the case, printed as key=value lines. + +usage: resolve-ect-case.py [--case NAME] [--table PATH] (empty/omitted case = the table's `default`) +Output keys: case, resolution, summary-file, restart-file, spinup-duration, run-duration, +ensemble-size, perturb-variable, perturb-magnitude, excluded-vars. +""" +import argparse, sys +from pathlib import Path +try: + import yaml +except ImportError: + sys.exit("::error::PyYAML is required (pip install pyyaml)") +ap = argparse.ArgumentParser() +ap.add_argument('--case', default='') +ap.add_argument('--table', default=str(Path(__file__).resolve().parent.parent / 'ect-cases.yml')) +a = ap.parse_args() +t = yaml.safe_load(open(a.table)) +cases = t.get('cases') or {} +name = a.case.strip() or t['default'] +if name not in cases: + sys.exit(f"::error::Unknown ECT case '{name}' in {a.table}. Available: {', '.join(cases)}") +r = dict(t.get('defaults') or {}); r.update(cases[name] or {}) +required = ['resolution', 'summary-file', 'restart-file', 'spinup-duration', 'run-duration', 'perturb-variable', + 'perturb-magnitude', 'excluded-vars'] +missing = [k for k in required if r.get(k) in (None, '')] +if missing: + sys.exit(f"::error::ECT case '{name}' is missing required field(s): {', '.join(missing)}") +for k in ['resolution', 'summary-file', 'restart-file', 'spinup-duration', 'run-duration', 'perturb-variable', + 'perturb-magnitude', 'excluded-vars']: + if k in r and not isinstance(r[k], str): + sys.exit(f"::error::ECT case '{name}': field {k} must be a quoted string in {a.table} (got {type(r[k]).__name__} {r[k]!r}; " + "YAML turns unquoted durations like 1_00:00:00 into numbers)") +out = {'case': name} +for k in ['resolution', 'summary-file', 'restart-file', 'spinup-duration', 'run-duration', 'ensemble-size', 'perturb-variable', + 'perturb-magnitude', 'excluded-vars']: + out[k] = str(r.get(k, '')).strip() +for k, v in out.items(): + if '\n' in v: + sys.exit(f"::error::ECT case field {k} must be a single line") + print(f"{k}={v}") diff --git a/.github/workflows/_test-compiler.yml b/.github/workflows/_test-compiler.yml index c6bc1bd700..a4f387e76e 100644 --- a/.github/workflows/_test-compiler.yml +++ b/.github/workflows/_test-compiler.yml @@ -13,6 +13,11 @@ permissions: on: workflow_call: inputs: + ect-case: + description: 'ECT case (a key under `cases` in .github/ect-cases.yml). Empty = the table default (mesoscale_reference).' + required: false + type: string + default: '' compiler: description: 'Compiler family (gcc, nvhpc, oneapi)' required: true @@ -39,6 +44,13 @@ jobs: outputs: image: ${{ steps.container.outputs.image }} ect-tag: ${{ steps.version.outputs.ect-tag }} + ect-resolution: ${{ steps.ectcase.outputs.resolution }} + ect-summary-file: ${{ steps.ectcase.outputs.summary-file }} + ect-restart-file: ${{ steps.ectcase.outputs.restart-file }} + ect-run-duration: ${{ steps.ectcase.outputs.run-duration }} + ect-excluded-vars: ${{ steps.ectcase.outputs.excluded-vars }} + ect-perturb-magnitude: ${{ steps.ectcase.outputs.perturb-magnitude }} + ect-case: ${{ steps.ectcase.outputs.case }} steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: @@ -54,6 +66,10 @@ jobs: mpi: ${{ inputs.mpi }} - uses: ./.github/actions/mpas-version id: version + - uses: ./.github/actions/resolve-ect-case + id: ectcase + with: + ect-case: ${{ inputs.ect-case }} build: needs: config @@ -127,7 +143,7 @@ jobs: if: steps.download.outcome == 'success' uses: ./.github/actions/download-testdata with: - resolution: 120km + resolution: ${{ needs.config.outputs.ect-resolution }} dest-dir: base-case - name: Restore cached restart @@ -136,7 +152,7 @@ jobs: uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 with: path: spinup-restart.nc - key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }} + key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }}-${{ needs.config.outputs.ect-case }} - name: Download spin-up restart if: steps.download.outcome == 'success' @@ -144,7 +160,7 @@ jobs: shell: bash run: | source .github/ci-config.env - RESTART="${ECT_RESTART_FILE}" + RESTART="${{ needs.config.outputs.ect-restart-file }}" RELEASE_TAG="${{ needs.config.outputs.ect-tag }}" if [ -f "spinup-restart.nc" ]; then @@ -174,13 +190,15 @@ jobs: if: steps.download.outcome == 'success' uses: ./.github/actions/run-perturb-mpas with: + excluded-vars: ${{ needs.config.outputs.ect-excluded-vars }} + perturb-magnitude: ${{ needs.config.outputs.ect-perturb-magnitude }} base-dir: base-case executable: ./atmosphere_model member-start: '${{ matrix.member }}' member-end: '${{ matrix.member }}' num-ranks: '4' mpi-impl: ${{ inputs.mpi }} - run-duration: '0_02:36:00' + run-duration: ${{ needs.config.outputs.ect-run-duration }} run-timeout: '45' restart-file: ${{ steps.restart.outputs.available == 'true' && steps.restart.outputs.file || '' }} @@ -224,6 +242,8 @@ jobs: history-dir: ect-test-files label: ${{ inputs.compiler }}/${{ inputs.mpi }}/smiol ect-tag: ${{ needs.config.outputs.ect-tag }} + summary-file: ${{ needs.config.outputs.ect-summary-file }} + ect-case: ${{ needs.config.outputs.ect-case }} dimensions: | compiler=${{ inputs.compiler }} mpi=${{ inputs.mpi }} diff --git a/.github/workflows/_test-gpu.yml b/.github/workflows/_test-gpu.yml index 4c44ffa426..c1eaefadbf 100644 --- a/.github/workflows/_test-gpu.yml +++ b/.github/workflows/_test-gpu.yml @@ -13,6 +13,11 @@ name: _test-gpu on: workflow_call: inputs: + ect-case: + description: 'ECT case (a key under `cases` in .github/ect-cases.yml). Empty = the table default (mesoscale_reference).' + required: false + type: string + default: '' mpi: description: 'MPI implementation (mpich, openmpi)' required: true @@ -43,6 +48,13 @@ jobs: image: ${{ steps.container.outputs.image }} image-gpu: ${{ steps.gpu-container.outputs.image }} ect-tag: ${{ steps.version.outputs.ect-tag }} + ect-resolution: ${{ steps.ectcase.outputs.resolution }} + ect-summary-file: ${{ steps.ectcase.outputs.summary-file }} + ect-restart-file: ${{ steps.ectcase.outputs.restart-file }} + ect-run-duration: ${{ steps.ectcase.outputs.run-duration }} + ect-excluded-vars: ${{ steps.ectcase.outputs.excluded-vars }} + ect-perturb-magnitude: ${{ steps.ectcase.outputs.perturb-magnitude }} + ect-case: ${{ steps.ectcase.outputs.case }} steps: - uses: actions/checkout@v5 with: @@ -63,6 +75,10 @@ jobs: gpu: cuda - uses: ./.github/actions/mpas-version id: version + - uses: ./.github/actions/resolve-ect-case + id: ectcase + with: + ect-case: ${{ inputs.ect-case }} build: needs: config @@ -148,7 +164,7 @@ jobs: if: steps.download.outcome == 'success' uses: ./.github/actions/download-testdata with: - resolution: 120km + resolution: ${{ needs.config.outputs.ect-resolution }} dest-dir: base-case - name: Restore cached restart @@ -157,7 +173,7 @@ jobs: uses: actions/cache/restore@v5 with: path: spinup-restart.nc - key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }} + key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }}-${{ needs.config.outputs.ect-case }} - name: Download spin-up restart if: steps.download.outcome == 'success' @@ -165,7 +181,7 @@ jobs: shell: bash run: | source .github/ci-config.env - RESTART="${ECT_RESTART_FILE}" + RESTART="${{ needs.config.outputs.ect-restart-file }}" RELEASE_TAG="${{ needs.config.outputs.ect-tag }}" if [ -f "spinup-restart.nc" ]; then @@ -195,13 +211,15 @@ jobs: if: steps.download.outcome == 'success' uses: ./.github/actions/run-perturb-mpas with: + excluded-vars: ${{ needs.config.outputs.ect-excluded-vars }} + perturb-magnitude: ${{ needs.config.outputs.ect-perturb-magnitude }} base-dir: base-case executable: ./atmosphere_model member-start: '${{ matrix.member }}' member-end: '${{ matrix.member }}' num-ranks: '4' mpi-impl: ${{ inputs.mpi }} - run-duration: '0_02:36:00' + run-duration: ${{ needs.config.outputs.ect-run-duration }} run-timeout: '45' restart-file: ${{ steps.restart.outputs.available == 'true' && steps.restart.outputs.file || '' }} gpu-aware-mpi: ${{ inputs.gpu-aware-mpi }} @@ -245,6 +263,8 @@ jobs: history-dir: ect-test-files label: nvhpc/${{ inputs.mpi }}/cuda/smiol ect-tag: ${{ needs.config.outputs.ect-tag }} + summary-file: ${{ needs.config.outputs.ect-summary-file }} + ect-case: ${{ needs.config.outputs.ect-case }} dimensions: | compiler=nvhpc mpi=${{ inputs.mpi }} diff --git a/.github/workflows/ect-ensemble-gen.yml b/.github/workflows/ect-ensemble-gen.yml index 8a1dd1f9fd..0d40647380 100644 --- a/.github/workflows/ect-ensemble-gen.yml +++ b/.github/workflows/ect-ensemble-gen.yml @@ -12,6 +12,11 @@ name: ECT Ensemble Generation on: workflow_dispatch: inputs: + ect-case: + description: 'ECT case (a key under `cases` in .github/ect-cases.yml). Empty = the table default (mesoscale_reference).' + required: false + type: string + default: '' mpas-repository: description: 'MPAS source repo (e.g. MPAS-Dev/MPAS-Model). Leave empty to use this repo.' required: false @@ -21,10 +26,10 @@ on: required: false default: '' ensemble-size: - description: 'Number of ensemble members to generate' - type: number + description: "Number of ensemble members to generate. Empty = the case's ensemble-size from .github/ect-cases.yml." + type: string required: false - default: 200 + default: '' members-per-job: description: 'Number of ensemble members to run per job (batch size)' type: number @@ -58,11 +63,25 @@ jobs: batches: ${{ steps.compute.outputs.batches }} ensemble-size: ${{ steps.compute.outputs.ensemble-size }} container-image: ${{ steps.container.outputs.image }} + ect-case: ${{ steps.ectcase.outputs.case }} + ect-resolution: ${{ steps.ectcase.outputs.resolution }} + ect-summary-file: ${{ steps.ectcase.outputs.summary-file }} + ect-restart-file: ${{ steps.ectcase.outputs.restart-file }} + ect-spinup-duration: ${{ steps.ectcase.outputs.spinup-duration }} + ect-run-duration: ${{ steps.ectcase.outputs.run-duration }} + ect-perturb-variable: ${{ steps.ectcase.outputs.perturb-variable }} + ect-perturb-magnitude: ${{ steps.ectcase.outputs.perturb-magnitude }} + ect-excluded-vars: ${{ steps.ectcase.outputs.excluded-vars }} steps: - uses: actions/checkout@v5 with: sparse-checkout: .github + - uses: ./.github/actions/resolve-ect-case + id: ectcase + with: + ect-case: ${{ inputs.ect-case }} + - uses: ./.github/actions/resolve-container id: container with: @@ -72,7 +91,7 @@ jobs: - name: Compute batch ranges id: compute run: | - ESIZE=${{ inputs.ensemble-size || 200 }} + ESIZE=${{ inputs.ensemble-size || steps.ectcase.outputs.ensemble-size }} BATCH=${{ inputs.members-per-job || 10 }} echo "ensemble-size=${ESIZE}" >> $GITHUB_OUTPUT @@ -172,7 +191,7 @@ jobs: uses: actions/cache/restore@v5 with: path: spinup-restart.nc - key: ect-spinup-restart-${{ needs.build.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }} + key: ect-spinup-restart-${{ needs.build.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }}-${{ needs.prepare.outputs.ect-case }} - name: Download executable if: steps.cache.outputs.cache-hit != 'true' @@ -189,21 +208,21 @@ jobs: uses: ./.github/actions/run-mpas with: executable: ./atmosphere_model - resolution: 120km + resolution: ${{ needs.prepare.outputs.ect-resolution }} num-procs: '4' mpi-impl: ${{ inputs.mpi-impl || 'mpich' }} - run-duration: '1_00:00:00' - restart-interval: '1_00:00:00' + run-duration: ${{ needs.prepare.outputs.ect-spinup-duration }} + restart-interval: ${{ needs.prepare.outputs.ect-spinup-duration }} run-timeout: '350' strict-exit-check: 'false' - name: Verify restart file if: steps.cache.outputs.cache-hit != 'true' run: | - RESTART=$(ls run-120km/restart.*.nc 2>/dev/null | head -1) + RESTART=$(ls run-${{ needs.prepare.outputs.ect-resolution }}/restart.*.nc 2>/dev/null | head -1) if [ -z "${RESTART}" ]; then echo "::error::Spin-up did not produce a restart file" - ls -la run-120km/*.nc 2>/dev/null || true + ls -la run-${{ needs.prepare.outputs.ect-resolution }}/*.nc 2>/dev/null || true exit 1 fi echo "Spin-up restart: ${RESTART} ($(du -h ${RESTART} | cut -f1))" @@ -214,7 +233,7 @@ jobs: uses: actions/cache/save@v5 with: path: spinup-restart.nc - key: ect-spinup-restart-${{ needs.build.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }} + key: ect-spinup-restart-${{ needs.build.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }}-${{ needs.prepare.outputs.ect-case }} - name: Upload restart artifact uses: actions/upload-artifact@v6 @@ -252,7 +271,7 @@ jobs: - name: Download test case uses: ./.github/actions/download-testdata with: - resolution: 120km + resolution: ${{ needs.prepare.outputs.ect-resolution }} dest-dir: base-case - name: Download spin-up restart @@ -263,13 +282,15 @@ jobs: - name: Run ensemble members ${{ matrix.batch.start }}-${{ matrix.batch.end }} uses: ./.github/actions/run-perturb-mpas with: + excluded-vars: ${{ needs.prepare.outputs.ect-excluded-vars }} + perturb-magnitude: ${{ needs.prepare.outputs.ect-perturb-magnitude }} base-dir: base-case executable: ./atmosphere_model member-start: ${{ matrix.batch.start }} member-end: ${{ matrix.batch.end }} num-ranks: ${{ inputs.num-ranks || 4 }} mpi-impl: ${{ inputs.mpi-impl || 'mpich' }} - run-duration: '0_02:36:00' + run-duration: ${{ needs.prepare.outputs.ect-run-duration }} run-timeout: '45' restart-file: spinup-restart.nc @@ -320,9 +341,12 @@ jobs: - name: Collect metadata id: meta run: | - source .github/ci-config.env + ECT_SUMMARY_FILE="${{ needs.prepare.outputs.ect-summary-file }}" + ECT_RESOLUTION="${{ needs.prepare.outputs.ect-resolution }}" + ECT_PERTURB_MAGNITUDE="${{ needs.prepare.outputs.ect-perturb-magnitude }}" + ECT_PERTURB_VARIABLE="${{ needs.prepare.outputs.ect-perturb-variable }}" + RUN_DURATION="${{ needs.prepare.outputs.ect-run-duration }}" ECT_SUMMARY_PREFIX="${ECT_SUMMARY_FILE%.nc}" - RUN_DURATION=0_02:36:00 VERSION="${{ needs.build.outputs.mpas-version }}" DATE=$(date -u +%Y-%m-%d) SUMMARY_NAME="${ECT_SUMMARY_PREFIX}_v${VERSION}_${DATE//-/}.nc" @@ -470,7 +494,7 @@ jobs: # Auto-creates the release (tagged by version series) if it doesn't exist. #=========================================================================== publish-restart: - needs: [build, spinup] + needs: [prepare, build, spinup] name: Publish Restart runs-on: ubuntu-latest permissions: @@ -504,7 +528,7 @@ jobs: --notes "ECT ensemble summary and spin-up restart shared by all MPAS v${SERIES}.x releases. Generated from v${VERSION}. Updated automatically by the ect-ensemble-gen workflow." fi - RESTART_NAME="${ECT_RESTART_FILE}" + RESTART_NAME="${{ needs.prepare.outputs.ect-restart-file }}" mv spinup-restart.nc "${RESTART_NAME}" echo "Compressing ${RESTART_NAME} ($(du -h "${RESTART_NAME}" | cut -f1))..." @@ -522,7 +546,7 @@ jobs: # Auto-creates the release (tagged by version series) if it doesn't exist. #=========================================================================== publish-summary: - needs: [build, generate-summary] + needs: [prepare, build, generate-summary] if: needs.generate-summary.result == 'success' name: Publish Summary runs-on: ubuntu-latest diff --git a/.github/workflows/ect-test.yml b/.github/workflows/ect-test.yml index 7dfbf69baf..0f941fe83b 100644 --- a/.github/workflows/ect-test.yml +++ b/.github/workflows/ect-test.yml @@ -15,6 +15,11 @@ on: branches: [master] workflow_dispatch: inputs: + ect-case: + description: 'ECT case (a key under `cases` in .github/ect-cases.yml). Empty = the table default (mesoscale_reference).' + required: false + type: string + default: '' mpas-repository: description: 'MPAS source repo (e.g. MPAS-Dev/MPAS-Model). Leave empty to use this repo.' required: false @@ -34,6 +39,13 @@ jobs: outputs: image: ${{ steps.container.outputs.image }} ect-tag: ${{ steps.version.outputs.ect-tag }} + ect-resolution: ${{ steps.ectcase.outputs.resolution }} + ect-summary-file: ${{ steps.ectcase.outputs.summary-file }} + ect-restart-file: ${{ steps.ectcase.outputs.restart-file }} + ect-run-duration: ${{ steps.ectcase.outputs.run-duration }} + ect-excluded-vars: ${{ steps.ectcase.outputs.excluded-vars }} + ect-perturb-magnitude: ${{ steps.ectcase.outputs.perturb-magnitude }} + ect-case: ${{ steps.ectcase.outputs.case }} steps: - uses: actions/checkout@v5 with: @@ -48,6 +60,10 @@ jobs: mpi: openmpi - uses: ./.github/actions/mpas-version id: version + - uses: ./.github/actions/resolve-ect-case + id: ectcase + with: + ect-case: ${{ inputs.ect-case }} #=========================================================================== # BUILD @@ -116,7 +132,7 @@ jobs: - name: Download test case uses: ./.github/actions/download-testdata with: - resolution: 120km + resolution: ${{ needs.config.outputs.ect-resolution }} dest-dir: base-case - name: Restore cached restart @@ -124,14 +140,14 @@ jobs: uses: actions/cache/restore@v5 with: path: spinup-restart.nc - key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }} + key: ect-spinup-restart-${{ needs.config.outputs.ect-tag }}-${{ hashFiles('.github/ci-config.env') }}-${{ needs.config.outputs.ect-case }} - name: Download spin-up restart id: restart shell: bash run: | source .github/ci-config.env - RESTART="${ECT_RESTART_FILE}" + RESTART="${{ needs.config.outputs.ect-restart-file }}" RELEASE_TAG="${{ needs.config.outputs.ect-tag }}" if [ -f "spinup-restart.nc" ]; then @@ -159,11 +175,13 @@ jobs: - name: Run perturbed MPAS-A uses: ./.github/actions/run-perturb-mpas with: + excluded-vars: ${{ needs.config.outputs.ect-excluded-vars }} + perturb-magnitude: ${{ needs.config.outputs.ect-perturb-magnitude }} base-dir: base-case executable: ./atmosphere_model member-start: ${{ matrix.member }} member-end: ${{ matrix.member }} - run-duration: '0_02:36:00' + run-duration: ${{ needs.config.outputs.ect-run-duration }} run-timeout: '45' restart-file: ${{ steps.restart.outputs.available == 'true' && steps.restart.outputs.file || '' }} @@ -214,6 +232,8 @@ jobs: history-dir: ect-test-files label: gcc/openmpi/ect-standalone ect-tag: ${{ needs.config.outputs.ect-tag }} + summary-file: ${{ needs.config.outputs.ect-summary-file }} + ect-case: ${{ needs.config.outputs.ect-case }} #=========================================================================== # CLEANUP