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/runtime/output.py ADDED
@@ -0,0 +1,374 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any, Mapping
7
+
8
+ import numpy as np
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RestartBundle:
13
+ case_name: str
14
+ parity_mode: str
15
+ component_labels: tuple[str, ...]
16
+ current_time: float
17
+ completed_steps: int
18
+ configured_timestep: float
19
+ state_variables: dict[str, np.ndarray]
20
+
21
+
22
+ def write_restart_bundle(
23
+ bundle: RestartBundle,
24
+ path: str | Path,
25
+ ) -> Path:
26
+ target = Path(path)
27
+ target.parent.mkdir(parents=True, exist_ok=True)
28
+ payload: dict[str, Any] = {
29
+ "__metadata__": np.asarray(
30
+ json.dumps(
31
+ {
32
+ "case_name": bundle.case_name,
33
+ "parity_mode": bundle.parity_mode,
34
+ "component_labels": list(bundle.component_labels),
35
+ "current_time": bundle.current_time,
36
+ "completed_steps": bundle.completed_steps,
37
+ "configured_timestep": bundle.configured_timestep,
38
+ "state_variables": sorted(bundle.state_variables),
39
+ },
40
+ sort_keys=True,
41
+ ),
42
+ dtype=np.str_,
43
+ ),
44
+ }
45
+ for name, value in bundle.state_variables.items():
46
+ payload[f"state:{name}"] = np.asarray(value)
47
+ np.savez_compressed(target, **payload)
48
+ return target
49
+
50
+
51
+ def load_restart_bundle(path: str | Path) -> RestartBundle:
52
+ source = Path(path)
53
+ with np.load(source, allow_pickle=False) as payload:
54
+ metadata = json.loads(str(np.asarray(payload["__metadata__"]).item()))
55
+ state_variables = {
56
+ key.removeprefix("state:"): np.asarray(payload[key])
57
+ for key in payload.files
58
+ if key.startswith("state:")
59
+ }
60
+ return RestartBundle(
61
+ case_name=str(metadata["case_name"]),
62
+ parity_mode=str(metadata["parity_mode"]),
63
+ component_labels=tuple(str(value) for value in metadata["component_labels"]),
64
+ current_time=float(metadata["current_time"]),
65
+ completed_steps=int(metadata["completed_steps"]),
66
+ configured_timestep=float(metadata["configured_timestep"]),
67
+ state_variables=state_variables,
68
+ )
69
+
70
+
71
+ def build_run_log_payload(
72
+ *,
73
+ input_file: str | Path,
74
+ case_name: str,
75
+ parity_mode: str,
76
+ capability_tier: str,
77
+ component_labels: tuple[str, ...],
78
+ time_points: tuple[float, ...],
79
+ dimensions: Mapping[str, int],
80
+ compare_variables: tuple[str, ...],
81
+ restart_supported: bool,
82
+ outputs: Mapping[str, str],
83
+ variable_summaries: Mapping[str, Any],
84
+ run_configuration: Mapping[str, Any] | None = None,
85
+ restart_info: Mapping[str, Any] | None = None,
86
+ events: tuple[Mapping[str, Any], ...] | None = None,
87
+ ) -> dict[str, Any]:
88
+ payload = {
89
+ "input_file": str(input_file),
90
+ "case_name": case_name,
91
+ "parity_mode": parity_mode,
92
+ "capability_tier": capability_tier,
93
+ "component_labels": list(component_labels),
94
+ "time_points": list(time_points),
95
+ "dimensions": dict(dimensions),
96
+ "compare_variables": list(compare_variables),
97
+ "restart_supported": bool(restart_supported),
98
+ "outputs": dict(outputs),
99
+ "variable_summaries": dict(variable_summaries),
100
+ }
101
+ if run_configuration is not None:
102
+ payload["run_configuration"] = dict(run_configuration)
103
+ if restart_info is not None:
104
+ payload["restart_info"] = dict(restart_info)
105
+ if events is not None:
106
+ rendered_events = [dict(event) for event in events]
107
+ payload["events"] = rendered_events
108
+ payload["event_count"] = len(rendered_events)
109
+ payload["event_stages"] = [str(event.get("stage", "")) for event in rendered_events]
110
+ return payload
111
+
112
+
113
+ def write_run_log_payload(payload: Mapping[str, Any], path: str | Path) -> Path:
114
+ target = Path(path)
115
+ target.parent.mkdir(parents=True, exist_ok=True)
116
+ target.write_text(json.dumps(dict(payload), indent=2, sort_keys=True), encoding="utf-8")
117
+ return target
118
+
119
+
120
+ def format_run_log_text(payload: Mapping[str, Any]) -> str:
121
+ outputs = payload.get("outputs", {})
122
+ output_lines = "\n".join(f" - {name}: {value}" for name, value in outputs.items()) if outputs else " - (none)"
123
+ time_points = payload.get("time_points", [])
124
+ if time_points:
125
+ time_line = f"{time_points[0]} -> {time_points[-1]} ({len(time_points)} stored states)"
126
+ else:
127
+ time_line = "(none)"
128
+ components = ", ".join(payload.get("component_labels", [])) or "(none)"
129
+ compare_variables = ", ".join(payload.get("compare_variables", [])) or "(none)"
130
+ run_configuration = payload.get("run_configuration", {})
131
+ restart_info = payload.get("restart_info", {})
132
+ mesh = run_configuration.get("mesh", {})
133
+ solver = run_configuration.get("solver", {})
134
+ time_cfg = run_configuration.get("time", {})
135
+ runtime_cfg = run_configuration.get("runtime", {})
136
+ runtime_cfg = run_configuration.get("runtime", {})
137
+ restart_lines = []
138
+ if restart_info:
139
+ for key in ("loaded_from", "start_time", "input_completed_steps", "requested_additional_steps", "saved_completed_steps"):
140
+ if key in restart_info:
141
+ restart_lines.append(f" - {key}: {restart_info[key]}")
142
+ restart_block = "\n".join(restart_lines) if restart_lines else " - (fresh run)"
143
+ return (
144
+ f"Run Summary\n"
145
+ f" input: {payload.get('input_file')}\n"
146
+ f" case: {payload.get('case_name')}\n"
147
+ f" mode: {payload.get('parity_mode')}\n"
148
+ f" capability tier: {payload.get('capability_tier', '(unknown)')}\n"
149
+ f" precision: {runtime_cfg.get('precision', '(default)')}\n"
150
+ f" configured nout/timestep: {time_cfg.get('nout', '(unknown)')} / {time_cfg.get('timestep', '(unknown)')}\n"
151
+ f" runtime: precision={runtime_cfg.get('precision', '(default)')}, backend={runtime_cfg.get('backend', '(unknown)')}, device={runtime_cfg.get('device', '(unknown)')}, elapsed={runtime_cfg.get('elapsed_seconds', '(unknown)')}\n"
152
+ f" mesh: nx={mesh.get('nx', '(unknown)')}, ny={mesh.get('ny', '(unknown)')}, nz={mesh.get('nz', '(unknown)')}, file={mesh.get('file', '<analytic mesh>')}\n"
153
+ f" solver: type={solver.get('type', '<native default>')}, mxstep={solver.get('mxstep', '(unknown)')}, rtol={solver.get('rtol', '(unknown)')}, atol={solver.get('atol', '(unknown)')}\n"
154
+ f" components: {components}\n"
155
+ f" compare variables: {compare_variables}\n"
156
+ f" dimensions: {payload.get('dimensions')}\n"
157
+ f" time: {time_line}\n"
158
+ f" events: {payload.get('event_count', 0)}\n"
159
+ f" restart supported: {'yes' if payload.get('restart_supported') else 'no'}\n"
160
+ f" restart:\n{restart_block}\n"
161
+ f" outputs:\n{output_lines}"
162
+ )
163
+
164
+
165
+ def print_run_log(payload: Mapping[str, Any], *, verbosity: str = "summary") -> None:
166
+ try:
167
+ from rich.console import Console
168
+ from rich.panel import Panel
169
+ from rich.table import Table
170
+ except Exception:
171
+ print(format_run_log_text(payload))
172
+ return
173
+
174
+ console = Console()
175
+ run_configuration = payload.get("run_configuration", {})
176
+ restart_info = payload.get("restart_info", {})
177
+ mesh = run_configuration.get("mesh", {})
178
+ solver = run_configuration.get("solver", {})
179
+ time_cfg = run_configuration.get("time", {})
180
+ runtime_cfg = run_configuration.get("runtime", {})
181
+
182
+ summary = Table.grid(padding=(0, 2))
183
+ summary.add_column(style="bold cyan")
184
+ summary.add_column()
185
+ summary.add_row("input", str(payload.get("input_file")))
186
+ summary.add_row("case", str(payload.get("case_name")))
187
+ summary.add_row("mode", str(payload.get("parity_mode")))
188
+ summary.add_row("tier", str(payload.get("capability_tier", "(unknown)")))
189
+ summary.add_row("precision", str(runtime_cfg.get("precision", "(default)")))
190
+ summary.add_row("backend", str(runtime_cfg.get("backend", "(unknown)")))
191
+ summary.add_row("device", str(runtime_cfg.get("device", "(unknown)")))
192
+ summary.add_row(
193
+ "elapsed",
194
+ "(unknown)" if runtime_cfg.get("elapsed_seconds") is None else f"{float(runtime_cfg['elapsed_seconds']):.3f} s",
195
+ )
196
+ summary.add_row("cache", str(runtime_cfg.get("compilation_cache_dir", "(none)")))
197
+ summary.add_row("time", f"{time_cfg.get('nout', '(unknown)')} outputs, dt={time_cfg.get('timestep', '(unknown)')}")
198
+ summary.add_row(
199
+ "mesh",
200
+ f"nx={mesh.get('nx', '(unknown)')}, ny={mesh.get('ny', '(unknown)')}, nz={mesh.get('nz', '(unknown)')}, file={mesh.get('file', '<analytic mesh>')}",
201
+ )
202
+ summary.add_row(
203
+ "solver",
204
+ f"type={solver.get('type', '<native default>')}, mxstep={solver.get('mxstep', '(unknown)')}, rtol={solver.get('rtol', '(unknown)')}, atol={solver.get('atol', '(unknown)')}",
205
+ )
206
+ summary.add_row("components", ", ".join(payload.get("component_labels", [])) or "(none)")
207
+ summary.add_row("compare vars", ", ".join(payload.get("compare_variables", [])) or "(none)")
208
+ summary.add_row("dimensions", json.dumps(payload.get("dimensions", {}), sort_keys=True))
209
+ summary.add_row("events", str(payload.get("event_count", 0)))
210
+ summary.add_row("restart", "yes" if payload.get("restart_supported") else "no")
211
+
212
+ outputs = Table(title="Outputs", show_header=True, header_style="bold magenta")
213
+ outputs.add_column("artifact")
214
+ outputs.add_column("path")
215
+ for name, value in payload.get("outputs", {}).items():
216
+ outputs.add_row(str(name), str(value))
217
+
218
+ restart_table = Table(title="Restart Provenance", show_header=True, header_style="bold magenta")
219
+ restart_table.add_column("field")
220
+ restart_table.add_column("value")
221
+ if restart_info:
222
+ for key in ("loaded_from", "start_time", "input_completed_steps", "requested_additional_steps", "saved_completed_steps"):
223
+ if key in restart_info:
224
+ restart_table.add_row(str(key), str(restart_info[key]))
225
+ else:
226
+ restart_table.add_row("state", "fresh run")
227
+
228
+ variable_table = Table(title="Variable Summaries", show_header=True, header_style="bold magenta")
229
+ variable_table.add_column("variable")
230
+ variable_table.add_column("min", justify="right")
231
+ variable_table.add_column("max", justify="right")
232
+ variable_table.add_column("mean", justify="right")
233
+ variable_table.add_column("delta", justify="right")
234
+ for name, summary_payload in payload.get("variable_summaries", {}).items():
235
+ delta = summary_payload.get("max_abs_delta_last_first")
236
+ variable_table.add_row(
237
+ str(name),
238
+ f"{float(summary_payload.get('minimum', 0.0)):.6e}",
239
+ f"{float(summary_payload.get('maximum', 0.0)):.6e}",
240
+ f"{float(summary_payload.get('mean', 0.0)):.6e}",
241
+ "(n/a)" if delta is None else f"{float(delta):.6e}",
242
+ )
243
+
244
+ console.print(Panel(summary, title="Run Summary", border_style="cyan"))
245
+ console.print(outputs)
246
+ if verbosity == "detailed":
247
+ console.print(restart_table)
248
+ console.print(variable_table)
249
+
250
+
251
+ def build_run_event(
252
+ *,
253
+ stage: str,
254
+ message: str,
255
+ elapsed_seconds: float | None = None,
256
+ details: Mapping[str, Any] | None = None,
257
+ ) -> dict[str, Any]:
258
+ event: dict[str, Any] = {
259
+ "stage": stage,
260
+ "message": message,
261
+ }
262
+ if elapsed_seconds is not None:
263
+ event["elapsed_seconds"] = elapsed_seconds
264
+ if details:
265
+ event["details"] = {str(key): _jsonify_event_value(value) for key, value in details.items()}
266
+ return event
267
+
268
+
269
+ def print_run_event(event: Mapping[str, Any], *, verbosity: str = "summary") -> None:
270
+ try:
271
+ from rich.console import Console
272
+ from rich.panel import Panel
273
+ from rich.table import Table
274
+ except Exception:
275
+ elapsed = event.get("elapsed_seconds")
276
+ prefix = f"[{float(elapsed):8.3f}s] " if elapsed is not None else ""
277
+ suffix = _format_event_summary_details(event)
278
+ print(f"{prefix}{event.get('stage', 'event')}: {event.get('message', '')}{suffix}")
279
+ details = event.get("details", {})
280
+ if verbosity == "detailed" and isinstance(details, Mapping):
281
+ for key, value in details.items():
282
+ print(f" - {key}: {value}")
283
+ return
284
+
285
+ console = Console()
286
+ if verbosity != "detailed":
287
+ elapsed = event.get("elapsed_seconds")
288
+ prefix = f"[{float(elapsed):8.3f}s] " if elapsed is not None else ""
289
+ suffix = _format_event_summary_details(event)
290
+ console.print(f"{prefix}[bold blue]{event.get('stage', 'event')}[/bold blue]: {event.get('message', '')}{suffix}")
291
+ return
292
+
293
+ details = event.get("details", {})
294
+ table = Table.grid(padding=(0, 2))
295
+ table.add_column(style="bold cyan")
296
+ table.add_column()
297
+ table.add_row("stage", str(event.get("stage", "")))
298
+ table.add_row("message", str(event.get("message", "")))
299
+ if event.get("elapsed_seconds") is not None:
300
+ table.add_row("elapsed", f"{float(event['elapsed_seconds']):.3f} s")
301
+ if isinstance(details, Mapping):
302
+ for key, value in details.items():
303
+ table.add_row(str(key), str(value))
304
+ console.print(Panel(table, border_style="blue"))
305
+
306
+
307
+ def _format_event_summary_details(event: Mapping[str, Any]) -> str:
308
+ details = event.get("details")
309
+ if not isinstance(details, Mapping):
310
+ return ""
311
+ stage = str(event.get("stage", ""))
312
+ if stage != "progress":
313
+ return ""
314
+ segments: list[str] = []
315
+ interval_index = details.get("interval_index")
316
+ steps = details.get("steps")
317
+ if interval_index is not None and steps is not None:
318
+ segments.append(f"interval {_format_count(interval_index)}/{_format_count(steps)}")
319
+ fraction_complete = details.get("fraction_complete")
320
+ if fraction_complete is not None:
321
+ try:
322
+ segments.append(f"{100.0 * float(fraction_complete):.1f}%")
323
+ except (TypeError, ValueError):
324
+ pass
325
+ solver_mode = details.get("solver_mode")
326
+ if solver_mode is not None:
327
+ segments.append(f"mode {solver_mode}")
328
+ accepted_dt = details.get("accepted_dt")
329
+ if accepted_dt is not None:
330
+ try:
331
+ segments.append(f"dt {float(accepted_dt):.3g}")
332
+ except (TypeError, ValueError):
333
+ pass
334
+ if bool(details.get("live_progress", True)):
335
+ eta = details.get("estimated_remaining_seconds")
336
+ if eta is not None:
337
+ try:
338
+ segments.append(f"eta {_format_elapsed_seconds(float(eta))}")
339
+ except (TypeError, ValueError):
340
+ pass
341
+ if not segments:
342
+ return ""
343
+ return f" ({', '.join(segments)})"
344
+
345
+
346
+ def _format_count(value: Any) -> str:
347
+ try:
348
+ numeric = float(value)
349
+ except (TypeError, ValueError):
350
+ return str(value)
351
+ if numeric.is_integer():
352
+ return str(int(numeric))
353
+ return f"{numeric:.3g}"
354
+
355
+
356
+ def _format_elapsed_seconds(value: float) -> str:
357
+ seconds = max(float(value), 0.0)
358
+ if seconds < 60.0:
359
+ return f"{seconds:.1f}s"
360
+ minutes, rem = divmod(seconds, 60.0)
361
+ if minutes < 60.0:
362
+ return f"{int(minutes)}m {rem:04.1f}s"
363
+ hours, minutes = divmod(minutes, 60.0)
364
+ return f"{int(hours)}h {int(minutes)}m"
365
+
366
+
367
+ def _jsonify_event_value(value: Any) -> Any:
368
+ if isinstance(value, Path):
369
+ return str(value)
370
+ if isinstance(value, Mapping):
371
+ return {str(key): _jsonify_event_value(item) for key, item in value.items()}
372
+ if isinstance(value, (list, tuple)):
373
+ return [_jsonify_event_value(item) for item in value]
374
+ return value
drbx/runtime/paths.py ADDED
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def repo_root() -> Path:
7
+ """Repository root (the directory containing ``src/``)."""
8
+
9
+ return Path(__file__).resolve().parents[3]
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shlex
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from ..config.boutinp import BoutConfig
11
+
12
+ _VALID_PRECISIONS = {"float32", "float64"}
13
+ _HOST_DEVICE_FLAG_PREFIX = "--xla_force_host_platform_device_count="
14
+
15
+
16
+ def resolve_runtime_precision(
17
+ *,
18
+ requested: str | None = None,
19
+ config: BoutConfig | None = None,
20
+ ) -> str:
21
+ candidate = requested
22
+ if candidate is None and config is not None and config.has_option("runtime", "precision"):
23
+ parsed = config.parsed("runtime", "precision")
24
+ candidate = str(parsed)
25
+ if candidate is None:
26
+ candidate = os.environ.get("DRBX_PRECISION", "float64")
27
+ normalized = str(candidate).strip().lower()
28
+ if normalized not in _VALID_PRECISIONS:
29
+ raise ValueError(f"Unsupported precision {candidate!r}; expected one of {sorted(_VALID_PRECISIONS)}")
30
+ return normalized
31
+
32
+
33
+ def resolve_host_device_count(*, requested: int | str | None = None) -> int | None:
34
+ candidate = requested
35
+ if candidate is None:
36
+ candidate = os.environ.get("DRBX_HOST_DEVICE_COUNT")
37
+ if candidate is None:
38
+ return None
39
+ normalized = str(candidate).strip()
40
+ if not normalized or normalized == "0":
41
+ return None
42
+ try:
43
+ value = int(normalized)
44
+ except ValueError as exc:
45
+ raise ValueError(f"Unsupported host device count {candidate!r}; expected a positive integer") from exc
46
+ if value <= 0:
47
+ raise ValueError(f"Unsupported host device count {candidate!r}; expected a positive integer")
48
+ return value
49
+
50
+
51
+ def configure_jax_runtime(
52
+ *,
53
+ precision: str | None = None,
54
+ host_device_count: int | str | None = None,
55
+ ) -> Path | None:
56
+ resolved_precision = resolve_runtime_precision(requested=precision)
57
+ os.environ["DRBX_PRECISION"] = resolved_precision
58
+ resolved_host_device_count = resolve_host_device_count(requested=host_device_count)
59
+ if resolved_host_device_count is not None:
60
+ os.environ["DRBX_HOST_DEVICE_COUNT"] = str(resolved_host_device_count)
61
+ _configure_host_device_count_xla_flags(resolved_host_device_count)
62
+ if os.environ.get("DRBX_DISABLE_COMPILATION_CACHE", "").strip().lower() in {"1", "true", "yes", "on"}:
63
+ cache_dir = None
64
+ else:
65
+ cache_dir = _compilation_cache_dir()
66
+ cache_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ import jax
69
+ jax.config.update("jax_enable_x64", resolved_precision == "float64")
70
+ if cache_dir is not None:
71
+ from jax.experimental.compilation_cache import compilation_cache as compilation_cache
72
+
73
+ jax.config.update("jax_enable_compilation_cache", True)
74
+ jax.config.update("jax_compilation_cache_dir", str(cache_dir))
75
+ if "DRBX_PERSISTENT_CACHE_MIN_COMPILE_TIME_SECS" in os.environ:
76
+ min_compile_time = float(os.environ["DRBX_PERSISTENT_CACHE_MIN_COMPILE_TIME_SECS"])
77
+ else:
78
+ min_compile_time = 0.0
79
+ if "DRBX_PERSISTENT_CACHE_MIN_ENTRY_SIZE_BYTES" in os.environ:
80
+ min_entry_size = int(os.environ["DRBX_PERSISTENT_CACHE_MIN_ENTRY_SIZE_BYTES"])
81
+ else:
82
+ min_entry_size = 0
83
+ jax.config.update("jax_persistent_cache_min_compile_time_secs", min_compile_time)
84
+ jax.config.update("jax_persistent_cache_min_entry_size_bytes", min_entry_size)
85
+ compilation_cache.set_cache_dir(str(cache_dir))
86
+ return cache_dir
87
+
88
+
89
+ def runtime_numpy_dtype(*, precision: str | None = None) -> Any:
90
+ import numpy as np
91
+
92
+ resolved = resolve_runtime_precision(requested=precision)
93
+ return np.float32 if resolved == "float32" else np.float64
94
+
95
+
96
+ def runtime_jax_dtype(*, precision: str | None = None) -> Any:
97
+ import jax.numpy as jnp
98
+
99
+ resolved = resolve_runtime_precision(requested=precision)
100
+ return jnp.float32 if resolved == "float32" else jnp.float64
101
+
102
+
103
+ def runtime_parallel_summary() -> dict[str, Any]:
104
+ import jax
105
+
106
+ requested_host_devices = resolve_host_device_count()
107
+ xla_flags = os.environ.get("XLA_FLAGS", "")
108
+ configured_host_devices = _extract_host_device_count_from_flags(xla_flags)
109
+ return {
110
+ "backend": jax.default_backend(),
111
+ "cpu_count": os.cpu_count(),
112
+ "device_count": jax.device_count(),
113
+ "local_device_count": jax.local_device_count(),
114
+ "devices": [str(device) for device in jax.devices()],
115
+ "requested_host_device_count": requested_host_devices,
116
+ "configured_host_device_count": configured_host_devices,
117
+ "explicit_host_device_parallelism_enabled": (
118
+ jax.default_backend() == "cpu" and jax.local_device_count() > 1
119
+ ),
120
+ "xla_flags": xla_flags,
121
+ }
122
+
123
+
124
+ def _compilation_cache_dir() -> Path:
125
+ override = os.environ.get("DRBX_CACHE_DIR")
126
+ if override:
127
+ return Path(override).expanduser()
128
+ return _default_user_cache_root() / "drbx" / "jax_compilation_cache"
129
+
130
+
131
+ def _default_user_cache_root() -> Path:
132
+ xdg = os.environ.get("XDG_CACHE_HOME")
133
+ if xdg:
134
+ return Path(xdg).expanduser()
135
+ if platform.system() == "Darwin":
136
+ return Path.home() / "Library" / "Caches"
137
+ return Path.home() / ".cache"
138
+
139
+
140
+ def _configure_host_device_count_xla_flags(host_device_count: int) -> None:
141
+ existing_flags = os.environ.get("XLA_FLAGS", "")
142
+ configured_count = _extract_host_device_count_from_flags(existing_flags)
143
+ if configured_count == host_device_count:
144
+ return
145
+ if "jax" in sys.modules:
146
+ raise RuntimeError(
147
+ "DRBX_HOST_DEVICE_COUNT must be set before importing jax/drbx so CPU devices can be configured."
148
+ )
149
+ tokens = [token for token in shlex.split(existing_flags) if not token.startswith(_HOST_DEVICE_FLAG_PREFIX)]
150
+ tokens.append(f"{_HOST_DEVICE_FLAG_PREFIX}{host_device_count}")
151
+ os.environ["XLA_FLAGS"] = " ".join(tokens)
152
+
153
+
154
+ def _extract_host_device_count_from_flags(flags: str) -> int | None:
155
+ for token in shlex.split(flags):
156
+ if token.startswith(_HOST_DEVICE_FLAG_PREFIX):
157
+ try:
158
+ return int(token.split("=", 1)[1])
159
+ except ValueError:
160
+ return None
161
+ return None