unxt 0.1.dev463__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of unxt might be problematic. Click here for more details.

Files changed (52) hide show
  1. unxt/__init__.py +79 -0
  2. unxt/_interop/__init__.py +18 -0
  3. unxt/_interop/optional_deps.py +13 -0
  4. unxt/_interop/unxt_interop_astropy/__init__.py +5 -0
  5. unxt/_interop/unxt_interop_astropy/custom_types.py +12 -0
  6. unxt/_interop/unxt_interop_astropy/dimensions.py +53 -0
  7. unxt/_interop/unxt_interop_astropy/quantity.py +308 -0
  8. unxt/_interop/unxt_interop_astropy/units.py +111 -0
  9. unxt/_interop/unxt_interop_gala/__init__.py +5 -0
  10. unxt/_interop/unxt_interop_gala/unitsystems.py +108 -0
  11. unxt/_interop/unxt_interop_mpl/__init__.py +81 -0
  12. unxt/_src/__init__.py +6 -0
  13. unxt/_src/dimensions.py +400 -0
  14. unxt/_src/experimental.py +234 -0
  15. unxt/_src/quantity/__init__.py +24 -0
  16. unxt/_src/quantity/angle.py +58 -0
  17. unxt/_src/quantity/base.py +1160 -0
  18. unxt/_src/quantity/base_angle.py +89 -0
  19. unxt/_src/quantity/base_parametric.py +290 -0
  20. unxt/_src/quantity/flag.py +137 -0
  21. unxt/_src/quantity/mixins.py +238 -0
  22. unxt/_src/quantity/quantity.py +123 -0
  23. unxt/_src/quantity/register_api.py +383 -0
  24. unxt/_src/quantity/register_conversions.py +95 -0
  25. unxt/_src/quantity/register_dispatches.py +246 -0
  26. unxt/_src/quantity/register_primitives.py +5075 -0
  27. unxt/_src/quantity/static_quantity.py +90 -0
  28. unxt/_src/quantity/unchecked.py +45 -0
  29. unxt/_src/quantity/value.py +289 -0
  30. unxt/_src/units.py +103 -0
  31. unxt/_src/unitsystems/__init__.py +40 -0
  32. unxt/_src/unitsystems/base.py +258 -0
  33. unxt/_src/unitsystems/builtin.py +190 -0
  34. unxt/_src/unitsystems/builtin_dimensions.py +21 -0
  35. unxt/_src/unitsystems/compare.py +13 -0
  36. unxt/_src/unitsystems/core.py +307 -0
  37. unxt/_src/unitsystems/flags.py +80 -0
  38. unxt/_src/unitsystems/realizations.py +73 -0
  39. unxt/_src/unitsystems/utils.py +95 -0
  40. unxt/_src/utils.py +112 -0
  41. unxt/_version.py +34 -0
  42. unxt/_version.pyi +2 -0
  43. unxt/dims.py +19 -0
  44. unxt/py.typed +0 -0
  45. unxt/quantity.py +172 -0
  46. unxt/setup_package.py +60 -0
  47. unxt/units.py +19 -0
  48. unxt/unitsystems.py +67 -0
  49. unxt-0.1.dev463.dist-info/METADATA +533 -0
  50. unxt-0.1.dev463.dist-info/RECORD +52 -0
  51. unxt-0.1.dev463.dist-info/WHEEL +4 -0
  52. unxt-0.1.dev463.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,90 @@
