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,307 @@
1
+ """Tools for representing systems of units using ``astropy.units``."""
2
+
3
+ __all__ = ("unitsystem", "unitsystem_of")
4
+
5
+
6
+ from collections.abc import Sequence
7
+ from dataclasses import field, make_dataclass
8
+ from typing import Annotated, Any
9
+
10
+ import equinox as eqx
11
+ import jax.tree_util as jtu
12
+ import numpy as np
13
+ from astropy.constants import G as const_G # noqa: N811, pylint: disable=E0611
14
+ from astropy.units import UnitBase as AstropyUnitBase
15
+ from plum import dispatch
16
+
17
+ from . import builtin_dimensions as ud
18
+ from .base import UNITSYSTEMS_REGISTRY, AbstractUnitSystem
19
+ from .builtin import DimensionlessUnitSystem
20
+ from .flags import AbstractUSysFlag, DynamicalSimUSysFlag, StandardUSysFlag
21
+ from .realizations import NAMED_UNIT_SYSTEMS, dimensionless
22
+ from .utils import parse_dimlike_name
23
+ from unxt.dims import dimension_of
24
+ from unxt.units import unit
25
+
26
+ # ===================================================================
27
+ # `unitsystem` function
28
+
29
+
30
+ @dispatch
31
+ def unitsystem(usys: AbstractUnitSystem, /) -> AbstractUnitSystem:
32
+ """Convert a UnitSystem to a UnitSystem.
33
+
34
+ Examples
35
+ --------
36
+ >>> from unxt.unitsystems import unitsystem
37
+ >>> usys = unitsystem("kpc", "Myr", "Msun", "radian")
38
+ >>> usys
39
+ unitsystem(kpc, Myr, solMass, rad)
40
+
41
+ >>> unitsystem(usys) is usys
42
+ True
43
+
44
+ """
45
+ return usys
46
+
47
+
48
+ @dispatch
49
+ def unitsystem(seq: Sequence[Any], /) -> AbstractUnitSystem:
50
+ """Convert a UnitSystem or tuple of arguments to a UnitSystem.
51
+
52
+ Examples
53
+ --------
54
+ >>> import unxt as u
55
+
56
+ >>> u.unitsystem(())
57
+ DimensionlessUnitSystem()
58
+
59
+ >>> u.unitsystem(("kpc", "Myr", "Msun", "radian"))
60
+ unitsystem(kpc, Myr, solMass, rad)
61
+
62
+ >>> u.unitsystem(["kpc", "Myr", "Msun", "radian"])
63
+ unitsystem(kpc, Myr, solMass, rad)
64
+
65
+ """
66
+ return unitsystem(*seq) if len(seq) > 0 else dimensionless
67
+
68
+
69
+ @dispatch
70
+ def unitsystem(_: None, /) -> DimensionlessUnitSystem:
71
+ """Dimensionless unit system from None.
72
+
73
+ Examples
74
+ --------
75
+ >>> from unxt.unitsystems import unitsystem
76
+ >>> unitsystem(None)
77
+ DimensionlessUnitSystem()
78
+
79
+ """
80
+ return dimensionless
81
+
82
+
83
+ @dispatch
84
+ def unitsystem(*args: Any) -> AbstractUnitSystem:
85
+ """Convert a set of arguments to a UnitSystem.
86
+
87
+ Examples
88
+ --------
89
+ >>> from unxt.unitsystems import unitsystem
90
+
91
+ >>> unitsystem("kpc", "Myr", "Msun", "radian")
92
+ unitsystem(kpc, Myr, solMass, rad)
93
+
94
+ """
95
+ # Convert everything to a unit
96
+ args = tuple(map(unit, args))
97
+
98
+ # Check that the units all have different dimensions
99
+ dims = tuple(map(dimension_of, args))
100
+ dims = eqx.error_if(
101
+ dims,
102
+ len(set(dims)) < len(dims),
103
+ "some dimensions are repeated",
104
+ )
105
+
106
+ # Return if the unit system is already registered
107
+ if dims in UNITSYSTEMS_REGISTRY:
108
+ return UNITSYSTEMS_REGISTRY[dims](*args)
109
+
110
+ # Otherwise, create a new unit system
111
+ # dimension names of all the units
112
+ du = {parse_dimlike_name(x).replace(" ", "_"): dimension_of(x) for x in args}
113
+ # name: physical types
114
+ cls_name = "".join(k.title().replace("_", "") for k in du) + "UnitSystem" # pylint: disable=unreachable
115
+ # fields: name, unit
116
+ fields = [
117
+ (
118
+ k,
119
+ Annotated[AstropyUnitBase, v],
120
+ field(init=True, repr=True, hash=True, compare=True), # pylint: disable=invalid-field-call
121
+ )
122
+ for k, v in du.items()
123
+ ]
124
+
125
+ def _reduce_(self: AbstractUnitSystem) -> tuple:
126
+ return (_call_unitsystem, self.base_units, None, None, None, None)
127
+
128
+ # Make and register the dataclass class
129
+ unitsystem_cls: type[AbstractUnitSystem] = make_dataclass(
130
+ cls_name,
131
+ fields,
132
+ bases=(AbstractUnitSystem,),
133
+ namespace={"__reduce__": _reduce_},
134
+ frozen=True,
135
+ slots=True,
136
+ eq=True,
137
+ repr=True,
138
+ init=True,
139
+ )
140
+
141
+ # Register the dynamically created unit system as a static PyTree node
142
+ jtu.register_static(unitsystem_cls)
143
+
144
+ # Make the dataclass instance
145
+ return unitsystem_cls(*args)
146
+
147
+
148
+ @dispatch
149
+ def unitsystem(name: str, /) -> AbstractUnitSystem:
150
+ """Return unit system from name.
151
+
152
+ Examples
153
+ --------
154
+ >>> from unxt.unitsystems import unitsystem
155
+ >>> unitsystem("galactic")
156
+ unitsystem(kpc, Myr, solMass, rad)
157
+
158
+ >>> unitsystem("solarsystem")
159
+ unitsystem(AU, yr, solMass, rad)
160
+
161
+ >>> unitsystem("dimensionless")
162
+ DimensionlessUnitSystem()
163
+
164
+ """
165
+ return NAMED_UNIT_SYSTEMS[name]
166
+
167
+
168
+ @dispatch
169
+ def unitsystem(usys: AbstractUnitSystem, *args: Any) -> AbstractUnitSystem:
170
+ """Create a unit system from an existing unit system and additional units.
171
+
172
+ Examples
173
+ --------
174
+ We can add a new unit definition to an existing unit system:
175
+
176
+ >>> from unxt.unitsystems import unitsystem
177
+ >>> usys = unitsystem("galactic")
178
+ >>> unitsystem(usys, "km/s")
179
+ LengthTimeMassAngleSpeedUnitSystem(length=Unit("kpc"), time=Unit("Myr"), mass=Unit("solMass"), angle=Unit("rad"), speed=Unit("km / s"))
180
+
181
+ We can also override the base unit of an existing unit system:
182
+
183
+ >>> new_usys = unitsystem(usys, "pc")
184
+ >>> new_usys
185
+ TimeMassAngleLengthUnitSystem(time=Unit("Myr"), mass=Unit("solMass"), angle=Unit("rad"), length=Unit("pc"))
186
+
187
+ """ # noqa: E501
188
+ # TODO: not need this hack for single-string inputs
189
+ # TODO: process new units without making a whole unit system
190
+ if len(args) == 1 and isinstance(args[0], str):
191
+ new_usys = unitsystem(unit(args[0]))
192
+ else:
193
+ new_usys = unitsystem(*args)
194
+
195
+ current_units = [
196
+ u for u in usys.base_units if dimension_of(u) not in new_usys.base_dimensions
197
+ ]
198
+ return unitsystem(*current_units, *args) # pylint: disable=unreachable
199
+
200
+
201
+ @dispatch
202
+ def unitsystem(flag: type[AbstractUSysFlag], *_: Any) -> AbstractUnitSystem:
203
+ """Raise an exception since the flag is abstract."""
204
+ msg = "Do not use the AbstractUSysFlag directly, only use subclasses."
205
+ raise TypeError(msg)
206
+
207
+
208
+ @dispatch
209
+ def unitsystem(flag: type[StandardUSysFlag], *args: Any) -> AbstractUnitSystem:
210
+ """Create a standard unit system using the inputted units.
211
+
212
+ Examples
213
+ --------
214
+ >>> from unxt import unitsystem, unitsystems
215
+ >>> unitsystem(unitsystems.StandardUSysFlag, "kpc", "Myr", "Msun")
216
+ LengthTimeMassUnitSystem(length=Unit("kpc"), time=Unit("Myr"), mass=Unit("solMass"))
217
+
218
+ """
219
+ return unitsystem(*args)
220
+
221
+
222
+ @dispatch
223
+ def unitsystem(
224
+ flag: type[DynamicalSimUSysFlag],
225
+ *args: Any,
226
+ G: float | int = 1.0, # noqa: N803
227
+ ) -> AbstractUnitSystem:
228
+ """Make a dynamical unit system.
229
+
230
+ Examples
231
+ --------
232
+ >>> from unxt.unitsystems import unitsystem, DynamicalSimUSysFlag
233
+
234
+ >>> unitsystem(DynamicalSimUSysFlag, "m", "kg")
235
+ LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("kg"), time=Unit("122404 s"))
236
+
237
+ """
238
+ tmp = unitsystem(*args)
239
+
240
+ # Use G for computing the missing units below:
241
+ G = G * const_G
242
+
243
+ added = ()
244
+ if ud.length in tmp.base_dimensions and ud.mass in tmp.base_dimensions:
245
+ time = 1 / np.sqrt(G * tmp["mass"] / tmp["length"] ** 3)
246
+ added = (time,)
247
+ elif ud.length in tmp.base_dimensions and ud.time in tmp.base_dimensions:
248
+ mass = 1 / G * tmp["length"] ** 3 / tmp["time"] ** 2
249
+ added = (mass,)
250
+ elif ud.length in tmp.base_dimensions and ud.speed in tmp.base_dimensions:
251
+ time = tmp["length"] / tmp["velocity"]
252
+ mass = tmp["velocity"] ** 2 / G * tmp["length"]
253
+ added = (time, mass)
254
+ elif ud.mass in tmp.base_dimensions and ud.time in tmp.base_dimensions:
255
+ length = np.cbrt(G * tmp["mass"] * tmp["time"] ** 2)
256
+ added = (length,)
257
+ elif ud.mass in tmp.base_dimensions and ud.speed in tmp.base_dimensions:
258
+ length = G * tmp["mass"] / tmp["velocity"] ** 2
259
+ time = length / tmp["velocity"]
260
+ added = (length, time)
261
+ elif ud.time in tmp.base_dimensions and ud.speed in tmp.base_dimensions:
262
+ mass = 1 / G * tmp["velocity"] ** 3 * tmp["time"]
263
+ length = G * mass / tmp["velocity"] ** 2
264
+ added = (mass, length)
265
+
266
+ return unitsystem(*tmp, *added)
267
+
268
+
269
+ # ----
270
+
271
+
272
+ def _call_unitsystem(*args: Any) -> AbstractUnitSystem:
273
+ return unitsystem(*args)
274
+
275
+
276
+ # ===================================================================
277
+ # `unitsystem_of` function
278
+
279
+
280
+ @dispatch
281
+ def unitsystem_of(obj: Any, /) -> DimensionlessUnitSystem:
282
+ """Return the unit system of the object.
283
+
284
+ Examples
285
+ --------
286
+ >>> from unxt.unitsystems import unitsystem_of
287
+
288
+ >>> unitsystem_of(1)
289
+ DimensionlessUnitSystem()
290
+
291
+ """
292
+ return dimensionless
293
+
294
+
295
+ @dispatch
296
+ def unitsystem_of(obj: AbstractUnitSystem, /) -> AbstractUnitSystem:
297
+ """Return the unit system from the unit system.
298
+
299
+ Examples
300
+ --------
301
+ >>> from unxt.unitsystems import galactic, unitsystem_of
302
+
303
+ >>> unitsystem_of(galactic) is galactic
304
+ True
305
+
306
+ """
307
+ return obj
@@ -0,0 +1,80 @@
1
+ """Flags for unit system definitions."""
2
+
3
+ __all__ = ("AbstractUSysFlag", "DynamicalSimUSysFlag", "StandardUSysFlag")
4
+
5
+ from typing import Any
6
+
7
+
8
+ class AbstractUSysFlag:
9
+ """Abstract class for unit system flags to provide dispatch control.
10
+
11
+ Unit system flags are used to indicate the type of unit system being
12
+ defined. They are not intended to be instantiated and are used to provide
13
+ dispatch control for defining unit systems.
14
+
15
+ Raises
16
+ ------
17
+ ValueError
18
+ If an attempt is made to instantiate a unit system flag class.
19
+
20
+ See Also
21
+ --------
22
+ `unxt.unitsystems.unitsystem` :
23
+ Function to define a unit system. The `AbstractUSysFlag` can be provided
24
+ as the first argument to indicate the type of unit system being defined.
25
+ This is useful for multiple-dispatching to the correct constructor.
26
+
27
+ Examples
28
+ --------
29
+ For this example we will use the `unxt.unitsystems.StandardUSysFlag`, which
30
+ indicates the standard construction of a unit system directly from the set
31
+ of base units. Flags like `unxt.unitsystems.DynamicalSimUSysFlag` can be
32
+ used to create a unit system where the gravitational constant ``G`` is
33
+ unit-less and units are defined accordingly.
34
+
35
+ >>> import unxt
36
+
37
+ Define a unit system with the standard flag:
38
+
39
+ >>> unxt.unitsystem(unxt.unitsystems.StandardUSysFlag, "m", "kg", "s")
40
+ LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("kg"), time=Unit("s"))
41
+
42
+ """
43
+
44
+ def __new__(cls, *_: Any, **__: Any) -> None: # type: ignore[misc]
45
+ msg = "unit system flag classes cannot be instantiated."
46
+ raise ValueError(msg)
47
+
48
+
49
+ class StandardUSysFlag(AbstractUSysFlag):
50
+ """Flag to indicate a standard unit system with no additional arguments.
51
+
52
+ Examples
53
+ --------
54
+ >>> from unxt.unitsystems import unitsystem, StandardUSysFlag
55
+
56
+ Define a unit system with the standard flag:
57
+
58
+ >>> unitsystem(StandardUSysFlag, "m", "kg", "s")
59
+ LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("kg"), time=Unit("s"))
60
+
61
+ Further examples may be found in the ``unitsystem`` docs.
62
+
63
+ """
64
+
65
+
66
+ class DynamicalSimUSysFlag(AbstractUSysFlag):
67
+ """Flag to indicate a unit system with optional definition of G.
68
+
69
+ Examples
70
+ --------
71
+ >>> from unxt.unitsystems import unitsystem, DynamicalSimUSysFlag
72
+
73
+ Define a unit system with the dynamical simulation flag:
74
+
75
+ >>> unitsystem(DynamicalSimUSysFlag, "m", "kg")
76
+ LengthMassTimeUnitSystem(length=Unit("m"), mass=Unit("kg"), time=Unit("122404 s"))
77
+
78
+ Further examples may be found in the ``unitsystem`` docs.
79
+
80
+ """
@@ -0,0 +1,73 @@
1
+ """Realizations of unit systems."""
2
+ # pylint: disable=no-member
3
+
4
+ __all__ = (
5
+ # unit system instance
6
+ "galactic",
7
+ "dimensionless",
8
+ "solarsystem",
9
+ "si",
10
+ "cgs",
11
+ # unit system alias
12
+ "NAMED_UNIT_SYSTEMS",
13
+ )
14
+
15
+ from .base import AbstractUnitSystem
16
+ from .builtin import (
17
+ CGSUnitSystem,
18
+ DimensionlessUnitSystem,
19
+ LTMAUnitSystem,
20
+ SIUnitSystem,
21
+ )
22
+ from unxt.units import unit
23
+
24
+ # Dimensionless. This is a singleton.
25
+ dimensionless = DimensionlessUnitSystem()
26
+
27
+ # Galactic unit system
28
+ galactic = LTMAUnitSystem(
29
+ length=unit("kpc"),
30
+ time=unit("Myr"),
31
+ mass=unit("Msun"),
32
+ angle=unit("radian"),
33
+ )
34
+
35
+ # Solar system units
36
+ solarsystem = LTMAUnitSystem(
37
+ length=unit("au"), time=unit("yr"), mass=unit("Msun"), angle=unit("radian")
38
+ )
39
+
40
+ #: International System of Units
41
+ si = SIUnitSystem(
42
+ length=unit("meter"),
43
+ time=unit("second"),
44
+ mass=unit("kilogram"),
45
+ electric_current=unit("ampere"),
46
+ temperature=unit("Kelvin"),
47
+ amount=unit("mole"),
48
+ luminous_intensity=unit("candela"),
49
+ angle=unit("radian"),
50
+ )
51
+
52
+ #: Centimeter, gram, second unit system
53
+ cgs = CGSUnitSystem(
54
+ length=unit("centimeter"),
55
+ time=unit("second"),
56
+ mass=unit("gram"),
57
+ angle=unit("radian"),
58
+ force=unit("dyne"),
59
+ energy=unit("erg"),
60
+ pressure=unit("barye"),
61
+ dynamic_viscosity=unit("poise"),
62
+ kinematic_viscosity=unit("stokes"),
63
+ )
64
+
65
+
66
+ #: Named unit systems
67
+ NAMED_UNIT_SYSTEMS: dict[str, AbstractUnitSystem] = {
68
+ "galactic": galactic,
69
+ "solarsystem": solarsystem,
70
+ "dimensionless": dimensionless,
71
+ "si": si,
72
+ "cgs": cgs,
73
+ }
@@ -0,0 +1,95 @@
1
+ """Unit system utils."""
2
+
3
+ __all__: tuple[str, ...] = ()
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from plum import dispatch
9
+
10
+ from zeroth import zeroth
11
+
12
+ from . import builtin_dimensions as bdims
13
+ from unxt.dims import AbstractDimension, dimension_of
14
+ from unxt.units import AbstractUnit
15
+
16
+ # ------------------------------------
17
+
18
+
19
+ @dispatch.abstract
20
+ def parse_dimlike_name(pt: Any, /) -> str:
21
+ """Get the dimension name from the object."""
22
+ raise NotImplementedError # pragma: no cover
23
+
24
+
25
+ @dispatch
26
+ def parse_dimlike_name(pt: str, /) -> str:
27
+ """Return the dimension name.
28
+
29
+ Note that this does not check for the existence of that dimension.
30
+
31
+ Examples
32
+ --------
33
+ >>> from unxt._src.unitsystems.utils import parse_dimlike_name
34
+
35
+ >>> parse_dimlike_name("length")
36
+ 'length'
37
+
38
+ >>> parse_dimlike_name("not real")
39
+ 'not_real'
40
+
41
+ >>> try:
42
+ ... parse_dimlike_name("*62")
43
+ ... except ValueError as e:
44
+ ... print(e)
45
+ Input contains non-letter characters
46
+
47
+ """
48
+ # A regex search to match anything that's not a letter or a whitespace.
49
+ if re.search(r"[^a-zA-Z_ ]", pt):
50
+ msg = "Input contains non-letter characters"
51
+ raise ValueError(msg)
52
+
53
+ return pt.replace(" ", "_")
54
+
55
+
56
+ @dispatch
57
+ def parse_dimlike_name(pt: AbstractDimension, /) -> str:
58
+ """Return the dimension name from a dimension.
59
+
60
+ Examples
61
+ --------
62
+ >>> import unxt as u
63
+ >>> from unxt._src.unitsystems.utils import parse_dimlike_name
64
+
65
+ >>> parse_dimlike_name(u.dimension("length"))
66
+ 'length'
67
+
68
+ >>> parse_dimlike_name(u.dimension("speed"))
69
+ 'speed'
70
+
71
+ """
72
+ # Note: this is not deterministic b/c ``_physical_type`` is a set
73
+ # that's why the `if` statement is needed.
74
+ match pt:
75
+ case bdims.speed:
76
+ out = "speed"
77
+ case _:
78
+ out = parse_dimlike_name(zeroth(pt._physical_type)) # noqa: SLF001
79
+ return out
80
+
81
+
82
+ @dispatch
83
+ def parse_dimlike_name(pt: AbstractUnit, /) -> str:
84
+ """Return the dimension name from a unit.
85
+
86
+ Examples
87
+ --------
88
+ >>> import unxt as u
89
+ >>> from unxt._src.unitsystems.utils import parse_dimlike_name
90
+
91
+ >>> parse_dimlike_name(u.unit("km"))
92
+ 'length'
93
+
94
+ """
95
+ return parse_dimlike_name(dimension_of(pt))
unxt/_src/utils.py ADDED
@@ -0,0 +1,112 @@
1
+ """Utilities.
2
+
3
+ Copyright (c) 2023 Galactic Dynamics. All rights reserved.
4
+ """
5
+
6
+ __all__: tuple[str, ...] = ()
7
+
8
+ from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast, runtime_checkable
9
+
10
+ from jax import dtypes
11
+ from jax.numpy import dtype as DType # noqa: N812
12
+
13
+ import quaxed.lax as qlax
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Self
17
+
18
+
19
+ _singleton_insts: dict[type, object] = {}
20
+
21
+
22
+ class SingletonMixin:
23
+ """Singleton class.
24
+
25
+ This class is a mixin that can be used to create singletons.
26
+
27
+ Examples
28
+ --------
29
+ >>> class MySingleton(SingletonMixin):
30
+ ... pass
31
+
32
+ >>> a = MySingleton()
33
+ >>> b = MySingleton()
34
+ >>> a is b
35
+ True
36
+
37
+ """
38
+
39
+ def __new__(cls, /, *_: Any, **__: Any) -> "Self":
40
+ # Check if instance already exists
41
+ if cls in _singleton_insts:
42
+ return cast("Self", _singleton_insts[cls])
43
+ # Create new instance and cache it
44
+ self = object.__new__(cls)
45
+ _singleton_insts[cls] = self
46
+ return self
47
+
48
+
49
+ @runtime_checkable
50
+ class HasDType(Protocol):
51
+ """Protocol for objects that have a dtype attribute."""
52
+
53
+ @property
54
+ def dtype(self) -> DType:
55
+ """The dtype of the object."""
56
+
57
+
58
+ HasDTypeT = TypeVar("HasDTypeT", bound=HasDType)
59
+
60
+
61
+ def promote_dtypes(*arrays: HasDTypeT) -> tuple[HasDTypeT, ...]:
62
+ """Promotes all input arrays to a common dtype.
63
+
64
+ Examples
65
+ --------
66
+ >>> import jax.numpy as jnp
67
+ >>> import unxt as u
68
+
69
+ >>> x1 = jnp.array([1, 2, 3], dtype=jnp.int32)
70
+ >>> x2 = jnp.array([4, 5, 6], dtype=jnp.float32)
71
+
72
+ >>> x1, x2 = promote_dtypes(x1, x2)
73
+ >>> x1.dtype, x2.dtype
74
+ (dtype('float32'), dtype('float32'))
75
+
76
+ >>> q1 = u.Quantity.from_([1, 2, 3], "m", dtype=int)
77
+ >>> q2 = u.Q([4.0, 5, 6], unit="km")
78
+ >>> q1, q2 = promote_dtypes(q1, q2)
79
+ >>> q1.dtype, q2.dtype
80
+ (dtype('float32'), dtype('float32'))
81
+
82
+ """
83
+ common_dtype = dtypes.result_type(*arrays)
84
+ # TODO: check if this copies.
85
+ return tuple([qlax.convert_element_type(arr, common_dtype) for arr in arrays]) # type: ignore[arg-type,misc] # pylint: disable=R1728
86
+
87
+
88
+ def promote_dtypes_if_needed(
89
+ original_dtypes: tuple[DType, ...], /, *args: HasDTypeT
90
+ ) -> tuple[HasDTypeT, ...]:
91
+ """Promotes dtypes of args if they differ from original_dtypes.
92
+
93
+ Examples
94
+ --------
95
+ >>> import jax.numpy as jnp
96
+ >>> import unxt as u
97
+ >>> from unxt._src.utils import promote_dtypes_if_needed
98
+ >>> x1 = jnp.array([1, 2, 3], dtype=jnp.int32)
99
+ >>> x2 = jnp.array([4, 5, 6], dtype=jnp.float32)
100
+ >>> original_dtypes = (x1.dtype, x1.dtype)
101
+ >>> x1_new, x2_new = promote_dtypes_if_needed(original_dtypes, x1, x2)
102
+ >>> x1_new.dtype, x2_new.dtype
103
+ (dtype('float32'), dtype('float32'))
104
+
105
+ """
106
+ # Compare equality of all `original_dtypes` and all `args` dtypes.
107
+ all_orig_dtypes_eq = all(dt == original_dtypes[0] for dt in original_dtypes)
108
+ all_new_dtypes_eq = all(arg.dtype == args[0].dtype for arg in args)
109
+ if all_orig_dtypes_eq and not all_new_dtypes_eq:
110
+ return promote_dtypes(*args)
111
+
112
+ return args
unxt/_version.py ADDED
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.dev463'
32
+ __version_tuple__ = version_tuple = (0, 1, 'dev463')
33
+
34
+ __commit_id__ = commit_id = None
unxt/_version.pyi ADDED
@@ -0,0 +1,2 @@
1
+ version: str
2
+ version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str]