drbx 2.0.0.dev0__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.
- drbx/__init__.py +20 -0
- drbx/__main__.py +5 -0
- drbx/cli.py +586 -0
- drbx/config/__init__.py +20 -0
- drbx/config/boutinp.py +556 -0
- drbx/config/model.py +34 -0
- drbx/config/normalization.py +66 -0
- drbx/data/atomic_rates/__init__.py +1 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
- drbx/geometry/__init__.py +207 -0
- drbx/geometry/embedding.py +56 -0
- drbx/geometry/essos_import.py +1385 -0
- drbx/geometry/fci_geometry.py +4622 -0
- drbx/geometry/fci_maps.py +85 -0
- drbx/geometry/island_divertor.py +291 -0
- drbx/geometry/metric_tensor.py +99 -0
- drbx/geometry/open_slab.py +150 -0
- drbx/geometry/rotating_ellipse.py +253 -0
- drbx/geometry/shifted_torus.py +225 -0
- drbx/geometry/stellarator.py +287 -0
- drbx/geometry/vmec_extender_import.py +499 -0
- drbx/geometry/vmec_jax_import.py +306 -0
- drbx/linear/__init__.py +37 -0
- drbx/linear/dispersion.py +138 -0
- drbx/linear/eigen.py +91 -0
- drbx/native/__init__.py +224 -0
- drbx/native/array_backend.py +64 -0
- drbx/native/deck_runner.py +779 -0
- drbx/native/electromagnetic.py +250 -0
- drbx/native/expression.py +173 -0
- drbx/native/fci.py +295 -0
- drbx/native/fci_2_field_rhs.py +182 -0
- drbx/native/fci_4_field_rhs.py +1267 -0
- drbx/native/fci_boundaries.py +2494 -0
- drbx/native/fci_differentiable_case.py +304 -0
- drbx/native/fci_drb_EB_rhs.py +1243 -0
- drbx/native/fci_drb_rhs.py +190 -0
- drbx/native/fci_halo.py +1575 -0
- drbx/native/fci_helpers.py +350 -0
- drbx/native/fci_model.py +294 -0
- drbx/native/fci_neutral.py +139 -0
- drbx/native/fci_operators.py +4081 -0
- drbx/native/fci_sharding.py +597 -0
- drbx/native/fci_sheath_recycling.py +206 -0
- drbx/native/fci_time_integrator.py +96 -0
- drbx/native/fci_vorticity.py +198 -0
- drbx/native/fluid_1d.py +330 -0
- drbx/native/hasegawa_wakatani.py +196 -0
- drbx/native/limiters.py +57 -0
- drbx/native/mesh.py +238 -0
- drbx/native/metrics.py +234 -0
- drbx/native/neutrals/__init__.py +58 -0
- drbx/native/neutrals/atomic_rates.py +134 -0
- drbx/native/neutrals/detachment_sol_model.py +221 -0
- drbx/native/neutrals/reactions.py +164 -0
- drbx/native/neutrals/recycling_sol_model.py +197 -0
- drbx/native/sol_flux_tube.py +133 -0
- drbx/native/stellarator_turbulence.py +343 -0
- drbx/native/transport.py +134 -0
- drbx/native/units.py +32 -0
- drbx/native/vorticity.py +252 -0
- drbx/runtime/__init__.py +53 -0
- drbx/runtime/artifacts.py +161 -0
- drbx/runtime/memory.py +144 -0
- drbx/runtime/output.py +374 -0
- drbx/runtime/paths.py +9 -0
- drbx/runtime/performance.py +161 -0
- drbx/runtime/run_config.py +184 -0
- drbx/runtime/scheduler.py +99 -0
- drbx/runtime/state.py +40 -0
- drbx/validation/__init__.py +424 -0
- drbx/validation/autodiff_diffusion.py +329 -0
- drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
- drbx/validation/diverted_tokamak_movie.py +558 -0
- drbx/validation/essos_fieldline_import_campaign.py +181 -0
- drbx/validation/essos_imported_artifact_audit.py +535 -0
- drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
- drbx/validation/essos_imported_fci_campaign.py +5241 -0
- drbx/validation/essos_imported_pytree_campaign.py +406 -0
- drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
- drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
- drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
- drbx/validation/fluid_1d_mms_convergence.py +250 -0
- drbx/validation/geometry_lineouts.py +136 -0
- drbx/validation/geometry_slices.py +178 -0
- drbx/validation/publication_plotting.py +91 -0
- drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
- drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
- drbx/validation/stellarator_fci_operator_campaign.py +304 -0
- drbx/validation/stellarator_fci_suite_campaign.py +264 -0
- drbx/validation/stellarator_metric_mms_campaign.py +289 -0
- drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
- drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
- drbx/validation/stellarator_sol_showcase.py +628 -0
- drbx/validation/stellarator_vorticity_campaign.py +304 -0
- drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
- drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
- drbx-2.0.0.dev0.dist-info/METADATA +380 -0
- drbx-2.0.0.dev0.dist-info/RECORD +106 -0
- drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
- drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
drbx/native/mesh.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import jax.numpy as jnp
|
|
6
|
+
|
|
7
|
+
from ..config.boutinp import BoutConfig
|
|
8
|
+
from ..runtime import runtime_jax_dtype
|
|
9
|
+
from ..runtime.run_config import RunConfiguration
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class StructuredMesh:
|
|
14
|
+
nx: int
|
|
15
|
+
ny: int
|
|
16
|
+
nz: int
|
|
17
|
+
mxg: int
|
|
18
|
+
myg: int
|
|
19
|
+
symmetric_global_x: bool
|
|
20
|
+
symmetric_global_y: bool
|
|
21
|
+
jyseps1_1: int
|
|
22
|
+
jyseps2_1: int
|
|
23
|
+
jyseps1_2: int
|
|
24
|
+
jyseps2_2: int
|
|
25
|
+
ny_inner: int
|
|
26
|
+
has_lower_y_target: bool
|
|
27
|
+
has_upper_y_target: bool
|
|
28
|
+
x: jnp.ndarray
|
|
29
|
+
y: jnp.ndarray
|
|
30
|
+
z: jnp.ndarray
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def xstart(self) -> int:
|
|
34
|
+
return self.mxg
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def xend(self) -> int:
|
|
38
|
+
return self.nx - self.mxg - 1
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def ystart(self) -> int:
|
|
42
|
+
return self.myg
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def yend(self) -> int:
|
|
46
|
+
return self.myg + self.ny - 1
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def local_ny(self) -> int:
|
|
50
|
+
return self.ny + 2 * self.myg
|
|
51
|
+
|
|
52
|
+
def expression_context(self, *, time: float = 0.0) -> dict[str, jnp.ndarray]:
|
|
53
|
+
dtype = runtime_jax_dtype()
|
|
54
|
+
x3 = self.x[:, None, None]
|
|
55
|
+
y3 = self.y[None, :, None]
|
|
56
|
+
z3 = self.z[None, None, :]
|
|
57
|
+
return {
|
|
58
|
+
"x": x3,
|
|
59
|
+
"y": 2.0 * jnp.pi * y3,
|
|
60
|
+
"z": 2.0 * jnp.pi * z3,
|
|
61
|
+
"t": jnp.array(time, dtype=dtype),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_structured_mesh(config: BoutConfig, run_config: RunConfiguration) -> StructuredMesh:
|
|
66
|
+
if run_config.mesh.nx is None or run_config.mesh.ny is None or run_config.mesh.nz is None:
|
|
67
|
+
raise ValueError("Structured native execution requires explicit mesh nx, ny, and nz.")
|
|
68
|
+
|
|
69
|
+
nx = run_config.mesh.nx
|
|
70
|
+
ny = run_config.mesh.ny
|
|
71
|
+
nz = run_config.mesh.nz
|
|
72
|
+
mxg = run_config.mesh.mxg
|
|
73
|
+
myg = run_config.mesh.myg
|
|
74
|
+
symmetric_global_x = _mesh_bool(config, "symmetricGlobalX", default=True)
|
|
75
|
+
symmetric_global_y = _mesh_bool(config, "symmetricGlobalY", default=True)
|
|
76
|
+
jyseps1_1 = _mesh_int(config, "jyseps1_1", default=-1)
|
|
77
|
+
jyseps2_1 = _mesh_int(config, "jyseps2_1", default=ny // 2)
|
|
78
|
+
jyseps1_2 = _mesh_int(config, "jyseps1_2", default=ny // 2)
|
|
79
|
+
jyseps2_2 = _mesh_int(config, "jyseps2_2", default=ny - 1)
|
|
80
|
+
ny_inner = _mesh_int(config, "ny_inner", default=jyseps2_1)
|
|
81
|
+
|
|
82
|
+
x = _global_x_coordinates(nx=nx, mxg=mxg, symmetric=symmetric_global_x)
|
|
83
|
+
y = _global_y_coordinates(
|
|
84
|
+
ny=ny,
|
|
85
|
+
myg=myg,
|
|
86
|
+
symmetric=symmetric_global_y,
|
|
87
|
+
jyseps1_1=jyseps1_1,
|
|
88
|
+
jyseps2_1=jyseps2_1,
|
|
89
|
+
jyseps1_2=jyseps1_2,
|
|
90
|
+
jyseps2_2=jyseps2_2,
|
|
91
|
+
ny_inner=ny_inner,
|
|
92
|
+
)
|
|
93
|
+
dtype = runtime_jax_dtype()
|
|
94
|
+
z = jnp.arange(nz, dtype=dtype) / float(nz)
|
|
95
|
+
has_explicit_y_boundaries = config.has_option("mesh", "ixseps1") or config.has_option("mesh", "ixseps2")
|
|
96
|
+
has_lower_y_target = False
|
|
97
|
+
has_upper_y_target = bool(has_explicit_y_boundaries)
|
|
98
|
+
return StructuredMesh(
|
|
99
|
+
nx=nx,
|
|
100
|
+
ny=ny,
|
|
101
|
+
nz=nz,
|
|
102
|
+
mxg=mxg,
|
|
103
|
+
myg=myg,
|
|
104
|
+
symmetric_global_x=symmetric_global_x,
|
|
105
|
+
symmetric_global_y=symmetric_global_y,
|
|
106
|
+
jyseps1_1=jyseps1_1,
|
|
107
|
+
jyseps2_1=jyseps2_1,
|
|
108
|
+
jyseps1_2=jyseps1_2,
|
|
109
|
+
jyseps2_2=jyseps2_2,
|
|
110
|
+
ny_inner=ny_inner,
|
|
111
|
+
has_lower_y_target=has_lower_y_target,
|
|
112
|
+
has_upper_y_target=has_upper_y_target,
|
|
113
|
+
x=x,
|
|
114
|
+
y=y,
|
|
115
|
+
z=z,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def broadcast_to_field_shape(value: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
120
|
+
dtype = runtime_jax_dtype()
|
|
121
|
+
array = jnp.asarray(value, dtype=dtype)
|
|
122
|
+
target_shape = (mesh.nx, mesh.local_ny, mesh.nz)
|
|
123
|
+
if array.shape == target_shape:
|
|
124
|
+
return array
|
|
125
|
+
return jnp.broadcast_to(array, target_shape)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def apply_zero_dirichlet_x_guards(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
129
|
+
dtype = runtime_jax_dtype()
|
|
130
|
+
result = jnp.asarray(field, dtype=dtype)
|
|
131
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
132
|
+
if mesh.mxg <= 0:
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
result = result.at[mesh.xstart - 1, y_slice, :].set(-result[mesh.xstart, y_slice, :])
|
|
136
|
+
result = result.at[mesh.xend + 1, y_slice, :].set(-result[mesh.xend, y_slice, :])
|
|
137
|
+
for offset in range(2, mesh.mxg + 1):
|
|
138
|
+
result = result.at[mesh.xstart - offset, y_slice, :].set(0.0)
|
|
139
|
+
result = result.at[mesh.xend + offset, y_slice, :].set(0.0)
|
|
140
|
+
return result
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def apply_neumann_x_guards(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
144
|
+
dtype = runtime_jax_dtype()
|
|
145
|
+
result = jnp.asarray(field, dtype=dtype)
|
|
146
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
147
|
+
if mesh.mxg <= 0:
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
for offset in range(1, mesh.mxg + 1):
|
|
151
|
+
result = result.at[mesh.xstart - offset, y_slice, :].set(result[mesh.xstart - 1 + offset, y_slice, :])
|
|
152
|
+
result = result.at[mesh.xend + offset, y_slice, :].set(result[mesh.xend + 1 - offset, y_slice, :])
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def communicate_y_guards(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
157
|
+
dtype = runtime_jax_dtype()
|
|
158
|
+
result = jnp.asarray(field, dtype=dtype)
|
|
159
|
+
for offset in range(mesh.myg):
|
|
160
|
+
result = result.at[:, mesh.ystart - 1 - offset, :].set(result[:, mesh.ystart + offset, :])
|
|
161
|
+
result = result.at[:, mesh.yend + 1 + offset, :].set(result[:, mesh.yend - offset, :])
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def project_nonnegative_x_boundaries(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
166
|
+
dtype = runtime_jax_dtype()
|
|
167
|
+
result = jnp.asarray(field, dtype=dtype)
|
|
168
|
+
y_slice = slice(mesh.ystart, mesh.yend + 1)
|
|
169
|
+
if mesh.mxg > 0:
|
|
170
|
+
left = result[:mesh.xstart, y_slice, :]
|
|
171
|
+
right = result[mesh.xend + 1 :, y_slice, :]
|
|
172
|
+
result = result.at[:mesh.xstart, y_slice, :].set(jnp.maximum(left, 0.0))
|
|
173
|
+
result = result.at[mesh.xend + 1 :, y_slice, :].set(jnp.maximum(right, 0.0))
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def apply_x_boundary(field: jnp.ndarray, mesh: StructuredMesh, boundary_kind: str) -> jnp.ndarray:
|
|
178
|
+
normalized = boundary_kind.strip().lower()
|
|
179
|
+
if normalized == "neumann":
|
|
180
|
+
return apply_neumann_x_guards(field, mesh)
|
|
181
|
+
if normalized in {"dirichlet", "dirichlet_zero", "zero"}:
|
|
182
|
+
return apply_zero_dirichlet_x_guards(field, mesh)
|
|
183
|
+
raise NotImplementedError(f"Unsupported X boundary kind {boundary_kind!r}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def apply_field_boundaries(field: jnp.ndarray, mesh: StructuredMesh, *, x_boundary: str) -> jnp.ndarray:
|
|
187
|
+
result = apply_x_boundary(field, mesh, x_boundary)
|
|
188
|
+
result = communicate_y_guards(result, mesh)
|
|
189
|
+
if x_boundary.strip().lower() in {"dirichlet", "dirichlet_zero", "zero"}:
|
|
190
|
+
result = project_nonnegative_x_boundaries(result, mesh)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _global_x_coordinates(*, nx: int, mxg: int, symmetric: bool) -> jnp.ndarray:
|
|
195
|
+
dtype = runtime_jax_dtype()
|
|
196
|
+
mx = nx - 2 * mxg
|
|
197
|
+
global_indices = jnp.arange(nx, dtype=dtype)
|
|
198
|
+
if symmetric:
|
|
199
|
+
return (0.5 + global_indices - (nx - mx) * 0.5) / float(mx)
|
|
200
|
+
return global_indices / float(mx)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _global_y_coordinates(
|
|
204
|
+
*,
|
|
205
|
+
ny: int,
|
|
206
|
+
myg: int,
|
|
207
|
+
symmetric: bool,
|
|
208
|
+
jyseps1_1: int,
|
|
209
|
+
jyseps2_1: int,
|
|
210
|
+
jyseps1_2: int,
|
|
211
|
+
jyseps2_2: int,
|
|
212
|
+
ny_inner: int,
|
|
213
|
+
) -> jnp.ndarray:
|
|
214
|
+
dtype = runtime_jax_dtype()
|
|
215
|
+
local_indices = jnp.arange(ny + 2 * myg, dtype=dtype) - float(myg)
|
|
216
|
+
nycore = float((jyseps2_1 - jyseps1_1) + (jyseps2_2 - jyseps1_2))
|
|
217
|
+
if symmetric:
|
|
218
|
+
before = (local_indices - (jyseps1_1 + 0.5)) / nycore
|
|
219
|
+
after = (local_indices - (jyseps1_1 + 0.5 + (jyseps1_2 - jyseps2_1))) / nycore
|
|
220
|
+
return jnp.where(local_indices < float(ny_inner), before, after)
|
|
221
|
+
|
|
222
|
+
core_indices = local_indices
|
|
223
|
+
lower = core_indices - (jyseps1_1 + 1.0)
|
|
224
|
+
upper = core_indices - (jyseps1_1 + 1.0 + (jyseps1_2 - jyseps2_1))
|
|
225
|
+
return jnp.where(core_indices <= float(jyseps2_1), lower / nycore, upper / nycore)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _mesh_bool(config: BoutConfig, key: str, *, default: bool) -> bool:
|
|
229
|
+
if not config.has_option("mesh", key):
|
|
230
|
+
return default
|
|
231
|
+
return bool(config.parsed("mesh", key))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _mesh_int(config: BoutConfig, key: str, *, default: int) -> int:
|
|
235
|
+
if not config.has_option("mesh", key):
|
|
236
|
+
return default
|
|
237
|
+
value = config.parsed("mesh", key)
|
|
238
|
+
return int(round(float(value)))
|
drbx/native/metrics.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
|
|
9
|
+
from ..config.boutinp import BoutConfig
|
|
10
|
+
from ..config.model import has_model_section, locate_model_section
|
|
11
|
+
from ..runtime import runtime_jax_dtype
|
|
12
|
+
from ..runtime.run_config import RunConfiguration
|
|
13
|
+
from .expression import ArrayExpressionEvaluator
|
|
14
|
+
from .mesh import StructuredMesh, broadcast_to_field_shape
|
|
15
|
+
from .units import resolved_dataset_scalars
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class StructuredMetrics:
|
|
20
|
+
dx: jnp.ndarray
|
|
21
|
+
dy: jnp.ndarray
|
|
22
|
+
dz: jnp.ndarray
|
|
23
|
+
J: jnp.ndarray
|
|
24
|
+
g11: jnp.ndarray
|
|
25
|
+
g33: jnp.ndarray
|
|
26
|
+
g22: jnp.ndarray
|
|
27
|
+
g_22: jnp.ndarray
|
|
28
|
+
g23: jnp.ndarray
|
|
29
|
+
Bxy: jnp.ndarray
|
|
30
|
+
g_23: jnp.ndarray | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_structured_metrics(
|
|
34
|
+
config: BoutConfig,
|
|
35
|
+
run_config: RunConfiguration,
|
|
36
|
+
mesh: StructuredMesh,
|
|
37
|
+
) -> StructuredMetrics:
|
|
38
|
+
dtype = runtime_jax_dtype()
|
|
39
|
+
evaluator = ArrayExpressionEvaluator(config, local_values=mesh.expression_context())
|
|
40
|
+
scalars = resolved_dataset_scalars(run_config)
|
|
41
|
+
rho_s0 = scalars["rho_s0"]
|
|
42
|
+
Bnorm = scalars["Bnorm"]
|
|
43
|
+
normalize_metric = _normalize_metric_enabled(config)
|
|
44
|
+
|
|
45
|
+
raw_dx = _metric_value(config, evaluator, "dx", default=1.0)
|
|
46
|
+
raw_dy = _metric_value(config, evaluator, "dy", default=1.0)
|
|
47
|
+
raw_dz = _metric_value(config, evaluator, "dz", default=(2.0 * math.pi) / float(mesh.nz))
|
|
48
|
+
raw_J = _metric_value(config, evaluator, "J", default=1.0)
|
|
49
|
+
raw_g11 = _metric_value(config, evaluator, "g11", default=1.0)
|
|
50
|
+
raw_g33 = _metric_value(config, evaluator, "g33", default=1.0)
|
|
51
|
+
raw_g22 = _metric_value(config, evaluator, "g22", default=1.0)
|
|
52
|
+
if config.has_section("mesh") and config.has_option("mesh", "g_22"):
|
|
53
|
+
raw_g_22 = _metric_value(config, evaluator, "g_22", default=1.0)
|
|
54
|
+
else:
|
|
55
|
+
raw_g_22 = 1.0 / jnp.asarray(raw_g22, dtype=dtype)
|
|
56
|
+
raw_g23 = _metric_value(config, evaluator, "g23", default=0.0)
|
|
57
|
+
raw_g_23 = _metric_value(config, evaluator, "g_23", default=0.0)
|
|
58
|
+
raw_Bxy = _metric_value(config, evaluator, "Bxy", default=1.0)
|
|
59
|
+
|
|
60
|
+
if _recalculate_metric_enabled(run_config):
|
|
61
|
+
recalculated = _recalculate_orthogonal_metrics(
|
|
62
|
+
config,
|
|
63
|
+
evaluator,
|
|
64
|
+
mesh,
|
|
65
|
+
rho_s0=rho_s0,
|
|
66
|
+
Bnorm=Bnorm,
|
|
67
|
+
raw_dx=raw_dx,
|
|
68
|
+
raw_dy=raw_dy,
|
|
69
|
+
raw_dz=raw_dz,
|
|
70
|
+
raw_Bxy=raw_Bxy,
|
|
71
|
+
raw_g_23=raw_g_23,
|
|
72
|
+
dtype=dtype,
|
|
73
|
+
)
|
|
74
|
+
raw_dx = recalculated.dx
|
|
75
|
+
raw_dy = recalculated.dy
|
|
76
|
+
raw_dz = recalculated.dz
|
|
77
|
+
raw_J = recalculated.J
|
|
78
|
+
raw_g11 = recalculated.g11
|
|
79
|
+
raw_g33 = recalculated.g33
|
|
80
|
+
raw_g22 = recalculated.g22
|
|
81
|
+
raw_g_22 = recalculated.g_22
|
|
82
|
+
raw_g23 = recalculated.g23
|
|
83
|
+
raw_g_23 = recalculated.g_23
|
|
84
|
+
raw_Bxy = recalculated.Bxy
|
|
85
|
+
|
|
86
|
+
normalized_g22 = _normalize_g22(
|
|
87
|
+
broadcast_to_field_shape(raw_g22, mesh),
|
|
88
|
+
normalize_metric=normalize_metric,
|
|
89
|
+
rho_s0=rho_s0,
|
|
90
|
+
)
|
|
91
|
+
normalized_g_22 = _normalize_g_22(
|
|
92
|
+
broadcast_to_field_shape(raw_g_22, mesh),
|
|
93
|
+
normalize_metric=normalize_metric,
|
|
94
|
+
rho_s0=rho_s0,
|
|
95
|
+
)
|
|
96
|
+
return StructuredMetrics(
|
|
97
|
+
dx=_normalize_dx(broadcast_to_field_shape(raw_dx, mesh), normalize_metric=normalize_metric, rho_s0=rho_s0, Bnorm=Bnorm),
|
|
98
|
+
dy=broadcast_to_field_shape(raw_dy, mesh),
|
|
99
|
+
dz=broadcast_to_field_shape(raw_dz, mesh),
|
|
100
|
+
J=_normalize_J(broadcast_to_field_shape(raw_J, mesh), normalize_metric=normalize_metric, rho_s0=rho_s0),
|
|
101
|
+
g11=_normalize_g11(broadcast_to_field_shape(raw_g11, mesh), normalize_metric=normalize_metric, rho_s0=rho_s0),
|
|
102
|
+
g33=_normalize_g33(broadcast_to_field_shape(raw_g33, mesh), normalize_metric=normalize_metric, rho_s0=rho_s0),
|
|
103
|
+
g22=normalized_g22,
|
|
104
|
+
g_22=normalized_g_22,
|
|
105
|
+
g23=broadcast_to_field_shape(raw_g23, mesh),
|
|
106
|
+
g_23=broadcast_to_field_shape(raw_g_23, mesh),
|
|
107
|
+
Bxy=_normalize_Bxy(broadcast_to_field_shape(raw_Bxy, mesh), normalize_metric=normalize_metric, Bnorm=Bnorm),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _metric_value(
|
|
112
|
+
config: BoutConfig,
|
|
113
|
+
evaluator: ArrayExpressionEvaluator,
|
|
114
|
+
key: str,
|
|
115
|
+
*,
|
|
116
|
+
default: float,
|
|
117
|
+
) -> Any:
|
|
118
|
+
if not config.has_section("mesh") or not config.has_option("mesh", key):
|
|
119
|
+
return default
|
|
120
|
+
return evaluator.resolve_option("mesh", key)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _normalize_metric_enabled(config: BoutConfig) -> bool:
|
|
124
|
+
if not has_model_section(config):
|
|
125
|
+
return True
|
|
126
|
+
model_section = locate_model_section(config)
|
|
127
|
+
if not config.has_option(model_section, "normalise_metric"):
|
|
128
|
+
return True
|
|
129
|
+
return bool(config.parsed(model_section, "normalise_metric"))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _recalculate_metric_enabled(run_config: RunConfiguration) -> bool:
|
|
133
|
+
return bool(run_config.normalization and run_config.normalization.metric_policy.recalculate_metric)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _recalculate_orthogonal_metrics(
|
|
137
|
+
config: BoutConfig,
|
|
138
|
+
evaluator: ArrayExpressionEvaluator,
|
|
139
|
+
mesh: StructuredMesh,
|
|
140
|
+
*,
|
|
141
|
+
rho_s0: float,
|
|
142
|
+
Bnorm: float,
|
|
143
|
+
raw_dx: Any,
|
|
144
|
+
raw_dy: Any,
|
|
145
|
+
raw_dz: Any,
|
|
146
|
+
raw_Bxy: Any,
|
|
147
|
+
raw_g_23: Any,
|
|
148
|
+
dtype: Any,
|
|
149
|
+
) -> StructuredMetrics:
|
|
150
|
+
Rxy = _metric_value(config, evaluator, "Rxy", default=1.0)
|
|
151
|
+
Bpxy = _metric_value(config, evaluator, "Bpxy", default=1.0)
|
|
152
|
+
Btxy = _metric_value(config, evaluator, "Btxy", default=0.0)
|
|
153
|
+
hthe = _metric_value(config, evaluator, "hthe", default=1.0)
|
|
154
|
+
sinty = _metric_value(config, evaluator, "sinty", default=0.0)
|
|
155
|
+
|
|
156
|
+
normalized_Rxy = broadcast_to_field_shape(Rxy, mesh) / rho_s0
|
|
157
|
+
normalized_hthe = broadcast_to_field_shape(hthe, mesh) / rho_s0
|
|
158
|
+
normalized_sinty = broadcast_to_field_shape(sinty, mesh) * (rho_s0 * rho_s0 * Bnorm)
|
|
159
|
+
normalized_Bpxy = broadcast_to_field_shape(Bpxy, mesh) / Bnorm
|
|
160
|
+
normalized_Btxy = broadcast_to_field_shape(Btxy, mesh) / Bnorm
|
|
161
|
+
normalized_Bxy = broadcast_to_field_shape(raw_Bxy, mesh) / Bnorm
|
|
162
|
+
|
|
163
|
+
if run_parallel_transform_type(config) == "shifted":
|
|
164
|
+
normalized_sinty = jnp.zeros_like(normalized_sinty, dtype=dtype)
|
|
165
|
+
|
|
166
|
+
sign_Bp = jnp.where(jnp.min(normalized_Bpxy) < 0.0, -1.0, 1.0)
|
|
167
|
+
g11_normalized = jnp.square(normalized_Rxy * normalized_Bpxy)
|
|
168
|
+
g22 = 1.0 / jnp.square(broadcast_to_field_shape(hthe, mesh))
|
|
169
|
+
g_22 = jnp.square(broadcast_to_field_shape(hthe, mesh))
|
|
170
|
+
g33_normalized = jnp.square(normalized_sinty) * g11_normalized + jnp.square(normalized_Bxy) / g11_normalized
|
|
171
|
+
g23 = -sign_Bp * normalized_Btxy / (normalized_hthe * normalized_Bpxy * normalized_Rxy)
|
|
172
|
+
J_normalized = normalized_hthe / normalized_Bpxy
|
|
173
|
+
|
|
174
|
+
return StructuredMetrics(
|
|
175
|
+
dx=broadcast_to_field_shape(raw_dx, mesh),
|
|
176
|
+
dy=broadcast_to_field_shape(raw_dy, mesh),
|
|
177
|
+
dz=broadcast_to_field_shape(raw_dz, mesh),
|
|
178
|
+
J=J_normalized * rho_s0,
|
|
179
|
+
g11=g11_normalized * (rho_s0 * rho_s0),
|
|
180
|
+
g33=g33_normalized / (rho_s0 * rho_s0),
|
|
181
|
+
g22=g22,
|
|
182
|
+
g_22=g_22,
|
|
183
|
+
g23=g23,
|
|
184
|
+
g_23=broadcast_to_field_shape(raw_g_23, mesh),
|
|
185
|
+
Bxy=broadcast_to_field_shape(raw_Bxy, mesh),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_parallel_transform_type(config: BoutConfig) -> str:
|
|
190
|
+
if not config.has_section("mesh:paralleltransform") or not config.has_option("mesh:paralleltransform", "type"):
|
|
191
|
+
return "identity"
|
|
192
|
+
return str(config.parsed("mesh:paralleltransform", "type"))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _normalize_dx(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float, Bnorm: float) -> jnp.ndarray:
|
|
196
|
+
if not normalize_metric:
|
|
197
|
+
return value
|
|
198
|
+
return value / (rho_s0 * rho_s0 * Bnorm)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _normalize_J(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float) -> jnp.ndarray:
|
|
202
|
+
if not normalize_metric:
|
|
203
|
+
return value
|
|
204
|
+
return value / rho_s0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _normalize_g11(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float) -> jnp.ndarray:
|
|
208
|
+
if not normalize_metric:
|
|
209
|
+
return value
|
|
210
|
+
return value / (rho_s0 * rho_s0)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _normalize_g33(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float) -> jnp.ndarray:
|
|
214
|
+
if not normalize_metric:
|
|
215
|
+
return value
|
|
216
|
+
return value * (rho_s0 * rho_s0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _normalize_g22(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float) -> jnp.ndarray:
|
|
220
|
+
if not normalize_metric:
|
|
221
|
+
return value
|
|
222
|
+
return value * (rho_s0 * rho_s0)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _normalize_g_22(value: jnp.ndarray, *, normalize_metric: bool, rho_s0: float) -> jnp.ndarray:
|
|
226
|
+
if not normalize_metric:
|
|
227
|
+
return value
|
|
228
|
+
return value / (rho_s0 * rho_s0)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _normalize_Bxy(value: jnp.ndarray, *, normalize_metric: bool, Bnorm: float) -> jnp.ndarray:
|
|
232
|
+
if not normalize_metric:
|
|
233
|
+
return value
|
|
234
|
+
return value / Bnorm
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Neutral / recycling physics for the 1D scrape-off-layer model.
|
|
2
|
+
|
|
3
|
+
Self-contained JAX implementations of the hydrogenic atomic reaction rates
|
|
4
|
+
(AMJUEL ionization / recombination fits, the Janev/AMJUEL charge-exchange
|
|
5
|
+
polynomial) used to couple a plasma fluid to a neutral fluid in the
|
|
6
|
+
scrape-off layer. All coefficients ship with the package
|
|
7
|
+
(``drbx.data.atomic_rates``); there is no external-database dependency.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .atomic_rates import (
|
|
11
|
+
charge_exchange_rate_coefficient,
|
|
12
|
+
energy_loss_coefficient,
|
|
13
|
+
eval_amjuel_fit,
|
|
14
|
+
load_amjuel_coefficients,
|
|
15
|
+
rate_coefficient,
|
|
16
|
+
)
|
|
17
|
+
from .reactions import (
|
|
18
|
+
HydrogenReactionSources,
|
|
19
|
+
PlasmaNormalization,
|
|
20
|
+
compute_hydrogen_reaction_sources,
|
|
21
|
+
)
|
|
22
|
+
from .detachment_sol_model import (
|
|
23
|
+
DetachmentSolParameters,
|
|
24
|
+
DetachmentSolState,
|
|
25
|
+
detachment_diagnostics,
|
|
26
|
+
detachment_sol_run,
|
|
27
|
+
detachment_sol_step,
|
|
28
|
+
)
|
|
29
|
+
from .recycling_sol_model import (
|
|
30
|
+
SolRecyclingParameters,
|
|
31
|
+
SolRecyclingState,
|
|
32
|
+
linear_target_temperature_profile,
|
|
33
|
+
sol_recycling_run,
|
|
34
|
+
sol_recycling_step,
|
|
35
|
+
target_ion_flux,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"rate_coefficient",
|
|
40
|
+
"energy_loss_coefficient",
|
|
41
|
+
"charge_exchange_rate_coefficient",
|
|
42
|
+
"eval_amjuel_fit",
|
|
43
|
+
"load_amjuel_coefficients",
|
|
44
|
+
"PlasmaNormalization",
|
|
45
|
+
"HydrogenReactionSources",
|
|
46
|
+
"compute_hydrogen_reaction_sources",
|
|
47
|
+
"SolRecyclingParameters",
|
|
48
|
+
"SolRecyclingState",
|
|
49
|
+
"linear_target_temperature_profile",
|
|
50
|
+
"sol_recycling_run",
|
|
51
|
+
"sol_recycling_step",
|
|
52
|
+
"target_ion_flux",
|
|
53
|
+
"DetachmentSolParameters",
|
|
54
|
+
"DetachmentSolState",
|
|
55
|
+
"detachment_diagnostics",
|
|
56
|
+
"detachment_sol_run",
|
|
57
|
+
"detachment_sol_step",
|
|
58
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Hydrogenic atomic reaction-rate coefficients (self-contained, pure JAX).
|
|
2
|
+
|
|
3
|
+
Ionization and recombination rate coefficients ``<sigma v>(T_e, n_e)`` come from
|
|
4
|
+
the packaged AMJUEL double-polynomial fits; the charge-exchange coefficient
|
|
5
|
+
``<sigma v>(T_eff)`` comes from the AMJUEL H.2 3.1.8 one-dimensional polynomial
|
|
6
|
+
(inlined here, as hermes-3 does, since it carries no tabulated file). Every
|
|
7
|
+
routine is pure ``jax.numpy`` and therefore ``jit``/``grad``/``vmap``
|
|
8
|
+
transparent; the coefficient tables ship with the package under
|
|
9
|
+
``drbx.data.atomic_rates`` (real published AMJUEL fits, verified against the
|
|
10
|
+
hermes-3 database provenance), so there is no runtime external dependency.
|
|
11
|
+
|
|
12
|
+
Temperatures are in eV and densities in m^-3; rate coefficients are returned in
|
|
13
|
+
m^3 / s. The AMJUEL fits are clamped to their fitted range
|
|
14
|
+
(``T in [0.1, 1e4] eV``, ``n in [1e14, 1e22] m^-3``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from functools import lru_cache
|
|
21
|
+
from importlib import resources
|
|
22
|
+
|
|
23
|
+
import jax.numpy as jnp
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"rate_coefficient",
|
|
28
|
+
"energy_loss_coefficient",
|
|
29
|
+
"charge_exchange_rate_coefficient",
|
|
30
|
+
"eval_amjuel_fit",
|
|
31
|
+
"load_amjuel_coefficients",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# AMJUEL fit files, keyed by (species, reaction). Deuterium and tritium share the
|
|
35
|
+
# hydrogen fits; helium has its own.
|
|
36
|
+
_AMJUEL_FILENAMES = {
|
|
37
|
+
("d", "iz"): "iz_AMJUEL_H.x_2.1.5.json",
|
|
38
|
+
("d", "rec"): "rec_AMJUEL_H.x_2.1.8.json",
|
|
39
|
+
("t", "iz"): "iz_AMJUEL_H.x_2.1.5.json",
|
|
40
|
+
("t", "rec"): "rec_AMJUEL_H.x_2.1.8.json",
|
|
41
|
+
("he", "iz"): "iz_AMJUEL_H.x_2.3.9a.json",
|
|
42
|
+
("he", "rec"): "rec_AMJUEL_H.x_2.3.13a.json",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# AMJUEL H.2 3.1.8 charge-exchange polynomial: ln(sigma v [cm^3/s]) as a degree-8
|
|
46
|
+
# polynomial in ln(T_eff [eV]) (density independent), as inlined in hermes-3's
|
|
47
|
+
# hydrogen_charge_exchange.cxx.
|
|
48
|
+
_CX_CONSTANT = -18.5028
|
|
49
|
+
_CX_COEFFICIENTS = (
|
|
50
|
+
0.3708409,
|
|
51
|
+
7.949876e-3,
|
|
52
|
+
-6.143769e-4,
|
|
53
|
+
-4.698969e-4,
|
|
54
|
+
-4.096807e-4,
|
|
55
|
+
1.440382e-4,
|
|
56
|
+
-1.514243e-5,
|
|
57
|
+
5.122435e-7,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@lru_cache(maxsize=None)
|
|
62
|
+
def load_amjuel_coefficients(species: str, reaction: str) -> tuple[np.ndarray, np.ndarray, float]:
|
|
63
|
+
"""Load one AMJUEL fit: ``(sigma_v_coeffs, sigma_v_E_coeffs, electron_heating)``.
|
|
64
|
+
|
|
65
|
+
``sigma_v_coeffs`` and ``sigma_v_E_coeffs`` are 9x9 log-log polynomial
|
|
66
|
+
coefficient matrices; ``electron_heating`` is the potential energy [eV]
|
|
67
|
+
returned to the electrons (e.g. 13.6 eV on recombination).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
filename = _AMJUEL_FILENAMES[(species, reaction)]
|
|
71
|
+
payload = json.loads(
|
|
72
|
+
resources.files("drbx.data.atomic_rates").joinpath(filename).read_text(encoding="utf-8")
|
|
73
|
+
)
|
|
74
|
+
return (
|
|
75
|
+
np.asarray(payload["sigma_v_coeffs"], dtype=np.float64),
|
|
76
|
+
np.asarray(payload["sigma_v_E_coeffs"], dtype=np.float64),
|
|
77
|
+
float(payload["electron_heating"]),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def eval_amjuel_fit(temperature_ev, density_m3, coefficients) -> jnp.ndarray:
|
|
82
|
+
"""Evaluate an AMJUEL double-polynomial fit, returning ``<sigma v>`` in m^3/s.
|
|
83
|
+
|
|
84
|
+
``<sigma v> = 1e-6 * exp( sum_ij c_ij (ln T)^i (ln(n/1e14))^j )`` with ``T``
|
|
85
|
+
and ``n`` clamped to the fit's validity range.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
temperature = jnp.clip(jnp.asarray(temperature_ev, dtype=jnp.float64), 0.1, 1.0e4)
|
|
89
|
+
density = jnp.clip(jnp.asarray(density_m3, dtype=jnp.float64), 1.0e14, 1.0e22)
|
|
90
|
+
log_t = jnp.log(temperature)
|
|
91
|
+
log_n = jnp.log(density / 1.0e14)
|
|
92
|
+
|
|
93
|
+
coeffs = jnp.asarray(coefficients, dtype=jnp.float64)
|
|
94
|
+
result = jnp.zeros_like(log_t)
|
|
95
|
+
for row in coeffs[::-1]:
|
|
96
|
+
row_result = jnp.zeros_like(log_n)
|
|
97
|
+
for coefficient in row[::-1]:
|
|
98
|
+
row_result = row_result * log_n + coefficient
|
|
99
|
+
result = result * log_t + row_result
|
|
100
|
+
return jnp.exp(result) * 1.0e-6
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def rate_coefficient(species: str, reaction: str, electron_temperature, electron_density) -> jnp.ndarray:
|
|
104
|
+
"""Ionization (``reaction="iz"``) or recombination (``"rec"``) ``<sigma v>`` [m^3/s]."""
|
|
105
|
+
|
|
106
|
+
sigma_v_coeffs, _, _ = load_amjuel_coefficients(species, reaction)
|
|
107
|
+
return eval_amjuel_fit(electron_temperature, electron_density, sigma_v_coeffs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def energy_loss_coefficient(species: str, reaction: str, electron_temperature, electron_density) -> jnp.ndarray:
|
|
111
|
+
"""Electron energy-loss coefficient ``<sigma v E>`` [eV m^3/s] for the reaction.
|
|
112
|
+
|
|
113
|
+
The recombination channel returns the ``electron_heating`` potential energy
|
|
114
|
+
(13.6 eV for hydrogen) as a heating term, so the net electron energy sink is
|
|
115
|
+
``<sigma v E> - electron_heating * <sigma v>``; ionization has no potential
|
|
116
|
+
add-back (``electron_heating = 0``).
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
sigma_v_coeffs, sigma_v_E_coeffs, electron_heating = load_amjuel_coefficients(species, reaction)
|
|
120
|
+
energy_loss = eval_amjuel_fit(electron_temperature, electron_density, sigma_v_E_coeffs)
|
|
121
|
+
rate = eval_amjuel_fit(electron_temperature, electron_density, sigma_v_coeffs)
|
|
122
|
+
return energy_loss - float(electron_heating) * rate
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def charge_exchange_rate_coefficient(effective_temperature_ev) -> jnp.ndarray:
|
|
126
|
+
"""Hydrogenic charge-exchange ``<sigma v>(T_eff)`` [m^3/s] (AMJUEL H.2 3.1.8)."""
|
|
127
|
+
|
|
128
|
+
log_t = jnp.log(jnp.clip(jnp.asarray(effective_temperature_ev, dtype=jnp.float64), 0.1, 1.0e4))
|
|
129
|
+
log_sigma_v = jnp.full_like(log_t, _CX_CONSTANT)
|
|
130
|
+
power = jnp.ones_like(log_t)
|
|
131
|
+
for coefficient in _CX_COEFFICIENTS:
|
|
132
|
+
power = power * log_t
|
|
133
|
+
log_sigma_v = log_sigma_v + coefficient * power
|
|
134
|
+
return jnp.exp(log_sigma_v) * 1.0e-6
|