Skip to content

Commit 9797c39

Browse files
Add ensemble monitoring dashboard and member surface field plotter
- ensemble_dashboard.py: live dashboard for bred vector ensemble with multi-member STDOUT monitoring (selectable members), convergence table showing per-variable RMS across breeding cycles, surface field viewer per member with variable selector and time slider - plot_ensemble_surface_fields.sh: SLURM array job (1-50) to generate SST/SSS/SSH/KE plots for each member in parallel - breed_vectors.py: rescale step now writes convergence.json log with per-member per-cycle diagnostics for the dashboard to consume Dashboard serves on port 8051 (separate from control dashboard on 8050), deployable on GCP login node via tailscale serve. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4e5899c commit 9797c39

3 files changed

Lines changed: 691 additions & 2 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
#SBATCH --array=1-50
3+
#SBATCH -n1
4+
#SBATCH -c4
5+
#SBATCH --time=12:00:00
6+
#SBATCH --job-name=spectre_ens_plot
7+
#SBATCH --output=ens_plot_%A_%a.out
8+
#SBATCH --error=ens_plot_%A_%a.out
9+
10+
# Each array task plots surface fields for one ensemble member.
11+
# SLURM_ARRAY_TASK_ID = member number (1-50)
12+
13+
MEMBER_ID=$(printf "%03d" $SLURM_ARRAY_TASK_ID)
14+
15+
if [ -n "${SLURM_JOB_ID:-}" ]; then
16+
SCRIPT_PATH=$(scontrol show job "$SLURM_JOB_ID" --json | jq -r '.jobs[0].command')
17+
SCRIPT_DIR=$(dirname "$(readlink -f "$SCRIPT_PATH")")
18+
SIMULATION_DIR=$(dirname $SCRIPT_DIR)
19+
else
20+
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
21+
SIMULATION_DIR=$(dirname $SCRIPT_DIR)
22+
fi
23+
24+
source $SCRIPT_DIR/env.sh
25+
26+
MEMBER_RUN_DIR="${SIMULATION_DIR}/ensemble/member_${MEMBER_ID}/run"
27+
28+
echo "Plotting surface fields for member ${MEMBER_ID}"
29+
echo "Run dir: ${MEMBER_RUN_DIR}"
30+
31+
srun --container-image=$SPECTRE_UTILS_IMG \
32+
--container-mounts=${HOME}:${HOME},${SIMULATION_DIR}:/workspace,${HOST_DATADIR}:/data \
33+
python /opt/spectre_utils/plot_surface_fields.py \
34+
/workspace/ensemble/member_${MEMBER_ID}/run \
35+
--plots-dir /workspace/ensemble/member_${MEMBER_ID}/run/plots \
36+
--poll 120 \
37+
--start-date 2002-07-01 \
38+
--dt 360.0

spectre_utils/breed_vectors.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import os
1818
import sys
1919
import re
20+
import json
2021
import argparse
2122
import yaml
2223
import numpy as np
@@ -311,7 +312,8 @@ def cmd_rescale(config, config_path, cycle):
311312
print(f"Control pickup (end of cycle {cycle}): {ctrl_pickup}")
312313
control_fields, meta = read_pickup(ctrl_pickup, Nx, Ny, Nr)
313314

314-
# Process each member
315+
# Process each member and collect diagnostics for convergence log
316+
cycle_diags = []
315317
for m in range(1, n_members + 1):
316318
member_dir = os.path.join(ensemble_dir, f"{paths['member_prefix']}_{m:03d}")
317319

@@ -336,13 +338,32 @@ def cmd_rescale(config, config_path, cycle):
336338
with open(os.path.join(member_dir, "nIter0.txt"), "w") as f:
337339
f.write(str(end_iter))
338340

341+
diag["member"] = m
342+
cycle_diags.append(diag)
343+
339344
print(f" Member {m:03d}: rescale={diag['rescale_factor']:.3f}, "
340345
f"T_rms={diag.get('Theta_rms', 0):.4f}°C, "
341346
f"S_rms={diag.get('Salt_rms', 0):.4f}, "
342347
f"U_rms={diag.get('Uvel_rms', 0):.4f} m/s, "
343348
f"Eta_rms={diag.get('EtaN_rms', 0):.4f} m")
344349

345-
print(f"\nCycle {cycle} rescaling complete")
350+
# Write convergence log (append per cycle)
351+
convergence_path = os.path.join(ensemble_dir, "convergence.json")
352+
if os.path.exists(convergence_path):
353+
with open(convergence_path, "r") as f:
354+
convergence = json.load(f)
355+
else:
356+
convergence = {"cycles": []}
357+
358+
convergence["cycles"].append({
359+
"cycle": cycle,
360+
"iteration": end_iter,
361+
"members": cycle_diags,
362+
})
363+
with open(convergence_path, "w") as f:
364+
json.dump(convergence, f, indent=2)
365+
366+
print(f"\nCycle {cycle} rescaling complete — convergence log: {convergence_path}")
346367

347368

348369
def cmd_status(config, config_path, cycle):

0 commit comments

Comments
 (0)