PR 7 — Pretraining Objective Experiment Framework

#16
This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .github/workflows/ci.yml +87 -0
  2. .gitignore +22 -6
  3. .python-version +1 -1
  4. AGENTS.md +463 -38
  5. CHANGELOG.md +13 -0
  6. MANIFEST.in +3 -0
  7. README.md +1 -0
  8. README_PROJECT.md +282 -0
  9. benchmarks/README.md +48 -0
  10. benchmarks/__init__.py +1 -0
  11. benchmarks/_common.py +70 -0
  12. benchmarks/benchmark_dataloader.py +63 -0
  13. benchmarks/benchmark_inference.py +52 -0
  14. benchmarks/benchmark_preprocessing.py +61 -0
  15. benchmarks/benchmark_training.py +73 -0
  16. configs/.gitkeep +1 -0
  17. configs/data/.gitkeep +1 -0
  18. configs/delphes_contrastive.yaml +0 -81
  19. configs/delphes_jepa.yaml +0 -77
  20. configs/delphes_jepa_edge.yaml +0 -82
  21. configs/delphes_masked.yaml +0 -78
  22. configs/delphes_pfn.yaml +0 -86
  23. configs/delphes_pretraining.yaml +0 -61
  24. configs/delphes_tabicl.yaml +0 -89
  25. configs/delphes_tabpfn.yaml +0 -89
  26. configs/environment/.gitkeep +1 -0
  27. configs/gamgam.yaml +0 -97
  28. configs/gamgam_deep_sets.yaml +0 -88
  29. configs/gamgam_finetune.yaml +0 -97
  30. configs/gamgam_jepa_finetune.yaml +0 -95
  31. configs/gamgam_pfn.yaml +0 -90
  32. configs/gamgam_tabdpt.yaml +0 -91
  33. configs/gamgam_tabicl.yaml +0 -95
  34. configs/gamgam_tabpfn.yaml +0 -96
  35. configs/gamgam_transformer.yaml +0 -92
  36. configs/model/.gitkeep +1 -0
  37. configs/model/root_gnn/.gitkeep +1 -0
  38. configs/opendata_pfn.yaml +0 -0
  39. configs/task/.gitkeep +1 -0
  40. configs/trainer/.gitkeep +1 -0
  41. configs/tth_cp_even_odd.yaml +0 -49
  42. data/fixtures/.gitkeep +1 -0
  43. data/raw/.gitkeep +1 -0
  44. docs/architecture.md +378 -0
  45. docs/compatibility.md +22 -0
  46. docs/configuration.md +96 -0
  47. docs/design.md +0 -540
  48. docs/end_to_end_validation.md +30 -0
  49. docs/export.md +36 -0
  50. docs/migration.md +295 -0
