Skip to content

WIP: Merge of GZ-COBALT and COBALT-DVM into eco-COBALT - #1

Open
jessluo wants to merge 11 commits into
dev/eco-cobaltfrom
feature/gz-cobalt-merge
Open

jessluo wants to merge 11 commits into
dev/eco-cobaltfrom
feature/gz-cobalt-merge

Conversation

@jessluo

@jessluo jessluo commented Jul 31, 2026

Copy link
Copy Markdown
Owner

[WIP] Merge GZ-COBALT into eco-cobalt: gelatinous zooplankton with diel vertical migration

Merges the two generations of COBALT development into a single "ecologically heavy" model: COBALTv3 + diel vertical migration (COBALT-DVM, now in dev/eco-cobalt) gains the two tunicate groups, food-dependent assimilation efficiency, and fast-sinking iron detritus from GZ-COBALT, with the large tunicates both migrating vertically and producing fast-sinking fecal pellets. This work was done with assistance from Claude Opus 5.

Base: dev/eco-cobalt · Diff: +2,868 / −1,230 across 4 files · 4 commits

dev/eco-cobalt is currently 1 commit ahead (15e766d, the jmetabo_n diagnostic fix) — see Follow-ups.


Background

Two lines of COBALT development needed reconciling:

  • dev/eco-cobalt — COBALTv3 (4 phytoplankton, 3 zooplankton, fast-sinking N and P detritus) plus Mathieu Poupon's diel vertical migration work, which added two vertically migrating zooplankton groups (vmmdz, vmlgz) carrying gut and metabolite pools.
  • GZ-COBALT — still using COBALTv2. Adds small tunicates (appendicularians) and large tunicates (salps), a food-dependent assimilation efficiency, and fast-sinking iron detritus.

The merge target is 7 zooplankton groups in which large tunicates migrate and short-circuit the biological pump via dense fecal pellets and salp falls.


Design decisions

Decision Choice Rationale
Zooplankton IDs Append-only: SMZ=1, MDZ=2, LGZ=3, VMMDZ=4, VMLGZ=5, SMT=6, LGT=7 Prey slots 1–10 keep their meaning; only detritus moves 11 → 13. Avoids renumbering ~200 references.
Migrator detection zoo%does_dvm logical Not an index-ordering test, so future groups can be appended without silently breaking DVM.
Assimilation efficiency Unified across all 7 groups phi_* become fractions of egestion summing to 1.0, not fractions of total ingestion.
Silica partitioning Unchanged (phi_det_si, no AE factor) Opal is never assimilated, so its split is independent of AE.
GZ features carried over Salp falls (LGT aggregation), phi_aresp phi_nh4/phi_po4 and fast-sinking Si were left out as out of scope.
LGT diet GZ's prey list plus COBALTv3's medium phytoplankton Leaving a v3 phytoplankton class invisible to salps is hard to defend mechanistically.
mesozoo_200 scope Crustacean only Targets the COPEPOD database, whose net sampling does not retain gelatinous zooplankton. Tunicates get their own tunicate_200.

The AE ↔ gge_max equivalence

Worth recording, because it governs how the legacy groups behave. GZ's metabolism reduces exactly to
COBALTv3's when AE is constant:

AE·I − bresp − phi_aresp·I  =  (AE − phi_aresp)·I − bresp     so   gge = AE − phi_aresp

The defaults AE = 0.7, phi_aresp = 0.3 therefore reproduce the previous gge_max = 0.4 for
smz/mdz/lgz. The two formulations still differ in one respect — see Answer changes.


What changed

cobalt_types.F90

  • NUM_ZOO 5 → 7, NUM_PREY 11 → 13.
  • Named IDs: SMZLGT and PREY_DIAZOPREY_DET, replacing bare numeric indices throughout.
  • Zooplankton type gains does_dvm, agg, frac_fast_det, ipa_smt, ipa_lgt, plus
    jaggloss_n/p and jprod_{n,p,fe}det_fast arrays and their diagnostic ids.
  • generic_COBALT_type gains the fedet_fast family, tunicate tendency/pointer members, and
    jprod_tunicate_200 / f_tunicate_200.

