PR 2 — Portable Public End-to-End Validation Fixture
#11
by ho22joshua - opened
- data/fixtures/testing/ttH_NLO_64.root +0 -0
- docs/configuration.md +31 -0
- docs/end_to_end_validation.md +9 -0
- pyproject.toml +1 -0
- src/gnn4colliders/cli/__init__.py +30 -4
- src/gnn4colliders/config/application.py +4 -4
- src/gnn4colliders/configs/config_hf_delphes.yaml +16 -0
- src/gnn4colliders/configs/config_hf_smoke.yaml +16 -0
- src/gnn4colliders/configs/data/hf_delphes.yaml +46 -0
- src/gnn4colliders/configs/data/hf_smoke.yaml +41 -0
- src/gnn4colliders/configs/model/root_gnn/edge_network_binary.yaml +5 -0
- src/gnn4colliders/data/__init__.py +2 -0
- src/gnn4colliders/data/huggingface.py +107 -0
- tests/integration/test_real_root_model.py +5 -1
- tests/integration/test_real_root_sample.py +3 -3
- tests/unit/data/test_huggingface_source.py +57 -0
- tests/unit/test_package_import.py +8 -0
- tests/unit/validation/test_fixture.py +19 -0
- uv.lock +40 -0
- validation/README.md +36 -16
- validation/extract_common.py +1 -1
- validation/fixture.py +111 -0
- validation/golden/testing/legacy/artifact.npz +3 -0
- validation/golden/testing/legacy/manifest.json +33 -0
- validation/manifests/dataset.json +1 -1
- validation/run_public_validation.py +77 -0
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=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 122 |
tree_name=str(data.tree_name),
|
| 123 |
-
label=
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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};
|
| 46 |
-
"
|
| 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.
|
| 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 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
| 20 |
generated output out of Git.
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
The
|
|
|
|
| 25 |
|
| 26 |
```bash
|
| 27 |
-
ROOT=/
|
| 28 |
HF_SHA=89d69eae9cd4d28a5d414d77bbc07185ea5b7de03481fdafe28899e2f3a09dec
|
| 29 |
|
| 30 |
conda run -n dgl env PYTHONPATH=.:legacy/root_gnn_dgl:src \
|
| 31 |
-
python validation/extract_legacy.py
|
| 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
|
| 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":
|
| 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": "
|
| 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())
|