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

CoilIntegratedCurvature objective #1485

Merged
merged 17 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ New Features
- Adds the output quantities ``wb`` and ``wp`` to ``VMECIO.save``.
- Change implementation of Dommaschk potentials to use recursive algorithm and symbolic integration.
- Changes hessian computation to use chunked ``jacfwd`` and ``jacrev``, allowing ``jac_chunk_size`` to now reduce hessian memory usage as well.
- Added an option to ``VMECIO.save`` to specify the grid resolution in real space.
- Adds an option to ``VMECIO.save`` to specify the grid resolution in real space.
- Adds a new objective ``desc.objectives.CoilIntegratedCurvature`` for targeting convex coils.

Bug Fixes

Expand Down
1 change: 1 addition & 0 deletions desc/objectives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
CoilArclengthVariance,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilSetLinkingNumber,
CoilSetMinDistance,
Expand Down
110 changes: 107 additions & 3 deletions desc/objectives/_coils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@

"""

__doc__ = __doc__.rstrip() + collect_docs(
coil=True,
)
__doc__ = __doc__.rstrip() + collect_docs(coil=True)

def __init__(
self,
Expand Down Expand Up @@ -602,6 +600,112 @@
return out


class CoilIntegratedCurvature(_CoilObjective):
"""Coil integrated curvature.

If a curve is convex, then the following condition must be true: ∫ κ |∂ₛx| ds = 2π
ddudt marked this conversation as resolved.
Show resolved Hide resolved
where κ is the scalar (unsigned) curvature, ∂ₛx is the first derivative of the
position vector along curve, and s is the curve parameter.

Parameters
----------
coil : CoilSet or Coil
Coil(s) that are to be optimized
grid : Grid, optional
Collocation grid containing the nodes to evaluate at.
Defaults to ``LinearGrid(N=2 * coil.N + 5, endpoint=True)``

"""

__doc__ = __doc__.rstrip() + collect_docs(
target_default="``target=2*np.pi``.",
bounds_default="``target=2*np.pi``.",
coil=True,
)

_scalar = False # not always a scalar, if a coilset is passed in
_units = "(dimensionless)"
_print_value_fmt = "Integrated curvature: "

def __init__(
self,
coil,
target=None,
bounds=None,
weight=1,
normalize=True,
normalize_target=True,
loss_function=None,
deriv_mode="auto",
grid=None,
name="coil integrated curvature",
jac_chunk_size=None,
):
if target is None and bounds is None:
target = 2 * np.pi
super().__init__(

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L644-L646

Added lines #L644 - L646 were not covered by tests
coil,
["ds", "x_s", "curvature"],
target=target,
bounds=bounds,
weight=weight,
normalize=normalize,
normalize_target=normalize_target,
loss_function=loss_function,
deriv_mode=deriv_mode,
grid=grid,
name=name,
jac_chunk_size=jac_chunk_size,
)

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.

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

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L672

Added line #L672 was not covered by tests

self._dim_f = self._num_coils
self._constants["quad_weights"] = 1

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L674-L675

Added lines #L674 - L675 were not covered by tests

_Objective.build(self, use_jit=use_jit, verbose=verbose)

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L677

Added line #L677 was not covered by tests

def compute(self, params, constants=None):
"""Compute integrated curvature.

Parameters
----------
params : dict
Dictionary of the coil's degrees of freedom.
constants : dict
Dictionary of constant data, e.g. transforms, profiles etc. Defaults to
self._constants.

Returns
-------
f : array of floats
Integrated curvature.

"""
data = super().compute(params, constants=constants)
data = tree_leaves(data, is_leaf=lambda x: isinstance(x, dict))
out = jnp.array(

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L696-L698

Added lines #L696 - L698 were not covered by tests
[
jnp.sum(
jnp.abs(dat["curvature"]) * safenorm(dat["x_s"], axis=1) * dat["ds"]
)
for dat in data
]
)
return out

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

View check run for this annotation

Codecov / codecov/patch

desc/objectives/_coils.py#L706

Added line #L706 was not covered by tests


class CoilSetMinDistance(_Objective):
"""Target the minimum distance between coils in a coilset.

