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/fluid_1d.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
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 .expression import ArrayExpressionEvaluator
|
|
9
|
+
from .limiters import periodic_monotonic_centered_edges_jax as _periodic_monotonic_centered_edges
|
|
10
|
+
from .mesh import StructuredMesh, broadcast_to_field_shape
|
|
11
|
+
from .metrics import StructuredMetrics
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Fluid1DState:
|
|
16
|
+
density: jnp.ndarray
|
|
17
|
+
pressure: jnp.ndarray
|
|
18
|
+
momentum: jnp.ndarray
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class Fluid1DRhsResult:
|
|
23
|
+
density: jnp.ndarray
|
|
24
|
+
pressure: jnp.ndarray
|
|
25
|
+
momentum: jnp.ndarray
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Fluid1DHistoryResult:
|
|
30
|
+
density_history: jnp.ndarray
|
|
31
|
+
pressure_history: jnp.ndarray
|
|
32
|
+
momentum_history: jnp.ndarray
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def initialize_mms_state(config: BoutConfig, *, section: str, mesh: StructuredMesh) -> Fluid1DState:
|
|
36
|
+
return Fluid1DState(
|
|
37
|
+
density=evaluate_field_option(config, f"N{section}", "solution", mesh=mesh, time=0.0),
|
|
38
|
+
pressure=evaluate_field_option(config, f"P{section}", "solution", mesh=mesh, time=0.0),
|
|
39
|
+
momentum=evaluate_field_option(config, f"NV{section}", "solution", mesh=mesh, time=0.0),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def evaluate_field_option(
|
|
44
|
+
config: BoutConfig,
|
|
45
|
+
variable_name: str,
|
|
46
|
+
option: str,
|
|
47
|
+
*,
|
|
48
|
+
mesh: StructuredMesh,
|
|
49
|
+
time: float,
|
|
50
|
+
) -> jnp.ndarray:
|
|
51
|
+
evaluator = ArrayExpressionEvaluator(config, local_values=mesh.expression_context(time=time))
|
|
52
|
+
field = broadcast_to_field_shape(evaluator.resolve_option(variable_name, option), mesh)
|
|
53
|
+
return apply_periodic_y_guards(field, mesh)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def compute_mms_rhs(
|
|
57
|
+
config: BoutConfig,
|
|
58
|
+
state: Fluid1DState,
|
|
59
|
+
*,
|
|
60
|
+
section: str,
|
|
61
|
+
mesh: StructuredMesh,
|
|
62
|
+
metrics: StructuredMetrics,
|
|
63
|
+
atomic_mass: float,
|
|
64
|
+
time: float,
|
|
65
|
+
) -> Fluid1DRhsResult:
|
|
66
|
+
guarded = Fluid1DState(
|
|
67
|
+
density=apply_periodic_y_guards(state.density, mesh),
|
|
68
|
+
pressure=apply_periodic_y_guards(state.pressure, mesh),
|
|
69
|
+
momentum=apply_periodic_y_guards(state.momentum, mesh),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
density = guarded.density
|
|
73
|
+
pressure = guarded.pressure
|
|
74
|
+
momentum = guarded.momentum
|
|
75
|
+
velocity = momentum / (atomic_mass * density)
|
|
76
|
+
temperature = pressure / density
|
|
77
|
+
fastest_wave = jnp.sqrt(temperature / atomic_mass)
|
|
78
|
+
|
|
79
|
+
density_rhs = -div_par_mod_periodic(density, velocity, fastest_wave, mesh=mesh, metrics=metrics)
|
|
80
|
+
density_rhs = density_rhs + evaluate_field_option(config, f"N{section}", "source", mesh=mesh, time=time)
|
|
81
|
+
|
|
82
|
+
pressure_rhs = -(5.0 / 3.0) * div_par_mod_periodic(pressure, velocity, fastest_wave, mesh=mesh, metrics=metrics)
|
|
83
|
+
pressure_rhs = pressure_rhs + (2.0 / 3.0) * velocity * grad_par_periodic(pressure, mesh=mesh, metrics=metrics)
|
|
84
|
+
pressure_rhs = pressure_rhs + evaluate_field_option(config, f"P{section}", "source", mesh=mesh, time=time)
|
|
85
|
+
|
|
86
|
+
momentum_rhs = -atomic_mass * div_par_fvv_periodic(density, velocity, fastest_wave, mesh=mesh, metrics=metrics)
|
|
87
|
+
momentum_rhs = momentum_rhs - grad_par_periodic(pressure, mesh=mesh, metrics=metrics)
|
|
88
|
+
momentum_rhs = momentum_rhs + evaluate_field_option(config, f"NV{section}", "source", mesh=mesh, time=time)
|
|
89
|
+
|
|
90
|
+
return Fluid1DRhsResult(
|
|
91
|
+
density=density_rhs,
|
|
92
|
+
pressure=pressure_rhs,
|
|
93
|
+
momentum=momentum_rhs,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def advance_mms_history(
|
|
98
|
+
config: BoutConfig,
|
|
99
|
+
*,
|
|
100
|
+
section: str,
|
|
101
|
+
mesh: StructuredMesh,
|
|
102
|
+
metrics: StructuredMetrics,
|
|
103
|
+
atomic_mass: float,
|
|
104
|
+
timestep: float,
|
|
105
|
+
steps: int,
|
|
106
|
+
substeps: int,
|
|
107
|
+
initial_state: Fluid1DState | None = None,
|
|
108
|
+
start_time: float = 0.0,
|
|
109
|
+
) -> Fluid1DHistoryResult:
|
|
110
|
+
if steps < 0:
|
|
111
|
+
raise ValueError("steps must be non-negative")
|
|
112
|
+
if substeps <= 0:
|
|
113
|
+
raise ValueError("substeps must be positive")
|
|
114
|
+
|
|
115
|
+
state = initial_state if initial_state is not None else initialize_mms_state(config, section=section, mesh=mesh)
|
|
116
|
+
density_history = [state.density]
|
|
117
|
+
pressure_history = [state.pressure]
|
|
118
|
+
momentum_history = [state.momentum]
|
|
119
|
+
current_time = float(start_time)
|
|
120
|
+
sub_timestep = timestep / float(substeps)
|
|
121
|
+
|
|
122
|
+
for _ in range(steps):
|
|
123
|
+
for _ in range(substeps):
|
|
124
|
+
state = _rk4_step(
|
|
125
|
+
config,
|
|
126
|
+
state,
|
|
127
|
+
section=section,
|
|
128
|
+
mesh=mesh,
|
|
129
|
+
metrics=metrics,
|
|
130
|
+
atomic_mass=atomic_mass,
|
|
131
|
+
time=current_time,
|
|
132
|
+
timestep=sub_timestep,
|
|
133
|
+
)
|
|
134
|
+
current_time += sub_timestep
|
|
135
|
+
density_history.append(state.density)
|
|
136
|
+
pressure_history.append(state.pressure)
|
|
137
|
+
momentum_history.append(state.momentum)
|
|
138
|
+
|
|
139
|
+
return Fluid1DHistoryResult(
|
|
140
|
+
density_history=jnp.stack(density_history, axis=0),
|
|
141
|
+
pressure_history=jnp.stack(pressure_history, axis=0),
|
|
142
|
+
momentum_history=jnp.stack(momentum_history, axis=0),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def apply_periodic_y_guards(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
147
|
+
result = jnp.asarray(field, dtype=jnp.float64)
|
|
148
|
+
if mesh.myg <= 0:
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
interior = result[:, mesh.ystart : mesh.yend + 1, :]
|
|
152
|
+
for offset in range(mesh.myg):
|
|
153
|
+
result = result.at[:, mesh.ystart - 1 - offset, :].set(interior[:, -(offset + 1), :])
|
|
154
|
+
result = result.at[:, mesh.yend + 1 + offset, :].set(interior[:, offset, :])
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def div_par_mod_periodic(
|
|
159
|
+
field: jnp.ndarray,
|
|
160
|
+
velocity: jnp.ndarray,
|
|
161
|
+
wave_speed: jnp.ndarray,
|
|
162
|
+
*,
|
|
163
|
+
mesh: StructuredMesh,
|
|
164
|
+
metrics: StructuredMetrics,
|
|
165
|
+
) -> jnp.ndarray:
|
|
166
|
+
field_interior = _interior_y(field, mesh)
|
|
167
|
+
velocity_interior = _interior_y(velocity, mesh)
|
|
168
|
+
wave_interior = _interior_y(wave_speed, mesh)
|
|
169
|
+
dy = _interior_y(metrics.dy, mesh)
|
|
170
|
+
J = _interior_y(metrics.J, mesh)
|
|
171
|
+
common = _face_common_factor(metrics, mesh)
|
|
172
|
+
|
|
173
|
+
field_left_cell, field_right_cell = _mc_cell_edges(field_interior)
|
|
174
|
+
velocity_left_cell, velocity_right_cell = _mc_cell_edges(velocity_interior)
|
|
175
|
+
|
|
176
|
+
field_left = field_right_cell
|
|
177
|
+
field_right = jnp.roll(field_left_cell, shift=-1, axis=1)
|
|
178
|
+
velocity_left = velocity_right_cell
|
|
179
|
+
velocity_right = jnp.roll(velocity_left_cell, shift=-1, axis=1)
|
|
180
|
+
amax = _face_wave_speed(velocity_interior, wave_interior)
|
|
181
|
+
|
|
182
|
+
flux = 0.5 * (
|
|
183
|
+
field_left * velocity_left
|
|
184
|
+
+ field_right * velocity_right
|
|
185
|
+
+ amax * (field_left - field_right)
|
|
186
|
+
)
|
|
187
|
+
return _scatter_face_divergence(flux, dy=dy, J=J, common=common, mesh=mesh)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def div_par_fvv_periodic(
|
|
191
|
+
density: jnp.ndarray,
|
|
192
|
+
velocity: jnp.ndarray,
|
|
193
|
+
wave_speed: jnp.ndarray,
|
|
194
|
+
*,
|
|
195
|
+
mesh: StructuredMesh,
|
|
196
|
+
metrics: StructuredMetrics,
|
|
197
|
+
) -> jnp.ndarray:
|
|
198
|
+
density_interior = _interior_y(density, mesh)
|
|
199
|
+
velocity_interior = _interior_y(velocity, mesh)
|
|
200
|
+
wave_interior = _interior_y(wave_speed, mesh)
|
|
201
|
+
dy = _interior_y(metrics.dy, mesh)
|
|
202
|
+
J = _interior_y(metrics.J, mesh)
|
|
203
|
+
common = _face_common_factor(metrics, mesh)
|
|
204
|
+
|
|
205
|
+
density_left_cell, density_right_cell = _mc_cell_edges(density_interior)
|
|
206
|
+
velocity_left_cell, velocity_right_cell = _mc_cell_edges(velocity_interior)
|
|
207
|
+
|
|
208
|
+
density_left = density_right_cell
|
|
209
|
+
density_right = jnp.roll(density_left_cell, shift=-1, axis=1)
|
|
210
|
+
velocity_left = velocity_right_cell
|
|
211
|
+
velocity_right = jnp.roll(velocity_left_cell, shift=-1, axis=1)
|
|
212
|
+
amax = _face_wave_speed(velocity_interior, wave_interior)
|
|
213
|
+
|
|
214
|
+
flux = 0.5 * (
|
|
215
|
+
density_left * velocity_left * velocity_left
|
|
216
|
+
+ density_right * velocity_right * velocity_right
|
|
217
|
+
+ amax * (density_left * velocity_left - density_right * velocity_right)
|
|
218
|
+
)
|
|
219
|
+
return _scatter_face_divergence(flux, dy=dy, J=J, common=common, mesh=mesh)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def grad_par_periodic(field: jnp.ndarray, *, mesh: StructuredMesh, metrics: StructuredMetrics) -> jnp.ndarray:
|
|
223
|
+
dy = _interior_y(metrics.dy, mesh)
|
|
224
|
+
field_interior = _interior_y(field, mesh)
|
|
225
|
+
if not jnp.allclose(dy, dy[:, :1, :], rtol=1e-12, atol=1e-12):
|
|
226
|
+
raise NotImplementedError("Native periodic fluid gradient currently requires uniform dy.")
|
|
227
|
+
|
|
228
|
+
spacing = dy[:, :1, :]
|
|
229
|
+
gradient = (jnp.roll(field_interior, shift=-1, axis=1) - jnp.roll(field_interior, shift=1, axis=1)) / (2.0 * spacing)
|
|
230
|
+
result = jnp.zeros_like(field, dtype=jnp.float64)
|
|
231
|
+
result = result.at[:, mesh.ystart : mesh.yend + 1, :].set(gradient)
|
|
232
|
+
return apply_periodic_y_guards(result, mesh)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _rk4_step(
|
|
236
|
+
config: BoutConfig,
|
|
237
|
+
state: Fluid1DState,
|
|
238
|
+
*,
|
|
239
|
+
section: str,
|
|
240
|
+
mesh: StructuredMesh,
|
|
241
|
+
metrics: StructuredMetrics,
|
|
242
|
+
atomic_mass: float,
|
|
243
|
+
time: float,
|
|
244
|
+
timestep: float,
|
|
245
|
+
) -> Fluid1DState:
|
|
246
|
+
k1 = compute_mms_rhs(config, state, section=section, mesh=mesh, metrics=metrics, atomic_mass=atomic_mass, time=time)
|
|
247
|
+
k2 = compute_mms_rhs(
|
|
248
|
+
config,
|
|
249
|
+
_add_state(state, k1, scale=0.5 * timestep),
|
|
250
|
+
section=section,
|
|
251
|
+
mesh=mesh,
|
|
252
|
+
metrics=metrics,
|
|
253
|
+
atomic_mass=atomic_mass,
|
|
254
|
+
time=time + 0.5 * timestep,
|
|
255
|
+
)
|
|
256
|
+
k3 = compute_mms_rhs(
|
|
257
|
+
config,
|
|
258
|
+
_add_state(state, k2, scale=0.5 * timestep),
|
|
259
|
+
section=section,
|
|
260
|
+
mesh=mesh,
|
|
261
|
+
metrics=metrics,
|
|
262
|
+
atomic_mass=atomic_mass,
|
|
263
|
+
time=time + 0.5 * timestep,
|
|
264
|
+
)
|
|
265
|
+
k4 = compute_mms_rhs(
|
|
266
|
+
config,
|
|
267
|
+
_add_state(state, k3, scale=timestep),
|
|
268
|
+
section=section,
|
|
269
|
+
mesh=mesh,
|
|
270
|
+
metrics=metrics,
|
|
271
|
+
atomic_mass=atomic_mass,
|
|
272
|
+
time=time + timestep,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
next_state = Fluid1DState(
|
|
276
|
+
density=state.density + (timestep / 6.0) * (k1.density + 2.0 * k2.density + 2.0 * k3.density + k4.density),
|
|
277
|
+
pressure=state.pressure
|
|
278
|
+
+ (timestep / 6.0) * (k1.pressure + 2.0 * k2.pressure + 2.0 * k3.pressure + k4.pressure),
|
|
279
|
+
momentum=state.momentum
|
|
280
|
+
+ (timestep / 6.0) * (k1.momentum + 2.0 * k2.momentum + 2.0 * k3.momentum + k4.momentum),
|
|
281
|
+
)
|
|
282
|
+
return Fluid1DState(
|
|
283
|
+
density=apply_periodic_y_guards(next_state.density, mesh),
|
|
284
|
+
pressure=apply_periodic_y_guards(next_state.pressure, mesh),
|
|
285
|
+
momentum=apply_periodic_y_guards(next_state.momentum, mesh),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _add_state(state: Fluid1DState, rhs: Fluid1DRhsResult, *, scale: float) -> Fluid1DState:
|
|
290
|
+
return Fluid1DState(
|
|
291
|
+
density=state.density + scale * rhs.density,
|
|
292
|
+
pressure=state.pressure + scale * rhs.pressure,
|
|
293
|
+
momentum=state.momentum + scale * rhs.momentum,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _interior_y(field: jnp.ndarray, mesh: StructuredMesh) -> jnp.ndarray:
|
|
298
|
+
return jnp.asarray(field, dtype=jnp.float64)[:, mesh.ystart : mesh.yend + 1, :]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _mc_cell_edges(field: jnp.ndarray) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
302
|
+
return _periodic_monotonic_centered_edges(field, axis=1)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _face_common_factor(metrics: StructuredMetrics, mesh: StructuredMesh) -> jnp.ndarray:
|
|
306
|
+
J = _interior_y(metrics.J, mesh)
|
|
307
|
+
g22 = _interior_y(metrics.g_22, mesh)
|
|
308
|
+
return (J + jnp.roll(J, shift=-1, axis=1)) / (jnp.sqrt(g22) + jnp.sqrt(jnp.roll(g22, shift=-1, axis=1)))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _face_wave_speed(velocity: jnp.ndarray, wave_speed: jnp.ndarray) -> jnp.ndarray:
|
|
312
|
+
return jnp.maximum(
|
|
313
|
+
jnp.maximum(wave_speed, jnp.roll(wave_speed, shift=-1, axis=1)),
|
|
314
|
+
jnp.maximum(jnp.abs(velocity), jnp.abs(jnp.roll(velocity, shift=-1, axis=1))),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _scatter_face_divergence(
|
|
319
|
+
flux: jnp.ndarray,
|
|
320
|
+
*,
|
|
321
|
+
dy: jnp.ndarray,
|
|
322
|
+
J: jnp.ndarray,
|
|
323
|
+
common: jnp.ndarray,
|
|
324
|
+
mesh: StructuredMesh,
|
|
325
|
+
) -> jnp.ndarray:
|
|
326
|
+
flux_right = flux * common / (dy * J)
|
|
327
|
+
flux_left = jnp.roll(flux, shift=1, axis=1) * jnp.roll(common, shift=1, axis=1) / (dy * J)
|
|
328
|
+
result = jnp.zeros((mesh.nx, mesh.local_ny, mesh.nz), dtype=jnp.float64)
|
|
329
|
+
result = result.at[:, mesh.ystart : mesh.yend + 1, :].set(flux_right - flux_left)
|
|
330
|
+
return apply_periodic_y_guards(result, mesh)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""JAX-native 2-D Hasegawa-Wakatani drift-wave turbulence model.
|
|
2
|
+
|
|
3
|
+
This is the closed-field-line (periodic flux-tube) drift-wave turbulence
|
|
4
|
+
flagship: a pseudo-spectral solver for the two-field Hasegawa-Wakatani system
|
|
5
|
+
in the perpendicular plane,
|
|
6
|
+
|
|
7
|
+
d/dt zeta = -{phi, zeta} + alpha (phi - n) - nu * lap^2 zeta - mu * zeta
|
|
8
|
+
d/dt n = -{phi, n} - kappa d/dy phi + alpha (phi - n) - nu * lap^2 n - mu * n
|
|
9
|
+
|
|
10
|
+
with vorticity ``zeta = lap phi`` (so ``phi_k = -zeta_k / k^2``), adiabaticity
|
|
11
|
+
``alpha``, background density gradient ``kappa``, hyperviscosity ``nu``, and an
|
|
12
|
+
optional scale-independent friction ``mu`` (default 0). The friction models
|
|
13
|
+
large-scale drag (e.g. sheath or neutral damping in the tokamak edge) and, by
|
|
14
|
+
absorbing the 2-D inverse cascade at the box scale, lets a fixed-step run reach
|
|
15
|
+
a statistically steady saturated state.
|
|
16
|
+
``{a, b} = da/dx db/dy - da/dy db/dx`` is the E x B Poisson bracket, evaluated
|
|
17
|
+
pseudo-spectrally with 2/3-rule dealiasing.
|
|
18
|
+
|
|
19
|
+
The whole right-hand side is written in JAX, so a run is ``jit``-compiled and
|
|
20
|
+
differentiable. Its single-mode linear growth rate reproduces the eigenvalue of
|
|
21
|
+
:func:`drbx.linear.resistive_drift_wave_operator` (benchmark B2); at finite
|
|
22
|
+
amplitude it develops nonlinear E x B transport with an outward particle flux
|
|
23
|
+
that saturates statistically when ``mu > 0`` absorbs the inverse cascade.
|
|
24
|
+
|
|
25
|
+
Initial spectra must be Hermitian (e.g. the FFT of a real field). The solver
|
|
26
|
+
evolves the complex spectral state as given; a non-Hermitian state corresponds
|
|
27
|
+
to complex-valued fields, i.e. an unphysical complexified system that does not
|
|
28
|
+
obey the real system's energy balance and can blow up nonlinearly.
|
|
29
|
+
|
|
30
|
+
References: Hasegawa & Wakatani, Phys. Rev. Lett. 50, 682 (1983); Numata
|
|
31
|
+
et al., Phys. Plasmas 14, 102312 (2007).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
|
|
38
|
+
import jax
|
|
39
|
+
import jax.numpy as jnp
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"HasegawaWakataniParameters",
|
|
43
|
+
"HasegawaWakataniGrid",
|
|
44
|
+
"hw_grid",
|
|
45
|
+
"potential_from_vorticity",
|
|
46
|
+
"hw_rhs",
|
|
47
|
+
"hw_step",
|
|
48
|
+
"hw_run",
|
|
49
|
+
"hw_run_flux_history",
|
|
50
|
+
"particle_flux",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class HasegawaWakataniParameters:
|
|
56
|
+
"""Physical parameters of the Hasegawa-Wakatani system."""
|
|
57
|
+
|
|
58
|
+
adiabaticity: float = 1.0 # alpha: parallel coupling
|
|
59
|
+
gradient: float = 1.0 # kappa: background density gradient drive
|
|
60
|
+
hyperviscosity: float = 1.0e-3 # nu: lap^2 damping of the grid scale
|
|
61
|
+
friction: float = 0.0 # mu: linear drag absorbing the inverse cascade
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class HasegawaWakataniGrid:
|
|
66
|
+
"""Periodic perpendicular grid and its spectral wavenumbers."""
|
|
67
|
+
|
|
68
|
+
n: int
|
|
69
|
+
length: float
|
|
70
|
+
kx: jnp.ndarray
|
|
71
|
+
ky: jnp.ndarray
|
|
72
|
+
k2: jnp.ndarray
|
|
73
|
+
inv_k2: jnp.ndarray
|
|
74
|
+
dealias: jnp.ndarray
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def hw_grid(n: int, length: float) -> HasegawaWakataniGrid:
|
|
78
|
+
"""Build an ``n x n`` periodic grid of side ``length`` with spectral operators."""
|
|
79
|
+
|
|
80
|
+
modes = jnp.fft.fftfreq(n, d=length / n) * 2.0 * jnp.pi
|
|
81
|
+
kx = modes[:, None] * jnp.ones((1, n))
|
|
82
|
+
ky = jnp.ones((n, 1)) * modes[None, :]
|
|
83
|
+
k2 = kx**2 + ky**2
|
|
84
|
+
inv_k2 = jnp.where(k2 > 0, 1.0 / jnp.where(k2 > 0, k2, 1.0), 0.0)
|
|
85
|
+
# 2/3-rule dealiasing mask.
|
|
86
|
+
kmax = jnp.max(jnp.abs(modes)) * (2.0 / 3.0)
|
|
87
|
+
dealias = (jnp.abs(kx) <= kmax) & (jnp.abs(ky) <= kmax)
|
|
88
|
+
return HasegawaWakataniGrid(
|
|
89
|
+
n=n, length=length, kx=kx, ky=ky, k2=k2, inv_k2=inv_k2,
|
|
90
|
+
dealias=dealias.astype(jnp.float64),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def potential_from_vorticity(zeta_hat: jnp.ndarray, grid: HasegawaWakataniGrid) -> jnp.ndarray:
|
|
95
|
+
"""Invert ``lap phi = zeta`` in Fourier space: ``phi_k = -zeta_k / k^2``."""
|
|
96
|
+
|
|
97
|
+
return -zeta_hat * grid.inv_k2
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _bracket_hat(phi_hat, field_hat, grid: HasegawaWakataniGrid) -> jnp.ndarray:
|
|
101
|
+
"""Poisson bracket ``{phi, field}`` returned in Fourier space (dealiased)."""
|
|
102
|
+
|
|
103
|
+
dphi_dx = jnp.fft.ifft2(1j * grid.kx * phi_hat)
|
|
104
|
+
dphi_dy = jnp.fft.ifft2(1j * grid.ky * phi_hat)
|
|
105
|
+
df_dx = jnp.fft.ifft2(1j * grid.kx * field_hat)
|
|
106
|
+
df_dy = jnp.fft.ifft2(1j * grid.ky * field_hat)
|
|
107
|
+
bracket = dphi_dx * df_dy - dphi_dy * df_dx
|
|
108
|
+
return jnp.fft.fft2(bracket) * grid.dealias
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def hw_rhs(zeta_hat, n_hat, grid: HasegawaWakataniGrid, params: HasegawaWakataniParameters):
|
|
112
|
+
"""Spectral right-hand side ``(d zeta_hat/dt, d n_hat/dt)``."""
|
|
113
|
+
|
|
114
|
+
phi_hat = potential_from_vorticity(zeta_hat, grid)
|
|
115
|
+
coupling = params.adiabaticity * (phi_hat - n_hat)
|
|
116
|
+
damping_zeta = params.hyperviscosity * grid.k2**2 * zeta_hat + params.friction * zeta_hat
|
|
117
|
+
damping_n = params.hyperviscosity * grid.k2**2 * n_hat + params.friction * n_hat
|
|
118
|
+
d_zeta = -_bracket_hat(phi_hat, zeta_hat, grid) + coupling - damping_zeta
|
|
119
|
+
d_n = (
|
|
120
|
+
-_bracket_hat(phi_hat, n_hat, grid)
|
|
121
|
+
- params.gradient * (1j * grid.ky) * phi_hat
|
|
122
|
+
+ coupling
|
|
123
|
+
- damping_n
|
|
124
|
+
)
|
|
125
|
+
return d_zeta, d_n
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def hw_step(zeta_hat, n_hat, grid, params, dt):
|
|
129
|
+
"""One classical RK4 step of the spectral system."""
|
|
130
|
+
|
|
131
|
+
def rhs(z, m):
|
|
132
|
+
return hw_rhs(z, m, grid, params)
|
|
133
|
+
|
|
134
|
+
k1z, k1n = rhs(zeta_hat, n_hat)
|
|
135
|
+
k2z, k2n = rhs(zeta_hat + 0.5 * dt * k1z, n_hat + 0.5 * dt * k1n)
|
|
136
|
+
k3z, k3n = rhs(zeta_hat + 0.5 * dt * k2z, n_hat + 0.5 * dt * k2n)
|
|
137
|
+
k4z, k4n = rhs(zeta_hat + dt * k3z, n_hat + dt * k3n)
|
|
138
|
+
zeta_next = zeta_hat + (dt / 6.0) * (k1z + 2 * k2z + 2 * k3z + k4z)
|
|
139
|
+
n_next = n_hat + (dt / 6.0) * (k1n + 2 * k2n + 2 * k3n + k4n)
|
|
140
|
+
return zeta_next, n_next
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def hw_run(zeta_hat, n_hat, grid, params, *, dt, steps):
|
|
144
|
+
"""Advance ``steps`` RK4 steps with a jitted ``lax.scan``; return final spectra.
|
|
145
|
+
|
|
146
|
+
``grid``, ``params``, ``dt``, and ``steps`` are captured as constants, so the
|
|
147
|
+
compiled function depends only on the two spectral state arrays.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
@jax.jit
|
|
151
|
+
def _run(zeta0, n0):
|
|
152
|
+
def body(carry, _):
|
|
153
|
+
z, m = carry
|
|
154
|
+
return hw_step(z, m, grid, params, dt), None
|
|
155
|
+
|
|
156
|
+
(zeta_final, n_final), _ = jax.lax.scan(body, (zeta0, n0), None, length=steps)
|
|
157
|
+
return zeta_final, n_final
|
|
158
|
+
|
|
159
|
+
return _run(zeta_hat, n_hat)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def hw_run_flux_history(zeta_hat, n_hat, grid, params, *, dt, steps, sample_every=1):
|
|
163
|
+
"""Advance ``steps`` RK4 steps; return final spectra and the sampled flux history.
|
|
164
|
+
|
|
165
|
+
Like :func:`hw_run`, but the jitted ``lax.scan`` also records the
|
|
166
|
+
domain-averaged radial particle flux every ``sample_every`` steps (an array
|
|
167
|
+
of length ``steps // sample_every``). Because the whole scan is JAX, any
|
|
168
|
+
reduction of the history (e.g. its time average over a saturated window) is
|
|
169
|
+
differentiable with respect to the physical parameters.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
outer = steps // sample_every
|
|
173
|
+
|
|
174
|
+
@jax.jit
|
|
175
|
+
def _run(zeta0, n0):
|
|
176
|
+
def inner(carry, _):
|
|
177
|
+
z, m = carry
|
|
178
|
+
return hw_step(z, m, grid, params, dt), None
|
|
179
|
+
|
|
180
|
+
def body(carry, _):
|
|
181
|
+
carry, _ = jax.lax.scan(inner, carry, None, length=sample_every)
|
|
182
|
+
return carry, particle_flux(carry[0], carry[1], grid)
|
|
183
|
+
|
|
184
|
+
(zeta_final, n_final), fluxes = jax.lax.scan(body, (zeta0, n0), None, length=outer)
|
|
185
|
+
return zeta_final, n_final, fluxes
|
|
186
|
+
|
|
187
|
+
return _run(zeta_hat, n_hat)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def particle_flux(zeta_hat, n_hat, grid: HasegawaWakataniGrid) -> jnp.ndarray:
|
|
191
|
+
"""Domain-averaged radial E x B particle flux ``<n v_x> = <n * (-d phi/dy)>``."""
|
|
192
|
+
|
|
193
|
+
phi_hat = potential_from_vorticity(zeta_hat, grid)
|
|
194
|
+
v_x = jnp.real(jnp.fft.ifft2(-1j * grid.ky * phi_hat))
|
|
195
|
+
density = jnp.real(jnp.fft.ifft2(n_hat))
|
|
196
|
+
return jnp.mean(density * v_x)
|
drbx/native/limiters.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def minmod3_jax(a: jnp.ndarray, b: jnp.ndarray, c: jnp.ndarray) -> jnp.ndarray:
|
|
8
|
+
same_sign = (a * b > 0.0) & (a * c > 0.0)
|
|
9
|
+
magnitude = jnp.minimum(jnp.abs(a), jnp.minimum(jnp.abs(b), jnp.abs(c)))
|
|
10
|
+
return jnp.where(same_sign, jnp.sign(a) * magnitude, 0.0)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def monotonic_centered_edges_jax(
|
|
14
|
+
center: jnp.ndarray,
|
|
15
|
+
minus: jnp.ndarray,
|
|
16
|
+
plus: jnp.ndarray,
|
|
17
|
+
) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
18
|
+
slope = minmod3_jax(2.0 * (plus - center), 0.5 * (plus - minus), 2.0 * (center - minus))
|
|
19
|
+
return center - 0.5 * slope, center + 0.5 * slope
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def periodic_monotonic_centered_edges_jax(field: jnp.ndarray, *, axis: int) -> tuple[jnp.ndarray, jnp.ndarray]:
|
|
23
|
+
center = jnp.asarray(field, dtype=jnp.float64)
|
|
24
|
+
minus = jnp.roll(center, shift=1, axis=axis)
|
|
25
|
+
plus = jnp.roll(center, shift=-1, axis=axis)
|
|
26
|
+
return monotonic_centered_edges_jax(center, minus, plus)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def minmod3_numpy(a: np.ndarray, b: np.ndarray, c: np.ndarray) -> np.ndarray:
|
|
30
|
+
same_sign = (a * b > 0.0) & (a * c > 0.0)
|
|
31
|
+
magnitude = np.minimum(np.abs(a), np.minimum(np.abs(b), np.abs(c)))
|
|
32
|
+
return np.where(same_sign, np.sign(a) * magnitude, 0.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def monotonic_centered_edges_numpy(
|
|
36
|
+
center: np.ndarray,
|
|
37
|
+
minus: np.ndarray,
|
|
38
|
+
plus: np.ndarray,
|
|
39
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
40
|
+
slope = minmod3_numpy(2.0 * (plus - center), 0.5 * (plus - minus), 2.0 * (center - minus))
|
|
41
|
+
return center - 0.5 * slope, center + 0.5 * slope
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def minmod3_scalar(a: float, b: float, c: float) -> float:
|
|
45
|
+
if (a * b <= 0.0) or (a * c <= 0.0):
|
|
46
|
+
return 0.0
|
|
47
|
+
magnitude = min(abs(a), abs(b), abs(c))
|
|
48
|
+
return float(np.sign(a) * magnitude)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def monotonic_centered_edges_scalar(center: float, minus: float, plus: float) -> tuple[float, float]:
|
|
52
|
+
slope = minmod3_scalar(
|
|
53
|
+
2.0 * (plus - center),
|
|
54
|
+
0.5 * (plus - minus),
|
|
55
|
+
2.0 * (center - minus),
|
|
56
|
+
)
|
|
57
|
+
return center - 0.5 * slope, center + 0.5 * slope
|