Ecology (generic_COBALT.F90)

  • Ingestion — all seven blocks regenerated in the COBALTv3 food1/food2 switching idiom with
    named prey indices. Tunicates enter the carnivory side of switching for mdz/lgz/vmmdz/vmlgz and are
    added to higher-predator switching (smt with the medium size class, lgt with the large).
  • Egestion and production — unified on the dynamic AE for all seven groups, with frac_fast_det
    splitting detritus between the standard and fast-sinking pools. Migrating groups still route
    egestion through gut clearance rather than directly from ingestion.
  • Salp falls — LGT aggregation (agg, quadratic, no stress dependence) routed entirely to the
    fast-sinking pools. Required adding zoo%agg and zoo%jaggloss_n/p to the v3 zooplankton type,
    which had no zooplankton aggregation at all.
  • DVM — generalized from two hardcoded groups to per-group arrays with a dvm_prey_wgt mask, so
    LGT migrates. Gated on does_dvm throughout.
  • Fast-sinking ironfedet_fast end to end: production from zooplankton and higher-predator
    egestion, jfe_ads_fast scavenging (with the half-the-available-iron limiter shared across both
    detrital pools so they compete for the same dissolved pool), jremin_fedet_fast on the fast N
    pool's own specific rate, source/sink assembly, and bottom flux.
  • Mass balance — tunicate pools added to the N/P/C sums; fedet_fast and the LGT gut pools to Fe
    and Si.

Diagnostics

85 new registered fields: full SMT and LGT sets mirroring vmLgz, per-group aggregation and
fast-detritus production (registered in a loop rather than group by group), the fast-iron fluxes, and
jprod_tunicate_200 / tunicate_200 ported from GZ-COBALT as the JeDI-comparable counterparts to the
COPEPOD-comparable mesozoo pair.


Pre-existing bugs found and fixed

These were latent on dev/eco-cobalt and are unrelated to the tunicates, but had to be resolved to
make the merge coherent.

1. zoo%assim_eff was computed but never used. It was calculated each timestep and registered as
a diagnostic, but no flux consumed it — the maths still used a static
1 − phi_det − phi_ldon − phi_sldon − phi_srdon. This is why the phi_* rescaling and the egestion
rewrite had to land together rather than as separable steps.

2. Seven zooplankton parameters were declared and read but never initialized: k_clear_gut,
k_temp_gut, k_clear_met, assim_eff_max, assim_eff_min, kae, phi_aresp. The first three
drive the live gut/metabolite path, so vmmdz and vmlgz were integrating on uninitialized memory.
All seven are now registered. Gut evacuation constants use the values from the COBALTv2-DVM code these
routines descend from (Poupon et al. 2024): k_clear_gut = 8.0 day⁻¹,
k_temp_gut = 4.32 day⁻¹ °C⁻¹, k_clear_met = 1.0 day⁻¹.

3. Sixty unguarded accesses to migrator-only arrays. does_dvm guards the allocation of the 21
gut and metabolite arrays, but accesses in three clusters — tracer fetches, vmove setters, and
tendency assembly — sat outside any guard. Benign on dev/eco-cobalt, where migration was not
switchable; fatal once it became a parameter. Now guarded per group.

Adaptations required by the convention change

Calcite and aragonite production. jprod_cadet_arag / jprod_cadet_calc used zoo%phi_det
directly as "the proportion of consumed material that ends up as detritus" — correct while phi_det
was a fraction of ingestion. Under the new convention it needed (1 − AE)·phi_det; without that
change calcite production would have roughly tripled.


Answer changes

Expected, and not a regression: the legacy groups will not reproduce dev/eco-cobalt
bit-for-bit even with tunicates switched off. The unified scheme applies N/P colimitation as
min(I_N, I_P/q) up front, whereas v3 applied a phosphorus cap after computing production. Small
drift is expected; large drift means something else is wrong.

jprod_mesozoo_200 and f_mesozoo_200 now include the migrating crustacean groups (from
dev/eco-cobalt), so both rise relative to pre-merge runs.