Expand Down
1 change: 1 addition & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ Objective Functions
desc.objectives.CoilArclengthVariance
desc.objectives.CoilCurrentLength
desc.objectives.CoilCurvature
desc.objectives.CoilIntegratedCurvature
desc.objectives.CoilLength
desc.objectives.CoilSetLinkingNumber
desc.objectives.CoilSetMinDistance
Expand Down
1 change: 1 addition & 0 deletions docs/api_objectives.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Coil Optimization
desc.objectives.CoilSetLinkingNumber
desc.objectives.CoilSetMinDistance
desc.objectives.PlasmaCoilSetMinDistance
desc.objectives.CoilIntegratedCurvature
desc.objectives.CoilCurrentLength
desc.objectives.CoilArclengthVariance
desc.objectives.ToroidalFlux
Expand Down
50 changes: 48 additions & 2 deletions tests/test_objective_funs.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
CoilArclengthVariance,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilSetLinkingNumber,
CoilSetMinDistance,
Expand Down Expand Up @@ -938,6 +939,46 @@ def test(coil, grid=None):
test(mixed_coils)
test(nested_coils, grid=grid)

@pytest.mark.unit
def test_integrated_curvature(self):
"""Tests integrated_curvature."""

def test(coil, grid=None, ans=2 * np.pi):
obj = CoilIntegratedCurvature(coil, grid=grid)
obj.build()
f = obj.compute(params=coil.params_dict)
np.testing.assert_allclose(f, ans, atol=2e-5)
assert f.shape == (obj.dim_f,)

# convex coils
coil = FourierPlanarCoil(r_n=[0.3, 1, 0.3], basis="rpz")
coils = CoilSet.linspaced_linear(
coil, n=3, displacement=[0, 3, 0], check_intersection=False
)
mixed_coils = MixedCoilSet.linspaced_linear(
coil, n=2, displacement=[0, 7, 0], check_intersection=False
)
nested_coils = MixedCoilSet(coils, mixed_coils, check_intersection=False)
test(coil)
test(coils)
test(mixed_coils)
test(nested_coils)

# not convex coils
coil = FourierPlanarCoil(r_n=[0.5, 1, 0.5], basis="rpz")
coils = CoilSet.linspaced_linear(
coil, n=3, displacement=[0, 3, 0], check_intersection=False
)
mixed_coils = MixedCoilSet.linspaced_linear(
coil, n=2, displacement=[0, 7, 0], check_intersection=False
)
nested_coils = MixedCoilSet(coils, mixed_coils, check_intersection=False)
ans = 1.104044 + 2 * np.pi
test(coil, ans=ans)
test(coils, ans=ans)
test(mixed_coils, ans=ans)
test(nested_coils, ans=ans)

@pytest.mark.unit
def test_coil_type_error(self):
"""Tests error when objective is not passed a coil."""
Expand Down Expand Up @@ -2664,6 +2705,7 @@ class TestComputeScalarResolution:
CoilArclengthVariance,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilSetLinkingNumber,
CoilSetMinDistance,
Expand Down Expand Up @@ -3086,6 +3128,7 @@ def test_compute_scalar_resolution_others(self, objective):
CoilArclengthVariance,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilTorsion,
CoilSetLinkingNumber,
Expand All @@ -3099,7 +3142,8 @@ def test_compute_scalar_resolution_coils(self, objective):
f = np.zeros_like(self.res_array, dtype=float)
for i, res in enumerate(self.res_array):
obj = ObjectiveFunction(
objective(coilset, grid=LinearGrid(N=int(5 + 3 * res))), use_jit=False
objective(coilset, grid=LinearGrid(N=int(5 + 3 * res))),
use_jit=False,
)
obj.build(verbose=0)
f[i] = obj.compute_scalar(obj.x())
Expand Down Expand Up @@ -3144,9 +3188,10 @@ class TestObjectiveNaNGrad:
BootstrapRedlConsistency,
BoundaryError,
CoilArclengthVariance,
CoilLength,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilSetLinkingNumber,
CoilSetMinDistance,
CoilTorsion,
Expand Down Expand Up @@ -3376,6 +3421,7 @@ def test_objective_no_nangrad(self, objective):
CoilArclengthVariance,
CoilCurrentLength,
CoilCurvature,
CoilIntegratedCurvature,
CoilLength,
CoilTorsion,
CoilSetLinkingNumber,
Expand Down
Loading