Repository navigation
feat: validate coordinate names in one place, for every backend - #659
Conversation
Saransh-cpp
left a comment
There was a problem hiding this comment.
Good catch, @ikrommyd! Thanks for working on this!!
Please see my comments below:
|
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 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 |
|
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: 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.) and (3.) solves this issue entirely I think, (2.) is in addition to make people aware of when they invoke the 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. 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? |
|
Sounds good to me, @pfackeldey! I'll be happy to review the PR in awkward and subsequently propagate the new immutability way to vector 🙂 |
|
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 |
|
Hi @eduardo-rodrigues, it's not trivial to catch when you do |
|
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. |
|
Did a full write-up here of the issue: #660 |
|
Linking my proposed solution here as well: #660 (comment) |
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
|
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. |
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.
|
@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
left a comment
There was a problem hiding this comment.
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?
| 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__()`. |
There was a problem hiding this comment.
Quick comment: should we move awkward's pin to 2.8.11 or is the current one okay?
There was a problem hiding this comment.
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.
|
@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. |
|
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 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 And it has to be a minor version bump I guess because with this PR previously invalid vectors will now raise for users. |
|
Sounds good, thanks for the heads up! |
|
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 👍! |
🤖 AI text below 🤖
Description
Rebased on
mainand 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(), theVectorObject*Dconstructors,array(), the six__array_finalize__,_check_names, theVectorSympy*Dconstructors, and the Numbavector.obj— and they disagreed. All of these work onmainand silently build the wrong vector:The fix
_methods._check_coordinate_namesis 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 bothvector.obj(deduces both) andMomentumNumpy4D(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, needsawkward>=2.8.11; older versions skip it), so what never went through a vector constructor is caught too:Subclassed behaviors (coffea's) inherit it. Cost is ~0.5 µs per array creation against ~130 µs for one
v.pton 100k entries;_check_coordinate_namesislru_cached on the field-name tuple.Intended breaks
charge,btag) are unaffected.VectorObject2D(px=1, py=2)used to return a non-momentum vector whilevector.obj(px=1, py=2)returnsMomentumObject2D. Momentum classes still take generic names.del jets["mass"], or a column-projected form that kept__record__but dropped coordinates. Onmainthose gave a half-broken vector that failed at first coordinate access.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
$ pre-commit run --all-filesor$ nox -s lint)$ pytestor$ nox -s tests)$ cd docs; make clean; make htmlor$ nox -s docs)$ pytest --doctest-plus src/vector/or$ nox -s doctests)