1
+ """Static Quantity Class."""
2
+
3
+ # pylint: disable=import-error, no-member, unsubscriptable-object
4
+ # b/c it doesn't understand dataclass fields
5
+
6
+ __all__ = ("StaticQuantity",)
7
+
8
+ from typing import Any, final
9
+
10
+ import equinox as eqx
11
+ import wadler_lindig as wl
12
+ from numpy.typing import NDArray
13
+ from plum import add_promotion_rule, parametric
14
+
15
+ from .base_parametric import AbstractParametricQuantity
16
+ from .quantity import Quantity
17
+ from .unchecked import BareQuantity
18
+ from .value import StaticValue
19
+ from unxt.units import AbstractUnit, unit as parse_unit
20
+
21
+
22
+ @final
23
+ @parametric
24
+ class StaticQuantity(AbstractParametricQuantity):
25
+ """Static quantities with associated units.
26
+
27
+ This class is parametrized by the dimensions of the units, just like
28
+ {class}`~unxt.quantity.Quantity`, but its value is always stored as a static
29
+ NumPy array. It accepts Python scalars and array-like inputs that can be
30
+ converted to NumPy arrays, and it rejects JAX arrays.
31
+
32
+ Examples
33
+ --------
34
+ >>> import numpy as np
35
+ >>> import unxt as u
36
+
37
+ Basic construction:
38
+
39
+ >>> q = u.StaticQuantity(np.array([1.0, 2.0]), "m")
40
+ >>> q
41
+ StaticQuantity(array([1., 2.]), unit='m')
42
+
43
+ Values are static and hashable:
44
+
45
+ >>> isinstance(hash(q), int)
46
+ True
47
+
48
+ JAX arrays are rejected:
49
+
50
+ >>> import jax.numpy as jnp
51
+ >>> try:
52
+ ... u.StaticQuantity(jnp.array([1.0, 2.0]), "m")
53
+ ... except TypeError as e:
54
+ ... print(e)
55
+ StaticQuantity does not accept JAX arrays. Use Quantity for traced values.
56
+
57
+ The Wadler-Lindig representation hides the internal static wrapper:
58
+
59
+ >>> import wadler_lindig as wl
60
+ >>> wl.pprint(q, short_arrays=False)
61
+ StaticQuantity(array([1., 2.]), unit='m')
62
+
63
+ """
64
+
65
+ value: StaticValue = eqx.field( # type: ignore[assignment]
66
+ static=True, converter=StaticValue.from_
67
+ )
68
+ """The static value of the `AbstractQuantity`."""
69
+
70
+ unit: AbstractUnit = eqx.field(static=True, converter=parse_unit)
71
+ """The unit associated with this value."""
72
+
73
+ def __hash__(self) -> int:
74
+ """Return the hash of the quantity."""
75
+ return hash((self.value, self.unit))
76
+
77
+ def __eq__(self, other: Any, /) -> bool | NDArray[bool]: # type: ignore[override]
78
+ """Return structural equality for static quantities."""
79
+ if isinstance(other, StaticQuantity):
80
+ return self.unit == other.unit and self.value == other.value
81
+ return super().__eq__(other)
82
+
83
+ def __pdoc__(self, *, show_wrapper: bool = False, **kwargs: Any) -> wl.AbstractDoc:
84
+ """Return the Wadler-Lindig representation of this class."""
85
+ return super().__pdoc__(show_wrapper=False, **kwargs)
86
+
87
+
88
+ add_promotion_rule(StaticQuantity, StaticQuantity, StaticQuantity)
89
+ add_promotion_rule(StaticQuantity, Quantity, Quantity)
90
+ add_promotion_rule(StaticQuantity, BareQuantity, BareQuantity)
@@ -0,0 +1,45 @@
1
+ # pylint: disable=import-error, no-member, unsubscriptable-object
2
+ # b/c it doesn't understand dataclass fields
3
+
4
+ __all__ = ("BareQuantity",)
5
+
6
+ from typing import Any
7
+
8
+ import equinox as eqx
9
+ from jaxtyping import Array, Shaped
10
+ from plum import add_promotion_rule
11
+
12
+ from .base import AbstractQuantity
13
+ from .quantity import Quantity
14
+ from .value import StaticValue, convert_to_quantity_value
15
+ from unxt.units import AbstractUnit, unit as parse_unit
16
+
17
+
18
+ class BareQuantity(AbstractQuantity):
19
+ """A fast implementation of the Quantity class.
20
+
21
+ This class is not parametrized by its dimensionality.
22
+ """
23
+
24
+ value: Shaped[Array | StaticValue, "*shape"] = eqx.field(
25
+ converter=convert_to_quantity_value
26
+ )
27
+ """The value of the `AbstractQuantity`."""
28
+
29
+ unit: AbstractUnit = eqx.field(static=True, converter=parse_unit)
30
+ """The unit associated with this value."""
31
+
32
+ def __class_getitem__(cls: "type[BareQuantity]", item: Any) -> "type[BareQuantity]":
33
+ """No-op support for `BareQuantity[...]` syntax.
34
+
35
+ This method is called when the class is subscripted, e.g.:
36
+
37
+ >>> from unxt.quantity import BareQuantity
38
+ >>> BareQuantity["length"]
39
+ <class 'unxt...quantity...BareQuantity'>
40
+
41
+ """
42
+ return cls
43
+
44
+
45
+ add_promotion_rule(BareQuantity, Quantity, Quantity)
@@ -0,0 +1,289 @@
1
+ """Conversion to quantity value types."""
2
+
3
+ __all__ = ("StaticValue", "convert_to_quantity_value")
4
+
5
+ import operator
6
+ import warnings
7
+ from typing import Any, final
8
+ from typing_extensions import override
9
+
10
+ import jax
11
+ import numpy as np
12
+ import quax
13
+ import wadler_lindig as wl
14
+ from jaxtyping import Array, ArrayLike
15
+ from numpy.typing import NDArray
16
+ from plum import dispatch
17
+
18
+ import quaxed.numpy as jnp
19
+
20
+
21
+ @final
22
+ class StaticValue:
23
+ """Immutable static value wrapper for `StaticQuantity`.
24
+
25
+ This stores a read-only NumPy array and is used to keep the value static
26
+ while avoiding Equinox's static-array warnings. Arithmetic operations
27
+ degrade to the wrapped array unless both operands are `StaticValue`, in
28
+ which case a `StaticValue` is returned.
29
+
30
+ Note that since the array is immutable, hashing is supported.
31
+ The hash is computed from the array's dtype, shape, and bytes.
32
+
33
+ """
34
+
35
+ __slots__ = ("_array",)
36
+
37
+ def __init__(self, array: np.ndarray, /) -> None:
38
+ value = np.asarray(array).copy()
39
+ value.setflags(write=False)
40
+ self._array = value
41
+
42
+ @property
43
+ def array(self) -> np.ndarray:
44
+ """Return the contained NumPy array."""
45
+ return self._array
46
+
47
+ @property
48
+ def _jnparray(self) -> Array:
49
+ """Return the contained array as a JAX array."""
50
+ return jnp.asarray(self._array)
51
+
52
+ # Constructor
53
+ @classmethod
54
+ @dispatch.abstract
55
+ def from_(cls: type["StaticValue"], *args: Any, **kwargs: Any) -> "StaticValue":
56
+ """Create a `StaticValue` from given arguments."""
57
+ raise NotImplementedError # pragma: no cover
58
+
59
+ # ==================================================
60
+ # NumPy API
61
+
62
+ def __array__(self, dtype: Any = None) -> np.ndarray:
63
+ if dtype is None:
64
+ return np.asarray(self._array)
65
+ return np.asarray(self._array, dtype=dtype)
66
+
67
+ # ==================================================
68
+ # Wadler-Lindig API
69
+
70
+ def __pdoc__(self, *, show_wrapper: bool = True, **kwargs: Any) -> wl.AbstractDoc:
71
+ """Return the Wadler-Lindig representation of this class."""
72
+ if not show_wrapper:
73
+ return wl.pdoc(self._array, **kwargs)
74
+ return (
75
+ wl.TextDoc("StaticValue(")
76
+ + wl.pdoc(self._array, **kwargs)
77
+ + wl.TextDoc(")")
78
+ )
79
+
80
+ # ==================================================
81
+ # Python container API
82
+
83
+ def __len__(self) -> int:
84
+ return len(self._array)
85
+
86
+ def __iter__(self) -> Any:
87
+ return iter(self._array)
88
+
89
+ def __getitem__(self, key: Any) -> Any:
90
+ return self._array[key]
91
+
92
+ def __getattr__(self, name: str) -> Any:
93
+ return getattr(jnp.asarray(self._array), name)
94
+
95
+ def __repr__(self) -> str:
96
+ return wl.pformat(self, short_arrays=False, max_width=80)
97
+
98
+ @override
99
+ def __eq__(self, other: object, /) -> bool | NDArray[np.bool_]: # type: ignore[override]
100
+ if isinstance(other, StaticValue):
101
+ return bool(np.array_equal(self._array, other._array))
102
+ if isinstance(other, (np.ndarray, Array, list, tuple)):
103
+ return self._array == other
104
+ return NotImplemented
105
+
106
+ def __ne__(self, other: object, /) -> bool | NDArray[np.bool_]: # type: ignore[override]
107
+ if isinstance(other, StaticValue):
108
+ return not bool(np.array_equal(self._array, other._array))
109
+ if isinstance(other, (np.ndarray, Array, list, tuple)):
110
+ return self._array != other
111
+ return NotImplemented
112
+
113
+ def __lt__(self, other: Any, /) -> NDArray[np.bool_]:
114
+ if isinstance(other, StaticValue):
115
+ other = other._array
116
+ return self._array < other
117
+
118
+ def __le__(self, other: Any, /) -> NDArray[np.bool_]:
119
+ if isinstance(other, StaticValue):
120
+ other = other._array
121
+ return self._array <= other
122
+
123
+ def __gt__(self, other: Any, /) -> NDArray[np.bool_]:
124
+ if isinstance(other, StaticValue):
125
+ other = other._array
126
+ return self._array > other
127
+
128
+ def __ge__(self, other: Any, /) -> NDArray[np.bool_]:
129
+ if isinstance(other, StaticValue):
130
+ other = other._array
131
+ return self._array >= other
132
+
133
+ def __hash__(self) -> int:
134
+ return hash((self._array.dtype.str, self._array.shape, self._array.tobytes()))
135
+
136
+ def _binary_op(self, other: Any, op: Any) -> Any:
137
+ if isinstance(other, StaticValue):
138
+ result = op(self._jnparray, jnp.asarray(other.array))
139
+ return StaticValue(np.asarray(result))
140
+ return op(self._jnparray, other)
141
+
142
+ def _rbinary_op(self, other: Any, op: Any) -> Any:
143
+ if isinstance(other, StaticValue):
144
+ result = op(jnp.asarray(other.array), self._jnparray)
145
+ return StaticValue(np.asarray(result))
146
+ return op(other, self._jnparray)
147
+
148
+ def __add__(self, other: Any) -> Any:
149
+ return self._binary_op(other, operator.add)
150
+
151
+ def __radd__(self, other: Any) -> Any:
152
+ return self._rbinary_op(other, operator.add)
153
+
154
+ def __sub__(self, other: Any) -> Any:
155
+ return self._binary_op(other, operator.sub)
156
+
157
+ def __rsub__(self, other: Any) -> Any:
158
+ return self._rbinary_op(other, operator.sub)
159
+
160
+ def __mul__(self, other: Any) -> Any:
161
+ return self._binary_op(other, operator.mul)
162
+
163
+ def __rmul__(self, other: Any) -> Any:
164
+ return self._rbinary_op(other, operator.mul)
165
+
166
+ def __truediv__(self, other: Any) -> Any:
167
+ return self._binary_op(other, operator.truediv)
168
+
169
+ def __rtruediv__(self, other: Any) -> Any:
170
+ return self._rbinary_op(other, operator.truediv)
171
+
172
+ def __floordiv__(self, other: Any) -> Any:
173
+ return self._binary_op(other, operator.floordiv)
174
+
175
+ def __rfloordiv__(self, other: Any) -> Any:
176
+ return self._rbinary_op(other, operator.floordiv)
177
+
178
+ def __pow__(self, other: Any) -> Any:
179
+ return self._binary_op(other, operator.pow)
180
+
181
+ def __rpow__(self, other: Any) -> Any:
182
+ return self._rbinary_op(other, operator.pow)
183
+
184
+ def __mod__(self, other: Any) -> Any:
185
+ return self._binary_op(other, operator.mod)
186
+
187
+ def __rmod__(self, other: Any) -> Any:
188
+ return self._rbinary_op(other, operator.mod)
189
+
190
+ def __matmul__(self, other: Any) -> Any:
191
+ return self._binary_op(other, operator.matmul)
192
+
193
+ def __rmatmul__(self, other: Any) -> Any:
194
+ return self._rbinary_op(other, operator.matmul)
195
+
196
+ def __neg__(self) -> Any:
197
+ return -self._jnparray
198
+
199
+ def __pos__(self) -> Any:
200
+ return +self._jnparray
201
+
202
+ def __abs__(self) -> Any:
203
+ return abs(self._jnparray)
204
+
205
+
206
+ @StaticValue.from_.dispatch
207
+ def from_(cls: type[StaticValue], value: object, /) -> StaticValue:
208
+ """Convert a value for `StaticQuantity`."""
209
+ return cls(np.asarray(value))
210
+
211
+
212
+ @StaticValue.from_.dispatch
213
+ def from_(cls: type[StaticValue], value: StaticValue, /) -> StaticValue:
214
+ """Convert a value for `StaticQuantity`."""
215
+ return value
216
+
217
+
218
+ @StaticValue.from_.dispatch
219
+ def from_(cls: type[StaticValue], value: jax.Array | jax.core.Tracer, /) -> StaticValue:
220
+ """Reject JAX arrays for `StaticQuantity`."""
221
+ msg = "StaticQuantity does not accept JAX arrays. Use Quantity for traced values."
222
+ raise TypeError(msg)
223
+
224
+
225
+ # ==================================================================
226
+ # Converters for quantity value field
227
+
228
+
229
+ @dispatch.abstract
230
+ def convert_to_quantity_value(obj: Any, /) -> Any:
231
+ """Convert for the value field of an `AbstractQuantity` subclass."""
232
+ raise NotImplementedError # pragma: no cover
233
+
234
+
235
+ @dispatch
236
+ def convert_to_quantity_value(obj: StaticValue, /) -> StaticValue:
237
+ """Allow static values in `Quantity`-like classes."""
238
+ return obj
239
+
240
+
241
+ @dispatch
242
+ def convert_to_quantity_value(obj: quax.ArrayValue, /) -> Any:
243
+ """Convert a `quax.ArrayValue` for the value field.
244
+
245
+ >>> import warnings
246
+ >>> import jax
247
+ >>> import jax.numpy as jnp
248
+ >>> from jaxtyping import Array
249
+ >>> from quax import ArrayValue
250
+
251
+ >>> class MyArray(ArrayValue):
252
+ ... value: Array
253
+ ...
254
+ ... def aval(self):
255
+ ... return jax.core.ShapedArray(self.value.shape, self.value.dtype)
256
+ ...
257
+ ... def materialise(self):
258
+ ... return self.value
259
+
260
+ >>> x = MyArray(jnp.array([1, 2, 3]))
261
+ >>> with warnings.catch_warnings(record=True, action="always") as w:
262
+ ... y = convert_to_quantity_value(x)
263
+ >>> y
264
+ MyArray(value=i32[3])
265
+ >>> print(f"Warning caught: {w[-1].message}")
266
+ Warning caught: 'quax.ArrayValue' subclass 'MyArray' ...
267
+
268
+ """
269
+ warnings.warn(
270
+ f"'quax.ArrayValue' subclass {type(obj).__name__!r} does not have a registered "
271
+ "converter. Returning the object as is.",
272
+ category=UserWarning,
273
+ stacklevel=2,
274
+ )
275
+ return obj
276
+
277
+
278
+ @dispatch
279
+ def convert_to_quantity_value(obj: ArrayLike | list[Any] | tuple[Any, ...], /) -> Array:
280
+ """Convert an array-like object to a `jax.numpy.ndarray`.
281
+
282
+ >>> import jax.numpy as jnp
283
+ >>> from unxt.quantity import convert_to_quantity_value
284
+
285
+ >>> convert_to_quantity_value([1, 2, 3])
286
+ Array([1, 2, 3], dtype=int32)
287
+
288
+ """
289
+ return jnp.asarray(obj)
unxt/_src/units.py ADDED
@@ -0,0 +1,103 @@
1
+ """Units objects in JAX.
2
+
3
+ Copyright (c) 2023 Galactic Dynamics. All rights reserved.
4
+ """
5
+
6
+ __all__ = ("unit", "unit_of", "AbstractUnit")
7
+
8
+ from typing import Any, TypeAlias
9
+
10
+ import astropy.units as apyu
11
+ from plum import dispatch
12
+
13
+ import unxt_api as uapi
14
+ from unxt.dims import AbstractDimension
15
+
16
+ AbstractUnit: TypeAlias = apyu.Unit | apyu.UnitBase | apyu.CompositeUnit
17
+
18
+
19
+ # ===================================================================
20
+ # Construct units
21
+
22
+
23
+ @dispatch
24
+ def unit(obj: AbstractUnit, /) -> AbstractUnit:
25
+ """Construct the units from a units object.
26
+
27
+ Examples
28
+ --------
29
+ >>> import unxt as u
30
+ >>> m = u.unit("m")
31
+
32
+ >>> u.unit(m) is m
33
+ True
34
+
35
+ """
36
+ return obj
37
+
38
+
39
+ @dispatch
40
+ def unit(obj: str, /) -> AbstractUnit:
41
+ """Construct units from a string.
42
+
43
+ Examples
44
+ --------
45
+ >>> import unxt as u
46
+ >>> m = u.unit("m")
47
+ >>> m
48
+ Unit("m")
49
+
50
+ """
51
+ return apyu.Unit(obj)
52
+
53
+
54
+ # ===================================================================
55
+ # Get units
56
+
57
+
58
+ @dispatch
59
+ def unit_of(obj: Any, /) -> None:
60
+ """Return the units of an object.
61
+
62
+ Examples
63
+ --------
64
+ >>> import unxt as u
65
+ >>> print(u.unit_of(1))
66
+ None
67
+
68
+ """
69
+ return None # noqa: RET501
70
+
71
+
72
+ @dispatch
73
+ def unit_of(obj: AbstractUnit, /) -> AbstractUnit:
74
+ """Return the units of an unit.
75
+
76
+ Examples
77
+ --------
78
+ >>> import unxt as u
79
+ >>> m = u.unit("m")
80
+
81
+ >>> u.unit_of(m)
82
+ Unit("m")
83
+
84
+ """
85
+ return obj
86
+
87
+
88
+ # ===================================================================
89
+ # Get dimensions
90
+
91
+
92
+ @dispatch
93
+ def dimension_of(obj: AbstractUnit, /) -> AbstractDimension:
94
+ """Return the dimensions of the given units.
95
+
96
+ Examples
97
+ --------
98
+ >>> import unxt as u
99
+ >>> u.dimension_of(u.unit("km"))
100
+ PhysicalType('length')
101
+
102
+ """
103
+ return uapi.dimension(obj.physical_type)
@@ -0,0 +1,40 @@
1
+ """Tools for representing systems of units using ``astropy.units``."""
2
+ # pylint: disable=duplicate-code
3
+
4
+ __all__ = (
5
+ "unitsystem",
6
+ "unitsystem_of",
7
+ # classes
8
+ "AbstractUnitSystem",
9
+ "UNITSYSTEMS_REGISTRY",
10
+ "DimensionlessUnitSystem",
11
+ "LTMAUnitSystem",
12
+ # unit system instance
13
+ "galactic",
14
+ "dimensionless",
15
+ "solarsystem",
16
+ "si",
17
+ "cgs",
18
+ # unit system alias
19
+ "NAMED_UNIT_SYSTEMS",
20
+ # unit system flags
21
+ "AbstractUSysFlag",
22
+ "StandardUSysFlag",
23
+ "DynamicalSimUSysFlag",
24
+ # functions
25
+ "equivalent",
26
+ )
27
+
28
+ from .base import UNITSYSTEMS_REGISTRY, AbstractUnitSystem
29
+ from .builtin import DimensionlessUnitSystem, LTMAUnitSystem
30
+ from .compare import equivalent
31
+ from .core import unitsystem, unitsystem_of
32
+ from .flags import AbstractUSysFlag, DynamicalSimUSysFlag, StandardUSysFlag
33
+ from .realizations import (
34
+ NAMED_UNIT_SYSTEMS,
35
+ cgs,
36
+ dimensionless,
37
+ galactic,
38
+ si,
39
+ solarsystem,
40
+ )