Differentiable alpha loss fraction and in-memory Vmec construction - #61
Differentiable alpha loss fraction and in-memory Vmec construction#61rogeriojorge wants to merge 1 commit into
Conversation
…rrogate Vmec.from_arrays builds the field from wout coefficients already in memory, so a caller holding live JAX arrays no longer has to write a netCDF file to trace in a VMEC equilibrium. Tracing.soft_loss_fraction is a smooth surrogate for the final loss fraction, giving alpha confinement a usable gradient, and loss_soft_lost_fraction exposes it as an objective. The exact loss_fraction diagnostic is untouched.
Codecov Report✅ All modified and coverable lines are covered by tests.
|
|
@eduardolneto @EstevaoMGomes I'll be asking for your reviews in a few ESSOS PRs, this one is the first one, as it will unblock a VMEX PR and merge https://github.com/uwplasma/VMEX to do stellarator optimization |
There was a problem hiding this comment.
Pull request overview
Adds in-memory VMEC construction from live JAX arrays and a differentiable soft alpha-particle loss objective while preserving the exact diagnostic.
Changes:
- Adds
Vmec.from_arraysand shared VMEC array metadata. - Adds
Tracing.soft_loss_fractionandloss_soft_lost_fraction. - Adds reconstruction, differentiation, and objective integration tests.
Review findings:
essos/dynamics.py:1326— Moderate (2 votes): non-finite solver failures may be misclassified as particle losses.essos/fields.py:229— Moderate (3 votes): concrete NumPy/JAX integer metadata should be normalized to Python integers.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_objective_functions.py |
Tests objective integration. |
tests/test_fields.py |
Tests VMEC reconstruction and gradient flow. |
tests/test_dynamics.py |
Tests surrogate convergence and end-to-end differentiation. |
essos/objective_functions.py |
Exposes the differentiable loss objective. |
essos/fields.py |
Supports in-memory VMEC construction; see finding above. |
essos/dynamics.py |
Implements the differentiable loss surrogate; see finding above. |
Suppressed comments (4)
essos/dynamics.py:1313
widthis used as a divisor in both the softmax temperature and sigmoid scale, sowidth=0produces NaNs and a negative width reverses the surrogate. Reject non-positive widths before this computation so an invalid optimization hyperparameter cannot silently produce an invalid objective.
def soft_loss_fraction(self, r_max=0.99, width=0.02):
essos/dynamics.py:1327
- This method assumes
trajectories[:, :, 0]is VMEC's flux coordinates, but the publicTracingAPI also supports Cartesian guiding-center/full-orbit traces andboundary=SurfaceClassifierlosses. For those calls the first component isx, while the exact diagnostic uses boundary distance, sosoft_loss_fractionsilently optimizessigmoid(x)and ignores the supplied boundary. Add a boundary-distance surrogate or reject unsupported field/model combinations instead of returning an incorrect objective.
trajectories_r = self.trajectories[:, :, 0]
trajectories_r = jnp.where(jnp.isfinite(trajectories_r), trajectories_r, 1.0)
r_soft = jnp.sum(trajectories_r * jax.nn.softmax(trajectories_r / width, axis=1), axis=1)
essos/dynamics.py:1326
- Please add a regression test for the non-finite radial-save branch. VMEC boundary-event traces can retain non-finite post-event saves, and this replacement is what gives the surrogate a loss signal for them; the current tests only exercise finite prescribed trajectories, so this behavior could regress unnoticed.
trajectories_r = jnp.where(jnp.isfinite(trajectories_r), trajectories_r, 1.0)
essos/fields.py:239
- This constructor creates a fresh
Vmecinstance for every call, but all VMEC field kernels (B,AbsB, etc.) are JIT-compiled withselfas a static argument. Since the default object hash/equality are identity-based, rebuilding the field from live arrays on each objective/gradient evaluation gives each kernel a new static cache key and forces recompilation (and retains the old compiled executable). In a vmex optimization loop this can dominate runtime and grow memory; use a stable static wrapper or make the coefficient state dynamic (for example, a pytree-based field) so coefficient updates reuse the compiled kernels.
self = cls.__new__(cls)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Saves past a terminating event count as reaching the boundary. | ||
| """ | ||
| trajectories_r = self.trajectories[:, :, 0] | ||
| trajectories_r = jnp.where(jnp.isfinite(trajectories_r), trajectories_r, 1.0) |
| tracers reach B, AbsB and the traced trajectories and the field stays | ||
| differentiable with respect to its spectral coefficients. ``nfp``, ``ns`` and | ||
| the mode numbers set array shapes and must be concrete. | ||
| """ |
Makes the alpha-particle loss fraction usable as a differentiable objective driven by a VMEC equilibrium whose spectral coefficients are live JAX arrays. This unblocks
vmex --trace wout_XXX.ncand boundary optimization against alpha losses in vmex, which holds the wout arrays in memory as tracers.Gap 1:
Vmeccould only be built from a netCDF fileVmec.__init__opened the wout withDataset(...). A caller already holdingbmnc,rmnc,bsup*, ... as traced JAX arrays had to write a file first, which severs the gradient path.API
Arguments carry the wout variable names and are stored as passed — no
np.asarrayround trip — so tracers reachB,AbsBand the traced trajectories.nfp,nsand the mode numbers set array shapes and must be concrete.VMEC_WOUT_ARRAYSnames the array arguments for callers that want to forward them in bulk.Vmec(wout_filename)reads the same values from the file and hands them to the same code path, so the file route is unchanged.Verified against
examples/input_files/wout_LandremanPaul2021_QA_reactorScale_lowres.nc, comparing a file-builtVmecwith one rebuilt from its own arrays:Bat 3 pointsAbsBat 3 pointssurface.gamma(nfp, ns, mpol, ntor)(2, 50, 4, 5)bothtmax=4e-6Gap 2: the loss fraction had an identically zero gradient
Tracing.loss_fractionbuildstrajectories_r >= r_max, thenargmax,bincount,cumsum. All piecewise-constant in the trajectories, sojax.gradreturns exactly zero and the objective is only reachable by gradient-free optimizers.Measured with 8 alphas launched at
s=0.85,tmax=5e-6, 30 saves,atol=rtol=1e-5, differentiating with respect to a scale on every B-related coefficient (bmnc,bsub*,bsup*) passed throughVmec.from_arrays, atr_max=0.88:jax.gradloss_fraction(exact)soft_loss_fraction,width=0.05soft_loss_fraction,width=0.02soft_loss_fraction,width=0.005The surrogate
For each particle
iwith radial coordinater_i(t):r_softis the softmax-weighted mean over time: a soft maximum that spreads the gradient over every sample near the radial excursion peak.jnp.maxis subdifferentiable but routes the gradient through the single peak sample, which is far too sparse to optimize against. It is also preferred over a plainwidth * logsumexp(r/width), which carries awidth * log(n_times)offset fixed by how densely the trajectories are saved rather than by the orbit; the softmax-weighted mean is exact for a flat profile and its bias on a peaked one isO(width)with non_timesdependence. Non-finite saves past a terminating event are treated as having reached the boundary.The single
widthkeyword sets both the soft-maximum temperature and the crossing sharpness, both in units ofs.Convergence in value
Same trace as above,
r_max=0.88, exactloss_fraction = 0.2500:On four prescribed sinusoidal radial excursions peaking at
s = 0.70, 0.93, 0.86, 0.99withr_max=0.9, exact loss fraction 0.5, the per-particle gradient atwidth=0.01is[3.1e-08, 1.757, 0.271, 5.1e-03]against[0, 0, 0, 0]for the exact one. The near-miss particle at 0.86 carries real weight, which is what lets an optimizer see a boundary before losses appear.Objective
Placed with the other particle confinement losses in
essos/objective_functions.py.The exact diagnostic is unchanged
Tracing.loss_fractionis untouched. It remains the physically correct reported quantity and still returns(loss_fractions, total_particles_lost, lost_times)exactly as before.soft_loss_fractionsits alongside it and is only for driving gradients.Tests
Added to the existing files, no new test module:
tests/test_fields.py—from_arraysreproduces the file-built field;jax.gradflows throughfrom_arraysintoAbsB.tests/test_dynamics.py— surrogate converges to the exact value aswidthshrinks; exact gradient is zero where the surrogate's is not;from_arraystraces identically to the file route;soft_loss_fractiondifferentiates VMEC coefficients end to end.tests/test_objective_functions.py—loss_soft_lost_fractionin the mocked particle-loss test.