Skip to content

Generalise FourierEncoder with a column/frequency schema - #913

Merged
sevmag merged 8 commits into
graphnet-team:mainfrom
sevmag:feature/fourier-encoder-schema
Oct 2, 2026
Merged

sevmag merged 8 commits into
graphnet-team:mainfrom
sevmag:feature/fourier-encoder-schema

Conversation

@sevmag

@sevmag sevmag commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses #753, following the design @RasmusOrsoe proposed in his review of #759. Picks up where @mobra7's PR left off.

Problem

FourierEncoder bakes in two assumptions:

  1. Column layout the input must be (x, y, z, time, charge, auxiliary), with the slicing hardcoded (x[:, :, :3], x[:, :, 3], …) and the presence of charge/auxiliary inferred from the feature count. This is the index error reported in FourierEncoder not flexible enough for different detectors #753 for FEATURES.ICECUBE86.
  2. Numeric range — the multipliers 4096 (xyz, time) and 1024 (charge) are fixed to the Kaggle normalisation.

Change

As proposed in the review of #759:

  • FourierEncoderEPJC — the existing class, behaviour unchanged, with a docstring noting it is the graphnet implementation of the EPJ-C solution (arXiv:2310.15674) and naming the assumptions that make it hard to use elsewhere. IceMix/DeepIce and ISeeCube now reference it, so nothing currently on main changes behaviour.
  • FourierEncoder — new, taking schema: Dict[int, float] mapping column index to multiplier, e.g. {1: 1024, 3: 4096}. Columns absent from the schema are not embedded. Nothing is assumed about column order or meaning, and the boolean embedding and MLP projection are left to the model, both being problem-specific. Exposes output_dim so the model can size what follows.

The schema docstring states the wavelength relation, so the next person choosing a value has the arithmetic rather than a constant copied from another detector.

Not included

The review also proposed renaming DeepIce to DeepIceEPJC with a refactored DeepIce owning the MLP, dropping the auxiliary embedding, and updating examples/04_training/06_train_icemix_model.py. Left for a follow-up: dropping the auxiliary embedding is a behaviour change worth deciding on its own, whereas this half is strictly additive. Happy to carry it through in this PR instead if you would rather have it in one piece.

@sevmag

sevmag commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator Author

Also I think we should move DeepIce out of the models/gnn folder and rename the icemix.py file to deepice.py . It is really confusing

@sevmag

sevmag commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

@pweigel I think your opinion on this is valuable!

@sevmag

sevmag commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

@Aske-Rosted and I had the concern that renaming the Kaggle version of the FourierEncoder to a different name will break backwards compatibility with older pretrained state dicts. I thought about this a bit more, and I actually do not think this is an issue, since the name of the nn.Modules is determined by the attribute name and not the class name.

