Skip to content

Commit 2dbde6d

Browse files
authored
Merge pull request #11 from SteSeg/report_generation
Copy `specifications` and `run_metadata` into `benchmark_results.h5`
2 parents 788f7b9 + f868d90 commit 2dbde6d

10 files changed

Lines changed: 365 additions & 0 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
"""Backfill specifications and run metadata into existing results files."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
from pathlib import Path
8+
9+
import h5py
10+
import numpy as np
11+
import yaml
12+
13+
14+
def _write_spec_snapshot(handle: h5py.File, spec_path: Path, benchmark_name: str | None) -> None:
15+
spec_yaml = spec_path.read_text(encoding="utf-8")
16+
spec_bytes = spec_yaml.encode("utf-8")
17+
18+
if "specifications" in handle:
19+
del handle["specifications"]
20+
group = handle.create_group("specifications")
21+
group.attrs["format"] = "yaml"
22+
if benchmark_name:
23+
group.attrs["benchmark_name"] = benchmark_name
24+
group.create_dataset("yaml", data=np.bytes_(spec_bytes))
25+
26+
27+
def _write_run_metadata(
28+
handle: h5py.File,
29+
code_name: str | None,
30+
code_version: str | None,
31+
nuclear_data_name: str | None,
32+
nuclear_data_version: str | None,
33+
geometry: str | None,
34+
) -> None:
35+
if "run_metadata" in handle:
36+
del handle["run_metadata"]
37+
group = handle.create_group("run_metadata")
38+
39+
if code_name:
40+
group.attrs["code_name"] = code_name
41+
if code_version:
42+
group.attrs["code_version"] = code_version
43+
if nuclear_data_name:
44+
group.attrs["nuclear_data_name"] = nuclear_data_name
45+
if nuclear_data_version:
46+
group.attrs["nuclear_data_version"] = nuclear_data_version
47+
if geometry:
48+
group.attrs["geometry"] = geometry
49+
50+
51+
def main() -> None:
52+
parser = argparse.ArgumentParser(
53+
description="Backfill specifications and run metadata into an OFB results file."
54+
)
55+
parser.add_argument("--file", type=Path, required=True, help="Path to results .h5 file")
56+
parser.add_argument("--spec-file", type=Path, required=True, help="Path to specifications.yaml")
57+
parser.add_argument(
58+
"--kind",
59+
choices=["experiment", "calculation"],
60+
required=True,
61+
help="Result kind (controls which metadata are written)",
62+
)
63+
parser.add_argument("--benchmark-name", type=str, default=None, help="Benchmark name label")
64+
parser.add_argument("--code-name", type=str, default=None, help="Code name (calculation only)")
65+
parser.add_argument("--code-version", type=str, default=None, help="Code version (calculation only)")
66+
parser.add_argument(
67+
"--nuclear-data-name",
68+
type=str,
69+
default=None,
70+
help="Nuclear data library name (calculation only)",
71+
)
72+
parser.add_argument(
73+
"--nuclear-data-version",
74+
type=str,
75+
default=None,
76+
help="Nuclear data library version (calculation only)",
77+
)
78+
parser.add_argument(
79+
"--geometry",
80+
type=str,
81+
default=None,
82+
choices=["cad", "csg"],
83+
help="Geometry type (calculation only)",
84+
)
85+
args = parser.parse_args()
86+
87+
if not args.file.exists():
88+
raise FileNotFoundError(f"Results file not found: {args.file}")
89+
if not args.spec_file.exists():
90+
raise FileNotFoundError(f"Spec file not found: {args.spec_file}")
91+
92+
# Validate the YAML early to fail fast if malformed.
93+
with args.spec_file.open("r", encoding="utf-8") as handle:
94+
yaml.safe_load(handle)
95+
96+
with h5py.File(args.file, "a") as handle:
97+
_write_spec_snapshot(handle, args.spec_file, args.benchmark_name)
98+
if args.kind == "calculation":
99+
_write_run_metadata(
100+
handle,
101+
code_name=args.code_name,
102+
code_version=args.code_version,
103+
nuclear_data_name=args.nuclear_data_name,
104+
nuclear_data_version=args.nuclear_data_version,
105+
geometry=args.geometry,
106+
)
107+
else:
108+
if "run_metadata" in handle:
109+
del handle["run_metadata"]
110+
111+
print(f"Updated: {args.file}")
112+
113+
114+
if __name__ == "__main__":
115+
main()

src/openmc_fusion_benchmarks/benchmark.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import yaml
4+
import h5py
45
from pathlib import Path
56
import warnings
67
from abc import ABC, abstractmethod
@@ -127,6 +128,58 @@ def metadata(self):
127128

128129
return self._metadata
129130

131+
def _write_spec_snapshot(self, filename: str = "benchmark_results.h5") -> None:
132+
"""Persist a snapshot of specifications.yaml into the results file."""
133+
path = Path(filename)
134+
if not path.exists():
135+
warnings.warn(
136+
f"Results file '{filename}' not found. Skipping spec snapshot.",
137+
UserWarning,
138+
)
139+
return
140+
141+
spec_yaml = yaml.safe_dump(self._benchmark_spec, sort_keys=False)
142+
spec_bytes = spec_yaml.encode("utf-8")
143+
144+
with h5py.File(path, "a") as handle:
145+
if "specifications" in handle:
146+
del handle["specifications"]
147+
group = handle.create_group("specifications")
148+
group.attrs["format"] = "yaml"
149+
group.attrs["benchmark_name"] = self.name
150+
group.create_dataset("yaml", data=np.bytes_(spec_bytes))
151+
152+
def _write_run_metadata(
153+
self,
154+
code_name: str,
155+
code_version: str,
156+
nuclear_data_name: str | None = None,
157+
nuclear_data_version: str | None = None,
158+
geometry: str | None = None,
159+
filename: str = "benchmark_results.h5",
160+
) -> None:
161+
"""Persist run metadata into the results file."""
162+
path = Path(filename)
163+
if not path.exists():
164+
warnings.warn(
165+
f"Results file '{filename}' not found. Skipping run metadata.",
166+
UserWarning,
167+
)
168+
return
169+
170+
with h5py.File(path, "a") as handle:
171+
if "run_metadata" in handle:
172+
del handle["run_metadata"]
173+
group = handle.create_group("run_metadata")
174+
group.attrs["code_name"] = str(code_name)
175+
group.attrs["code_version"] = str(code_version)
176+
if nuclear_data_name is not None:
177+
group.attrs["nuclear_data_name"] = str(nuclear_data_name)
178+
if nuclear_data_version is not None:
179+
group.attrs["nuclear_data_version"] = str(nuclear_data_version)
180+
if geometry is not None:
181+
group.attrs["geometry"] = str(geometry)
182+
130183

131184
class OpenmcBenchmark(Benchmark):
132185
def __init__(self, name: str):
@@ -415,8 +468,12 @@ def _postprocess(self, statepoint: openmc.StatePoint | str | Path, mesh: str = '
415468
normalizer = make_default_openmc_normalizer(mesh)
416469

417470
# Accept both already-open StatePoint objects and statepoint file paths.
471+
code_version = "unknown"
472+
418473
if isinstance(statepoint, openmc.StatePoint):
419474
sp = statepoint
475+
if hasattr(sp, "version"):
476+
code_version = ".".join(str(v) for v in sp.version)
420477
save_openmc_statepoint_tallies(
421478
statepoint=sp,
422479
filename="benchmark_results.h5",
@@ -428,6 +485,8 @@ def _postprocess(self, statepoint: openmc.StatePoint | str | Path, mesh: str = '
428485
)
429486
else:
430487
with openmc.StatePoint(str(statepoint)) as sp:
488+
if hasattr(sp, "version"):
489+
code_version = ".".join(str(v) for v in sp.version)
431490
save_openmc_statepoint_tallies(
432491
statepoint=sp,
433492
filename="benchmark_results.h5",
@@ -438,6 +497,18 @@ def _postprocess(self, statepoint: openmc.StatePoint | str | Path, mesh: str = '
438497
normalizer=normalizer,
439498
)
440499

500+
if hasattr(self, "_write_spec_snapshot"):
501+
self._write_spec_snapshot("benchmark_results.h5")
502+
if hasattr(self, "_write_run_metadata"):
503+
self._write_run_metadata(
504+
code_name="openmc",
505+
code_version=code_version,
506+
nuclear_data_name=None,
507+
nuclear_data_version=None,
508+
geometry="cad",
509+
filename="benchmark_results.h5",
510+
)
511+
441512
return
442513

443514
def _uncertainty_quantification(self, *args, **kwargs):

src/openmc_fusion_benchmarks/benchmark_results.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import json
4+
import yaml
45
from pathlib import Path
56
from typing import Union
67
import h5py
@@ -163,6 +164,42 @@ def _loads_attr(attrs, key, default):
163164

164165
return report
165166

167+
def get_spec_snapshot(self) -> dict:
168+
"""Return the embedded specifications.yaml snapshot if present."""
169+
with h5py.File(self.filepath, "r") as f:
170+
if "specifications" not in f:
171+
raise ValueError("No specifications snapshot found in results file")
172+
group = f["specifications"]
173+
if "yaml" not in group:
174+
raise ValueError("Specifications snapshot is missing 'yaml' dataset")
175+
raw = group["yaml"][()]
176+
if isinstance(raw, np.ndarray):
177+
if raw.size == 0:
178+
raise ValueError("Specifications snapshot dataset is empty")
179+
raw = raw[()]
180+
if isinstance(raw, bytes):
181+
yaml_text = raw.decode("utf-8")
182+
else:
183+
yaml_text = str(raw)
184+
try:
185+
return json.loads(json.dumps(yaml.safe_load(yaml_text)))
186+
except Exception as exc:
187+
raise ValueError("Failed to parse specifications snapshot YAML") from exc
188+
189+
def get_run_metadata(self) -> dict:
190+
"""Return run metadata embedded in the results file, if present."""
191+
with h5py.File(self.filepath, "r") as f:
192+
if "run_metadata" not in f:
193+
raise ValueError("No run metadata found in results file")
194+
attrs = f["run_metadata"].attrs
195+
data: dict[str, str] = {}
196+
for key, value in attrs.items():
197+
if isinstance(value, bytes):
198+
data[key] = value.decode("utf-8")
199+
else:
200+
data[key] = str(value)
201+
return data
202+
166203

167204
class BenchmarkResults(Results):
168205
"""Backward-compatible alias class for benchmark-centric naming."""
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import importlib.util
2+
import sys
3+
import types
4+
from pathlib import Path
5+
6+
import h5py
7+
import pytest
8+
import yaml
9+
10+
11+
def _module_available(name: str) -> bool:
12+
if name in sys.modules:
13+
return True
14+
try:
15+
return importlib.util.find_spec(name) is not None
16+
except ValueError:
17+
return False
18+
19+
20+
# Provide minimal stubs so package imports work in minimal test envs.
21+
if not _module_available("openmc"):
22+
openmc_stub = types.ModuleType("openmc")
23+
openmc_stub.__path__ = []
24+
sys.modules.setdefault("openmc", openmc_stub)
25+
26+
if not _module_available("sandy"):
27+
sys.modules.setdefault("sandy", types.ModuleType("sandy"))
28+
29+
30+
from scripts.backfill_results_metadata import main as backfill_main
31+
32+
33+
def _write_empty_results(filepath: Path) -> None:
34+
with h5py.File(filepath, "w") as handle:
35+
handle.create_group("tally_1")
36+
37+
38+
def test_backfill_experiment(tmp_path, monkeypatch):
39+
results_path = tmp_path / "experiment.h5"
40+
spec_path = tmp_path / "specifications.yaml"
41+
42+
_write_empty_results(results_path)
43+
spec_path.write_text(yaml.safe_dump({"metadata": {"title": "Test"}}), encoding="utf-8")
44+
45+
monkeypatch.setattr(
46+
sys,
47+
"argv",
48+
[
49+
"backfill_results_metadata.py",
50+
"--file",
51+
str(results_path),
52+
"--spec-file",
53+
str(spec_path),
54+
"--kind",
55+
"experiment",
56+
"--benchmark-name",
57+
"test",
58+
],
59+
)
60+
61+
backfill_main()
62+
63+
with h5py.File(results_path, "r") as handle:
64+
assert "specifications" in handle
65+
assert "run_metadata" not in handle
66+
spec_group = handle["specifications"]
67+
assert spec_group.attrs["benchmark_name"] == "test"
68+
69+
70+
def test_backfill_calculation(tmp_path, monkeypatch):
71+
results_path = tmp_path / "calc.h5"
72+
spec_path = tmp_path / "specifications.yaml"
73+
74+
_write_empty_results(results_path)
75+
spec_path.write_text(yaml.safe_dump({"metadata": {"title": "Test"}}), encoding="utf-8")
76+
77+
monkeypatch.setattr(
78+
sys,
79+
"argv",
80+
[
81+
"backfill_results_metadata.py",
82+
"--file",
83+
str(results_path),
84+
"--spec-file",
85+
str(spec_path),
86+
"--kind",
87+
"calculation",
88+
"--code-name",
89+
"tripoli",
90+
"--code-version",
91+
"4.11",
92+
"--nuclear-data-name",
93+
"FENDL",
94+
"--nuclear-data-version",
95+
"3.2b",
96+
"--geometry",
97+
"cad",
98+
],
99+
)
100+
101+
backfill_main()
102+
103+
with h5py.File(results_path, "r") as handle:
104+
assert "specifications" in handle
105+
assert "run_metadata" in handle
106+
meta = handle["run_metadata"].attrs
107+
assert meta["code_name"] == "tripoli"
108+
assert meta["code_version"] == "4.11"
109+
assert meta["nuclear_data_name"] == "FENDL"
110+
assert meta["nuclear_data_version"] == "3.2b"
111+
assert meta["geometry"] == "cad"

0 commit comments

Comments
 (0)