Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2f8ac94
feat(topology): register CAM_MAM4 and CAM_MAM5 — MAM5 now has data, n…
aliakherati Aug 20, 2026
98589fe
docs(plan 025): establish what a CAM driver can legitimately reuse
aliakherati Aug 21, 2026
a387fed
fix(amicphys): make the clear-sky-only restriction actually enforced
aliakherati Aug 21, 2026
2070fbc
feat(rename): add CAM's rename algorithm as a selectable branch
aliakherati Aug 21, 2026
c5f1f15
test(rename): validate the CAM branch against Fortran, and scope the …
aliakherati Aug 21, 2026
66c990d
test(rename): validate CAM's branch to machine precision where it div…
aliakherati Aug 21, 2026
7c52e83
docs(plan 025): record the divergent-regime validation, and correct A10
aliakherati Aug 21, 2026
f798680
Add a CAM-vs-E3SM comparison figure
aliakherati Aug 21, 2026
53947f5
Merge main (v0.4.0: pcarbon aging, AmicphysParams, f32 coag) into fea…
aliakherati Aug 26, 2026
04a47c7
feat(strat): port CAM's sulfeq equilibrium cluster, validated at mach…
aliakherati Aug 26, 2026
9a8ad5d
feat(strat): port the reversible H2SO4 uptake branch (sulfeq consumer)
aliakherati Aug 26, 2026
626c77b
Merge remote-tracking branch 'origin/feat/cam-mam5-topology' into fea…
aliakherati Aug 26, 2026
1777b0f
docs(plan 025): driver sub-plan G0-G5; merge the CAM topology branch …
aliakherati Aug 26, 2026
c13dc14
feat(cam): add the generated CAM parameter layer (specmw, adv_mass) —…
aliakherati Aug 26, 2026
d050077
feat(cam): port modal_aero_gasaerexch_sub, SO4-only — plan 025 G1
aliakherati Aug 26, 2026
cc91349
feat(cam): port modal_aero_newnuc_sub's wrapper — plan 025 G2
aliakherati Aug 26, 2026
5e6afa0
feat(cam): port modal_aero_coag_sub, pair_option 3 — plan 025 G3
aliakherati Aug 27, 2026
3e3ea2c
feat(cam): the microphysics sequence, mam_microphysics_cam — plan 025…
aliakherati Aug 27, 2026
633a086
feat(cam): the CAM box driver, validated end-to-end — plan 025 G4b + G5
aliakherati Aug 27, 2026
b8fcac4
feat(cam): default n_substeps = 16 — ADR-021 (plan 025 A6 resolved)
aliakherati Aug 27, 2026
8a232f5
docs: FEATURES + PROGRESS entries for the CAM-driver arc (rule #5)
aliakherati Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ Status legend: **planned**, **in progress**, **ported (validated)**, **deferred*
| Constants and species table | `e3sm_src/modal_aero_data.F90`, `e3sm_src/shr_const_mod.F90` | compile-time + runtime indices hard-coded in `mam4_jax/data.py` (0-based, with sentinel `-1` for unused slots); provenance at `tests/reference/indices/reference.npz` |
| Error function / special functions | `box_model_utils/error_function.F90`, `e3sm_src/shr_spfn_mod.F90` | use `jax.scipy.special` if available; otherwise port closed-form |

## CESM/CAM variant (plans 024/025, branch `feat/cam-driver`)

The CAM code line shares leaf kernels with E3SM but none of the orchestration (`grep -rI amicphys` over CAM `src/` is empty). Reference: the sibling repo `mam-box-fortran` (CESM3 `cam6_4_187`), **fixdumfac** builds. All rows below are on `feat/cam-driver` (PR #74), not yet on `main`.

| Feature | Fortran source | Status |
| --- | --- | --- |
| CAM topologies + parameters | read out of an initialised box model (not transcribable) | **landed** — `core/cam_topologies.py` (`cam_mam4`, `cam_mam5` incl. `coarse_strat`) + `core/cam_params.py` (per-type `specmw`, mechanism `adv_mass`, tracer names), both generated + sha-stamped; `tests/test_cam_{topology,params}.py` |
| Stratospheric sulfate equilibrium (`sulfeq` cluster) | `modal_aero_wateruptake.F90:895-1171` | **ported (validated)** — `physics/strat_sulfate.py`: Tabazadeh wt%, Ayers/Kulmala vapor pressure, dual Kelvin factors, + the reversible H2SO4 uptake consumer (`modal_aero_gasaerexch.F90:523-566`). Machine ε vs `tools/capture_sulfeq` (wtpct 1.3e-15, qeq 3.5e-14); upstream surf-tension interp bug preserved + written up |
| Mixed-phase table saturation (`estblf`/generic `qsat`) | `wv_saturation.F90` | **ported (validated)** — `physics/cam_saturation.py`; ~2× off the direct over-water formula at 200 K, which is why it exists separately |
| gasaerexch (SO4-only) | `modal_aero_gasaerexch.F90` | **ported (validated)** — `coupling/cam_driver.py`: third-variant uptake rates, fgain/reversible condensation, the legacy **8.0-monolayer** aging block, rename A1, tendency application. 1.1e-15 vs `tools/capture_gasaerexch`, both topologies |
| newnuc wrapper | `modal_aero_newnuc.F90:59-520` | **ported (validated)** — step-average H2SO4 reconstruction, table-qsat RH, cutoffs/floor/size constraints over the shared leafs. 2.0e-11 (ulp × J∝a¹⁰ amplification) vs `tools/capture_newnuc` |
| coag (`pair_option_acoag = 3`) | `modal_aero_coag.F90:73-990` | **ported (validated)** — three pairs, sequential three-branch number solves, aged-through aitken transfer with shell accounting, coag-side 8-monolayer aging, over the byte-identical `getcoags`. 6e-16 vs `tools/capture_coag`. Found + fixed en route: `shr_const_rgas` is the product 8314.467591, not 8314.46 |
| rename A1 (`no_acc_crs`) | `modal_aero_rename.F90:243-624` | **ported (validated)** — inside `cam_driver.py`; A2 (`acc_crs`) stays deferred (measured inert below `qso2` ~1e-5); references pinned `no_acc_crs` |
| calcsize / wateruptake topology threading | shared kernels | **landed** — optional `tables` bundles (E3SM default bit-identical); wateruptake gains `qv=` and the `strat=` wt%-composition water branch |
| CAM box driver | `mam_box_driver_cam.F90` | **ported (validated end-to-end)** — `cam_run_step`/`cam_run_timesteps`: SO2 stub → calcsize → sulfeq → wateruptake → mmr↔vmr → microphysics, substep loop wrapping the whole step (**default `n_substeps = 16`, ADR-021**; parity tests pin 1). 120 steps × {cam_mam4, cam_mam5} × {trop, strat}: every printed tracer at the reference's 7-digit print floor (~5e-7), total sulfur **4.5e-15**. `tests/test_cam_driver.py` |

## Modes and species

The MAM4-MOM (with `RAIN_EVAP_TO_COARSE_AERO`) reference configuration has four modes in this Fortran order (from `modal_aero_data.F90:104-109, 121-123`):
Expand Down
29 changes: 29 additions & 0 deletions docs/KEY_DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,32 @@ Status values: **Accepted**, **Proposed**, **Superseded by ADR-NNN**.
- **Keep the static per-call argument.** Rejected: it is the recompile-per-value and non-differentiable path, and it made the config surface two-headed (global + static kwarg) for one number.
- **Put everything, selectors included, in one struct.** Rejected: `backend` and `mdo_*` must be static to select traced code; mixing them in would either force `static_argnums` on the whole struct — undoing the traced-leaf benefit — or require a `flax.struct.field(pytree_node=False)` dependency the package does not carry.
- **Drop the `configure_*` globals entirely in favour of `params`.** Rejected for this PR: it is a breaking change for existing hosts and unrelated to pcarbon aging. Worth revisiting once `params` has usage.

---

## ADR-021 — The CAM driver defaults to `n_substeps = 16`, deviating from reference-faithful defaults

- **Status:** Accepted 2026-08-26 (owner: "I think 16 is the better number but we need to document it"). Plan 025 assumption A6, resolved. Introduced on `feat/cam-driver` (PR [#74](https://github.com/reflective-org/MAM4-JAX/pull/74)).
- **Context:** CAM couples MAM microphysics as an UN-SUBSTEPPED sequential chain (gasaerexch → newnuc → coag once per Δt; `aero_model.F90:1202-1247`), and that splitting does not converge while nucleation is active — nucleation and condensation compete for the same H₂SO₄ within the step, and the sequential split resolves the competition wrongly at coarse Δt. The Fortran box study measured a **2.08×** spread in accumulation sulfate across dt 120 s → 1.875 s (vs 1.3 % with nucleation off). The JAX driver (`coupling/cam_driver.py:cam_run_step`) wraps the WHOLE per-step physics in a substep loop, so `n_substeps = n` is semantically identical to running the box at `Δt/n`.
- **Measurement** (default scenario, dt = 30 s, error vs `n_substeps = 32`):

| n_substeps | num_a2 | num_a1 | so4_a1 | h2so4 | cost |
|---|---|---|---|---|---|
| 1 (= CAM itself) | 26 % | 60 % | 57 % | 78 % | 1× |
| 2 | 16 % | 34 % | 31 % | 51 % | 2× |
| 4 | 9 % | 17 % | 16 % | 27 % | 4× |
| 8 | 4 % | 8 % | 7 % | 13 % | 8× |
| **16 (default)** | **1.4 %** | **2.5 %** | **2.2 %** | **4.4 %** | 16× |

First-order convergence: the error halves per doubling; cost is linear.
- **Decision:**
1. **`cam_run_step` / `cam_run_timesteps` default `n_substeps = 16`.** This is a deliberate deviation from the repo convention that defaults reproduce the reference (established in the PR #75 review): CAM's own behaviour (`n_substeps = 1`) is 26–78 % from the converged answer at a 30 s step, and shipping a known-O(50 %) configuration as the default answer was judged worse than deviating from the reference. 16 lands in the ~1.4–4.4 % band.
2. **Reference parity is opt-in and pinned:** every parity test passes `n_substeps = 1` explicitly, and `tests/test_cam_driver.py` locks both the default value and the convergence direction/rate.
3. The knob stays fully exposed; hosts trading accuracy for cost pick their own n with the table above.
- **Consequences:**
- A plain `cam_run_step(state)` is ~16× the reference's cost and does NOT bit-match the Fortran box; `n_substeps = 1` does (to the reference output's own 7-significant-digit print floor; total sulfur at 4.5e-15).
- The E3SM driver (`mam4_jax/driver.py`) is untouched — its amicphys path has its own substepping story (diffrax / ADR-013).
- **Alternatives considered:**
- **Default 1 (reference-faithful).** Rejected by the owner: this driver exists to produce usable answers, and the un-substepped splitting error dwarfs every other error source in the port by ~5 orders of magnitude.
- **Default 8.** Rejected: still 4–13 % from converged; 16's ~2 % is the first rung that is small against typical host-model uncertainty, at a cost that is still trivial for a box model.
- **Adaptive substepping.** Out of scope here (that is the diffrax branch's approach on the E3SM side); a fixed count keeps the CAM driver structurally faithful to "CAM at a finer dt".
10 changes: 10 additions & 0 deletions docs/PROGRESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ A running, append-only log of project milestones. Most-recent entry on top. Upda
Each entry: date, short title, links to commits / PRs, one-paragraph summary.

---
## 2026-08-25/26 — CAM driver: sulfeq cluster + full CAM chain, end-to-end validated; MAM5's first physics (branch `feat/cam-driver`)

- PR: [#74](https://github.com/reflective-org/MAM4-JAX/pull/74) (open — owner is holding merges to `main`). Plan: `docs/plans/025-cam-driver.md` §6–7 (G0–G5, all done). Merged `main`@v0.4.0 and PR #73's topology branch into the branch en route.
- **What landed**: the complete CESM/CAM driver for the SO4-only box scope — see the new "CESM/CAM variant" section in `FEATURES.md` for the per-component table. Every component validated against fresh captures of the real Fortran (five new `tools/capture_*` in `mam-box-fortran`), both topologies, machine ε at each layer; end-to-end 120-step trajectories match the fixdumfac reference builds at the reference's own print floor with total sulfur at 4.5e-15. This is the first time `cam_mam5` (incl. `coarse_strat`) produced physics against an independent reference.
- **ADR-021**: `n_substeps` defaults to 16 (owner decision) — CAM's own un-substepped splitting is 26–78% from converged at dt=30 s with nucleation active; a deliberate, documented deviation from the defaults-reproduce-the-reference convention. Parity tests pin `n_substeps=1`.
- **Findings recorded**: the box reference's `is_first_step()` shim is true every step, so the lagged-wet-diameter sulfeq feedback does not exist in the reference (`reseed_dgnwet_each_step` exposes both behaviours); `shr_const_rgas` is the product 8314.467591 (the rounded 8314.46 cost 2e-6 in the coag number solves); upstream surf-tension interpolation bug in `calc_h2so4_equilib_mixrat` (written up in `mam-box-fortran/docs/bugs/`); CAM's `gas_aer_uptkrates` is a third variant, distinct from both E3SM ports.
- Suite: **270 passed**.

---


## 2026-08-25 — Primary-carbon aging + float32-safe coagulation mass transfer (`main`)

Expand Down
Binary file added docs/figures/cam_vs_e3sm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading