Skip to content

feat: validate coordinate names in one place, for every backend - #659

Merged
Saransh-cpp merged 7 commits into
scikit-hep:mainfrom
ikrommyd:patch
Sep 19, 2026
Merged

Saransh-cpp merged 7 commits into
scikit-hep:mainfrom
ikrommyd:patch

Conversation

@ikrommyd

@ikrommyd ikrommyd commented Oct 31, 2025 •

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Description

Rebased on main and reimplemented. Solves the half of #660 that vector can fix on its own.

The problem

Each backend had its own rules for which coordinate names make a vector — obj(), the VectorObject*D constructors, array(), the six __array_finalize__, _check_names, the VectorSympy*D constructors, and the Numba vector.obj — and they disagreed. All of these work on main and silently build the wrong vector:

vector.array({"pt": ..., "phi": ..., "eta": ..., "mass": ..., "energy": ...})  # energy wins, mass is dead
vector.array({"x": ..., "y": ..., "rho": ..., "phi": ...})                     # rho, phi are dead
vector.array({"x": ..., "y": ..., "z": ..., "eta": ...})                       # eta is dead
vector.zip({"rho": ..., "phi": ..., "pt": ...})                                # pt is dead
vector.VectorObject2D(x=1, px=2, y=3)                                          # x == 2

The fix

_methods._check_coordinate_names is now the only implementation. It returns (is_momentum, dimension, coordinates, extra) or raises, and takes the dimension and momentum-ness a constructor is restricted to, so it serves both vector.obj (deduces both) and MomentumNumpy4D (fixes both). Every backend calls it, and the error messages come from one table instead of the nine hand-written copies. Source diff: −1016/+567.

The awkward behaviors validate through __awkward_validation__ (awkward#3710, needs awkward>=2.8.11; older versions skip it), so what never went through a vector constructor is caught too:

>>> ak.zip({"pt": pt, "eta": eta, "phi": phi, "mass": mass, "energy": energy}, with_name="Momentum4D")
TypeError: MomentumArray4D with fields [...]: specify t= or tau=, but not more than one

>>> jets["rho"] = rho
TypeError: MomentumArray4D with fields [...]: 'pt' and 'rho' both map to 'rho'

Subclassed behaviors (coffea's) inherit it. Cost is ~0.5 µs per array creation against ~130 µs for one v.pt on 100k entries; _check_coordinate_names is lru_cached on the field-name tuple.

Intended breaks

  1. A field that is a coordinate under another name is no longer carried as payload. Ordinary extra fields (charge, btag) are unaffected.
  2. Generic vectors reject momentum-aliases: VectorObject2D(px=1, py=2) used to return a non-momentum vector while vector.obj(px=1, py=2) returns MomentumObject2D. Momentum classes still take generic names.
  3. An array whose fields stop describing its record name is rejected — del jets["mass"], or a column-projected form that kept __record__ but dropped coordinates. On main those gave a half-broken vector that failed at first coordinate access.
  4. Error messages changed. No test asserted on their text.

Tests

tests/test_pr_659.py: all 511 subsets of the generic names and all 222 combinations with momentum-aliases, per backend, against a reference implementation written independently of the one under test; plus the record names accepted and rejected by __awkward_validation__, and the numba path. 3969 tests in ~5 s, 100% coverage of the new code.

Coffea's validation and this agree on all 48,597 combinations I compared, and coffea's suite passes against this branch unchanged (its own hooks shadow vector's, so nothing there needs to change).

Checklist

  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't any other open Pull Requests for the required change?
  • Does your submission pass pre-commit? ($ pre-commit run --all-files or $ nox -s lint)
  • Does your submission pass tests? ($ pytest or $ nox -s tests)
  • Does the documentation build with your changes? ($ cd docs; make clean; make html or $ nox -s docs)
  • Does your submission pass the doctests? ($ pytest --doctest-plus src/vector/ or $ nox -s doctests)

@ikrommyd ikrommyd changed the title Patch feat: improve errors for invalid combinations of arguments in vector constructor methods Oct 31, 2025

@Saransh-cpp Saransh-cpp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good catch, @ikrommyd! Thanks for working on this!!

Please see my comments below:

Comment thread src/vector/backends/object.py Resolved
Comment thread src/vector/backends/object.py Resolved
Comment thread src/vector/backends/numpy.py Resolved
Comment thread tests/test_pr_659.py Resolved
@ikrommyd

ikrommyd commented Nov 1, 2025 •

Copy link
Copy Markdown
Member Author

Thanks @Saransh-cpp for the feedback. I'll tackle it as soon as find a time slot.

There is one more case that I'd like to solve which was the original inspiration for this. That's when people do ak.zip and use with_name but I don't see at all how we can solve this easily. For example

In [12]: x = ak.zip({"pt": events.Electron.pt, "eta": events.Electron.eta, "phi": events.Electron.phi, "mass": events.Electron.mass, "energy": -999, "rho": 1001}, with_name="Momentum4D")

In [13]: x.pt
Out[13]: <Array [[1001], [], [], ..., [1001, 1001], [1001]] type='55342 * var * int64'>

In [14]: x.mass
Out[14]: <Array [[-3.98e+03], [], ..., [...], [-1.53e+03]] type='55342 * var * float64'>

People can assign nonsense fields together like rho and energy here without knowing that rho it is an alias for pt for example and that entirely messes up your 4-vector math (silently).
Or people can do jets["rho"] = something. But I really don't know how to prevent this. Awkward just blindly assigns a name without caring at all what that implies and without knowing at all what behaviors it carries over.
Even worse things happen when it comes straight from the root file. In CMS, we have a flavor of nanoaod that has pt, eta, phi, phi, mass and energy. But "energy" is not the "correct" energy you'd get from the pt-eta-phi-mass 4-vector. It's a different energy measurement. And when you define a 4-vector with those 5 using with_name, that's bad.
But I really don't know if it's even possible to solve this because it's awkward who is assigning the names and behaviors without any knowledge.

@Saransh-cpp

Copy link
Copy Markdown
Member

I think @pfackeldey might be able to help with this - can we add constructor checks to our custom Awkward behaviors?

@pfackeldey

Copy link
Copy Markdown
Collaborator

I think @pfackeldey might be able to help with this - can we add constructor checks to our custom Awkward behaviors?

I'm not sure about the following:
I think you can't really change the way __init__ works through behaviors because awkward updates it's base class after the constructor has been run.

If my understanding here is wrong, you can do the following:

class MyBehavior:
  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    # check
    if set(self._layout.fields) != {"pt", "eta", "phi", "mass"}:
      raise ValueError('got different fields than this behavior expected')

However, @ikrommyd and me talked about this and these constructor checks are indeed helpful, but do not prevent to break the behaviors. What would instead prevent this thoroughly would be:

  1. Awkward Arrays are fully immutable (you can't even set/update/delete fields)
  2. we disallow getattr to retrieve fields so it's clear to the user if they invoke a property or access a field
  3. vector constructors are only allowed to have access to certain fixed fields

(1.) and (3.) solves this issue entirely I think, (2.) is in addition to make people aware of when they invoke the mass property and when they access the mass field.

This is of course major break in API, so I'm not sure if we can/want to do this. I'm adding this here to write down the fix that solves this issue thoroughly.
There could be ways to enforce this immutability wrt fields per array instance. Then we can preserve the current functionality in general awkward, but vector could enforce true immutability which does not allow to update/add/del fields to arrays with vector behaviors.
About (2.) I don't think there's really a way to solve this except for a API breakage in awkward. But this is not the issue, it's just hard to understand when one accesses a property and when a field through dot-access.

I could add a way in awkward-array to make them truly immutable that you can define through behaviors in vector. And in theory I could also think of a way to make post init constructor checks in awkward arrays possible for behaviors.

If you want @ikrommyd & @Saransh-cpp I can prepare a PR and you can have a look? Or what do you think?

@Saransh-cpp

Copy link
Copy Markdown
Member

Sounds good to me, @pfackeldey! I'll be happy to review the PR in awkward and subsequently propagate the new immutability way to vector 🙂

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hi all. The subject sounded interesting/perplexing hence I had a read.

Indeed good catches, @ikrommyd 👍.

Now, there are 2 different matters being mixed together, as it were, it seems to me:

The real problem of one being able to create a vector with some nonsensical combination is the main thing (to be) addressed in this PR, which is important to get fixed while providing relevant error messages to users so that they can see the issues in the way they wrote things. All good here and I won't comment on the details as @Saransh-cpp knows them infinitely better than me,

The other issue mentioned relates to constructions such as In [12]: x = ak.zip({"pt": events.Electron.pt, "eta": events.Electron.eta, "phi": events.Electron.phi, "mass": events.Electron.mass, "energy": -999, "rho": 1001}, with_name="Momentum4D"), which relates to vector only by the time the actual 4-vector is put together behind the scenes. At construction level a user could in principle assign events.Electron.pt to px, events.Electron.eta to phi, etc., if they had some crazy idea for renaming variables. It is only the with_name that will impose constraints on what variables to assign to what. Admittedly, in the case you @ikrommyd provide, it seems to me that the issue should be trivial to catch because 5 variables to make a 4-vector is over-constraining the system and should not be allowed (this comment holds for the "real problem" just above). Or am I reading too quickly and misundestanding things? In any case I don't think you can in general leverage 4-vector knowledge in the Vector package at the level of ak.zip since the latter is generic. Unless you explicitly make the connection awkward-vector with a with_name parameter that is a Vector class. Else you implicitly make an awkward coupling with subtle behaviours, I feel.

@ikrommyd

ikrommyd commented Nov 3, 2025 •

Copy link
Copy Markdown
Member Author

Hi @eduardo-rodrigues, it's not trivial to catch when you do ak.zip though and you don't use any of the vector constructor methods. Because ak.zip does not know what Momentum4D means. It does not know or care what behaviors it carries over. More than that, we can hard-code the vector behavior checks explicitly in awkward-array sure but that's not the full story. You can subclass and define your own behaviors (coffea does that a lot). So then you can do with_name="PtEtaPhiMCandidate" which is something that coffea defines. So what behaviors do we check? We can't hard-code all the user-defined stuff? And with_name, all it does is it assigns a __record__ parameter to the array's layout to a string. It is completely blind as to what this implies.

@eduardo-rodrigues

Copy link
Copy Markdown
Member

You are emphasising of my comments in some way, meaning that it is hard and/or dangerous to couple the 2 packages, but if you have no knowledge, then you can easily get into trouble. That's why I mentioned the possibility of having an explicit coupling using a class rather than a string, so with_name=vector.MomentumObject4D rather than with_name="Momentum4D". At least then somebody subclassing would be on its own, taking responsibility, and for other standard use cases, there would be some checks implemented. But I'm still likely being naive here and am showing how little I know about the internals of awkward, sorry if this is not helpful.

@ikrommyd

ikrommyd commented Nov 3, 2025

Copy link
Copy Markdown
Member Author

Did a full write-up here of the issue: #660
This PR solves part of it. For further discussion, I suggest we move there to not make the PR discussion messy.

Comment thread src/vector/backends/numpy.py Resolved
@pfackeldey

Copy link
Copy Markdown
Collaborator

Linking my proposed solution here as well: #660 (comment)

@codecov

codecov Bot commented Jan 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.35165% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.88%. Comparing base (462a31a) to head (e5b7837).

Files with missing lines Patch % Lines
src/vector/backends/_numba_object.py 87.50% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #659      +/-   ##
==========================================
+ Coverage   87.84%   88.88%   +1.04%     
==========================================
  Files          96       96              
  Lines       11243    10783     -460     
==========================================
- Hits         9876     9585     -291     
+ Misses       1367     1198     -169     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ikrommyd

ikrommyd commented Feb 5, 2026

Copy link
Copy Markdown
Member Author

The more I investigate, the more horrible it becomes. There are a ton of inconsistencies between backends and some bugs regarding which coordinate combinations are allowed. Converting to draft. Need to come back with a different approach.

@ikrommyd
ikrommyd marked this pull request as draft February 5, 2026 01:50
@ikrommyd ikrommyd changed the title feat: improve errors for invalid combinations of arguments in vector constructor methods feat: validate coordinate names in one place, for every backend Jul 20, 2026
Each backend had its own rules for which coordinate names make a vector, and
they disagreed. These all worked on main and silently built the wrong vector:

    vector.array({"pt": ..., "phi": ..., "eta": ..., "mass": ..., "energy": ...})
    vector.array({"x": ..., "y": ..., "rho": ..., "phi": ...})
    vector.zip({"rho": ..., "phi": ..., "pt": ...})
    vector.VectorObject2D(x=1, px=2, y=3)

_methods._check_coordinate_names is now the only implementation; object, numpy,
awkward, sympy, and numba all call it, and the error messages come from one
table instead of nine copies.

The awkward behaviors validate through __awkward_validation__ too, so
ak.zip(..., with_name="Momentum4D") and jets["rho"] = ... are caught, which is
the part of scikit-hep#660 that vector can fix on its own. Needs awkward>=2.8.11;
older versions skip it.

Two intended breaks: a field that is a coordinate under another name is no
longer carried as payload, and generic vectors reject momentum-aliases.

tests/test_pr_659.py covers all 511 subsets of the generic names and all 222
alias combinations, per backend, against a separate reference implementation.
@ikrommyd
ikrommyd marked this pull request as ready for review July 21, 2026 00:11
@ikrommyd

ikrommyd commented Jul 21, 2026 •

Copy link
Copy Markdown
Member Author

@henryiii send an agent over for review. I did the rebasing and consolidation with a coder and an adverserial reviewer myself. The goal was to have the validation only in one place for all backends and to have 100% test coverage.

Brings in the pre-commit hook update (scikit-hep#735), which moves ruff to v0.16.6 and
rewrites `SameVectorType` annotations to `Self`.

Conflict in src/vector/backends/object.py: main only reordered the `None`
members of type annotations inside the old body of `_gather_coordinates`,
which this branch replaced, so the branch's version is kept.

Code added on this branch is brought in line with the updated hooks:
`type[A] | type[B]` becomes `type[A | B]` in the three NumPy coordinate-type
tables (PYI055), and the deliberate `exec` that compiles every Numba call
site in one function is marked `noqa: S102`.

Assisted-by: ClaudeCode:claude-fable-5.1
Brings in "rm typing_extension" (scikit-hep#743) and "give a useful error when
ProjectionClass*D is missing" (scikit-hep#737).

Conflict in src/vector/backends/awkward.py (`_wrap_result`, two hunks): both
sides changed the same lines. This branch replaced the hand-written name
tuples in the conditions with the shared `_temporal_fields` and
`_longitudinal_fields` sets, and scikit-hep#737 replaced `cls.ProjectionClassND` in the
bodies with `_projection_class(cls, N)`. The resolution keeps both: this
branch's conditions with main's bodies.

Assisted-by: ClaudeCode:claude-fable-5.1

@Saransh-cpp Saransh-cpp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks a lot for working on this, @ikrommyd! 🚀

This cleaned up the codebase quite well.

There is one place where the current errors diverge:

import awkward as ak, vector
b = vector.backends.awkward.behavior

for fields in ({"x": 3.0, "y": 4.0, "z": 12.0}, {"x": 3.0, "y": 4.0, "t": 99.0}):
    try:
        v = ak.Array([fields], with_name="Vector2D", behavior=b)
        print(list(fields), "-> abs =", abs(v)[0])
    except TypeError as e:
        print(list(fields), "->", str(e))
['x', 'y', 'z'] -> VectorArray2D with fields ['x', 'y', 'z']: these are the coordinates of a 3D vector, not of a 2D vector, allowed combinations are:

    x= y=
    rho= phi=
['x', 'y', 't'] -> VectorArray2D with fields ['x', 'y', 't']: unrecognized combination of coordinates, allowed combinations are:

    x= y=
    rho= phi=

I believe it is okay; given the amount of coordinate options and backends we have, it would be difficult to pin everything down to the exact same error. Do you think we should try making them the same?

Comment thread docs/src/make_awkward.md
TypeError: MomentumArray4D with fields ['pt', 'phi', 'eta', 'mass', 'energy']: specify t= or tau=, but not more than one (got 'energy', 'mass')
```

This runs whenever the behaviors are attached, not only in `vector.Array` and `vector.zip`, so `jets["rho"] = ...` is caught too. It is the `__awkward_validation__` hook of each behavior class and needs `awkward>=2.8.11`; older versions skip it. A rejected in-place assignment is only undone by `awkward>=2.12`: before that, the `TypeError` is raised but the array keeps the offending field. Subclassed behaviors (see [Advanced: subclassing Awkward-Vector behaviors](awkward.ipynb)) inherit the check and can extend it by overriding `__awkward_validation__` and calling `super().__awkward_validation__()`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Quick comment: should we move awkward's pin to 2.8.11 or is the current one okay?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't think so, it will just be ignored in previous versions. We effectively just have a useless function sitting there, it's fine.

@ikrommyd

ikrommyd commented Sep 18, 2026 •

Copy link
Copy Markdown
Member Author

@Saransh-cpp regarding the divergene you said above, do you prefer them to be the same? In the first example it is a valid combination, just not for a 2d vector. So perhaps the user got the with_name="Vector2D" wrong and not the coordinates. That was the point of the different error really.

@Saransh-cpp

Copy link
Copy Markdown
Member

Ah I see, that does make sense. Everything looks good to me as it is then. I'll merge this and prepare a release. Thanks a lot!

@Saransh-cpp
Saransh-cpp merged commit 971a602 into scikit-hep:main Sep 19, 2026
20 checks passed
@ikrommyd

ikrommyd commented Sep 19, 2026 •

Copy link
Copy Markdown
Member Author

@Saransh-cpp if you wanna give it a day for the integration testing CI to pick it up that would be great https://github.com/scikit-hep/integration-tests
Just to make sure this didn't break anything downstreeam

And it has to be a minor version bump I guess because with this PR previously invalid vectors will now raise for users.

@ikrommyd
ikrommyd deleted the patch branch September 19, 2026 11:31
@Saransh-cpp

Copy link
Copy Markdown
Member

Sounds good, thanks for the heads up!

@eduardo-rodrigues

Copy link
Copy Markdown
Member

Hello @Saransh-cpp, since you pinged me: apologies for the slow reaction but I was largely offline last week. Yes, the different comment you triggered on above makes sense. All fine by me.

As for this massive PR, I did not review it per se but read a bit in diagonal, and read the threads. Great and important work, @ikrommyd 👍!

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.

4 participants