ohmic baseline mode: FUSE+IDA hybrid current with explicit Ip closure and a closure_channel toggle - #42
ohmic baseline mode: FUSE+IDA hybrid current with explicit Ip closure and a closure_channel toggle#42d-burg wants to merge 20 commits into
Conversation
…closed on j_ohmic only
…, not the cylindrical l_i proxy; proxy recorded alongside; refuse if FUSE total misses Ip by >2%
…linterp) on a copy_eq snapshot; OFT compute_flux_integral and the cylindrical proxy recorded only
… legacy-ravgs OFT builds; record fsa-convention Ip as cross-check
… total carries Ip (user-approved 2026-08-21)
…e term carried once (was triple-counted); 0.05% post-closure check
…ll Ip integrals on that snapshot (SWB-landed geometry read +31% of Ip on an ohmic-ramp slice)
…j_BS (j_ohmic untouched); the two channels bracket the closure uncertainty
…baseline_mode=ohmic)
…nt; ohmic-channel closure manufactures q_min inflation at high f_BS (see the q_min source comparison)
…cale_jBS as the per-draw spike -- draws were losing (1-bs)/bs of the pedestal bootstrap; zero error only at bs_scale=1
…rium roundtrip is resolution-INDEPENDENT at 1.305%)
…trips the equilibrium own Ip) instead of sign(dot(eq j_phi, cp j_tor)). The heuristic tested a current-direction convention, not the P-prime term sign: on ohmic ramp shots (eq j_phi > 0 while FUSE dd carries Ip < 0) it flipped and cost +1.31%25 of Ip. Ramp roundtrip 1.306%25 -> 0.002%25; flattop unchanged (bit-for-bit).
…iptions Public repo -- shot numbers stay out of code, docs and comments. The measured percentages are kept; only the case identifiers are generalised. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tests/test_ohmic_closure.py: the affine FSA measure (c counted once, not per component -- the triple-count regression), exact cross-convention agreement on the equilibrium's own GS profile (the identity behind the 0.5% roundtrip gate), pprime_sign behaviour, both closure channels' scale formulas closing Ip exactly, validator acceptance, and defaults. - _validate_workflow now actually refuses generate() in ohmic mode (baseline-only until the draw path's sigma=0 reproduction is verified) -- the PR description promised this guard but it was never implemented; workflow='custom' downgrades it to a warning as usual. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…draw scale distribution's center
OFT applies scale_jBS INSIDE solve_with_bootstrap, so a 1.0 reference
makes the delta composition
bs*SWB0 + bs*SWB_pert - 1.0*SWB0
lose (1-bs)/bs of the pedestal bootstrap on every draw -- zero error only
at bs_scale=1, which is why the original validation passed.
The center is sigma0_reference_scale(jBS_scale_range): the midpoint of
the configured range (run.py has already re-centred it on bl.bs_scale),
or 1.0 when no range is set (draws then all run at scale 1.0). The
per-draw scale_jBS local CANNOT be used here -- it is bound inside the
draw loop, and referencing it before that raised an UnboundLocalError
that the cache block's blanket except swallowed into a silent
cache-disable fallback.
tests/test_delta_cache_reference.py pins the center semantics, the
helper wiring, and the scoping property itself (an AST load-before-store
check over generate_bouquet's locals that fails on the broken form).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he gate reference, refuse NaN, extract close_ip, plug the jphi_diff leak Adversarial-review findings on the ohmic baseline mode: - P' sign: identically +1 by construction -- the affine c term is built from the ANCHOR's own P' on the anchor's own geometry, so the GS identity fixes its orientation and the data's current-direction convention enters only through the sign of the linear parts. The 'self-consistency probe' this replaces was a tautology (the probe was built with pprime_sign=+1 and reproduced the anchor's Ip to machine precision, so +1 could never lose), able to mask up to ~2c (~6% of Ip) on a genuinely flipped case with no gate able to catch it. - Roundtrip gate now references the anchor's ACHIEVED Ip, not Ip_target: a first pass converging 0.6% off target failed a perfect measure, and a 0.4% solver offset ate most of the 0.5% budget. Ip_anchor is recorded in ip_closure. - NaN can no longer pass the gates: the measure (weights, affine c, roundtrip) is checked finite by name, fsa_current_geometry now validates <1/R^2> and P' finiteness, and close_ip refuses NaN scales. - Closure algebra extracted to utils.close_ip (both channels, the ~0 divisor refusals, and the [0.2, 5] bounds); run.py and the tests now exercise the same shipped formulas instead of parallel re-derivations. - closure_channel is validated before solve_with_bootstrap (and in the workflow validator) instead of after the full SWB iteration sequence. - bl.jphi_diff is cleared after being recorded: generate() passes it through unconditionally, so a workflow='custom' ohmic generate folded it into every draw while the baseline solve omitted it. - sgn is taken from the linear total (the c term could flip the vote on a low-Ip ramp slice); recorded-only would-be scales bind each integrator once and record NaN instead of dividing by ~0. - Comment corrections: the reader's j_inductive is a RESIDUAL (j_tor - j_BS - j_NBI - j_RF), not to_toroidal(j_ohmic) -- verified on postdictive FUSE output, where the sawteeth source nets exactly zero current and is already folded into the diffused j_ohmic, while a near-axis j_non_inductive artifact (~0.4% of Ip at flattop) is the real unmodelled term the closure scale absorbs; the closure-channel default is 'bootstrap', not 'ohmic'. Also cherry-picked: the corrected issue-#44 sigma0 reference fix (sigma0_reference_scale + its tests) replacing the UnboundLocalError form. Fast suite: 414 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2f69012 to
54a5265
Compare
There was a problem hiding this comment.
🟡 Changes recommended
utils.close_ip has a concrete edge-case correctness bug (possible division by zero when Ip_target_signed is 0) and a bounds semantics mismatch that should be fixed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces an IMAS-path third baseline mode (jBS_baseline_mode="ohmic") that builds a hybrid current recipe (FUSE ohmic + SWB bootstrap + FUSE fixed components) and closes (I_p) via an explicit affine FSA-measure closure, plus a closure_channel toggle to attribute the deficit to either the bootstrap or ohmic channel.
Changes:
- Add
GenerationConfig.closure_channeland newjBS_baseline_mode="ohmic"workflow/validation behavior. - Implement
utils.close_ip(...)for shared closure algebra and add additional FSA geometry finiteness guards. - Add targeted tests for the affine FSA measure, closure algebra, workflow guard, and the sigma=0 delta-cache reference scaling.
File summaries
| File | Description |
|---|---|
bouquet/utils.py |
Adds close_ip helper and strengthens fsa_current_geometry finiteness checks. |
bouquet/run.py |
Implements the new "ohmic" baseline mode (anchor-geometry snapshot, closure logic, diagnostics) and extends workflow validation. |
bouquet/TokaMaker_interface.py |
Adds sigma0_reference_scale and uses it for the pre-loop SWB cache reference scale. |
bouquet/config.py |
Documents and adds GenerationConfig.closure_channel. |
bouquet/baseline.py |
Adds Baseline.ohm_scale and Baseline.ip_closure bookkeeping fields. |
tests/test_ohmic_closure.py |
New solve-free tests for affine measure, convention identity, closure algebra, and workflow guard behavior. |
tests/test_delta_cache_reference.py |
New tests pinning sigma=0 reference scaling semantics and guarding against the previous scoping regression. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 6
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| _npsi_env = __import__("os").environ.get("BQ_FSA_NPSI") | ||
| _cap = _cef(mygs, | ||
| npsi=int(_npsi_env) if _npsi_env else max(257, psi_N.size), | ||
| psi_pad=psi_pad, exact_inv_R2=True) |
| Ip_t = abs(float(Ip_target_signed)) | ||
| deficit = float(Ip_target_signed) - float(c_affine) - float(ip_fix) |
| from .utils import (fsa_current_geometry, Ip_fsa_integral, | ||
| Ip_fsa_weights, eq_jphi_profile, close_ip) |
| "(expected 'ohmic' or 'bootstrap')") | ||
| lo, hi = scale_bounds | ||
| for name, s in zip(("ohm_scale", "bs_scale"), scales): | ||
| if not (lo < s < hi): |
| src = inspect.getsource(tmi) | ||
| assert "_scale_ref = sigma0_reference_scale(jBS_scale_range)" in src |
| import warnings | ||
|
|
||
| import numpy as np | ||
| import pytest | ||
| from scipy.integrate import trapezoid |
jBS_baseline_mode="ohmic": FUSE+IDA hybrid current with explicit Ip closureAdds a third baseline mode for the IMAS path:
with Ip closed exactly on the LCFS-truncated FSA measure (
utils.Ip_fsa_integral,jphi-linterp convention, affine P′ term carried once), evaluated on a
copy_eq()snapshot of the anchor geometry taken before
solve_with_bootstrap(theSWB-landed geometry mis-integrates Ip by tens of percent in high-βp cases).
Closure channel toggle (
GenerationConfig.closure_channel)The hybrid components generally do not sum to Ip; the deficit has to land somewhere:
closure_channel="bootstrap"(default): keepj_inductiveexactly as FUSEdiffused it and rescale
j_BS—lin(ohm) + s_BS·lin(bs) + lin(fix) + c = Ip.Default because it preserves the source's core current: closure on
j_ohmichollows the core and inflates
q_minagainst the measured value whenever therecomputed bootstrap fraction is far above the source's.
closure_channel="ohmic": rescalej_inductiveonly, preserving its diffusedshape —
s·lin(ohm) + lin(bs) + lin(fix) + c = Ip. Useful as the other edge ofthe closure bracket.
The two are the extreme attributions of the same deficit; running both brackets the
closure uncertainty of the recipe. The applied scale (
ohm_scale/bs_scale) is afirst-class per-slice output, with the full integral decomposition recorded in
baseline.ip_closure(roundtrip check, FUSE-total error, per-component linear parts,and recorded-only alternative integrators).
P′-term sign: +1 by construction
The affine c term is built from the anchor's own P′ on the anchor's own
geometry, so the GS identity fixes its orientation; the external profile's
current-direction convention enters only through the sign of the linear parts.
Two failed detectors are retired: the original
sign(dot(eq j_phi, cp j_tor))heuristic (flipped on ohmic-ramp slices, +1.31% of Ip) and the
"self-consistency probe" that replaced it, which adversarial review showed to
be a tautology — the probe was built with
pprime_sign=+1and reproduced theanchor's Ip to machine precision, so +1 could never lose.
Hardening from adversarial review
(recorded as
ip_closure["Ip_anchor"]), notIp_target— a first passconverging 0.6% off target no longer fails a perfect measure.
checked finite by name,
fsa_current_geometryvalidates<1/R^2>/P′finiteness, and
close_iprefuses NaN scales.utils.close_ip(both channels, ~0-divisorrefusals, the [0.2, 5] bounds) — run.py and the tests exercise the same
shipped formulas.
closure_channelis validated beforesolve_with_bootstrap(a typo nolonger burns the full SWB iteration sequence).
bl.jphi_diffis recorded and then cleared in ohmic mode, so aworkflow='custom'generate can no longer fold the dropped anchor intodraws the baseline solve omitted.
j_inductiveis documented as what it is — a residualj_tor − j_BS − j_NBI − j_RF, notto_toroidal(j_ohmic); on postdictiveFUSE output the sawteeth source nets exactly zero current (already folded
into the diffused j_ohmic), while a near-axis
j_non_inductiveartifact(~0.4% of Ip at flattop) is the real unmodelled term the closure scale
absorbs.
Guards
within 0.5 %;
its σ=0 reproduction has been verified —
_validate_workflowraises ongenerate()(baseline-only), with the usualworkflow='custom'downgradeto a warning for the verification work itself.
Tests
tests/test_ohmic_closure.py(solve-free, synthetic FSA geometry): the affinemeasure identity (
I_p[J] = ∫wJ + c, withccounted once, not percomponent — the regression the linear-parts closure exists to prevent), exact
cross-convention agreement of the equilibrium's own GS profile between
jphi-linterpandfsa(the identity behind the 0.5 % roundtrip gate),pprime_signbehaviour, both closure channels' scale formulas closing Ipexactly, validator acceptance/refusal, and config/Baseline defaults.
Diagnostics
BQ_FSA_NPSIenv var: FSA quadrature-resolution knob (diagnostic; theramp-equilibrium roundtrip is resolution-independent).
BQ_CLOSURE_DUMP=<path.npz>: dumps the measure's ingredients so a roundtriperror can be localised in psi_N.
Not included / known limits
jphi_diffFUSE-equilibrium anchor is not applied in ohmic mode (recorded).the source reconstruction's (core hollowing, q0 rising against the measured value)
— that regime is exactly what the
bootstrapchannel + bracketing is for.closure_channel="mse") is split outinto a stacked follow-up PR (
feat/mse-closure-channel) so this PR stays areviewable unit.
🤖 Generated with Claude Code