From f1c15fc4b658f695623d2eee54ba7c2b0eef2b1e Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 14 Nov 2023 15:53:37 -0500 Subject: [PATCH 01/60] add simple circular distance objective for when vessel surface is circular axisymmetric, with a signed distance --- desc/objectives/__init__.py | 1 + desc/objectives/_geometry.py | 231 +++++++++++++++++++++++++++++++++++ tests/test_objective_funs.py | 51 ++++++++ 3 files changed, 283 insertions(+) diff --git a/desc/objectives/__init__.py b/desc/objectives/__init__.py index 453f4e85a7..c37736ff66 100644 --- a/desc/objectives/__init__.py +++ b/desc/objectives/__init__.py @@ -16,6 +16,7 @@ Elongation, MeanCurvature, PlasmaVesselDistance, + PlasmaVesselDistanceCircular, PrincipalCurvature, Volume, ) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index abd113c958..9a7766c08d 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -440,6 +440,12 @@ class PlasmaVesselDistance(_Objective): Collocation grid containing the nodes to evaluate plasma geometry at. use_softmin: bool, optional Use softmin or hard min. + use_signed_distance: bool, optional + Whether to use absolute value of distance or a signed distance, with d + being positive if the plasma is inside of the bounding surface, and + negative if outside of the bounding surface. + NOTE: signed distance currently only works for circular XS or elliptical XS + axisymmetric winding surfaces. False by default alpha: float, optional Parameter used for softmin. The larger alpha, the closer the softmin approximates the hardmin. softmin -> hardmin as alpha -> infinity. @@ -467,6 +473,7 @@ def __init__( surface_grid=None, plasma_grid=None, use_softmin=False, + use_signed_distance=False, alpha=1.0, name="plasma-vessel distance", ): @@ -476,6 +483,9 @@ def __init__( self._surface_grid = surface_grid self._plasma_grid = plasma_grid self._use_softmin = use_softmin + self._use_signed_distance = use_signed_distance + if use_signed_distance: + raise NotImplementedError("this is not yet implemented!") self._alpha = alpha super().__init__( things=[eq, self._surface], @@ -486,6 +496,12 @@ def __init__( normalize_target=normalize_target, name=name, ) + # possible easy signed distance: + # at each zeta point in plas_grid, take as the "center" of the plane to be + # the eq axis at that zeta + # then compute minor radius to that point, for each zeta + # (so just (R(phi)-R0(phi),Z(phi)-Z0(phi) for both plasma and surface)) + # then take sign(r_surf - r_plasma) and multiply d by that? def build(self, use_jit=True, verbose=1): """Build constant arrays. @@ -616,6 +632,221 @@ def compute(self, equil_params, surface_params, constants=None): return d.min(axis=0) +class PlasmaVesselDistanceCircular(_Objective): + """Target the distance between the plasma and a surrounding circular torus. + + Computes the radius from the axis of the circular toroidal surface for each + point in the plas_grid, and subtracts that from the radius of the circular + bounding surface given to yield the distance from the plasma to the + circular bounding surface. + + NOTE: for best results, use this objective in combination with either MeanCurvature + or PrincipalCurvature, to penalize the tendency for the optimizer to only move the + points on surface corresponding to the grid that the plasma-vessel distance + is evaluated at, which can cause cusps or regions of very large curvature. + + NOTE: When use_softmin=True, ensures that alpha*values passed in is + at least >1, otherwise the softmin will return inaccurate approximations + of the minimum. Will automatically multiply array values by 2 / min_val if the min + of alpha*array is <1. This is to avoid inaccuracies that arise when values <1 + are present in the softmin, which can cause inaccurate mins or even incorrect + signs of the softmin versus the actual min. + + Parameters + ---------- + eq : Equilibrium, optional + Equilibrium that will be optimized to satisfy the Objective. + surface : Surface + Bounding surface to penalize distance to. + target : {float, ndarray}, optional + Target value(s) of the objective. Only used if bounds is None. + 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 + 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. + surface_grid : Grid, optional + Collocation grid containing the nodes to evaluate surface geometry at. + plasma_grid : Grid, optional + Collocation grid containing the nodes to evaluate plasma geometry at. + use_signed_distance: bool, optional + Whether to use absolute value of distance or a signed distance, with d + being positive if the plasma is inside of the bounding surface, and + negative if outside of the bounding surface. + NOTE: signed distance currently only works for circular XS or elliptical XS + axisymmetric winding surfaces. False by default + name : str, optional + Name of the objective function. + """ + + _coordinates = "rtz" + _units = "(m)" + _print_value_fmt = "Plasma-circular-vessel distance: {:10.3e} " + + def __init__( + self, + eq, + surface, + target=None, + bounds=None, + weight=1, + normalize=True, + normalize_target=True, + plasma_grid=None, + use_signed_distance=False, + name="plasma-circular-vessel distance", + ): + if target is None and bounds is None: + bounds = (1, np.inf) + self._surface = surface + self._plasma_grid = plasma_grid + self._use_signed_distance = use_signed_distance + super().__init__( + things=[eq, self._surface], + target=target, + bounds=bounds, + weight=weight, + normalize=normalize, + normalize_target=normalize_target, + 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] + surface = self.things[1] + # if things[1] is different than self._surface, update self._surface + if surface != self._surface: + self._surface = surface + if self._plasma_grid is None: + plasma_grid = LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP) + else: + plasma_grid = self._plasma_grid + + if not np.allclose(plasma_grid.nodes[:, 0], 1): + warnings.warn("Plasma grid includes interior points, should be rho=1") + minor_radius_coef_index = surface.R_basis.get_idx(L=0, M=1, N=0) + major_radius_coef_index = surface.R_basis.get_idx(L=0, M=0, N=0) + should_be_zero_indices = np.delete( + np.arange(surface.R_basis.num_modes), + [minor_radius_coef_index, major_radius_coef_index], + ) + + if not np.allclose(surface.R_lmn[should_be_zero_indices], 0.0): + warnings.warn( + "PlasmaVesselDistanceCircular only works for axisymmetric" + " circular toroidal bounding surfaces!" + ) + + self._surface_minor_radius = surface.R_lmn[minor_radius_coef_index] + self._surface_major_radius = surface.R_lmn[major_radius_coef_index] + + self._dim_f = plasma_grid.num_nodes + self._equil_data_keys = ["R", "Z"] + + timer = Timer() + if verbose > 0: + print("Precomputing transforms") + timer.start("Precomputing transforms") + + equil_profiles = get_profiles( + self._equil_data_keys, + obj=eq, + grid=plasma_grid, + has_axis=plasma_grid.axis.size, + ) + equil_transforms = get_transforms( + self._equil_data_keys, + obj=eq, + grid=plasma_grid, + has_axis=plasma_grid.axis.size, + ) + # make the axis from which we will calculate minor radius for the surface + # i.e this is (R0_surface,0) + self._surface_axis_points = np.vstack( + [ + self._surface_major_radius * np.ones(plasma_grid.num_nodes), + np.zeros(plasma_grid.num_nodes), + ] + ).T + + self._constants = { + "transforms": equil_transforms, + "equil_profiles": equil_profiles, + "surface_axis_points": self._surface_axis_points, + } + + timer.stop("Precomputing transforms") + if verbose > 1: + timer.disp("Precomputing transforms") + + if self._normalize: + scales = compute_scaling_factors(eq) + self._normalization = scales["a"] + + super().build(use_jit=use_jit, verbose=verbose) + + def compute(self, equil_params, surface_params, constants=None): + """Compute plasma-surface distance. + + Parameters + ---------- + equil_params : dict + Dictionary of equilibrium degrees of freedom, eg Equilibrium.params_dict + surface_params : dict + Dictionary of surface degrees of freedom, eg Surface.params_dict + constants : dict + Dictionary of constant data, eg transforms, profiles etc. Defaults to + self.constants + + Returns + ------- + d : ndarray, shape(surface_grid.num_nodes,) + For each point in the surface grid, approximate distance to plasma. + + """ + if constants is None: + constants = self.constants + data = compute_fun( + "desc.equilibrium.equilibrium.Equilibrium", + self._equil_data_keys, + params=equil_params, + transforms=constants["transforms"], + profiles=constants["equil_profiles"], + ) + plasma_coords_dist_vectors = jnp.array( + [data["R"] - self._surface_major_radius, data["Z"]] + ).T + + # compute the minor radius of the surface at each point in the plasma grid, + # signed to be positive if plasma inside vessel, and negative if plasma + # outside vessel + d = self._surface_minor_radius - jnp.linalg.norm( + plasma_coords_dist_vectors, axis=-1 + ) + if self._use_signed_distance: + return d + else: + return jnp.abs(d) + + class MeanCurvature(_Objective): """Target a particular value for the mean curvature. diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index c3445d940e..3aa16c7321 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -34,6 +34,7 @@ ObjectiveFromUser, ObjectiveFunction, PlasmaVesselDistance, + PlasmaVesselDistanceCircular, Pressure, PrincipalCurvature, QuasisymmetryBoozer, @@ -697,6 +698,56 @@ def test_plasma_vessel_distance(): np.testing.assert_allclose(d, a_s - a_p) +@pytest.mark.unit +def test_circular_plasma_vessel_distance(): + """Test calculation of min distance from plasma to circular vessel.""" + R0 = 10.0 + a_p = 1.0 + a_s = 2.0 + # default eq has R0=10, a=1 + eq = Equilibrium(M=3, N=2) + # surface with same R0, a=2, so true d=1 for all pts + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] + ) + # For equally spaced grids, should get true d=1 + plas_grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistanceCircular(eq=eq, plasma_grid=plas_grid, surface=surface) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + np.testing.assert_allclose(d, a_s - a_p) + + # ensure that it works (dimension-wise) when compute_scaled is called + _ = obj.compute_scaled(*obj.xs(eq, surface)) + + # check warning for non-circular-axisymmetric vessel + a2 = 0.1 + surf = FourierRZToroidalSurface( + R_lmn=[R0, a_s, a2], + Z_lmn=[-a_s], + modes_R=[[0, 0], [1, 0], [0, 1]], + modes_Z=[[-1, 0]], + ) + obj = PlasmaVesselDistanceCircular(surface=surf, plasma_grid=plas_grid, eq=eq) + with pytest.warns(UserWarning): + obj.build() + + # For plasma outside surface, should get signed distance + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_p * 0.5], + Z_lmn=[-a_p * 0.5], + modes_R=[[0, 0], [1, 0]], + modes_Z=[[-1, 0]], + ) + plas_grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistanceCircular( + eq=eq, plasma_grid=plas_grid, surface=surface, use_signed_distance=True + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + np.testing.assert_allclose(d, -0.5 * a_p) + + @pytest.mark.unit def test_mean_curvature(): """Test for mean curvature objective function.""" From 3027b8d1e820e0514612142345a70bc3e0aaf524 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 15 Nov 2023 11:52:31 -0500 Subject: [PATCH 02/60] remove unused attribute in doc --- desc/objectives/_geometry.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 9a7766c08d..3353fdaa78 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -673,8 +673,6 @@ class PlasmaVesselDistanceCircular(_Objective): 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. - surface_grid : Grid, optional - Collocation grid containing the nodes to evaluate surface geometry at. plasma_grid : Grid, optional Collocation grid containing the nodes to evaluate plasma geometry at. use_signed_distance: bool, optional From 003deecdd117c8589a2c0b6de871595454d1ce1c Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 16 Nov 2023 10:52:48 -0500 Subject: [PATCH 03/60] add absolute value to minor radius fetch --- desc/objectives/_geometry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 3353fdaa78..3d1581427b 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -753,8 +753,8 @@ def build(self, use_jit=True, verbose=1): " circular toroidal bounding surfaces!" ) - self._surface_minor_radius = surface.R_lmn[minor_radius_coef_index] - self._surface_major_radius = surface.R_lmn[major_radius_coef_index] + self._surface_minor_radius = np.abs(surface.R_lmn[minor_radius_coef_index]) + self._surface_major_radius = np.abs(surface.R_lmn[major_radius_coef_index]) self._dim_f = plasma_grid.num_nodes self._equil_data_keys = ["R", "Z"] From 16018794cac8b519c321ff5d5adfb183f97bc717 Mon Sep 17 00:00:00 2001 From: "Dario G. Panici" Date: Sun, 7 Jan 2024 17:12:03 -0500 Subject: [PATCH 04/60] add signed plasmavesseldistance using normal vector and tests, currently only for hardmin --- desc/objectives/_geometry.py | 51 ++++++++++++++++++------- tests/test_objective_funs.py | 48 ++++++++++++++++++++++++ tests/test_optimizer.py | 72 ++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 14 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 60ea91b6ae..e1fe612402 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -7,7 +7,7 @@ from desc.backend import jnp from desc.compute import compute as compute_fun from desc.compute import get_profiles, get_transforms, rpz2xyz -from desc.compute.utils import safenorm +from desc.compute.utils import dot, safenorm from desc.grid import LinearGrid, QuadratureGrid from desc.utils import Timer @@ -525,8 +525,10 @@ class PlasmaVesselDistance(_Objective): Whether to use absolute value of distance or a signed distance, with d being positive if the plasma is inside of the bounding surface, and negative if outside of the bounding surface. - NOTE: signed distance currently only works for circular XS or elliptical XS - axisymmetric winding surfaces. False by default + NOTE: this convention assumes that both surface and equilibrium have + poloidal angles defined such that they are in a right-handed coordinate + system with the surface normal vector pointing outwards + NOTE: only works with use_softmin=False currently surface_fixed: bool, optional Whether the surface the distance from the plasma is computed to is fixed or not. If True, the surface is fixed and its coordinates are @@ -575,8 +577,11 @@ def __init__( self._plasma_grid = plasma_grid self._use_softmin = use_softmin self._use_signed_distance = use_signed_distance - if use_signed_distance: - raise NotImplementedError("this is not yet implemented!") + if use_softmin and use_signed_distance: + warnings.warn( + "signed distance cannot currently" " be used with use_softmin=True!", + UserWarning, + ) self._surface_fixed = surface_fixed self._alpha = alpha super().__init__( @@ -628,7 +633,7 @@ def build(self, use_jit=True, verbose=1): self._dim_f = surface_grid.num_nodes self._equil_data_keys = ["R", "phi", "Z"] - self._surface_data_keys = ["x"] + self._surface_data_keys = ["x", "n_rho"] if self._use_signed_distance else ["x"] timer = Timer() if verbose > 0: @@ -672,15 +677,15 @@ def build(self, use_jit=True, verbose=1): if self._surface_fixed: # precompute the surface coordinates # as the surface is fixed during the optimization - surface_coords = compute_fun( + data_surf = compute_fun( self._surface, self._surface_data_keys, params=self._surface.params_dict, transforms=surface_transforms, profiles={}, basis="xyz", - )["x"] - self._constants["surface_coords"] = surface_coords + ) + self._constants["data_surf"] = data_surf timer.stop("Precomputing transforms") if verbose > 1: @@ -723,22 +728,40 @@ def compute(self, equil_params, surface_params=None, constants=None): ) plasma_coords = rpz2xyz(jnp.array([data["R"], data["phi"], data["Z"]]).T) if self._surface_fixed: - surface_coords = constants["surface_coords"] + data_surf = constants["data_surf"] else: - surface_coords = compute_fun( + data_surf = compute_fun( self._surface, self._surface_data_keys, params=surface_params, transforms=constants["surface_transforms"], profiles={}, basis="xyz", - )["x"] - d = safenorm(plasma_coords[:, None, :] - surface_coords[None, :, :], axis=-1) + ) + + surface_coords = data_surf["x"] + diff_vec = plasma_coords[:, None, :] - surface_coords[None, :, :] + d = safenorm(diff_vec, axis=-1) if self._use_softmin: # do softmin return jnp.apply_along_axis(softmin, 0, d, self._alpha) else: # do hardmin - return d.min(axis=0) + if not self._use_signed_distance: + return d.min(axis=0) + else: + min_inds = d.argmin(axis=0, keepdims=True) + min_ds = jnp.take_along_axis(d, min_inds, axis=0).squeeze() + diff_vecs_at_mins = jnp.take_along_axis( + diff_vec, min_inds[..., None], axis=0 + ).squeeze() + n_rho_dot_diff_vec = dot( + data_surf["n_rho"], diff_vecs_at_mins, axis=-1 + ).squeeze() + # we mult by -1 because diff_vec points from surface to + # the plasma, so + # if plasma is inside the surface, n_rho dot diff vec <0, + # but we define "plasma inside surface" as >0 + return min_ds * jnp.sign(n_rho_dot_diff_vec) * -1 class PlasmaVesselDistanceCircular(_Objective): diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 4ccb9f3a3a..fabbe303a3 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -875,6 +875,54 @@ def test_circular_plasma_vessel_distance(): np.testing.assert_allclose(d, -0.5 * a_p) +@pytest.mark.unit +def test_signed_plasma_vessel_distance(): + """Test calculation of signed distance from plasma to vessel.""" + R0 = 10.0 + a_p = 1.0 + a_s = 2.0 + # default eq has R0=10, a=1 + eq = Equilibrium(M=3, N=2) + # surface with same R0, a=2, so true d=1 for all pts + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] + ) + # For equally spaced grids, should get true d=1 + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=grid, + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + np.testing.assert_allclose(d, a_s - a_p) + + # ensure that it works (dimension-wise) when compute_scaled is called + _ = obj.compute_scaled(*obj.xs(eq, surface)) + + # For plasma outside surface, should get signed distance + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_p * 0.5], + Z_lmn=[-a_p * 0.5], + modes_R=[[0, 0], [1, 0]], + modes_Z=[[-1, 0]], + ) + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=grid, + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + np.testing.assert_allclose(d, -0.5 * a_p) + + @pytest.mark.unit def test_mean_curvature(): """Test for mean curvature objective function.""" diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 34118cf049..72e5860832 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1017,3 +1017,75 @@ def test_proximal_with_PlasmaVesselDistance(): verbose=3, maxiter=3, ) + + # check that works with signed distance + a = 0.5 + R0 = 4 + surf = FourierRZToroidalSurface( + R_lmn=[R0, a], + Z_lmn=[0.0, -a], + modes_R=np.array([[0, 0], [1, 0]]), + modes_Z=np.array([[0, 0], [-1, 0]]), + sym=True, + NFP=eq.NFP, + ) + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=0.5, + plasma_grid=grid, + surface_fixed=True, + use_signed_distance=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("proximal-lsq-exact") + eq, result = optimizer.optimize( + eq, + objective, + constraints, + verbose=3, + maxiter=3, + ) + + np.testing.assert_allclose(obj.compute(*obj.xs(eq)), 0.5, rtol=1e-2) + + +@pytest.mark.slow +@pytest.mark.unit +@pytest.mark.optimize +def test_signed_PlasmaVesselDistance(): + """Tests that signed distance works with surface optimization.""" + eq = desc.examples.get("SOLOVEV") + + constraints = (FixParameter(eq),) # don't want eq to change + # circular surface + a = 0.5 + R0 = 4 + surf = FourierRZToroidalSurface( + R_lmn=[R0, a], + Z_lmn=[0.0, -a], + modes_R=np.array([[0, 0], [1, 0]]), + modes_Z=np.array([[0, 0], [-1, 0]]), + sym=True, + NFP=eq.NFP, + ) + surf.change_resolution(M=2) + + grid = LinearGrid(M=eq.M * 2, N=0, NFP=eq.NFP) + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=0.5, + surface_grid=grid, + plasma_grid=grid, + use_signed_distance=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (eq, surf), result = optimizer.optimize( + (eq, surf), objective, constraints, verbose=3, maxiter=30, ftol=1e-4 + ) + + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) From fa19f8d53b1d3d62ad3b4e14f7ad19bc8ee9a155 Mon Sep 17 00:00:00 2001 From: "Dario G. Panici" Date: Sun, 7 Jan 2024 17:23:08 -0500 Subject: [PATCH 05/60] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e8ac03330..f7179cbd25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ object e.g. optimizing only a ``FourierRZToroidalSurface`` object to have a cert ``Volume``. - Many functions from ``desc.plotting`` now also work for plotting quantities from ``Curve`` and ``Surface`` classes. +- Adds ``PlasmaVesselDistanceCircular`` as an objective, which is a simpler +way to calculate the plasma-vessel separation when the vessel is a circular +axisymmetric torus +- Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` and ``PlasmaVesselDistanceCircular`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance. v0.10.3 ------- From a492e97674edf46b61aca855020e81b8e6ac79a9 Mon Sep 17 00:00:00 2001 From: "Dario G. Panici" Date: Sun, 7 Jan 2024 17:42:40 -0500 Subject: [PATCH 06/60] add surface fixed to PlasmaVesselDistanceCircular --- desc/objectives/_geometry.py | 43 ++++++++++++++++++++++-------------- tests/test_objective_funs.py | 20 +++++++++++++++-- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index e1fe612402..98defdf778 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -811,8 +811,13 @@ class PlasmaVesselDistanceCircular(_Objective): Whether to use absolute value of distance or a signed distance, with d being positive if the plasma is inside of the bounding surface, and negative if outside of the bounding surface. - NOTE: signed distance currently only works for circular XS or elliptical XS - axisymmetric winding surfaces. False by default + surface_fixed: bool, optional + Whether the surface the distance from the plasma is computed to + is fixed or not. If True, the surface is fixed and its radius and axis are + precomputed, which saves on computation time during optimization, and + self.things = [eq] only. + If False, the surface geometry parameters are computed at every iteration. + False by default, so that self.things = [eq, surface] name : str, optional Name of the objective function. """ @@ -832,6 +837,7 @@ def __init__( normalize_target=True, plasma_grid=None, use_signed_distance=False, + surface_fixed=False, name="plasma-circular-vessel distance", ): if target is None and bounds is None: @@ -839,8 +845,9 @@ def __init__( self._surface = surface self._plasma_grid = plasma_grid self._use_signed_distance = use_signed_distance + self._surface_fixed = surface_fixed super().__init__( - things=[eq, self._surface], + things=[eq, self._surface] if not surface_fixed else [eq], target=target, bounds=bounds, weight=weight, @@ -885,6 +892,9 @@ def build(self, use_jit=True, verbose=1): " circular toroidal bounding surfaces!" ) + self._surface_minor_radius_coef_index = minor_radius_coef_index + self._surface_major_radius_coef_index = major_radius_coef_index + self._surface_minor_radius = np.abs(surface.R_lmn[minor_radius_coef_index]) self._surface_major_radius = np.abs(surface.R_lmn[major_radius_coef_index]) @@ -908,19 +918,10 @@ def build(self, use_jit=True, verbose=1): grid=plasma_grid, has_axis=plasma_grid.axis.size, ) - # make the axis from which we will calculate minor radius for the surface - # i.e this is (R0_surface,0) - self._surface_axis_points = np.vstack( - [ - self._surface_major_radius * np.ones(plasma_grid.num_nodes), - np.zeros(plasma_grid.num_nodes), - ] - ).T self._constants = { "transforms": equil_transforms, "equil_profiles": equil_profiles, - "surface_axis_points": self._surface_axis_points, } timer.stop("Precomputing transforms") @@ -961,16 +962,26 @@ def compute(self, equil_params, surface_params, constants=None): transforms=constants["transforms"], profiles=constants["equil_profiles"], ) + + if self._surface_fixed: + surface_major_radius = self._surface_major_radius + surface_minor_radius = self._surface_minor_radius + else: + surface_minor_radius = jnp.abs( + surface_params["R_lmn"][self._surface_minor_radius_coef_index] + ) + surface_major_radius = jnp.abs( + surface_params["R_lmn"][self._surface_major_radius_coef_index] + ) + plasma_coords_dist_vectors = jnp.array( - [data["R"] - self._surface_major_radius, data["Z"]] + [data["R"] - surface_major_radius, data["Z"]] ).T # compute the minor radius of the surface at each point in the plasma grid, # signed to be positive if plasma inside vessel, and negative if plasma # outside vessel - d = self._surface_minor_radius - jnp.linalg.norm( - plasma_coords_dist_vectors, axis=-1 - ) + d = surface_minor_radius - jnp.linalg.norm(plasma_coords_dist_vectors, axis=-1) if self._use_signed_distance: return d else: diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index fabbe303a3..217c5e7e15 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -837,7 +837,7 @@ def test_circular_plasma_vessel_distance(): surface = FourierRZToroidalSurface( R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] ) - # For equally spaced grids, should get true d=1 + plas_grid = LinearGrid(M=5, N=6) obj = PlasmaVesselDistanceCircular(eq=eq, plasma_grid=plas_grid, surface=surface) obj.build() @@ -868,12 +868,28 @@ def test_circular_plasma_vessel_distance(): ) plas_grid = LinearGrid(M=5, N=6) obj = PlasmaVesselDistanceCircular( - eq=eq, plasma_grid=plas_grid, surface=surface, use_signed_distance=True + eq=eq, + plasma_grid=plas_grid, + surface=surface, + surface_fixed=False, + use_signed_distance=True, ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) np.testing.assert_allclose(d, -0.5 * a_p) + # with surface_fixed=True + obj = PlasmaVesselDistanceCircular( + eq=eq, + plasma_grid=plas_grid, + surface=surface, + surface_fixed=True, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq)) + np.testing.assert_allclose(d, -0.5 * a_p) + @pytest.mark.unit def test_signed_plasma_vessel_distance(): From ad3a01533aa198551c5b0bfc765b86650e50ebe8 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sun, 7 Jan 2024 19:21:07 -0500 Subject: [PATCH 07/60] fix surface_fixed flag for PlasmaVesselDistanceCircular --- desc/objectives/_geometry.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 98defdf778..5e856ada5d 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -868,7 +868,7 @@ def build(self, use_jit=True, verbose=1): """ eq = self.things[0] - surface = self.things[1] + surface = self._surface if self._surface_fixed else self.things[1] # if things[1] is different than self._surface, update self._surface if surface != self._surface: self._surface = surface @@ -934,7 +934,7 @@ def build(self, use_jit=True, verbose=1): super().build(use_jit=use_jit, verbose=verbose) - def compute(self, equil_params, surface_params, constants=None): + def compute(self, equil_params, surface_params=None, constants=None): """Compute plasma-surface distance. Parameters @@ -943,6 +943,7 @@ def compute(self, equil_params, surface_params, constants=None): Dictionary of equilibrium degrees of freedom, eg Equilibrium.params_dict surface_params : dict Dictionary of surface degrees of freedom, eg Surface.params_dict + Only needed if self._surface_fixed = False constants : dict Dictionary of constant data, eg transforms, profiles etc. Defaults to self.constants From 4383407be39139a8310b803d4f7b385097d4122c Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sun, 7 Jan 2024 20:05:59 -0500 Subject: [PATCH 08/60] fix tests --- tests/test_optimizer.py | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 72e5860832..4361bd71a5 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -997,8 +997,7 @@ def test_proximal_with_PlasmaVesselDistance(): objective = ObjectiveFunction((obj,)) optimizer = Optimizer("proximal-lsq-exact") - eq, result = optimizer.optimize( - eq, + eq.optimize( objective, constraints, verbose=3, @@ -1018,38 +1017,6 @@ def test_proximal_with_PlasmaVesselDistance(): maxiter=3, ) - # check that works with signed distance - a = 0.5 - R0 = 4 - surf = FourierRZToroidalSurface( - R_lmn=[R0, a], - Z_lmn=[0.0, -a], - modes_R=np.array([[0, 0], [1, 0]]), - modes_Z=np.array([[0, 0], [-1, 0]]), - sym=True, - NFP=eq.NFP, - ) - obj = PlasmaVesselDistance( - surface=surf, - eq=eq, - target=0.5, - plasma_grid=grid, - surface_fixed=True, - use_signed_distance=True, - ) - objective = ObjectiveFunction((obj,)) - - optimizer = Optimizer("proximal-lsq-exact") - eq, result = optimizer.optimize( - eq, - objective, - constraints, - verbose=3, - maxiter=3, - ) - - np.testing.assert_allclose(obj.compute(*obj.xs(eq)), 0.5, rtol=1e-2) - @pytest.mark.slow @pytest.mark.unit From 985d40cb3823ed80c4d93b60e50116986890a25d Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sun, 7 Jan 2024 20:19:58 -0500 Subject: [PATCH 09/60] fix args in test --- tests/test_optimizer.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 4361bd71a5..0485493680 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -34,6 +34,7 @@ MeanCurvature, ObjectiveFunction, PlasmaVesselDistance, + PlasmaVesselDistanceCircular, Volume, get_fixed_boundary_constraints, ) @@ -1056,3 +1057,33 @@ def test_signed_PlasmaVesselDistance(): ) np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) + + # test with circular surface and changing eq + a = 0.75 + R0 = 10 + surf = FourierRZToroidalSurface( + R_lmn=[R0, a], + Z_lmn=[0.0, -a], + modes_R=np.array([[0, 0], [1, 0]]), + modes_Z=np.array([[0, 0], [-1, 0]]), + sym=True, + NFP=eq.NFP, + ) + # not caring about force balance, just want the eq surface to become circular + eq = Equilibrium(L=2, M=2, NFP=1, sym=True) + constraints = (FixParameter(surf), FixPressure(eq), FixCurrent(eq), FixPsi(eq)) + obj = PlasmaVesselDistanceCircular( + surface=surf, + eq=eq, + target=0.5, + plasma_grid=grid, + use_signed_distance=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (eq, surf), result = optimizer.optimize( + (eq, surf), objective, constraints, verbose=3, maxiter=30, ftol=1e-4 + ) + + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) From 2fcfb70234c562179763eb5551d7f843f5d115d8 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sun, 7 Jan 2024 21:24:17 -0500 Subject: [PATCH 10/60] increase test coverage --- tests/test_objective_funs.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 217c5e7e15..2378f62e50 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -838,8 +838,7 @@ def test_circular_plasma_vessel_distance(): R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] ) - plas_grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistanceCircular(eq=eq, plasma_grid=plas_grid, surface=surface) + obj = PlasmaVesselDistanceCircular(eq=eq, surface=surface) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) np.testing.assert_allclose(d, a_s - a_p) @@ -855,7 +854,14 @@ def test_circular_plasma_vessel_distance(): modes_R=[[0, 0], [1, 0], [0, 1]], modes_Z=[[-1, 0]], ) + plas_grid = LinearGrid(M=5, N=6) obj = PlasmaVesselDistanceCircular(surface=surf, plasma_grid=plas_grid, eq=eq) + with pytest.warns(UserWarning): + obj.build() + # check warning for grid with non-unity rho + obj = PlasmaVesselDistanceCircular( + surface=surf, plasma_grid=LinearGrid(rho=np.array(0.9)), eq=eq + ) with pytest.warns(UserWarning): obj.build() From 35fc568a09d9c9e82cc9a5074078b07c379fd321 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sun, 7 Jan 2024 21:49:43 -0500 Subject: [PATCH 11/60] use nonaxisymmetric eq for test --- tests/test_optimizer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 0485493680..48a13705ab 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1024,12 +1024,12 @@ def test_proximal_with_PlasmaVesselDistance(): @pytest.mark.optimize def test_signed_PlasmaVesselDistance(): """Tests that signed distance works with surface optimization.""" - eq = desc.examples.get("SOLOVEV") + eq = desc.examples.get("HELIOTRON") constraints = (FixParameter(eq),) # don't want eq to change # circular surface a = 0.5 - R0 = 4 + R0 = 10 surf = FourierRZToroidalSurface( R_lmn=[R0, a], Z_lmn=[0.0, -a], @@ -1038,9 +1038,9 @@ def test_signed_PlasmaVesselDistance(): sym=True, NFP=eq.NFP, ) - surf.change_resolution(M=2) + surf.change_resolution(M=2, N=2) - grid = LinearGrid(M=eq.M * 2, N=0, NFP=eq.NFP) + grid = LinearGrid(M=eq.M * 2, N=eq.N * 2) obj = PlasmaVesselDistance( surface=surf, eq=eq, @@ -1053,7 +1053,7 @@ def test_signed_PlasmaVesselDistance(): optimizer = Optimizer("lsq-exact") (eq, surf), result = optimizer.optimize( - (eq, surf), objective, constraints, verbose=3, maxiter=30, ftol=1e-4 + (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-6 ) np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) @@ -1070,7 +1070,6 @@ def test_signed_PlasmaVesselDistance(): NFP=eq.NFP, ) # not caring about force balance, just want the eq surface to become circular - eq = Equilibrium(L=2, M=2, NFP=1, sym=True) constraints = (FixParameter(surf), FixPressure(eq), FixCurrent(eq), FixPsi(eq)) obj = PlasmaVesselDistanceCircular( surface=surf, From 15808d7ea05bcfec7924eedb14bc69bcc8ee34dd Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 20 May 2024 09:59:30 -0400 Subject: [PATCH 12/60] implement algorithm for signed distance --- desc/objectives/_geometry.py | 129 +++++++++++++++++++++++++++++++---- tests/test_optimizer.py | 4 +- 2 files changed, 116 insertions(+), 17 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 1feba2ba6d..d645fded12 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -6,10 +6,10 @@ from desc.backend import jnp from desc.compute import compute as compute_fun -from desc.compute import get_profiles, get_transforms, rpz2xyz -from desc.compute.utils import dot, safenorm +from desc.compute import get_profiles, get_transforms, rpz2xyz, xyz2rpz +from desc.compute.utils import safenorm from desc.grid import LinearGrid, QuadratureGrid -from desc.utils import Timer +from desc.utils import Timer, errorif from .normalization import compute_scaling_factors from .objective_funs import _Objective @@ -571,6 +571,8 @@ class PlasmaVesselDistance(_Objective): Whether to use absolute value of distance or a signed distance, with d being positive if the plasma is inside of the bounding surface, and negative if outside of the bounding surface. + NOTE: ``plasma_grid`` and ``surface_grid`` must have the same + toroidal angle values for signed distance to be used. NOTE: this convention assumes that both surface and equilibrium have poloidal angles defined such that they are in a right-handed coordinate system with the surface normal vector pointing outwards @@ -677,6 +679,18 @@ def build(self, use_jit=True, verbose=1): if not np.allclose(plasma_grid.nodes[:, 0], 1): warnings.warn("Plasma grid includes interior points, should be rho=1") + # TODO: How to use with generalized toroidal angle? + errorif( + self._use_signed_distance + and not np.allclose( + plasma_grid.nodes[plasma_grid.unique_zeta_idx, 2], + surface_grid.nodes[surface_grid.unique_zeta_idx, 2], + ), + ValueError, + "Plasma grid and surface grid must contain points only at the " + "same zeta values in order to use signed distance", + ) + self._dim_f = surface_grid.num_nodes self._equil_data_keys = ["R", "phi", "Z"] self._surface_data_keys = ["x", "n_rho"] if self._use_signed_distance else ["x"] @@ -719,6 +733,21 @@ def build(self, use_jit=True, verbose=1): "quad_weights": w, } + if self._use_signed_distance: + # get the indices corresponding to the grid points + # at each distinct zeta plane, so that can be used + # in compute to separate the computed pts by zeta plane + zetas = plasma_grid.nodes[plasma_grid.unique_zeta_idx, 2] + plasma_zeta_indices = [ + np.where(np.isclose(plasma_grid.nodes[:, 2], zeta))[0] for zeta in zetas + ] + surface_zeta_indices = [ + np.where(np.isclose(surface_grid.nodes[:, 2], zeta))[0] + for zeta in zetas + ] + self._constants["plasma_zeta_indices"] = plasma_zeta_indices + self._constants["surface_zeta_indices"] = surface_zeta_indices + if self._surface_fixed: # precompute the surface coordinates # as the surface is fixed during the optimization @@ -771,7 +800,8 @@ def compute(self, equil_params, surface_params=None, constants=None): transforms=constants["equil_transforms"], profiles=constants["equil_profiles"], ) - plasma_coords = rpz2xyz(jnp.array([data["R"], data["phi"], data["Z"]]).T) + plasma_coords_rpz = jnp.array([data["R"], data["phi"], data["Z"]]).T + plasma_coords = rpz2xyz(plasma_coords_rpz) if self._surface_fixed: data_surf = constants["data_surf"] else: @@ -794,19 +824,88 @@ def compute(self, equil_params, surface_params=None, constants=None): if not self._use_signed_distance: return d.min(axis=0) else: + surface_coords_rpz = xyz2rpz(surface_coords) + + # TODO: currently this fxn only works on one pt + # on surface at a time so we need 2 for loops, vectorize it + def _find_angle_vec(R, Z, Rtest, Ztest): + # R Z and surface points, + # Rtest Ztest are the point we wanna check is inside + # the surfaceor not + + # R Z can be vectors? + Rbool = R > Rtest + Zbool = Z > Ztest + return_data = jnp.zeros_like(R) + return_data = jnp.where( + jnp.logical_and(Rbool, Zbool), 0, return_data + ) + return_data = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, return_data + ) + return_data = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), + 2, + return_data, + ) + return_data = jnp.where( + jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, return_data + ) + return return_data + + point_signs = jnp.zeros(plasma_coords.shape[0]) + for plasma_zeta_idx, surface_zeta_idx in zip( + constants["plasma_zeta_indices"], constants["surface_zeta_indices"] + ): + plasma_pts_at_zeta_plane = plasma_coords_rpz[plasma_zeta_idx, :] + surface_pts_at_zeta_plane = surface_coords_rpz[surface_zeta_idx, :] + surface_pts_at_zeta_plane = jnp.vstack( + (surface_pts_at_zeta_plane, surface_pts_at_zeta_plane[0, :]) + ) + for i, plasma_pt in enumerate(plasma_pts_at_zeta_plane): + quads = _find_angle_vec( + surface_pts_at_zeta_plane[:, 0], + surface_pts_at_zeta_plane[:, 2], + plasma_pt[0], + plasma_pt[2], + ) + deltas = quads[1:] - quads[0:-1] + deltas = jnp.where(deltas == 3, -1, deltas) + deltas = jnp.where(deltas == -3, 1, deltas) + # then flip sign if the R intercept is > Rtest and the + # quadrant flipped over a diagonal + R = surface_pts_at_zeta_plane[:, 0] + Z = surface_pts_at_zeta_plane[:, 2] + b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = plasma_pt[0, None] - b * (R[1:] - R[0:-1]) / ( + Z[1:] - Z[0:-1] + ) + deltas = jnp.where( + jnp.logical_and(jnp.abs(deltas) == 2, Rint > plasma_pt[0]), + -deltas, + deltas, + ) + pt_sign = jnp.sum(deltas) + # positive distance if the plasma pt is inside the surface, else + # negative distance is assigned + pt_sign = jnp.where(jnp.isclose(pt_sign, 0), -1, 1) + # need to assign to the correct index of the point on the plasma + point_signs = point_signs.at[plasma_zeta_idx[i]].set(pt_sign) + # at end here, point_signs is either +/- 1 with + # positive meaning the plasma pt + # is inside the surface and -1 if the plasma pt is + # outside the surface + + # FIXME" the min dists are per surface point, not per plasma pt, + # so need to re-arrange above so it says ifthe SURFACE is + # inside the plasma + # or not (and mult by a negative one since its the opposite + # convention now) + min_inds = d.argmin(axis=0, keepdims=True) min_ds = jnp.take_along_axis(d, min_inds, axis=0).squeeze() - diff_vecs_at_mins = jnp.take_along_axis( - diff_vec, min_inds[..., None], axis=0 - ).squeeze() - n_rho_dot_diff_vec = dot( - data_surf["n_rho"], diff_vecs_at_mins, axis=-1 - ).squeeze() - # we mult by -1 because diff_vec points from surface to - # the plasma, so - # if plasma is inside the surface, n_rho dot diff vec <0, - # but we define "plasma inside surface" as >0 - return min_ds * jnp.sign(n_rho_dot_diff_vec) * -1 + + return min_ds * point_signs class PlasmaVesselDistanceCircular(_Objective): diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 524350c932..cafb2ac942 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1058,7 +1058,7 @@ def test_signed_PlasmaVesselDistance(): ) surf.change_resolution(M=2, N=2) - grid = LinearGrid(M=eq.M * 2, N=eq.N * 2) + grid = LinearGrid(M=6, N=6, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, eq=eq, @@ -1070,7 +1070,7 @@ def test_signed_PlasmaVesselDistance(): objective = ObjectiveFunction((obj,)) optimizer = Optimizer("lsq-exact") - (eq, surf), result = optimizer.optimize( + (eq, surf), _ = optimizer.optimize( (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-6 ) From cf9a4c6d7205b8dd9e3d73e400cbfdfc03e4f5ad Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 20 May 2024 13:21:29 -0400 Subject: [PATCH 13/60] add signed distance (with for loop implementation) and tests --- desc/objectives/_geometry.py | 43 +++++++++++++++++------------------- tests/test_optimizer.py | 21 ++++++------------ 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index d645fded12..3dd822f4f5 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -853,54 +853,51 @@ def _find_angle_vec(R, Z, Rtest, Ztest): ) return return_data - point_signs = jnp.zeros(plasma_coords.shape[0]) + point_signs = jnp.zeros(surface_coords.shape[0]) for plasma_zeta_idx, surface_zeta_idx in zip( constants["plasma_zeta_indices"], constants["surface_zeta_indices"] ): plasma_pts_at_zeta_plane = plasma_coords_rpz[plasma_zeta_idx, :] surface_pts_at_zeta_plane = surface_coords_rpz[surface_zeta_idx, :] - surface_pts_at_zeta_plane = jnp.vstack( - (surface_pts_at_zeta_plane, surface_pts_at_zeta_plane[0, :]) + plasma_pts_at_zeta_plane = jnp.vstack( + (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) ) - for i, plasma_pt in enumerate(plasma_pts_at_zeta_plane): + for i, surf_pt in enumerate(surface_pts_at_zeta_plane): quads = _find_angle_vec( - surface_pts_at_zeta_plane[:, 0], - surface_pts_at_zeta_plane[:, 2], - plasma_pt[0], - plasma_pt[2], + plasma_pts_at_zeta_plane[:, 0], + plasma_pts_at_zeta_plane[:, 2], + surf_pt[0], + surf_pt[2], ) deltas = quads[1:] - quads[0:-1] deltas = jnp.where(deltas == 3, -1, deltas) deltas = jnp.where(deltas == -3, 1, deltas) # then flip sign if the R intercept is > Rtest and the # quadrant flipped over a diagonal - R = surface_pts_at_zeta_plane[:, 0] - Z = surface_pts_at_zeta_plane[:, 2] + R = plasma_pts_at_zeta_plane[:, 0] + Z = plasma_pts_at_zeta_plane[:, 2] b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = plasma_pt[0, None] - b * (R[1:] - R[0:-1]) / ( + Rint = surf_pt[0, None] - b * (R[1:] - R[0:-1]) / ( Z[1:] - Z[0:-1] ) deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint > plasma_pt[0]), + jnp.logical_and(jnp.abs(deltas) == 2, Rint > surf_pt[0]), -deltas, deltas, ) pt_sign = jnp.sum(deltas) + # here, if pt_sign is +/-4,means that SURFACE is inside PLASMA + # while if 0, means SURFACE is outside PLASMA + # positive distance if the plasma pt is inside the surface, else # negative distance is assigned - pt_sign = jnp.where(jnp.isclose(pt_sign, 0), -1, 1) - # need to assign to the correct index of the point on the plasma - point_signs = point_signs.at[plasma_zeta_idx[i]].set(pt_sign) + pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) + # need to assign to correct index of the point on the surface + point_signs = point_signs.at[surface_zeta_idx[i]].set(pt_sign) # at end here, point_signs is either +/- 1 with - # positive meaning the plasma pt - # is inside the surface and -1 if the plasma pt is - # outside the surface - - # FIXME" the min dists are per surface point, not per plasma pt, - # so need to re-arrange above so it says ifthe SURFACE is + # positive meaning the surface pt + # is outside the plasma and -1 if the surface pt is # inside the plasma - # or not (and mult by a negative one since its the opposite - # convention now) min_inds = d.argmin(axis=0, keepdims=True) min_ds = jnp.take_along_axis(d, min_inds, axis=0).squeeze() diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index cafb2ac942..513dae73c3 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1048,21 +1048,14 @@ def test_signed_PlasmaVesselDistance(): # circular surface a = 0.5 R0 = 10 - surf = FourierRZToroidalSurface( - R_lmn=[R0, a], - Z_lmn=[0.0, -a], - modes_R=np.array([[0, 0], [1, 0]]), - modes_Z=np.array([[0, 0], [-1, 0]]), - sym=True, - NFP=eq.NFP, - ) - surf.change_resolution(M=2, N=2) + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=6, N=6, NFP=eq.NFP) + grid = LinearGrid(M=eq.M, N=eq.N, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, eq=eq, - target=0.5, + target=-0.25, surface_grid=grid, plasma_grid=grid, use_signed_distance=True, @@ -1071,10 +1064,10 @@ def test_signed_PlasmaVesselDistance(): optimizer = Optimizer("lsq-exact") (eq, surf), _ = optimizer.optimize( - (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-6 + (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) # test with circular surface and changing eq a = 0.75 @@ -1088,7 +1081,7 @@ def test_signed_PlasmaVesselDistance(): NFP=eq.NFP, ) # not caring about force balance, just want the eq surface to become circular - constraints = (FixParameters(surf), FixPressure(eq), FixCurrent(eq), FixPsi(eq)) + constraints = (FixParameters(surf), FixPressure(eq), FixIota(eq), FixPsi(eq)) obj = PlasmaVesselDistanceCircular( surface=surf, eq=eq, From 0ab58884628f30692ba4de72b1324ec085651ef0 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 20 May 2024 14:44:06 -0400 Subject: [PATCH 14/60] fix tests --- tests/test_objective_funs.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index f50f575f3a..ff7cbe0e23 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -2022,6 +2022,7 @@ class TestComputeScalarResolution: specials = [ # these require special logic PlasmaVesselDistance, + PlasmaVesselDistanceCircular, BootstrapRedlConsistency, BoundaryError, VacuumBoundaryError, @@ -2065,6 +2066,27 @@ def test_compute_scalar_resolution_plasma_vessel(self): f[i] = obj.compute_scalar(obj.x()) np.testing.assert_allclose(f, f[-1], rtol=5e-2) + @pytest.mark.regression + def test_compute_scalar_resolution_plasma_vessel_circular(self): + """PlasmaVesselDistanceCircular.""" + f = np.zeros_like(self.res_array, dtype=float) + surface = FourierRZToroidalSurface( + R_lmn=[10, 1.5], Z_lmn=[-1.5], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] + ) + for i, res in enumerate(self.res_array): + grid = LinearGrid( + M=int(self.eq.M * res), N=int(self.eq.N * res), NFP=self.eq.NFP + ) + obj = ObjectiveFunction( + PlasmaVesselDistanceCircular( + surface=surface, eq=self.eq, plasma_grid=grid + ), + use_jit=False, + ) + obj.build(verbose=0) + f[i] = obj.compute_scalar(obj.x()) + np.testing.assert_allclose(f, f[-1], rtol=5e-2) + @pytest.mark.regression def test_compute_scalar_resolution_bootstrap(self): """BootstrapRedlConsistency.""" @@ -2346,6 +2368,7 @@ class TestObjectiveNaNGrad: specials = [ # these require special logic PlasmaVesselDistance, + PlasmaVesselDistanceCircular, ForceBalanceAnisotropic, BootstrapRedlConsistency, BoundaryError, @@ -2375,6 +2398,16 @@ def test_objective_no_nangrad_plasma_vessel(self): g = obj.grad(obj.x(eq, surf)) assert not np.any(np.isnan(g)), "plasma vessel distance" + @pytest.mark.unit + def test_objective_no_nangrad_plasma_vessel_circular(self): + """PlasmaVesselDistanceCircular.""" + eq = Equilibrium(L=2, M=2, N=2) + surf = FourierRZToroidalSurface() + obj = ObjectiveFunction(PlasmaVesselDistanceCircular(eq, surf), use_jit=False) + obj.build() + g = obj.grad(obj.x(eq, surf)) + assert not np.any(np.isnan(g)), "plasma vessel distance circular" + @pytest.mark.unit def test_objective_no_nangrad_anisotropy(self): """ForceBalanceAnisotropic.""" From 017599b6b77bb601ba2280213ec223c446d53b8e Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 20 May 2024 15:17:15 -0400 Subject: [PATCH 15/60] fix import --- desc/objectives/_geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 440dd292ae..8e3ce75fd1 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -5,8 +5,8 @@ import numpy as np from desc.backend import jnp -from desc.compute import _compute as compute_fun from desc.compute import get_profiles, get_transforms, rpz2xyz, xyz2rpz +from desc.compute.utils import _compute as compute_fun from desc.compute.utils import safenorm from desc.grid import LinearGrid, QuadratureGrid from desc.utils import Timer, errorif From 7b0c89a671f232336306623da6e07844fb2123b1 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 22 May 2024 11:30:46 -0400 Subject: [PATCH 16/60] put logic inside the function --- desc/objectives/_geometry.py | 65 ++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 8e3ce75fd1..53264706ef 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -832,26 +832,41 @@ def _find_angle_vec(R, Z, Rtest, Ztest): # R Z and surface points, # Rtest Ztest are the point we wanna check is inside # the surfaceor not - - # R Z can be vectors? Rbool = R > Rtest Zbool = Z > Ztest - return_data = jnp.zeros_like(R) - return_data = jnp.where( - jnp.logical_and(Rbool, Zbool), 0, return_data - ) - return_data = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, return_data + quads = jnp.zeros_like(R) + quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) + quads = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quads ) - return_data = jnp.where( + quads = jnp.where( jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), 2, - return_data, + quads, + ) + quads = jnp.where( + jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quads ) - return_data = jnp.where( - jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, return_data + deltas = quads[1:] - quads[0:-1] + deltas = jnp.where(deltas == 3, -1, deltas) + deltas = jnp.where(deltas == -3, 1, deltas) + # then flip sign if the R intercept is > Rtest and the + # quadrant flipped over a diagonal + b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = surf_pt[0, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) + deltas = jnp.where( + jnp.logical_and(jnp.abs(deltas) == 2, Rint > surf_pt[0]), + -deltas, + deltas, ) - return return_data + pt_sign = jnp.sum(deltas) + # here, if pt_sign is +/-4,means that SURFACE is inside PLASMA + # while if 0, means SURFACE is outside PLASMA + + # positive distance if the plasma pt is inside the surface, else + # negative distance is assigned + pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) + return pt_sign point_signs = jnp.zeros(surface_coords.shape[0]) for plasma_zeta_idx, surface_zeta_idx in zip( @@ -863,35 +878,13 @@ def _find_angle_vec(R, Z, Rtest, Ztest): (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) ) for i, surf_pt in enumerate(surface_pts_at_zeta_plane): - quads = _find_angle_vec( + pt_sign = _find_angle_vec( plasma_pts_at_zeta_plane[:, 0], plasma_pts_at_zeta_plane[:, 2], surf_pt[0], surf_pt[2], ) - deltas = quads[1:] - quads[0:-1] - deltas = jnp.where(deltas == 3, -1, deltas) - deltas = jnp.where(deltas == -3, 1, deltas) - # then flip sign if the R intercept is > Rtest and the - # quadrant flipped over a diagonal - R = plasma_pts_at_zeta_plane[:, 0] - Z = plasma_pts_at_zeta_plane[:, 2] - b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = surf_pt[0, None] - b * (R[1:] - R[0:-1]) / ( - Z[1:] - Z[0:-1] - ) - deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint > surf_pt[0]), - -deltas, - deltas, - ) - pt_sign = jnp.sum(deltas) - # here, if pt_sign is +/-4,means that SURFACE is inside PLASMA - # while if 0, means SURFACE is outside PLASMA - # positive distance if the plasma pt is inside the surface, else - # negative distance is assigned - pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) # need to assign to correct index of the point on the surface point_signs = point_signs.at[surface_zeta_idx[i]].set(pt_sign) # at end here, point_signs is either +/- 1 with From 0cb382218e5ad59c63b6340b079ef6140db098d3 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 22 May 2024 11:52:32 -0400 Subject: [PATCH 17/60] vectorize signed distance over the surface points --- desc/objectives/_geometry.py | 45 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 53264706ef..d77fe468d6 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -831,10 +831,11 @@ def compute(self, equil_params, surface_params=None, constants=None): def _find_angle_vec(R, Z, Rtest, Ztest): # R Z and surface points, # Rtest Ztest are the point we wanna check is inside - # the surfaceor not - Rbool = R > Rtest - Zbool = Z > Ztest - quads = jnp.zeros_like(R) + # the surface or not + Rbool = R[:, None] > Rtest + Zbool = Z[:, None] > Ztest + # these Rbool are now size (Nsurf, Ntest) + quads = jnp.zeros_like(Rbool) quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) quads = jnp.where( jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quads @@ -847,24 +848,25 @@ def _find_angle_vec(R, Z, Rtest, Ztest): quads = jnp.where( jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quads ) - deltas = quads[1:] - quads[0:-1] + deltas = quads[1:, :] - quads[0:-1, :] deltas = jnp.where(deltas == 3, -1, deltas) deltas = jnp.where(deltas == -3, 1, deltas) # then flip sign if the R intercept is > Rtest and the # quadrant flipped over a diagonal b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = surf_pt[0, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = Rtest[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint > surf_pt[0]), + jnp.logical_and(jnp.abs(deltas) == 2, Rint > Rtest), -deltas, deltas, ) - pt_sign = jnp.sum(deltas) - # here, if pt_sign is +/-4,means that SURFACE is inside PLASMA - # while if 0, means SURFACE is outside PLASMA - + pt_sign = jnp.sum(deltas, axis=0) # positive distance if the plasma pt is inside the surface, else # negative distance is assigned + # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, + # assign positive distance + # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so + # assign negative distance pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) return pt_sign @@ -877,16 +879,17 @@ def _find_angle_vec(R, Z, Rtest, Ztest): plasma_pts_at_zeta_plane = jnp.vstack( (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) ) - for i, surf_pt in enumerate(surface_pts_at_zeta_plane): - pt_sign = _find_angle_vec( - plasma_pts_at_zeta_plane[:, 0], - plasma_pts_at_zeta_plane[:, 2], - surf_pt[0], - surf_pt[2], - ) - - # need to assign to correct index of the point on the surface - point_signs = point_signs.at[surface_zeta_idx[i]].set(pt_sign) + + surface_pts_at_zeta_plane + pt_sign = _find_angle_vec( + plasma_pts_at_zeta_plane[:, 0], + plasma_pts_at_zeta_plane[:, 2], + surface_pts_at_zeta_plane[:, 0], + surface_pts_at_zeta_plane[:, 2], + ) + + # need to assign to correct index of the points on the surface + point_signs = point_signs.at[surface_zeta_idx].set(pt_sign) # at end here, point_signs is either +/- 1 with # positive meaning the surface pt # is outside the plasma and -1 if the surface pt is From 38a6c117702aaf4c34bf92d7c3b9446821f2c553 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 22 May 2024 12:01:14 -0400 Subject: [PATCH 18/60] add signed distance to softmin --- desc/objectives/_geometry.py | 157 ++++++++++++++++------------------- tests/test_optimizer.py | 20 +++++ 2 files changed, 93 insertions(+), 84 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index d77fe468d6..4266553762 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -625,11 +625,6 @@ def __init__( self._plasma_grid = plasma_grid self._use_softmin = use_softmin self._use_signed_distance = use_signed_distance - if use_softmin and use_signed_distance: - warnings.warn( - "signed distance cannot currently" " be used with use_softmin=True!", - UserWarning, - ) self._surface_fixed = surface_fixed self._alpha = alpha super().__init__( @@ -818,87 +813,81 @@ def compute(self, equil_params, surface_params=None, constants=None): diff_vec = plasma_coords[:, None, :] - surface_coords[None, :, :] d = safenorm(diff_vec, axis=-1) + point_signs = jnp.ones(surface_coords.shape[0]) + if self._use_signed_distance: + surface_coords_rpz = xyz2rpz(surface_coords) + + def _find_angle_vec(R, Z, Rtest, Ztest): + # R Z and surface points, + # Rtest Ztest are the point we wanna check is inside + # the surface or not + Rbool = R[:, None] > Rtest + Zbool = Z[:, None] > Ztest + # these Rbool are now size (Nsurf, Ntest) + quads = jnp.zeros_like(Rbool) + quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) + quads = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quads + ) + quads = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), + 2, + quads, + ) + quads = jnp.where( + jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quads + ) + deltas = quads[1:, :] - quads[0:-1, :] + deltas = jnp.where(deltas == 3, -1, deltas) + deltas = jnp.where(deltas == -3, 1, deltas) + # then flip sign if the R intercept is > Rtest and the + # quadrant flipped over a diagonal + b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = Rtest[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) + deltas = jnp.where( + jnp.logical_and(jnp.abs(deltas) == 2, Rint > Rtest), + -deltas, + deltas, + ) + pt_sign = jnp.sum(deltas, axis=0) + # positive distance if the plasma pt is inside the surface, else + # negative distance is assigned + # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, + # assign positive distance + # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so + # assign negative distance + pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) + return pt_sign + + # loop over zeta planes + for plasma_zeta_idx, surface_zeta_idx in zip( + constants["plasma_zeta_indices"], constants["surface_zeta_indices"] + ): + plasma_pts_at_zeta_plane = plasma_coords_rpz[plasma_zeta_idx, :] + surface_pts_at_zeta_plane = surface_coords_rpz[surface_zeta_idx, :] + plasma_pts_at_zeta_plane = jnp.vstack( + (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) + ) + + surface_pts_at_zeta_plane + pt_sign = _find_angle_vec( + plasma_pts_at_zeta_plane[:, 0], + plasma_pts_at_zeta_plane[:, 2], + surface_pts_at_zeta_plane[:, 0], + surface_pts_at_zeta_plane[:, 2], + ) + + # need to assign to correct index of the points on the surface + point_signs = point_signs.at[surface_zeta_idx].set(pt_sign) + # at end here, point_signs is either +/- 1 with + # positive meaning the surface pt + # is outside the plasma and -1 if the surface pt is + # inside the plasma + if self._use_softmin: # do softmin - return jnp.apply_along_axis(softmin, 0, d, self._alpha) + return jnp.apply_along_axis(softmin, 0, d, self._alpha) * point_signs else: # do hardmin - if not self._use_signed_distance: - return d.min(axis=0) - else: - surface_coords_rpz = xyz2rpz(surface_coords) - - # TODO: currently this fxn only works on one pt - # on surface at a time so we need 2 for loops, vectorize it - def _find_angle_vec(R, Z, Rtest, Ztest): - # R Z and surface points, - # Rtest Ztest are the point we wanna check is inside - # the surface or not - Rbool = R[:, None] > Rtest - Zbool = Z[:, None] > Ztest - # these Rbool are now size (Nsurf, Ntest) - quads = jnp.zeros_like(Rbool) - quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) - quads = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quads - ) - quads = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), - 2, - quads, - ) - quads = jnp.where( - jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quads - ) - deltas = quads[1:, :] - quads[0:-1, :] - deltas = jnp.where(deltas == 3, -1, deltas) - deltas = jnp.where(deltas == -3, 1, deltas) - # then flip sign if the R intercept is > Rtest and the - # quadrant flipped over a diagonal - b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = Rtest[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) - deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint > Rtest), - -deltas, - deltas, - ) - pt_sign = jnp.sum(deltas, axis=0) - # positive distance if the plasma pt is inside the surface, else - # negative distance is assigned - # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, - # assign positive distance - # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so - # assign negative distance - pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) - return pt_sign - - point_signs = jnp.zeros(surface_coords.shape[0]) - for plasma_zeta_idx, surface_zeta_idx in zip( - constants["plasma_zeta_indices"], constants["surface_zeta_indices"] - ): - plasma_pts_at_zeta_plane = plasma_coords_rpz[plasma_zeta_idx, :] - surface_pts_at_zeta_plane = surface_coords_rpz[surface_zeta_idx, :] - plasma_pts_at_zeta_plane = jnp.vstack( - (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) - ) - - surface_pts_at_zeta_plane - pt_sign = _find_angle_vec( - plasma_pts_at_zeta_plane[:, 0], - plasma_pts_at_zeta_plane[:, 2], - surface_pts_at_zeta_plane[:, 0], - surface_pts_at_zeta_plane[:, 2], - ) - - # need to assign to correct index of the points on the surface - point_signs = point_signs.at[surface_zeta_idx].set(pt_sign) - # at end here, point_signs is either +/- 1 with - # positive meaning the surface pt - # is outside the plasma and -1 if the surface pt is - # inside the plasma - - min_inds = d.argmin(axis=0, keepdims=True) - min_ds = jnp.take_along_axis(d, min_inds, axis=0).squeeze() - - return min_ds * point_signs + return d.min(axis=0) * point_signs class PlasmaVesselDistanceCircular(_Objective): diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 513dae73c3..a74e90c174 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1069,6 +1069,26 @@ def test_signed_PlasmaVesselDistance(): np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) + # with softmin + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=-0.25, + surface_grid=grid, + plasma_grid=grid, + use_signed_distance=True, + use_softmin=True, + alpha=100, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (eq, surf), _ = optimizer.optimize( + (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 + ) + + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) + # test with circular surface and changing eq a = 0.75 R0 = 10 From 66f44f77655753fa91d0cd8c681fcc62f32bd605 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 22 May 2024 12:11:56 -0400 Subject: [PATCH 19/60] correct docstring --- desc/objectives/_geometry.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 4266553762..760326b70f 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -573,10 +573,6 @@ class PlasmaVesselDistance(_Objective): negative if outside of the bounding surface. NOTE: ``plasma_grid`` and ``surface_grid`` must have the same toroidal angle values for signed distance to be used. - NOTE: this convention assumes that both surface and equilibrium have - poloidal angles defined such that they are in a right-handed coordinate - system with the surface normal vector pointing outwards - NOTE: only works with use_softmin=False currently surface_fixed: bool, optional Whether the surface the distance from the plasma is computed to is fixed or not. If True, the surface is fixed and its coordinates are From 9b1ed31d7ec620f0890e0244da19149907076cc2 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 23 May 2024 15:33:49 -0400 Subject: [PATCH 20/60] remove circular distance --- desc/objectives/__init__.py | 1 - desc/objectives/_geometry.py | 231 ----------------------------------- tests/test_objective_funs.py | 106 ---------------- tests/test_optimizer.py | 58 +++++---- 4 files changed, 33 insertions(+), 363 deletions(-) diff --git a/desc/objectives/__init__.py b/desc/objectives/__init__.py index bf33f4035e..7f3b6c61da 100644 --- a/desc/objectives/__init__.py +++ b/desc/objectives/__init__.py @@ -26,7 +26,6 @@ GoodCoordinates, MeanCurvature, PlasmaVesselDistance, - PlasmaVesselDistanceCircular, PrincipalCurvature, Volume, ) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 760326b70f..ead9a64235 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -634,12 +634,6 @@ def __init__( deriv_mode=deriv_mode, name=name, ) - # possible easy signed distance: - # at each zeta point in plas_grid, take as the "center" of the plane to be - # the eq axis at that zeta - # then compute minor radius to that point, for each zeta - # (so just (R(phi)-R0(phi),Z(phi)-Z0(phi) for both plasma and surface)) - # then take sign(r_surf - r_plasma) and multiply d by that? def build(self, use_jit=True, verbose=1): """Build constant arrays. @@ -886,231 +880,6 @@ def _find_angle_vec(R, Z, Rtest, Ztest): return d.min(axis=0) * point_signs -class PlasmaVesselDistanceCircular(_Objective): - """Target the distance between the plasma and a surrounding circular torus. - - Computes the radius from the axis of the circular toroidal surface for each - point in the plas_grid, and subtracts that from the radius of the circular - bounding surface given to yield the distance from the plasma to the - circular bounding surface. - - NOTE: for best results, use this objective in combination with either MeanCurvature - or PrincipalCurvature, to penalize the tendency for the optimizer to only move the - points on surface corresponding to the grid that the plasma-vessel distance - is evaluated at, which can cause cusps or regions of very large curvature. - - NOTE: When use_softmin=True, ensures that alpha*values passed in is - at least >1, otherwise the softmin will return inaccurate approximations - of the minimum. Will automatically multiply array values by 2 / min_val if the min - of alpha*array is <1. This is to avoid inaccuracies that arise when values <1 - are present in the softmin, which can cause inaccurate mins or even incorrect - signs of the softmin versus the actual min. - - Parameters - ---------- - eq : Equilibrium, optional - Equilibrium that will be optimized to satisfy the Objective. - surface : Surface - Bounding surface to penalize distance to. - target : {float, ndarray}, optional - Target value(s) of the objective. Only used if bounds is None. - 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 - 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. - plasma_grid : Grid, optional - Collocation grid containing the nodes to evaluate plasma geometry at. - use_signed_distance: bool, optional - Whether to use absolute value of distance or a signed distance, with d - being positive if the plasma is inside of the bounding surface, and - negative if outside of the bounding surface. - surface_fixed: bool, optional - Whether the surface the distance from the plasma is computed to - is fixed or not. If True, the surface is fixed and its radius and axis are - precomputed, which saves on computation time during optimization, and - self.things = [eq] only. - If False, the surface geometry parameters are computed at every iteration. - False by default, so that self.things = [eq, surface] - name : str, optional - Name of the objective function. - """ - - _coordinates = "rtz" - _units = "(m)" - _print_value_fmt = "Plasma-circular-vessel distance: {:10.3e} " - - def __init__( - self, - eq, - surface, - target=None, - bounds=None, - weight=1, - normalize=True, - normalize_target=True, - plasma_grid=None, - use_signed_distance=False, - surface_fixed=False, - name="plasma-circular-vessel distance", - ): - if target is None and bounds is None: - bounds = (1, np.inf) - self._surface = surface - self._plasma_grid = plasma_grid - self._use_signed_distance = use_signed_distance - self._surface_fixed = surface_fixed - super().__init__( - things=[eq, self._surface] if not surface_fixed else [eq], - target=target, - bounds=bounds, - weight=weight, - normalize=normalize, - normalize_target=normalize_target, - 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] - surface = self._surface if self._surface_fixed else self.things[1] - # if things[1] is different than self._surface, update self._surface - if surface != self._surface: - self._surface = surface - if self._plasma_grid is None: - plasma_grid = LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP) - else: - plasma_grid = self._plasma_grid - - if not np.allclose(plasma_grid.nodes[:, 0], 1): - warnings.warn("Plasma grid includes interior points, should be rho=1") - minor_radius_coef_index = surface.R_basis.get_idx(L=0, M=1, N=0) - major_radius_coef_index = surface.R_basis.get_idx(L=0, M=0, N=0) - should_be_zero_indices = np.delete( - np.arange(surface.R_basis.num_modes), - [minor_radius_coef_index, major_radius_coef_index], - ) - - if not np.allclose(surface.R_lmn[should_be_zero_indices], 0.0): - warnings.warn( - "PlasmaVesselDistanceCircular only works for axisymmetric" - " circular toroidal bounding surfaces!" - ) - - self._surface_minor_radius_coef_index = minor_radius_coef_index - self._surface_major_radius_coef_index = major_radius_coef_index - - self._surface_minor_radius = np.abs(surface.R_lmn[minor_radius_coef_index]) - self._surface_major_radius = np.abs(surface.R_lmn[major_radius_coef_index]) - - self._dim_f = plasma_grid.num_nodes - self._equil_data_keys = ["R", "Z"] - - timer = Timer() - if verbose > 0: - print("Precomputing transforms") - timer.start("Precomputing transforms") - - equil_profiles = get_profiles( - self._equil_data_keys, - obj=eq, - grid=plasma_grid, - has_axis=plasma_grid.axis.size, - ) - equil_transforms = get_transforms( - self._equil_data_keys, - obj=eq, - grid=plasma_grid, - has_axis=plasma_grid.axis.size, - ) - - self._constants = { - "transforms": equil_transforms, - "equil_profiles": equil_profiles, - } - - timer.stop("Precomputing transforms") - if verbose > 1: - timer.disp("Precomputing transforms") - - if self._normalize: - scales = compute_scaling_factors(eq) - self._normalization = scales["a"] - - super().build(use_jit=use_jit, verbose=verbose) - - def compute(self, equil_params, surface_params=None, constants=None): - """Compute plasma-surface distance. - - Parameters - ---------- - equil_params : dict - Dictionary of equilibrium degrees of freedom, eg Equilibrium.params_dict - surface_params : dict - Dictionary of surface degrees of freedom, eg Surface.params_dict - Only needed if self._surface_fixed = False - constants : dict - Dictionary of constant data, eg transforms, profiles etc. Defaults to - self.constants - - Returns - ------- - d : ndarray, shape(surface_grid.num_nodes,) - For each point in the surface grid, approximate distance to plasma. - - """ - if constants is None: - constants = self.constants - data = compute_fun( - "desc.equilibrium.equilibrium.Equilibrium", - self._equil_data_keys, - params=equil_params, - transforms=constants["transforms"], - profiles=constants["equil_profiles"], - ) - - if self._surface_fixed: - surface_major_radius = self._surface_major_radius - surface_minor_radius = self._surface_minor_radius - else: - surface_minor_radius = jnp.abs( - surface_params["R_lmn"][self._surface_minor_radius_coef_index] - ) - surface_major_radius = jnp.abs( - surface_params["R_lmn"][self._surface_major_radius_coef_index] - ) - - plasma_coords_dist_vectors = jnp.array( - [data["R"] - surface_major_radius, data["Z"]] - ).T - - # compute the minor radius of the surface at each point in the plasma grid, - # signed to be positive if plasma inside vessel, and negative if plasma - # outside vessel - d = surface_minor_radius - jnp.linalg.norm(plasma_coords_dist_vectors, axis=-1) - if self._use_signed_distance: - return d - else: - return jnp.abs(d) - - class MeanCurvature(_Objective): """Target a particular value for the mean curvature. diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index ff7cbe0e23..176dee569e 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -51,7 +51,6 @@ ObjectiveFunction, Omnigenity, PlasmaVesselDistance, - PlasmaVesselDistanceCircular, Pressure, PrincipalCurvature, QuadraticFlux, @@ -1208,78 +1207,6 @@ def test_plasma_vessel_distance(): np.testing.assert_allclose(d, a_s - a_p) -@pytest.mark.unit -def test_circular_plasma_vessel_distance(): - """Test calculation of min distance from plasma to circular vessel.""" - R0 = 10.0 - a_p = 1.0 - a_s = 2.0 - # default eq has R0=10, a=1 - eq = Equilibrium(M=3, N=2) - # surface with same R0, a=2, so true d=1 for all pts - surface = FourierRZToroidalSurface( - R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] - ) - - obj = PlasmaVesselDistanceCircular(eq=eq, surface=surface) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - np.testing.assert_allclose(d, a_s - a_p) - - # ensure that it works (dimension-wise) when compute_scaled is called - _ = obj.compute_scaled(*obj.xs(eq, surface)) - - # check warning for non-circular-axisymmetric vessel - a2 = 0.1 - surf = FourierRZToroidalSurface( - R_lmn=[R0, a_s, a2], - Z_lmn=[-a_s], - modes_R=[[0, 0], [1, 0], [0, 1]], - modes_Z=[[-1, 0]], - ) - plas_grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistanceCircular(surface=surf, plasma_grid=plas_grid, eq=eq) - with pytest.warns(UserWarning): - obj.build() - # check warning for grid with non-unity rho - obj = PlasmaVesselDistanceCircular( - surface=surf, plasma_grid=LinearGrid(rho=np.array(0.9)), eq=eq - ) - with pytest.warns(UserWarning): - obj.build() - - # For plasma outside surface, should get signed distance - surface = FourierRZToroidalSurface( - R_lmn=[R0, a_p * 0.5], - Z_lmn=[-a_p * 0.5], - modes_R=[[0, 0], [1, 0]], - modes_Z=[[-1, 0]], - ) - plas_grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistanceCircular( - eq=eq, - plasma_grid=plas_grid, - surface=surface, - surface_fixed=False, - use_signed_distance=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - np.testing.assert_allclose(d, -0.5 * a_p) - - # with surface_fixed=True - obj = PlasmaVesselDistanceCircular( - eq=eq, - plasma_grid=plas_grid, - surface=surface, - surface_fixed=True, - use_signed_distance=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq)) - np.testing.assert_allclose(d, -0.5 * a_p) - - @pytest.mark.unit def test_signed_plasma_vessel_distance(): """Test calculation of signed distance from plasma to vessel.""" @@ -2022,7 +1949,6 @@ class TestComputeScalarResolution: specials = [ # these require special logic PlasmaVesselDistance, - PlasmaVesselDistanceCircular, BootstrapRedlConsistency, BoundaryError, VacuumBoundaryError, @@ -2066,27 +1992,6 @@ def test_compute_scalar_resolution_plasma_vessel(self): f[i] = obj.compute_scalar(obj.x()) np.testing.assert_allclose(f, f[-1], rtol=5e-2) - @pytest.mark.regression - def test_compute_scalar_resolution_plasma_vessel_circular(self): - """PlasmaVesselDistanceCircular.""" - f = np.zeros_like(self.res_array, dtype=float) - surface = FourierRZToroidalSurface( - R_lmn=[10, 1.5], Z_lmn=[-1.5], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] - ) - for i, res in enumerate(self.res_array): - grid = LinearGrid( - M=int(self.eq.M * res), N=int(self.eq.N * res), NFP=self.eq.NFP - ) - obj = ObjectiveFunction( - PlasmaVesselDistanceCircular( - surface=surface, eq=self.eq, plasma_grid=grid - ), - use_jit=False, - ) - obj.build(verbose=0) - f[i] = obj.compute_scalar(obj.x()) - np.testing.assert_allclose(f, f[-1], rtol=5e-2) - @pytest.mark.regression def test_compute_scalar_resolution_bootstrap(self): """BootstrapRedlConsistency.""" @@ -2368,7 +2273,6 @@ class TestObjectiveNaNGrad: specials = [ # these require special logic PlasmaVesselDistance, - PlasmaVesselDistanceCircular, ForceBalanceAnisotropic, BootstrapRedlConsistency, BoundaryError, @@ -2398,16 +2302,6 @@ def test_objective_no_nangrad_plasma_vessel(self): g = obj.grad(obj.x(eq, surf)) assert not np.any(np.isnan(g)), "plasma vessel distance" - @pytest.mark.unit - def test_objective_no_nangrad_plasma_vessel_circular(self): - """PlasmaVesselDistanceCircular.""" - eq = Equilibrium(L=2, M=2, N=2) - surf = FourierRZToroidalSurface() - obj = ObjectiveFunction(PlasmaVesselDistanceCircular(eq, surf), use_jit=False) - obj.build() - g = obj.grad(obj.x(eq, surf)) - assert not np.any(np.isnan(g)), "plasma vessel distance circular" - @pytest.mark.unit def test_objective_no_nangrad_anisotropy(self): """ForceBalanceAnisotropic.""" diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index a74e90c174..8009473db2 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -36,7 +36,6 @@ MeanCurvature, ObjectiveFunction, PlasmaVesselDistance, - PlasmaVesselDistanceCircular, QuasisymmetryTripleProduct, Volume, get_fixed_boundary_constraints, @@ -1045,9 +1044,6 @@ def test_signed_PlasmaVesselDistance(): eq = desc.examples.get("HELIOTRON") constraints = (FixParameters(eq),) # don't want eq to change - # circular surface - a = 0.5 - R0 = 10 surf = eq.surface.copy() surf.change_resolution(M=1, N=1) @@ -1067,9 +1063,13 @@ def test_signed_PlasmaVesselDistance(): (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) + np.testing.assert_allclose( + obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2, err_msg="Using hardmin" + ) # with softmin + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) obj = PlasmaVesselDistance( surface=surf, eq=eq, @@ -1084,39 +1084,47 @@ def test_signed_PlasmaVesselDistance(): optimizer = Optimizer("lsq-exact") (eq, surf), _ = optimizer.optimize( - (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 + (eq, surf), + objective, + constraints, + verbose=3, + maxiter=60, + ftol=1e-8, + xtol=1e-9, ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) - - # test with circular surface and changing eq - a = 0.75 - R0 = 10 - surf = FourierRZToroidalSurface( - R_lmn=[R0, a], - Z_lmn=[0.0, -a], - modes_R=np.array([[0, 0], [1, 0]]), - modes_Z=np.array([[0, 0], [-1, 0]]), - sym=True, - NFP=eq.NFP, + np.testing.assert_allclose( + obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2, err_msg="Using softmin" ) - # not caring about force balance, just want the eq surface to become circular - constraints = (FixParameters(surf), FixPressure(eq), FixIota(eq), FixPsi(eq)) - obj = PlasmaVesselDistanceCircular( + + # with changing eq + eq = Equilibrium(M=1, N=1) + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) + grid = LinearGrid(M=eq.M * 3, N=eq.N, NFP=eq.NFP) + + obj = PlasmaVesselDistance( surface=surf, eq=eq, - target=0.5, + target=-0.25, + surface_grid=grid, plasma_grid=grid, use_signed_distance=True, ) objective = ObjectiveFunction((obj,)) optimizer = Optimizer("lsq-exact") - (eq, surf), result = optimizer.optimize( - (eq, surf), objective, constraints, verbose=3, maxiter=30, ftol=1e-4 + (eq, surf), _ = optimizer.optimize( + (eq, surf), + objective, + constraints=(FixParameters(surf),), + verbose=3, + maxiter=60, + ftol=1e-8, + xtol=1e-9, ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), 0.5, atol=1e-2) + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) @pytest.mark.unit From 2d3779f0e835608e017d67d9f85d969107ac37b5 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 23 May 2024 15:39:47 -0400 Subject: [PATCH 21/60] fix test with eq changing --- tests/test_optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 8009473db2..da9bc0ad5d 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1101,7 +1101,7 @@ def test_signed_PlasmaVesselDistance(): eq = Equilibrium(M=1, N=1) surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=eq.M * 3, N=eq.N, NFP=eq.NFP) + grid = LinearGrid(M=12, N=2, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, From 9bf94b75e9d0dc9db42be3fc051fc0787113f9c0 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 23 May 2024 15:42:06 -0400 Subject: [PATCH 22/60] update changelog --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07bb4e0b0e..f7de2ccad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,7 @@ New Features - Add method ``from_values`` to ``FourierRZCurve`` to allow fitting of data points to a ``FourierRZCurve`` object, and ``to_FourierRZCurve`` methods to ``Curve`` class. -- Adds ``PlasmaVesselDistanceCircular`` as an objective, which is a simpler -way to calculate the plasma-vessel separation when the vessel is a circular -axisymmetric torus -- Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` and ``PlasmaVesselDistanceCircular`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance. +- Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance. v0.11.1 ------- From 6d110e8a1507d36390aa5cd404db4d3c536fe909 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 23 May 2024 15:50:26 -0400 Subject: [PATCH 23/60] remove unneeded/mistakenly added tests. Add dim_f check to distance test --- tests/test_objective_funs.py | 163 +---------------------------------- tests/test_optimizer.py | 53 ------------ 2 files changed, 2 insertions(+), 214 deletions(-) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 176dee569e..db181a5729 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -614,6 +614,7 @@ def test_plasma_vessel_distance(self): ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert d.size == obj.dim_f assert abs(d.min() - (a_s - a_p)) < 1e-14 assert abs(d.max() - (a_s - a_p)) < surf_grid.spacing[0, 1] * a_p @@ -653,6 +654,7 @@ def test_plasma_vessel_distance(self): ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert d.size == obj.dim_f assert np.all(np.abs(d) < a_s - a_p) # for large enough alpha, should be same as actual min @@ -1122,91 +1124,6 @@ def test_target_profiles(): ) -@pytest.mark.unit -def test_plasma_vessel_distance(): - """Test calculation of min distance from plasma to vessel.""" - R0 = 10.0 - a_p = 1.0 - a_s = 2.0 - # default eq has R0=10, a=1 - eq = Equilibrium(M=3, N=2) - # surface with same R0, a=2, so true d=1 for all pts - surface = FourierRZToroidalSurface( - R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] - ) - # For equally spaced grids, should get true d=1 - surf_grid = LinearGrid(M=5, N=6) - plas_grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistance( - eq=eq, plasma_grid=plas_grid, surface_grid=surf_grid, surface=surface - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - np.testing.assert_allclose(d, a_s - a_p) - - # for unequal M, should have error of order M_spacing*a_p - surf_grid = LinearGrid(M=5, N=6) - plas_grid = LinearGrid(M=10, N=6) - obj = PlasmaVesselDistance( - eq=eq, - plasma_grid=plas_grid, - surface_grid=surf_grid, - surface=surface, - surface_fixed=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert abs(d.min() - (a_s - a_p)) < 1e-14 - assert abs(d.max() - (a_s - a_p)) < surf_grid.spacing[0, 1] * a_p - - # for unequal N, should have error of order N_spacing*R0 - surf_grid = LinearGrid(M=5, N=6) - plas_grid = LinearGrid(M=5, N=12) - obj = PlasmaVesselDistance( - eq=eq, plasma_grid=plas_grid, surface_grid=surf_grid, surface=surface - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert abs(d.min() - (a_s - a_p)) < 1e-14 - assert abs(d.max() - (a_s - a_p)) < surf_grid.spacing[0, 2] * R0 - # ensure that it works (dimension-wise) when compute_scaled is called - _ = obj.compute_scaled(*obj.xs(eq, surface)) - - grid = LinearGrid(L=3, M=3, N=3) - eq = Equilibrium() - surf = FourierRZToroidalSurface() - obj = PlasmaVesselDistance(surface=surf, surface_grid=grid, plasma_grid=grid, eq=eq) - with pytest.warns(UserWarning): - obj.build() - - # test softmin, should give value less than true minimum - surf_grid = LinearGrid(M=5, N=6) - plas_grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistance( - eq=eq, - plasma_grid=plas_grid, - surface_grid=surf_grid, - surface=surface, - use_softmin=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert np.all(np.abs(d) < a_s - a_p) - - # for large enough alpha, should be same as actual min - obj = PlasmaVesselDistance( - eq=eq, - plasma_grid=plas_grid, - surface_grid=surf_grid, - surface=surface, - use_softmin=True, - alpha=100, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - np.testing.assert_allclose(d, a_s - a_p) - - @pytest.mark.unit def test_signed_plasma_vessel_distance(): """Test calculation of signed distance from plasma to vessel.""" @@ -1255,82 +1172,6 @@ def test_signed_plasma_vessel_distance(): np.testing.assert_allclose(d, -0.5 * a_p) -@pytest.mark.unit -def test_mean_curvature(): - """Test for mean curvature objective function.""" - # torus should have mean curvature negative everywhere - eq = Equilibrium() - obj = MeanCurvature(eq=eq) - obj.build() - H = obj.compute_unscaled(*obj.xs(eq)) - assert np.all(H <= 0) - - # more shaped case like NCSX should have some positive curvature - eq = get("NCSX") - obj = MeanCurvature(eq=eq) - obj.build() - H = obj.compute_unscaled(*obj.xs(eq)) - assert np.any(H > 0) - - # check using the surface - obj = MeanCurvature(eq=eq.surface) - obj.build() - H = obj.compute_unscaled(*obj.xs(eq.surface)) - assert np.any(H > 0) - - -@pytest.mark.unit -def test_principal_curvature(): - """Test for principal curvature objective function.""" - eq1 = get("DSHAPE") - eq2 = get("NCSX") - obj1 = PrincipalCurvature(eq=eq1, normalize=False) - obj1.build() - K1 = obj1.compute_unscaled(*obj1.xs(eq1)) - obj2 = PrincipalCurvature(eq=eq2, normalize=False) - obj2.build() - K2 = obj2.compute_unscaled(*obj2.xs(eq2)) - - # simple test: NCSX should have higher mean absolute curvature than DSHAPE - assert K1.mean() < K2.mean() - - # same test but using the surface directly - obj1 = PrincipalCurvature(eq=eq1.surface, normalize=False) - obj1.build() - K1 = obj1.compute_unscaled(*obj1.xs(eq1.surface)) - obj2 = PrincipalCurvature(eq=eq2.surface, normalize=False) - obj2.build() - K2 = obj2.compute_unscaled(*obj2.xs(eq2.surface)) - - # simple test: NCSX should have higher mean absolute curvature than DSHAPE - assert K1.mean() < K2.mean() - - -@pytest.mark.unit -def test_field_scale_length(): - """Test for B field scale length objective function.""" - surf1 = FourierRZToroidalSurface( - R_lmn=[5, 1], Z_lmn=[-1], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]], NFP=1 - ) - surf2 = FourierRZToroidalSurface( - R_lmn=[10, 2], Z_lmn=[-2], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]], NFP=1 - ) - eq1 = Equilibrium(L=2, M=2, N=0, surface=surf1) - eq2 = Equilibrium(L=2, M=2, N=0, surface=surf2) - eq1.solve() - eq2.solve() - - obj1 = BScaleLength(eq=eq1, normalize=False) - obj2 = BScaleLength(eq=eq2, normalize=False) - obj1.build() - obj2.build() - - L1 = obj1.compute_unscaled(*obj1.xs(eq1)) - L2 = obj2.compute_unscaled(*obj2.xs(eq2)) - - np.testing.assert_array_less(L1, L2) - - @pytest.mark.unit def test_profile_objective_print(capsys): """Test that the profile objectives print correctly.""" diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index da9bc0ad5d..5596dd64dc 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -983,59 +983,6 @@ def test_constrained_AL_scalar(): np.testing.assert_array_less(-Dwell, ctol) -@pytest.mark.slow -@pytest.mark.unit -@pytest.mark.optimize -def test_proximal_with_PlasmaVesselDistance(): - """Tests that the proximal projection works with fixed surface distance obj.""" - eq = desc.examples.get("SOLOVEV") - - constraints = ( - ForceBalance(eq=eq), - FixPressure(eq=eq), # fix pressure profile - FixIota(eq=eq), # fix rotational transform profile - FixPsi(eq=eq), # fix total toroidal magnetic flux - ) - # circular surface - a = 2 - R0 = 4 - surf = FourierRZToroidalSurface( - R_lmn=[R0, a], - Z_lmn=[0.0, -a], - modes_R=np.array([[0, 0], [1, 0]]), - modes_Z=np.array([[0, 0], [-1, 0]]), - sym=True, - NFP=eq.NFP, - ) - - grid = LinearGrid(M=eq.M, N=0, NFP=eq.NFP) - obj = PlasmaVesselDistance( - surface=surf, eq=eq, target=0.5, plasma_grid=grid, surface_fixed=True - ) - objective = ObjectiveFunction((obj,)) - - optimizer = Optimizer("proximal-lsq-exact") - eq.optimize( - objective, - constraints, - verbose=3, - maxiter=3, - ) - - # make sure it also works if proximal is given multiple objects in things - obj = PlasmaVesselDistance( - surface=surf, eq=eq, target=0.5, plasma_grid=grid, surface_fixed=False - ) - objective = ObjectiveFunction((obj,)) - (eq, surf), result = optimizer.optimize( - (eq, surf), - objective, - constraints, - verbose=3, - maxiter=3, - ) - - @pytest.mark.slow @pytest.mark.unit @pytest.mark.optimize From b5a8a83c0e2a4cf9b451eeaf1de45b1192769b03 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 23 May 2024 16:16:33 -0400 Subject: [PATCH 24/60] fix dimension error when grids have different poloidal resolution, add test --- desc/objectives/_geometry.py | 24 ++++++++++++++---------- tests/test_objective_funs.py | 22 +++++++++++++++++++--- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index ead9a64235..43279634af 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -807,13 +807,17 @@ def compute(self, equil_params, surface_params=None, constants=None): if self._use_signed_distance: surface_coords_rpz = xyz2rpz(surface_coords) - def _find_angle_vec(R, Z, Rtest, Ztest): - # R Z and surface points, - # Rtest Ztest are the point we wanna check is inside - # the surface or not - Rbool = R[:, None] > Rtest - Zbool = Z[:, None] > Ztest - # these Rbool are now size (Nsurf, Ntest) + def _find_angle_vec(R, Z, Rsurf, Zsurf): + # R Z are plasma points, + # Rsurf Zsurf are the surface points being checked for whether + # or not they are inside the plasma + + # algorithm based off of "An Incremental Angle Point in Polygon Test", + # K. Weiler, https://doi.org/10.1016/B978-0-12-336156-1.50012-4 + + Rbool = R[:, None] > Rsurf + Zbool = Z[:, None] > Zsurf + # these are now size (Nplasma, Nsurf) quads = jnp.zeros_like(Rbool) quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) quads = jnp.where( @@ -830,12 +834,12 @@ def _find_angle_vec(R, Z, Rtest, Ztest): deltas = quads[1:, :] - quads[0:-1, :] deltas = jnp.where(deltas == 3, -1, deltas) deltas = jnp.where(deltas == -3, 1, deltas) - # then flip sign if the R intercept is > Rtest and the + # then flip sign if the R intercept is > Rsurf and the # quadrant flipped over a diagonal b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = Rtest[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = Rsurf[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint > Rtest), + jnp.logical_and(jnp.abs(deltas) == 2, Rint.T > Rsurf), -deltas, deltas, ) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index db181a5729..c5b9f5e450 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -1136,17 +1136,17 @@ def test_signed_plasma_vessel_distance(): surface = FourierRZToroidalSurface( R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] ) - # For equally spaced grids, should get true d=1 grid = LinearGrid(M=5, N=6) obj = PlasmaVesselDistance( eq=eq, surface_grid=grid, - plasma_grid=grid, + plasma_grid=LinearGrid(M=10, N=6), surface=surface, use_signed_distance=True, ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size np.testing.assert_allclose(d, a_s - a_p) # ensure that it works (dimension-wise) when compute_scaled is called @@ -1163,14 +1163,30 @@ def test_signed_plasma_vessel_distance(): obj = PlasmaVesselDistance( eq=eq, surface_grid=grid, - plasma_grid=grid, + plasma_grid=LinearGrid(M=10, N=6), surface=surface, use_signed_distance=True, ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size np.testing.assert_allclose(d, -0.5 * a_p) + # ensure it works with different sized grids (poloidal resolution different) + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=LinearGrid(M=10, N=6), + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size + assert abs(d.max() - (-0.5 * a_p)) < 1e-14 + assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 + @pytest.mark.unit def test_profile_objective_print(capsys): From 01699648c2b2844253341171725137a2d146153b Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 24 May 2024 11:32:29 -0400 Subject: [PATCH 25/60] fix test --- tests/test_objective_funs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index c5b9f5e450..a1fdd9f967 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -1140,7 +1140,7 @@ def test_signed_plasma_vessel_distance(): obj = PlasmaVesselDistance( eq=eq, surface_grid=grid, - plasma_grid=LinearGrid(M=10, N=6), + plasma_grid=grid, surface=surface, use_signed_distance=True, ) @@ -1163,7 +1163,7 @@ def test_signed_plasma_vessel_distance(): obj = PlasmaVesselDistance( eq=eq, surface_grid=grid, - plasma_grid=LinearGrid(M=10, N=6), + plasma_grid=grid, surface=surface, use_signed_distance=True, ) From b0ada02034e7382d874bfa1980db198f3da88baf Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 24 May 2024 14:22:43 -0400 Subject: [PATCH 26/60] use vmap instead of for loop --- desc/objectives/_geometry.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 43279634af..ae195d3ffc 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -3,6 +3,7 @@ import warnings import numpy as np +from jax import vmap from desc.backend import jnp from desc.compute import get_profiles, get_transforms, rpz2xyz, xyz2rpz @@ -853,12 +854,19 @@ def _find_angle_vec(R, Z, Rsurf, Zsurf): pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) return pt_sign + plasma_coords_rpz = plasma_coords_rpz.reshape( + constants["equil_transforms"]["grid"].num_zeta, + constants["equil_transforms"]["grid"].num_theta, + 3, + ) + surface_coords_rpz = surface_coords_rpz.reshape( + constants["surface_transforms"]["grid"].num_zeta, + constants["surface_transforms"]["grid"].num_theta, + 3, + ) + # loop over zeta planes - for plasma_zeta_idx, surface_zeta_idx in zip( - constants["plasma_zeta_indices"], constants["surface_zeta_indices"] - ): - plasma_pts_at_zeta_plane = plasma_coords_rpz[plasma_zeta_idx, :] - surface_pts_at_zeta_plane = surface_coords_rpz[surface_zeta_idx, :] + def fun(plasma_pts_at_zeta_plane, surface_pts_at_zeta_plane): plasma_pts_at_zeta_plane = jnp.vstack( (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) ) @@ -872,7 +880,11 @@ def _find_angle_vec(R, Z, Rsurf, Zsurf): ) # need to assign to correct index of the points on the surface - point_signs = point_signs.at[surface_zeta_idx].set(pt_sign) + return pt_sign + + point_signs = vmap(fun, in_axes=0)( + plasma_coords_rpz, surface_coords_rpz + ).flatten() # at end here, point_signs is either +/- 1 with # positive meaning the surface pt # is outside the plasma and -1 if the surface pt is From 55b7056ae57681fb5745f4d67ce1304b5bfbe53d Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 24 May 2024 14:23:42 -0400 Subject: [PATCH 27/60] remove comment --- desc/objectives/_geometry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index ae195d3ffc..43cd738ce0 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -879,7 +879,6 @@ def fun(plasma_pts_at_zeta_plane, surface_pts_at_zeta_plane): surface_pts_at_zeta_plane[:, 2], ) - # need to assign to correct index of the points on the surface return pt_sign point_signs = vmap(fun, in_axes=0)( From 868ce08f266faae843d52748b971f3e27c00d7df Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 24 May 2024 14:25:29 -0400 Subject: [PATCH 28/60] remove unneeded indexing --- desc/objectives/_geometry.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 43cd738ce0..d3a8e6eb09 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -719,21 +719,6 @@ def build(self, use_jit=True, verbose=1): "quad_weights": w, } - if self._use_signed_distance: - # get the indices corresponding to the grid points - # at each distinct zeta plane, so that can be used - # in compute to separate the computed pts by zeta plane - zetas = plasma_grid.nodes[plasma_grid.unique_zeta_idx, 2] - plasma_zeta_indices = [ - np.where(np.isclose(plasma_grid.nodes[:, 2], zeta))[0] for zeta in zetas - ] - surface_zeta_indices = [ - np.where(np.isclose(surface_grid.nodes[:, 2], zeta))[0] - for zeta in zetas - ] - self._constants["plasma_zeta_indices"] = plasma_zeta_indices - self._constants["surface_zeta_indices"] = surface_zeta_indices - if self._surface_fixed: # precompute the surface coordinates # as the surface is fixed during the optimization From 31f01845ab8e7b1def144342b182832385a60dc9 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 10 Jun 2024 12:19:49 -0400 Subject: [PATCH 29/60] address comments --- desc/objectives/_geometry.py | 29 +++++++++++++++-------------- tests/test_examples.py | 2 +- tests/test_objective_funs.py | 2 +- tests/test_optimizer.py | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index d3a8e6eb09..3a7a17c27f 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -3,9 +3,8 @@ import warnings import numpy as np -from jax import vmap -from desc.backend import jnp +from desc.backend import jnp, vmap from desc.compute import get_profiles, get_transforms, rpz2xyz, xyz2rpz from desc.compute.utils import _compute as compute_fun from desc.compute.utils import safenorm @@ -522,10 +521,10 @@ class PlasmaVesselDistance(_Objective): points on surface corresponding to the grid that the plasma-vessel distance is evaluated at, which can cause cusps or regions of very large curvature. - NOTE: When use_softmin=True, ensures that alpha*values passed in is + NOTE: When use_softmin=True, ensures that softmin_alpha*values passed in is at least >1, otherwise the softmin will return inaccurate approximations of the minimum. Will automatically multiply array values by 2 / min_val if the min - of alpha*array is <1. This is to avoid inaccuracies that arise when values <1 + of softmin_alpha*array is <1. This is to avoid inaccuracies when values <1 are present in the softmin, which can cause inaccurate mins or even incorrect signs of the softmin versus the actual min. @@ -581,13 +580,13 @@ class PlasmaVesselDistance(_Objective): self.things = [eq] only. If False, the surface coordinates are computed at every iteration. False by default, so that self.things = [eq, surface] - alpha: float, optional - Parameter used for softmin. The larger alpha, the closer the softmin - approximates the hardmin. softmin -> hardmin as alpha -> infinity. - if alpha*array < 1, the underlying softmin will automatically multiply - the array by 2/min_val to ensure that alpha*array>1. Making alpha larger - than this minimum value will make the softmin a more accurate approximation - of the true min. + softmin_alpha: float, optional + Parameter used for softmin. The larger softmin_alpha, the closer the softmin + approximates the hardmin. softmin -> hardmin as softmin_alpha -> infinity. + if softmin_alpha*array < 1, the underlying softmin will automatically multiply + the array by 2/min_val to ensure that softmin_alpha*array>1. Making + softmin_alpha larger than this minimum value will make the softmin a + more accurate approximation of the true min. name : str, optional Name of the objective function. """ @@ -612,7 +611,7 @@ def __init__( use_softmin=False, use_signed_distance=False, surface_fixed=False, - alpha=1.0, + softmin_alpha=1.0, name="plasma-vessel distance", ): if target is None and bounds is None: @@ -623,7 +622,7 @@ def __init__( self._use_softmin = use_softmin self._use_signed_distance = use_signed_distance self._surface_fixed = surface_fixed - self._alpha = alpha + self._softmin_alpha = softmin_alpha super().__init__( things=[eq, self._surface] if not surface_fixed else [eq], target=target, @@ -875,7 +874,9 @@ def fun(plasma_pts_at_zeta_plane, surface_pts_at_zeta_plane): # inside the plasma if self._use_softmin: # do softmin - return jnp.apply_along_axis(softmin, 0, d, self._alpha) * point_signs + return ( + jnp.apply_along_axis(softmin, 0, d, self._softmin_alpha) * point_signs + ) else: # do hardmin return d.min(axis=0) * point_signs diff --git a/tests/test_examples.py b/tests/test_examples.py index b84cd967b1..333b0d6950 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -983,7 +983,7 @@ def test_non_eq_optimization(): use_softmin=True, surface_grid=grid, plasma_grid=grid, - alpha=5000, + softmin_alpha=5000, ) objective = ObjectiveFunction((obj,)) optimizer = Optimizer("lsq-auglag") diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index a1fdd9f967..b6ce46724a 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -664,7 +664,7 @@ def test_plasma_vessel_distance(self): surface_grid=surf_grid, surface=surface, use_softmin=True, - alpha=100, + sofmtin_alpha=100, ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 5596dd64dc..681553df5e 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -1025,7 +1025,7 @@ def test_signed_PlasmaVesselDistance(): plasma_grid=grid, use_signed_distance=True, use_softmin=True, - alpha=100, + softmin_alpha=100, ) objective = ObjectiveFunction((obj,)) From 8e590b64d4bac25f43490cbd442cee72a7230dbe Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 18 Jun 2024 10:21:42 -0400 Subject: [PATCH 30/60] remove unneeded where, change quads to quadrants --- desc/objectives/_geometry.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 3a7a17c27f..252cadbb15 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -803,20 +803,19 @@ def _find_angle_vec(R, Z, Rsurf, Zsurf): Rbool = R[:, None] > Rsurf Zbool = Z[:, None] > Zsurf # these are now size (Nplasma, Nsurf) - quads = jnp.zeros_like(Rbool) - quads = jnp.where(jnp.logical_and(Rbool, Zbool), 0, quads) - quads = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quads + quadrants = jnp.zeros_like(Rbool) + quadrants = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quadrants ) - quads = jnp.where( + quadrants = jnp.where( jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), 2, - quads, + quadrants, ) - quads = jnp.where( - jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quads + quadrants = jnp.where( + jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quadrants ) - deltas = quads[1:, :] - quads[0:-1, :] + deltas = quadrants[1:, :] - quadrants[0:-1, :] deltas = jnp.where(deltas == 3, -1, deltas) deltas = jnp.where(deltas == -3, 1, deltas) # then flip sign if the R intercept is > Rsurf and the From 94dcfab7f7be9ee27f9cde2405b88bc7868595e7 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 20 Jun 2024 18:30:48 -0400 Subject: [PATCH 31/60] address comments --- desc/objectives/_geometry.py | 9 ++- tests/test_objective_funs.py | 146 ++++++++++++++++++++--------------- 2 files changed, 88 insertions(+), 67 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 252cadbb15..90be98d85d 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -9,7 +9,7 @@ from desc.compute.utils import _compute as compute_fun from desc.compute.utils import safenorm from desc.grid import LinearGrid, QuadratureGrid -from desc.utils import Timer, errorif +from desc.utils import Timer, errorif, parse_argname_change from .normalization import compute_scaling_factors from .objective_funs import _Objective @@ -609,10 +609,11 @@ def __init__( surface_grid=None, plasma_grid=None, use_softmin=False, - use_signed_distance=False, surface_fixed=False, softmin_alpha=1.0, name="plasma-vessel distance", + use_signed_distance=False, + **kwargs ): if target is None and bounds is None: bounds = (1, np.inf) @@ -622,7 +623,9 @@ def __init__( self._use_softmin = use_softmin self._use_signed_distance = use_signed_distance self._surface_fixed = surface_fixed - self._softmin_alpha = softmin_alpha + self._softmin_alpha = parse_argname_change( + softmin_alpha, kwargs, "alpha", "softmin_alpha" + ) super().__init__( things=[eq, self._surface] if not surface_fixed else [eq], target=target, diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index b6ce46724a..0d789d85fd 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -940,6 +940,88 @@ def test(eq, field, correct_value, rtol=1e-14, grid=None): test(eq, field, psi_from_field) + @pytest.mark.unit + def test_signed_plasma_vessel_distance(self): + """Test calculation of signed distance from plasma to vessel.""" + R0 = 10.0 + a_p = 1.0 + a_s = 2.0 + # default eq has R0=10, a=1 + eq = Equilibrium(M=3, N=2) + # surface with same R0, a=2, so true d=1 for all pts + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] + ) + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=grid, + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size + np.testing.assert_allclose(d, a_s - a_p) + + # ensure that it works (dimension-wise) when compute_scaled is called + _ = obj.compute_scaled(*obj.xs(eq, surface)) + + # For plasma outside surface, should get signed distance + surface = FourierRZToroidalSurface( + R_lmn=[R0, a_p * 0.5], + Z_lmn=[-a_p * 0.5], + modes_R=[[0, 0], [1, 0]], + modes_Z=[[-1, 0]], + ) + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=grid, + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size + np.testing.assert_allclose(d, -0.5 * a_p) + + # ensure it works with different sized grids (poloidal resolution different) + grid = LinearGrid(M=5, N=6) + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=LinearGrid(M=10, N=6), + surface=surface, + use_signed_distance=True, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size + assert abs(d.max() - (-0.5 * a_p)) < 1e-14 + assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 + + # ensure it works with different sized grids (poloidal resolution different) + # and using softmin (with deprecated name alpha) + grid = LinearGrid(M=5, N=6) + with pytest.raises(FutureWarning): + obj = PlasmaVesselDistance( + eq=eq, + surface_grid=grid, + plasma_grid=LinearGrid(M=10, N=6), + surface=surface, + use_signed_distance=True, + use_softmin=True, + alpha=4000, + ) + obj.build() + d = obj.compute_unscaled(*obj.xs(eq, surface)) + assert obj.dim_f == d.size + assert abs(d.max() - (-0.5 * a_p)) < 1e-14 + assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 + @pytest.mark.regression def test_derivative_modes(): @@ -1124,70 +1206,6 @@ def test_target_profiles(): ) -@pytest.mark.unit -def test_signed_plasma_vessel_distance(): - """Test calculation of signed distance from plasma to vessel.""" - R0 = 10.0 - a_p = 1.0 - a_s = 2.0 - # default eq has R0=10, a=1 - eq = Equilibrium(M=3, N=2) - # surface with same R0, a=2, so true d=1 for all pts - surface = FourierRZToroidalSurface( - R_lmn=[R0, a_s], Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]] - ) - grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistance( - eq=eq, - surface_grid=grid, - plasma_grid=grid, - surface=surface, - use_signed_distance=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert obj.dim_f == d.size - np.testing.assert_allclose(d, a_s - a_p) - - # ensure that it works (dimension-wise) when compute_scaled is called - _ = obj.compute_scaled(*obj.xs(eq, surface)) - - # For plasma outside surface, should get signed distance - surface = FourierRZToroidalSurface( - R_lmn=[R0, a_p * 0.5], - Z_lmn=[-a_p * 0.5], - modes_R=[[0, 0], [1, 0]], - modes_Z=[[-1, 0]], - ) - grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistance( - eq=eq, - surface_grid=grid, - plasma_grid=grid, - surface=surface, - use_signed_distance=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert obj.dim_f == d.size - np.testing.assert_allclose(d, -0.5 * a_p) - - # ensure it works with different sized grids (poloidal resolution different) - grid = LinearGrid(M=5, N=6) - obj = PlasmaVesselDistance( - eq=eq, - surface_grid=grid, - plasma_grid=LinearGrid(M=10, N=6), - surface=surface, - use_signed_distance=True, - ) - obj.build() - d = obj.compute_unscaled(*obj.xs(eq, surface)) - assert obj.dim_f == d.size - assert abs(d.max() - (-0.5 * a_p)) < 1e-14 - assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 - - @pytest.mark.unit def test_profile_objective_print(capsys): """Test that the profile objectives print correctly.""" From 36d1aba19e9c6ce1a47fdd27a2713f7770613e89 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 20 Jun 2024 18:57:27 -0400 Subject: [PATCH 32/60] change test to regression --- tests/test_optimizer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 681553df5e..3a57d4e16a 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -984,17 +984,18 @@ def test_constrained_AL_scalar(): @pytest.mark.slow -@pytest.mark.unit +@pytest.mark.regression @pytest.mark.optimize def test_signed_PlasmaVesselDistance(): """Tests that signed distance works with surface optimization.""" eq = desc.examples.get("HELIOTRON") + eq.change_resolution(M=2, N=2) constraints = (FixParameters(eq),) # don't want eq to change surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=eq.M, N=eq.N, NFP=eq.NFP) + grid = LinearGrid(M=10, N=2, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, eq=eq, @@ -1048,7 +1049,7 @@ def test_signed_PlasmaVesselDistance(): eq = Equilibrium(M=1, N=1) surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=12, N=2, NFP=eq.NFP) + grid = LinearGrid(M=10, N=2, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, From 8f998f9e8ce864c92536f3bbdedbbd8a634c9b00 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 21 Jun 2024 09:20:26 -0400 Subject: [PATCH 33/60] fix error in test --- desc/objectives/_geometry.py | 5 ++++- tests/test_objective_funs.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 90be98d85d..dc64448a62 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -613,7 +613,7 @@ def __init__( softmin_alpha=1.0, name="plasma-vessel distance", use_signed_distance=False, - **kwargs + **kwargs, ): if target is None and bounds is None: bounds = (1, np.inf) @@ -626,6 +626,9 @@ def __init__( self._softmin_alpha = parse_argname_change( softmin_alpha, kwargs, "alpha", "softmin_alpha" ) + assert ( + len(kwargs) == 0 + ), f"PlasmaVesselDistance got unexpected keyword argument: {kwargs.keys()}" super().__init__( things=[eq, self._surface] if not surface_fixed else [eq], target=target, diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index 0d789d85fd..7efb45f1a1 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -664,7 +664,7 @@ def test_plasma_vessel_distance(self): surface_grid=surf_grid, surface=surface, use_softmin=True, - sofmtin_alpha=100, + softmin_alpha=100, ) obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) From 004108a97c183e26fdb5983b20c24007ec13a75d Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 14:54:39 -0400 Subject: [PATCH 34/60] move sign distance function to a utility function --- desc/objectives/_geometry.py | 49 ++----------------------- desc/objectives/utils.py | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 8d3b325cc2..dc3728cbaa 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -11,7 +11,7 @@ from .normalization import compute_scaling_factors from .objective_funs import _Objective -from .utils import softmin +from .utils import check_if_points_are_inside_perimeter, softmin class AspectRatio(_Objective): @@ -802,51 +802,6 @@ def compute(self, equil_params, surface_params=None, constants=None): if self._use_signed_distance: surface_coords_rpz = xyz2rpz(surface_coords) - def _find_angle_vec(R, Z, Rsurf, Zsurf): - # R Z are plasma points, - # Rsurf Zsurf are the surface points being checked for whether - # or not they are inside the plasma - - # algorithm based off of "An Incremental Angle Point in Polygon Test", - # K. Weiler, https://doi.org/10.1016/B978-0-12-336156-1.50012-4 - - Rbool = R[:, None] > Rsurf - Zbool = Z[:, None] > Zsurf - # these are now size (Nplasma, Nsurf) - quadrants = jnp.zeros_like(Rbool) - quadrants = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quadrants - ) - quadrants = jnp.where( - jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), - 2, - quadrants, - ) - quadrants = jnp.where( - jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quadrants - ) - deltas = quadrants[1:, :] - quadrants[0:-1, :] - deltas = jnp.where(deltas == 3, -1, deltas) - deltas = jnp.where(deltas == -3, 1, deltas) - # then flip sign if the R intercept is > Rsurf and the - # quadrant flipped over a diagonal - b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) - Rint = Rsurf[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) - deltas = jnp.where( - jnp.logical_and(jnp.abs(deltas) == 2, Rint.T > Rsurf), - -deltas, - deltas, - ) - pt_sign = jnp.sum(deltas, axis=0) - # positive distance if the plasma pt is inside the surface, else - # negative distance is assigned - # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, - # assign positive distance - # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so - # assign negative distance - pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) - return pt_sign - plasma_coords_rpz = plasma_coords_rpz.reshape( constants["equil_transforms"]["grid"].num_zeta, constants["equil_transforms"]["grid"].num_theta, @@ -865,7 +820,7 @@ def fun(plasma_pts_at_zeta_plane, surface_pts_at_zeta_plane): ) surface_pts_at_zeta_plane - pt_sign = _find_angle_vec( + pt_sign = check_if_points_are_inside_perimeter( plasma_pts_at_zeta_plane[:, 0], plasma_pts_at_zeta_plane[:, 2], surface_pts_at_zeta_plane[:, 0], diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 55f4f671c2..e0d2833514 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -307,3 +307,73 @@ def _parse_callable_target_bounds(target, bounds, x): if bounds is not None and callable(bounds[1]): bounds = (bounds[0], bounds[1](x)) return target, bounds + + +# TODO: dont need to be R,Z, can by x1, x2 or something +def check_if_points_are_inside_perimeter(R, Z, Rcheck, Zcheck): + """Function to check if the given points is inside the given polyognal perimeter. + + Rcheck, Zcheck are the points to check, and R, Z define the perimeter + in which to check. This function assumes that all points are in the same + plane. Function will return an array pf signs (+/- 1), with positive sign meaning + the point is inside of the given perimeter, and a negative sign meaning the point + is outside of the given perimeter. + + Algorithm based off of "An Incremental Angle Point in Polygon Test", + K. Weiler, https://doi.org/10.1016/B978-0-12-336156-1.50012-4 + + Parameters + ---------- + R,Z : ndarray + 1-D arrays of coordinates of the points defining the polygonal + perimeter. The function will determine if the check point is inside + or outside of this perimeter. + Rcheck, Zcheck : ndarray + coordinates of the points being checked if they are inside or outside of the + given perimeter. + + Returns + ------- + pt_sign : ndarray of {-1,1} + Integers corresponding to if the given point is inside or outside of the given + perimeter, with pt_sign[i]>0 meaning the point given by Rcheck[i], Zcheck[i] is + inside of the given perimeter, and a negative sign meaning the point is outside + of the given perimeter. + + """ + # R Z are the perimeter points + # Rcheck Zcheck are the surface points being checked for whether + # or not they are inside the plasma + + Rbool = R[:, None] > Rcheck + Zbool = Z[:, None] > Zcheck + # these are now size (Nplasma, Nsurf) + quadrants = jnp.zeros_like(Rbool) + quadrants = jnp.where(jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quadrants) + quadrants = jnp.where( + jnp.logical_and(jnp.logical_not(Rbool), jnp.logical_not(Zbool)), + 2, + quadrants, + ) + quadrants = jnp.where(jnp.logical_and(Rbool, jnp.logical_not(Zbool)), 3, quadrants) + deltas = quadrants[1:, :] - quadrants[0:-1, :] + deltas = jnp.where(deltas == 3, -1, deltas) + deltas = jnp.where(deltas == -3, 1, deltas) + # then flip sign if the R intercept is > Rcheck and the + # quadrant flipped over a diagonal + b = (Z[1:] / R[1:] - Z[0:-1] / R[0:-1]) / (Z[1:] - Z[0:-1]) + Rint = Rcheck[:, None] - b * (R[1:] - R[0:-1]) / (Z[1:] - Z[0:-1]) + deltas = jnp.where( + jnp.logical_and(jnp.abs(deltas) == 2, Rint.T > Rcheck), + -deltas, + deltas, + ) + pt_sign = jnp.sum(deltas, axis=0) + # positive distance if the plasma pt is inside the surface, else + # negative distance is assigned + # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, + # assign positive distance + # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so + # assign negative distance + pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) + return pt_sign From 22c882eaee1f015a71891eeeb79f043f60753a3b Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 15:10:03 -0400 Subject: [PATCH 35/60] relabel radii in test --- tests/test_objective_funs.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_objective_funs.py b/tests/test_objective_funs.py index a6c6822e2b..94e63ba286 100644 --- a/tests/test_objective_funs.py +++ b/tests/test_objective_funs.py @@ -1157,9 +1157,10 @@ def test_signed_plasma_vessel_distance(self): _ = obj.compute_scaled(*obj.xs(eq, surface)) # For plasma outside surface, should get signed distance + a_s = 0.5 * a_p surface = FourierRZToroidalSurface( - R_lmn=[R0, a_p * 0.5], - Z_lmn=[-a_p * 0.5], + R_lmn=[R0, a_s], + Z_lmn=[-a_s], modes_R=[[0, 0], [1, 0]], modes_Z=[[-1, 0]], ) @@ -1174,7 +1175,7 @@ def test_signed_plasma_vessel_distance(self): obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) assert obj.dim_f == d.size - np.testing.assert_allclose(d, -0.5 * a_p) + np.testing.assert_allclose(d, a_s - a_p) # ensure it works with different sized grids (poloidal resolution different) grid = LinearGrid(M=5, N=6) @@ -1188,8 +1189,8 @@ def test_signed_plasma_vessel_distance(self): obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) assert obj.dim_f == d.size - assert abs(d.max() - (-0.5 * a_p)) < 1e-14 - assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 + assert abs(d.max() - (-a_s)) < 1e-14 + assert abs(d.min() - (-a_s)) < grid.spacing[0, 1] * a_s # ensure it works with different sized grids (poloidal resolution different) # and using softmin (with deprecated name alpha) @@ -1207,8 +1208,8 @@ def test_signed_plasma_vessel_distance(self): obj.build() d = obj.compute_unscaled(*obj.xs(eq, surface)) assert obj.dim_f == d.size - assert abs(d.max() - (-0.5 * a_p)) < 1e-14 - assert abs(d.min() - (-0.5 * a_p)) < grid.spacing[0, 1] * a_p * 0.5 + assert abs(d.max() - (-a_s)) < 1e-14 + assert abs(d.min() - (-a_s)) < grid.spacing[0, 1] * a_s @pytest.mark.regression From 3cb05acf3a556587803e6157e584d4bc3071db66 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 16:01:12 -0400 Subject: [PATCH 36/60] add eq_fixed flag --- desc/objectives/_geometry.py | 91 +++++++++++++++++++++++++----------- tests/test_optimizer.py | 18 +++---- 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index dc3728cbaa..661add99c3 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -571,13 +571,13 @@ class PlasmaVesselDistance(_Objective): negative if outside of the bounding surface. NOTE: ``plasma_grid`` and ``surface_grid`` must have the same toroidal angle values for signed distance to be used. - surface_fixed: bool, optional - Whether the surface the distance from the plasma is computed to - is fixed or not. If True, the surface is fixed and its coordinates are - precomputed, which saves on computation time during optimization, and - self.things = [eq] only. - If False, the surface coordinates are computed at every iteration. + eq_fixed, surface_fixed: bool, optional + Whether the eq/surface is fixed or not. If True, the eq/surface is fixed + and its coordinates are precomputed, which saves on computation time during + optimization, and self.things = [surface]/[eq] only. + If False, the eq/surface coordinates are computed at every iteration. False by default, so that self.things = [eq, surface] + Both cannot be True. softmin_alpha: float, optional Parameter used for softmin. The larger softmin_alpha, the closer the softmin approximates the hardmin. softmin -> hardmin as softmin_alpha -> infinity. @@ -607,6 +607,7 @@ def __init__( surface_grid=None, plasma_grid=None, use_softmin=False, + eq_fixed=False, surface_fixed=False, softmin_alpha=1.0, name="plasma-vessel distance", @@ -621,14 +622,27 @@ def __init__( self._use_softmin = use_softmin self._use_signed_distance = use_signed_distance self._surface_fixed = surface_fixed + self._eq_fixed = eq_fixed + self._eq = eq + errorif( + eq_fixed and surface_fixed, ValueError, "Cannot fix both eq and surface" + ) + self._softmin_alpha = parse_argname_change( softmin_alpha, kwargs, "alpha", "softmin_alpha" ) - assert ( - len(kwargs) == 0 - ), f"PlasmaVesselDistance got unexpected keyword argument: {kwargs.keys()}" + errorif( + len(kwargs) != 0, + AssertionError, + f"PlasmaVesselDistance got unexpected keyword argument: {kwargs.keys()}", + ) + things = [] + if not eq_fixed: + things.append(eq) + if not surface_fixed: + things.append(surface) super().__init__( - things=[eq, self._surface] if not surface_fixed else [eq], + things=things, target=target, bounds=bounds, weight=weight, @@ -650,11 +664,15 @@ def build(self, use_jit=True, verbose=1): Level of output. """ - eq = self.things[0] - surface = self._surface if self._surface_fixed else self.things[1] - # if things[1] is different than self._surface, update self._surface - if surface != self._surface: - self._surface = surface + if self._eq_fixed: + eq = self._eq + surface = self.things[0] + elif self._surface_fixed: + eq = self.things[0] + surface = self._surface + else: + eq = self.things[0] + surface = self.things[1] if self._surface_grid is None: surface_grid = LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP) else: @@ -740,7 +758,15 @@ def build(self, use_jit=True, verbose=1): basis="xyz", ) self._constants["data_surf"] = data_surf - + elif self._eq_fixed: + data_eq = compute_fun( + self._eq, + self._equil_data_keys, + params=self._eq.params_dict, + transforms=equil_transforms, + profiles=equil_profiles, + ) + self._constants["data_equil"] = data_eq timer.stop("Precomputing transforms") if verbose > 1: timer.disp("Precomputing transforms") @@ -751,14 +777,15 @@ def build(self, use_jit=True, verbose=1): super().build(use_jit=use_jit, verbose=verbose) - def compute(self, equil_params, surface_params=None, constants=None): + def compute(self, params_1, params_2=None, constants=None): """Compute plasma-surface distance. Parameters ---------- - equil_params : dict - Dictionary of equilibrium degrees of freedom, eg Equilibrium.params_dict - surface_params : dict + params_1 : dict + Dictionary of equilibrium degrees of freedom, eg Equilibrium.params_dict, + if eq_fixed is False, else the surface degrees of freedom + params_2 : dict Dictionary of surface degrees of freedom, eg Surface.params_dict Only needed if self._surface_fixed = False constants : dict @@ -773,13 +800,23 @@ def compute(self, equil_params, surface_params=None, constants=None): """ if constants is None: constants = self.constants - data = compute_fun( - "desc.equilibrium.equilibrium.Equilibrium", - self._equil_data_keys, - params=equil_params, - transforms=constants["equil_transforms"], - profiles=constants["equil_profiles"], - ) + if self._eq_fixed: + surface_params = params_1 + elif self._surface_fixed: + equil_params = params_1 + else: + equil_params = params_1 + surface_params = params_2 + if not self._eq_fixed: + data = compute_fun( + "desc.equilibrium.equilibrium.Equilibrium", + self._equil_data_keys, + params=equil_params, + transforms=constants["equil_transforms"], + profiles=constants["equil_profiles"], + ) + else: + data = constants["data_equil"] plasma_coords_rpz = jnp.array([data["R"], data["phi"], data["Z"]]).T plasma_coords = rpz2xyz(plasma_coords_rpz) if self._surface_fixed: diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 3a3c514088..c9c01d0d12 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -998,11 +998,10 @@ def test_signed_PlasmaVesselDistance(): eq = desc.examples.get("HELIOTRON") eq.change_resolution(M=2, N=2) - constraints = (FixParameters(eq),) # don't want eq to change surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=10, N=2, NFP=eq.NFP) + grid = LinearGrid(M=10, N=4, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, eq=eq, @@ -1010,16 +1009,17 @@ def test_signed_PlasmaVesselDistance(): surface_grid=grid, plasma_grid=grid, use_signed_distance=True, + eq_fixed=True, ) objective = ObjectiveFunction((obj,)) optimizer = Optimizer("lsq-exact") - (eq, surf), _ = optimizer.optimize( - (eq, surf), objective, constraints, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 + (surf,), _ = optimizer.optimize( + (surf,), objective, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 ) np.testing.assert_allclose( - obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2, err_msg="Using hardmin" + obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using hardmin" ) # with softmin @@ -1034,14 +1034,14 @@ def test_signed_PlasmaVesselDistance(): use_signed_distance=True, use_softmin=True, softmin_alpha=100, + eq_fixed=True, ) objective = ObjectiveFunction((obj,)) optimizer = Optimizer("lsq-exact") - (eq, surf), _ = optimizer.optimize( - (eq, surf), + (surf,), _ = optimizer.optimize( + (surf,), objective, - constraints, verbose=3, maxiter=60, ftol=1e-8, @@ -1049,7 +1049,7 @@ def test_signed_PlasmaVesselDistance(): ) np.testing.assert_allclose( - obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2, err_msg="Using softmin" + obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using softmin" ) # with changing eq From 08fd594c2f6f9ca27e5a63c766da9ee42c169480 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 16:32:29 -0400 Subject: [PATCH 37/60] change comments to be general --- desc/objectives/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index e0d2833514..23c7fae159 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -342,12 +342,12 @@ def check_if_points_are_inside_perimeter(R, Z, Rcheck, Zcheck): """ # R Z are the perimeter points - # Rcheck Zcheck are the surface points being checked for whether - # or not they are inside the plasma + # Rcheck Zcheck are the points being checked for whether + # or not they are inside the check Rbool = R[:, None] > Rcheck Zbool = Z[:, None] > Zcheck - # these are now size (Nplasma, Nsurf) + # these are now size (Ncheck, Nperimeter) quadrants = jnp.zeros_like(Rbool) quadrants = jnp.where(jnp.logical_and(jnp.logical_not(Rbool), Zbool), 1, quadrants) quadrants = jnp.where( @@ -369,11 +369,11 @@ def check_if_points_are_inside_perimeter(R, Z, Rcheck, Zcheck): deltas, ) pt_sign = jnp.sum(deltas, axis=0) - # positive distance if the plasma pt is inside the surface, else + # positive distance if the check pt is inside the perimeter, else # negative distance is assigned - # pt_sign = 0 : Means SURFACE is OUTSIDE of the PLASMA, + # pt_sign = 0 : Means point is OUTSIDE of the perimeter, # assign positive distance - # pt_sign = +/-4: Means SURFACE is INSIDE PLASMA, so + # pt_sign = +/-4: Means point is INSIDE perimeter, so # assign negative distance pt_sign = jnp.where(jnp.isclose(pt_sign, 0), 1, -1) return pt_sign From 18eae455b8b7ad85cd715b23d41a7e1955e8226f Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 16:38:19 -0400 Subject: [PATCH 38/60] move test --- tests/test_examples.py | 92 +++++++++++++++++++++++++++++++++++++++++ tests/test_optimizer.py | 92 ----------------------------------------- 2 files changed, 92 insertions(+), 92 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index a029d81dbf..47f5c3eb05 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1527,3 +1527,95 @@ def circle_constraint(params): abs(surf_opt.Z_lmn[surf_opt.Z_basis.get_idx(M=-1, N=0)]) + offset, rtol=2e-2, ) + + +@pytest.mark.slow +@pytest.mark.regression +@pytest.mark.optimize +def test_signed_PlasmaVesselDistance(): + """Tests that signed distance works with surface optimization.""" + eq = get("HELIOTRON") + eq.change_resolution(M=2, N=2) + + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) + + grid = LinearGrid(M=10, N=4, NFP=eq.NFP) + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=-0.25, + surface_grid=grid, + plasma_grid=grid, + use_signed_distance=True, + eq_fixed=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (surf,), _ = optimizer.optimize( + (surf,), objective, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 + ) + + np.testing.assert_allclose( + obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using hardmin" + ) + + # with softmin + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=-0.25, + surface_grid=grid, + plasma_grid=grid, + use_signed_distance=True, + use_softmin=True, + softmin_alpha=100, + eq_fixed=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (surf,), _ = optimizer.optimize( + (surf,), + objective, + verbose=3, + maxiter=60, + ftol=1e-8, + xtol=1e-9, + ) + + np.testing.assert_allclose( + obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using softmin" + ) + + # with changing eq + eq = Equilibrium(M=1, N=1) + surf = eq.surface.copy() + surf.change_resolution(M=1, N=1) + grid = LinearGrid(M=10, N=2, NFP=eq.NFP) + + obj = PlasmaVesselDistance( + surface=surf, + eq=eq, + target=-0.25, + surface_grid=grid, + plasma_grid=grid, + use_signed_distance=True, + ) + objective = ObjectiveFunction((obj,)) + + optimizer = Optimizer("lsq-exact") + (eq, surf), _ = optimizer.optimize( + (eq, surf), + objective, + constraints=(FixParameters(surf),), + verbose=3, + maxiter=60, + ftol=1e-8, + xtol=1e-9, + ) + + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index c9c01d0d12..7c5f810d98 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -990,98 +990,6 @@ def test_constrained_AL_scalar(): np.testing.assert_array_less(-Dwell, ctol) -@pytest.mark.slow -@pytest.mark.regression -@pytest.mark.optimize -def test_signed_PlasmaVesselDistance(): - """Tests that signed distance works with surface optimization.""" - eq = desc.examples.get("HELIOTRON") - eq.change_resolution(M=2, N=2) - - surf = eq.surface.copy() - surf.change_resolution(M=1, N=1) - - grid = LinearGrid(M=10, N=4, NFP=eq.NFP) - obj = PlasmaVesselDistance( - surface=surf, - eq=eq, - target=-0.25, - surface_grid=grid, - plasma_grid=grid, - use_signed_distance=True, - eq_fixed=True, - ) - objective = ObjectiveFunction((obj,)) - - optimizer = Optimizer("lsq-exact") - (surf,), _ = optimizer.optimize( - (surf,), objective, verbose=3, maxiter=60, ftol=1e-8, xtol=1e-9 - ) - - np.testing.assert_allclose( - obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using hardmin" - ) - - # with softmin - surf = eq.surface.copy() - surf.change_resolution(M=1, N=1) - obj = PlasmaVesselDistance( - surface=surf, - eq=eq, - target=-0.25, - surface_grid=grid, - plasma_grid=grid, - use_signed_distance=True, - use_softmin=True, - softmin_alpha=100, - eq_fixed=True, - ) - objective = ObjectiveFunction((obj,)) - - optimizer = Optimizer("lsq-exact") - (surf,), _ = optimizer.optimize( - (surf,), - objective, - verbose=3, - maxiter=60, - ftol=1e-8, - xtol=1e-9, - ) - - np.testing.assert_allclose( - obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using softmin" - ) - - # with changing eq - eq = Equilibrium(M=1, N=1) - surf = eq.surface.copy() - surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=10, N=2, NFP=eq.NFP) - - obj = PlasmaVesselDistance( - surface=surf, - eq=eq, - target=-0.25, - surface_grid=grid, - plasma_grid=grid, - use_signed_distance=True, - ) - objective = ObjectiveFunction((obj,)) - - optimizer = Optimizer("lsq-exact") - (eq, surf), _ = optimizer.optimize( - (eq, surf), - objective, - constraints=(FixParameters(surf),), - verbose=3, - maxiter=60, - ftol=1e-8, - xtol=1e-9, - ) - - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) - - @pytest.mark.unit @pytest.mark.optimize def test_optimize_multiple_things_different_order(): From 34b778c4bcd361ccfbb71461acb113d3ab6ce696 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Sat, 6 Jul 2024 16:42:30 -0400 Subject: [PATCH 39/60] relabel target in test --- tests/test_examples.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 47f5c3eb05..a3921dda6c 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1540,11 +1540,13 @@ def test_signed_PlasmaVesselDistance(): surf = eq.surface.copy() surf.change_resolution(M=1, N=1) + target_dist = -0.25 + grid = LinearGrid(M=10, N=4, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, eq=eq, - target=-0.25, + target=target_dist, surface_grid=grid, plasma_grid=grid, use_signed_distance=True, @@ -1558,7 +1560,7 @@ def test_signed_PlasmaVesselDistance(): ) np.testing.assert_allclose( - obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using hardmin" + obj.compute(*obj.xs(surf)), target_dist, atol=1e-2, err_msg="Using hardmin" ) # with softmin @@ -1567,7 +1569,7 @@ def test_signed_PlasmaVesselDistance(): obj = PlasmaVesselDistance( surface=surf, eq=eq, - target=-0.25, + target=target_dist, surface_grid=grid, plasma_grid=grid, use_signed_distance=True, @@ -1588,7 +1590,7 @@ def test_signed_PlasmaVesselDistance(): ) np.testing.assert_allclose( - obj.compute(*obj.xs(surf)), -0.25, atol=1e-2, err_msg="Using softmin" + obj.compute(*obj.xs(surf)), target_dist, atol=1e-2, err_msg="Using softmin" ) # with changing eq @@ -1600,7 +1602,7 @@ def test_signed_PlasmaVesselDistance(): obj = PlasmaVesselDistance( surface=surf, eq=eq, - target=-0.25, + target=target_dist, surface_grid=grid, plasma_grid=grid, use_signed_distance=True, @@ -1618,4 +1620,4 @@ def test_signed_PlasmaVesselDistance(): xtol=1e-9, ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), -0.25, atol=1e-2) + np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), target_dist, atol=1e-2) From 49b19e1c0b5d2e40b75201061d8a6d9710b56b8b Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 8 Jul 2024 09:50:23 -0400 Subject: [PATCH 40/60] fix typo in docstring, remove todo --- desc/objectives/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 23c7fae159..e7349bb275 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -309,16 +309,21 @@ def _parse_callable_target_bounds(target, bounds, x): return target, bounds -# TODO: dont need to be R,Z, can by x1, x2 or something def check_if_points_are_inside_perimeter(R, Z, Rcheck, Zcheck): """Function to check if the given points is inside the given polyognal perimeter. Rcheck, Zcheck are the points to check, and R, Z define the perimeter in which to check. This function assumes that all points are in the same - plane. Function will return an array pf signs (+/- 1), with positive sign meaning + plane. Function will return an array of signs (+/- 1), with positive sign meaning the point is inside of the given perimeter, and a negative sign meaning the point is outside of the given perimeter. + NOTE: it does not matter if the input coordinates are cylindrical (R,Z) or + cartesian (X,Y), these are equivalent as long as they are in the same phi plane. + This function will work even if points are not in the same phi plane, but the + input coordinates must then be the equivalent of cartesian coordinates for whatever + plane the points lie in. + Algorithm based off of "An Incremental Angle Point in Polygon Test", K. Weiler, https://doi.org/10.1016/B978-0-12-336156-1.50012-4 From 27833462dc94fc7b0b4a79490e5615e87b449041 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Mon, 22 Jul 2024 11:45:33 -0400 Subject: [PATCH 41/60] cleanup changelog --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d286b9c5d..d96de88b4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,6 @@ Changelog New Features -- Add method ``from_values`` to ``FourierRZCurve`` to allow fitting of data points -to a ``FourierRZCurve`` object, and ``to_FourierRZCurve`` methods to ``Curve`` class. -- Add the objective `CoilsetMinDistance`, which returns the minimum distance to another -coil for each coil in a coilset. -- Add the objective `PlasmaCoilsetMinDistance`, which returns the minimum distance to the -plasma surface for each coil in a coilset. - Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance. - Add utility function ``check_if_points_are_inside_perimeter`` to ``desc.objectives.utils`` which will check whether a given set of points is inside of a given perimeter. From 86155fd3beb60141ed8daad150396f6a2898c42a Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 23 Jul 2024 09:30:05 -0400 Subject: [PATCH 42/60] remove unneeded line --- desc/objectives/_geometry.py | 1 - desc/objectives/utils.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/desc/objectives/_geometry.py b/desc/objectives/_geometry.py index 48927de63c..f72e9f7605 100644 --- a/desc/objectives/_geometry.py +++ b/desc/objectives/_geometry.py @@ -855,7 +855,6 @@ def fun(plasma_pts_at_zeta_plane, surface_pts_at_zeta_plane): (plasma_pts_at_zeta_plane, plasma_pts_at_zeta_plane[0, :]) ) - surface_pts_at_zeta_plane pt_sign = check_if_points_are_inside_perimeter( plasma_pts_at_zeta_plane[:, 0], plasma_pts_at_zeta_plane[:, 2], diff --git a/desc/objectives/utils.py b/desc/objectives/utils.py index 6a5d4b74bc..27a8d30b37 100644 --- a/desc/objectives/utils.py +++ b/desc/objectives/utils.py @@ -320,7 +320,7 @@ def check_if_points_are_inside_perimeter(R, Z, Rcheck, Zcheck): R,Z : ndarray 1-D arrays of coordinates of the points defining the polygonal perimeter. The function will determine if the check point is inside - or outside of this perimeter. + or outside of this perimeter. These should form a closed curve. Rcheck, Zcheck : ndarray coordinates of the points being checked if they are inside or outside of the given perimeter. From f75e4a2b3e6b39c0ccde11e8028c5484b8cbe478 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 23 Jul 2024 15:37:00 -0400 Subject: [PATCH 43/60] remove ds from compute index in favor of using grad spacing directly --- desc/compute/_curve.py | 34 ++++++++-------------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/desc/compute/_curve.py b/desc/compute/_curve.py index 818da18092..41cd2e44e3 100644 --- a/desc/compute/_curve.py +++ b/desc/compute/_curve.py @@ -26,25 +26,6 @@ def _s(params, transforms, profiles, data, **kwargs): return data -@register_compute_fun( - name="ds", - label="ds", - units="~", - units_long="None", - description="Spacing of curve parameter", - dim=1, - params=[], - transforms={"grid": []}, - profiles=[], - coordinates="s", - data=[], - parameterization="desc.geometry.core.Curve", -) -def _ds(params, transforms, profiles, data, **kwargs): - data["ds"] = transforms["grid"].spacing[:, 2] - return data - - @register_compute_fun( name="X", label="X", @@ -980,17 +961,17 @@ def _torsion(params, transforms, profiles, data, **kwargs): description="Length of the curve", dim=0, params=[], - transforms={}, + transforms={"grid": []}, profiles=[], coordinates="", - data=["ds", "x_s"], + data=["x_s"], parameterization=["desc.geometry.core.Curve"], ) def _length(params, transforms, profiles, data, **kwargs): T = jnp.linalg.norm(data["x_s"], axis=-1) # this is equivalent to jnp.trapz(T, s) for a closed curve, # but also works if grid.endpoint is False - data["length"] = jnp.sum(T * data["ds"]) + data["length"] = jnp.sum(T * transforms["grid"].spacing[:, 2]) return data @@ -1002,10 +983,10 @@ def _length(params, transforms, profiles, data, **kwargs): description="Length of the curve", dim=0, params=[], - transforms={}, + transforms={"grid": []}, profiles=[], coordinates="", - data=["ds", "x", "x_s"], + data=["x", "x_s"], parameterization="desc.geometry.curve.SplineXYZCurve", method="Interpolation type, Default 'cubic'. See SplineXYZCurve docs for options.", ) @@ -1015,7 +996,8 @@ def _length_SplineXYZCurve(params, transforms, profiles, data, **kwargs): if kwargs.get("basis", "rpz").lower() == "rpz": coords = rpz2xyz(coords) # ensure curve is closed - # if it's already closed this doesn't add any length since ds will be zero + # if it's already closed this doesn't add any length since + # grid spacing will be zero at the duplicate point coords = jnp.concatenate([coords, coords[:1]]) X = coords[:, 0] Y = coords[:, 1] @@ -1026,5 +1008,5 @@ def _length_SplineXYZCurve(params, transforms, profiles, data, **kwargs): T = jnp.linalg.norm(data["x_s"], axis=-1) # this is equivalent to jnp.trapz(T, s) for a closed curve # but also works if grid.endpoint is False - data["length"] = jnp.sum(T * data["ds"]) + data["length"] = jnp.sum(T * transforms["grid"].spacing[:, 2]) return data From a0b7fa70eb00f501c51ae45ca160260e85da759b Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 23 Jul 2024 20:17:02 -0400 Subject: [PATCH 44/60] update changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96de88b4d..3a39eedf98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,6 @@ Changelog New Features - Add ``use_signed_distance`` flag to ``PlasmaVesselDistance`` which will use a signed distance as the target, which is positive when the plasma is inside of the vessel surface and negative if the plasma is outside of the vessel surface, to allow optimizer to distinguish if the equilbrium surface exits the vessel surface and guard against it by targeting a positive signed distance. -- Add utility function ``check_if_points_are_inside_perimeter`` to ``desc.objectives.utils`` which will check whether a given set -of points is inside of a given perimeter. v0.12.0 From 1da67200198a692c080b5bd97e5ad60190b630c7 Mon Sep 17 00:00:00 2001 From: Rory Conlin Date: Wed, 24 Jul 2024 13:56:25 -0400 Subject: [PATCH 45/60] Add utility for determine tiers in data_index --- desc/compute/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/desc/compute/__init__.py b/desc/compute/__init__.py index 416825cc55..87f03068bb 100644 --- a/desc/compute/__init__.py +++ b/desc/compute/__init__.py @@ -94,3 +94,30 @@ def _build_data_index(): _build_data_index() + + +def set_tier(name, p): + """Determine how deep in the dependency tree a given name is. + + tier of 0 means no dependencies on other data, + tier of 1 means it depends on only tier 0 stuff, + tier of 2 means it depends on tier 0 and tier 1, etc etc. + + Designed such that if you compute things in the order determined by tiers, + all dependencies will always be computed in the correct order. + """ + if "tier" in data_index[p][name]: + return + if len(data_index[p][name]["full_with_axis_dependencies"]["data"]) == 0: + data_index[p][name]["tier"] = 0 + else: + thistier = 0 + for name1 in data_index[p][name]["full_with_axis_dependencies"]["data"]: + set_tier(name1, p) + thistier = max(thistier, data_index[p][name1]["tier"]) + data_index[p][name]["tier"] = thistier + 1 + + +for par in data_index.keys(): + for name in data_index[par]: + set_tier(name, par) From a43a5bf52a0d90c22c35f266af9f9007dca81ffa Mon Sep 17 00:00:00 2001 From: unalmis Date: Sun, 11 Aug 2024 21:27:34 -0400 Subject: [PATCH 46/60] Resolve Github issue #1174, bug in surface computations --- desc/compute/_basis_vectors.py | 1 - desc/compute/_core.py | 545 ++++------------------- desc/compute/_field.py | 2 +- desc/compute/_profiles.py | 4 +- desc/compute/_surface.py | 60 --- desc/compute/data_index.py | 2 + tests/inputs/master_compute_data_rpz.pkl | Bin 8075396 -> 9315427 bytes 7 files changed, 92 insertions(+), 522 deletions(-) diff --git a/desc/compute/_basis_vectors.py b/desc/compute/_basis_vectors.py index 70bc1fa6e5..8fca1346d2 100644 --- a/desc/compute/_basis_vectors.py +++ b/desc/compute/_basis_vectors.py @@ -1431,7 +1431,6 @@ def _e_sub_rho(params, transforms, profiles, data, **kwargs): # At the magnetic axis, this function returns the multivalued map whose # image is the set { 𝐞ᵨ | ρ=0 }. data["e_rho"] = jnp.array([data["R_r"], data["R"] * data["omega_r"], data["Z_r"]]).T - return data diff --git a/desc/compute/_core.py b/desc/compute/_core.py index e0e8312b9b..6e5a137ed3 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -22,12 +22,37 @@ "desc.geometry.core.Surface", "desc.geometry.core.Curve", ], + aliases=["rho_t", "rho_z", "theta_r", "theta_z", "zeta_r", "zeta_t"], ) def _0(params, transforms, profiles, data, **kwargs): data["0"] = jnp.zeros(transforms["grid"].num_nodes) return data +@register_compute_fun( + name="1", + label="1", + units="~", + units_long="None", + description="Ones", + dim=1, + params=[], + transforms={"grid": []}, + profiles=[], + coordinates="rtz", + data=[], + parameterization=[ + "desc.equilibrium.equilibrium.Equilibrium", + "desc.geometry.core.Surface", + "desc.geometry.core.Curve", + ], + aliases=["rho_r", "theta_t", "zeta_z"], +) +def _1(params, transforms, profiles, data, **kwargs): + data["1"] = jnp.ones(transforms["grid"].num_nodes) + return data + + @register_compute_fun( name="R", label="R", @@ -1624,6 +1649,7 @@ def _lambda(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_r", ) def _lambda_r(params, transforms, profiles, data, **kwargs): data["lambda_r"] = transforms["L"].transform(params["L_lmn"], 1, 0, 0) @@ -1642,6 +1668,7 @@ def _lambda_r(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rr", ) def _lambda_rr(params, transforms, profiles, data, **kwargs): data["lambda_rr"] = transforms["L"].transform(params["L_lmn"], 2, 0, 0) @@ -1660,6 +1687,7 @@ def _lambda_rr(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rrr", ) def _lambda_rrr(params, transforms, profiles, data, **kwargs): data["lambda_rrr"] = transforms["L"].transform(params["L_lmn"], 3, 0, 0) @@ -1679,6 +1707,7 @@ def _lambda_rrr(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rrrt", ) def _lambda_rrrt(params, transforms, profiles, data, **kwargs): data["lambda_rrrt"] = transforms["L"].transform(params["L_lmn"], 3, 1, 0) @@ -1698,6 +1727,7 @@ def _lambda_rrrt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rrrz", ) def _lambda_rrrz(params, transforms, profiles, data, **kwargs): data["lambda_rrrz"] = transforms["L"].transform(params["L_lmn"], 3, 0, 1) @@ -1717,6 +1747,7 @@ def _lambda_rrrz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rrt", ) def _lambda_rrt(params, transforms, profiles, data, **kwargs): data["lambda_rrt"] = transforms["L"].transform(params["L_lmn"], 2, 1, 0) @@ -1736,6 +1767,7 @@ def _lambda_rrt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rrz", ) def _lambda_rrz(params, transforms, profiles, data, **kwargs): data["lambda_rrz"] = transforms["L"].transform(params["L_lmn"], 2, 0, 1) @@ -1755,6 +1787,7 @@ def _lambda_rrz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rt", ) def _lambda_rt(params, transforms, profiles, data, **kwargs): data["lambda_rt"] = transforms["L"].transform(params["L_lmn"], 1, 1, 0) @@ -1774,6 +1807,7 @@ def _lambda_rt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rtt", ) def _lambda_rtt(params, transforms, profiles, data, **kwargs): data["lambda_rtt"] = transforms["L"].transform(params["L_lmn"], 1, 2, 0) @@ -1793,6 +1827,7 @@ def _lambda_rtt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rtz", ) def _lambda_rtz(params, transforms, profiles, data, **kwargs): data["lambda_rtz"] = transforms["L"].transform(params["L_lmn"], 1, 1, 1) @@ -1812,6 +1847,7 @@ def _lambda_rtz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rz", ) def _lambda_rz(params, transforms, profiles, data, **kwargs): data["lambda_rz"] = transforms["L"].transform(params["L_lmn"], 1, 0, 1) @@ -1831,6 +1867,7 @@ def _lambda_rz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_rzz", ) def _lambda_rzz(params, transforms, profiles, data, **kwargs): data["lambda_rzz"] = transforms["L"].transform(params["L_lmn"], 1, 0, 2) @@ -1867,6 +1904,7 @@ def _lambda_t(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_tt", ) def _lambda_tt(params, transforms, profiles, data, **kwargs): data["lambda_tt"] = transforms["L"].transform(params["L_lmn"], 0, 2, 0) @@ -1885,6 +1923,7 @@ def _lambda_tt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_ttt", ) def _lambda_ttt(params, transforms, profiles, data, **kwargs): data["lambda_ttt"] = transforms["L"].transform(params["L_lmn"], 0, 3, 0) @@ -1904,6 +1943,7 @@ def _lambda_ttt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_ttz", ) def _lambda_ttz(params, transforms, profiles, data, **kwargs): data["lambda_ttz"] = transforms["L"].transform(params["L_lmn"], 0, 2, 1) @@ -1923,6 +1963,7 @@ def _lambda_ttz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_tz", ) def _lambda_tz(params, transforms, profiles, data, **kwargs): data["lambda_tz"] = transforms["L"].transform(params["L_lmn"], 0, 1, 1) @@ -1942,6 +1983,7 @@ def _lambda_tz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_tzz", ) def _lambda_tzz(params, transforms, profiles, data, **kwargs): data["lambda_tzz"] = transforms["L"].transform(params["L_lmn"], 0, 1, 2) @@ -1960,6 +2002,7 @@ def _lambda_tzz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_z", ) def _lambda_z(params, transforms, profiles, data, **kwargs): data["lambda_z"] = transforms["L"].transform(params["L_lmn"], 0, 0, 1) @@ -1978,6 +2021,7 @@ def _lambda_z(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_zz", ) def _lambda_zz(params, transforms, profiles, data, **kwargs): data["lambda_zz"] = transforms["L"].transform(params["L_lmn"], 0, 0, 2) @@ -1996,6 +2040,7 @@ def _lambda_zz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], + aliases="theta_PEST_zzz", ) def _lambda_zzz(params, transforms, profiles, data, **kwargs): data["lambda_zzz"] = transforms["L"].transform(params["L_lmn"], 0, 0, 3) @@ -2040,6 +2085,7 @@ def _omega(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_r", ) def _omega_r(params, transforms, profiles, data, **kwargs): data["omega_r"] = data["0"] @@ -2062,6 +2108,7 @@ def _omega_r(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rr", ) def _omega_rr(params, transforms, profiles, data, **kwargs): data["omega_rr"] = data["0"] @@ -2084,6 +2131,7 @@ def _omega_rr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrr", ) def _omega_rrr(params, transforms, profiles, data, **kwargs): data["omega_rrr"] = data["0"] @@ -2106,6 +2154,7 @@ def _omega_rrr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrrr", ) def _omega_rrrr(params, transforms, profiles, data, **kwargs): data["omega_rrrr"] = data["0"] @@ -2129,6 +2178,7 @@ def _omega_rrrr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrrt", ) def _omega_rrrt(params, transforms, profiles, data, **kwargs): data["omega_rrrt"] = data["0"] @@ -2152,6 +2202,7 @@ def _omega_rrrt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrrz", ) def _omega_rrrz(params, transforms, profiles, data, **kwargs): data["omega_rrrz"] = data["0"] @@ -2175,6 +2226,7 @@ def _omega_rrrz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrt", ) def _omega_rrt(params, transforms, profiles, data, **kwargs): data["omega_rrt"] = data["0"] @@ -2198,6 +2250,7 @@ def _omega_rrt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrtt", ) def _omega_rrtt(params, transforms, profiles, data, **kwargs): data["omega_rrtt"] = data["0"] @@ -2221,6 +2274,7 @@ def _omega_rrtt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrtz", ) def _omega_rrtz(params, transforms, profiles, data, **kwargs): data["omega_rrtz"] = data["0"] @@ -2244,6 +2298,7 @@ def _omega_rrtz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrz", ) def _omega_rrz(params, transforms, profiles, data, **kwargs): data["omega_rrz"] = data["0"] @@ -2267,6 +2322,7 @@ def _omega_rrz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rrzz", ) def _omega_rrzz(params, transforms, profiles, data, **kwargs): data["omega_rrzz"] = data["0"] @@ -2290,6 +2346,7 @@ def _omega_rrzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rt", ) def _omega_rt(params, transforms, profiles, data, **kwargs): data["omega_rt"] = data["0"] @@ -2313,6 +2370,7 @@ def _omega_rt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rtt", ) def _omega_rtt(params, transforms, profiles, data, **kwargs): data["omega_rtt"] = data["0"] @@ -2336,6 +2394,7 @@ def _omega_rtt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rttt", ) def _omega_rttt(params, transforms, profiles, data, **kwargs): data["omega_rttt"] = data["0"] @@ -2359,6 +2418,7 @@ def _omega_rttt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rttz", ) def _omega_rttz(params, transforms, profiles, data, **kwargs): data["omega_rttz"] = data["0"] @@ -2382,6 +2442,7 @@ def _omega_rttz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rtz", ) def _omega_rtz(params, transforms, profiles, data, **kwargs): data["omega_rtz"] = data["0"] @@ -2405,6 +2466,7 @@ def _omega_rtz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rtzz", ) def _omega_rtzz(params, transforms, profiles, data, **kwargs): data["omega_rtzz"] = data["0"] @@ -2428,6 +2490,7 @@ def _omega_rtzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rz", ) def _omega_rz(params, transforms, profiles, data, **kwargs): data["omega_rz"] = data["0"] @@ -2451,6 +2514,7 @@ def _omega_rz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rzz", ) def _omega_rzz(params, transforms, profiles, data, **kwargs): data["omega_rzz"] = data["0"] @@ -2474,6 +2538,7 @@ def _omega_rzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_rzzz", ) def _omega_rzzz(params, transforms, profiles, data, **kwargs): data["omega_rzzz"] = data["0"] @@ -2496,6 +2561,7 @@ def _omega_rzzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_t", ) def _omega_t(params, transforms, profiles, data, **kwargs): data["omega_t"] = data["0"] @@ -2518,6 +2584,7 @@ def _omega_t(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_tt", ) def _omega_tt(params, transforms, profiles, data, **kwargs): data["omega_tt"] = data["0"] @@ -2540,6 +2607,7 @@ def _omega_tt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_ttt", ) def _omega_ttt(params, transforms, profiles, data, **kwargs): data["omega_ttt"] = data["0"] @@ -2563,6 +2631,7 @@ def _omega_ttt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_ttz", ) def _omega_ttz(params, transforms, profiles, data, **kwargs): data["omega_ttz"] = data["0"] @@ -2586,6 +2655,7 @@ def _omega_ttz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_tz", ) def _omega_tz(params, transforms, profiles, data, **kwargs): data["omega_tz"] = data["0"] @@ -2609,6 +2679,7 @@ def _omega_tz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_tzz", ) def _omega_tzz(params, transforms, profiles, data, **kwargs): data["omega_tzz"] = data["0"] @@ -2653,6 +2724,7 @@ def _omega_z(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_zz", ) def _omega_zz(params, transforms, profiles, data, **kwargs): data["omega_zz"] = data["0"] @@ -2675,197 +2747,13 @@ def _omega_zz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], + aliases="phi_zzz", ) def _omega_zzz(params, transforms, profiles, data, **kwargs): data["omega_zzz"] = data["0"] return data -@register_compute_fun( - name="phi", - label="\\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["zeta", "omega"], -) -def _phi(params, transforms, profiles, data, **kwargs): - data["phi"] = data["zeta"] + data["omega"] - return data - - -@register_compute_fun( - name="phi_r", - label="\\partial_{\\rho} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_r"], -) -def _phi_r(params, transforms, profiles, data, **kwargs): - data["phi_r"] = data["omega_r"] - return data - - -@register_compute_fun( - name="phi_rr", - label="\\partial_{\\rho \\rho} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_rr"], -) -def _phi_rr(params, transforms, profiles, data, **kwargs): - data["phi_rr"] = data["omega_rr"] - return data - - -@register_compute_fun( - name="phi_rt", - label="\\partial_{\\rho \\theta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt radial and " - "poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_rt"], -) -def _phi_rt(params, transforms, profiles, data, **kwargs): - data["phi_rt"] = data["omega_rt"] - return data - - -@register_compute_fun( - name="phi_rz", - label="\\partial_{\\rho \\zeta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt radial and " - "toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_rz"], -) -def _phi_rz(params, transforms, profiles, data, **kwargs): - data["phi_rz"] = data["omega_rz"] - return data - - -@register_compute_fun( - name="phi_t", - label="\\partial_{\\theta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_t"], -) -def _phi_t(params, transforms, profiles, data, **kwargs): - data["phi_t"] = data["omega_t"] - return data - - -@register_compute_fun( - name="phi_tt", - label="\\partial_{\\theta \\theta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt poloidal " - "coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_tt"], -) -def _phi_tt(params, transforms, profiles, data, **kwargs): - data["phi_tt"] = data["omega_tt"] - return data - - -@register_compute_fun( - name="phi_tz", - label="\\partial_{\\theta \\zeta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt poloidal and " - "toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_tz"], -) -def _phi_tz(params, transforms, profiles, data, **kwargs): - data["phi_tz"] = data["omega_tz"] - return data - - -@register_compute_fun( - name="phi_z", - label="\\partial_{\\zeta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_z"], -) -def _phi_z(params, transforms, profiles, data, **kwargs): - data["phi_z"] = 1 + data["omega_z"] - return data - - -@register_compute_fun( - name="phi_zz", - label="\\partial_{\\zeta \\zeta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, second derivative wrt toroidal " - "coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["omega_zz"], -) -def _phi_zz(params, transforms, profiles, data, **kwargs): - data["phi_zz"] = data["omega_zz"] - return data - - @register_compute_fun( name="rho", label="\\rho", @@ -2890,75 +2778,6 @@ def _rho(params, transforms, profiles, data, **kwargs): return data -@register_compute_fun( - name="rho_r", - label="\\partial_{\\rho} \\rho", - units="~", - units_long="None", - description="Radial coordinate, proportional to the square root " - + "of the toroidal flux, derivative wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="r", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _rho_r(params, transforms, profiles, data, **kwargs): - data["rho_r"] = jnp.ones_like(data["0"]) - return data - - -@register_compute_fun( - name="rho_t", - label="\\partial_{\\theta} \\rho", - units="~", - units_long="None", - description="Radial coordinate, proportional to the square root " - "of the toroidal flux, derivative wrt poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="r", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _rho_t(params, transforms, profiles, data, **kwargs): - data["rho_t"] = data["0"] - return data - - -@register_compute_fun( - name="rho_z", - label="\\partial_{\\zeta} \\rho", - units="~", - units_long="None", - description="Radial coordinate, proportional to the square root " - "of the toroidal flux, derivative wrt toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="r", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _rho_z(params, transforms, profiles, data, **kwargs): - data["rho_z"] = data["0"] - return data - - @register_compute_fun( name="theta", label="\\theta", @@ -2999,25 +2818,6 @@ def _theta_PEST(params, transforms, profiles, data, **kwargs): return data -@register_compute_fun( - name="theta_PEST_r", - label="\\partial_{\\rho} \\vartheta", - units="rad", - units_long="radians", - description="PEST straight field line poloidal angular coordinate, derivative wrt " - "radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["lambda_r"], -) -def _theta_PEST_r(params, transforms, profiles, data, **kwargs): - data["theta_PEST_r"] = data["lambda_r"] - return data - - @register_compute_fun( name="theta_PEST_t", label="\\partial_{\\theta} \\vartheta", @@ -3037,151 +2837,6 @@ def _theta_PEST_t(params, transforms, profiles, data, **kwargs): return data -@register_compute_fun( - name="theta_PEST_tt", - label="\\partial_{\\theta \\theta} \\vartheta", - units="rad", - units_long="radians", - description="PEST straight field line poloidal angular coordinate, second " - "derivative wrt poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["lambda_tt"], -) -def _theta_PEST_tt(params, transforms, profiles, data, **kwargs): - data["theta_PEST_tt"] = data["lambda_tt"] - return data - - -@register_compute_fun( - name="theta_PEST_tz", - label="\\partial_{\\theta \\zeta} \\vartheta", - units="rad", - units_long="radians", - description="PEST straight field line poloidal angular coordinate, derivative wrt " - "poloidal and toroidal coordinates", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["lambda_tz"], -) -def _theta_PEST_tz(params, transforms, profiles, data, **kwargs): - data["theta_PEST_tz"] = data["lambda_tz"] - return data - - -@register_compute_fun( - name="theta_PEST_z", - label="\\partial_{\\zeta} \\vartheta", - units="rad", - units_long="radians", - description="PEST straight field line poloidal angular coordinate, derivative wrt " - "toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["lambda_z"], -) -def _theta_PEST_z(params, transforms, profiles, data, **kwargs): - data["theta_PEST_z"] = data["lambda_z"] - return data - - -@register_compute_fun( - name="theta_PEST_zz", - label="\\partial_{\\zeta \\zeta} \\vartheta", - units="rad", - units_long="radians", - description="PEST straight field line poloidal angular coordinate, second " - "derivative wrt toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["lambda_zz"], -) -def _theta_PEST_zz(params, transforms, profiles, data, **kwargs): - data["theta_PEST_zz"] = data["lambda_zz"] - return data - - -@register_compute_fun( - name="theta_r", - label="\\partial_{\\rho} \\theta", - units="rad", - units_long="radians", - description="Poloidal angular coordinate (geometric, not magnetic), " - "derivative wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="t", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _theta_r(params, transforms, profiles, data, **kwargs): - data["theta_r"] = data["0"] - return data - - -@register_compute_fun( - name="theta_t", - label="\\partial_{\\theta} \\theta", - units="rad", - units_long="radians", - description="Poloidal angular coordinate (geometric, not magnetic), " - "derivative wrt poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="t", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _theta_t(params, transforms, profiles, data, **kwargs): - data["theta_t"] = jnp.ones_like(data["0"]) - return data - - -@register_compute_fun( - name="theta_z", - label="\\partial_{\\zeta} \\theta", - units="rad", - units_long="radians", - description="Poloidal angular coordinate (geometric, not magnetic), " - "derivative wrt toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="t", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _theta_z(params, transforms, profiles, data, **kwargs): - data["theta_z"] = data["0"] - return data - - @register_compute_fun( name="zeta", label="\\zeta", @@ -3205,66 +2860,40 @@ def _zeta(params, transforms, profiles, data, **kwargs): @register_compute_fun( - name="zeta_r", - label="\\partial_{\\rho} \\zeta", - units="rad", - units_long="radians", - description="Toroidal angular coordinate derivative, wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="z", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], -) -def _zeta_r(params, transforms, profiles, data, **kwargs): - data["zeta_r"] = data["0"] - return data - - -@register_compute_fun( - name="zeta_t", - label="\\partial_{\\theta} \\zeta", + name="phi", + label="\\phi", units="rad", units_long="radians", - description="Toroidal angular coordinate, derivative wrt poloidal coordinate", + description="Toroidal angle in lab frame", dim=1, params=[], transforms={}, profiles=[], - coordinates="z", - data=["0"], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], + coordinates="rtz", + data=["zeta", "omega"], ) -def _zeta_t(params, transforms, profiles, data, **kwargs): - data["zeta_t"] = data["0"] +def _phi(params, transforms, profiles, data, **kwargs): + data["phi"] = (data["zeta"] + data["omega"]) % (2 * jnp.pi) return data @register_compute_fun( - name="zeta_z", - label="\\partial_{\\zeta} \\zeta", + name="phi_z", + label="\\partial_{\\zeta} \\phi", units="rad", units_long="radians", - description="Toroidal angular coordinate, derivative wrt toroidal coordinate", + description="Toroidal angle in lab frame, derivative wrt toroidal coordinate", dim=1, params=[], transforms={}, profiles=[], - coordinates="z", - data=["0"], + coordinates="rtz", + data=["omega_z"], parameterization=[ "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], ) -def _zeta_z(params, transforms, profiles, data, **kwargs): - data["zeta_z"] = jnp.ones_like(data["0"]) +def _phi_z(params, transforms, profiles, data, **kwargs): + data["phi_z"] = 1 + data["omega_z"] return data diff --git a/desc/compute/_field.py b/desc/compute/_field.py index eef1354e3d..e53f033464 100644 --- a/desc/compute/_field.py +++ b/desc/compute/_field.py @@ -2935,7 +2935,7 @@ def _gradB2(params, transforms, profiles, data, **kwargs): @register_compute_fun( name="|grad(|B|^2)|/2mu0", - label="|\\nabla |B|^{2}/(2\\mu_0)|", + label="|\\nabla |B|^{2}|/(2\\mu_0)", units="N \\cdot m^{-3}", units_long="Newton / cubic meter", description="Magnitude of magnetic pressure gradient", diff --git a/desc/compute/_profiles.py b/desc/compute/_profiles.py index ccb701f072..1cfbc2f23f 100644 --- a/desc/compute/_profiles.py +++ b/desc/compute/_profiles.py @@ -82,10 +82,10 @@ def _psi_r(params, transforms, profiles, data, **kwargs): transforms={}, profiles=[], coordinates="r", - data=["rho"], + data=["1"], ) def _psi_rr(params, transforms, profiles, data, **kwargs): - data["psi_rr"] = params["Psi"] * jnp.ones_like(data["rho"]) / jnp.pi + data["psi_rr"] = data["1"] * params["Psi"] / jnp.pi return data diff --git a/desc/compute/_surface.py b/desc/compute/_surface.py index 5fb84fa060..d37f82337e 100644 --- a/desc/compute/_surface.py +++ b/desc/compute/_surface.py @@ -118,66 +118,6 @@ def _phi_Surface(params, transforms, profiles, data, **kwargs): return data -@register_compute_fun( - name="phi_r", - label="\\partial_{\\rho} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt radial coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["e_rho"], - parameterization="desc.geometry.core.Surface", -) -def _phi_r_Surface(params, transforms, profiles, data, **kwargs): - coords_r = data["e_rho"] - data["phi_r"] = coords_r[:, 1] - return data - - -@register_compute_fun( - name="phi_t", - label="\\partial_{\\theta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt poloidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["e_theta"], - parameterization="desc.geometry.core.Surface", -) -def _phi_t_Surface(params, transforms, profiles, data, **kwargs): - coords_t = data["e_theta"] - data["phi_t"] = coords_t[:, 1] - return data - - -@register_compute_fun( - name="phi_z", - label="\\partial_{\\zeta} \\phi", - units="rad", - units_long="radians", - description="Toroidal angle in lab frame, derivative wrt toroidal coordinate", - dim=1, - params=[], - transforms={}, - profiles=[], - coordinates="rtz", - data=["e_zeta"], - parameterization="desc.geometry.core.Surface", -) -def _phi_z_Surface(params, transforms, profiles, data, **kwargs): - coords_z = data["e_zeta"] - data["phi_z"] = coords_z[:, 1] - return data - - @register_compute_fun( name="Z", label="Z", diff --git a/desc/compute/data_index.py b/desc/compute/data_index.py index de4ecf9314..26341ec587 100644 --- a/desc/compute/data_index.py +++ b/desc/compute/data_index.py @@ -132,6 +132,8 @@ def register_compute_fun( # noqa: C901 source_grid_requirement = {} if not isinstance(parameterization, (tuple, list)): parameterization = [parameterization] + if not isinstance(aliases, (tuple, list)): + aliases = [aliases] deps = { "params": params, diff --git a/tests/inputs/master_compute_data_rpz.pkl b/tests/inputs/master_compute_data_rpz.pkl index c787da504609e729c4fbbd2f905244a6ae2accbf..8a9086d237b4ba94e03209b8fead9406c04acd9e 100644 GIT binary patch delta 61246 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 delta 71648 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 Date: Mon, 12 Aug 2024 01:18:21 -0400 Subject: [PATCH 47/60] Undo mod by 2pi on phi --- desc/compute/_core.py | 2 +- tests/test_equilibrium.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index 6e5a137ed3..f5ae9a2552 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -2873,7 +2873,7 @@ def _zeta(params, transforms, profiles, data, **kwargs): data=["zeta", "omega"], ) def _phi(params, transforms, profiles, data, **kwargs): - data["phi"] = (data["zeta"] + data["omega"]) % (2 * jnp.pi) + data["phi"] = data["zeta"] + data["omega"] return data diff --git a/tests/test_equilibrium.py b/tests/test_equilibrium.py index 834775ecae..52958a6a9b 100644 --- a/tests/test_equilibrium.py +++ b/tests/test_equilibrium.py @@ -15,6 +15,7 @@ from desc.io import InputReader from desc.objectives import ForceBalance, ObjectiveFunction, get_equilibrium_objective from desc.profiles import PowerSeriesProfile +from desc.utils import errorif from .utils import area_difference, compute_coords @@ -55,16 +56,27 @@ def test_map_coordinates(): iota = grid.compress(eq.compute("iota", grid=grid)["iota"]) iota = np.broadcast_to(iota, shape=(n,)) - out_1 = eq.map_coordinates(coords, inbasis=["rho", "alpha", "zeta"], iota=iota) + tol = 1e-5 + out_1 = eq.map_coordinates( + coords, inbasis=["rho", "alpha", "zeta"], iota=iota, tol=tol + ) assert np.isfinite(out_1).all() out_2 = eq.map_coordinates( coords, inbasis=["rho", "alpha", "zeta"], period=(np.inf, 2 * np.pi, np.inf), + tol=tol, ) assert np.isfinite(out_2).all() - diff = (out_1 - out_2) % (2 * np.pi) - assert np.all(np.isclose(diff, 0) | np.isclose(np.abs(diff), 2 * np.pi)) + diff = out_1 - out_2 + errorif( + not np.all( + np.isclose(diff, 0, atol=2 * tol) + | np.isclose(np.abs(diff), 2 * np.pi, atol=2 * tol) + ), + AssertionError, + f"diff: {diff}", + ) eq = get("DSHAPE") From 0937fa668e0ef1b56fa9cff0344f7c0e566db219 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 12 Aug 2024 19:46:04 -0400 Subject: [PATCH 48/60] Don't alias existing angle derivatives to stream functions --- desc/compute/_core.py | 309 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 272 insertions(+), 37 deletions(-) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index f5ae9a2552..65cef12f39 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -1649,7 +1649,6 @@ def _lambda(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_r", ) def _lambda_r(params, transforms, profiles, data, **kwargs): data["lambda_r"] = transforms["L"].transform(params["L_lmn"], 1, 0, 0) @@ -1904,7 +1903,6 @@ def _lambda_t(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_tt", ) def _lambda_tt(params, transforms, profiles, data, **kwargs): data["lambda_tt"] = transforms["L"].transform(params["L_lmn"], 0, 2, 0) @@ -1963,7 +1961,6 @@ def _lambda_ttz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_tz", ) def _lambda_tz(params, transforms, profiles, data, **kwargs): data["lambda_tz"] = transforms["L"].transform(params["L_lmn"], 0, 1, 1) @@ -2002,7 +1999,6 @@ def _lambda_tzz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_z", ) def _lambda_z(params, transforms, profiles, data, **kwargs): data["lambda_z"] = transforms["L"].transform(params["L_lmn"], 0, 0, 1) @@ -2021,7 +2017,6 @@ def _lambda_z(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_zz", ) def _lambda_zz(params, transforms, profiles, data, **kwargs): data["lambda_zz"] = transforms["L"].transform(params["L_lmn"], 0, 0, 2) @@ -2085,7 +2080,6 @@ def _omega(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_r", ) def _omega_r(params, transforms, profiles, data, **kwargs): data["omega_r"] = data["0"] @@ -2108,7 +2102,6 @@ def _omega_r(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rr", ) def _omega_rr(params, transforms, profiles, data, **kwargs): data["omega_rr"] = data["0"] @@ -2346,7 +2339,6 @@ def _omega_rrzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rt", ) def _omega_rt(params, transforms, profiles, data, **kwargs): data["omega_rt"] = data["0"] @@ -2490,7 +2482,6 @@ def _omega_rtzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rz", ) def _omega_rz(params, transforms, profiles, data, **kwargs): data["omega_rz"] = data["0"] @@ -2561,7 +2552,6 @@ def _omega_rzzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_t", ) def _omega_t(params, transforms, profiles, data, **kwargs): data["omega_t"] = data["0"] @@ -2584,7 +2574,6 @@ def _omega_t(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_tt", ) def _omega_tt(params, transforms, profiles, data, **kwargs): data["omega_tt"] = data["0"] @@ -2655,7 +2644,6 @@ def _omega_ttz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_tz", ) def _omega_tz(params, transforms, profiles, data, **kwargs): data["omega_tz"] = data["0"] @@ -2724,7 +2712,6 @@ def _omega_z(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_zz", ) def _omega_zz(params, transforms, profiles, data, **kwargs): data["omega_zz"] = data["0"] @@ -2754,6 +2741,199 @@ def _omega_zzz(params, transforms, profiles, data, **kwargs): return data +@register_compute_fun( + name="phi", + label="\\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["zeta", "omega"], +) +def _phi(params, transforms, profiles, data, **kwargs): + data["phi"] = data["zeta"] + data["omega"] + return data + + +@register_compute_fun( + name="phi_r", + label="\\partial_{\\rho} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, derivative wrt radial coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_r"], +) +def _phi_r(params, transforms, profiles, data, **kwargs): + data["phi_r"] = data["omega_r"] + return data + + +@register_compute_fun( + name="phi_rr", + label="\\partial_{\\rho \\rho} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt radial coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_rr"], +) +def _phi_rr(params, transforms, profiles, data, **kwargs): + data["phi_rr"] = data["omega_rr"] + return data + + +@register_compute_fun( + name="phi_rt", + label="\\partial_{\\rho \\theta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt radial and " + "poloidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_rt"], +) +def _phi_rt(params, transforms, profiles, data, **kwargs): + data["phi_rt"] = data["omega_rt"] + return data + + +@register_compute_fun( + name="phi_rz", + label="\\partial_{\\rho \\zeta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt radial and " + "toroidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_rz"], +) +def _phi_rz(params, transforms, profiles, data, **kwargs): + data["phi_rz"] = data["omega_rz"] + return data + + +@register_compute_fun( + name="phi_t", + label="\\partial_{\\theta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, derivative wrt poloidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_t"], + parameterization=[ + "desc.equilibrium.equilibrium.Equilibrium", + "desc.geometry.core.Surface", + ], +) +def _phi_t(params, transforms, profiles, data, **kwargs): + data["phi_t"] = data["omega_t"] + return data + + +@register_compute_fun( + name="phi_tt", + label="\\partial_{\\theta \\theta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt poloidal " + "coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_tt"], +) +def _phi_tt(params, transforms, profiles, data, **kwargs): + data["phi_tt"] = data["omega_tt"] + return data + + +@register_compute_fun( + name="phi_tz", + label="\\partial_{\\theta \\zeta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt poloidal and " + "toroidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_tz"], +) +def _phi_tz(params, transforms, profiles, data, **kwargs): + data["phi_tz"] = data["omega_tz"] + return data + + +@register_compute_fun( + name="phi_z", + label="\\partial_{\\zeta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, derivative wrt toroidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_z"], + parameterization=[ + "desc.equilibrium.equilibrium.Equilibrium", + "desc.geometry.core.Surface", + ], +) +def _phi_z(params, transforms, profiles, data, **kwargs): + data["phi_z"] = 1 + data["omega_z"] + return data + + +@register_compute_fun( + name="phi_zz", + label="\\partial_{\\zeta \\zeta} \\phi", + units="rad", + units_long="radians", + description="Toroidal angle in lab frame, second derivative wrt toroidal " + "coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["omega_zz"], +) +def _phi_zz(params, transforms, profiles, data, **kwargs): + data["phi_zz"] = data["omega_zz"] + return data + + @register_compute_fun( name="rho", label="\\rho", @@ -2818,6 +2998,25 @@ def _theta_PEST(params, transforms, profiles, data, **kwargs): return data +@register_compute_fun( + name="theta_PEST_r", + label="\\partial_{\\rho} \\vartheta", + units="rad", + units_long="radians", + description="PEST straight field line poloidal angular coordinate, derivative wrt " + "radial coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["lambda_r"], +) +def _theta_PEST_r(params, transforms, profiles, data, **kwargs): + data["theta_PEST_r"] = data["lambda_r"] + return data + + @register_compute_fun( name="theta_PEST_t", label="\\partial_{\\theta} \\vartheta", @@ -2838,62 +3037,98 @@ def _theta_PEST_t(params, transforms, profiles, data, **kwargs): @register_compute_fun( - name="zeta", - label="\\zeta", + name="theta_PEST_tt", + label="\\partial_{\\theta \\theta} \\vartheta", units="rad", units_long="radians", - description="Toroidal angular coordinate", + description="PEST straight field line poloidal angular coordinate, second " + "derivative wrt poloidal coordinate", dim=1, params=[], - transforms={"grid": []}, + transforms={}, profiles=[], - coordinates="z", - data=[], - parameterization=[ - "desc.equilibrium.equilibrium.Equilibrium", - "desc.geometry.core.Surface", - ], + coordinates="rtz", + data=["lambda_tt"], ) -def _zeta(params, transforms, profiles, data, **kwargs): - data["zeta"] = transforms["grid"].nodes[:, 2] +def _theta_PEST_tt(params, transforms, profiles, data, **kwargs): + data["theta_PEST_tt"] = data["lambda_tt"] return data @register_compute_fun( - name="phi", - label="\\phi", + name="theta_PEST_tz", + label="\\partial_{\\theta \\zeta} \\vartheta", units="rad", units_long="radians", - description="Toroidal angle in lab frame", + description="PEST straight field line poloidal angular coordinate, derivative wrt " + "poloidal and toroidal coordinates", dim=1, params=[], transforms={}, profiles=[], coordinates="rtz", - data=["zeta", "omega"], + data=["lambda_tz"], ) -def _phi(params, transforms, profiles, data, **kwargs): - data["phi"] = data["zeta"] + data["omega"] +def _theta_PEST_tz(params, transforms, profiles, data, **kwargs): + data["theta_PEST_tz"] = data["lambda_tz"] return data @register_compute_fun( - name="phi_z", - label="\\partial_{\\zeta} \\phi", + name="theta_PEST_z", + label="\\partial_{\\zeta} \\vartheta", units="rad", units_long="radians", - description="Toroidal angle in lab frame, derivative wrt toroidal coordinate", + description="PEST straight field line poloidal angular coordinate, derivative wrt " + "toroidal coordinate", dim=1, params=[], transforms={}, profiles=[], coordinates="rtz", - data=["omega_z"], + data=["lambda_z"], +) +def _theta_PEST_z(params, transforms, profiles, data, **kwargs): + data["theta_PEST_z"] = data["lambda_z"] + return data + + +@register_compute_fun( + name="theta_PEST_zz", + label="\\partial_{\\zeta \\zeta} \\vartheta", + units="rad", + units_long="radians", + description="PEST straight field line poloidal angular coordinate, second " + "derivative wrt toroidal coordinate", + dim=1, + params=[], + transforms={}, + profiles=[], + coordinates="rtz", + data=["lambda_zz"], +) +def _theta_PEST_zz(params, transforms, profiles, data, **kwargs): + data["theta_PEST_zz"] = data["lambda_zz"] + return data + + +@register_compute_fun( + name="zeta", + label="\\zeta", + units="rad", + units_long="radians", + description="Toroidal angular coordinate", + dim=1, + params=[], + transforms={"grid": []}, + profiles=[], + coordinates="z", + data=[], parameterization=[ "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], ) -def _phi_z(params, transforms, profiles, data, **kwargs): - data["phi_z"] = 1 + data["omega_z"] +def _zeta(params, transforms, profiles, data, **kwargs): + data["zeta"] = transforms["grid"].nodes[:, 2] return data From e178e4c2355cabf3e406c4403ce4404a5616c2bc Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 12 Aug 2024 20:02:49 -0400 Subject: [PATCH 49/60] Remove unused aliases --- desc/compute/_core.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index 65cef12f39..4e08571f58 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -1667,7 +1667,6 @@ def _lambda_r(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rr", ) def _lambda_rr(params, transforms, profiles, data, **kwargs): data["lambda_rr"] = transforms["L"].transform(params["L_lmn"], 2, 0, 0) @@ -1686,7 +1685,6 @@ def _lambda_rr(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rrr", ) def _lambda_rrr(params, transforms, profiles, data, **kwargs): data["lambda_rrr"] = transforms["L"].transform(params["L_lmn"], 3, 0, 0) @@ -1706,7 +1704,6 @@ def _lambda_rrr(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rrrt", ) def _lambda_rrrt(params, transforms, profiles, data, **kwargs): data["lambda_rrrt"] = transforms["L"].transform(params["L_lmn"], 3, 1, 0) @@ -1726,7 +1723,6 @@ def _lambda_rrrt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rrrz", ) def _lambda_rrrz(params, transforms, profiles, data, **kwargs): data["lambda_rrrz"] = transforms["L"].transform(params["L_lmn"], 3, 0, 1) @@ -1766,7 +1762,6 @@ def _lambda_rrt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rrz", ) def _lambda_rrz(params, transforms, profiles, data, **kwargs): data["lambda_rrz"] = transforms["L"].transform(params["L_lmn"], 2, 0, 1) @@ -1846,7 +1841,6 @@ def _lambda_rtz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rz", ) def _lambda_rz(params, transforms, profiles, data, **kwargs): data["lambda_rz"] = transforms["L"].transform(params["L_lmn"], 1, 0, 1) @@ -1866,7 +1860,6 @@ def _lambda_rz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rzz", ) def _lambda_rzz(params, transforms, profiles, data, **kwargs): data["lambda_rzz"] = transforms["L"].transform(params["L_lmn"], 1, 0, 2) @@ -2035,7 +2028,6 @@ def _lambda_zz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_zzz", ) def _lambda_zzz(params, transforms, profiles, data, **kwargs): data["lambda_zzz"] = transforms["L"].transform(params["L_lmn"], 0, 0, 3) @@ -2124,7 +2116,6 @@ def _omega_rr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrr", ) def _omega_rrr(params, transforms, profiles, data, **kwargs): data["omega_rrr"] = data["0"] @@ -2147,7 +2138,6 @@ def _omega_rrr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrrr", ) def _omega_rrrr(params, transforms, profiles, data, **kwargs): data["omega_rrrr"] = data["0"] @@ -2171,7 +2161,6 @@ def _omega_rrrr(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrrt", ) def _omega_rrrt(params, transforms, profiles, data, **kwargs): data["omega_rrrt"] = data["0"] @@ -2195,7 +2184,6 @@ def _omega_rrrt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrrz", ) def _omega_rrrz(params, transforms, profiles, data, **kwargs): data["omega_rrrz"] = data["0"] @@ -2219,7 +2207,6 @@ def _omega_rrrz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrt", ) def _omega_rrt(params, transforms, profiles, data, **kwargs): data["omega_rrt"] = data["0"] @@ -2243,7 +2230,6 @@ def _omega_rrt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrtt", ) def _omega_rrtt(params, transforms, profiles, data, **kwargs): data["omega_rrtt"] = data["0"] @@ -2267,7 +2253,6 @@ def _omega_rrtt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrtz", ) def _omega_rrtz(params, transforms, profiles, data, **kwargs): data["omega_rrtz"] = data["0"] @@ -2315,7 +2300,6 @@ def _omega_rrz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrzz", ) def _omega_rrzz(params, transforms, profiles, data, **kwargs): data["omega_rrzz"] = data["0"] @@ -2362,7 +2346,6 @@ def _omega_rt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rtt", ) def _omega_rtt(params, transforms, profiles, data, **kwargs): data["omega_rtt"] = data["0"] @@ -2386,7 +2369,6 @@ def _omega_rtt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rttt", ) def _omega_rttt(params, transforms, profiles, data, **kwargs): data["omega_rttt"] = data["0"] @@ -2410,7 +2392,6 @@ def _omega_rttt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rttz", ) def _omega_rttz(params, transforms, profiles, data, **kwargs): data["omega_rttz"] = data["0"] @@ -2458,7 +2439,6 @@ def _omega_rtz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rtzz", ) def _omega_rtzz(params, transforms, profiles, data, **kwargs): data["omega_rtzz"] = data["0"] @@ -2529,7 +2509,6 @@ def _omega_rzz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rzzz", ) def _omega_rzzz(params, transforms, profiles, data, **kwargs): data["omega_rzzz"] = data["0"] @@ -2596,7 +2575,6 @@ def _omega_tt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_ttt", ) def _omega_ttt(params, transforms, profiles, data, **kwargs): data["omega_ttt"] = data["0"] From 99ebe6eeed5e335a9339ff92153f5cb27683200c Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 12 Aug 2024 22:11:02 -0400 Subject: [PATCH 50/60] Add missing paramterization to phi_z --- desc/compute/_core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index 4e08571f58..38e2d0c257 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -2749,6 +2749,10 @@ def _phi(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=["omega_r"], + parameterization=[ + "desc.equilibrium.equilibrium.Equilibrium", + "desc.geometry.core.Surface", + ], ) def _phi_r(params, transforms, profiles, data, **kwargs): data["phi_r"] = data["omega_r"] From 2b5d3c950f3754e2d1c53c97a3089e1d8d477e18 Mon Sep 17 00:00:00 2001 From: unalmis Date: Mon, 12 Aug 2024 22:14:15 -0400 Subject: [PATCH 51/60] Remove stream function alias to angles --- desc/compute/_core.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/desc/compute/_core.py b/desc/compute/_core.py index 38e2d0c257..7bc6e8acb3 100644 --- a/desc/compute/_core.py +++ b/desc/compute/_core.py @@ -1742,7 +1742,6 @@ def _lambda_rrrz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rrt", ) def _lambda_rrt(params, transforms, profiles, data, **kwargs): data["lambda_rrt"] = transforms["L"].transform(params["L_lmn"], 2, 1, 0) @@ -1781,7 +1780,6 @@ def _lambda_rrz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rt", ) def _lambda_rt(params, transforms, profiles, data, **kwargs): data["lambda_rt"] = transforms["L"].transform(params["L_lmn"], 1, 1, 0) @@ -1801,7 +1799,6 @@ def _lambda_rt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rtt", ) def _lambda_rtt(params, transforms, profiles, data, **kwargs): data["lambda_rtt"] = transforms["L"].transform(params["L_lmn"], 1, 2, 0) @@ -1821,7 +1818,6 @@ def _lambda_rtt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_rtz", ) def _lambda_rtz(params, transforms, profiles, data, **kwargs): data["lambda_rtz"] = transforms["L"].transform(params["L_lmn"], 1, 1, 1) @@ -1914,7 +1910,6 @@ def _lambda_tt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_ttt", ) def _lambda_ttt(params, transforms, profiles, data, **kwargs): data["lambda_ttt"] = transforms["L"].transform(params["L_lmn"], 0, 3, 0) @@ -1934,7 +1929,6 @@ def _lambda_ttt(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_ttz", ) def _lambda_ttz(params, transforms, profiles, data, **kwargs): data["lambda_ttz"] = transforms["L"].transform(params["L_lmn"], 0, 2, 1) @@ -1973,7 +1967,6 @@ def _lambda_tz(params, transforms, profiles, data, **kwargs): profiles=[], coordinates="rtz", data=[], - aliases="theta_PEST_tzz", ) def _lambda_tzz(params, transforms, profiles, data, **kwargs): data["lambda_tzz"] = transforms["L"].transform(params["L_lmn"], 0, 1, 2) @@ -2276,7 +2269,6 @@ def _omega_rrtz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rrz", ) def _omega_rrz(params, transforms, profiles, data, **kwargs): data["omega_rrz"] = data["0"] @@ -2415,7 +2407,6 @@ def _omega_rttz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rtz", ) def _omega_rtz(params, transforms, profiles, data, **kwargs): data["omega_rtz"] = data["0"] @@ -2485,7 +2476,6 @@ def _omega_rz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_rzz", ) def _omega_rzz(params, transforms, profiles, data, **kwargs): data["omega_rzz"] = data["0"] @@ -2598,7 +2588,6 @@ def _omega_ttt(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_ttz", ) def _omega_ttz(params, transforms, profiles, data, **kwargs): data["omega_ttz"] = data["0"] @@ -2645,7 +2634,6 @@ def _omega_tz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_tzz", ) def _omega_tzz(params, transforms, profiles, data, **kwargs): data["omega_tzz"] = data["0"] @@ -2712,7 +2700,6 @@ def _omega_zz(params, transforms, profiles, data, **kwargs): "desc.equilibrium.equilibrium.Equilibrium", "desc.geometry.core.Surface", ], - aliases="phi_zzz", ) def _omega_zzz(params, transforms, profiles, data, **kwargs): data["omega_zzz"] = data["0"] From b9ba0beaf15f1e992a89f4d8004fafce4bf6c96c Mon Sep 17 00:00:00 2001 From: YigitElma Date: Tue, 13 Aug 2024 00:57:59 -0400 Subject: [PATCH 52/60] fix the sign error in wide QR factorization --- desc/optimize/tr_subproblems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/optimize/tr_subproblems.py b/desc/optimize/tr_subproblems.py index c9149d2bb3..b107bca0a4 100644 --- a/desc/optimize/tr_subproblems.py +++ b/desc/optimize/tr_subproblems.py @@ -421,7 +421,7 @@ def trust_region_step_exact_qr( p_newton = solve_triangular_regularized(R, -Q.T @ f) else: Q, R = qr(J.T, mode="economic") - p_newton = Q @ solve_triangular_regularized(R.T, f, lower=True) + p_newton = Q @ solve_triangular_regularized(R.T, -f, lower=True) def truefun(*_): return p_newton, False, 0.0 From 9549b7d0d59149b1bd8d02fc55deff9a080bf297 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Tue, 13 Aug 2024 20:25:21 -0400 Subject: [PATCH 53/60] reduce test stringency for NAE --- tests/test_examples.py | 103 ++++++----------------------------------- 1 file changed, 15 insertions(+), 88 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 8f6e42ccec..430258cb8f 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -440,7 +440,6 @@ def test_NAE_QSC_solve(): orig_Rax_val = eq.axis.R_n orig_Zax_val = eq.axis.Z_n - eq_fit = eq.copy() eq_lambda_fixed_0th_order = eq.copy() eq_lambda_fixed_1st_order = eq.copy() @@ -472,7 +471,6 @@ def test_NAE_QSC_solve(): xtol=1e-6, constraints=constraints, ) - grid = LinearGrid(L=10, M=20, N=20, NFP=eq.NFP, sym=True, axis=False) grid_axis = LinearGrid(rho=0.0, theta=0.0, N=eq.N_grid, NFP=eq.NFP) # Make sure axis is same for eqq, string in zip( @@ -482,24 +480,15 @@ def test_NAE_QSC_solve(): np.testing.assert_array_almost_equal(orig_Rax_val, eqq.axis.R_n, err_msg=string) np.testing.assert_array_almost_equal(orig_Zax_val, eqq.axis.Z_n, err_msg=string) - # Make sure surfaces of solved equilibrium are similar near axis as QSC - rho_err, theta_err = area_difference_desc(eqq, eq_fit) + # Make sure iota of solved equilibrium is same on-axis as QSC - np.testing.assert_allclose(rho_err[:, 0:-6], 0, atol=1.5e-2, err_msg=string) - np.testing.assert_allclose(theta_err[:, 0:-6], 0, atol=1e-3, err_msg=string) - - # Make sure iota of solved equilibrium is same near axis as QSC - - iota = grid.compress(eqq.compute("iota", grid=grid)["iota"]) + iota = eqq.compute("iota", grid=LinearGrid(rho=0.0))["iota"] np.testing.assert_allclose(iota[0], qsc.iota, atol=1e-5, err_msg=string) - np.testing.assert_allclose(iota[1:10], qsc.iota, atol=1e-3, err_msg=string) - # check lambda to match near axis - # Evaluate lambda near the axis + # check lambda to match on-axis data_nae = eqq.compute(["lambda", "|B|"], grid=grid_axis) lam_nae = data_nae["lambda"] - # Reshape to form grids on theta and phi phi = np.squeeze(grid_axis.nodes[:, 2]) np.testing.assert_allclose( @@ -526,7 +515,6 @@ def test_NAE_QSC_solve_near_axis_based_off_eq(): orig_Rax_val = eq.axis.R_n orig_Zax_val = eq.axis.Z_n - eq_fit = eq.copy() eq_lambda_fixed_0th_order = eq.copy() eq_lambda_fixed_1st_order = eq.copy() @@ -558,7 +546,6 @@ def test_NAE_QSC_solve_near_axis_based_off_eq(): xtol=1e-6, constraints=constraints, ) - grid = LinearGrid(L=10, M=20, N=20, NFP=eq.NFP, sym=True, axis=False) grid_axis = LinearGrid(rho=0.0, theta=0.0, N=eq.N_grid, NFP=eq.NFP) # Make sure axis is same for eqq, string in zip( @@ -568,18 +555,11 @@ def test_NAE_QSC_solve_near_axis_based_off_eq(): np.testing.assert_array_almost_equal(orig_Rax_val, eqq.axis.R_n, err_msg=string) np.testing.assert_array_almost_equal(orig_Zax_val, eqq.axis.Z_n, err_msg=string) - # Make sure surfaces of solved equilibrium are similar near axis as QSC - rho_err, theta_err = area_difference_desc(eqq, eq_fit) - - np.testing.assert_allclose(rho_err[:, 0:-6], 0, atol=1.5e-2, err_msg=string) - np.testing.assert_allclose(theta_err[:, 0:-6], 0, atol=1e-3, err_msg=string) - - # Make sure iota of solved equilibrium is same near axis as QSC + # Make sure iota of solved equilibrium is same on axis as QSC - iota = grid.compress(eqq.compute("iota", grid=grid)["iota"]) + iota = eqq.compute("iota", grid=LinearGrid(rho=0.0))["iota"] np.testing.assert_allclose(iota[0], qsc.iota, atol=1e-5, err_msg=string) - np.testing.assert_allclose(iota[1:10], qsc.iota, rtol=5e-3, err_msg=string) ### check lambda to match on axis # Evaluate lambda on the axis @@ -603,18 +583,16 @@ def test_NAE_QSC_solve_near_axis_based_off_eq(): def test_NAE_QIC_solve(): """Test O(rho) NAE QIC constraints solve.""" # get Qic example - qic = Qic.from_paper("QI NFP2 r2", nphi=301, order="r1") + qic = Qic.from_paper("QI NFP2 r2", nphi=199, order="r1") qic.lasym = False # don't need to consider stellarator asym for order 1 constraints ntheta = 75 r = 0.01 - N = 11 + N = 9 eq = Equilibrium.from_near_axis(qic, r=r, L=7, M=7, N=N, ntheta=ntheta) orig_Rax_val = eq.axis.R_n orig_Zax_val = eq.axis.Z_n - eq_fit = eq.copy() - # this has all the constraints we need, cs = get_NAE_constraints(eq, qic, order=1) @@ -629,75 +607,24 @@ def test_NAE_QIC_solve(): np.testing.assert_array_almost_equal(orig_Rax_val, eq.axis.R_n) np.testing.assert_array_almost_equal(orig_Zax_val, eq.axis.Z_n) - # Make sure surfaces of solved equilibrium are similar near axis as QIC - rho_err, theta_err = area_difference_desc(eq, eq_fit) - - np.testing.assert_allclose(rho_err[:, 0:3], 0, atol=5e-2) - # theta error isn't really an indicator of near axis behavior - # since it's computed over the full radius, but just indicates that - # eq is similar to eq_fit - np.testing.assert_allclose(theta_err, 0, atol=5e-2) - # Make sure iota of solved equilibrium is same near axis as QIC - grid = LinearGrid(L=10, M=20, N=20, NFP=eq.NFP, sym=True, axis=False) - iota = grid.compress(eq.compute("iota", grid=grid)["iota"]) + iota = eq.compute("iota", grid=LinearGrid(rho=0.0))["iota"] np.testing.assert_allclose(iota[0], qic.iota, rtol=1e-5) - np.testing.assert_allclose(iota[1:10], qic.iota, rtol=1e-3) - # check lambda to match near axis - grid_2d_05 = LinearGrid(rho=np.array(1e-6), M=50, N=50, NFP=eq.NFP, endpoint=True) + grid_axis = LinearGrid(rho=0.0, theta=0.0, zeta=qic.phi, NFP=eq.NFP) + phi = grid_axis.nodes[:, 2].squeeze() - # Evaluate lambda near the axis - data_nae = eq.compute("lambda", grid=grid_2d_05) - lam_nae = data_nae["lambda"] + # check lambda to match on-axis + lam_nae = eq.compute("lambda", grid=grid_axis)["lambda"] - # Reshape to form grids on theta and phi - zeta = ( - grid_2d_05.nodes[:, 2] - .reshape( - (grid_2d_05.num_theta, grid_2d_05.num_rho, grid_2d_05.num_zeta), order="F" - ) - .squeeze() - ) - - lam_nae = lam_nae.reshape( - (grid_2d_05.num_theta, grid_2d_05.num_rho, grid_2d_05.num_zeta), order="F" - ) - - phi = np.squeeze(zeta[0, :]) - lam_nae = np.squeeze(lam_nae[:, 0, :]) - - lam_av_nae = np.mean(lam_nae, axis=0) np.testing.assert_allclose( - lam_av_nae, -qic.iota * qic.nu_spline(phi), atol=1e-4, rtol=1e-2 + lam_nae, -qic.iota * qic.nu_spline(phi), atol=1e-4, rtol=1e-2 ) # check |B| on axis - - grid = LinearGrid(M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP, rho=np.array(1e-6)) - # Evaluate B modes near the axis - data_nae = eq.compute(["|B|_mn", "B modes"], grid=grid) - modes = data_nae["B modes"] - B_mn_nae = data_nae["|B|_mn"] - # Evaluate B on an angular grid - theta = np.linspace(0, 2 * np.pi, 150) - phi = np.linspace(0, 2 * np.pi, qic.nphi) - th, ph = np.meshgrid(theta, phi) - B_nae = np.zeros((qic.nphi, 150)) - - for i, (l, m, n) in enumerate(modes): - if m >= 0 and n >= 0: - B_nae += B_mn_nae[i] * np.cos(m * th) * np.cos(n * ph) - elif m >= 0 > n: - B_nae += -B_mn_nae[i] * np.cos(m * th) * np.sin(n * ph) - elif m < 0 <= n: - B_nae += -B_mn_nae[i] * np.sin(m * th) * np.cos(n * ph) - elif m < 0 and n < 0: - B_nae += B_mn_nae[i] * np.sin(m * th) * np.sin(n * ph) - # Eliminate the poloidal angle to focus on the toroidal behavior - B_av_nae = np.mean(B_nae, axis=1) - np.testing.assert_allclose(B_av_nae, np.ones(np.size(phi)) * qic.B0, atol=2e-2) + B_nae = eq.compute(["|B|"], grid=grid_axis) + np.testing.assert_allclose(B_nae, qic.B0, atol=1e-3) @pytest.mark.unit From b397e89a32a0895c6ef9ffa0a3de7e3e5e233908 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 14 Aug 2024 11:00:01 -0400 Subject: [PATCH 54/60] fix test --- tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 430258cb8f..8187375caf 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -623,7 +623,7 @@ def test_NAE_QIC_solve(): ) # check |B| on axis - B_nae = eq.compute(["|B|"], grid=grid_axis) + B_nae = eq.compute(["|B|"], grid=grid_axis)["|B|"] np.testing.assert_allclose(B_nae, qic.B0, atol=1e-3) From 550b20d5c190ba610449d342eaed1216857d13fb Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 14 Aug 2024 11:04:06 -0400 Subject: [PATCH 55/60] Revert "remove ds from compute index in favor of using grad spacing directly" This reverts commit f75e4a2b3e6b39c0ccde11e8028c5484b8cbe478. --- desc/compute/_curve.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/desc/compute/_curve.py b/desc/compute/_curve.py index 41cd2e44e3..818da18092 100644 --- a/desc/compute/_curve.py +++ b/desc/compute/_curve.py @@ -26,6 +26,25 @@ def _s(params, transforms, profiles, data, **kwargs): return data +@register_compute_fun( + name="ds", + label="ds", + units="~", + units_long="None", + description="Spacing of curve parameter", + dim=1, + params=[], + transforms={"grid": []}, + profiles=[], + coordinates="s", + data=[], + parameterization="desc.geometry.core.Curve", +) +def _ds(params, transforms, profiles, data, **kwargs): + data["ds"] = transforms["grid"].spacing[:, 2] + return data + + @register_compute_fun( name="X", label="X", @@ -961,17 +980,17 @@ def _torsion(params, transforms, profiles, data, **kwargs): description="Length of the curve", dim=0, params=[], - transforms={"grid": []}, + transforms={}, profiles=[], coordinates="", - data=["x_s"], + data=["ds", "x_s"], parameterization=["desc.geometry.core.Curve"], ) def _length(params, transforms, profiles, data, **kwargs): T = jnp.linalg.norm(data["x_s"], axis=-1) # this is equivalent to jnp.trapz(T, s) for a closed curve, # but also works if grid.endpoint is False - data["length"] = jnp.sum(T * transforms["grid"].spacing[:, 2]) + data["length"] = jnp.sum(T * data["ds"]) return data @@ -983,10 +1002,10 @@ def _length(params, transforms, profiles, data, **kwargs): description="Length of the curve", dim=0, params=[], - transforms={"grid": []}, + transforms={}, profiles=[], coordinates="", - data=["x", "x_s"], + data=["ds", "x", "x_s"], parameterization="desc.geometry.curve.SplineXYZCurve", method="Interpolation type, Default 'cubic'. See SplineXYZCurve docs for options.", ) @@ -996,8 +1015,7 @@ def _length_SplineXYZCurve(params, transforms, profiles, data, **kwargs): if kwargs.get("basis", "rpz").lower() == "rpz": coords = rpz2xyz(coords) # ensure curve is closed - # if it's already closed this doesn't add any length since - # grid spacing will be zero at the duplicate point + # if it's already closed this doesn't add any length since ds will be zero coords = jnp.concatenate([coords, coords[:1]]) X = coords[:, 0] Y = coords[:, 1] @@ -1008,5 +1026,5 @@ def _length_SplineXYZCurve(params, transforms, profiles, data, **kwargs): T = jnp.linalg.norm(data["x_s"], axis=-1) # this is equivalent to jnp.trapz(T, s) for a closed curve # but also works if grid.endpoint is False - data["length"] = jnp.sum(T * transforms["grid"].spacing[:, 2]) + data["length"] = jnp.sum(T * data["ds"]) return data From b369c5ea6e1ee2c1e9782f6e3672f98198de6daf Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 14 Aug 2024 11:06:05 -0400 Subject: [PATCH 56/60] just change description of ds --- desc/compute/_curve.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/desc/compute/_curve.py b/desc/compute/_curve.py index 818da18092..d1bd231f6c 100644 --- a/desc/compute/_curve.py +++ b/desc/compute/_curve.py @@ -31,7 +31,10 @@ def _s(params, transforms, profiles, data, **kwargs): label="ds", units="~", units_long="None", - description="Spacing of curve parameter", + description=( + "Quadrature weights for integration along the curve, " + + " i.e. an alias for ``grid.spacing[:,2]``" + ), dim=1, params=[], transforms={"grid": []}, From 98190a31839b5ad3a0d02093b1ddcb8294d903b7 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Wed, 14 Aug 2024 16:59:18 -0400 Subject: [PATCH 57/60] remove double space --- desc/compute/_curve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desc/compute/_curve.py b/desc/compute/_curve.py index d1bd231f6c..2e96e7a767 100644 --- a/desc/compute/_curve.py +++ b/desc/compute/_curve.py @@ -32,7 +32,7 @@ def _s(params, transforms, profiles, data, **kwargs): units="~", units_long="None", description=( - "Quadrature weights for integration along the curve, " + "Quadrature weights for integration along the curve," + " i.e. an alias for ``grid.spacing[:,2]``" ), dim=1, From c5a49f66ae146418c545e4585953c9e30e2ab664 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 15 Aug 2024 13:34:07 -0400 Subject: [PATCH 58/60] increasing grid resolution seems to resolve issue --- tests/test_examples.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index 8f6e42ccec..c336a2f624 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1700,7 +1700,7 @@ def test_signed_PlasmaVesselDistance(): eq = Equilibrium(M=1, N=1) surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=10, N=2, NFP=eq.NFP) + grid = LinearGrid(M=10, N=4, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, @@ -1723,4 +1723,9 @@ def test_signed_PlasmaVesselDistance(): xtol=1e-9, ) - np.testing.assert_allclose(obj.compute(*obj.xs(eq, surf)), target_dist, atol=1e-2) + np.testing.assert_allclose( + obj.compute(*obj.xs(eq, surf)), + target_dist, + atol=1e-2, + err_msg="allowing eq to change", + ) From 4aeb0a73a1913d7a0019ddd4c88a986aad7d110d Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Thu, 15 Aug 2024 14:35:39 -0400 Subject: [PATCH 59/60] further increase grid res --- tests/test_examples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_examples.py b/tests/test_examples.py index c336a2f624..9b07014042 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -1700,7 +1700,7 @@ def test_signed_PlasmaVesselDistance(): eq = Equilibrium(M=1, N=1) surf = eq.surface.copy() surf.change_resolution(M=1, N=1) - grid = LinearGrid(M=10, N=4, NFP=eq.NFP) + grid = LinearGrid(M=20, N=8, NFP=eq.NFP) obj = PlasmaVesselDistance( surface=surf, @@ -1710,7 +1710,7 @@ def test_signed_PlasmaVesselDistance(): plasma_grid=grid, use_signed_distance=True, ) - objective = ObjectiveFunction((obj,)) + objective = ObjectiveFunction(obj) optimizer = Optimizer("lsq-exact") (eq, surf), _ = optimizer.optimize( From e39d08fc65087c18b8d1b409d55c14cd66cdbb71 Mon Sep 17 00:00:00 2001 From: Dario Panici Date: Fri, 16 Aug 2024 10:42:54 -0400 Subject: [PATCH 60/60] improve error msg for plot coilis --- desc/plotting.py | 8 ++++++++ tests/test_plotting.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/desc/plotting.py b/desc/plotting.py index ba822a5b2b..478e11dc66 100644 --- a/desc/plotting.py +++ b/desc/plotting.py @@ -2384,6 +2384,8 @@ def plot_coils(coils, grid=None, fig=None, return_data=False, **kwargs): dictionary of the data plotted, only returned if ``return_data=True`` """ + from desc.coils import _Coil + lw = kwargs.pop("lw", 5) ls = kwargs.pop("ls", "solid") figsize = kwargs.pop("figsize", (10, 10)) @@ -2394,6 +2396,12 @@ def plot_coils(coils, grid=None, fig=None, return_data=False, **kwargs): ValueError, f"plot_coils got unexpected keyword argument: {kwargs.keys()}", ) + errorif( + not isinstance(coils, _Coil), + ValueError, + "Expected `coils` to be object of type `_Coil`, instead got type" + f" {type(coils)}", + ) if not isinstance(lw, (list, tuple)): lw = [lw] diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 8252d349d8..1351b46c93 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -801,6 +801,8 @@ def test_plot_coils(): coil.rotate(angle=np.pi / N) coils = CoilSet.linspaced_angular(coil, I, [0, 0, 1], np.pi / NFP, N // NFP // 2) coils2 = MixedCoilSet.from_symmetry(coils, NFP, True) + with pytest.raises(ValueError, match="Expected `coils`"): + plot_coils("not a coil") fig, data = plot_coils(coils2, return_data=True) def flatten_coils(coilset):