Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LinkingCurrent objective #1222

Merged
merged 37 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
ad68afa
LinkingCurrent objective
daniel-dudt Aug 22, 2024
e7351d3
get linking current working in single-stage
daniel-dudt Aug 23, 2024
59499bf
Merge branch 'master' into dd/linking_current
ddudt Aug 25, 2024
e489539
Merge branch 'master' into dd/linking_current
ddudt Aug 25, 2024
fc76f96
fix print_values
daniel-dudt Aug 26, 2024
9b88103
Merge branch 'master' into dd/linking_current
dpanici Aug 27, 2024
fd4542e
Merge branch 'master' into dd/linking_current
dpanici Aug 28, 2024
61112d3
Merge branch 'master' into dd/linking_current
f0uriest Oct 11, 2024
31e6228
Merge branch 'rc/linking_number' into dd/linking_current
f0uriest Oct 11, 2024
31eb816
Use linking number for linking current objective
f0uriest Oct 11, 2024
d941da7
Merge branch 'master' into dd/linking_current
f0uriest Oct 15, 2024
8db268d
fix test_objective_docstring
YigitElma Oct 15, 2024
fc23893
Merge branch 'master' into dd/linking_current
dpanici Oct 16, 2024
30dd572
Merge branch 'master' into dd/linking_current
f0uriest Oct 16, 2024
f5abd60
Add option to fix eq for LinkingCurrent
f0uriest Oct 16, 2024
4c5ec6c
Merge branch 'master' into dd/linking_current
f0uriest Nov 13, 2024
ed450b8
Add handling for linking currents from virtual coils
f0uriest Nov 13, 2024
db96420
Merge branch 'master' into dd/linking_current
f0uriest Nov 13, 2024
ea81721
Fix test
f0uriest Nov 13, 2024
6b102be
Update changelog
f0uriest Nov 13, 2024
48f4c40
Merge branch 'master' into dd/linking_current
f0uriest Nov 13, 2024
8a1958a
Update changelog action version
f0uriest Nov 13, 2024
87fb202
Merge branch 'dd/linking_current' of github.com:PlasmaControl/DESC in…
f0uriest Nov 13, 2024
0337023
Merge branch 'master' into dd/linking_current
f0uriest Nov 19, 2024
73f1286
Merge branch 'master' into dd/linking_current
f0uriest Dec 3, 2024
95cc0e8
Merge branch 'master' into dd/linking_current
dpanici Dec 3, 2024
4fd41f1
Merge branch 'master' into dd/linking_current
f0uriest Dec 4, 2024
815ad41
Merge branch 'master' into dd/linking_current
f0uriest Dec 5, 2024
f060c38
Merge branch 'master' into dd/linking_current
f0uriest Dec 10, 2024
d48f484
Merge branch 'master' into dd/linking_current
f0uriest Dec 11, 2024
39fec41
Merge branch 'master' into dd/linking_current
f0uriest Dec 11, 2024
a63f2ae
Rename linking current objective and expand docstring
f0uriest Dec 11, 2024
8f39791
suppress warning about incompatible constraints
f0uriest Dec 12, 2024
bffa25d
Merge branch 'master' into dd/linking_current
ddudt Dec 12, 2024
7dcd99f
Update desc/objectives/_coils.py
f0uriest Dec 13, 2024
88eaa93
Merge branch 'master' into dd/linking_current
f0uriest Dec 13, 2024
ab74f48
Merge branch 'master' into dd/linking_current
dpanici Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desc/objectives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CoilSetLinkingNumber,
CoilSetMinDistance,
CoilTorsion,
LinkingCurrent,
PlasmaCoilSetMinDistance,
QuadraticFlux,
ToroidalFlux,
Expand Down
165 changes: 164 additions & 1 deletion desc/objectives/_coils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numbers

import numpy as np
from scipy.constants import mu_0

from desc.backend import (
fori_loop,
Expand All @@ -14,7 +15,7 @@
from desc.compute.utils import _compute as compute_fun
from desc.grid import LinearGrid, _Grid
from desc.integrals import compute_B_plasma
from desc.utils import Timer, errorif, safenorm, warnif
from desc.utils import Timer, broadcast_tree, errorif, safenorm, warnif

from .normalization import compute_scaling_factors
from .objective_funs import _Objective, collect_docs
Expand Down Expand Up @@ -1364,6 +1365,168 @@
return Psi


class LinkingCurrent(_Objective):
"""Target the self-consistent poloidal linking current between the plasma and coils.
f0uriest marked this conversation as resolved.
Show resolved Hide resolved

Assumes the coil topology does not change (ie the linking number with the plasma
is fixed).

Parameters
----------
eq : Equilibrium
Equilibrium that will be optimized to satisfy the Objective.
coil : CoilSet
Coil(s) that are to be optimized.
grid : Grid, optional
Collocation grid containing the nodes to evaluate plasma current at.
Defaults to ``LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)``.
dpanici marked this conversation as resolved.
Show resolved Hide resolved

"""

__doc__ = __doc__.rstrip() + collect_docs(
target_default="``target=0``.",
bounds_default="``target=0``.",
)

_scalar = True
_units = "(A)"
_print_value_fmt = "Linking current: "

def __init__(
self,
eq,
coil,
target=None,
bounds=None,
weight=1,
normalize=True,
normalize_target=True,
loss_function=None,
deriv_mode="auto",
grid=None,
jac_chunk_size=None,
name="linking current",
):
if target is None and bounds is None:
target = 0
self._grid = grid
super().__init__(
things=[eq, coil],
target=target,
bounds=bounds,
weight=weight,
normalize=normalize,
normalize_target=normalize_target,
loss_function=loss_function,
deriv_mode=deriv_mode,
jac_chunk_size=jac_chunk_size,
name=name,
)

def build(self, use_jit=True, verbose=1):
"""Build constant arrays.

Parameters
----------
use_jit : bool, optional
Whether to just-in-time compile the objective and derivatives.
verbose : int, optional
Level of output.

"""
eq = self.things[0]
coil = self.things[1]
grid = self._grid or LinearGrid(
M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym
)
warnif(
not np.allclose(grid.nodes[:, 0], 1),
UserWarning,
"grid includes interior points, should be rho=1.",
)

self._dim_f = 1
self._data_keys = ["G"]

all_params = tree_map(lambda dim: np.arange(dim), coil.dimensions)
current_params = tree_map(lambda idx: {"current": idx}, True)
# indices of coil currents
self._indices = tree_leaves(broadcast_tree(current_params, all_params))
self._num_coils = coil.num_coils

profiles = get_profiles(self._data_keys, obj=eq, grid=grid)
transforms = get_transforms(self._data_keys, obj=eq, grid=grid)

# compute linking number of coils with plasma. To do this we add a fake "coil"
# along the magnetic axis and compute the linking number of that coilset
from desc.coils import FourierRZCoil, MixedCoilSet

axis_coil = FourierRZCoil(
1.0,
eq.axis.R_n,
eq.axis.Z_n,
eq.axis.R_basis.modes[:, 2],
eq.axis.Z_basis.modes[:, 2],
eq.axis.NFP,
)
dummy_coilset = MixedCoilSet(axis_coil, coil)
# linking number for coils with axis
link = np.round(dummy_coilset._compute_linking_number())[0, 1:]

self._constants = {
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 could be 1 line instead of 4

"profiles": profiles,
"transforms": transforms,
"quad_weights": 1.0,
"link": link,
dpanici marked this conversation as resolved.
Show resolved Hide resolved
}

if self._normalize:
params = tree_leaves(
coil.params_dict, is_leaf=lambda x: isinstance(x, dict)
)
self._normalization = np.sum([np.abs(param["current"]) for param in params])

super().build(use_jit=use_jit, verbose=verbose)

def compute(self, eq_params, coil_params, constants=None):
"""Compute linking current error.

Parameters
----------
eq_params : dict
Dictionary of equilibrium degrees of freedom, eg ``Equilibrium.params_dict``
coil_params : dict
Dictionary of coilset degrees of freedom, eg ``CoilSet.params_dict``
constants : dict
Dictionary of constant data, eg transforms, profiles etc.
Defaults to self._constants.

Returns
-------
f : array of floats
Linking current error.
Copy link
Collaborator

Choose a reason for hiding this comment

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

techincally just a single float but not a big deal


"""
if constants is None:
constants = self.constants

Check warning on line 1511 in desc/objectives/_coils.py

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L1511

Added line #L1511 was not covered by tests
data = compute_fun(
"desc.equilibrium.equilibrium.Equilibrium",
self._data_keys,
params=eq_params,
transforms=constants["transforms"],
profiles=constants["profiles"],
)
eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0
coil_currents = jnp.concatenate(
[
jnp.atleast_1d(param[idx])
for param, idx in zip(tree_leaves(coil_params), self._indices)
]
)
coil_linking_current = jnp.sum(constants["link"] * coil_currents)
f0uriest marked this conversation as resolved.
Show resolved Hide resolved
return eq_linking_current - coil_linking_current


class CoilSetLinkingNumber(_Objective):
"""Prevents coils from becoming interlinked.

Expand Down
48 changes: 48 additions & 0 deletions tests/test_objective_funs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
HeatingPowerISS04,
Isodynamicity,
LinearObjectiveFromUser,
LinkingCurrent,
MagneticWell,
MeanCurvature,
MercierStability,
Expand Down Expand Up @@ -1297,6 +1298,18 @@ def test_signed_plasma_vessel_distance(self):
assert abs(d.max() - (-a_s)) < 1e-14
assert abs(d.min() - (-a_s)) < grid.spacing[0, 1] * a_s

@pytest.mark.unit
def test_linking_current(self):
"""Test calculation of signed linking current from coils to plasma."""
eq = Equilibrium()
G = eq.compute("G", grid=LinearGrid(rho=1.0))["G"] * 2 * jnp.pi / mu_0
coil = FourierPlanarCoil(current=G / 8, center=[10, 1, 0])
coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True)
obj = LinkingCurrent(eq, coilset)
obj.build()
f = obj.compute(eq.params_dict, coilset.params_dict)
np.testing.assert_allclose(f, 0)


@pytest.mark.regression
def test_derivative_modes():
Expand Down Expand Up @@ -2271,6 +2284,7 @@ class TestComputeScalarResolution:
FusionPower,
GenericObjective,
HeatingPowerISS04,
LinkingCurrent,
Omnigenity,
PlasmaCoilSetMinDistance,
PlasmaVesselDistance,
Expand Down Expand Up @@ -2650,6 +2664,26 @@ def test_compute_scalar_resolution_coils(self, objective):
f[i] = obj.compute_scalar(obj.x())
np.testing.assert_allclose(f, f[-1], rtol=1e-2, atol=1e-12)

@pytest.mark.unit
def test_compute_scalar_resolution_linking_current(self):
"""LinkingCurrent."""
coil = FourierPlanarCoil(center=[10, 1, 0])
eq = Equilibrium()
coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True)
f = np.zeros_like(self.res_array, dtype=float)
for i, res in enumerate(self.res_array):
obj = ObjectiveFunction(
LinkingCurrent(
eq,
coilset,
grid=LinearGrid(M=int(eq.M_grid * res), N=int(eq.N_grid * res)),
),
use_jit=False,
)
obj.build(verbose=0)
f[i] = obj.compute_scalar(obj.x())
np.testing.assert_allclose(f, f[-1], rtol=1e-2, atol=1e-12)


class TestObjectiveNaNGrad:
"""Make sure reverse mode AD works correctly for all objectives."""
Expand Down Expand Up @@ -2677,6 +2711,7 @@ class TestObjectiveNaNGrad:
ForceBalanceAnisotropic,
FusionPower,
HeatingPowerISS04,
LinkingCurrent,
Omnigenity,
PlasmaCoilSetMinDistance,
PlasmaVesselDistance,
Expand Down Expand Up @@ -2919,6 +2954,17 @@ def test_objective_no_nangrad_ballooning(self):
g = obj.grad(obj.x())
assert not np.any(np.isnan(g))

@pytest.mark.unit
def test_objective_no_nangrad_linking_current(self):
"""LinkingCurrent."""
coil = FourierPlanarCoil(center=[10, 1, 0])
coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True)
eq = Equilibrium()
obj = ObjectiveFunction(LinkingCurrent(eq, coilset))
obj.build()
g = obj.grad(obj.x())
assert not np.any(np.isnan(g))


@pytest.mark.unit
def test_asymmetric_normalization():
Expand All @@ -2943,6 +2989,7 @@ def test_asymmetric_normalization():
assert np.all(np.isfinite(val))


@pytest.mark.unit
def test_objective_print_widths():
"""Test that the objective's name is shorter than max."""
subclasses = _Objective.__subclasses__()
Expand Down Expand Up @@ -2971,6 +3018,7 @@ def test_objective_print_widths():
)


@pytest.mark.unit
f0uriest marked this conversation as resolved.
Show resolved Hide resolved
def test_objective_docstring():
"""Test that the objective docstring and collect_docs are consistent."""
objective_docs = _Objective.__doc__.rstrip()
Expand Down
Loading