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

PR 2 — Portable Public End-to-End Validation Fixture (#11)

Browse files

- validation: use pinned public Hugging Face fixture (528346c0a2dcf57bc6f47e0e929113ae0f5ec47a)
- data: resolve training sources from Hugging Face (02920c2576f35911f7068c0379c06585ae9c8b57)

data/fixtures/testing/ttH_NLO_64.root ADDED
Binary file (15.1 kB). View file
 
docs/configuration.md CHANGED
@@ -76,6 +76,37 @@ selection and must be disjoint. Model/task mismatches are rejected during
76
  config validation; binary tasks require `model.out_size=1`, while multiclass
77
  tasks require `model.out_size=task.num_classes`.
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  ## Checkpoints and resolved configuration
80
 
81
  Training writes `epoch_####.pt` and the fully resolved configuration at
 
76
  config validation; binary tasks require `model.out_size=1`, while multiclass
77
  tasks require `model.out_size=task.num_classes`.
78
 
79
+ ## Hugging Face ROOT data
80
+
81
+ Training preparation can resolve ROOT files directly from a pinned Hugging Face
82
+ dataset. Use `data.source` instead of local `data.files`:
83
+
84
+ ```yaml
85
+ source:
86
+ type: huggingface
87
+ repo_id: HWresearch/Delphes
88
+ repo_type: dataset
89
+ revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
90
+ files:
91
+ - path: testing/ttH_NLO_64.root
92
+ label: 0
93
+ sha256: 89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
94
+ size_bytes: 15050
95
+ ```
96
+
97
+ The resolver uses the Hugging Face cache, downloads only configured files, and
98
+ verifies optional size and SHA-256 metadata before ROOT ingestion. Ready-to-use
99
+ configurations are `config_hf_smoke` for the 64-event fixture and
100
+ `config_hf_delphes` for the pinned 12-process pretraining sources:
101
+
102
+ ```bash
103
+ uv run gnn4colliders prepare --config-name config_hf_smoke
104
+ uv run gnn4colliders prepare --config-name config_hf_delphes
105
+ ```
106
+
107
+ The full Delphes configuration may download a large dataset. It is never used
108
+ by the default configuration and is cached by the Hub client for later runs.
109
+
110
  ## Checkpoints and resolved configuration
111
 
112
  Training writes `epoch_####.pt` and the fully resolved configuration at
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
pyproject.toml CHANGED
@@ -11,6 +11,7 @@ requires-python = ">=3.12,<3.13"
11
  dependencies = [
12
  "awkward>=2.0",
13
  "hydra-core>=1.3",
 
14
  "numpy>=1.24,<2",
15
  "scikit-learn>=1.3",
16
  "torch==2.2.2",
 
11
  dependencies = [
12
  "awkward>=2.0",
13
  "hydra-core>=1.3",
14
+ "huggingface-hub>=0.36,<1",
15
  "numpy>=1.24,<2",
16
  "scikit-learn>=1.3",
17
  "torch==2.2.2",
src/gnn4colliders/cli/__init__.py CHANGED
@@ -26,7 +26,7 @@ def _is_main_process() -> bool:
26
 
27
  def _help(command: str | None = None) -> None:
28
  if command:
29
- print(f"Usage: gnn4colliders {command} [key=value ...]")
30
  print("Compose Hydra configuration with semantic overrides.")
31
  else:
32
  print(
@@ -36,11 +36,36 @@ def _help(command: str | None = None) -> None:
36
  print("Use gnn4colliders <command> --help for command help.")
37
 
38
 
39
- def _config(overrides: list[str]):
40
  config_resource = resources.files("gnn4colliders.configs")
41
  with resources.as_file(config_resource) as config_dir:
42
  with initialize_config_dir(version_base=None, config_dir=str(config_dir)):
43
- return compose(config_name="config", overrides=overrides)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
  def main(argv: list[str] | None = None) -> int:
@@ -59,7 +84,8 @@ def main(argv: list[str] | None = None) -> int:
59
  _help(command)
60
  return 0
61
  try:
62
- config = _config(args)
 
63
  logging.basicConfig(
64
  level=getattr(logging, str(config.logging.level).upper(), logging.INFO)
65
  )
 
26
 
27
  def _help(command: str | None = None) -> None:
28
  if command:
29
+ print(f"Usage: gnn4colliders {command} [--config-name NAME] [key=value ...]")
30
  print("Compose Hydra configuration with semantic overrides.")
31
  else:
32
  print(
 
36
  print("Use gnn4colliders <command> --help for command help.")
37
 
38
 
39
+ def _config(overrides: list[str], *, config_name: str = "config"):
40
  config_resource = resources.files("gnn4colliders.configs")
41
  with resources.as_file(config_resource) as config_dir:
42
  with initialize_config_dir(version_base=None, config_dir=str(config_dir)):
43
+ return compose(config_name=config_name, overrides=overrides)
44
+
45
+
46
+ def _config_name(args: list[str]) -> tuple[str, list[str]]:
47
+ """Extract the optional config name while preserving Hydra overrides."""
48
+
49
+ remaining: list[str] = []
50
+ config_name = "config"
51
+ index = 0
52
+ while index < len(args):
53
+ argument = args[index]
54
+ if argument == "--config-name":
55
+ if index + 1 >= len(args):
56
+ raise ValueError("--config-name requires a config name")
57
+ config_name = args[index + 1]
58
+ index += 2
59
+ continue
60
+ if argument.startswith("--config-name="):
61
+ config_name = argument.split("=", 1)[1]
62
+ if not config_name:
63
+ raise ValueError("--config-name requires a config name")
64
+ index += 1
65
+ continue
66
+ remaining.append(argument)
67
+ index += 1
68
+ return config_name, remaining
69
 
70
 
71
  def main(argv: list[str] | None = None) -> int:
 
84
  _help(command)
85
  return 0
86
  try:
87
+ config_name, overrides = _config_name(args)
88
+ config = _config(overrides, config_name=config_name)
89
  logging.basicConfig(
90
  level=getattr(logging, str(config.logging.level).upper(), logging.INFO)
91
  )
src/gnn4colliders/config/application.py CHANGED
@@ -18,6 +18,7 @@ from gnn4colliders.data import (
18
  GraphSampleCache,
19
  RootEventDataset,
20
  SplitDefinition,
 
21
  select_split,
22
  )
23
  from gnn4colliders.distributed import (
@@ -108,8 +109,7 @@ def prepare(config: DictConfig) -> Path:
108
  "prepare does not support distributed launch; run it once on rank 0"
109
  )
110
  data = config.data
111
- if not data.files:
112
- raise ValueError("prepare requires data.files")
113
  feature_branches = plain(data.get("feature_branches"))
114
  object_types = plain(data.get("object_types"))
115
  scales = plain(data.get("scales"))
@@ -118,9 +118,9 @@ def prepare(config: DictConfig) -> Path:
118
  "prepare requires data.feature_branches, object_types, and scales"
119
  )
120
  source = RootEventDataset(
121
- data.files,
122
  tree_name=str(data.tree_name),
123
- label=data.get("label", 1),
124
  feature_branches=feature_branches,
125
  global_features=plain(data.get("global_features", [])),
126
  fold_var=str(data.get("fold_var", "eventNumber")),
 
18
  GraphSampleCache,
19
  RootEventDataset,
20
  SplitDefinition,
21
+ resolve_data_files,
22
  select_split,
23
  )
24
  from gnn4colliders.distributed import (
 
109
  "prepare does not support distributed launch; run it once on rank 0"
110
  )
111
  data = config.data
112
+ files, labels = resolve_data_files(data)
 
113
  feature_branches = plain(data.get("feature_branches"))
114
  object_types = plain(data.get("object_types"))
115
  scales = plain(data.get("scales"))
 
118
  "prepare requires data.feature_branches, object_types, and scales"
119
  )
120
  source = RootEventDataset(
121
+ files,
122
  tree_name=str(data.tree_name),
123
+ label=labels,
124
  feature_branches=feature_branches,
125
  global_features=plain(data.get("global_features", [])),
126
  fold_var=str(data.get("fold_var", "eventNumber")),
src/gnn4colliders/configs/config_hf_delphes.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - data: hf_delphes
3
+ - model: root_gnn/edge_network
4
+ - task: pretraining_multiclass
5
+ - trainer: default
6
+ - checkpoint: default
7
+ - inference: default
8
+ - environment: local
9
+ - distributed: single
10
+ - export: onnx
11
+ - _self_
12
+
13
+ experiment:
14
+ name: hf_delphes_pretraining
15
+ logging:
16
+ level: INFO
src/gnn4colliders/configs/config_hf_smoke.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - data: hf_smoke
3
+ - model: root_gnn/edge_network_binary
4
+ - task: binary_classification
5
+ - trainer: debug
6
+ - checkpoint: default
7
+ - inference: default
8
+ - environment: local
9
+ - distributed: single
10
+ - export: onnx
11
+ - _self_
12
+
13
+ experiment:
14
+ name: hf_smoke
15
+ logging:
16
+ level: INFO
src/gnn4colliders/configs/data/hf_delphes.yaml ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - _self_
3
+
4
+ source:
5
+ type: huggingface
6
+ repo_id: HWresearch/Delphes
7
+ repo_type: dataset
8
+ revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
9
+ cache_dir: null
10
+ files:
11
+ - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
12
+ - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
13
+ - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
14
+ - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
15
+ - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
16
+ - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
17
+ - {path: samples/photon/ttyy/ttyy.root, label: 6}
18
+ - {path: samples/top/multitop/tttt.root, label: 7}
19
+ - {path: samples/top/single-top/SingleT_schan.root, label: 8}
20
+ - {path: samples/top/ttbar/ttbar.root, label: 9}
21
+ - {path: samples/top/ttv/ttW.root, label: 10}
22
+ - {path: samples/top/multitop/ttt.root, label: 11}
23
+ tree_name: output
24
+ feature_branches:
25
+ - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
26
+ - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
27
+ - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
28
+ - CALC_E
29
+ - [jet_btag, 0, 0, 0, 0]
30
+ - [0, ele_charge, mu_charge, 0, 0]
31
+ - NODE_TYPE
32
+ object_types: [vector, vector, vector, vector, single]
33
+ scales: [0.1, 1, 1, 0.1, 1, 1, 1]
34
+ global_features: []
35
+ fold_var: Number
36
+ weight_var: weight
37
+ batch_size: 1024
38
+ num_workers: 0
39
+ shuffle: true
40
+ seed: 42
41
+ cache:
42
+ path: outputs/hf_delphes/graphs
43
+ splits:
44
+ train_folds: [1, 2, 3]
45
+ validation_folds: [0]
46
+ test_folds: [4]
src/gnn4colliders/configs/data/hf_smoke.yaml ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - _self_
3
+
4
+ source:
5
+ type: huggingface
6
+ repo_id: HWresearch/Delphes
7
+ repo_type: dataset
8
+ revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
9
+ cache_dir: null
10
+ files:
11
+ - path: testing/ttH_NLO_64.root
12
+ label: 0
13
+ sha256: 89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
14
+ size_bytes: 15050
15
+ tree_name: output
16
+ feature_branches:
17
+ - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
18
+ - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
19
+ - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
20
+ - CALC_E
21
+ - [jet_btag, 0, 0, 0, 0]
22
+ - [0, ele_charge, mu_charge, 0, 0]
23
+ - NODE_TYPE
24
+ object_types: [vector, vector, vector, vector, single]
25
+ scales: [0.1, 1, 1, 0.1, 1, 1, 1]
26
+ global_features: []
27
+ fold_var: Number
28
+ weight_var: weight
29
+ batch_size: 8
30
+ num_workers: 0
31
+ shuffle: true
32
+ seed: 42
33
+ cache:
34
+ path: outputs/hf_smoke/graphs
35
+ splits:
36
+ train_folds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
37
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
38
+ 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43,
39
+ 44, 45, 46, 47]
40
+ validation_folds: [48, 49, 50, 51, 52, 53, 54, 55]
41
+ test_folds: [56, 57, 58, 59, 60, 61, 62, 63]
src/gnn4colliders/configs/model/root_gnn/edge_network_binary.yaml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ defaults:
2
+ - /model/root_gnn/edge_network
3
+ - _self_
4
+
5
+ out_size: 1
src/gnn4colliders/data/__init__.py CHANGED
@@ -11,6 +11,7 @@ from .graph_dataset import (
11
  TensorGraph,
12
  batch_graph_samples,
13
  )
 
14
  from .metadata import (
15
  CACHE_SCHEMA_VERSION,
16
  FEATURE_SCHEMA_VERSION,
@@ -42,6 +43,7 @@ __all__ = [
42
  "GRAPH_SCHEMA_VERSION",
43
  "CACHE_SCHEMA_VERSION",
44
  "RootEventDataset",
 
45
  "branch_names_from_specs",
46
  "read_tree",
47
  "tree_num_entries",
 
11
  TensorGraph,
12
  batch_graph_samples,
13
  )
14
+ from .huggingface import resolve_data_files
15
  from .metadata import (
16
  CACHE_SCHEMA_VERSION,
17
  FEATURE_SCHEMA_VERSION,
 
43
  "GRAPH_SCHEMA_VERSION",
44
  "CACHE_SCHEMA_VERSION",
45
  "RootEventDataset",
46
+ "resolve_data_files",
47
  "branch_names_from_specs",
48
  "read_tree",
49
  "tree_num_entries",
src/gnn4colliders/data/huggingface.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Hub data-source resolution for ROOT event files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from collections.abc import Mapping, Sequence
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from huggingface_hub import hf_hub_download
11
+
12
+
13
+ def _sha256(path: Path) -> str:
14
+ digest = hashlib.sha256()
15
+ with path.open("rb") as stream:
16
+ for chunk in iter(lambda: stream.read(1024 * 1024), b""):
17
+ digest.update(chunk)
18
+ return digest.hexdigest()
19
+
20
+
21
+ def _verify_download(path: Path, entry: Mapping[str, Any]) -> Path:
22
+ expected_size = entry.get("size_bytes")
23
+ if expected_size is not None and path.stat().st_size != int(expected_size):
24
+ raise ValueError(
25
+ f"Hugging Face file size mismatch for {path}: expected "
26
+ f"{expected_size}, got {path.stat().st_size}"
27
+ )
28
+ expected_sha256 = entry.get("sha256")
29
+ if expected_sha256 is not None:
30
+ actual_sha256 = _sha256(path)
31
+ if actual_sha256 != str(expected_sha256):
32
+ raise ValueError(
33
+ f"Hugging Face file SHA-256 mismatch for {path}: expected "
34
+ f"{expected_sha256}, got {actual_sha256}"
35
+ )
36
+ return path
37
+
38
+
39
+ def _entries(source: Mapping[str, Any]) -> list[Mapping[str, Any]]:
40
+ values = source.get("files", [])
41
+ if not isinstance(values, Sequence) or isinstance(values, (str, bytes)):
42
+ raise TypeError("data.source.files must be a sequence")
43
+ entries: list[Mapping[str, Any]] = []
44
+ for value in values:
45
+ if isinstance(value, str):
46
+ entries.append({"path": value})
47
+ elif isinstance(value, Mapping):
48
+ entries.append(value)
49
+ else:
50
+ raise TypeError("each data.source.files entry must be a path or mapping")
51
+ return entries
52
+
53
+
54
+ def resolve_data_files(
55
+ data: Mapping[str, Any],
56
+ ) -> tuple[list[Path], Any]:
57
+ """Resolve configured ROOT files and labels for preparation.
58
+
59
+ A Hugging Face source is pinned by repository revision. Downloads use the
60
+ Hub cache and are verified when an entry supplies ``sha256`` or
61
+ ``size_bytes``. The returned paths have the same contract as the existing
62
+ local ``data.files`` interface.
63
+ """
64
+
65
+ source = data.get("source")
66
+ if not source:
67
+ files = data.get("files", [])
68
+ if not files:
69
+ raise ValueError("data.files or data.source is required")
70
+ return [Path(path) for path in files], data.get("label", 1)
71
+ if not isinstance(source, Mapping):
72
+ raise TypeError("data.source must be a mapping")
73
+ if str(source.get("type", "")).lower() != "huggingface":
74
+ raise ValueError(f"unsupported data.source.type: {source.get('type')!r}")
75
+ repo_id = source.get("repo_id")
76
+ revision = source.get("revision")
77
+ if not repo_id or not revision:
78
+ raise ValueError("Hugging Face data sources require repo_id and revision")
79
+ repo_type = str(source.get("repo_type", "dataset"))
80
+ cache_dir = source.get("cache_dir")
81
+ local_files_only = bool(source.get("local_files_only", False))
82
+ entries = _entries(source)
83
+ if not entries:
84
+ raise ValueError("data.source.files must not be empty")
85
+
86
+ paths: list[Path] = []
87
+ labels: list[Any] = []
88
+ has_entry_labels = []
89
+ for entry in entries:
90
+ filename = entry.get("path")
91
+ if not filename:
92
+ raise ValueError("each Hugging Face source file needs a path")
93
+ kwargs = {
94
+ "repo_id": str(repo_id),
95
+ "filename": str(filename),
96
+ "repo_type": repo_type,
97
+ "revision": str(revision),
98
+ "cache_dir": cache_dir,
99
+ "local_files_only": local_files_only,
100
+ }
101
+ path = Path(hf_hub_download(**kwargs))
102
+ paths.append(_verify_download(path, entry))
103
+ has_entry_labels.append("label" in entry)
104
+ labels.append(entry.get("label"))
105
+ if any(has_entry_labels) and not all(has_entry_labels):
106
+ raise ValueError("either every or no Hugging Face source file needs a label")
107
+ return paths, labels if all(has_entry_labels) else data.get("label", 1)
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/data/test_huggingface_source.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from gnn4colliders.data.huggingface import resolve_data_files
9
+
10
+
11
+ def test_huggingface_source_resolves_files_and_labels(monkeypatch, tmp_path):
12
+ root_file = tmp_path / "sample.root"
13
+ root_file.write_bytes(b"fixture")
14
+ monkeypatch.setattr(
15
+ "gnn4colliders.data.huggingface.hf_hub_download",
16
+ lambda **kwargs: str(root_file),
17
+ )
18
+ paths, labels = resolve_data_files(
19
+ {
20
+ "source": {
21
+ "type": "huggingface",
22
+ "repo_id": "org/data",
23
+ "revision": "abc123",
24
+ "files": [{"path": "sample.root", "label": 4}],
25
+ }
26
+ }
27
+ )
28
+ assert paths == [root_file]
29
+ assert labels == [4]
30
+
31
+
32
+ def test_huggingface_source_verifies_checksum(monkeypatch, tmp_path):
33
+ root_file = tmp_path / "sample.root"
34
+ root_file.write_bytes(b"fixture")
35
+ monkeypatch.setattr(
36
+ "gnn4colliders.data.huggingface.hf_hub_download",
37
+ lambda **kwargs: str(root_file),
38
+ )
39
+ checksum = hashlib.sha256(b"wrong").hexdigest()
40
+ with pytest.raises(ValueError, match="SHA-256 mismatch"):
41
+ resolve_data_files(
42
+ {
43
+ "source": {
44
+ "type": "huggingface",
45
+ "repo_id": "org/data",
46
+ "revision": "abc123",
47
+ "files": [{"path": "sample.root", "sha256": checksum}],
48
+ }
49
+ }
50
+ )
51
+
52
+
53
+ def test_local_source_remains_unchanged(tmp_path: Path):
54
+ local = tmp_path / "events.root"
55
+ paths, label = resolve_data_files({"files": [str(local)], "label": 2})
56
+ assert paths == [local]
57
+ assert label == 2
tests/unit/test_package_import.py CHANGED
@@ -37,3 +37,11 @@ def test_hydra_config_is_discoverable_from_package() -> None:
37
  config = _config([])
38
  assert config.model.name == "edge_network"
39
  assert config.task.type == "multiclass_classification"
 
 
 
 
 
 
 
 
 
37
  config = _config([])
38
  assert config.model.name == "edge_network"
39
  assert config.task.type == "multiclass_classification"
40
+
41
+
42
+ def test_huggingface_training_config_is_discoverable() -> None:
43
+ from gnn4colliders.cli import _config
44
+
45
+ config = _config([], config_name="config_hf_smoke")
46
+ assert config.data.source.repo_id == "HWresearch/Delphes"
47
+ assert config.model.out_size == 1
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)
uv.lock CHANGED
@@ -348,6 +348,7 @@ name = "gnn4colliders"
348
  source = { editable = "." }
349
  dependencies = [
350
  { name = "awkward" },
 
351
  { name = "hydra-core" },
352
  { name = "numpy" },
353
  { name = "scikit-learn" },
@@ -384,6 +385,7 @@ requires-dist = [
384
  { name = "awkward", specifier = ">=2.0" },
385
  { name = "dgl", marker = "platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'root-gnn'", specifier = "==2.2.0" },
386
  { name = "dgl", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'root-gnn'", specifier = "==2.4.0+cu121", index = "https://data.dgl.ai/wheels/torch-2.2/cu121/repo.html" },
 
387
  { name = "hydra-core", specifier = ">=1.3" },
388
  { name = "numpy", specifier = ">=1.24,<2" },
389
  { name = "onnx", marker = "extra == 'onnx'", specifier = ">=1.16,<2" },
@@ -408,6 +410,41 @@ dev = [
408
  { name = "twine", specifier = ">=6,<7" },
409
  ]
410
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  [[package]]
412
  name = "hydra-core"
413
  version = "1.3.5"
@@ -1340,6 +1377,9 @@ wheels = [
1340
  name = "tqdm"
1341
  version = "4.70.0"
1342
  source = { registry = "https://pypi.org/simple" }
 
 
 
1343
  sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
1344
  wheels = [
1345
  { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
 
348
  source = { editable = "." }
349
  dependencies = [
350
  { name = "awkward" },
351
+ { name = "huggingface-hub" },
352
  { name = "hydra-core" },
353
  { name = "numpy" },
354
  { name = "scikit-learn" },
 
385
  { name = "awkward", specifier = ">=2.0" },
386
  { name = "dgl", marker = "platform_machine == 'arm64' and sys_platform == 'darwin' and extra == 'root-gnn'", specifier = "==2.2.0" },
387
  { name = "dgl", marker = "platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'root-gnn'", specifier = "==2.4.0+cu121", index = "https://data.dgl.ai/wheels/torch-2.2/cu121/repo.html" },
388
+ { name = "huggingface-hub", specifier = ">=0.36,<1" },
389
  { name = "hydra-core", specifier = ">=1.3" },
390
  { name = "numpy", specifier = ">=1.24,<2" },
391
  { name = "onnx", marker = "extra == 'onnx'", specifier = ">=1.16,<2" },
 
410
  { name = "twine", specifier = ">=6,<7" },
411
  ]
412
 
413
+ [[package]]
414
+ name = "hf-xet"
415
+ version = "1.6.0"
416
+ source = { registry = "https://pypi.org/simple" }
417
+ sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
418
+ wheels = [
419
+ { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
420
+ { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
421
+ { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
422
+ { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
423
+ { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
424
+ { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
425
+ { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
426
+ { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
427
+ ]
428
+
429
+ [[package]]
430
+ name = "huggingface-hub"
431
+ version = "0.36.2"
432
+ source = { registry = "https://pypi.org/simple" }
433
+ dependencies = [
434
+ { name = "filelock" },
435
+ { name = "fsspec" },
436
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
437
+ { name = "packaging" },
438
+ { name = "pyyaml" },
439
+ { name = "requests" },
440
+ { name = "tqdm" },
441
+ { name = "typing-extensions" },
442
+ ]
443
+ sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
444
+ wheels = [
445
+ { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
446
+ ]
447
+
448
  [[package]]
449
  name = "hydra-core"
450
  version = "1.3.5"
 
1377
  name = "tqdm"
1378
  version = "4.70.0"
1379
  source = { registry = "https://pypi.org/simple" }
1380
+ dependencies = [
1381
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1382
+ ]
1383
  sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" }
1384
  wheels = [
1385
  { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" },
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())