Skip to content

Inverse solver: enabling second order magnetic null constraints (for snowflake divertors)#56

Open
kpentland wants to merge 13 commits into
mainfrom
inverse_solver_snowlake_constraints
Open

Inverse solver: enabling second order magnetic null constraints (for snowflake divertors)#56
kpentland wants to merge 13 commits into
mainfrom
inverse_solver_snowlake_constraints

Conversation

@kpentland
Copy link
Copy Markdown
Collaborator

@kpentland kpentland commented Feb 12, 2026

In addition to being able to set first order magnetic null constraints in the inverse solver, we've now added functionality for setting second order null constraints. This is particularly useful for making snowflake divertor geometries.

Screenshot 2026-02-12 at 17 45 07

There are updates to the Example01b notebook for how to use these constraints and an example snowflake equilibrium.

Screenshot 2026-02-13 at 10 52 51

Note that this requires PR 50 in FreeGS4E.

@kpentland kpentland added the enhancement New feature or request label Feb 12, 2026
@kpentland kpentland added ready-for-final-tests Pull request is ready to run final pre-merge tests feature request labels Mar 24, 2026
@kpentland kpentland self-assigned this Mar 24, 2026
@kpentland kpentland added ready-for-final-tests Pull request is ready to run final pre-merge tests and removed ready-for-final-tests Pull request is ready to run final pre-merge tests labels Mar 24, 2026
@timothy-nunn
Copy link
Copy Markdown
Contributor

Sorry @kpentland I forgot about this, I have put it on my list for review tomorrow!

Comment thread examples/example01b - advanced_static_inverse_solve.ipynb
Comment thread examples/example01b - advanced_static_inverse_solve.ipynb
Comment thread examples/example01b - advanced_static_inverse_solve.ipynb
Comment thread freegsnke/inverse.py
Comment thread freegsnke/inverse.py
Comment on lines +104 to +110
isoflux_set : list or np.array, optional
list of isoflux objects, each with structure
[Rcoords, Zcoords]
with Rcoords and Zcoords being 1D lists of the coords of all points that are requested to have the same flux value
null_points : list or np.array, optional
structure [Rcoords, Zcoords], with Rcoords and Zcoords being 1D lists
Sets the coordinates of the desired null points, including both Xpoints and Opoints
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isoflux_set and null_points is already documented above, is this a duplicate?

Comment thread freegsnke/inverse.py
Comment on lines +115 to +118
psi_vals : list or np.array, optional
structure [Rcoords, Zcoords, psi_values]
with Rcoords, Zcoords and psi_values having the same shape
Sets the desired values of psi for a set of coordinates, possibly an entire map
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

psi_vals is documented above, is this a duplicate?

Comment thread freegsnke/inverse.py
Comment on lines +999 to +1111
def build_psi_vals_lsq(self, full_currents_vec):
"""
Construct a least-squares system enforcing direct flux value constraints.

This method builds the optimisation system:

A I_control ≈ b

where:

ψ_model(R_i, Z_i) = ψ_target(R_i, Z_i)

is enforced by matching magnetic flux values at specified locations.

The optimisation residual is defined as:

b = ψ_tokamak(I) + ψ_plasma
- ψ_target
- ⟨b⟩

Mean flux removal is applied to remove arbitrary vertical flux offsets,
since the Grad–Shafranov equation is invariant under constant flux shifts.

Parameters
----------
full_currents_vec : ndarray
Full coil current vector.

Example:
eq.tokamak.getCurrentsVec()

Returns
-------
A : ndarray
Jacobian matrix mapping coil current perturbations → flux changes.

b : ndarray
Flux mismatch residual vector.

normalised_loss : list of float
Normalised constraint violation magnitude.

Notes
-----
This constraint formulation is commonly used for:

• Magnetic axis pinning
• Boundary flux matching
• Profile shape control

Mathematical formulation
------------------------
Solve:

min_I || G I + ψ_plasma − ψ_target ||²
"""

# flux response wrt coil currents
A_r = self.Gbr_2nd_order[self.control_mask].T
b_r = np.sum(
self.Gbr_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_r += self.Brp_2nd_order # plasma contribution
loss = [np.linalg.norm(b_r)]

# Bz field constraint
A_z = self.Gbz_2nd_order[self.control_mask].T
b_z = np.sum(
self.Gbz_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_z += self.Bzp_2nd_order # plasma contribution
loss.append(np.linalg.norm(b_z))

# dBrdr field constraint
A_r_deriv = self.Gdbrdr_2nd_order[self.control_mask].T
b_r_deriv = np.sum(
self.Gdbrdr_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_r_deriv += self.dBrdrp_2nd_order # plasma contribution
loss.append(np.linalg.norm(b_r_deriv))

# dBzdz field constraint
A_z_deriv = self.Gdbzdz_2nd_order[self.control_mask].T
b_z_deriv = np.sum(
self.Gdbzdz_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_z_deriv += self.dBzdzp_2nd_order # plasma contribution
loss.append(np.linalg.norm(b_z_deriv))

# dBrdz field constraint
A_r_deriv_cross = self.Gdbrdz_2nd_order[self.control_mask].T
b_r_deriv_cross = np.sum(
self.Gdbrdz_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_r_deriv_cross += self.dBrdzp_2nd_order # plasma contribution
loss.append(np.linalg.norm(b_r_deriv_cross))

# dBzdr field constraint
A_z_deriv_cross = self.Gdbzdr_2nd_order[self.control_mask].T
b_z_deriv_cross = np.sum(
self.Gdbzdr_2nd_order * full_currents_vec[:, np.newaxis], axis=0
) # coils contribution
b_z_deriv_cross += self.dBzdrp_2nd_order # plasma contribution
loss.append(np.linalg.norm(b_z_deriv_cross))

A = np.concatenate(
(A_r, A_z, A_r_deriv, A_z_deriv, A_r_deriv_cross, A_z_deriv_cross), axis=0
)
b = -np.concatenate(
(b_r, b_z, b_r_deriv, b_z_deriv, b_r_deriv_cross, b_z_deriv_cross), axis=0
)
return A, b, loss

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Has this been added by mistake?

  1. A function build_psi_vals_lsq is already defined (just below)
  2. Isn't this a duplicate of build_null_points_2nd_order_lsq?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants