Skip to content

Conversation

@ddudt
Copy link
Collaborator

@ddudt ddudt commented Apr 16, 2024

Computes B_plasma in a volume as:

𝐁α΅₯(𝐫) = ΞΌβ‚€/4Ο€ ∫ 𝐉(𝐫') Γ— (𝐫 βˆ’ 𝐫')/|𝐫 βˆ’ 𝐫'|Β³ d³𝐫'

This is useful for when you need B_total = B_plasma + B_external in the full computational domain, not just on the boundary surface.

@codecov
Copy link

codecov bot commented Apr 16, 2024

Codecov Report

❌ Patch coverage is 96.40411% with 21 lines in your changes missing coverage. Please review.
βœ… Project coverage is 94.56%. Comparing base (7aa703f) to head (8f18cf4).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
desc/basis.py 95.23% 7 Missing ⚠️
desc/grid.py 95.31% 6 Missing ⚠️
desc/magnetic_fields/_core.py 75.00% 5 Missing ⚠️
desc/magnetic_fields/_plasma_field.py 97.33% 2 Missing ⚠️
desc/equilibrium/equilibrium.py 98.24% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #996      +/-   ##
==========================================
+ Coverage   94.52%   94.56%   +0.03%     
==========================================
  Files         102      105       +3     
  Lines       28712    29272     +560     
==========================================
+ Hits        27141    27680     +539     
- Misses       1571     1592      +21     
Files with missing lines Coverage Ξ”
desc/coils.py 97.96% <100.00%> (-0.01%) ⬇️
desc/compute/_fast_ion.py 100.00% <100.00%> (ΓΈ)
desc/compute/nabla.py 100.00% <100.00%> (ΓΈ)
desc/integrals/virtual_casing.py 100.00% <100.00%> (ΓΈ)
desc/magnetic_fields/__init__.py 100.00% <100.00%> (ΓΈ)
desc/transform.py 94.21% <100.00%> (+1.56%) ⬆️
desc/equilibrium/equilibrium.py 96.21% <98.24%> (+0.13%) ⬆️
desc/magnetic_fields/_plasma_field.py 97.33% <97.33%> (ΓΈ)
desc/magnetic_fields/_core.py 96.10% <75.00%> (-0.52%) ⬇️
desc/grid.py 94.76% <95.31%> (+0.03%) ⬆️
... and 1 more

... and 3 files with indirect coverage changes

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

@github-actions
Copy link
Contributor

github-actions bot commented Apr 16, 2024

