feat: validate coordinate names in one place, for every backend#659
feat: validate coordinate names in one place, for every backend#659ikrommyd wants to merge 1 commit into
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:
| @@ -3211,7 +3211,7 @@ def obj(**coordinates: float) -> VectorObject: | |||
| if "E" in coordinates: | |||
There was a problem hiding this comment.
Should this also have the and "t" not in generic_coordinates condition?
There was a problem hiding this comment.
Isn't this unnecessary? This is the first if condition where generic_coordinates might be populated with t. So and "t" not in generic_coordinates is a useless check no? Same goes for tau in your other comment below.
There was a problem hiding this comment.
I guess we can add it for "visual OCD reasons" but it's a check that's always going to be false right?
| @@ -3220,7 +3220,7 @@ def obj(**coordinates: float) -> VectorObject: | |||
| if "M" in coordinates: | |||
| # Validate coordinates using dimension-guard pattern (same as awkward _check_names) | ||
| _validate_numpy_coordinates(names) |
There was a problem hiding this comment.
vector.array is just a wrapper around individual constructors of Vector/MomentumNumpy*D, which can be used to construct vectors (unlike the Awkward backend). Hence, it would be better if we move this check to the __array_finalize__ method of each class:
vector/src/vector/backends/numpy.py
Line 1168 in e374915
There was a problem hiding this comment.
I vaguely remember that I had some issue with __array_finalize__ regarding when is it being ran when I was making these edits but I will take a look again.
There was a problem hiding this comment.
I've moved calling this function inside the __array_finalize__ of the 2d, 3d, and 4d classes. The tests pass so I'll need to play with it a bit more in a terminal in case there's something I'm not remembering here.
There was a problem hiding this comment.
Oh..now I think I remember. Look at this
vector/src/vector/backends/numpy.py
Lines 2160 to 2169 in 776f405
It makes a decision whether it is momentum or not and which class constructor to use based on the field names. That's why I think I wanted it in the
vector.array implementation and not in the finalize. I think it's best to validate the fields first and then decide which constructor to use. I think it's best to move it back to where it was. I have reverted the change.
There was a problem hiding this comment.
Actually, it needs to be in both places because classes like MomentumNumpy4D can be directly instantiated. And therefore this works fine and it shouldn't.
In [1]: import vector
...:
...: vec = vector.MomentumNumpy4D([(1.1, 2.1, 3.1, 4.1, 4.1), (1.2, 2.2, 3.2, 4.2, 4.2), (1.3, 2.3, 3.3, 4.3, 4.3), (1.4, 2.4, 3.4, 4.4, 4.4), (1.5, 2.5, 3.5, 4.5, 4.5)],
...:
...: dtype=[('x', float), ('y', float), ('z', float), ('e', float), ('mass', float)])
...:
...: vec
Out[1]:
MomentumNumpy4D([(1.1, 2.1, 3.1, 4.1, 4.1), (1.2, 2.2, 3.2, 4.2, 4.2),
(1.3, 2.3, 3.3, 4.3, 4.3), (1.4, 2.4, 3.4, 4.4, 4.4),
(1.5, 2.5, 3.5, 4.5, 4.5)],
dtype=[('x', '<f8'), ('y', '<f8'), ('z', '<f8'), ('t', '<f8'), ('tau', '<f8')])
So I think we need to validate in vector.array and in __array_finalize__ too because both are valid instantiation methods.
There was a problem hiding this comment.
Given that these tests are related to constructing vectors, we should move them to the appropriate test backend files. The object ones can go in:
vector/tests/backends/test_object.py
Line 14 in e374915
The NumPy ones (we don't have much constructor tests for NumPy at the moment, so this is perfect 😅):
vector/tests/backends/test_numpy.py
Line 85 in e374915
The Awkward ones (same as NumPy):
vector/tests/backends/test_awkward.py
Line 161 in e374915
There was a problem hiding this comment.
Sure. I just thought that maybe it should be a new test because it's like 1K lines but yeah I can move them under their respective backend tests.
There was a problem hiding this comment.
Ah, I see, it might make sense to create a new function under test_issues.py for this then 🤔
There was a problem hiding this comment.
Any final suggestion here? To me, putting 1K lines under test_issues.py is not a great idea. My understanding is that this file is for small individual issues. The only refactoring that I think may be good here is just splitting this 1K file into multiple files per backend. Any takes?
There was a problem hiding this comment.
Having a different file sounds good, @ikrommyd. But, can we rename it to test_issue_xxx.py with either the issue/PR number such that it is easy to spot and stays somewhat aligned with the issue_xxx tests?
There was a problem hiding this comment.
I've renamed to tests/test_pr_659.py because there is no exact issue, only the long discussion. It's best to have the pr number I think as this partially solves the discussion (the other half done by the validation hook Peter added in awkward).
|
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 |
|
|
||
| Raises TypeError if duplicate or conflicting coordinates are detected. | ||
| """ | ||
| complaint1 = "duplicate coordinates (through momentum-aliases): " + ", ".join( |
There was a problem hiding this comment.
It's a picky comment but it's a bit sub-optimal that we duplicate these "complaint strings" in several submodules. For better maintainability it's probably best to move them to a trival submodule and import the strings in the several places, such as here but also in awkward_constructions.py for example.
There was a problem hiding this comment.
is there a suggestion regarding where to put these complaint strings?
There was a problem hiding this comment.
I think we can put it in _methods.py as all the common methods/base classes and protocols live there. This exact string is used at several other places as well, so those can be refactored too (perhaps in another PR if this PR gets too big).
There was a problem hiding this comment.
Honestly, all "types" of vector constructors and backend literally validate the coordinates in different places each with their own implementations. I'd rather not do this here and just a minimal working case and do a whole refactoring of validation in a separate PR. obj numpy, awkward, numba, sympy vectors all validate in different places with slightly different mechanisms each. We need a general refactoring of the coordinate presence validation that is shared amongst all of those types of vectors.
|
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.72% 88.85% +1.13%
==========================================
Files 96 96
Lines 11194 10769 -425
==========================================
- Hits 9820 9569 -251
+ Misses 1374 1200 -174 ☔ 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. |
🤖 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)