ho22joshua commited on
Commit
528346c
·
1 Parent(s): a900118

validation: use pinned public Hugging Face fixture

Browse files
data/fixtures/testing/ttH_NLO_64.root ADDED
Binary file (15.1 kB). View file
 
docs/end_to_end_validation.md CHANGED
@@ -16,6 +16,15 @@ The captured runtime versions and seed policy are recorded in
16
  `validation/manifests/environments.json`; the HF composite fixture provenance
17
  is recorded in `validation/manifests/multiclass_fixture.json`.
18
 
 
 
 
 
 
 
 
 
 
19
  Strict parity applies to preprocessing, topology, fixed forward, loss, and
20
  one-step CPU updates. Multi-epoch, GPU and DDP comparisons are scientific:
21
  compare curves, metrics, distributions and event-level correlations. Named
 
16
  `validation/manifests/environments.json`; the HF composite fixture provenance
17
  is recorded in `validation/manifests/multiclass_fixture.json`.
18
 
19
+ For the portable public regression, run:
20
+
21
+ ```bash
22
+ uv run python -m validation.run_public_validation
23
+ ```
24
+
25
+ This downloads or verifies the pinned `HWresearch/Delphes` 64-event fixture
26
+ and compares the current rewrite against the checked-in legacy golden output.
27
+
28
  Strict parity applies to preprocessing, topology, fixed forward, loss, and
29
  one-step CPU updates. Multi-epoch, GPU and DDP comparisons are scientific:
30
  compare curves, metrics, distributions and event-level correlations. Named
tests/integration/test_real_root_model.py CHANGED
@@ -34,7 +34,11 @@ FEATURE_SCALES = [0.1, 1, 1, 0.1, 1, 1, 1]
34
 
35
  def _fixture_path() -> Path:
36
  configured = os.environ.get("GNN4COLLIDERS_ROOT_FIXTURE")
37
- return Path(configured) if configured else Path("data/processed/ttH_NLO_64.root")
 
 
 
 
38
 
39
 
40
  @pytest.fixture(scope="module")
 
34
 
35
  def _fixture_path() -> Path:
36
  configured = os.environ.get("GNN4COLLIDERS_ROOT_FIXTURE")
37
+ return (
38
+ Path(configured)
39
+ if configured
40
+ else Path("data/fixtures/testing/ttH_NLO_64.root")
41
+ )
42
 
43
 
44
  @pytest.fixture(scope="module")
tests/integration/test_real_root_sample.py CHANGED
@@ -34,7 +34,7 @@ def _fixture_path() -> Path:
34
  configured = os.environ.get("GNN4COLLIDERS_ROOT_FIXTURE")
35
  if configured:
36
  return Path(configured)
37
- return Path("data/processed/ttH_NLO_64.root")
38
 
39
 
40
  @pytest.fixture(scope="module")
@@ -42,8 +42,8 @@ def root_tree():
42
  path = _fixture_path()
43
  if not path.exists():
44
  pytest.skip(
45
- f"ROOT sample fixture is absent: {path}; download ttH_NLO.root and "
46
- "create the reduced fixture first"
47
  )
48
  root_file = uproot.open(path)
49
  tree = root_file["output"]
 
34
  configured = os.environ.get("GNN4COLLIDERS_ROOT_FIXTURE")
35
  if configured:
36
  return Path(configured)
37
+ return Path("data/fixtures/testing/ttH_NLO_64.root")
38
 
39
 
40
  @pytest.fixture(scope="module")
 
42
  path = _fixture_path()
43
  if not path.exists():
44
  pytest.skip(
45
+ f"ROOT sample fixture is absent: {path}; run the public validation "
46
+ "command to download it"
47
  )
48
  root_file = uproot.open(path)
49
  tree = root_file["output"]
tests/unit/validation/test_fixture.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+
5
+ import pytest
6
+ from validation.fixture import TESTING_FIXTURE, ensure_fixture, verify_fixture
7
+
8
+
9
+ def test_checked_in_public_fixture_has_pinned_identity():
10
+ path = ensure_fixture("data/fixtures/testing/ttH_NLO_64.root", download=False)
11
+ assert path.stat().st_size == TESTING_FIXTURE.size_bytes
12
+
13
+
14
+ def test_fixture_checksum_mismatch_is_rejected(tmp_path):
15
+ path = tmp_path / "fixture.root"
16
+ shutil.copyfile("data/fixtures/testing/ttH_NLO_64.root", path)
17
+ path.write_bytes(path.read_bytes() + b"corruption")
18
+ with pytest.raises(ValueError, match=r"fixture (size|SHA-256) mismatch"):
19
+ verify_fixture(path)
validation/README.md CHANGED
@@ -7,38 +7,58 @@ flattened storage plus offsets. Exact fields (IDs, labels, folds, offsets,
7
  topology) are exact-compared; floating fields use `tolerances.yaml`.