|             benchmark_name             |         dt(%)          |         dt(s)          |        t_new(s)        |        t_old(s)        | 
| -------------------------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- |
 test_build_transform_fft_lowres         |     +0.65 +/- 3.17     | +3.28e-03 +/- 1.60e-02 |  5.08e-01 +/- 1.0e-02  |  5.05e-01 +/- 1.3e-02  |
 test_build_transform_fft_midres         |     +0.48 +/- 1.36     | +2.80e-03 +/- 8.02e-03 |  5.91e-01 +/- 4.7e-03  |  5.89e-01 +/- 6.5e-03  |
 test_build_transform_fft_highres        |     +0.19 +/- 2.26     | +1.84e-03 +/- 2.21e-02 |  9.79e-01 +/- 1.0e-02  |  9.77e-01 +/- 2.0e-02  |
 test_equilibrium_init_lowres            |     +0.51 +/- 0.61     | +1.86e-02 +/- 2.21e-02 |  3.63e+00 +/- 1.5e-02  |  3.61e+00 +/- 1.6e-02  |
 test_equilibrium_init_medres            |     +0.10 +/- 1.63     | +4.19e-03 +/- 6.64e-02 |  4.09e+00 +/- 5.4e-02  |  4.09e+00 +/- 3.9e-02  |
 test_equilibrium_init_highres           |     +0.32 +/- 0.52     | +1.76e-02 +/- 2.87e-02 |  5.50e+00 +/- 2.1e-02  |  5.48e+00 +/- 1.9e-02  |
 test_objective_compile_dshape_current   |     +0.18 +/- 0.92     | +6.74e-03 +/- 3.45e-02 |  3.76e+00 +/- 2.5e-02  |  3.75e+00 +/- 2.4e-02  |
 test_objective_compile_atf              |     +0.08 +/- 1.25     | +6.46e-03 +/- 1.02e-01 |  8.12e+00 +/- 7.9e-02  |  8.11e+00 +/- 6.4e-02  |
 test_objective_compute_dshape_current   |     +1.45 +/- 5.20     | +1.82e-05 +/- 6.52e-05 |  1.27e-03 +/- 5.9e-05  |  1.26e-03 +/- 2.7e-05  |
 test_objective_compute_atf              |     -0.32 +/- 4.53     | -1.38e-05 +/- 1.92e-04 |  4.23e-03 +/- 1.5e-04  |  4.25e-03 +/- 1.2e-04  |
 test_objective_jac_dshape_current       |     -5.67 +/- 11.28    | -2.18e-03 +/- 4.33e-03 |  3.62e-02 +/- 3.1e-03  |  3.84e-02 +/- 3.0e-03  |
 test_objective_jac_atf                  |     +0.14 +/- 2.41     | +2.62e-03 +/- 4.53e-02 |  1.89e+00 +/- 3.8e-02  |  1.88e+00 +/- 2.4e-02  |
 test_perturb_1                          |     +0.50 +/- 0.52     | +6.47e-02 +/- 6.73e-02 |  1.30e+01 +/- 4.8e-02  |  1.30e+01 +/- 4.7e-02  |
 test_perturb_2                          |     +0.51 +/- 0.55     | +9.10e-02 +/- 9.81e-02 |  1.79e+01 +/- 8.3e-02  |  1.78e+01 +/- 5.3e-02  |
 test_proximal_jac_atf                   |     +0.16 +/- 0.96     | +1.18e-02 +/- 7.01e-02 |  7.31e+00 +/- 5.7e-02  |  7.30e+00 +/- 4.1e-02  |
 test_proximal_freeb_compute             |     +1.08 +/- 0.70     | +1.90e-03 +/- 1.23e-03 |  1.78e-01 +/- 7.3e-04  |  1.76e-01 +/- 9.8e-04  |
 test_proximal_freeb_jac                 |     +0.39 +/- 0.99     | +2.83e-02 +/- 7.19e-02 |  7.31e+00 +/- 5.2e-02  |  7.28e+00 +/- 4.9e-02  |
 test_solve_fixed_iter                   |     +0.80 +/- 8.77     | +1.17e-01 +/- 1.29e+00 |  1.48e+01 +/- 9.3e-01  |  1.47e+01 +/- 8.9e-01  |

@dpanici
Copy link
Collaborator

dpanici commented Apr 24, 2024

use this fxn

def biot_savart_general(re, rs, J, dV):

@dpanici
Copy link
Collaborator

dpanici commented Jul 3, 2024

scipy.integrate.tpl_quad to check this

@f0uriest
Copy link
Member

Several people at the scidac meeting today are using the BMW code: https://github.com/ORNL-Fusion/BMW/tree/master

refs: https://scipub.euro-fusion.org/wp-content/uploads/eurofusion/WPS1CPR17_17542_submitted-4.pdf
https://bpb-us-e2.wpmucdn.com/wordpress.auburn.edu/dist/5/118/files/2022/05/ISHW2017.pdf

basically it does a biot-savart volume integral to compute A (not B), then takes the finite difference curl of A to get B. As far as I can tell from the code the source grid is hardcoded to ntheta=101, ns, nzeta taken from the vmec/mgrid file. Allegedly it works well and gives accurate results?

Could try something similar (and maybe use spectral derivatives for going A->B)

@ddudt
Copy link
Collaborator Author

ddudt commented Jul 18, 2024

Try implementing Eq. 17-18 of Peraza-Rodriguez et al. Phys. Plasmas 2017
It should be a simple linear system solve to get the Fourier-Zernike coefficients of the vector potential as:
$\nabla \times (\nabla \times \mathbf{A}) = \mu_0 \mathbf{J}$
Eq. 16 might be needed for boundary conditions on $\mathbf{A}$ at $\rho=0$ and $\rho=1$. Then the magnetic field is:
$\mathbf{B} = \nabla \times \mathbf{A}$
And all of this can be done in the native $(\rho,\theta,\zeta)$ coordinate system.

@ddudt ddudt self-assigned this May 21, 2025
@github-actions
Copy link
Contributor

github-actions bot commented May 21, 2025

Memory benchmark result

|               Test Name                |      %Ξ”      |    Master (MB)     |      PR (MB)       |    Ξ” (MB)    |    Time PR (s)     |  Time Master (s)   |
| -------------------------------------- | ------------ | ------------------ | ------------------ | ------------ | ------------------ | ------------------ |
  test_objective_jac_w7x                 |    4.53 %    |     3.796e+03      |     3.968e+03      |    171.93    |       39.19        |       36.44        |
  test_proximal_jac_w7x_with_eq_update   |    0.82 %    |     6.440e+03      |     6.492e+03      |    52.57     |       161.59       |       161.18       |
  test_proximal_freeb_jac                |   -0.29 %    |     1.324e+04      |     1.320e+04      |    -38.53    |       82.12        |       83.13        |
  test_proximal_freeb_jac_blocked        |   -0.81 %    |     7.549e+03      |     7.487e+03      |    -61.39    |       72.69        |       72.61        |
  test_proximal_freeb_jac_batched        |    0.37 %    |     7.465e+03      |     7.493e+03      |    27.90     |       72.59        |       71.79        |
  test_proximal_jac_ripple               |    1.04 %    |     3.458e+03      |     3.494e+03      |    35.98     |       64.79        |       65.09        |
  test_proximal_jac_ripple_bounce1d      |   -0.96 %    |     3.517e+03      |     3.483e+03      |    -33.85    |       76.15        |       76.24        |
  test_eq_solve                          |    5.97 %    |     2.016e+03      |     2.136e+03      |    120.25    |       93.58        |       93.37        |

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

@ddudt
Copy link
Collaborator Author

ddudt commented Jun 3, 2025

Update: I fixed a silly bug and now this is working. Direct Biot-Savart volume integral to compute B and the similar volume integral to compute A both agree well with BMW results at moderate resolution. But more benchmarking is needed.

@rahulgaur104
Copy link
Collaborator

Thank you! Could be really useful for my umbilic stuff!


