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/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ from .runtime import configure_jax_runtime
2
+
3
+ # Configure the default precision/cache policy as early as possible so user
4
+ # imports that touch JAX arrays through drbx keep the requested dtype.
5
+ configure_jax_runtime()
6
+
7
+ from .config.boutinp import BoutConfig, NumericResolver, ROOT_SECTION, load_bout_input, parse_bout_input
8
+ from .config.normalization import MetricPolicy, ModelNormalization
9
+
10
+ __all__ = [
11
+ "BoutConfig",
12
+ "MetricPolicy",
13
+ "ModelNormalization",
14
+ "NumericResolver",
15
+ "ROOT_SECTION",
16
+ "load_bout_input",
17
+ "parse_bout_input",
18
+ ]
19
+
20
+ __version__ = "2.0.0.dev0"
drbx/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
drbx/cli.py ADDED
@@ -0,0 +1,586 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ from pathlib import Path
6
+ import platform
7
+ import sys
8
+ import time
9
+ from typing import Any, Mapping
10
+
11
+ from .config.boutinp import load_bout_input
12
+ from .runtime import configure_jax_runtime, resolve_runtime_precision
13
+ from .runtime.run_config import RunConfiguration
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ normalized_argv = _normalize_cli_argv(sys.argv[1:] if argv is None else argv)
18
+ parser = _build_parser()
19
+ args = parser.parse_args(normalized_argv)
20
+ return args.command(args)
21
+
22
+
23
+ def _build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="drbx",
26
+ description="Inspect or run JAX-DRB inputs using the native model configuration structure.",
27
+ )
28
+ subparsers = parser.add_subparsers(dest="subcommand", required=False)
29
+
30
+ inspect_parser = subparsers.add_parser(
31
+ "inspect", help="Inspect an input deck and print the resolved plan."
32
+ )
33
+ inspect_parser.add_argument("input_file", type=Path)
34
+ inspect_parser.set_defaults(command=_inspect_command)
35
+
36
+ run_parser = subparsers.add_parser(
37
+ "run",
38
+ help="Run a supported native input, write result artifacts, and optionally continue from a restart bundle.",
39
+ )
40
+ run_parser.add_argument("input_file", type=Path)
41
+ run_parser.add_argument(
42
+ "--dry-run",
43
+ action="store_true",
44
+ help="Only inspect configuration and exit successfully.",
45
+ )
46
+ run_parser.add_argument(
47
+ "--precision",
48
+ choices=("float32", "float64"),
49
+ default=None,
50
+ help="Override runtime floating-point precision for this run.",
51
+ )
52
+ run_parser.add_argument(
53
+ "--case-name",
54
+ type=str,
55
+ default=None,
56
+ help="Optional case label for output metadata.",
57
+ )
58
+ run_parser.add_argument(
59
+ "--output-dir",
60
+ type=Path,
61
+ default=None,
62
+ help="Write standard run artifacts into this directory.",
63
+ )
64
+ run_parser.add_argument(
65
+ "--json-out", type=Path, default=None, help="Write the portable summary JSON."
66
+ )
67
+ run_parser.add_argument(
68
+ "--arrays-out", type=Path, default=None, help="Write the portable array NPZ."
69
+ )
70
+ run_parser.add_argument(
71
+ "--restart-out", type=Path, default=None, help="Write the restart NPZ bundle."
72
+ )
73
+ run_parser.add_argument(
74
+ "--log-out", type=Path, default=None, help="Write a verbose run log JSON."
75
+ )
76
+ run_parser.add_argument(
77
+ "--restart-in",
78
+ type=Path,
79
+ default=None,
80
+ help="Resume from a previously written restart NPZ bundle.",
81
+ )
82
+ run_parser.add_argument(
83
+ "--resume-steps",
84
+ type=int,
85
+ default=None,
86
+ help="Additional output intervals to run after loading --restart-in.",
87
+ )
88
+ run_parser.add_argument(
89
+ "--verbose",
90
+ action="store_true",
91
+ help="Emit detailed staged terminal output for this run.",
92
+ )
93
+ run_parser.add_argument(
94
+ "--quiet", action="store_true", help="Suppress the pretty terminal run summary."
95
+ )
96
+ run_parser.set_defaults(command=_run_command)
97
+
98
+ parser.set_defaults(command=_default_command)
99
+ return parser
100
+
101
+
102
+ def _default_command(args: argparse.Namespace) -> int:
103
+ if getattr(args, "subcommand", None) is None:
104
+ raise SystemExit(
105
+ "Use `drbx inspect <input>` or `drbx <input> --dry-run`."
106
+ )
107
+ return args.command(args)
108
+
109
+
110
+ def _normalize_cli_argv(argv: list[str]) -> list[str]:
111
+ if not argv:
112
+ return argv
113
+ known_subcommands = {
114
+ "inspect",
115
+ "run",
116
+ }
117
+ head = argv[0]
118
+ if head in known_subcommands or head.startswith("-"):
119
+ return argv
120
+ return ["run", *argv]
121
+
122
+
123
+ def _inspect_command(args: argparse.Namespace) -> int:
124
+ config = load_bout_input(args.input_file)
125
+ configure_jax_runtime(precision=resolve_runtime_precision(config=config))
126
+ run_config = RunConfiguration.from_config(config)
127
+
128
+ print(f"input: {args.input_file}")
129
+ print(f"sections: {', '.join(config.section_names())}")
130
+ print(f"time: nout={run_config.time.nout}, timestep={run_config.time.timestep:g}")
131
+ print(
132
+ "mesh: "
133
+ f"nx={run_config.mesh.nx}, ny={run_config.mesh.ny}, nz={run_config.mesh.nz}, "
134
+ f"MXG={run_config.mesh.mxg}, MYG={run_config.mesh.myg}, "
135
+ f"parallel_transform={run_config.mesh.parallel_transform.type}"
136
+ )
137
+ print(
138
+ f"scheduled components: {', '.join(request.label for request in run_config.components)}"
139
+ )
140
+
141
+ if run_config.normalization is not None:
142
+ normalization = run_config.normalization
143
+ print(
144
+ "normalization: "
145
+ f"Nnorm={normalization.Nnorm:g}, "
146
+ f"Tnorm={normalization.Tnorm:g}, "
147
+ f"Bnorm={normalization.Bnorm:g}, "
148
+ f"Cs0={normalization.Cs0:.8e}, "
149
+ f"Omega_ci={normalization.Omega_ci:.8e}, "
150
+ f"rho_s0={normalization.rho_s0:.8e}"
151
+ )
152
+ else:
153
+ print("normalization: unresolved (missing one or more of Nnorm, Tnorm, Bnorm)")
154
+
155
+ return 0
156
+
157
+
158
+ def _run_command(args: argparse.Namespace) -> int:
159
+ if args.dry_run:
160
+ return _inspect_command(args)
161
+ config = load_bout_input(args.input_file)
162
+ run_config = RunConfiguration.from_config(config)
163
+ resolved_precision = resolve_runtime_precision(
164
+ requested=args.precision, config=config
165
+ )
166
+ cache_dir = configure_jax_runtime(precision=resolved_precision)
167
+ import jax
168
+ from .native import run_input_case
169
+ from .native.deck_runner import (
170
+ NativeRestartState,
171
+ build_portable_array_payload,
172
+ build_restart_state,
173
+ write_portable_array_payload,
174
+ write_portable_summary_payload,
175
+ )
176
+ from .runtime import (
177
+ build_run_log_payload,
178
+ load_restart_bundle,
179
+ print_run_log,
180
+ write_restart_bundle,
181
+ write_run_log_payload,
182
+ )
183
+ from .runtime.output import build_run_event, print_run_event
184
+
185
+ command_started_at = time.perf_counter()
186
+ output_dir = args.output_dir or _config_path(config, "output", "directory")
187
+ case_name = (
188
+ args.case_name
189
+ or _config_string(config, "output", "case_name")
190
+ or args.input_file.stem
191
+ )
192
+ restart_in = args.restart_in or _config_path(config, "restart", "input")
193
+ resume_steps = (
194
+ args.resume_steps
195
+ if args.resume_steps is not None
196
+ else _config_int(config, "restart", "resume_steps")
197
+ )
198
+ logging_quiet = _config_bool(config, "runtime:logging", "quiet", default=False)
199
+ logging_verbose = _config_optional_bool(config, "runtime:logging", "verbose")
200
+ logging_verbosity = _config_string(config, "runtime:logging", "verbosity")
201
+ if logging_verbosity is None:
202
+ logging_verbosity = "detailed" if logging_verbose else "summary"
203
+ if args.verbose:
204
+ logging_verbosity = "detailed"
205
+ emit_terminal_log = not args.quiet and not logging_quiet
206
+ write_summary = _config_bool(config, "output", "write_summary", default=True)
207
+ write_arrays = _config_bool(config, "output", "write_arrays", default=True)
208
+ write_restart = _config_bool(config, "output", "write_restart", default=True)
209
+ write_log = _config_bool(config, "output", "write_log", default=True)
210
+ if args.json_out is None:
211
+ args.json_out = _config_path(config, "output", "summary_json")
212
+ if args.arrays_out is None:
213
+ args.arrays_out = _config_path(config, "output", "arrays_npz")
214
+ if args.restart_out is None:
215
+ args.restart_out = _config_path(config, "output", "restart_npz")
216
+ if args.log_out is None:
217
+ args.log_out = _config_path(config, "output", "run_log_json")
218
+ events: list[dict[str, Any]] = []
219
+
220
+ def record_event(stage: str, message: str, **details: Any) -> None:
221
+ event = build_run_event(
222
+ stage=stage,
223
+ message=message,
224
+ elapsed_seconds=time.perf_counter() - command_started_at,
225
+ details=details or None,
226
+ )
227
+ events.append(event)
228
+ if emit_terminal_log:
229
+ print_run_event(event, verbosity=logging_verbosity)
230
+
231
+ record_event(
232
+ "configuration",
233
+ "Loaded input configuration",
234
+ input_file=args.input_file,
235
+ case_name=case_name,
236
+ capability_tier="native_exact",
237
+ precision=resolved_precision,
238
+ nout=run_config.time.nout,
239
+ timestep=run_config.time.timestep,
240
+ output_dir=output_dir if output_dir is not None else "(none)",
241
+ verbosity=logging_verbosity,
242
+ verbose=logging_verbosity == "detailed",
243
+ )
244
+ restart_state = None
245
+ bundle = None
246
+ if restart_in is not None:
247
+ bundle = load_restart_bundle(restart_in)
248
+ restart_state = NativeRestartState(
249
+ time_offset=bundle.current_time,
250
+ completed_steps=bundle.completed_steps,
251
+ configured_timestep=bundle.configured_timestep,
252
+ variables=bundle.state_variables,
253
+ )
254
+ record_event(
255
+ "restart",
256
+ "Loaded restart bundle",
257
+ restart_in=restart_in,
258
+ current_time=bundle.current_time,
259
+ completed_steps=bundle.completed_steps,
260
+ variables=",".join(sorted(bundle.state_variables)),
261
+ requested_resume_steps=resume_steps
262
+ if resume_steps is not None
263
+ else "(default)",
264
+ )
265
+
266
+ def relay_native_event(event: Mapping[str, Any]) -> None:
267
+ if str(event.get("stage", "")) != "progress":
268
+ return
269
+ details = event.get("details")
270
+ if isinstance(details, Mapping):
271
+ record_event(
272
+ str(event.get("stage", "progress")),
273
+ str(event.get("message", "Native progress update")),
274
+ **dict(details),
275
+ )
276
+ else:
277
+ record_event(
278
+ str(event.get("stage", "progress")),
279
+ str(event.get("message", "Native progress update")),
280
+ )
281
+
282
+ started_at = time.perf_counter()
283
+ record_event(
284
+ "run", "Launching native run", mode="run", restart=restart_state is not None
285
+ )
286
+ result = run_input_case(
287
+ args.input_file,
288
+ case_name=case_name,
289
+ parity_mode="run",
290
+ restart_state=restart_state,
291
+ output_steps=resume_steps,
292
+ verbose=False,
293
+ event_logger=relay_native_event,
294
+ )
295
+ elapsed_seconds = time.perf_counter() - started_at
296
+ record_event(
297
+ "run",
298
+ "Native run completed",
299
+ elapsed_seconds=f"{elapsed_seconds:.3f}",
300
+ stored_states=len(result.time_points),
301
+ compare_variables=",".join(result.variables),
302
+ )
303
+
304
+ output_paths: dict[str, str] = {}
305
+ if output_dir is not None:
306
+ output_dir.mkdir(parents=True, exist_ok=True)
307
+ if args.json_out is None and write_summary:
308
+ args.json_out = output_dir / f"{case_name}_summary.json"
309
+ if args.arrays_out is None and write_arrays:
310
+ args.arrays_out = output_dir / f"{case_name}_arrays.npz"
311
+ if args.restart_out is None and write_restart:
312
+ args.restart_out = output_dir / f"{case_name}_restart.npz"
313
+ if args.log_out is None and write_log:
314
+ args.log_out = output_dir / f"{case_name}_run_log.json"
315
+ record_event(
316
+ "artifacts",
317
+ "Resolved artifact destinations",
318
+ summary_json=args.json_out if args.json_out is not None else "(disabled)",
319
+ arrays_npz=args.arrays_out if args.arrays_out is not None else "(disabled)",
320
+ restart_npz=args.restart_out
321
+ if args.restart_out is not None
322
+ else "(disabled)",
323
+ run_log_json=args.log_out if args.log_out is not None else "(disabled)",
324
+ )
325
+
326
+ if args.json_out is not None:
327
+ path = write_portable_summary_payload(result.payload, args.json_out)
328
+ output_paths["summary_json"] = _sanitize_logged_path(path) or str(path)
329
+ record_event("artifacts", "Wrote summary JSON", path=path)
330
+ if args.arrays_out is not None:
331
+ array_payload = build_portable_array_payload(
332
+ case_name=str(result.payload["case_name"]),
333
+ parity_mode=str(result.payload["parity_mode"]),
334
+ capability_tier=str(result.payload.get("capability_tier", "native_exact")),
335
+ compare_variables=tuple(str(name) for name in result.variables),
336
+ component_labels=tuple(result.payload.get("component_labels", [])),
337
+ dimensions=result.payload.get("dimensions", {}),
338
+ time_points=tuple(float(value) for value in result.time_points),
339
+ dataset_scalars=result.payload.get("dataset_scalars", {}),
340
+ variables=result.variables,
341
+ overrides=tuple(result.payload.get("overrides", [])),
342
+ configured_nout=result.payload.get("configured_nout"),
343
+ configured_timestep=result.payload.get("configured_timestep"),
344
+ producer=str(result.payload.get("producer", "drbx")),
345
+ )
346
+ path = write_portable_array_payload(array_payload, args.arrays_out)
347
+ output_paths["arrays_npz"] = _sanitize_logged_path(path) or str(path)
348
+ record_event(
349
+ "artifacts", "Wrote arrays NPZ", path=path, variables=len(result.variables)
350
+ )
351
+
352
+ restart_bundle = build_restart_state(result, parity_mode="run")
353
+ if args.restart_out is not None and restart_bundle is not None:
354
+ path = write_restart_bundle(restart_bundle, args.restart_out)
355
+ output_paths["restart_npz"] = _sanitize_logged_path(path) or str(path)
356
+ record_event(
357
+ "artifacts",
358
+ "Wrote restart bundle",
359
+ path=path,
360
+ completed_steps=restart_bundle.completed_steps,
361
+ current_time=restart_bundle.current_time,
362
+ )
363
+ elif args.restart_out is not None and restart_bundle is None:
364
+ output_paths["restart_npz"] = "(unsupported for this component set)"
365
+ record_event(
366
+ "artifacts",
367
+ "Restart bundle unsupported for this run",
368
+ path=args.restart_out,
369
+ )
370
+
371
+ if args.log_out is not None:
372
+ output_paths["run_log_json"] = _sanitize_logged_path(args.log_out) or str(
373
+ args.log_out
374
+ )
375
+ if output_paths:
376
+ record_event("artifacts", "Planned run artifacts", **output_paths)
377
+
378
+ log_payload = build_run_log_payload(
379
+ input_file=_sanitize_logged_path(args.input_file) or args.input_file,
380
+ case_name=case_name,
381
+ parity_mode="run",
382
+ capability_tier=str(result.payload.get("capability_tier", "native_exact")),
383
+ component_labels=tuple(result.payload.get("component_labels", [])),
384
+ time_points=tuple(float(value) for value in result.time_points),
385
+ dimensions=result.payload.get("dimensions", {}),
386
+ compare_variables=tuple(result.payload.get("compare_variables", [])),
387
+ restart_supported=restart_bundle is not None,
388
+ outputs=output_paths,
389
+ variable_summaries=result.payload.get("variable_summaries", {}),
390
+ run_configuration=_serialize_run_configuration(
391
+ run_config,
392
+ precision=resolved_precision,
393
+ backend=jax.default_backend(),
394
+ device=str(jax.devices()[0]) if jax.devices() else None,
395
+ jax_version=getattr(jax, "__version__", None),
396
+ cache_dir=cache_dir,
397
+ elapsed_seconds=elapsed_seconds,
398
+ output_directory=output_dir,
399
+ logging_verbosity=logging_verbosity,
400
+ logging_quiet=emit_terminal_log is False,
401
+ restart_in=restart_in,
402
+ resume_steps=resume_steps,
403
+ working_directory=Path.cwd(),
404
+ ),
405
+ restart_info=_serialize_restart_info(
406
+ restart_in=restart_in,
407
+ loaded_bundle=bundle if restart_in is not None else None,
408
+ requested_additional_steps=resume_steps,
409
+ saved_bundle=restart_bundle,
410
+ ),
411
+ events=tuple(events),
412
+ )
413
+ if args.log_out is not None:
414
+ record_event(
415
+ "artifacts",
416
+ "Writing verbose run log JSON",
417
+ path=args.log_out,
418
+ event_count=len(events),
419
+ )
420
+ log_payload["events"] = list(events)
421
+ log_payload["event_count"] = len(events)
422
+ log_payload["event_stages"] = [str(event.get("stage", "")) for event in events]
423
+ path = write_run_log_payload(log_payload, args.log_out)
424
+ output_paths["run_log_json"] = _sanitize_logged_path(path) or str(path)
425
+ log_payload["outputs"] = output_paths
426
+ log_payload["events"] = list(events)
427
+ log_payload["event_count"] = len(events)
428
+ log_payload["event_stages"] = [str(event.get("stage", "")) for event in events]
429
+
430
+ if emit_terminal_log:
431
+ print_run_log(log_payload, verbosity=logging_verbosity)
432
+ return 0
433
+
434
+
435
+ def _serialize_run_configuration(
436
+ run_config: RunConfiguration,
437
+ *,
438
+ precision: str,
439
+ backend: str | None = None,
440
+ device: str | None = None,
441
+ cache_dir: Path | None = None,
442
+ elapsed_seconds: float | None = None,
443
+ output_directory: Path | None = None,
444
+ logging_verbosity: str | None = None,
445
+ logging_quiet: bool | None = None,
446
+ restart_in: Path | None = None,
447
+ resume_steps: int | None = None,
448
+ jax_version: str | None = None,
449
+ working_directory: Path | None = None,
450
+ ) -> dict[str, object]:
451
+ return {
452
+ "time": {
453
+ "nout": run_config.time.nout,
454
+ "timestep": run_config.time.timestep,
455
+ },
456
+ "mesh": {
457
+ "nx": run_config.mesh.nx,
458
+ "ny": run_config.mesh.ny,
459
+ "nz": run_config.mesh.nz,
460
+ "mxg": run_config.mesh.mxg,
461
+ "myg": run_config.mesh.myg,
462
+ "file": run_config.mesh.file,
463
+ "parallel_transform": run_config.mesh.parallel_transform.type,
464
+ },
465
+ "solver": {
466
+ "type": run_config.solver.type,
467
+ "mxstep": run_config.solver.mxstep,
468
+ "rtol": run_config.solver.rtol,
469
+ "atol": run_config.solver.atol,
470
+ "use_precon": run_config.solver.use_precon,
471
+ "cvode_max_order": run_config.solver.cvode_max_order,
472
+ },
473
+ "runtime": {
474
+ "precision": precision,
475
+ "backend": backend,
476
+ "device": device,
477
+ "jax_version": jax_version,
478
+ "python_version": platform.python_version(),
479
+ "platform": platform.platform(),
480
+ "process_id": os.getpid(),
481
+ "compilation_cache_dir": _sanitize_logged_path(cache_dir),
482
+ "elapsed_seconds": elapsed_seconds,
483
+ "logging": {
484
+ "verbosity": logging_verbosity,
485
+ "verbose": logging_verbosity == "detailed",
486
+ "quiet": logging_quiet,
487
+ },
488
+ },
489
+ "output": {
490
+ "directory": _sanitize_logged_path(output_directory),
491
+ "working_directory": _sanitize_logged_path(working_directory),
492
+ },
493
+ "restart_request": {
494
+ "restart_in": _sanitize_logged_path(restart_in),
495
+ "resume_steps": resume_steps,
496
+ },
497
+ "components": [request.label for request in run_config.components],
498
+ "root_scalars": dict(run_config.root_scalars),
499
+ "model_scalars": dict(run_config.model_scalars),
500
+ }
501
+
502
+
503
+ def _serialize_restart_info(
504
+ *,
505
+ restart_in: Path | None,
506
+ loaded_bundle,
507
+ requested_additional_steps: int | None,
508
+ saved_bundle,
509
+ ) -> dict[str, object]:
510
+ payload: dict[str, object] = {}
511
+ if restart_in is not None and loaded_bundle is not None:
512
+ payload["loaded_from"] = _sanitize_logged_path(restart_in)
513
+ payload["start_time"] = loaded_bundle.current_time
514
+ payload["input_completed_steps"] = loaded_bundle.completed_steps
515
+ payload["loaded_state_variables"] = sorted(loaded_bundle.state_variables)
516
+ if requested_additional_steps is not None:
517
+ payload["requested_additional_steps"] = requested_additional_steps
518
+ if saved_bundle is not None:
519
+ payload["saved_completed_steps"] = saved_bundle.completed_steps
520
+ payload["saved_current_time"] = saved_bundle.current_time
521
+ payload["saved_state_variables"] = sorted(saved_bundle.state_variables)
522
+ return payload
523
+
524
+
525
+ def _sanitize_logged_path(path: str | Path | None) -> str | None:
526
+ if path is None:
527
+ return None
528
+ resolved = Path(path).expanduser()
529
+ try:
530
+ resolved = resolved.resolve()
531
+ except FileNotFoundError:
532
+ resolved = resolved.absolute()
533
+ try:
534
+ return resolved.relative_to(Path.cwd().resolve()).as_posix()
535
+ except ValueError:
536
+ pass
537
+ try:
538
+ return f"~/{resolved.relative_to(Path.home()).as_posix()}"
539
+ except ValueError:
540
+ return resolved.as_posix()
541
+
542
+
543
+ def _config_value(config, section: str, key: str, default: Any = None) -> Any:
544
+ if config.has_option(section, key):
545
+ return config.parsed(section, key)
546
+ return default
547
+
548
+
549
+ def _config_string(
550
+ config, section: str, key: str, default: str | None = None
551
+ ) -> str | None:
552
+ value = _config_value(config, section, key, default)
553
+ if value is None:
554
+ return None
555
+ return str(value)
556
+
557
+
558
+ def _config_int(
559
+ config, section: str, key: str, default: int | None = None
560
+ ) -> int | None:
561
+ value = _config_value(config, section, key, default)
562
+ if value is None:
563
+ return None
564
+ return int(value)
565
+
566
+
567
+ def _config_bool(config, section: str, key: str, default: bool = False) -> bool:
568
+ value = _config_value(config, section, key, default)
569
+ return bool(value)
570
+
571
+
572
+ def _config_optional_bool(config, section: str, key: str) -> bool | None:
573
+ if not config.has_option(section, key):
574
+ return None
575
+ return bool(config.parsed(section, key))
576
+
577
+
578
+ def _config_path(config, section: str, key: str) -> Path | None:
579
+ value = _config_value(config, section, key)
580
+ if value in (None, ""):
581
+ return None
582
+ return Path(str(value))
583
+
584
+
585
+ if __name__ == "__main__":
586
+ raise SystemExit(main())
@@ -0,0 +1,20 @@
1
+ from .boutinp import BoutConfig, NumericResolver, OptionEntry, OptionSection, OptionValue, ROOT_SECTION, load_bout_input, parse_bout_input, rewrite_input_precision
2
+ from .model import PUBLIC_MODEL_SECTION, has_model_section, locate_model_section
3
+ from .normalization import MetricPolicy, ModelNormalization
4
+
5
+ __all__ = [
6
+ "BoutConfig",
7
+ "MetricPolicy",
8
+ "ModelNormalization",
9
+ "NumericResolver",
10
+ "OptionEntry",
11
+ "OptionSection",
12
+ "OptionValue",
13
+ "PUBLIC_MODEL_SECTION",
14
+ "ROOT_SECTION",
15
+ "has_model_section",
16
+ "load_bout_input",
17
+ "locate_model_section",
18
+ "parse_bout_input",
19
+ "rewrite_input_precision",
20
+ ]