Skip to content

Conversation

@smiet
Copy link
Contributor

@smiet smiet commented Sep 19, 2025

Revamp of fieldline integration. Some tests still need to be written, but this should simplify the user experience in generating Poincare plots.

What’s new

Integrator base class
with subclasses:

  • SimsoptFieldlineIntegrator (interface to the sopp integration routines
  • ScipyFieldlineIntegrator (integration of the magnetic field using scipy.solve_ivp methods).

These subclasses provide methods to return Poincare sections, three dimensional field line trajectories, as well as the ability to integrate from any starting point over a given toroidal distance.

The ScipyFieldlineIntegrator provides methods to integrate $R$ and $Z$ using $\phi$ as the 'time' variable in the ODE, providing crisper Poincare sections and integrates about as fast as the sopp methods (which evolves the three coordinates independently).

The Integrator subclasses also provide an interface to the PoincarePlotter class, which provides an easy interface to create Poincare plots of the field.

The PoincarePlotter uses caching and the simsopt dependency graph (parent Integrator with parent MagneticField) to trigger recomputes when necessary.
Manipulation of the res_phi_hits and res_tys arrays is a thing of the past!

give it a spin!:

from simsopt.configs import get_data
from simsopt.field import SimsoptFieldlineIntegrator, PoincarePlotter
from simsopt.geo import plot
import numpy as np

base_coils, base_currents, ma, nfp, bs = get_data('ncsx')

axis_rz = ma.gamma()[0][::2]
# startpoints linspace from axis out: 
start_points_RZ = np.linspace(axis_rz, axis_rz+np.array([0.1, 0]), 10)


integrator = SimsoptFieldlineIntegrator(bs, stellsym=True, nfp=nfp, R0=axis_rz[0], tol=1e-9)
pplotter = PoincarePlotter(integrator, start_points_RZ, n_transits=200, phis=4)

fig, axs = pplotter.plot_poincare_all()


pplotter.plot_fieldline_trajectories_3d(engine='mayavi', show=False, opacity=0.2, tube_radius=0.001)
plot(bs.coils, engine='mayavi', show=False)
pplotter.plot_poincare_in_3d(engine='mayavi', show=True, scale_factor=0.01)
image

@smiet smiet requested a review from mishapadidar September 29, 2025 14:00
@smiet smiet marked this pull request as ready for review September 29, 2025 14:02
@codecov
Copy link

codecov bot commented Sep 29, 2025

Codecov Report

❌ Patch coverage is 89.14186% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.45%. Comparing base (0da1f07) to head (bdc0ab5).
⚠️ Report is 13 commits behind head on master.

Files with missing lines Patch % Lines
src/simsopt/field/tracing.py 89.14% 62 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #555      +/-   ##
==========================================
- Coverage   92.56%   92.45%   -0.12%     
==========================================
  Files          83       83              
  Lines       16229    16798     +569     
==========================================
+ Hits        15023    15530     +507     
- Misses       1206     1268      +62     
Flag Coverage Δ
unittests 92.45% <89.14%> (-0.12%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@andrewgiuliani andrewgiuliani left a comment

Choose a reason for hiding this comment

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

Thanks Chris!

There are a few lines in code coverage that are missing, we should have unit tests for these.

By the way, we still cover plotting code in unit tests, but the canvas is just not plotted by skipping the mlab.show() command, like we do here

@smiet
Copy link
Contributor Author

smiet commented Oct 22, 2025

@andrewgiuliani Thank you for your review, I have added tests to cover all the missed lines that I could. The ones that are not hit are:

  • the mayavi plotting routines (because we do not have a runner with mayavi installed, but they will run when that is the case)
  • The lines that gather data from several processes (though these should be hit in the examples, which are run with multiprocessing, but maybe coverage is unaware)
  • a few others for which I could not find an elegant way to hit, like the exception to not make proc0print fail on non-mpi but mpi-capable systems.

I hope that we can accept the few misses, the diff is only a few percent below the current coverage.

There are some other failing tests because SPEC install on pytnon 3.9 is currently broken.

@landreman @mishapadidar, could I ask you for a review if you have a bit of time to spare? A few people already using it have told me this is very pleasant to work with, and I would like to make these features available in the not-too-distant-future.

I know that there are many more features to add, but I will deal with these in later PRs, otherwise this one will bloat even larger than it currently is.

Future plans include:

  • make InterpolatedField aware of its underlying MagneticField DoF's and trigger re-computation on first access after DoF change.
  • include a SimsoptParticleIntegrator (requires me to first familiarize myself with how particle tracing works... or I could delegate...)
  • deal with BoozerMagneticField in integration and in particle integration.
  • methods for Integrator to generate Surface and Curve objects from the integration results.

@smiet smiet requested a review from andrewgiuliani October 22, 2025 13:07
and being an Optimizable, the results of integrations can be
optimized.
"""
def __init__(self, field: MagneticField, comm=None, nfp=None, stellsym=False, R0=None, test_symmetries=True):
Copy link
Contributor

Choose a reason for hiding this comment

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

can we remove test_symmetries?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As in completely remove the functionality?
We can, but I thought it prudent to have a few basic checks before we create an object that a user will blindly trust. It is too easy to pass the wrong NFP or assume something is stellarator-symmetric.
These properties are relied upon by PoincarePlotter, and the test is also super fast compared to the time it takes to integrate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to keep this functionality until in a next PR we can move it to the MagneticField class (which should carry information on NFP, symmetries, and a method to test them).

Copy link
Contributor

@mishapadidar mishapadidar left a comment

Choose a reason for hiding this comment

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

The Integrator class is proving two general benefits: it makes integrators Optimizable and it provides convenience methods for unpacking the res_phi_hits data. Not all of the instances of the class adhere to this framework, so I suggest we make an attempt to align the usage. In particular, currently Simsopt traces fieldlines through the compute_fieldlines function which calls the sopp integrators. The development of the ScipyFieldlineIntegrator class suggests that there may be desire for using scipy integrators in place of the sopp integrators. To align with the current simsopt style, we should implement a function similar to sopp.fieldline_tracing but for the scipy integrators. Then we modify the compute_fieldlines function to accept a method=... argument. Finally, the Integrator class can take a method=... argument as well. This would allow us to eliminate the instances of the FieldlineIntegrator that are specific to integration methods, i.e. the ScipyFieldLineInegrator, sliming the code down to have a single FieldLineIntegrator class. In this framework, other instances of the Integrator class would solve other ODES. For example, a GuidingCenterIntegrator class.

A second point is that, since we are setting up field line integration as an Optimizable, we really should be implementing derivatives. Differentiating the field line integration is tractable and greatly enhances the usability and applicability of this class.

I have added a lot of comments, and I have not finished reviewing the PoincarePlotter.
Here are some overarching themes to the comments.

  • Add clean doc strings to all functions with a succinct description of the method, arguments and returns.
  • The integrator classes have some convenience functions which lead to duplication. Consider removing the duplication.

if not periodicity_test:
raise ValueError(f"The field does not adhere to the {self.nfp} field periods it is set to be.")

def Interpolate_field(self, rrange, phirange, zrange, degree=2, skip=None, extrapolate=True):
Copy link
Contributor

Choose a reason for hiding this comment

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

The main purpose of this function is to build an interpolatedfield from a field. Since the method only depends on the field and not the inegrator, this convenience function would be most suited to the MagneticField class rather than the Integrator class.
Removing this method simplifies the Integrator class and should not effect user experience much (they could initialize the Integrator from the interpolatedField easily).
Consider removing this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I remove this classmethod and will in the future make InterpolatedField self-updating.

def generate_symmetry_planes(phis, nfp=1):
"""
Given a list of phis in [0, 2pi/nfp], generate the full list of phis
in [0, 2pi] by adding the symmetry planes.
Copy link
Contributor

Choose a reason for hiding this comment

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

add args

return self._randomcolors

@staticmethod
def generate_phis(nplanes, nfp=1):
Copy link
Contributor

Choose a reason for hiding this comment

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

docstring

return self._res_tys

@property
def res_phi_hits(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

docstring

"""
Generate a hash from dofs, self.phis, self.start_points_RZ, and self.n_transits
"""
hash_list = self.integrator.field.full_x.tolist() + self.phis.tolist() + self.start_points_RZ.flatten().tolist() + [self.n_transits]
Copy link
Contributor

Choose a reason for hiding this comment

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

use the uuid class or similar

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That would cause recompute if a second plotter with an identical field and startpoints, wanted to plot the same. Or in the next session or different kernel. This way, if a file has been generated, and has the same field with dofs, it will not recompute. Except if pythons hash function changes (it had recently), but that is too much of an edge case.

poincare_hash = hash(tuple(hash_list))
return poincare_hash

def save_to_disk(self, filename=None, name=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that the functionality here is to initialize a poincare plotter from data. Rather than save the class, which potentially conflicts with Simsopt save/load structure, we could write a from_phi_hits method than initializes the class from the necessary data.

Copy link
Contributor Author

@smiet smiet Oct 22, 2025

Choose a reason for hiding this comment

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

I can call it differently, this is indeed confusing. It is only to store/retrieve poincare data to/from a file, not the entire object.

@smiet
Copy link
Contributor Author

smiet commented Oct 23, 2025

Thanks @mishapadidar for your points so far. You raise a few points for discussion:

Implement compute_fieldlines with the scipy integration?

I would like to move away completely from the functional paradigm for Poincare plotting, and suggest users to only use class-based Integrators. I left the compute_fieldlines method for reverse compatibility, but want to deprecate that method of performing integration. @landreman @andrewgiuliani, what do you think, update the compute_fieldlines function or move towards deprecating it (and leave it be for now)?

Limiting the scope of each integrator

Scipy and Simsopt fieldline integrators perform different computations, and use different methods, but should we limit each integrator to only one way of solving the equations? Should we merge the two integrators and let it switch between whether sopp or scipy is used based on the requested data from the PoincarePlotter?

I would hope to leave this work to a later PR, and I am not equipped to modify the sopp integrator to implement the features I want

Derivatives of integration

This is in the pipeline. I am trying to get this PR out to cement the class interfaces, and am developing a topological optimisation toolset in parallel that will do this. This will involve giving the ScipyFieldlineIntegrator methods to differentiate integrations, evaluate action integrals, etc. and defining new Optimizables with proper J and dJ functions.

@smiet smiet requested a review from mishapadidar October 27, 2025 16:15
@smiet
Copy link
Contributor Author

smiet commented Jan 7, 2026

@andrewgiuliani @mishapadidar @landreman Think we're good to go! lmk if more changes are needed.

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