.github/workflows/ci.yml ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ concurrency:
8
+ group: ci-${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ lint:
13
+ name: lint
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v6
18
+ with:
19
+ version: "0.8.x"
20
+ enable-cache: true
21
+ - run: uv sync --dev
22
+ - run: uv run ruff check .
23
+ - run: uv run ruff format --check .
24
+
25
+ test:
26
+ name: test
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v6
31
+ with:
32
+ version: "0.8.x"
33
+ enable-cache: true
34
+ - run: uv sync --dev
35
+ - run: uv run pytest
36
+
37
+ test-macos:
38
+ name: test (macOS ROOT-GNN CPU)
39
+ runs-on: macos-14
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+ - uses: astral-sh/setup-uv@v6
43
+ with:
44
+ version: "0.8.x"
45
+ enable-cache: true
46
+ - run: uv sync --dev --extra root-gnn
47
+ - run: uv run pytest tests/unit tests/integration
48
+
49
+ package:
50
+ name: package
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: astral-sh/setup-uv@v6
55
+ with:
56
+ version: "0.8.x"
57
+ enable-cache: true
58
+ - run: uv sync --dev
59
+ - run: uv build
60
+ - run: uv run python -m twine check dist/*
61
+ - name: Wheel install smoke test
62
+ shell: bash
63
+ run: |
64
+ set -euo pipefail
65
+ smoke_dir="$(mktemp -d)"
66
+ trap 'rm -rf "$smoke_dir"' EXIT
67
+ uv venv "$smoke_dir/venv"
68
+ uv pip install --python "$smoke_dir/venv/bin/python" dist/*.whl
69
+ (
70
+ cd "$smoke_dir"
71
+ "$smoke_dir/venv/bin/python" -c 'import gnn4colliders; print(gnn4colliders.__version__)'
72
+ "$smoke_dir/venv/bin/gnn4colliders" --help
73
+ "$smoke_dir/venv/bin/gnn4colliders" train --help
74
+ "$smoke_dir/venv/bin/gnn4colliders" export --help
75
+ )
76
+
77
+ onnx:
78
+ name: onnx
79
+ runs-on: ubuntu-latest
80
+ steps:
81
+ - uses: actions/checkout@v4
82
+ - uses: astral-sh/setup-uv@v6
83
+ with:
84
+ version: "0.8.x"
85
+ enable-cache: true
86
+ - run: uv sync --dev --extra root-gnn --extra onnx
87
+ - run: uv run pytest tests/unit/export -v
.gitignore CHANGED
@@ -1,16 +1,32 @@
1
- # Python tooling
2
  __pycache__/
 
 
 
 
 
 
 
3
  *.py[cod]
4
- *.egg-info/
5
- .venv/
6
  .pytest_cache/
7
  .ruff_cache/
 
 
 
 
 
8
  .coverage
 
 
9
  dist/
10
  build/
 
 
11
 
12
  # Local data and generated outputs
13
- data/
 
 
14
  outputs/
15
- logs/
16
- *.onnx
 
 
 
1
  __pycache__/
2
+ trainings/
3
+ scores/
4
+ slurm/
5
+ .onnx
6
+ .png
7
+
8
+ # Python tooling
9
  *.py[cod]
 
 
10
  .pytest_cache/
11
  .ruff_cache/
12
+ .mypy_cache/
13
+ *.egg-info/
14
+ .venv/
15
+ venv/
16
+ htmlcov/
17
  .coverage
18
+ profiles/
19
+ *.trace.json
20
  dist/
21
  build/
22
+ benchmark-results/
23
+ validation_output/
24
 
25
  # Local data and generated outputs
26
+ data/raw/*
27
+ !data/raw/.gitkeep
28
+ data/processed/
29
  outputs/
30
+ checkpoints/
31
+
32
+ # Keep the legacy implementation immutable by convention; do not add generated files there.
.python-version CHANGED
@@ -1 +1 @@
1
- 3.14
 
1
+ 3.12
AGENTS.md CHANGED
@@ -1,52 +1,477 @@
1
- # GNN4Colliders agent instructions
2
 
3
- GNN4Colliders trains event classifiers (the ROOT-GNN edge network of Ho et
4
- al., JINST 21 (2026) P08006, a transformer and Deep Sets) on reconstructed
5
- collider objects. Keep it small and readable: a physicist should be able to follow a
6
- run from `cli.py` to the model in a few files.
7
 
8
- ## Layout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  ```text
11
- src/gnn4colliders/ flat modules; see docs/design.md for what each holds
12
- configs/ one self-contained YAML file per experiment
13
- scripts/ Slurm wrappers and small helpers (no core logic)
14
- tests/ pytest, with small ROOT fixtures in tests/fixtures
15
- Pretrained_GNN/ the published pretrained checkpoints (Git LFS)
16
  ```
17
 
18
- * `README.md` is the paper's Hugging Face model card. Do not edit it; usage
19
- goes in `docs/usage.md`, design and results in `docs/design.md`.
20
- * The main benchmark is `configs/gamgam.yaml` (ATLAS Open Data GamGam, 5
21
- classes); compare with Table 3 of the paper (`docs/design.md`).
22
 
23
- ## Rules
24
 
25
- * Add a module or an abstraction only when existing code cannot hold it
26
- clearly. No new frameworks (Lightning, Hydra, DGL, PyG) without agreement.
27
- * Configuration is plain dataclasses in `config.py`; every key has a default
28
- or is required, and unknown keys are errors. Site settings (accounts,
29
- paths, `UV_CACHE_DIR`) belong in scripts, not in configs or code.
30
- * Name domain quantities (`labels`, `weight`, `fold`, `event_index`) instead
31
- of relying on tensor column positions, except for the documented seven node
32
- columns.
33
- * Randomness is seeded explicitly (`train.seed`, `data.seed`); never mutate
34
- global random state elsewhere. Do not claim bitwise reproducibility on GPUs.
35
- * The node schema, edge construction, MLP layout and parameter names must
36
- stay compatible with `Pretrained_GNN/`; a test loads both pretrained models.
37
- * Changes to features, splits, loss or metrics change physics results: say so
38
- explicitly and re-check the GamGam benchmark.
39
 
40
- ## Checks
41
 
42
  ```bash
43
- uv sync --dev
44
- uv run pytest # GPU tests run on a CUDA node
45
- uv run ruff check . && uv run ruff format --check .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  ```
47
 
48
- On Perlmutter, run GPU work on compute nodes (`salloc -C gpu -q
49
- shared_interactive ...`) with `UV_CACHE_DIR=$SCRATCH/.cache/uv`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- Report what changed, what was run and what was not, and any change to
52
- results.
 
1
+ # GNN4Colliders Agent Instructions
2
 
3
+ ## Project purpose
 
 
 
4
 
5
+ GNN4Colliders is a collider machine-learning package intended to support multiple model architectures over shared collider-physics data infrastructure.
6
+
7
+ The first model family being rewritten is:
8
+
9
+ ```text
10
+ root_gnn
11
+ ```
12
+
13
+ Future model families may include architectures such as:
14
+
15
+ ```text
16
+ root_transformer
17
+ ```
18
+
19
+ Do not design shared infrastructure around assumptions that only apply to GNNs.
20
+
21
+ ---
22
+
23
+ ## Source of truth
24
+
25
+ Before making non-trivial changes, inspect:
26
+
27
+ ```text
28
+ docs/architecture.md
29
+ docs/migration.md
30
+ ```
31
+
32
+ and the relevant existing source and tests.
33
+
34
+ The `legacy/` tree is a behavioral reference for the rewrite.
35
+
36
+ Unless explicitly instructed otherwise:
37
+
38
+ * do not modify legacy code
39
+ * do not reorganize legacy code
40
+ * do not mechanically copy legacy architecture into the new package
41
+
42
+ When legacy behavior and documentation disagree, identify the discrepancy rather than silently choosing one.
43
+
44
+ ---
45
+
46
+ ## Package architecture
47
+
48
+ Production Python code belongs under:
49
+
50
+ ```text
51
+ src/gnn4colliders/
52
+ ```
53
+
54
+ The intended boundaries are:
55
+
56
+ ```text
57
+ data/
58
+ Data access, datasets, caching, batching, folds, and generic
59
+ representation-independent data infrastructure.
60
+
61
+ features/
62
+ Collider-domain feature transformations, selections, scaling,
63
+ and derived physics quantities.
64
+
65
+ graphs/
66
+ Graph representation, topology, edge construction, and other
67
+ graph-specific transformations.
68
+
69
+ models/
70
+ Architecture-specific neural network implementations.
71
+
72
+ models/root_gnn/
73
+ ROOT-GNN-specific model components.
74
+
75
+ training/
76
+ Training lifecycle, losses, metrics, checkpointing,
77
+ reproducibility, and distributed-training utilities where
78
+ architecture-independent.
79
+
80
+ inference/
81
+ Prediction, evaluation, output writing, and model export.
82
+
83
+ cli/
84
+ Thin command-line entry points only.
85
+ ```
86
+
87
+ Do not place ROOT file-reading logic inside a specific model family unless it is genuinely architecture-specific.
88
+
89
+ Do not place reusable physics-feature logic inside `root_gnn`.
90
+
91
+ Do not place core implementation in notebooks or shell scripts.
92
+
93
+ ---
94
+
95
+ ## Shared infrastructure vs model-specific code
96
+
97
+ Use this decision rule:
98
+
99
+ **Shared collider or experiment behavior → shared package module.**
100
+
101
+ Examples:
102
+
103
+ ```text
104
+ ROOT reading
105
+ event selections
106
+ physics features
107
+ dataset splits
108
+ metrics
109
+ checkpoint orchestration
110
+ ```
111
+
112
+ **Representation-specific behavior → representation module.**
113
+
114
+ Examples:
115
+
116
+ ```text
117
+ graph topology
118
+ edge construction
119
+ sequence/token construction
120
+ ```
121
+
122
+ **Architecture-specific behavior → models/<model_family>/.**
123
+
124
+ Examples:
125
+
126
+ ```text
127
+ message-passing network
128
+ transformer encoder
129
+ architecture-specific layers
130
+ ```
131
+
132
+ The package should allow future architectures to reuse the same data and physics infrastructure where practical.
133
+
134
+ ---
135
+
136
+ ## Configuration
137
+
138
+ Experiment configuration should describe intent rather than expose implementation details.
139
+
140
+ Prefer:
141
+
142
+ ```yaml
143
+ model:
144
+ type: root_gnn
145
+ ```
146
+
147
+ over configuration that directly names Python module paths and class names.
148
+
149
+ Configuration should eventually support composition across concerns such as:
150
 
151
  ```text
152
+ data
153
+ model
154
+ task
155
+ trainer
156
+ environment
157
  ```
158
 
159
+ Do not hardcode site-specific filesystem paths, CUDA settings, Slurm settings, or machine configuration into model or task definitions.
160
+
161
+ Environment-specific configuration belongs in an environment configuration layer.
 
162
 
163
+ ---
164
 
165
+ ## CLI design
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ The intended user-facing interface is a single project CLI with subcommands conceptually similar to:
168
 
169
  ```bash
170
+ gnn4colliders prepare
171
+ gnn4colliders train
172
+ gnn4colliders evaluate
173
+ gnn4colliders predict
174
+ gnn4colliders export
175
+ ```
176
+
177
+ CLI modules should be thin.
178
+
179
+ They may:
180
+
181
+ * parse/compose configuration
182
+ * construct application objects
183
+ * invoke library functions
184
+ * handle user-facing errors
185
+
186
+ They should not contain core training, data-processing, graph-building, or model logic.
187
+
188
+ ---
189
+
190
+ ## Rewrite strategy
191
+
192
+ This repository is being rewritten incrementally.
193
+
194
+ Do not attempt to rewrite the entire legacy repository in one task.
195
+
196
+ For substantial migrations:
197
+
198
+ 1. inspect the relevant legacy implementation
199
+ 2. identify externally observable behavior
200
+ 3. inspect existing characterization/parity tests
201
+ 4. state or infer the intended new interface
202
+ 5. implement the smallest coherent unit
203
+ 6. add or update tests
204
+ 7. run relevant validation
205
+ 8. report intentional differences and unresolved ambiguity
206
+
207
+ Prefer vertical, testable migration steps over large speculative refactors.
208
+
209
+ ---
210
+
211
+ ## Behavioral parity
212
+
213
+ The legacy implementation defines important behavior that may need to remain compatible during migration.
214
+
215
+ Important compatibility areas include, where applicable:
216
+
217
+ * input feature definitions
218
+ * feature ordering
219
+ * tensor shapes
220
+ * tensor dtypes
221
+ * graph topology
222
+ * edge feature definitions
223
+ * label semantics
224
+ * event weights
225
+ * fold semantics
226
+ * loss calculations
227
+ * metric definitions
228
+ * model outputs
229
+ * checkpoint compatibility
230
+ * inference outputs
231
+
232
+ Do not alter behavior merely because the legacy implementation appears unusual.
233
+
234
+ If behavior seems incorrect or ambiguous:
235
+
236
+ 1. document it
237
+ 2. characterize it with a test when possible
238
+ 3. separate compatibility from proposed improvement
239
+
240
+ Improvements can be made deliberately after the behavior is understood.
241
+
242
+ ---
243
+
244
+ ## Testing
245
+
246
+ Tests belong under:
247
+
248
+ ```text
249
+ tests/unit/
250
+ tests/integration/
251
+ tests/parity/
252
  ```
253
 
254
+ ### Unit tests
255
+
256
+ Use unit tests for isolated transformations and components.
257
+
258
+ They should be:
259
+
260
+ * fast
261
+ * deterministic
262
+ * focused
263
+ * independent of large external datasets
264
+
265
+ ### Integration tests
266
+
267
+ Use integration tests for small end-to-end workflows such as:
268
+
269
+ ```text
270
+ input fixture
271
+ → features
272
+ → representation
273
+ → model
274
+ → output
275
+ ```
276
+
277
+ ### Parity tests
278
+
279
+ Use parity tests to compare the rewrite with the legacy implementation.
280
+
281
+ Prefer small deterministic fixtures and reference outputs.
282
+
283
+ Do not weaken or delete parity tests simply to make new code pass.
284
+
285
+ If a parity difference is intentional, document the reason.
286
+
287
+ ---
288
+
289
+ ## Test data
290
+
291
+ Large production datasets do not belong in the repository.
292
+
293
+ Small deterministic fixtures may live under:
294
+
295
+ ```text
296
+ tests/fixtures/
297
+ ```
298
+
299
+ Fixtures should be only large enough to exercise relevant behavior.
300
+
301
+ Where possible, create reference outputs for deterministic legacy behavior before replacing that behavior.
302
+
303
+ ---
304
+
305
+ ## Reproducibility
306
+
307
+ Reproducibility is a project requirement.
308
+
309
+ Randomness should be explicit and controllable.
310
+
311
+ Where relevant, account for:
312
+
313
+ * Python random state
314
+ * NumPy random state
315
+ * PyTorch random state
316
+ * CUDA random state
317
+ * DataLoader workers
318
+ * shuffling
319
+ * sampling
320
+ * data augmentation
321
+ * distributed execution
322
+
323
+ Do not introduce hidden global random-state mutation.
324
+
325
+ Seeds should be passed or configured explicitly.
326
+
327
+ Do not claim bitwise deterministic training unless the execution environment actually guarantees it.
328
+
329
+ ---
330
+
331
+ ## Code quality
332
+
333
+ Domain meaning must be represented by named fields or schemas, not only by
334
+ positional column indices. New APIs must not require callers to know that a
335
+ particular tensor column means fold, weight, or another domain field.
336
+
337
+ Prefer:
338
+
339
+ * small modules with clear responsibilities
340
+ * typed function signatures
341
+ * explicit inputs and outputs
342
+ * dataclasses or typed configuration where appropriate
343
+ * composition over hidden global state
344
+ * readable names over abbreviations
345
+ * dependency injection over implicit filesystem/environment assumptions
346
+
347
+ Avoid:
348
+
349
+ * repository-relative `sys.path` modifications
350
+ * mutable global configuration
351
+ * hidden singleton state
352
+ * wildcard imports
353
+ * giant utility modules
354
+ * giant training scripts
355
+ * model-specific behavior in generic data code
356
+ * duplicated preprocessing logic
357
+ * unnecessary abstraction introduced before a second use case exists
358
+
359
+ Keep public interfaces intentionally small.
360
+
361
+ ---
362
+
363
+ ## Dependencies
364
+
365
+ Do not introduce a dependency merely to simplify a small amount of code.
366
+
367
+ Before adding a major framework or runtime dependency, explain why it is necessary.
368
+
369
+ In particular, do not introduce architectural frameworks such as:
370
+
371
+ ```text
372
+ PyTorch Lightning
373
+ Kedro
374
+ ```
375
+
376
+ unless the task explicitly calls for evaluating or adopting them.
377
+
378
+ Scientific dependencies should be introduced intentionally and with environment compatibility in mind.
379
+
380
+ PyTorch is shared infrastructure for the expected model families and belongs
381
+ in the base package dependencies. Architecture-specific dependencies belong in
382
+ named extras, for example `root-gnn` for DGL. The canonical development
383
+ environment for the active ROOT-GNN rewrite is:
384
+
385
+ ```bash
386
+ uv sync --extra root-gnn
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Documentation
392
+
393
+ Update documentation when a change alters:
394
+
395
+ * architecture
396
+ * configuration
397
+ * public interfaces
398
+ * expected workflow
399
+ * compatibility guarantees
400
+
401
+ Do not document speculative functionality as if it already exists.
402
+
403
+ Use:
404
+
405
+ ```text
406
+ docs/architecture.md
407
+ ```
408
+
409
+ for the target system structure and important architectural decisions.
410
+
411
+ Use:
412
+
413
+ ```text
414
+ docs/migration.md
415
+ ```
416
+
417
+ for rewrite progress and migration sequencing.
418
+
419
+ ---
420
+
421
+ ## Scope discipline
422
+
423
+ Do not make unrelated changes.
424
+
425
+ When implementing a task:
426
+
427
+ * modify only the modules necessary for that task
428
+ * do not opportunistically refactor unrelated code
429
+ * do not rename public concepts without a clear reason
430
+ * do not remove compatibility behavior without explicit instruction
431
+ * do not modify generated artifacts unless required
432
+
433
+ If a larger architectural issue is discovered, report it rather than expanding the task automatically.
434
+
435
+ ---
436
+
437
+ ## Validation before finishing
438
+
439
+ After code changes, run the relevant available checks.
440
+
441
+ As the project matures, the expected baseline should include:
442
+
443
+ ```bash
444
+ pytest
445
+ ruff check .
446
+ ```
447
+
448
+ ROOT-GNN parity validation requires DGL. A package-wide environment where DGL
449
+ is intentionally absent may skip DGL-dependent tests, but that is incomplete
450
+ ROOT-GNN validation. The required parity gate is:
451
+
452
+ ```bash
453
+ GNN4COLLIDERS_REQUIRE_ROOT_GNN=1 pytest
454
+ ```
455
+
456
+ The gate must fail when DGL cannot be imported.
457
+
458
+ Run more focused tests first when appropriate.
459
+
460
+ If the full suite is expensive, run the relevant subset and clearly report what was and was not run.
461
+
462
+ Do not claim validation succeeded unless the commands actually succeeded.
463
+
464
+ ---
465
+
466
+ ## Completion report
467
+
468
+ For non-trivial changes, finish with a concise report containing:
469
+
470
+ * files changed
471
+ * behavior implemented
472
+ * tests/checks run
473
+ * parity status where relevant
474
+ * intentional deviations from legacy behavior
475
+ * unresolved questions or ambiguities
476
 
477
+ Do not hide failed checks or incomplete behavior.
 
CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ### Added
6
+
7
+ - Release validation and CI coverage for the rewritten package.
8
+ - Installed-package Hydra configuration discovery and public import smoke tests.
9
+
10
+ ### Compatibility
11
+
12
+ - The `0.1.0` package version remains a static, single-source version exposed
13
+ as `gnn4colliders.__version__`.
MANIFEST.in ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ include README.md
2
+ include CHANGELOG.md
3
+ recursive-include docs *.md
README.md CHANGED
@@ -6,6 +6,7 @@ tags:
6
  - particle-physics
7
  - event-classification
8
  - pytorch
 
9
  datasets:
10
  - HWresearch/Delphes
11
  ---
 
6
  - particle-physics
7
  - event-classification
8
  - pytorch
9
+ - dgl
10
  datasets:
11
  - HWresearch/Delphes
12
  ---
README_PROJECT.md ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GNN4Colliders
2
+
3
+ GNN4Colliders is a collider-machine-learning toolkit. The repository name
4
+ reflects its first production model family, ROOT-GNN; the Python package is
5
+ `gnn4colliders`, and the configuration identifier is `root_gnn`. Shared ROOT
6
+ ingestion, collider features, metadata, tasks, training, inference, and
7
+ distributed utilities are designed so that a future sequence model can reuse
8
+ them without requiring every event to be a graph.
9
+
10
+ ```text
11
+ ROOT files -> EventSample -> shared collider features
12
+ ├── GraphSample -> ROOT-GNN
13
+ └── future SequenceSample -> ROOT-Transformer
14
+ ```
15
+
16
+ The new implementation lives under [`src/gnn4colliders`](src/gnn4colliders/).
17
+ [`legacy/`](legacy/) is a frozen behavioral reference for parity work and
18
+ historical checkpoint investigation, not a supported runtime backend.
19
+
20
+ ## Installation
21
+
22
+ The supported development environment is Python 3.12 (`>=3.12,<3.13`). Core
23
+ development is supported on macOS and Linux:
24
+
25
+ ```bash
26
+ # macOS (Apple Silicon): CPU ROOT-GNN development and tests
27
+ uv sync --dev --extra root-gnn
28
+
29
+ # Linux x86_64 with an NVIDIA GPU: validated ROOT-GNN development
30
+ uv sync --dev --extra root-gnn
31
+ ```
32
+
33
+ The core package can be installed without DGL when only shared data or task
34
+ code is needed. ROOT-GNN models, graph construction, and ROOT-GNN parity tests
35
+ require the `root-gnn` extra. On Linux x86_64, it uses the validated CUDA 12.1
36
+ wheels configured in `pyproject.toml`; a compatible NVIDIA driver is still
37
+ required. On Apple Silicon macOS, it installs the CPU DGL wheel, supporting
38
+ local graph/cache development. The default ROOT-GNN backend performs training
39
+ with native PyTorch graph tensors, so it runs on Apple MPS, NVIDIA CUDA, and
40
+ CPU; DGL remains a cache and legacy-compatibility adapter. Do not add
41
+ site-specific CUDA, Slurm, or filesystem paths to model or task configuration.
42
+
43
+ Use the MPS profile on an Apple Silicon Mac:
44
+
45
+ ```bash
46
+ uv run gnn4colliders train environment=macos
47
+ ```
48
+
49
+ ## Data samples
50
+
51
+ ROOT inputs are available from the
52
+ [HWresearch/Delphes dataset](https://huggingface.co/datasets/HWresearch/Delphes).
53
+ Download the 64-event smoke-test sample with the Hugging Face CLI:
54
+
55
+ ```bash
56
+ hf download HWresearch/Delphes testing/ttH_NLO_64.root \
57
+ --repo-type dataset --local-dir data/raw
58
+ ```
59
+
60
+ The sample is `data/raw/testing/ttH_NLO_64.root`, has tree name `output`, and
61
+ is suitable for checking the prepare/train workflow. The dataset also provides
62
+ larger process-specific ROOT samples under `samples/`, derived datasets under
63
+ `derived/`, and analysis-specific ntuples under `analyses/`. These data are
64
+ intentionally ignored by Git; inspect a selected ROOT file's tree and branches
65
+ before writing its preparation configuration.
66
+
67
+ ## Quick start
68
+
69
+ Prepare a graph cache from a ROOT tree. The feature specifications below are
70
+ illustrative placeholders; replace them with the branches in the input tree.
71
+ The full preparation interface is documented in
72
+ [`docs/configuration.md`](docs/configuration.md).
73
+
74
+ ```bash
75
+ uv run gnn4colliders prepare \
76
+ data.files=[data/events.root] \
77
+ data.tree_name=Events \
78
+ data.cache.path=cache/events.pt \
79
+ 'data.feature_branches=[["jet_pt"],["jet_eta"],["jet_phi"],CALC_E,[1.0],[0.0],NODE_TYPE]' \
80
+ data.object_types=[vector] \
81
+ data.scales=[1,1,1,1,1,1,1]
82
+ ```
83
+
84
+ Train, evaluate, and predict from that cache:
85
+
86
+ ```bash
87
+ uv run gnn4colliders train \
88
+ data.cache.path=cache/events.pt \
89
+ trainer.max_epochs=1 \
90
+ environment.output_root=outputs/pretraining_multiclass
91
+
92
+ uv run gnn4colliders evaluate \
93
+ data.cache.path=cache/events.pt \
94
+ inference.checkpoint=outputs/pretraining_multiclass/checkpoints/epoch_0000.pt
95
+
96
+ uv run gnn4colliders predict \
97
+ data.cache.path=cache/events.pt \
98
+ inference.checkpoint=outputs/pretraining_multiclass/checkpoints/epoch_0000.pt \
99
+ inference.output=outputs/pretraining_multiclass/predictions.npz
100
+ ```
101
+
102
+ For a dependency-complete, temporary-data version of this flow, run
103
+ `uv run python scripts/dev/smoke_end_to_end.py`.
104
+
105
+ ## Core concepts
106
+
107
+ `EventSample` is the architecture-neutral event boundary. It contains the
108
+ selected `objects`, `label`, `global_features`, and named `EventMetadata`.
109
+ Metadata includes `fold`, `weight`, and stable `sample_id`; callers should not
110
+ interpret public `tracking[:, N]` columns. Legacy tracking mappings exist only
111
+ at compatibility boundaries.
112
+
113
+ The ROOT-GNN adapter converts shared features to a directed, fully connected
114
+ graph with no self-loops: an event with `N` nodes has `N * (N - 1)` edges.
115
+ Node columns are, in order, `pt`, `eta`, `phi`, `energy`, `btag`, `charge`,
116
+ and `node_type`. Edge columns are `deta`, wrapped `dphi`, and `dR`.
117
+ Object collections are concatenated in configured object-type order. The
118
+ compatibility energy is `pt * cosh(eta)` before per-column scaling.
119
+
120
+ `GraphSampleCache` stores processed graph samples and schema metadata. It is a
121
+ Level-2 graph cache, not the universal event cache. Feature, graph, and cache
122
+ schema versions are checked when loading; incompatible versions fail before
123
+ training.
124
+
125
+ ## ROOT-GNN training and transfer
126
+
127
+ `EdgeNetwork` encodes node, edge, and global features, performs iterative
128
+ edge/node/global message passing, decodes a graph representation, and applies
129
+ the classifier. Its output is raw logits; sigmoid or softmax is task-owned.
130
+
131
+ Multiclass pretraining uses the semantic `model=root_gnn/edge_network` and
132
+ `task=pretraining_multiclass` groups:
133
+
134
+ ```bash
135
+ uv run gnn4colliders train \
136
+ data.cache.path=cache/events.pt \
137
+ model=root_gnn/edge_network task=pretraining_multiclass \
138
+ trainer.max_epochs=20 data.batch_size=64 \
139
+ environment.output_root=outputs/pretraining_multiclass
140
+ ```
141
+
142
+ Fine-tuning is a separate workflow. It loads a pretrained backbone, replaces
143
+ the classifier, and creates a new task/head optimizer:
144
+
145
+ ```bash
146
+ uv run gnn4colliders train \
147
+ data.cache.path=cache/target.pt \
148
+ model=root_gnn/fine_tuned_edge_network \
149
+ task=binary_classification \
150
+ checkpoint.pretrained=/path/to/pretrained.pt \
151
+ model.freeze_backbone=true \
152
+ trainer.max_epochs=10
153
+ ```
154
+
155
+ Set `model.freeze_backbone=false` to train the reused backbone as well.
156
+ Transfer learning is not resume training:
157
+
158
+ | Workflow | Meaning | Restored state |
159
+ | --- | --- | --- |
160
+ | Resume | Continue the same task/run | model, optimizer, scheduler, trainer, early stopping, and RNG state when present |
161
+ | Transfer | Start a new task from a pretrained backbone | model weights only; new classifier and optimizer |
162
+
163
+ Resume example:
164
+
165
+ ```bash
166
+ uv run gnn4colliders train \
167
+ data.cache.path=cache/events.pt \
168
+ checkpoint.resume=outputs/pretraining_multiclass/checkpoints/epoch_0000.pt \
169
+ trainer.max_epochs=20
170
+ ```
171
+
172
+ Validation is evaluated each epoch and drives scheduling/early stopping;
173
+ `test` remains held out. Evaluation computes task metrics over the complete
174
+ selected split, including weighted ROC AUC where defined:
175
+
176
+ ```bash
177
+ uv run gnn4colliders evaluate \
178
+ data.cache.path=cache/events.pt \
179
+ inference.split=test \
180
+ inference.checkpoint=/path/to/checkpoint.pt
181
+ ```
182
+
183
+ Prediction writes a named compressed NPZ. Labeled data includes `labels`;
184
+ `fold` and `weight` are included when available. Every result includes
185
+ `sample_id`, `logits`, `scores`, and `predictions`:
186
+
187
+ ```bash
188
+ uv run gnn4colliders predict \
189
+ data.cache.path=cache/events.pt \
190
+ inference.checkpoint=/path/to/checkpoint.pt \
191
+ inference.output=outputs/predictions.npz
192
+ ```
193
+
194
+ Optional Python-level ROOT writing is provided by
195
+ `gnn4colliders.inference.write_root_scores`. It clones the selected tree,
196
+ adds `score` (or `score_class_N`), and writes `selection_pass`; IDs ending in
197
+ `:<entry>` preserve alignment and unselected entries receive NaN scores. The
198
+ CLI currently exposes NPZ output only.
199
+
200
+ The supported legacy checkpoint, metadata, and output boundary is documented
201
+ in [`docs/compatibility.md`](docs/compatibility.md). New code should use named
202
+ metadata fields; positional tracking is accepted only by the explicit
203
+ compatibility adapter.
204
+
205
+ ### ONNX export
206
+
207
+ Install the optional export dependencies and export a prepared graph-cache
208
+ checkpoint with numerical ONNX validation:
209
+
210
+ ```bash
211
+ uv sync --extra root-gnn --extra onnx
212
+ uv run gnn4colliders export \
213
+ export.checkpoint=/path/to/checkpoint.pt \
214
+ export.output=model.onnx \
215
+ data.cache.path=/path/to/graph-cache.pt
216
+ ```
217
+
218
+ The model accepts processed graph tensors and returns raw logits. See
219
+ [`docs/export.md`](docs/export.md) for the tensor contract and limitations.
220
+
221
+ ## Configuration and environments
222
+
223
+ Hydra groups are `data`, `model`, `task`, `trainer`, `checkpoint`,
224
+ `inference`, `environment`, and `distributed`. Use configuration for a new
225
+ experiment and Python for new behavior. Examples:
226
+
227
+ ```bash
228
+ uv run gnn4colliders train trainer.max_epochs=50 data.batch_size=64
229
+ uv run gnn4colliders train environment=perlmutter environment.device=cuda
230
+ uv run gnn4colliders train distributed=ddp environment=perlmutter
231
+ ```
232
+
233
+ Each run writes a resolved configuration to
234
+ `<environment.output_root>/resolved_config.yaml`. See
235
+ [`docs/configuration.md`](docs/configuration.md) for the group reference and
236
+ [`docs/perlmutter.md`](docs/perlmutter.md) for launch examples.
237
+
238
+ ## Distributed execution and reproducibility
239
+
240
+ Launch DDP with `torchrun` or the provided Slurm wrappers. `data.batch_size`
241
+ and `data.num_workers` are per process, so the ordinary effective batch size
242
+ is `batch_size * world_size`. Training shards may be padded for equal steps;
243
+ validation and prediction are unpadded. Rank 0 writes shared checkpoints,
244
+ configs, and predictions, and metrics/results are gathered across ranks.
245
+
246
+ The configured seed controls initialization and deterministic local loader
247
+ ordering; distributed process seeds are rank-offset and samplers use
248
+ `set_epoch`. CPU runs are reproducible for fixed inputs and environment. GPU
249
+ kernels, DGL, and distributed scheduling can remain nondeterministic, so the
250
+ project does not promise bitwise GPU reproducibility.
251
+
252
+ ## Development and validation
253
+
254
+ ```bash
255
+ uv run pytest
256
+ uv run pytest tests/unit
257
+ GNN4COLLIDERS_REQUIRE_ROOT_GNN=1 uv run pytest tests/parity -v
258
+ uv run ruff check .
259
+ uv run ruff format --check .
260
+ uv run python benchmarks/benchmark_preprocessing.py
261
+ uv run python benchmarks/benchmark_training.py --device cpu
262
+ ```
263
+
264
+ Unit tests cover isolated components, integration tests cover small workflows,
265
+ and parity tests compare deterministic behavior with the frozen legacy
266
+ reference. Performance guidance and measured caveats are in
267
+ [`docs/performance.md`](docs/performance.md) and
268
+ [`benchmarks/README.md`](benchmarks/README.md).
269
+ See [`docs/testing.md`](docs/testing.md) for test layers, optional dependency
270
+ markers, and package smoke validation.
271
+
272
+ ## Architecture and migration status
273
+
274
+ See [`docs/architecture.md`](docs/architecture.md) for responsibility
275
+ boundaries and the future sequence-model extension point. See
276
+ [`docs/migration.md`](docs/migration.md) for the migration matrix,
277
+ intentional redesigns, compatibility limits, and deferred work.
278
+
279
+ ROOT-GNN v1 covers ROOT preparation, validated feature/graph/model/task
280
+ behavior, training, fine-tuning, checkpoint resume, evaluation, prediction,
281
+ single-process/DDP execution, and validated ONNX export. Streaming distributed
282
+ output, legacy cleanup, and ROOT-Transformer remain follow-up work.
benchmarks/README.md ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Performance benchmarks
2
+
3
+ These scripts use fixed synthetic inputs, explicit seeds, warmup iterations,
4
+ and JSON-lines output. GPU timings synchronize after every measured operation;
5
+ warmup and one-time setup costs are excluded from steady-state numbers.
6
+
7
+ ```bash
8
+ uv run python benchmarks/benchmark_preprocessing.py
9
+ uv run python benchmarks/benchmark_dataloader.py
10
+ uv run python benchmarks/benchmark_training.py --device cpu
11
+ uv run python benchmarks/benchmark_inference.py --device cpu
12
+ uv run python benchmarks/benchmark_training.py --device cuda --profile
13
+ ```
14
+
15
+ DGL-dependent scripts report a structured `skipped` result when the optional
16
+ `root-gnn` extra is absent. Profiler traces go under `profiles/` and are not
17
+ committed.
18
+
19
+ ## Baseline measurements
20
+
21
+ The portable baseline in this checkout uses Python 3.12.13, PyTorch 2.2.2+cu121,
22
+ DGL 2.4.0+cu121, CPU, `nodes=32`, `iterations=10`, and `warmup=3`. Exact timings are machine
23
+ dependent; the JSON output from a local run is authoritative.
24
+
25
+ | Measurement | Mean | Median |
26
+ | --- | ---: | ---: |
27
+ | feature construction | 0.47 ms/event | 0.46 ms/event |
28
+ | edge features | 0.11 ms/graph | 0.11 ms/graph |
29
+ | graph-sample loader | 1.65 ms/iteration | 1.65 ms/iteration |
30
+ | ROOT-GNN training step | 21.19 ms/step | 5.32 ms/step |
31
+ | ROOT-GNN inference | 1.48 ms/graph | 1.49 ms/graph |
32
+
33
+ On the available NVIDIA A100-PCIE-40GB with CUDA 12.1 runtime, the same small
34
+ synthetic ROOT-GNN benchmark measured 6.41 ms/step (17.3 MiB peak allocated)
35
+ and 2.00 ms/graph. These are microbenchmarks, not production-workload claims.
36
+
37
+ DGL is available in this environment. The training mean is skewed by one CPU
38
+ warmup-adjacent outlier; median is the more useful steady-state indicator. DDP
39
+ measurements require a multi-process run and are not inferred from CPU numbers.
40
+
41
+ The topology cache is bounded to 32 node-count/device/policy entries and only
42
+ stores reusable index tensors, never event-specific edge features.
43
+
44
+ On the same CPU, a direct topology microbenchmark for 64 nodes measured
45
+ 201.6 microseconds per cold construction versus 10.5 microseconds for a warm
46
+ cache lookup (about 19x for this isolated operation). This is a targeted
47
+ index-construction result, not an end-to-end training speedup; graph feature
48
+ construction and DGL message passing remain separate costs.
benchmarks/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Standalone, reproducible performance measurements for GNN4Colliders."""
benchmarks/_common.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small helpers shared by benchmark entry points."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import platform
8
+ import statistics
9
+ import time
10
+ from collections.abc import Callable
11
+ from typing import Any
12
+
13
+ import torch
14
+
15
+
16
+ def common_parser(description: str) -> argparse.ArgumentParser:
17
+ parser = argparse.ArgumentParser(description=description)
18
+ parser.add_argument("--iterations", type=int, default=20)
19
+ parser.add_argument("--warmup", type=int, default=5)
20
+ parser.add_argument("--device", default="cpu")
21
+ parser.add_argument("--seed", type=int, default=1234)
22
+ return parser
23
+
24
+
25
+ def synchronize(device: torch.device) -> None:
26
+ if device.type == "cuda":
27
+ torch.cuda.synchronize(device)
28
+
29
+
30
+ def measure(
31
+ operation: Callable[[], Any], *, iterations: int, warmup: int, device: torch.device
32
+ ) -> dict[str, float]:
33
+ for _ in range(warmup):
34
+ operation()
35
+ synchronize(device)
36
+ durations = []
37
+ for _ in range(iterations):
38
+ start = time.perf_counter()
39
+ operation()
40
+ synchronize(device)
41
+ durations.append(time.perf_counter() - start)
42
+ return {
43
+ "mean_ms": statistics.mean(durations) * 1000,
44
+ "median_ms": statistics.median(durations) * 1000,
45
+ "min_ms": min(durations) * 1000,
46
+ "max_ms": max(durations) * 1000,
47
+ "stdev_ms": statistics.stdev(durations) * 1000 if len(durations) > 1 else 0.0,
48
+ }
49
+
50
+
51
+ def metadata(device: torch.device) -> dict[str, Any]:
52
+ result: dict[str, Any] = {
53
+ "python": platform.python_version(),
54
+ "torch": torch.__version__,
55
+ "device": str(device),
56
+ "cuda_available": torch.cuda.is_available(),
57
+ }
58
+ if device.type == "cuda" and torch.cuda.is_available():
59
+ result["gpu"] = torch.cuda.get_device_name(device)
60
+ try:
61
+ import dgl
62
+
63
+ result["dgl"] = dgl.__version__
64
+ except ImportError:
65
+ result["dgl"] = None
66
+ return result
67
+
68
+
69
+ def report(name: str, values: dict[str, Any]) -> None:
70
+ print(json.dumps({"benchmark": name, **values}, sort_keys=True))
benchmarks/benchmark_dataloader.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure deterministic loader/batch construction when ROOT-GNN is installed."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+ try:
8
+ from ._common import common_parser, measure, metadata, report
9
+ except ImportError:
10
+ from _common import common_parser, measure, metadata, report
11
+
12
+
13
+ def main() -> None:
14
+ parser = common_parser(__doc__)
15
+ parser.add_argument("--batch-size", type=int, default=8)
16
+ parser.add_argument("--samples", type=int, default=64)
17
+ args = parser.parse_args()
18
+ device = torch.device(args.device)
19
+ try:
20
+ import dgl
21
+
22
+ from gnn4colliders.data import (
23
+ EventMetadata,
24
+ GraphDataLoader,
25
+ GraphDataset,
26
+ GraphSample,
27
+ )
28
+ except ImportError:
29
+ report(
30
+ "dataloader",
31
+ {**metadata(device), "status": "skipped: install root-gnn extra"},
32
+ )
33
+ return
34
+ samples = []
35
+ for index in range(args.samples):
36
+ graph = dgl.graph((torch.tensor([0, 1]), torch.tensor([1, 0])), num_nodes=2)
37
+ graph.ndata["features"] = torch.ones(2, 7)
38
+ graph.edata["features"] = torch.ones(2, 3)
39
+ samples.append(
40
+ GraphSample(
41
+ graph, torch.tensor(index % 2), None, EventMetadata(0, 1.0, str(index))
42
+ )
43
+ )
44
+ loader = GraphDataLoader(GraphDataset(samples), args.batch_size)
45
+ result = measure(
46
+ lambda: list(loader),
47
+ iterations=args.iterations,
48
+ warmup=args.warmup,
49
+ device=device,
50
+ )
51
+ report(
52
+ "dataloader",
53
+ {
54
+ **metadata(device),
55
+ **result,
56
+ "batch_size": args.batch_size,
57
+ "batches_per_second": 1000 / result["mean_ms"],
58
+ },
59
+ )
60
+
61
+
62
+ if __name__ == "__main__":
63
+ main()
benchmarks/benchmark_inference.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure ROOT-GNN inference with fixed synthetic graph input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+ try:
8
+ from ._common import common_parser, measure, metadata, report
9
+ except ImportError:
10
+ from _common import common_parser, measure, metadata, report
11
+
12
+
13
+ def main() -> None:
14
+ parser = common_parser(__doc__)
15
+ args = parser.parse_args()
16
+ device = torch.device(args.device)
17
+ try:
18
+ import dgl
19
+
20
+ from gnn4colliders.models.root_gnn import EdgeNetwork
21
+ except ImportError:
22
+ report(
23
+ "inference",
24
+ {**metadata(device), "status": "skipped: install root-gnn extra"},
25
+ )
26
+ return
27
+ graph = dgl.graph((torch.tensor([0, 1]), torch.tensor([1, 0])), num_nodes=2).to(
28
+ device
29
+ )
30
+ graph.ndata["features"] = torch.ones(2, 7, device=device)
31
+ graph.edata["features"] = torch.ones(2, 3, device=device)
32
+ model = (
33
+ EdgeNetwork(graph, None, hid_size=16, out_size=2, n_layers=1, n_proc_steps=1)
34
+ .to(device)
35
+ .eval()
36
+ )
37
+
38
+ def infer() -> None:
39
+ with torch.inference_mode():
40
+ model(graph)
41
+
42
+ result = measure(
43
+ infer, iterations=args.iterations, warmup=args.warmup, device=device
44
+ )
45
+ report(
46
+ "inference",
47
+ {**metadata(device), **result, "graphs_per_second": 1000 / result["mean_ms"]},
48
+ )
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
benchmarks/benchmark_preprocessing.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure shared feature and graph preprocessing on fixed synthetic events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+
7
+ from gnn4colliders.features import build_node_features
8
+ from gnn4colliders.graphs import build_edge_features, fully_connected_edges
9
+
10
+ try:
11
+ from ._common import common_parser, measure, metadata, report
12
+ except ImportError:
13
+ from _common import common_parser, measure, metadata, report
14
+
15
+
16
+ def main() -> None:
17
+ parser = common_parser(__doc__)
18
+ parser.add_argument("--nodes", type=int, default=32)
19
+ args = parser.parse_args()
20
+ torch.manual_seed(args.seed)
21
+ event = {
22
+ "pt": torch.arange(args.nodes, dtype=torch.float32) + 1,
23
+ "eta": torch.linspace(-2, 2, args.nodes),
24
+ "phi": torch.linspace(-3.0, 3.0, args.nodes),
25
+ }
26
+ branches = [["pt"], ["eta"], ["phi"], "CALC_E", [1.0], [0.0], "NODE_TYPE"]
27
+ object_types = ["vector"]
28
+ scales = [1.0] * 7
29
+ device = torch.device(args.device)
30
+
31
+ feature_result = measure(
32
+ lambda: build_node_features(event, branches, object_types, scales),
33
+ iterations=args.iterations,
34
+ warmup=args.warmup,
35
+ device=device,
36
+ )
37
+ nodes = build_node_features(event, branches, object_types, scales)[0]
38
+ src, dst = fully_connected_edges(args.nodes)
39
+ graph_result = measure(
40
+ lambda: build_edge_features(nodes, src, dst, eta_index=1, phi_index=2),
41
+ iterations=args.iterations,
42
+ warmup=args.warmup,
43
+ device=device,
44
+ )
45
+ base = {**metadata(device), "nodes": args.nodes, "iterations": args.iterations}
46
+ report(
47
+ "feature_construction",
48
+ {
49
+ **base,
50
+ **feature_result,
51
+ "events_per_second": 1000 / feature_result["mean_ms"],
52
+ },
53
+ )
54
+ report(
55
+ "edge_features",
56
+ {**base, **graph_result, "graphs_per_second": 1000 / graph_result["mean_ms"]},
57
+ )
58
+
59
+
60
+ if __name__ == "__main__":
61
+ main()
benchmarks/benchmark_training.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Measure a short ROOT-GNN training section and optionally emit a profiler trace."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import torch
8
+
9
+ try:
10
+ from ._common import common_parser, measure, metadata, report
11
+ except ImportError:
12
+ from _common import common_parser, measure, metadata, report
13
+
14
+
15
+ def main() -> None:
16
+ parser = common_parser(__doc__)
17
+ parser.add_argument("--batch-size", type=int, default=4)
18
+ parser.add_argument("--profile", action="store_true")
19
+ parser.add_argument("--profile-dir", default="profiles")
20
+ args = parser.parse_args()
21
+ device = torch.device(args.device)
22
+ try:
23
+ import dgl
24
+
25
+ from gnn4colliders.models.root_gnn import EdgeNetwork
26
+ except ImportError:
27
+ report(
28
+ "training",
29
+ {**metadata(device), "status": "skipped: install root-gnn extra"},
30
+ )
31
+ return
32
+ torch.manual_seed(args.seed)
33
+ graph = dgl.graph((torch.tensor([0, 1]), torch.tensor([1, 0])), num_nodes=2).to(
34
+ device
35
+ )
36
+ graph.ndata["features"] = torch.randn(2, 7, device=device)
37
+ graph.edata["features"] = torch.randn(2, 3, device=device)
38
+ model = EdgeNetwork(
39
+ graph, None, hid_size=16, out_size=2, n_layers=1, n_proc_steps=1
40
+ ).to(device)
41
+ optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
42
+
43
+ def step() -> None:
44
+ optimizer.zero_grad(set_to_none=True)
45
+ loss = model(graph).square().mean()
46
+ loss.backward()
47
+ optimizer.step()
48
+
49
+ if args.profile:
50
+ profile_dir = Path(args.profile_dir)
51
+ profile_dir.mkdir(parents=True, exist_ok=True)
52
+ with torch.profiler.profile(record_shapes=True, profile_memory=True) as prof:
53
+ for _ in range(args.warmup + args.iterations):
54
+ step()
55
+ prof.export_chrome_trace(str(profile_dir / "training.trace.json"))
56
+ result = measure(
57
+ step, iterations=args.iterations, warmup=args.warmup, device=device
58
+ )
59
+ report(
60
+ "training_step",
61
+ {
62
+ **metadata(device),
63
+ **result,
64
+ "steps_per_second": 1000 / result["mean_ms"],
65
+ "peak_memory_mb": torch.cuda.max_memory_allocated(device) / 2**20
66
+ if device.type == "cuda"
67
+ else 0.0,
68
+ },
69
+ )
70
+
71
+
72
+ if __name__ == "__main__":
73
+ main()
configs/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/data/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/delphes_contrastive.yaml DELETED
@@ -1,81 +0,0 @@
1
- # Contrastive (SimCLR) pretraining of the object transformer on the 12
2
- # Delphes samples of configs/delphes_jepa.yaml, without their labels (same
3
- # event cache): two random views of every event (rotation, eta flip, energy
4
- # scale, object dropout) are pulled together (src/gnn4colliders/contrastive.py).
5
- # Fine-tune with configs/gamgam_jepa_finetune.yaml
6
- # model.pretrained=outputs/delphes_contrastive/last.pt, or compare encoders
7
- # without fine-tuning: gnn4colliders probe configs/gamgam_jepa_finetune.yaml
8
- # data.train_events=1000 split=validation model.pretrained=...
9
- #
10
- # gnn4colliders train configs/delphes_contrastive.yaml
11
- name: delphes_contrastive
12
-
13
- data:
14
- hf_repo: HWresearch/Delphes
15
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
16
- files:
17
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
18
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
19
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
20
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
21
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
22
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
23
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
24
- - {path: samples/top/multitop/tttt.root, label: 7}
25
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
26
- - {path: samples/top/ttbar/ttbar.root, label: 9}
27
- - {path: samples/top/ttv/ttW.root, label: 10}
28
- - {path: samples/top/multitop/ttt.root, label: 11}
29
- tree_name: output
30
- feature_branches:
31
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
32
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
33
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
34
- - CALC_E
35
- - [jet_btag, 0, 0, 0, 0]
36
- - [0, ele_charge, mu_charge, 0, 0]
37
- - NODE_TYPE
38
- object_types: [vector, vector, vector, vector, single]
39
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
40
- # Number is the event index; Number % 10 is the fold. Pretraining trained
41
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
42
- fold_var: Number
43
- fold_modulus: 10
44
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
45
- validation_folds: [0]
46
- test_folds: [1]
47
- # JEPA ignores labels and weights; the cache is that of
48
- # configs/delphes_pretraining.yaml.
49
- weight_var: weight
50
- cache: outputs/delphes_pretraining/events.pt
51
- batch_size: 1024
52
- prepare_workers: 8
53
- loader_workers: 4
54
- pin_memory: true
55
-
56
- model:
57
- name: contrastive
58
- backbone: transformer
59
- # The encoder; configs/gamgam_jepa_finetune.yaml must match it.
60
- hid_size: 128
61
- n_layers: 4
62
- n_heads: 8
63
- ffn_size: 512
64
- dropout: 0.0
65
- pair_bias: true
66
- temperature: 0.1
67
- scale_jitter: 0.1
68
- object_dropout: 0.1
69
-
70
- task:
71
- name: contrastive
72
-
73
- train:
74
- max_epochs: 30
75
- optimizer: adamw
76
- learning_rate: 5.0e-4
77
- weight_decay: 0.05
78
- lr_decay: 0.95
79
- early_stopping: null
80
- # Fine-tune from last.pt.
81
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_jepa.yaml DELETED
@@ -1,77 +0,0 @@
1
- # JEPA pretraining of the object transformer on the 12 Delphes samples of
2
- # the supervised pretraining (configs/delphes_pretraining.yaml), without
3
- # their labels. Fine-tune with configs/gamgam_jepa_finetune.yaml.
4
- #
5
- # gnn4colliders prepare configs/delphes_jepa.yaml
6
- # gnn4colliders train configs/delphes_jepa.yaml
7
- name: delphes_jepa
8
-
9
- data:
10
- hf_repo: HWresearch/Delphes
11
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
12
- files:
13
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
14
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
15
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
16
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
17
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
18
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
19
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
20
- - {path: samples/top/multitop/tttt.root, label: 7}
21
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
22
- - {path: samples/top/ttbar/ttbar.root, label: 9}
23
- - {path: samples/top/ttv/ttW.root, label: 10}
24
- - {path: samples/top/multitop/ttt.root, label: 11}
25
- tree_name: output
26
- feature_branches:
27
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
28
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
29
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
30
- - CALC_E
31
- - [jet_btag, 0, 0, 0, 0]
32
- - [0, ele_charge, mu_charge, 0, 0]
33
- - NODE_TYPE
34
- object_types: [vector, vector, vector, vector, single]
35
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
36
- # Number is the event index; Number % 10 is the fold. Pretraining trained
37
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
38
- fold_var: Number
39
- fold_modulus: 10
40
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
41
- validation_folds: [0]
42
- test_folds: [1]
43
- # JEPA ignores labels and weights; the cache is that of
44
- # configs/delphes_pretraining.yaml.
45
- weight_var: weight
46
- cache: outputs/delphes_pretraining/events.pt
47
- batch_size: 1024
48
- prepare_workers: 8
49
- loader_workers: 4
50
- pin_memory: true
51
-
52
- model:
53
- name: jepa
54
- # The encoder; configs/gamgam_jepa_finetune.yaml must match it.
55
- hid_size: 128
56
- n_layers: 4
57
- n_heads: 8
58
- ffn_size: 512
59
- dropout: 0.0
60
- pair_bias: true
61
- predictor_layers: 2
62
- mask_fraction: 0.4
63
- ema_momentum: 0.996
64
-
65
- task:
66
- name: jepa
67
-
68
- train:
69
- max_epochs: 30
70
- optimizer: adamw
71
- learning_rate: 5.0e-4
72
- weight_decay: 0.05
73
- lr_decay: 0.95
74
- early_stopping: null
75
- # The target drifts with the EMA, so the loss does not compare epochs;
76
- # fine-tune from last.pt.
77
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_jepa_edge.yaml DELETED
@@ -1,82 +0,0 @@
1
- # JEPA pretraining of the edge network, of the size of the supervised
2
- # pretrained models (Pretrained_GNN/), on the same 12 Delphes samples without
3
- # their labels. Fine-tune with configs/gamgam_finetune.yaml and
4
- # model.pretrained=outputs/delphes_jepa_edge/last.pt.
5
- #
6
- # gnn4colliders prepare configs/delphes_jepa_edge.yaml
7
- # gnn4colliders train configs/delphes_jepa_edge.yaml
8
- name: delphes_jepa_edge
9
-
10
- data:
11
- hf_repo: HWresearch/Delphes
12
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
13
- files:
14
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
15
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
16
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
17
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
18
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
19
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
20
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
21
- - {path: samples/top/multitop/tttt.root, label: 7}
22
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
23
- - {path: samples/top/ttbar/ttbar.root, label: 9}
24
- - {path: samples/top/ttv/ttW.root, label: 10}
25
- - {path: samples/top/multitop/ttt.root, label: 11}
26
- tree_name: output
27
- feature_branches:
28
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
29
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
30
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
31
- - CALC_E
32
- - [jet_btag, 0, 0, 0, 0]
33
- - [0, ele_charge, mu_charge, 0, 0]
34
- - NODE_TYPE
35
- object_types: [vector, vector, vector, vector, single]
36
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
37
- # Number is the event index; Number % 10 is the fold. Pretraining trained
38
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
39
- fold_var: Number
40
- fold_modulus: 10
41
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
42
- validation_folds: [0]
43
- test_folds: [1]
44
- # JEPA ignores labels and weights; the cache is that of
45
- # configs/delphes_pretraining.yaml.
46
- weight_var: weight
47
- cache: outputs/delphes_pretraining/events.pt
48
- batch_size: 1024
49
- prepare_workers: 8
50
- loader_workers: 4
51
- pin_memory: true
52
-
53
- model:
54
- name: jepa
55
- backbone: edge_network
56
- # The encoder: as in Pretrained_GNN/*/config.yaml.
57
- hid_size: 64
58
- n_layers: 4
59
- n_proc_steps: 4
60
- dropout: 0.0
61
- # The predictor: transformer blocks.
62
- predictor_layers: 2
63
- n_heads: 4
64
- ffn_size: 256
65
- mask_fraction: 0.4
66
- ema_momentum: 0.996
67
- # Without it the encodings collapse (docs/design.md, JEPA pretraining).
68
- variance_weight: 0.1
69
-
70
- task:
71
- name: jepa
72
-
73
- train:
74
- max_epochs: 30
75
- optimizer: adamw
76
- learning_rate: 5.0e-4
77
- weight_decay: 0.05
78
- lr_decay: 0.95
79
- early_stopping: null
80
- # The target drifts with the EMA, so the loss does not compare epochs;
81
- # fine-tune from last.pt.
82
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_masked.yaml DELETED
@@ -1,78 +0,0 @@
1
- # Masked object modelling of the object transformer on the 12 Delphes
2
- # samples of configs/delphes_jepa.yaml, without their labels (same event
3
- # cache): JEPA's masking and predictor, but the predictor predicts the hidden
4
- # objects' kinematics instead of their target encodings (model.jepa_target:
5
- # features; src/gnn4colliders/jepa.py). Fine-tune or probe with
6
- # configs/gamgam_jepa_finetune.yaml model.pretrained=outputs/delphes_masked/last.pt.
7
- #
8
- # gnn4colliders train configs/delphes_masked.yaml
9
- name: delphes_masked
10
-
11
- data:
12
- hf_repo: HWresearch/Delphes
13
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
14
- files:
15
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
16
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
17
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
18
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
19
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
20
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
21
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
22
- - {path: samples/top/multitop/tttt.root, label: 7}
23
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
24
- - {path: samples/top/ttbar/ttbar.root, label: 9}
25
- - {path: samples/top/ttv/ttW.root, label: 10}
26
- - {path: samples/top/multitop/ttt.root, label: 11}
27
- tree_name: output
28
- feature_branches:
29
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
30
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
31
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
32
- - CALC_E
33
- - [jet_btag, 0, 0, 0, 0]
34
- - [0, ele_charge, mu_charge, 0, 0]
35
- - NODE_TYPE
36
- object_types: [vector, vector, vector, vector, single]
37
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
38
- # Number is the event index; Number % 10 is the fold. Pretraining trained
39
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
40
- fold_var: Number
41
- fold_modulus: 10
42
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
43
- validation_folds: [0]
44
- test_folds: [1]
45
- # JEPA ignores labels and weights; the cache is that of
46
- # configs/delphes_pretraining.yaml.
47
- weight_var: weight
48
- cache: outputs/delphes_pretraining/events.pt
49
- batch_size: 1024
50
- prepare_workers: 8
51
- loader_workers: 4
52
- pin_memory: true
53
-
54
- model:
55
- name: jepa
56
- jepa_target: features
57
- # The encoder; configs/gamgam_jepa_finetune.yaml must match it.
58
- hid_size: 128
59
- n_layers: 4
60
- n_heads: 8
61
- ffn_size: 512
62
- dropout: 0.0
63
- pair_bias: true
64
- predictor_layers: 2
65
- mask_fraction: 0.4
66
-
67
- task:
68
- name: jepa
69
-
70
- train:
71
- max_epochs: 30
72
- optimizer: adamw
73
- learning_rate: 5.0e-4
74
- weight_decay: 0.05
75
- lr_decay: 0.95
76
- early_stopping: null
77
- # Fine-tune from last.pt.
78
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_pfn.yaml DELETED
@@ -1,86 +0,0 @@
1
- # Meta-training of a prior-fitted network (src/gnn4colliders/pfn.py) on
2
- # classification tasks drawn from the 12 Delphes samples: it learns to
3
- # classify events given labelled context events, and is then evaluated in
4
- # context on new data (configs/gamgam_pfn.yaml). The encoder has the sizes of
5
- # configs/delphes_jepa.yaml, so model.pretrained=outputs/delphes_jepa/last.pt
6
- # starts it from the JEPA encoder. task.prior_classes=[1,6,7,8,9,10,11]
7
- # leaves out the five processes of the GamGam task (ttH, ggF, VBF, WH, ZH).
8
- #
9
- # gnn4colliders train configs/delphes_pfn.yaml
10
- name: delphes_pfn
11
-
12
- data:
13
- hf_repo: HWresearch/Delphes
14
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
15
- files:
16
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
17
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
18
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
19
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
20
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
21
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
22
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
23
- - {path: samples/top/multitop/tttt.root, label: 7}
24
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
25
- - {path: samples/top/ttbar/ttbar.root, label: 9}
26
- - {path: samples/top/ttv/ttW.root, label: 10}
27
- - {path: samples/top/multitop/ttt.root, label: 11}
28
- tree_name: output
29
- feature_branches:
30
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
31
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
32
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
33
- - CALC_E
34
- - [jet_btag, 0, 0, 0, 0]
35
- - [0, ele_charge, mu_charge, 0, 0]
36
- - NODE_TYPE
37
- object_types: [vector, vector, vector, vector, single]
38
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
39
- # Number is the event index; Number % 10 is the fold. Pretraining trained
40
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
41
- fold_var: Number
42
- fold_modulus: 10
43
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
44
- validation_folds: [0]
45
- test_folds: [1]
46
- # Tasks use the labels, not the weights; the cache is that of
47
- # configs/delphes_pretraining.yaml.
48
- weight_var: weight
49
- cache: outputs/delphes_pretraining/events.pt
50
- # Events per step (tasks of context and query events).
51
- batch_size: 4096
52
- prepare_workers: 8
53
- loader_workers: 4
54
- pin_memory: true
55
-
56
- model:
57
- name: pfn
58
- hid_size: 128
59
- n_layers: 4
60
- n_heads: 8
61
- ffn_size: 512
62
- dropout: 0.0
63
- pair_bias: true
64
- pretrained: null
65
- max_classes: 10
66
- pfn_layers: 6
67
-
68
- task:
69
- name: pfn
70
- # The prior: classes per task, context events (log-uniform) and queries.
71
- prior_classes: null
72
- task_classes: [2, 10]
73
- context_events: [16, 2048]
74
- query_events: 256
75
- energy_jitter: 0.2
76
- steps_per_epoch: 1000
77
- validation_steps: 50
78
-
79
- train:
80
- max_epochs: 50
81
- optimizer: adamw
82
- learning_rate: 1.0e-4
83
- weight_decay: 0.01
84
- lr_decay: 0.95
85
- early_stopping: null
86
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_pretraining.yaml DELETED
@@ -1,61 +0,0 @@
1
- # 12-class pretraining on the Delphes samples of Ho et al. (2026), read from
2
- # the HWresearch/Delphes Hugging Face dataset.
3
- name: delphes_pretraining
4
-
5
- data:
6
- hf_repo: HWresearch/Delphes
7
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
8
- files:
9
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
10
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
11
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
12
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
13
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
14
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
15
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
16
- - {path: samples/top/multitop/tttt.root, label: 7}
17
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
18
- - {path: samples/top/ttbar/ttbar.root, label: 9}
19
- - {path: samples/top/ttv/ttW.root, label: 10}
20
- - {path: samples/top/multitop/ttt.root, label: 11}
21
- tree_name: output
22
- feature_branches:
23
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
24
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
25
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
26
- - CALC_E
27
- - [jet_btag, 0, 0, 0, 0]
28
- - [0, ele_charge, mu_charge, 0, 0]
29
- - NODE_TYPE
30
- object_types: [vector, vector, vector, vector, single]
31
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
32
- # Number is the event index; Number % 10 is the fold. Pretraining trained
33
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
34
- fold_var: Number
35
- fold_modulus: 10
36
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
37
- validation_folds: [0]
38
- test_folds: [1]
39
- weight_var: weight
40
- cache: outputs/delphes_pretraining/events.pt
41
- batch_size: 1024
42
- prepare_workers: 8
43
- loader_workers: 4
44
- pin_memory: true
45
-
46
- model:
47
- name: edge_network
48
- hid_size: 64
49
- out_size: 12
50
- n_layers: 4
51
- n_proc_steps: 4
52
-
53
- task:
54
- name: multiclass
55
-
56
- train:
57
- max_epochs: 100
58
- learning_rate: 1.0e-4
59
- lr_decay: 0.99
60
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
61
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_tabicl.yaml DELETED
@@ -1,89 +0,0 @@
1
- # Meta-training of the TabICL network (src/gnn4colliders/tabular.py; BSD-3,
2
- # so meta-trained weights can be shared freely) on the classification tasks
3
- # of configs/delphes_pfn.yaml, on the fixed-width event rows of
4
- # configs/gamgam_tabicl.yaml. By default TabICL is frozen and its in-context
5
- # blocks learn LoRA updates (model.lora_rank; 0 trains all 28M weights).
6
- # task.prior_classes=[1,6,7,8,9,10,11] leaves out the five processes of the
7
- # GamGam task. Evaluate in context with
8
- # gnn4colliders evaluate configs/gamgam_tabicl.yaml split=validation \
9
- # checkpoint=outputs/delphes_tabicl/best.pt
10
- # Needs `uv sync --extra tabicl`.
11
- #
12
- # gnn4colliders train configs/delphes_tabicl.yaml
13
- name: delphes_tabicl
14
-
15
- data:
16
- hf_repo: HWresearch/Delphes
17
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
18
- files:
19
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
20
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
21
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
22
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
23
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
24
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
25
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
26
- - {path: samples/top/multitop/tttt.root, label: 7}
27
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
28
- - {path: samples/top/ttbar/ttbar.root, label: 9}
29
- - {path: samples/top/ttv/ttW.root, label: 10}
30
- - {path: samples/top/multitop/ttt.root, label: 11}
31
- tree_name: output
32
- feature_branches:
33
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
34
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
35
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
36
- - CALC_E
37
- - [jet_btag, 0, 0, 0, 0]
38
- - [0, ele_charge, mu_charge, 0, 0]
39
- - NODE_TYPE
40
- object_types: [vector, vector, vector, vector, single]
41
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
42
- # Number is the event index; Number % 10 is the fold. Pretraining trained
43
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
44
- fold_var: Number
45
- fold_modulus: 10
46
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
47
- validation_folds: [0]
48
- test_folds: [1]
49
- # Tasks use the labels, not the weights; the cache is that of
50
- # configs/delphes_pretraining.yaml.
51
- weight_var: weight
52
- cache: outputs/delphes_pretraining/events.pt
53
- # Events per step (tasks of context and query events). TabICL (28M
54
- # weights) recomputes activations in the backward pass.
55
- batch_size: 4096
56
- prepare_workers: 8
57
- loader_workers: 4
58
- pin_memory: true
59
-
60
- model:
61
- name: tabicl
62
- tabicl_checkpoint: tabicl-classifier-v2-20260212.ckpt
63
- # Leading jets, electrons, muons, photons and MET in a table row.
64
- slots: [6, 2, 2, 3, 1]
65
- # As in configs/gamgam_tabicl.yaml, which evaluates this model.
66
- relative_phi: false
67
- pair_objects: 0
68
- max_classes: 10
69
- lora_rank: 8
70
-
71
- task:
72
- name: pfn
73
- # The prior: classes per task, context events (log-uniform) and queries.
74
- prior_classes: null
75
- task_classes: [2, 10]
76
- context_events: [16, 2048]
77
- query_events: 256
78
- energy_jitter: 0.2
79
- steps_per_epoch: 500
80
- validation_steps: 50
81
-
82
- train:
83
- max_epochs: 20
84
- optimizer: adamw
85
- learning_rate: 1.0e-4
86
- weight_decay: 0.01
87
- lr_decay: 0.95
88
- early_stopping: null
89
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/delphes_tabpfn.yaml DELETED
@@ -1,89 +0,0 @@
1
- # Meta-training of the TabPFN-3.5 network (src/gnn4colliders/tabular.py) on
2
- # the classification tasks of configs/delphes_pfn.yaml, on the fixed-width
3
- # event rows of configs/gamgam_tabpfn.yaml. By default TabPFN is frozen and
4
- # its in-context blocks learn LoRA updates (model.lora_rank; 0 trains all
5
- # weights). task.prior_classes=[1,6,7,8,9,10,11] leaves out the five
6
- # processes of the GamGam task. Evaluate in context with
7
- # gnn4colliders evaluate configs/gamgam_tabpfn.yaml split=validation \
8
- # checkpoint=outputs/delphes_tabpfn/best.pt
9
- # Needs `uv sync --extra tabpfn` and the TabPFN-3.5 weights (docs/usage.md).
10
- #
11
- # gnn4colliders train configs/delphes_tabpfn.yaml
12
- name: delphes_tabpfn
13
-
14
- data:
15
- hf_repo: HWresearch/Delphes
16
- hf_revision: 76a6c362bfba8e766ba7255a5c08a55257f78d0e
17
- files:
18
- - {path: samples/higgs/top-associated/tth/ttH_NLO_inc.root, label: 0}
19
- - {path: samples/higgs/top-associated/thjb/tHjb_NLO_inc.root, label: 1}
20
- - {path: samples/higgs/ggf/ggF_NLO_inc.root, label: 2}
21
- - {path: samples/higgs/vbf/VBF_NLO_inc.root, label: 3}
22
- - {path: samples/higgs/vh/wh/WH_NLO_inc.root, label: 4}
23
- - {path: samples/higgs/vh/zh/ZH_NLO_inc.root, label: 5}
24
- - {path: samples/photon/ttyy/ttyy.root, label: 6}
25
- - {path: samples/top/multitop/tttt.root, label: 7}
26
- - {path: samples/top/single-top/SingleT_schan.root, label: 8}
27
- - {path: samples/top/ttbar/ttbar.root, label: 9}
28
- - {path: samples/top/ttv/ttW.root, label: 10}
29
- - {path: samples/top/multitop/ttt.root, label: 11}
30
- tree_name: output
31
- feature_branches:
32
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
33
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
34
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
35
- - CALC_E
36
- - [jet_btag, 0, 0, 0, 0]
37
- - [0, ele_charge, mu_charge, 0, 0]
38
- - NODE_TYPE
39
- object_types: [vector, vector, vector, vector, single]
40
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
41
- # Number is the event index; Number % 10 is the fold. Pretraining trained
42
- # on folds 2-9 and tested on 0-1; here fold 0 validates and fold 1 tests.
43
- fold_var: Number
44
- fold_modulus: 10
45
- train_folds: [2, 3, 4, 5, 6, 7, 8, 9]
46
- validation_folds: [0]
47
- test_folds: [1]
48
- # Tasks use the labels, not the weights; the cache is that of
49
- # configs/delphes_pretraining.yaml.
50
- weight_var: weight
51
- cache: outputs/delphes_pretraining/events.pt
52
- # Events per step (tasks of context and query events): one task of the
53
- # largest context. TabPFN recomputes activations in the backward pass.
54
- batch_size: 2304
55
- prepare_workers: 8
56
- loader_workers: 4
57
- pin_memory: true
58
-
59
- model:
60
- name: tabpfn
61
- tabpfn_version: v3.5
62
- tabpfn_checkpoint: null
63
- # Leading jets, electrons, muons, photons and MET in a table row.
64
- slots: [6, 2, 2, 3, 1]
65
- # As in configs/gamgam_tabpfn.yaml, which evaluates this model.
66
- relative_phi: false
67
- pair_objects: 0
68
- max_classes: 10
69
- lora_rank: 8
70
-
71
- task:
72
- name: pfn
73
- # The prior: classes per task, context events (log-uniform) and queries.
74
- prior_classes: null
75
- task_classes: [2, 10]
76
- context_events: [16, 2048]
77
- query_events: 256
78
- energy_jitter: 0.2
79
- steps_per_epoch: 500
80
- validation_steps: 50
81
-
82
- train:
83
- max_epochs: 20
84
- optimizer: adamw
85
- learning_rate: 1.0e-4
86
- weight_decay: 0.01
87
- lr_decay: 0.95
88
- early_stopping: null
89
- best_metric: loss
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/environment/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/gamgam.yaml DELETED
@@ -1,97 +0,0 @@
1
- # Higgs production-mode classification on ATLAS Open Data (13 TeV, 2020
2
- # release) GamGam MC: the 5-class from-scratch baseline of Ho et al.,
3
- # JINST 21 (2026) P08006, Tables 1 and 3. 2,473,335 events.
4
- #
5
- # gnn4colliders prepare configs/gamgam.yaml
6
- # gnn4colliders train configs/gamgam.yaml [train.fast=true] [train.seed=2]
7
- # gnn4colliders evaluate configs/gamgam.yaml
8
- name: gamgam
9
-
10
- data:
11
- hf_repo: ho22joshua/atlas_opendata
12
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
13
- files:
14
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
15
- label: 0
16
- size_bytes: 217819076
17
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
18
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
19
- label: 1
20
- size_bytes: 218418347
21
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
22
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
23
- label: 2
24
- size_bytes: 108616148
25
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
26
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
27
- label: 3
28
- size_bytes: 29757932
29
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
30
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
31
- label: 4
32
- size_bytes: 58701804
33
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
34
- class_names: [ttH, ggF, VBF, WH, ZH]
35
- tree_name: mini
36
- # Electrons and muons share the lep_* collection; b-tagging uses the
37
- # MV2c10 70% efficiency working point of the Open Data documentation.
38
- derived_branches:
39
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
40
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
41
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
42
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
43
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
44
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
45
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
46
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
47
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
48
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
49
- feature_branches:
50
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
51
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
52
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
53
- - CALC_E
54
- - [jet_btag, 0, 0, 0, 0]
55
- - [0, ele_charge, mu_charge, 0, 0]
56
- - NODE_TYPE
57
- object_types: [vector, vector, vector, vector, single]
58
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
59
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
60
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
61
- fold_var: eventNumber
62
- fold_modulus: 10
63
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
64
- validation_folds: [8]
65
- test_folds: [9]
66
- # Unweighted: each event counts once and classes are balanced by the
67
- # per-class mean loss (23% of ttH events have mcWeight = -1).
68
- weight_var: null
69
- cache: outputs/gamgam/events.pt
70
- batch_size: 1024
71
- prepare_workers: 8
72
- loader_workers: 4
73
- pin_memory: true
74
-
75
- # Baseline GNN of Ho et al. (2026), Table 1.
76
- model:
77
- name: edge_network
78
- hid_size: 64
79
- out_size: 5
80
- n_layers: 4
81
- n_proc_steps: 4
82
- dropout: 0.0
83
-
84
- task:
85
- name: multiclass
86
-
87
- # Sections 3.2 and 3.5: Adam at 1e-4 decayed by 0.99 per epoch, stopped
88
- # after 5 epochs without training-loss improvement; the epoch with the best
89
- # validation AUC is kept (best.pt). train.fast=true adds torch.compile, CUDA
90
- # graphs, bfloat16 and TF32 (about 3x faster on an A100; results agree to
91
- # rounding, not bitwise).
92
- train:
93
- max_epochs: 200
94
- learning_rate: 1.0e-4
95
- lr_decay: 0.99
96
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
97
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_deep_sets.yaml DELETED
@@ -1,88 +0,0 @@
1
- # The GamGam 5-class task with Deep Sets (a per-object MLP, summed): the
2
- # baseline without object interactions (no paper reference). Uses the cache
3
- # of configs/gamgam.yaml.
4
- #
5
- # gnn4colliders train configs/gamgam_deep_sets.yaml
6
- name: gamgam_deep_sets
7
-
8
- data:
9
- hf_repo: ho22joshua/atlas_opendata
10
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
11
- files:
12
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
13
- label: 0
14
- size_bytes: 217819076
15
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
16
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
17
- label: 1
18
- size_bytes: 218418347
19
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
20
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
21
- label: 2
22
- size_bytes: 108616148
23
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
24
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
25
- label: 3
26
- size_bytes: 29757932
27
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
28
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
29
- label: 4
30
- size_bytes: 58701804
31
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
32
- class_names: [ttH, ggF, VBF, WH, ZH]
33
- tree_name: mini
34
- # Electrons and muons share the lep_* collection; b-tagging uses the
35
- # MV2c10 70% efficiency working point of the Open Data documentation.
36
- derived_branches:
37
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
38
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
39
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
40
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
41
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
42
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
43
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
44
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
45
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
46
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
47
- feature_branches:
48
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
49
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
50
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
51
- - CALC_E
52
- - [jet_btag, 0, 0, 0, 0]
53
- - [0, ele_charge, mu_charge, 0, 0]
54
- - NODE_TYPE
55
- object_types: [vector, vector, vector, vector, single]
56
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
57
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
58
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
59
- fold_var: eventNumber
60
- fold_modulus: 10
61
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
62
- validation_folds: [8]
63
- test_folds: [9]
64
- # Unweighted: each event counts once and classes are balanced by the
65
- # per-class mean loss (23% of ttH events have mcWeight = -1).
66
- weight_var: null
67
- cache: outputs/gamgam/events.pt
68
- batch_size: 1024
69
- prepare_workers: 8
70
- loader_workers: 4
71
- pin_memory: true
72
-
73
- model:
74
- name: deep_sets
75
- hid_size: 64
76
- out_size: 5
77
- n_layers: 3
78
- dropout: 0.0
79
-
80
- task:
81
- name: multiclass
82
-
83
- train:
84
- max_epochs: 100
85
- learning_rate: 1.0e-4
86
- lr_decay: 0.99
87
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
88
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_finetune.yaml DELETED
@@ -1,97 +0,0 @@
1
- # The GamGam 5-class task fine-tuned from the multiclass pretrained model of
2
- # Ho et al. (2026): its edge network with a new 5-class classifier, all
3
- # weights trained. Uses the cache of configs/gamgam.yaml.
4
- #
5
- # gnn4colliders train configs/gamgam_finetune.yaml
6
- name: gamgam_finetune
7
-
8
- data:
9
- hf_repo: ho22joshua/atlas_opendata
10
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
11
- files:
12
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
13
- label: 0
14
- size_bytes: 217819076
15
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
16
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
17
- label: 1
18
- size_bytes: 218418347
19
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
20
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
21
- label: 2
22
- size_bytes: 108616148
23
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
24
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
25
- label: 3
26
- size_bytes: 29757932
27
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
28
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
29
- label: 4
30
- size_bytes: 58701804
31
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
32
- class_names: [ttH, ggF, VBF, WH, ZH]
33
- tree_name: mini
34
- # Electrons and muons share the lep_* collection; b-tagging uses the
35
- # MV2c10 70% efficiency working point of the Open Data documentation.
36
- derived_branches:
37
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
38
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
39
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
40
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
41
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
42
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
43
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
44
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
45
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
46
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
47
- feature_branches:
48
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
49
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
50
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
51
- - CALC_E
52
- - [jet_btag, 0, 0, 0, 0]
53
- - [0, ele_charge, mu_charge, 0, 0]
54
- - NODE_TYPE
55
- object_types: [vector, vector, vector, vector, single]
56
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
57
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
58
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
59
- fold_var: eventNumber
60
- fold_modulus: 10
61
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
62
- validation_folds: [8]
63
- test_folds: [9]
64
- # Unweighted: each event counts once and classes are balanced by the
65
- # per-class mean loss (23% of ttH events have mcWeight = -1).
66
- weight_var: null
67
- cache: outputs/gamgam/events.pt
68
- batch_size: 1024
69
- prepare_workers: 8
70
- loader_workers: 4
71
- pin_memory: true
72
-
73
- # The pretrained network's size (Pretrained_GNN/*/config.yaml).
74
- model:
75
- name: edge_network
76
- pretrained: Pretrained_GNN/multiclass_pretrained_model_12/model_epoch_71.pt
77
- freeze_backbone: false
78
- hid_size: 64
79
- out_size: 5
80
- n_layers: 4
81
- n_proc_steps: 4
82
- dropout: 0.0
83
-
84
- task:
85
- name: multiclass
86
-
87
- # Sections 3.2 and 3.5: Adam at 1e-4 decayed by 0.99 per epoch, stopped
88
- # after 5 epochs without training-loss improvement; the epoch with the best
89
- # validation AUC is kept (best.pt). train.fast=true adds torch.compile, CUDA
90
- # graphs, bfloat16 and TF32 (about 3x faster on an A100; results agree to
91
- # rounding, not bitwise).
92
- train:
93
- max_epochs: 200
94
- learning_rate: 1.0e-4
95
- lr_decay: 0.99
96
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
97
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_jepa_finetune.yaml DELETED
@@ -1,95 +0,0 @@
1
- # The GamGam 5-class task with the transformer encoder pretrained by JEPA on
2
- # the Delphes samples (configs/delphes_jepa.yaml). model.pretrained=null
3
- # trains the same architecture from scratch. Uses the cache of
4
- # configs/gamgam.yaml.
5
- #
6
- # gnn4colliders train configs/gamgam_jepa_finetune.yaml
7
- name: gamgam_jepa_finetune
8
-
9
- data:
10
- hf_repo: ho22joshua/atlas_opendata
11
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
12
- files:
13
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
14
- label: 0
15
- size_bytes: 217819076
16
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
17
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
18
- label: 1
19
- size_bytes: 218418347
20
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
21
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
22
- label: 2
23
- size_bytes: 108616148
24
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
25
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
26
- label: 3
27
- size_bytes: 29757932
28
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
29
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
30
- label: 4
31
- size_bytes: 58701804
32
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
33
- class_names: [ttH, ggF, VBF, WH, ZH]
34
- tree_name: mini
35
- # Electrons and muons share the lep_* collection; b-tagging uses the
36
- # MV2c10 70% efficiency working point of the Open Data documentation.
37
- derived_branches:
38
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
39
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
40
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
41
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
42
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
43
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
44
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
45
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
46
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
47
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
48
- feature_branches:
49
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
50
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
51
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
52
- - CALC_E
53
- - [jet_btag, 0, 0, 0, 0]
54
- - [0, ele_charge, mu_charge, 0, 0]
55
- - NODE_TYPE
56
- object_types: [vector, vector, vector, vector, single]
57
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
58
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
59
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
60
- fold_var: eventNumber
61
- fold_modulus: 10
62
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
63
- validation_folds: [8]
64
- test_folds: [9]
65
- # Unweighted: each event counts once and classes are balanced by the
66
- # per-class mean loss (23% of ttH events have mcWeight = -1).
67
- weight_var: null
68
- cache: outputs/gamgam/events.pt
69
- batch_size: 1024
70
- prepare_workers: 8
71
- loader_workers: 4
72
- pin_memory: true
73
-
74
- model:
75
- name: transformer
76
- out_size: 5
77
- hid_size: 128
78
- n_layers: 4
79
- n_heads: 8
80
- ffn_size: 512
81
- dropout: 0.0
82
- pair_bias: true
83
- pretrained: outputs/delphes_jepa/last.pt
84
- # true trains only the classifier (linear probe).
85
- freeze_backbone: false
86
-
87
- task:
88
- name: multiclass
89
-
90
- train:
91
- max_epochs: 100
92
- learning_rate: 1.0e-4
93
- lr_decay: 0.99
94
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
95
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_pfn.yaml DELETED
@@ -1,90 +0,0 @@
1
- # The GamGam 5-class task classified in context by the PFN meta-trained on the
2
- # Delphes samples (configs/delphes_pfn.yaml), with no training on GamGam:
3
- # data.train_events labelled training events, an equal number per class, are
4
- # the context. The model section must match the meta-training. Uses the
5
- # cache of configs/gamgam.yaml.
6
- #
7
- # gnn4colliders evaluate configs/gamgam_pfn.yaml split=validation
8
- name: gamgam_pfn
9
- checkpoint: outputs/delphes_pfn/best.pt
10
-
11
- data:
12
- hf_repo: ho22joshua/atlas_opendata
13
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
14
- files:
15
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
16
- label: 0
17
- size_bytes: 217819076
18
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
19
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
20
- label: 1
21
- size_bytes: 218418347
22
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
23
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
24
- label: 2
25
- size_bytes: 108616148
26
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
27
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
28
- label: 3
29
- size_bytes: 29757932
30
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
31
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
32
- label: 4
33
- size_bytes: 58701804
34
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
35
- class_names: [ttH, ggF, VBF, WH, ZH]
36
- tree_name: mini
37
- # Electrons and muons share the lep_* collection; b-tagging uses the
38
- # MV2c10 70% efficiency working point of the Open Data documentation.
39
- derived_branches:
40
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
41
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
42
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
43
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
44
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
45
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
46
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
47
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
48
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
49
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
50
- feature_branches:
51
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
52
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
53
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
54
- - CALC_E
55
- - [jet_btag, 0, 0, 0, 0]
56
- - [0, ele_charge, mu_charge, 0, 0]
57
- - NODE_TYPE
58
- object_types: [vector, vector, vector, vector, single]
59
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
60
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
61
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
62
- fold_var: eventNumber
63
- fold_modulus: 10
64
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
65
- validation_folds: [8]
66
- test_folds: [9]
67
- # Labelled events (equal per class) that form the context.
68
- train_events: 1000
69
- # Unweighted: each event counts once and classes are balanced by the
70
- # per-class mean loss (23% of ttH events have mcWeight = -1).
71
- weight_var: null
72
- cache: outputs/gamgam/events.pt
73
- batch_size: 1024
74
- prepare_workers: 8
75
- loader_workers: 4
76
- pin_memory: true
77
-
78
- model:
79
- name: pfn
80
- out_size: 5
81
- hid_size: 128
82
- n_layers: 4
83
- n_heads: 8
84
- ffn_size: 512
85
- pair_bias: true
86
- max_classes: 10
87
- pfn_layers: 6
88
-
89
- task:
90
- name: multiclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_tabdpt.yaml DELETED
@@ -1,91 +0,0 @@
1
- # The GamGam 5-class task classified in context by TabDPT (Layer 6;
2
- # Apache-2.0 weights from Hugging Face), with no training: data.train_events
3
- # labelled training events, an equal number per class, are the context of
4
- # every prediction (src/gnn4colliders/tabular.py). TabDPT is trained on real
5
- # tables; with model.tabdpt_context_size each event's context is its nearest
6
- # context events (retrieval), which allows contexts beyond what fits at once.
7
- # Needs `uv sync --extra tabdpt`. Uses the cache of configs/gamgam.yaml.
8
- #
9
- # gnn4colliders evaluate configs/gamgam_tabdpt.yaml split=validation
10
- name: gamgam_tabdpt
11
-
12
- data:
13
- hf_repo: ho22joshua/atlas_opendata
14
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
15
- files:
16
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
17
- label: 0
18
- size_bytes: 217819076
19
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
20
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
21
- label: 1
22
- size_bytes: 218418347
23
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
24
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
25
- label: 2
26
- size_bytes: 108616148
27
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
28
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
29
- label: 3
30
- size_bytes: 29757932
31
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
32
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
33
- label: 4
34
- size_bytes: 58701804
35
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
36
- class_names: [ttH, ggF, VBF, WH, ZH]
37
- tree_name: mini
38
- # Electrons and muons share the lep_* collection; b-tagging uses the
39
- # MV2c10 70% efficiency working point of the Open Data documentation.
40
- derived_branches:
41
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
42
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
43
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
44
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
45
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
46
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
47
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
48
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
49
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
50
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
51
- feature_branches:
52
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
53
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
54
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
55
- - CALC_E
56
- - [jet_btag, 0, 0, 0, 0]
57
- - [0, ele_charge, mu_charge, 0, 0]
58
- - NODE_TYPE
59
- object_types: [vector, vector, vector, vector, single]
60
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
61
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
62
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
63
- fold_var: eventNumber
64
- fold_modulus: 10
65
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
66
- validation_folds: [8]
67
- test_folds: [9]
68
- # Labelled events (equal per class) that form the context.
69
- train_events: 1000
70
- # Unweighted: each event counts once and classes are balanced by the
71
- # per-class mean loss (23% of ttH events have mcWeight = -1).
72
- weight_var: null
73
- cache: outputs/gamgam/events.pt
74
- # Events classified per TabDPT call.
75
- batch_size: 8192
76
- prepare_workers: 8
77
- loader_workers: 4
78
- pin_memory: true
79
-
80
- model:
81
- name: tabdpt
82
- out_size: 5
83
- tabdpt_estimators: 8
84
- tabdpt_context_size: null
85
- # Leading jets, electrons, muons, photons and MET in a table row.
86
- slots: [6, 2, 2, 3, 1]
87
- relative_phi: false
88
- pair_objects: 0
89
-
90
- task:
91
- name: multiclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_tabicl.yaml DELETED
@@ -1,95 +0,0 @@
1
- # The GamGam 5-class task classified in context by TabICLv2 (Inria; BSD-3
2
- # weights from Hugging Face), with no training: data.train_events labelled
3
- # training events, an equal number per class, are the context of every
4
- # prediction (src/gnn4colliders/tabular.py). Needs `uv sync --extra tabicl`.
5
- # Uses the cache of configs/gamgam.yaml. The rows are those of
6
- # configs/gamgam_tabpfn.yaml; model.relative_phi=true and
7
- # model.pair_objects=4 add the physics features.
8
- #
9
- # gnn4colliders evaluate configs/gamgam_tabicl.yaml split=validation
10
- #
11
- # With checkpoint=outputs/delphes_tabicl/best.pt, the TabICL network
12
- # meta-trained by configs/delphes_tabicl.yaml (model.lora_rank and
13
- # model.max_classes must match it) classifies with its own forward pass.
14
- name: gamgam_tabicl
15
-
16
- data:
17
- hf_repo: ho22joshua/atlas_opendata
18
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
19
- files:
20
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
21
- label: 0
22
- size_bytes: 217819076
23
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
24
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
25
- label: 1
26
- size_bytes: 218418347
27
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
28
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
29
- label: 2
30
- size_bytes: 108616148
31
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
32
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
33
- label: 3
34
- size_bytes: 29757932
35
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
36
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
37
- label: 4
38
- size_bytes: 58701804
39
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
40
- class_names: [ttH, ggF, VBF, WH, ZH]
41
- tree_name: mini
42
- # Electrons and muons share the lep_* collection; b-tagging uses the
43
- # MV2c10 70% efficiency working point of the Open Data documentation.
44
- derived_branches:
45
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
46
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
47
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
48
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
49
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
50
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
51
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
52
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
53
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
54
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
55
- feature_branches:
56
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
57
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
58
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
59
- - CALC_E
60
- - [jet_btag, 0, 0, 0, 0]
61
- - [0, ele_charge, mu_charge, 0, 0]
62
- - NODE_TYPE
63
- object_types: [vector, vector, vector, vector, single]
64
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
65
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
66
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
67
- fold_var: eventNumber
68
- fold_modulus: 10
69
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
70
- validation_folds: [8]
71
- test_folds: [9]
72
- # Labelled events (equal per class) that form the context.
73
- train_events: 1000
74
- # Unweighted: each event counts once and classes are balanced by the
75
- # per-class mean loss (23% of ttH events have mcWeight = -1).
76
- weight_var: null
77
- cache: outputs/gamgam/events.pt
78
- # Events classified per TabICL call.
79
- batch_size: 8192
80
- prepare_workers: 8
81
- loader_workers: 4
82
- pin_memory: true
83
-
84
- model:
85
- name: tabicl
86
- out_size: 5
87
- tabicl_checkpoint: tabicl-classifier-v2-20260212.ckpt
88
- tabicl_estimators: 8
89
- # Leading jets, electrons, muons, photons and MET in a table row.
90
- slots: [6, 2, 2, 3, 1]
91
- relative_phi: false
92
- pair_objects: 0
93
-
94
- task:
95
- name: multiclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_tabpfn.yaml DELETED
@@ -1,96 +0,0 @@
1
- # The GamGam 5-class task classified in context by TabPFN (Prior Labs), with
2
- # no training: data.train_events labelled training events, an equal number
3
- # per class, are the context of every prediction (src/gnn4colliders/tabular.py).
4
- # Needs `uv sync --extra tabpfn` and, for TabPFN-2.5 and later, a one-time
5
- # login (a token in ~/.cache/tabpfn or TABPFN_TOKEN). Uses the cache of
6
- # configs/gamgam.yaml.
7
- #
8
- # gnn4colliders evaluate configs/gamgam_tabpfn.yaml split=validation
9
- #
10
- # With checkpoint=outputs/delphes_tabpfn/best.pt, the TabPFN network
11
- # meta-trained by configs/delphes_tabpfn.yaml (model.lora_rank and
12
- # model.max_classes must match it) classifies with its own forward pass.
13
- name: gamgam_tabpfn
14
-
15
- data:
16
- hf_repo: ho22joshua/atlas_opendata
17
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
18
- files:
19
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
20
- label: 0
21
- size_bytes: 217819076
22
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
23
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
24
- label: 1
25
- size_bytes: 218418347
26
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
27
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
28
- label: 2
29
- size_bytes: 108616148
30
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
31
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
32
- label: 3
33
- size_bytes: 29757932
34
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
35
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
36
- label: 4
37
- size_bytes: 58701804
38
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
39
- class_names: [ttH, ggF, VBF, WH, ZH]
40
- tree_name: mini
41
- # Electrons and muons share the lep_* collection; b-tagging uses the
42
- # MV2c10 70% efficiency working point of the Open Data documentation.
43
- derived_branches:
44
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
45
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
46
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
47
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
48
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
49
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
50
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
51
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
52
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
53
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
54
- feature_branches:
55
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
56
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
57
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
58
- - CALC_E
59
- - [jet_btag, 0, 0, 0, 0]
60
- - [0, ele_charge, mu_charge, 0, 0]
61
- - NODE_TYPE
62
- object_types: [vector, vector, vector, vector, single]
63
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
64
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
65
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
66
- fold_var: eventNumber
67
- fold_modulus: 10
68
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
69
- validation_folds: [8]
70
- test_folds: [9]
71
- # Labelled events (equal per class) that form the context.
72
- train_events: 1000
73
- # Unweighted: each event counts once and classes are balanced by the
74
- # per-class mean loss (23% of ttH events have mcWeight = -1).
75
- weight_var: null
76
- cache: outputs/gamgam/events.pt
77
- # Events classified per TabPFN call.
78
- batch_size: 8192
79
- prepare_workers: 8
80
- loader_workers: 4
81
- pin_memory: true
82
-
83
- model:
84
- name: tabpfn
85
- out_size: 5
86
- tabpfn_version: v3.5
87
- tabpfn_estimators: 4
88
- # Leading jets, electrons, muons, photons and MET in a table row.
89
- slots: [6, 2, 2, 3, 1]
90
- # Physics features: azimuths from the leading object's, and the mass and
91
- # Delta R of every pair of the leading pair_objects objects.
92
- relative_phi: false
93
- pair_objects: 0
94
-
95
- task:
96
- name: multiclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/gamgam_transformer.yaml DELETED
@@ -1,92 +0,0 @@
1
- # The GamGam 5-class task with a transformer over the same objects (no
2
- # paper reference). Uses the cache of configs/gamgam.yaml.
3
- #
4
- # gnn4colliders train configs/gamgam_transformer.yaml
5
- name: gamgam_transformer
6
-
7
- data:
8
- hf_repo: ho22joshua/atlas_opendata
9
- hf_revision: d1bcc9ccaa304ea09e11f7d724342568f72a0d6e
10
- files:
11
- - path: GamGam/2020/MC/mc_341081.ttH125_gamgam.GamGam.root
12
- label: 0
13
- size_bytes: 217819076
14
- sha256: c498974926f6c55143d17fc4d3d6fa69bd3718ff92cf7d5cad5c3d67a836c7b7
15
- - path: GamGam/2020/MC/mc_343981.ggH125_gamgam.GamGam.root
16
- label: 1
17
- size_bytes: 218418347
18
- sha256: 05b3e6709e8836a7b862a7fde09086b3a9d4d494d62a6984d5df39d8daad13eb
19
- - path: GamGam/2020/MC/mc_345041.VBFH125_gamgam.GamGam.root
20
- label: 2
21
- size_bytes: 108616148
22
- sha256: 92334fce3bffc9eb6e0745dd6ef354d5ce33da764858511b8f9ca49b862b44e2
23
- - path: GamGam/2020/MC/mc_345318.WpH125J_Wincl_gamgam.GamGam.root
24
- label: 3
25
- size_bytes: 29757932
26
- sha256: f255a9c3c6935f976f788b6f32d9a8f6bf576eb12ecd03a6f3fd0a5dea9d1248
27
- - path: GamGam/2020/MC/mc_345319.ZH125J_Zincl_gamgam.GamGam.root
28
- label: 4
29
- size_bytes: 58701804
30
- sha256: c85623b04ac0176c64acdb2ecf8e992a63824cfc430a421df742319518ff6748
31
- class_names: [ttH, ggF, VBF, WH, ZH]
32
- tree_name: mini
33
- # Electrons and muons share the lep_* collection; b-tagging uses the
34
- # MV2c10 70% efficiency working point of the Open Data documentation.
35
- derived_branches:
36
- ele_pt: {source: lep_pt, where: {lep_type: 11}}
37
- ele_eta: {source: lep_eta, where: {lep_type: 11}}
38
- ele_phi: {source: lep_phi, where: {lep_type: 11}}
39
- ele_charge: {source: lep_charge, where: {lep_type: 11}}
40
- mu_pt: {source: lep_pt, where: {lep_type: 13}}
41
- mu_eta: {source: lep_eta, where: {lep_type: 13}}
42
- mu_phi: {source: lep_phi, where: {lep_type: 13}}
43
- mu_charge: {source: lep_charge, where: {lep_type: 13}}
44
- jet_btag: {source: jet_MV2c10, above: 0.8244273}
45
- # Object types (node types) as in pretraining: jet, electron, muon, photon, MET.
46
- feature_branches:
47
- - [jet_pt, ele_pt, mu_pt, photon_pt, met_et]
48
- - [jet_eta, ele_eta, mu_eta, photon_eta, 0]
49
- - [jet_phi, ele_phi, mu_phi, photon_phi, met_phi]
50
- - CALC_E
51
- - [jet_btag, 0, 0, 0, 0]
52
- - [0, ele_charge, mu_charge, 0, 0]
53
- - NODE_TYPE
54
- object_types: [vector, vector, vector, vector, single]
55
- # Momenta are in MeV; 1e-4 gives the pretraining unit of 10 GeV.
56
- scales: [1.0e-4, 1, 1, 1.0e-4, 1, 1, 1]
57
- # eventNumber % 10 gives the paper's 80:10:10 train:validation:test split.
58
- fold_var: eventNumber
59
- fold_modulus: 10
60
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
61
- validation_folds: [8]
62
- test_folds: [9]
63
- # Unweighted: each event counts once and classes are balanced by the
64
- # per-class mean loss (23% of ttH events have mcWeight = -1).
65
- weight_var: null
66
- cache: outputs/gamgam/events.pt
67
- batch_size: 1024
68
- prepare_workers: 8
69
- loader_workers: 4
70
- pin_memory: true
71
-
72
- model:
73
- name: transformer
74
- hid_size: 64
75
- out_size: 5
76
- n_layers: 2
77
- n_heads: 4
78
- ffn_size: 256
79
- dropout: 0.1
80
- # Attention bias from object-pair (deta, dphi, dR, mass) features, as in the
81
- # Particle Transformer; false gives a plain transformer.
82
- pair_bias: true
83
-
84
- task:
85
- name: multiclass
86
-
87
- train:
88
- max_epochs: 100
89
- learning_rate: 1.0e-4
90
- lr_decay: 0.99
91
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
92
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
configs/model/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/model/root_gnn/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/opendata_pfn.yaml DELETED
The diff for this file is too large to render. See raw diff
 
configs/task/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/trainer/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
configs/tth_cp_even_odd.yaml DELETED
@@ -1,49 +0,0 @@
1
- # CP-even versus CP-odd top-Yukawa ttH (H -> gamma gamma), Delphes samples.
2
- # Point data.files at local copies of the two samples.
3
- name: tth_cp_even_odd
4
-
5
- data:
6
- files:
7
- - {path: data/raw/ttH_NLO_download/samples/higgs/top-associated/tth/ttH_NLO.root, label: 0}
8
- - {path: data/raw/ttH_cp_even_odd/samples/higgs/top-associated/tth/ttH_CPodd.root, label: 1}
9
- tree_name: output
10
- feature_branches:
11
- - [jet_pt, ele_pt, mu_pt, ph_pt, MET_met]
12
- - [jet_eta, ele_eta, mu_eta, ph_eta, 0]
13
- - [jet_phi, ele_phi, mu_phi, ph_phi, MET_phi]
14
- - CALC_E
15
- - [jet_btag, 0, 0, 0, 0]
16
- - [0, ele_charge, mu_charge, 0, 0]
17
- - NODE_TYPE
18
- object_types: [vector, vector, vector, vector, single]
19
- # Delphes momenta are in GeV; 0.1 gives the pretraining unit of 10 GeV.
20
- scales: [0.1, 1, 1, 0.1, 1, 1, 1]
21
- # Number is the event index; Number % 10 is the fold.
22
- fold_var: Number
23
- fold_modulus: 10
24
- train_folds: [0, 1, 2, 3, 4, 5, 6, 7]
25
- validation_folds: [8]
26
- test_folds: [9]
27
- weight_var: weight
28
- cache: outputs/tth_cp_even_odd/events.pt
29
- batch_size: 1024
30
-
31
- model:
32
- name: edge_network
33
- hid_size: 64
34
- out_size: 1
35
- n_layers: 4
36
- n_proc_steps: 4
37
-
38
- # NLO samples have signed weights; |weight| keeps the loss denominator
39
- # stable (metrics use events with positive weight).
40
- task:
41
- name: binary
42
- absolute_weights: true
43
-
44
- train:
45
- max_epochs: 100
46
- learning_rate: 1.0e-4
47
- lr_decay: 0.99
48
- early_stopping: {monitor: train_loss, patience: 5, min_delta: 0.0}
49
- best_metric: auc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
data/fixtures/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
data/raw/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
docs/architecture.md ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GNN4Colliders architecture
2
+
3
+ ## Current v1 architecture
4
+
5
+ The supported rewrite is layered around an architecture-neutral event boundary:
6
+
7
+ ```text
8
+ ROOT/Awkward
9
+ ↓
10
+ EventSample + EventMetadata
11
+ ↓
12
+ shared collider feature construction
13
+ ├── GraphSample -> versioned graph cache -> GraphBatch
14
+ └── future SequenceSample -> ROOT-Transformer (not implemented)
15
+ ↓
16
+ ROOT-GNN EdgeNetwork
17
+ ↓
18
+ raw logits -> Task
19
+ ├── loss
20
+ ├── predictions
21
+ └── full-split metrics
22
+ ↓
23
+ Trainer / Predictor / outputs
24
+ ```
25
+
26
+ | Layer | Responsibility |
27
+ | --- | --- |
28
+ | `data` | ROOT/Awkward ingestion, event samples, metadata, graph caches, folds, and batching |
29
+ | `features` | Shared collider-object features and derived physics quantities |
30
+ | `graphs` | Topology, edge features, and the DGL representation adapter |
31
+ | `models/root_gnn` | ROOT-GNN encoders, portable tensor message passing, classifier, and transfer boundary |
32
+ | `tasks` | Loss, score/prediction, labels, weights, and metrics |
33
+ | `training` | Optimizer lifecycle, validation, early stopping, checkpointing, and reproducibility |
34
+ | `inference` | Ordered prediction/evaluation and NPZ/ROOT output adapters |
35
+ | `distributed` | Rank-local devices, sharding, DDP, and cross-rank collection |
36
+ | `config` / `cli` | Semantic Hydra composition and thin user-facing commands |
37
+
38
+ `EventSample` is shared infrastructure, not a ROOT-GNN object. `GraphSample`
39
+ is the current representation-specific adapter. At batching, the default
40
+ ROOT-GNN backend converts cached DGL graphs into `TensorGraph` values with
41
+ explicit node/edge indices and graph membership. Native message passing then
42
+ uses only PyTorch tensor operations, allowing the same model to train on CPU,
43
+ CUDA, and Apple MPS. This separation is the extension point for a future
44
+ sequence/token representation.
45
+
46
+ The new public metadata contract is named `EventMetadata(fold, weight,
47
+ sample_id, extra)`. The legacy positional tracking tensor is accepted only by
48
+ compatibility-facing ingestion code. A `GraphSampleCache` is deliberately a
49
+ Level-2 graph cache; replacing it with a universal cache would couple future
50
+ model families to DGL.
51
+
52
+ For deployment, a prepared `GraphBatch` can pass through the isolated
53
+ `RootGNNExportAdapter` into an ONNX model. ONNX is still an inference boundary:
54
+ it does not read ROOT or construct collider features.
55
+
56
+ ## Current public workflow
57
+
58
+ `prepare` reads ROOT through `RootEventDataset`, builds shared features and
59
+ DGL graphs, and saves a schema-checked cache. `train` creates a model/task and
60
+ `Trainer`; validation is the model-selection split and test is held out.
61
+ `evaluate` computes metrics after collecting the complete split. `predict`
62
+ returns detached CPU tensors in loader order and writes named NPZ fields.
63
+ `write_root_scores` is an optional Python adapter with explicit entry alignment;
64
+ the CLI currently exposes NPZ output.
65
+
66
+ Compatibility responsibilities are isolated in `gnn4colliders.compat`.
67
+ Supported historical checkpoint prefixes, classifier names, and the two-column
68
+ tracking conversion are listed in [`compatibility.md`](compatibility.md).
69
+ The modern pipeline does not propagate positional tracking or historical NPZ
70
+ fields.
71
+
72
+ Checkpoints are independent of the model implementation: they carry model and
73
+ task metadata, lifecycle state, schema versions, and optional RNG state.
74
+ Prefix normalization supports DDP `module.` and compiled `_orig_mod.` weights,
75
+ plus the active ROOT-GNN historical classifier-name compatibility path.
76
+
77
+ ---
78
+
79
+ ## Historical behavioral reference
80
+
81
+ This document covers the target system in `legacy/root_gnn_dgl/`. The sibling
82
+ `legacy/physicsnemo/` tree is a prior rewrite attempt and is not a behavioral
83
+ target.
84
+
85
+ ## 1. High-level system description
86
+
87
+ The active rewrite exposes `gnn4colliders.models.root_gnn.EdgeNetwork`. Its
88
+ encoders and message-passing blocks form a reusable backbone whose decoded
89
+ graph representation is passed to an explicit classifier. `FineTunedEdgeNetwork`
90
+ reuses that backbone and replaces only the task-specific classifier, with
91
+ explicit frozen or trainable-backbone control.
92
+
93
+ `root_gnn_dgl` is a ROOT-to-DGL graph classification system. YAML selects
94
+ dataset, model, loss, and finish-function classes by import path. The dataset
95
+ reads ROOT trees, converts collider objects to fully connected DGL graphs, and
96
+ saves graph chunks. Training loads those chunks, applies fold selection and
97
+ optional pre-batching/padding, trains a graph network, writes one PyTorch
98
+ checkpoint per epoch, and reports weighted loss, accuracy, and ROC AUC.
99
+
100
+ The primary model is `models.GCN.Edge_Network` ([`GCN.py:182-251`](../legacy/root_gnn_dgl/models/GCN.py)).
101
+ It encodes node, edge, and global features, repeats edge -> node -> global
102
+ message passing `n_proc_steps` times, decodes the global state, and applies
103
+ `classify`. Fine-tuning uses `models.GCN.Transferred_Learning_Finetuning`
104
+ ([`GCN.py:884-997`](../legacy/root_gnn_dgl/models/GCN.py)), which loads a
105
+ pretrained `Edge_Network`, removes its final classifier, and applies a new one.
106
+ The active configs use output size 12 for multiclass pretraining and output
107
+ size 1 for binary tasks ([`configs/stats_100K/pretraining_multiclass.yaml:1-45`](../legacy/root_gnn_dgl/configs/stats_100K/pretraining_multiclass.yaml),
108
+ [`configs/stats_100K/finetuning_ttH_CP_even_vs_odd.yaml:1-45`](../legacy/root_gnn_dgl/configs/stats_100K/finetuning_ttH_CP_even_vs_odd.yaml)).
109
+
110
+ ### Entry points and flows
111
+
112
+ - `scripts/training_script.py:main` and its CLI parser load YAML, create
113
+ loaders, construct the model, and call `train`; `--evaluate` calls
114
+ `evaluate` ([`training_script.py:638-843`](../legacy/root_gnn_dgl/scripts/training_script.py)).
115
+ - `scripts/prep_data.py:main` creates configured graph caches
116
+ ([`prep_data.py:68-110`](../legacy/root_gnn_dgl/scripts/prep_data.py)).
117
+ - `scripts/inference.py:main` reconstructs an unlazy dataset, loads one or
118
+ more checkpoints, and writes `.npz` or ROOT scores
119
+ ([`inference.py:163-387`](../legacy/root_gnn_dgl/scripts/inference.py)).
120
+ - `scripts/export_onnx.py:main` exports an ONNX-friendly model
121
+ ([`export_onnx.py:979-1035`](../legacy/root_gnn_dgl/scripts/export_onnx.py)).
122
+ - `selections.py:main`, `check_dataset_files.py:main`, and
123
+ `plot_config_distributions.py:main` are diagnostic entry points. `run_demo.sh`
124
+ sequences pretraining, binary training, fine-tuning, and inference
125
+ ([`run_demo.sh:3-59`](../legacy/root_gnn_dgl/run_demo.sh)).
126
+
127
+ The training flow is:
128
+
129
+ ```text
130
+ YAML -> load_config/buildFromConfig -> RootDataset/LazyDataset
131
+ -> ROOT/Awkward -> DGL graph + labels/tracking/globals -> .bin cache
132
+ -> fold_selection -> prebatch/padding -> GraphDataLoader
133
+ -> Edge_Network or transfer model -> weighted loss/metrics
134
+ -> model_epoch_N.pt, logs, evaluation/inference output
135
+ ```
136
+
137
+ `training_script.train` is the lifecycle implementation
138
+ ([`training_script.py:143-614`](../legacy/root_gnn_dgl/scripts/training_script.py));
139
+ distributed paths use NCCL/DDP ([`training_script.py:616-839`](../legacy/root_gnn_dgl/scripts/training_script.py)).
140
+ Inference uses `CustomPreBatchedDataset`, applies a configured finish function,
141
+ and collects `scores`, `labels`, and `tracking_info`
142
+ ([`inference.py:20-76`](../legacy/root_gnn_dgl/scripts/inference.py),
143
+ [`inference.py:223-325`](../legacy/root_gnn_dgl/scripts/inference.py)).
144
+
145
+ ## 2. Dependency and data-flow map
146
+
147
+ ```text
148
+ ROOT files (raw_dir/file_names, tree_name)
149
+ -> gnn4colliders.data.RootEventDataset (Uproot/Awkward)
150
+ -> node feature construction (dataset.py:15-50)
151
+ -> full_connected_graph (dataset.py:52-59)
152
+ -> EdgeDataset.make_graph: [deta, dphi, dR] (dataset.py:471-482)
153
+ -> DGL .bin cache / LazyDataset / PreBatchedDataset
154
+ -> GraphDataLoader -> models.GCN -> loss/metrics -> outputs
155
+ ```
156
+
157
+ `RootDataset` provides `process`, `save`, `load`, `__getitem__`, and `__len__`
158
+ ([`dataset.py:160-469`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py));
159
+ `LazyDataset` loads one chunk through a ring buffer
160
+ ([`dataset.py:525-578`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py));
161
+ `PreBatchedDataset.process` selects, shuffles, batches, pads, and caches
162
+ ([`batched_dataset.py:34-146`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)).
163
+
164
+ `load_config` uses PyYAML `FullLoader` and shallow `include` merging, while
165
+ `buildFromConfig` dynamically imports `module`, resolves `class`, merges extra
166
+ keys into `args`, converts list-valued weights to tensors, and injects runtime
167
+ arguments ([`utils.py:10-43`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)).
168
+ This reflection shape is a de facto interface for configured components.
169
+
170
+ ### Data and preprocessing
171
+
172
+ The active node schema is seven columns: `pt`, `eta`, `phi`, `energy`, `btag`,
173
+ `charge`, and `node_type`. `CALC_E` is `pt*cosh(eta)`, constants are broadcast
174
+ per object type, `NODE_TYPE` is an integer type code, and feature scales are
175
+ applied columnwise ([`dataset.py:15-50`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)).
176
+ `full_connected_graph` makes directed all-pairs edges; `EdgeDataset` requests
177
+ no self-loops and stores `[deta, dphi, dR]`
178
+ ([`dataset.py:52-59`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py),
179
+ [`dataset.py:471-482`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)).
180
+
181
+ Selections are strings evaluated with builtins disabled or
182
+ `(variable, cut, operator)` triples (`check_selection`, `selection_mask`;
183
+ [`dataset.py:75-145`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)). Fold
184
+ selection uses `tracking[:,0] % n_folds`; tracking column 0 is fold and column 1
185
+ is weight ([`utils.py:121-143`](../legacy/root_gnn_dgl/root_gnn_base/utils.py),
186
+ [`dataset.py:176-182`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)).
187
+ `hash_partition` and a seeded Torch generator control pre-batch order
188
+ ([`batched_dataset.py:27-99`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)).
189
+ Padding modes are `NONE`, `STEPS`, `FIXED`, and `NODE`; `FIXED` is hardcoded to
190
+ 16,000 nodes and 104,000 edges ([`batched_dataset.py:100-125`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)).
191
+
192
+ ### Model, losses, and metrics
193
+
194
+ `Make_MLP` builds linear/ReLU/dropout blocks followed by LayerNorm
195
+ ([`GCN.py:18-35`](../legacy/root_gnn_dgl/models/GCN.py)). Each `Edge_Network`
196
+ step encodes inputs, copies source/destination states to edges, updates edges,
197
+ sums edge messages into nodes, updates nodes, then mean-pools nodes/edges to
198
+ update globals ([`GCN.py:195-249`](../legacy/root_gnn_dgl/models/GCN.py)). It
199
+ returns logits `[graphs, out_size]` without sigmoid/softmax.
200
+
201
+ The default objective is elementwise `BCEWithLogitsLoss`, multiplied by
202
+ `tracking[:,1]`, averaged separately per unique label, then averaged across
203
+ labels ([`training_script.py:143-185`](../legacy/root_gnn_dgl/scripts/training_script.py),
204
+ [`training_script.py:320-359`](../legacy/root_gnn_dgl/scripts/training_script.py)).
205
+ `--abs` makes weights positive. Binary metrics use sigmoid threshold 0.5 and
206
+ weighted ROC AUC; multiclass metrics use argmax and one-vs-rest ROC AUC
207
+ ([`training_script.py:438-510`](../legacy/root_gnn_dgl/scripts/training_script.py)).
208
+ Additional configurable losses/finishers live in `models/loss.py`
209
+ ([`loss.py:6-310`](../legacy/root_gnn_dgl/models/loss.py)).
210
+
211
+ ### Checkpoints and outputs
212
+
213
+ Training writes `Training_Directory/model_epoch_<epoch>.pt` containing `epoch`,
214
+ `model_state_dict`, `optimizer_state_dict`, and serialized `early_stop`
215
+ ([`training_script.py:565-604`](../legacy/root_gnn_dgl/scripts/training_script.py)).
216
+ Keys strip `module.` and compiled models save the underlying `_orig_mod` state;
217
+ `get_last_epoch`, `get_specific_epoch`, and `get_best_epoch` load the files
218
+ ([`utils.py:145-248`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)).
219
+ `evaluate` writes `evaluation_<epoch>.npz`; inference writes `.npz` fields
220
+ `scores`, `labels`, `tracking_info`, or adds score branches and `selection_pass`
221
+ to a cloned ROOT tree ([`training_script.py:57-140`](../legacy/root_gnn_dgl/scripts/training_script.py),
222
+ [`inference.py:328-385`](../legacy/root_gnn_dgl/scripts/inference.py)).
223
+
224
+ ### Training lifecycle boundary
225
+
226
+ The active rewrite keeps lifecycle orchestration architecture-independent:
227
+
228
+ ```text
229
+ GraphDataLoader -> GraphBatch -> Model -> Task -> Trainer
230
+ loss/metrics
231
+ ```
232
+
233
+ `gnn4colliders.training.Trainer` owns device placement, train/evaluation mode,
234
+ gradient and optimizer steps, epoch aggregation, optional scheduler stepping,
235
+ early stopping, and in-memory history. Tasks own loss and metric semantics;
236
+ the trainer does not inspect positional tracking columns or collider-specific
237
+ features. Evaluation concatenates detached outputs across the complete split
238
+ before calling task metrics, so ROC AUC is not computed per mini-batch.
239
+
240
+ Checkpoint persistence and the Python inference/output layer are implemented
241
+ as separate adapters. The semantic CLI and distributed application boundary
242
+ are implemented in the current stack. `gnn4colliders.inference.Predictor` accumulates detached CPU
243
+ logits, task-defined scores/predictions, labels, and named event metadata in
244
+ loader order; `write_npz` is the primary named-field format and ROOT score
245
+ writing is an optional alignment-aware adapter.
246
+
247
+ The new lifecycle uses conventional split semantics. `train` updates model
248
+ parameters, `validation` is evaluated after every epoch and drives scheduler,
249
+ early-stopping, and later model selection, and `test` is held out. The trainer
250
+ does not accept a test loader in `fit`; callers evaluate the held-out test set
251
+ separately after training. This deliberately corrects the legacy convention
252
+ where a loader named `test` was used for model selection and `val` represented
253
+ held-out testing.
254
+
255
+ ## Distributed execution
256
+
257
+ `gnn4colliders.distributed` contains the small DDP boundary used by the
258
+ application layer. `DistributedContext` reads the standard `torchrun`
259
+ environment (`RANK`, `LOCAL_RANK`, and `WORLD_SIZE`), selects the rank-local
260
+ device, and owns process-group cleanup. Graph samples are sharded before
261
+ batching; training may pad rank shards for equal step counts, while validation
262
+ and prediction use unpadded shards so events are not counted twice.
263
+
264
+ The configured graph `batch_size` is per process. DDP wraps an otherwise
265
+ ordinary model after device placement, and checkpoint state is normalized to
266
+ the underlying model keys. Loss gradients are synchronized by DDP; epoch
267
+ metrics and evaluation outputs are gathered across ranks. Rank 0 writes
268
+ resolved configuration, checkpoints, and NPZ predictions. Moderate-size
269
+ prediction gathering is in-memory; streaming/sharded output is a future
270
+ extension.
271
+
272
+
273
+ ## Randomness, external services, and coupling
274
+
275
+ The CLI exposes `--seed`, but `main` passes it to model construction rather
276
+ than globally seeding Python, NumPy, or Torch
277
+ ([`training_script.py:638-753`](../legacy/root_gnn_dgl/scripts/training_script.py)).
278
+ Pre-batching has explicit seeds, but `AugmentedDataset` mutates the process-wide
279
+ NumPy seed ([`dataset.py:716-827`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)),
280
+ clustering uses unseeded `torch.randperm`/`randint` (`loss.py:259-295`), and
281
+ model reset/fine-tuning hardcodes `torch.manual_seed(2)`
282
+ ([`GCN.py:58-65`](../legacy/root_gnn_dgl/models/GCN.py),
283
+ [`GCN.py:900-915`](../legacy/root_gnn_dgl/models/GCN.py)). CUDA kernels, DDP,
284
+ and DataLoader behavior are not made deterministic.
285
+
286
+ Implicit coupling includes repository-relative `sys.path` insertion
287
+ ([`training_script.py:14-20`](../legacy/root_gnn_dgl/scripts/training_script.py)),
288
+ dynamic imports, mutable default lists/dicts, global `FEATURE_DTYPE`
289
+ ([`dataset.py:13`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)),
290
+ in-place `tracking_info` mutation ([`dataset.py:176-181`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)),
291
+ and in-place DGL graph mutation during forward.
292
+
293
+ The legacy environment assumes Python 3.8, PyTorch 2.0.1, CUDA 11.8, DGL
294
+ 1.1.1, ROOT, Awkward, Uproot, PyYAML, and scikit-learn
295
+ ([`setup/environment.yml:1-10`](../legacy/root_gnn_dgl/setup/environment.yml),
296
+ [`setup/environment.yml:240-295`](../legacy/root_gnn_dgl/setup/environment.yml)).
297
+ The active Linux development environment is intentionally separate: Python
298
+ 3.12, PyTorch 2.2.2/CUDA 12.1, and DGL 2.4.0 from the official DGL wheel
299
+ repository. CUDA runtime wheels do not replace the compatible host NVIDIA
300
+ driver and do not encode Perlmutter module settings.
301
+ Standard configs assume `/global/cfs/` and `/pscratch/` paths, CUDA/NCCL,
302
+ Slurm, and optionally Podman-HPC. `setup/download_data.sh` downloads the
303
+ external Hugging Face dataset `HWresearch/Delphes`
304
+ ([`download_data.sh:13-67`](../legacy/root_gnn_dgl/setup/download_data.sh)).
305
+
306
+ ## Apparent unused or secondary code
307
+
308
+ Not selected by the standard stats/Delphes configs, or only reachable from
309
+ optional workflows, are `GCN_global`, `GCN_global_2way`, most transfer variants,
310
+ attention models, `MultiModel`, and `Clustering`
311
+ ([`GCN.py:122-1933`](../legacy/root_gnn_dgl/models/GCN.py)); `UprootDataset`,
312
+ `tHbbEdgeDataset`, `AugmentedDataset`, and photon-ID paths
313
+ ([`uproot_dataset.py:10-31`](../legacy/root_gnn_dgl/root_gnn_base/uproot_dataset.py),
314
+ [`dataset.py:484-827`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py),
315
+ [`photon_ID_dataset.py:1-33`](../legacy/root_gnn_dgl/root_gnn_base/photon_ID_dataset.py));
316
+ optional loss and similarity utilities; and the no-op
317
+ `root_gnn_base.utils.graph_augmentation` ([`utils.py:393-395`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)).
318
+ The main path evaluates `test_loaders`; validation loaders are only assembled
319
+ when a config has a validation fold ([`training_script.py:682-747`](../legacy/root_gnn_dgl/scripts/training_script.py)).
320
+
321
+ ## 3. De facto interfaces to preserve
322
+
323
+ ### Metadata-aware dataset boundary
324
+
325
+ The rewrite uses named `EventMetadata` (`fold`, `weight`, and stable
326
+ `sample_id`) instead of exposing the legacy positional tracking tensor.
327
+ `GraphSample`, `GraphBatch`, `SplitDefinition`, and `GraphDataLoader` form the
328
+ ROOT-GNN orchestration boundary. Graph caches carry feature, graph, and cache
329
+ schema versions and reject incompatible artifacts before loading.
330
+
331
+ The current cache implementation stores processed `GraphSample` values (the
332
+ Level-2 cache). The separation from `RootEventDataset` is intentional: a
333
+ future Level-1 cache can store normalized `EventSample`/feature data for
334
+ sequence or transformer representations without requiring DGL graph caches.
335
+
336
+ The rewrite's shared data boundary is `gnn4colliders.data`: it reads selected
337
+ ROOT/Awkward branches and returns architecture-neutral event samples. Feature
338
+ construction and graph building remain separate downstream boundaries, so the
339
+ same samples can be reused by non-graph model families.
340
+
341
+ ### Configuration and CLI boundary
342
+
343
+ Hydra composes semantic YAML groups under `configs/` and passes the resolved
344
+ configuration to explicit application factories in `gnn4colliders.config`.
345
+ Those factories allow-list supported models, tasks, and trainer components;
346
+ YAML is never treated as an arbitrary Python import specification. The thin
347
+ `gnn4colliders` CLI selects `prepare`, `train`, `evaluate`, or `predict` and
348
+ delegates to the stable data, training, checkpoint, and inference APIs. A new
349
+ experiment should generally be a YAML change; new behavior belongs in Python.
350
+
351
+ 1. YAML `module`, `class`, `args`, plus runtime `sample_graph` and
352
+ `sample_global` injection.
353
+ 2. Dataset items `(DGLGraph, label, tracking, global_features)`.
354
+ 3. `ndata['features']`, `edata['features']`, seven node columns, and three edge
355
+ columns in `[deta, dphi, dR]` order.
356
+ 4. Tracking column 0 fold and column 1 weight semantics.
357
+ 5. `model(graph, global_feats)`, logits shape `[batch, out_size]`, and
358
+ `representation` where used.
359
+ 6. Weighted per-label loss, metric thresholds, checkpoint keys/prefix cleanup,
360
+ epoch filenames, and `.npz`/ROOT output fields.
361
+
362
+ ## 4. Ambiguous behavior
363
+
364
+ - Historical edge order/self-loop expectations; empty and padding graph inputs.
365
+ - Whether negative weights are meaningful or should always be absolute.
366
+ - Whether “validation” is intended to differ from the active test-loader path.
367
+ - Shape semantics of multi-label finishers and experimental transfer classes.
368
+ - Whether chunk IDs must match historical `np.array_split` boundaries.
369
+ - Required behavior for missing branches and dynamic selection expressions.
370
+
371
+ ## 5. Recommended rewrite boundaries
372
+
373
+ Separate typed configuration; ROOT/Awkward I/O; selections/folds/features/
374
+ edges; DGL cache/lazy loading/batching; active models and checkpoint adapters;
375
+ objectives/metrics; training lifecycle; and inference/ONNX applications.
376
+ Establish parity for the active `LazyDataset -> PreBatchedDataset ->
377
+ Edge_Network` binary/multiclass path first. Add experimental classes only when
378
+ a config or consumer proves they are required.
docs/compatibility.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Compatibility boundary
2
+
3
+ The package's canonical APIs use named `EventMetadata` fields and the version
4
+ 1 checkpoint schema. Compatibility is explicit and one-way: old artifacts are
5
+ adapted into the new representation and are never rewritten implicitly.
6
+
7
+ | Artifact or behavior | Supported | Boundary | Notes |
8
+ | --- | :---: | --- | --- |
9
+ | New checkpoint (`schema_version: 1`) | yes | `CheckpointManager` | Full model/lifecycle resume when state is present |
10
+ | Legacy `model_epoch_N.pt` checkpoint | yes | `gnn4colliders.compat.load_legacy_checkpoint` | Model state and active early-stop fields are adapted |
11
+ | Legacy DDP/compiled prefixes | yes | `normalize_legacy_state_dict_keys` | Supports `module.` and `_orig_mod.` |
12
+ | Legacy ROOT-GNN classifier name | yes | `map_legacy_edge_network_state_dict` | Maps `classify` to `classifier` |
13
+ | Legacy optimizer state | partial | legacy checkpoint adapter | Loaded when present; scheduler state is not available in the historical format |
14
+ | Legacy scheduler state | no | — | Historical checkpoints do not carry a supported scheduler state |
15
+ | Positional tracking rows | yes, at ingestion boundary | `EventMetadata.from_legacy_tracking` | Exactly `tracking[0] = fold`, `tracking[1] = weight`; shorter rows fail |
16
+ | Generic/unknown tracking layouts | no | — | The package does not guess historical column meanings |
17
+ | Modern NPZ output | yes | `inference.write_npz` | Named fields: `sample_id`, `logits`, `scores`, `predictions`, and available metadata |
18
+ | Historical `tracking_info` NPZ output | no | — | No active consumer remains; positional output is intentionally unsupported |
19
+ | Legacy YAML `module`/`class`/`args` | compatibility only | configuration boundary | Accepted only where the semantic factory can safely interpret it; new configs use semantic model names |
20
+
21
+ The frozen `legacy/` tree remains available to parity tests and historical
22
+ investigation. Production modules do not import executable code from it.
docs/configuration.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration guide
2
+
3
+ The CLI composes the installed `gnn4colliders.configs/config.yaml` with semantic
4
+ Hydra groups. A config
5
+ describes experiment intent; it does not contain arbitrary Python module or
6
+ class import paths.
7
+
8
+ ## Groups
9
+
10
+ | Group | Purpose |
11
+ | --- | --- |
12
+ | `data` | ROOT source or graph-cache path, batch settings, and fold splits |
13
+ | `model` | Model family and supported ROOT-GNN constructor settings |
14
+ | `task` | Binary or multiclass loss, score, and metric semantics |
15
+ | `trainer` | Device, seed, epochs, optimizer, scheduler, and early stopping |
16
+ | `checkpoint` | Output directory, resume checkpoint, or pretrained checkpoint |
17
+ | `inference` | Checkpoint, split, output path, and output format |
18
+ | `environment` | Device and experiment output root |
19
+ | `distributed` | Single-process/DDP selection and process-group backend |
20
+
21
+ The active model groups are `root_gnn/edge_network` and
22
+ `root_gnn/fine_tuned_edge_network`. The active task groups are
23
+ `pretraining_multiclass`, `binary_classification`, and `tth_cp_finetune`.
24
+
25
+ ## Preparation
26
+
27
+ `prepare` requires `data.files`, `data.cache.path`, `data.feature_branches`,
28
+ `data.object_types`, and `data.scales`. `feature_branches` follows the shared
29
+ seven-column feature contract: one branch/constant specification per output
30
+ column and one entry per configured object type. `CALC_E` and `NODE_TYPE` are
31
+ reserved derived specifications. `object_types` entries are `vector` or
32
+ `single`. Preparation reads the configured tree in file order and writes a
33
+ versioned `GraphSampleCache`.
34
+
35
+ Example overrides are easiest to maintain in a YAML file for real datasets:
36
+
37
+ ```yaml
38
+ # project-local example: data/my_events.yaml
39
+ files: [data/events.root]
40
+ tree_name: Events
41
+ cache:
42
+ path: cache/events.pt
43
+ feature_branches:
44
+ - [jet_pt]
45
+ - [jet_eta]
46
+ - [jet_phi]
47
+ - CALC_E
48
+ - [1.0]
49
+ - [0.0]
50
+ - NODE_TYPE
51
+ object_types: [vector]
52
+ scales: [1, 1, 1, 1, 1, 1, 1]
53
+ fold_var: eventNumber
54
+ weight_var: weight
55
+ ```
56
+
57
+ Then compose it with `data=my_events`. The cache stores processed graph
58
+ samples, labels, globals, named metadata, and feature/graph/cache schema
59
+ versions. Changing the feature or graph schema requires a new compatible cache;
60
+ loading a mismatched schema raises an error.
61
+
62
+ ## Common overrides
63
+
64
+ ```bash
65
+ uv run gnn4colliders train \
66
+ data.cache.path=cache/events.pt \
67
+ data.batch_size=64 \
68
+ trainer.max_epochs=50 \
69
+ trainer.seed=123 \
70
+ environment.output_root=outputs/my_run
71
+ ```
72
+
73
+ `data.batch_size` is per process. `data.splits.train_folds`,
74
+ `validation_folds`, and `test_folds` define conventional train/validation/test
75
+ 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
82
+ `<environment.output_root>/resolved_config.yaml`. A checkpoint includes schema
83
+ version, model weights/config, task config, trainer/optimizer/scheduler state,
84
+ early stopping state, metadata, and optional RNG state. Set
85
+ `checkpoint.resume=/path/to/epoch_####.pt` to continue a run. Set
86
+ `checkpoint.pretrained=/path/to/epoch_####.pt` with the fine-tuned model group
87
+ to load weights into a new task head; these options are mutually exclusive.
88
+
89
+ ## Environment profiles
90
+
91
+ `environment=local` selects CPU by default. `environment=macos` selects the
92
+ Apple MPS device for the portable PyTorch ROOT-GNN backend.
93
+ `environment=perlmutter` selects
94
+ the CUDA device and a conventional output-root pattern. Profiles should hold
95
+ device/output policy only; site-specific module loads and filesystem paths
96
+ belong in a launcher or shell environment.
docs/design.md DELETED
@@ -1,540 +0,0 @@
1
- # Design
2
-
3
- ## Modules
4
-
5
- | Module | Contents |
6
- | --- | --- |
7
- | `config.py` | the experiment dataclasses, YAML loading and overrides |
8
- | `data.py` | ROOT reading, Hugging Face download, derived branches, object features, `PackedEvents` and its cache |
9
- | `loader.py` | `EventLoader`: deterministic shuffling, DDP sharding, collation workers |
10
- | `graphs.py` | fully connected `TensorGraph` batches, edge features, CUDA-graph padding buffers |
11
- | `sequences.py` | padded token batches for the transformer and Deep Sets, event augmentations |
12
- | `models.py` | `EdgeNetwork`, `FineTunedEdgeNetwork`, `ObjectTransformer`, `TransformerClassifier`, `DeepSets`, checkpoint loading |
13
- | `jepa.py` | JEPA pretraining of the transformer or edge network encoder (encoding or kinematic targets), event embeddings |
14
- | `contrastive.py` | contrastive (SimCLR) pretraining of the same encoders |
15
- | `probe.py` | k-NN and linear probes of frozen encoders, linear CKA between them |
16
- | `pfn.py` | our prior-fitted network over events and the task prior of meta-training |
17
- | `tabular.py` | event rows and encoder embeddings for TabPFN, TabICL and TabDPT; LoRA meta-training of TabPFN and TabICL |
18
- | `tasks.py` | multiclass and binary losses and metrics, the pretraining and PFN losses |
19
- | `train.py` | `Trainer`, early stopping, checkpoints |
20
- | `cuda_graphs.py` | captured training steps |
21
- | `distributed.py` | `torchrun` process group and collectives |
22
- | `inference.py` | model outputs over a loader, prediction files |
23
- | `export.py` | ONNX export |
24
- | `cli.py` | the six commands |
25
-
26
- ## Data flow
27
-
28
- `prepare` reads each ROOT file in chunks of 100,000 entries (in parallel with
29
- `data.prepare_workers`) and stores every event's objects as rows of one
30
- `[objects, 7]` tensor with offsets, plus per-event labels, folds, weights and
31
- source file and entry. Nothing in the cache depends on the model: graphs and
32
- token sequences are built per batch, in the loader workers.
33
-
34
- A batch of the edge network is a set of fully connected graphs in flat
35
- edge-index form (`edge_src`, `edge_dst`, `node_batch`, `edge_batch`). Edges
36
- are directed, source-major and without self-loops except for one-node graphs,
37
- with features `[deta, dphi, dR]`. These conventions, the seven node columns,
38
- the MLP layout and the parameter names are those of the original ROOT-GNN, so
39
- the pretrained checkpoints in `Pretrained_GNN/` load without conversion.
40
-
41
- The transformer and Deep Sets take each event's objects as tokens
42
- `[events, objects, 7]`, zero-padded to the largest event of the batch, with
43
- a mask of real objects. A token is a linear map of the kinematic columns
44
- (`phi` as its cosine and sine) plus a learned embedding of `node_type`. The
45
- objects are stored by type (jets, electrons, muons, photons, MET), so a
46
- position embedding would mostly relearn the type; without one, neither
47
- model depends on object order, as with the edge network.
48
-
49
- * The transformer is a stack of pre-norm attention blocks, a mean over real
50
- objects and a linear classifier. With `model.pair_bias` (as in the
51
- Particle Transformer, Qu et al. 2022), an MLP of the pair features
52
- `[deta, dphi, dR, log(1 + m^2)]` (the edge features plus the invariant
53
- mass of the pair, objects taken as massless) adds a bias per head to the
54
- attention logits, so the transformer sees the same pairwise angles as the
55
- edge network.
56
- * Deep Sets (Zaheer et al. 2017; the particle flow network of Komiske et
57
- al. 2019) applies an MLP to each object, sums over real objects and
58
- applies an MLP to the sum: the baseline without object interactions.
59
-
60
- ### JEPA pretraining
61
-
62
- `model.name: jepa` pretrains an object encoder without labels, as in
63
- I-JEPA (Assran et al. 2023), on the 12 Delphes samples of the supervised
64
- pretraining. `model.backbone` selects the encoder: the transformer's
65
- (`configs/delphes_jepa.yaml`) or the edge network, of the size of the
66
- supervised pretrained models (`configs/delphes_jepa_edge.yaml`), whose
67
- object encodings are its node states after the last processing step and
68
- whose context is the fully connected graph of the visible objects only.
69
-
70
- * a random `model.mask_fraction` of each event's objects (rounded, at least
71
- one, and at least one left visible) is hidden;
72
- * the context encoder sees the visible objects; the target encoder, an
73
- exponential moving average of it (momentum rising linearly from
74
- `model.ema_momentum` to 1 over training) without gradients, sees all
75
- objects;
76
- * a predictor (`model.predictor_layers` attention blocks over the visible
77
- objects' encodings and one query per hidden object) predicts the hidden
78
- objects' layer-normalized target encodings, with a mean squared error.
79
- A query is told only the hidden object's type and its index among the
80
- objects of that type (their pt order), not its kinematics; pair features
81
- are used only between visible objects. For the edge network, its decoded
82
- global state (what its classifier reads) is one more predictor input, so
83
- the whole network is pretrained.
84
-
85
- The history reports `target_spread`, the standard deviation of the target
86
- encodings over a batch's objects averaged over dimensions, which falls
87
- towards zero if the representation collapses. The validation loss compares
88
- predictions with a moving target, so it does not select epochs: fine-tuning
89
- starts from `last.pt` (`configs/gamgam_jepa_finetune.yaml` for the
90
- transformer, `configs/gamgam_finetune.yaml` with `model.pretrained` for the
91
- edge network; encoder sizes must match). With `model.freeze_backbone=true` only the classifier is
92
- trained (a linear probe); `model.pretrained=null` trains the same
93
- architecture from scratch.
94
-
95
- `model.variance_weight` adds the VICReg variance term (Bardes et al. 2022):
96
- the mean shortfall below 1 of each dimension's standard deviation over the
97
- batch's context encodings (and the edge network's global states).
98
-
99
- First checks (one A100, `train.fast=true`, 2 pretraining epochs on Delphes
100
- fold 2, then 2 GamGam epochs, validation AUC, seed 1; not converged):
101
-
102
- | GamGam model | Validation AUC |
103
- | --- | --- |
104
- | transformer from scratch / from JEPA | 0.898 / 0.904 |
105
- | edge network from scratch | 0.895 |
106
- | edge network from the supervised pretrained model | 0.907 |
107
- | edge network from JEPA, last node states only | 0.845 |
108
- | edge network from JEPA, mean node states | 0.869 |
109
- | the same with `variance_weight` 0.1 / 1 | 0.874 / 0.810 |
110
-
111
- The edge network's JEPA encodings collapse without the variance term
112
- (`target_spread` 0.09 against about 1 with it), and none of the edge
113
- network variants yet beats training from scratch.
114
-
115
- ### Contrastive and masked-object pretraining, and probes
116
-
117
- Two more label-free objectives pretrain the same encoders on the same
118
- samples, and their encoders load the same way (`context_encoder`):
119
-
120
- * `contrastive.py` (`configs/delphes_contrastive.yaml`): SimCLR, as JetCLR
121
- and RS3L for jets. Two views of each event (random phi rotation, eta
122
- flip, momentum scale within `model.scale_jitter`, each object but the
123
- first dropped with `model.object_dropout`) are embedded and projected;
124
- the NT-Xent loss at `model.temperature` contrasts them with the other
125
- events of the batch. `embedding_spread` watches for collapse.
126
- * `model.jepa_target: features` (`configs/delphes_masked.yaml`): masked
127
- object modelling (Golling et al. 2024). JEPA's masking and predictor
128
- predict the hidden objects' kinematics instead of their target
129
- encodings, with no target encoder: the comparison with JEPA isolates
130
- predicting in embedding space.
131
-
132
- `gnn4colliders probe` compares encoders without fine-tuning (as jBOT):
133
- k-NN (cosine, `model.probe_neighbours`) and linear (L-BFGS logistic
134
- regression, `model.probe_l2`) classifiers of frozen event embeddings,
135
- trained on `data.train_events` events. A randomly initialized encoder
136
- (`model.pretrained=null`) is the reference; supervised checkpoints probe
137
- too. Results on GamGam: "Label efficiency on GamGam" below.
138
-
139
- `scripts/compare_encoders.py` compares encoders of one architecture by
140
- linear CKA (Kornblith et al. 2019), as the paper does: of their event
141
- embeddings (a matrix), and layer by layer against the first one (the mean
142
- over objects after each transformer block, or the edge network's mean node
143
- states after each step, then the event embedding). With a random encoder
144
- first, it shows where each objective moves the representation.
145
-
146
- ### In-context models: TabPFN, TabICL, TabDPT and a collider PFN
147
-
148
- Both classify a dataset's events given `data.train_events` labelled
149
- training events (an equal number per class) as context, without training on
150
- that dataset; `cli.in_context_forward` builds them for `evaluate` and
151
- `predict`.
152
-
153
- * `tabular.py`: TabPFN (Prior Labs; `model.tabpfn_version`, default 3.5)
154
- on fixed-width rows: the leading `model.slots` objects of each type (6
155
- jets, 2 electrons, 2 muons, 3 photons, MET) with their six kinematic
156
- columns, NaN when absent, and the number of objects of each type.
157
- * `pfn.py`: our own prior-fitted network. The object transformer (which
158
- can start from the JEPA encoder) embeds each event; the context events get
159
- a label embedding; `model.pfn_layers` attention blocks run over a task's
160
- events, every event attending to the context events only, so queries are
161
- independent and the context's keys and values are computed once. It is
162
- meta-trained (`task.name: pfn`) on tasks drawn from the Delphes samples:
163
- 2-10 classes of `task.prior_classes` in random order, Dirichlet class
164
- proportions, 16-2048 context events (log-uniform), 256 queries, and per
165
- event a random phi rotation and eta flip, per task a momentum and energy
166
- scale within `task.energy_jitter`. The GamGam processes are among the
167
- Delphes samples; `task.prior_classes=[1,6,7,8,9,10,11]` leaves them out.
168
- `configs/opendata_pfn.yaml` (written by `scripts/opendata_prior.py`)
169
- meta-trains it on the ATLAS Open Data 2025 MC instead: 925 classes of 331
170
- processes, H -> gamma gamma left out, a tenth of the processes held out
171
- for validation, classes mixing 1 to 3 processes.
172
- * Options that bring the PFN closer to TabPFN, all off by default (the
173
- default model's outputs match the earlier code to 2e-7):
174
- `model.pfn_summary_tokens` learned tokens carry each event through the
175
- encoder, with a context event's label added at the input instead of after
176
- the encoder; `model.pfn_alternating` adds attention across the task's
177
- events (summary tokens, context only) after every encoder block, as
178
- TabPFN alternates attention over features and over rows, so an event's
179
- encoding depends on the task; `model.pfn_normalize` scales each task's
180
- momenta so that the context's geometric-mean object pT is 50 GeV (as
181
- TabPFN standardizes features by the context); `model.pfn_ensemble`
182
- averages the class probabilities of passes with the events rotated in
183
- phi, flipped in eta and the class labels permuted (as TabPFN ensembles
184
- over class orders and preprocessings); and `task.synthetic_tasks` makes
185
- a fraction of the training tasks synthetic, as TabPFN's prior draws
186
- random functions: events of 1 to 3 processes ranked by a random network
187
- (16 tanh units) of 1 to 4 of 21 event observables (per object type the
188
- count, scalar sum pT, leading pT and mass; the b-tag count), cut into
189
- classes of Dirichlet fractions. A large context is encoded in chunks, one
190
- block at a time, but the alternating PFN keeps every context event's
191
- tokens and the keys and values of every block on the GPU: 100k events
192
- fit, all 1.98M GamGam training events do not (out of memory on a 40 GB
193
- A100).
194
- * Options from TabPFN-3.5 (219M parameters, 79 times ours; read from
195
- its checkpoint and code), all off by default: `model.pfn_retrieval` makes
196
- the logits the log of an attention-weighted vote of the context's labels
197
- (TabPFN's many-class decoder) instead of a linear classifier;
198
- `model.pfn_qk_norm` RMS-norms queries and keys per head and scales the
199
- queries by a learned factor times the log of the number of keys
200
- (TabPFN-3's scalable softmax), in all attention across events;
201
- `model.pfn_query_kv` lets the queries read the context through one
202
- key/value head of their own, so the memory of a large context is
203
- `n_heads` times smaller; `model.pfn_concat` joins the summary tokens into
204
- one token `pfn_summary_tokens * hid_size` wide for the blocks after the
205
- encoder (TabPFN-3.5 joins 8 x 128 to 1024), with the label embedded
206
- again; `model.pfn_fourier` learned frequencies of each object's log pT,
207
- eta and phi and `model.pfn_ranks` (each object's pT and energy ranked
208
- among the context's objects of its type, as TabPFN adds ECDF ranks) are
209
- added to the object embeddings, through maps that start at zero.
210
- `train.lr_schedule: cosine` warms up over `train.warmup_epochs`.
211
- * Meta-training speed: training batches are encoded in 4 parts of events
212
- with similar numbers of objects (only 37% of the padded object tokens
213
- were real), tasks are sampled in the loader workers rather than all at
214
- the start of an epoch, and under `torchrun` all ranks draw the same
215
- context size for a step, so that they wait less for each other. Steps are
216
- drawn from a generator per seed, epoch, rank and step, so the tasks
217
- differ from those of runs before this change. In-context inference sorts
218
- the context by number of objects and attends in chunks of
219
- `data.batch_size` events.
220
-
221
- * `tabular.py` also runs TabICLv2 (Qu et al., ICML 2026; BSD-3, open
222
- pretraining code) on the same rows, as a second tabular in-context model.
223
- Rows can measure azimuths from the leading object's (`model.relative_phi`)
224
- and end with the invariant mass and Delta R of every pair of the
225
- `model.pair_objects` leading objects by pt (energies are `pt cosh(eta)`,
226
- so objects are massless; a pair with MET gives a transverse-like mass).
227
- Both are off by default, so the default rows are unchanged.
228
- * `tabular.py` also runs TabDPT (Ma et al. 2024; Apache-2.0), trained on
229
- real tables rather than a synthetic prior; with
230
- `model.tabdpt_context_size` each event's context is its nearest context
231
- events (faiss retrieval), beyond what fits in one pass. It computes in
232
- float64 in places (on a Mac it runs on the CPU), and on macOS faiss and
233
- torch crash together unless `OMP_NUM_THREADS=1`.
234
- * Tabular models can see, instead of or beside the rows, the event
235
- embeddings of a pretrained object encoder (`model.tabular_inputs:
236
- embeddings` or `both`, the encoder of `model.backbone` and
237
- `model.pretrained`): physics-aware embeddings under a generic tabular
238
- prior. In meta-training the encoder trains with the LoRA updates unless
239
- `model.freeze_backbone`.
240
- * `tabular.py` also meta-trains the TabPFN-3.5 network itself on the
241
- PFN's tasks (`model.name: tabpfn`, `task.name: pfn`), on the rows above
242
- (built after the augmentation), starting from Prior Labs' weights. Calling
243
- the network directly skips TabPFN's preprocessing and ensembling, which
244
- on the GamGam fixtures cost nothing (validation AUC 0.852 against 0.843
245
- with `predict_proba`, 61 events). By default the network is frozen and
246
- the four attention projections and two MLP layers of each of its 24
247
- in-context blocks get rank-8 LoRA updates (2.8M parameters, zero at the
248
- start, so training starts from TabPFN as released); `model.lora_rank=0`
249
- trains all weights. In context, the context's keys and values are cached
250
- (the same logits as one pass over context and queries, to 2e-5). It
251
- separates what Prior Labs' prior and what collider tasks contribute:
252
- TabPFN as released, TabPFN meta-trained here, and our PFN. TabICL
253
- (`configs/delphes_tabicl.yaml`; BSD-3, so meta-trained weights can be
254
- shared) meta-trains the same way. Its attention reads fused projection
255
- weights directly, so LoRA is a parametrization of the weights (12 blocks,
256
- 0.59M of 28M parameters at rank 8), for TabPFN too. TabICL's network takes
257
- no NaN: rows are standardized by the context's statistics, absent values
258
- at the mean, clipped at 8 sigma. On the fixtures this gives AUC 0.827
259
- against 0.820 with its classifier, and the cached in-context forward
260
- equals the training forward only in float32 (with mixed precision on the
261
- CPU, AUC 0.59).
262
-
263
- ### Label efficiency on GamGam (first study, 2026-10-22)
264
-
265
- GamGam validation AUC (%) against the number of labelled training events N
266
- (`data.train_events`, equal per class), one seed, on one 4-GPU A100 node in
267
- an interactive allocation (3.5 h). Each entry is evaluated on 20,000 random
268
- validation events (`data.eval_events=20000`, about +-0.3); on all 246,922,
269
- evaluated for the supervised runs and the leading TabPFN runs, each is 0.05
270
- to 0.41 lower, in the same order (TabPFN-3.5: 80.28, 86.92, 89.01, 0.08 to
271
- 0.13 lower, matching the M2 Max's 80.26 and 86.93).
272
-
273
- | model | N = 100 | 1000 | 10000 | 50000 |
274
- |---|---|---|---|---|
275
- | edge network, from scratch | 78.00 | 82.74 | 87.66 | |
276
- | edge network, from `Pretrained_GNN` | 84.95 | 87.48 | 89.98 | |
277
- | transformer, from scratch | 72.33 | 83.47 | 87.03 | |
278
- | transformer, from JEPA | 73.25 | 85.10 | 88.60 | |
279
- | transformer, from masked objects | 75.92 | 86.12 | 88.73 | |
280
- | TabPFN-3.5 (rows) | 80.41 | 87.05 | 89.09 | 89.56 |
281
- | TabPFN-3.5 (rows, `relative_phi`, `pair_objects: 4`) | 80.61 | 87.78 | 89.56 | |
282
- | TabPFN-3.5 (rows + JEPA embeddings) | 80.97 | 87.43 | 89.44 | |
283
- | TabPFN-3.5 (rows + masked-object embeddings) | 82.99 | 88.22 | 89.94 | |
284
- | TabPFN-3.5 (rows + `Pretrained_GNN` embeddings) | 87.32 | 88.96 | 90.07 | |
285
- | TabPFN-3.5, released network, direct forward | 80.74 | 86.64 | 89.03 | |
286
- | TabPFN-3.5, meta-trained on Delphes (LoRA) | 80.52 | 84.77 | 88.34 | |
287
- | TabICLv2 | 80.53 | 85.68 | 88.41 | 89.12 |
288
- | TabDPT | 77.48 | 84.71 | 88.42 | |
289
- | our PFN, meta-trained on Delphes | 78.13 | 76.18 | 76.51 | |
290
-
291
- Probes (k-NN / linear) of the frozen encoders, the same N:
292
-
293
- | encoder | N = 100 | 1000 | 10000 |
294
- |---|---|---|---|
295
- | random | 73.4 / 72.6 | 76.6 / 83.1 | 79.5 / 85.4 |
296
- | JEPA | 73.0 / 73.4 | 78.9 / 83.4 | 81.8 / 87.9 |
297
- | masked objects | 75.0 / 77.5 | 81.7 / 85.7 | 84.4 / 88.5 |
298
- | contrastive | 61.6 / 67.9 | 70.0 / 80.3 | 76.4 / 85.9 |
299
-
300
- Setup (every run and its overrides: `scripts/label_efficiency_study.sh`):
301
- the pretraining runs saw the same 4M class-balanced Delphes events for 8
302
- epochs (`data.train_events=4000000`, 50 to 70 min each); the PFN
303
- meta-trained for 25 epochs of 1000 steps (25,000 steps, 95 min) and TabPFN
304
- for 6 epochs of 500 (3,000 steps, 48 min). Supervised runs train for a
305
- fixed number of steps with early stopping off (`train.early_stopping=null`;
306
- batch size, epochs: 100, 1000 for N = 100; 128, 400 for N = 1000; 256, 100
307
- for N = 10000; 20 to 35 min at N = 100, 2 to 4 min at N = 10000) and keep
308
- the epoch of best validation AUC, chosen on the same 20,000 events that are
309
- reported. That choice uses validation labels the in-context models do not
310
- get, and from N = 1000 most supervised runs overfit: their last epoch is far
311
- below the kept one. On all validation events (92% of them not used for the
312
- choice) the kept epochs lose 0.05 to 0.41, no more than the in-context runs
313
- lose, so picking the maximum over noisy evaluations inflates them little:
314
-
315
- | supervised run (N = 100 / 1000 / 10000) | kept epoch, 20k | kept epoch, all | last epoch, 20k |
316
- |---|---|---|---|
317
- | edge network, from scratch | 78.00 / 82.74 / 87.66 | 77.80 / 82.69 / 87.42 | 68.88 / 78.88 / 87.65 |
318
- | edge network, from `Pretrained_GNN` | 84.95 / 87.48 / 89.98 | 84.54 / 87.09 / 89.74 | 80.75 / 81.58 / 86.85 |
319
- | transformer, from scratch | 72.33 / 83.47 / 87.03 | 71.99 / 83.35 / 86.95 | 72.33 / 83.44 / 87.03 |
320
- | transformer, from JEPA | 73.25 / 85.10 / 88.60 | 73.07 / 84.96 / 88.45 | 72.34 / 78.82 / 81.46 |
321
- | transformer, from masked objects | 75.92 / 86.12 / 88.73 | 75.73 / 85.84 / 88.60 | 75.92 / 79.73 / 85.62 |
322
-
323
- With only N labelled events there is no large labelled set to choose the
324
- epoch on; without the choice (last epoch) every supervised model is below
325
- TabPFN-3.5 as released at every N except `Pretrained_GNN` at N = 100.
326
-
327
- With the configs' settings instead (batch size 1024, at most 100 epochs,
328
- early stopping after 5 epochs without training-loss improvement) N = 1000
329
- is one step per epoch, at most 100 steps: the transformer then reaches
330
- 70.04 at N = 1000 instead of 83.47. The in-context models take 10 to 30 s
331
- for the 20,000 queries at N = 1000 and 10000 (TabPFN 109 s at N = 50000;
332
- the first run of each, N = 100, also loads the weights).
333
-
334
- Findings:
335
-
336
- * TabPFN-3.5 as released, without training, comes within 0.4 (N = 1000)
337
- and 0.9 (N = 10000) of fine-tuning the supervised `Pretrained_GNN` (0.2
338
- and 0.7 on all validation events), with its epoch chosen on validation
339
- labels, and above it from N = 1000 without that choice. It beats every
340
- model trained from scratch at every N. TabICL is close behind; TabDPT is
341
- behind at small N. The physics row features add up to 0.7.
342
- * Encoder embeddings help TabPFN most at small N. With the label-free
343
- masked-object encoder: +2.6 at N = 100. With `Pretrained_GNN` (trained
344
- with labels, on Delphes samples that include the GamGam processes): the
345
- best result at every N run (100 to 10000; not run at 50000), +6.9 at
346
- N = 100.
347
- * Meta-training on Delphes tasks does not help. TabPFN with LoRA loses 1.9
348
- at N = 1000 against the same network with zero updates (its direct
349
- forward costs at most 0.4 against the classifier's preprocessing and
350
- ensembling). Our PFN is below TabPFN and gets worse with more context
351
- (76 at N = 1000, inside the meta-training context sizes 16 to 2048): to
352
- investigate before the held-out-processes run, which was not done.
353
- * Of the label-free pretraining objectives, masked-object modelling is the
354
- best, in probes and fine-tuned; JEPA's prediction in embedding space adds
355
- nothing over it here. Both improve fine-tuning the transformer over
356
- training it from scratch (+0.9 to +3.6) but stay below fine-tuning the
357
- edge network from `Pretrained_GNN`. That gap mixes the architecture (from
358
- scratch, the edge network is 5.7 above the transformer at N = 100) with
359
- the objective (label-free or supervised); the like-for-like comparison,
360
- JEPA of the edge network (`configs/delphes_jepa_edge.yaml`), was not run.
361
- * Contrastive pretraining is below a random encoder in the k-NN probe at
362
- every N and in the linear probe at N = 100 and 1000 (67.9 against 72.6,
363
- 80.3 against 83.1); at N = 10000 its linear probe is 0.5 above (85.9
364
- against 85.4). Without the momentum-scale augmentation
365
- (`model.scale_jitter=0`, 4 epochs) it is no better (72.4 / 84.3 at
366
- N = 10000).
367
-
368
- Not done:
369
-
370
- * the held-out-processes meta-training (`task.prior_classes` without the
371
- five GamGam processes) of the PFN, gated on it beating TabPFN, and of
372
- TabPFN and TabICL;
373
- * TabICL meta-training and `model.lora_rank=0`;
374
- * fine-tuning from contrastive pretraining (only probed), and JEPA of the
375
- edge network;
376
- * more seeds, and the test split.
377
-
378
- The loss is a weighted mean per class present in the batch, averaged over
379
- those classes, which balances the classes without explicit class weights.
380
- Metrics (accuracy, one-vs-rest AUC averaged over classes, macro precision,
381
- recall and F1) use events with positive weight.
382
-
383
- ### The TabPFN-like PFN on GamGam (2026-10-27)
384
-
385
- GamGam test AUC (%) in context, against the number of labelled context
386
- events N (equal per class), on 50,000 test events, one seed. Every PFN is
387
- meta-trained on the Open Data prior (`configs/opendata_pfn.yaml`). The
388
- earlier PFN (pooled events, contexts of 16 to 4096, random encoder, fp32)
389
- is the checkpoint of epoch 38. The new ones train 48 epochs of 1000 steps
390
- (2.3 h on one A100 each, bf16) with 2 summary tokens, alternating
391
- attention, the per-task momentum scale, the masked-object encoder
392
- (`delphes_masked`), half synthetic tasks, contexts of 16 to 16384 and
393
- batches of 16384 events; each ablation changes one of them.
394
-
395
- | PFN | N = 100 | 1000 | 10000 | 100000 |
396
- |---|---|---|---|---|
397
- | earlier | 80.63 | 82.47 | 83.54 | 83.53 |
398
- | earlier, ensemble of 8 | 81.92 | 83.87 | 84.67 | 84.77 |
399
- | new | 79.12 | 80.39 | 83.79 | 84.18 |
400
- | new, ensemble of 8 | 79.67 | 82.26 | 84.46 | 84.77 |
401
- | new, no alternating attention | 77.14 | 81.21 | 84.43 | 84.68 |
402
- | new, random encoder | 76.47 | 81.76 | 83.42 | 83.56 |
403
- | new, no synthetic tasks | 81.28 | 83.14 | 84.72 | 85.45 |
404
- | new, no synthetic tasks, ensemble of 8 | **83.75** | **86.11** | **86.54** | **86.52** |
405
-
406
- Validation on held-out Open Data processes after 48 epochs (loss /
407
- accuracy): new 0.772 / 0.722, no alternating 0.800 / 0.712, random encoder
408
- 0.809 / 0.711, no synthetic tasks 0.665 / 0.764; all still falling. One
409
- pass takes 1 to 3 s for the 50,000 queries at N = 1000 and 10000 and 35 to
410
- 58 s at N = 100000; the ensemble of 8 takes 460 s at N = 100000.
411
-
412
- Findings:
413
-
414
- * Synthetic tasks hurt at every N (0.9 to 2.8 against the same PFN
415
- without them; 1.8 to 4.1 ensembled) and in validation. Without them the
416
- PFN beats the earlier one at every N, by 1.8 to 2.2 with both
417
- ensembled.
418
- * The ensemble of 8 adds 1.1 to 3.0 to the PFN without synthetic tasks.
419
- * The masked-object encoder adds 2.7 at N = 100 and 0.6 at N = 100000.
420
- * Alternating attention adds 2.0 at N = 100 and costs 0.5 to 0.8 from
421
- N = 1000; it makes training 20% slower.
422
- * Against the validation AUCs of the first study (on other events, so
423
- only roughly comparable): the best PFN is above TabPFN-3.5 on rows
424
- (80.41) and every supervised transformer at N = 100, below the edge
425
- network from `Pretrained_GNN` (84.95) and TabPFN-3.5 with its embeddings
426
- (87.32). At N = 1000 it equals the transformer from masked objects
427
- (86.12) and is 0.9 and 1.4 below TabPFN-3.5 and `Pretrained_GNN`; at
428
- 10000 it is 2.2 to 3.4 below all three, and it gains nothing from 10000
429
- to 100000.
430
-
431
- Not done: all 1.98M training events as context (out of memory; see the
432
- PFN options above), longer meta-training, and the held-out-processes
433
- prior.
434
-
435
- ### TabPFN-3.5 options and a larger PFN on GamGam (2026-10-28)
436
-
437
- Both PFNs start from the best one above (no synthetic tasks) and add the
438
- TabPFN-3.5 options `pfn_retrieval`, `pfn_qk_norm`, `pfn_query_kv`,
439
- `pfn_fourier=16` and `pfn_ranks`, the cosine schedule (2 warm-up epochs,
440
- learning rate 1e-4) and the faster meta-training. "Small" keeps its size
441
- (2.9M parameters) and trains 40 epochs on one A100 (168 s each). "Large"
442
- has 4 summary tokens joined to one 512-wide token and 12 blocks after the
443
- encoder (42M parameters) and trains 26 epochs on three A100s under
444
- `torchrun` (280 s each, three times the tasks per step). Same test events
445
- and protocol as above, one seed.
446
-
447
- | PFN | N = 100 | 1000 | 10000 | 100000 |
448
- |---|---|---|---|---|
449
- | best above, ensemble of 8 | 83.75 | 86.11 | 86.54 | 86.52 |
450
- | small | 81.73 | 84.65 | 86.76 | 87.66 |
451
- | small, ensemble of 8 | 83.15 | 86.20 | 87.63 | 88.20 |
452
- | large | 82.90 | 85.83 | 87.72 | 88.43 |
453
- | large, ensemble of 8 | **84.06** | **86.80** | **88.24** | **88.62** |
454
-
455
- Validation on held-out Open Data processes in the last epoch (loss /
456
- accuracy): small 0.599 / 0.789, large 0.561 / 0.799 (best above 0.665 /
457
- 0.764). At N = 100000 one pass takes 21 s (small) and 87 s (large), the
458
- ensemble of 8 169 s and 698 s; the large ensemble peaks at 13 GiB.
459
-
460
- Findings:
461
-
462
- * The options, schedule and faster training add 0.5 to 2.2 to one pass of
463
- the best PFN above (81.28, 83.14, 84.72, 85.45), and the AUC now rises
464
- from 10000 to 100000 (by 0.4 to 0.9) instead of staying flat.
465
- * The larger PFN adds 0.8 to 1.2 more. One pass of it is 0.9 and 0.3
466
- below the earlier ensemble at N = 100 and 1000 and 1.2 and 1.9 above it
467
- from 10000; ensembled, it gains 0.3 to 2.1 on the earlier ensemble.
468
- * The ensemble adds less than before: 0.2 to 1.6.
469
- * The options were not ablated one by one, and the two runs differ in
470
- epochs and GPUs as well as size.
471
- * Against the validation AUCs of the first study (only roughly
472
- comparable): at N = 100 the large ensemble is 3.7 above TabPFN-3.5 on
473
- rows and 0.9 below `Pretrained_GNN`; at 1000 it is 0.3 and 0.7 below
474
- them, at 10000 0.9 and 1.7 below.
475
-
476
- Not done: all 1.98M training events as context, one ablation per option,
477
- more seeds.
478
-
479
- ## Performance
480
-
481
- Graphs differ in size, which suits neither `torch.compile` nor CUDA graphs
482
- out of the box:
483
-
484
- * `torch.compile(dynamic=True)` treats node and edge counts as symbolic
485
- sizes, so one compiled graph serves all batches without padding.
486
- * CUDA graph replay needs fixed shapes. Batches are copied into buffers
487
- rounded up to multiples of 512 nodes and 4096 edges; the remainder forms 64
488
- padding graphs connected only to themselves, so the real graphs' outputs do
489
- not change. A step is captured once per bucket, a few per epoch. The
490
- optimizer runs in Adam's `capturable` form with device-tensor learning
491
- rates, which the scheduler updates in place.
492
- * bfloat16 autocast covers the forward pass; logits, loss, graph sums (which
493
- divide by edge counts) and the optimizer stay float32.
494
-
495
- GamGam training step, 1024-event batches (about 5,600 nodes and 30,000
496
- edges), one A100-SXM4-80GB, `OMP_NUM_THREADS=8`, four loader workers (step
497
- times measured before the move to flat modules, epoch times after):
498
-
499
- | Mode | ms per batch | Epoch with validation |
500
- | --- | ---: | ---: |
501
- | float32 eager | 19.2 | 37 s |
502
- | CUDA graphs + TF32 | 8.5 | |
503
- | `torch.compile` | 13.0 | |
504
- | compile + CUDA graphs + TF32 | 6.1 | |
505
- | compile + CUDA graphs + bfloat16 (`train.fast`) | 5.9 | 9 s (first: 59 s, compiling) |
506
-
507
- Eager training is bound by kernel launches (about a thousand small kernels
508
- per step), not by arithmetic; bfloat16 alone is slower because of the extra
509
- cast kernels, which `torch.compile` fuses. Results of the fast modes agree
510
- with float32 to rounding, not bitwise (validation AUC 0.895 against 0.897
511
- after three epochs). Preparing the 2.5M GamGam events with 8 workers takes
512
- about 9 s on a login node (40 s was seen on a compute node, likely a cold
513
- file cache).
514
-
515
- PyTorch sizes its CPU thread pool to every visible core; on a 128-core node
516
- that makes batch construction several times slower and CPU reductions
517
- non-reproducible, so set `OMP_NUM_THREADS`. Single-threaded CPU training is
518
- bitwise reproducible.
519
-
520
- ## ATLAS Open Data GamGam versus Ho et al. (2026)
521
-
522
- The main benchmark reproduces the from-scratch baseline of Table 3 (5
523
- classes, 2.5M events, 80:10:10 split, five seeds). Choices the paper does not
524
- specify:
525
-
526
- * electrons and muons are split from `lep_*` by `lep_type` (11, 13);
527
- * b-tagging is `jet_MV2c10 > 0.8244273`, the 70% working point;
528
- * momenta and energies are scaled by 1e-4 (MeV to the pretraining unit of
529
- 10 GeV);
530
- * the fold is `eventNumber % 10`: folds 0-7 train, 8 validates, 9 tests;
531
- * training is unweighted (23% of ttH events have `mcWeight = -1`).
532
-
533
- The paper's stopping rule (5 epochs without training-loss improvement) never
534
- fires within 200 epochs on this task; validation AUC peaks earlier (epoch 67
535
- for seed 1) and `best.pt` keeps that epoch.
536
-
537
- | Test split | Accuracy | AUC | F1 | Precision | Recall |
538
- | --- | ---: | ---: | ---: | ---: | ---: |
539
- | Paper (5 seeds) | 71.8 ± 0.2 | 91.2 | 63.0 | 62.2 | 66.3 |
540
- | This package, seed 1 (CUDA graphs + TF32) | 71.1 | 91.3 | 62.6 | 62.4 | 66.7 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/end_to_end_validation.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # End-to-end legacy/rewrite validation
2
+
3
+ Task 21 compares staged event identity, labels, folds, weights, globals, node
4
+ features, topology, edge features, batching, fixed-weight forward, loss and
5
+ metrics, one optimizer step, short training, checkpoint reload, and inference.
6
+ The first failing stage is retained in JSON and Markdown reports.
7
+
8
+ Legacy and rewrite extraction may use separate environments. Each writes the
9
+ version-1 artifact described in `validation/README.md`; the comparator never
10
+ imports legacy modules or DGL graph objects. Record ROOT file/tree, selection,
11
+ event count, file size, SHA-256, branches, seeds, versions, device and source
12
+ identities in the artifact manifest. Do not use an absolute personal path as a
13
+ scientific event identity.
14
+
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
22
+ metadata, modern NPZ names and the version-1 checkpoint schema are expected
23
+ interface differences and must be reported explicitly.
24
+
25
+ Run the existing gates separately:
26
+
27
+ ```bash
28
+ GNN4COLLIDERS_REQUIRE_ROOT_GNN=1 uv run pytest
29
+ uv run ruff check .
30
+ ```
docs/export.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ROOT-GNN ONNX export
2
+
3
+ ONNX export is an inference-only adapter for prepared ROOT-GNN graph batches.
4
+ It does not read ROOT files or move feature construction into the deployment
5
+ model. Install the optional dependencies with:
6
+
7
+ ```bash
8
+ uv sync --extra root-gnn --extra onnx
9
+ ```
10
+
11
+ Export a checkpoint with the project CLI:
12
+
13
+ ```bash
14
+ uv run gnn4colliders export \
15
+ export.checkpoint=/path/to/checkpoint.pt \
16
+ export.output=model.onnx \
17
+ data.cache.path=/path/to/graph-cache.pt
18
+ ```
19
+
20
+ The exported model accepts six tensor inputs: `node_features`,
21
+ `edge_features`, `edge_src`, `edge_dst`, `node_batch`, and `global_features`.
22
+ `node_batch` identifies the graph for each node; edge membership is derived
23
+ from `node_batch[edge_src]`. Graph, node, edge, and batch dimensions are
24
+ dynamic. The model returns raw `logits`; task postprocessing and metadata stay
25
+ outside ONNX.
26
+
27
+ The exporter uses ONNX opset 17, validates the structure with `onnx.checker`,
28
+ and writes compact provenance/schema metadata to `model.onnx.json`. It
29
+ supports `EdgeNetwork` multiclass models and `FineTunedEdgeNetwork` binary
30
+ models. Empty graphs are invalid; single-node graphs (zero edges) are
31
+ represented and pooled with a zero edge contribution.
32
+
33
+ Direct DGL export was not retained: the active model uses DGL graph mutation
34
+ and reductions (`apply_edges`, `update_all`, and graph pooling) that are not a
35
+ portable ONNX contract. The adapter expresses those operations with standard
36
+ tensor indexing, `index_add`, and per-graph means.
docs/migration.md ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Incremental migration plan: `root_gnn_dgl`
2
+
3
+ The target is `legacy/root_gnn_dgl/`. `legacy/physicsnemo/` is a prior rewrite
4
+ attempt and may inspire abstractions, but it is not a parity target. Neither
5
+ legacy tree should be modified during migration.
6
+
7
+ Each phase should add focused unit tests, a deterministic fixture in
8
+ `data/fixtures/`, and parity tests under `tests/parity/` before moving upward.
9
+ Record intentional differences and checkpoint consequences here.
10
+
11
+ ## Phase 0 — freeze observations and fixtures
12
+
13
+ Capture a small representative ROOT-equivalent fixture containing the seven
14
+ active node features, three edge features, labels, fold values, weights, and
15
+ globals. Record outputs of `node_features_from_tree`, `full_connected_graph`,
16
+ `EdgeDataset.make_graph`, and `fold_selection`
17
+ ([`dataset.py:15-59`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py),
18
+ [`dataset.py:471-482`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py),
19
+ [`utils.py:121-143`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Preserve
20
+ one `.bin`, one `model_epoch_N.pt`, one evaluation `.npz`, and one inference
21
+ `.npz` fixture if available.
22
+
23
+ Task 3 characterization records the active-path observations. Node rows are
24
+ concatenated by object type in the configured order (jets, electrons, muons,
25
+ photons, MET), and the seven columns are `[pt, eta, phi, energy, btag, charge,
26
+ node_type]`. `CALC_E` is `pt*cosh(eta)` before the configured column scale is
27
+ applied. The graph is directed and uses all ordered pairs except self-loops
28
+ for graphs with more than one node; edge order is source-major. A one-node
29
+ graph is a special case: the no-self-loop branch retains its sole self-loop.
30
+ Edge columns are `[deta, dphi, dR]`, with `dphi` wrapped into `[-pi, pi]`.
31
+ Dataset items expose `(graph, label, tracking, global_features)`; tracking
32
+ column 0 is the fold identifier and column 1 is the event weight. These are
33
+ compatibility observations, not proposed fixes.
34
+
35
+ ## Phase 1 — configuration boundary
36
+
37
+ Implement a typed configuration layer that reads `Training`, `Model`,
38
+ optional `Loss`, and `Datasets`. Initially retain a compatibility adapter for
39
+ `module`/`class`/`args` and runtime injection of `sample_graph` and
40
+ `sample_global`, matching `buildFromConfig`
41
+ ([`utils.py:10-43`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Keep
42
+ dynamic imports isolated at this boundary rather than spreading reflection
43
+ through new code.
44
+
45
+ ## Phase 2 — pure preprocessing parity
46
+
47
+ Port and test, in isolation:
48
+
49
+ - branch-to-node conversion, `CALC_E`, `NODE_TYPE`, constants, scaling, empty
50
+ objects, and dtypes (`node_features_from_tree`,
51
+ [`dataset.py:15-50`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py));
52
+ - string/tuple selections and cutflow (`check_selection`, `selection_mask`,
53
+ `compute_cutflow`, [`dataset.py:75-158`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py));
54
+ - fold masks and cache suffixes (`fold_selection`, `fold_selection_name`,
55
+ [`utils.py:121-143`](../legacy/root_gnn_dgl/root_gnn_base/utils.py));
56
+ - deterministic chunk partitioning (`hash_partition`,
57
+ [`batched_dataset.py:27-31`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)).
58
+
59
+ This is the highest-value parity layer: model parity is invalid if graph inputs
60
+ differ.
61
+
62
+ Task 4 implements the shared branch-to-node feature builder under
63
+ `gnn4colliders.features`. It preserves the active seven-column schema,
64
+ object-type ordering, explicit scales, derived `CALC_E`, node-type codes,
65
+ float32 output, and supported empty vector collections. Selection, fold, and
66
+ chunk helpers remain deferred to later data-infrastructure work.
67
+
68
+ Task 6 implements the shared ROOT/Awkward ingestion boundary under
69
+ `gnn4colliders.data`. `RootEventDataset` returns immutable, architecture-neutral
70
+ `EventSample` values with selected branch data, labels, tracking, and globals;
71
+ events are ordered by input file order with a global zero-based index. Fold
72
+ filtering, caching, batching, and model-specific conversion remain deferred.
73
+
74
+ ## Phase 3 — graph construction and cache format
75
+
76
+ Implement graph construction with tests for node/edge counts, directed edge
77
+ ordering, self-loop policy, `[deta, dphi, dR]` order, metadata, and empty graphs.
78
+ Preserve the dataset item contract `(graph, label, tracking, global_features)`
79
+ from `RootDataset.__getitem__`
80
+ ([`dataset.py:465-469`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py)).
81
+
82
+ Then implement DGL `.bin` serialization, lazy chunk loading, pre-batching, and
83
+ padding. Compare against `RootDataset.save/load`, `LazyDataset`, and
84
+ `PreBatchedDataset` ([`dataset.py:396-469`](../legacy/root_gnn_dgl/root_gnn_base/dataset.py),
85
+ [`batched_dataset.py:129-174`](../legacy/root_gnn_dgl/root_gnn_base/batched_dataset.py)).
86
+ Treat `NONE`, `STEPS`, `FIXED`, and `NODE` as explicit features; do not hide
87
+ the hardcoded fixed padding sizes.
88
+
89
+ Task 7 establishes the metadata-aware orchestration boundary around this
90
+ phase: `EventMetadata`, `GraphSample`, `GraphBatch`, fold-based split
91
+ selection, deterministic batching, and a version-checked graph-sample cache.
92
+ The cache is deliberately Level 2; normalized event caching remains a future
93
+ extension so non-graph model families can reuse ROOT preprocessing.
94
+
95
+ ## Phase 4 — active model parity
96
+
97
+ Task 8 adds the active `EdgeNetwork` and `FineTunedEdgeNetwork` under
98
+ `gnn4colliders.models.root_gnn`. The update order and MLP ordering follow the
99
+ legacy active path. The rewrite uses an explicit backbone/classifier boundary,
100
+ local DGL graph scope, and does not mutate global RNG state in constructors.
101
+ Model parity now covers fixed-weight pretraining and transfer paths, including
102
+ historical checkpoint prefixes. The legacy transfer implementation has an
103
+ active bug when nonempty globals are supplied (`Pretrained_Output` ignores its
104
+ argument); parity therefore characterizes its supported no-global path, while
105
+ the rewritten model supports both global and fallback modes.
106
+
107
+ Task 22 adds `TensorEdgeNetwork`, a native PyTorch realization of the same
108
+ message-passing equations. It receives explicit node/edge index tensors from
109
+ the batching boundary, retains parameter names for legacy checkpoint loading,
110
+ and has fixed-weight CPU parity coverage against the DGL backend. It is the
111
+ default training backend for CPU, CUDA, and Apple MPS; DGL remains available as
112
+ a cache and compatibility adapter during the transition.
113
+
114
+ Port `models.GCN.Edge_Network` first. Preserve constructor parameters,
115
+ `forward(graph, global_feats)`, feature keys, processor order, MLP LayerNorm
116
+ placement, and logits shape. Compare intermediate and final tensors on fixed
117
+ graphs using the legacy architecture
118
+ ([`GCN.py:18-35`](../legacy/root_gnn_dgl/models/GCN.py),
119
+ [`GCN.py:182-251`](../legacy/root_gnn_dgl/models/GCN.py)).
120
+
121
+ Next port `Transferred_Learning_Finetuning`, including pretrained
122
+ `model_state_dict` loading, removal of the final classifier, and new classifier
123
+ initialization ([`GCN.py:884-997`](../legacy/root_gnn_dgl/models/GCN.py)). Test
124
+ both frozen and unfrozen modes. Defer other model classes until an active
125
+ config or consumer proves they are needed.
126
+
127
+ ## Phase 5 — objectives and metrics
128
+
129
+ Implement the default objective exactly: elementwise configured loss,
130
+ tracking-column weights, per-unique-label normalization, and averaging across
131
+ labels ([`training_script.py:320-359`](../legacy/root_gnn_dgl/scripts/training_script.py)).
132
+ Add parity cases for positive, zero, and negative weights and binary versus
133
+ multiclass shapes.
134
+
135
+ Port metric behavior from
136
+ [`training_script.py:438-510`](../legacy/root_gnn_dgl/scripts/training_script.py):
137
+ sigmoid threshold 0.5, argmax, weight masking, weighted ROC AUC, one-vs-rest
138
+ multiclass AUC, and NaN behavior when AUC is undefined. Add `models/loss.py`
139
+ classes only with dedicated tests; do not substitute their reductions.
140
+
141
+ ## Phase 6 — checkpoint and lifecycle
142
+
143
+ Task 10 implemented the in-memory single-process training lifecycle before the
144
+ checkpoint portion of this phase: `Trainer`, explicit optimizer/scheduler
145
+ builders, `EarlyStopping`, reproducibility seeding, `GraphBatch.to`, and
146
+ epoch/history result types. Checkpoint persistence/resume and the Python
147
+ inference/evaluation and named NPZ/ROOT output layers are now implemented.
148
+ Distributed execution and CLI wiring were completed in the later phases.
149
+
150
+ Task 10 also establishes corrected split semantics: validation is evaluated
151
+ every epoch and is the only split used for model selection or early stopping;
152
+ the test split remains held out and is evaluated separately after fitting. The
153
+ legacy loader naming inversion (`test` used for selection and `val` held out)
154
+ is not carried into the rewrite.
155
+
156
+ Create a checkpoint adapter preserving `model_epoch_<epoch>.pt` and keys
157
+ `epoch`, `model_state_dict`, `optimizer_state_dict`, and `early_stop`
158
+ ([`training_script.py:565-604`](../legacy/root_gnn_dgl/scripts/training_script.py)).
159
+ Support legacy DDP/compiled prefixes (`module.` and `_orig_mod.`) as exercised
160
+ by checkpoint lookup and inference
161
+ ([`utils.py:145-248`](../legacy/root_gnn_dgl/root_gnn_base/utils.py),
162
+ [`inference.py:274-290`](../legacy/root_gnn_dgl/scripts/inference.py)). Port
163
+ `EarlyStop` state and log parsing separately
164
+ ([`utils.py:325-390`](../legacy/root_gnn_dgl/root_gnn_base/utils.py)). Verify
165
+ resume, restart, early termination, and `.npz` fields before distributed work.
166
+
167
+ ## Phase 7 — CLI, inference, and export
168
+
169
+ Task 12 implemented ordered prediction/evaluation, task-owned score
170
+ semantics, checkpoint weight-only loading, named metadata retention, NPZ
171
+ output, and explicit ROOT entry alignment. The semantic CLI and the validated
172
+ ROOT-GNN ONNX export adapter are implemented.
173
+
174
+ Task 13 adds Hydra composition and a single-process CLI around those existing
175
+ APIs. The current application data boundary is a versioned
176
+ `GraphSampleCache`; ROOT preparation converts events through the shared
177
+ feature and graph builders before writing that cache.
178
+
179
+ Build thin new applications around tested library interfaces in this order:
180
+
181
+ 1. preprocessing/cache generation (`scripts/prep_data.py`);
182
+ 2. training/evaluation (`scripts/training_script.py`);
183
+ 3. inference to `.npz` and ROOT (`scripts/inference.py`);
184
+ 4. ONNX export after PyTorch parity (`gnn4colliders export`).
185
+
186
+ Use subprocess integration tests with tiny fixtures. Preserve CLI options only
187
+ where they serve an active workflow; document removed diagnostic/cluster-only
188
+ options.
189
+
190
+ ## Phase 8 — reproducibility and deployment
191
+
192
+ Task 14 adds the initial deployment boundary: CPU/GPU DDP through standard
193
+ `torchrun` variables, rank-local graph-sample sharding, global metric/output
194
+ gathering, rank-0 checkpoint/config writing, and Perlmutter-oriented Slurm
195
+ examples. Evaluation deliberately avoids sampler padding duplicates. The
196
+ remaining follow-up is a streaming or sharded output path for very large
197
+ distributed inference jobs.
198
+
199
+ The seed policy remains explicit: the configured seed is offset by rank for
200
+ process-local randomness, while distributed sample assignment is derived from
201
+ the configured seed, world size, and epoch. GPU kernel nondeterminism and
202
+ exact per-rank RNG checkpoint replay remain environment-dependent. Slurm/NCCL,
203
+ Podman-HPC, ROOT, and Hugging Face integrations stay in launcher/adapters
204
+ rather than package code.
205
+
206
+ ## Checkpoint compatibility checklist
207
+
208
+ - [x] Load a checked-in or generated multiclass pretrained checkpoint.
209
+ - [x] Load a legacy fine-tuning checkpoint after prefix normalization.
210
+ - [x] Resume optimizer and early-stop state.
211
+ - [x] Produce equivalent logits on a deterministic graph fixture.
212
+ - [x] Produce equivalent `.npz` score, label, and metadata fields.
213
+ - [x] Preserve ROOT scalar/vector score branch conventions in the Python adapter.
214
+
215
+ Known risks are documented in [`architecture.md`](architecture.md): edge order,
216
+ self-loops, weight semantics, validation/test naming, padding, dynamic
217
+ selection evaluation, reproducibility, and the experimental model/loss surface.
218
+
219
+ ## Migration closure status
220
+
221
+ ### Task 18 compatibility closure
222
+
223
+ The compatibility boundary is now explicit in `gnn4colliders.compat`.
224
+ Production ingestion stores named `EventMetadata`; legacy two-column tracking
225
+ is converted only at the compatibility boundary. Checkpoint prefix cleanup and
226
+ the historical ROOT-GNN `classify` to `classifier` mapping have one canonical
227
+ implementation. The new checkpoint schema and named NPZ output remain
228
+ canonical. See [`compatibility.md`](compatibility.md) for the supported and
229
+ intentionally unsupported historical artifacts.
230
+
231
+ The following matrix describes the supported new stack, rather than every
232
+ class that exists in `legacy/`:
233
+
234
+ | Legacy area | New-stack status | Notes |
235
+ | --- | --- | --- |
236
+ | ROOT/Awkward ingestion | migrated | `RootEventDataset` returns `EventSample` in file/event order |
237
+ | node features | migrated + parity-tested | seven-column schema, `CALC_E`, ordering, scales, float32 |
238
+ | edge construction | migrated + parity-tested | directed source-major topology and `[deta,dphi,dR]` |
239
+ | graph cache | migrated | versioned `GraphSampleCache`; graph-level cache only |
240
+ | folds and weights | migrated | named `EventMetadata.fold` and `.weight` |
241
+ | batching | migrated | deterministic local loader and DDP sharding |
242
+ | legacy padding modes | deferred | no active new-stack consumer |
243
+ | `Edge_Network` | migrated + parity-tested | `EdgeNetwork`, raw logits |
244
+ | transfer/fine-tuning | migrated + parity-tested | frozen or trainable backbone |
245
+ | loss and metrics | migrated + parity-tested | task-owned weighted reductions and full-split AUC |
246
+ | training lifecycle | migrated | `Trainer`, validation semantics, scheduler, early stopping |
247
+ | checkpoints/resume | migrated | schema v1; historical weight/prefix adapter |
248
+ | inference/NPZ | migrated | named output fields and ordered accumulation |
249
+ | ROOT score output | compatibility adapter | Python API supported; CLI currently NPZ-only |
250
+ | DDP | migrated | torchrun boundary, rank-0 artifacts, gathered metrics |
251
+ | Slurm/Perlmutter | launcher examples | site policy remains outside package code |
252
+ | ONNX export | migrated for ROOT-GNN | tensor-only adapter, ONNX Runtime validation, and `export` CLI; raw graph tensors are the input contract |
253
+
254
+ ### Intentional redesigns
255
+
256
+ These are deliberate new-stack contracts, not accidental parity failures:
257
+
258
+ * `tracking[:, 0]` and `tracking[:, 1]` become named `metadata.fold` and
259
+ `metadata.weight`; public consumers do not depend on positional columns.
260
+ * Dynamic legacy YAML `module`/`class` construction becomes allow-listed
261
+ semantic Hydra configuration.
262
+ * The monolithic training script becomes `Task` + `Trainer` + checkpoint and
263
+ inference adapters.
264
+ * Graph state is scoped to the forward pass rather than relying on persistent
265
+ mutation of shared graph state.
266
+ * Model constructors do not mutate global RNG state; seeding is explicit in
267
+ the training/application boundary.
268
+ * Validation is the selection/early-stopping split and test is held out. This
269
+ corrects the legacy loader-name inversion.
270
+
271
+ Compatibility preserves externally observable scientific behavior where it is
272
+ validated; it does not promise to preserve every legacy implementation bug.
273
+ The characterized legacy transfer path had a nonempty-global handling bug;
274
+ the rewrite supports named globals. Negative weights, rare empty graphs,
275
+ historical checkpoint variants, and legacy padding edge cases remain areas to
276
+ audit when a supported consumer requires them.
277
+
278
+ ## ROOT-GNN v1 completion checklist
279
+
280
+ - [x] active ROOT data path and graph cache
281
+ - [x] validated feature, graph, model, task, and metric behavior
282
+ - [x] train from scratch and fine-tune a pretrained backbone
283
+ - [x] resume new-stack checkpoints and load supported historical weights
284
+ - [x] evaluate and predict named outputs
285
+ - [x] single-process and DDP application boundaries
286
+ - [x] Perlmutter/Slurm launcher examples and profiling guidance
287
+ - [x] ROOT-GNN ONNX export and CPU Runtime parity
288
+ - [ ] streaming/sharded large-scale prediction output
289
+ - [ ] removal of frozen legacy reference
290
+ - [ ] ROOT-Transformer representation/model
291
+
292
+ ROOT-GNN v1 is complete when the checked-in new stack can prepare active data,
293
+ reproduce validated legacy behavior, train, transfer, resume, evaluate,
294
+ predict, and run single-process or DDP workflows. The remaining unchecked
295
+ items are intentionally deferred rather than undocumented promises.