Skip to content

Adding QUADCOIL integration to DESC as an objective#2110

Open
lankef wants to merge 45 commits intomasterfrom
ffu/quadcoil-qss-pre-commit
Open

Adding QUADCOIL integration to DESC as an objective#2110
lankef wants to merge 45 commits intomasterfrom
ffu/quadcoil-qss-pre-commit

Conversation

@lankef
Copy link
Copy Markdown
Collaborator

@lankef lankef commented Mar 3, 2026

This pull request adds two files under /objective, _quadcoil.py and _quadcoil_utils.py. These allows one to call QUADCOIL using a DESC equilibrium and filament coil set as input, and perform quasi-single-stage optimization on the equilibrium and/or the coilset. There are some additions in coils and curves for conversion from simsopt objects. (since quadcoil is written in the simsopt convention). Winding surface can be either fixed or auto-generated, but optimizing the geometry of the winding surface is not yet implemented. The unit test compares quadcoil to desc's built-in REGCOIL.

lankef and others added 30 commits April 4, 2025 12:23
… optimizable_io.py and objective_funs.py to avoid error for static, empty tuple attributes and _objectives with self.coordinates == \'\'
…nversion, but it's not working because quadcoilproxy does not directly store the input dictionary, and desc static dict cannot contain strings (?)
…: Colorbar layout of new layout engine not compatible with old engine, and a colorbar has been created. Engine not changed.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 3, 2026

Memory benchmark result

|               Test Name                |      %Δ      |    Master (MB)     |      PR (MB)       |    Δ (MB)    |    Time PR (s)     |  Time Master (s)   |
| -------------------------------------- | ------------ | ------------------ | ------------------ | ------------ | ------------------ | ------------------ |
  test_objective_jac_w7x                 |   -1.07 %    |     3.954e+03      |     3.912e+03      |    -42.27    |       39.45        |       37.44        |
  test_proximal_jac_w7x_with_eq_update   |    0.78 %    |     6.635e+03      |     6.687e+03      |    51.93     |       161.98       |       163.60       |
  test_proximal_freeb_jac                |   -0.39 %    |     1.327e+04      |     1.321e+04      |    -51.91    |       85.20        |       86.51        |
  test_proximal_freeb_jac_blocked        |   -1.85 %    |     7.607e+03      |     7.466e+03      |   -141.04    |       75.89        |       74.68        |
  test_proximal_freeb_jac_batched        |    0.76 %    |     7.467e+03      |     7.524e+03      |    56.92     |       74.79        |       74.27        |
  test_proximal_jac_ripple               |    1.60 %    |     3.567e+03      |     3.624e+03      |    56.99     |       68.48        |       66.98        |
  test_proximal_jac_ripple_bounce1d      |   -1.17 %    |     3.566e+03      |     3.524e+03      |    -41.86    |       79.14        |       80.93        |
  test_eq_solve                          |    1.85 %    |     1.993e+03      |     2.030e+03      |    36.78     |       96.32        |       95.31        |

For the memory plots, go to the summary of Memory Benchmarks workflow and download the artifact.

@lankef lankef requested review from dpanici and f0uriest March 5, 2026 15:27
Copy link
Copy Markdown
Member

@f0uriest f0uriest left a comment

Choose a reason for hiding this comment

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

Just some initial comments after a first pass, I'll have to understand quadcoil a bit better to give a more detailed review.

Also, you should add quadcoil to devtools/dev-requirements.txt so it will be installed for testing. (and maybe also requirements.txt? Or we can leave it as an optional dependency?)

