Skip to content

Commit

Permalink
Add NFP to ScalarPotentialField and its subclasses (#1416)
Browse files Browse the repository at this point in the history
Adds `NFP` attribute to `ScalarPotentialField` and subclasses so that if
used with `save_mgrid` method, the mgrid will correctly save the NFP
dpanici authored Dec 4, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 1d03918 + 3e24666 commit bb14269
Showing 4 changed files with 45 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ New Features
* use of both this and the ``QuadraticFlux`` objective allows for REGCOIL solutions to be obtained through the optimization framework, and combined with other objectives as well.
- Changes local area weighting of Bn in QuadraticFlux objective to be the square root of the local area element (Note that any existing optimizations using this objective may need different weights to achieve the same result now.)
- Adds a new tutorial showing how to use``REGCOIL`` features.
- Adds an ``NFP`` attribute to ``ScalarPotentialField``, ``VectorPotentialField`` and ``DommaschkPotentialField``, to allow ``SplineMagneticField.from_field`` and ``MagneticField.save_mgrid`` to efficiently take advantage of the discrete toroidal symmetry of these fields, if present.
- Adds ``SurfaceQuadraticFlux`` objective which minimizes the quadratic magnetic flux through a ``FourierRZToroidalSurface`` object, allowing for optimizing for Quadratic flux minimizing (QFM) surfaces.
- Allows ``ToroidalFlux`` objective to accept ``FourierRZToroidalSurface`` so it can be used to specify the toroidal flux through a QFM surface.
- Adds ``eq_fixed`` flag to ``ToroidalFlux`` to allow for the equilibrium/QFM surface to vary during optimization, useful for single-stage optimizations.
30 changes: 26 additions & 4 deletions desc/magnetic_fields/_core.py
Original file line number Diff line number Diff line change
@@ -1885,7 +1885,7 @@ def from_mgrid(cls, mgrid_file, extcur=None, method="cubic", extrap=False):

@classmethod
def from_field(
cls, field, R, phi, Z, params=None, method="cubic", extrap=False, NFP=1
cls, field, R, phi, Z, params=None, method="cubic", extrap=False, NFP=None
):
"""Create a splined magnetic field from another field for faster evaluation.
@@ -1903,14 +1903,16 @@ def from_field(
extrap : bool
whether to extrapolate splines beyond specified R,phi,Z
NFP : int, optional
Number of toroidal field periods.
Number of toroidal field periods. If not provided, will default to 1 or
the provided field's NFP, if it has that attribute.
"""
R, phi, Z = map(np.asarray, (R, phi, Z))
rr, pp, zz = np.meshgrid(R, phi, Z, indexing="ij")
shp = rr.shape
coords = np.array([rr.flatten(), pp.flatten(), zz.flatten()]).T
BR, BP, BZ = field.compute_magnetic_field(coords, params, basis="rpz").T
NFP = getattr(field, "_NFP", 1)
try:
AR, AP, AZ = field.compute_magnetic_vector_potential(
coords, params, basis="rpz"
@@ -1948,12 +1950,22 @@ class ScalarPotentialField(_MagneticField):
R,phi,Z are arrays of cylindrical coordinates.
params : dict, optional
default parameters to pass to potential function
NFP : int, optional
Whether the field has a discrete periodicity. This is only used when making
a ``SplineMagneticField`` from this field using its ``from_field`` method,
or when saving this field as an mgrid file using the ``save_mgrid`` method.
"""

def __init__(self, potential, params=None):
def __init__(self, potential, params=None, NFP=1):
self._potential = potential
self._params = params
self._NFP = NFP

@property
def NFP(self):
"""int: Number of (toroidal) field periods."""
return self._NFP

def compute_magnetic_field(
self, coords, params=None, basis="rpz", source_grid=None, transforms=None
@@ -2042,12 +2054,22 @@ class VectorPotentialField(_MagneticField):
R,phi,Z are arrays of cylindrical coordinates.
params : dict, optional
default parameters to pass to potential function
NFP : int, optional
Whether the field has a discrete periodicity. This is only used when making
a ``SplineMagneticField`` from this field using its ``from_field`` method,
or when saving this field as an mgrid file using the ``save_mgrid`` method.
"""

def __init__(self, potential, params=None):
def __init__(self, potential, params=None, NFP=1):
self._potential = potential
self._params = params
self._NFP = NFP

@property
def NFP(self):
"""int: Number of (toroidal) field periods."""
return self._NFP

def _compute_A_or_B(
self,
12 changes: 11 additions & 1 deletion desc/magnetic_fields/_dommaschk.py
Original file line number Diff line number Diff line change
@@ -38,6 +38,10 @@ class DommaschkPotentialField(ScalarPotentialField):
d_m_l coefficients of V_m_l terms, which multiply the sin(m*phi)*N_m_l-1 terms
B0: float
scale strength of the magnetic field's 1/R portion
NFP : int, optional
Whether the field has a discrete periodicity. This is only used when making
a ``SplineMagneticField`` from this field using its ``from_field`` method,
or when saving this field as an mgrid file using the ``save_mgrid`` method.
"""

@@ -50,6 +54,7 @@ def __init__(
c_arr=jnp.array([0.0]),
d_arr=jnp.array([0.0]),
B0=1.0,
NFP=1,
):
ms = jnp.atleast_1d(jnp.asarray(ms))
ls = jnp.atleast_1d(jnp.asarray(ls))
@@ -68,6 +73,11 @@ def __init__(
jnp.isscalar(B0) or jnp.atleast_1d(B0).size == 1
), "B0 should be a scalar value!"

ms_over_NFP = ms / NFP
assert jnp.allclose(
ms_over_NFP, ms_over_NFP.astype(int)
), "To enforce desired NFP, `ms` should be all integer multiples of NFP"

params = {}
params["ms"] = ms
params["ls"] = ls
@@ -77,7 +87,7 @@ def __init__(
params["d_arr"] = d_arr
params["B0"] = B0

super().__init__(dommaschk_potential, params)
super().__init__(dommaschk_potential, params, NFP)

@classmethod
def fit_magnetic_field( # noqa: C901
12 changes: 7 additions & 5 deletions tests/test_magnetic_fields.py
Original file line number Diff line number Diff line change
@@ -1045,11 +1045,11 @@ def test_fourier_current_potential_field_coil_cut_warnings(self):
@pytest.mark.unit
def test_spline_field(self, tmpdir_factory):
"""Test accuracy of spline magnetic field."""
field1 = ScalarPotentialField(phi_lm, args)
field1 = ScalarPotentialField(phi_lm, args, NFP=5)
R = np.linspace(0.5, 1.5, 20)
Z = np.linspace(-1.5, 1.5, 20)
p = np.linspace(0, 2 * np.pi / 5, 40)
field2 = SplineMagneticField.from_field(field1, R, p, Z, NFP=5)
field2 = SplineMagneticField.from_field(field1, R, p, Z)

np.testing.assert_allclose(
field1([1.0, 1.0, 1.0]), field2([1.0, 1.0, 1.0]), rtol=1e-2, atol=1e-2
@@ -1450,11 +1450,13 @@ def test_dommaschk_field_errors():
b_arr = [1]
c_arr = [1]
d_arr = [1, 1] # length is not equal to the rest
with pytest.raises(AssertionError):
with pytest.raises(AssertionError, match="size"):
DommaschkPotentialField(ms, ls, a_arr, b_arr, c_arr, d_arr)
d_arr = [1]
d_arr = [1] # test with incorrect NFP
with pytest.raises(AssertionError, match="desired NFP"):
DommaschkPotentialField(ms, ls, a_arr, b_arr, c_arr, d_arr, NFP=2)
ms = [-1] # negative mode number
with pytest.raises(AssertionError):
with pytest.raises(AssertionError, match=">= 0"):
DommaschkPotentialField(ms, ls, a_arr, b_arr, c_arr, d_arr)


0 comments on commit bb14269

Please sign in to comment.