GNN4Colliders / tasks /task3.md
ho22joshua's picture
rewriting codebase (#7)
916755e
|
Raw History Blame Contribute Delete
12.5 kB

Task 3: Characterize Legacy Data and Graph Behavior

Build a deterministic characterization/parity test suite for the active legacy ROOT-GNN preprocessing path.

The ROOT-GNN development environment is now expected to include working PyTorch and DGL support.

Do not implement the new production data pipeline yet.

Before making changes, read:

AGENTS.md
docs/architecture.md
docs/migration.md

Then inspect the relevant legacy implementation under:

legacy/root_gnn_dgl/

Focus only on the active data/graph behavior required by the standard ROOT-GNN workflow.


Goal

Capture the externally observable behavior of the legacy preprocessing stack so that the new implementation can later be rewritten against explicit parity tests.

The relevant flow is:

ROOT/Awkward event data
    ->
node feature construction
    ->
graph topology
    ->
edge feature construction
    ->
labels / tracking / global features

The tests must characterize what the legacy implementation actually does.

Do not encode assumptions solely because they appear in documentation.

If documentation, previous expectations, and observed legacy behavior disagree, treat the running legacy implementation as the behavior to characterize and document the discrepancy.


Environment validation

Before writing or modifying parity tests, verify that the ROOT-GNN environment is functional.

Run:

uv sync --extra root-gnn

uv run python -c "import torch; print(torch.__version__)"
uv run python -c "import dgl; print(dgl.__version__)"

DGL-dependent parity tests must not silently skip because DGL is absent.

If DGL cannot import, stop and report the environment failure rather than treating skipped tests as successful characterization.


1. Node feature characterization

Characterize the active legacy node-feature construction.

Verify:

  • feature count
  • feature ordering
  • feature values
  • dtype
  • shape
  • concatenation across object types where applicable
  • feature scaling behavior

The active schema is expected to include:

pt
eta
phi
energy
btag
charge
node_type

But verify this against the legacy implementation rather than assuming it.

Characterize derived behavior including:

CALC_E = pt * cosh(eta)

if that is what the active legacy path actually implements.

Also verify:

  • constant-valued branches
  • NODE_TYPE
  • scale application
  • object ordering when multiple object types are combined

Use explicit expected arrays where practical.


2. Graph topology characterization

Characterize the graph produced by the active legacy graph builder.

Verify:

  • whether the graph is directed
  • whether self-loops are present
  • number of nodes
  • number of edges
  • source indices
  • destination indices
  • edge ordering if deterministic
  • behavior for small graph sizes

Do not assume a two-node fully connected directed graph has four edges.

Explicitly characterize the observed behavior.

For example, if the legacy implementation produces:

0 -> 1
1 -> 0

for a two-node graph, encode that exact behavior.

If the topology is fully connected without self-loops, verify the expected edge count:

N * (N - 1)

for multiple values of N.

At minimum test:

N = 1
N = 2
N = 3

where supported by the legacy implementation.

For edge ordering:

  • assert exact ordering only if it is deterministic and observable
  • otherwise test topology independent of ordering and document the ambiguity

3. Edge feature characterization

Characterize the active legacy edge-feature construction.

Verify that edge features correspond to:

deta
dphi
dR

in the actual stored order.

Test at least one case where phi wrapping matters.

For example, use objects near:

phi = +pi
phi = -pi

so that naive subtraction differs from wrapped angular distance.

Verify:

dR = sqrt(deta^2 + dphi^2)

if that is the actual legacy behavior.

Important:

Expected edge-feature arrays must correspond exactly to the graph edges produced by the legacy implementation.

Do not duplicate expectations for edges that do not exist.

Where ordering is deterministic, verify the feature associated with each (src, dst) pair.


4. Tracking semantics

Characterize tracking information.

Verify at minimum the observed semantics of:

tracking[:, 0]
tracking[:, 1]

Expected meanings from the architecture documentation are:

column 0 -> fold identifier
column 1 -> event weight

Confirm those semantics against the legacy implementation.

Also characterize:

  • dtype
  • shape
  • whether tracking information is mutated during dataset processing
  • behavior of negative or non-unit weights where easily testable

Do not expand into training-weight semantics yet.


5. Labels

Characterize labels produced by the active standard dataset path.

Verify:

  • shape
  • dtype
  • scalar vs vector representation
  • binary-task representation where applicable
  • multiclass representation where practical

Focus on the active configurations.

Do not characterize every experimental loss or task variant.


6. Global features

Characterize global-feature behavior in the active path.

Verify:

  • whether globals are present
  • shape
  • dtype
  • empty/default behavior when global features are unused

Do not invent a new global-feature abstraction.

The purpose is only to record legacy behavior.


7. Fixtures

Use deterministic fixtures under:

tests/fixtures/

Prefer, in order:

  1. small in-memory NumPy/Awkward objects
  2. a very small ROOT file only where actual ROOT I/O must be tested
  3. compact stored expected outputs only when inline arrays become unwieldy

Avoid large external datasets.

Fixtures should exercise meaningful cases such as:

  • one object
  • two objects
  • three or more objects
  • multiple object types if relevant
  • nontrivial eta values
  • phi wrapping across -pi/pi
  • different node types
  • non-unit event weights

Test empty object collections only if the active legacy path supports them meaningfully.

Do not invent impossible legacy inputs just to increase coverage.


8. Test organization

Place characterization tests under:

tests/parity/

A reasonable organization is:

tests/parity/
    conftest.py
    test_legacy_node_features.py
    test_legacy_graph_and_edges.py
    test_legacy_tracking.py
    test_legacy_labels_and_globals.py

Use fewer files if that produces clearer tests.

Shared legacy import/setup helpers may live in:

tests/parity/conftest.py

Do not copy production functions from the legacy tree into tests.

Tests should invoke the legacy implementation directly.


9. Legacy imports

Avoid permanently modifying Python import paths outside the test process.

If the legacy package requires path adjustment because it is not installable, isolate that behavior in test setup.

Do not modify:

legacy/root_gnn_dgl/

to make imports easier.

Do not introduce repository-wide sys.path hacks into production code.


10. Expected outputs

Prefer readable explicit expectations.

For example:

expected_features = torch.tensor(
    [
        [...],
        [...],
    ],
    dtype=torch.float32,
)

For topology:

expected_src = torch.tensor([...])
expected_dst = torch.tensor([...])

Use exact equality for:

  • shapes
  • integer IDs
  • node-type codes
  • edge endpoints
  • fold values

where deterministic.

Use numerical comparison for floating-point values.

Avoid opaque snapshot testing unless there is a strong reason.


11. Floating-point tolerances

Use explicit tolerances for numerical parity.

For normal float32 feature calculations, prefer something approximately like:

torch.testing.assert_close(
    actual,
    expected,
    rtol=1e-5,
    atol=1e-6,
)

unless observed legacy behavior requires a different tolerance.

Do not use unnecessarily loose tolerances.

If a different tolerance is required, explain why.


12. Characterization vs specification

These tests are characterization tests.

They answer:

What does the legacy system currently do?

They do not automatically answer:

What should the redesigned system do forever?

When unusual behavior is discovered, classify it as one of:

compatibility requirement
legacy observation
ambiguous behavior
likely bug requiring later design decision

Do not silently "fix" questionable behavior in this task.


13. Known graph expectation correction

A previous test incorrectly expected four edges for a two-node graph.

The running legacy implementation produced two edges.

Investigate and characterize this behavior explicitly.

If confirmed, the likely topology is:

directed fully connected graph
without self-loops

which gives:

num_edges = N * (N - 1)

Do not modify legacy code to make it match the old test expectation.

Update the parity test to match actual observed behavior.

Also verify the actual (src, dst) pairs and corresponding edge features.


14. Documentation of ambiguities

Record meaningful discoveries in:

docs/migration.md

or a concise dedicated section if appropriate.

Potential ambiguities include:

  • exact edge ordering
  • single-node graph behavior
  • empty graph behavior
  • missing ROOT branches
  • phi wrapping convention
  • negative weights
  • object concatenation ordering
  • shape behavior for unused global features

Only document ambiguities actually encountered.

Do not expand documentation unnecessarily.


Out of scope

Do not implement or modify:

src/gnn4colliders/data/
src/gnn4colliders/features/
src/gnn4colliders/graphs/
src/gnn4colliders/models/
src/gnn4colliders/training/
src/gnn4colliders/inference/

except for existing nonfunctional scaffolding if absolutely required for test discovery, and preferably not at all.

Do not implement:

  • new data readers
  • new feature builders
  • new graph builders
  • model code
  • training code
  • losses
  • metrics
  • checkpoint handling
  • Hydra configuration behavior
  • runtime optimizations

Do not refactor legacy code.

Do not modernize legacy code.

Do not run Ruff against the frozen legacy tree.


Validation

Run:

uv run python -c "import torch, dgl; print(torch.__version__, dgl.__version__)"

uv run pytest tests/parity -v

uv run ruff check tests

uv run ruff format --check tests

Then run the full current suite:

uv run pytest

The expected result for Task 3 is:

all characterization tests pass
no DGL tests skipped because DGL is unavailable

If a test fails because its expectation disagrees with running legacy behavior:

  1. investigate the legacy implementation
  2. determine the observed behavior
  3. fix the test expectation if the test was wrong
  4. document ambiguity if appropriate

Do not modify legacy behavior to satisfy a characterization test.


Git inspection

Before committing, inspect:

git status
git diff

Verify that:

  • legacy files are unchanged
  • no new production implementation exists
  • fixtures are small
  • no caches are tracked
  • no model checkpoints were added
  • no generated DGL files were added unintentionally
  • no unrelated changes are included

Completion criteria

Task 3 is complete when:

  1. PyTorch and DGL import successfully.
  2. Active node-feature behavior is characterized.
  3. Graph topology is characterized.
  4. Edge features and phi wrapping are characterized.
  5. Tracking semantics are characterized.
  6. Labels and global-feature behavior are characterized to the extent exercised by the active path.
  7. Important ambiguities are documented.
  8. All parity tests pass.
  9. No DGL-dependent parity tests are skipped because of missing dependencies.
  10. Legacy production code remains untouched.

Completion report

Report:

  1. files created
  2. files modified
  3. fixtures created
  4. behaviors characterized
  5. graph topology observed
  6. edge ordering observed
  7. numerical tolerances used
  8. ambiguities discovered
  9. behavior classified as compatibility requirement vs observation
  10. validation commands and results
  11. anything intentionally deferred

After validation succeeds, create one Git commit containing only Task 3 changes.

Use:

test: characterize legacy data and graph behavior

Before committing, inspect the final diff and ensure no unrelated files are included.