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.
- drbx/__init__.py +20 -0
- drbx/__main__.py +5 -0
- drbx/cli.py +586 -0
- drbx/config/__init__.py +20 -0
- drbx/config/boutinp.py +556 -0
- drbx/config/model.py +34 -0
- drbx/config/normalization.py +66 -0
- drbx/data/atomic_rates/__init__.py +1 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.1.5.json +213 -0
- drbx/data/atomic_rates/iz_AMJUEL_H.x_2.3.9a.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.1.8.json +213 -0
- drbx/data/atomic_rates/rec_AMJUEL_H.x_2.3.13a.json +213 -0
- drbx/geometry/__init__.py +207 -0
- drbx/geometry/embedding.py +56 -0
- drbx/geometry/essos_import.py +1385 -0
- drbx/geometry/fci_geometry.py +4622 -0
- drbx/geometry/fci_maps.py +85 -0
- drbx/geometry/island_divertor.py +291 -0
- drbx/geometry/metric_tensor.py +99 -0
- drbx/geometry/open_slab.py +150 -0
- drbx/geometry/rotating_ellipse.py +253 -0
- drbx/geometry/shifted_torus.py +225 -0
- drbx/geometry/stellarator.py +287 -0
- drbx/geometry/vmec_extender_import.py +499 -0
- drbx/geometry/vmec_jax_import.py +306 -0
- drbx/linear/__init__.py +37 -0
- drbx/linear/dispersion.py +138 -0
- drbx/linear/eigen.py +91 -0
- drbx/native/__init__.py +224 -0
- drbx/native/array_backend.py +64 -0
- drbx/native/deck_runner.py +779 -0
- drbx/native/electromagnetic.py +250 -0
- drbx/native/expression.py +173 -0
- drbx/native/fci.py +295 -0
- drbx/native/fci_2_field_rhs.py +182 -0
- drbx/native/fci_4_field_rhs.py +1267 -0
- drbx/native/fci_boundaries.py +2494 -0
- drbx/native/fci_differentiable_case.py +304 -0
- drbx/native/fci_drb_EB_rhs.py +1243 -0
- drbx/native/fci_drb_rhs.py +190 -0
- drbx/native/fci_halo.py +1575 -0
- drbx/native/fci_helpers.py +350 -0
- drbx/native/fci_model.py +294 -0
- drbx/native/fci_neutral.py +139 -0
- drbx/native/fci_operators.py +4081 -0
- drbx/native/fci_sharding.py +597 -0
- drbx/native/fci_sheath_recycling.py +206 -0
- drbx/native/fci_time_integrator.py +96 -0
- drbx/native/fci_vorticity.py +198 -0
- drbx/native/fluid_1d.py +330 -0
- drbx/native/hasegawa_wakatani.py +196 -0
- drbx/native/limiters.py +57 -0
- drbx/native/mesh.py +238 -0
- drbx/native/metrics.py +234 -0
- drbx/native/neutrals/__init__.py +58 -0
- drbx/native/neutrals/atomic_rates.py +134 -0
- drbx/native/neutrals/detachment_sol_model.py +221 -0
- drbx/native/neutrals/reactions.py +164 -0
- drbx/native/neutrals/recycling_sol_model.py +197 -0
- drbx/native/sol_flux_tube.py +133 -0
- drbx/native/stellarator_turbulence.py +343 -0
- drbx/native/transport.py +134 -0
- drbx/native/units.py +32 -0
- drbx/native/vorticity.py +252 -0
- drbx/runtime/__init__.py +53 -0
- drbx/runtime/artifacts.py +161 -0
- drbx/runtime/memory.py +144 -0
- drbx/runtime/output.py +374 -0
- drbx/runtime/paths.py +9 -0
- drbx/runtime/performance.py +161 -0
- drbx/runtime/run_config.py +184 -0
- drbx/runtime/scheduler.py +99 -0
- drbx/runtime/state.py +40 -0
- drbx/validation/__init__.py +424 -0
- drbx/validation/autodiff_diffusion.py +329 -0
- drbx/validation/autodiff_diffusion_uncertainty.py +235 -0
- drbx/validation/diverted_tokamak_movie.py +558 -0
- drbx/validation/essos_fieldline_import_campaign.py +181 -0
- drbx/validation/essos_imported_artifact_audit.py +535 -0
- drbx/validation/essos_imported_drb_movie_campaign.py +2826 -0
- drbx/validation/essos_imported_fci_campaign.py +5241 -0
- drbx/validation/essos_imported_pytree_campaign.py +406 -0
- drbx/validation/essos_vmec_closed_field_campaign.py +314 -0
- drbx/validation/essos_vmec_closed_field_transient_campaign.py +629 -0
- drbx/validation/essos_vmec_fieldline_surface_campaign.py +620 -0
- drbx/validation/fluid_1d_mms_convergence.py +250 -0
- drbx/validation/geometry_lineouts.py +136 -0
- drbx/validation/geometry_slices.py +178 -0
- drbx/validation/publication_plotting.py +91 -0
- drbx/validation/stellarator_drb_pytree_campaign.py +621 -0
- drbx/validation/stellarator_fci_geometry_campaign.py +200 -0
- drbx/validation/stellarator_fci_operator_campaign.py +304 -0
- drbx/validation/stellarator_fci_suite_campaign.py +264 -0
- drbx/validation/stellarator_metric_mms_campaign.py +289 -0
- drbx/validation/stellarator_neutral_physics_campaign.py +255 -0
- drbx/validation/stellarator_sheath_recycling_campaign.py +331 -0
- drbx/validation/stellarator_sol_showcase.py +628 -0
- drbx/validation/stellarator_vorticity_campaign.py +304 -0
- drbx/validation/vmec_extender_edge_field_campaign.py +260 -0
- drbx/validation/vmec_extender_sol_smoke_campaign.py +365 -0
- drbx-2.0.0.dev0.dist-info/METADATA +380 -0
- drbx-2.0.0.dev0.dist-info/RECORD +106 -0
- drbx-2.0.0.dev0.dist-info/WHEEL +5 -0
- drbx-2.0.0.dev0.dist-info/entry_points.txt +2 -0
- drbx-2.0.0.dev0.dist-info/licenses/LICENSE +21 -0
- 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,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
|