8
 
9
  ```bash
10
- uv run python -m validation.run_full_validation --smoke
11
- uv run python -m validation.run_full_validation \
12
- --legacy-artifact validation_output/legacy \
13
- --rewrite-artifact validation_output/rewrite \
14
- --output validation_output/reports
15
  ```
16
 
17
- Smoke validates serialization and comparison plumbing only; it does not claim
18
- legacy parity. Full scientific validation uses the same ROOT sample in both
19
- runtimes, with `GNN4COLLIDERS_E2E_FIXTURE` selecting an external fixture. Keep
 
20
  generated output out of Git.
21
 
22
- ## Current HF fixture campaign
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- The small fixture can be extracted in both environments:
 
25
 
26
  ```bash
27
- ROOT=/global/cfs/projectdirs/atlas/joshua/gnn_data/hf_validation/testing/ttH_NLO_64.root
28
  HF_SHA=89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
29
 
30
  conda run -n dgl env PYTHONPATH=.:legacy/root_gnn_dgl:src \
31
- python validation/extract_legacy.py /tmp/legacy_ttree.root validation_output/legacy \
32
  --source-sha256 "$HF_SHA"
33
 
34
  PYTHONPATH=.:src uv run python validation/extract_rewrite.py "$ROOT" \
35
  validation_output/rewrite --source-sha256 "$HF_SHA"
36
  ```
37
 
38
- The legacy environment is Python 3.8/Uproot 5.3 and cannot read the HF
39
- RNTuple directly. Convert the fixture to a temporary TTree with a modern
40
- Uproot process; this changes only the ROOT container representation, not event
41
- values. The manifest retains the original HF SHA-256. Then run `forward.py`
42
  with `model_epoch_71.pt`, `train_step.py`, `compare_step.py`, and
43
  `compare_tasks.py` to generate fixed-forward, one-step, and task reports.
44
 
 
7
  topology) are exact-compared; floating fields use `tolerances.yaml`.
8
 
9
  ```bash
10
+ uv run python -m validation.run_public_validation
 
 
 
 
11
  ```
12
 
13
+ The command verifies the local fixture, downloads it from the pinned Hugging
14
+ Face revision when necessary, extracts the rewrite, compares it with the
15
+ checked-in legacy golden artifact, writes JSON and Markdown reports, and exits
16
+ nonzero on a scientific mismatch. Use `--no-download` for offline runs. Keep
17
  generated output out of Git.
18
 
19
+ The lower-level artifact tooling remains available for regenerating references
20
+ in the historical legacy environment:
21
+
22
+ ```bash
23
+ uv run python -m validation.run_full_validation --smoke
24
+ ```
25
+
26
+ ## Current public HF fixture
27
+
28
+ The normal public regression path requires only the current environment. It
29
+ verifies the pinned public fixture, downloads it when missing, extracts the
30
+ rewrite, and compares it with the legacy golden artifact:
31
+
32
+ ```bash
33
+ uv run python -m validation.run_public_validation
34
+ ```
35
+
36
+ The fixture identity is:
37
+
38
+ ```text
39
+ repository: HWresearch/Delphes (dataset)
40
+ revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
41
+ path: testing/ttH_NLO_64.root
42
+ sha256: 89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
43
+ events: 64
44
+ ```
45
 
46
+ The historical extraction commands below are retained only for regenerating or
47
+ auditing the golden artifact in a legacy environment:
48
 
49
  ```bash
50
+ ROOT=data/fixtures/testing/ttH_NLO_64.root
51
  HF_SHA=89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
52
 
53
  conda run -n dgl env PYTHONPATH=.:legacy/root_gnn_dgl:src \
54
+ python validation/extract_legacy.py "$ROOT" validation_output/legacy \
55
  --source-sha256 "$HF_SHA"
56
 
57
  PYTHONPATH=.:src uv run python validation/extract_rewrite.py "$ROOT" \
58
  validation_output/rewrite --source-sha256 "$HF_SHA"
59
  ```
60
 
61
+ The manifest retains the original HF SHA-256. Then run `forward.py`
 
 
 
62
  with `model_epoch_71.pt`, `train_step.py`, `compare_step.py`, and
63
  `compare_tasks.py` to generate fixed-forward, one-step, and task reports.
