Skip to content

Conversation

@ddudt
Copy link
Collaborator

@ddudt ddudt commented Jan 7, 2026

Resolves #1840

Restructures and generalizes the Grid classes. The new ABC AbstractGrid that all other grids inherit from does not assume a particular coordinate system. The new ABC AbstractRTZGrid is specific to ($\rho,\theta,\zeta$) coordinates and is the parent class of all existing grid classes in flux coordinates. Most of the changes involved moving existing code into one of these new ABCs.

The goal is to allow for new grid classes in the future that represent other coordinate systems, without changing the existing public API.

To-Do:

  • Ensure tests pass
  • Generalize Basis dimensions (no references to flux coords)
  • Include isinstance(grid, AbstractGridCurve) checks wherever appropriate
  • Add 1D grids for coil filaments
  • Alias flux grid class names like Grid -> CustomGridFlux, etc. (do not deprecate)

@ddudt ddudt self-assigned this Jan 7, 2026
@ddudt ddudt added interface New feature or request to make the code more usable or compatibility with another code functionality New feature or request to do things the code can't do now. enhancement General label for enhancement. Please also tag with "Speed", "Interface", "Functionality", etc labels Jan 7, 2026
and self.grid.NFP != self.basis.NFP
and self.basis.N != 0
and grid.node_pattern != "custom"
and not isinstance(grid, Grid)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is equivalent to the old code, since the old Grid class was hard-coded to have node_pattern="custom". In the new code, node_pattern is only an attribute of the ConcentricGrid class.

@dpanici
Copy link
Collaborator

dpanici commented Jan 7, 2026

Add checks in objectives and inside geometric/magneticfield/coil compute methods to ensure grids passed in are correct (i.e. 1D for a filament coil, 3D for finite build coil, 1D or 2D rtz for surface)

@dpanici
Copy link
Collaborator

dpanici commented Jan 7, 2026

Rename AbstractRTZGrid to AbstractGridFlux instead of RTZ

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

Memory benchmark result

|               Test Name                |      %Δ      |    Master (MB)     |      PR (MB)       |    Δ (MB)    |    Time PR (s)     |  Time Master (s)   |
| -------------------------------------- | ------------ | ------------------ | ------------------ | ------------ | ------------------ | ------------------ |
  test_objective_jac_w7x                 |    1.92 %    |     3.843e+03      |     3.917e+03      |    73.83     |       38.77        |       35.84        |
  test_proximal_jac_w7x_with_eq_update   |   -0.75 %    |     6.614e+03      |     6.564e+03      |    -49.76    |       160.17       |       158.04       |
  test_proximal_freeb_jac                |    0.12 %    |     1.322e+04      |     1.323e+04      |    16.08     |       83.02        |       80.99        |
  test_proximal_freeb_jac_blocked        |   -0.32 %    |     7.564e+03      |     7.540e+03      |    -24.30    |       72.80        |       71.30        |
  test_proximal_freeb_jac_batched        |    0.20 %    |     7.479e+03      |     7.494e+03      |    15.05     |       72.90        |       73.29        |
  test_proximal_jac_ripple               |   -2.18 %    |     3.631e+03      |     3.552e+03      |    -79.10    |       63.47        |       64.59        |
  test_proximal_jac_ripple_bounce1d      |   -1.00 %    |     3.548e+03      |     3.512e+03      |    -35.59    |       74.33        |       74.43        |
  test_eq_solve                          |   -1.13 %    |     2.034e+03      |     2.011e+03      |    -23.03    |       92.25        |       91.33        |

For the memory plots, go to the summary of Memory Benchmarks workflow and download the artifact.

@codecov
Copy link

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 78.77049% with 259 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.91%. Comparing base (7aa703f) to head (d4b5ee4).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
desc/grid/surface.py 24.06% 142 Missing ⚠️
desc/grid/curve.py 72.72% 39 Missing ⚠️
desc/grid/flux.py 77.90% 38 Missing ⚠️
desc/grid/core.py 94.17% 11 Missing ⚠️
desc/geometry/core.py 58.82% 7 Missing ⚠️
desc/external/terpsichore.py 0.00% 6 Missing ⚠️
desc/plotting.py 93.22% 4 Missing ⚠️
desc/coils.py 88.23% 2 Missing ⚠️
desc/grid/utils.py 98.56% 2 Missing ⚠️
desc/particles.py 66.66% 2 Missing ⚠️
... and 6 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2053      +/-   ##
==========================================
- Coverage   94.52%   93.91%   -0.62%     
==========================================
  Files         102      107       +5     
  Lines       28712    29196     +484     
