Skip to content

Support different sources for trapped particle fraction#2283

Open
theo-brown wants to merge 5 commits into
google-deepmind:mainfrom
theo-brown:trapped-particle-integral
Open

Support different sources for trapped particle fraction#2283
theo-brown wants to merge 5 commits into
google-deepmind:mainfrom
theo-brown:trapped-particle-integral

Conversation

@theo-brown

Copy link
Copy Markdown
Collaborator

The Sauter model for trapped particle fraction is not valid across all aspect ratios.

This PR adds additional options for f_trap:

  1. Loading from file. CHEASE and IMAS both natively provide f_trap.
  2. Computing from scratch. Can be computed using contour integrals, for EQDSK and IMAS.
  3. Fall back to the Sauter method.

As this is done once at geometry construction time, it is a one-time cost.

@theo-brown
theo-brown requested a review from jcitrin July 13, 2026 12:31
(1/4) Adds support for different methods of computing f_trap. Defaults to Sauter analytic approximation.
(2/4) Adds support for loading the trapped fraction from CHEASE and IMAS, where it is already computed.
(3/4) Adds support for computing the trapped fraction from the full bounce-averaged integral over the 2D equilibrium, for EQDSK and IMAS.
(4/4) Documents the SAUTER/FILE/EXACT options and switches the STEP flattop example to use the exact bounce-averaged integral.
@theo-brown
theo-brown force-pushed the trapped-particle-integral branch from 36f8124 to e8232c1 Compare July 13, 2026 12:44
@google-deepmind google-deepmind deleted a comment from google-cla Bot Jul 13, 2026
@theo-brown
theo-brown requested a review from Nush395 July 15, 2026 10:04

@jcitrin jcitrin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great stuff. Beyond the comments in the review:

  1. Please solve merge conflicts
  2. The PR is very large and contains several independent refactors/features. For example, the refactor of the sauter formula, and the modification of its calculation in GeometryIntermediates instead of in the neoclassical calculation functions (which is what is leading to the nc file updates), is its own PR. Then beyond that, could consider splitting the rest into several smaller PRs (e.g. on the new config plumbing, then implementation of the FILE cases, then the EXACT formulas and implementation, etc.)
  3. Should there be a new sim test or two with EXACT and FILE cases?

Comment on lines +39 to +44
self.assertTrue(np.all(trapped_fraction >= 0.0))
self.assertTrue(np.all(trapped_fraction <= 1.0))
self.assertGreater(
np.mean(np.diff(trapped_fraction) >= -1e-6),
0.8,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: subtests

Comment on lines +97 to +108
# No trapped particles on the magnetic axis, where B is uniform.
self.assertAlmostEqual(float(trapped_fraction[0]), 0.0)
# The trapped particle fraction is a fraction, so must lie in [0, 1].
self.assertTrue(np.all(trapped_fraction >= 0.0))
self.assertTrue(np.all(trapped_fraction <= 1.0))
# Trapped fraction increases with normalized radius over most of the
# profile (small deviations from strict monotonicity are possible near
# the edge for diverted geometries, due to the X-point).
self.assertGreater(
np.mean(np.diff(trapped_fraction) >= -1e-6),
0.8,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: subtests

Comment on lines +89 to +98
self.assertIsNotNone(trapped_fraction)
self.assertTrue(np.all(trapped_fraction >= 0.0))
self.assertTrue(np.all(trapped_fraction <= 1.0))
# Trapped fraction increases with normalized radius over most of the
# profile (small deviations from strict monotonicity are possible near
# the edge for diverted geometries).
self.assertGreater(
np.mean(np.diff(trapped_fraction) >= -1e-6),
0.8,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: subtests

Comment on lines +135 to +140
self.assertTrue(np.all(trapped_fraction >= 0.0))
self.assertTrue(np.all(trapped_fraction <= 1.0))
self.assertGreater(
np.mean(np.diff(trapped_fraction) >= -1e-6),
0.8,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: subtests

Comment on lines +328 to +341
case base.TrappedFractionSource.FILE:
if not IMAS_data.profiles_1d.trapped_fraction:
raise ValueError(
"trapped_fraction_source=FILE requires the equilibrium IDS to"
" populate profiles_1d.trapped_fraction, but this IDS does"
" not. Use trapped_fraction_source=EXACT to compute it directly"
" from the 2D equilibrium instead, or SAUTER for the analytic"
" approximation."
)
trapped_fraction = np.asarray(IMAS_data.profiles_1d.trapped_fraction)
case base.TrappedFractionSource.EXACT:
exact_trapped_fraction = _calculate_exact_trapped_fraction(
IMAS_data, np.asarray(IMAS_data.profiles_1d.gm5)
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It would be good to have a test that tests whether the FILE and EXACT calculations agree, for an IDS that has both available

Comment on lines +452 to +455
trapped_fraction = formulas.calculate_sauter_trapped_fraction(
epsilon=LY['epsilon'],
delta=0.5 * (LY['deltau'] + LY['deltal']),
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can future proof with a match case, or just have a TODO to extend when MEQ team provides FILE values

_IMAS_RECTANGULAR_GRID_TYPE = 1


def _calculate_exact_trapped_fraction(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not unit tested.

Also, playing Devil's Advocate. Is this needed in TORAX? If trapped fraction using the same formula is part of the equilibrium IDS, then could it just be the responsibility of the data owner to make sure it's provided? In other words, is there a use-case of having both EXACT and FILE for IMAS?

Comment on lines +354 to +361
exact_is_unreliable = (
np.isnan(exact_trapped_fraction)
| (exact_trapped_fraction < 0.0)
| (exact_trapped_fraction > 1.0)
)
trapped_fraction = np.where(
exact_is_unreliable, sauter_trapped_fraction, exact_trapped_fraction
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same comment as in EQDSK regarding the silent overwriting by Sauter

The effective trapped particle fraction of this flux surface.
"""
B_max = B.max()
lam = np.linspace(0.0, 1.0, 101) / B_max

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why 101 and not some other number? Also, this could be a module private constant variable

)


def calculate_bounce_averaged_trapped_fraction(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not unit tested

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