Example:
The FourierEncoder is initialized like this:
self.fourier_ext = FourierEncoder(
The torch state_dicts will then have a key called fourier_ext holding its weights. Changing FourierEncoder into FourierEncoderEPJC is not an issue at all.

@sevmag
sevmag force-pushed the feature/fourier-encoder-schema branch from f2a211a to 3b9a2d1 Compare September 11, 2026 15:27
@Aske-Rosted
Aske-Rosted self-requested a review September 15, 2026 12:34

@Aske-Rosted Aske-Rosted left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the description is a bit verbose and historical. I think it's better to provide just a description of what the class does but exclude information about how it came to exist. If this was a special scenario of the more general class then it might make sense to have that description but not the other way around.

@Aske-Rosted Aske-Rosted left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the doc string looks fine to me.

felixyu7 added a commit to felixyu7/graphnet that referenced this pull request Sep 30, 2026
…-backbone

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@felixyu7

Copy link
Copy Markdown

We've built Neptune (#923) on this FourierEncoder, and the per-column (multiplier, n_freq) schema covers everything it needs. Thanks! One request: under bf16-true / 16-true, the input and freqs are cast to low precision, which is too coarse for the phases at large multipliers. Neptune works around this by calling the encoder in float32 with autocast off. Could SinusoidalPosEmb compute the phases in float32 itself, so each model doesn't need its own guard?

sevmag and others added 8 commits October 2, 2026 16:31
`FourierEncoder` assumed its input was ordered (x, y, z, time, charge,
auxiliary) and applied multipliers fixed to the Kaggle dataset's
normalisation, so it index-errors or silently mis-encodes on data laid out
differently (issue graphnet-team#753). The multipliers are the more subtle half: they set
which separations the sinusoidal ladder can resolve, so carrying them to a
detector with a different normalisation changes the resolved physical range
without any error.

Rename the existing class to `FourierEncoderEPJC`, documented as the EPJ-C
Kaggle implementation with those assumptions, and add a `FourierEncoder`
taking `schema: Dict[int, float]` -- column index to multiplier. Nothing is
assumed about column order or meaning, and the boolean embedding and MLP
projection are left to the model, both being problem-specific.

Follows the design proposed in the review of graphnet-team#759.

The refactor is representation-preserving: the sinusoidal encoding is
elementwise, so one embedding per column lays channels out exactly as the
strided xyz block did. `tests/models/test_embedding.py` asserts that, and
that a schema of the EPJC columns reproduces its pre-projection embedding
bit-for-bit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F75gXoxTcs4nVRxGp91ENf
`seq_length` is a per-sequence quantity, while `x` is per-step, and the name
is also the constructor's embedding-width argument -- so state the `[B]`
shape and that each step receives a copy of its sequence's embedded length.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F75gXoxTcs4nVRxGp91ENf
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F75gXoxTcs4nVRxGp91ENf
A column's resolvable band has two ends: the multiplier fixes the finest
wavelength, `n_freq` fixes how far the ladder reaches beyond it. Only the
first was per column, so features wanting different spans could not get
them -- and they do differ. On a 1 km neutrino detector the sensor spacing
and extent ask for a span of ~180x, event timing for ~3600x and the charge
quantisation for ~25x, against the single hardcoded 10000 (a ~9085x ladder)
every feature shares today.

`schema` now takes either a multiplier or a `(multiplier, n_freq)` pair per
column, with a bare multiplier falling back to the encoder's `n_freq`, so
existing schemas are unchanged. One `SinusoidalPosEmb` is built per distinct
span rather than per column, which keeps the common single-span case at one
module and one learnable scale under `scaled`.

`SinusoidalPosEmb` also precomputes its ladder into a non-persistent buffer
instead of rebuilding it every forward pass, so the extra spans cost
nothing and checkpoints are untouched. Its `n_freq` docstring said "Number
of frequencies", which is `dim / 2`; it is the span of the ladder, and that
mislabel is what hid the missing knob.

Verified bit-identical against the previous implementation for the ladder
itself, `FourierEncoderEPJC`, `SpacetimeEncoder`, and `FourierEncoder` with
bare multipliers; a per-column span changes only its own block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F75gXoxTcs4nVRxGp91ENf
Refactor FourierEncoderEPJC to remove dataset assumptions and allow flexible schema input.
Clarified usage instructions and limitations of the model.
Whitespace-only: rewrap the class summary to the line width docformatter
v1.7.7 enforces, which the pre-commit hook otherwise reformats.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pqUKMMW7ZPkhc9EChzCux
Add `phase_dtype` to `SinusoidalPosEmb` and thread it through
`FourierEncoderEPJC` and `FourierEncoder`. At large multipliers the
sinusoidal phase grows beyond what the bf16/fp16 mantissa can represent,
so neighbouring positions collapse to the same embedding under
low-precision training. Evaluating the phase and its sine and cosine in
`phase_dtype` (float32 by default) and casting the result back to the
input dtype removes that failure while leaving float32 runs unchanged.
Pass `phase_dtype=None` to keep the input dtype.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016pqUKMMW7ZPkhc9EChzCux
@sevmag
sevmag force-pushed the feature/fourier-encoder-schema branch from e3daeab to 611f153 Compare October 2, 2026 20:36
@sevmag

sevmag commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

We've built Neptune (#923) on this FourierEncoder, and the per-column (multiplier, n_freq) schema covers everything it needs. Thanks! One request: under bf16-true / 16-true, the input and freqs are cast to low precision, which is too coarse for the phases at large multipliers. Neptune works around this by calling the encoder in float32 with autocast off. Could SinusoidalPosEmb compute the phases in float32 itself, so each model doesn't need its own guard?

good point, checkout the way I handled it @felixyu7

@sevmag
sevmag merged commit f7caa15 into graphnet-team:main Oct 2, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants