Skip to content

ohmic baseline mode: FUSE+IDA hybrid current with explicit Ip closure and a closure_channel toggle - #42

Open
d-burg wants to merge 20 commits into
mainfrom
hybrid-ohmic-mode
Open

ohmic baseline mode: FUSE+IDA hybrid current with explicit Ip closure and a closure_channel toggle#42
d-burg wants to merge 20 commits into
mainfrom
hybrid-ohmic-mode

Conversation

@d-burg

@d-burg d-burg commented Aug 24, 2026

Copy link
Copy Markdown
Owner

jBS_baseline_mode="ohmic": FUSE+IDA hybrid current with explicit Ip closure

Adds a third baseline mode for the IMAS path:

j_total = j_ohmic(FUSE) + j_BS(SWB on the kinetic source) + j_NBCD(FUSE)

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 (the
SWB-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): keep j_inductive exactly as FUSE
    diffused it and rescale j_BSlin(ohm) + s_BS·lin(bs) + lin(fix) + c = Ip.
    Default because it preserves the source's core current: closure on j_ohmic
    hollows the core and inflates q_min against the measured value whenever the
    recomputed bootstrap fraction is far above the source's.
  • closure_channel="ohmic": rescale j_inductive only, preserving its diffused
    shape — s·lin(ohm) + lin(bs) + lin(fix) + c = Ip. Useful as the other edge of
    the 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 a
first-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=+1 and reproduced the
anchor's Ip to machine precision, so +1 could never lose.

Hardening from adversarial review

  • The 0.5% roundtrip gate references the anchor's achieved Ip
    (recorded as ip_closure["Ip_anchor"]), not Ip_target — a first pass
    converging 0.6% off target no longer fails a perfect measure.
  • NaN cannot pass the gates: the measure (weights, affine c, roundtrip) is
    checked finite by name, fsa_current_geometry validates <1/R^2>/P′
    finiteness, and close_ip refuses NaN scales.
  • The closure algebra lives in utils.close_ip (both channels, ~0-divisor
    refusals, the [0.2, 5] bounds) — run.py and the tests exercise the same
    shipped formulas.
  • closure_channel is validated before solve_with_bootstrap (a typo no
    longer burns the full SWB iteration sequence).
  • bl.jphi_diff is recorded and then cleared in ohmic mode, so a
    workflow='custom' generate can no longer fold the dropped anchor into
    draws the baseline solve omitted.
  • The reader's j_inductive is documented as what it is — a residual
    j_tor − j_BS − j_NBI − j_RF, not to_toroidal(j_ohmic); on postdictive
    FUSE output the sawteeth source nets exactly zero current (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.

Guards

  • refuse if the measure does not round-trip the equilibrium's own profile to Ip
    within 0.5 %;
  • refuse if the closed hybrid misses Ip by more than 0.05 % (algebra error);
  • refuse scales outside [0.2, 5];
  • validator accepts the new mode; the UQ draw path refuses ohmic mode until
    its σ=0 reproduction has been verified — _validate_workflow raises on
    generate() (baseline-only), with the usual workflow='custom' downgrade
    to a warning for the verification work itself.

Tests

tests/test_ohmic_closure.py (solve-free, synthetic FSA geometry): the affine
measure identity (I_p[J] = ∫wJ + c, with c counted once, not per
component — the regression the linear-parts closure exists to prevent), exact
cross-convention agreement of the equilibrium's own GS profile between
jphi-linterp and fsa (the identity behind the 0.5 % roundtrip gate),
pprime_sign behaviour, both closure channels' scale formulas closing Ip
exactly, validator acceptance/refusal, and config/Baseline defaults.

Diagnostics

  • BQ_FSA_NPSI env var: FSA quadrature-resolution knob (diagnostic; the
    ramp-equilibrium roundtrip is resolution-independent).
  • BQ_CLOSURE_DUMP=<path.npz>: dumps the measure's ingredients so a roundtrip
    error can be localised in psi_N.

Not included / known limits

  • The jphi_diff FUSE-equilibrium anchor is not applied in ohmic mode (recorded).
  • Ohmic-only closure is invalid where the recomputed bootstrap fraction is far above
    the source reconstruction's (core hollowing, q0 rising against the measured value)
    — that regime is exactly what the bootstrap channel + bracketing is for.
  • An MSE-constrained third closure channel (closure_channel="mse") is split out
    into a stacked follow-up PR (feat/mse-closure-channel) so this PR stays a
    reviewable unit.

🤖 Generated with Claude Code

d-burg added 12 commits August 21, 2026 12:05
…, 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
…nt; ohmic-channel closure manufactures q_min inflation at high f_BS (see the q_min source comparison)
d-burg and others added 8 commits August 31, 2026 17:30
…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>
Copilot AI lite review requested due to automatic review settings September 4, 2026 19:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_channel and new jBS_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.

Comment thread bouquet/run.py
Comment on lines +739 to +742
_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)
Comment thread bouquet/utils.py
Comment on lines +677 to +678
Ip_t = abs(float(Ip_target_signed))
deficit = float(Ip_target_signed) - float(c_affine) - float(ip_fix)
Comment thread bouquet/run.py
Comment on lines +814 to +815
from .utils import (fsa_current_geometry, Ip_fsa_integral,
Ip_fsa_weights, eq_jphi_profile, close_ip)
Comment thread bouquet/utils.py
"(expected 'ohmic' or 'bootstrap')")
lo, hi = scale_bounds
for name, s in zip(("ohm_scale", "bs_scale"), scales):
if not (lo < s < hi):
Comment on lines +41 to +42
src = inspect.getsource(tmi)
assert "_scale_ref = sigma0_reference_scale(jBS_scale_range)" in src
Comment on lines +19 to +23
import warnings

import numpy as np
import pytest
from scipy.integrate import trapezoid
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.

2 participants