Gut residence times shorten in warm water relative to the WIP commit's initial Dam & Peterson
estimate: the two forms cross near 4.5 °C, and at 20 °C the adopted values give ~94 vs ~64 day⁻¹
(≈15 vs ≈22 min), which weakens active transport in the tropics and subtropics.


New model interface

9 prognostic tracers: nsmt, nlgt, nlgt_gut, plgt_gut, felgt_gut, silgt_gut,
nlgt_met, fedet_fast, fedet_fast_btf.

146 new namelist parameters, chiefly: full trait sets for smt and lgt; assim_eff_max,
assim_eff_min, kae, phi_aresp, agg, frac_fast_det, does_dvm, and the three gut/metabolite
kinetic constants for all seven groups; 26 new innate prey availabilities.

Restart compatibility: the new tracers are prognostic and TRACERS_MAY_REINIT defaults to
.false., so restarting from a pre-merge restart file is a fatal error. Cold-starting from
GENERIC_TRACER_IC_FILE is the supported path. Note that nsmt and nlgt need non-zero values
in that file: zero biomass is an absorbing state, since ingestion and basal respiration both carry
zoo%f_n as a multiplicative factor, so they would remain identically zero for the whole run. The
gut, metabolite and fedet_fast pools are fine starting at zero.


Configuration notes

does_dvm may only be enabled for vmmdz, vmlgz and lgt — the three groups that own gut and
metabolite tracers. Enabling it elsewhere is now rejected at initialization with an explicit message
rather than dying later with an opaque nitrogen imbalance.

Switching it off is fully supported and enables two useful sensitivity experiments:

Configuration Isolates
swim_max_lgt = 0.0 Salps keep gut-transit physiology but never move vertically → the vertical transport alone
does_dvm_lgt = .false. and swim_max_lgt = 0.0 Salps egest where they feed, as in GZ-COBALT → transport and gut-lag physiology together

Both settings are needed for the second: the non-migrating branch still derives a swimming speed from
swim_max, and nlgt carries move_vertical, so LGT would otherwise still oscillate vertically with
none of the gut machinery.

Tracer registration is deliberately not conditional on does_dvm, so the tracer list — and hence
restart file contents — stays identical between the migrating and non-migrating configurations, which
is what makes them directly comparable.


Testing status

Done: all four modified files pass gfortran -fsyntax-only, and generic_COBALT.F90 compiles at
-O1 -Wall, against throwaway stub modules for the twelve FMS/MOM6 dependencies. This validates block
structure, every variable and derived-type component name, and array ranks across the whole diff,
including the programmatically generated diagnostic and ingestion blocks. A warning comparison against
dev/eco-cobalt under identical stubs shows no new warnings. The does_dvm guarding commit was
additionally verified to be pure gating: a line-by-line comparison ignoring comments and whitespace
confirms zero code lines removed or reordered, so the default configuration is unchanged.

Not done — this has never been built against real FMS/MOM6, and has never been run. The stub check
cannot catch argument disagreement with real FMS interfaces.

Suggested sequence:

  1. Build against FMS/MOM6.
  2. Short run with tunicates off (imax_smt = imax_lgt = 0) and do_fastsinking = .true.. Since
    the merge altered the egestion and production maths for all seven groups, this separates "did the
    refactor break the existing model?" from "do the new groups behave?" The pre/post N, P, C, Fe and Si
    conservation checks in generic_COBALT_update_from_source are the primary test — they call
    mpp_error(FATAL, …) past imbalance_tolerance, so a run that survives them exercises nearly all
    the new flux routing.
  3. Enable tunicates; confirm plausible biomass, non-zero jprod_ndet_fast_Lgt, and a diel cycle in
    vmove_Lgt matching vmmdz/vmlgz.
  4. Confirm jfe_ads_fast and jremin_fedet_fast are non-zero and the Fe budget still closes.

Known limitations and follow-ups

  • Tuning. Tunicate trait defaults are GZ-COBALT's COBALTv2-tuned values carried over unchanged
    into a different food web. They are a starting point, not a calibration.
  • Tunicate gut kinetics inherit copepod values. k_clear_gut / k_temp_gut are the crustacean
    numbers. Salp gut passage differs, and since LGT both migrates and produces the fast-sinking
    pellets, these directly control how much salp flux reaches depth. Flagged in-code as retuning
    candidates.
  • bresp ×0.9 for migrators. Poupon's COBALTv2-DVM scaled migrator basal respiration to 90% of
    the resident equivalent, presumably because the DVM path adds a swimming cost on top. eco-cobalt
    carries the unscaled values. Unresolved: port bug or deliberate?
  • Diagnostic registration coverage. Which DVM-era diagnostics are registered for which group is
    inconsistent in places. Harmless — every array is now allocated for every group, so a mismatch costs
    a missing-value field rather than a crash — but worth a tidy-up pass.
  • gge_max is still registered but no longer used for zooplankton. Left in place with a comment
    rather than removed, to avoid breaking existing parameter files.
  • dev/eco-cobalt is 1 commit ahead (15e766d, jmetabo_n). Should merge cleanly; the send it
    adds is guarded on numeric group indices and should become does_dvm here so LGT is included.

Commits

Commit Contents
9b8811f The GZ-COBALT merge itself: 7 groups, named IDs, tunicate tracers and ingestion, unified dynamic AE, fedet_fast, salp falls, generalized DVM, mass balance. ~2,700 lines.
946acbb Merge dev/eco-cobalt. Resolves the mesozoo_200 and deallocation-guard conflicts; fixes a 100 m allocated-vs-registered mismatch introduced by 9b8811f; ports the tunicate 200 m diagnostics.
7cf40b6 Replace the Dam & Peterson gut evacuation estimate with the COBALTv2-DVM values.
e934fc1 Make does_dvm a valid off-switch: guard the three access clusters, reject unsupported configurations, correct a misleading comment.

jessluo and others added 10 commits July 28, 2026 09:54
Brings in the mesozooplankton 200m diagnostic fixes (14ef4fb, b389801,
9e0bfda) and the allocation/deallocation consistency fixes (8195fae).

Conflict resolution:

- jprod_mesozoo_200 / f_mesozoo_200: took the dev/eco-cobalt content
  (vertically migrating crustacean zooplankton included, with their gut
  and metabolite nitrogen, plus the drho_dzt fix) expressed with the
  named zooplankton IDs this branch introduced.  The tunicates are
  deliberately excluded: these diagnostics target the COPEPOD database,
  whose net sampling does not retain gelatinous zooplankton.

- Zooplankton deallocation guard: kept this branch's
  "if (zoo(n)%does_dvm)" rather than the numeric "n == 4 .or. n == 5",
  since large tunicates now migrate and would otherwise have their gut
  and metabolite arrays left unallocated.  Both branches had
  independently added deallocate(zoo(n)%jmetabo_n); the duplicate was
  dropped.

Also in this commit:

- Fix an allocated-vs-registered mismatch introduced by the WIP commit.
  The SMT and LGT diagnostic blocks were generated from the vmLgz block,
  so their *_100 ids were registered while the allocation and
  computation loops still ran over n = 2,5.  cobalt_send_diag loops over
  all NUM_ZOO groups and reads the array whenever the id is registered,
  making this an unallocated read at the first diagnostic write.  The
  100m zooplankton arrays are now allocated, computed and deallocated
  over the full group range, with registration alone deciding what
  reaches the output.

- Register jzloss_n_100 and jprod_don_100 for both tunicate groups.

- Port jprod_tunicate_200 and f_tunicate_200 from GZ-COBALT, the
  gelatinous counterparts to the mesozoo pair, accumulated in the
  existing 200m integration loops.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
k_clear_gut and k_temp_gut were among seven zooplankton parameters that
are declared and read but never initialized on dev/eco-cobalt.  The WIP
merge filled them from the Dam and Peterson (1988) gut evacuation
relationship; replace that estimate with the values actually used in the
COBALTv2-DVM code these routines descend from (M. Poupon, pers. comm.
2024):

  k_clear_gut   16.85 -> 8.0   day-1
  k_temp_gut     2.37 -> 4.32  day-1 degC-1

