agnimhd 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agnimhd/__init__.py +52 -0
- agnimhd/_version.py +24 -0
- agnimhd/adapters/__init__.py +10 -0
- agnimhd/adapters/desc.py +139 -0
- agnimhd/adapters/desc_objective.py +355 -0
- agnimhd/assemble.py +1212 -0
- agnimhd/backend.py +87 -0
- agnimhd/basis/__init__.py +43 -0
- agnimhd/basis/diffmat.py +957 -0
- agnimhd/basis/zernike.py +482 -0
- agnimhd/cli.py +257 -0
- agnimhd/config.py +302 -0
- agnimhd/equilibrium.py +724 -0
- agnimhd/multigpu.py +92 -0
- agnimhd/objective.py +912 -0
- agnimhd/plotting.py +474 -0
- agnimhd/py.typed +0 -0
- agnimhd/quadrature.py +284 -0
- agnimhd/solvers.py +857 -0
- agnimhd/sources.py +81 -0
- agnimhd/symmetry.py +119 -0
- agnimhd-0.1.0.dist-info/METADATA +148 -0
- agnimhd-0.1.0.dist-info/RECORD +27 -0
- agnimhd-0.1.0.dist-info/WHEEL +5 -0
- agnimhd-0.1.0.dist-info/entry_points.txt +2 -0
- agnimhd-0.1.0.dist-info/licenses/LICENSE +21 -0
- agnimhd-0.1.0.dist-info/top_level.txt +1 -0
agnimhd/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""AGNI -- Analysis of Global Normal-modes in Ideal MHD.
|
|
2
|
+
|
|
3
|
+
A differentiable finite-n ideal MHD stability solver.
|
|
4
|
+
|
|
5
|
+
AGNI solves ideal MHD stability from a **variational principle**: it discretizes
|
|
6
|
+
the energy functional rather than the force operator. That gives a generalized
|
|
7
|
+
symmetric eigenvalue problem ``A x = lambda B x`` with ``B`` (the kinetic/mass
|
|
8
|
+
matrix) symmetric positive definite. ``B`` is Cholesky-factored to reduce this to
|
|
9
|
+
a standard symmetric eigenvalue problem. Everything agnimhd returns is the
|
|
10
|
+
squared growth rate ``gamma^2 = -lambda`` of the most negative eigenvalue
|
|
11
|
+
``lambda``: **positive means unstable**.
|
|
12
|
+
|
|
13
|
+
The package depends on ``jax``, ``numpy``, ``scipy`` and ``matfree``.
|
|
14
|
+
:func:`from_desc` imports DESC when it is called; nothing else does. Other
|
|
15
|
+
equilibrium codes fill an :class:`EquilibriumData` directly; see
|
|
16
|
+
``docs/interface.md``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .adapters import from_desc
|
|
20
|
+
from .basis import Basis, DiffMat
|
|
21
|
+
from .config import AssemblyConfig, SolverConfig
|
|
22
|
+
from .equilibrium import FORMAT_VERSION, EquilibriumData
|
|
23
|
+
from .objective import (
|
|
24
|
+
eigenpair,
|
|
25
|
+
growth_rate,
|
|
26
|
+
growth_rate_and_grad,
|
|
27
|
+
growth_rate_of,
|
|
28
|
+
solve,
|
|
29
|
+
)
|
|
30
|
+
from .sources import load
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AssemblyConfig",
|
|
34
|
+
"Basis",
|
|
35
|
+
"DiffMat",
|
|
36
|
+
"EquilibriumData",
|
|
37
|
+
"FORMAT_VERSION",
|
|
38
|
+
"SolverConfig",
|
|
39
|
+
"eigenpair",
|
|
40
|
+
"from_desc",
|
|
41
|
+
"growth_rate",
|
|
42
|
+
"growth_rate_and_grad",
|
|
43
|
+
"growth_rate_of",
|
|
44
|
+
"load",
|
|
45
|
+
"solve",
|
|
46
|
+
"__version__",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
try: # written by setuptools_scm at build or install time
|
|
50
|
+
from ._version import __version__
|
|
51
|
+
except ImportError: # a bare checkout on sys.path
|
|
52
|
+
__version__ = "0+unknown"
|
agnimhd/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Adapters from equilibrium codes to :class:`agnimhd.EquilibriumData`.
|
|
2
|
+
|
|
3
|
+
Each adapter imports its equilibrium code lazily, inside the function, so the
|
|
4
|
+
package itself stays free of those dependencies: ``agnimhd`` installs and runs
|
|
5
|
+
without DESC; ``from_desc`` only works when DESC is importable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .desc import from_desc
|
|
9
|
+
|
|
10
|
+
__all__ = ["from_desc"]
|
agnimhd/adapters/desc.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""DESC -> :class:`agnimhd.EquilibriumData`.
|
|
2
|
+
|
|
3
|
+
``from_desc`` takes a DESC ``Equilibrium`` or the path of a DESC ``.h5`` file,
|
|
4
|
+
evaluates the contract fields on the PEST nodes of a :class:`~agnimhd.Basis` and
|
|
5
|
+
returns the ``EquilibriumData`` together with the ``DiffMat`` on the same nodes.
|
|
6
|
+
DESC is imported inside the function only.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ..equilibrium import EquilibriumData
|
|
12
|
+
|
|
13
|
+
__all__ = ["desc_equilibrium", "from_desc", "is_desc_file"]
|
|
14
|
+
|
|
15
|
+
#: DESC compute key -> EquilibriumData field.
|
|
16
|
+
KEY_MAP = {
|
|
17
|
+
"g_rr|PEST": "g_rr",
|
|
18
|
+
"g_rv|PEST": "g_rv",
|
|
19
|
+
"g_rp|PEST": "g_rp",
|
|
20
|
+
"g_vv|PEST": "g_vv",
|
|
21
|
+
"g_vp|PEST": "g_vp",
|
|
22
|
+
"g_pp|PEST": "g_pp",
|
|
23
|
+
"g^rr": "g_sup_rr",
|
|
24
|
+
"sqrt(g)_PEST": "sqrtg",
|
|
25
|
+
"(sqrt(g)_PEST_r)|PEST": "sqrtg_r",
|
|
26
|
+
"(sqrt(g)_PEST_v)|PEST": "sqrtg_v",
|
|
27
|
+
"(sqrt(g)_PEST_p)|PEST": "sqrtg_p",
|
|
28
|
+
"J^zeta": "J_sup_zeta",
|
|
29
|
+
"|J|": "abs_J",
|
|
30
|
+
"iota": "iota",
|
|
31
|
+
"psi_r": "psi_r",
|
|
32
|
+
"psi_rr": "psi_rr",
|
|
33
|
+
"p": "p",
|
|
34
|
+
"p_r": "p_r",
|
|
35
|
+
"J x grad(rho)": "J_cross_grad_rho",
|
|
36
|
+
"(B*grad) grad(rho)": "B_dot_grad_grad_rho",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_desc_file(path):
|
|
41
|
+
"""True if ``path`` is an HDF5 file written by DESC (``desc.*`` class tag)."""
|
|
42
|
+
try:
|
|
43
|
+
import h5py
|
|
44
|
+
|
|
45
|
+
with h5py.File(path, "r") as f:
|
|
46
|
+
return (
|
|
47
|
+
str(f.attrs.get("__class__", f.get("__class__", [b""])[()])).find(
|
|
48
|
+
"desc."
|
|
49
|
+
)
|
|
50
|
+
>= 0
|
|
51
|
+
)
|
|
52
|
+
except Exception: # not HDF5, no h5py, or no class tag
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def desc_equilibrium(eq):
|
|
57
|
+
"""``eq`` itself, or the last equilibrium in the DESC ``.h5`` file ``eq``."""
|
|
58
|
+
if not isinstance(eq, str):
|
|
59
|
+
return eq
|
|
60
|
+
from desc.io import load
|
|
61
|
+
|
|
62
|
+
eq = load(eq)
|
|
63
|
+
return eq[-1] if isinstance(eq, (list, tuple)) or hasattr(eq, "__getitem__") else eq
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def from_desc(eq, basis, family=0, density=False, coarse=None):
|
|
67
|
+
"""Evaluate a DESC equilibrium on the PEST nodes of ``basis``.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
eq : desc.equilibrium.Equilibrium or str
|
|
72
|
+
A DESC ``Equilibrium``, or the path of a DESC ``.h5`` file (the last
|
|
73
|
+
equilibrium of a family is used).
|
|
74
|
+
basis : agnimhd.Basis
|
|
75
|
+
Nodes and derivative matrices; see ``docs/options.md``.
|
|
76
|
+
family : int
|
|
77
|
+
Toroidal mode family of the returned ``DiffMat``, ``n = family + k NFP``
|
|
78
|
+
(:meth:`~agnimhd.Basis.nodes_and_diffmat`). The geometry does not depend
|
|
79
|
+
on it: another family's ``DiffMat`` is
|
|
80
|
+
``basis.nodes_and_diffmat(eq_data.NFP, family=x)[1]``.
|
|
81
|
+
density : bool
|
|
82
|
+
Store DESC's ``ni`` on the nodes, normalized to its maximum (ones if the
|
|
83
|
+
equilibrium has no density profile), as ``eq_data.density``: the mass
|
|
84
|
+
weighting every solver then uses.
|
|
85
|
+
coarse : Basis, optional
|
|
86
|
+
``basis.coarse(n_theta, n_zeta)``: also return the coarse level of
|
|
87
|
+
``eigensolver="jd"``, the equilibrium on its nodes as
|
|
88
|
+
:meth:`~agnimhd.Basis.coarse_level` packs it for
|
|
89
|
+
``growth_rate(..., coarse=coarse)``.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
eq_data : EquilibriumData
|
|
94
|
+
diffmat : DiffMat
|
|
95
|
+
Differentiation matrices on exactly the nodes the geometry was
|
|
96
|
+
evaluated at. Use these two together.
|
|
97
|
+
coarse : tuple, only if ``coarse`` is given
|
|
98
|
+
"""
|
|
99
|
+
from desc.grid import Grid
|
|
100
|
+
|
|
101
|
+
eq = desc_equilibrium(eq)
|
|
102
|
+
n_rho, n_theta, n_zeta = basis.n_rho, basis.n_theta, basis.n_zeta
|
|
103
|
+
nodes, diffmat = basis.nodes_and_diffmat(eq.NFP, family)
|
|
104
|
+
rho, theta, zeta = (np.asarray(nodes[k]) for k in ("rho", "theta", "zeta"))
|
|
105
|
+
R, T, Z = np.meshgrid(rho, theta, zeta, indexing="ij") # rho-major
|
|
106
|
+
pest = np.stack([R.ravel(), T.ravel(), Z.ravel()], axis=-1)
|
|
107
|
+
rtz = eq.map_coordinates(
|
|
108
|
+
pest,
|
|
109
|
+
inbasis=("rho", "theta_PEST", "zeta"),
|
|
110
|
+
outbasis=("rho", "theta", "zeta"),
|
|
111
|
+
period=(np.inf, 2 * np.pi, np.inf),
|
|
112
|
+
tol=1e-12,
|
|
113
|
+
maxiter=50,
|
|
114
|
+
)
|
|
115
|
+
keys = list(KEY_MAP) + ["a"] + (["ni"] if density else [])
|
|
116
|
+
data = eq.compute(keys, grid=Grid(rtz))
|
|
117
|
+
n = n_rho * n_theta * n_zeta
|
|
118
|
+
fields = {
|
|
119
|
+
dst: np.asarray(data[src]).reshape(n, -1).squeeze()
|
|
120
|
+
for src, dst in KEY_MAP.items()
|
|
121
|
+
}
|
|
122
|
+
if density:
|
|
123
|
+
ni = np.asarray(data["ni"]).reshape(-1)
|
|
124
|
+
ok = np.isfinite(ni).any() and np.nanmax(ni) > 0
|
|
125
|
+
ni = np.nan_to_num(ni / np.nanmax(ni), nan=1.0) if ok else np.ones_like(ni)
|
|
126
|
+
fields["density"] = ni
|
|
127
|
+
eq_data = EquilibriumData(
|
|
128
|
+
n_rho=n_rho,
|
|
129
|
+
n_theta=n_theta,
|
|
130
|
+
n_zeta=n_zeta,
|
|
131
|
+
NFP=int(eq.NFP),
|
|
132
|
+
Psi=float(np.asarray(eq.Psi)),
|
|
133
|
+
a=float(np.asarray(data["a"]).reshape(-1)[0]),
|
|
134
|
+
**fields,
|
|
135
|
+
)
|
|
136
|
+
if coarse is None:
|
|
137
|
+
return eq_data, diffmat
|
|
138
|
+
eq_coarse = from_desc(eq, coarse, density=density)[0]
|
|
139
|
+
return eq_data, diffmat, basis.coarse_level(eq_coarse, family)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""``AgniStability``: agnimhd's growth rate as a DESC optimization objective.
|
|
2
|
+
|
|
3
|
+
Imports DESC at module import, so ``import agnimhd`` never loads this file; use
|
|
4
|
+
``from agnimhd.adapters.desc_objective import AgniStability``. Inside DESC's
|
|
5
|
+
optimizer (e.g. ``ProximalProjection``) the equilibrium is re-solved every step
|
|
6
|
+
and the chain rule through force balance gives d(gamma^2)/d(boundary, profiles).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import jax
|
|
10
|
+
import numpy as np
|
|
11
|
+
from desc.backend import jnp
|
|
12
|
+
from desc.compute.utils import _compute as compute_fun
|
|
13
|
+
from desc.compute.utils import get_profiles, get_transforms
|
|
14
|
+
from desc.grid import Grid, LinearGrid, QuadratureGrid
|
|
15
|
+
from desc.objectives.objective_funs import _Objective
|
|
16
|
+
from jax.experimental import io_callback
|
|
17
|
+
|
|
18
|
+
from ..assemble import keep_indices, operator_dtype
|
|
19
|
+
from ..backend import errorif
|
|
20
|
+
from ..config import AssemblyConfig, SolverConfig
|
|
21
|
+
from ..equilibrium import EquilibriumData
|
|
22
|
+
from ..objective import growth_rate_of
|
|
23
|
+
from .desc import KEY_MAP
|
|
24
|
+
|
|
25
|
+
__all__ = ["AgniStability"]
|
|
26
|
+
|
|
27
|
+
#: Flux functions: computed on a LinearGrid at the PEST rho values (it has the
|
|
28
|
+
#: quadrature weights the PEST grid lacks) and copied onto the PEST nodes.
|
|
29
|
+
FLUX_KEYS = [
|
|
30
|
+
"iota",
|
|
31
|
+
"iota_r",
|
|
32
|
+
"iota_den",
|
|
33
|
+
"iota_den_r",
|
|
34
|
+
"iota_num",
|
|
35
|
+
"iota_num_r",
|
|
36
|
+
"iota_num current",
|
|
37
|
+
"iota_num_r current",
|
|
38
|
+
"iota_num vacuum",
|
|
39
|
+
"iota_num_r vacuum",
|
|
40
|
+
"psi_r",
|
|
41
|
+
"psi_rr",
|
|
42
|
+
"p",
|
|
43
|
+
"p_r",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _WarmStart:
|
|
48
|
+
"""The last converged eigenvector, on the host, between DESC's calls.
|
|
49
|
+
|
|
50
|
+
``read`` and ``write`` are called from inside DESC's jitted compute through
|
|
51
|
+
``io_callback``: the read feeds the solve and the write needs its result,
|
|
52
|
+
so within a call they run before and after it, and between calls the
|
|
53
|
+
device finishes one program, callbacks included, before the next starts.
|
|
54
|
+
A vector that is not finite is not kept.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, n, dtype):
|
|
58
|
+
self.vector_spec = jax.ShapeDtypeStruct((n,), dtype)
|
|
59
|
+
self.v = np.zeros(n, dtype)
|
|
60
|
+
self.valid = False
|
|
61
|
+
self.gamma2 = 0.0
|
|
62
|
+
self.reads = self.hits = 0 # calls, and calls that started from a kept vector
|
|
63
|
+
|
|
64
|
+
def __deepcopy__(self, memo):
|
|
65
|
+
"""DESC's optimizers copy the objective; the copies share this cache, so
|
|
66
|
+
the original sees the solves made through them and a copy does not
|
|
67
|
+
recompile (the store is static to DESC's jit, by identity)."""
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def read(self):
|
|
71
|
+
"""``((vector, valid), gamma^2)`` as traced arrays."""
|
|
72
|
+
flag = jax.ShapeDtypeStruct((), bool)
|
|
73
|
+
g = jax.ShapeDtypeStruct((), np.float64)
|
|
74
|
+
v, valid, gamma2 = io_callback(self._read, (self.vector_spec, flag, g))
|
|
75
|
+
return (v, valid), gamma2
|
|
76
|
+
|
|
77
|
+
def _read(self):
|
|
78
|
+
self.reads += 1
|
|
79
|
+
self.hits += self.valid
|
|
80
|
+
return self.v, np.bool_(self.valid), np.float64(self.gamma2)
|
|
81
|
+
|
|
82
|
+
def write(self, v, gamma2):
|
|
83
|
+
"""Keep the converged eigenvector and its ``gamma^2`` for the next call."""
|
|
84
|
+
io_callback(self._write, None, v, gamma2)
|
|
85
|
+
|
|
86
|
+
def _write(self, v, gamma2):
|
|
87
|
+
v = np.asarray(v)
|
|
88
|
+
if np.isfinite(v).all() and np.isfinite(gamma2):
|
|
89
|
+
self.v, self.gamma2, self.valid = v, float(gamma2), True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AgniStability(_Objective):
|
|
93
|
+
"""Finite-n ideal MHD squared growth rate ``gamma^2 = -lambda`` from agnimhd.
|
|
94
|
+
|
|
95
|
+
Positive means unstable; the default target is 0. The PEST nodes are
|
|
96
|
+
mapped to DESC's ``theta`` at the current parameters on every call, so the
|
|
97
|
+
objective and its gradient follow the equilibrium as it moves. ``a`` is
|
|
98
|
+
computed on a ``QuadratureGrid``. With ``eigensolver="jd"`` the equilibrium
|
|
99
|
+
is also evaluated on the coarse level's nodes at every call, and that level
|
|
100
|
+
seeds and deflates the solve; no gradient flows through it. With
|
|
101
|
+
``density=True`` the kinetic energy is weighted by DESC's ``ni`` normalized
|
|
102
|
+
to its maximum, as ``from_desc(..., density=True)``, on both levels.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
_coordinates = ""
|
|
106
|
+
_units = "(dimensionless)"
|
|
107
|
+
_print_value_fmt = "finite-n gamma^2 (agnimhd): "
|
|
108
|
+
_static_attrs = _Objective._static_attrs + [
|
|
109
|
+
"_basis",
|
|
110
|
+
"_family",
|
|
111
|
+
"_assembly",
|
|
112
|
+
"_solver",
|
|
113
|
+
"_coarse_basis",
|
|
114
|
+
"_density",
|
|
115
|
+
"_warm_start",
|
|
116
|
+
"_sigma_factor",
|
|
117
|
+
"_sigma_floor",
|
|
118
|
+
"_warm", # the host-side store: static to DESC's jit, by identity
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
eq,
|
|
124
|
+
basis,
|
|
125
|
+
family=0,
|
|
126
|
+
assembly=None,
|
|
127
|
+
solver=None,
|
|
128
|
+
coarse=None,
|
|
129
|
+
density=False,
|
|
130
|
+
warm_start=False,
|
|
131
|
+
sigma_factor=None,
|
|
132
|
+
sigma_floor=1e-7,
|
|
133
|
+
target=None,
|
|
134
|
+
bounds=None,
|
|
135
|
+
weight=1.0,
|
|
136
|
+
name="agni finite-n",
|
|
137
|
+
):
|
|
138
|
+
"""Store the choices; nodes and DESC transforms are made in :meth:`build`.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
eq : desc.equilibrium.Equilibrium
|
|
143
|
+
basis : agnimhd.Basis
|
|
144
|
+
family : int
|
|
145
|
+
Toroidal mode family ``n = family + k NFP`` whose ``gamma^2`` is
|
|
146
|
+
returned (:meth:`agnimhd.Basis.families`); one objective per family.
|
|
147
|
+
assembly, solver : AssemblyConfig, SolverConfig, optional
|
|
148
|
+
coarse : agnimhd.Basis, optional
|
|
149
|
+
``eigensolver="jd"`` only: the coarse level,
|
|
150
|
+
``basis.coarse(n_theta, n_zeta)``; default ``basis.coarse()``.
|
|
151
|
+
density : bool
|
|
152
|
+
Weight the kinetic energy with ``ni / max(ni)``; needs
|
|
153
|
+
``eq.electron_density``.
|
|
154
|
+
warm_start : bool
|
|
155
|
+
``eigensolver="jd"`` only: start each solve from the previous
|
|
156
|
+
call's converged eigenvector, kept on the host. The solve still
|
|
157
|
+
runs to ``jd_tol`` at every call, so the value and the derivative
|
|
158
|
+
are those of a converged vector; at the same point (DESC's
|
|
159
|
+
Jacobian after its value) the start is already converged and JD
|
|
160
|
+
stops within a round or two. The first call starts from the coarse
|
|
161
|
+
level as usual.
|
|
162
|
+
sigma_factor : float, optional
|
|
163
|
+
Needs ``warm_start``. Adapt the shift of the JD solve: after the
|
|
164
|
+
first call, ``sigma = sigma_factor * gamma^2`` of the previous call
|
|
165
|
+
(at least ``sigma_floor``) instead of the fixed ``solver.sigma``. JD
|
|
166
|
+
needs fewer rounds the closer the shift is to ``gamma^2``: measured at
|
|
167
|
+
``gamma^2 = 3.8e-6``, 894 rounds at ``sigma = 1e-3``, 295 at 3e-5.
|
|
168
|
+
The shift must stay above the largest ``gamma^2``; a solve whose
|
|
169
|
+
``gamma^2`` ends within a factor 1.3 of its shift (the optimizer's
|
|
170
|
+
trial points can raise ``gamma^2`` by a factor 3 or more) is redone at
|
|
171
|
+
``solver.sigma``. ``2`` is a good factor.
|
|
172
|
+
sigma_floor : float
|
|
173
|
+
Lower bound of the adapted shift.
|
|
174
|
+
target, bounds, weight, name
|
|
175
|
+
As for every DESC objective; the default target is 0.
|
|
176
|
+
"""
|
|
177
|
+
if target is None and bounds is None:
|
|
178
|
+
target = 0.0
|
|
179
|
+
self._basis = basis
|
|
180
|
+
self._family = int(family)
|
|
181
|
+
self._assembly = assembly or AssemblyConfig()
|
|
182
|
+
self._solver = solver or SolverConfig()
|
|
183
|
+
self._density = bool(density)
|
|
184
|
+
errorif(
|
|
185
|
+
self._density and eq.electron_density is None,
|
|
186
|
+
ValueError,
|
|
187
|
+
"density=True needs a density profile, eq.electron_density.",
|
|
188
|
+
)
|
|
189
|
+
self._warm_start = bool(warm_start)
|
|
190
|
+
errorif(
|
|
191
|
+
self._warm_start and self._solver.eigensolver != "jd",
|
|
192
|
+
ValueError,
|
|
193
|
+
'warm_start=True needs eigensolver="jd" (the dense solvers factor '
|
|
194
|
+
"the matrix and gain nothing from a start vector).",
|
|
195
|
+
)
|
|
196
|
+
errorif(
|
|
197
|
+
sigma_factor is not None and not self._warm_start,
|
|
198
|
+
ValueError,
|
|
199
|
+
"sigma_factor needs warm_start=True (it uses the kept gamma^2)",
|
|
200
|
+
)
|
|
201
|
+
errorif(
|
|
202
|
+
sigma_factor is not None and sigma_factor <= 1,
|
|
203
|
+
ValueError,
|
|
204
|
+
"sigma_factor must exceed 1: the shift lies above gamma^2",
|
|
205
|
+
)
|
|
206
|
+
self._sigma_factor = sigma_factor
|
|
207
|
+
self._sigma_floor = float(sigma_floor)
|
|
208
|
+
self._warm = None
|
|
209
|
+
self._coarse_basis = None
|
|
210
|
+
if self._solver.eigensolver == "jd":
|
|
211
|
+
coarse = basis.coarse() if coarse is None else coarse
|
|
212
|
+
errorif(
|
|
213
|
+
coarse != basis.coarse(coarse.n_theta, coarse.n_zeta),
|
|
214
|
+
ValueError,
|
|
215
|
+
"coarse must be basis.coarse(n_theta, n_zeta): the same radial "
|
|
216
|
+
"nodes, map, mpol and ntor on fewer angular nodes.",
|
|
217
|
+
)
|
|
218
|
+
self._coarse_basis = coarse
|
|
219
|
+
super().__init__(
|
|
220
|
+
things=eq,
|
|
221
|
+
target=target,
|
|
222
|
+
bounds=bounds,
|
|
223
|
+
weight=weight,
|
|
224
|
+
normalize=False,
|
|
225
|
+
normalize_target=False,
|
|
226
|
+
name=name,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def build(self, use_jit=True, verbose=1):
|
|
230
|
+
"""Fixed PEST nodes, DiffMat and DESC transforms (and the coarse level's)."""
|
|
231
|
+
eq = self.things[0]
|
|
232
|
+
rho, diffmat, nodes = self._pest_nodes(self._basis)
|
|
233
|
+
flux_grid = LinearGrid(rho=rho, M=eq.M_grid, N=eq.N_grid, NFP=eq.NFP)
|
|
234
|
+
quad = QuadratureGrid(eq.L_grid, eq.M_grid, eq.N_grid, eq.NFP)
|
|
235
|
+
self._dim_f = 1
|
|
236
|
+
self._constants = {
|
|
237
|
+
"diffmat": diffmat,
|
|
238
|
+
"nodes": nodes,
|
|
239
|
+
"flux_grid": flux_grid,
|
|
240
|
+
"flux_transforms": get_transforms(FLUX_KEYS, obj=eq, grid=flux_grid),
|
|
241
|
+
"flux_profiles": get_profiles(FLUX_KEYS, obj=eq, grid=flux_grid),
|
|
242
|
+
"a_transforms": get_transforms(["a"], obj=eq, grid=quad),
|
|
243
|
+
"a_profiles": get_profiles(["a"], obj=eq, grid=quad),
|
|
244
|
+
"quad_weights": 1.0, # scalar objective: no grid weights
|
|
245
|
+
}
|
|
246
|
+
if self._coarse_basis is not None: # same radial nodes: same flux grid
|
|
247
|
+
c = self._constants
|
|
248
|
+
c["coarse_nodes"] = self._pest_nodes(self._coarse_basis)[2]
|
|
249
|
+
eq_c = self._equilibrium_data(eq.params_dict, c, coarse=True)
|
|
250
|
+
_, c["coarse_diffmat"], c["coarse_transfer"] = self._basis.coarse_level(
|
|
251
|
+
eq_c, self._family
|
|
252
|
+
)
|
|
253
|
+
if self._warm_start and self._warm is None: # a rebuilt copy keeps the store
|
|
254
|
+
b = self._basis
|
|
255
|
+
n = int(keep_indices(b.n_rho, b.n_theta, b.n_zeta).size)
|
|
256
|
+
self._warm = _WarmStart(n, operator_dtype(self._assembly, diffmat))
|
|
257
|
+
super().build(use_jit=use_jit, verbose=verbose)
|
|
258
|
+
|
|
259
|
+
def _pest_nodes(self, basis):
|
|
260
|
+
"""``(rho, diffmat, nodes)``: ``basis``' PEST nodes, rho-major, with
|
|
261
|
+
DESC's unique-rho index maps."""
|
|
262
|
+
eq = self.things[0]
|
|
263
|
+
nodes, diffmat = basis.nodes_and_diffmat(eq.NFP, self._family)
|
|
264
|
+
rho, theta, zeta = (np.asarray(nodes[k]) for k in ("rho", "theta", "zeta"))
|
|
265
|
+
R, T, Z = np.meshgrid(rho, theta, zeta, indexing="ij")
|
|
266
|
+
pest = np.stack([R.ravel(), T.ravel(), Z.ravel()], axis=-1)
|
|
267
|
+
_, u_idx, i_idx = np.unique(pest[:, 0], return_index=True, return_inverse=True)
|
|
268
|
+
nodes = {"pest": pest, "u_idx": u_idx, "i_idx": i_idx}
|
|
269
|
+
return rho, diffmat, {k: jnp.asarray(v) for k, v in nodes.items()}
|
|
270
|
+
|
|
271
|
+
def compute(self, params, constants=None):
|
|
272
|
+
"""``gamma^2`` at ``params``, differentiable with respect to them."""
|
|
273
|
+
c = constants or self._constants
|
|
274
|
+
coarse = None
|
|
275
|
+
if self._coarse_basis is not None:
|
|
276
|
+
eq_c = self._equilibrium_data(params, c, coarse=True)
|
|
277
|
+
coarse = (eq_c, c["coarse_diffmat"], c["coarse_transfer"])
|
|
278
|
+
warm, guess, sigma = self._warm, None, None
|
|
279
|
+
if warm is not None:
|
|
280
|
+
guess, last = warm.read()
|
|
281
|
+
if self._sigma_factor is not None: # the first call: the fixed sigma
|
|
282
|
+
kept = guess[1] & (last > 0)
|
|
283
|
+
adapted = jnp.maximum(self._sigma_factor * last, self._sigma_floor)
|
|
284
|
+
sigma = jnp.where(kept, adapted, self._solver.sigma)
|
|
285
|
+
gamma2 = growth_rate_of(
|
|
286
|
+
params,
|
|
287
|
+
lambda p: self._equilibrium_data(p, c),
|
|
288
|
+
c["diffmat"],
|
|
289
|
+
self._assembly,
|
|
290
|
+
self._solver,
|
|
291
|
+
v_guess=guess,
|
|
292
|
+
coarse=coarse,
|
|
293
|
+
on_vector=None if warm is None else warm.write,
|
|
294
|
+
sigma=sigma,
|
|
295
|
+
)
|
|
296
|
+
return jnp.atleast_1d(gamma2)
|
|
297
|
+
|
|
298
|
+
def _equilibrium_data(self, params, c, coarse=False):
|
|
299
|
+
"""``params -> EquilibriumData`` through DESC's compute functions, on
|
|
300
|
+
the PEST nodes of the basis or, with ``coarse``, of the coarse level."""
|
|
301
|
+
eq = self.things[0]
|
|
302
|
+
nodes = c["coarse_nodes"] if coarse else c["nodes"]
|
|
303
|
+
rtz = eq.map_coordinates(
|
|
304
|
+
nodes["pest"],
|
|
305
|
+
inbasis=("rho", "theta_PEST", "zeta"),
|
|
306
|
+
outbasis=("rho", "theta", "zeta"),
|
|
307
|
+
period=(jnp.inf, 2 * jnp.pi, jnp.inf),
|
|
308
|
+
tol=1e-12,
|
|
309
|
+
maxiter=50,
|
|
310
|
+
params=params,
|
|
311
|
+
)
|
|
312
|
+
grid = Grid(
|
|
313
|
+
rtz,
|
|
314
|
+
coordinates="rtz",
|
|
315
|
+
sort=False,
|
|
316
|
+
jitable=True,
|
|
317
|
+
_unique_rho_idx=nodes["u_idx"],
|
|
318
|
+
_inverse_rho_idx=nodes["i_idx"],
|
|
319
|
+
)
|
|
320
|
+
a = compute_fun(
|
|
321
|
+
eq,
|
|
322
|
+
["a"],
|
|
323
|
+
params=params,
|
|
324
|
+
transforms=c["a_transforms"],
|
|
325
|
+
profiles=c["a_profiles"],
|
|
326
|
+
)["a"]
|
|
327
|
+
flux = compute_fun(
|
|
328
|
+
eq,
|
|
329
|
+
FLUX_KEYS,
|
|
330
|
+
params=params,
|
|
331
|
+
transforms=c["flux_transforms"],
|
|
332
|
+
profiles=c["flux_profiles"],
|
|
333
|
+
data={"a": a},
|
|
334
|
+
)
|
|
335
|
+
data = {"a": a}
|
|
336
|
+
for k in FLUX_KEYS:
|
|
337
|
+
data[k] = grid.copy_data_from_other(
|
|
338
|
+
flux[k], c["flux_grid"], surface_label="rho"
|
|
339
|
+
)
|
|
340
|
+
keys = list(KEY_MAP) + ["ni"] * self._density
|
|
341
|
+
data = eq.compute(
|
|
342
|
+
keys, grid=grid, params=params, data=data, override_grid=False
|
|
343
|
+
)
|
|
344
|
+
basis = self._coarse_basis if coarse else self._basis
|
|
345
|
+
return EquilibriumData(
|
|
346
|
+
n_rho=basis.n_rho,
|
|
347
|
+
n_theta=basis.n_theta,
|
|
348
|
+
n_zeta=basis.n_zeta,
|
|
349
|
+
NFP=eq.NFP,
|
|
350
|
+
Psi=params["Psi"],
|
|
351
|
+
a=a,
|
|
352
|
+
density=data["ni"] / jnp.max(data["ni"]) if self._density else None,
|
|
353
|
+
validate=False,
|
|
354
|
+
**{v: data[k] for k, v in KEY_MAP.items()},
|
|
355
|
+
)
|