The build sequence for the ECCO skills, from the proven environment foundation through
the physical-oceanography calculations. This is the plan; design.md is the detailed
source of truth for how each skill works. Keep both in sync as skills get built.
Sequencing rule: each phase is usable on its own and de-risks the next. We don't build higher-level physics until the Level 0–1 primitives are proven on real data.
Build pattern (every calculation skill): ships as SKILL.md (guidance +
teach-as-you-go) + vetted scripts/ + references/ (closure/acceptance snippets). A
skill isn't "done" until it clears the docs/verify.md V&V protocol (official-helper
match, tutorial reproduction, conservation, teeth-verified tests, and a standing
adversarial-review pass) — reproducing "seems reasonable" is not enough; independent
evidence is required. Skills invoke .venv/bin/python directly (see design.md →
Interpreter policy).
| Skill | Status | Purpose |
|---|---|---|
ecco-setup |
✅ done | Survey Python → build .venv → install stack → auto-run verify |
ecco-setup — verify mode |
✅ done | Prove the environment works (imports + real get_llc_grid smoke test). Consolidated into ecco-setup 2026-08-06 (was a separate ecco-setup-verify skill); still runnable standalone. |
Get real data flowing and prove the whole skill pattern end-to-end with the simplest calculation.
| Skill | Level | Status | Notes |
|---|---|---|---|
ecco-common (shared lib) |
— | ✅ done | load_grid(), load_field(), CMR download, ./data/ecco cache; the Option A composition backbone |
load-grid |
0 | ✅ done | Downloads geometry via CMR direct URL, returns grid dataset + get_llc_grid object; cache-hit verified |
load-field |
0 | ✅ done | Downloads any ECCO field by ShortName + months=; size guard + month-edge fix verified |
compute-ocean-heat-content |
4 | ✅ done | Recipe 1 — volume-weighted OHC + change between months; validation trail; acceptance recorded |
Phase 1 complete. The load skills + Recipe 1 are built and tested end-to-end. Recipe
1 established the calc-skill pattern (SKILL.md + shared-helper composition +
validation trail + references/acceptance.md) that every later skill copies. Acceptance
highlights: volume-mean THETA 3.59 degC (matches known ocean value), ocean volume within
0.4% of literature, Jan 2000→2010 warming +7.8×10²² J.
Round 3 additions: (a) size-guard bug — granule size now read from the archive
entry matching the .nc filename (preferring SizeInBytes), not the "first MB entry"
which could be the tiny .sha512 sidecar and defeat the guard; checksum captured too.
(b) Whole-request guard — size is checked once across ALL requested months/days
(per-key checking let a 40-day/1.2 GB request slip through 29 MB at a time — caught by
testing, now fixed). (c) days= selector — daily granules overlap at midnight
like monthly at month-edges; days=['YYYY-MM-DD'] queries mid-day + filename-filters.
(d) Daily offline backfill — the index key regex only matched monthly YYYY-MM;
now handles daily YYYY-MM-DD (self-caught during testing). (e) transport recipe:
reduce each face on its own dims before adding (avoids xarray outer-broadcast); Recipe 4
summary lists both X and Y flux names. (f) curl pseudocode branch fixed (tracer-point
inputs skip interp cleanly). (g) OHC SKILL.md L2 relabeled "numeric sanity."
Automated test suite added (eval-round-3 #4): .claude/skills/ecco-common/tests/
has 13 offline regression tests for the loader/cache/access layer (size-by-filename,
pagination, whole-request guard, selectors, backfill, offline reuse, live resolution);
run_all_tests.py runs all 23 tests (13 + 10 OHC). Verified it catches a reintroduced
size-guard regression.
Still open: wire tests into CI; exact lockfile; cached-file checksum verification (captured, not yet checked); Recipe 2 acceptance contract; index locking for concurrent runs.
Two review rounds by an external model. Round-2 additions (verified against the
tutorials): tracer-section transport now combines both X- and Y-face masks
(ADVx+DFx·maskW + ADVy+DFy·maskS, per the MHT tutorial); curl now has the second
derivative-vector rotation; daily collections + live CMR ShortName resolution;
load_field selector validation; cache-index backfill + atomic writes; OHC
time-coordinate check; corrected the geos_vel_compute reference (it's in
ecco_po_tutorials.py, not ecco_v4_py) and confirmed plot_proj_to_latlon_grid exists.
Round-1 issues, now fixed:
- Geostrophic formula omitted density (
design.mdhadv_g=(1/f)·∂Φ/∂x; correct isdp=rhoConst·∂Φ/∂x, then/(ρ·f)withρ=rhoConst+RHOAnoma). Fixed incompute-geostrophic-velocity, Recipe 2, and gotcha #1. Recipe 2 now listsRHOAnoma. - Section transport double-counted
hFac—*VELMASSis already mass-weighted; matches officialcalc_section_vol_trsp(UVELMASS·drF·dyG, nohFac, no bolus for volume). Fixed incompute-transport-across-section, Recipe 4, gotcha #6. - Curl too naive for LLC — first pass added one rotation; eval #2 caught that a SECOND rotation (of the derivative vectors) was still missing. Now fully specified (rotate components → diff both axes → interp → rotate derivatives → combine).
- CWD dependence — project root now derived from file location, not
os.getcwd(). - Offline cache + CMR pagination — added a cache index (offline reuse) and
CMR-Search-Afterpaging (no silent 200-granule truncation). - OHC validation NaN gap — L1 now catches wet-cell NaNs; L2 is a real assertion.
- Doc drift —
--reset(built),load-field(no S3), tutorial count (four), Python-3.12 cap reframed as tested policy.
Still open (correctly gated): budget term lists / residual definitions (Phil), an exact lockfile + CI golden-value regression, Linux/Windows testing.
Phase 2 — Grid operation primitives (Level 1) (✅ EXTRACTION PASS DONE 2026-08-05 — partial, evidence-driven)
The reusable C-grid mechanics the physics calcs need. These are ecco_common library
functions, NOT standalone skills (same category as load_grid/plots) — they live in
the new module ecco_common/grid_ops.py, imported by the calc skills.
Decision (2026-07-25): DEFER standalone extraction up front — the logic was inlined and Rung-1-verified inside the calc skills; extracting against a single caller bakes in the wrong interface. Executed as a partial pass 2026-08-05 once the Phil-free calc set was complete (OHC, geostrophy, thermal wind, curl, steric). Key principle held: extract only primitives with ≥2 real callers, verified by auditing the actual code — not the count this roadmap originally guessed.
Primitive (in ecco_common.grid_ops) |
Real callers | Status |
|---|---|---|
OMEGA + coriolis(ds_grid) (was compute-coriolis) |
3 — geostrophy, thermal-wind, curl | ✅ EXTRACTED |
canon(da) (dim-order normalize after xgcm ops) |
3 — thermal-wind, curl, steric | ✅ EXTRACTED |
grad_to_center(scalar, …) (was spatial-difference + spatial-interpolation, fused: diff/dxC + interp_2d_vector) |
2 — geostrophy, thermal-wind | ✅ EXTRACTED |
rotate-to-geographic (CS/SN) |
1 — curl only | ⏸️ NOT extracted (1 caller) |
vertical-difference (∂/∂k ÷ drC) |
1 — thermal-wind only | ⏸️ NOT extracted (1 caller) |
volume/area weighting (rA·drF·hFacC, area-weighted mean) |
2 — OHC, steric | ⏸️ NOT extracted (deferred; related-but-not-identical) |
Correction to the earlier framing: this roadmap and design.md had listed five Level-1
primitives as if all were due. The real caller audit showed only three have ≥2 callers.
rotate-to-geographic and vertical-difference each have exactly one caller — so
extracting them now would violate our own "shape against several callers" rule. They stay
inlined until a genuine 2nd caller appears (that build is the natural trigger). Note curl's
rotation is bit-identical to the official ecco_v4_py.vector_calc.UEVNfromUXVY — when a 2nd
rotation caller arrives, adopting the official helper is likely better than extracting
our own.
Neutrality proven: all four refactored skills (geostrophy, thermal-wind, curl, steric)
re-ran their full Rung-1/cross-check suites after the refactor with identical numbers
(geostrophy still matches geos_vel_compute <1e-9; thermal-wind identity corr 0.9992; curl
rotation max|Δ|=0 vs UEVNfromUXVY; steric sum-of-parts 0.0046 m). grad_to_center returns
native (un-canon'd) dim order on purpose so geostrophy's reference match stays exact;
callers needing positional numpy call canon() themselves. OHC was NOT refactored (it uses
none of the three). New offline test: ecco-common/tests/test_grid_ops.py.
Reminder: ECCO tutorials use the xgcm boundary=/fill_value= API and
ecco.get_llc_grid() — correct for our pinned xgcm 0.9.0 (do not use the 0.10
padding= API). See design.md gotcha #9.
| Skill | Level | Status |
|---|---|---|
compute-geostrophic-balance (Recipe 2) |
4 | ✅ BUILT 2026-07-25 (Rung-1 verified) |
plot-ecco-field — one skill, --mode tile|alltiles|global; consolidates the originally-planned plot-single-tile / plot-masked-2d / plot-global-map |
6 (visualization) | ✅ BUILT 2026-07-25 |
volume-weight, area-weight, apply-mask, global-sum, global-mean |
2 | implicit in calc skills so far; not yet standalone building blocks |
Recipe 2 status: compute-geostrophic-balance is built and Rung-1 verified — its
u_g/v_g match the official ecco_po_tutorials.geos_vel_compute to <1e-9 m/s over 2.24M
points (first skill to satisfy Rung 1). Computed in model coordinates; equator masked.
✅ Rung-7 adversarial pass cleared (2026-07-25) — geostrophy is DONE. Note we built the
calculation directly against the official helper rather than first extracting the separate
Level-1 primitives (spatial-difference, etc.) — those were later refactored into
ecco_common/grid_ops.py (2026-08-05). The geostrophy-vs-model-velocity comparison skill
remains to build.
The higher-value science Phil asked about.
| Skill | Level |
|---|---|
find-indices-along-latitude, extract-transect-data, make-section-mask |
3 |
compute-transport-across-section + Recipe 4 |
4 (bolus + face-area care) |
compute-tracer-budget (volume/heat/salt budget) |
4 (closure oracle — the crown jewel) |
decompose-flux (Recipe 5) |
4 |
| Skill | Level | Status |
|---|---|---|
compute-curl + Recipe 6 (Ekman pumping) |
4–5 | ✅ DONE 2026-08-06 (curl + Ekman pumping; Rung-7 adversarial pass clean — docs/eval5.md; both historical rotation bugs confirmed blocked) |
compute-thermal-wind + reconstruct-velocity-from-thermal-wind (Recipe 3) |
4 | ✅ DONE 2026-08-06 (one skill; Rung-7 adversarial pass clean — docs/eval4.md) |
compute-steric-height (Recipe 3-steric) |
4 | ✅ DONE 2026-08-06 (steric + thermo/halo split; Rung-7 adversarial pass clean — docs/eval6.md) |
compute-ekman-transport (Q6 — transport M=τ/(ρf) vs upper-ocean velocity) |
4–5 | ⏸️ DEFERRED — gated on Phil Q5 (see below) |
compute-normalized-difference |
5 | designed |
🎯 The Phil-free calculation set is COMPLETE and fully verified (OHC, geostrophy, thermal
wind, curl+Ekman pumping, steric height — all five ✅ DONE, all cleared Rung-7 adversarial
review; evals 4–6 on 2026-08-06 found zero confirmed errors, one test-hardening fix each).
The Level-1 primitive-extraction pass this unlocked has also been done (partial — see
Phase 2). Everything remaining is either Phil-gated (transports, budgets, decompose-flux,
and compute-ekman-transport — see below) or a small add (compute-normalized-difference).
compute-ekman-transport DEFERRED (2026-08-05, gated on Phil Q5). Unlike every other
Phil-free skill, Ekman transport has no ECCO tutorial (only 4 Intro-to-PO notebooks
exist: geostrophic, thermal wind, steric ×2) — so no Rung-1 helper and no Rung-2 number. The
formula M = (τ_y/ρf, −τ_x/ρf) is trivial; the entire scientific value is the comparison to
the model, which is precisely Phil's Q6 tangled with his scientific-fit question. Rather than
guess the comparison (ageostrophic residual vs raw upper-ocean velocity vs …) and ship a
skill whose verification bottoms out below the bar the other five cleared, we deferred it and
added Q5 to docs/questions-for-phil.md asking which comparison + what "upper ocean"
depth he wants. His answer unblocks it immediately; the build itself is small (reuses curl's
stress handling + geostrophy's velocities).
Steric-height status (built 2026-08-05): steric height anomaly (∫−V'_sp/g dp to 2000 dbar)
- thermosteric/halosteric decomposition. Vendored the MITgcm JMD95 EOS (
ecco-common/ vendor/jmd95.py, pinned 3f0fcca) since no EOS was available (gsw/TEOS-10 absent, none inecco_v4_py); base term uses the model's own RHOAnoma. No steric helper (Rung-1 N/A; EOS check-value anchor instead). Verified: sum-of-parts (thermo+halo ≈ full) median 0.005 m / corr 0.9998; steric ≈ SSH corr 0.921 (independent, different collection). Teeth: specvol sign flip → steric-vs-SSH corr −0.92. Fixed a de-meaning/masking bug found during the build. ✅ Rung-7 adversarial pass clean (2026-08-06,docs/eval6.md) — one caveat fixed (added a thermo/halo label-swap guard vs SST, which sum-of-parts couldn't catch). Steric is DONE.
Recipe 6 status (built 2026-08-05): compute-curl does wind-stress curl (the LLC
two-rotation sequence) + Ekman pumping vs the model's actual WVEL. No official curl
helper exists (Rung-1 N/A); verified by matching the official rotation helper
vector_calc.UEVNfromUXVY bit-for-bit and by w_E vs WVEL (corr 0.74, sign-agree 0.89
at ~30 m). Teeth: dropping the 2nd rotation shifts the curl ~30%; sign flip drops the WVEL
corr to −0.56. Fixed two design-doc errors during the build: oceTAUX/oceTAUY are on the
U/V faces (not tracer points, as the doc claimed) so they're interpolated first; and the
two rotations use the same formula (the "differing sign conventions" note was wrong).
✅ Rung-7 adversarial pass clean (2026-08-06, docs/eval5.md) — both historical rotation
bugs confirmed blocked; one loose-teeth-threshold caveat fixed (0.05→0.20). Curl is DONE.
compute-ekman-transport (Q6) is a separate skill, now DEFERRED pending Phil Q5 (see the
deferral note above).
Recipe 3 status (built ahead of the rest of Phase 5, as the safe next step):
compute-thermal-wind covers both the shear and the velocity reconstruction. No official
helper exists (Rung 1 N/A — only geos_vel_compute is in ecco_po_tutorials.py), so it's
verified by three cross-checks: an analytic identity vs ∂/∂z of geostrophic velocity (corr
0.999), an independent comparison vs the model's actual velocity shear (corr 0.64/0.85), and
the tutorial's reconstruction-vs-actual normalized-diff diagnostic (~0.23). Teeth-verified.
Level-1 primitives still inlined — thermal wind reused geostrophy's diff/interp pattern
by re-implementing it (not by extracting shared primitives); extraction still deferred to the
next caller (curl).
-
Phase 4 is the value peak but also the most gated. It's what Phil actually asked for (Atlantic transports, budgets), but it can't be finalized without his three answers. Worth a nudge to Phil before we get there.
-
Phases 2–3 could compress. If we want science faster, we could build only the Level 1 primitives that Recipe 2 specifically needs, rather than all of Level 1–2 upfront. Trade-off: less reuse groundwork, but a faster path to the geostrophic- balance demo.
-
Cross-platform / lockfile debt carried from the environment phase: test setup on Linux/Windows and on a machine lacking a supported Python; freeze an exact
pip freezelockfile. Not blocking Phase 2, but shouldn't be forgotten. -
Carried TODOs (non-blocking):
load-field: friendlier UX when~/.netrcEarthdata credentials are missing (currently surfaces a 401 with a pointer, could guide account/.netrcsetup).- Recipe 1: seasonal-cycle averaging for a rigorous OHC trend (snapshot month-to-month differences are noisier).
- ✅ Git repo now set up — public at https://github.com/podaac/ecco-skills
(remote
origin, branchmain)..gitignore(/data/,/.venv/,/plots/) is live;data/,.venv/,plots/confirmed untracked. Remaining: add a README before it's useful to outside visitors; wire the test suite into CI.
-
Every calc skill ships a
scripts/test_validation.py(negative + positive guard tests), per the pattern Recipe 1 established. A guard that only ever passes could be silently broken; the tests prove it fires on bad input too. -
Keep skills as composable BUILDING BLOCKS for now (decided 2026-07-25). Each skill does one thing (load / compute / plot) with a clean CLI + importable helper. We are deferring higher-level composition skills (e.g. "plot two fields side by side", "geostrophy-vs-model-velocity diagnostic") until the building-block set is broad enough to know which compositions are actually worth wrapping. Rationale: less surface to verify/maintain, and premature convenience wrappers tend to bake in the wrong shape. Novel one-off combinations can be done as thin glue over the skill helpers in the meantime (like the scratch
make_gallery.py/plot_ssh_vs_geos.pyat repo root — those are demos, NOT skills). Revisit addingplot_pair()/ comparison modes later.