k_clear_met was already 1.0 day-1, which matches, so it is unchanged.

The two forms cross near 4.5 degC.  Above that the Poupon values clear
guts faster (about 94 vs 64 day-1 at 20 degC, i.e. roughly 15 vs 22 min
residence time), so this shortens gut residence and weakens active
transport in warm water relative to the previous defaults.

All seven groups take these defaults, including the large tunicates.
Salp gut passage is not copepod gut passage, and since LGT both migrates
and produces the fast-sinking pellets, k_clear_gut_lgt and k_temp_gut_lgt
are flagged in-code as retuning candidates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
does_dvm guards the allocation of the 21 gut and metabolite arrays, but 60
accesses to those arrays sat outside any guard, so setting does_dvm to
.false. for a group that has them would write into unallocated memory.  The
first such access, the g_tracer_get_values fetch of nlgt_met, would corrupt
memory silently without bounds checking or abort cleanly with -fcheck=all.

Guard the three clusters where those arrays are touched, per group so that
vmmdz, vmlgz and lgt can be switched independently:

  - the gut and metabolite tracer fetches
  - the vmove setters for the gut and metabolite tracers (note that the
    biomass vmove array is allocated for every group, so that setter stays
    unconditional)
  - the gut and metabolite tendency assembly

Tracer registration is deliberately left unconditional.  Keeping the tracer
list fixed means restart files stay interchangeable between the migrating
and non-migrating configurations, which is the point of running them as a
pair.  A group whose migration is off simply leaves its gut and metabolite
tracers untouched at their initial values, contributing zero to the mass
budget.

Also reject an unsupported configuration up front.  does_dvm can only be
enabled for the three groups that own gut and metabolite tracers; enabling
it elsewhere would load ingestion into a gut that is never carried or
cleared and die later with an opaque nitrogen imbalance.  Fail in
user_add_params instead, where the cause is legible.  The parameter block
comment claiming migration "can be switched on for any group from the
parameter file" was wrong and has been corrected.

This is pure gating: no code line was removed or reordered, so with all
three groups migrating the model executes exactly as before.

Enables the non-migrating large tunicate experiment, in which LGT egests
where it feeds as it did in GZ-COBALT.  Note that swim_max_lgt should also
be set to 0 for that run: the non-migrating branch of section 3.2.4 still
derives a swimming speed from swim_max, and nlgt carries move_vertical, so
LGT would otherwise still rise at night and sink by day without any of the
gut machinery.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cherry-picked from 15e766d, with two adjustments for this branch.

The registration block conflicted: 15e766d changed only longname and
units, while this branch had independently switched the group indices to
named constants and added the two tunicate groups. Kept this branch's
structure and applied the metadata change to all seven groups.
jmetabo_n is sent with no rho_dzt factor, so the units are
mol N kg-1 s-1 and ", layer integral" was wrong.

15e766d guards the send with "n .eq. 4 .or. n .eq. 5". On this branch
LGT also has does_dvm = .true. by default, and jmetabo_n is allocated
under that flag (generic_COBALT.F90:9471), so the literal indices would
leave jmetabo_nLgt permanently dead and would read unallocated memory if
does_dvm_vmmdz were ever set .false. Guard on zoo(n)%does_dvm instead,
which is what the adjacent comment already claimed.

Also corrects two longnames that read "vertically small/large
tunicates", a leftover from copy-pasting the migrating-group text.

Verified on mac-m1/osx-gnu/repro: builds clean; 2-day BATS run closes
all five element budgets at imbalance_tolerance = 1e-9; jmetabo_nvmMdz,
jmetabo_nvmLgz and jmetabo_nLgt all produce data while jmetabo_nSmz is
correctly absent; ocean.stats and all tracer fields bitwise unchanged.

(cherry picked from commit 15e766d)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cherry-picked from 49d4056 on dev/eco-cobalt. The generic_tracer.F90
hunk applied as-is; the cobalt_send_diag.F90 fix was reapplied to the
same six lines rather than patched, because this branch's copies of the
surrounding blocks differ.

cobalt_send_diag.F90 computes tot_layer_int_c, _poc and _n as
whole-array expressions over (:,:,:), but the six migrating-zooplankton
terms were indexed (i,j,k,tau). Outside the i/j/k loops those variables
are stale -- zero on a cold start -- so with bounds checking the run
aborts on the first diagnostic call:

  Fortran runtime error: Index '0' of dimension 3 of array
  'cobalt%p_nvmmdz' below lower bound of 1

Without bounds checking it silently reads out of bounds instead. The
tot_layer_int_p block immediately below already used (:,:,:,tau) for the
same tracers, which is what the other three should have been.

generic_tracer.F90 documents why any DVM configuration must set
use_Press_et_al_tridiag_solver = .true.: only the Press branch of
g_tracer_vertdiff_G sign-splits the vertical motion into
max(sink,0)/min(sink,0), so the default GOLD branch does not conserve
mass for upward (negative vmove) swimming. Left .false. with migrating
groups active, generic_COBALT aborts almost at once on a nitrogen or
carbon source/sink imbalance. The default stays .false. so non-DVM
configurations keep their existing answers.

(cherry picked from commit 49d4056)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ankton (NOAA-CEFI-Regional-Ocean-Modeling#177)

* Added DVM to COBALTv3

* revert unintended changes

* line spacing

* reverting unintended changes to FMS_coupler_util

* missing parentheses

* removed duplicate declarations of kirr_hp and hp_phi_vis

* mesozooplankton 200-m integrated biomass should also include vertically migrating zooplankton

* fixed errors in last commit

* bug fix for drho_dzt

* fix inconsistencies with allocation/deallocation

* clean up jmetabo diagnostics, pass dvm zooplankton jmetabo_n to g_send_data. bug found by claude opus 5.

* bug fixes and improved comments on using Press algorithm

* adding parameter calls for a range of COBALT-dvm variables

---------

Co-authored-by: Mathieu A. Poupon <mpoupon@tiger3.princeton.edu>
cefi/dev/eco-cobalt's tip (80e5c6e, "Implement upward and downward
swimming for vertically migrating zooplankton (NOAA-CEFI-Regional-Ocean-Modeling#177)") is a
squash-merge whose content is byte-identical to what this branch
already had prior to the dev/cefi merge (verified via
`git diff 80e5c6e 6a6fb45` on the touched files, which is empty).
All resulting conflicts here were the earlier zoo/prey identifier
rename touching the same lines as that squash commit; resolved by
keeping this branch's already-correct content. This merge exists to
make 80e5c6e an ancestor so the push to cefi/dev/eco-cobalt is a
fast-forward rather than requiring a force-push.
…merge

feature/gz-cobalt-merge already carries an extended, generalized version
of dev/eco-cobalt's DVM/named-parameter work (adding tunicate groups SMT
and LGT, generic dvm_* tracking machinery keyed by zoo(n)%does_dvm instead
of hardcoded vmmd_*/vmlg_* variables, and NUM_ZOO/NUM_PREY-wide loops that
rely on cobalt_reg_diag registration rather than hardcoded group ranges).
Almost every conflict was this branch's content being a superset of
dev/eco-cobalt's; resolved by keeping this branch's version throughout,
consistent with the guidance in TODO.eco-cobalt.md.

One substantive (non-naming) difference was found and preserved: phi_det
has been redefined on this branch from "fraction of ingestion routed to
detritus" (dev/eco-cobalt, under a fixed 30% egestion budget) to "fraction
of egested material routed to detritus" (this branch, consistent with the
variable assim_eff Michaelis-Menten bioenergetics this branch already has
and dev/eco-cobalt lacks) — the two are not numerically interchangeable,
so dev/eco-cobalt's phi_det formulas/defaults were not merged in.
@jessluo
jessluo force-pushed the feature/gz-cobalt-merge branch from 8a48f0b to a5b7fb1 Compare September 15, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant