Skip to content

New Grid API#2053

Open
ddudt wants to merge 92 commits intomasterfrom
dd/grids
Open

New Grid API#2053
ddudt wants to merge 92 commits intomasterfrom
dd/grids

Conversation

@ddudt
Copy link
Copy Markdown
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 AbstractGridFlux 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 Curve objects
  • Add 2D grids for FourierRZToroidalSurface objects
  • 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
Comment thread desc/transform.py Outdated
and self.grid.NFP != self.basis.NFP
and self.basis.N != 0
and grid.node_pattern != "custom"
and not isinstance(grid, Grid)
Copy link
Copy Markdown
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.

Comment thread tests/test_grid.py
Comment thread desc/grid/core.py
Comment thread desc/grid/core.py
Comment thread desc/grid/flux.py
@dpanici
Copy link
Copy Markdown
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
Copy Markdown
Collaborator

dpanici commented Jan 7, 2026

Rename AbstractRTZGrid to AbstractGridFlux instead of RTZ

@github-actions
Copy link
Copy Markdown
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.

Comment thread desc/grid/core.py Outdated
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 7, 2026

Codecov Report

❌ Patch coverage is 87.59287% with 167 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.21%. Comparing base (f8c0ec0) to head (cc782ec).

Files with missing lines Patch % Lines
desc/grid/surface.py 80.80% 43 Missing ⚠️
desc/grid/flux.py 77.90% 38 Missing ⚠️
desc/grid/curve.py 75.35% 35 Missing ⚠️
desc/grid/core.py 94.17% 11 Missing ⚠️
desc/geometry/core.py 71.42% 8 Missing ⚠️
desc/objectives/_geometry.py 83.33% 7 Missing ⚠️
desc/external/terpsichore.py 0.00% 6 Missing ⚠️
desc/plotting.py 93.93% 6 Missing ⚠️
desc/coils.py 88.88% 2 Missing ⚠️
desc/grid/utils.py 98.56% 2 Missing ⚠️
... and 7 more
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2053      +/-   ##
==========================================
- Coverage   94.48%   94.21%   -0.27%     
==========================================
  Files         102      107       +5     
  Lines       28721    29280     +559     
==========================================
+ Hits        27137    27587     +450     
- Misses       1584     1693     +109     
Files with missing lines Coverage Δ
desc/basis.py 97.74% <100.00%> (ø)
desc/compat.py 88.61% <100.00%> (ø)
desc/compute/_geometry.py 98.47% <100.00%> (+0.37%) ⬆️
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.67% <100.00%> (ø)
... and 38 more

... and 2 files with indirect coverage changes

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

@unalmis
Copy link
Copy Markdown
Collaborator

unalmis commented Feb 26, 2026

If this PR gets merged before the older PR #1919 , then please address the merge conflicts that this PR #2053 introduces.

@ddudt
Copy link
Copy Markdown
Collaborator Author

ddudt commented Mar 9, 2026

Presently we cannot do the following, because it throws an error that a variable is used before it is defined:

from desc import grid
grid = grid.Grid()

The easy solution is to use a different variable name other than grid, but it is a little annoying. I know we wanted to keep our API the same, but this would be another argument in favor of switching the submodule name to desc.grids (plural) instead of desc.grid.

@f0uriest
Copy link
Copy Markdown
Member

f0uriest commented Mar 9, 2026

Presently we cannot do the following, because it throws an error that a variable is used before it is defined:

from desc import grid
grid = grid.Grid()

The easy solution is to use a different variable name other than grid, but it is a little annoying. I know we wanted to keep our API the same, but this would be another argument in favor of switching the submodule name to desc.grids (plural) instead of desc.grid.

Does this work on master? I feel like it should behave the same. I don't think I've ever imported the grid module by itself though, only desc.grid

@dpanici
Copy link
Copy Markdown
Collaborator

dpanici commented Mar 12, 2026

Presently we cannot do the following, because it throws an error that a variable is used before it is defined:

from desc import grid
grid = grid.Grid()

The easy solution is to use a different variable name other than grid, but it is a little annoying. I know we wanted to keep our API the same, but this would be another argument in favor of switching the submodule name to desc.grids (plural) instead of desc.grid.

Does this work on master? I feel like it should behave the same. I don't think I've ever imported the grid module by itself though, only desc.grid

this works on master

from desc import grid

g = grid.LinearGrid()

@f0uriest
Copy link
Copy Markdown
Member

Presently we cannot do the following, because it throws an error that a variable is used before it is defined:

from desc import grid
grid = grid.Grid()

The easy solution is to use a different variable name other than grid, but it is a little annoying. I know we wanted to keep our API the same, but this would be another argument in favor of switching the submodule name to desc.grids (plural) instead of desc.grid.

Does this work on master? I feel like it should behave the same. I don't think I've ever imported the grid module by itself though, only desc.grid

this works on master

from desc import grid

g = grid.LinearGrid()

This also seems to work on this branch:

from desc import grid
grid = grid.LinearGrid()

@ddudt are you sure this is an issue?

@YigitElma
Copy link
Copy Markdown
Collaborator

If you try to create a new grid.Grid for example, the above code will behave badly, because it replaces the module with an object instance. The suggestion we discussed in dev meeting was to use

from desc import grid as grid_module

grid = grid_module.Grid()

@YigitElma YigitElma added the hackathon Stuff to work on during hackathon label Mar 19, 2026
Comment thread desc/compute/_geometry.py
safenorm(data["e_theta"], axis=-1),
line_label="theta",
fix_surface=("rho", max_rho),
fix_surface=("x0", 0.0),
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 this is 0?

"""
line_label = grid.get_label(line_label)
line_label = grid.get_label(line_label) # general coordinate -> x0, x1, or x2
fix_label = grid.get_label(fix_surface[0])
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.

Should the default of fix_surface change? Currently it is ("rho", 1.0)

Comment thread desc/external/paraview.py
quantities on `LinearGrid(rho=rho, theta=Np, zeta=Nt, NFP=1, endpoint=True)`
and add it to the mesh by `mesh['name'] = value`. Once the mesh is changed,
Created structured grid object. With this object one can compute more quantities
and add them to the mesh by `mesh['name'] = value`. Once the mesh is changed,
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.

I prefer to keep the default grid info since it needs to be the same if someone wants to add data to the mesh

Comment thread desc/grid/flux.py
del self._inverse_theta_idx
if hasattr(self, "_inverse_poloidal_idx"):
self._inverse_x1_idx = self._inverse_poloidal_idx
del self._inverse_theta_idx
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.

Suggested change
del self._inverse_theta_idx
del self._inverse_poloidal_idx

Comment thread desc/grid/flux.py
if coordinates[1] == "a":
period = (np.inf, np.inf, 2 * np.pi / NFP)
else:
period(np.inf, 2 * np.pi, 2 * np.pi / NFP)
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.

Suggested change
period(np.inf, 2 * np.pi, 2 * np.pi / NFP)
period = (np.inf, 2 * np.pi, 2 * np.pi / NFP)

?

Comment thread desc/grid/surface.py
) = self._find_unique_inverse_nodes()
self._weights = self._scale_weights()

def _create_nodes( # noqa: C901
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 function and all create_nodes methods of linear grid types are repeated code, right? Would it be better to have some utility functions for theta, zeta parts?

Biot-Savart. Should NOT include endpoint at 2pi.
vc_source_grid : LinearGrid
LinearGrid to use for the singular integral for the virtual casing
vc_source_grid : LinearGridFlux
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.

Suggested change
vc_source_grid : LinearGridFlux
vc_source_grid : LinearGridToroidalSurface

vc_source_grid : LinearGrid
LinearGrid to use for the singular integral for the virtual casing
vc_source_grid : LinearGridFlux
LinearGridFlux to use for the singular integral for the virtual casing
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.

Suggested change
LinearGridFlux to use for the singular integral for the virtual casing
LinearGridToroidalSurface to use for the singular integral for the virtual casing

LinearGrid to use for the singular integral for the virtual casing
``LinearGridToroidalSurface(M=max(3 * current_potential_field.M_Phi, 30),
N=max(3 * current_potential_field.N_Phi, 30), NFP=eq.NFP)``
vc_source_grid : LinearGridFlux
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.

Suggested change
vc_source_grid : LinearGridFlux
vc_source_grid : LinearGridToroidalSurface

Comment thread desc/objectives/_coils.py
default_M = 2 * eq.M if not hasattr(eq, "_M_grid") else eq.M_grid
default_N = 2 * eq.N if not hasattr(eq, "_M_grid") else eq.N_grid
plasma_grid = self._plasma_grid or LinearGrid(
plasma_grid = self._plasma_grid or LinearGridFlux(
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.

Suggested change
plasma_grid = self._plasma_grid or LinearGridFlux(
plasma_grid = self._plasma_grid or LinearGridToroidalSurface(

@YigitElma
Copy link
Copy Markdown
Collaborator

Github acts buggy for this PR (probably due to high number of changes), I will review the rest later

Copy link
Copy Markdown
Collaborator

@YigitElma YigitElma left a comment

Choose a reason for hiding this comment

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

Do we eventually want to always enforce the proper grid type (no matter if the computation is the same for both)? I think that is slightly annoying, because for example, now we have a dedicated curve grid classes but we don't use them for magnetic axis. Or we have a toroidal surface class, but using them doesn't make the computation any different.

If you can explain which grid should be used when, and when it will be enforced in the grid notebook, it would help a lot for other reviewers, too.

Still need to review 40% of the files :)

else:
grid = self._grid

errorif(
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.

There will be couple hundred of this error in many places, might consider a helper function

# inside desc.utils or somewhere

def errorif_wrong_grid(grid, type, grid_name="grid"):
    errorif(
        not isinstance(grid, type),
        ValueError,
        msg=f"{grid_name} must be of type {type.__name__}, but got type {type(grid)}.",
    )

# the sign convention is positive poloidal current flows up through the torus
# hole
grid_at_surf = LinearGrid(rho=1.0, M=eq.M_grid, N=eq.N_grid)
grid_at_surf = LinearGridFlux(rho=1.0, M=eq.M_grid, N=eq.N_grid)
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.

Suggested change
grid_at_surf = LinearGridFlux(rho=1.0, M=eq.M_grid, N=eq.N_grid)
grid_at_surf = LinearGridToroidalSurface(rho=1.0, M=eq.M_grid, N=eq.N_grid)

dphi = phi[1] - phi[0]

grid = LinearGrid(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
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.

Suggested change
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridCurve(M=0, L=0, zeta=phi, NFP=nfp)

?

Zbasis_sin = FourierSeries(N=N, NFP=nfp, sym=False)

grid = LinearGrid(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
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.

Suggested change
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridCurve(M=0, L=0, zeta=phi, NFP=nfp)

?

bases["Zbasis_sin"] = Zbasis_sin

grid = LinearGrid(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
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.

Suggested change
grid = LinearGridFlux(M=0, L=0, zeta=phi, NFP=nfp)
grid = LinearGridCurve(M=0, L=0, zeta=phi, NFP=nfp)

?

# need some sensible default grid
if self._grid is None:
grid = LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)
grid = LinearGridFlux(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)
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.

These are also better be LienarGridToroidalSurface

Comment thread tests/conftest.py
eq,
eval_grid=LinearGrid(M=20, N=20, NFP=eq.NFP, sym=True),
source_grid=LinearGrid(M=40, N=40, NFP=eq.NFP),
eval_grid=LinearGridFlux(M=20, N=20, NFP=eq.NFP, sym=True),
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.

Suggested change
eval_grid=LinearGridFlux(M=20, N=20, NFP=eq.NFP, sym=True),
eval_grid=LinearGridToroidalSurface(M=20, N=20, NFP=eq.NFP, sym=True),

Comment thread tests/conftest.py
eq,
eval_grid=LinearGrid(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
source_grid=LinearGrid(M=M_sgrid, N=N_sgrid, NFP=eq.NFP),
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
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.

Suggested change
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
eval_grid=LinearGridToroidalSurface(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),

Comment thread tests/conftest.py
eq,
eval_grid=LinearGrid(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
source_grid=LinearGrid(M=M_sgrid, N=N_sgrid, NFP=eq.NFP),
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
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.

Suggested change
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
eval_grid=LinearGridToroidalSurface(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),

Comment thread tests/conftest.py
eq,
eval_grid=LinearGrid(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
source_grid=LinearGrid(M=M_sgrid, N=N_sgrid, NFP=eq.NFP),
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
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.

Suggested change
eval_grid=LinearGridFlux(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),
eval_grid=LinearGridToroidalSurface(M=M_egrid, N=N_egrid, NFP=eq.NFP, sym=True),

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. hackathon Stuff to work on during hackathon 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

6 participants