From a4f947f726a03cea706ed5a7611ed0d082882822 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sat, 1 Jun 2024 21:26:50 -0500 Subject: [PATCH 01/58] Add Gamma_c to neoclassical compute quantities --- desc/compute/_neoclassical.py | 162 ++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 56d8c2a0aa..2d48debc58 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -352,3 +352,165 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): * data["effective ripple raw"] ) return data + + +@register_compute_fun( + name="Gamma_c", + label="π/(2√2) ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ \\rangle", + units="~", + units_long="None", + description="Energetic ion confinement proxy", + dim=1, + params=[], + transforms={"grid": []}, + profiles=[], + coordinates="r", + data=[ + "min_tz |B|", + "max_tz |B|", + "B^zeta", + "|B|", + "|B|_z|r,a", + "cvdrift0", + "gbdrift", + "L|r,a", + ], + grid_requirement=[ + "source_grid", + lambda grid: grid.source_grid.coordinates == "raz" + and grid.source_grid.is_meshgrid, + ], + bounce_integral=( + "callable : Method to compute bounce integrals. " + "(You may want to wrap desc.compute.bounce_integral.bounce_integral " + "to change optional parameters such as quadrature resolution, etc.)." + ), + batch="bool : Whether to perform computation in a batched manner.", + quad=( + "callable : Quadrature method over velocity coordinate. " + "Default is composite Simpson's rule. " + "Accepts any callable with signature matching quad(f(λ), λ, ..., axis). " + "Accepts adaptive quadrature methods from quadax wrapped with vec_quadax. " + "If using an adaptive method, it is highly recommended to set batch=True." + ), + num_pitch=( + "int : Resolution for quadrature over velocity coordinate. " + "Default is 75. This setting is ignored for adaptive quadrature." + ), +) +def _Gamma_c(params, transforms, profiles, data, **kwargs): + """Energetic ion confinement proxy. + + A model for the fast evaluation of prompt losses of energetic ions in stellarators. + J.L. Velasco et al 2021 Nucl. Fusion 61 116059. + https://doi.org/10.1088/1741-4326/ac2994. + Equation 16. (Not equation 18). + + Poloidal motion of trapped particle orbits in real-space coordinates. + V. V. Nemov, S. V. Kasilov, W. Kernbichler, G. O. Leitold. + Phys. Plasmas 1 May 2008; 15 (5): 052501. + https://doi.org/10.1063/1.2912456. + Equation 61, using Velasco's γ_c from equation 15 of the above paper. + + Besides the difference in γ_c mentioned above, Nemov's Γ_c and Velasco Γ_c + as defined in equation 16 are identical, although Nemov's expression is + precise while Velasco's requires a flexible interpretation for the expression + to be well-defined. + + Also, Velasco Γ_c as defined in equation 18 does not seem to match + equation 16. Note that + dλ v τ_b = - 4 (∂I/∂b) db = 4 ∂I/∂((λB₀)⁻¹) B₀⁻¹λ⁻² dλ + 4π² ∂Ψₜ/∂V = lim{L → ∞} ( [∫₀ᴸ ds/(B √g)] / [∫₀ᴸ ds/B] ) + where the integrals are along an irrational field line with + ds / B given by dζ / B^ζ + √g the (Ψ, α, ζ)-coordinate Jacobian + If the (missing?) √g factor in Velasco Γ_c equation 18 is pushed into the + integral over alpha in Velasco equation 18 then we have that + eq. 18 Velasco Γ_c ∼ eq. 16 Velasco Γ_c * lim{L → ∞} ∫₀ᴸ ds/(B √g). + + """ + bounce = kwargs.get("bounce_integral", bounce_integral) + batch = kwargs.get("batch", False) + quad = kwargs.get("quad", quadax.simpson) + num_pitch = kwargs.get("num_pitch", 75) + + g = transforms["grid"].source_grid + knots = g.compress(g.nodes[:, 2], surface_label="zeta") + pitch = _get_pitch(g, data, quad, num_pitch) + + def d_gamma_c(f, B, pitch): + return f * (1 - pitch * B / 2) / jnp.sqrt(1 - pitch * B) + + def dK(B, pitch): + return 1 / jnp.sqrt(1 - pitch * B) + + if _is_Newton_Cotes(quad): + bounce_integrate, _ = bounce( + data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots + ) + + def d_Gamma_c(pitch): + """Return 2λ⁻²B₀⁻¹ ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ evaluated at λ = pitch. + + Parameters + ---------- + pitch : Array, shape(*pitch.shape[:-1], g.num_rho * g.num_alpha) + Pitch angle. + + Returns + ------- + d_Gamma_c : Array, shape(pitch.shape) + 2λ⁻²B₀⁻¹ ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ + + """ + gamma_c = ( + 2 + / jnp.pi + * jnp.arctan( + bounce_integrate(d_gamma_c, data["cvdrift0"], pitch, batch=batch) + / bounce_integrate(d_gamma_c, data["gbdrift"], pitch, batch=batch) + ) + ) + # K = 2λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where I is given in Nemov equation 36. + # The factor of B₀ cancels, making this quantity independent of the + # chosen reference magnetic field strength. + K = bounce_integrate(dK, [], pitch, batch=batch) + return jnp.nansum(gamma_c**2 * K, axis=-1) + + # This has units of meters / tesla. + Gamma_c = quad(d_Gamma_c(pitch), pitch, axis=0) + else: + # Use adaptive quadrature. + + def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): + bounce_integrate, _ = bounce(B_sup_z, B, B_z_ra, knots) + gamma_c = ( + 2 + / jnp.pi + * jnp.arctan( + bounce_integrate(d_gamma_c, cvdrift0, pitch, batch=batch) + / bounce_integrate(d_gamma_c, gbdrift, pitch, batch=batch) + ) + ) + K = bounce_integrate(dK, [], pitch, batch=batch) + return jnp.squeeze(jnp.nansum(gamma_c**2 * K, axis=-1)) + + args = [ + f.reshape(g.num_rho, g.num_alpha, g.num_zeta) + for f in [ + data["B^zeta"], + data["|B|"], + data["|B|_z|r,a"], + data["cvdrift0"], + data["gbdrift"], + ] + ] + Gamma_c = quad(d_Gamma_c, pitch, *args) + + Gamma_c = ( + jnp.pi + / (4 * 2**0.5) + * _poloidal_average(g, Gamma_c.reshape(g.num_rho, g.num_alpha) / data["L|r,a"]) + ) + data["Gamma_c"] = g.expand(Gamma_c) + return data From 2cea6389b36d624ec224c2ff1a0c48ff45718893 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 3 Jun 2024 15:36:37 -0500 Subject: [PATCH 02/58] Update thing after merge --- desc/compute/_neoclassical.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index dc0e28d2d5..e936bc8328 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -323,6 +323,9 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): "to change optional parameters such as quadrature resolution, etc.)." ), batch="bool : Whether to perform computation in a batched manner.", + # Composite quadrature should perform better than higher order methods. + # TODO: Two layers of pitch for quadrature, linearly spaced minB to maxB + # and then second layer for extrema in fixed [0, zeta*]. quad=( "callable : Quadrature method over velocity coordinate. " "Default is composite Simpson's rule. " @@ -381,7 +384,7 @@ def d_gamma_c(f, B, pitch): def dK(B, pitch): return 1 / jnp.sqrt(1 - pitch * B) - if _is_Newton_Cotes(quad): + if not _is_adaptive(quad): bounce_integrate, _ = bounce( data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots ) @@ -447,7 +450,7 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): Gamma_c = ( jnp.pi / (4 * 2**0.5) - * _poloidal_average(g, Gamma_c.reshape(g.num_rho, g.num_alpha) / data["L|r,a"]) + * _poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha) / data["L|r,a"]) ) data["Gamma_c"] = g.expand(Gamma_c) return data From c17a9df808b2f628a9a21c114642099b0bc89ba6 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 4 Jun 2024 13:37:25 -0500 Subject: [PATCH 03/58] Add Gamma_c plot --- desc/compute/_neoclassical.py | 10 ++++------ tests/test_neoclassical.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index e936bc8328..1521cdc0f2 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -293,7 +293,7 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="Gamma_c", - label="π/(2√2) ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ \\rangle", + label="Γ_c = π/(2√2) ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ \\rangle", units="~", units_long="None", description="Energetic ion confinement proxy", @@ -312,11 +312,7 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): "gbdrift", "L|r,a", ], - grid_requirement=[ - "source_grid", - lambda grid: grid.source_grid.coordinates == "raz" - and grid.source_grid.is_meshgrid, - ], + source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, bounce_integral=( "callable : Method to compute bounce integrals. " "(You may want to wrap desc.compute.bounce_integral.bounce_integral " @@ -338,6 +334,8 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): "Default is 75. This setting is ignored for adaptive quadrature." ), ) +# temporary +@partial(jit, static_argnames=["bounce_integral", "batch", "quad", "num_pitch"]) def _Gamma_c(params, transforms, profiles, data, **kwargs): """Energetic ion confinement proxy. diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index b591b894d4..75f8328b5b 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -4,6 +4,7 @@ import numpy as np import pytest +from desc.backend import jnp from desc.equilibrium import Equilibrium from desc.equilibrium.coords import rtz_grid from desc.vmec import VMECIO @@ -98,6 +99,34 @@ def test_effective_ripple(): plt.close() +@pytest.mark.unit +def test_Gamma_c(): + """Compare DESC effective ripple against NEO STELLOPT.""" + eq = Equilibrium.load( + "tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0" + ".15_L_9_M_9_N_24_output.h5" + ) + rho = jnp.linspace(0, 1, 20) + alpha = jnp.array([0]) + # TODO: Here's a potential issue, resolve with 2d spline. + knots = jnp.linspace(-30 * jnp.pi, 30 * jnp.pi, 2000) + grid = rtz_grid( + eq, rho, alpha, knots, coordinates="raz", period=(jnp.inf, 2 * jnp.pi, jnp.inf) + ) + data = eq.compute("Gamma_c", grid=grid) + assert np.isfinite(data["Gamma_c"]).all() + Gamma_c = grid.compress(data["Gamma_c"]) + + fig, ax = plt.subplots() + ax.plot(rho, Gamma_c, marker="o") + ax.set_xlabel(r"$\rho$") + ax.set_ylabel(r"$\Gamma_{c}$") + ax.set_title(r"DESC $\Gamma_{c}$") + plt.tight_layout() + plt.show() + plt.close() + + class NEOWrapper: """Class to easily make NEO and BOOZxform inputs from DESC equilibria.""" From c616ee5b4e48d7acbed5aa24905822e1c9258652 Mon Sep 17 00:00:00 2001 From: unalmis Date: Wed, 12 Jun 2024 02:13:19 -0500 Subject: [PATCH 04/58] Merge branch 'ripple' into Gamma_c --- desc/compute/_metric.py | 21 +- desc/compute/_neoclassical.py | 383 ++++++++---------- desc/compute/bounce_integral.py | 241 ++++++----- tests/baseline/test_Gamma_c.png | Bin 0 -> 14770 bytes tests/baseline/test_effective_ripple.png | Bin 0 -> 13223 bytes ...nk_fixed_bdry_r0.15_L_9_M_9_N_24_output.h5 | Bin 131569 -> 0 bytes ....QI_plunk_fixed_surf_r0.15_N_24_hires_ns99 | 98 ----- tests/test_axis_limits.py | 9 +- tests/test_bounce_integral.py | 37 +- tests/test_neoclassical.py | 224 ++-------- 10 files changed, 360 insertions(+), 653 deletions(-) create mode 100644 tests/baseline/test_Gamma_c.png create mode 100644 tests/baseline/test_effective_ripple.png delete mode 100644 tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0.15_L_9_M_9_N_24_output.h5 delete mode 100644 tests/inputs/neo_out.QI_plunk_fixed_surf_r0.15_N_24_hires_ns99 diff --git a/desc/compute/_metric.py b/desc/compute/_metric.py index 2eecb559cc..15db95860f 100644 --- a/desc/compute/_metric.py +++ b/desc/compute/_metric.py @@ -1858,8 +1858,8 @@ def _gradzeta(params, transforms, profiles, data, **kwargs): # https://tinyurl.com/54udvaa4 label="\\mathrm{gbdrift} = 1/B^{2} (\\mathbf{b}\\times\\nabla B) \\cdot" + "\\nabla \\alpha", - units="1/(T-m^{2})", - units_long="inverse Tesla meters^2", + units="1 / Wb", + units_long="Inverse webers", description="Binormal component of the geometric part of the gradB drift" + " used for local stability analyses, Gamma_c, epsilon_eff etc.", dim=1, @@ -1885,8 +1885,8 @@ def _gbdrift(params, transforms, profiles, data, **kwargs): # https://tinyurl.com/54udvaa4 label="\\mathrm{cvdrift} = 1/B^{3} (\\mathbf{b}\\times\\nabla(p + B^2/2))" + "\\cdot \\nabla \\alpha", - units="1/(T-m^{2})", - units_long="inverse Tesla meters^2", + units="1 / Wb", + units_long="Inverse webers", description="Binormal component of the geometric part of the curvature drift" + " used for local stability analyses, Gamma_c, epsilon_eff etc.", dim=1, @@ -1908,9 +1908,9 @@ def _cvdrift(params, transforms, profiles, data, **kwargs): # eqn. 48 of Introduction to Quasisymmetry by Landreman # https://tinyurl.com/54udvaa4 label="\\mathrm{cvdrift0} = 1/B^{2} (\\mathbf{b}\\times\\nabla B)" - + "\\cdot \\nabla \\rho", - units="1/(T-m^{2})", - units_long="inverse Tesla meters^2", + + "\\cdot (2 \\rho \\nabla \\rho)", + units="1 / Wb", + units_long="Inverse webers", description="Radial component of the geometric part of the curvature drift" + " used for local stability analyses for Gamma_c.", dim=1, @@ -1918,10 +1918,13 @@ def _cvdrift(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="rtz", - data=["|B|^2", "b", "e^rho", "grad(|B|)"], + data=["|B|^2", "b", "e^rho", "grad(|B|)", "rho"], ) def _cvdrift0(params, transforms, profiles, data, **kwargs): data["cvdrift0"] = ( - 1 / data["|B|^2"] * (dot(data["b"], cross(data["grad(|B|)"], data["e^rho"]))) + 2 + * data["rho"] + / data["|B|^2"] + * dot(data["b"], cross(data["grad(|B|)"], data["e^rho"])) ) return data diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 1521cdc0f2..afc64d3bde 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -11,25 +11,19 @@ from functools import partial -import quadax +from orthax.legendre import leggauss +from quadax import romberg, simpson from termcolor import colored -from desc.backend import jit, jnp, trapezoid +from desc.backend import imap, jit, jnp, trapezoid from ..utils import warnif from .bounce_integral import bounce_integral, get_pitch from .data_index import register_compute_fun -def _is_adaptive(quad): - if hasattr(quad, "is_adaptive"): - return quad.is_adaptive - else: - return quad not in [trapezoid, quadax.trapezoid, quadax.simpson] - - -def vec_quadax(quad): - """Vectorize an adaptive quadrature method from quadax to compute ripple. +def _vec_quadax(quad, **kwargs): + """Vectorize an adaptive quadrature method from quadax. Parameters ---------- @@ -42,11 +36,9 @@ def vec_quadax(quad): Vectorized adaptive quadrature method. """ - if not _is_adaptive(quad): - return quad def vec_quad(fun, interval, B_sup_z, B, B_z_ra, arg1, arg2): - return quad(fun, interval, args=(B_sup_z, B, B_z_ra, arg1, arg2))[0] + return quad(fun, interval, args=(B_sup_z, B, B_z_ra, arg1, arg2), **kwargs)[0] vec_quad = jnp.vectorize( vec_quad, signature="(2),(m),(m),(m),(m),(m)->()", excluded={0} @@ -55,84 +47,102 @@ def vec_quad(fun, interval, B_sup_z, B, B_z_ra, arg1, arg2): def _poloidal_mean(grid, f): + """Integrate f over poloidal angle and divide by 2π.""" assert f.shape[-1] == grid.num_poloidal - if grid.spacing is None: - warnif( - grid.num_poloidal != 1, - msg=colored("Reduced via uniform poloidal mean.", "yellow"), - ) + if grid.num_poloidal == 1: + return jnp.squeeze(f, axis=-1) + if not hasattr(grid, "spacing"): + warnif(True, msg=colored("Reduced via uniform poloidal mean.", "yellow")) return jnp.mean(f, axis=-1) - else: - assert grid.is_meshgrid - dp = grid.compress(grid.spacing[:, 1], surface_label="poloidal") - return f @ dp / jnp.sum(dp) + assert grid.is_meshgrid + dp = grid.compress(grid.spacing[:, 1], surface_label="poloidal") + return f @ dp / jnp.sum(dp) + + +def _get_pitch(grid, data, num, for_adaptive=False): + """Get points for quadrature over velocity coordinate. + + Parameters + ---------- + grid : Grid + The grid on which data is computed. + data : dict + Dictionary containing min and max |B| over each flux surface. + num : int + Number of values to uniformly space in between. + for_adaptive : bool + Whether to return just the points useful for an adaptive quadrature. + Returns + ------- + pitch : Array + Pitch values in the desired shape to use in compute methods. -def _get_pitch(grid, data, quad, num=75): - # Get endpoints of integral over pitch for each flux surface. - # with num values uniformly spaced in between. + """ min_B = grid.compress(data["min_tz |B|"]) max_B = grid.compress(data["max_tz |B|"]) - if not _is_adaptive(quad): + if for_adaptive: + pitch = jnp.expand_dims(1 / jnp.stack([max_B, min_B], axis=-1), axis=1) + assert pitch.shape == (grid.num_rho, 1, 2) + else: pitch = get_pitch(min_B, max_B, num) pitch = jnp.broadcast_to( pitch[..., jnp.newaxis], (pitch.shape[0], grid.num_rho, grid.num_alpha) ).reshape(pitch.shape[0], grid.num_rho * grid.num_alpha) - else: - pitch = 1 / jnp.stack([max_B, min_B], axis=-1)[:, jnp.newaxis] - assert pitch.shape == (grid.num_rho, 1, 2) return pitch @register_compute_fun( - name="L|r,a", + name="", label="\\int_{\\zeta_{\\mathrm{min}}}^{\\zeta_{\\mathrm{max}}" " \\frac{d\\zeta}{B^{\\zeta}}", units="m / T", units_long="Meter / tesla", - description="Length along field line", - dim=2, + description="(Mean) length along field line(s)", + dim=1, params=[], transforms={"grid": []}, profiles=[], - coordinates="ra", + coordinates="r", data=["B^zeta"], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, ) -def _L_ra(data, transforms, profiles, **kwargs): +def _L_ra_fsa(data, transforms, profiles, **kwargs): g = transforms["grid"].source_grid shape = (g.num_rho, g.num_alpha, g.num_zeta) - data["L|r,a"] = quadax.simpson( + L_ra = simpson( jnp.reshape(1 / data["B^zeta"], shape), jnp.reshape(g.nodes[:, 2], shape), axis=-1, ) + data[""] = g.expand(_poloidal_mean(g, L_ra)) return data @register_compute_fun( - name="G|r,a", + name="", label="\\int_{\\zeta_{\\mathrm{min}}}^{\\zeta_{\\mathrm{max}}" " \\frac{d\\zeta}{B^{\\zeta} \\sqrt g}", units="1 / Wb", units_long="Inverse webers", - description="Length over volume along field line", - dim=2, + description="(Mean) length over volume along field line(s)", + dim=1, params=[], transforms={"grid": []}, profiles=[], - coordinates="ra", + coordinates="r", data=["B^zeta", "sqrt(g)"], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, ) -def _G_ra(data, transforms, profiles, **kwargs): +def _G_ra_fsa(data, transforms, profiles, **kwargs): g = transforms["grid"].source_grid shape = (g.num_rho, g.num_alpha, g.num_zeta) - data["G|r,a"] = quadax.simpson( + G_ra = simpson( jnp.reshape(1 / (data["B^zeta"] * data["sqrt(g)"]), shape), jnp.reshape(g.nodes[:, 2], shape), axis=-1, ) + data[""] = g.expand(_poloidal_mean(g, G_ra)) return data @@ -155,115 +165,95 @@ def _G_ra(data, transforms, profiles, **kwargs): "|B|_z|r,a", "|grad(psi)|", "kappa_g", - "L|r,a", + "", ], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - bounce_integral=( - "callable : Method to compute bounce integrals. " - "(You may want to wrap desc.compute.bounce_integral.bounce_integral " - "to change optional parameters such as quadrature resolution, etc.)." - ), - batch="bool : Whether to perform computation in a batched manner.", - # Composite quadrature should perform better than higher order methods. - quad=( - "callable : Quadrature method over velocity coordinate. " - "Default is composite Simpson's rule. " - "Accepts any callable with signature matching quad(f(λ), λ, ..., axis). " - "Accepts adaptive quadrature methods from quadax wrapped with vec_quadax. " - "If using an adaptive method, it is highly recommended to set batch=True." + num_quad=( + "int : Resolution for quadrature of bounce integrals. Default is 31, " + "which gets sufficient convergence, so higher values are likely unnecessary." ), num_pitch=( - "int : Resolution for quadrature over velocity coordinate. " - "Default is 75. This setting is ignored for adaptive quadrature." + "int : Resolution for quadrature over velocity coordinate, preferably odd. " + "Default is 125. Effective ripple will look smoother at high values. " + "(If computed on many flux surfaces and micro oscillation is seen " + "between neighboring surfaces, increasing num_pitch will smooth the profile)." ), + # Some notes on choosing the resolution hyperparameters: + # The default settings above were chosen such that the effective ripple profile on + # the W7-X stellarator looks similar to the profile computed at higher resolution, + # indicating convergence. The final resolution parameter to keep in mind is that + # the supplied grid should sufficiently cover the flux surfaces. At/above the + # num_quad and num_pitch parameters chosen above, the grid coverage should be the + # parameter that has the strongest effect on the profile. + # As a reference for W7-X, when computing the effective ripple by tracing a single + # field line on each flux surface, a density of 100 knots per toroidal transit + # accurately reconstructs the ripples along the field line. Truncating the field + # line to [0, 20π] offers good convergence (after [0, 30π] the returns diminish). + # Note that when further truncating the field line to [0, 10π], a dip/cusp appears + # between the rho=0.7 and rho=0.8 surfaces, indicating that more coverage is + # required to resolve the effective ripple in this region. + # TODO: Improve performance... related to GitHub issue #1045. + # The difficulty is computing the magnetic field is expensive: + # the ripples along field lines are fine compared to the length of the field line + # required for sufficient coverage of the surface. This requires many knots to + # for the spline of the magnetic field to capture fine ripples in a large interval. ) -# temporary -@partial(jit, static_argnames=["bounce_integral", "batch", "quad", "num_pitch"]) +@partial(jit, static_argnames=["num_quad", "num_pitch"]) def _effective_ripple_raw(params, transforms, profiles, data, **kwargs): - bounce = kwargs.get("bounce_integral", bounce_integral) - batch = kwargs.get("batch", False) - quad = kwargs.get("quad", quadax.simpson) - num_pitch = kwargs.get("num_pitch", 75) - g = transforms["grid"].source_grid - knots = g.compress(g.nodes[:, 2], surface_label="zeta") - pitch = _get_pitch(g, data, quad, num_pitch) + bounce_integrate, _ = bounce_integral( + data["B^zeta"], + data["|B|"], + data["|B|_z|r,a"], + knots=g.compress(g.nodes[:, 2], surface_label="zeta"), + quad=leggauss(kwargs.get("num_quad", 31)), + ) - def dH(grad_psi_norm, kappa_g, B, pitch): - # Pulled out dimensionless factor of (λB₀)¹ᐧ⁵ from integrand of - # Nemov equation 30. Multiplied back in at end. + def dH(grad_psi_norm_kappa_g, B, pitch): + # Removed dimensionless (λB₀)¹ᐧ⁵ from integrand of Nemov equation 30. + # Reintroduced later. return ( - jnp.sqrt(1 - pitch * B) - * (4 / (pitch * B) - 1) - * grad_psi_norm - * kappa_g - / B + jnp.sqrt(1 - pitch * B) * (4 / (pitch * B) - 1) * grad_psi_norm_kappa_g / B ) - def dI(B, pitch): - # Integrand of Nemov equation 31. + def dI(B, pitch): # Integrand of Nemov equation 31. return jnp.sqrt(1 - pitch * B) / B - if not _is_adaptive(quad): - bounce_integrate, _ = bounce( - data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots - ) - - def d_ripple(pitch): - """Return λ⁻²B₀⁻¹ ∑ⱼ Hⱼ²/Iⱼ evaluated at λ = pitch. - - Parameters - ---------- - pitch : Array, shape(*pitch.shape[:-1], g.num_rho * g.num_alpha) - Pitch angle. - - Returns - ------- - d_ripple : Array, shape(pitch.shape) - λ⁻²B₀⁻¹ ∑ⱼ Hⱼ²/Iⱼ - - """ - H = bounce_integrate( - dH, [data["|grad(psi)|"], data["kappa_g"]], pitch, batch=batch - ) - I = bounce_integrate(dI, [], pitch, batch=batch) - # (λB₀)³ db = (λB₀)³ B₀⁻¹λ⁻² (-dλ) = B₀²λ (-dλ) - # We chose B₀ = 1 (inverse units of λ). - # TODO: Think Neo chooses B₀ = "max_tz |B|". - # The minus sign is accounted for with the integration order. - return pitch * jnp.nansum(H**2 / I, axis=-1) - - # This has units of tesla meters. - ripple = quad(d_ripple(pitch), pitch, axis=0) - else: - # Use adaptive quadrature. - - def d_ripple(pitch, B_sup_z, B, B_z_ra, grad_psi_norm, kappa_g): - bounce_integrate, _ = bounce(B_sup_z, B, B_z_ra, knots) - H = bounce_integrate(dH, [grad_psi_norm, kappa_g], pitch, batch=batch) - I = bounce_integrate(dI, [], pitch, batch=batch) - return jnp.squeeze(pitch * jnp.nansum(H**2 / I, axis=-1)) - - args = [ - f.reshape(g.num_rho, g.num_alpha, g.num_zeta) - for f in [ - data["B^zeta"], - data["|B|"], - data["|B|_z|r,a"], - data["|grad(psi)|"], - data["kappa_g"], - ] - ] - ripple = quad(d_ripple, pitch, *args) - - ripple = _poloidal_mean(g, ripple.reshape(g.num_rho, g.num_alpha) / data["L|r,a"]) - data["effective ripple raw"] = g.expand(ripple) + def d_ripple(pitch): + """Return λ⁻²B₀⁻³ ∑ⱼ Hⱼ²/Iⱼ evaluated at λ = pitch. + + Parameters + ---------- + pitch : Array, shape(*pitch.shape[:-1], g.num_rho * g.num_alpha) + Pitch angle. + + Returns + ------- + d_ripple : Array + Returned array has shape atleast_2d(pitch.shape). + + """ + # Interpolate |∇ψ| κ_g together since it is smoother than κ_g alone. + H = bounce_integrate(dH, data["|grad(psi)|"] * data["kappa_g"], pitch) + I = bounce_integrate(dI, [], pitch) + return pitch * jnp.nansum(H**2 / I, axis=-1) + # (λB₀)³ db = (λB₀)³ λ⁻²B₀⁻¹ (-dλ) = λB₀² (-dλ) where B₀ has units of λ⁻¹. + + pitch = _get_pitch(g, data, kwargs.get("num_pitch", 125)) + # The integrand is continuous and likely poorly approximated by a polynomial, + # so composite quadrature should perform better than higher order methods. + ripple = simpson(jnp.squeeze(imap(d_ripple, pitch), axis=1), pitch, axis=0) + data["effective ripple raw"] = ( + g.expand(_poloidal_mean(g, ripple.reshape(g.num_rho, g.num_alpha))) + * data["max_tz |B|"] ** 2 + / data[""] + ) return data @register_compute_fun( name="effective ripple", # this is ε¹ᐧ⁵ - label="π/(8√2) (R₀(∂V/∂ψ)/S)² ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ Hⱼ²/Iⱼ \\rangle", + label="ε¹ᐧ⁵ = π/(8√2) (R₀(∂V/∂ψ)/S)² ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ Hⱼ²/Iⱼ \\rangle", units="~", units_long="None", description="Effective ripple modulation amplitude", @@ -275,8 +265,9 @@ def d_ripple(pitch, B_sup_z, B, B_z_ra, grad_psi_norm, kappa_g): data=["R0", "V_r(r)", "psi_r", "S(r)", "effective ripple raw"], ) def _effective_ripple(params, transforms, profiles, data, **kwargs): - """Evaluation of 1/ν neoclassical transport in stellarators. + """ε¹ᐧ⁵ = π/(8√2) (R₀(∂V/∂ψ)/S)² ∫dλ λ⁻²B₀⁻¹ 〈 ∑ⱼ Hⱼ²/Iⱼ 〉. + Evaluation of 1/ν neoclassical transport in stellarators. V. V. Nemov, S. V. Kasilov, W. Kernbichler, M. F. Heyn. Phys. Plasmas 1 December 1999; 6 (12): 4622–4632. https://doi.org/10.1063/1.873749. @@ -293,7 +284,7 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="Gamma_c", - label="Γ_c = π/(2√2) ∫dλ λ⁻²B₀⁻¹ \\langle ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ \\rangle", + label="Γ_c = π/(8√2) ∫dλ \\langle ∑ⱼ [v τ γ_c²]ⱼ \\rangle", units="~", units_long="None", description="Energetic ion confinement proxy", @@ -310,39 +301,27 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): "|B|_z|r,a", "cvdrift0", "gbdrift", - "L|r,a", + "", ], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - bounce_integral=( - "callable : Method to compute bounce integrals. " - "(You may want to wrap desc.compute.bounce_integral.bounce_integral " - "to change optional parameters such as quadrature resolution, etc.)." - ), - batch="bool : Whether to perform computation in a batched manner.", - # Composite quadrature should perform better than higher order methods. - # TODO: Two layers of pitch for quadrature, linearly spaced minB to maxB - # and then second layer for extrema in fixed [0, zeta*]. - quad=( - "callable : Quadrature method over velocity coordinate. " - "Default is composite Simpson's rule. " - "Accepts any callable with signature matching quad(f(λ), λ, ..., axis). " - "Accepts adaptive quadrature methods from quadax wrapped with vec_quadax. " - "If using an adaptive method, it is highly recommended to set batch=True." - ), + num_quad="int : Resolution for quadrature of bounce integrals. Default is 31.", num_pitch=( - "int : Resolution for quadrature over velocity coordinate. " - "Default is 75. This setting is ignored for adaptive quadrature." + "int : Resolution for quadrature over velocity coordinate. Default is 125." + ), + adaptive=( + "bool : Whether to adaptively integrate over the velocity coordinate. " + "If true, then num_pitch specifies an upper bound on the maximum number " + "of function evaluations." ), ) -# temporary -@partial(jit, static_argnames=["bounce_integral", "batch", "quad", "num_pitch"]) +@partial(jit, static_argnames=["num_quad", "num_pitch", "adaptive"]) def _Gamma_c(params, transforms, profiles, data, **kwargs): - """Energetic ion confinement proxy. + """Γ_c = π/(8√2) ∫dλ 〈 ∑ⱼ [v τ γ_c²]ⱼ 〉. A model for the fast evaluation of prompt losses of energetic ions in stellarators. - J.L. Velasco et al 2021 Nucl. Fusion 61 116059. + J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. https://doi.org/10.1088/1741-4326/ac2994. - Equation 16. (Not equation 18). + Equation 16. Poloidal motion of trapped particle orbits in real-space coordinates. V. V. Nemov, S. V. Kasilov, W. Kernbichler, G. O. Leitold. @@ -350,45 +329,27 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): https://doi.org/10.1063/1.2912456. Equation 61, using Velasco's γ_c from equation 15 of the above paper. - Besides the difference in γ_c mentioned above, Nemov's Γ_c and Velasco Γ_c - as defined in equation 16 are identical, although Nemov's expression is - precise while Velasco's requires a flexible interpretation for the expression - to be well-defined. - - Also, Velasco Γ_c as defined in equation 18 does not seem to match - equation 16. Note that - dλ v τ_b = - 4 (∂I/∂b) db = 4 ∂I/∂((λB₀)⁻¹) B₀⁻¹λ⁻² dλ - 4π² ∂Ψₜ/∂V = lim{L → ∞} ( [∫₀ᴸ ds/(B √g)] / [∫₀ᴸ ds/B] ) - where the integrals are along an irrational field line with - ds / B given by dζ / B^ζ - √g the (Ψ, α, ζ)-coordinate Jacobian - If the (missing?) √g factor in Velasco Γ_c equation 18 is pushed into the - integral over alpha in Velasco equation 18 then we have that - eq. 18 Velasco Γ_c ∼ eq. 16 Velasco Γ_c * lim{L → ∞} ∫₀ᴸ ds/(B √g). - """ - bounce = kwargs.get("bounce_integral", bounce_integral) - batch = kwargs.get("batch", False) - quad = kwargs.get("quad", quadax.simpson) - num_pitch = kwargs.get("num_pitch", 75) - g = transforms["grid"].source_grid knots = g.compress(g.nodes[:, 2], surface_label="zeta") - pitch = _get_pitch(g, data, quad, num_pitch) + quad = leggauss(kwargs.get("num_quad", 31)) + num_pitch = kwargs.get("num_pitch", 125) + adaptive = kwargs.get("adaptive", False) + pitch = _get_pitch(g, data, num_pitch, adaptive) + + def d_v_tau(B, pitch): + return 2 / jnp.sqrt(1 - pitch * B) def d_gamma_c(f, B, pitch): return f * (1 - pitch * B / 2) / jnp.sqrt(1 - pitch * B) - def dK(B, pitch): - return 1 / jnp.sqrt(1 - pitch * B) - - if not _is_adaptive(quad): - bounce_integrate, _ = bounce( - data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots + if not adaptive: + bounce_integrate, _ = bounce_integral( + data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots, quad ) def d_Gamma_c(pitch): - """Return 2λ⁻²B₀⁻¹ ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ evaluated at λ = pitch. + """Return ∑ⱼ [v τ γ_c²]ⱼ evaluated at λ = pitch. Parameters ---------- @@ -397,41 +358,41 @@ def d_Gamma_c(pitch): Returns ------- - d_Gamma_c : Array, shape(pitch.shape) - 2λ⁻²B₀⁻¹ ∑ⱼ [γ_c² ∂I/∂((λB₀)⁻¹)]ⱼ + d_Gamma_c : Array + Returned array has shape atleast_2d(pitch.shape). """ + # v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, + # τ is the bounce time, and I is defined in Nemov eq. 36. + v_tau = bounce_integrate(d_v_tau, [], pitch) gamma_c = ( 2 / jnp.pi * jnp.arctan( - bounce_integrate(d_gamma_c, data["cvdrift0"], pitch, batch=batch) - / bounce_integrate(d_gamma_c, data["gbdrift"], pitch, batch=batch) + bounce_integrate(d_gamma_c, data["cvdrift0"], pitch) + / bounce_integrate(d_gamma_c, data["gbdrift"], pitch) ) ) - # K = 2λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where I is given in Nemov equation 36. - # The factor of B₀ cancels, making this quantity independent of the - # chosen reference magnetic field strength. - K = bounce_integrate(dK, [], pitch, batch=batch) - return jnp.nansum(gamma_c**2 * K, axis=-1) - - # This has units of meters / tesla. - Gamma_c = quad(d_Gamma_c(pitch), pitch, axis=0) + return jnp.nansum(v_tau * gamma_c**2, axis=-1) + + # The integrand is piecewise continuous and likely poorly approximated by a + # polynomial, so composite quadrature should perform better than higher order + # methods. + Gamma_c = trapezoid(jnp.squeeze(imap(d_Gamma_c, pitch), axis=1), pitch, axis=0) else: - # Use adaptive quadrature. def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): - bounce_integrate, _ = bounce(B_sup_z, B, B_z_ra, knots) + bounce_integrate, _ = bounce_integral(B_sup_z, B, B_z_ra, knots, quad) + v_tau = bounce_integrate(d_v_tau, [], pitch) gamma_c = ( 2 / jnp.pi * jnp.arctan( - bounce_integrate(d_gamma_c, cvdrift0, pitch, batch=batch) - / bounce_integrate(d_gamma_c, gbdrift, pitch, batch=batch) + bounce_integrate(d_gamma_c, cvdrift0, pitch) + / bounce_integrate(d_gamma_c, gbdrift, pitch) ) ) - K = bounce_integrate(dK, [], pitch, batch=batch) - return jnp.squeeze(jnp.nansum(gamma_c**2 * K, axis=-1)) + return jnp.squeeze(jnp.nansum(v_tau * gamma_c**2, axis=-1)) args = [ f.reshape(g.num_rho, g.num_alpha, g.num_zeta) @@ -443,12 +404,14 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): data["gbdrift"], ] ] - Gamma_c = quad(d_Gamma_c, pitch, *args) + Gamma_c = _vec_quadax(romberg, divmax=jnp.log2(num_pitch + 1))( + d_Gamma_c, pitch, *args + ) - Gamma_c = ( + data["Gamma_c"] = ( jnp.pi - / (4 * 2**0.5) - * _poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha) / data["L|r,a"]) + / (8 * 2**0.5) + * g.expand(_poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha))) + / data[""] ) - data["Gamma_c"] = g.expand(Gamma_c) return data diff --git a/desc/compute/bounce_integral.py b/desc/compute/bounce_integral.py index 9eaa61de4c..e2f272ccac 100644 --- a/desc/compute/bounce_integral.py +++ b/desc/compute/bounce_integral.py @@ -6,7 +6,7 @@ from matplotlib import pyplot as plt from orthax.legendre import leggauss -from desc.backend import complex_sqrt, flatnonzero, imap, jnp, put_along_axis, take +from desc.backend import flatnonzero, imap, jnp, put_along_axis, take from desc.compute.utils import safediv from desc.utils import errorif @@ -90,56 +90,75 @@ def _filter_real(a, a_min=-jnp.inf, a_max=jnp.inf): ) +def _nan_concat(r, num=1): + # Concat nan num times to r on last axis. + nan = jnp.broadcast_to(jnp.nan, (*r.shape[:-1], num)) + return jnp.concatenate([r, nan], axis=-1) + + def _root_linear(a, b, distinct=False): """Return r such that a r + b = 0.""" return safediv(-b, a, fill=jnp.where(jnp.isclose(b, 0), 0, jnp.nan)) def _root_quadratic(a, b, c, distinct=False): - """Return r such that a r² + b r + c = 0.""" + """Return r such that a r² + b r + c = 0, assuming real coefficients.""" # numerical.recipes/book.html, page 227 discriminant = b**2 - 4 * a * c - C = complex_sqrt(discriminant) - sgn = jnp.sign(jnp.real(jnp.conj(b) * C)) - q = -0.5 * (b + sgn * C) - is_linear = jnp.isclose(a, 0) - suppress_root = distinct & jnp.isclose(discriminant, 0) - r1 = jnp.where(is_linear, _root_linear(b, c), safediv(q, a)) - r2 = jnp.where(is_linear | suppress_root, jnp.nan, safediv(c, q)) - return r1, r2 + q = -0.5 * (b + jnp.sign(b) * jnp.sqrt(discriminant)) + r1 = safediv(q, a, _root_linear(b, c, distinct)) + # more robust to remove repeated roots with discriminant + r2 = jnp.where( + distinct & jnp.isclose(discriminant, 0), jnp.nan, safediv(c, q, jnp.nan) + ) + return jnp.stack([r1, r2], axis=-1) def _root_cubic(a, b, c, d, distinct=False): - """Return r such that a r³ + b r² + c r + d = 0.""" - # https://en.wikipedia.org/wiki/Cubic_equation#General_cubic_formula - t_0 = b**2 - 3 * a * c - t_1 = 2 * b**3 - 9 * a * b * c + 27 * a**2 * d - discriminant = t_1**2 - 4 * t_0**3 - C = ((t_1 + complex_sqrt(discriminant)) / 2) ** (1 / 3) - C_is_zero = jnp.isclose(C, 0) - - def root(xi): - return safediv(b + xi * C + jnp.where(C_is_zero, 0, t_0 / (xi * C)), -3 * a) - - xi0 = 1 - xi1 = (-1 + (-3) ** 0.5) / 2 - xi2 = xi1**2 - is_quadratic = jnp.isclose(a, 0) - # C = 0 is equivalent to existence of triple root. - # Assuming the coefficients are real, it is also equivalent to - # existence of any real roots with multiplicity > 1. - suppress_root = distinct & C_is_zero - q1, q2 = _root_quadratic(b, c, d, distinct) - r1 = jnp.where(is_quadratic, q1, root(xi0)) - r2 = jnp.where(is_quadratic, q2, jnp.where(suppress_root, jnp.nan, root(xi1))) - r3 = jnp.where(is_quadratic | suppress_root, jnp.nan, root(xi2)) - return r1, r2, r3 + """Return r such that a r³ + b r² + c r + d = 0, assuming real coefficients.""" + # numerical.recipes/book.html, page 228 + + def irreducible(Q, R, b): + # Three irrational real roots. + theta = jnp.arccos(R / jnp.sqrt(Q**3)) + j = -2 * jnp.sqrt(Q) + r1 = j * jnp.cos(theta / 3) - b / 3 + r2 = j * jnp.cos((theta + 2 * jnp.pi) / 3) - b / 3 + r3 = j * jnp.cos((theta - 2 * jnp.pi) / 3) - b / 3 + return jnp.stack([r1, r2, r3], axis=-1) + + def reducible(Q, R, b): + # One real and two complex roots. + A = -jnp.sign(R) * (jnp.abs(R) + jnp.sqrt(R**2 - Q**3)) ** (1 / 3) + B = safediv(Q, A) + r1 = (A + B) - b / 3 + return _nan_concat(r1[..., jnp.newaxis], 2) + + def root(b, c, d): + b = safediv(b, a) + c = safediv(c, a) + d = safediv(d, a) + Q = (b**2 - 3 * c) / 9 + R = (2 * b**3 - 9 * b * c + 27 * d) / 54 + return jnp.where( + jnp.expand_dims(R**2 < Q**3, axis=-1), + irreducible(Q, R, b), + reducible(Q, R, b), + ) + + return jnp.where( + jnp.isclose(a, 0)[..., jnp.newaxis], + _nan_concat(_root_quadratic(b, c, d, distinct)), + root(b, c, d), + ) _roots = jnp.vectorize(partial(jnp.roots, strip_zeros=False), signature="(m)->(n)") -def _poly_root(c, k=0, a_min=None, a_max=None, sort=False, distinct=False): +def _poly_root( + c, k=0, a_min=None, a_max=None, sort=False, distinct=False, real_coef=True +): """Roots of polynomial with given coefficients. Parameters @@ -161,6 +180,8 @@ def _poly_root(c, k=0, a_min=None, a_max=None, sort=False, distinct=False): distinct : bool Whether to only return the distinct roots. If true, when the multiplicity is greater than one, the repeated roots are set to nan. + real_coef : bool + Whether the coefficients ``c`` and ``k`` are real. Returns ------- @@ -168,31 +189,26 @@ def _poly_root(c, k=0, a_min=None, a_max=None, sort=False, distinct=False): The roots of the polynomial, iterated over the last axis. """ - keep_only_real = not (a_min is None and a_max is None) + just_real = not (a_min is None and a_max is None) func = {2: _root_linear, 3: _root_quadratic, 4: _root_cubic} - if c.shape[0] in func: - # Compute from analytic formula. + if c.shape[0] in func and real_coef and just_real: + # Compute from analytic formula to avoid the issue of complex roots + # with small imaginary parts. r = func[c.shape[0]](*c[:-1], c[-1] - k, distinct) - if keep_only_real: - r = [_filter_real(rr, a_min, a_max) for rr in r] - r = jnp.stack(r, axis=-1) - # We had ignored the case of double complex roots. - distinct = distinct and c.shape[0] > 3 and not keep_only_real + distinct = distinct and c.shape[0] > 3 else: # Compute from eigenvalues of polynomial companion matrix. - # This method can fail to detect roots near extrema, which is often - # where we want to detect roots for bounce integrals. c_n = c[-1] - k c = [jnp.broadcast_to(c_i, c_n.shape) for c_i in c[:-1]] c.append(c_n) c = jnp.stack(c, axis=-1) r = _roots(c) - if keep_only_real: - if a_min is not None: - a_min = a_min[..., jnp.newaxis] - if a_max is not None: - a_max = a_max[..., jnp.newaxis] - r = _filter_real(r, a_min, a_max) + if just_real: + if a_min is not None: + a_min = a_min[..., jnp.newaxis] + if a_max is not None: + a_max = a_max[..., jnp.newaxis] + r = _filter_real(r, a_min, a_max) if sort or distinct: r = jnp.sort(r, axis=-1) @@ -200,7 +216,7 @@ def _poly_root(c, k=0, a_min=None, a_max=None, sort=False, distinct=False): # Atol needs to be low enough that distinct roots which are close do not # get removed, otherwise algorithms that rely on continuity of the spline # such as bounce_points() will fail. The current atol was chosen so that - # test_bounce_points() passes when this block is forced to run. + # test_bounce_points() passes. mask = jnp.isclose(jnp.diff(r, axis=-1, prepend=jnp.nan), 0, atol=1e-15) r = jnp.where(mask, jnp.nan, r) return r @@ -319,9 +335,6 @@ def get_pitch(min_B, max_B, num, relative_shift=1e-6): # extrema. Shift values slightly to resolve this issue. min_B = (1 + relative_shift) * min_B max_B = (1 - relative_shift) * max_B - # λ is the pitch angle. Note Nemov dimensionless integration variable b = (λB₀)⁻¹. - # Uniformly space in pitch (as opposed to 1/pitch) to get faster convergence in - # an integration over pitch. pitch = composite_linspace(1 / jnp.stack([max_B, min_B]), num) assert pitch.shape == (num + 2, *pitch.shape[1:]) return pitch @@ -419,9 +432,7 @@ def get_extrema(knots, B_c, B_z_ra_c, relative_shift=1e-6): """ B_c, B_z_ra_c, _ = _check_shape(knots, B_c, B_z_ra_c) S, N, degree = B_c.shape[1], knots.size - 1, B_c.shape[0] - 1 - extrema = _poly_root( - c=B_z_ra_c, a_min=jnp.array([0]), a_max=jnp.diff(knots), distinct=True - ) + extrema = _poly_root(c=B_z_ra_c, a_min=jnp.array([0]), a_max=jnp.diff(knots)) assert extrema.shape == (S, N, degree - 1) B_extrema = _poly_val(x=extrema, c=B_c[..., jnp.newaxis]) B_zz_ra_extrema = _poly_val(x=extrema, c=_poly_der(B_z_ra_c)[..., jnp.newaxis]) @@ -442,7 +453,7 @@ def get_extrema(knots, B_c, B_z_ra_c, relative_shift=1e-6): return B_extrema -def bounce_points(pitch, knots, B_c, B_z_ra_c, check=False, plot=False): +def bounce_points(pitch, knots, B_c, B_z_ra_c, check=False, plot=False, **kwargs): """Compute the bounce points given spline of |B| and pitch λ. Parameters @@ -534,11 +545,11 @@ def bounce_points(pitch, knots, B_c, B_z_ra_c, check=False, plot=False): # we ignore the bounce points of particles assigned to a class that are # trapped outside this snapshot of the field line. if check: - _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot) + _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot, **kwargs) return bp1, bp2 -def _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot=False): +def _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot=False, **kwargs): """Check that bounce points are computed correctly. Parameters @@ -573,8 +584,8 @@ def _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot=False): _filter_not_nan, (bp1[p, s], bp2[p, s], B_mid) ) if plot: - plot_field_line_with_ripple( - B, pitch[p, s], bp1_p, bp2_p, id=f"{p},{s}" + plot_field_line( + B, pitch[p, s], bp1_p, bp2_p, id=f"{p},{s}", **kwargs ) print("bp1:", bp1_p) print("bp2:", bp2_p) @@ -587,19 +598,22 @@ def _check_bounce_points(bp1, bp2, pitch, knots, B_c, plot=False): ) assert not err_3, msg_3 if plot: - plot_field_line_with_ripple(B, pitch[:, s], bp1[:, s], bp2[:, s], id=str(s)) + plot_field_line(B, pitch[:, s], bp1[:, s], bp2[:, s], id=str(s), **kwargs) -def plot_field_line_with_ripple( +def plot_field_line( B, pitch=None, bp1=jnp.array([]), bp2=jnp.array([]), start=None, stop=None, - num=500, + num=1000, title=r"Computed bounce points for $\vert B \vert$ and pitch $\lambda$", id=None, + include_knots=True, + alpha_knot=0.1, + alpha_pitch=0.25, show=True, ): """Plot the field line given spline of |B| and bounce points etc. @@ -619,11 +633,17 @@ def plot_field_line_with_ripple( stop : float Maximum ζ of plot. num : int - Number of ζ points to plot. + Number of ζ points to plot. Pick a big number. title : str Plot title. id : str Identifier string to append to plot title. + include_knots : bool + Whether to plot vertical lines at the knots. + alpha_knot : float + Transparency of knot lines. + alpha_pitch : float + Transparency of pitch lines. show : bool Whether to show the plot. @@ -643,8 +663,9 @@ def add(lines): legend[label] = line fig, ax = plt.subplots() - for knot in B.x: - add(ax.axvline(x=knot, color="tab:blue", alpha=0.25, label="knot")) + if include_knots: + for knot in B.x: + add(ax.axvline(x=knot, color="tab:blue", alpha=alpha_knot, label="knot")) z = jnp.linspace( start=B.x[0] if start is None else start, stop=B.x[-1] if stop is None else stop, @@ -655,7 +676,11 @@ def add(lines): if pitch is not None: b = 1 / jnp.atleast_1d(pitch) for val in b: - add(ax.axhline(val, color="tab:purple", alpha=0.25, label=r"$1 / \lambda$")) + add( + ax.axhline( + val, color="tab:purple", alpha=alpha_pitch, label=r"$1 / \lambda$" + ) + ) bp1, bp2 = jnp.atleast_2d(bp1, bp2) for i in range(bp1.shape[0]): bp1_i, bp2_i = map(_filter_not_nan, (bp1[i], bp2[i])) @@ -680,12 +705,12 @@ def add(lines): ax.set_xlabel(r"Field line $\zeta$") ax.set_ylabel(r"$\vert B \vert \sim 1 / \lambda$") - ax.legend(legend.values(), legend.keys()) + ax.legend(legend.values(), legend.keys(), loc="lower right") if id is not None: title = f"{title}. id = {id}." ax.set_title(title) + plt.tight_layout() if show: - plt.tight_layout() plt.show() plt.close() return fig, ax @@ -706,16 +731,11 @@ def grad_affine_bijection(a, b): def automorphism_arcsin(x): """[-1, 1] ∋ x ↦ y ∈ [−1, 1]. - The gradient of the arcsin automorphism introduces a singularity that augments - the singularity in the bounce integral. Therefore, the quadrature scheme + The arcsin transformation introduces a singularity that augments + the singularity in the bounce integral, so the quadrature scheme used to evaluate the integral must work well on functions with large derivative near the boundary. - The arcsin automorphism pulls points in [−1, 1] away from the boundary. - This can reduce floating point error if paired with a quadrature - scheme that is aggressive with placing nodes near endpoints, such as - Tanh-Sinh quadrature. - Parameters ---------- x : Array @@ -743,22 +763,18 @@ def grad_automorphism_arcsin(x): def automorphism_sin(x, s=0, m=10): """[-1, 1] ∋ x ↦ y ∈ [−1, 1]. - The gradient of the sin automorphism is Lipschitz. - When this automorphism is used as the change of variable map for the bounce - integral, the Lipschitzness prevents generation of new singularities. + The sin transformation is Lipschitz. + When used as the change of variable map for the bounce integral, the + Lipschitzness prevents generation of new singularities. Furthermore, its derivative vanishes to zero slowly near the boundary, which will suppress the large derivatives near the boundary of singular integrals. - Therefore, this automorphism pulls the mass of the bounce integral away + In effect, this automorphism pulls the mass of the bounce integral away from the singularities, which should improve convergence of the quadrature to the true integral, so long as the quadrature performs better on less singular integrands. Pairs well with Gauss-Legendre quadrature. - The sin automorphism pushes points in [−1, 1] toward the boundary. - This can increase floating point error if paired with a quadrature - scheme that is aggressive with placing nodes near endpoints. - Parameters ---------- x : Array @@ -777,16 +793,11 @@ def automorphism_sin(x, s=0, m=10): errorif(not (0 <= s <= 1)) # s = 0 -> derivative vanishes like cosine. # s = 1 -> derivative vanishes like cosine^k. - # Integrate cosine, cosine^k, and normalize codomain to [-1, 1] to get - # two automorphisms. Connect with homotopy, jointly continuous in s ∈ [0, 1]. - # Then derivative suppression is continuous in s for finite k. - # As k → ∞ and s → 1, all integrable singularities and oscillations - # are removed; the integrand becomes a delta function. - # Setting s = 0 is optimal to integrate singularities of the form 1 / (1 - |x|) - # Setting s = 1 is optimal to integrate singularities of the form 1 / (1 - |x|)^k. y0 = jnp.sin(jnp.pi * x / 2) y1 = x + jnp.sin(jnp.pi * x) / jnp.pi # k = 2 y = (1 - s) * y0 + s * y1 + # y is an expansion, so y(x) > x near x ∈ {−1, 1} and there is a tendency + # for floating point error to overshoot the true value. eps = m * jnp.finfo(jnp.array(1.0).dtype).eps return jnp.clip(y, -1 + eps, 1 - eps) @@ -878,21 +889,13 @@ def tanh_sinh(deg, m=10): _interp1d_vec = jnp.vectorize( - interp1d, - signature="(m),(n),(n)->(m)", - excluded={"method", "derivative", "extrap", "period"}, + interp1d, signature="(m),(n),(n)->(m)", excluded={"method"} ) -@partial( - jnp.vectorize, - signature="(m),(n),(n),(n)->(m)", - excluded={"method", "derivative", "extrap", "period"}, -) -def _interp1d_vec_with_df( - xq, x, f, fx, method="cubic", derivative=0, extrap=False, period=None -): - return interp1d(xq, x, f, method, derivative, extrap, period, fx=fx) +@partial(jnp.vectorize, signature="(m),(n),(n),(n)->(m)", excluded={"method"}) +def _interp1d_vec_with_df(xq, x, f, fx, method="cubic"): + return interp1d(xq, x, f, method, fx=fx) def _interpolatory_quadrature( @@ -953,7 +956,7 @@ def _interpolatory_quadrature( ) if check: Z = Z.reshape(shape) - _assert_finite_and_hairy(Z, f, b_sup_z, B, B_z_ra, inner_product) + _check_interpolation(Z, f, b_sup_z, B, B_z_ra, inner_product) if plot: _plot(Z, B, id=r"$\vert B \vert$") _plot(Z, b_sup_z, id=r"$ (B/\vert B \vert) \cdot e^{\zeta}$") @@ -968,7 +971,7 @@ def _interpolatory_quadrature( ) -def _assert_finite_and_hairy(Z, f, B_sup_z, B, B_z_ra, inner_product): +def _check_interpolation(Z, f, B_sup_z, B, B_z_ra, inner_product): """Check for floating point errors. Parameters @@ -1202,21 +1205,13 @@ def bounce_integral( the labels (ρ, α) are interpreted as the index into the first axis that corresponds to that field line. knots : Array, shape(knots.size, ) - Field line following coordinate values at which ``B_sup_z``, - ``B``, and ``B_z_ra`` were evaluated. - These knots are used to compute a spline of |B| and interpolate - the integrand. The number of knots specifies a grid resolution - as increasing the number of knots increases the accuracy of - representing the integrand and the accuracy of the locations of - the bounce points. The default spline method for |B| is a cubic - Hermite spline. This is preferred because the strength of the - singularity typical in bounce integral is ~ 1 / |∂|B|/∂_ζ|, so - the derivative information should be captured without compromise. - Can also specify to use a monotonic interpolation for |B| rather - than a cubic Hermite spline with keyword argument ``monotonic=True``. + Field line following coordinate values at which ``B_sup_z``, ``B``, and + ``B_z_ra`` were evaluated. These knots are used to compute a spline of |B| + and interpolate the integrand. A good reference density is 100 knots per + toroidal transit. quad : (Array, Array) Quadrature points xₖ and weights wₖ for the approximate evaluation - of an integral ∫₋₁¹ g(x) dx = ∑ₖ wₖ g(xₖ). + of an integral ∫₋₁¹ g(x) dx = ∑ₖ wₖ g(xₖ). Default is 21 points. automorphism : (callable, callable) or None The first callable should be an automorphism of the real interval [-1, 1]. The second callable should be the derivative of the first. @@ -1226,9 +1221,9 @@ def bounce_integral( augment or suppress singularities. Keep this in mind when choosing the quadrature method. B_ref : float - Reference magnetic field strength for normalization. + Optional. Reference magnetic field strength for normalization. L_ref : float - Reference length scale for normalization. + Optional. Reference length scale for normalization. check : bool Flag for debugging. plot : bool diff --git a/tests/baseline/test_Gamma_c.png b/tests/baseline/test_Gamma_c.png new file mode 100644 index 0000000000000000000000000000000000000000..ed105d7dbe087955ff8f17ffc7375477b6f06e43 GIT binary patch literal 14770 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 Kd)`&6NB;|@AE5{U literal 0 HcmV?d00001 diff --git a/tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0.15_L_9_M_9_N_24_output.h5 b/tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0.15_L_9_M_9_N_24_output.h5 deleted file mode 100644 index ab5f5d2499491fede2f5506560b3999586719a6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 131569 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RTYGn(jzy306Hw<-SGhad zQ>gQa&=?QNc2$v#kQHEuKQPdH?OS8Nyq|mK<9GeQoGWn?Wcz^`hR4Yvz~+O_L+5i( JtY7o-{{e@1NXq~K diff --git a/tests/inputs/neo_out.QI_plunk_fixed_surf_r0.15_N_24_hires_ns99 b/tests/inputs/neo_out.QI_plunk_fixed_surf_r0.15_N_24_hires_ns99 deleted file mode 100644 index 844967f0b0..0000000000 --- a/tests/inputs/neo_out.QI_plunk_fixed_surf_r0.15_N_24_hires_ns99 +++ /dev/null @@ -1,98 +0,0 @@ - 2 0.8461201253E-03 0.3394379165E+00 -0.7055289723E+00 0.1209357718E+01 0.1009257284E+01 - 3 0.8961274543E-03 0.7315225853E+00 -0.7055496401E+00 0.1219826918E+01 0.1009257284E+01 - 4 0.9554876720E-03 0.1035346285E+01 -0.7055688078E+00 0.1226453474E+01 0.1009257284E+01 - 5 0.1024174835E-02 0.1292226518E+01 -0.7055868077E+00 0.1231531120E+01 0.1009257284E+01 - 6 0.1101017837E-02 0.1518864120E+01 -0.7056039752E+00 0.1235735256E+01 0.1009257284E+01 - 7 0.1185587630E-02 0.1723952046E+01 -0.7056206482E+00 0.1239375055E+01 0.1009257284E+01 - 8 0.1275305900E-02 0.1912685181E+01 -0.7056371665E+00 0.1242620512E+01 0.1009257284E+01 - 9 0.1369774407E-02 0.2088460278E+01 -0.7056538712E+00 0.1245575339E+01 0.1009257284E+01 - 10 0.1468559496E-02 0.2253645988E+01 -0.7056711042E+00 0.1248307348E+01 0.1009257284E+01 - 11 0.1571633106E-02 0.2409967652E+01 -0.7056892078E+00 0.1250863188E+01 0.1009257284E+01 - 12 0.1677840282E-02 0.2558726480E+01 -0.7057085236E+00 0.1253276240E+01 0.1009257284E+01 - 13 NaN NaN -0.7057293924E+00 0.1255571170E+01 0.1009257284E+01 - 14 NaN NaN -0.7057521534E+00 0.1257766691E+01 0.1009257284E+01 - 15 0.2013999837E-02 NaN -0.7057771439E+00 0.1259877324E+01 0.1009257284E+01 - 16 0.2135957338E-02 NaN -0.7058046985E+00 0.1261914558E+01 0.1009257284E+01 - 17 0.2261802958E-02 NaN -0.7058351486E+00 0.1263887632E+01 0.1009257284E+01 - 18 0.2391858200E-02 NaN -0.7058688219E+00 0.1265804087E+01 0.1009257284E+01 - 19 0.2523349879E-02 NaN -0.7059060422E+00 0.1267670159E+01 0.1009257284E+01 - 20 0.2656855493E-02 NaN -0.7059471283E+00 0.1269491064E+01 0.1009257284E+01 - 21 0.2792438476E-02 NaN -0.7059923940E+00 0.1271271210E+01 0.1009257284E+01 - 22 0.2930873502E-02 NaN -0.7060421475E+00 0.1273014355E+01 0.1009257284E+01 - 23 0.3069323608E-02 NaN -0.7060966910E+00 0.1274723733E+01 0.1009257284E+01 - 24 0.3212345870E-02 NaN -0.7061563202E+00 0.1276402148E+01 0.1009257284E+01 - 25 0.3357728997E-02 NaN -0.7062213237E+00 0.1278052043E+01 0.1009257284E+01 - 26 0.3516518868E-02 NaN -0.7062919832E+00 0.1279675568E+01 0.1009257284E+01 - 27 0.3668656359E-02 NaN -0.7063685723E+00 0.1281274621E+01 0.1009257284E+01 - 28 NaN NaN -0.7064513569E+00 0.1282850887E+01 0.1009257284E+01 - 29 0.3954557983E-02 NaN -0.7065405943E+00 0.1284405871E+01 0.1009257284E+01 - 30 0.4122220876E-02 NaN -0.7066365332E+00 0.1285940920E+01 0.1009257284E+01 - 31 0.4279190709E-02 NaN -0.7067394133E+00 0.1287457251E+01 0.1009257284E+01 - 32 NaN NaN -0.7068494649E+00 0.1288955960E+01 0.1009257284E+01 - 33 NaN NaN -0.7069669089E+00 0.1290438042E+01 0.1009257284E+01 - 34 0.4772429162E-02 NaN -0.7070919560E+00 0.1291904403E+01 0.1009257284E+01 - 35 NaN NaN -0.7072248073E+00 0.1293355870E+01 0.1009257284E+01 - 36 0.5133695747E-02 NaN -0.7073656533E+00 0.1294793197E+01 0.1009257284E+01 - 37 0.5306271038E-02 NaN -0.7075146739E+00 0.1296217078E+01 0.1009257284E+01 - 38 0.5457658261E-02 NaN -0.7076720386E+00 0.1297628151E+01 0.1009257284E+01 - 39 0.5668510743E-02 NaN -0.7078379058E+00 0.1299027003E+01 0.1009257284E+01 - 40 0.5836593078E-02 NaN -0.7080124229E+00 0.1300414176E+01 0.1009257284E+01 - 41 0.6042715237E-02 NaN -0.7081957263E+00 0.1301790172E+01 0.1009257284E+01 - 42 0.6228079888E-02 NaN -0.7083879408E+00 0.1303155455E+01 0.1009257284E+01 - 43 NaN NaN -0.7085891799E+00 0.1304510458E+01 0.1009257284E+01 - 44 0.6618827481E-02 NaN -0.7087995456E+00 0.1305855581E+01 0.1009257284E+01 - 45 0.6869769900E-02 NaN -0.7090191283E+00 0.1307191196E+01 0.1009257284E+01 - 46 0.7098030699E-02 NaN -0.7092480066E+00 0.1308517654E+01 0.1009257284E+01 - 47 0.7319838422E-02 NaN -0.7094862475E+00 0.1309835280E+01 0.1009257284E+01 - 48 0.7522111213E-02 NaN -0.7097339059E+00 0.1311144378E+01 0.1009257284E+01 - 49 0.7827650962E-02 NaN -0.7099910251E+00 0.1312445235E+01 0.1009257284E+01 - 50 0.8095367291E-02 NaN -0.7102576364E+00 0.1313738119E+01 0.1009257284E+01 - 51 0.8363307233E-02 NaN -0.7105337590E+00 0.1315023283E+01 0.1009257284E+01 - 52 0.8618857858E-02 NaN -0.7108194006E+00 0.1316300966E+01 0.1009257284E+01 - 53 NaN NaN -0.7111145564E+00 0.1317571395E+01 0.1009257284E+01 - 54 0.9218283270E-02 NaN -0.7114192100E+00 0.1318834783E+01 0.1009257284E+01 - 55 0.9524032831E-02 NaN -0.7117333330E+00 0.1320091334E+01 0.1009257284E+01 - 56 0.9801628957E-02 NaN -0.7120568851E+00 0.1321341243E+01 0.1009257284E+01 - 57 NaN NaN -0.7123898138E+00 0.1322584696E+01 0.1009257284E+01 - 58 0.1044967150E-01 NaN -0.7127320551E+00 0.1323821873E+01 0.1009257284E+01 - 59 0.1083716998E-01 NaN -0.7130835328E+00 0.1325052943E+01 0.1009257284E+01 - 60 0.1120932760E-01 NaN -0.7134441591E+00 0.1326278074E+01 0.1009257284E+01 - 61 0.1160289076E-01 NaN -0.7138138342E+00 0.1327497426E+01 0.1009257284E+01 - 62 0.1195283865E-01 NaN -0.7141924467E+00 0.1328711156E+01 0.1009257284E+01 - 63 0.1236280391E-01 NaN -0.7145798734E+00 0.1329919417E+01 0.1009257284E+01 - 64 NaN NaN -0.7149759794E+00 0.1331122355E+01 0.1009257284E+01 - 65 0.1315702363E-01 NaN -0.7153806181E+00 0.1332320118E+01 0.1009257284E+01 - 66 0.1360917089E-01 NaN -0.7157936313E+00 0.1333512848E+01 0.1009257284E+01 - 67 NaN NaN -0.7162148493E+00 0.1334700686E+01 0.1009257284E+01 - 68 0.1445976355E-01 NaN -0.7166440907E+00 0.1335883769E+01 0.1009257284E+01 - 69 0.1494376674E-01 NaN -0.7170811627E+00 0.1337062232E+01 0.1009257284E+01 - 70 NaN NaN -0.7175258610E+00 0.1338236211E+01 0.1009257284E+01 - 71 0.1592437145E-01 NaN -0.7179779697E+00 0.1339405836E+01 0.1009257284E+01 - 72 0.1641839664E-01 NaN -0.7184372618E+00 0.1340571238E+01 0.1009257284E+01 - 73 0.1694237958E-01 NaN -0.7189034984E+00 0.1341732542E+01 0.1009257284E+01 - 74 NaN NaN -0.7193764295E+00 0.1342889874E+01 0.1009257284E+01 - 75 0.1803969194E-01 NaN -0.7198557935E+00 0.1344043354E+01 0.1009257284E+01 - 76 0.1861927765E-01 NaN -0.7203413175E+00 0.1345193102E+01 0.1009257284E+01 - 77 NaN NaN -0.7208327171E+00 0.1346339235E+01 0.1009257284E+01 - 78 0.1978791459E-01 NaN -0.7213296964E+00 0.1347481865E+01 0.1009257284E+01 - 79 0.2042669506E-01 NaN -0.7218319479E+00 0.1348621099E+01 0.1009257284E+01 - 80 0.2105454640E-01 NaN -0.7223391527E+00 0.1349757039E+01 0.1009257284E+01 - 81 0.2165758679E-01 NaN -0.7228509802E+00 0.1350889787E+01 0.1009257284E+01 - 82 0.2238886486E-01 NaN -0.7233670880E+00 0.1352019438E+01 0.1009257284E+01 - 83 0.2307992406E-01 NaN -0.7238871220E+00 0.1353146083E+01 0.1009257284E+01 - 84 NaN NaN -0.7244107164E+00 0.1354269806E+01 0.1009257284E+01 - 85 0.2452316260E-01 NaN -0.7249374930E+00 0.1355390688E+01 0.1009257284E+01 - 86 0.2522557595E-01 NaN -0.7254670617E+00 0.1356508801E+01 0.1009257284E+01 - 87 0.2606183827E-01 NaN -0.7259990201E+00 0.1357624216E+01 0.1009257284E+01 - 88 0.2687512129E-01 NaN -0.7265329533E+00 0.1358736994E+01 0.1009257284E+01 - 89 0.2772172477E-01 NaN -0.7270684338E+00 0.1359847198E+01 0.1009257284E+01 - 90 0.2856864588E-01 NaN -0.7276050212E+00 0.1360954883E+01 0.1009257284E+01 - 91 0.2959799567E-01 NaN -0.7281422620E+00 0.1362060101E+01 0.1009257284E+01 - 92 0.3045385139E-01 NaN -0.7286796896E+00 0.1363162899E+01 0.1009257284E+01 - 93 0.3143314477E-01 NaN -0.7292168234E+00 0.1364263323E+01 0.1009257284E+01 - 94 0.3233212683E-01 NaN -0.7297531695E+00 0.1365361416E+01 0.1009257284E+01 - 95 0.3351922398E-01 NaN -0.7302882194E+00 0.1366457221E+01 0.1009257284E+01 - 96 0.3441879233E-01 NaN -0.7308214502E+00 0.1367550780E+01 0.1009257284E+01 - 97 0.3576105370E-01 NaN -0.7313523243E+00 0.1368642137E+01 0.1009257284E+01 - 98 0.3701525621E-01 NaN -0.7318802888E+00 0.1369731340E+01 0.1009257284E+01 - 99 0.3822220655E-01 NaN -0.7324047753E+00 0.1370818441E+01 0.1009257284E+01 diff --git a/tests/test_axis_limits.py b/tests/test_axis_limits.py index 3757527d9a..bd367e3a1d 100644 --- a/tests/test_axis_limits.py +++ b/tests/test_axis_limits.py @@ -92,7 +92,6 @@ "K_vc", # only defined on surface "iota_num_rrr", "iota_den_rrr", - "ripple", # implemented but requires source grid } @@ -131,6 +130,14 @@ def _skip_this(eq, name): or (eq.anisotropy is None and "beta_a" in name) or (eq.pressure is not None and " Redl" in name) or (eq.current is None and "iota_num" in name) + # These quantities require a coordinate mapping to compute and special grids, so + # it's not economical to test their axis limits here. Instead, a grid that + # includes the axis should be used in existing unit tests for these quantities. + or bool( + data_index["desc.equilibrium.equilibrium.Equilibrium"][name][ + "source_grid_requirement" + ] + ) ) diff --git a/tests/test_bounce_integral.py b/tests/test_bounce_integral.py index 5f0f0bf73f..39107a7af5 100644 --- a/tests/test_bounce_integral.py +++ b/tests/test_bounce_integral.py @@ -29,7 +29,7 @@ grad_affine_bijection, grad_automorphism_arcsin, grad_automorphism_sin, - plot_field_line_with_ripple, + plot_field_line, take_mask, tanh_sinh, ) @@ -281,16 +281,16 @@ def test_bp1_before_extrema(): k, np.cos(k) + 2 * np.sin(-2 * k), -np.sin(k) - 4 * np.cos(-2 * k) ) B_z_ra = B.derivative() - pitch = 1 / B(B_z_ra.roots(extrapolate=False))[3] + pitch = 1 / B(B_z_ra.roots(extrapolate=False))[3] + 1e-13 bp1, bp2 = bounce_points(pitch, k, B.c, B_z_ra.c, check=True) bp1, bp2 = map(_filter_not_nan, (bp1, bp2)) assert bp1.size and bp2.size - # Our routine correctly detects intersection, while scipy, jnp.root fails. intersect = B.solve(1 / pitch, extrapolate=False) - np.testing.assert_allclose(bp1[1], 1.9827671337414938) - intersect = np.insert(intersect, np.searchsorted(intersect, bp1[1]), bp1[1]) - np.testing.assert_allclose(bp1, intersect[[1, 2]]) - np.testing.assert_allclose(bp2, intersect[[2, 3]]) + np.testing.assert_allclose(bp1[1], 1.982767, rtol=1e-6) + np.testing.assert_allclose(bp1, intersect[[1, 2]], rtol=1e-6) + # intersect array could not resolve double root as single at index 2,3 + np.testing.assert_allclose(intersect[2], intersect[3], rtol=1e-6) + np.testing.assert_allclose(bp2, intersect[[3, 4]], rtol=1e-6) def test_bp2_before_extrema(): start = -1.2 * np.pi @@ -320,19 +320,17 @@ def test_extrema_first_and_before_bp1(plot=False): -np.sin(k) - 4 * np.cos(-2 * k) + 1 / 20, ) B_z_ra = B.derivative() - pitch = 1 / B(B_z_ra.roots(extrapolate=False))[2] + pitch = 1 / B(B_z_ra.roots(extrapolate=False))[2] - 1e-13 bp1, bp2 = bounce_points(pitch, k[2:], B.c[:, 2:], B_z_ra.c[:, 2:], check=True) if plot: - plot_field_line_with_ripple(B, pitch, bp1, bp2, start=k[2]) + plot_field_line(B, pitch, bp1, bp2, start=k[2]) bp1, bp2 = map(_filter_not_nan, (bp1, bp2)) assert bp1.size and bp2.size - # Our routine correctly detects intersection, while scipy, jnp.root fails. intersect = B.solve(1 / pitch, extrapolate=False) - np.testing.assert_allclose(bp1[0], 0.8353192766102349) - intersect = np.insert(intersect, np.searchsorted(intersect, bp1[0]), bp1[0]) + np.testing.assert_allclose(bp1[0], 0.835319, rtol=1e-6) intersect = intersect[intersect >= k[2]] - np.testing.assert_allclose(bp1, intersect[[0, 1, 3]]) - np.testing.assert_allclose(bp2, intersect[[0, 2, 4]]) + np.testing.assert_allclose(bp1, intersect[[0, 2, 4]], rtol=1e-6) + np.testing.assert_allclose(bp2, intersect[[0, 3, 5]], rtol=1e-6) def test_extrema_first_and_before_bp2(): start = -1.2 * np.pi @@ -344,7 +342,7 @@ def test_extrema_first_and_before_bp2(): -np.sin(k) - 4 * np.cos(-2 * k) + 1 / 10, ) B_z_ra = B.derivative() - pitch = 1 / B(B_z_ra.roots(extrapolate=False))[1] + pitch = 1 / B(B_z_ra.roots(extrapolate=False))[1] + 1e-13 # If a regression fails this test, this note will save many hours of debugging. # If the filter in place to return only the distinct roots is too coarse, # in particular atol < 1e-15, then this test will error. In the resulting @@ -362,10 +360,11 @@ def test_extrema_first_and_before_bp2(): assert bp1.size and bp2.size # Our routine correctly detects intersection, while scipy, jnp.root fails. intersect = B.solve(1 / pitch, extrapolate=False) - np.testing.assert_allclose(bp1[0], -0.6719044147510538) - intersect = np.insert(intersect, np.searchsorted(intersect, bp1[0]), bp1[0]) - np.testing.assert_allclose(bp1, intersect[0::2]) - np.testing.assert_allclose(bp2, intersect[1::2]) + np.testing.assert_allclose(bp1[0], -0.671904, rtol=1e-6) + np.testing.assert_allclose(bp1, intersect[[0, 3, 5]], rtol=1e-5) + # intersect array could not resolve double root as single at index 0,1 + np.testing.assert_allclose(intersect[0], intersect[1], rtol=1e-5) + np.testing.assert_allclose(bp2, intersect[[2, 4, 6]], rtol=1e-5) test_bp1_first() test_bp2_first() diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 75f8328b5b..1b2238f611 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -3,229 +3,67 @@ import matplotlib.pyplot as plt import numpy as np import pytest +from tests.test_plotting import tol_1d -from desc.backend import jnp -from desc.equilibrium import Equilibrium +from desc import examples from desc.equilibrium.coords import rtz_grid -from desc.vmec import VMECIO @pytest.mark.unit def test_field_line_average(): """Test that field line average converges to surface average.""" - eq = Equilibrium.load( - "tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0" - ".15_L_9_M_9_N_24_output.h5" - ) - rho = np.linspace(0, 1, 5) - alpha = np.array([0]) - L = 10 * np.pi # Large enough to pass the test, apparently. - zeta = np.linspace(0, L, 20) + eq = examples.get("W7-X") grid = rtz_grid( - eq, rho, alpha, zeta, coordinates="raz", period=(np.inf, 2 * np.pi, np.inf) + eq, + np.array([0, 0.5]), + np.array([0]), + np.linspace(0, 40 * np.pi, 200), + coordinates="raz", + period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute(["L|r,a", "G|r,a", "V_r(r)"], grid=grid) + data = eq.compute(["", "", "V_r(r)"], grid=grid) np.testing.assert_allclose( - np.squeeze(data["L|r,a"] / data["G|r,a"]), - grid.compress(data["V_r(r)"]) / (4 * np.pi**2), - rtol=2e-2, + data[""] / data[""], data["V_r(r)"] / (4 * np.pi**2), rtol=1e-3 ) @pytest.mark.unit +@pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_1d) def test_effective_ripple(): - """Compare DESC effective ripple against NEO STELLOPT.""" - eq = Equilibrium.load( - "tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0" - ".15_L_9_M_9_N_24_output.h5" - ) - rho = np.linspace(0, 1, 40) - # TODO: Here's a potential issue, resolve with 2d spline. - knots = np.linspace(0, 100 * np.pi, 1000) + """Test effective ripple with W7-X.""" + eq = examples.get("W7-X") + rho = np.linspace(0, 1, 10) grid = rtz_grid( eq, rho, np.array([0]), - knots, + np.linspace(0, 20 * np.pi, 1000), coordinates="raz", period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute( - "effective ripple", - grid=grid, - batch=False, - ) + data = eq.compute("effective ripple", grid=grid) assert np.isfinite(data["effective ripple"]).all() - eps_eff = grid.compress(data["effective ripple"]) - - # Plot DESC effective ripple. - fig, ax = plt.subplots() - rho = grid.compress(grid.nodes[:, 0]) - ax.plot(rho, eps_eff, marker="o") - ax.set_xlabel(r"$\rho$") - ax.set_ylabel(r"$\epsilon_{\text{eff}}^{3/2}$") - ax.set_title("DESC effective ripple ε¹ᐧ⁵") - plt.tight_layout() - plt.show() - plt.close() - - # Plot NEO effective ripple. - # This should be ε¹ᐧ⁵, but need to check if it's just ε. - neo_eps = np.array( - read_neo_out("tests/inputs/neo_out.QI_plunk_fixed_surf_r0.15_N_24_hires_ns99") - ) - assert neo_eps.ndim == 1 - neo_rho = np.sqrt(np.linspace(1 / (neo_eps.size + 1), 1, neo_eps.size)) - fig, ax = plt.subplots() - ax.plot(neo_rho, neo_eps, marker="o") - ax.set_xlabel(r"$\rho$") - ax.set_ylabel(r"$\epsilon_{\text{eff}}^{3/2}$") - ax.set_title("NEO effective ripple ε¹ᐧ⁵?") - plt.tight_layout() - plt.show() - plt.close() - - # Plot DESC vs NEO effective ripple. fig, ax = plt.subplots() - ax.plot(rho, eps_eff, marker="o", label="ε¹ᐧ⁵ DESC") - # Looks more similar when neo_eps -> neo_eps**1.5... - ax.plot(neo_rho, neo_eps, marker="o", label="ε¹ᐧ⁵? NEO") - ax.legend() - ax.set_xlabel(r"$\rho$") - ax.set_ylabel(r"$\epsilon_{\text{eff}}^{3/2}$") - ax.set_title("DESC vs. NEO effective ripple") - plt.tight_layout() - plt.show() - plt.close() + ax.plot(rho, grid.compress(data["effective ripple"]), marker="o") + return fig @pytest.mark.unit +@pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_1d) def test_Gamma_c(): - """Compare DESC effective ripple against NEO STELLOPT.""" - eq = Equilibrium.load( - "tests/inputs/DESC_from_NAE_O_r1_precise_QI_plunk_fixed_bdry_r0" - ".15_L_9_M_9_N_24_output.h5" - ) - rho = jnp.linspace(0, 1, 20) - alpha = jnp.array([0]) - # TODO: Here's a potential issue, resolve with 2d spline. - knots = jnp.linspace(-30 * jnp.pi, 30 * jnp.pi, 2000) + """Test Γ_c with W7-X.""" + eq = examples.get("W7-X") + rho = np.linspace(0, 1, 10) grid = rtz_grid( - eq, rho, alpha, knots, coordinates="raz", period=(jnp.inf, 2 * jnp.pi, jnp.inf) + eq, + rho, + np.array([0]), + np.linspace(0, 20 * np.pi, 1000), + coordinates="raz", + period=(np.inf, 2 * np.pi, np.inf), ) data = eq.compute("Gamma_c", grid=grid) assert np.isfinite(data["Gamma_c"]).all() - Gamma_c = grid.compress(data["Gamma_c"]) - fig, ax = plt.subplots() - ax.plot(rho, Gamma_c, marker="o") - ax.set_xlabel(r"$\rho$") - ax.set_ylabel(r"$\Gamma_{c}$") - ax.set_title(r"DESC $\Gamma_{c}$") - plt.tight_layout() - plt.show() - plt.close() - - -class NEOWrapper: - """Class to easily make NEO and BOOZxform inputs from DESC equilibria.""" - - def __init__(self, basename, eq=None, ns=None, M_booz=None, N_booz=None): - - self.basename = basename - self.M_booz = M_booz - self.N_booz = N_booz - - if eq: - self.build(eq, basename, ns=ns) - - def build(self, eq, basename, **kwargs): - """Pass as input an already-solved Equilibrium from DESC.""" - # equilibrium parameters - self.eq = eq - self.sym = eq.sym - self.L = eq.L - self.M = eq.M - self.N = eq.N - self.NFP = eq.NFP - self.spectral_indexing = eq.spectral_indexing - self.pressure = eq.pressure - self.iota = eq.iota - self.current = eq.current - - # wout parameters - self.ns = kwargs.get("ns", 256) - - # booz parameters - if self.M_booz is None: - self.M_booz = 3 * eq.M + 1 - if self.N_booz is None: - self.N_booz = 3 * eq.N - - # basename for files - self.basename = basename - - def save_VMEC(self): - """Save VMEC file.""" - self.eq.solved = True # must set this for NEO to run correctly - print(f"Saving VMEC wout file to wout_{self.basename}.nc") - VMECIO.save(self.eq, f"wout_{self.basename}.nc", surfs=self.ns, verbose=0) - - def write_booz(self): - """Write BOOZ_XFORM input file.""" - print(f"Writing BOOZ_XFORM input file to in_booz.{self.basename}") - with open(f"in_booz.{self.basename}", "w+") as f: - f.write("{} {}\n".format(self.M_booz, self.N_booz)) - f.write(f"'{self.basename}'\n") - f.write( - "\n".join([str(x) for x in range(2, self.ns + 1)]) - ) # surface indices - - def write_neo(self, N_particles=150): - """Write NEO input file.""" - print(f"Writing NEO input file neo_in.{self.basename}") - with open(f"neo_in.{self.basename}", "w+") as f: - f.write("'#'\n'#'\n'#'\n") - f.write(f" boozmn_{self.basename}.nc\n") # booz out file - f.write(f" neo_out.{self.basename}\n") # desired NEO out file - f.write(f" {self.ns-1}\n") # number of surfaces - f.write( - " ".join([str(x) for x in range(2, self.ns + 1)]) + "\n" - ) # surface indices - f.write( - " 300 ! number of theta points\n " - "300 ! number of zeta points\n " - + f"\n 0\n 0\n {N_particles} ! number of test particles\n " - f"1 ! 1 = singly trapped particles\n " - + "0.001 ! integration accuracy\n 100 ! number of poloidal bins\n " - "10 ! integration steps per field period\n " - + "500 ! min number of field periods\n " - "5000 ! max number of field periods\n" - ) # default values - f.write( - " 0\n 1\n 0\n 0\n 2 ! 2 = reference |B| used is max on each surface" - " \n 0\n 0\n 0\n 0\n 0\n" - ) - f.write("'#'\n'#'\n'#'\n") - f.write(" 0\n") - f.write(f"neo_cur_{self.basename}\n") - f.write(" 200\n 2\n 0\n") - - -def read_neo_out(fname): - """Read ripple from text file.""" - - def nan_helper(y): - return np.isnan(y), lambda z: z.nonzero()[0] - - # import all data from text file as an array - with open(f"{fname}") as f: - array = np.array([[float(x) for x in line.split()] for line in f]) - - eps_eff = array[:, 1] # epsilon_eff^(3/2) is the second column - nans, x = nan_helper(eps_eff) # find NaN values - - # replace NaN values with linear interpolation - eps_eff[nans] = np.interp(x(nans), x(~nans), eps_eff[~nans]) - - return eps_eff + ax.plot(rho, grid.compress(data["Gamma_c"]), marker="o") + return fig From 9979ab55a54bbe6ff35b89046a34d8f254af4101 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 13 Jun 2024 16:49:48 -0500 Subject: [PATCH 05/58] Fix docstring --- desc/objectives/_neoclassical.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 4ea8c24207..f4c137e8d5 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -298,13 +298,9 @@ class GammaC(_Objective): Unique coordinate values for field line label alpha, and field line following coordinate zeta. num_quad : int - Resolution for quadrature of bounce integrals. Default is 31, - which gets sufficient convergence, so higher values are likely unnecessary. + Resolution for quadrature of bounce integrals. Default is 31. num_pitch : int - Resolution for quadrature over velocity coordinate, preferably odd. - Default is 99. Effective ripple will look smoother at high values. - (If computed on many flux surfaces and micro oscillation is seen - between neighboring surfaces, increasing num_pitch will smooth the profile). + Resolution for quadrature over velocity coordinate. Default is 99. batch : bool Whether to vectorize part of the computation. Default is true. name : str, optional @@ -411,7 +407,7 @@ def build(self, use_jit=True, verbose=1): super().build(use_jit=use_jit, verbose=verbose) def compute(self, params, constants=None): - """Compute the effective ripple. + """Compute Γ_c. Parameters ---------- @@ -423,7 +419,7 @@ def compute(self, params, constants=None): Returns ------- - effective_ripple : ndarray + Gamma_c : ndarray """ if constants is None: @@ -447,16 +443,17 @@ def compute(self, params, constants=None): period=(np.inf, 2 * np.pi, np.inf), iota=SplineProfile(iota, df=iota_r, knots=self._rho, name="iota", jnp=jnp), ) + data = { + key: grid.copy_data_from_other(data[key], self._grid_1dr) + for key in self._keys_1dr + } data = compute_fun( eq, self._keys, params, get_transforms(self._keys, eq, grid, jitable=True), get_profiles(self._keys, eq, grid, jitable=True), - data={ - key: grid.copy_data_from_other(data[key], self._grid_1dr) - for key in self._keys_1dr - }, + data=data, **self._hyperparameters, ) return grid.compress(data["Gamma_c"]) From 595634959afb79f9fa96dc986838e820211fb64e Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 17 Jun 2024 21:48:56 -0500 Subject: [PATCH 06/58] Update Gamma_c test after merge --- desc/compute/_neoclassical.py | 4 ++-- desc/compute/bounce_integral.py | 2 +- tests/baseline/test_Gamma_c.png | Bin 14770 -> 14271 bytes 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index a5eefbd7dd..ad5e565a33 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -324,7 +324,7 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): quad = leggauss(kwargs.get("num_quad", 31)) num_pitch = kwargs.get("num_pitch", 125) adaptive = kwargs.get("adaptive", False) - pitch = _get_pitch(g, data, num_pitch, adaptive) + pitch = _get_pitch(g, data["min_tz |B|"], data["max_tz |B|"], num_pitch, adaptive) def d_v_tau(B, pitch): return 2 / jnp.sqrt(1 - pitch * B) @@ -353,7 +353,7 @@ def d_Gamma_c(pitch): return jnp.nansum(v_tau * gamma_c**2, axis=-1) # The integrand is piecewise continuous and likely poorly approximated by a - # polynomial, so composite quadrature should perform better than higher order + # polynomial. Composite quadrature should perform better than higher order # methods. Gamma_c = trapezoid(jnp.squeeze(imap(d_Gamma_c, pitch), axis=1), pitch, axis=0) else: diff --git a/desc/compute/bounce_integral.py b/desc/compute/bounce_integral.py index 8c2dd5dd75..495ba8fa57 100644 --- a/desc/compute/bounce_integral.py +++ b/desc/compute/bounce_integral.py @@ -1184,7 +1184,7 @@ def bounce_integral( grid returned from the method ``desc.equilibrium.coords.rtz_grid``. See ``tests.test_bounce_integral.test_bounce_integral_checks`` for example use. - The strictly increasing knots requirement enforces dζ > 0, which constraints the + The strictly increasing knots requirement enforces dζ > 0, which constrains the signs of B^ζ and ∂/∂ζ. The signs of B^ζ and (∂|B|/∂ζ)|ρ,α will automatically be corrected to match this requirement. Pass in ``check=True`` to be notified if the signs for B^ζ and (∂|B|/∂ζ)|ρ,α required correction. diff --git a/tests/baseline/test_Gamma_c.png b/tests/baseline/test_Gamma_c.png index ed105d7dbe087955ff8f17ffc7375477b6f06e43..664d4a20c15a905207625d83f1b80a5f402e413d 100644 GIT binary patch literal 14271 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∾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

fTpgf^TjZz;b*+&B6}=yl+wzWch{5r@Budv>qsg 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 Date: Mon, 24 Jun 2024 15:10:49 -0500 Subject: [PATCH 07/58] Fix calls to rtz_grid after merge --- desc/objectives/_neoclassical.py | 3 +-- tests/test_neoclassical.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 6b32361426..bf5ccb1366 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -441,8 +441,7 @@ def compute(self, params, constants=None): ) iota = self._grid_1dr.compress(data["iota"]) iota_r = self._grid_1dr.compress(data["iota_r"]) - grid = rtz_grid( - eq, + grid = eq.rtz_grid( self._rho, self._alpha, self._zeta, diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 699f47767a..59eb62158a 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -86,8 +86,7 @@ def test_Gamma_c(): """Test Γ_c with W7-X.""" eq = get("W7-X") rho = np.linspace(0, 1, 10) - grid = rtz_grid( - eq, + grid = eq.rtz_grid( rho, np.array([0]), np.linspace(0, 20 * np.pi, 1000), From f3361cb1af9c2d8f6fdf3f637dd10d159b4a0132 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 25 Jun 2024 14:22:57 -0500 Subject: [PATCH 08/58] average before integration to reduce computation --- desc/compute/_neoclassical.py | 21 +++++++++++++-------- desc/objectives/_neoclassical.py | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 5a9123c581..96875e5d57 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -283,7 +283,10 @@ def d_ripple(pitch): "", ], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - num_quad="int : Resolution for quadrature of bounce integrals. Default is 31.", + num_quad=( + "int : Resolution for quadrature of bounce integrals. Default is 31, " + "which gets sufficient convergence, so higher values are likely unnecessary." + ), num_pitch=( "int : Resolution for quadrature over velocity coordinate. Default is 125." ), @@ -352,7 +355,13 @@ def d_Gamma_c(pitch): # The integrand is piecewise continuous and likely poorly approximated by a # polynomial. Composite quadrature should perform better than higher order # methods. - Gamma_c = trapezoid(jnp.squeeze(imap(d_Gamma_c, pitch), axis=1), pitch, axis=0) + Gamma_c = trapezoid( + _poloidal_mean( + g, imap(d_Gamma_c, pitch).reshape(-1, g.num_rho, g.num_alpha) + ), + pitch, + axis=0, + ) else: def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): @@ -383,11 +392,7 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): Gamma_c = _vec_quadax(romberg, divmax=jnp.log2(num_pitch + 1))( d_Gamma_c, pitch, *args ) + Gamma_c = _poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha)) - data["Gamma_c"] = ( - jnp.pi - / (8 * 2**0.5) - * g.expand(_poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha))) - / data[""] - ) + data["Gamma_c"] = jnp.pi / (8 * 2**0.5) * g.expand(Gamma_c) / data[""] return data diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index bf5ccb1366..afc46c1edf 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -306,6 +306,7 @@ class GammaC(_Objective): more toroidal transits will give more accurate result, with diminishing returns. num_quad : int Resolution for quadrature of bounce integrals. Default is 31. + which gets sufficient convergence, so higher values are likely unnecessary. num_pitch : int Resolution for quadrature over velocity coordinate. Default is 99. batch : bool From e6adeb25aa74e1614582fef10a1e8a27760c1e14 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 16 Jul 2024 17:14:25 -0400 Subject: [PATCH 09/58] remove unused jitable arg causing error --- desc/objectives/_neoclassical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 8986d8a94e..296dbdd24f 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -454,7 +454,7 @@ def compute(self, params, constants=None): self._keys, params, get_transforms(self._keys, eq, grid, jitable=True), - get_profiles(self._keys, eq, grid, jitable=True), + get_profiles(self._keys, eq, grid), data=data, **self._hyperparameters, ) From 1462d2838f957df0422ceffd9804c0b01856ee56 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 17 Jul 2024 13:46:11 -0400 Subject: [PATCH 10/58] remove unneeded jitable call --- desc/objectives/_neoclassical.py | 2 +- tests/test_objective_funs.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 296dbdd24f..bf0f34a834 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -242,7 +242,7 @@ def compute(self, params, constants=None): self._keys, params, get_transforms(self._keys, eq, grid, jitable=True), - get_profiles(self._keys, eq, grid, jitable=True), + get_profiles(self._keys, eq, grid), data=data, **self._hyperparameters, ) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index ae3414da15..209f2b24bd 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -2287,6 +2287,8 @@ class TestObjectiveNaNGrad: ObjectiveFromUser, # TODO: add Omnigenity objective (see GH issue #943) Omnigenity, + GammaC, + EffectiveRipple, ] other_objectives = list(set(objectives) - set(specials)) From 1c8d4eb34541ab329bb1c72acc17fcf4fd5c387c Mon Sep 17 00:00:00 2001 From: unalmis Date: Wed, 24 Jul 2024 22:21:38 -0400 Subject: [PATCH 11/58] Only get profiles in objective build method --- desc/compute/_neoclassical.py | 5 +---- desc/objectives/_neoclassical.py | 9 ++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 877bed2b2f..226079b8dc 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -293,10 +293,7 @@ def d_ripple(pitch): "", ], source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - num_quad=( - "int : Resolution for quadrature of bounce integrals. Default is 31, " - "which gets sufficient convergence, so higher values are likely unnecessary." - ), + num_quad="int : Resolution for quadrature of bounce integrals. Default is 31.", num_pitch=( "int : Resolution for quadrature over velocity coordinate. Default is 125." ), diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index f4a4365b25..2e364a9b53 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -302,7 +302,6 @@ class GammaC(_Objective): more toroidal transits will give more accurate result, with diminishing returns. num_quad : int Resolution for quadrature of bounce integrals. Default is 31. - which gets sufficient convergence, so higher values are likely unnecessary. num_pitch : int Resolution for quadrature over velocity coordinate. Default is 99. batch : bool @@ -397,8 +396,8 @@ def build(self, use_jit=True, verbose=1): self._constants["transforms_1dr"] = get_transforms( self._keys_1dr, eq, self._grid_1dr ) - self._constants["profiles_1dr"] = get_profiles( - self._keys_1dr, eq, self._grid_1dr + self._constants["profiles"] = get_profiles( + self._keys_1dr + self._keys, eq, self._grid_1dr ) timer.stop("Precomputing transforms") @@ -431,7 +430,7 @@ def compute(self, params, constants=None): self._keys_1dr, params, constants["transforms_1dr"], - constants["profiles_1dr"], + constants["profiles"], ) iota = self._grid_1dr.compress(data["iota"]) iota_r = self._grid_1dr.compress(data["iota_r"]) @@ -453,7 +452,7 @@ def compute(self, params, constants=None): self._keys, params, get_transforms(self._keys, eq, grid, jitable=True), - get_profiles(self._keys, eq, grid), + constants["profiles"], data=data, **self._hyperparameters, ) From 2ab29a9e6f74b6c91c4a27a361bd04ec8f917dc5 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 25 Jul 2024 16:09:43 -0400 Subject: [PATCH 12/58] Add num_wells parameter to increase performance --- desc/compute/_neoclassical.py | 36 +++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index e903c759a4..22ef11509a 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -315,6 +315,13 @@ def d_ripple(pitch): "of function evaluations." ), batch="bool : Whether to vectorize part of the computation. Default is true.", + num_wells=( + "int : Maximum number of wells to detect for each pitch and field line. " + "Default is to detect all wells, but due to limitations in JAX this option " + "may consume more memory. Specifying a number that tightly upper bounds " + "the number of wells will increase performance. " + "As a reference, there are typically <= 5 wells per toroidal transit." + ), ) @partial(jit, static_argnames=["num_quad", "num_pitch", "adaptive", "batch"]) def _Gamma_c(params, transforms, profiles, data, **kwargs): @@ -332,6 +339,7 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): Equation 61, using Velasco's γ_c from equation 15 of the above paper. """ batch = kwargs.get("batch", True) + num_wells = kwargs.get("num_wells", None) g = transforms["grid"].source_grid knots = g.compress(g.nodes[:, 2], surface_label="zeta") quad = leggauss(kwargs.get("num_quad", 31)) @@ -354,17 +362,27 @@ def d_Gamma_c(pitch): # Return ∑ⱼ [v τ γ_c²]ⱼ evaluated at λ = pitch. # Note v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, # τ is the bounce time, and I is defined in Nemov eq. 36. - v_tau = bounce_integrate(d_v_tau, [], pitch, batch=batch) + v_tau = bounce_integrate( + d_v_tau, [], pitch, batch=batch, num_wells=num_wells + ) gamma_c = ( 2 / jnp.pi * jnp.arctan( safediv( bounce_integrate( - d_gamma_c, data["cvdrift0"], pitch, batch=batch + d_gamma_c, + data["cvdrift0"], + pitch, + batch=batch, + num_wells=num_wells, ), bounce_integrate( - d_gamma_c, data["gbdrift"], pitch, batch=batch + d_gamma_c, + data["gbdrift"], + pitch, + batch=batch, + num_wells=num_wells, ), ) ) @@ -385,14 +403,20 @@ def d_Gamma_c(pitch): def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): bounce_integrate, _ = bounce_integral(B_sup_z, B, B_z_ra, knots, quad) - v_tau = bounce_integrate(d_v_tau, [], pitch, batch=batch) + v_tau = bounce_integrate( + d_v_tau, [], pitch, batch=batch, num_wells=num_wells + ) gamma_c = ( 2 / jnp.pi * jnp.arctan( safediv( - bounce_integrate(d_gamma_c, cvdrift0, pitch, batch=batch), - bounce_integrate(d_gamma_c, gbdrift, pitch, batch=batch), + bounce_integrate( + d_gamma_c, cvdrift0, pitch, batch=batch, num_wells=num_wells + ), + bounce_integrate( + d_gamma_c, gbdrift, pitch, batch=batch, num_wells=num_wells + ), ) ) ) From 67586ffa1f36f11796c6992f738b8b79aaf08a43 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 25 Jul 2024 18:13:11 -0400 Subject: [PATCH 13/58] Add num_wells to Gamma_c objective --- desc/objectives/_neoclassical.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 857cab084f..38b94ab12c 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -316,6 +316,14 @@ class GammaC(_Objective): Resolution for quadrature over velocity coordinate. Default is 99. batch : bool Whether to vectorize part of the computation. Default is true. + num_wells : int + Maximum number of wells to detect for each pitch and field line. + Default is to detect all wells, but due to limitations in JAX this option + may consume more memory. Specifying a number that tightly upper bounds + the number of wells will increase performance. + As a reference, there are typically <= 5 wells per toroidal transit. + There exist utilities to plot the field line with the bounce points + to see how many wells there are. name : str, optional Name of the objective function. @@ -341,6 +349,7 @@ def __init__( num_quad=31, num_pitch=99, batch=True, + num_wells=None, name="Gamma_c", ): if bounds is not None: @@ -360,6 +369,7 @@ def __init__( "num_quad": num_quad, "num_pitch": num_pitch, "batch": batch, + "num_wells": num_wells, } super().__init__( From 6ec5bd2ef1de09474852829b569b8ec6106f8e3a Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 25 Jul 2024 21:17:39 -0400 Subject: [PATCH 14/58] Add num_wells as static parameter to compute fun --- desc/compute/_neoclassical.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index ec3cd541e3..139566eeb6 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -309,12 +309,6 @@ def d_ripple(pitch): num_pitch=( "int : Resolution for quadrature over velocity coordinate. Default is 125." ), - adaptive=( - "bool : Whether to adaptively integrate over the velocity coordinate. " - "If true, then num_pitch specifies an upper bound on the maximum number " - "of function evaluations." - ), - batch="bool : Whether to vectorize part of the computation. Default is true.", num_wells=( "int : Maximum number of wells to detect for each pitch and field line. " "Default is to detect all wells, but due to limitations in JAX this option " @@ -322,8 +316,16 @@ def d_ripple(pitch): "the number of wells will increase performance. " "As a reference, there are typically <= 5 wells per toroidal transit." ), + batch="bool : Whether to vectorize part of the computation. Default is true.", + adaptive=( + "bool : Whether to adaptively integrate over the velocity coordinate. " + "If true, then num_pitch specifies an upper bound on the maximum number " + "of function evaluations." + ), +) +@partial( + jit, static_argnames=["num_quad", "num_pitch", "num_wells", "batch", "adaptive"] ) -@partial(jit, static_argnames=["num_quad", "num_pitch", "adaptive", "batch"]) def _Gamma_c(params, transforms, profiles, data, **kwargs): """Energetic ion confinement proxy. From 2c54f9d2d61ca10cb000b46a3d081687f9f8cbe2 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 25 Jul 2024 22:16:29 -0400 Subject: [PATCH 15/58] Specify kwargs for trapezoid integration after quadax API changes from previous commit --- desc/compute/_neoclassical.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 20bbf4be5d..46bee54781 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -395,10 +395,10 @@ def d_Gamma_c(pitch): # polynomial. Composite quadrature should perform better than higher order # methods. Gamma_c = trapezoid( - _poloidal_mean( + y=_poloidal_mean( g, imap(d_Gamma_c, pitch).reshape(-1, g.num_rho, g.num_alpha) ), - pitch, + x=pitch, axis=0, ) else: From 921f4d52b8acda70f901ce17e7832d9cb8805356 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 5 Aug 2024 17:02:26 -0400 Subject: [PATCH 16/58] Add Nemov's Gamma_c - Adds compute functions for Nemov's Gamma_c - Implements Nemov's Gamma_c - Replaces unused get_extrema function in bounce integral utities with new method to return extremum points rather than extremum values - Fixes test from https://github.com/PlasmaControl/DESC/pull/1027/files/148d08d97cf6c08b922d0883700b2758dc9b000c#r1704624731 so that equality of dependencies is checked. - Cosmetic: Use floats instead of ints in some places --- desc/compute/_basis_vectors.py | 107 ++++++++--- desc/compute/_core.py | 4 +- desc/compute/_field.py | 225 ++++++++++++++++++++++- desc/compute/_neoclassical.py | 173 ++++++++++++++++- desc/compute/bounce_integral.py | 141 +++++++------- tests/inputs/master_compute_data_rpz.pkl | Bin 8075396 -> 8171686 bytes tests/inputs/master_compute_data_xyz.pkl | Bin 8000161 -> 8096445 bytes tests/test_bounce_integral.py | 25 ++- tests/test_compute_funs.py | 79 +++++--- tests/test_data_index.py | 4 +- tests/test_neoclassical.py | 3 +- 11 files changed, 620 insertions(+), 141 deletions(-) diff --git a/desc/compute/_basis_vectors.py b/desc/compute/_basis_vectors.py index 70bc1fa6e5..4b0dd47978 100644 --- a/desc/compute/_basis_vectors.py +++ b/desc/compute/_basis_vectors.py @@ -34,7 +34,7 @@ def _b(params, transforms, profiles, data, **kwargs): @register_compute_fun( - name="e^rho", # ∇ρ is the same in (ρ,θ,ζ) and (ρ,α,ζ) coordinates. + name="e^rho", # ∇ρ is the same in any coordinate system. label="\\mathbf{e}^{\\rho}", units="m^{-1}", units_long="inverse meters", @@ -927,7 +927,7 @@ def _e_sup_theta_zz(params, transforms, profiles, data, **kwargs): @register_compute_fun( - name="e^zeta", # ∇ζ is the same in (ρ,θ,ζ) and (ρ,α,ζ) coordinates. + name="e^zeta", # ∇ζ is the same in any coordinate system. label="\\mathbf{e}^{\\zeta}", units="m^{-1}", units_long="inverse meters", @@ -2450,10 +2450,10 @@ def _e_sub_theta_over_sqrt_g(params, transforms, profiles, data, **kwargs): ) def _e_sub_vartheta_rp(params, transforms, profiles, data, **kwargs): # constant ρ and ϕ - e_vartheta = ( - data["e_theta"].T * data["phi_z"] - data["e_zeta"].T * data["phi_t"] - ) / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) - data["e_theta_PEST"] = e_vartheta.T + data["e_theta_PEST"] = ( + (data["e_theta"].T * data["phi_z"] - data["e_zeta"].T * data["phi_t"]) + / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) + ).T return data @@ -2474,11 +2474,13 @@ def _e_sub_vartheta_rp(params, transforms, profiles, data, **kwargs): ) def _e_sub_phi_rv(params, transforms, profiles, data, **kwargs): # constant ρ and ϑ - e_phi = ( - data["e_zeta"].T * data["theta_PEST_t"] - - data["e_theta"].T * data["theta_PEST_z"] - ) / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) - data["e_phi|r,v"] = e_phi.T + data["e_phi|r,v"] = ( + ( + data["e_zeta"].T * data["theta_PEST_t"] + - data["e_theta"].T * data["theta_PEST_z"] + ) + / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) + ).T return data @@ -2500,10 +2502,10 @@ def _e_sub_phi_rv(params, transforms, profiles, data, **kwargs): def _e_sub_rho_vp(params, transforms, profiles, data, **kwargs): # constant ϑ and ϕ data["e_rho|v,p"] = ( - data["e_rho"].T - - data["e_vartheta"].T * data["theta_PEST_r"] - - data["e_phi|r,v"].T * data["phi_r"] - ).T + data["e_rho"] + - data["e_vartheta"] * data["theta_PEST_r"][:, jnp.newaxis] + - data["e_phi|r,v"] * data["phi_r"][:, jnp.newaxis] + ) return data @@ -3207,7 +3209,28 @@ def _e_sub_zeta_zz(params, transforms, profiles, data, **kwargs): data["Z_zzz"], ] ).T + return data + +@register_compute_fun( + name="grad(phi)", + label="\\nabla \\phi", + units="m^{-1}", + units_long="Inverse meters", + description="Gradient of cylindrical toroidal angle ϕ.", + dim=3, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["R", "0"], + parameterization=[ + "desc.equilibrium.equilibrium.Equilibrium", + "desc.geometry.surface.FourierRZToroidalSurface", + ], +) +def _grad_phi(params, transforms, profiles, data, **kwargs): + data["grad(phi)"] = jnp.column_stack([data["0"], 1 / data["R"], data["0"]]) return data @@ -3413,7 +3436,7 @@ def _e_sub_theta_rp(params, transforms, profiles, data, **kwargs): label="\\mathbf{e}_{\\rho} |_{\\alpha, \\zeta}", units="m", units_long="meters", - description="Tangent vector along radial field line label", + description="Covariant radial basis vector in (ρ, α, ζ) Clebsch coordinates.", dim=3, params=[], transforms={}, @@ -3434,7 +3457,7 @@ def _e_rho_az(params, transforms, profiles, data, **kwargs): label="\\mathbf{e}_{\\alpha}", units="m", units_long="meters", - description="Tangent vector along poloidal field line label", + description="Covariant poloidal basis vector in (ρ, α, ζ) Clebsch coordinates.", dim=3, params=[], transforms={}, @@ -3453,8 +3476,8 @@ def _e_alpha(params, transforms, profiles, data, **kwargs): label="\\partial_{\\theta} \\mathbf{e}_{\\alpha}", units="m", units_long="meters", - description="Tangent vector along poloidal field line label, derivative wrt" - " DESC poloidal angle", + description="Covariant poloidal basis vector in (ρ, α, ζ) Clebsch coordinates," + " derivative wrt DESC poloidal angle", dim=3, params=[], transforms={}, @@ -3475,8 +3498,8 @@ def _e_alpha_t(params, transforms, profiles, data, **kwargs): label="\\partial_{\\zeta} \\mathbf{e}_{\\alpha}", units="m", units_long="meters", - description="Tangent vector along poloidal field line label, " - "derivative wrt DESC toroidal angle", + description="Covariant poloidal basis vector in (ρ, α, ζ) Clebsch coordinates, " + "derivative wrt DESC toroidal angle at fixed ρ,θ.", dim=3, params=[], transforms={}, @@ -3560,7 +3583,7 @@ def _e_zeta_ra_a(params, transforms, profiles, data, **kwargs): units="m", units_long="meters", description="Tangent vector along (collinear to) field line, " - "derivative wrt DESC toroidal angle", + "derivative wrt DESC toroidal angle at fixed ρ,θ.", dim=3, params=[], transforms={}, @@ -3636,3 +3659,43 @@ def _d_ell_d_zeta_z(params, transforms, profiles, data, **kwargs): dot(data["(e_zeta|r,a)_z|r,a"], data["e_zeta|r,a"]) / data["|e_zeta|r,a|"] ) return data + + +@register_compute_fun( + name="e_alpha|r,p", + label="\\mathbf{e}_{\\alpha} |_{\\rho, \\phi}", + units="m", + units_long="meters", + description="Covariant poloidal basis vector in (ρ, α, ϕ) Clebsch coordinates.", + dim=3, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["e_theta", "alpha_t", "e_zeta", "alpha_z", "phi_t", "phi_z"], +) +def _e_alpha_rp(params, transforms, profiles, data, **kwargs): + data["e_alpha|r,p"] = ( + (data["e_theta"].T * data["phi_z"] - data["e_zeta"].T * data["phi_t"]) + / (data["alpha_t"] * data["phi_z"] - data["alpha_z"] * data["phi_t"]) + ).T + return data + + +@register_compute_fun( + name="|e_alpha|r,p|", + label="|\\mathbf{e}_{\\alpha} |_{\\rho, \\phi}|", + units="m", + units_long="meters", + description="Norm of covariant poloidal basis vector in (ρ, α, ϕ) Clebsch " + "coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["e_alpha|r,p"], +) +def _e_alpha_rp_norm(params, transforms, profiles, data, **kwargs): + data["|e_alpha|r,p|"] = jnp.linalg.norm(data["e_alpha|r,p"], axis=-1) + return data diff --git a/desc/compute/_core.py b/desc/compute/_core.py index 2947e244a2..e0e8312b9b 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -1475,10 +1475,10 @@ def _Z_zzz(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="rtz", - data=["theta_PEST", "zeta", "iota"], + data=["theta_PEST", "phi", "iota"], ) def _alpha(params, transforms, profiles, data, **kwargs): - data["alpha"] = (data["theta_PEST"] - data["iota"] * data["zeta"]) % (2 * jnp.pi) + data["alpha"] = (data["theta_PEST"] - data["iota"] * data["phi"]) % (2 * jnp.pi) return data diff --git a/desc/compute/_field.py b/desc/compute/_field.py index eef1354e3d..f0cb1d2391 100644 --- a/desc/compute/_field.py +++ b/desc/compute/_field.py @@ -106,6 +106,220 @@ def _B_sup_zeta(params, transforms, profiles, data, **kwargs): return data +@register_compute_fun( + name="B^phi", + label="B^{\\phi}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["B0", "phi_z", "lambda_t", "phi_t", "lambda_z"], +) +def _B_sup_phi(params, transforms, profiles, data, **kwargs): + data["B^phi"] = data["B0"] * ( + data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + ) + return data + + +@register_compute_fun( + name="B^phi_r", + label="\\partial_{\\rho} B^{\\phi} |_{\\theta, \\zeta}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt ρ in (ρ, θ, ζ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=[ + "B0", + "phi_z", + "lambda_t", + "phi_t", + "lambda_z", + "B0_r", + "phi_rz", + "lambda_rt", + "phi_rt", + "lambda_rz", + ], +) +def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): + data["B^phi_r"] = data["B0_r"] * ( + data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + ) + data["B0"] * ( + data["phi_rz"] * (data["lambda_t"] + 1) + + data["phi_z"] * data["lambda_rt"] + - data["phi_rt"] * data["lambda_z"] + - data["phi_t"] * data["lambda_rz"] + ) + return data + + +@register_compute_fun( + name="B^phi_t", + label="\\partial_{\\theta} B^{\\phi} |_{\\rho, \\zeta}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt θ in (ρ, θ, ζ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=[ + "B0", + "phi_z", + "lambda_t", + "phi_t", + "lambda_z", + "B0_t", + "phi_tz", + "lambda_tt", + "phi_tt", + "lambda_tz", + ], +) +def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): + data["B^phi_t"] = data["B0_t"] * ( + data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + ) + data["B0"] * ( + data["phi_tz"] * (data["lambda_t"] + 1) + + data["phi_z"] * data["lambda_tt"] + - data["phi_tt"] * data["lambda_z"] + - data["phi_t"] * data["lambda_tz"] + ) + return data + + +@register_compute_fun( + name="B^phi_z", + label="\\partial_{\\zeta} B^{\\phi} |_{\\rho, \\theta}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt ζ in (ρ, θ, ζ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=[ + "B0", + "phi_z", + "lambda_t", + "phi_t", + "lambda_z", + "B0_z", + "phi_zz", + "lambda_tz", + "phi_tz", + "lambda_zz", + ], +) +def _B_sup_phi_z(params, transforms, profiles, data, **kwargs): + data["B^phi_z"] = data["B0_z"] * ( + data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + ) + data["B0"] * ( + data["phi_zz"] * (data["lambda_t"] + 1) + + data["phi_z"] * data["lambda_tz"] + - data["phi_tz"] * data["lambda_z"] + - data["phi_t"] * data["lambda_zz"] + ) + return data + + +@register_compute_fun( + name="B^phi_v|r,p", + label="\\partial_{\\vartheta} B^{\\phi} |_{\\rho, \\phi}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt ϑ in (ρ, ϑ, ϕ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["B^phi_t", "B^phi_z", "theta_PEST_t", "theta_PEST_z", "phi_t", "phi_z"], +) +def _B_sup_phi_v_rp(params, transforms, profiles, data, **kwargs): + data["B^phi_v|r,p"] = ( + data["B^phi_t"] * data["phi_z"] - data["B^phi_z"] * data["phi_t"] + ) / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) + return data + + +@register_compute_fun( + name="B^phi_p|r,v", + label="\\partial_{\\phi} B^{\\phi} |_{\\rho, \\vartheta}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt ϕ in (ρ, ϑ, ϕ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["B^phi_t", "B^phi_z", "theta_PEST_t", "theta_PEST_z", "phi_t", "phi_z"], +) +def _B_sup_phi_p_rv(params, transforms, profiles, data, **kwargs): + data["B^phi_p|r,v"] = ( + data["B^phi_z"] * data["theta_PEST_t"] - data["B^phi_t"] * data["theta_PEST_z"] + ) / (data["theta_PEST_t"] * data["phi_z"] - data["theta_PEST_z"] * data["phi_t"]) + return data + + +@register_compute_fun( + name="B^phi_r|v,p", + label="\\partial_{\\rho} B^{\\phi} |_{\\vartheta, \\phi}", + units="T \\cdot m^{-1}", + units_long="Tesla / meter", + description="Contravariant cylindrical toroidal angle component of magnetic field," + " partial derivative wrt ρ in (ρ, ϑ, ϕ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["B^phi_r", "B^phi_v|r,p", "B^phi_p|r,v", "theta_PEST_r", "phi_r"], +) +def _B_sup_phi_r_vp(params, transforms, profiles, data, **kwargs): + data["B^phi_r|v,p"] = ( + data["B^phi_r"] + - data["B^phi_v|r,p"] * data["theta_PEST_r"] + - data["B^phi_p|r,v"] * data["phi_r"] + ) + return data + + +@register_compute_fun( + name="|B|_r|v,p", + label="\\partial_{\\rho} |\\mathbf{B}| |_{\\vartheta, \\phi}", + units="T", + units_long="Tesla", + description="Magnetic field norm, derivative wrt ρ in (ρ, ϑ, ϕ) coordinates.", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["grad(|B|)", "e_rho|v,p"], +) +def _B_norm_r_vp(params, transforms, profiles, data, **kwargs): + data["|B|_r|v,p"] = dot(data["grad(|B|)"], data["e_rho|v,p"]) + return data + + @register_compute_fun( name="B", label="\\mathbf{B}", @@ -128,7 +342,7 @@ def _B(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="B_R", - label="B_{R}", + label="B_{R} = \\mathbf{B} \\cdot \\hat{R}", units="T", units_long="Tesla", description="Radial component of magnetic field in lab frame", @@ -146,7 +360,7 @@ def _B_R(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="B_phi", - label="B_{\\phi}", + label="B_{\\phi} = \\mathbf{B} \\cdot \\hat{\\phi}", units="T", units_long="Tesla", description="Toroidal component of magnetic field in lab frame", @@ -155,16 +369,16 @@ def _B_R(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="rtz", - data=["B"], + data=["B^phi", "R"], ) def _B_phi(params, transforms, profiles, data, **kwargs): - data["B_phi"] = data["B"][:, 1] + data["B_phi"] = data["R"] * data["B^phi"] return data @register_compute_fun( name="B_Z", - label="B_{Z}", + label="B_{Z} = \\mathbf{B} \\cdot \\hat{Z}", units="T", units_long="Tesla", description="Vertical component of magnetic field in lab frame", @@ -2371,6 +2585,7 @@ def _B_mag_alpha(params, transforms, profiles, data, **kwargs): data=["|B|_z", "|B|_a", "alpha_z"], ) def _B_mag_z_constant_rho_alpha(params, transforms, profiles, data, **kwargs): + # Same as grad(|B|) dot e_zeta|r,a but avoids radial derivatives. data["|B|_z|r,a"] = data["|B|_z"] - data["|B|_a"] * data["alpha_z"] return data diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 46bee54781..3c6dbc5445 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -18,7 +18,7 @@ from .bounce_integral import bounce_integral, get_pitch from .data_index import register_compute_fun -from .utils import safediv +from .utils import cross, dot, safediv def _vec_quadax(quad, **kwargs): @@ -248,9 +248,9 @@ def dI(B, pitch): # Integrand of Nemov eq. 31. def d_ripple(pitch): # Return (∂ψ/∂ρ)⁻² λ⁻²B₀⁻³ ∑ⱼ Hⱼ²/Iⱼ evaluated at λ = pitch. # Note (λB₀)³ db = (λB₀)³ λ⁻²B₀⁻¹ (-dλ) = λB₀² (-dλ) where B₀ has units of λ⁻¹. - # Interpolate |∇ρ| κ_g since it is smoother than κ_g alone. H = bounce_integrate( dH, + # Interpolate |∇ρ| κ_g since it is smoother than κ_g alone. data["|grad(rho)|"] * data["kappa_g"], pitch, batch=batch, @@ -327,7 +327,7 @@ def d_ripple(pitch): jit, static_argnames=["num_quad", "num_pitch", "num_wells", "batch", "adaptive"] ) def _Gamma_c(params, transforms, profiles, data, **kwargs): - """Energetic ion confinement proxy. + """Energetic ion confinement proxy as defined by Velasco et al. A model for the fast evaluation of prompt losses of energetic ions in stellarators. J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. @@ -441,3 +441,170 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): data["Gamma_c"] = jnp.pi / (8 * 2**0.5) * g.expand(Gamma_c) / data[""] return data + + +@register_compute_fun( + name="Gamma_c Nemov", + label=( + # Γ_c = π/(8√2) ∫dλ 〈 ∑ⱼ [v τ γ_c²]ⱼ 〉 + "\\Gamma_c = \\frac{\\pi}{8 \\sqrt{2}} " + "\\int d\\lambda \\langle \\sum_j (v \\tau \\gamma_c^2)_j \\rangle" + ), + units="~", + units_long="None", + description="Energetic ion confinement proxy, Nemov et al.", + dim=1, + params=[], + transforms={"grid": []}, + profiles=[], + coordinates="r", + data=[ + "min_tz |B|", + "max_tz |B|", + "B^zeta", + "B^phi", + "B^phi_r|v,p", + "b", + "|B|", + "|B|_z|r,a", + "|B|_r|v,p", + "", + "iota_r", + "grad(phi)", + "e^rho", + "|grad(rho)|", + "|e_alpha|r,p|", + "kappa_g", + "psi_r", + ], + source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, + num_quad="int : Resolution for quadrature of bounce integrals. Default is 31.", + num_pitch=( + "int : Resolution for quadrature over velocity coordinate. Default is 125." + ), + num_wells=( + "int : Maximum number of wells to detect for each pitch and field line. " + "Default is to detect all wells, but due to limitations in JAX this option " + "may consume more memory. Specifying a number that tightly upper bounds " + "the number of wells will increase performance. " + "As a reference, there are typically <= 5 wells per toroidal transit." + ), + batch="bool : Whether to vectorize part of the computation. Default is true.", + adaptive=( + "bool : Whether to adaptively integrate over the velocity coordinate. " + "If true, then num_pitch specifies an upper bound on the maximum number " + "of function evaluations." + ), +) +@partial( + jit, static_argnames=["num_quad", "num_pitch", "num_wells", "batch", "adaptive"] +) +def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): + """Energetic ion confinement proxy as defined by Nemov et al. + + Poloidal motion of trapped particle orbits in real-space coordinates. + V. V. Nemov, S. V. Kasilov, W. Kernbichler, G. O. Leitold. + Phys. Plasmas 1 May 2008; 15 (5): 052501. + https://doi.org/10.1063/1.2912456. + Equation 61. + + The radial electric field has a negligible effect on alpha particle confinement, + so it is assumed to be zero. + """ + batch = kwargs.get("batch", True) + num_wells = kwargs.get("num_wells", None) + g = transforms["grid"].source_grid + knots = g.compress(g.nodes[:, 2], surface_label="zeta") + quad = leggauss(kwargs.get("num_quad", 31)) + num_pitch = kwargs.get("num_pitch", 125) + adaptive = kwargs.get("adaptive", False) + pitch = _get_pitch(g, data["min_tz |B|"], data["max_tz |B|"], num_pitch, adaptive) + + # The derivative (∂/∂ψ)|ϑ,ϕ belongs to flux coordinates which satisfy + # α = ϑ − χ(ψ) ϕ where α is the poloidal label of ψ,α Clebsch coordinates. + # Choosing χ = ι implies ϑ, ϕ are PEST angles. + # ∂G/∂((λB₀)⁻¹) = λ²B₀ ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) ∂|B|/∂ψ / |B| + # ∂V/∂((λB₀)⁻¹) = 3/2 λ²B₀ ∫ dℓ √(1 − λ|B|) K / |B| + # ∂g/∂((λB₀)⁻¹) = λ²B₀² ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ψ| κ_g / |B| + # tan(π/2 γ_c) = + # ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ρ| κ_g / |B| + # -------------------------------------------- + # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| ] + + def d_v_tau(B, pitch): + return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) + + def num(grad_rho_norm_kappa_g, B, pitch): + return ( + safediv(1 - pitch * B / 2, jnp.sqrt(jnp.abs(1 - pitch * B))) + * grad_rho_norm_kappa_g + / B + ) + + def den(dB_dpsi, K, B, pitch): + return ( + jnp.sqrt(jnp.abs(1 - pitch * B)) + * (safediv(1 - pitch * B / 2, 1 - pitch * B) * dB_dpsi + K) + / B + ) + + bounce_integrate, _ = bounce_integral( + data["B^zeta"], data["|B|"], data["|B|_z|r,a"], knots, quad + ) + + def d_Gamma_c(pitch): + # Return ∑ⱼ [v τ γ_c²]ⱼ evaluated at λ = pitch. + # Note v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, + # τ is the bounce time, and I is defined in Nemov eq. 36. + v_tau = bounce_integrate(d_v_tau, [], pitch, batch=batch, num_wells=num_wells) + gamma_c = ( + 2 + / jnp.pi + * jnp.arctan( + safediv( + bounce_integrate( + num, + grad_rho_norm_kappa_g, + pitch, + batch=batch, + num_wells=num_wells, + ), + bounce_integrate( + den, + [dB_dpsi, K], + pitch, + batch=batch, + num_wells=num_wells, + weight=weight, + ), + ) + ) + ) + return jnp.sum(v_tau * gamma_c**2, axis=-1) + + grad_rho_norm_kappa_g = data["|grad(rho)|"] * data["kappa_g"] + dB_dpsi = data["|B|_r|v,p"] / data["psi_r"] + weight = data["|grad(rho)|"] * data["|e_alpha|r,p|"] + K = ( + # TODO: Confirm if K is smoother than individual components. + # If not, should spline separately. + data["iota_r"] * dot(cross(data["e^rho"], data["b"]), data["grad(phi)"]) + # Behaves as log derivative if one ignores the issue of an argument with units. + # Smoothness guaranteed by + lower bound of argument ∂log(|B|²/B^ϕ)/∂ψ |B|. + # Note that Nemov assumes B^ϕ > 0; this is not true in DESC, but we account + # for that in this computation. + - (2 * data["|B|_r|v,p"] - data["|B|"] * data["B^phi_r|v,p"] / data["B^phi"]) + / data["psi_r"] + ) + + # The integrand is piecewise continuous and likely poorly approximated by a + # polynomial. Composite quadrature should perform better than higher order + # methods. + Gamma_c = trapezoid( + y=_poloidal_mean(g, imap(d_Gamma_c, pitch).reshape(-1, g.num_rho, g.num_alpha)), + x=pitch, + axis=0, + ) + + data["Gamma_c Nemov"] = jnp.pi / (8 * 2**0.5) * g.expand(Gamma_c) / data[""] + return data diff --git a/desc/compute/bounce_integral.py b/desc/compute/bounce_integral.py index e344dd3383..2f744fab79 100644 --- a/desc/compute/bounce_integral.py +++ b/desc/compute/bounce_integral.py @@ -3,7 +3,8 @@ from functools import partial import numpy as np -from interpax import CubicHermiteSpline, PchipInterpolator, PPoly, interp1d +from interpax import CubicHermiteSpline, PPoly, interp1d +from jax.nn import softmax from matplotlib import pyplot as plt from orthax.legendre import leggauss @@ -563,7 +564,7 @@ def bounce_points( First axis enumerates the coefficients of power series. Second axis enumerates the splines along the field lines. Last axis enumerates the polynomials that compose the spline along a particular field line. - num_wells : int + num_wells : int or None If not specified, then all bounce points are returned in an array whose last axis has size ``(knots.size - 1) * (B_c.shape[0] - 1)``. If there were less than that many wells detected along a field line, then the last @@ -600,7 +601,7 @@ def bounce_points( intersect = _poly_root( c=B_c, k=jnp.reciprocal(pitch)[..., jnp.newaxis], - a_min=jnp.array([0]), + a_min=jnp.array([0.0]), a_max=jnp.diff(knots), sort=True, sentinel=-1, @@ -691,14 +692,8 @@ def get_pitch(min_B, max_B, num, relative_shift=1e-6): return pitch -def get_extrema(knots, B_c, B_z_ra_c, relative_shift=1e-6): - """Return |B| values at extrema. - - The quantity 1 / √(1 − λ |B|) common to bounce integrals is singular with - strength ~ |ζ_b₂ - ζ_b₁| / |(∂|B|/∂ζ)|ρ,α|. Therefore, an integral over the pitch - angle λ may have mass concentrated near λ = 1 / |B|(ζ*) where |B|(ζ*) is a - local maximum. Depending on the quantity to integrate, it may be beneficial - to place quadrature points at these regions. +def _get_extrema(knots, B_c, B_z_ra_c, sentinel=jnp.nan): + """Return extrema of |B| along field line. Sort order is arbitrary. Parameters ---------- @@ -711,49 +706,31 @@ def get_extrema(knots, B_c, B_z_ra_c, relative_shift=1e-6): First axis enumerates the coefficients of power series. Second axis enumerates the splines along the field lines. Last axis enumerates the polynomials that compose the spline along a particular field line. - B_z_ra_c : jnp.ndarray + B_z_ra_c : jnp.ndarray Shape (B_c.shape[0] - 1, *B_c.shape[1:]). Polynomial coefficients of the spline of (∂|B|/∂ζ)|ρ,α in local power basis. First axis enumerates the coefficients of power series. Second axis enumerates the splines along the field lines. Last axis enumerates the polynomials that compose the spline along a particular field line. - relative_shift : float - Relative amount to shift maxima down and minima up to avoid floating point - errors in downstream routines. + sentinel : float + Value with which to pad array to return fixed shape. Returns ------- - B_extrema : jnp.ndarray - Shape (N * (degree - 1), S). - For the shaping notation, the ``degree`` of the spline of |B| matches - ``B_c.shape[0]-1``, the number of polynomials per spline ``N`` matches - ``knots.size-1``, and the number of field lines is denoted by ``S``. - If there were less than ``N*degree`` bounce points detected along a field line, - then the last axis, which enumerates the bounce points for a particular field - line, is padded with nan. + extrema, B_extrema : jnp.ndarray + Shape (S, N * (degree - 1)). """ B_c, B_z_ra_c, _ = _check_shape(knots, B_c, B_z_ra_c) S, N, degree = B_c.shape[1], knots.size - 1, B_c.shape[0] - 1 - extrema = _poly_root(c=B_z_ra_c, a_min=jnp.array([0]), a_max=jnp.diff(knots)) - assert extrema.shape == (S, N, degree - 1) - B_extrema = _poly_val(x=extrema, c=B_c[..., jnp.newaxis]) - B_zz_ra_extrema = _poly_val(x=extrema, c=_poly_der(B_z_ra_c)[..., jnp.newaxis]) - # Floating point error impedes consistent detection of bounce points riding - # extrema. Shift pitch values slightly to resolve this issue. - B_extrema = ( - jnp.where( - # Higher priority to shift down maxima than shift up minima, so identify - # near equality with zero as maxima. - B_zz_ra_extrema <= 0, - (1 - relative_shift) * B_extrema, - (1 + relative_shift) * B_extrema, - ) - .reshape(S, -1) - .T + extrema = _poly_root( + c=B_z_ra_c, a_min=jnp.array([0.0]), a_max=jnp.diff(knots), sentinel=sentinel ) - assert B_extrema.shape == (N * (degree - 1), S) - return B_extrema + assert extrema.shape == (S, N, degree - 1) + B_extrema = _poly_val(x=extrema, c=B_c[..., jnp.newaxis]).reshape(S, -1) + # Transform out of local power basis expansion. + extrema = (extrema + knots[:-1, jnp.newaxis]).reshape(S, -1) + return extrema, B_extrema def affine_bijection_to_disc(x, a, b): @@ -913,7 +890,7 @@ def _plot(Z, V, title_id=""): plt.show() -def _check_interp(Z, f, B_sup_z, B, B_z_ra, inner_product, plot): +def _check_interp(Z, f, B_sup_z, B, B_z_ra, result, plot): """Check for floating point errors. Parameters @@ -930,8 +907,8 @@ def _check_interp(Z, f, B_sup_z, B, B_z_ra, inner_product, plot): B_z_ra : jnp.ndarray Norm of magnetic field, derivative with respect to field-line following coordinate, interpolated to Z. - inner_product : jnp.ndarray - Output of ``_interpolatory_quadrature``. + result : jnp.ndarray + Output of ``_interpolate_and_integrate``. plot : bool Whether to plot stuff. @@ -953,7 +930,7 @@ def _check_interp(Z, f, B_sup_z, B, B_z_ra, inner_product, plot): assert not jnp.isclose(B_sup_z, 0).any(), msg # Number of those integrals that were computed. - actual = jnp.sum(marked & jnp.isfinite(inner_product)) + actual = jnp.sum(marked & jnp.isfinite(result)) assert goal == actual, ( f"Lost {goal - actual} integrals from NaN generation in the integrand. This " "can be caused by floating point error or a poor choice of quadrature nodes." @@ -984,7 +961,6 @@ def _interpolate_and_integrate( pitch, knots, method, - method_B="cubic", check=False, plot=False, ): @@ -998,7 +974,7 @@ def _interpolate_and_integrate( Returns ------- - inner_product : jnp.ndarray + result : jnp.ndarray Shape Z.shape[:-1]. Quadrature for every pitch along every field line. @@ -1017,15 +993,15 @@ def _interpolate_and_integrate( # that the singularity near the bounce points can be captured more accurately than # can be by any polynomial. f = [_interp1d_vec(Z, knots, f_i, method=method).reshape(shape) for f_i in f] - # TODO: Pass in derivative and use method_B. + # TODO: Pass in derivative and use cubic method. b_sup_z = _interp1d_vec(Z, knots, B_sup_z / B, method=method).reshape(shape) - B = _interp1d_vec_with_df(Z, knots, B, B_z_ra, method=method_B).reshape(shape) - inner_product = jnp.dot(integrand(*f, B=B, pitch=pitch) / b_sup_z, w) + B = _interp1d_vec_with_df(Z, knots, B, B_z_ra, method="cubic").reshape(shape) + result = jnp.dot(integrand(*f, B=B, pitch=pitch) / b_sup_z, w) if check: - _check_interp(Z.reshape(shape), f, b_sup_z, B, B_z_ra, inner_product, plot) + _check_interp(Z.reshape(shape), f, b_sup_z, B, B_z_ra, result, plot) - return inner_product + return result def _bounce_quadrature( @@ -1041,7 +1017,6 @@ def _bounce_quadrature( pitch, knots, method="akima", - method_B="cubic", batch=True, check=False, ): @@ -1097,8 +1072,6 @@ def _bounce_quadrature( Method of interpolation for functions contained in ``f``. See https://interpax.readthedocs.io/en/latest/_api/interpax.interp1d.html. Default is akima spline. - method_B : str - Method of interpolation for |B|. Default is C1 cubic Hermite spline. batch : bool Whether to perform computation in a batched manner. Default is true. check : bool @@ -1134,7 +1107,6 @@ def _bounce_quadrature( pitch, knots, method, - method_B, check, # Only developers doing debugging want to see these plots. plot=False, @@ -1155,7 +1127,6 @@ def loop(bp): pitch, knots, method, - method_B, check=False, plot=False, ) @@ -1288,13 +1259,8 @@ def bounce_integral( B_sup_z, B, B_z_ra = (f.reshape(-1, knots.size) for f in [B_sup_z, B, B_z_ra]) # Compute splines. - monotonic = kwargs.pop("monotonic", False) # Interpax interpolation requires strictly increasing knots. - B_c = ( - PchipInterpolator(knots, B, axis=-1, check=check).c - if monotonic - else CubicHermiteSpline(knots, B, B_z_ra, axis=-1, check=check).c - ) + B_c = CubicHermiteSpline(knots, B, B_z_ra, axis=-1, check=check).c B_c = jnp.moveaxis(B_c, source=1, destination=-1) B_z_ra_c = _poly_der(B_c) degree = 3 @@ -1312,7 +1278,13 @@ def bounce_integral( x = auto(x) def bounce_integrate( - integrand, f, pitch, method="akima", batch=True, num_wells=None + integrand, + f, + pitch, + method="akima", + batch=True, + num_wells=None, + weight=None, ): """Bounce integrate ∫ f(ℓ) dℓ. @@ -1340,7 +1312,7 @@ def bounce_integrate( Default is akima spline. batch : bool Whether to perform computation in a batched manner. Default is true. - num_wells : int + num_wells : int or None If not specified, then all bounce integrals are returned in an array whose last axis has size ``(knots.size - 1) * degree``. If there were less than that many wells detected along a field line, then the last @@ -1354,6 +1326,11 @@ def bounce_integrate( identified. This will be done automatically if the ``bounce_integral`` function is called with ``check=True`` and ``plot=True``. As a reference, there are typically <= 5 wells per toroidal transit. + weight : jnp.ndarray + Shape (S, knots.size) or (S * knots.size). + If supplied, the bounce integral labeled by well j is weighted such that + the returned value is w(j) ∫ f(ℓ) dℓ, where w(j) is ``weight`` + evaluated at the deepest point in the magnetic well. Returns ------- @@ -1377,11 +1354,43 @@ def bounce_integrate( pitch, knots, method, - method_B="monotonic" if monotonic else "cubic", batch=batch, check=check, ) + if weight is not None: + result *= _compute_at_deepest( + bp1, bp2, knots, B_c, B_z_ra_c, weight.reshape(-1, knots.size), method + ) assert result.shape[-1] == setdefault(num_wells, (knots.size - 1) * degree) return result return bounce_integrate, spline + + +def _compute_at_deepest(bp1, bp2, knots, B_c, B_z_ra_c, f, method, beta=-50): + """Compute ``f`` at deepest point in the magnetic well. + + Let E = {ζ ∣ ζ₁ < ζ < ζ₂} and A = argmin_E |B|(ζ). Returns f_min = mean_A f. + + Parameters + ---------- + beta : float + More negative gives exponentially better approximation to f_min at the + expense of sharper gradients. + + """ + extrema, B_extrema = _get_extrema(knots, B_c, B_z_ra_c, sentinel=0) + P, S, num_wells = bp1.shape + assert extrema.shape == B_extrema.shape == (S, extrema.shape[-1]) + B = jnp.where( + (bp1[..., jnp.newaxis] < extrema[:, jnp.newaxis]) + & (extrema[:, jnp.newaxis] < bp2[..., jnp.newaxis]), + (B_extrema / jnp.mean(B_extrema, axis=-1, keepdims=True))[:, jnp.newaxis], + 100, # 100 >> max(|B|) / mean(|B|) + ) + f_min = jnp.linalg.vecdot( + softmax(beta * B, axis=-1), + _interp1d_vec(extrema, knots, f, method=method)[:, jnp.newaxis], + ) + assert f_min.shape == (P, S, num_wells) + return f_min diff --git a/tests/inputs/master_compute_data_rpz.pkl b/tests/inputs/master_compute_data_rpz.pkl index 3d55ded55a55c774c0e3fc5b497bd733574202d3..65ce5afca5d7eef6459f7857585b2a8f6fcb9cba 100644 GIT binary patch delta 84932 zcmaHScOaJE8@Ii;$H<6CNM@ylOGJ@MnzB;Lh(c)~4@x0L)|0I4nayn%CEZzu$iE`})K4+~+y>xvz7r`<&0`TzB8jQ^t=jQBt{51@S$mQl`9i#s2ZWL|V7z zgK1W!{*T|82OOZ3CA~Z?Nf+>#d?yvXlCx$&Ii*klk%B-k<8UodFVKhlSb(WDXMim^ zO+YS%5QXg(68WeCVda`DG2qK-ljlZz1Fr?%4wSIq^RnQ#5D2Iw-^XC_`{uo?Hw#m4 z2*4fg!vURDP z2mxE%Sh5YQ^Ch#ZP?;o_bdkJ^fPGOS?;&6=PUKAliXdr{t_TGW<{3%RO<=kmxF$2r zNfZ0ELH4HHA!Og3iIoi4=*EU|>rnI(u!UywF#^_pbY+xnapVP(*_^@n*E$D^0Rpy{ zlf0WiZAC#&MutUrQ}pAHU|V~HD>FqiA>i5!JND)*-j2n8BFF#l-9dqiWG=GO(mYO% z4=*G^j*p{NxAG{<9EuSFwqa=HJ9dQ&rMuz>)&(XDs^Ey;bJay#j)t~vfNB}N&{Y4E_j zG6gRMcJD6bI01{kN%xX~EpDc&BTx*k%DxT@`pJy9EibJopcAMD2&A+3IjOMg1eQL+ z+26ul`z6aC;r?#8_Pc@MxA=cre#!g0!>=LNe#VR?S@=~Ld2vOoTHZ>4WU4B^Z$)^j z;BpN*lAnkPA4&Bd4HNcIl4WfwE%)d{BuO7SHnCAqo*|WAR1v}C0%>D#bJQ`fm}4C_ zl}JlaS()_J zl$-KK>|q+^yz+l$>Gn-pij=g&ikHN8>O4QGOpK0ot=Ed!Z*!@~|BSzvPvh74EDhA- z%7Q`ctbYmkZ|%v)J4j)2^fa4SCsuNvZjwMjp+=IwAWjI#P-7-(#zjq+j##3nq>vc_watlHV3l+(_d)e6ipRwqweKAVJap znGMFUjxa=^P@%*O_tUfyNU`ng+?co>4L%h{3T*v78ovK}%xIRuFOL!3Lq&2m4SC_f~-Zq0~`JnR$70wFB_Bh^11 zx@x=MF-8@PKNQ0s%CS0WNh-oHfmwy}S8pFGH8~P)K@rlK0xmgh%#q8PKz;x-@#V^s zWcZ^|73>Q&^9%u#FW~B0u>);07ys&L2OkS}5J(*-*g3fW8Isoas-=m&{c66zR5}@I zCI2%O+(Um&W&00?HGjPRkPcVlg;NZiG<0iT>35g%SEtBm+-d*gnk(wW@Brjn6=7VM zshc1ar24m+{*h;8imOrhb~{t*%IqSvnFa~uH!vp?CM1bZXO9B{GqEGCZs`jMj% z|I3vLOT-J~#wAf;wu9V)DilF)Nv2|_xUn!k9sw0Bv!9Nt z{srDi0``VZq);;W9t|xil4l(?_iFFox$N4t-_D(=Ai|*{8007N|96i)q_VTJRHSct zK!*wD=yIq~t+*nVb(&|R3WXdsM!|4eNQL>2{a1f%ILwQ`JbNfGnJKCzJ;5M`zcv2H zoAD&M66U1Eu?w;!HW>mJN%iD9Z&HLI4IQSzOM84Z#a;eDw-!SQ$EhX=SgIuz&;KiG z$~CA+);l-+%CoA8RDU_dU;h5PJBFv_H+TdG|D7=L1o3c$Zg@37b4Y3+r^Q+(J_BJ)};OPUoc~S#}bbupmU}B(PAgc+u}S7%l1b^d>

aO=a2-g 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 diff --git a/tests/inputs/master_compute_data_xyz.pkl b/tests/inputs/master_compute_data_xyz.pkl index e7a44c775dfe89c4fdfac74b2ce783a98aac1240..2c940b07d360518fb17aee922378894e9b8479db 100644 GIT binary patch delta 85284 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%0DAKl-QV64juhf!cp6Y1d|`jP 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 F{|6X3S6=`C diff --git a/tests/test_bounce_integral.py b/tests/test_bounce_integral.py index 08793b57fa..b222a0918a 100644 --- a/tests/test_bounce_integral.py +++ b/tests/test_bounce_integral.py @@ -18,6 +18,7 @@ _composite_linspace, _filter_nonzero_measure, _filter_not_nan, + _get_extrema, _poly_der, _poly_root, _poly_val, @@ -27,7 +28,6 @@ automorphism_sin, bounce_integral, bounce_points, - get_extrema, get_pitch, grad_affine_bijection, grad_automorphism_arcsin, @@ -214,7 +214,7 @@ def test(x, c): @pytest.mark.unit def test_get_extrema(): - """Test that these pitch intersect extrema of |B|.""" + """Test computation of extrema of |B|.""" start = -np.pi end = -2 * start k = np.linspace(start, end, 5) @@ -222,13 +222,15 @@ def test_get_extrema(): k, np.cos(k) + 2 * np.sin(-2 * k), -np.sin(k) - 4 * np.cos(-2 * k) ) B_z_ra = B.derivative() - extrema_scipy = np.sort(B(B_z_ra.roots(extrapolate=False))) - rtol = 1e-7 - extrema = get_extrema(k, B.c, B_z_ra.c, relative_shift=rtol) - eps = 100 * np.finfo(float).eps - extrema = np.sort(_filter_not_nan(extrema)) + extrema, B_extrema = _get_extrema(k, B.c, B_z_ra.c) + extrema, B_extrema = map(_filter_not_nan, (extrema, B_extrema)) + idx = np.argsort(extrema) + + extrema_scipy = np.sort(B_z_ra.roots(extrapolate=False)) + B_extrema_scipy = B(extrema_scipy) assert extrema.size == extrema_scipy.size - np.testing.assert_allclose(extrema, extrema_scipy, rtol=rtol + eps) + np.testing.assert_allclose(extrema[idx], extrema_scipy) + np.testing.assert_allclose(B_extrema[idx], B_extrema_scipy) @pytest.mark.unit @@ -754,6 +756,7 @@ def integrand_den(B, pitch): f=[], pitch=pitch[:, np.newaxis], num_wells=1, + weight=np.ones(zeta.size), ) drift_numerical_num = np.squeeze(drift_numerical_num) @@ -769,7 +772,11 @@ def integrand_den(B, pitch): # Test if differentiable. def dummy_fun(pitch): - return jnp.sum(bounce_integrate(integrand_num, [cvdrift, gbdrift], pitch)) + return jnp.sum( + bounce_integrate( + integrand_num, [cvdrift, gbdrift], pitch, weight=np.ones(zeta.size) + ) + ) assert np.isclose(grad(dummy_fun)(1.0), 650, rtol=1e-3) diff --git a/tests/test_compute_funs.py b/tests/test_compute_funs.py index b0e5932a74..9fa4cc1bd3 100644 --- a/tests/test_compute_funs.py +++ b/tests/test_compute_funs.py @@ -5,7 +5,7 @@ from scipy.signal import convolve2d from desc.compute import rpz2xyz_vec -from desc.compute.utils import dot +from desc.compute.utils import cross, dot from desc.equilibrium import Equilibrium from desc.equilibrium.coords import get_rtz_grid from desc.examples import get @@ -1523,35 +1523,54 @@ def test_surface_equilibrium_geometry(): @pytest.mark.unit -def test_parallel_grad(): - """Test geometric and physical methods of computing parallel gradients agree.""" - eq = get("W7-X") - with pytest.warns(UserWarning, match="Reducing radial"): - eq.change_resolution(2, 2, 2, 4, 4, 4) - data = eq.compute( - [ - "e_zeta|r,a", - "B", - "B^zeta", - "|B|_z|r,a", - "grad(|B|)", - "|e_zeta|r,a|_z|r,a", - "B^zeta_z|r,a", - "|B|", - ], - ) - np.testing.assert_allclose(data["e_zeta|r,a"], (data["B"].T / data["B^zeta"]).T) - np.testing.assert_allclose( - data["|B|_z|r,a"], dot(data["grad(|B|)"], data["e_zeta|r,a"]) - ) - np.testing.assert_allclose( - data["|e_zeta|r,a|_z|r,a"], - data["|B|_z|r,a"] / np.abs(data["B^zeta"]) - - data["|B|"] - * data["B^zeta_z|r,a"] - * np.sign(data["B^zeta"]) - / data["B^zeta"] ** 2, - ) +def test_clebsch_sfl_funs(): + """Test geometric and physical methods of computing B agree.""" + + def test(eq): + with pytest.warns(UserWarning, match="Reducing radial"): + eq.change_resolution(2, 2, 2, 4, 4, 4) + data = eq.compute( + [ + "e_zeta|r,a", + "B", + "B^zeta", + "B^phi", + "|B|_z|r,a", + "grad(|B|)", + "|e_zeta|r,a|_z|r,a", + "B^zeta_z|r,a", + "|B|", + "sqrt(g)_Clebsch", + "psi_r", + "grad(psi)", + "grad(alpha)", + "grad(phi)", + "B_phi", + ], + ) + np.testing.assert_allclose(data["e_zeta|r,a"], (data["B"].T / data["B^zeta"]).T) + np.testing.assert_allclose( + data["|B|_z|r,a"], dot(data["grad(|B|)"], data["e_zeta|r,a"]) + ) + np.testing.assert_allclose( + data["|e_zeta|r,a|_z|r,a"], + data["|B|_z|r,a"] / np.abs(data["B^zeta"]) + - data["|B|"] + * data["B^zeta_z|r,a"] + * np.sign(data["B^zeta"]) + / data["B^zeta"] ** 2, + ) + np.testing.assert_allclose( + data["B^zeta"], data["psi_r"] / data["sqrt(g)_Clebsch"] + ) + np.testing.assert_allclose( + data["B"], cross(data["grad(psi)"], data["grad(alpha)"]) + ) + np.testing.assert_allclose(data["B^phi"], dot(data["B"], data["grad(phi)"])) + np.testing.assert_allclose(data["B_phi"], data["B"][:, 1]) + + test(get("W7-X")) + test(get("NCSX")) @pytest.mark.unit diff --git a/tests/test_data_index.py b/tests/test_data_index.py index b582ff878c..cbf918c3aa 100644 --- a/tests/test_data_index.py +++ b/tests/test_data_index.py @@ -118,9 +118,7 @@ def test_data_index_deps(self): # the source code but actually needed for the computation. This # is a temporary fix until we have a better way to automatically # handle this. - assert queried_deps[p][name]["data"].issubset( - data | axis_limit_data - ), err_msg + assert queried_deps[p][name]["data"] == data | axis_limit_data, err_msg errorif( name not in queried_deps[p], AssertionError, diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 16b8d4a59e..497cef6dc8 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -79,8 +79,9 @@ def test_Gamma_c(): coordinates="raz", period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute("Gamma_c", grid=grid) + data = eq.compute(["Gamma_c"], grid=grid) assert np.isfinite(data["Gamma_c"]).all() fig, ax = plt.subplots() ax.plot(rho, grid.compress(data["Gamma_c"]), marker="o") + plt.show() return fig From 5ca2630f0d2fe8353194623824c5e172f13a813e Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 5 Aug 2024 17:12:04 -0400 Subject: [PATCH 17/58] Remove hanging plt.show() from last commit --- tests/test_neoclassical.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 497cef6dc8..16b8d4a59e 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -79,9 +79,8 @@ def test_Gamma_c(): coordinates="raz", period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute(["Gamma_c"], grid=grid) + data = eq.compute("Gamma_c", grid=grid) assert np.isfinite(data["Gamma_c"]).all() fig, ax = plt.subplots() ax.plot(rho, grid.compress(data["Gamma_c"]), marker="o") - plt.show() return fig From cd3b6b680f99f23696286d89812306fe0e5473ef Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 5 Aug 2024 17:25:26 -0400 Subject: [PATCH 18/58] Fix comment --- desc/compute/_neoclassical.py | 2 +- desc/compute/bounce_integral.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 3c6dbc5445..7b83a0ec75 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -528,7 +528,7 @@ def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): # ∂g/∂((λB₀)⁻¹) = λ²B₀² ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ψ| κ_g / |B| # tan(π/2 γ_c) = # ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ρ| κ_g / |B| - # -------------------------------------------- + # ---------------------------------------------- # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| ] def d_v_tau(B, pitch): diff --git a/desc/compute/bounce_integral.py b/desc/compute/bounce_integral.py index 2f744fab79..5d2c008510 100644 --- a/desc/compute/bounce_integral.py +++ b/desc/compute/bounce_integral.py @@ -706,7 +706,7 @@ def _get_extrema(knots, B_c, B_z_ra_c, sentinel=jnp.nan): First axis enumerates the coefficients of power series. Second axis enumerates the splines along the field lines. Last axis enumerates the polynomials that compose the spline along a particular field line. - B_z_ra_c : jnp.ndarray + B_z_ra_c : jnp.ndarray Shape (B_c.shape[0] - 1, *B_c.shape[1:]). Polynomial coefficients of the spline of (∂|B|/∂ζ)|ρ,α in local power basis. First axis enumerates the coefficients of power series. Second axis From 4577d4f0ad622ef1dee1e0c8a20978f4a9e9a336 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 5 Aug 2024 17:27:03 -0400 Subject: [PATCH 19/58] Fix another comment --- desc/compute/_neoclassical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 7b83a0ec75..e4823eba0a 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -529,7 +529,7 @@ def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): # tan(π/2 γ_c) = # ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ρ| κ_g / |B| # ---------------------------------------------- - # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| ] + # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| def d_v_tau(B, pitch): return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) From 4595546dc13ac0f4efc52ca79c1485f6bbe25758 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sat, 17 Aug 2024 23:18:08 -0400 Subject: [PATCH 20/58] Fix reshape error raised by JAX by reverting average before pitch integral - can use multiple alphas now --- desc/compute/_neoclassical.py | 40 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 4313408340..48db05cf6c 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -390,13 +390,7 @@ def d_Gamma_c(pitch): # The integrand is piecewise continuous and likely poorly approximated by a # polynomial. Composite quadrature should perform better than higher order # methods. - Gamma_c = trapezoid( - y=_poloidal_mean( - g, imap(d_Gamma_c, pitch).reshape(-1, g.num_rho, g.num_alpha) - ), - x=pitch, - axis=0, - ) + Gamma_c = trapezoid(y=imap(d_Gamma_c, pitch).squeeze(axis=1), x=pitch, axis=0) else: def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): @@ -433,9 +427,13 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): Gamma_c = _vec_quadax(romberg, divmax=jnp.log2(num_pitch + 1))( d_Gamma_c, pitch, *args ) - Gamma_c = _poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha)) - data["Gamma_c"] = jnp.pi / (8 * 2**0.5) * g.expand(Gamma_c) / data[""] + data["Gamma_c"] = ( + jnp.pi + / (8 * 2**0.5) + * g.expand(_poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha))) + / data[""] + ) return data @@ -486,15 +484,8 @@ def d_Gamma_c(pitch, B_sup_z, B, B_z_ra, cvdrift0, gbdrift): "As a reference, there are typically <= 5 wells per toroidal transit." ), batch="bool : Whether to vectorize part of the computation. Default is true.", - adaptive=( - "bool : Whether to adaptively integrate over the velocity coordinate. " - "If true, then num_pitch specifies an upper bound on the maximum number " - "of function evaluations." - ), -) -@partial( - jit, static_argnames=["num_quad", "num_pitch", "num_wells", "batch", "adaptive"] ) +@partial(jit, static_argnames=["num_quad", "num_pitch", "num_wells", "batch"]) def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): """Energetic ion confinement proxy as defined by Nemov et al. @@ -513,8 +504,7 @@ def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): knots = g.compress(g.nodes[:, 2], surface_label="zeta") quad = leggauss(kwargs.get("num_quad", 31)) num_pitch = kwargs.get("num_pitch", 125) - adaptive = kwargs.get("adaptive", False) - pitch = _get_pitch(g, data["min_tz |B|"], data["max_tz |B|"], num_pitch, adaptive) + pitch = _get_pitch(g, data["min_tz |B|"], data["max_tz |B|"], num_pitch) # The derivative (∂/∂ψ)|ϑ,ϕ belongs to flux coordinates which satisfy # α = ϑ − χ(ψ) ϕ where α is the poloidal label of ψ,α Clebsch coordinates. @@ -596,11 +586,11 @@ def d_Gamma_c(pitch): # The integrand is piecewise continuous and likely poorly approximated by a # polynomial. Composite quadrature should perform better than higher order # methods. - Gamma_c = trapezoid( - y=_poloidal_mean(g, imap(d_Gamma_c, pitch).reshape(-1, g.num_rho, g.num_alpha)), - x=pitch, - axis=0, + Gamma_c = trapezoid(y=imap(d_Gamma_c, pitch).squeeze(axis=1), x=pitch, axis=0) + data["Gamma_c Nemov"] = ( + jnp.pi + / (8 * 2**0.5) + * g.expand(_poloidal_mean(g, Gamma_c.reshape(g.num_rho, g.num_alpha))) + / data[""] ) - - data["Gamma_c Nemov"] = jnp.pi / (8 * 2**0.5) * g.expand(Gamma_c) / data[""] return data From 75bc784b74b9e3b597ab13d0bed265e87960035b Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 18 Aug 2024 03:31:09 -0400 Subject: [PATCH 21/58] Use general angles rather than stream function aliases --- desc/compute/_core.py | 19 ++++++++ desc/compute/_field.py | 58 +++++++++++------------ tests/inputs/master_compute_data_rpz.pkl | Bin 8463814 -> 8483231 bytes tests/test_compute_funs.py | 6 ++- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index f92f3963aa..57c7a8a099 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -3119,6 +3119,25 @@ def _theta_PEST_rt(params, transforms, profiles, data, **kwargs): return data +@register_compute_fun( + name="theta_PEST_rz", + label="\\partial_{\\rho \\zeta} \\vartheta", + units="rad", + units_long="radians", + description="PEST straight field line poloidal angular coordinate, derivative wrt " + "radial and DESC toroidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["lambda_rz"], +) +def _theta_PEST_rz(params, transforms, profiles, data, **kwargs): + data["theta_PEST_rz"] = data["lambda_rz"] + return data + + @register_compute_fun( name="theta_PEST_rrt", label="\\partial_{\\rho \\rho \\theta} \\vartheta", diff --git a/desc/compute/_field.py b/desc/compute/_field.py index 039d15a0eb..98571c18a6 100644 --- a/desc/compute/_field.py +++ b/desc/compute/_field.py @@ -117,11 +117,11 @@ def _B_sup_zeta(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="rtz", - data=["B0", "phi_z", "lambda_t", "phi_t", "lambda_z"], + data=["B0", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z"], ) def _B_sup_phi(params, transforms, profiles, data, **kwargs): data["B^phi"] = data["B0"] * ( - data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) return data @@ -141,24 +141,24 @@ def _B_sup_phi(params, transforms, profiles, data, **kwargs): data=[ "B0", "phi_z", - "lambda_t", + "theta_PEST_t", "phi_t", - "lambda_z", + "theta_PEST_z", "B0_r", "phi_rz", - "lambda_rt", + "theta_PEST_rt", "phi_rt", - "lambda_rz", + "theta_PEST_rz", ], ) def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): data["B^phi_r"] = data["B0_r"] * ( - data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) + data["B0"] * ( - data["phi_rz"] * (data["lambda_t"] + 1) - + data["phi_z"] * data["lambda_rt"] - - data["phi_rt"] * data["lambda_z"] - - data["phi_t"] * data["lambda_rz"] + data["phi_rz"] * data["theta_PEST_t"] + + data["phi_z"] * data["theta_PEST_rt"] + - data["phi_rt"] * data["theta_PEST_z"] + - data["phi_t"] * data["theta_PEST_rz"] ) return data @@ -178,24 +178,24 @@ def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): data=[ "B0", "phi_z", - "lambda_t", + "theta_PEST_t", "phi_t", - "lambda_z", + "theta_PEST_z", "B0_t", "phi_tz", - "lambda_tt", + "theta_PEST_tt", "phi_tt", - "lambda_tz", + "theta_PEST_tz", ], ) def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): data["B^phi_t"] = data["B0_t"] * ( - data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) + data["B0"] * ( - data["phi_tz"] * (data["lambda_t"] + 1) - + data["phi_z"] * data["lambda_tt"] - - data["phi_tt"] * data["lambda_z"] - - data["phi_t"] * data["lambda_tz"] + data["phi_tz"] * data["theta_PEST_t"] + + data["phi_z"] * data["theta_PEST_tt"] + - data["phi_tt"] * data["theta_PEST_z"] + - data["phi_t"] * data["theta_PEST_tz"] ) return data @@ -215,24 +215,24 @@ def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): data=[ "B0", "phi_z", - "lambda_t", + "theta_PEST_t", "phi_t", - "lambda_z", + "theta_PEST_z", "B0_z", "phi_zz", - "lambda_tz", + "theta_PEST_tz", "phi_tz", - "lambda_zz", + "theta_PEST_zz", ], ) def _B_sup_phi_z(params, transforms, profiles, data, **kwargs): data["B^phi_z"] = data["B0_z"] * ( - data["phi_z"] * (data["lambda_t"] + 1) - data["phi_t"] * data["lambda_z"] + data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) + data["B0"] * ( - data["phi_zz"] * (data["lambda_t"] + 1) - + data["phi_z"] * data["lambda_tz"] - - data["phi_tz"] * data["lambda_z"] - - data["phi_t"] * data["lambda_zz"] + data["phi_zz"] * data["theta_PEST_t"] + + data["phi_z"] * data["theta_PEST_tz"] + - data["phi_tz"] * data["theta_PEST_z"] + - data["phi_t"] * data["theta_PEST_zz"] ) return data diff --git a/tests/inputs/master_compute_data_rpz.pkl b/tests/inputs/master_compute_data_rpz.pkl index b9dd93ac14327e7150e210eb503234408a668836..04a61d7aaaaa76456df0ac29e746ea8ce40336b2 100644 GIT binary patch delta 40567 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 jHkFc+OAA|P!E_`x7Z&!nA_5|#O~ikOOQ77GIo Date: Thu, 22 Aug 2024 17:35:01 -0600 Subject: [PATCH 22/58] LinkingCurrent objective --- desc/objectives/__init__.py | 1 + desc/objectives/_coils.py | 157 ++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/desc/objectives/__init__.py b/desc/objectives/__init__.py index 3ae2e59af0..954bee2ad4 100644 --- a/desc/objectives/__init__.py +++ b/desc/objectives/__init__.py @@ -7,6 +7,7 @@ CoilLength, CoilSetMinDistance, CoilTorsion, + LinkingCurrent, PlasmaCoilSetMinDistance, QuadraticFlux, ToroidalFlux, diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 228c7354ea..778fddcd18 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1,6 +1,7 @@ import numbers import numpy as np +from scipy.constants import mu_0 from desc.backend import ( fori_loop, @@ -1502,3 +1503,159 @@ def compute(self, field_params=None, constants=None): ) return Psi + + +class LinkingCurrent(_Objective): + """Target the self-consistent poloidal linking current between the plasma and coils. + + Parameters + ---------- + eq : Equilibrium + Equilibrium that will be optimized to satisfy the Objective. + coil : CoilSet + Coil(s) that are to be optimized. + target : float, ndarray, optional + Target value(s) of the objective. Only used if bounds is None. + Must be broadcastable to Objective.dim_f. If array, it has to + be flattened according to the number of inputs. + bounds : tuple of float, ndarray, optional + Lower and upper bounds on the objective. Overrides target. + Both bounds must be broadcastable to to Objective.dim_f + weight : float, ndarray, optional + Weighting to apply to the Objective, relative to other Objectives. + Must be broadcastable to to Objective.dim_f + normalize : bool, optional + Whether to compute the error in physical units or non-dimensionalize. + normalize_target : bool, optional + Whether target and bounds should be normalized before comparing to computed + values. If `normalize` is `True` and the target is in physical units, + this should also be set to True. + be set to True. + loss_function : {None, 'mean', 'min', 'max'}, optional + Loss function to apply to the objective values once computed. This loss function + is called on the raw compute value, before any shifting, scaling, or + normalization. Operates over all coils, not each individial coil. + deriv_mode : {"auto", "fwd", "rev"} + Specify how to compute jacobian matrix, either forward mode or reverse mode AD. + "auto" selects forward or reverse mode based on the size of the input and output + of the objective. Has no effect on self.grad or self.hess which always use + reverse mode and forward over reverse mode respectively. + grid : Grid, optional + Collocation grid containing the nodes to evaluate plasma current at. + Defaults to ``LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)``. + name : str, optional + Name of the objective function. + + """ + + _scalar = True + _units = "(A)" + _print_value_fmt = "Linking current: {:10.3e} " + + def __init__( + self, + eq, + coil, + target=None, + bounds=None, + weight=1, + normalize=True, + normalize_target=True, + loss_function=None, + deriv_mode="auto", + grid=None, + name="linking current", + ): + if target is None and bounds is None: + target = 0 + self._grid = grid + super().__init__( + things=[eq, coil], + target=target, + bounds=bounds, + weight=weight, + normalize=normalize, + normalize_target=normalize_target, + loss_function=loss_function, + deriv_mode=deriv_mode, + name=name, + ) + + 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. + + """ + eq = self.things[0] + coil = self.things[1] + grid = self._grid or LinearGrid( + M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym + ) + warnif( + not np.allclose(grid.nodes[:, 0], 1), + UserWarning, + "grid includes interior points, should be rho=1.", + ) + + self._dim_f = 1 + self._data_keys = ["G"] + + profiles = get_profiles(self._data_keys, obj=eq, grid=grid) + transforms = get_transforms(self._data_keys, obj=eq, grid=grid) + + self._constants = { + "eq": eq, + "coil": coil, + "grid": grid, + "profiles": profiles, + "transforms": transforms, + "quad_weights": 1.0, + } + + if self._normalize: + params = tree_leaves( + coil.params_dict, is_leaf=lambda x: isinstance(x, dict) + ) + self._normalization = np.sum([np.abs(param["current"]) for param in params]) + + super().build(use_jit=use_jit, verbose=verbose) + + def compute(self, eq_params, coil_params, constants=None): + """Compute linking current error. + + Parameters + ---------- + eq_params : dict + Dictionary of equilibrium degrees of freedom, eg ``Equilibrium.params_dict`` + coil_params : dict + Dictionary of coilset degrees of freedom, eg ``CoilSet.params_dict`` + constants : dict + Dictionary of constant data, eg transforms, profiles etc. + Defaults to self._constants. + + Returns + ------- + f : array of floats + Linking current error. + + """ + data = compute_fun( + "desc.equilibrium.equilibrium.Equilibrium", + self._data_keys, + params=eq_params, + transforms=constants["transforms"], + profiles=constants["profiles"], + ) + eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 + coil_linking_current = jnp.sum( + jnp.concatenate( + [jnp.atleast_1d(param["current"]) for param in tree_leaves(coil_params)] + ) + ) + return eq_linking_current - coil_linking_current From e7351d36fac4741743c8f008aeab83c004970d2f Mon Sep 17 00:00:00 2001 From: daniel-dudt Date: Fri, 23 Aug 2024 16:24:10 -0600 Subject: [PATCH 23/58] get linking current working in single-stage --- desc/objectives/_coils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 778fddcd18..d75572c9b5 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -16,7 +16,7 @@ from desc.compute.utils import safenorm from desc.grid import LinearGrid, _Grid from desc.integrals import compute_B_plasma -from desc.utils import Timer, errorif, warnif +from desc.utils import Timer, broadcast_tree, errorif, warnif from .normalization import compute_scaling_factors from .objective_funs import _Objective @@ -1606,13 +1606,15 @@ def build(self, use_jit=True, verbose=1): self._dim_f = 1 self._data_keys = ["G"] + all_params = tree_map(lambda dim: np.arange(dim), coil.dimensions) + current_params = tree_map(lambda idx: {"current": idx}, True) + self._indices = tree_leaves(broadcast_tree(current_params, all_params)) + self._num_coils = coil.num_coils + profiles = get_profiles(self._data_keys, obj=eq, grid=grid) transforms = get_transforms(self._data_keys, obj=eq, grid=grid) self._constants = { - "eq": eq, - "coil": coil, - "grid": grid, "profiles": profiles, "transforms": transforms, "quad_weights": 1.0, @@ -1653,9 +1655,12 @@ def compute(self, eq_params, coil_params, constants=None): profiles=constants["profiles"], ) eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 - coil_linking_current = jnp.sum( + coil_linking_current = self._num_coils * jnp.mean( jnp.concatenate( - [jnp.atleast_1d(param["current"]) for param in tree_leaves(coil_params)] + [ + jnp.atleast_1d(param[idx]) + for param, idx in zip(tree_leaves(coil_params), self._indices) + ] ) ) return eq_linking_current - coil_linking_current From fc76f96d944308e778215cf8e9e4bf964e90f5eb Mon Sep 17 00:00:00 2001 From: daniel-dudt Date: Sun, 25 Aug 2024 23:03:58 -0600 Subject: [PATCH 24/58] fix print_values --- desc/objectives/_coils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index f6a78c6007..5a0390fbd5 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1550,7 +1550,7 @@ class LinkingCurrent(_Objective): _scalar = True _units = "(A)" - _print_value_fmt = "Linking current: {:10.3e} " + _print_value_fmt = "Linking current: " def __init__( self, @@ -1647,6 +1647,8 @@ def compute(self, eq_params, coil_params, constants=None): Linking current error. """ + if constants is None: + constants = self.constants data = compute_fun( "desc.equilibrium.equilibrium.Equilibrium", self._data_keys, From 2363303932bb5dd37bb045bcc15a425baa504468 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 3 Sep 2024 20:17:51 -0400 Subject: [PATCH 25/58] Atone for B0 name change made in pull request #1168 --- desc/compute/_field.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/desc/compute/_field.py b/desc/compute/_field.py index 27a150c010..d975411c68 100644 --- a/desc/compute/_field.py +++ b/desc/compute/_field.py @@ -116,10 +116,10 @@ def _B_sup_zeta(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="rtz", - data=["B0", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z"], + data=["psi_r/sqrt(g)", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z"], ) def _B_sup_phi(params, transforms, profiles, data, **kwargs): - data["B^phi"] = data["B0"] * ( + data["B^phi"] = data["psi_r/sqrt(g)"] * ( data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) return data @@ -138,12 +138,12 @@ def _B_sup_phi(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[ - "B0", + "psi_r/sqrt(g)", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z", - "B0_r", + "(psi_r/sqrt(g))_r", "phi_rz", "theta_PEST_rt", "phi_rt", @@ -151,9 +151,9 @@ def _B_sup_phi(params, transforms, profiles, data, **kwargs): ], ) def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): - data["B^phi_r"] = data["B0_r"] * ( + data["B^phi_r"] = data["(psi_r/sqrt(g))_r"] * ( data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] - ) + data["B0"] * ( + ) + data["psi_r/sqrt(g)"] * ( data["phi_rz"] * data["theta_PEST_t"] + data["phi_z"] * data["theta_PEST_rt"] - data["phi_rt"] * data["theta_PEST_z"] @@ -175,12 +175,12 @@ def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[ - "B0", + "psi_r/sqrt(g)", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z", - "B0_t", + "(psi_r/sqrt(g))_t", "phi_tz", "theta_PEST_tt", "phi_tt", @@ -188,9 +188,9 @@ def _B_sup_phi_r(params, transforms, profiles, data, **kwargs): ], ) def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): - data["B^phi_t"] = data["B0_t"] * ( + data["B^phi_t"] = data["(psi_r/sqrt(g))_t"] * ( data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] - ) + data["B0"] * ( + ) + data["psi_r/sqrt(g)"] * ( data["phi_tz"] * data["theta_PEST_t"] + data["phi_z"] * data["theta_PEST_tt"] - data["phi_tt"] * data["theta_PEST_z"] @@ -212,12 +212,12 @@ def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[ - "B0", + "psi_r/sqrt(g)", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z", - "B0_z", + "(psi_r/sqrt(g))_z", "phi_zz", "theta_PEST_tz", "phi_tz", @@ -225,9 +225,9 @@ def _B_sup_phi_t(params, transforms, profiles, data, **kwargs): ], ) def _B_sup_phi_z(params, transforms, profiles, data, **kwargs): - data["B^phi_z"] = data["B0_z"] * ( + data["B^phi_z"] = data["(psi_r/sqrt(g))_z"] * ( data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] - ) + data["B0"] * ( + ) + data["psi_r/sqrt(g)"] * ( data["phi_zz"] * data["theta_PEST_t"] + data["phi_z"] * data["theta_PEST_tz"] - data["phi_tz"] * data["theta_PEST_z"] From d94ecd623660c1ecc91b1d14bbe479ee574a9b0f Mon Sep 17 00:00:00 2001 From: unalmis Date: Wed, 4 Sep 2024 00:24:22 -0400 Subject: [PATCH 26/58] Update Gamma_c objective for recent changes on master --- desc/compute/_neoclassical.py | 1 - desc/objectives/_neoclassical.py | 20 ++++++++++---------- tests/baseline/test_Gamma_c.png | Bin 14770 -> 13048 bytes tests/test_neoclassical.py | 20 ++++++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index ae97b3890f..376eb9b766 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -337,7 +337,6 @@ def compute(data): "B^phi", "B^phi_r|v,p", "b", - "|B|", "|B|_r|v,p", "", "iota_r", diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 1d5dc16784..dcf2e9ce9f 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -304,19 +304,19 @@ class GammaC(_Objective): For axisymmetric devices only one toroidal transit is necessary. Otherwise, more toroidal transits will give more accurate result, with diminishing returns. num_quad : int - Resolution for quadrature of bounce integrals. Default is 31. + Resolution for quadrature of bounce integrals. Default is 32. num_pitch : int - Resolution for quadrature over velocity coordinate. Default is 99. + Resolution for quadrature over velocity coordinate, preferably odd. + Default is 99. Profile will look smoother at high values. + (If computed on many flux surfaces and small oscillations is seen + between neighboring surfaces, increasing this will smooth the profile). batch : bool Whether to vectorize part of the computation. Default is true. - num_wells : int + num_well : int Maximum number of wells to detect for each pitch and field line. Default is to detect all wells, but due to limitations in JAX this option may consume more memory. Specifying a number that tightly upper bounds the number of wells will increase performance. - As a reference, there are typically <= 5 wells per toroidal transit. - There exist utilities to plot the field line with the bounce points - to see how many wells there are. name : str, optional Name of the objective function. @@ -324,7 +324,7 @@ class GammaC(_Objective): _coordinates = "r" _units = "~" - _print_value_fmt = "Γ_c: {:10.3e} " + _print_value_fmt = "Γ_c: " def __init__( self, @@ -339,10 +339,10 @@ def __init__( grid=None, alpha=np.array([0]), zeta=np.linspace(0, 2 * np.pi, 100), - num_quad=31, + num_quad=32, num_pitch=99, batch=True, - num_wells=None, + num_well=None, name="Gamma_c", ): if bounds is not None: @@ -362,7 +362,7 @@ def __init__( "num_quad": num_quad, "num_pitch": num_pitch, "batch": batch, - "num_wells": num_wells, + "num_well": num_well, } super().__init__( diff --git a/tests/baseline/test_Gamma_c.png b/tests/baseline/test_Gamma_c.png index ed105d7dbe087955ff8f17ffc7375477b6f06e43..de5be2f3a3e5da8653eaaee9d7d4d0144b14001b 100644 GIT binary patch literal 13048 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 Date: Wed, 4 Sep 2024 00:34:47 -0400 Subject: [PATCH 27/58] Add Gamma_c_Nemov baseline image for test --- tests/baseline/test_Gamma_c_Nemov.png | Bin 0 -> 16282 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/baseline/test_Gamma_c_Nemov.png diff --git a/tests/baseline/test_Gamma_c_Nemov.png b/tests/baseline/test_Gamma_c_Nemov.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebd4403e63d6a36707db81c6923f3f14744dedc GIT binary patch literal 16282 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 W?8aREpbe8E+NTZv$US-K=Kled!K32< literal 0 HcmV?d00001 From c2e455d1ac7fdedce34b57a992b0c869fee3681f Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 15 Sep 2024 15:08:56 -0400 Subject: [PATCH 28/58] Allow using other Gamma_c in objective --- desc/compute/_neoclassical.py | 41 ++++++++++-------------- desc/objectives/_neoclassical.py | 28 +++++++++++----- tests/baseline/test_Gamma_c.png | Bin 13048 -> 17281 bytes tests/baseline/test_Gamma_c_Nemov.png | Bin 16282 -> 0 bytes tests/baseline/test_Gamma_c_Velasco.png | Bin 0 -> 14278 bytes tests/test_neoclassical.py | 16 ++++----- tests/test_objective_funs.py | 6 ++-- 7 files changed, 49 insertions(+), 42 deletions(-) delete mode 100644 tests/baseline/test_Gamma_c_Nemov.png create mode 100644 tests/baseline/test_Gamma_c_Velasco.png diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index c35de82cc4..e0fccadce7 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -11,6 +11,7 @@ from functools import partial +from orthax.legendre import leggauss from quadax import simpson from desc.backend import jit, jnp, trapezoid @@ -210,7 +211,7 @@ def compute(data): @register_compute_fun( - name="Gamma_c", + name="Gamma_c Velasco", label=( # Γ_c = π/(8√2) ∫dλ 〈 ∑ⱼ [v τ γ_c²]ⱼ 〉 "\\Gamma_c = \\frac{\\pi}{8 \\sqrt{2}} " @@ -247,24 +248,18 @@ def compute(data): batch="bool : Whether to vectorize part of the computation. Default is true.", ) @partial(jit, static_argnames=["num_pitch", "num_well", "batch"]) -def _Gamma_c(params, transforms, profiles, data, **kwargs): +def _Gamma_c_Velasco(params, transforms, profiles, data, **kwargs): """Energetic ion confinement proxy as defined by Velasco et al. A model for the fast evaluation of prompt losses of energetic ions in stellarators. J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. https://doi.org/10.1088/1741-4326/ac2994. Equation 16. - - Poloidal motion of trapped particle orbits in real-space coordinates. - V. V. Nemov, S. V. Kasilov, W. Kernbichler, G. O. Leitold. - Phys. Plasmas 1 May 2008; 15 (5): 052501. - https://doi.org/10.1063/1.2912456. - Equation 61, using Velasco's γ_c from equation 15 of the above paper. """ quad = ( kwargs["quad"] if "quad" in kwargs - else get_quadrature(leggauss_lob(32), Bounce1D._default_automorphism) + else get_quadrature(leggauss(32), Bounce1D._default_automorphism) ) num_pitch = kwargs.get("num_pitch", 75) num_well = kwargs.get("num_well", None) @@ -274,13 +269,11 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): def d_v_tau(B, pitch): return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) - def d_gamma_c(f, B, pitch): + def drift(f, B, pitch): return safediv(f * (1 - pitch * B / 2), jnp.sqrt(jnp.abs(1 - pitch * B))) def compute(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" - # Note v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, - # τ is the bounce time, and I is defined in Nemov eq. 36. bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) v_tau = bounce.integrate( d_v_tau, data["pitch_inv"], batch=batch, num_well=num_well @@ -291,14 +284,14 @@ def compute(data): * jnp.arctan( safediv( bounce.integrate( - d_gamma_c, + drift, data["pitch_inv"], data["cvdrift0"], batch=batch, num_well=num_well, ), bounce.integrate( - d_gamma_c, + drift, data["pitch_inv"], data["gbdrift"], batch=batch, @@ -319,12 +312,12 @@ def compute(data): } _data["pitch_inv"] = _get_pitch_inv(grid, data, num_pitch) out = _poloidal_mean(grid, map2(compute, _data)) - data["Gamma_c"] = jnp.pi / (8 * 2**0.5) * grid.expand(out) / data[""] + data["Gamma_c Velasco"] = jnp.pi / (8 * 2**0.5) * grid.expand(out) / data[""] return data @register_compute_fun( - name="Gamma_c Nemov", + name="Gamma_c", label=( # Γ_c = π/(8√2) ∫dλ 〈 ∑ⱼ [v τ γ_c²]ⱼ 〉 "\\Gamma_c = \\frac{\\pi}{8 \\sqrt{2}} " @@ -370,7 +363,7 @@ def compute(data): batch="bool : Whether to vectorize part of the computation. Default is true.", ) @partial(jit, static_argnames=["num_pitch", "num_well", "batch"]) -def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): +def _Gamma_c(params, transforms, profiles, data, **kwargs): """Energetic ion confinement proxy as defined by Nemov et al. Poloidal motion of trapped particle orbits in real-space coordinates. @@ -385,7 +378,7 @@ def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): quad = ( kwargs["quad"] if "quad" in kwargs - else get_quadrature(leggauss_lob(32), Bounce1D._default_automorphism) + else get_quadrature(leggauss(32), Bounce1D._default_automorphism) ) num_pitch = kwargs.get("num_pitch", 75) num_well = kwargs.get("num_well", None) @@ -406,14 +399,14 @@ def _Gamma_c_Nemov(params, transforms, profiles, data, **kwargs): def d_v_tau(B, pitch): return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) - def num(grad_rho_norm_kappa_g, B, pitch): + def drift_radial(grad_rho_norm_kappa_g, B, pitch): return ( safediv(1 - pitch * B / 2, jnp.sqrt(jnp.abs(1 - pitch * B))) * grad_rho_norm_kappa_g / B ) - def den(B_psi, K, B, pitch): + def drift_poloidal(B_psi, K, B, pitch): return ( jnp.sqrt(jnp.abs(1 - pitch * B)) * (safediv(1 - pitch * B / 2, 1 - pitch * B) * B_psi + K) @@ -434,14 +427,14 @@ def compute(data): * jnp.arctan( safediv( bounce.integrate( - num, + drift_radial, data["pitch_inv"], data["|grad(rho)|*kappa_g"], batch=batch, num_well=num_well, ), bounce.integrate( - den, + drift_poloidal, data["pitch_inv"], [data["|B|_psi|v,p"], data["K"]], batch=batch, @@ -476,7 +469,7 @@ def compute(data): # If not, should spline separately. data["iota_r"] * dot(cross(data["e^rho"], data["b"]), data["grad(phi)"]) # Behaves as log derivative if one ignores the issue of an argument with units. - # Smoothness guaranteed by + lower bound of argument ∂log(|B|²/B^ϕ)/∂ψ |B|. + # Smoothness determined by + lower bound of argument ∂log(|B|²/B^ϕ)/∂ψ |B|. # Note that Nemov assumes B^ϕ > 0; this is not true in DESC, but we account # for that in this computation. - (2 * data["|B|_r|v,p"] - data["|B|"] * data["B^phi_r|v,p"] / data["B^phi"]) @@ -484,5 +477,5 @@ def compute(data): ) _data["pitch_inv"] = _get_pitch_inv(grid, data, num_pitch) out = _poloidal_mean(grid, map2(compute, _data)) - data["Gamma_c Nemov"] = jnp.pi / (8 * 2**0.5) * grid.expand(out) / data[""] + data["Gamma_c"] = jnp.pi / (8 * 2**0.5) * grid.expand(out) / data[""] return data diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 8c64a56f3f..1b3af193dc 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -1,6 +1,7 @@ """Objectives for targeting neoclassical transport.""" import numpy as np +from orthax.legendre import leggauss from desc.compute import get_profiles, get_transforms from desc.compute.utils import _compute as compute_fun @@ -266,10 +267,16 @@ class GammaC(_Objective): References ---------- - https://doi.org/10.1088/1741-4326/ac2994. - Equation 16. + Poloidal motion of trapped particle orbits in real-space coordinates. + V. V. Nemov, S. V. Kasilov, W. Kernbichler, G. O. Leitold. + Phys. Plasmas 1 May 2008; 15 (5): 052501. + https://doi.org/10.1063/1.2912456. + Equation 61. + A model for the fast evaluation of prompt losses of energetic ions in stellarators. J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. + https://doi.org/10.1088/1741-4326/ac2994. + Equation 16. Parameters ---------- @@ -329,6 +336,9 @@ class GammaC(_Objective): the number of wells will increase performance. name : str, optional Name of the objective function. + Nemov : bool + Whether to use the Γ_c as defined by Nemov et al. or Velasco et al. + Default is Nemov. """ @@ -339,7 +349,7 @@ class GammaC(_Objective): def __init__( self, eq, - target=0, + target=0.0, bounds=None, weight=1, normalize=True, @@ -355,11 +365,13 @@ def __init__( batch=True, num_well=None, name="Gamma_c", + Nemov=True, ): if bounds is not None: target = None self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] + self._key = "Gamma_c" if Nemov else "Gamma_c Velasco" self._constants = { "quad_weights": 1, "alpha": alpha, @@ -408,7 +420,7 @@ def build(self, use_jit=True, verbose=1): rho = self._grid_1dr.compress(self._grid_1dr.nodes[:, 0]) self._constants["rho"] = rho self._constants["quad"] = get_quadrature( - leggauss_lob(self._hyperparameters.pop("num_quad")), + leggauss(self._hyperparameters.pop("num_quad")), Bounce1D._default_automorphism, ) self._dim_f = rho.size @@ -425,7 +437,7 @@ def build(self, use_jit=True, verbose=1): self._keys_1dr, eq, self._grid_1dr ) self._constants["profiles"] = get_profiles( - self._keys_1dr + ["Gamma_c"], eq, self._grid_1dr + self._keys_1dr + [self._key], eq, self._grid_1dr ) timer.stop("Precomputing transforms") @@ -479,12 +491,12 @@ def compute(self, params, constants=None): } data = compute_fun( eq, - "Gamma_c", + self._key, params, - get_transforms("Gamma_c", eq, grid, jitable=True), + get_transforms(self._key, eq, grid, jitable=True), constants["profiles"], data=data, quad=constants["quad"], **self._hyperparameters, ) - return grid.compress(data["Gamma_c"]) + return grid.compress(data[self._key]) diff --git a/tests/baseline/test_Gamma_c.png b/tests/baseline/test_Gamma_c.png index de5be2f3a3e5da8653eaaee9d7d4d0144b14001b..01d2a7623bc46a0efc35cbc047d4a6338c7b6b84 100644 GIT binary patch literal 17281 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 z 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 IdE2}H16*I;qyPW_ literal 13048 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 W?8aREpbe8E+NTZv$US-K=Kled!K32< diff --git a/tests/baseline/test_Gamma_c_Velasco.png b/tests/baseline/test_Gamma_c_Velasco.png new file mode 100644 index 0000000000000000000000000000000000000000..2b7fb6f643ec9322b92797799d9445767ceead7a GIT binary patch literal 14278 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 literal 0 HcmV?d00001 diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index c40963ef88..9b1cb1e255 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -63,7 +63,7 @@ def test_effective_ripple(): @pytest.mark.unit @pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_1d) -def test_Gamma_c(): +def test_Gamma_c_Velasco(): """Test Γ_c with W7-X.""" eq = get("W7-X") rho = np.linspace(0, 1, 10) @@ -74,16 +74,16 @@ def test_Gamma_c(): coordinates="raz", period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute("Gamma_c", grid=grid) - assert np.isfinite(data["Gamma_c"]).all() + data = eq.compute("Gamma_c Velasco", grid=grid) + assert np.isfinite(data["Gamma_c Velasco"]).all() fig, ax = plt.subplots() - ax.plot(rho, grid.compress(data["Gamma_c"]), marker="o") + ax.plot(rho, grid.compress(data["Gamma_c Velasco"]), marker="o") return fig @pytest.mark.unit @pytest.mark.mpl_image_compare(remove_text=True, tolerance=tol_1d) -def test_Gamma_c_Nemov(): +def test_Gamma_c(): """Test Γ_c Nemov with W7-X.""" eq = get("W7-X") rho = np.linspace(0, 1, 10) @@ -94,8 +94,8 @@ def test_Gamma_c_Nemov(): coordinates="raz", period=(np.inf, 2 * np.pi, np.inf), ) - data = eq.compute("Gamma_c Nemov", grid=grid) - assert np.isfinite(data["Gamma_c Nemov"]).all() + data = eq.compute("Gamma_c", grid=grid) + assert np.isfinite(data["Gamma_c"]).all() fig, ax = plt.subplots() - ax.plot(rho, grid.compress(data["Gamma_c Nemov"]), marker="o") + ax.plot(rho, grid.compress(data["Gamma_c"]), marker="o") return fig diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 6e847925a0..e3a6f75b3c 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -2805,7 +2805,9 @@ def test_objective_no_nangrad_effective_ripple(self): eq = get("ESTELL") with pytest.warns(UserWarning, match="Reducing radial"): eq.change_resolution(2, 2, 2, 4, 4, 4) - obj = ObjectiveFunction([EffectiveRipple(eq)]) + obj = ObjectiveFunction( + [EffectiveRipple(eq, num_transit=3, knots_per_transit=50)] + ) obj.build(verbose=0) g = obj.grad(obj.x()) assert not np.any(np.isnan(g)) @@ -2816,7 +2818,7 @@ def test_objective_no_nangrad_Gamma_c(self): eq = get("ESTELL") with pytest.warns(UserWarning, match="Reducing radial"): eq.change_resolution(2, 2, 2, 4, 4, 4) - obj = ObjectiveFunction([GammaC(eq)]) + obj = ObjectiveFunction([GammaC(eq, num_transit=3, knots_per_transit=50)]) obj.build(verbose=0) g = obj.grad(obj.x()) assert not np.any(np.isnan(g)) From 9ed4a4748862cb1852d56f56f09d5d20e7e7c196 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 16 Sep 2024 02:30:07 -0400 Subject: [PATCH 29/58] black format --- desc/compute/_neoclassical.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 4b057da2c0..d5f760d28c 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -225,13 +225,7 @@ def compute(data): transforms={"grid": []}, profiles=[], coordinates="r", - data=[ - "min_tz |B|", - "max_tz |B|", - "cvdrift0", - "gbdrift", - "", - ] + data=["min_tz |B|", "max_tz |B|", "cvdrift0", "gbdrift", ""] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, quad="jnp.ndarray : Optional, quadrature points and weights for bounce integrals.", @@ -264,10 +258,10 @@ def _Gamma_c_Velasco(params, transforms, profiles, data, **kwargs): grid = transforms["grid"].source_grid def d_v_tau(B, pitch): - return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) + return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) def drift(f, B, pitch): - return safediv(f * (1 - pitch * B / 2), jnp.sqrt(jnp.abs(1 - pitch * B))) + return safediv(f * (1 - 0.5 * pitch * B), jnp.sqrt(jnp.abs(1 - pitch * B))) def compute(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" @@ -397,11 +391,11 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| def d_v_tau(B, pitch): - return safediv(2, jnp.sqrt(jnp.abs(1 - pitch * B))) + return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) def drift_radial(grad_rho_norm_kappa_g, B, pitch): return ( - safediv(1 - pitch * B / 2, jnp.sqrt(jnp.abs(1 - pitch * B))) + safediv(1 - 0.5 * pitch * B, jnp.sqrt(jnp.abs(1 - pitch * B))) * grad_rho_norm_kappa_g / B ) @@ -409,7 +403,7 @@ def drift_radial(grad_rho_norm_kappa_g, B, pitch): def drift_poloidal(B_psi, K, B, pitch): return ( jnp.sqrt(jnp.abs(1 - pitch * B)) - * (safediv(1 - pitch * B / 2, 1 - pitch * B) * B_psi + K) + * (safediv(1 - 0.5 * pitch * B, 1 - pitch * B) * B_psi + K) / B ) From 9acc9001e8ddc032d40296a8539f6044df687d31 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 19 Sep 2024 15:18:01 -0400 Subject: [PATCH 30/58] Use infinite period for alpha --- desc/equilibrium/coords.py | 2 +- desc/objectives/_neoclassical.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/desc/equilibrium/coords.py b/desc/equilibrium/coords.py index c5c4b6b79d..b638cd88c1 100644 --- a/desc/equilibrium/coords.py +++ b/desc/equilibrium/coords.py @@ -18,7 +18,7 @@ def _periodic(x, period): def _fixup_residual(r, period): r = _periodic(r, period) - # r should be between -period and period + # r should be between -period/2 and period/2 return jnp.where((r > period / 2) & jnp.isfinite(period), -period + r, r) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index a49d858c2e..df457cfa2f 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -480,7 +480,6 @@ def compute(self, params, constants=None): constants["alpha"], constants["zeta"], coordinates="raz", - period=(np.inf, 2 * np.pi, np.inf), iota=self._grid_1dr.compress(data["iota"]), params=params, ) From e31b602cba30a99b76c4ee6528b2029c5e84ebe0 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sat, 21 Sep 2024 06:50:20 -0400 Subject: [PATCH 31/58] Use better quadrature for part of integral in Nemov Gamma_c --- desc/compute/_neoclassical.py | 15 +++++++++------ desc/integrals/bounce_integral.py | 30 ++++++++++++++++++++++++++++-- desc/objectives/_neoclassical.py | 12 ++++++++++-- tests/baseline/test_Gamma_c.png | Bin 16550 -> 17271 bytes 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index a3c721515a..1f269dfeb2 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -368,6 +368,7 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): if "quad" in kwargs else get_quadrature(leggauss(32), (automorphism_sin, grad_automorphism_sin)) ) + quad2 = kwargs["quad2"] if "quad2" in kwargs else chebgauss2(quad[0].size) num_pitch = kwargs.get("num_pitch", 64) num_well = kwargs.get("num_well", None) batch = kwargs.get("batch", True) @@ -382,7 +383,7 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): # tan(π/2 γ_c) = # ∫ dℓ (1 − λ|B|/2) / √(1 − λ|B|) |∇ρ| κ_g / |B| # ---------------------------------------------- - # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ √(1 − λ|B|) [ (1 − λ|B|/2)/(1 − λ|B|) ∂|B|/∂ψ + K ] / |B| + # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ [ (1 − λ|B|/2)/√(1 − λ|B|) ∂|B|/∂ψ + √(1 − λ|B|) K ] / |B| def d_v_tau(B, pitch): return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) @@ -394,13 +395,14 @@ def drift_radial(grad_rho_norm_kappa_g, B, pitch): / B ) - def drift_poloidal(B_psi, K, B, pitch): + def drift_poloidal_1(B_psi, B, pitch): return ( - jnp.sqrt(jnp.abs(1 - pitch * B)) - * (safediv(1 - 0.5 * pitch * B, 1 - pitch * B) * B_psi + K) - / B + safediv(1 - 0.5 * pitch * B, jnp.sqrt(jnp.abs(1 - pitch * B))) * B_psi / B ) + def drift_poloidal_2(K, B, pitch): + return jnp.sqrt(jnp.abs(1 - pitch * B)) * K / B + def compute(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" # Note v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, @@ -422,12 +424,13 @@ def compute(data): num_well=num_well, ), bounce.integrate( - drift_poloidal, + [drift_poloidal_1, drift_poloidal_2], data["pitch_inv"], [data["|B|_psi|v,p"], data["K"]], batch=batch, num_well=num_well, weight=data["weight"], + quad2=quad2, ), ) ) diff --git a/desc/integrals/bounce_integral.py b/desc/integrals/bounce_integral.py index 5fe8e7f7a1..5b326aec32 100644 --- a/desc/integrals/bounce_integral.py +++ b/desc/integrals/bounce_integral.py @@ -304,6 +304,7 @@ def integrate( batch=True, check=False, plot=False, + quad2=None, ): """Bounce integrate ∫ f(λ, ℓ) dℓ. @@ -364,15 +365,24 @@ def integrate( flux surface, and pitch value. """ + if isinstance(integrand, (list, tuple)): + integrand_0 = integrand[0] + integrand_1 = integrand[1] + f_0 = f[0] + f_1 = f[1] + else: + integrand_0 = integrand + f_0 = f + f_1 = integrand_1 = None z1, z2 = self.points(pitch_inv, num_well=num_well) result = _bounce_quadrature( x=self._x, w=self._w, z1=z1, z2=z2, - integrand=integrand, + integrand=integrand_0, pitch_inv=pitch_inv, - f=setdefault(f, []), + f=setdefault(f_0, []), data=self._data, knots=self._zeta, method=method, @@ -380,6 +390,22 @@ def integrate( check=check, plot=plot, ) + if integrand_1 is not None: + result += _bounce_quadrature( + x=self._x if quad2 is None else quad2[0], + w=self._w if quad2 is None else quad2[1], + z1=z1, + z2=z2, + integrand=integrand_1, + pitch_inv=pitch_inv, + f=setdefault(f_1, []), + data=self._data, + knots=self._zeta, + method=method, + batch=batch, + check=check, + plot=plot, + ) if weight is not None: result *= interp_to_argmin( weight, diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index df457cfa2f..adc646cd2c 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -369,8 +369,6 @@ def __init__( if target is None and bounds is None: target = 0.0 - self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] - self._key = "Gamma_c" if Nemov else "Gamma_c Velasco" self._constants = { "quad_weights": 1, "alpha": alpha, @@ -385,6 +383,12 @@ def __init__( "num_well": num_well, } self._grid_1dr = grid + self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] + if Nemov: + self._key = "Gamma_c" + self._constants["quad2"] = chebgauss2(num_pitch) + else: + self._key = "Gamma_c Velasco" super().__init__( things=eq, @@ -487,6 +491,9 @@ def compute(self, params, constants=None): key: grid.copy_data_from_other(data[key], self._grid_1dr) for key in self._keys_1dr } + quad2 = {} + if "quad2" in constants: + quad2["quad2"] = constants["quad2"] data = compute_fun( eq, self._key, @@ -495,6 +502,7 @@ def compute(self, params, constants=None): constants["profiles"], data=data, quad=constants["quad"], + **quad2, **self._hyperparameters, ) return grid.compress(data[self._key]) diff --git a/tests/baseline/test_Gamma_c.png b/tests/baseline/test_Gamma_c.png index b8c6c9e812d4698face19d740ed9ffb467ce276a..0c0179993166ef5600833bcba783c283ec662e09 100644 GIT binary patch literal 17271 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 literal 16550 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 Date: Sat, 21 Sep 2024 06:56:26 -0400 Subject: [PATCH 32/58] Add quad2 kwargs for weak singular integrals --- desc/compute/_neoclassical.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 1f269dfeb2..d54f8a356c 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -340,7 +340,10 @@ def compute(data): ] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - quad="jnp.ndarray : Optional, quadrature points and weights for bounce integrals.", + quad="jnp.ndarray : Optional, quadrature points and weights for strongly singular " + "bounce integrals.", + quad2="jnp.ndarray : Optional, quadrature points and weights for weakly singular " + "bounce integrals.", num_pitch="int : Resolution for quadrature over velocity coordinate. Default 64.", num_well=( "int : Maximum number of wells to detect for each pitch and field line. " From 503f0d601dc5501c3ff4d017d603ea7f41b31279 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sat, 21 Sep 2024 22:38:40 -0400 Subject: [PATCH 33/58] Avoid redundant computation of bounce points --- desc/compute/_neoclassical.py | 88 ++++++++++++++----------------- desc/integrals/bounce_integral.py | 59 +++++++-------------- tests/test_integrals.py | 4 +- 3 files changed, 62 insertions(+), 89 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 395076096b..58fc47d194 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -17,6 +17,7 @@ from desc.backend import jit, jnp from ..integrals.bounce_integral import Bounce1D +from ..integrals.bounce_utils import interp_to_argmin from ..integrals.quad_utils import ( automorphism_sin, chebgauss2, @@ -255,32 +256,19 @@ def drift(f, B, pitch): def compute(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) - v_tau = bounce.integrate( - d_v_tau, data["pitch_inv"], batch=batch, num_well=num_well - ) - gamma_c = ( - 2 - / jnp.pi - * jnp.arctan( - safediv( - bounce.integrate( - drift, - data["pitch_inv"], - data["cvdrift0"], - batch=batch, - num_well=num_well, - ), - bounce.integrate( - drift, - data["pitch_inv"], - data["gbdrift"], - batch=batch, - num_well=num_well, - ), - ) + points = bounce.points(data["pitch_inv"], num_well=num_well) + v_tau = bounce.integrate(d_v_tau, points, data["pitch_inv"], batch=batch) + gamma_c = jnp.arctan( + safediv( + bounce.integrate( + drift, points, data["pitch_inv"], data["cvdrift0"], batch=batch + ), + bounce.integrate( + drift, points, data["pitch_inv"], data["gbdrift"], batch=batch + ), ) ) - return ( + return (4 / jnp.pi**2) * ( (v_tau * gamma_c**2).sum(axis=-1) * data["pitch_inv"] ** (-2) * data["pitch_inv weight"] @@ -384,19 +372,19 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): def d_v_tau(B, pitch): return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) - def drift_radial(grad_rho_norm_kappa_g, B, pitch): + def drift1(grad_rho_norm_kappa_g, B, pitch): return ( safediv(1 - 0.5 * pitch * B, jnp.sqrt(jnp.abs(1 - pitch * B))) * grad_rho_norm_kappa_g / B ) - def drift_poloidal_1(B_psi, B, pitch): + def drift2(B_psi, B, pitch): return ( safediv(1 - 0.5 * pitch * B, jnp.sqrt(jnp.abs(1 - pitch * B))) * B_psi / B ) - def drift_poloidal_2(K, B, pitch): + def drift3(K, B, pitch): return jnp.sqrt(jnp.abs(1 - pitch * B)) * K / B def compute(data): @@ -404,34 +392,40 @@ def compute(data): # Note v τ = 4λ⁻²B₀⁻¹ ∂I/∂((λB₀)⁻¹) where v is the particle velocity, # τ is the bounce time, and I is defined in Nemov eq. 36. bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) - v_tau = bounce.integrate( - d_v_tau, data["pitch_inv"], batch=batch, num_well=num_well - ) - gamma_c = ( - 2 - / jnp.pi - * jnp.arctan( - safediv( + points = bounce.points(data["pitch_inv"], num_well=num_well) + v_tau = bounce.integrate(d_v_tau, points, data["pitch_inv"], batch=batch) + gamma_c = jnp.arctan( + safediv( + bounce.integrate( + drift1, + points, + data["pitch_inv"], + data["|grad(rho)|*kappa_g"], + batch=batch, + ), + ( bounce.integrate( - drift_radial, + drift2, + points, data["pitch_inv"], - data["|grad(rho)|*kappa_g"], + data["|B|_psi|v,p"], batch=batch, - num_well=num_well, - ), - bounce.integrate( - [drift_poloidal_1, drift_poloidal_2], + ) + + bounce.integrate( + drift3, + points, data["pitch_inv"], - [data["|B|_psi|v,p"], data["K"]], + data["K"], batch=batch, - num_well=num_well, - weight=data["weight"], - quad2=quad2, - ), + quad=quad2, + ) ) + * interp_to_argmin( + data["weight"], points, bounce.zeta, bounce.B, bounce.dB_dz + ), ) ) - return ( + return (4 / jnp.pi**2) * ( (v_tau * gamma_c**2).sum(axis=-1) * data["pitch_inv"] ** (-2) * data["pitch_inv weight"] diff --git a/desc/integrals/bounce_integral.py b/desc/integrals/bounce_integral.py index a43ddb5538..82966f1a8f 100644 --- a/desc/integrals/bounce_integral.py +++ b/desc/integrals/bounce_integral.py @@ -178,10 +178,10 @@ def __init__( self._x, self._w = get_quadrature(quad, automorphism) # Compute local splines. - self._zeta = grid.compress(grid.nodes[:, 2], surface_label="zeta") + self.zeta = grid.compress(grid.nodes[:, 2], surface_label="zeta") self.B = jnp.moveaxis( CubicHermiteSpline( - x=self._zeta, + x=self.zeta, y=self._data["|B|"], dydx=self._data["|B|_z|r,a"], axis=-1, @@ -190,7 +190,7 @@ def __init__( source=(0, 1), destination=(-1, -2), ) - self._dB_dz = polyder_vec(self.B) + self.dB_dz = polyder_vec(self.B) # Add axis here instead of in ``_bounce_quadrature``. for name in self._data: @@ -251,7 +251,7 @@ def points(self, pitch_inv, *, num_well=None): line and pitch, is padded with zero. """ - return bounce_points(pitch_inv, self._zeta, self.B, self._dB_dz, num_well) + return bounce_points(pitch_inv, self.zeta, self.B, self.dB_dz, num_well) def check_points(self, points, pitch_inv, *, plot=True, **kwargs): """Check that bounce points are computed correctly. @@ -283,7 +283,7 @@ def check_points(self, points, pitch_inv, *, plot=True, **kwargs): z1=points[0], z2=points[1], pitch_inv=pitch_inv, - knots=self._zeta, + knots=self.zeta, B=self.B, plot=plot, **kwargs, @@ -301,7 +301,7 @@ def integrate( batch=True, check=False, plot=False, - quad2=None, + quad=None, ): """Bounce integrate ∫ f(λ, ℓ) dℓ. @@ -348,6 +348,9 @@ def integrate( plot : bool Whether to plot the quantities in the integrand interpolated to the quadrature points of each integral. Ignored if ``check`` is false. + quad : tuple[jnp.ndarray] + Optional quadrature points and weights. If given this overrides + the quadrature chosen when this object was made. Returns ------- @@ -357,51 +360,27 @@ def integrate( flux surface, and pitch value. """ - if isinstance(integrand, (list, tuple)): - integrand_0 = integrand[0] - integrand_1 = integrand[1] - f_0 = f[0] - f_1 = f[1] - else: - integrand_0 = integrand - f_0 = f - f_1 = integrand_1 = None result = _bounce_quadrature( - x=self._x, - w=self._w, - integrand=integrand_0, + x=self._x if quad is None else quad[0], + w=self._w if quad is None else quad[1], + integrand=integrand, points=points, pitch_inv=pitch_inv, - f=setdefault(f_0, []), + f=setdefault(f, []), data=self._data, - knots=self._zeta, + knots=self.zeta, method=method, batch=batch, check=check, plot=plot, ) - if integrand_1 is not None: - result += _bounce_quadrature( - x=self._x if quad2 is None else quad2[0], - w=self._w if quad2 is None else quad2[1], - integrand=integrand_1, - points=points, - pitch_inv=pitch_inv, - f=setdefault(f_1, []), - data=self._data, - knots=self._zeta, - method=method, - batch=batch, - check=check, - plot=plot, - ) if weight is not None: result *= interp_to_argmin( weight, points, - self._zeta, + self.zeta, self.B, - self._dB_dz, + self.dB_dz, method, ) assert result.shape == points[0].shape @@ -428,7 +407,7 @@ def plot(self, m, l, pitch_inv=None, **kwargs): Matplotlib (fig, ax) tuple. """ - B, dB_dz = self.B, self._dB_dz + B, dB_dz = self.B, self.dB_dz if B.ndim == 4: B = B[m] dB_dz = dB_dz[m] @@ -440,9 +419,9 @@ def plot(self, m, l, pitch_inv=None, **kwargs): pitch_inv.ndim > 1, msg=f"Got pitch_inv.ndim={pitch_inv.ndim}, but expected 1.", ) - z1, z2 = bounce_points(pitch_inv, self._zeta, B, dB_dz) + z1, z2 = bounce_points(pitch_inv, self.zeta, B, dB_dz) kwargs["z1"] = z1 kwargs["z2"] = z2 kwargs["k"] = pitch_inv - fig, ax = plot_ppoly(PPoly(B.T, self._zeta), **_set_default_plot_kwargs(kwargs)) + fig, ax = plot_ppoly(PPoly(B.T, self.zeta), **_set_default_plot_kwargs(kwargs)) return fig, ax diff --git a/tests/test_integrals.py b/tests/test_integrals.py index eeb83c6b02..3d7062fecc 100644 --- a/tests/test_integrals.py +++ b/tests/test_integrals.py @@ -937,7 +937,7 @@ def test_bounce_quadrature(self, is_strong, quad, automorphism): to supress the derivative (as expected from chain rule), so we need to use the sin automorphism. We choose to apply that map to ``leggauss`` instead of ``_chebgauss1`` because the extra cosine term in ``_chebgauss1`` increases the - polynomial complexity of the integrand and supresses the derivative too strong + polynomial complexity of the integrand and suppresses the derivative too strong for a quadrature that already clusters near edge with density 1/(1−x²). This is why ``_chebgauss1`` required more nodes in this test, and in general would require more nodes for functions with more features. @@ -1190,7 +1190,7 @@ def dg_dz(z): points = (np.array(0, ndmin=4), np.array(2 * np.pi, ndmin=4)) argmin = 5.61719 h_min = h(argmin) - result = func(h(zeta), points, zeta, bounce.B, bounce._dB_dz) + result = func(h(zeta), points, zeta, bounce.B, bounce.dB_dz) assert result.shape == points[0].shape np.testing.assert_allclose(h_min, result, rtol=1e-3) From 9dac89e3741d46ef3ce7906573569edae8d23c99 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 24 Sep 2024 03:45:09 -0400 Subject: [PATCH 34/58] Document that Velasco's expression converges to zero --- desc/objectives/_neoclassical.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index d4ef0424cc..0535c03f9d 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -330,7 +330,15 @@ class GammaC(_Objective): Name of the objective function. Nemov : bool Whether to use the Γ_c as defined by Nemov et al. or Velasco et al. - Default is Nemov. + Default is Nemov. Set to ``False`` to use Velascos's. + + Note that Nemov's Γ_c converges to a finite nonzero value in the + infinity limit of the number of toroidal transits. + Velasco's expression is defined to be zero on irrational surfaces; + and therefore, the numerical computation will converge to zero as the + number of toroidal transits increases. This is mentioned to remind + users that an optimization using Velasco's metric should be evaluated by + measuring decrease in Γ_c at a fixed number of toroidal transits. """ From 7268ad8c316be1f4bc32f8d1df8a815c00b79a52 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 24 Sep 2024 14:20:05 -0400 Subject: [PATCH 35/58] Update after merging --- desc/compute/_neoclassical.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 2436924cfc..c2e9a6caba 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -284,14 +284,22 @@ def compute(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) points = bounce.points(data["pitch_inv"], num_well=num_well) - v_tau = bounce.integrate(d_v_tau, points, data["pitch_inv"], batch=batch) + v_tau = bounce.integrate(d_v_tau, data["pitch_inv"], points=points, batch=batch) gamma_c = jnp.arctan( safediv( bounce.integrate( - drift, points, data["pitch_inv"], data["cvdrift0"], batch=batch + drift, + data["pitch_inv"], + data["cvdrift0"], + points=points, + batch=batch, ), bounce.integrate( - drift, points, data["pitch_inv"], data["gbdrift"], batch=batch + drift, + data["pitch_inv"], + data["gbdrift"], + points=points, + batch=batch, ), ) ) @@ -396,6 +404,12 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): # ---------------------------------------------- # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ [ (1 − λ|B|/2)/√(1 − λ|B|) ∂|B|/∂ψ + √(1 − λ|B|) K ] / |B| + # Note that we rewrite equivalents of Nemov et. al's expression's using + # single valued maps of a physical coordinates. This avoids the computational + # issues of multivalued maps. It further enables use of more efficient methods, + # such as fast transforms and fixed computational grids throughout optimization, + # which are used in the ``Bounce2D`` operator on a developer branch. + def d_v_tau(B, pitch): return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) @@ -420,29 +434,29 @@ def compute(data): # τ is the bounce time, and I is defined in Nemov eq. 36. bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) points = bounce.points(data["pitch_inv"], num_well=num_well) - v_tau = bounce.integrate(d_v_tau, points, data["pitch_inv"], batch=batch) + v_tau = bounce.integrate(d_v_tau, data["pitch_inv"], points=points, batch=batch) gamma_c = jnp.arctan( safediv( bounce.integrate( drift1, - points, data["pitch_inv"], data["|grad(rho)|*kappa_g"], + points=points, batch=batch, ), ( bounce.integrate( drift2, - points, data["pitch_inv"], data["|B|_psi|v,p"], + points=points, batch=batch, ) + bounce.integrate( drift3, - points, data["pitch_inv"], data["K"], + points=points, batch=batch, quad=quad2, ) From 021b28a2a621e27ecdd1e2db3013f27e86a88d85 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 26 Sep 2024 16:37:57 -0400 Subject: [PATCH 36/58] Add chunk_size option --- desc/objectives/_neoclassical.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index c95f9ceefd..4054071ef8 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -339,8 +339,6 @@ class GammaC(_Objective): Default is to detect all wells, but due to limitations in JAX this option may consume more memory. Specifying a number that tightly upper bounds the number of wells will increase performance. - name : str, optional - Name of the objective function. Nemov : bool Whether to use the Γ_c as defined by Nemov et al. or Velasco et al. Default is Nemov. Set to ``False`` to use Velascos's. @@ -352,6 +350,19 @@ class GammaC(_Objective): number of toroidal transits increases. This is mentioned to remind users that an optimization using Velasco's metric should be evaluated by measuring decrease in Γ_c at a fixed number of toroidal transits. + name : str, optional + Name of the objective function. + jac_chunk_size : int , optional + Will calculate the Jacobian for this objective ``jac_chunk_size`` + columns at a time, instead of all at once. The memory usage of the + Jacobian calculation is roughly ``memory usage = m0 + m1*jac_chunk_size``: + the smaller the chunk size, the less memory the Jacobian calculation + will require (with some baseline memory usage). The time to compute the + Jacobian is roughly ``t=t0 +t1/jac_chunk_size``, so the larger the + ``jac_chunk_size``, the faster the calculation takes, at the cost of + requiring more memory. A ``jac_chunk_size`` of 1 corresponds to the least + memory intensive, but slowest method of calculating the Jacobian. + If None, it will use the largest size i.e ``obj.dim_x``. """ @@ -378,8 +389,9 @@ def __init__( num_pitch=64, batch=True, num_well=None, - name="Gamma_c", Nemov=True, + name="Gamma_c", + jac_chunk_size=None, ): if target is None and bounds is None: target = 0.0 @@ -417,6 +429,7 @@ def __init__( loss_function=loss_function, deriv_mode=deriv_mode, name=name, + jac_chunk_size=jac_chunk_size, ) def build(self, use_jit=True, verbose=1): From 779f57cdbea1a4c438d71948e1dc1ac08aeae082 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 29 Sep 2024 01:30:20 -0400 Subject: [PATCH 37/58] Make sure API is compatible for Gamma_d etc. --- desc/compute/_neoclassical.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 6c5d691964..ea0d4505b8 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -39,7 +39,7 @@ def _alpha_mean(f): return f.mean(axis=0) -def _compute(fun, interp_data, data, grid, num_pitch): +def _compute(fun, interp_data, data, grid, num_pitch, reduce=True): """Compute ``fun`` for each α and ρ value iteratively to reduce memory usage. Parameters @@ -52,6 +52,9 @@ def _compute(fun, interp_data, data, grid, num_pitch): Reshaped automatically. data : dict[str, jnp.ndarray] DESC data dict. + reduce : bool + Whether to compute mean over α and expand to grid. + Default is true. """ pitch_inv, pitch_inv_weight = get_pitch_inv_quad( @@ -71,7 +74,8 @@ def for_each_rho(x): interp_data = dict( zip(interp_data.keys(), Bounce1D.reshape_data(grid, *interp_data.values())) ) - return grid.expand(_alpha_mean(imap(for_each_rho, interp_data))) + out = imap(for_each_rho, interp_data) + return grid.expand(_alpha_mean(out)) if reduce else out @register_compute_fun( From 954b9ecce5f281455f7a515cc796867680709d93 Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 29 Sep 2024 16:12:52 -0400 Subject: [PATCH 38/58] address my review comment --- desc/compute/_field.py | 4 ++- desc/compute/_neoclassical.py | 56 ++++++++++++++--------------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/desc/compute/_field.py b/desc/compute/_field.py index d975411c68..6d8e0a5044 100644 --- a/desc/compute/_field.py +++ b/desc/compute/_field.py @@ -119,6 +119,7 @@ def _B_sup_zeta(params, transforms, profiles, data, **kwargs): data=["psi_r/sqrt(g)", "phi_z", "theta_PEST_t", "phi_t", "theta_PEST_z"], ) def _B_sup_phi(params, transforms, profiles, data, **kwargs): + # Written like this, independent of iota, to enable computing without integration. data["B^phi"] = data["psi_r/sqrt(g)"] * ( data["phi_z"] * data["theta_PEST_t"] - data["phi_t"] * data["theta_PEST_z"] ) @@ -359,7 +360,8 @@ def _B_R(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="B_phi", - label="B_{\\phi} = \\mathbf{B} \\cdot \\hat{\\phi}", + label="B_{\\phi} = \\mathbf{B} \\cdot \\hat{\\phi} " + "= \\mathbf{B} \\cdot R^{-1} \\mathbf{e}_{\\phi} |_{R, Z}", units="T", units_long="Tesla", description="Toroidal component of magnetic field in lab frame", diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index ea0d4505b8..c7786ccb41 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -27,6 +27,25 @@ from ..utils import cross, dot, safediv from .data_index import register_compute_fun +_bounce_doc = { + "quad": ( + "tuple[jnp.ndarray] : Quadrature points and weights for bounce integrals. " + "Default option is well tested." + ), + "num_quad": ( + "int : Resolution for quadrature of bounce integrals. " + "Default is 32. This option is ignored if given ``quad``." + ), + "num_pitch": "int : Resolution for quadrature over velocity coordinate.", + "num_well": ( + "int : Maximum number of wells to detect for each pitch and field line. " + "Default is to detect all wells, but due to limitations in JAX this option " + "may consume more memory. Specifying a number that tightly upper bounds " + "the number of wells will increase performance." + ), + "batch": "bool : Whether to vectorize part of the computation. Default is true.", +} + def _alpha_mean(f): """Simple mean over field lines. @@ -161,16 +180,7 @@ def _G_ra_fsa(data, transforms, profiles, **kwargs): + Bounce1D.required_names, resolution_requirement="z", source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - quad="jnp.ndarray : Optional, quadrature points and weights for bounce integrals.", - num_quad="int : Bounce integral resolution. Ignored if given ``quad``. Default 32.", - num_pitch="int : Resolution for quadrature over velocity coordinate. Default 50.", - num_well=( - "int : Maximum number of wells to detect for each pitch and field line. " - "Default is to detect all wells, but due to limitations in JAX this option " - "may consume more memory. Specifying a number that tightly upper bounds " - "the number of wells will increase performance." - ), - batch="bool : Whether to vectorize part of the computation. Default is true.", + **_bounce_doc, # Some notes on choosing the resolution hyperparameters: # The default settings were chosen such that the effective ripple profile on # the W7-X stellarator looks similar to the profile computed at higher resolution, @@ -286,16 +296,7 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): data=["min_tz |B|", "max_tz |B|", "cvdrift0", "gbdrift", ""] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - quad="jnp.ndarray : Optional, quadrature points and weights for bounce integrals.", - num_quad="int : Bounce integral resolution. Ignored if given ``quad``. Default 32.", - num_pitch="int : Resolution for quadrature over velocity coordinate. Default 64.", - num_well=( - "int : Maximum number of wells to detect for each pitch and field line. " - "Default is to detect all wells, but due to limitations in JAX this option " - "may consume more memory. Specifying a number that tightly upper bounds " - "the number of wells will increase performance. " - ), - batch="bool : Whether to vectorize part of the computation. Default is true.", + **_bounce_doc, ) @partial(jit, static_argnames=["num_quad", "num_pitch", "num_well", "batch"]) def _Gamma_c_Velasco(params, transforms, profiles, data, **kwargs): @@ -398,19 +399,8 @@ def Gamma_c_Velasco(data): ] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, - quad="jnp.ndarray : Optional, quadrature points and weights for strongly singular " - "bounce integrals.", - quad2="jnp.ndarray : Optional, quadrature points and weights for weakly singular " - "bounce integrals.", - num_quad="int : Bounce integral resolution. Ignored if given ``quad``. Default 32.", - num_pitch="int : Resolution for quadrature over velocity coordinate. Default 64.", - num_well=( - "int : Maximum number of wells to detect for each pitch and field line. " - "Default is to detect all wells, but due to limitations in JAX this option " - "may consume more memory. Specifying a number that tightly upper bounds " - "the number of wells will increase performance. " - ), - batch="bool : Whether to vectorize part of the computation. Default is true.", + **_bounce_doc, + quad2="Same as ``quad`` for the weak singular integrals in particular.", ) @partial(jit, static_argnames=["num_quad", "num_pitch", "num_well", "batch"]) def _Gamma_c(params, transforms, profiles, data, **kwargs): From bc5f6d30531ffb9760d3875971367346fcbe5e7f Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 29 Sep 2024 17:56:10 -0400 Subject: [PATCH 39/58] Resolve todo comment out smoothness --- desc/compute/_neoclassical.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index c7786ccb41..e5b8fb1841 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -439,12 +439,6 @@ def _Gamma_c(params, transforms, profiles, data, **kwargs): # ---------------------------------------------- # (|∇ρ| ‖e_α|ρ,ϕ‖)ᵢ ∫ dℓ [ (1 − λ|B|/2)/√(1 − λ|B|) ∂|B|/∂ψ + √(1 − λ|B|) K ] / |B| - # Note that we rewrite equivalents of Nemov et al.'s expression's using - # single valued maps of a physical coordinates. This avoids the computational - # issues of multivalued maps. It further enables use of more efficient methods, - # such as fast transforms and fixed computational grids throughout optimization, - # which are used in the ``Bounce2D`` operator on a developer branch. - def d_v_tau(B, pitch): return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) @@ -511,17 +505,25 @@ def Gamma_c(data): * data["pitch_inv weight"] ).sum(axis=-1) + # We rewrite equivalents of Nemov et al.'s expression's using single-valued + # maps of a physical coordinates. This avoids the computational issues of + # multivalued maps. It further enables use of more efficient methods, such as + # fast transforms and fixed computational grids throughout optimization, which + # are used in the ``Bounce2D`` operator on a developer branch. Also, Nemov + # assumes B^ϕ > 0 in some comments; this is not true in DESC, but the + # computations done here are invariant to the sign. + + # It is assumed the grid is sufficiently dense to reconstruct |B|, + # so anything smoother than |B| may be captured accurately as a single + # spline rather than splining each component. interp_data = { "|grad(rho)|*kappa_g": data["|grad(rho)|"] * data["kappa_g"], "|grad(rho)|*|e_alpha|r,p|": data["|grad(rho)|"] * data["|e_alpha|r,p|"], "|B|_psi|v,p": data["|B|_r|v,p"] / data["psi_r"], - # TODO: Confirm if K is smoother than individual components. - # If not, should spline separately. "K": data["iota_r"] * dot(cross(data["e^rho"], data["b"]), data["grad(phi)"]) - # Behaves as log derivative if one ignores the issue of an argument with units. - # Smoothness determined by + lower bound of argument ∂log(|B|²/B^ϕ)/∂ψ |B|. - # Note that Nemov assumes B^ϕ > 0; this is not true in DESC, but we account - # for that in this computation. + # Behaves as ∂log(|B|²/B^ϕ)/∂ψ |B| if one ignores the issue of a log argument + # with units. Smoothness determined by positive lower bound of log argument, + # and hence behaves as ∂log(|B|)/∂ψ |B| = ∂|B|/∂ψ. - (2 * data["|B|_r|v,p"] - data["|B|"] * data["B^phi_r|v,p"] / data["B^phi"]) / data["psi_r"], } From 21c2e6b88d2e40df863a6adb894c910e51d7d3f6 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 3 Oct 2024 00:08:55 -0400 Subject: [PATCH 40/58] fix method name in gammaC compute call --- desc/objectives/_neoclassical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index ccf2adb436..0d63b7b323 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -504,7 +504,7 @@ def compute(self, params, constants=None): constants["profiles"], ) # TODO: interpolate all deps to this grid with fft utilities from fourier bounce - grid = eq.get_rtz_grid( + grid = eq._get_rtz_grid( constants["rho"], constants["alpha"], constants["zeta"], From 31eb81656cce946b12472fb9c2bf0450941d5e88 Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Fri, 11 Oct 2024 15:32:18 -0400 Subject: [PATCH 41/58] Use linking number for linking current objective --- desc/objectives/_coils.py | 69 ++++++++++++++++++------------------ tests/test_objective_funs.py | 48 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index ee602203cf..9f607eeef6 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1368,46 +1368,26 @@ def compute(self, field_params=None, constants=None): class LinkingCurrent(_Objective): """Target the self-consistent poloidal linking current between the plasma and coils. + Assumes the coil topology does not change (ie the linking number with the plasma + is fixed). + Parameters ---------- eq : Equilibrium Equilibrium that will be optimized to satisfy the Objective. coil : CoilSet Coil(s) that are to be optimized. - target : float, ndarray, optional - Target value(s) of the objective. Only used if bounds is None. - Must be broadcastable to Objective.dim_f. If array, it has to - be flattened according to the number of inputs. - bounds : tuple of float, ndarray, optional - Lower and upper bounds on the objective. Overrides target. - Both bounds must be broadcastable to to Objective.dim_f - weight : float, ndarray, optional - Weighting to apply to the Objective, relative to other Objectives. - Must be broadcastable to to Objective.dim_f - normalize : bool, optional - Whether to compute the error in physical units or non-dimensionalize. - normalize_target : bool, optional - Whether target and bounds should be normalized before comparing to computed - values. If `normalize` is `True` and the target is in physical units, - this should also be set to True. - be set to True. - loss_function : {None, 'mean', 'min', 'max'}, optional - Loss function to apply to the objective values once computed. This loss function - is called on the raw compute value, before any shifting, scaling, or - normalization. Operates over all coils, not each individial coil. - deriv_mode : {"auto", "fwd", "rev"} - Specify how to compute jacobian matrix, either forward mode or reverse mode AD. - "auto" selects forward or reverse mode based on the size of the input and output - of the objective. Has no effect on self.grad or self.hess which always use - reverse mode and forward over reverse mode respectively. grid : Grid, optional Collocation grid containing the nodes to evaluate plasma current at. Defaults to ``LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)``. - name : str, optional - Name of the objective function. """ + __doc__ = __doc__.rstrip() + collect_docs( + target_default="``target=0``.", + bounds_default="``target=0``.", + ) + _scalar = True _units = "(A)" _print_value_fmt = "Linking current: " @@ -1424,6 +1404,7 @@ def __init__( loss_function=None, deriv_mode="auto", grid=None, + jac_chunk_size=None, name="linking current", ): if target is None and bounds is None: @@ -1438,6 +1419,7 @@ def __init__( normalize_target=normalize_target, loss_function=loss_function, deriv_mode=deriv_mode, + jac_chunk_size=jac_chunk_size, name=name, ) @@ -1468,16 +1450,34 @@ def build(self, use_jit=True, verbose=1): all_params = tree_map(lambda dim: np.arange(dim), coil.dimensions) current_params = tree_map(lambda idx: {"current": idx}, True) + # indices of coil currents self._indices = tree_leaves(broadcast_tree(current_params, all_params)) self._num_coils = coil.num_coils profiles = get_profiles(self._data_keys, obj=eq, grid=grid) transforms = get_transforms(self._data_keys, obj=eq, grid=grid) + # compute linking number of coils with plasma. To do this we add a fake "coil" + # along the magnetic axis and compute the linking number of that coilset + from desc.coils import FourierRZCoil, MixedCoilSet + + axis_coil = FourierRZCoil( + 1.0, + eq.axis.R_n, + eq.axis.Z_n, + eq.axis.R_basis.modes[:, 2], + eq.axis.Z_basis.modes[:, 2], + eq.axis.NFP, + ) + dummy_coilset = MixedCoilSet(axis_coil, coil) + # linking number for coils with axis + link = np.round(dummy_coilset._compute_linking_number())[0, 1:] + self._constants = { "profiles": profiles, "transforms": transforms, "quad_weights": 1.0, + "link": link, } if self._normalize: @@ -1517,14 +1517,13 @@ def compute(self, eq_params, coil_params, constants=None): profiles=constants["profiles"], ) eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 - coil_linking_current = self._num_coils * jnp.mean( - jnp.concatenate( - [ - jnp.atleast_1d(param[idx]) - for param, idx in zip(tree_leaves(coil_params), self._indices) - ] - ) + coil_currents = jnp.concatenate( + [ + jnp.atleast_1d(param[idx]) + for param, idx in zip(tree_leaves(coil_params), self._indices) + ] ) + coil_linking_current = jnp.sum(constants["link"] * coil_currents) return eq_linking_current - coil_linking_current diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index fccfaae389..9ed01e0eae 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -56,6 +56,7 @@ HeatingPowerISS04, Isodynamicity, LinearObjectiveFromUser, + LinkingCurrent, MagneticWell, MeanCurvature, MercierStability, @@ -1297,6 +1298,18 @@ def test_signed_plasma_vessel_distance(self): assert abs(d.max() - (-a_s)) < 1e-14 assert abs(d.min() - (-a_s)) < grid.spacing[0, 1] * a_s + @pytest.mark.unit + def test_linking_current(self): + """Test calculation of signed linking current from coils to plasma.""" + eq = Equilibrium() + G = eq.compute("G", grid=LinearGrid(rho=1.0))["G"] * 2 * jnp.pi / mu_0 + coil = FourierPlanarCoil(current=G / 8, center=[10, 1, 0]) + coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) + obj = LinkingCurrent(eq, coilset) + obj.build() + f = obj.compute(eq.params_dict, coilset.params_dict) + np.testing.assert_allclose(f, 0) + @pytest.mark.regression def test_derivative_modes(): @@ -2271,6 +2284,7 @@ class TestComputeScalarResolution: FusionPower, GenericObjective, HeatingPowerISS04, + LinkingCurrent, Omnigenity, PlasmaCoilSetMinDistance, PlasmaVesselDistance, @@ -2650,6 +2664,26 @@ def test_compute_scalar_resolution_coils(self, objective): f[i] = obj.compute_scalar(obj.x()) np.testing.assert_allclose(f, f[-1], rtol=1e-2, atol=1e-12) + @pytest.mark.unit + def test_compute_scalar_resolution_linking_current(self): + """LinkingCurrent.""" + coil = FourierPlanarCoil(center=[10, 1, 0]) + eq = Equilibrium() + coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) + f = np.zeros_like(self.res_array, dtype=float) + for i, res in enumerate(self.res_array): + obj = ObjectiveFunction( + LinkingCurrent( + eq, + coilset, + grid=LinearGrid(M=int(eq.M_grid * res), N=int(eq.N_grid * res)), + ), + use_jit=False, + ) + obj.build(verbose=0) + f[i] = obj.compute_scalar(obj.x()) + np.testing.assert_allclose(f, f[-1], rtol=1e-2, atol=1e-12) + class TestObjectiveNaNGrad: """Make sure reverse mode AD works correctly for all objectives.""" @@ -2677,6 +2711,7 @@ class TestObjectiveNaNGrad: ForceBalanceAnisotropic, FusionPower, HeatingPowerISS04, + LinkingCurrent, Omnigenity, PlasmaCoilSetMinDistance, PlasmaVesselDistance, @@ -2919,6 +2954,17 @@ def test_objective_no_nangrad_ballooning(self): g = obj.grad(obj.x()) assert not np.any(np.isnan(g)) + @pytest.mark.unit + def test_objective_no_nangrad_linking_current(self): + """LinkingCurrent.""" + coil = FourierPlanarCoil(center=[10, 1, 0]) + coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) + eq = Equilibrium() + obj = ObjectiveFunction(LinkingCurrent(eq, coilset)) + obj.build() + g = obj.grad(obj.x()) + assert not np.any(np.isnan(g)) + @pytest.mark.unit def test_asymmetric_normalization(): @@ -2943,6 +2989,7 @@ def test_asymmetric_normalization(): assert np.all(np.isfinite(val)) +@pytest.mark.unit def test_objective_print_widths(): """Test that the objective's name is shorter than max.""" subclasses = _Objective.__subclasses__() @@ -2971,6 +3018,7 @@ def test_objective_print_widths(): ) +@pytest.mark.unit def test_objective_docstring(): """Test that the objective docstring and collect_docs are consistent.""" objective_docs = _Objective.__doc__.rstrip() From 8db268d84c48f44a05a2dc9c9b2cc10a5d909927 Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 15 Oct 2024 13:45:52 -0400 Subject: [PATCH 42/58] fix test_objective_docstring --- desc/objectives/objective_funs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/desc/objectives/objective_funs.py b/desc/objectives/objective_funs.py index 28d7530d1e..9f0ca3945f 100644 --- a/desc/objectives/objective_funs.py +++ b/desc/objectives/objective_funs.py @@ -1016,10 +1016,10 @@ class _Objective(IOAble, ABC): Must be broadcastable to Objective.dim_f. bounds : tuple of {float, ndarray}, optional Lower and upper bounds on the objective. Overrides target. - Both bounds must be broadcastable to to Objective.dim_f + Both bounds must be broadcastable to Objective.dim_f weight : {float, ndarray}, optional Weighting to apply to the Objective, relative to other Objectives. - Must be broadcastable to to Objective.dim_f + Must be broadcastable to Objective.dim_f normalize : bool, optional Whether to compute the error in physical units or non-dimensionalize. normalize_target : bool, optional @@ -1031,7 +1031,7 @@ class _Objective(IOAble, ABC): is called on the raw compute value, before any shifting, scaling, or normalization. deriv_mode : {"auto", "fwd", "rev"} - Specify how to compute jacobian matrix, either forward mode or reverse mode AD. + Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. "auto" selects forward or reverse mode based on the size of the input and output of the objective. Has no effect on self.grad or self.hess which always use reverse mode and forward over reverse mode respectively. From f5abd60cbc56dab4bd5a8c9f18378725f5cb769e Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 16 Oct 2024 15:46:42 -0400 Subject: [PATCH 43/58] Add option to fix eq for LinkingCurrent --- desc/objectives/_coils.py | 59 ++++++++++++++++++++++++++++----------- tests/test_optimizer.py | 4 +-- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 21ebbf6e45..59211428d8 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1380,7 +1380,9 @@ class LinkingCurrent(_Objective): grid : Grid, optional Collocation grid containing the nodes to evaluate plasma current at. Defaults to ``LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym)``. - + eq_fixed : bool + Whether the equilibrium is assumed fixed (should be true for stage 2, false + for single stage). """ __doc__ = __doc__.rstrip() + collect_docs( @@ -1404,14 +1406,20 @@ def __init__( loss_function=None, deriv_mode="auto", grid=None, + eq_fixed=False, jac_chunk_size=None, name="linking current", ): if target is None and bounds is None: target = 0 self._grid = grid + self._eq_fixed = eq_fixed + self._linear = eq_fixed + self._eq = eq + self._coil = coil + super().__init__( - things=[eq, coil], + things=[coil] if eq_fixed else [coil, eq], target=target, bounds=bounds, weight=weight, @@ -1434,8 +1442,8 @@ def build(self, use_jit=True, verbose=1): Level of output. """ - eq = self.things[0] - coil = self.things[1] + eq = self._eq + coil = self._coil grid = self._grid or LinearGrid( M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym ) @@ -1474,12 +1482,24 @@ def build(self, use_jit=True, verbose=1): link = np.round(dummy_coilset._compute_linking_number())[0, 1:] self._constants = { - "profiles": profiles, - "transforms": transforms, "quad_weights": 1.0, "link": link, } + if self._eq_fixed: + data = compute_fun( + "desc.equilibrium.equilibrium.Equilibrium", + self._data_keys, + params=eq.params_dict, + transforms=transforms, + profiles=profiles, + ) + eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 + self._constants["eq_linking_current"] = (eq_linking_current,) + else: + self._constants["profiles"] = profiles + self._constants["transforms"] = transforms + if self._normalize: params = tree_leaves( coil.params_dict, is_leaf=lambda x: isinstance(x, dict) @@ -1488,15 +1508,16 @@ def build(self, use_jit=True, verbose=1): super().build(use_jit=use_jit, verbose=verbose) - def compute(self, eq_params, coil_params, constants=None): + def compute(self, coil_params, eq_params=None, constants=None): """Compute linking current error. Parameters ---------- - eq_params : dict - Dictionary of equilibrium degrees of freedom, eg ``Equilibrium.params_dict`` coil_params : dict Dictionary of coilset degrees of freedom, eg ``CoilSet.params_dict`` + eq_params : dict + Dictionary of equilibrium degrees of freedom, eg ``Equilibrium.params_dict`` + Only required if eq_fixed=False. constants : dict Dictionary of constant data, eg transforms, profiles etc. Defaults to self._constants. @@ -1509,14 +1530,18 @@ def compute(self, eq_params, coil_params, constants=None): """ if constants is None: constants = self.constants - data = compute_fun( - "desc.equilibrium.equilibrium.Equilibrium", - self._data_keys, - params=eq_params, - transforms=constants["transforms"], - profiles=constants["profiles"], - ) - eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 + if self._eq_fixed: + eq_linking_current = constants["eq_linking_current"] + else: + data = compute_fun( + "desc.equilibrium.equilibrium.Equilibrium", + self._data_keys, + params=eq_params, + transforms=constants["transforms"], + profiles=constants["profiles"], + ) + eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 + coil_currents = jnp.concatenate( [ jnp.atleast_1d(param[idx]) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 4f0cce0e0b..4b07aa9ccd 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -33,9 +33,9 @@ FixParameters, FixPressure, FixPsi, - FixSumCoilCurrent, ForceBalance, GenericObjective, + LinkingCurrent, MagneticWell, MeanCurvature, ObjectiveFunction, @@ -1367,7 +1367,7 @@ def test_optimize_coil_currents(DummyCoilSet): coil.current = current / coils.num_coils objective = ObjectiveFunction(QuadraticFlux(eq=eq, field=coils, vacuum=True)) - constraints = FixSumCoilCurrent(coils) + constraints = LinkingCurrent(eq, coils, eq_fixed=True) optimizer = Optimizer("lsq-exact") [coils_opt], _ = optimizer.optimize( things=coils, From d5f447331794a28bb30ac651d95b30535df81285 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 17 Oct 2024 00:53:11 -0400 Subject: [PATCH 44/58] Update comment about convergence to zero with warning to use adaptive integration --- desc/objectives/_neoclassical.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 0d63b7b323..7f537143ed 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -346,11 +346,12 @@ class GammaC(_Objective): Note that Nemov's Γ_c converges to a finite nonzero value in the infinity limit of the number of toroidal transits. - Velasco's expression is defined to be zero on irrational surfaces; - and therefore, the numerical computation will converge to zero as the - number of toroidal transits increases. This is mentioned to remind - users that an optimization using Velasco's metric should be evaluated by - measuring decrease in Γ_c at a fixed number of toroidal transits. + Velasco's expression has a secular term that will drive the result + to zero as the number of toroidal transits increases unless the + secular term is averaged out from all the singular integrals. + Therefore, an optimization using Velasco's metric should be evaluated by + measuring decrease in Γ_c at a fixed number of toroidal transits until + unless an adaptive quadrature is used. name : str, optional Name of the objective function. jac_chunk_size : int , optional From 5be0f27b89fcd8679a2ecb4e5fd255bd63306cda Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 20 Oct 2024 11:04:15 -0400 Subject: [PATCH 45/58] Merging ripple --- desc/compute/_neoclassical.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 8da2ceaffe..90ae5f0a28 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -293,7 +293,7 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): transforms={"grid": []}, profiles=[], coordinates="r", - data=["min_tz |B|", "max_tz |B|", "cvdrift0", "gbdrift", ""] + data=["min_tz |B|", "max_tz |B|", "cvdrift0", "gbdrift", "fieldline length"] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, **_bounce_doc, @@ -361,7 +361,7 @@ def Gamma_c_Velasco(data): * _compute( Gamma_c_Velasco, interp_data, data, grid, kwargs.get("num_pitch", 64) ) - / data[""] + / data["fieldline length"] ) return data @@ -388,7 +388,6 @@ def Gamma_c_Velasco(data): "B^phi_r|v,p", "b", "|B|_r|v,p", - "", "iota_r", "grad(phi)", "e^rho", @@ -396,6 +395,7 @@ def Gamma_c_Velasco(data): "|e_alpha|r,p|", "kappa_g", "psi_r", + "fieldline length", ] + Bounce1D.required_names, source_grid_requirement={"coordinates": "raz", "is_meshgrid": True}, @@ -531,6 +531,6 @@ def Gamma_c(data): jnp.pi / (8 * 2**0.5) * _compute(Gamma_c, interp_data, data, grid, kwargs.get("num_pitch", 64)) - / data[""] + / data["fieldline length"] ) return data From ed450b8bef3802ac0ce0ec63f15a0199c1255bca Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 13 Nov 2024 16:08:06 -0500 Subject: [PATCH 46/58] Add handling for linking currents from virtual coils --- desc/coils.py | 25 +++++++++++++++++ desc/objectives/_coils.py | 3 +- tests/test_objective_funs.py | 53 ++++++++++++++++++++++++++++++++---- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/desc/coils.py b/desc/coils.py index ea0da348a5..50fd4d37b7 100644 --- a/desc/coils.py +++ b/desc/coils.py @@ -1300,6 +1300,15 @@ def current(self, new): for coil, cur in zip(self.coils, new): coil.current = cur + def _all_currents(self, currents=None): + """Return an array of all the currents (including those in virtual coils).""" + if currents is None: + currents = self.current + currents = jnp.asarray(currents) + if self.sym: + currents = jnp.concatenate([currents, -1 * currents[::-1]]) + return jnp.tile(currents, self.NFP) + def _make_arraylike(self, x): if isinstance(x, dict): x = [x] * len(self) @@ -2318,6 +2327,22 @@ def num_coils(self): """int: Number of coils.""" return sum([c.num_coils for c in self]) + def _all_currents(self, currents=None): + """Return an array of all the currents (including those in virtual coils).""" + if currents is None: + currents = jnp.array(flatten_list(self.current)) + all_currents = [] + i = 0 + for coil in self.coils: + if isinstance(coil, CoilSet): + curr = currents[i : i + len(coil)] + all_currents += [coil._all_currents(curr)] + i += len(coil) + else: + all_currents += [jnp.atleast_1d(currents[i])] + i += 1 + return jnp.concatenate(all_currents) + def compute( self, names, diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 929ba25654..b8aa701959 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1599,7 +1599,7 @@ def build(self, use_jit=True, verbose=1): eq.axis.Z_basis.modes[:, 2], eq.axis.NFP, ) - dummy_coilset = MixedCoilSet(axis_coil, coil) + dummy_coilset = MixedCoilSet(axis_coil, coil, check_intersection=False) # linking number for coils with axis link = np.round(dummy_coilset._compute_linking_number())[0, 1:] @@ -1670,6 +1670,7 @@ def compute(self, coil_params, eq_params=None, constants=None): for param, idx in zip(tree_leaves(coil_params), self._indices) ] ) + coil_currents = self.things[0]._all_currents(coil_currents) coil_linking_current = jnp.sum(constants["link"] * coil_currents) return eq_linking_current - coil_linking_current diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 982ae0d5f3..5e364083eb 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -1327,14 +1327,57 @@ def test_signed_plasma_vessel_distance(self): def test_linking_current(self): """Test calculation of signed linking current from coils to plasma.""" eq = Equilibrium() - G = eq.compute("G", grid=LinearGrid(rho=1.0))["G"] * 2 * jnp.pi / mu_0 - coil = FourierPlanarCoil(current=G / 8, center=[10, 1, 0]) - coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) - obj = LinkingCurrent(eq, coilset) + G = eq.compute("G", grid=LinearGrid(rho=1.0))["G"][0] * 2 * jnp.pi / mu_0 + c = G / 8 + coil1 = FourierPlanarCoil(current=1.5 * c, center=[10, 1, 0]) + coil2 = FourierPlanarCoil(current=0.5 * c, center=[10, 2, 0]) + # explicit symmetry coils + coilset1 = CoilSet.from_symmetry((coil1, coil2), NFP=2, sym=True) + expected_currents = [ + c * 1.5, # these are the 2 actual coils, with different currents + c * 0.5, + -c * 0.5, # these are the stellarator symmetric ones in first field period + -c * 1.5, + c * 1.5, # these next 4 are the ones from the 2nd field period + c * 0.5, + -c * 0.5, + -c * 1.5, + ] + np.testing.assert_allclose(coilset1._all_currents(), expected_currents) + obj = LinkingCurrent(eq, coilset1) obj.build() - f = obj.compute(eq.params_dict, coilset.params_dict) + f = obj.compute(coilset1.params_dict, eq.params_dict) np.testing.assert_allclose(f, 0) + # same with virtual coils + coilset2 = CoilSet(coil1, coil2, NFP=2, sym=True) + np.testing.assert_allclose(coilset2._all_currents(), expected_currents) + obj = LinkingCurrent(eq, coilset2) + obj.build() + f = obj.compute(coilset2.params_dict, eq.params_dict) + np.testing.assert_allclose(f, 0) + + # both coilsets together. These have overlapping coils but it doesn't + # affect the linking number + coilset3 = MixedCoilSet(coilset1, coilset2, check_intersection=False) + np.testing.assert_allclose( + coilset3._all_currents(), expected_currents + expected_currents + ) + obj = LinkingCurrent(eq, coilset3) + obj.build() + f = obj.compute(coilset3.params_dict, eq.params_dict) + np.testing.assert_allclose(f, -G) # coils provide 2G so error is -G + + # CoilSet + 1 extra coil + coilset4 = MixedCoilSet(coilset1, coil2, check_intersection=False) + np.testing.assert_allclose( + coilset4._all_currents(), expected_currents + [0.5 * G / 8] + ) + obj = LinkingCurrent(eq, coilset4) + obj.build() + f = obj.compute(coilset4.params_dict, eq.params_dict) + np.testing.assert_allclose(f, -0.5 * G / 8) + @pytest.mark.regression def test_derivative_modes(): From ea8172168d947d27e403eff066b40cd44407e29f Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 13 Nov 2024 18:40:04 -0500 Subject: [PATCH 47/58] Fix test --- desc/objectives/_coils.py | 4 ++-- tests/test_optimizer.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index b8aa701959..8d821623af 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1514,7 +1514,7 @@ class LinkingCurrent(_Objective): _scalar = True _units = "(A)" - _print_value_fmt = "Linking current: " + _print_value_fmt = "Linking current error: " def __init__( self, @@ -1617,7 +1617,7 @@ def build(self, use_jit=True, verbose=1): profiles=profiles, ) eq_linking_current = 2 * jnp.pi * data["G"][0] / mu_0 - self._constants["eq_linking_current"] = (eq_linking_current,) + self._constants["eq_linking_current"] = eq_linking_current else: self._constants["profiles"] = profiles self._constants["transforms"] = transforms diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 4b07aa9ccd..7d6b912cf8 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1378,6 +1378,6 @@ def test_optimize_coil_currents(DummyCoilSet): ) # check that optimized coil currents changed by more than 15% from initial values np.testing.assert_array_less( - np.asarray(coils.current) * 0.15, - np.abs(np.asarray(coils_opt.current) - np.asarray(coils.current)), + np.asarray(coils.current).mean() * 0.15, + np.abs(np.asarray(coils_opt.current) - np.asarray(coils.current)).mean(), ) From 6b102be52c00c1f29573bc85aa2d7e100bc55254 Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 13 Nov 2024 18:45:34 -0500 Subject: [PATCH 48/58] Update changelog --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 931e5621d4..8b0d53b12e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Changelog ========= New Features +- Add ``desc.objectives.LinkingCurrent`` for ensuring that coils in a stage 2 or single stage optimization provide the required linking current for a given equilibrium. - Add ``from_input_file`` method to ``Equilibrium`` class to generate an ``Equilibrium`` object with boundary, profiles, resolution and flux specified in a given DESC or VMEC input file @@ -266,7 +267,7 @@ optimization. Set to False by default. non-singular, non-degenerate) coordinate mappings for initial guesses. This is applied automatically when creating a new `Equilibrium` if the default initial guess of scaling the boundary surface produces self-intersecting surfaces. This can be disabled by -passing `ensure_nested=False` when constructing the `Equilibrum`. +passing `ensure_nested=False` when constructing the `Equilibrium`. - Adds `loss_function` argument to all `Objective`s for applying one of min/max/mean to objective function values (for targeting the average value of a profile, etc). - `Equilibrium.get_profile` now allows user to choose a profile type (power series, spline, etc) @@ -418,7 +419,7 @@ Breaking changes - Renames ``theta_sfl`` to ``theta_PEST`` in compute functions to avoid confusion with other straight field line coordinate systems. - Makes plotting kwargs a bit more uniform. ``zeta``, ``nzeta``, ``nphi`` have all been -superceded by ``phi`` which can be an integer for equally spaced angles or a float or +superseded by ``phi`` which can be an integer for equally spaced angles or a float or array of float to specify angles manually. Bug fixes @@ -488,7 +489,7 @@ the future all quantities should evaluate correctly at the magnetic axis. Note t evaluating quantities at the axis generally requires higher order derivatives and so can be much more expensive than evaluating at nonsingular points, so during optimization it is not recommended to include a grid point at the axis. Generally a small finite value -such as ``rho = 1e-6`` will avoid the singuarlity with a negligible loss in accuracy for +such as ``rho = 1e-6`` will avoid the singularity with a negligible loss in accuracy for analytic quantities. - Adds new optimizers ``fmin-auglag`` and ``lsq-auglag`` for performing constrained optimization using the augmented Lagrangian method. These generally perform much better @@ -504,7 +505,7 @@ the existing methods ``compute_theta_coordinates`` and ``compute_flux_coordinate but allows mapping between arbitrary coordinates. - Adds calculation of $\nabla \mathbf{B}$ tensor and corresponding $L_{\nabla B}$ metric - Adds objective ``BScaleLength`` for penalizing strong magnetic field curvature. -- Adds objective ``ObjectiveFromUser`` for wrapping an arbitary user defined function. +- Adds objective ``ObjectiveFromUser`` for wrapping an arbitrary user defined function. - Adds utilities ``desc.grid.find_least_rational_surfaces`` and ``desc.grid.find_most_rational_surfaces`` for finding the least/most rational surfaces for a given rotational transform profile. @@ -1040,7 +1041,7 @@ New Features: - Updates `Equilibrium` to make creating them more straightforward. - Instead of a dictionary of arrays and values, init method now takes individual arguments. These can either be objects of the - correct type (ie `Surface` objects for boundary condiitons, + correct type (ie `Surface` objects for boundary conditions, `Profile` for pressure and iota etc,) or ndarrays which will get parsed into objects of the correct type (for backwards compatibility) @@ -1324,7 +1325,7 @@ Major changes: indexing, where only M+1 points were used instead of the correct 2\*M+1 - Rotated concentric grids by 2pi/3M to avoid symmetry plane at - theta=0,pi. Previously, for stellarator symmetic cases, the nodes at + theta=0,pi. Previously, for stellarator symmetric cases, the nodes at theta=0 did not contribute to helical force balance. - Added [L\_grid]{.title-ref} parameter to specify radial resolution of grid nodes directly and making the API more consistent. @@ -1490,7 +1491,7 @@ saved, and objectives getting compiled more often than necessary Major Changes: -- Changes to Equilibium/EquilibriaFamily: +- Changes to Equilibrium/EquilibriaFamily: - general switching to using properties rather than direct attributes when referencing things (ie, `eq.foo`, not `eq._foo`). This allows getter methods to have safeguards if From 8a1958a8df9e60afcebc12a6d12989d5190ec9fd Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 13 Nov 2024 18:57:16 -0500 Subject: [PATCH 49/58] Update changelog action version --- .github/workflows/changelog_update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/changelog_update.yml b/.github/workflows/changelog_update.yml index c3b1691be6..78ca014c42 100644 --- a/.github/workflows/changelog_update.yml +++ b/.github/workflows/changelog_update.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip_changelog') && env.has_changes == 'true'}} - uses: danieljimeneznz/ensure-files-changed@v4.1.0 + uses: danieljimeneznz/ensure-files-changed@v4.1.1 with: require-changes-to: | CHANGELOG.md From 27cfacf57d485dd63f5791f891c83fe6655657e6 Mon Sep 17 00:00:00 2001 From: Greta Hibbard Date: Mon, 2 Dec 2024 19:14:50 -0500 Subject: [PATCH 50/58] fixing #1288, adding opt tests for Gamma_c --- desc/objectives/_neoclassical.py | 312 +++++++++++++++++++++++++++++-- tests/test_neoclassical.py | 123 ++++++++++++ 2 files changed, 415 insertions(+), 20 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 7f537143ed..24fc61e016 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -281,7 +281,8 @@ class GammaC(_Objective): https://doi.org/10.1063/1.2912456. Equation 61. - A model for the fast evaluation of prompt losses of energetic ions in stellarators. + A model for the fast evaluation of prompt losses of energetic ions in + stellarators. J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. https://doi.org/10.1088/1741-4326/ac2994. Equation 16. @@ -346,12 +347,11 @@ class GammaC(_Objective): Note that Nemov's Γ_c converges to a finite nonzero value in the infinity limit of the number of toroidal transits. - Velasco's expression has a secular term that will drive the result - to zero as the number of toroidal transits increases unless the - secular term is averaged out from all the singular integrals. - Therefore, an optimization using Velasco's metric should be evaluated by - measuring decrease in Γ_c at a fixed number of toroidal transits until - unless an adaptive quadrature is used. + Velasco's expression is defined to be zero on irrational surfaces; + and therefore, the numerical computation will converge to zero as the + number of toroidal transits increases. This is mentioned to remind + users that an optimization using Velasco's metric should be evaluated by + measuring decrease in Γ_c at a fixed number of toroidal transits. name : str, optional Name of the objective function. jac_chunk_size : int , optional @@ -400,13 +400,14 @@ def __init__( rho, alpha = np.atleast_1d(rho, alpha) self._dim_f = rho.size + + zeta = np.linspace(0, 2 * np.pi * num_transit, knots_per_transit * num_transit) + + grid = LinearGrid(rho=rho, theta=alpha, zeta=zeta) + self._constants = { + "grid": grid, "quad_weights": 1, - "rho": rho, - "alpha": alpha, - "zeta": np.linspace( - 0, 2 * np.pi * num_transit, knots_per_transit * num_transit - ), } self._hyperparameters = { "num_quad": num_quad, @@ -414,6 +415,7 @@ def __init__( "batch": batch, "num_well": num_well, } + self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] if Nemov: self._key = "Gamma_c" @@ -421,6 +423,8 @@ def __init__( else: self._key = "Gamma_c Velasco" + self._constants["quad2"] = chebgauss2(num_quad) + super().__init__( things=eq, target=target, @@ -434,7 +438,7 @@ def __init__( jac_chunk_size=jac_chunk_size, ) - def build(self, use_jit=True, verbose=1): + def build(self, use_jit=True, verbose=1, constants=None): """Build constant arrays. Parameters @@ -445,16 +449,24 @@ def build(self, use_jit=True, verbose=1): Level of output. """ + if constants is None: + constants = self.constants + rtzgrid = constants["grid"] + eq = self.things[0] self._grid_1dr = LinearGrid( - rho=self._constants["rho"], M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym + rho=rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + sym=eq.sym, ) self._constants["quad"] = get_quadrature( leggauss(self._hyperparameters.pop("num_quad")), (automorphism_sin, grad_automorphism_sin), ) self._target, self._bounds = _parse_callable_target_bounds( - self._target, self._bounds, self._constants["rho"] + self._target, self._bounds, rtzgrid.nodes[rtzgrid.unique_rho_idx, 0] ) timer = Timer() @@ -496,7 +508,9 @@ def compute(self, params, constants=None): if constants is None: constants = self.constants eq = self.things[0] - # TODO: compute all deps of gamma here + + rtzgrid = constants["grid"] + data = compute_fun( eq, self._keys_1dr, @@ -504,15 +518,273 @@ def compute(self, params, constants=None): constants["transforms_1dr"], constants["profiles"], ) - # TODO: interpolate all deps to this grid with fft utilities from fourier bounce + grid = eq._get_rtz_grid( - constants["rho"], - constants["alpha"], - constants["zeta"], + rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], + rtzgrid.nodes[rtzgrid.unique_theta_idx, 1], + rtzgrid.nodes[rtzgrid.unique_zeta_idx, 2], coordinates="raz", iota=self._grid_1dr.compress(data["iota"]), params=params, ) + + data = { + key: grid.copy_data_from_other(data[key], self._grid_1dr) + for key in self._keys_1dr + } + quad2 = {} + if "quad2" in constants: + quad2["quad2"] = constants["quad2"] + data = compute_fun( + eq, + self._key, + params, + get_transforms(self._key, eq, grid, jitable=True), + constants["profiles"], + data=data, + quad=constants["quad"], + **quad2, + **self._hyperparameters, + ) + return grid.compress(data[self._key]) + + +class Gammad(_Objective): + """Γ_d is a proxy for measuring energetic ion confinement. + + References + ---------- + A model for the fast evaluation of prompt losses of energetic ions in stellarators. + J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. + https://doi.org/10.1088/1741-4326/ac2994. + Equation 22. + + Parameters + ---------- + eq : Equilibrium + Equilibrium that will be optimized to satisfy the Objective. + target : {float, ndarray, callable}, optional + Target value(s) of the objective. Only used if bounds is None. + Must be broadcastable to Objective.dim_f. If a callable, should take a + single argument ``rho`` and return the desired value of the profile at those + locations. Defaults to 0. + bounds : tuple of {float, ndarray, callable}, optional + Lower and upper bounds on the objective. Overrides target. + Both bounds must be broadcastable to Objective.dim_f. + If a callable, each should take a single argument ``rho`` and return the + desired bound (lower or upper) of the profile at those locations. + weight : {float, ndarray}, optional + Weighting to apply to the Objective, relative to other Objectives. + Must be broadcastable to Objective.dim_f + normalize : bool, optional + Whether to compute the error in physical units or non-dimensionalize. + normalize_target : bool, optional + Whether target and bounds should be normalized before comparing to computed + values. If `normalize` is `True` and the target is in physical units, + this should also be set to True. + loss_function : {None, 'mean', 'min', 'max'}, optional + Loss function to apply to the objective values once computed. This loss function + is called on the raw compute value, before any shifting, scaling, or + normalization. + deriv_mode : {"auto", "fwd", "rev"} + Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. + "auto" selects forward or reverse mode based on the size of the input and output + of the objective. Has no effect on self.grad or self.hess which always use + reverse mode and forward over reverse mode respectively. + rho : ndarray + Unique coordinate values specifying flux surfaces to compute on. + alpha : ndarray + Unique coordinate values specifying field line labels to compute on. + num_transit : int + Number of toroidal transits to follow field line. + For axisymmetric devices, one poloidal transit is sufficient. Otherwise, + more transits will give more accurate result, with diminishing returns. + knots_per_transit : int + Number of points per toroidal transit at which to sample data along field + line. Default is 100. + num_quad : int + Resolution for quadrature of bounce integrals. Default is 32. + num_pitch : int + Resolution for quadrature over velocity coordinate. Default is 64. + batch : bool + Whether to vectorize part of the computation. Default is true. + num_well : int + Maximum number of wells to detect for each pitch and field line. + Default is to detect all wells, but due to limitations in JAX this option + may consume more memory. Specifying a number that tightly upper bounds + the number of wells will increase performance. + name : str, optional + Name of the objective function. + jac_chunk_size : int , optional + Will calculate the Jacobian for this objective ``jac_chunk_size`` + columns at a time, instead of all at once. The memory usage of the + Jacobian calculation is roughly ``memory usage = m0 + m1*jac_chunk_size``: + the smaller the chunk size, the less memory the Jacobian calculation + will require (with some baseline memory usage). The time to compute the + Jacobian is roughly ``t=t0 +t1/jac_chunk_size``, so the larger the + ``jac_chunk_size``, the faster the calculation takes, at the cost of + requiring more memory. A ``jac_chunk_size`` of 1 corresponds to the least + memory intensive, but slowest method of calculating the Jacobian. + If None, it will use the largest size i.e ``obj.dim_x``. + + """ + + _coordinates = "r" + _units = "~" + _print_value_fmt = "Γ_d: " + + def __init__( + self, + eq, + target=None, + bounds=None, + weight=1, + normalize=True, + normalize_target=True, + loss_function=None, + deriv_mode="auto", + rho=np.linspace(0.5, 1, 3), + alpha=np.array([0]), + *, + num_transit=10, + knots_per_transit=100, + num_quad=32, + num_pitch=64, + batch=True, + num_well=None, + Nemov=True, + name="Gamma_c", + jac_chunk_size=None, + ): + + if target is None and bounds is None: + target = 0.0 + + rho, alpha = np.atleast_1d(rho, alpha) + self._dim_f = rho.size + + zeta = np.linspace(0, 2 * np.pi * num_transit, knots_per_transit * num_transit) + + grid = LinearGrid(rho=rho, theta=alpha, zeta=zeta) + + self._constants = { + "grid": grid, + "quad_weights": 1, + } + self._hyperparameters = { + "num_quad": num_quad, + "num_pitch": num_pitch, + "batch": batch, + "num_well": num_well, + } + self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] + + self._key = "Gamma_d" + self._constants["quad2"] = chebgauss2(num_quad) + + super().__init__( + things=eq, + target=target, + bounds=bounds, + weight=weight, + normalize=normalize, + normalize_target=normalize_target, + loss_function=loss_function, + deriv_mode=deriv_mode, + name=name, + jac_chunk_size=jac_chunk_size, + ) + + def build(self, use_jit=True, verbose=1, constants=None): + """Build constant arrays. + + Parameters + ---------- + use_jit : bool, optional + Whether to just-in-time compile the objective and derivatives. + verbose : int, optional + Level of output. + + """ + if constants is None: + constants = self.constants + rtzgrid = constants["grid"] + + eq = self.things[0] + self._grid_1dr = LinearGrid( + rho=rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], + M=eq.M_grid, + N=eq.N_grid, + NFP=eq.NFP, + sym=eq.sym, + ) + self._constants["quad"] = get_quadrature( + leggauss(self._hyperparameters.pop("num_quad")), + (automorphism_sin, grad_automorphism_sin), + ) + self._target, self._bounds = _parse_callable_target_bounds( + self._target, self._bounds, rtzgrid.nodes[rtzgrid.unique_rho_idx, 0] + ) + + timer = Timer() + if verbose > 0: + print("Precomputing transforms") + timer.start("Precomputing transforms") + + self._constants["transforms_1dr"] = get_transforms( + self._keys_1dr, eq, self._grid_1dr + ) + self._constants["profiles"] = get_profiles( + self._keys_1dr + [self._key], eq, self._grid_1dr + ) + + timer.stop("Precomputing transforms") + if verbose > 1: + timer.disp("Precomputing transforms") + + super().build(use_jit=use_jit, verbose=verbose) + + def compute(self, params, constants=None): + """Compute Γ_c. + + Parameters + ---------- + params : dict + Dictionary of equilibrium degrees of freedom, e.g. + ``Equilibrium.params_dict`` + constants : dict + Dictionary of constant data, e.g. transforms, profiles etc. + Defaults to ``self.constants``. + + Returns + ------- + result : ndarray + Γ_c as a function of the flux surface label. + + """ + if constants is None: + constants = self.constants + eq = self.things[0] + + rtzgrid = constants["grid"] + + data = compute_fun( + eq, + self._keys_1dr, + params, + constants["transforms_1dr"], + constants["profiles"], + ) + + grid = eq._get_rtz_grid( + rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], + rtzgrid.nodes[rtzgrid.unique_theta_idx, 1], + rtzgrid.nodes[rtzgrid.unique_zeta_idx, 2], + coordinates="raz", + iota=self._grid_1dr.compress(data["iota"]), + params=params, + ) + data = { key: grid.copy_data_from_other(data[key], self._grid_1dr) for key in self._keys_1dr diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 9aacd2afaa..4bd3eb96df 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -10,6 +10,16 @@ from desc.equilibrium.coords import get_rtz_grid from desc.examples import get from desc.grid import LinearGrid +from desc.objectives import ( + FixBoundaryR, + FixBoundaryZ, + FixCurrent, + FixPressure, + FixPsi, + ForceBalance, + GammaC, + ObjectiveFunction, +) from desc.utils import setdefault from desc.vmec import VMECIO @@ -109,6 +119,119 @@ def test_Gamma_c(): return fig +def test_Gamma_c_opt(): + """Test that an optimization with Gamma_c works without failing.""" + eq = get("ESTELL") + with pytest.warns(UserWarning): + eq.change_resolution(4, 4, 4, 8, 8, 8) + k = 1 + + alpha = np.array([0.0]) + rho = np.linspace(0.80, 0.95, 2) + + objective = ObjectiveFunction( + ( + GammaC( + eq=eq, + rho=rho, + alpha=alpha, + deriv_mode="fwd", + batch=False, + num_pitch=3, + num_quad=3, + num_transit=2, + ), + ), + ) + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixCurrent(eq=eq), + FixPsi(eq=eq), + ) + + eq.optimize( + objective=objective, + constraints=constraints, + maxiter=2, # just testing that no errors occur during JIT/AD of the objective + ) + # run same thing again to ensure the bug in #1288 is fixed + eq.optimize( + objective=objective, + constraints=constraints, + maxiter=2, # just testing that no errors occur during JIT/AD of the objective + ) + + +def test_Gamma_c_opt_batch_True(): + """Test that an optimization with Gamma_c works without failing w/ batch=True.""" + eq = get("ESTELL") + with pytest.warns(UserWarning): + eq.change_resolution(4, 4, 4, 8, 8, 8) + k = 1 + + alpha = np.array([0.0]) + rho = np.linspace(0.80, 0.95, 2) + + objective = ObjectiveFunction( + ( + GammaC( + eq=eq, + rho=rho, + alpha=alpha, + deriv_mode="fwd", + batch=True, + num_pitch=3, + num_quad=3, + num_transit=2, + ), + ), + ) + R_modes = np.vstack( + ( + [0, 0, 0], + eq.surface.R_basis.modes[ + np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : + ], + ) + ) + Z_modes = eq.surface.Z_basis.modes[ + np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : + ] + constraints = ( + ForceBalance(eq), + FixBoundaryR(eq=eq, modes=R_modes), + FixBoundaryZ(eq=eq, modes=Z_modes), + FixPressure(eq=eq), + FixCurrent(eq=eq), + FixPsi(eq=eq), + ) + + eq.optimize( + objective=objective, + constraints=constraints, + maxiter=2, # just testing that no errors occur during JIT/AD of the objective + ) + eq.optimize( + objective=objective, + constraints=constraints, + maxiter=2, # testing JIT grid error + ) + + class NeoIO: """Class to interface with NEO.""" From 2f8ba3ce19a4d75a85cf8f49de2af82842548896 Mon Sep 17 00:00:00 2001 From: Greta Hibbard Date: Tue, 3 Dec 2024 11:44:38 -0500 Subject: [PATCH 51/58] Revert "fixing #1288, adding opt tests for Gamma_c" This reverts commit 27cfacf57d485dd63f5791f891c83fe6655657e6. --- desc/objectives/_neoclassical.py | 312 ++----------------------------- tests/test_neoclassical.py | 123 ------------ 2 files changed, 20 insertions(+), 415 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 24fc61e016..7f537143ed 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -281,8 +281,7 @@ class GammaC(_Objective): https://doi.org/10.1063/1.2912456. Equation 61. - A model for the fast evaluation of prompt losses of energetic ions in - stellarators. + A model for the fast evaluation of prompt losses of energetic ions in stellarators. J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. https://doi.org/10.1088/1741-4326/ac2994. Equation 16. @@ -347,11 +346,12 @@ class GammaC(_Objective): Note that Nemov's Γ_c converges to a finite nonzero value in the infinity limit of the number of toroidal transits. - Velasco's expression is defined to be zero on irrational surfaces; - and therefore, the numerical computation will converge to zero as the - number of toroidal transits increases. This is mentioned to remind - users that an optimization using Velasco's metric should be evaluated by - measuring decrease in Γ_c at a fixed number of toroidal transits. + Velasco's expression has a secular term that will drive the result + to zero as the number of toroidal transits increases unless the + secular term is averaged out from all the singular integrals. + Therefore, an optimization using Velasco's metric should be evaluated by + measuring decrease in Γ_c at a fixed number of toroidal transits until + unless an adaptive quadrature is used. name : str, optional Name of the objective function. jac_chunk_size : int , optional @@ -400,14 +400,13 @@ def __init__( rho, alpha = np.atleast_1d(rho, alpha) self._dim_f = rho.size - - zeta = np.linspace(0, 2 * np.pi * num_transit, knots_per_transit * num_transit) - - grid = LinearGrid(rho=rho, theta=alpha, zeta=zeta) - self._constants = { - "grid": grid, "quad_weights": 1, + "rho": rho, + "alpha": alpha, + "zeta": np.linspace( + 0, 2 * np.pi * num_transit, knots_per_transit * num_transit + ), } self._hyperparameters = { "num_quad": num_quad, @@ -415,7 +414,6 @@ def __init__( "batch": batch, "num_well": num_well, } - self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] if Nemov: self._key = "Gamma_c" @@ -423,8 +421,6 @@ def __init__( else: self._key = "Gamma_c Velasco" - self._constants["quad2"] = chebgauss2(num_quad) - super().__init__( things=eq, target=target, @@ -438,264 +434,7 @@ def __init__( jac_chunk_size=jac_chunk_size, ) - def build(self, use_jit=True, verbose=1, constants=None): - """Build constant arrays. - - Parameters - ---------- - use_jit : bool, optional - Whether to just-in-time compile the objective and derivatives. - verbose : int, optional - Level of output. - - """ - if constants is None: - constants = self.constants - rtzgrid = constants["grid"] - - eq = self.things[0] - self._grid_1dr = LinearGrid( - rho=rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - sym=eq.sym, - ) - self._constants["quad"] = get_quadrature( - leggauss(self._hyperparameters.pop("num_quad")), - (automorphism_sin, grad_automorphism_sin), - ) - self._target, self._bounds = _parse_callable_target_bounds( - self._target, self._bounds, rtzgrid.nodes[rtzgrid.unique_rho_idx, 0] - ) - - timer = Timer() - if verbose > 0: - print("Precomputing transforms") - timer.start("Precomputing transforms") - - self._constants["transforms_1dr"] = get_transforms( - self._keys_1dr, eq, self._grid_1dr - ) - self._constants["profiles"] = get_profiles( - self._keys_1dr + [self._key], eq, self._grid_1dr - ) - - timer.stop("Precomputing transforms") - if verbose > 1: - timer.disp("Precomputing transforms") - - super().build(use_jit=use_jit, verbose=verbose) - - def compute(self, params, constants=None): - """Compute Γ_c. - - Parameters - ---------- - params : dict - Dictionary of equilibrium degrees of freedom, e.g. - ``Equilibrium.params_dict`` - constants : dict - Dictionary of constant data, e.g. transforms, profiles etc. - Defaults to ``self.constants``. - - Returns - ------- - result : ndarray - Γ_c as a function of the flux surface label. - - """ - if constants is None: - constants = self.constants - eq = self.things[0] - - rtzgrid = constants["grid"] - - data = compute_fun( - eq, - self._keys_1dr, - params, - constants["transforms_1dr"], - constants["profiles"], - ) - - grid = eq._get_rtz_grid( - rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], - rtzgrid.nodes[rtzgrid.unique_theta_idx, 1], - rtzgrid.nodes[rtzgrid.unique_zeta_idx, 2], - coordinates="raz", - iota=self._grid_1dr.compress(data["iota"]), - params=params, - ) - - data = { - key: grid.copy_data_from_other(data[key], self._grid_1dr) - for key in self._keys_1dr - } - quad2 = {} - if "quad2" in constants: - quad2["quad2"] = constants["quad2"] - data = compute_fun( - eq, - self._key, - params, - get_transforms(self._key, eq, grid, jitable=True), - constants["profiles"], - data=data, - quad=constants["quad"], - **quad2, - **self._hyperparameters, - ) - return grid.compress(data[self._key]) - - -class Gammad(_Objective): - """Γ_d is a proxy for measuring energetic ion confinement. - - References - ---------- - A model for the fast evaluation of prompt losses of energetic ions in stellarators. - J.L. Velasco et al. 2021 Nucl. Fusion 61 116059. - https://doi.org/10.1088/1741-4326/ac2994. - Equation 22. - - Parameters - ---------- - eq : Equilibrium - Equilibrium that will be optimized to satisfy the Objective. - target : {float, ndarray, callable}, optional - Target value(s) of the objective. Only used if bounds is None. - Must be broadcastable to Objective.dim_f. If a callable, should take a - single argument ``rho`` and return the desired value of the profile at those - locations. Defaults to 0. - bounds : tuple of {float, ndarray, callable}, optional - Lower and upper bounds on the objective. Overrides target. - Both bounds must be broadcastable to Objective.dim_f. - If a callable, each should take a single argument ``rho`` and return the - desired bound (lower or upper) of the profile at those locations. - weight : {float, ndarray}, optional - Weighting to apply to the Objective, relative to other Objectives. - Must be broadcastable to Objective.dim_f - normalize : bool, optional - Whether to compute the error in physical units or non-dimensionalize. - normalize_target : bool, optional - Whether target and bounds should be normalized before comparing to computed - values. If `normalize` is `True` and the target is in physical units, - this should also be set to True. - loss_function : {None, 'mean', 'min', 'max'}, optional - Loss function to apply to the objective values once computed. This loss function - is called on the raw compute value, before any shifting, scaling, or - normalization. - deriv_mode : {"auto", "fwd", "rev"} - Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. - "auto" selects forward or reverse mode based on the size of the input and output - of the objective. Has no effect on self.grad or self.hess which always use - reverse mode and forward over reverse mode respectively. - rho : ndarray - Unique coordinate values specifying flux surfaces to compute on. - alpha : ndarray - Unique coordinate values specifying field line labels to compute on. - num_transit : int - Number of toroidal transits to follow field line. - For axisymmetric devices, one poloidal transit is sufficient. Otherwise, - more transits will give more accurate result, with diminishing returns. - knots_per_transit : int - Number of points per toroidal transit at which to sample data along field - line. Default is 100. - num_quad : int - Resolution for quadrature of bounce integrals. Default is 32. - num_pitch : int - Resolution for quadrature over velocity coordinate. Default is 64. - batch : bool - Whether to vectorize part of the computation. Default is true. - num_well : int - Maximum number of wells to detect for each pitch and field line. - Default is to detect all wells, but due to limitations in JAX this option - may consume more memory. Specifying a number that tightly upper bounds - the number of wells will increase performance. - name : str, optional - Name of the objective function. - jac_chunk_size : int , optional - Will calculate the Jacobian for this objective ``jac_chunk_size`` - columns at a time, instead of all at once. The memory usage of the - Jacobian calculation is roughly ``memory usage = m0 + m1*jac_chunk_size``: - the smaller the chunk size, the less memory the Jacobian calculation - will require (with some baseline memory usage). The time to compute the - Jacobian is roughly ``t=t0 +t1/jac_chunk_size``, so the larger the - ``jac_chunk_size``, the faster the calculation takes, at the cost of - requiring more memory. A ``jac_chunk_size`` of 1 corresponds to the least - memory intensive, but slowest method of calculating the Jacobian. - If None, it will use the largest size i.e ``obj.dim_x``. - - """ - - _coordinates = "r" - _units = "~" - _print_value_fmt = "Γ_d: " - - def __init__( - self, - eq, - target=None, - bounds=None, - weight=1, - normalize=True, - normalize_target=True, - loss_function=None, - deriv_mode="auto", - rho=np.linspace(0.5, 1, 3), - alpha=np.array([0]), - *, - num_transit=10, - knots_per_transit=100, - num_quad=32, - num_pitch=64, - batch=True, - num_well=None, - Nemov=True, - name="Gamma_c", - jac_chunk_size=None, - ): - - if target is None and bounds is None: - target = 0.0 - - rho, alpha = np.atleast_1d(rho, alpha) - self._dim_f = rho.size - - zeta = np.linspace(0, 2 * np.pi * num_transit, knots_per_transit * num_transit) - - grid = LinearGrid(rho=rho, theta=alpha, zeta=zeta) - - self._constants = { - "grid": grid, - "quad_weights": 1, - } - self._hyperparameters = { - "num_quad": num_quad, - "num_pitch": num_pitch, - "batch": batch, - "num_well": num_well, - } - self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] - - self._key = "Gamma_d" - self._constants["quad2"] = chebgauss2(num_quad) - - super().__init__( - things=eq, - target=target, - bounds=bounds, - weight=weight, - normalize=normalize, - normalize_target=normalize_target, - loss_function=loss_function, - deriv_mode=deriv_mode, - name=name, - jac_chunk_size=jac_chunk_size, - ) - - def build(self, use_jit=True, verbose=1, constants=None): + def build(self, use_jit=True, verbose=1): """Build constant arrays. Parameters @@ -706,24 +445,16 @@ def build(self, use_jit=True, verbose=1, constants=None): Level of output. """ - if constants is None: - constants = self.constants - rtzgrid = constants["grid"] - eq = self.things[0] self._grid_1dr = LinearGrid( - rho=rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], - M=eq.M_grid, - N=eq.N_grid, - NFP=eq.NFP, - sym=eq.sym, + rho=self._constants["rho"], M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym ) self._constants["quad"] = get_quadrature( leggauss(self._hyperparameters.pop("num_quad")), (automorphism_sin, grad_automorphism_sin), ) self._target, self._bounds = _parse_callable_target_bounds( - self._target, self._bounds, rtzgrid.nodes[rtzgrid.unique_rho_idx, 0] + self._target, self._bounds, self._constants["rho"] ) timer = Timer() @@ -765,9 +496,7 @@ def compute(self, params, constants=None): if constants is None: constants = self.constants eq = self.things[0] - - rtzgrid = constants["grid"] - + # TODO: compute all deps of gamma here data = compute_fun( eq, self._keys_1dr, @@ -775,16 +504,15 @@ def compute(self, params, constants=None): constants["transforms_1dr"], constants["profiles"], ) - + # TODO: interpolate all deps to this grid with fft utilities from fourier bounce grid = eq._get_rtz_grid( - rtzgrid.nodes[rtzgrid.unique_rho_idx, 0], - rtzgrid.nodes[rtzgrid.unique_theta_idx, 1], - rtzgrid.nodes[rtzgrid.unique_zeta_idx, 2], + constants["rho"], + constants["alpha"], + constants["zeta"], coordinates="raz", iota=self._grid_1dr.compress(data["iota"]), params=params, ) - data = { key: grid.copy_data_from_other(data[key], self._grid_1dr) for key in self._keys_1dr diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 4bd3eb96df..9aacd2afaa 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -10,16 +10,6 @@ from desc.equilibrium.coords import get_rtz_grid from desc.examples import get from desc.grid import LinearGrid -from desc.objectives import ( - FixBoundaryR, - FixBoundaryZ, - FixCurrent, - FixPressure, - FixPsi, - ForceBalance, - GammaC, - ObjectiveFunction, -) from desc.utils import setdefault from desc.vmec import VMECIO @@ -119,119 +109,6 @@ def test_Gamma_c(): return fig -def test_Gamma_c_opt(): - """Test that an optimization with Gamma_c works without failing.""" - eq = get("ESTELL") - with pytest.warns(UserWarning): - eq.change_resolution(4, 4, 4, 8, 8, 8) - k = 1 - - alpha = np.array([0.0]) - rho = np.linspace(0.80, 0.95, 2) - - objective = ObjectiveFunction( - ( - GammaC( - eq=eq, - rho=rho, - alpha=alpha, - deriv_mode="fwd", - batch=False, - num_pitch=3, - num_quad=3, - num_transit=2, - ), - ), - ) - R_modes = np.vstack( - ( - [0, 0, 0], - eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : - ], - ) - ) - Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : - ] - constraints = ( - ForceBalance(eq), - FixBoundaryR(eq=eq, modes=R_modes), - FixBoundaryZ(eq=eq, modes=Z_modes), - FixPressure(eq=eq), - FixCurrent(eq=eq), - FixPsi(eq=eq), - ) - - eq.optimize( - objective=objective, - constraints=constraints, - maxiter=2, # just testing that no errors occur during JIT/AD of the objective - ) - # run same thing again to ensure the bug in #1288 is fixed - eq.optimize( - objective=objective, - constraints=constraints, - maxiter=2, # just testing that no errors occur during JIT/AD of the objective - ) - - -def test_Gamma_c_opt_batch_True(): - """Test that an optimization with Gamma_c works without failing w/ batch=True.""" - eq = get("ESTELL") - with pytest.warns(UserWarning): - eq.change_resolution(4, 4, 4, 8, 8, 8) - k = 1 - - alpha = np.array([0.0]) - rho = np.linspace(0.80, 0.95, 2) - - objective = ObjectiveFunction( - ( - GammaC( - eq=eq, - rho=rho, - alpha=alpha, - deriv_mode="fwd", - batch=True, - num_pitch=3, - num_quad=3, - num_transit=2, - ), - ), - ) - R_modes = np.vstack( - ( - [0, 0, 0], - eq.surface.R_basis.modes[ - np.max(np.abs(eq.surface.R_basis.modes), 1) > k, : - ], - ) - ) - Z_modes = eq.surface.Z_basis.modes[ - np.max(np.abs(eq.surface.Z_basis.modes), 1) > k, : - ] - constraints = ( - ForceBalance(eq), - FixBoundaryR(eq=eq, modes=R_modes), - FixBoundaryZ(eq=eq, modes=Z_modes), - FixPressure(eq=eq), - FixCurrent(eq=eq), - FixPsi(eq=eq), - ) - - eq.optimize( - objective=objective, - constraints=constraints, - maxiter=2, # just testing that no errors occur during JIT/AD of the objective - ) - eq.optimize( - objective=objective, - constraints=constraints, - maxiter=2, # testing JIT grid error - ) - - class NeoIO: """Class to interface with NEO.""" From c3a72967b8ec6e27a336b57086a7e658afa27550 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 5 Dec 2024 15:07:58 -0500 Subject: [PATCH 52/58] Merging part 2 --- desc/compute/_neoclassical.py | 8 ++++---- desc/integrals/bounce_integral.py | 16 ++++++++-------- desc/objectives/_neoclassical.py | 12 +++++++----- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 62a9685f0a..17c3ab2c76 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -321,7 +321,7 @@ def _Gamma_c_Velasco(params, transforms, profiles, data, **kwargs): batch = kwargs.get("batch", True) grid = transforms["grid"].source_grid - def d_v_tau(B, pitch): + def d_v_tau(data, B, pitch): return safediv(2.0, jnp.sqrt(jnp.abs(1 - pitch * B))) def _cvdrift0(data, B, pitch): @@ -458,8 +458,8 @@ def drift2(data, B, pitch): / B ) - def drift3(K, B, pitch): - return jnp.sqrt(jnp.abs(1 - pitch * B)) * K / B + def drift3(data, B, pitch): + return jnp.sqrt(jnp.abs(1 - pitch * B)) * data["K"] / B def Gamma_c(data): """∫ dλ ∑ⱼ [v τ γ_c²]ⱼ.""" @@ -468,7 +468,7 @@ def Gamma_c(data): bounce = Bounce1D(grid, data, quad, automorphism=None, is_reshaped=True) points = bounce.points(data["pitch_inv"], num_well=num_well) v_tau, f1, f2 = bounce.integrate( - [d_v_tau, drift1, drift1], + [d_v_tau, drift1, drift2], data["pitch_inv"], data, ["|grad(rho)|*kappa_g", "|B|_psi|v,p"], diff --git a/desc/integrals/bounce_integral.py b/desc/integrals/bounce_integral.py index c914b63fce..b18d607152 100644 --- a/desc/integrals/bounce_integral.py +++ b/desc/integrals/bounce_integral.py @@ -1090,10 +1090,10 @@ def __init__( self._x, self._w = get_quadrature(quad, automorphism) # Compute local splines. - self.zeta = grid.compress(grid.nodes[:, 2], surface_label="zeta") + self._zeta = grid.compress(grid.nodes[:, 2], surface_label="zeta") self.B = jnp.moveaxis( CubicHermiteSpline( - x=self.zeta, + x=self._zeta, y=self._data["|B|"], dydx=self._data["|B|_z|r,a"], axis=-1, @@ -1167,7 +1167,7 @@ def points(self, pitch_inv, *, num_well=None): line and pitch, is padded with zero. """ - return bounce_points(pitch_inv, self.zeta, self.B, self._dB_dz, num_well) + return bounce_points(pitch_inv, self._zeta, self.B, self._dB_dz, num_well) def check_points(self, points, pitch_inv, *, plot=True, **kwargs): """Check that bounce points are computed correctly. @@ -1200,7 +1200,7 @@ def check_points(self, points, pitch_inv, *, plot=True, **kwargs): z1=points[0], z2=points[1], pitch_inv=pitch_inv, - knots=self.zeta, + knots=self._zeta, B=self.B, plot=plot, **kwargs, @@ -1288,7 +1288,7 @@ def integrate( result = _bounce_quadrature( x=self._x if quad is None else quad[0], w=self._w if quad is None else quad[1], - knots=self.zeta, + knots=self._zeta, integrand=integrand, pitch_inv=pitch_inv, data=data | self._data, @@ -1329,7 +1329,7 @@ def interp_to_argmin(self, f, points, *, method="cubic"): ``f`` interpolated to the deepest point between ``points``. """ - return interp_to_argmin(f, points, self.zeta, self.B, self._dB_dz, method) + return interp_to_argmin(f, points, self._zeta, self.B, self._dB_dz, method) def plot(self, m, l, pitch_inv=None, **kwargs): """Plot the field line and bounce points of the given pitch angles. @@ -1364,9 +1364,9 @@ def plot(self, m, l, pitch_inv=None, **kwargs): jnp.ndim(pitch_inv) > 1, msg=f"Got pitch_inv.ndim={jnp.ndim(pitch_inv)}, but expected 1.", ) - z1, z2 = bounce_points(pitch_inv, self.zeta, B, dB_dz) + z1, z2 = bounce_points(pitch_inv, self._zeta, B, dB_dz) kwargs["z1"] = z1 kwargs["z2"] = z2 kwargs["k"] = pitch_inv - fig, ax = plot_ppoly(PPoly(B.T, self.zeta), **_set_default_plot_kwargs(kwargs)) + fig, ax = plot_ppoly(PPoly(B.T, self._zeta), **_set_default_plot_kwargs(kwargs)) return fig, ax diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 921ab207fe..617835b24f 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -413,7 +413,9 @@ def __init__( self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] if Nemov: self._key = "Gamma_c" - self._constants["quad2"] = chebgauss2(num_quad) + self._constants["quad2 x"], self._constants["quad2 w"] = chebgauss2( + num_quad + ) else: self._key = "Gamma_c Velasco" @@ -445,7 +447,7 @@ def build(self, use_jit=True, verbose=1): self._grid_1dr = LinearGrid( rho=self._constants["rho"], M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym ) - self._constants["quad"] = get_quadrature( + self._constants["quad x"], self._constants["quad w"] = get_quadrature( leggauss(self._hyperparameters.pop("num_quad")), (automorphism_sin, grad_automorphism_sin), ) @@ -514,8 +516,8 @@ def compute(self, params, constants=None): for key in self._keys_1dr } quad2 = {} - if "quad2" in constants: - quad2["quad2"] = constants["quad2"] + if "quad2 x" in constants: + quad2["quad2"] = (constants["quad2 x"], constants["quad2 w"]) data = compute_fun( eq, self._key, @@ -523,7 +525,7 @@ def compute(self, params, constants=None): get_transforms(self._key, eq, grid, jitable=True), constants["profiles"], data=data, - quad=constants["quad"], + quad=(constants["quad x"], constants["quad w"]), **quad2, **self._hyperparameters, ) From ec937d5626f4f9ec5f492708cb0760f04b349117 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 10 Dec 2024 17:02:47 -0500 Subject: [PATCH 53/58] Review requests --- desc/compute/_neoclassical.py | 7 +- desc/objectives/_neoclassical.py | 235 +++++++++++++------------------ tests/test_neoclassical.py | 68 ++++++++- 3 files changed, 161 insertions(+), 149 deletions(-) diff --git a/desc/compute/_neoclassical.py b/desc/compute/_neoclassical.py index 17c3ab2c76..ecf7922e51 100644 --- a/desc/compute/_neoclassical.py +++ b/desc/compute/_neoclassical.py @@ -289,7 +289,8 @@ def _effective_ripple(params, transforms, profiles, data, **kwargs): ), units="~", units_long="None", - description="Energetic ion confinement proxy", + description="Energetic ion confinement proxy " + "as defined by Velasco (doi:10.1088/1741-4326/ac2994)", dim=1, params=[], transforms={"grid": []}, @@ -503,9 +504,7 @@ def Gamma_c(data): # maps of a physical coordinates. This avoids the computational issues of # multivalued maps. It further enables use of more efficient methods, such as # fast transforms and fixed computational grids throughout optimization, which - # are used in the ``Bounce2D`` operator on a developer branch. Also, Nemov - # assumes B^ϕ > 0 in some comments; this is not true in DESC, but the - # computations done here are invariant to the sign. + # are used in the ``Bounce2D`` operator on a developer branch. # It is assumed the grid is sufficiently dense to reconstruct |B|, # so anything smoother than |B| may be captured accurately as a single diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 617835b24f..433e4ee460 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -14,9 +14,23 @@ get_quadrature, grad_automorphism_sin, ) -from .objective_funs import _Objective +from .objective_funs import _Objective, collect_docs from .utils import _parse_callable_target_bounds +_bounce_overwrite = { + "deriv_mode": """ + deriv_mode : {"auto", "fwd", "rev"} + Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. + ``auto`` selects forward or reverse mode based on the size of the input and + output of the objective. Has no effect on ``self.grad`` or ``self.hess`` which + always use reverse mode and forward over reverse mode respectively. + + Default is ``fwd``. If ``rev`` is chosen, then ``jac_chunk_size=1`` is chosen + by default. In ``rev`` mode, reducing the pitch angle parameter ``batch_size`` + does not reduce memory, so it is recommended to retain the default for that. + """ +} + class EffectiveRipple(_Objective): """The effective ripple is a proxy for neoclassical transport. @@ -39,74 +53,50 @@ class EffectiveRipple(_Objective): Parameters ---------- eq : Equilibrium - Equilibrium that will be optimized to satisfy the Objective. - target : {float, ndarray, callable}, optional - Target value(s) of the objective. Only used if bounds is None. - Must be broadcastable to Objective.dim_f. If a callable, should take a - single argument ``rho`` and return the desired value of the profile at those - locations. Defaults to 0. - bounds : tuple of {float, ndarray, callable}, optional - Lower and upper bounds on the objective. Overrides target. - Both bounds must be broadcastable to Objective.dim_f. - If a callable, each should take a single argument ``rho`` and return the - desired bound (lower or upper) of the profile at those locations. - weight : {float, ndarray}, optional - Weighting to apply to the Objective, relative to other Objectives. - Must be broadcastable to Objective.dim_f - normalize : bool, optional - This quantity is already normalized so this parameter is ignored. - Whether to compute the error in physical units or non-dimensionalize. - normalize_target : bool, optional - Whether target and bounds should be normalized before comparing to computed - values. If `normalize` is ``True`` and the target is in physical units, - this should also be set to True. - loss_function : {None, 'mean', 'min', 'max'}, optional - Loss function to apply to the objective values once computed. This loss function - is called on the raw compute value, before any shifting, scaling, or - normalization. - deriv_mode : {"auto", "fwd", "rev"} - Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. - "auto" selects forward or reverse mode based on the size of the input and output - of the objective. Has no effect on self.grad or self.hess which always use - reverse mode and forward over reverse mode respectively. + ``Equilibrium`` to be optimized. rho : ndarray Unique coordinate values specifying flux surfaces to compute on. alpha : ndarray Unique coordinate values specifying field line labels to compute on. + batch : bool + Whether to vectorize part of the computation. Default is true. Y_B : int - Number of points per toroidal transit at which to sample data along field - line. Default is 100. + Desired resolution for algorithm to compute bounce points. + Default is double ``Y``. Something like 100 is usually sufficient. + Currently, this is the number of knots per toroidal transit over + to approximate |B| with cubic splines. num_transit : int Number of toroidal transits to follow field line. For axisymmetric devices, one poloidal transit is sufficient. Otherwise, - more transits will give more accurate result, with diminishing returns. + assuming the surface is not near rational, more transits will + approximate surface averages better, with diminishing returns. + num_well : int + Maximum number of wells to detect for each pitch and field line. + Giving ``None`` will detect all wells but due to current limitations in + JAX this will have worse performance. + Specifying a number that tightly upper bounds the number of wells will + increase performance. In general, an upper bound on the number of wells + per toroidal transit is ``Aι+B`` where ``A``,``B`` are the poloidal and + toroidal Fourier resolution of |B|, respectively, in straight-field line + PEST coordinates, and ι is the rotational transform normalized by 2π. + A tighter upper bound than ``num_well=(Aι+B)*num_transit`` is preferable. + The ``check_points`` or ``plot`` methods in ``desc.integrals.Bounce2D`` + are useful to select a reasonable value. num_quad : int Resolution for quadrature of bounce integrals. Default is 32. num_pitch : int Resolution for quadrature over velocity coordinate. Default is 50. - batch : bool - Whether to vectorize part of the computation. Default is true. - num_well : int - Maximum number of wells to detect for each pitch and field line. - Default is to detect all wells, but due to limitations in JAX this option - may consume more memory. Specifying a number that tightly upper bounds - the number of wells will increase performance. - name : str, optional - Name of the objective function. - jac_chunk_size : int , optional - Will calculate the Jacobian for this objective ``jac_chunk_size`` - columns at a time, instead of all at once. The memory usage of the - Jacobian calculation is roughly ``memory usage = m0 + m1*jac_chunk_size``: - the smaller the chunk size, the less memory the Jacobian calculation - will require (with some baseline memory usage). The time to compute the - Jacobian is roughly ``t=t0 +t1/jac_chunk_size``, so the larger the - ``jac_chunk_size``, the faster the calculation takes, at the cost of - requiring more memory. A ``jac_chunk_size`` of 1 corresponds to the least - memory intensive, but slowest method of calculating the Jacobian. - If None, it will use the largest size i.e ``obj.dim_x``. """ + __doc__ = __doc__.rstrip() + collect_docs( + target_default="``target=0``.", + bounds_default="``target=0``.", + normalize_detail=" Note: Has no effect for this objective.", + normalize_target_detail=" Note: Has no effect for this objective.", + overwrite=_bounce_overwrite, + ) + _coordinates = "r" _units = "~" _print_value_fmt = "Effective ripple ε: " @@ -124,11 +114,11 @@ def __init__( *, rho=1.0, alpha=0.0, + batch=True, Y_B=100, num_transit=10, num_quad=32, num_pitch=50, - batch=True, num_well=None, name="Effective ripple", jac_chunk_size=None, @@ -147,12 +137,12 @@ def __init__( "R0", # TODO: GitHub PR #1094 ] self._constants = { - "quad_weights": 1, + "quad_weights": 1.0, "rho": rho, "alpha": alpha, "zeta": np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit), + "quad": chebgauss2(num_quad), } - self._constants["quad x"], self._constants["quad w"] = chebgauss2(num_quad) self._hyperparameters = { "num_pitch": num_pitch, "batch": batch, @@ -230,7 +220,6 @@ def compute(self, params, constants=None): if constants is None: constants = self.constants eq = self.things[0] - # TODO: compute all deps of effective ripple here data = compute_fun( eq, self._keys_1dr, @@ -238,7 +227,6 @@ def compute(self, params, constants=None): constants["transforms_1dr"], constants["profiles"], ) - # TODO: interpolate all deps to this grid with fft utilities from fourier bounce grid = eq._get_rtz_grid( constants["rho"], constants["alpha"], @@ -262,7 +250,7 @@ def compute(self, params, constants=None): get_transforms("effective ripple", eq, grid, jitable=True), constants["profiles"], data=data, - quad=(constants["quad x"], constants["quad w"]), + quad=constants["quad"], **self._hyperparameters, ) return grid.compress(data["effective ripple"]) @@ -287,85 +275,61 @@ class GammaC(_Objective): Parameters ---------- eq : Equilibrium - Equilibrium that will be optimized to satisfy the Objective. - target : {float, ndarray, callable}, optional - Target value(s) of the objective. Only used if bounds is None. - Must be broadcastable to Objective.dim_f. If a callable, should take a - single argument ``rho`` and return the desired value of the profile at those - locations. Defaults to 0. - bounds : tuple of {float, ndarray, callable}, optional - Lower and upper bounds on the objective. Overrides target. - Both bounds must be broadcastable to Objective.dim_f. - If a callable, each should take a single argument ``rho`` and return the - desired bound (lower or upper) of the profile at those locations. - weight : {float, ndarray}, optional - Weighting to apply to the Objective, relative to other Objectives. - Must be broadcastable to Objective.dim_f - normalize : bool, optional - Whether to compute the error in physical units or non-dimensionalize. - normalize_target : bool, optional - Whether target and bounds should be normalized before comparing to computed - values. If `normalize` is `True` and the target is in physical units, - this should also be set to True. - loss_function : {None, 'mean', 'min', 'max'}, optional - Loss function to apply to the objective values once computed. This loss function - is called on the raw compute value, before any shifting, scaling, or - normalization. - deriv_mode : {"auto", "fwd", "rev"} - Specify how to compute Jacobian matrix, either forward mode or reverse mode AD. - "auto" selects forward or reverse mode based on the size of the input and output - of the objective. Has no effect on self.grad or self.hess which always use - reverse mode and forward over reverse mode respectively. + ``Equilibrium`` to be optimized. rho : ndarray Unique coordinate values specifying flux surfaces to compute on. alpha : ndarray Unique coordinate values specifying field line labels to compute on. + batch : bool + Whether to vectorize part of the computation. Default is true. + Y_B : int + Desired resolution for algorithm to compute bounce points. + Default is double ``Y``. Something like 100 is usually sufficient. + Currently, this is the number of knots per toroidal transit over + to approximate |B| with cubic splines. num_transit : int Number of toroidal transits to follow field line. For axisymmetric devices, one poloidal transit is sufficient. Otherwise, - more transits will give more accurate result, with diminishing returns. - Y_B : int - Number of points per toroidal transit at which to sample data along field - line. Default is 100. + assuming the surface is not near rational, more transits will + approximate surface averages better, with diminishing returns. + num_well : int + Maximum number of wells to detect for each pitch and field line. + Giving ``None`` will detect all wells but due to current limitations in + JAX this will have worse performance. + Specifying a number that tightly upper bounds the number of wells will + increase performance. In general, an upper bound on the number of wells + per toroidal transit is ``Aι+B`` where ``A``,``B`` are the poloidal and + toroidal Fourier resolution of |B|, respectively, in straight-field line + PEST coordinates, and ι is the rotational transform normalized by 2π. + A tighter upper bound than ``num_well=(Aι+B)*num_transit`` is preferable. + The ``check_points`` or ``plot`` methods in ``desc.integrals.Bounce2D`` + are useful to select a reasonable value. num_quad : int Resolution for quadrature of bounce integrals. Default is 32. num_pitch : int Resolution for quadrature over velocity coordinate. Default is 64. - batch : bool - Whether to vectorize part of the computation. Default is true. - num_well : int - Maximum number of wells to detect for each pitch and field line. - Default is to detect all wells, but due to limitations in JAX this option - may consume more memory. Specifying a number that tightly upper bounds - the number of wells will increase performance. Nemov : bool Whether to use the Γ_c as defined by Nemov et al. or Velasco et al. Default is Nemov. Set to ``False`` to use Velascos's. - Note that Nemov's Γ_c converges to a finite nonzero value in the - infinity limit of the number of toroidal transits. - Velasco's expression has a secular term that will drive the result - to zero as the number of toroidal transits increases unless the - secular term is averaged out from all the singular integrals. - Therefore, an optimization using Velasco's metric should be evaluated by - measuring decrease in Γ_c at a fixed number of toroidal transits until - unless an adaptive quadrature is used. - name : str, optional - Name of the objective function. - jac_chunk_size : int , optional - Will calculate the Jacobian for this objective ``jac_chunk_size`` - columns at a time, instead of all at once. The memory usage of the - Jacobian calculation is roughly ``memory usage = m0 + m1*jac_chunk_size``: - the smaller the chunk size, the less memory the Jacobian calculation - will require (with some baseline memory usage). The time to compute the - Jacobian is roughly ``t=t0 +t1/jac_chunk_size``, so the larger the - ``jac_chunk_size``, the faster the calculation takes, at the cost of - requiring more memory. A ``jac_chunk_size`` of 1 corresponds to the least - memory intensive, but slowest method of calculating the Jacobian. - If None, it will use the largest size i.e ``obj.dim_x``. + Nemov's Γ_c converges to a finite nonzero value in the infinity limit + of the number of toroidal transits. Velasco's expression has a secular + term that drives the result to zero as the number of toroidal transits + increases if the secular term is not averaged out from the singular + integrals. Currently, an optimization using Velasco's metric may need + to be evaluated by measuring decrease in Γ_c at a fixed number of toroidal + transits. """ + __doc__ = __doc__.rstrip() + collect_docs( + target_default="``target=0``.", + bounds_default="``target=0``.", + normalize_detail=" Note: Has no effect for this objective.", + normalize_target_detail=" Note: Has no effect for this objective.", + overwrite=_bounce_overwrite, + ) + _coordinates = "r" _units = "~" _print_value_fmt = "Γ_c: " @@ -380,14 +344,14 @@ def __init__( normalize_target=True, loss_function=None, deriv_mode="auto", + *, rho=np.linspace(0.5, 1, 3), alpha=np.array([0]), - *, + batch=True, num_transit=10, Y_B=100, num_quad=32, num_pitch=64, - batch=True, num_well=None, Nemov=True, name="Gamma_c", @@ -399,7 +363,7 @@ def __init__( rho, alpha = np.atleast_1d(rho, alpha) self._dim_f = rho.size self._constants = { - "quad_weights": 1, + "quad_weights": 1.0, "rho": rho, "alpha": alpha, "zeta": np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit), @@ -411,13 +375,7 @@ def __init__( "num_well": num_well, } self._keys_1dr = ["iota", "iota_r", "min_tz |B|", "max_tz |B|"] - if Nemov: - self._key = "Gamma_c" - self._constants["quad2 x"], self._constants["quad2 w"] = chebgauss2( - num_quad - ) - else: - self._key = "Gamma_c Velasco" + self._key = "Gamma_c" if Nemov else "Gamma_c Velasco" super().__init__( things=eq, @@ -447,10 +405,13 @@ def build(self, use_jit=True, verbose=1): self._grid_1dr = LinearGrid( rho=self._constants["rho"], M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, sym=eq.sym ) - self._constants["quad x"], self._constants["quad w"] = get_quadrature( - leggauss(self._hyperparameters.pop("num_quad")), + num_quad = self._hyperparameters.pop("num_quad") + self._constants["quad"] = get_quadrature( + leggauss(num_quad), (automorphism_sin, grad_automorphism_sin), ) + if self._key == "Gamma_c": + self._constants["quad2"] = chebgauss2(num_quad) self._target, self._bounds = _parse_callable_target_bounds( self._target, self._bounds, self._constants["rho"] ) @@ -494,7 +455,6 @@ def compute(self, params, constants=None): if constants is None: constants = self.constants eq = self.things[0] - # TODO: compute all deps of gamma here data = compute_fun( eq, self._keys_1dr, @@ -502,7 +462,6 @@ def compute(self, params, constants=None): constants["transforms_1dr"], constants["profiles"], ) - # TODO: interpolate all deps to this grid with fft utilities from fourier bounce grid = eq._get_rtz_grid( constants["rho"], constants["alpha"], @@ -516,8 +475,8 @@ def compute(self, params, constants=None): for key in self._keys_1dr } quad2 = {} - if "quad2 x" in constants: - quad2["quad2"] = (constants["quad2 x"], constants["quad2 w"]) + if self._key == "Gamma_c": + quad2["quad2"] = constants["quad2"] data = compute_fun( eq, self._key, @@ -525,7 +484,7 @@ def compute(self, params, constants=None): get_transforms(self._key, eq, grid, jitable=True), constants["profiles"], data=data, - quad=(constants["quad x"], constants["quad w"]), + quad=constants["quad"], **quad2, **self._hyperparameters, ) diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 9aacd2afaa..22e92068d6 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -10,6 +10,7 @@ from desc.equilibrium.coords import get_rtz_grid from desc.examples import get from desc.grid import LinearGrid +from desc.objectives import EffectiveRipple, GammaC from desc.utils import setdefault from desc.vmec import VMECIO @@ -59,7 +60,9 @@ def test_effective_ripple(): eq = get("W7-X") rho = np.linspace(0, 1, 10) alpha = np.array([0]) - zeta = np.linspace(0, 20 * np.pi, 1000) + Y_B = 100 + num_transit = 10 + zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) grid = get_rtz_grid(eq, rho, alpha, zeta, coordinates="raz") data = eq.compute("effective ripple", grid=grid) assert np.isfinite(data["effective ripple"]).all() @@ -83,9 +86,11 @@ def test_Gamma_c_Velasco(): """Test Γ_c with W7-X.""" eq = get("W7-X") rho = np.linspace(0, 1, 10) - grid = eq._get_rtz_grid( - rho, np.array([0]), np.linspace(0, 20 * np.pi, 1000), coordinates="raz" - ) + alpha = np.array([0]) + Y_B = 100 + num_transit = 10 + zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) + grid = eq._get_rtz_grid(rho, alpha, zeta, coordinates="raz") data = eq.compute("Gamma_c Velasco", grid=grid) assert np.isfinite(data["Gamma_c Velasco"]).all() fig, ax = plt.subplots() @@ -99,9 +104,11 @@ def test_Gamma_c(): """Test Γ_c Nemov with W7-X.""" eq = get("W7-X") rho = np.linspace(0, 1, 10) - grid = eq._get_rtz_grid( - rho, np.array([0]), np.linspace(0, 20 * np.pi, 1000), coordinates="raz" - ) + alpha = np.array([0]) + Y_B = 100 + num_transit = 10 + zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) + grid = eq._get_rtz_grid(rho, alpha, zeta, coordinates="raz") data = eq.compute("Gamma_c", grid=grid) assert np.isfinite(data["Gamma_c"]).all() fig, ax = plt.subplots() @@ -109,6 +116,53 @@ def test_Gamma_c(): return fig +@pytest.mark.unit +def test_objective_compute(): + """To avoid issues such as #1424.""" + eq = get("W7-X") + rho = np.linspace(0, 1, 4) + alpha = np.array([0]) + Y_B = 50 + num_transit = 4 + num_pitch = 16 + num_quad = 16 + zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) + grid = get_rtz_grid(eq, rho, alpha, zeta, coordinates="raz") + data = eq.compute( + ["effective ripple", "Gamma_c"], + grid=grid, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj = EffectiveRipple( + eq, + rho=rho, + alpha=alpha, + Y_B=Y_B, + num_transit=num_transit, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj.build() + # TODO(#1094) + np.testing.assert_allclose( + obj.compute(eq.params_dict), grid.compress(data["effective ripple"]), rtol=0.002 + ) + obj = GammaC( + eq, + rho=rho, + alpha=alpha, + Y_B=Y_B, + num_transit=num_transit, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj.build() + np.testing.assert_allclose( + obj.compute(eq.params_dict), grid.compress(data["Gamma_c"]) + ) + + class NeoIO: """Class to interface with NEO.""" From 86a0c2fe4e4431d3c70329cf8b3afc01e024aa91 Mon Sep 17 00:00:00 2001 From: unalmis Date: Tue, 10 Dec 2024 18:06:56 -0500 Subject: [PATCH 54/58] Review request 2 --- tests/test_neoclassical.py | 48 ----------------------------------- tests/test_objective_funs.py | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 48 deletions(-) diff --git a/tests/test_neoclassical.py b/tests/test_neoclassical.py index 22e92068d6..8a7b44fdc8 100644 --- a/tests/test_neoclassical.py +++ b/tests/test_neoclassical.py @@ -10,7 +10,6 @@ from desc.equilibrium.coords import get_rtz_grid from desc.examples import get from desc.grid import LinearGrid -from desc.objectives import EffectiveRipple, GammaC from desc.utils import setdefault from desc.vmec import VMECIO @@ -116,53 +115,6 @@ def test_Gamma_c(): return fig -@pytest.mark.unit -def test_objective_compute(): - """To avoid issues such as #1424.""" - eq = get("W7-X") - rho = np.linspace(0, 1, 4) - alpha = np.array([0]) - Y_B = 50 - num_transit = 4 - num_pitch = 16 - num_quad = 16 - zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) - grid = get_rtz_grid(eq, rho, alpha, zeta, coordinates="raz") - data = eq.compute( - ["effective ripple", "Gamma_c"], - grid=grid, - num_quad=num_quad, - num_pitch=num_pitch, - ) - obj = EffectiveRipple( - eq, - rho=rho, - alpha=alpha, - Y_B=Y_B, - num_transit=num_transit, - num_quad=num_quad, - num_pitch=num_pitch, - ) - obj.build() - # TODO(#1094) - np.testing.assert_allclose( - obj.compute(eq.params_dict), grid.compress(data["effective ripple"]), rtol=0.002 - ) - obj = GammaC( - eq, - rho=rho, - alpha=alpha, - Y_B=Y_B, - num_transit=num_transit, - num_quad=num_quad, - num_pitch=num_pitch, - ) - obj.build() - np.testing.assert_allclose( - obj.compute(eq.params_dict), grid.compress(data["Gamma_c"]) - ) - - class NeoIO: """Class to interface with NEO.""" diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 348bd4a950..04cf8ffe22 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -23,6 +23,7 @@ ) from desc.compute import get_transforms from desc.equilibrium import Equilibrium +from desc.equilibrium.coords import get_rtz_grid from desc.examples import get from desc.geometry import FourierPlanarCurve, FourierRZToroidalSurface, FourierXYZCurve from desc.grid import ConcentricGrid, LinearGrid, QuadratureGrid @@ -1477,6 +1478,54 @@ def test(field, grid): np.testing.assert_allclose(result1 * 2, result5) np.testing.assert_allclose(result2, result4) + @pytest.mark.unit + def test_objective_compute(self): + """To avoid issues such as #1424.""" + eq = get("W7-X") + rho = np.linspace(0.1, 1, 3) + alpha = np.array([0]) + Y_B = 50 + num_transit = 4 + num_pitch = 16 + num_quad = 16 + zeta = np.linspace(0, 2 * np.pi * num_transit, Y_B * num_transit) + grid = get_rtz_grid(eq, rho, alpha, zeta, coordinates="raz") + data = eq.compute( + ["effective ripple", "Gamma_c"], + grid=grid, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj = EffectiveRipple( + eq, + rho=rho, + alpha=alpha, + Y_B=Y_B, + num_transit=num_transit, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj.build() + # TODO(#1094) + np.testing.assert_allclose( + obj.compute(eq.params_dict), + grid.compress(data["effective ripple"]), + rtol=0.004, + ) + obj = GammaC( + eq, + rho=rho, + alpha=alpha, + Y_B=Y_B, + num_transit=num_transit, + num_quad=num_quad, + num_pitch=num_pitch, + ) + obj.build() + np.testing.assert_allclose( + obj.compute(eq.params_dict), grid.compress(data["Gamma_c"]) + ) + @pytest.mark.regression def test_derivative_modes(): From a63f2ae120ca027cebd72648e01e21642816dc65 Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 11 Dec 2024 18:24:45 -0500 Subject: [PATCH 55/58] Rename linking current objective and expand docstring --- CHANGELOG.md | 2 +- desc/objectives/__init__.py | 2 +- desc/objectives/_coils.py | 13 ++++++++++--- docs/api.rst | 1 + docs/api_objectives.rst | 1 + tests/test_objective_funs.py | 22 +++++++++++----------- tests/test_optimizer.py | 4 ++-- 7 files changed, 27 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae716dc279..19200c73c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changelog New Feature - Adds a new profile class ``PowerProfile`` for raising profiles to a power. -- Add ``desc.objectives.LinkingCurrent`` for ensuring that coils in a stage 2 or single stage optimization provide the required linking current for a given equilibrium. +- Add ``desc.objectives.LinkingCurrentConsistency`` for ensuring that coils in a stage 2 or single stage optimization provide the required linking current for a given equilibrium. Bug Fixes diff --git a/desc/objectives/__init__.py b/desc/objectives/__init__.py index d6244beb3d..f97fb16606 100644 --- a/desc/objectives/__init__.py +++ b/desc/objectives/__init__.py @@ -9,7 +9,7 @@ CoilSetLinkingNumber, CoilSetMinDistance, CoilTorsion, - LinkingCurrent, + LinkingCurrentConsistency, PlasmaCoilSetMinDistance, QuadraticFlux, SurfaceCurrentRegularization, diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index ed8a0cfc98..86fa9cac40 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1763,9 +1763,15 @@ def compute(self, params_1, params_2=None, constants=None): return Psi -class LinkingCurrent(_Objective): +class LinkingCurrentConsistency(_Objective): """Target the self-consistent poloidal linking current between the plasma and coils. + A self-consistent coil + plasma configuration must have the sum of the signed + currents in the coils that poloidally link the plasma equal to the total poloidal + current in the plasma G. This objective computes the difference between these two + quantities, such that a value of zero means the coils create the correct net + poloidal current. + Assumes the coil topology does not change (ie the linking number with the plasma is fixed). @@ -1796,6 +1802,9 @@ def __init__( self, eq, coil, + *, + grid=None, + eq_fixed=False, target=None, bounds=None, weight=1, @@ -1803,8 +1812,6 @@ def __init__( normalize_target=True, loss_function=None, deriv_mode="auto", - grid=None, - eq_fixed=False, jac_chunk_size=None, name="linking current", ): diff --git a/docs/api.rst b/docs/api.rst index fa32edb869..7e0c848a19 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -208,6 +208,7 @@ Objective Functions desc.objectives.HelicalForceBalance desc.objectives.Isodynamicity desc.objectives.LinearObjectiveFromUser + desc.objectives.LinkingCurrentConsistency desc.objectives.MagneticWell desc.objectives.MeanCurvature desc.objectives.MercierStability diff --git a/docs/api_objectives.rst b/docs/api_objectives.rst index 5fddaaed08..4096748af6 100644 --- a/docs/api_objectives.rst +++ b/docs/api_objectives.rst @@ -108,6 +108,7 @@ Coil Optimization desc.objectives.CoilArclengthVariance desc.objectives.ToroidalFlux desc.objectives.SurfaceCurrentRegularization + desc.objectives.LinkingCurrentConsistency Profiles diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 38c67a0743..e1efadc53b 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -59,7 +59,7 @@ HeatingPowerISS04, Isodynamicity, LinearObjectiveFromUser, - LinkingCurrent, + LinkingCurrentConsistency, MagneticWell, MeanCurvature, MercierStability, @@ -1445,7 +1445,7 @@ def test_linking_current(self): -c * 1.5, ] np.testing.assert_allclose(coilset1._all_currents(), expected_currents) - obj = LinkingCurrent(eq, coilset1) + obj = LinkingCurrentConsistency(eq, coilset1) obj.build() f = obj.compute(coilset1.params_dict, eq.params_dict) np.testing.assert_allclose(f, 0) @@ -1453,7 +1453,7 @@ def test_linking_current(self): # same with virtual coils coilset2 = CoilSet(coil1, coil2, NFP=2, sym=True) np.testing.assert_allclose(coilset2._all_currents(), expected_currents) - obj = LinkingCurrent(eq, coilset2) + obj = LinkingCurrentConsistency(eq, coilset2) obj.build() f = obj.compute(coilset2.params_dict, eq.params_dict) np.testing.assert_allclose(f, 0) @@ -1464,7 +1464,7 @@ def test_linking_current(self): np.testing.assert_allclose( coilset3._all_currents(), expected_currents + expected_currents ) - obj = LinkingCurrent(eq, coilset3) + obj = LinkingCurrentConsistency(eq, coilset3) obj.build() f = obj.compute(coilset3.params_dict, eq.params_dict) np.testing.assert_allclose(f, -G) # coils provide 2G so error is -G @@ -1474,7 +1474,7 @@ def test_linking_current(self): np.testing.assert_allclose( coilset4._all_currents(), expected_currents + [0.5 * G / 8] ) - obj = LinkingCurrent(eq, coilset4) + obj = LinkingCurrentConsistency(eq, coilset4) obj.build() f = obj.compute(coilset4.params_dict, eq.params_dict) np.testing.assert_allclose(f, -0.5 * G / 8) @@ -2515,7 +2515,7 @@ class TestComputeScalarResolution: FusionPower, GenericObjective, HeatingPowerISS04, - LinkingCurrent, + LinkingCurrentConsistency, Omnigenity, PlasmaCoilSetMinDistance, PlasmaVesselDistance, @@ -2949,14 +2949,14 @@ def test_compute_scalar_resolution_coils(self, objective): @pytest.mark.unit def test_compute_scalar_resolution_linking_current(self): - """LinkingCurrent.""" + """LinkingCurrentConsistency.""" coil = FourierPlanarCoil(center=[10, 1, 0]) eq = Equilibrium() coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) f = np.zeros_like(self.res_array, dtype=float) for i, res in enumerate(self.res_array): obj = ObjectiveFunction( - LinkingCurrent( + LinkingCurrentConsistency( eq, coilset, grid=LinearGrid(M=int(eq.M_grid * res), N=int(eq.N_grid * res)), @@ -2996,7 +2996,7 @@ class TestObjectiveNaNGrad: ForceBalanceAnisotropic, FusionPower, HeatingPowerISS04, - LinkingCurrent, + LinkingCurrentConsistency, Omnigenity, PlasmaCoilSetMinDistance, PlasmaVesselDistance, @@ -3283,11 +3283,11 @@ def test_objective_no_nangrad_ballooning(self): @pytest.mark.unit def test_objective_no_nangrad_linking_current(self): - """LinkingCurrent.""" + """LinkingCurrentConsistency.""" coil = FourierPlanarCoil(center=[10, 1, 0]) coilset = CoilSet.from_symmetry(coil, NFP=4, sym=True) eq = Equilibrium() - obj = ObjectiveFunction(LinkingCurrent(eq, coilset)) + obj = ObjectiveFunction(LinkingCurrentConsistency(eq, coilset)) obj.build() g = obj.grad(obj.x()) assert not np.any(np.isnan(g)) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index ccb4c13f59..3a31eaecec 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -35,7 +35,7 @@ FixPsi, ForceBalance, GenericObjective, - LinkingCurrent, + LinkingCurrentConsistency, MagneticWell, MeanCurvature, ObjectiveFunction, @@ -1405,7 +1405,7 @@ def test_optimize_coil_currents(DummyCoilSet): coil.current = current / coils.num_coils objective = ObjectiveFunction(QuadraticFlux(eq=eq, field=coils, vacuum=True)) - constraints = LinkingCurrent(eq, coils, eq_fixed=True) + constraints = LinkingCurrentConsistency(eq, coils, eq_fixed=True) optimizer = Optimizer("lsq-exact") [coils_opt], _ = optimizer.optimize( things=coils, From 8f39791ee8c7320e6db5aea54e938376817850f9 Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 11 Dec 2024 20:13:29 -0500 Subject: [PATCH 56/58] suppress warning about incompatible constraints --- tests/test_optimizer.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 3a31eaecec..637d247a87 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1407,13 +1407,15 @@ def test_optimize_coil_currents(DummyCoilSet): objective = ObjectiveFunction(QuadraticFlux(eq=eq, field=coils, vacuum=True)) constraints = LinkingCurrentConsistency(eq, coils, eq_fixed=True) optimizer = Optimizer("lsq-exact") - [coils_opt], _ = optimizer.optimize( - things=coils, - objective=objective, - constraints=constraints, - verbose=2, - copy=True, - ) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*\n.*\nIncompatible") + [coils_opt], _ = optimizer.optimize( + things=coils, + objective=objective, + constraints=constraints, + verbose=2, + copy=True, + ) # check that optimized coil currents changed by more than 15% from initial values np.testing.assert_array_less( np.asarray(coils.current).mean() * 0.15, From 7dcd99f630f7aa97d9557ce0236b9c54904f1e7c Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Thu, 12 Dec 2024 20:53:47 -0500 Subject: [PATCH 57/58] Update desc/objectives/_coils.py Co-authored-by: Dario Panici <37969854+dpanici@users.noreply.github.com> --- desc/objectives/_coils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/_coils.py b/desc/objectives/_coils.py index 86fa9cac40..f419bf5e4d 100644 --- a/desc/objectives/_coils.py +++ b/desc/objectives/_coils.py @@ -1768,7 +1768,7 @@ class LinkingCurrentConsistency(_Objective): A self-consistent coil + plasma configuration must have the sum of the signed currents in the coils that poloidally link the plasma equal to the total poloidal - current in the plasma G. This objective computes the difference between these two + current required to be linked by the plasma according to the loop integral of its toroidal magnetic field, given by `G(rho=1)`. This objective computes the difference between these two quantities, such that a value of zero means the coils create the correct net poloidal current. From c312aa19d5b5d205069fbd6a1b837af35c942bd5 Mon Sep 17 00:00:00 2001 From: unalmis Date: Thu, 12 Dec 2024 23:34:38 -0500 Subject: [PATCH 58/58] review request to make objective only allow kwarg --- desc/objectives/_neoclassical.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/objectives/_neoclassical.py b/desc/objectives/_neoclassical.py index 433e4ee460..a2fb539517 100644 --- a/desc/objectives/_neoclassical.py +++ b/desc/objectives/_neoclassical.py @@ -104,6 +104,7 @@ class EffectiveRipple(_Objective): def __init__( self, eq, + *, target=None, bounds=None, weight=1, @@ -111,7 +112,6 @@ def __init__( normalize_target=True, loss_function=None, deriv_mode="auto", - *, rho=1.0, alpha=0.0, batch=True, @@ -337,6 +337,7 @@ class GammaC(_Objective): def __init__( self, eq, + *, target=None, bounds=None, weight=1, @@ -344,7 +345,6 @@ def __init__( normalize_target=True, loss_function=None, deriv_mode="auto", - *, rho=np.linspace(0.5, 1, 3), alpha=np.array([0]), batch=True,

!TRPisu_Z-isYOtslf*F(IvXqh< zm}7C_Qo=Bp=h3@#<&NiL*siO9<;=sJ)eLWXN`U%`VrVWs*!0>+6L9aVL4Aj%0cB# zF6+k5c8E&v@0kyI34Hf|hQ|$0!{%M$@73Lj<1nJ1c<$4&ZtzsEyl8*93XXCc-{Y9= zf;f}UH})=$g0c6{n=caQAt~eV%a*VyI8?qfxWQ`xT&u30IHOz(?#c5K2|0Z*`r2=I zlK3RtnbqU1(O!i0NqHwNDQBRIaiqpCd<4q6-4eR!8oP7yaU@w6AdP^IT+!l571~IgZpth$(w}QK{3TZoSI_{Y$8GrN<1XVqse{##-@SU( zPI^%e!jEUm5*0Fl`U>4whRg!ES#>~pn`1Rt`e?njdiDYeYl}ktu6Kcvi^w^eiAK2E zTXXQLXd!sM7o)B;EhhpQyZetr)6Eb&LG3cj(hkkB+rt|AUcyv?y~mA^PN;D?Y;|qE z0{FZh%0Dcx0v3ICONyQj@XU>R|7N%cGM;L=n{toC=f%%uYLo+Dq%M2wjF7s^ zVVAa>gTR!KaPo@47@YDnj$}VN3#UCqhAPa)p-x2Ei{Gjf3RE@~iVQUXFJI2#Nt#hm z8jU?Yv}+O`c13%rhAn`|o$D?}0h18dlIZTn)d$ZTlBAtXo1t%9<;%9t3FzM97=K0H zY8sCAkj~6|FT$7|g`V2X6kKuP+mU~F2t>z%&LwPa1E$6et*MV@Abx8Kb(Qm)*=|NU zmd1JHz~Hq@@~z?t@YLYZ%VYF05cP29+kEp($oX`tKx8Hl(3yITGq3VMC4`OjsALT& z8j42Jt}B4*Ll1N+-(~{kw{4zt(Pcn4slC2(eGN2yOV}GXR1K9IH=Px0YJ-J->Dl4i zRq*WKhJ_&A0?6FWbNF0lEfJp9)ZUPlXa!gAyG9W?EnrQyqy83qAJ_|8r@wjD1k1GT zluSPm6nEqom^L)QJ~<(!df_e*Nr))Q&gz7_+d87Y;?c=@FNU<#&;b^zksY=gmB2Y9 z8OrU`2AWE}Ay;_%A<(2gx;ml{x`HW;81lwJne)rpyf$Jl&~CKdLXlYmh-sm17fmNb zO>l; zZL3f}4t~npO{bOTA#S&TY4q$Un6z9jusqxjsUM#X-s2mFL$x*M-%HJd;CQw4ySzzK zo0+LR(F^+AL&no#FUP%#K7|-KP%Knoo|6PV4anpXAse10`)w?C&IO6Whbp;3*-)XF zT|^^Z4$rE)Zikv?Lvi3x;ek*2fHh*X4-+fE!bbJ8R%#V&&lil#7%Yc0^T>*9qXwXi z`;^}6Py*kQ!sGPTSHRU1n>85EH4p(c4NSe^X@(Ow8kFs}*Fj5(mBy~k9q=he-k@I|pA{zCK+ZkJvLn0$Y$h*w1kJR7MUX75V@oga_k7&t6I=(6$R3@d zpJ{-W7V{-*_HK|&qM&XR?uB&?b*5e-9q=k~!Xeji2v~k_$Go#6wt)0qJKcWzRw&7P zo=<+QAMWe1eJO1j1UISuXEtu>2Dw8e$H>}8;nK-*Ew1EtaMx-uNf~Ja1~SU8A-cnm z-%+3XXv-+PnWKwKpXi0U%%}}YFah`HbJJh?c0tt4K8A?04v2A-ccxGrgE}$sZ_#K1 zn0N(LY~{5FpwLH&Z}jdI#GET94kNG^l+6Iv|pB9(A2C?_U2q8$3{6cffJi{Yb^alnR> z3P@uVrFVE$2EMnRo3qK(f$rGFR&JF>A_Vi_Sr(P5gKMIf9Tym?fZg=9;@6-$U>9N~ z-?pg+GQ=6<6DL~1egnlrfv|SCa*w+45O*tF7N^>`t)LEWryPq&+Sdx&kx5^^t#1J9 z_R~#1d%A!^GHaW~jvnA>u05!1*a6J!r_zgETL4Bjy)91YfUWhEa-S!0vrXtPcWB?} zgIabk$`r#v=yJ|{#^BHmN-AC)HXZG7@Z>&L%BgO+!29wl>yCD~bW!wZ^0}9AcBzaz zUu6_xJiTQ#pY=gcd>N|?M>jBiQYK$d(GR=c?tV$8-3cd0KPr-?kAPa&lU-#eCSdC> zVu6z7OVGWCJ|35M=z}1^2HD*khM@7_qW$IvT_86SqJBAJ9K^5vtGQD-JbnJokAs%Q2` zMkRq?rrDyUN*?ItQWNqys6PB1$|LnNy7JhkmBnt(_`Bi1}O`wRtu0 z)?SpAywD2%2ENfY#U1dlbkBjgy=}Mvm*04qH$VnMGo38{_MG=IwXl(>2VQ?-$>p9- zcv!(cJq>aVu~>#@mmgzYWh=x=m+vwt&iP*2|YevsG)Th49| znJFHEgLeLV=<@oYo}H=6mbC*CG#M`!@92On-YjVRKpTWBM+*Loc?n%k&rip_8HE%& z^Wz^mhJn1#%6_j$7qGOl4wOmvz|qIL+cpihgC%qLxo_`B;N#D?AG+^MfYgvMA@ax= za4FU>R6m;L2;@l5Q2S(q$Kh*3;o6y?uz8_8StJGM zn@Lj!^YPFqnJOe?SquUfHb&MTt%T$Y#b$}sKCvI%a;t9_@^(NT1w+k_ie~uyFk~@t=Yz?3ljptx1q~F_rEf z9)|bI`5x~_dO&9U2djmG-T*9qS$eHCF$PZ*i1}h$=OCoWYWu*aR}e&qQ6Kjmg&!kr zO$TTPA^F`%cZBILyPbQg#P#4&CdB%>8eiC)3x@~FDq=Kq0AB2TW678S6W=u-q#Z~A z2EE|hca!3w$-p9b9aSY1-bmv==~@FWEb24<{tQ>NAFFGF7U(gT*x3RH)dEJ>_0$mIc#5nafm9Bna)}=wE|r0|{H5-9 z#4c!fp;Si9?gh0O!=ezLE+|5iH`x4}z@t(7d0s#*bYe#hzfIRbv}|3TWA6Y+WxW~< ztsMprL-FtarTx&HYMs)?(GJV@TZ3mFH9^oFue4GYf`>DKRUKqZJ?*X)T z=?rAcjsO)$nb$0-3+Vle^;C-6;8eeOR2+3X#H93QG;>VA_G9j{)R|L|VdBnpv2+a5 zOX{2G3i{x!XL-t%+)n5S33YxN+XY+c8m`wnzk-tNIf^KjS>SN&GJaJz0Z~ksemrCy zg7E?4Z8A$e@Su|0CRZV=59kbo4;Eu{U}@eo)nN7p)Hcd8)EZ5JjW}n0MbijG(QI_B z%o~LK++#PRq!$Lh(e!y5rloj-wBVhK}D6I}c zi^23}PpCQ|PH2@zK6dl2rJ%?QPy~}8-f$Qe|H-CzZf_#$0 z#;01NAb54Td2et(6r8ckGP=Du{a}5-eo~&)zh*awElhLQhYP@{#r1X@QvuvfR{3x_Hw*Uuw6K!xiG}AVL`T_+qE=lpz2w+U+6V}2IhE+ayLCm~zQxEPM;A@BDtwZLK* zsTO~72qr&q7=7R$gtrNr`sapm!^dlQKGUm#_}gg5_0S4<<-4vZsHO?50`Cba-WZ3R z`wdes@HMn~t5d3mav!Af?9)g(-U!tV`WxznYQc5Dol>p44ID;pSnr5=1=}W#&X|T1 zC*iH&zJlDp7wk z3rGRyK&_EB{ycbmgErtSWiw1qWyF^!)Penw7H7u3GEkA!II}Y{9hiceU!O=X0N#TF z)3@oX!GPU-zi@aDe95mA-tw*kwmEha`*3K)(n-QVM?aqkx?iq0)^k)q2}9lsrwh$s zpy9uK{qP95knL~XXgL7SsI-(QB-)^df}&eHvkc}r55+BK)xhzZa`u$+4rt=s#Gu|j z0dKS#^@LqVf!=)Rw8A|fA0Md)hgTn&9M1Pb(+^AgSCTVe>~tin zKpMB3pP)0BKidG5@bFOzU8#rG=NEa+7nB>=*;D zVmwp47y}WLPC%DJi?)_eE^A4gZ#Z*39yavCC8`&49$Kj3veb0rM z0eG{=_1c>woq%QATYe~;ByC9NRd!Vce5?;vph0oDG%dOp-=Oh~PQBBBAp zRC1U!v#c|Lbhwj%I=sZe&x8L<6krm8>(;=}={S}a$ra9`poN7)h17a_FForY6xWAF&&2*3DJq&n z1p9H-0H+RdT7rR;QNMwnZ3Vqvl?;alS0p3pjtTOw@e{AVN&f}U;&}ZQlIz^wRmyLL zHKW45P)M_=aL)fzOG`->Fw+j1S)DhoZMNR26-ua9$^JTr*&ML0T%U&odbt*o&PVAcw33C zX(=$8qvaQcLBGaM;8e{oFk09_XN|&evgce=rA`>9z^eVM`Z-r;A~q!3P9OwD|NRIY zbKX-TKTROL_GV?k_#f|XUV)!`t)%h!GQ5-RUb&i$+s*l3Mq)rh`{jQ%@sE04H`s_` zXBiFFCXaOpq)So;d9Tr^D|Vx!wYOh29On65;QzsD(vjxg+w-rD@;B@9j}_N?t+9fC zRB$(6aCO2Lngmx(w|7i1Uy?IOg!eC)Tm_@P&N_=@*=oV5yVhfV?I-gHOn+>PGg2hG zCc$6qPl)g@ewf^3{qTS1-F9KeMv`p#-i;(bM{E8S@(`n0I-W}4`zOX|99Q_3Kd`tO zX^~~yNz8-5>%2;2P)hPPi(zT!c}vA8DCn`vUc8tXWzeG?q;gJ6E^J#mFHWsLzj|OT z9Icp5MhJ|ZI8go{B3SpS92H5f^ODe-GFH8Rg(4l>Dki2vx}Qv7TPKL$vqtm$p`7V{ z(L9X@*WzDLjv43O^suq%P59G-oJn@_lsqeUGjrkwyYz=ATH|@JFTOi^2^4yinBaz; zVk#8I^w>U=odW-I6fJse>C^5te&y)_{#9b5a)b|OByj%b*$2KJ0;N?a4W@6;pN3mV za_9e8h&_xS@0>)BZDitJ8|HP!8Wq-9{p*)t5_5$lW>wD2szRE}mZkrXxBMo_>9AIF z7ViHccK+o_m>df$GaiD<|4Soa2Xt6hJxbD)6_*ANm{;Dc!z*!78_8O>!q|D%v91N% z3toY>V2!6`>mvM{xlEK6z{x&6%tK9JoeHN9^IucNO=Ubo5M7PfJK|g$elcOn<&s?g z(kI@lxk-GP!0yZNi#_>$JOXRdlw(&B8~J-&WBT88AicAivx&X)^)0seQ59ig?y1g; z)46)tr!_ zMvlXJBR1i~n-#j@=U(2@28#jMvE*5IRj3?&vmGlOxDkggf7#f`b5-pe63p%udJ zT?1kGyd+8*?hk!NncBGJB0lKN6omB646FuVCHE4af0_vSw6*fI>gL~PU=>>xQ zu-(t(=RKzpF#rB~vTI*1Qq;A)BBMa`0nI?Z$)FEauoU(0CYH~u z?i3#dOUa}&^Fn#ZAo#J)hYMUiUG zOle5>V|h#1bR1&3{6OCOP8Bqs%MBL~lp)XFM|(Sl!r-*xN85dq4d6YeNI#l_ z|L&HUtZFw132|>VJC6;3*bMzkP`-YT_2>$a4Hu*Ly6XYB6% z5#NipKJu`75Htk)UfU#I8hnlv*eI_nd6z*(lKY8!sa;@|a)v)HdkBu3CbSAQje=>o zt4svWGY!5kdnue9k4CmUxYVRm1*QwrRu7uG!-!WWQFy$v`f~?CBn;i17GCePjrtP6=tnlaGUM(<_nO4P%hDnd>pl z^DLwgG)}g|x&qX%E+$yjbwaE_y=|6MKiD@rh&p79!r0_`q+*8iSh=4mPZRUe;n`zC z-En1*DH&qSPSXq-#1|R?TGzUOGylYM2d$SNQ+#_m5l5qRu4uArFXf?|)$KA>O=TdQ zsJZz)DhBo|<4!`}ZJ;CdjK0LXA2uoq@+&Tmz(`2&(etGR$d*gr@RoKe823E*tSM9n z{_V$kPwi|63V*4iN`gnpGiPO*UZ@h!e|M|RD5AKA8o}=P&MWs z*bU9Ef9SAtGRh zQzLcydO&-0>3UO1Hsrmo8M3?I0u;svk0#ppfg_eKu@1+T81CNO_n2xF=I$3w(6#hI zb18*KhzACj=8Bf`&$I$Va+Y})zR$6!Hk^^iQFiW)*yd~AW1u+k)30E;9|l;0sM5Rh zU|90U#)BEn;ImwsI~0x6G2KKRtse^`V835gdN+>q$u*6bj~fg?`vmjkPxCBLEDPPK z8(#-Ky#p@m4|PGMds2hm`+!(Jr3rVCV`4(0JgaeUUpQ?fMwC%BX@NxA$Wl4 zaCby2+?QIvZU5ULxD+XbvM0yD15pi_DfL4>Ny6?JQ9A{5iy;oHgyVxgc5C4Lb#+C%HdAg>!F#w%4)}?#p-7jj1-nY_wm%X%`%l zU0A<>M)AFIQUzp*6~RWYOEh^AnXu#WyaV~o7eM%+qw#*J4LBm7xS2P0fOf%`2I4Z- z3KwtWgfWU$!bv&}vT~0i*dz3H=Fs*=2ot`}VWr#&mfZ=QZ-sjx%bP|t|9b}re5+x; zkWmXGJ(}UH&gEdRFuKW$q8T3fSzcOB>wvq5}zkN5|PHKml(+bQ#RcD2MlY1zx9E#!S_kCj}15@cFA%x&Y)ht z?@*p4)d1V{i#@||Ojl`gdc7CxFx1;wUp;~+7Dh2PvOPx{p#4qy<0~i1;Qef&f!*C| z$Wpb+_`9<{%7qayL4h49|xX%0dA) zJ6geN@!5RSp<&q2onStR?}asZ?b+*{YM{N^kSW_E0Tvr20EH^0 z4l2cA;8|8s9JCw;&u`WrB0DNU_(Ox`Evsj!LXupR@DcM=7WLPq0Cdk zdcb@#VJz@KHJnvs8Mh5;1fqa#?bKJ(RyfI+E@J532sSp~f>=1p;4UdXQ;9JYtdd^- zusUA@4IJ(7xYC=zxVYAwETbJlNF0RM)NOEBKjqODJOh$hM-pdG!~=S?(`m@C8o<3y z#oezFJU3e~yuaTG!Gn6!l))XayXsrJ`cMV1srS#ZZcZk`lXafY{I8S)@4jIE&qVyR z#E}puVrUmcr26KyR&)XF(d~+j?PURKmcAqNRE zN}OmIskrTC>FWccZ7+GbSY1&-wZ460PZ{Ekj#zw@QGk9dB%YAdXb0j%+CvNfKG2jo zZ&v1Ae#_CvmYSzUNR)3VJ^Er9AOi33~f3H9V8M8v3@- zkvWu9!8$wmnYYNepYz3>z7*i zIvW3_ioY7RU%%G$Bp8FUnA3@uQP#Lu$k$dHrlXB$c1$Cz1{@DOp5{vgDv&DO}SUy|Vlz5R%RGnQvA^qtwR?E~osez;S1ZGkY}tX5ZDczrw5;CU0$- zKfk*i=4mb^Hhg~y75lqw3EjR(!h4SzO*Mjx=haQ6#_PfC!GrZr=UU-?=Nl;-`f>;? ziA^7(e+<1(p6+s}B$|SW>-e!ahYZ-mlxXzkaxFBR&alvvYKLj-0K4R$#V~@OweX&Q zh$s?Dnae)ihtWVmi&y0&R1H?w2#3p{lzf}Wb<_^W>OR_iW6go_NB)J6nX-}9H{ubl zZLUz*Zy}zxz8tk5pj8XYjfTVDy}IZa+reT~OEHyb?+bHGP_O}2qs1n!6tAa#=u?+~ zf1zqCnwQ+Wn~SRiCAZue^26|)7=6o6sn7#Um^E1{)vcxn=4C)78SQ~wN3!+5UBdv={w(D4m5v7)tZeL(25>-lQOh~K(o-pAwpWS6(m^XUp)4mi?^(eGH!5f7H z2qJvXHszrJ=M(|wNDPS+7nc_JYJkGZ?S9PtDoD?++nQ3C3wIMa8m`;lXMtqg)TKRdyj}4SaESbdzq{lov|hvz&b)KM9=;eP(s&L@1&# zL$Rke7DM{ogzFwyJ$Sh(9`E>84i*~3B=bS;5H!S?oqEt99r0h)cYI49f?TMT_iPG` z1#WWZ*If%Qz;?;&lk3Xc;7q&zRkav&@6&+oXYM>CoODDdIwl$gu2*(4>d8b_*4G-7 zy{e$?_L74^a1qS&r4%FFMDvf!mo||~(e7;ekNed!ke2MvNNc_ZL_?gJdDCE32ri6D zJ{!&^q7ugSy;>hDkh8y-&#v$)MCQEt4ueZEa^qS}x-eD0nuL+pT2e-gYe^byT}#?% za4m_WHv&aCA<9 zwyQ1+a)x4V(tQgdqAQdms(w|^(W3jY!Y-Xaqe*xy_UyxDE);Xg5u z%xNNSc5*APFsoeSf!3+!DS!vsEZPO@Ihr2iK643OJdY_r-;+2&@&WTywPC zk&N4wh9AMnNN#vaWcboU5aS4HGFD0lRiTU~ctb3Kfa{~rnJNk)wY*-RGw30b*wd$m z)HBh}fx)8VS?-|ha(JGOG6RZ6OK6*(X98XL$?XCMvVm$>?^9v<41`B@F*8pQTBM$` zEcx;XF|iz_D$R<8nn^^8vhYG9p^Wcer6*NqZC;DO zuFv_1H-e{UgD?pV&g4!{*kysr6Y~={NokO5!gQ=HE&&O(eNC($EI`|CzaDOnN=I_C zJ(Hr3@4}V@toOP^E;N5qCBEqt$bmy2hQ8_De~RwZ4A`az=c3u9sR?3NG%`yS2zzQA z4ktpmWHj$(!>GV<=bQ4mu;a>4+q+Kw=uZBYRyCGfl&Hp~vwS%Wsb=l$``8`b=u&tNi5pOF z#~M8J*5BA4Sc}Y-;IP7}RwPh$PNYw+7B#B~y)6mOLvaT#S&e_IMBQt9AhdpMPlQ59 zH}CMX5}&MXozVQ+RtlP)deevAuR%W3CtW>nH>3WWXLcE}HlnOs^m~I6%Mcl#v|7TM z%TTx3hSlIsB_!~>JP6oZhKBDmibUtuqN-j`r^A6&h>6tx-04vk;^mvi9;v0kOR^#H zd*pQ>^t4-Cmn#qP{hYUczFba3HSI^PicpoHRwq~UCZ}*z7Wc-ma{~q^?pT;tkvGB7 zL$$G`ld0(0aBp16ks|b}=#~6|&>Tb-G-~*o?l^j~f0;rO-y{Aw=*7r{bIUyYHJ-Vh zjYV}Hv-j&yF+_5jL6k=^gn9L*blQN&;s(Kv=G03Zd zFjBRci;{(69~M2#Mr79xV0dd*)4923z|R(XI5(#tMsp?3chW^j z<9NaPpzvZucgG|~?_e}Kr{X62;{pZ(!BMH_7@B~+?Y^K)M;?-x4`7|GuSA-o*r(Hh zHRyfX-nd6Zs}d9#@l9s0UMftS`w@QUVjZlrG0KQ%t3WbbcTY~{HKDA0+e1$(+L2Sm z-N41j7L;l+x{qZc4+7;}ovE92<;$Z$+S+X z^aU!TrFnP1rWy@B4If5)IcO(qq1gg=3Q*Kd@o(B!hbM48S$_PGQhtGtw`OMripU*x z7SJw5wxXFQj){k(v-`7`*}C(=ntbw#)<_fRFi#S<=DPKOo4?ePOo0-_5J1(`F`k2j zFGP+Mc{#(Gr_1ejd1cU&pu8truNCr+#Mb8Z^+HNAxu{T5KB_q5$~}BL13mkhKS`(z z2m77U6c5d+;Hs~_b;j#fxVGbqjhRdz3?*SU_c#lX3@J77^t~)(p{!B1fiVj9i4*1b zH2BnlNRx-z;&LnWMQLq+9oGvKA48Say)8xJS6}5eE*7G)#o^{0;seN%>G_-kbs(d8 z#7sN773PLx52U&DKml#2OV7P(G)-qdR7R>qPijTBOmBOJF53%8HFLiJp+jG9>7QtW zVC=>{p$DC?)02YRX>${z+J5Q0z>rZRqBEJ)r}i&Ii#s?Mxup?oT~}vJ`q2kPuQwQ5 zt?z`ue~Ry{aP&Cgx>TVgc@lQ+nbd|=)YM~0N@Nv-E+k5?g0qfE(wsP;durqVLak3; zNe}%O#m8AIQIg?tePL4TS>5$m*(>S1|1vr&Fh54Gt@CO1yga?2+$xwoEiX4nVEl7L z9Vdj?LkxG2oR8~EV6%$4YlrK@!q=xuu2i`RvivDtTO)8b#B2W|aN=s$|3z1=K-bs^ zwGbCxN1^%uD`onJc)@!sU{3pl)~Fy$ccE3(TAL)a%7bi56~c>1ZeUd9LOlf14*@y) zRi=-$=*>z``pkcnha^`>&VY$@3#AeS|KUOYDem}1U|0!YxH1H|R_X?xQf`_etX9;m z77xTb>>)i%wEjgx;6-)gIQ8fFHztjv*i-k!gjFc5aK_+`OK_nzlpX)Z!WM29;MF zb^0Y@jpzE#6~ZI-n#AM^i!pk2JlE15g5}C?zVX#cl(!w1ele>|{|HEBPLfaEdkjmg!-?vD z4~K22lcnVPPq_-td#{$VNMJ&*^kWEuL9f{U$^?JL#nt1+X(fxw`QOb)9j);q zB`q5&l4{x^iGK=aT4#l(SH)bV|5)kd1hDj{$l>96?)-)kr$+7N_ z)BTe=XxHe))uOkbP2y}6SSgD{7zge@2GQzkT$k&JI!=puV@4m;S1a5E`$TwDI3N7& zM|ey$FKk(hudQu*V+5)d1{5niy$g>g-v?6ESdFqaUM7t*h58?ltfuDg+T3{1(42DMR_kCCl%8-EBZ~}ybdyZh7+-gI_L7|HXlxKP$sFs1+o{kx z%DJWCN)~j^wfcW^$pn@1N1wb)6QMPJ8*SqBW5}qUR9Gh%1^tNjNg_oaB*pWJC1w;s zF4uOI+uj9uanZ#LcP6SVsJ=cgKI-rkWYoq42HO)rEcDKSmSgxq?o9)8WyTdi!)5+n z!?+C0&gUlHbHt!*>v&C7K@>c*^V!OVZv!Kn#I?g-R)X<$Gx9G_YM|GFt!Lf-O1MTj zmC;640vFU}wT@)OgX+g>TLYIYP+p2YykRr34#K2}KBL5Xc(Z%Z)nBt3c9;h5yBt;y zO{Y3ERox2&m3?Fozy64)#Y;m{K>%vy>uGiUrsH_pvn5zVDjnzS!n+8!XzkKCfp&Zfm{CF@` zuzCpS7*}ub%a4Xcz87B#bz)$9wlZsOV;Ja$N!gJu-v{gN?QQqAxS)XAH-W7MNpSP6 z^rxQGY%sEp*EFBXhWeV9T{nGFA=YU6Smui$m^gkJ>%gGUNrdgobMI{8iKVey;L|FvVQEs$Bhic%WMdz7R z97;7k7Cq$TSp>E!7Bdq!V&Ge%P{h-mDD<>gor5a0971_wPjjIr;L&b6CGoHha%|k5 z9||ginOVxDnyv&$s}i|-$~OU-E-Mv9;Pp|e9pSMTi7(nACG$4xjaN-DXZk&l&Zhzf ztdhUyh(Cwim>``k2h&h!SxN1UeGOn#Whuf!?1qAUH5p{C_({{MruZ8Xm2jm{M@(}* z3)*wCBiZ7!5NE?qyP!e*py9jiL3t&8(1_g4wz;-}*rf}eZcqgxH#<+Ns275FQTmIm zX#u&2F0yr&NY@3$D(|@TumPB!#N_Rk+d*O?L^%6$4cK-X5ydUaz?3|E#7!)pw3lg} z1j+V|6+Q9A$ndOXOcc~DYAuUSMuQ3aIa9HZ;V>m8d063*4^+<+;`Zj)Bcm_gUIl?c zNRHO=@kZ+`IAKm*>k^m^nb><~ioP^(I_0;i*g70KNU@&kAKj5z%q5Lw{W!GYdF;+F zb;ZDcv74N1TPf_?IF|e|3$NTv3aLfOvv^|T~qa6;VE6nTGH^a~P1!ud(N?3Z= zb0cpo1eFETL?$b|u0>j8@6{iw^nh3WdfO+L+rf5Om8SS~8+5o|q_PUE24Nj>fuskK z_)JZ!_G;Cy7*BY`cX-Ls5<6l0WDeJH z4L{T}LJkVALs2Mo5tWc=2Dm&p`^0xwDp0?5bZK#p1|kp9OHt@5h~=y1i#J6gf#xAe z{izg`Nc?7WGynsSFBVT~ICDYs%La{$Z0R8MwrF?%MIR!BbR2Yw<4Qyq%&eZjCp}5upru&jwcxH%=p@IR3sXBUFm7I(UD-Y#G$9HR;PfcDNOnrRS24U z$HPcJ6N?@*KKw8y5r=4>m%9x)B|%HDwBh=p81OvtAXK&CF5I{9{jv}oO+;!E?*_vZ zGEk|Bvjlqd92uP-l=2WvgYP@d?T(wKL6+K>iq6zi5IQ&V{?XYa)bGOVJ(-t>s;4cN zq!Tj{P0uOTowVt2$NbVT!(1+W|8R{`m@OH|j8f&Uwq&4f>XkayvZcss`cB4Tdlt%3 zJDX}MkOIw{F37Mq6H9?HHMC|!P%f0|HY{u4R%T>5lX=Lc61`B*+S5LniF(7!6-ET2 zz@yFm9j9bN4x6K^OoL^IX_a-=$mr|8_wTA533GP zQ_SIvY-fe`u4w%Dk8+5%VOuq#zU+UL=3Ofy6Qs3e+M4tXtxI^t6Y7|aTverYg~;3B zgG|hTuk;%`QrPq&9l1p{*{i~iRajC`OX231zEqqL{-r^<2$1JBfeuzbZ_Z?%EwiF?ujAhOm zsa#ZH_bFCzC>fo7e)dq>t7j0l`NSuyNn!@1P^Nz}KM;pf9a8qw?yf+7k9P0>{vi)- zs=W6;B`Xazy&m0uIwb+ZggEHFs1*Y@Mg1A~?Q!VX-P=FyzP~_hi%lFHe0eC3=T=SO zLI$ekCVi13&wzBTZcVkUPjzJP+ifK_;1!CCC zF46lMAy1cuZ$A$`MTVS0@2}KGgZo&pX`yZgoJ78TMD}2G`xBR%*4r9XK1UJ6kAcA2Bc?m$Mys53lzSzBSlgD z4w~5DR`@YzL`A129(RRE`pNz4Ajps z7?1Jf!HVU?NxJry^u)^~&zzWoVALXgCMhU22yHZG9ou*7Aqw2O`)Jn@8>GY2d9iPU zE4-*VH!o)R7#e7vkECnPx|)Ps^xr#HM%*Eyg|IXE3@jnAcMBUEw{JWw zc&YFzRcE8V<6CUaI`h#nS2ppZdvV&qq)=$c%^fXpEX9iUL<60`o0j;v6zJxp`agVK zc_5YF)29fLgvxTWhm<8GTMm+>LfTUZg|v_;!c7}lBI2>{`;y&5$|OmJc5Qr1n-(dO zQcCZ+_u6m2*B|#>&vTx$&73*Uote*!zOiEk?7GtBojvCY)LC1Y+7eap#3+?5h}#p+ zykK*@`5_N!ow3{)+f{_$-={ZjlP`zN0RDqtDyj8w=has(^?d-V?rVQI{Kya~?G#~W zIbMoB*y<$;3gd7Yc6t1}VvAte4ZY>L3md^Yj)nabZ#|TLzQ>(^_!ROWlD6{7RHMQA z0sFk}azr|+DNhwhg)Da$icUZi@bq89wT^WSg4x}|-}`g04>(B*c+^nKS+o~YVtvqvPIdg9TC zoM%yQf8%syS51Vg$8Mdd_m2Pz4~y-CdAlIxR`#AC&0zHU>(;a%?XgH~u(Zc~VKTb+ z^govL1s5Pl>xSwI%?#kS&Qc&h`-0--v+Fs2$D-(q)@qgkIp~awV(YHHLKISXYW3+Q zByjk&{{9$L42nU6pY8O6AoU{kx1M`C6_v9%oOZG)LA5)Rl7}oRP$B24Rc)34$!_12D=I-h{kb4o$70nQQd!!z;n@&pStOb~M z?jy^NPtj1+#^t;qp#&{%Yo9mhRgLV8Gn8dm0M#DZzQ3*cIy_q}CSLN!&Ec^p zV}Ppl-f$OwF+Ig%e2-1%5*oO$XmONC9WuVtA8ZG3e&b%xZ z{zbivswjisJqj9LTx@k%Y>;t%G;LcE=ijwf9blx>oMQ9_*|pFjHv0 zdHrRS{7HUxl;<@%@#bRU2~*V!qa@O%60rT9PkAAyJE$IR*qGZFjOGRU9PJWMKzS!N zTe-`fa31O}aDVYOEk<1fO{c?I0j+2|mS3-P6$NB(TN9XHhy1q*uw7QF zhi@W3wmV2sqrj4rdQm5=6kW;4;k&i!D$1H=Z`sA$fK>Ksm(Q-gh9nzTuGnpach0S6 z8f8DCAXm;TC1q9JYFWJp$|FsSTov!6=IJ zV^vLS0%Dc7U2=^#4W-`T32mF7ie8m3mAEYsgS=t?P*TV#__5KcY2AYaR69H6L~mvR z;(yDPc4=P;nklEgwA{WJg+x&IBp${g4m69=Oa5Sp`|tV*gQP4pmwcncv9to^o>&oV zs)=@)m8!3z zj##|Ay3SqHzJ<5MB(DWYrOj+*5xIs|=m}eYUxi`g%GwT( z*jmoQrXLw^Fa1}JBvjZQ@MHJjKOLFd#3CM`9YHP$=K`>QRxG>t@uxRw#)cc!$%qh!4J9iHm1Jchve|Gaqo4VMmKYQ@osO346QGIp#`X_?iqr8As^mRJc+;n?l^6>?DAVK6lMOlV=cJDFS)^Sr9 zZ$KKc_;J$s4&%2LNzL!(-p3w2ZNi)A-h7Q>Y$!-I%XyL?KRMh4}A@%rIgf z5TY9d$vdGU{Cd2j#sC~75nIVh@&ql(-uWV@89=^6!*M!ynv2kk&O84xiq}mdboF%W zT6L+3NFtpjwZ zp;D1l$N0F4>^SJ)Vhryh@A@dDW6R~7$pgc>aGNXz`oK_)0u9Yn`Bni>;h#nj8jlke z_ZPE~^ZuAIY^yF*X~x>N*~}5brfSK_Qe$-8FxTPX%M-=678C+w1nD+N5dy@o{Y#kiW?!yu9 z*oc{td&K{N*Q8rU?pDc3kWOpyo|rMZUqiTzZHQublX(mtehbSA1bIeTH)NVI!D`Pg z%D8#;A-;*3*OO|74S{tdrZzn9(WrWKy9!-uEg}(aX{;Z1Q)1RbSs7BbWqpRAz`$fO zVdL@Le%0qV>aMqWE8RLbR~liN;Cr4NIBQ5@fK74^w)S9q`isaNSia$<^E9Jz)#!XNMMgU2A%JxlwYlx&_DP4r;@=02 zh=Tc>7cuO<6C^dwXiH+F?KiprZQEji!L_4%W<2f3wk+m(HiB=HFk5uPZfsB1M8j4` zW~egVB7C-egTxr7H17T;2JPge7cwA*Ecu)W-B{<8 zGlsIsmrfg($Jr|Q>yBJ+_WbN-@nTRxwLRT~Q<<=4f*vYUUs@eeI{OUP(X6Uu@w)r0%z21h~i&^+;W ziak)W$uEIl{iyIziAzd+bt33knzv7j&4yk7twe0=3LsN$qvM`=DUc)JZ6xMlhrAzX z_$5#*z&#-CWFj>cLd-rV^6BKll&&c%k!y~RGBP2dWZUv(ONwEWz3n-ype_cTP?e+e|C~dw-=d}!EwM;5HHmT! z6~os#Z0PK;>V;;r66u2YxC_pbI{)vGny(B%i)=O zz_q`(vd}MXDVJBD)1h&pwa#4eS`g-G-AjpcLr-QhO7j_vvX~2(RD45b_=5a!`Lu$o z3M7}H4+s0EaID&h92Wc#;k@dA_GR+3&60LUD!X0yWxA=TB-XBdMfNGgTEUieN7@AD z^Hgt4verd&!>pW5*UO0tfqUl5=*{ z%|08^v|Ue|%cj}F)xy6)Zxr5%V?G@B(1z>U7&)j%U zCYU#x?+`u_1mWqmXSo}zP}&PN1y7N;7T&Y+g(0l)NJ|MZg z#;*&LAz1d+#rA+&WLBl9wfS!aiuPTtT!;0yB^JW9>;O=1&ECO-O%=Sup4A z{Rgh0^;lxgmZX+FP-y?WF&XaOXOv4h9-JFANd>i~{DorsQlWh9b*IY@Q^4|7ppAiY zGW6ODuXOfIfU28vro4~h;783ashP*IZ*j(at3l^<5J>T?4&qCLuGgF21^FfdtK2N% z$J?So^2MTc(;r5Fr*Tb!$?SAE<-Bk2-UXRdn3YxXW5c;L_*mZiN5Vc9dQ_HX<>Ed3 zNbA70j(t9GMCRo2x1QuiK>sZyLCZz>5?4N z!~Q54J%9J?Njex_+bz%rh$peiiwgO+bM8UZ`Q`|Mw4gz7z?s(};B6{g`{gdeZ3&^rUW|hT` z3=qBb+w0@he9#t8yZF;-C-xw1yTX3y1bU?~sFI17L^9Ski?$|bzz0FL zCqA=tq01tOyS^a?-8^FWE{uxR7ZQuMi5l%MKt3OPFTYySfPNh6YX5d16Ry0p5VJp< z2~*$fo>AGAgSJIeoewV0z>aI73gH1IDD|aDH2=H1$iO*o2bWYn$PAY&hRcxQQfj!= z94_^?A1}GDbOHSKSvXJ+%qf7jy6Pg`kX%@G?1SuX$OZ|{D`K2h*-%0iifj|k0fV#O ztmjk|1AAdYQ_6w@=y+p-WaePdLXA{U?3^@UFC_&YF2W>P>z|Zfl?nYNe^R3Elz_h0 zc530fT)0cTauC%?fn^Cx1SGxBgWH}{PgJ|3fkPsn>K7B40WC65P9{$;g%LLtdBU%DWE@?NNQ;CKZFZK`Z z_xC~34ctkZN28FjIPb=OfiP6>+?;$QD;EkJX57t8DTK{z8+eXLA41-rc6@!kCKVNb zraI(5nwpKiHEpn~kI6)PL^kr~yeo!X>8YW!o3miIoqzB?^$Zk$DtX1{dBtd*ifNbr zcf9njb{79(5Bm7Gs&-x_mEd^WJW+ODJbe2cu>O5PIr4aLv{v~{C3=63x8}P+H8Q$6 zkVgrwMfsop^V*Vo2|jPAbSigW2YOW7z~((Jb!g+k^B1aZNOVW8W=$u?$kk6>p}SZR z$KwqrKe*4h3bI;{;`-G{&>Jr250|RL<*MOQZn)gv=QCe%bv1CV)j#JGQU$q9_J7NR zNQkx|imbyb;kbyBpiGs}wz*7#4>^ta z%TfvRZ(QK&epU_(cX2j_sFZ(#Xz|UDu7K_H z4QIWbQ3024e$!LxErCTZ-n@7@h6z@a)P? zj%O)>Rn1yivnwt_fB)wPm5x`zq+9Z$oH(R zVkmj#`>ee876`T7u}kK<3oqo(?Omf;4!QAK7EkB4!qnE6PdiI1z+C%*#x?68h;y@! z)y}vLX+7`CDt9~q_OHIV#e!wvQ+m<#9Cc#{G_V*)&;JB)c8L4o)lCK9J6r}2mwv-# z)^Hg$TweQS6T+HT3{)(>*&4RJQE0mFGwWL0-#0>~qs0 z=AuX{mrWUbOyAme>rnwDz4uX~S{_V+!K@3bMQVfLK*1aB`^Qg0mYq9Ap*a#Rsh{nd zxvCOi?@^x6xy9g9<-%hih{1}jv*m1N2cqnSy-q7FLXp@>U-huVPSF0oMlbbtH6-3W z^!%x386>wfvO9eWfU*Z0DBtkOXO@xDQWBPdHcPpp#Wzl)vuW1UrHSgbaHn0(SVXZJ zimq%qr$W`Gq;XKO##|Y?ZA-;L;eR^y&QHu%tSOsTI6e^yLcl?XsUL z^5Y09IIX_Y@#You>gqfF!KD|`rx@#eg`hN~c&AKRsrniWoZr>ZYTF1KRa&(3uLhxY zwK;=|ZZ}ZT3hJ%0rAM!#VD1i?vL)EYe8TcbZru$ynR#q^{fWEqEs*t4nR5u*_V$$E z**F|9R&xty%WMqBkAH3XS*{!@*T;4H>o&v5-62~qnm>lPGo*@I+G=$9*^?uc3Xd?v zHBDy-hm^|u=$*Z5cO9~1kPdAeF4s_pzib*VjYe(^mrt@zc12t*MMTLneX_9(-7r+) z&DmOp_UU$ScEFm`7RjB(0V;*)sZY#fJMV1te7(bzKkqA%*-I}KBi(A`AJ!lCwYLgm z>|1Rzp&YpsuX^1rP>2>w)rw9L!Y&%B%T+x<;3Cp7Z+LXes1_}6h~4aq0KE#|{p<3~ zD)c(kv+?-me6*!S)&FBa3~JlaO^A9zo|CRGoEy zP+KhB2cs+#4vK3yJPZYWF1H&p(P=Pi z_M@wQZDq)LbN519C;J=7{coL{lItUs*i>t@Mdc>Sd&_g)O*Rj}j>kDpt^jh(H-{N% zW}s`$x|Cl>Xiye2@`zqLIu{c!UcKNRy8g6FT)C?PqJ(t!ws@4lEQSb1Xt)$4g0IgO zr)C?ZdAj&kqb;S)TNOW7Au0CoDbMpr)V41###Xu#-LoQ(sj^fc+whlAl2?v2d(w*8 z-qfJ1KfZypdoH51`=K4%UXv*0HCKZrYdLE0c+!9VYzYcBc))WHmnXwnI=VD#(ITb& zol7inXnCQ{C$w@%wAbp_$wNDe@tb$y4dWdrb5Zn?emA|+e571hBWpFv3Lewpk772#zXCkUY4RQ z=lXTcJ!4=~dm)5+}uctCe|9a*n*4SkY0@~Km#7&R4mW&GtU zgFs_lR~CGxg)Lso))8F-&%S9mo!oW_wY=xNO1a;Fx*r;qay`sNJLhpONw2SiL-H<3 zmDd}<+4Z4E7WEo{V7u-W={qD^xp7s^-@ryBkQ|#b+aUw}o~|d%my5wg@>7j9PTT{n zHK!6HwQhhJuk?VD&<{iIOFl+L==oNKdO71f6kYC7p;w=c zl4sxAd_XJ*?c`HR(A$uMEW!$(HuK=vhz>)Id?$<0%(g!{$1Y@|YkP(cLS7p3^a=d1 zVn-HgH96G1HzXHzsrYvi^!F?}xb03n`mFBM+wnIF&DrIa&^9vz zO?my9=YUKhdfWY~qA;owl|?i=d0-w%`TpiPm$(-aS~5G|Hhv9K_;^80wIT&QyRt*s zE29KytA;&(b%jLb55DxwP02=fpDV8vT#bTDi_i96-WCssKcpx8WQ#%O4b%m9s~!}(xk&R`J;y*}HOJv#uEx`gzcJ6(&OR-IPX zdW)kTG#v^9`!IA?W~p?B$3>_+@PTc9Zw+wOYm{7ja12r$ri*51H=xc!mbn+js!{1h zHwVkilRy!!xVt0g8mytJUI`3xyAG;ZHDcMBX|P{pTE{Ij9Q~2;`vB=(4Gc`mLI=z<d3fn&n?K+Zt0BM95Y&Q7d zhTa_B&utKrfVPGR^JE34qiEyG*1u&0O5XIQ+0iWuiee5u6+ILP!XbOUye;;D87|X8 zOWdfbs5x+1@>`L7q`ua-#UDp<_1$!Eed5PV=ssd>vdtg`46|g-$lCMZQN87+U{ekX znD=l_CywBne&M$3E;($IobR}jpKyenMBpFH$$Qiy~%g|-O>NA!eCLT+W z@Z6S8qvISv{XxAf$72hgOLi|#`RxH)4WIFKW*>)l+ur{8^!qdjoe3QxPCB9YyLzW3 ziXTAA&%I7;xMYQ1d_B){r~D9*Z;q~`DrAI$ztNMLzI*2(vusCXqryJ)h7d6fP@*DU zvDOox{irDH?-7=J5AC2i;lKW<`;qY0{N2nM#woy;;-MU=?u@=h^9MXu3_(7!IjNg% zBT&4-j0H;)_8{|>in@X42~fS!*K+T8Hq31+<>n~ZfIb#B-)QEEK|31PP}#qfrXWf+ z)XXFb<{peNl`A2!^UI#pP;44KxIBte74Gc#)12sN6 z&Pl-Uru-zq{366P@BQR9Lns_%+0Cz6bz^@WH@Dmw>#I-{$Gj~(3AkUCKLI4{URGzs+Y-oCr4t_B_6yupT;Rfp#43ty|65d{x9MF+TYQ$ezq`m^_N zBaFuqV#vtf@9QTi?CJI^tiF~QHB`uugF=)^`eCd@RwnkYMV!RiH%^Sue{>uZv!WOy z3mJ;yC(E@uFldKWfE=n&pM&isx0to;W1;3E}PiH(y9A(5;+4o0?i`w|9v zwP3nF*6L$Z#o>jE8DB!h^)cqgnmnYg|4wj<6AziQ%lfS#chCqNi)L1$;VnA`jX4SZ z9wQM(o~e0J%FLLdF^(GH7$LGJR264L>Ep}|FiIbXx1`y%S1aEUBzn4+ zFj?76h#^mhVLycl41jlqG9lLICC&gXWwEcoro>Sb7G88ZsBfs+O`PoEQJ&-j;AUd0 zMq+~z!!)|YWXpR&p3zX9A)JQ91!uNlOX7y+)mWO0XJCvRRwIWJtr!n~(lHm-ql7YB z;wR8jp2XHv#s484=r%YEO&p*T;r1Q9Nt7C4VTq9pk{F`MjdOF3pDu|