Lake Erken — Parity Status and Limitations¶
Status: PARTIAL PARITY (FAIL). The hypsography / inflow / outflow /
water-balance is at exact parity. The residual FAIL has two independent
causes: (1) a mild, seasonal under-mixing of the summer thermocline — a
real but small physical difference affecting the native turbulence/temperature
fields — and (2) the selmaprotbas biogeochemistry (BGC) is limited by a FABM
library build flag. This page records exactly what is at parity and what is not,
with source-level and data-level evidence, so anyone picking up the project knows
precisely where it stands.
The case¶
Property |
Value |
|---|---|
Period / step |
1999-02-01 → 2020-12-31 (~22 yr), 3600 s hourly (~192k steps) |
Grid |
21 m, 42 layers, hypsographic (cross-sectional area varies with depth) |
Turbulence |
second-order closure ( |
Light |
very turbid ( |
BGC |
FABM |
Output |
daily means ( |
FABM numerics |
|
Lineage |
reference built from gotm-lake (older GOTM fork + its own bundled FABM); pyGOTM derives from gotm-model + conda |
Because pyGOTM and the reference come from different lineages, mismatches may originate in gotm-model vs gotm-lake, the translation, the pyFABM coupling, the FABM library build, or the reporting — not every mismatch is a pyGOTM bug.
Variable inventory (current)¶
114 native variables and 41 FABM variables are compared. Native:
60 PASS / 5 MARGINAL / 23 DISCREPANT / 26 BROKEN; FABM: 0 / 0 / 3 / 38.
All 64 BROKEN variables are in lake_erken; no other case has any BROKEN.
The 26 native BROKEN variables all trace to one root residual — the summer-thermocline under-mixing described below — either directly or through a sensitive derived/diagnostic quantity. They split as:
Core turbulence/stratification, just over the 0.20 threshold:
num(0.245),nuh/nus(0.280),cmue2(0.238),tke(0.208),as(0.208),SS(0.217),NN/NNT(0.203),G(0.217),avh(0.288),P(0.323).Sensitive derived diagnostics that amplify the same residual:
Rig(0.93 —NN/SS, which blows up whereverSS → 0), the Reynolds-stress diagnosticsuu/vv/ww(0.75–0.79), and the SST-driven surface turbulent heat fluxesqe/qh(0.79/0.68) plus the ice fieldsHfrazil/Tice_surface.Momentum:
u(0.218),taux/tauy(0.26/0.30),Ekin(0.23).Mixed-layer-depth diagnostics:
mld_bott(0.54),mld_surf(0.45) — see the diagnostic-method note below.
For calibration: temp itself is only DISCREPANT (d_norm = 0.123),
rho = 0.141, eps = 0.101. The BROKEN turbulence variables sit just above the
0.20 line because the normalized Fréchet metric is sensitive for these
floor-dominated, multi-decade fields; the underlying physical difference is small.
What is at exact parity (verified)¶
Hypsography / water-balance is exact (
d_norm = 0.0):Af,Qlayer,Qs,wq,FQ,Qres,zeta,h,salt, and the integratedint_precip/int_evap/int_inflow/int_outflow/int_water_balanceterms (the last three PASS withind_norm < 0.003). The momentum equations and bottom friction are hypsography-aware (area-weighted diffusion/advection,wqadvection, per-layer bottom drag), mirroring gotm-lake. This is a genuine and significant accomplishment.
The native residual: a mild, seasonal summer-thermocline under-mixing¶
A full-record (8005-day) comparison of the pyGOTM run against the reference NetCDF localises the residual precisely. It is seasonal, not catastrophic:
Season |
temp |err| |
interior |
stratification (surface − bottom) |
|---|---|---|---|
Winter / ice (Nov–Mar) |
0.06–0.27 °C |
~0.88–1.00 |
matches |
Summer stratified (Jun–Aug) |
0.86–1.38 °C |
0.44–0.68 |
py ~2 °C too strong |
Annual temp mean-abs-error is 0.55 °C (median 0.35 °C). The largest single point is 6.8 °C, but that is a >99.9th-percentile transient: only ~2 % of points exceed 3 °C and ~0.2 % exceed 5 °C.
The signed bias is a clean dipole: surface +0.49 °C (too warm), bottom −0.20 °C (too cold) on the annual mean; in midsummer the surface runs ~+1 °C and the bottom ~−1 to −1.5 °C. A representative 1-Aug profile: pyGOTM bottom 12.4 °C / surface 22.0 °C (ΔT 9.6 °C) vs reference 13.9 °C / 20.9 °C (ΔT 7.0 °C).
The
numdeficit is concentrated at the thermocline depth (worst around interface level 32; near-surface and near-bed diffusivity match) and only in the stratified months.The velocity field matches the reference to ~90 % (near-bottom mean speed: pyGOTM
3.8e-3vs reference4.1e-3), and the columns agree in winter and below the thermocline.
Mechanism. During the stratified season pyGOTM holds a sharper, shallower
thermocline than the reference: the cross-thermocline turbulent diffusivity is
lower, so less heat mixes downward, the surface stays warmer, the bottom stays
cooler, and the stratification is ~2 °C too strong. In winter, and below the
thermocline, the columns agree. Lake Erken is the case most exposed to this:
it is a shallow (21 m), very turbid lake whose shortwave is absorbed almost
entirely in the top ~2 m (g2 = 1.74 m), producing an unusually sharp, shallow
summer thermocline where any small cross-thermocline mixing difference is
maximally amplified. The same closure is only mildly different (MARGINAL/
DISCREPANT) in the deeper, weakly-stratified second-order ocean cases
(gotland, nns_annual, resolute).
The second-order closure is identical between the lineages¶
The residual is not a closure-coefficient difference. A direct source diff
(gotm-lake/.../turbulence vs gotm-model/code/src/turbulence) shows the
closure is the same in both lineages:
Closure component |
gotm-lake vs gotm-model |
|---|---|
Stability functions |
byte-identical |
Cheng et al. 2002 coefficients ( |
byte-identical |
|
identical except Stokes/Langmuir terms ( |
|
all in gotm-model-only additions (k-omega namelist, Stokes vectors, renamed enums) that are inactive here |
Every closure knob is correctly plumbed from the YAML at runtime (verified
through the driver): scnd_method = 1 (quasi-equilibrium), scnd_coeff = 7
(Cheng), length_lim = true, galp = 0.53, compute_c3 = true,
Ri_st = 0.25, stab_method = 3. Internal-wave mixing is off in both runs:
the case’s iw: block sets only alpha = 0.7 with no method, so iw_model = 0
in pyGOTM, and gotm-lake’s internal_wave.F90 only acts for iw_model == 2. The
reference’s interior num shows no ~1e-4 floor, confirming IW mixing is
inactive.
Interpretation. With identical closure coefficients and identical, correctly applied configuration, the residual is most consistent with a small numerical difference in how the two codes resolve a sharp, shallow thermocline (advection/diffusion discretisation, accumulated over 22 years) — pyGOTM holds the slightly sharper gradient. There is no evidence-justified code fix: the closure is shared with the 14 passing ocean cases, and forcing pyGOTM to match the reference here would mean adding spurious cross-thermocline mixing, which would degrade those cases. It is recorded as a documented, bounded lineage difference.
Mixed-layer-depth diagnostic: a method-default difference (now configurable)¶
mld_bott and mld_surf are post-hoc diagnostics (diagnostics.F90,
mld_method); they do not feed back into the physics. Their d_norm reflects a
diagnostic-method default difference between the lineages:
gotm-lake defaults
mld_method = 1(TKE criterion, which also computes the bottom mixed layer); gotm-model (pyGOTM’s basis) defaultsmld_method = 2(critical-Richardson, which computes only the surface MLD and leavesmld_bott = 0). The case YAML overrides neither, so the reference ran method 1 (confirmed: itsmld_bottreaches 21 m) while gotm-model’s default is method 2 (mld_bott ≡ 0).pyGOTM now supports an optional
mld:block (method/diff_k/Ri_crit), defaulting to the gotm-model value so the other 22 cases are unchanged. The lake_erken config setsmld: method: 1to use the same diagnostic as the reference. This is a documented input-config setting (the reference output is untouched); it is diagnostic-only and does not alter any physics field.Effect (verified):
mld_bottd_normis0.5423andmld_surf0.4477under method 1 (versus1.0000and0.5950under the gotm-model default). Both remain BROKEN, because once computed by the reference’s method they faithfully reflect the same summer-thermoclinetkeresidual as the other turbulence variables — they are part of the single coherent story rather than a separate artifact. The case verdict and the 26-native-BROKEN count are unchanged.
FABM coupling fixes already applied (no library rebuild)¶
Two correct, gated fixes live in src/pygotm/fabm/fabm_loop.py (only lake_erken
is affected; the other 22 cases are byte-identical):
Daily-mean FABM output (gated on
output_reduce_mode): accumulate FABM state and diagnostics over each output window and write the window mean, instead of an instantaneous local-midnight snapshot that biased diurnal quantities.repair_stateclipping (gated on thefabm: repair_stateflag): callmodel.check_state(repair=True)after transport and after the source update, matching gotm-lakedo_repair_state. This eliminates a catastrophicselmaprotbas_pophosphate NaN (verified: 0 NaN in the current output).
pyfabm limitations preventing complete BGC validation¶
Two limitations of the stock conda pyfabm library (vs the custom FABM that
gotm-lake bundles) mean the selmaprotbas biogeochemistry cannot reach full
parity with the reference, independent of the native under-mixing above.
1. variable_bottom_index (benthic coupling). The worst FABM variables are
sediment-coupled (selmaprotbas_o2, DO_mg, benthic-regenerated nutrients).
gotm-lake’s bottom_everywhere distributes the benthic flux over the whole
sloping bed (rhs(k) *= (Af(k)-Af(k-1))/Vc(k) per layer, via
set_bottom_index(k)), which requires a FABM library compiled with
_FABM_BOTTOM_INDEX_ = -1 (variable bottom index). The conda
pyfabm 3.0.0 is built with the default 0; link_bottom_index(...) raises
"… compiled without support for variable bottom indices". The reference runs
bottom_everywhere only because gotm-lake builds its own FABM with that flag.
2. selmaprotbas alpha/beta phytoplankton parameters (interior BGC). The
reference fabm.yaml sets two GOTM-lake-only parameters on each
selmaprotbas/phytoplankton instance — alpha (nutrient-uptake
half-saturation) and beta (temperature growth-correction). Conda pyfabm does
not declare them, so loading the reference YAML verbatim raises an “invalid
configuration” error. pyGOTM therefore strips alpha/beta (see
src/pygotm/fabm/config.py → _normalized_fabm_config_path) and runs with the
upstream defaults, so the interior phytoplankton growth differs from the
GOTM-lake-tuned reference. This is why the bundle’s fabm.yaml is a
materialized (stripped) copy rather than a byte copy of the reference — the
validation runner stages exactly that materialized YAML, and its hash is
recorded as fabm_yaml_sha256, so re-running the bundle is reproducible.
Decision: do not rebuild pyfabm locally (conda-forge divergence + maintenance
burden on every FABM bump; the variable-bottom build is non-default for the
python host and unverified; per-layer evaluation costs nlev × getRates per
step; and even with both limitations resolved, full PASS is unlikely given the
native under-mixing above). The actionable path is upstream: a conda-forge
pyfabm variant that enables variable_bottom_index and exposes the
selmaprotbas alpha/beta parameters.
Validation reporting — variable ownership¶
PYGOTM_VARIABLES (the native-vs-FABM section classifier in
src/pygotm/validation/tolerances.py) lists the native fields. It mirrors
register_all_variables.py plus the runtime lake/air-sea outputs (air-sea, ice,
observation, energetics and lake-hypsography fields are native). Normalization is
decoupled from ownership — full-range normalization is reserved for the
floor-dominated mean-flow/turbulence fields — so section labels and the
normalization choice are independent and every reported d_norm is unaffected by
the classification.
Reproduction¶
# Re-run lake_erken and regenerate its bundle (~5 min):
conda run -n pygotm python -m pygotm.validation.run_validation --cases lake_erken
# Re-compare existing NetCDFs without re-running:
conda run -n pygotm python -m pygotm.validation.run_validation --all --no-run
Localization diagnostic: compare temp, num, NN at depth between
validation/runs/lake_erken/lake_erken.nc and
validation/reference/lake_erken/output.nc, grouped by month — the columns track
in winter and below the thermocline; the divergence is the summer thermocline.
Bottom line¶
Hypsography / water-balance: exact parity. Native physics: a mild, seasonal
under-mixing of the summer thermocline (~0.5 °C annual-mean temp error;
stratification ~2 °C too strong in midsummer). The second-order closure source is
byte-identical between the lineages and every closure knob is correctly plumbed,
so the residual is a small numerical difference in resolving a sharp, shallow
thermocline; there is no safe, evidence-justified code fix and the shared closure
must not be distorted toward the reference. The mld_bott/mld_surf diagnostics
use the reference’s mld_method via the new mld: config block. BGC: two correct
FABM-coupling fixes landed; the dominant FABM blocker is bottom_everywhere,
which needs a FABM library compiled with variable_bottom_index. The overall
case remains FAIL; the physics is close but not at parity.