64
 
validation/extract_common.py CHANGED
@@ -136,7 +136,7 @@ def extract_root(
136
  "events": event_count,
137
  "sha256": file_sha256(path),
138
  "source_sha256": source_sha256 or file_sha256(path),
139
- "input_path": str(path),
140
  "feature_schema": [
141
  "pt",
142
  "eta",
 
136
  "events": event_count,
137
  "sha256": file_sha256(path),
138
  "source_sha256": source_sha256 or file_sha256(path),
139
+ "input_path": path.name,
140
  "feature_schema": [
141
  "pt",
142
  "eta",
validation/fixture.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pinned public validation-fixture discovery, download, and verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import tempfile
8
+ import urllib.request
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PublicFixture:
15
+ repository: str
16
+ revision: str
17
+ repository_path: str
18
+ sha256: str
19
+ size_bytes: int
20
+
21
+ @property
22
+ def url(self) -> str:
23
+ return (
24
+ f"https://huggingface.co/datasets/{self.repository}/resolve/"
25
+ f"{self.revision}/{self.repository_path}?download=true"
26
+ )
27
+
28
+
29
+ TESTING_FIXTURE = PublicFixture(
30
+ repository="HWresearch/Delphes",
31
+ revision="76a6c362bfba8e766ba7255a5c08a55257f78d0e",
32
+ repository_path="testing/ttH_NLO_64.root",
33
+ sha256="89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec",
34
+ size_bytes=15050,
35
+ )
36
+
37
+
38
+ def default_fixture_path(root: str | Path = ".") -> Path:
39
+ return Path(root) / "data" / "fixtures" / "testing" / "ttH_NLO_64.root"
40
+
41
+
42
+ def file_sha256(path: str | Path) -> str:
43
+ digest = hashlib.sha256()
44
+ with Path(path).open("rb") as stream:
45
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
46
+ digest.update(chunk)
47
+ return digest.hexdigest()
48
+
49
+
50
+ def verify_fixture(path: str | Path, fixture: PublicFixture = TESTING_FIXTURE) -> Path:
51
+ """Verify size and SHA-256 before allowing a fixture into validation."""
52
+
53
+ path = Path(path)
54
+ if not path.is_file():
55
+ raise FileNotFoundError(f"validation fixture is missing: {path}")
56
+ actual_size = path.stat().st_size
57
+ if actual_size != fixture.size_bytes:
58
+ raise ValueError(
59
+ f"fixture size mismatch for {path}: expected {fixture.size_bytes}, "
60
+ f"got {actual_size}"
61
+ )
62
+ actual_sha256 = file_sha256(path)
63
+ if actual_sha256 != fixture.sha256:
64
+ raise ValueError(
65
+ f"fixture SHA-256 mismatch for {path}: expected {fixture.sha256}, "
66
+ f"got {actual_sha256}"
67
+ )
68
+ return path
69
+
70
+
71
+ def download_fixture(
72
+ path: str | Path,
73
+ fixture: PublicFixture = TESTING_FIXTURE,
74
+ *,
75
+ timeout: float = 60.0,
76
+ ) -> Path:
77
+ """Download a pinned fixture atomically and verify it before returning."""
78
+
79
+ path = Path(path)
80
+ path.parent.mkdir(parents=True, exist_ok=True)
81
+ fd, temporary_name = tempfile.mkstemp(
82
+ prefix=f".{path.name}.", suffix=".download", dir=path.parent
83
+ )
84
+ os.close(fd)
85
+ temporary = Path(temporary_name)
86
+ try:
87
+ with urllib.request.urlopen(fixture.url, timeout=timeout) as response:
88
+ with temporary.open("wb") as output:
89
+ while chunk := response.read(1024 * 1024):
90
+ output.write(chunk)
91
+ verify_fixture(temporary, fixture)
92
+ os.replace(temporary, path)
93
+ finally:
94
+ temporary.unlink(missing_ok=True)
95
+ return path
96
+
97
+
98
+ def ensure_fixture(
99
+ path: str | Path | None = None,
100
+ *,
101
+ download: bool = True,
102
+ fixture: PublicFixture = TESTING_FIXTURE,
103
+ ) -> Path:
104
+ """Return a verified local fixture, downloading it when necessary."""
105
+
106
+ path = Path(path) if path is not None else default_fixture_path()
107
+ if path.is_file():
108
+ return verify_fixture(path, fixture)
109
+ if not download:
110
+ raise FileNotFoundError(f"validation fixture is missing: {path}")
111
+ return download_fixture(path, fixture)
validation/golden/testing/legacy/artifact.npz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cc5164e3eda8f88991fc87a70e13e766b1ae2ef3604a614273d9f6c52cdcf792
3
+ size 40151
validation/golden/testing/legacy/manifest.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "arrays": [
3
+ "edge_dst_flat",
4
+ "edge_features_flat",
5
+ "edge_offsets",
6
+ "edge_src_flat",
7
+ "folds",
8
+ "globals",
9
+ "labels",
10
+ "node_features_flat",
11
+ "node_offsets",
12
+ "sample_id",
13
+ "weights"
14
+ ],
15
+ "event_count": 64,
16
+ "events": 64,
17
+ "feature_schema": [
18
+ "pt",
19
+ "eta",
20
+ "phi",
21
+ "energy",
22
+ "btag",
23
+ "charge",
24
+ "node_type"
25
+ ],
26
+ "input_path": "ttH_NLO_64.root",
27
+ "repository_path": "testing/ttH_NLO_64.root",
28
+ "schema_version": 1,
29
+ "sha256": "89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec",
30
+ "source": "huggingface://datasets/HWresearch/Delphes",
31
+ "source_sha256": "89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec",
32
+ "tree": "output"
33
+ }
validation/manifests/dataset.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "source": "https://huggingface.co/datasets/HWresearch/Delphes",
3
- "revision": "main",
4
  "files": [
5
  {
6
  "repository_path": "testing/ttH_NLO_64.root",
 
1
  {
2
  "source": "https://huggingface.co/datasets/HWresearch/Delphes",
3
+ "revision": "76a6c362bfba8e766ba7255a5c08a55257f78d0e",
4
  "files": [
5
  {
6
  "repository_path": "testing/ttH_NLO_64.root",
validation/run_public_validation.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run rewrite-vs-golden validation using the pinned public HF fixture."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from validation.compare import compare_artifacts, write_reports
11
+ from validation.extract_common import (
12
+ FEATURE_BRANCHES,
13
+ OBJECT_TYPES,
14
+ SCALES,
15
+ extract_root,
16
+ )
17
+ from validation.fixture import TESTING_FIXTURE, ensure_fixture
18
+
19
+
20
+ def _extract_rewrite(root_file: Path, output: Path) -> None:
21
+ from gnn4colliders.features import build_node_features
22
+
23
+ def build(event):
24
+ features, _ = build_node_features(event, FEATURE_BRANCHES, OBJECT_TYPES, SCALES)
25
+ return features
26
+
27
+ extract_root(
28
+ root_file,
29
+ output,
30
+ build_features=build,
31
+ sample_name=TESTING_FIXTURE.repository_path,
32
+ source_sha256=TESTING_FIXTURE.sha256,
33
+ )
34
+
35
+
36
+ def main() -> int:
37
+ parser = argparse.ArgumentParser(description=__doc__)
38
+ parser.add_argument("--fixture", type=Path)
39
+ parser.add_argument("--output", type=Path, default=Path("validation_output/public"))
40
+ parser.add_argument(
41
+ "--no-download",
42
+ action="store_true",
43
+ help="fail instead of downloading a missing fixture",
44
+ )
45
+ args = parser.parse_args()
46
+
47
+ fixture = ensure_fixture(args.fixture, download=not args.no_download)
48
+ golden = Path(__file__).parent / "golden" / "testing" / "legacy"
49
+ if not (golden / "manifest.json").is_file():
50
+ raise FileNotFoundError(f"golden reference is missing: {golden}")
51
+ rewrite = args.output / "rewrite"
52
+ _extract_rewrite(fixture, rewrite)
53
+ report = compare_artifacts(golden, rewrite)
54
+ report["fixture"] = {
55
+ "repository": TESTING_FIXTURE.repository,
56
+ "revision": TESTING_FIXTURE.revision,
57
+ "repository_path": TESTING_FIXTURE.repository_path,
58
+ "sha256": TESTING_FIXTURE.sha256,
59
+ "size_bytes": TESTING_FIXTURE.size_bytes,
60
+ }
61
+ report_dir = args.output / "report"
62
+ write_reports(report, report_dir)
63
+ print(
64
+ json.dumps(
65
+ {
66
+ "overall_status": report["overall_status"],
67
+ "fixture": str(fixture),
68
+ "report": str(report_dir),
69
+ },
70
+ indent=2,
71
+ )
72
+ )
73
+ return 0 if report["overall_status"] == "PASS" else 1
74
+
75
+
76
+ if __name__ == "__main__":
77
+ raise SystemExit(main())