Comment thread desc/geometry/curve.py
Comment thread desc/coils.py
Comment thread desc/compute/data_index.py Outdated
"desc.geometry.core.Surface",
"desc.magnetic_fields._core.MagneticField",
],
"desc.magnetic_fields._quadcoil_field.QuadcoilField": [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this class missing?

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.

Thanks for catching this. It's a WIP class and I've decided to not include it here

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.

removed now

return net_poloidal_current_amperes


def _ptolemy_identity_rev_precompute(m_1, n_1):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you can use desc.vmec_utils.ptolemy_linear_transform instead of this. It returns a matrix A such that A@x_desc = x_vmec. You can create the matrix in the build method. See the QuasisymmetryBoozer objective for an example

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.

I'm already using ptolemy_linear_transform inside this function. It's just a wrapper to reduce some code reusing.

Comment thread desc/objectives/_quadcoil_utils.py Outdated
Comment on lines +354 to +357
if "winding_stellsym" in source_kwargs.keys():
winding_stellsym = source_kwargs["winding_stellsym"]
else:
winding_stellsym = sym_default
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

could be winding_stellsym = source_kwargs.get("winding_stellsym", sym_default)

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.

Fixed. Also replaced a few other occurrences.

def __init__( # noqa: C901
self,
eq,
quadcoil_kwargs,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is there some sensible default we can give for quadcoil kwargs?

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.

I can make it solve the NESCOIL problem by default but it will require a default value for surface_plasma_distance, too. I feel maybe it's sensible to have the user think about it themselves? I've uploaded example ones in the tutorial notebook too.

Comment thread desc/objectives/_quadcoil.py Outdated
name="QUADCOIL Proxy",
source_grid=None,
# External coils - no external coils by default
field=[],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lists are mutable so they generally shouldn't be used as default args since it can lead to some weird behavior. Better to use None or an empty tuple

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.

Fixed to None.

Comment thread desc/objectives/_quadcoil.py Outdated
target = 0 # default target value
# Uses LSE to smooth non-smooth problems rather than slack variables
# by default.
if "smoothing" not in quadcoil_kwargs.keys():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can use quadcoil_kwargs.setdefault("smoothing", "approx")

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.

Fixed

Comment thread desc/objectives/_quadcoil.py Outdated
else:
self.net_toroidal_current_amperes = 0.0
if "plasma_coil_distance" in quadcoil_kwargs.keys():
# A sign flip is necessary here!
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why sign flip? shouldn't distance always be positive?

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.

Because simsopt surface and DESC surface have different handedness. QUADCOIL uses the simsopt convention so a sign flip is necessary. I've added more comments here.

if self._field:
from desc.magnetic_fields import SumMagneticField

self._constants["sum_field"] = SumMagneticField(self._field)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's better to assume that the user will already give a sum field, rather than a possible list of fields to sum. The general format we've been working with is that field is always assumed to be a single MagneticField object

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.

We silently allow this in BoundaryError, VacuumBoundaryError and QuadraticFlux as a stopgap before we actually make things work correctly with SumMagneticField being passed in directly... this is just further impetus to actually get that working

@lankef
Copy link
Copy Markdown
Collaborator Author

lankef commented Mar 6, 2026

devtools/dev-requirements.txt

It can be an optional dependence. I've just added it to dev-requirements.

"_use_jit",
]
_static_attrs = [
"_static_attrs", # A bug as of Oct 10 2025
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.

specifically, for some reason one needs static_attrs included in the static_attrs or one will get JAX errors. This seems unintuitive but is empirically what we've found

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.

Should I keep this? I don't need to do anything about this right?

Comment thread docs/index.rst
notebooks/tutorials/coil_optimization_QUADCOIL.ipynb
notebooks/tutorials/omnigenity.ipynb
notebooks/tutorials/bootstrap_current.ipynb
notebooks/tutorials/coil_stage_two_optimization.ipynb
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.

Also need to add QUADCOIL stuff to the API, I think the file is https://github.com/PlasmaControl/DESC/blob/master/docs/api_objectives.rst?plain=1

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.

add the objectives, I mean

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.

Added!
9faf614

Comment thread desc/objectives/_quadcoil.py Outdated
targets for each objective terms individually besides using ``target``
and ``bounds`` that comes with other DESC objectives.
Targets of each property. 0 by default.
metric_weight : scalar or ndarray, default=None
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.

1.0 not None

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.

Should be fixed now.

return Bnormal


# Flipping the current potential of a quadcoil
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.

what are the different coordinate directions/representations/normalizations used? it woudl be good to list these all somewhere, even if just as a comment or in this PR, of the differences in convention btwn DESC and QUADCOIL. Just so we have it clearly down somewhere. Seems phi is flipped?

return phi_swapped


def _quadcoil_phi_to_desc_phi(phi_mn_quadcoil, stellsym, mpol, ntor):
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 is converting just phi_mn quadcoil to desc?

return Phi_mn, modes_M, modes_N


def _create_source(eq, source_grid, eval_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.

this could be placed perhaps into the integrals/singularities.py or in a util file there. Also could be used again inside of the other places like BoundaryError. I think it is a reasonable util function, with perhaps some additional documentation attached to it

return (source_profiles, source_transforms, interpolator)


def _quadcoil_kwargs_to_field_kwargs( # 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.

would be nice to document what is being done here exactly?

Comment thread desc/objectives/_quadcoil.py Outdated
plasma_dofs = jnp.concatenate([rc, rs, zc, zs])
return plasma_dofs

def compute_surface_current_field(self, *all_params):
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.

instead of compute_surface_current_field maybe something more like run_regcoil or solve_quadcoil_surface_current_field or something is better?

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.

I renamed this to solve_quadcoil_surface_current_field, I also renamed compute_full to solve_quadcoil.

Copy link
Copy Markdown
Collaborator Author

@lankef lankef left a comment

Choose a reason for hiding this comment

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

Addressed the comments

@dpanici dpanici requested review from dpanici and f0uriest March 26, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants