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,258 @@
1
+ """Tools for representing systems of units using ``astropy.units``."""
2
+
3
+ __all__ = ("UNITSYSTEMS_REGISTRY", "AbstractUnitSystem")
4
+
5
+ from collections.abc import Iterator
6
+ from dataclasses import dataclass
7
+ from types import MappingProxyType
8
+ from typing import ClassVar, get_args, get_type_hints
9
+
10
+ import jax.tree_util as jtu
11
+ from astropy.units import PhysicalType, UnitBase as AstropyUnitBase
12
+ from astropy.units.physical import _physical_unit_mapping
13
+
14
+ from is_annotated import isannotated
15
+
16
+ from .utils import parse_dimlike_name
17
+ from unxt.dims import AbstractDimension, dimension
18
+ from unxt.units import AbstractUnit, unit
19
+
20
+ Unit = AstropyUnitBase
21
+
22
+ _UNITSYSTEMS_REGISTRY: dict[
23
+ tuple[AbstractDimension, ...], type["AbstractUnitSystem"]
24
+ ] = {}
25
+ UNITSYSTEMS_REGISTRY = MappingProxyType(_UNITSYSTEMS_REGISTRY)
26
+
27
+
28
+ # Register AbstractUnitSystem as a static PyTree node
29
+ # This ensures unit systems are treated as constants in JAX transformations
30
+ @jtu.register_static
31
+ @dataclass(frozen=True, slots=True, eq=True)
32
+ class AbstractUnitSystem:
33
+ """Represents a system of units.
34
+
35
+ This class behaves like a dictionary with keys set by physical types (i.e.
36
+ "length", "velocity", "energy", etc.). If a unit for a particular physical
37
+ type is not specified on creation, a composite unit will be created with the
38
+ base units. See the examples below for some demonstrations.
39
+
40
+ Examples
41
+ --------
42
+ If only base units are specified, any physical type specified as a key to
43
+ this object will be composed out of the base units:
44
+
45
+ >>> from unxt import unitsystem
46
+ >>> usys = unitsystem("m", "s", "kg", "radian")
47
+ >>> usys
48
+ unitsystem(m, s, kg, rad)
49
+
50
+ >>> usys["velocity"]
51
+ Unit("m / s")
52
+
53
+ This unit system defines energy:
54
+
55
+ >>> usys = unitsystem("m", "s", "kg", "radian", "erg")
56
+ >>> usys["energy"]
57
+ Unit("erg")
58
+
59
+ This is useful for Galactic dynamics where lengths and times are usually
60
+ given in terms of ``kpc`` and ``Myr``, but velocities are often specified in
61
+ ``km/s``:
62
+
63
+ >>> usys = unitsystem("kpc", "Myr", "Msun", "radian", "km / s")
64
+ >>> usys["velocity"]
65
+ Unit("km / s")
66
+
67
+ Unit systems can be hashed:
68
+
69
+ >>> isinstance(hash(usys), int)
70
+ True
71
+
72
+ And iterated over:
73
+
74
+ >>> [x for x in usys]
75
+ [Unit("kpc"), Unit("Myr"), Unit("solMass"), Unit("rad"), Unit("km / s")]
76
+
77
+ With length equal to the number of base units
78
+
79
+ >>> len(usys)
80
+ 5
81
+
82
+ """
83
+
84
+ # ===============================================================
85
+ # Class-level
86
+
87
+ _base_field_names: ClassVar[tuple[str, ...]]
88
+ _base_dimensions: ClassVar[tuple[AbstractDimension, ...]]
89
+
90
+ def __init_subclass__(cls) -> None:
91
+ # Register class with a tuple of it's dimensions.
92
+ # This requires processing the type hints, not the dataclass fields
93
+ # since those are made after the original class is defined.
94
+ field_names, dims = parse_field_names_and_dimensions(cls)
95
+
96
+ # Check the unitsystem is not already registered
97
+ # If `make_dataclass(slots=True)` then the class is made twice, the
98
+ # second time adding the `__slots__` attribute
99
+ if dims in _UNITSYSTEMS_REGISTRY and "__slots__" not in cls.__dict__:
100
+ msg = f"Unit system with dimensions {dims} already exists."
101
+ raise ValueError(msg)
102
+
103
+ # Add attributes to the class
104
+ cls._base_field_names = tuple(field_names)
105
+ cls._base_dimensions = dims
106
+
107
+ _UNITSYSTEMS_REGISTRY[dims] = cls
108
+
109
+ # ===============================================================
110
+ # USys API
111
+
112
+ @property # TODO: classproperty
113
+ def base_dimensions(self) -> tuple[AbstractDimension, ...]:
114
+ """Dimensions required for the unit system."""
115
+ return self._base_dimensions
116
+
117
+ @property
118
+ def base_units(self) -> tuple[AbstractUnit, ...]:
119
+ """List of core units."""
120
+ return tuple(getattr(self, k) for k in self._base_field_names)
121
+
122
+ # ===============================================================
123
+ # Python stuff
124
+
125
+ def __getitem__(self, key: AbstractDimension | str) -> AbstractUnit:
126
+ """Get the unit for a given physical type.
127
+
128
+ Examples
129
+ --------
130
+ >>> from unxt import unitsystem
131
+ >>> usys = unitsystem("m", "s", "kg", "radian")
132
+
133
+ Something in the base dimensions:
134
+
135
+ >>> usys["length"]
136
+ Unit("m")
137
+
138
+ Something not in the base dimensions:
139
+
140
+ >>> usys["velocity"]
141
+ Unit("m / s")
142
+
143
+ """
144
+ key = dimension(key)
145
+ if key in self.base_dimensions:
146
+ return getattr(self, parse_dimlike_name(key))
147
+
148
+ out: AbstractUnit
149
+ for k, v in _physical_unit_mapping.items():
150
+ if v == key:
151
+ out = unit(" ".join([f"{x}**{y}" for x, y in k]))
152
+ break
153
+ # IDK if it's possible to get here
154
+ else:
155
+ msg = f"Physical type {key!r} doesn't exist in unit registry." # pragma: no cover # noqa: E501
156
+ raise ValueError(msg) # pragma: no cover
157
+
158
+ out = out.decompose(self.base_units)
159
+ out._scale = 1.0 # noqa: SLF001
160
+ return out
161
+
162
+ def __len__(self) -> int:
163
+ """Return the number of base units in the system.
164
+
165
+ Examples
166
+ --------
167
+ >>> from unxt import unitsystem
168
+ >>> usys = unitsystem("m", "s", "kg", "radian")
169
+ >>> len(usys)
170
+ 4
171
+
172
+ """
173
+ # Note: This is required for q.decompose(usys) to work, where q is a Quantity
174
+ return len(self.base_dimensions)
175
+
176
+ # TODO: should this be changed to _base_field_names -> Iterator[str]?
177
+ def __iter__(self) -> Iterator[AbstractUnit]:
178
+ """Iterate over the base units.
179
+
180
+ This is different than a dictionary, which would iterate over the
181
+ physical types (the keys).
182
+
183
+ Examples
184
+ --------
185
+ >>> from unxt import unitsystem
186
+ >>> usys = unitsystem("m", "s", "kg", "radian")
187
+ >>> list(iter(usys))
188
+ [Unit("m"), Unit("s"), Unit("kg"), Unit("rad")]
189
+
190
+ """
191
+ yield from self.base_units
192
+
193
+ def __str__(self) -> str:
194
+ """Return a string representation of the unit system.
195
+
196
+ Examples
197
+ --------
198
+ >>> from unxt import unitsystem
199
+ >>> usys = unitsystem("m", "s", "kg", "radian")
200
+ >>> str(usys)
201
+ 'LTMAUnitSystem(length, time, mass, angle)'
202
+
203
+ """
204
+ fs = ", ".join(map(str, self._base_field_names))
205
+ return f"{type(self).__name__}({fs})"
206
+
207
+ # ===============================================================
208
+ # Plum stuff
209
+
210
+ #: This tells `plum` that this type can be efficiently cached.
211
+ __faithful__: ClassVar[bool] = True
212
+
213
+
214
+ # ---------------------------------------------------------------
215
+
216
+
217
+ def parse_field_names_and_dimensions(
218
+ cls: type,
219
+ ) -> tuple[tuple[str, ...], tuple[AbstractDimension, ...]]:
220
+ # Register class with a tuple of it's dimensions.
221
+ # This requires processing the type hints, not the dataclass fields
222
+ # since those are made after the original class is defined.
223
+ type_hints = get_type_hints(cls, include_extras=True)
224
+
225
+ field_names = []
226
+ dims = []
227
+ for name, type_hint in type_hints.items():
228
+ # Check it's Annotated
229
+ if not isannotated(type_hint):
230
+ continue
231
+
232
+ # Get the arguments to Annotated
233
+ origin, *f_args = get_args(type_hint)
234
+
235
+ # Check that the first argument is a UnitBase
236
+ if not issubclass(origin, Unit):
237
+ continue
238
+
239
+ # Need for one of the arguments to be a Dimension
240
+ f_dim = [x for x in f_args if isinstance(x, PhysicalType)]
241
+ if not f_dim:
242
+ msg = f"Field {name!r} must be an Annotated with a dimension."
243
+ raise TypeError(msg)
244
+ if len(f_dim) > 1:
245
+ msg = (
246
+ f"Field {name!r} must be an Annotated with only one dimension; "
247
+ f"got {f_dim}"
248
+ )
249
+ raise TypeError(msg)
250
+
251
+ field_names.append(parse_dimlike_name(name))
252
+ dims.append(f_dim[0])
253
+
254
+ if len(set(dims)) < len(dims):
255
+ msg = "Some dimensions are repeated."
256
+ raise ValueError(msg)
257
+
258
+ return tuple(field_names), tuple(dims)
@@ -0,0 +1,190 @@
1
+ """Built-in unit systems."""
2
+
3
+ __all__ = ("DimensionlessUnitSystem", "LTMAUnitSystem")
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Annotated, TypeAlias, final
7
+ from typing_extensions import override
8
+
9
+ import jax.tree_util as jtu
10
+ from astropy.units import UnitBase as AstropyUnitBase, dimensionless_unscaled
11
+
12
+ from . import builtin_dimensions as ud
13
+ from .base import AbstractUnitSystem
14
+ from unxt._src.utils import SingletonMixin
15
+
16
+ Unit: TypeAlias = AstropyUnitBase
17
+
18
+
19
+ @jtu.register_static
20
+ @final
21
+ @dataclass(frozen=True, slots=True)
22
+ class DimensionlessUnitSystem(SingletonMixin, AbstractUnitSystem):
23
+ """A unit system with only dimensionless units.
24
+
25
+ This is a singleton class.
26
+
27
+ Examples
28
+ --------
29
+ >>> from unxt.unitsystems import DimensionlessUnitSystem
30
+ >>> dims1 = DimensionlessUnitSystem()
31
+ >>> dims2 = DimensionlessUnitSystem()
32
+ >>> dims1 is dims2
33
+ True
34
+
35
+ """
36
+
37
+ #: The dimensionless unit.
38
+ dimensionless: Annotated[Unit, ud.dimensionless] = dimensionless_unscaled
39
+
40
+ def __repr__(self) -> str:
41
+ return "DimensionlessUnitSystem()"
42
+
43
+ @override
44
+ def __str__(self) -> str:
45
+ return self.__repr__()
46
+
47
+
48
+ @jtu.register_static
49
+ @final
50
+ @dataclass(frozen=True, slots=True, repr=False)
51
+ class LTMAUnitSystem(AbstractUnitSystem):
52
+ """Length, time, mass, angle unit system."""
53
+
54
+ #: Units for the length dimension.
55
+ length: Annotated[Unit, ud.length]
56
+
57
+ #: Units for the time dimension.
58
+ time: Annotated[Unit, ud.time]
59
+
60
+ #: Units for the mass dimension.
61
+ mass: Annotated[Unit, ud.mass]
62
+
63
+ #: Units for the angle 'dimension'.
64
+ angle: Annotated[Unit, ud.angle]
65
+
66
+ def __repr__(self) -> str:
67
+ fs = ", ".join(map(str, self.base_units))
68
+ return f"unitsystem({fs})"
69
+
70
+
71
+ @jtu.register_static
72
+ @final
73
+ @dataclass(frozen=True, slots=True)
74
+ class SIUnitSystem(SingletonMixin, AbstractUnitSystem):
75
+ """SI unit system + angles.
76
+
77
+ Note: this is not part of the public API! Use the `si` instance (realization) from
78
+ `unxt.unitsystems` instead.
79
+
80
+ Examples
81
+ --------
82
+ >>> import unxt as u
83
+ >>> from unxt._src.unitsystems.builtin import SIUnitSystem
84
+ >>> usys = SIUnitSystem(
85
+ ... length=u.unit("meter"),
86
+ ... time=u.unit("second"),
87
+ ... mass=u.unit("kilogram"),
88
+ ... electric_current=u.unit("ampere"),
89
+ ... temperature=u.unit("Kelvin"),
90
+ ... amount=u.unit("mole"),
91
+ ... luminous_intensity=u.unit("candela"),
92
+ ... angle=u.unit("radian"),
93
+ ... )
94
+ >>> usys
95
+ unitsystem(m, kg, s, mol, A, K, cd, rad)
96
+
97
+ """
98
+
99
+ # Base SI dimensions
100
+ #: Units for the length dimension.
101
+ length: Annotated[Unit, ud.length]
102
+
103
+ #: Units for the mass dimension.
104
+ mass: Annotated[Unit, ud.mass]
105
+
106
+ #: Units for the time dimension.
107
+ time: Annotated[Unit, ud.time]
108
+
109
+ #: Units for the amount of substance dimension.
110
+ amount: Annotated[Unit, ud.amount]
111
+
112
+ #: Units for the electric current dimension.
113
+ electric_current: Annotated[Unit, ud.current]
114
+
115
+ #: Units for the temperature dimension.
116
+ temperature: Annotated[Unit, ud.temperature]
117
+
118
+ #: Units for the luminous intensity dimension.
119
+ luminous_intensity: Annotated[Unit, ud.luminous_intensity]
120
+
121
+ # + angles
122
+ #: Units for the angle 'dimension'.
123
+ angle: Annotated[Unit, ud.angle]
124
+
125
+ def __repr__(self) -> str:
126
+ fs = ", ".join(map(str, self.base_units))
127
+ return f"unitsystem({fs})"
128
+
129
+
130
+ @jtu.register_static
131
+ @final
132
+ @dataclass(frozen=True, slots=True)
133
+ class CGSUnitSystem(SingletonMixin, AbstractUnitSystem):
134
+ """CGS unit system + angles.
135
+
136
+ Note: this is not part of the public API! Use the `cgs` instance (realization) from
137
+ `unxt.unitsystems` instead.
138
+
139
+ Examples
140
+ --------
141
+ >>> import unxt as u
142
+ >>> from unxt._src.unitsystems.builtin import CGSUnitSystem
143
+ >>> usys = CGSUnitSystem(
144
+ ... length=u.unit("centimeter"),
145
+ ... time=u.unit("second"),
146
+ ... mass=u.unit("gram"),
147
+ ... angle=u.unit("radian"),
148
+ ... force=u.unit("dyne"),
149
+ ... energy=u.unit("erg"),
150
+ ... pressure=u.unit("barye"),
151
+ ... dynamic_viscosity=u.unit("poise"),
152
+ ... kinematic_viscosity=u.unit("stokes"),
153
+ ... )
154
+ >>> usys
155
+ unitsystem(cm, g, s, dyn, erg, Ba, P, St, rad)
156
+
157
+ """
158
+
159
+ # Base CGS dimensions
160
+ #: Units for the length dimension.
161
+ length: Annotated[Unit, ud.length]
162
+
163
+ #: Units for the mass dimension.
164
+ mass: Annotated[Unit, ud.mass]
165
+
166
+ #: Units for the time dimension.
167
+ time: Annotated[Unit, ud.time]
168
+
169
+ #: Units for the force dimension.
170
+ force: Annotated[Unit, ud.force]
171
+
172
+ #: Units for the energy dimension.
173
+ energy: Annotated[Unit, ud.energy]
174
+
175
+ #: Units for the pressure dimension.
176
+ pressure: Annotated[Unit, ud.pressure]
177
+
178
+ #: Units for the dynamic viscosity dimension.
179
+ dynamic_viscosity: Annotated[Unit, ud.dynamic_viscosity]
180
+
181
+ #: Units for the kinematic viscosity dimension.
182
+ kinematic_viscosity: Annotated[Unit, ud.kinematic_viscosity]
183
+
184
+ # + angles
185
+ #: Units for the angle 'dimension'.
186
+ angle: Annotated[Unit, ud.angle]
187
+
188
+ def __repr__(self) -> str:
189
+ fs = ", ".join(map(str, self.base_units))
190
+ return f"unitsystem({fs})"
@@ -0,0 +1,21 @@
1
+ """Built-in dimensions."""
2
+
3
+ __all__: tuple[str, ...] = ()
4
+
5
+ from unxt._src.dimensions import dimension
6
+
7
+ dimensionless = dimension("dimensionless")
8
+ length = dimension("length")
9
+ mass = dimension("mass")
10
+ time = dimension("time")
11
+ speed = dimension("speed")
12
+ angle = dimension("angle")
13
+ amount = dimension("amount of substance")
14
+ current = dimension("electrical current")
15
+ temperature = dimension("temperature")
16
+ luminous_intensity = dimension("luminous intensity")
17
+ pressure = dimension("pressure")
18
+ force = dimension("force")
19
+ energy = dimension("energy")
20
+ dynamic_viscosity = dimension("dynamic viscosity")
21
+ kinematic_viscosity = dimension("kinematic viscosity")
@@ -0,0 +1,13 @@
1
+ """Comparison functions."""
2
+
3
+ __all__ = ("equivalent",)
4
+
5
+ from plum import dispatch
6
+
7
+ from .base import AbstractUnitSystem
8
+
9
+
10
+ @dispatch
11
+ def equivalent(a: AbstractUnitSystem, b: AbstractUnitSystem, /) -> bool:
12
+ """Check if two unit systems are equivalent."""
13
+ return set(a.base_dimensions) == set(b.base_dimensions)