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,779 @@
1
+ """Slim native runner for the proved deck models.
2
+
3
+ This module supersedes the historical ``native.runner`` recycling/reference
4
+ lane. It keeps only the accuracy-tested execution branches that back the
5
+ ``drbx run`` command:
6
+
7
+ * single-component ``evolve_density`` (one-rhs),
8
+ * anomalous diffusion (:mod:`drbx.native.transport`),
9
+ * periodic fluid MMS (:mod:`drbx.native.fluid_1d`),
10
+ * electrostatic vorticity (:mod:`drbx.native.vorticity`).
11
+
12
+ It imports only kept modules and carries slim, dependency-free copies of the
13
+ portable-summary / portable-array helpers so the CLI can write run artifacts
14
+ without the removed ``parity`` package.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable, Mapping as ABCMapping
20
+ from dataclasses import dataclass, field
21
+ import json
22
+ from pathlib import Path
23
+ from typing import Any, Mapping
24
+
25
+ import jax.numpy as jnp
26
+ import numpy as np
27
+
28
+ from ..config.boutinp import BoutConfig, NumericResolver, load_bout_input
29
+ from ..runtime import runtime_numpy_dtype
30
+ from ..runtime.output import RestartBundle, build_run_event, print_run_event
31
+ from ..runtime.run_config import RunConfiguration
32
+ from .expression import ArrayExpressionEvaluator
33
+ from .fluid_1d import Fluid1DState, advance_mms_history, compute_mms_rhs, initialize_mms_state
34
+ from .mesh import (
35
+ StructuredMesh,
36
+ apply_field_boundaries,
37
+ broadcast_to_field_shape,
38
+ build_structured_mesh,
39
+ )
40
+ from .metrics import StructuredMetrics, build_structured_metrics
41
+ from .transport import advance_anomalous_diffusion_history
42
+ from .units import resolved_dataset_scalars
43
+ from .vorticity import (
44
+ advance_vorticity_history,
45
+ apply_vorticity_boundaries,
46
+ build_vorticity_operator,
47
+ compute_vorticity_rhs,
48
+ )
49
+
50
+
51
+ # --------------------------------------------------------------------------- #
52
+ # Result / restart containers (formerly native.runner_state)
53
+ # --------------------------------------------------------------------------- #
54
+ @dataclass(frozen=True)
55
+ class NativeRunResult:
56
+ payload: Mapping[str, Any]
57
+ variables: Mapping[str, Any]
58
+ time_points: tuple[float, ...]
59
+ run_config: RunConfiguration
60
+ mesh: StructuredMesh
61
+ metrics: StructuredMetrics
62
+ diagnostics: Mapping[str, Any] = field(default_factory=dict)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class NativeExecutionResult:
67
+ time_points: tuple[float, ...]
68
+ variables: Mapping[str, Any]
69
+ diagnostics: Mapping[str, Any] = field(default_factory=dict)
70
+
71
+ def __iter__(self):
72
+ yield self.time_points
73
+ yield self.variables
74
+
75
+
76
+ def coerce_native_execution_result(result: object) -> NativeExecutionResult:
77
+ if isinstance(result, NativeExecutionResult):
78
+ return result
79
+ time_points, variables = result # type: ignore[misc]
80
+ return NativeExecutionResult(time_points=tuple(time_points), variables=variables)
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class NativeRestartState:
85
+ time_offset: float
86
+ completed_steps: int
87
+ configured_timestep: float
88
+ variables: Mapping[str, np.ndarray]
89
+
90
+
91
+ def _restart_variable_names(run_config: RunConfiguration) -> tuple[str, ...]:
92
+ implementations = tuple(component.implementation for component in run_config.components)
93
+ sections = tuple(dict.fromkeys(component.section for component in run_config.components))
94
+ if implementations == ("evolve_density", "evolve_pressure", "anomalous_diffusion") and len(sections) == 1:
95
+ section = sections[0]
96
+ return (f"N{section}", f"P{section}")
97
+ if implementations == ("evolve_density", "evolve_pressure", "evolve_momentum") and len(sections) == 1:
98
+ section = sections[0]
99
+ return (f"N{section}", f"P{section}", f"NV{section}")
100
+ if implementations == ("vorticity",):
101
+ return ("Vort",)
102
+ return ()
103
+
104
+
105
+ def build_restart_state(result: NativeRunResult, *, parity_mode: str) -> RestartBundle | None:
106
+ names = _restart_variable_names(result.run_config)
107
+ if not names:
108
+ return None
109
+ dtype = runtime_numpy_dtype()
110
+ final_state = {
111
+ name: np.asarray(result.variables[name][-1], dtype=dtype)
112
+ for name in names
113
+ if name in result.variables
114
+ }
115
+ if tuple(final_state) != names:
116
+ return None
117
+ return RestartBundle(
118
+ case_name=str(result.payload.get("case_name", "run")),
119
+ parity_mode=parity_mode,
120
+ component_labels=tuple(request.label for request in result.run_config.components),
121
+ current_time=float(result.time_points[-1]) if result.time_points else 0.0,
122
+ completed_steps=max(len(result.time_points) - 1, 0),
123
+ configured_timestep=float(result.run_config.time.timestep),
124
+ state_variables=final_state,
125
+ )
126
+
127
+
128
+ # --------------------------------------------------------------------------- #
129
+ # Portable summary / array payload helpers (formerly parity.portable / arrays)
130
+ # --------------------------------------------------------------------------- #
131
+ def _summarize_array(name: str, data: np.ndarray, dimension_names: tuple[str, ...]) -> dict[str, Any]:
132
+ delta = None
133
+ if data.ndim >= 1 and data.shape[0] >= 2:
134
+ delta = float(np.max(np.abs(data[-1] - data[0])))
135
+ if len(dimension_names) == data.ndim:
136
+ dimensions = dimension_names
137
+ else:
138
+ dimensions = tuple(["t", *[f"dim_{index}" for index in range(1, data.ndim)]])
139
+ return {
140
+ "name": name,
141
+ "dimensions": list(dimensions),
142
+ "shape": [int(value) for value in data.shape],
143
+ "minimum": float(np.min(data)),
144
+ "maximum": float(np.max(data)),
145
+ "mean": float(np.mean(data)),
146
+ "max_abs_delta_last_first": delta,
147
+ }
148
+
149
+
150
+ def build_portable_summary_payload(
151
+ *,
152
+ case_name: str,
153
+ parity_mode: str,
154
+ capability_tier: str,
155
+ compare_variables: tuple[str, ...],
156
+ component_labels: tuple[str, ...],
157
+ dimensions: Mapping[str, int],
158
+ time_points: tuple[float, ...],
159
+ dataset_scalars: Mapping[str, float],
160
+ variables: Mapping[str, Any],
161
+ overrides: tuple[str, ...] = (),
162
+ configured_nout: int | None = None,
163
+ configured_timestep: float | None = None,
164
+ producer: str = "drbx",
165
+ ) -> dict[str, Any]:
166
+ summary_dimensions = tuple(dimensions)
167
+ summaries = {
168
+ name: _summarize_array(name, np.asarray(variables[name], dtype=np.float64), summary_dimensions)
169
+ for name in compare_variables
170
+ if name in variables
171
+ }
172
+ payload: dict[str, Any] = {
173
+ "case_name": case_name,
174
+ "parity_mode": parity_mode,
175
+ "capability_tier": capability_tier,
176
+ "producer": producer,
177
+ "overrides": list(overrides),
178
+ "compare_variables": list(compare_variables),
179
+ "component_labels": list(component_labels),
180
+ "dimensions": dict(dimensions),
181
+ "time_points": list(time_points),
182
+ "dataset_scalars": {key: float(value) for key, value in dataset_scalars.items()},
183
+ "variable_summaries": summaries,
184
+ "effective_output_points": len(time_points),
185
+ }
186
+ if configured_nout is not None:
187
+ payload["configured_nout"] = configured_nout
188
+ if configured_timestep is not None:
189
+ payload["configured_timestep"] = configured_timestep
190
+ return payload
191
+
192
+
193
+ def write_portable_summary_payload(payload: Mapping[str, Any], path: str | Path) -> Path:
194
+ target = Path(path)
195
+ target.parent.mkdir(parents=True, exist_ok=True)
196
+ target.write_text(json.dumps(dict(payload), indent=2, sort_keys=True), encoding="utf-8")
197
+ return target
198
+
199
+
200
+ def build_portable_array_payload(
201
+ *,
202
+ case_name: str,
203
+ parity_mode: str,
204
+ capability_tier: str,
205
+ compare_variables: tuple[str, ...],
206
+ component_labels: tuple[str, ...],
207
+ dimensions: Mapping[str, int],
208
+ time_points: tuple[float, ...],
209
+ dataset_scalars: Mapping[str, float],
210
+ variables: Mapping[str, Any],
211
+ variable_dimensions: Mapping[str, tuple[str, ...]] | None = None,
212
+ overrides: tuple[str, ...] = (),
213
+ configured_nout: int | None = None,
214
+ configured_timestep: float | None = None,
215
+ producer: str = "drbx",
216
+ ) -> dict[str, Any]:
217
+ summary_dimensions = tuple(dimensions)
218
+ payload_variables: dict[str, np.ndarray] = {}
219
+ payload_variable_dimensions: dict[str, list[str]] = {}
220
+ for name in compare_variables:
221
+ if name not in variables:
222
+ continue
223
+ array = np.asarray(variables[name], dtype=np.float64)
224
+ payload_variables[name] = array
225
+ if variable_dimensions is not None and name in variable_dimensions:
226
+ dims = variable_dimensions[name]
227
+ elif len(summary_dimensions) == array.ndim:
228
+ dims = summary_dimensions
229
+ else:
230
+ dims = tuple(["t", *[f"dim_{index}" for index in range(1, array.ndim)]])
231
+ payload_variable_dimensions[name] = list(dims)
232
+
233
+ payload: dict[str, Any] = {
234
+ "case_name": case_name,
235
+ "parity_mode": parity_mode,
236
+ "capability_tier": capability_tier,
237
+ "producer": producer,
238
+ "overrides": list(overrides),
239
+ "compare_variables": list(compare_variables),
240
+ "component_labels": list(component_labels),
241
+ "dimensions": dict(dimensions),
242
+ "time_points": list(time_points),
243
+ "dataset_scalars": {key: float(value) for key, value in dataset_scalars.items()},
244
+ "variable_dimensions": payload_variable_dimensions,
245
+ "variables": payload_variables,
246
+ "effective_output_points": len(time_points),
247
+ }
248
+ if configured_nout is not None:
249
+ payload["configured_nout"] = configured_nout
250
+ if configured_timestep is not None:
251
+ payload["configured_timestep"] = configured_timestep
252
+ return payload
253
+
254
+
255
+ def write_portable_array_payload(payload: Mapping[str, Any], path: str | Path) -> Path:
256
+ target = Path(path)
257
+ target.parent.mkdir(parents=True, exist_ok=True)
258
+ metadata = {key: value for key, value in payload.items() if key != "variables"}
259
+ arrays = {
260
+ f"var__{name}": np.asarray(value, dtype=np.float64)
261
+ for name, value in payload.get("variables", {}).items()
262
+ }
263
+ np.savez_compressed(target, __metadata__=json.dumps(metadata, sort_keys=True), **arrays)
264
+ return target
265
+
266
+
267
+ def load_portable_array_payload(path: str | Path) -> dict[str, Any]:
268
+ with np.load(Path(path), allow_pickle=False) as payload:
269
+ metadata = json.loads(str(payload["__metadata__"]))
270
+ variables = {
271
+ key.removeprefix("var__"): np.asarray(payload[key], dtype=np.float64)
272
+ for key in payload.files
273
+ if key.startswith("var__")
274
+ }
275
+ metadata["variables"] = variables
276
+ return metadata
277
+
278
+
279
+ # --------------------------------------------------------------------------- #
280
+ # Execution helpers
281
+ # --------------------------------------------------------------------------- #
282
+ def _effective_output_steps(parity_mode: str, *, configured_nout: int) -> int:
283
+ if parity_mode == "one_rhs":
284
+ return 0
285
+ if parity_mode == "one_step":
286
+ return 1
287
+ return configured_nout
288
+
289
+
290
+ def _default_overrides(parity_mode: str) -> tuple[str, ...]:
291
+ if parity_mode == "one_rhs":
292
+ return ("nout=0",)
293
+ if parity_mode == "one_step":
294
+ return ("nout=1",)
295
+ return ()
296
+
297
+
298
+ def _prepare_compare_variables(
299
+ variables: Mapping[str, Any],
300
+ mesh: StructuredMesh,
301
+ *,
302
+ trim_x_guards: bool,
303
+ trim_y_guards: bool,
304
+ ) -> dict[str, Any]:
305
+ prepared: dict[str, Any] = {}
306
+ for name, value in variables.items():
307
+ array = np.asarray(value, dtype=np.float64)
308
+ if trim_x_guards and array.ndim >= 4 and array.shape[1] > 2 * mesh.mxg:
309
+ array = array[:, mesh.mxg : -mesh.mxg, ...]
310
+ if trim_y_guards and array.ndim >= 4 and array.shape[2] > 2 * mesh.myg:
311
+ array = array[:, :, mesh.myg : -mesh.myg, ...]
312
+ prepared[name] = array
313
+ return prepared
314
+
315
+
316
+ # --------------------------------------------------------------------------- #
317
+ # Public run entry points
318
+ # --------------------------------------------------------------------------- #
319
+ def run_input_case(
320
+ input_path: str | Path,
321
+ *,
322
+ case_name: str | None = None,
323
+ parity_mode: str = "manual",
324
+ compare_variables: tuple[str, ...] = (),
325
+ restart_state: NativeRestartState | None = None,
326
+ output_steps: int | None = None,
327
+ verbose: bool = False,
328
+ verbosity: str = "detailed",
329
+ event_logger: Callable[[ABCMapping[str, Any]], None] | None = None,
330
+ ) -> NativeRunResult:
331
+ config = load_bout_input(input_path)
332
+ resolved_case_name = case_name or Path(input_path).stem
333
+ return run_config_case(
334
+ config,
335
+ case_name=resolved_case_name,
336
+ parity_mode=parity_mode,
337
+ compare_variables=compare_variables,
338
+ restart_state=restart_state,
339
+ output_steps=output_steps,
340
+ verbose=verbose,
341
+ verbosity=verbosity,
342
+ event_logger=event_logger,
343
+ )
344
+
345
+
346
+ def run_config_case(
347
+ config: BoutConfig,
348
+ *,
349
+ case_name: str,
350
+ parity_mode: str,
351
+ compare_variables: tuple[str, ...] = (),
352
+ restart_state: NativeRestartState | None = None,
353
+ output_steps: int | None = None,
354
+ verbose: bool = False,
355
+ verbosity: str = "detailed",
356
+ event_logger: Callable[[ABCMapping[str, Any]], None] | None = None,
357
+ ) -> NativeRunResult:
358
+ event_sink = event_logger
359
+ if event_sink is None and verbose:
360
+ event_sink = lambda event: print_run_event(event, verbosity=verbosity)
361
+
362
+ def emit(stage: str, message: str, **details: Any) -> None:
363
+ if event_sink is None:
364
+ return
365
+ event_sink(build_run_event(stage=stage, message=message, details=details or None))
366
+
367
+ run_config = RunConfiguration.from_config(config)
368
+ emit(
369
+ "configuration",
370
+ "Resolved native run configuration",
371
+ case_name=case_name,
372
+ parity_mode=parity_mode,
373
+ capability_tier="native_exact",
374
+ nout=run_config.time.nout,
375
+ timestep=run_config.time.timestep,
376
+ components=",".join(component.label for component in run_config.components),
377
+ restart=restart_state is not None,
378
+ )
379
+ mesh = build_structured_mesh(config, run_config)
380
+ metrics = build_structured_metrics(config, run_config, mesh)
381
+ emit(
382
+ "mesh",
383
+ "Built structured mesh and metrics",
384
+ nx=mesh.nx,
385
+ ny=mesh.local_ny,
386
+ nz=mesh.nz,
387
+ mxg=mesh.mxg,
388
+ myg=mesh.myg,
389
+ file=run_config.mesh.file or "<analytic mesh>",
390
+ )
391
+ execution = coerce_native_execution_result(
392
+ _execute_supported_case(
393
+ config,
394
+ run_config,
395
+ mesh,
396
+ metrics,
397
+ parity_mode=parity_mode,
398
+ restart_state=restart_state,
399
+ output_steps=output_steps,
400
+ )
401
+ )
402
+ time_points = execution.time_points
403
+ variables = execution.variables
404
+ emit(
405
+ "run",
406
+ "Native solver completed",
407
+ stored_states=len(time_points),
408
+ compare_variables=",".join(compare_variables or tuple(variables)),
409
+ )
410
+ compare_names = compare_variables or tuple(variables)
411
+ trimmed_variables = _prepare_compare_variables(
412
+ variables,
413
+ mesh,
414
+ trim_x_guards=False,
415
+ trim_y_guards=False,
416
+ )
417
+ dataset_scalars = resolved_dataset_scalars(run_config)
418
+ payload = build_portable_summary_payload(
419
+ case_name=case_name,
420
+ parity_mode=parity_mode,
421
+ capability_tier="native_exact",
422
+ compare_variables=compare_names,
423
+ component_labels=tuple(component.label for component in run_config.components),
424
+ dimensions={"t": len(time_points), "x": mesh.nx, "y": mesh.local_ny, "z": mesh.nz},
425
+ time_points=time_points,
426
+ dataset_scalars=dataset_scalars,
427
+ variables={name: np.asarray(value, dtype=np.float64) for name, value in trimmed_variables.items()},
428
+ overrides=_default_overrides(parity_mode),
429
+ configured_nout=run_config.time.nout,
430
+ configured_timestep=run_config.time.timestep,
431
+ producer="drbx",
432
+ )
433
+ emit(
434
+ "summary",
435
+ "Prepared portable summary payload",
436
+ variables=",".join(sorted(trimmed_variables)),
437
+ time_start=time_points[0] if time_points else 0.0,
438
+ time_end=time_points[-1] if time_points else 0.0,
439
+ )
440
+ return NativeRunResult(
441
+ payload=payload,
442
+ variables=trimmed_variables,
443
+ time_points=time_points,
444
+ run_config=run_config,
445
+ mesh=mesh,
446
+ metrics=metrics,
447
+ diagnostics=execution.diagnostics,
448
+ )
449
+
450
+
451
+ def _execute_supported_case(
452
+ config: BoutConfig,
453
+ run_config: RunConfiguration,
454
+ mesh: StructuredMesh,
455
+ metrics: StructuredMetrics,
456
+ *,
457
+ parity_mode: str,
458
+ restart_state: NativeRestartState | None = None,
459
+ output_steps: int | None = None,
460
+ ) -> NativeExecutionResult | tuple[tuple[float, ...], dict[str, Any]]:
461
+ implementations = tuple(component.implementation for component in run_config.components)
462
+ if len(run_config.components) == 1 and implementations == ("evolve_density",):
463
+ component = run_config.components[0]
464
+ variable_name = f"N{component.section}"
465
+ if parity_mode != "one_rhs":
466
+ raise NotImplementedError(
467
+ "Native single-component density execution currently supports one_rhs parity only."
468
+ )
469
+ field_values = _initialize_species_field(config, variable_name, mesh)
470
+ return (0.0,), {variable_name: field_values[None, ...]}
471
+
472
+ if _is_supported_diffusion_case(run_config):
473
+ return _execute_diffusion_case(
474
+ config,
475
+ run_config,
476
+ mesh,
477
+ metrics,
478
+ parity_mode=parity_mode,
479
+ restart_state=restart_state,
480
+ output_steps=output_steps,
481
+ )
482
+
483
+ if _is_supported_periodic_fluid_mms_case(config, run_config, mesh, metrics):
484
+ return _execute_periodic_fluid_mms_case(
485
+ config,
486
+ run_config,
487
+ mesh,
488
+ metrics,
489
+ parity_mode=parity_mode,
490
+ restart_state=restart_state,
491
+ output_steps=output_steps,
492
+ )
493
+
494
+ if _is_supported_electrostatic_vorticity_case(config, run_config, mesh, metrics):
495
+ return _execute_electrostatic_vorticity_case(
496
+ config,
497
+ run_config,
498
+ mesh,
499
+ metrics,
500
+ parity_mode=parity_mode,
501
+ restart_state=restart_state,
502
+ output_steps=output_steps,
503
+ )
504
+
505
+ raise NotImplementedError(
506
+ "Native execution is not implemented for the configured component set: "
507
+ + ", ".join(component.label for component in run_config.components)
508
+ )
509
+
510
+
511
+ def _execute_diffusion_case(
512
+ config: BoutConfig,
513
+ run_config: RunConfiguration,
514
+ mesh: StructuredMesh,
515
+ metrics: StructuredMetrics,
516
+ *,
517
+ parity_mode: str,
518
+ restart_state: NativeRestartState | None = None,
519
+ output_steps: int | None = None,
520
+ ) -> tuple[tuple[float, ...], dict[str, Any]]:
521
+ dtype = runtime_numpy_dtype()
522
+ section = run_config.components[0].section
523
+ density_name = f"N{section}"
524
+ pressure_name = f"P{section}"
525
+ if restart_state is None:
526
+ density = _initialize_species_field(config, density_name, mesh)
527
+ pressure = _initialize_species_field(config, pressure_name, mesh)
528
+ time_offset = 0.0
529
+ else:
530
+ density = np.asarray(restart_state.variables[density_name], dtype=dtype)
531
+ pressure = np.asarray(restart_state.variables[pressure_name], dtype=dtype)
532
+ time_offset = restart_state.time_offset
533
+ if not np.allclose(np.asarray(density), np.asarray(pressure), rtol=1e-12, atol=1e-12):
534
+ raise NotImplementedError(
535
+ "Native anomalous diffusion currently requires identical density and pressure initial states."
536
+ )
537
+ if not np.allclose(np.asarray(metrics.g23), 0.0, rtol=1e-12, atol=1e-12):
538
+ raise NotImplementedError("Native anomalous diffusion currently supports g23 = 0 structured metrics only.")
539
+ density_boundary = _field_boundary_kind(config, density_name)
540
+ pressure_boundary = _field_boundary_kind(config, pressure_name)
541
+
542
+ if bool(config.parsed(section, "thermal_conduction")) if config.has_option(section, "thermal_conduction") else True:
543
+ raise NotImplementedError("Native one-step anomalous diffusion currently requires thermal_conduction = false.")
544
+
545
+ resolver = NumericResolver(config)
546
+ scalars = resolved_dataset_scalars(run_config)
547
+ anomalous_D = resolver.resolve(section, "anomalous_D") / (scalars["rho_s0"] * scalars["rho_s0"] * scalars["Omega_ci"])
548
+ if config.has_option(section, "anomalous_chi") and abs(resolver.resolve(section, "anomalous_chi")) > 0.0:
549
+ raise NotImplementedError("Native one-step anomalous diffusion does not yet support anomalous_chi.")
550
+ if config.has_option(section, "anomalous_nu") and abs(resolver.resolve(section, "anomalous_nu")) > 0.0:
551
+ raise NotImplementedError("Native one-step anomalous diffusion does not yet support anomalous_nu.")
552
+
553
+ steps = output_steps if output_steps is not None else _effective_output_steps(parity_mode, configured_nout=run_config.time.nout)
554
+ history = advance_anomalous_diffusion_history(
555
+ density,
556
+ pressure,
557
+ mesh=mesh,
558
+ metrics=metrics,
559
+ anomalous_D=anomalous_D,
560
+ density_boundary=density_boundary,
561
+ pressure_boundary=pressure_boundary,
562
+ timestep=run_config.time.timestep,
563
+ steps=steps,
564
+ )
565
+ time_points = tuple(time_offset + run_config.time.timestep * index for index in range(steps + 1))
566
+ return time_points, {
567
+ density_name: np.asarray(history.density_history, dtype=dtype),
568
+ pressure_name: np.asarray(history.pressure_history, dtype=dtype),
569
+ }
570
+
571
+
572
+ def _initialize_species_field(config: BoutConfig, variable_name: str, mesh: StructuredMesh) -> Any:
573
+ if not config.has_section(variable_name):
574
+ raise KeyError(f"Missing initial condition section for {variable_name}.")
575
+ if config.has_option(variable_name, "function"):
576
+ option_name = "function"
577
+ elif config.has_option(variable_name, "solution"):
578
+ option_name = "solution"
579
+ else:
580
+ raise KeyError(f"Missing initial condition function or solution for {variable_name}.")
581
+ evaluator = ArrayExpressionEvaluator(config, local_values=mesh.expression_context())
582
+ field_values = broadcast_to_field_shape(
583
+ evaluator.evaluate(config.raw(variable_name, option_name), current_section=variable_name),
584
+ mesh,
585
+ )
586
+ field_values = apply_field_boundaries(field_values, mesh, x_boundary=_field_boundary_kind(config, variable_name))
587
+ return field_values
588
+
589
+
590
+ def _field_boundary_kind(config: BoutConfig, variable_name: str) -> str:
591
+ if config.has_option(variable_name, "bndry_all"):
592
+ return str(config.parsed(variable_name, "bndry_all")).strip().lower()
593
+ return "dirichlet_zero"
594
+
595
+
596
+ def _is_supported_diffusion_case(run_config: RunConfiguration) -> bool:
597
+ implementations = tuple(component.implementation for component in run_config.components)
598
+ if implementations != ("evolve_density", "evolve_pressure", "anomalous_diffusion"):
599
+ return False
600
+ sections = {component.section for component in run_config.components}
601
+ return len(sections) == 1
602
+
603
+
604
+ def _execute_periodic_fluid_mms_case(
605
+ config: BoutConfig,
606
+ run_config: RunConfiguration,
607
+ mesh: StructuredMesh,
608
+ metrics: StructuredMetrics,
609
+ *,
610
+ parity_mode: str,
611
+ restart_state: NativeRestartState | None = None,
612
+ output_steps: int | None = None,
613
+ ) -> tuple[tuple[float, ...], dict[str, Any]]:
614
+ section = run_config.components[0].section
615
+ atomic_mass = float(config.parsed(section, "AA"))
616
+ if parity_mode == "one_rhs":
617
+ state = initialize_mms_state(config, section=section, mesh=mesh)
618
+ rhs = compute_mms_rhs(config, state, section=section, mesh=mesh, metrics=metrics, atomic_mass=atomic_mass, time=0.0)
619
+ return (0.0,), {
620
+ f"ddt(N{section})": np.asarray(rhs.density[None, ...], dtype=np.float64),
621
+ f"ddt(P{section})": np.asarray(rhs.pressure[None, ...], dtype=np.float64),
622
+ f"ddt(NV{section})": np.asarray(rhs.momentum[None, ...], dtype=np.float64),
623
+ }
624
+
625
+ steps = output_steps if output_steps is not None else _effective_output_steps(parity_mode, configured_nout=run_config.time.nout)
626
+ initial_state = (
627
+ None
628
+ if restart_state is None
629
+ else Fluid1DState(
630
+ density=jnp.asarray(restart_state.variables[f"N{section}"], dtype=jnp.float64),
631
+ pressure=jnp.asarray(restart_state.variables[f"P{section}"], dtype=jnp.float64),
632
+ momentum=jnp.asarray(restart_state.variables[f"NV{section}"], dtype=jnp.float64),
633
+ )
634
+ )
635
+ time_offset = 0.0 if restart_state is None else restart_state.time_offset
636
+ history = advance_mms_history(
637
+ config,
638
+ section=section,
639
+ mesh=mesh,
640
+ metrics=metrics,
641
+ atomic_mass=atomic_mass,
642
+ timestep=run_config.time.timestep,
643
+ steps=steps,
644
+ substeps=20,
645
+ initial_state=initial_state,
646
+ start_time=time_offset,
647
+ )
648
+ time_points = tuple(time_offset + run_config.time.timestep * index for index in range(steps + 1))
649
+ return time_points, {
650
+ f"N{section}": np.asarray(history.density_history, dtype=np.float64),
651
+ f"P{section}": np.asarray(history.pressure_history, dtype=np.float64),
652
+ f"NV{section}": np.asarray(history.momentum_history, dtype=np.float64),
653
+ }
654
+
655
+
656
+ def _execute_electrostatic_vorticity_case(
657
+ config: BoutConfig,
658
+ run_config: RunConfiguration,
659
+ mesh: StructuredMesh,
660
+ metrics: StructuredMetrics,
661
+ *,
662
+ parity_mode: str,
663
+ restart_state: NativeRestartState | None = None,
664
+ output_steps: int | None = None,
665
+ ) -> tuple[tuple[float, ...], dict[str, Any]]:
666
+ initial = (
667
+ apply_vorticity_boundaries(_initialize_species_field(config, "Vort", mesh), mesh)
668
+ if restart_state is None
669
+ else apply_vorticity_boundaries(np.asarray(restart_state.variables["Vort"], dtype=np.float64), mesh)
670
+ )
671
+ time_offset = 0.0 if restart_state is None else restart_state.time_offset
672
+ average_atomic_mass = float(config.parsed("vorticity", "average_atomic_mass")) if config.has_option("vorticity", "average_atomic_mass") else 2.0
673
+ operator = build_vorticity_operator(mesh=mesh, metrics=metrics, average_atomic_mass=average_atomic_mass)
674
+
675
+ if parity_mode == "one_rhs":
676
+ rhs = compute_vorticity_rhs(initial, mesh=mesh, metrics=metrics, operator=operator)
677
+ return (0.0,), {"ddt(Vort)": np.asarray(rhs.vorticity[None, ...], dtype=np.float64)}
678
+
679
+ steps = output_steps if output_steps is not None else _effective_output_steps(parity_mode, configured_nout=run_config.time.nout)
680
+ history = advance_vorticity_history(
681
+ initial,
682
+ mesh=mesh,
683
+ metrics=metrics,
684
+ operator=operator,
685
+ timestep=run_config.time.timestep,
686
+ steps=steps,
687
+ start_time=time_offset,
688
+ rtol=1e-6,
689
+ atol=1e-8,
690
+ mxstep=20000,
691
+ )
692
+ time_points = tuple(time_offset + run_config.time.timestep * index for index in range(steps + 1))
693
+ return time_points, {
694
+ "Vort": np.asarray(history.vorticity_history, dtype=np.float64),
695
+ "phi": np.asarray(history.potential_history, dtype=np.float64),
696
+ }
697
+
698
+
699
+ def _is_supported_periodic_fluid_mms_case(
700
+ config: BoutConfig,
701
+ run_config: RunConfiguration,
702
+ mesh: StructuredMesh,
703
+ metrics: StructuredMetrics,
704
+ ) -> bool:
705
+ implementations = tuple(component.implementation for component in run_config.components)
706
+ if implementations != ("evolve_density", "evolve_pressure", "evolve_momentum"):
707
+ return False
708
+ if len({component.section for component in run_config.components}) != 1:
709
+ return False
710
+ section = run_config.components[0].section
711
+ if run_config.mesh.nx != 1 or run_config.mesh.nz != 1:
712
+ return False
713
+ if run_config.solver.mms is not True:
714
+ return False
715
+ if bool(config.parsed(section, "thermal_conduction")) if config.has_option(section, "thermal_conduction") else True:
716
+ return False
717
+ if bool(config.parsed(section, "p_div_v")) if config.has_option(section, "p_div_v") else False:
718
+ return False
719
+ if not _uniform_identity_parallel_metric(mesh, metrics=metrics):
720
+ return False
721
+ return all(
722
+ config.has_section(name) and config.has_option(name, "solution") and config.has_option(name, "source")
723
+ for name in (f"N{section}", f"P{section}", f"NV{section}")
724
+ )
725
+
726
+
727
+ def _is_supported_electrostatic_vorticity_case(
728
+ config: BoutConfig,
729
+ run_config: RunConfiguration,
730
+ mesh: StructuredMesh,
731
+ metrics: StructuredMetrics,
732
+ ) -> bool:
733
+ if tuple(component.implementation for component in run_config.components) != ("vorticity",):
734
+ return False
735
+ if run_config.mesh.ny != 1 or run_config.mesh.myg != 0:
736
+ return False
737
+ if mesh.mxg != 2:
738
+ return False
739
+ if not config.has_section("Vort") or not config.has_option("Vort", "function"):
740
+ return False
741
+ option_defaults = {
742
+ "diamagnetic": False,
743
+ "diamagnetic_polarisation": False,
744
+ "bndry_flux": False,
745
+ "poloidal_flows": False,
746
+ "split_n0": False,
747
+ "phi_dissipation": False,
748
+ "vort_dissipation": False,
749
+ "collisional_friction": False,
750
+ "phi_boundary_relax": False,
751
+ "phi_sheath_dissipation": False,
752
+ "damp_core_vorticity": False,
753
+ }
754
+ for key, expected in option_defaults.items():
755
+ value = bool(config.parsed("vorticity", key)) if config.has_option("vorticity", key) else (True if key == "phi_dissipation" else False)
756
+ if value != expected:
757
+ return False
758
+ exb_advection = bool(config.parsed("vorticity", "exb_advection")) if config.has_option("vorticity", "exb_advection") else True
759
+ exb_advection_simplified = (
760
+ bool(config.parsed("vorticity", "exb_advection_simplified"))
761
+ if config.has_option("vorticity", "exb_advection_simplified")
762
+ else True
763
+ )
764
+ if not exb_advection or not exb_advection_simplified:
765
+ return False
766
+ if not np.allclose(np.asarray(metrics.g23), 0.0, rtol=1e-12, atol=1e-12):
767
+ return False
768
+ return True
769
+
770
+
771
+ def _uniform_identity_parallel_metric(mesh: StructuredMesh, *, metrics: StructuredMetrics) -> bool:
772
+ if not np.allclose(np.asarray(metrics.J), 1.0, rtol=1e-12, atol=1e-12):
773
+ return False
774
+ if not np.allclose(np.asarray(metrics.g22), 1.0, rtol=1e-12, atol=1e-12):
775
+ return False
776
+ if not np.allclose(np.asarray(metrics.g23), 0.0, rtol=1e-12, atol=1e-12):
777
+ return False
778
+ dy = np.asarray(metrics.dy[:, mesh.ystart : mesh.yend + 1, :], dtype=np.float64)
779
+ return np.allclose(dy, dy[:, :1, :], rtol=1e-12, atol=1e-12)