==========================================
+ Hits        27141    27419     +278     
- Misses       1571     1777     +206     
Files with missing lines Coverage Δ
desc/basis.py 97.74% <100.00%> (ø)
desc/compat.py 88.61% <100.00%> (ø)
desc/compute/_geometry.py 98.85% <100.00%> (+0.76%) ⬆️
desc/compute/_neoclassical.py 100.00% <ø> (ø)
desc/compute/_old.py 100.00% <ø> (ø)
desc/compute/data_index.py 95.89% <ø> (ø)
desc/compute/utils.py 97.71% <100.00%> (+0.01%) ⬆️
desc/equilibrium/initial_guess.py 93.79% <100.00%> (ø)
desc/geometry/curve.py 96.44% <100.00%> (ø)
desc/geometry/surface.py 97.60% <100.00%> (ø)
... and 38 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

return self.nodes.shape[0]
def period(self):
"""Periodicity of coordinates."""
if self.coordinates in ["rtz", "rvp"]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think other coordinate pairs are checked inside of Equilibrium.compute when a grid which is not an "rtz" grid is passed, in which case it calls map_coordinates with the correct arguments. So technically we support all of the combinations of

            inbasis = {
                "r": "rho",
                "t": "theta",
                "v": "theta_PEST",
                "a": "alpha",
                "z": "zeta",
                "p": "phi",
            }

with outbasis being "rtz"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK, the x0 coordinate is always rho, and the x2 coordinate is always zeta/phi. So the only real difference is in the x1 coordinate? And then I think the only option that needs special treatment is alpha, which is the field line label instead of a polar angle. Does this look like what we want:

@property
def bounds(self):
    """Bounds of coordinates."""
    if self.coordinates[1] == "a":
        return ((0, 1), (0, np.inf), (0, 2 * np.pi))
    else:
        return ((0, 1), (0, 2 * np.pi), (0, 2 * np.pi))

@property
def period(self):
    """Periodicity of coordinates."""
    if self.coordinates[1] == "a":
        return (np.inf, np.inf, 2 * np.pi / self.NFP)
    else:
        return (np.inf, 2 * np.pi, 2 * np.pi / self.NFP)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Technically zeta and phi will be different as well after #465, though I guess the periodicity of them will remain the same.

Also, I am not certain if we always want to be modding vartheta? not sure tho, just remembering the issue #1180

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes zeta and phi should always have the same bounds and periodicity, so that shouldn't be an issue.

I think it is safe to mod vartheta when the grid is first created. This period property is only used to mod the angles when setting the grid nodes, and $\vartheta + 2 \pi$ is physically the same angle as $\vartheta$. That issue was about moding angles within compute functions, which is still a concern but not relevant to creating the initial grid nodes.

Copy link
Collaborator Author

@ddudt ddudt Jan 28, 2026

Choose a reason for hiding this comment

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

@f0uriest @dpanici is this what we decided?

@property
def bounds(self):
    """Bounds of coordinates."""
    return ((0, 1), (0, 2 * np.pi), (0, 2 * np.pi))

@property
def period(self):
    """Periodicity of coordinates."""
    if self.coordinates[1] == "a":
        return (np.inf, np.inf, np.inf)
    else:
        return (np.inf, 2 * np.pi, 2 * np.pi / self.NFP)

Bounds are always $[0, 2\pi)$, and the period is $\infty$ whenever $\alpha$ is involved?

dx0 = self.bounds[0][1] - self.bounds[0][0]
dx1 = self.bounds[1][1] - self.bounds[1][0]
dx2 = self.bounds[2][1] - self.bounds[2][0]
volume = dx0 * dx1 * dx2
Copy link
Collaborator

@dpanici dpanici Jan 27, 2026

Choose a reason for hiding this comment

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

This will be wrong for the AbstractGridCurve as is (meaning if it has (0,0) for its bounds), since there you set the bounds for x0 and x1 to (0,0) so dx0 and dx1 are zero and hence volume will be zero and so will the weights. Maybe can be trivially fixed by adding a conditional checking if each are 0 and only multiplying if they are nonzero

Techincally I guess it is "correct" as is in that the volumes of a 1D grid and 2D grid are zero, but I know you did not mean to go for that, we are going here for quadrature weights that should sum to yield the expected integral over those coordinates not necessarily "volume"

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think, I did not check out the PR to check this myself but from just looking at it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for catching this. See my changes and the test I added in this commit. Now I ensure that np.sum(grid.weights) is equal to the full "volume", although for 1D or 2D coordinate systems that isn't a physical 3D volume.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do we really want to add these new surface grid classes?

The FourierRZToroidalSurface already has an attribute rho and its default compute grid is:

LinearGrid(rho=np.array(self.rho), M=2 * self.M + 5, N=2 * self.N + 5, NFP=self.NFP)

So we would probably also need to remove that rho attribute, and then it gets complicated to tell when surfaces are flux surfaces or generic geometric surfaces.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement General label for enhancement. Please also tag with "Speed", "Interface", "Functionality", etc functionality New feature or request to do things the code can't do now. interface New feature or request to make the code more usable or compatibility with another code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New Grid API

7 participants