Skip to content

Commit

Permalink
Merge branch 'master' into rc/stopping_criteria
Browse files Browse the repository at this point in the history
  • Loading branch information
YigitElma authored Dec 4, 2024
2 parents d870e8c + 4e723e4 commit 1317873
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 12 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,10 +25,10 @@ New Features

Bug Fixes

- Fixes bug that occurs when taking the gradient of ``root`` and ``root_scalar`` with newer versions of JAX (>=0.4.34) and unpins the JAX version
- Fixes bug that occurs when taking the gradient of ``root`` and ``root_scalar`` with newer versions of JAX (>=0.4.34) and unpins the JAX version.
- Changes ``FixLambdaGauge`` constraint to now enforce zero flux surface average for lambda, instead of enforcing lambda(rho,0,0)=0 as it was incorrectly doing before.
- Fixes bug in ``softmin/softmax`` implementation.

- Fixes bug that occured when using ``ProximalProjection`` with a scalar optimization algorithm.

v0.12.3
-------
Expand Down
30 changes: 26 additions & 4 deletions desc/magnetic_fields/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 11 additions & 1 deletion desc/magnetic_fields/_dommaschk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -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))
Expand All @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions desc/optimize/_constraint_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,25 @@ def compute_scaled_error(self, x, constants=None):
xopt, _ = self._update_equilibrium(x, store=False)
return self._objective.compute_scaled_error(xopt, constants[0])

def compute_scalar(self, x, constants=None):
"""Compute the sum of squares error.
Parameters
----------
x : ndarray
State vector.
constants : list
Constant parameters passed to sub-objectives.
Returns
-------
f : float
Objective function scalar value.
"""
f = jnp.sum(self.compute_scaled_error(x, constants=constants) ** 2) / 2
return f

def compute_unscaled(self, x, constants=None):
"""Compute the raw value of the objective function.
Expand Down
12 changes: 7 additions & 5 deletions tests/test_magnetic_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
38 changes: 38 additions & 0 deletions tests/test_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,44 @@ def test_no_iterations():
np.testing.assert_allclose(x0, out2["x"])


@pytest.mark.regression
@pytest.mark.optimize
def test_proximal_scalar():
"""Test that proximal scalar optimization works."""
# test fix for GH issue #1403

# optimize to reduce DSHAPE volume from 100 m^3 to 90 m^3
eq = desc.examples.get("DSHAPE")
optimizer = Optimizer("proximal-fmintr") # proximal scalar optimizer
R_modes = np.vstack(
(
[0, 0, 0],
eq.surface.R_basis.modes[
np.max(np.abs(eq.surface.R_basis.modes), 1) > 1, :
],
)
)
Z_modes = eq.surface.Z_basis.modes[
np.max(np.abs(eq.surface.Z_basis.modes), 1) > 1, :
]
objective = ObjectiveFunction(Volume(eq=eq, target=90)) # scalar objective function
constraints = (
FixBoundaryR(eq=eq, modes=R_modes),
FixBoundaryZ(eq=eq, modes=Z_modes),
FixIota(eq=eq),
FixPressure(eq=eq),
FixPsi(eq=eq),
ForceBalance(eq=eq), # force balance constraint for proximal projection
)
[eq], _ = optimizer.optimize(
things=eq,
objective=objective,
constraints=constraints,
verbose=3,
)
np.testing.assert_allclose(eq.compute("V")["V"], 90)


@pytest.mark.regression
@pytest.mark.slow
@pytest.mark.optimize
Expand Down

0 comments on commit 1317873

Please sign in to comment.