Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add constructors for VectorObject2D and MomentumObject2D #89

Merged
merged 15 commits into from
Oct 18, 2022
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Pull request template [#271][]
- docs: add a developer guide [#233][]
- ci: Test notebooks on PRs [#272][]
- feat: add constructors for `VectorObject2D` and `MomentumObject2D` [#89][]

[#256]: https://github.com/scikit-hep/vector/pull/256
[#254]: https://github.com/scikit-hep/vector/pull/254
Expand All @@ -27,6 +28,7 @@
[#271]: https://github.com/scikit-hep/vector/pull/271
[#233]: https://github.com/scikit-hep/vector/pull/233
[#272]: https://github.com/scikit-hep/vector/pull/272
[#89]: https://github.com/scikit-hep/vector/pull/89

## Version 0.10

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ messages_control.disable = [
"useless-option-value",
"cast_python_value",
"unknown-option-value",
"no-else-raise",
"unidiomatic-typecheck",
]

[tool.mypy]
Expand All @@ -185,7 +187,6 @@ python_version = "3.8"
strict = true
warn_return_any = false
show_error_codes = true
warn_unreachable = true
enable_error_code = [
"ignore-without-code",
"truthy-bool",
Expand Down
46 changes: 42 additions & 4 deletions src/vector/_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import typing
from contextlib import suppress

import vector
from vector._typeutils import (
BoolCollection,
ScalarCollection,
Expand Down Expand Up @@ -144,7 +145,9 @@ class VectorProtocol:
vectors without momentum-synonyms.
"""

lib: Module
@property
def lib(self) -> Module:
...

def _wrap_result(
self,
Expand Down Expand Up @@ -1348,6 +1351,40 @@ def transverse_mass2(self) -> ScalarCollection:


class Vector(VectorProtocol):
@typing.overload
def __new__(cls, *, x: float, y: float) -> vector.VectorObject2D:
...

@typing.overload
def __new__(cls, *, rho: float, phi: float) -> vector.VectorObject2D:
...

@typing.overload
def __new__(cls, *, px: float, py: float) -> vector.MomentumObject2D:
...

@typing.overload
def __new__(cls, *, x: float, py: float) -> vector.MomentumObject2D:
...

@typing.overload
def __new__(cls, *, px: float, y: float) -> vector.MomentumObject2D:
...

@typing.overload
def __new__(cls, *, pt: float, phi: float) -> vector.MomentumObject2D:
...

@typing.overload
def __new__(cls, __azumthal: Azimuthal) -> Vector:
...

def __new__(cls, *args: Azimuthal, **kwargs: float) -> Vector:
if cls is not Vector:
return super().__new__(cls)

return vector.obj(*args, **kwargs)

def to_xy(self) -> VectorProtocolPlanar:
from vector._compute import planar

Expand Down Expand Up @@ -2671,10 +2708,11 @@ def _lib_of(*objects: VectorProtocol) -> Module: # NumPy-like module
for obj in objects:
if isinstance(obj, Vector):
if lib is None:
lib = obj.lib
elif lib is not obj.lib:
# Not sure why mypy complains about member not being assignable here
lib = obj.lib # type: ignore[misc]
elif lib is not obj.lib: # type: ignore[misc]
raise TypeError(
f"cannot use {lib} and {obj.lib} in the same calculation"
f"cannot use {lib} and {obj.lib} in the same calculation" # type: ignore[misc]
)

assert lib is not None
Expand Down
12 changes: 6 additions & 6 deletions src/vector/backends/awkward.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,12 +576,12 @@ def elements(self) -> tuple[ArrayOrRecord]:

def _class_to_name(cls: type[VectorProtocol]) -> str:
if issubclass(cls, Momentum):
if issubclass(cls, Vector2D): # type: ignore[unreachable]
return "Momentum2D" # type: ignore[unreachable]
elif issubclass(cls, Vector3D): # type: ignore[unreachable]
return "Momentum3D" # type: ignore[unreachable]
elif issubclass(cls, Vector4D): # type: ignore[unreachable]
return "Momentum4D" # type: ignore[unreachable]
if issubclass(cls, Vector2D):
return "Momentum2D"
elif issubclass(cls, Vector3D):
return "Momentum3D"
elif issubclass(cls, Vector4D):
return "Momentum4D"
else:
if issubclass(cls, Vector2D):
return "Vector2D"
Expand Down
1 change: 0 additions & 1 deletion src/vector/backends/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1899,7 +1899,6 @@ def array(*args: typing.Any, **kwargs: typing.Any) -> VectorNumpy:
else:
cls = MomentumNumpy2D if is_momentum else VectorNumpy2D

# VectorNumpy has no constructor, so mypy flags this line
return cls(*args, **kwargs)


Expand Down
123 changes: 96 additions & 27 deletions src/vector/backends/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,27 @@
# Distributed under the 3-clause BSD license, see accompanying file LICENSE
# or https://github.com/scikit-hep/vector for details.
"""
Defines behaviors for Object vectors. New vectors created with the
Defines behaviors for Object vectors. New vectors created with the respective classes

.. code-block:: python

vector.obj(...)

vector.VectorObject2D(x=..., y=...)

function will have these behaviors built in (and will pass them to any derived
objects).
will have these behaviors built in (and will pass them to any derived objects).

Additionally, the class methods can also be used to construct object type
vectors -
The class methods can also be used to construct object type vectors -

.. code-block:: python

vec = vector.VectorObject2D.from_xy(1, 2)

Additionally, object type vectors can also be constructed using -

.. code-block:: python

vector.obj(...)

function.
"""
from __future__ import annotations

Expand Down Expand Up @@ -56,6 +60,7 @@
_handler_of,
_ltype,
_repr_generic_to_momentum,
_repr_momentum_to_generic,
_ttype,
)
from vector._typeutils import FloatArray
Expand Down Expand Up @@ -106,7 +111,7 @@ def elements(self) -> tuple[float, float]:

Examples:
>>> import vector
>>> v = vector.obj(x=1, y=2)
>>> v = vector.VectorObject2D(x=1, y=2)
>>> az = v.azimuthal
>>> az.elements
(1, 2)
Expand Down Expand Up @@ -135,7 +140,7 @@ def elements(self) -> tuple[float, float]:

Examples:
>>> import vector
>>> v = vector.obj(rho=1, phi=2)
>>> v = vector.VectorObject2D(rho=1, phi=2)
>>> az = v.azimuthal
>>> az.elements
(1, 2)
Expand Down Expand Up @@ -598,12 +603,27 @@ def __array_ufunc__(
class VectorObject2D(VectorObject, Planar, Vector2D):
"""
Two dimensional vector class for the object backend.
Use the class methods -

Examples:
>>> import vector
>>> vec = vector.VectorObject2D(x=1, y=2)
>>> vec.x, vec.y
(1, 2)
>>> vec = vector.VectorObject2D(rho=1, phi=2)
>>> vec.rho, vec.phi
(1, 2)
>>> vec = vector.VectorObject2D(azimuthal=vector.backends.object.AzimuthalObjectXY(1, 2))
>>> vec.x, vec.y
(1, 2)

The following class methods can also be used to
construct 2D object type vectors -

- :meth:`VectorObject2D.from_xy`
- :meth:`VectorObject2D.from_rhophi`

to construct 2D Vector objects.
Additionally, the :func:`vector.obj` function can
also be used to construct 2D object type vectors.

For two dimensional momentum vector objects, see
:class:`vector.backends.object.MomentumObject2D`.
Expand All @@ -625,9 +645,9 @@ def from_xy(cls, x: float, y: float) -> VectorObject2D:
>>> import vector
>>> vec = vector.VectorObject2D.from_xy(1, 2)
>>> vec
vector.obj(x=1, y=2)
VectorObject2D(x=1, y=2)
"""
return cls(AzimuthalObjectXY(x, y))
return cls(azimuthal=AzimuthalObjectXY(x, y))

@classmethod
def from_rhophi(cls, rho: float, phi: float) -> VectorObject2D:
Expand All @@ -641,17 +661,45 @@ def from_rhophi(cls, rho: float, phi: float) -> VectorObject2D:
>>> import vector
>>> vec = vector.VectorObject2D.from_rhophi(1, 2)
>>> vec
vector.obj(rho=1, phi=2)
VectorObject2D(rho=1, phi=2)
"""
return cls(AzimuthalObjectRhoPhi(rho, phi))
return cls(azimuthal=AzimuthalObjectRhoPhi(rho, phi))

def __init__(self, azimuthal: AzimuthalObject) -> None:
self.azimuthal = azimuthal
def __init__(
self, azimuthal: AzimuthalObject | None = None, **kwargs: float
) -> None:

if not _is_type_safe(kwargs):
raise TypeError("a coordinate must be of the type int or float")

for k, v in kwargs.copy().items():
kwargs.pop(k)
kwargs[_repr_momentum_to_generic.get(k, k)] = v

if not kwargs and azimuthal is not None:
self.azimuthal = azimuthal
elif kwargs and azimuthal is None:
if set(kwargs) == {"x", "y"}:
self.azimuthal = AzimuthalObjectXY(kwargs["x"], kwargs["y"])
elif set(kwargs) == {"rho", "phi"}:
self.azimuthal = AzimuthalObjectRhoPhi(kwargs["rho"], kwargs["phi"])
else:
complaint = """unrecognized combination of coordinates, allowed combinations are:\n
x= y=
rho= phi=""".replace(
" ", " "
)
if type(self) == VectorObject2D:
raise TypeError(complaint)
else:
raise TypeError(f"{complaint}\n\nor their momentum equivalents")
else:
raise TypeError("must give Azimuthal if not giving keyword arguments")

def __repr__(self) -> str:
aznames = _coordinate_class_to_names[_aztype(self)]
out = [f"{x}={getattr(self.azimuthal, x)}" for x in aznames]
return "vector.obj(" + ", ".join(out) + ")"
return "VectorObject2D(" + ", ".join(out) + ")"

def __array__(self) -> FloatArray:
from vector.backends.numpy import VectorNumpy2D
Expand Down Expand Up @@ -690,7 +738,7 @@ def _wrap_result(
and issubclass(returns[0], Azimuthal)
):
azcoords = _coord_object_type[returns[0]](result[0], result[1])
return cls.ProjectionClass2D(azcoords)
return cls.ProjectionClass2D(azimuthal=azcoords)

elif (
len(returns) == 2
Expand All @@ -699,7 +747,7 @@ def _wrap_result(
and returns[1] is None
):
azcoords = _coord_object_type[returns[0]](result[0], result[1])
return cls.ProjectionClass2D(azcoords)
return cls.ProjectionClass2D(azimuthal=azcoords)

elif (
len(returns) == 2
Expand Down Expand Up @@ -778,6 +826,21 @@ class MomentumObject2D(PlanarMomentum, VectorObject2D):
"""
Two dimensional momentum vector class for the object backend.

Examples:
>>> import vector
>>> vec = vector.MomentumObject2D(px=1, py=2)
>>> vec.px, vec.py
(1, 2)
>>> vec = vector.MomentumObject2D(pt=1, phi=2)
>>> vec.pt, vec.phi
(1, 2)
>>> vec = vector.MomentumObject2D(azimuthal=vector.backends.object.AzimuthalObjectXY(1, 2))
>>> vec.px, vec.py
(1, 2)

The :func:`vector.obj` function can also be
used to construct 2D momentum object type vectors.

For two dimensional vector objects, see
:class:`vector.backends.object.VectorObject2D`.
"""
Expand All @@ -788,7 +851,7 @@ def __repr__(self) -> str:
for x in aznames:
y = _repr_generic_to_momentum.get(x, x)
out.append(f"{y}={getattr(self.azimuthal, x)}")
return "vector.obj(" + ", ".join(out) + ")"
return "MomentumObject2D(" + ", ".join(out) + ")"

def __array__(self) -> FloatArray:
from vector.backends.numpy import MomentumNumpy2D
Expand Down Expand Up @@ -1011,7 +1074,7 @@ def _wrap_result(
and returns[1] is None
):
azcoords = _coord_object_type[returns[0]](result[0], result[1])
return cls.ProjectionClass2D(azcoords)
return cls.ProjectionClass2D(azimuthal=azcoords)

elif (
len(returns) == 2
Expand Down Expand Up @@ -1587,7 +1650,7 @@ def _wrap_result(
and returns[1] is None
):
azcoords = _coord_object_type[returns[0]](result[0], result[1])
return cls.ProjectionClass2D(azcoords)
return cls.ProjectionClass2D(azimuthal=azcoords)

elif (
len(returns) == 2
Expand Down Expand Up @@ -1822,6 +1885,13 @@ def mass(self, mass: float) -> None:
self.temporal = TemporalObjectTau(mass)


def _is_type_safe(coordinates: dict[str, typing.Any]) -> bool:
for _, value in coordinates.items():
if not issubclass(type(value), numbers.Real) or isinstance(value, bool):
return False
return True


def _gather_coordinates(
planar_class: type[VectorObject2D],
spatial_class: type[VectorObject3D],
Expand Down Expand Up @@ -1875,7 +1945,7 @@ def _gather_coordinates(

if not coordinates:
if azimuthal is not None and longitudinal is None and temporal is None:
return planar_class(azimuthal)
return planar_class(azimuthal=azimuthal)
if azimuthal is not None and longitudinal is not None and temporal is None:
return spatial_class(azimuthal, longitudinal)
if azimuthal is not None and longitudinal is not None and temporal is not None:
Expand Down Expand Up @@ -3090,9 +3160,8 @@ def obj(**coordinates: float) -> VectorObject:
is_momentum = False
generic_coordinates = {}

for _, value in coordinates.items():
if not issubclass(type(value), numbers.Real) or isinstance(value, bool):
raise TypeError("a coordinate must be of the type int or float")
if not _is_type_safe(coordinates):
raise TypeError("a coordinate must be of the type int or float")

if "px" in coordinates:
is_momentum = True
Expand Down
Loading