From ed870e688059539c95d9ece362cac958f3b3f205 Mon Sep 17 00:00:00 2001 From: Yun Date: Tue, 8 Sep 2026 20:07:05 +0900 Subject: [PATCH] Fix #607: standardize beta_n unit string to dimensionless '1' - Change beta_n unit string from '% m T / MA' to '1' in derive_global_descriptors - Record conventional '% m T / MA' scaling in DerivedValue.quality metadata - Document Troyon formula and IMAS dimensionless convention under Convention - Remove #607 tracking note from Limitations - Update tutorial notebook and test assertions --- notebooks/mhd_equilibrium_analysis.ipynb | 4 ++-- test/test_parametric_equilibrium.py | 11 +++++++++++ vaft/process/_equilibrium_parametric.py | 20 ++++++++++++++------ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/notebooks/mhd_equilibrium_analysis.ipynb b/notebooks/mhd_equilibrium_analysis.ipynb index c1e9c260d..2b685ee52 100644 --- a/notebooks/mhd_equilibrium_analysis.ipynb +++ b/notebooks/mhd_equilibrium_analysis.ipynb @@ -207,7 +207,7 @@ "q95 8.671 - [linear interpolation]\n", "q_edge 11.73 - [profile endpoint]\n", "beta_t 0.001274 - [volume and pressure integration]\n", - "beta_n 0.0702 % m T / MA [derived]\n", + "beta_n 0.0702 - [derived]\n", "li_virial 0.6876 - [Lao virial closure]\n", "shafranov_shift 0.0284 m [difference]\n", "magnetic_shear 129-point profile - [finite differences]\n" @@ -221,7 +221,7 @@ " (\"elongation\", 1.0, \"-\"), (\"triangularity_upper\", 1.0, \"-\"),\n", " (\"triangularity_lower\", 1.0, \"-\"), (\"volume\", 1.0, \"m^3\"),\n", " (\"cross_section_area\", 1.0, \"m^2\"), (\"q0\", 1.0, \"-\"), (\"q95\", 1.0, \"-\"),\n", - " (\"q_edge\", 1.0, \"-\"), (\"beta_t\", 1.0, \"-\"), (\"beta_n\", 1.0, \"% m T / MA\"),\n", + " (\"q_edge\", 1.0, \"-\"), (\"beta_t\", 1.0, \"-\"), (\"beta_n\", 1.0, \"-\"),\n", " (\"li_virial\", 1.0, \"-\"), (\"shafranov_shift\", 1.0, \"m\"),\n", " (\"magnetic_shear\", 1.0, \"-\"),\n", ")\n", diff --git a/test/test_parametric_equilibrium.py b/test/test_parametric_equilibrium.py index 75faebf6f..1a25f7fea 100644 --- a/test/test_parametric_equilibrium.py +++ b/test/test_parametric_equilibrium.py @@ -129,6 +129,9 @@ def test_global_descriptors_and_radial_coordinates_have_definitions(): assert descriptors["volume"].value == pytest.approx(2 * np.pi**2 * 1.0 * 0.35 * 0.5, rel=2e-3) assert descriptors["thermal_energy"].available assert descriptors["beta_t"].available + assert descriptors["beta_n"].available + assert descriptors["beta_n"].unit == "1" + assert descriptors["beta_n"].quality.get("scaling_convention") == "% m T / MA" assert len(descriptors.rational_surfaces[1.5]) == 1 radial = derive_radial_coordinates(eq) np.testing.assert_allclose(radial["psi_n"].value[[0, -1]], [0, 1]) @@ -576,3 +579,11 @@ def test_explicit_flux_tolerance_overrides_the_derived_window(): strict = derive_boundary_representation(eq, flux_tolerance=0.0) assert strict.provenance.tolerances["xpoint_flux_psi_n"] == 0.0 assert not strict.topology.is_diverted + + +def test_unavailable_beta_n_has_dimensionless_unit(): + eq = _analytic_equilibrium() + eq_no_p = EquilibriumData(**{**eq.__dict__, "pressure": None}) + descriptors = derive_global_descriptors(eq_no_p) + assert not descriptors["beta_n"].available + assert descriptors["beta_n"].unit == "1" diff --git a/vaft/process/_equilibrium_parametric.py b/vaft/process/_equilibrium_parametric.py index b704149cb..16337a873 100644 --- a/vaft/process/_equilibrium_parametric.py +++ b/vaft/process/_equilibrium_parametric.py @@ -779,7 +779,12 @@ def derive_global_descriptors( centimetres apart. The poloidal field is computed through the equilibrium's own declared COCOS and per-radian flag, so a wrongly declared convention propagates into every field-derived quantity here. The magnetic shear is - taken against the poloidal radius. + taken against the poloidal radius. The normalized beta follows the Troyon + convention ``beta_N = 100 * beta_t * a * |B_t0| / |I_p|`` (with ``a`` in + metres, ``B_t0`` in tesla, and ``I_p`` in megamperes); its published unit + is ``"1"`` (dimensionless) conforming to IMAS ``global_quantities.beta_normal``, + with the conventional ``% * m * T / MA`` scaling factor folded into the value + and recorded in the derivation quality metadata. Applicability ------------- @@ -790,9 +795,8 @@ def derive_global_descriptors( Every quantity degrades independently: a missing pressure profile costs the energies and the betas but not the geometry. A failure inside the pressure integration is captured as a warning on the returned report rather than - raised. The normalized beta carries a non-SI unit string where the rest of - the module is SI, tracked in #607. The virial internal inductance is - ill-conditioned as its coefficient approaches one. + raised. The virial internal inductance is ill-conditioned as its coefficient + approaches one. Provenance ---------- @@ -916,11 +920,15 @@ def derive_global_descriptors( values["beta_t"] = _derived(eq, beta_t, "1", "2*mu0*

