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.
Files changed (106) hide show
  1. drbx/__init__.py +20 -0
  2. drbx/__main__.py +5 -0
  3. drbx/cli.py +586 -0
  4. drbx/config/__init__.py +20 -0
  5. drbx/config/boutinp.py +556 -0
  6. drbx/config/model.py +34 -0
  7. drbx/config/normalization.py +66 -0
  8. drbx/data/atomic_rates/__init__.py +1 -0
  9. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
  10. drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
  11. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
  12. drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
  13. drbx/geometry/__init__.py +207 -0
  14. drbx/geometry/embedding.py +56 -0
  15. drbx/geometry/essos_import.py +1385 -0
  16. drbx/geometry/fci_geometry.py +4622 -0
  17. drbx/geometry/fci_maps.py +85 -0
  18. drbx/geometry/island_divertor.py +291 -0
  19. drbx/geometry/metric_tensor.py +99 -0
  20. drbx/geometry/open_slab.py +150 -0
  21. drbx/geometry/rotating_ellipse.py +253 -0
  22. drbx/geometry/shifted_torus.py +225 -0
  23. drbx/geometry/stellarator.py +287 -0
  24. drbx/geometry/vmec_extender_import.py +499 -0
  25. drbx/geometry/vmec_jax_import.py +306 -0
  26. drbx/linear/__init__.py +37 -0
  27. drbx/linear/dispersion.py +138 -0
  28. drbx/linear/eigen.py +91 -0
  29. drbx/native/__init__.py +224 -0
  30. drbx/native/array_backend.py +64 -0
  31. drbx/native/deck_runner.py +779 -0
  32. drbx/native/electromagnetic.py +250 -0
  33. drbx/native/expression.py +173 -0
  34. drbx/native/fci.py +295 -0
  35. drbx/native/fci_2_field_rhs.py +182 -0
  36. drbx/native/fci_4_field_rhs.py +1267 -0
  37. drbx/native/fci_boundaries.py +2494 -0
  38. drbx/native/fci_differentiable_case.py +304 -0
  39. drbx/native/fci_drb_EB_rhs.py +1243 -0
  40. drbx/native/fci_drb_rhs.py +190 -0
  41. drbx/native/fci_halo.py +1575 -0
  42. drbx/native/fci_helpers.py +350 -0
  43. drbx/native/fci_model.py +294 -0
  44. drbx/native/fci_neutral.py +139 -0
  45. drbx/native/fci_operators.py +4081 -0
  46. drbx/native/fci_sharding.py +597 -0
  47. drbx/native/fci_sheath_recycling.py +206 -0
  48. drbx/native/fci_time_integrator.py +96 -0
  49. drbx/native/fci_vorticity.py +198 -0
  50. drbx/native/fluid_1d.py +330 -0
  51. drbx/native/hasegawa_wakatani.py +196 -0
  52. drbx/native/limiters.py +57 -0
  53. drbx/native/mesh.py +238 -0
  54. drbx/native/metrics.py +234 -0
  55. drbx/native/neutrals/__init__.py +58 -0
  56. drbx/native/neutrals/atomic_rates.py +134 -0
  57. drbx/native/neutrals/detachment_sol_model.py +221 -0
  58. drbx/native/neutrals/reactions.py +164 -0
  59. drbx/native/neutrals/recycling_sol_model.py +197 -0
  60. drbx/native/sol_flux_tube.py +133 -0
  61. drbx/native/stellarator_turbulence.py +343 -0
  62. drbx/native/transport.py +134 -0
  63. drbx/native/units.py +32 -0
  64. drbx/native/vorticity.py +252 -0
  65. drbx/runtime/__init__.py +53 -0
  66. drbx/runtime/artifacts.py +161 -0
  67. drbx/runtime/memory.py +144 -0
  68. drbx/runtime/output.py +374 -0
  69. drbx/runtime/paths.py +9 -0
  70. drbx/runtime/performance.py +161 -0
  71. drbx/runtime/run_config.py +184 -0
  72. drbx/runtime/scheduler.py +99 -0
  73. drbx/runtime/state.py +40 -0
  74. drbx/validation/__init__.py +424 -0
  75. drbx/validation/autodiff_diffusion.py +329 -0
  76. drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
  77. drbx/validation/diverted_tokamak_movie.py +558 -0
  78. drbx/validation/essos_fieldline_import_campaign.py +181 -0
  79. drbx/validation/essos_imported_artifact_audit.py +535 -0
  80. drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
  81. drbx/validation/essos_imported_fci_campaign.py +5241 -0
  82. drbx/validation/essos_imported_pytree_campaign.py +406 -0
  83. drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
  84. drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
  85. drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
  86. drbx/validation/fluid_1d_mms_convergence.py +250 -0
  87. drbx/validation/geometry_lineouts.py +136 -0
  88. drbx/validation/geometry_slices.py +178 -0
  89. drbx/validation/publication_plotting.py +91 -0
  90. drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
  91. drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
  92. drbx/validation/stellarator_fci_operator_campaign.py +304 -0
  93. drbx/validation/stellarator_fci_suite_campaign.py +264 -0
  94. drbx/validation/stellarator_metric_mms_campaign.py +289 -0
  95. drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
  96. drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
  97. drbx/validation/stellarator_sol_showcase.py +628 -0
  98. drbx/validation/stellarator_vorticity_campaign.py +304 -0
  99. drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
  100. drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
  101. drbx-2.0.0.dev0.dist-info/METADATA +380 -0
  102. drbx-2.0.0.dev0.dist-info/RECORD +106 -0
  103. drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
  104. drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
  105. drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
  106. drbx-2.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -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)
@@ -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