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
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