/Bt0^2", "volume and pressure integration", ("pressure", "bt0")) if geometry and eq.ip not in (None, 0): beta_n = 100 * beta_t * values["minor_radius"].value * abs(eq.bt0) / (abs(eq.ip) / 1e6) - values["beta_n"] = _derived(eq, beta_n, "% m T / MA", "100*beta_t*a*abs(Bt0)/abs(Ip_MA)", "derived", ("beta_t", "minor_radius", "bt0", "ip")) + values["beta_n"] = _derived( + eq, beta_n, "1", "100*beta_t*a*abs(Bt0)/abs(Ip_MA)", "derived", + ("beta_t", "minor_radius", "bt0", "ip"), + {"scaling_convention": "% m T / MA"}, + ) if average_pressure is not None and bpa not in (None, 0): values["beta_p_boundary_average"] = _derived(eq, 2 * MU0 * average_pressure / bpa**2, "1", "2*mu0*

/_boundary^2", "boundary-field average", ("pressure", "psi", "lcfs")) for name, definition in (("beta_t", "2*mu0*

/Bt0^2"), ("beta_n", "100*beta_t*a*abs(Bt0)/abs(Ip_MA)"), ("beta_p_boundary_average", "2*mu0*

/_boundary^2")): - values.setdefault(name, _unavailable(eq, "1" if name != "beta_n" else "% m T / MA", definition, "required pressure, geometry, current, or magnetic field is unavailable")) + values.setdefault(name, _unavailable(eq, "1", definition, "required pressure, geometry, current, or magnetic field is unavailable")) for name in ("s1", "s2", "s3", "alpha"): values[name] = _derived(eq, virial[name], "1", f"Shafranov boundary integral {name}", "shafranov_integrals", ("psi", "lcfs")) if name in virial else _unavailable(eq, "1", f"Shafranov {name}", "valid grid, LCFS, and boundary poloidal field are required") values["li_virial"] = _derived(eq, virial["li"], "1", "[S1/2+S2/2*(1-RT/R0)-S3]/(alpha-1)", "Lao virial closure", ("psi", "f", "pressure", "lcfs")) if "li" in virial else _unavailable(eq, "1", "Lao virial internal inductance", "F, pressure, field grid, and a well-conditioned alpha are required")