# shape = (source_grid.num_nodes, eval_grid.num_nodes, 3) # noqa: E800
dx = rpz2xyz(eval_grid)[None, :, :] - rpz2xyz(data["x"])[:, None, :]
B = np.sum( # sum over source_grid nodes
Copy link
Collaborator

Choose a reason for hiding this comment

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

use biot_savart_general instead

@dpanici
Copy link
Collaborator

dpanici commented Jun 5, 2025

  • check error for numpy vs jax.numpy
  • show plots of error
  • errors may be due to large matrix dot product errors?

@ddudt
Copy link
Collaborator Author

ddudt commented Jun 5, 2025

Possibly redundant with #1436, should merge them once we decide on a public API.

@maya-avida maya-avida marked this pull request as ready for review August 29, 2025 15:24
@maya-avida maya-avida requested a review from dpanici September 2, 2025 17:45
self._set_up()


class DoubleChebyshevFourierBasis(_Basis):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to make things clearer that this is a real space basis (as opposed to flux coordinates) in the name/docstring?

Maximum vertical resolution.
NFP : int
Number of field periods.
sym : {``'cos'``, ``'sin'``, ``False``}
Copy link
Collaborator

Choose a reason for hiding this comment

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

there is not 't' in real space, what do these symmetries refer to now?

----------
L : int
Maximum radial resolution.
M : int
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't love this, almost everywhere in the code N is toroidal, yet here we change it so M is toroidal. I would prefer if here we don't use M, and keep N as toroidal and use something else for the vertical resolution (Or be explicit and write N_R N_phi N_Z as the resolutions

Parameters
----------
grid : Grid or ndarray of float, size(num_nodes,3)
Node coordinates, in (rho,phi,Z).
Copy link
Collaborator

Choose a reason for hiding this comment

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

rho or R?

grid : Grid or ndarray of float, size(num_nodes,3)
Node coordinates, in (rho,phi,Z).
derivatives : ndarray of int, shape(num_derivatives,3)
Order of derivatives to compute in (rho,phi,Z).
Copy link
Collaborator

Choose a reason for hiding this comment

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

rho or R?

return np.array([]).reshape((grid.num_nodes, 0))

# Get the nodes of the grid
r, p, z = grid.nodes.T
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick but if r is really cylindrical R, use R, same for Z

l, m, n = modes.T

# The coordinates in the grid are hardcoded to be called rho, theta, zeta,
# but these vars are just unique values for 1st, 2nd, and 3rd coordinates
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will change, presumedly, when we figure out our new grid stuff @ddudt #1840

)


def chebfit(y, axis):
Copy link
Collaborator

Choose a reason for hiding this comment

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

we may aready have some utility functions that do this, see

def compute_cheb(self, x):

return idct(y_c / f.reshape(f_shape), axis=axis, type=1, norm=None)


def fftfit(y, axis, n=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

if these are to be public, some more safeguards should be added. If these are not meant to be public, add _ before the name

"""
Real fast fourier transform along axis.

Designed to convert coefficients into the form expected by the
Copy link
Collaborator

Choose a reason for hiding this comment

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

what is meant here? takes in a function evaluated at linearly spaced nodes, then fourier transforms it, then adjusts the coefficients to fit what DESC's fourier function requires?

Parameters
----------
y : ndarray, shape(...,2N+1,...)
Function to decompose into basis of Chebyshev functions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this docstring accurate?

transform from the spectral basis on which A is calculated to the
real grid on which the curl is to be evaluated.
the transform must have been built, with derivs>=1.
scales: jnp.ndarray, shape (3,)
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't this be done in the transform itself, not here?

Returns
-------
curl_A : ndarray, shape(n,3)
The curl of the vector field, in cylindrical coordinates.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
The curl of the vector field, in cylindrical coordinates.
The curl of the vector field, in cylindrical coordinates, at the points specified by `out_transform`.

out_R : ndarray, shape(n,)
radial location for each point at which to calculate
the curl. out_R should never be 0.
in_transform: Transform
Copy link
Collaborator

@dpanici dpanici Sep 8, 2025

Choose a reason for hiding this comment

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

specify these should be bases and grids of R, phi, Z

for d in range(3):
if c == 1 and d == 0:
# partial (R*A_phi)/partial R instead of partial A_phi/partial R
term_cd = out_transform.transform(RA_phi_coeff, dr=1)
Copy link
Collaborator

@dpanici dpanici Sep 8, 2025

Choose a reason for hiding this comment

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

cant you just use chain rule and do d/dR (R Aphi) = Aphi + R d(Aphi)/dR? then you only need to do one fit above, and all of the partial derivatives are "consistent" in that they are all derivaitves of A, instead of derivatives of A and some derivatves of R*A (which may have a different spectral width than A i.e. same resolution basis may be a worse for for R*A then for A)

A_coeff = in_transform.fit(
A * jnp.stack([in_R, jnp.ones_like(in_R), jnp.ones_like(in_R)], axis=-1)
)
# Calculate matrix of terms for the curl
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
# Calculate matrix of terms for the curl
# Calculate matrix of terms for the div

@@ -0,0 +1,163 @@
"""
Functions for taking the curl and divergence in cylindrical coordinates.
Copy link
Collaborator

Choose a reason for hiding this comment

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

usually desc.compute has things for computing stuff in the compute dicts. This I could see as useful in the future when we have things in the compute dict using RpZ grids (#1805 ), for now let's call this like _nabla_utils.py maybe or _real_space_deriv_utils.py

coords = jnp.atleast_2d(coords)

# Biot-Savart method
if method == methods[0]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
if method == methods[0]:
if method == "biot-savart":

I prefer to be verbose in the code here

B = xyz2rpz_vec(B, phi=coords[:, 1])

# Virtual casing method
elif method == methods[1]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
elif method == methods[1]:
elif method == "virtual casing":

from desc.utils import errorif, xyz2rpz_vec


def integrate_surface(coords, source_data, source_grid, kernel, chunk_size=None):
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe different name like integrate_surface_at_external_point

it does not need to be strictly outside per-say right? just needs to be off-surface, the kernels don't really care about outside or inside as far as the singularity goes

Copy link
Collaborator

@dpanici dpanici left a comment

Choose a reason for hiding this comment

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

partially reviewed but already some changes noted

field = args[0]
r = rpz[:, 0]
br, bp, bz = (
scale
Copy link
Contributor

Choose a reason for hiding this comment

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

[flake8] <821> reported by reviewdog 🐢
undefined name 'scale'

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants