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,1160 @@
1
+ # pylint: disable=import-error, no-member, unsubscriptable-object
2
+ # b/c it doesn't understand dataclass fields
3
+
4
+ __all__ = ("AbstractQuantity", "is_any_quantity")
5
+
6
+ import functools as ft
7
+ import warnings
8
+ from collections.abc import Mapping
9
+ from types import ModuleType
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ Any,
13
+ ClassVar,
14
+ Literal,
15
+ NoReturn,
16
+ TypeAlias,
17
+ TypeGuard,
18
+ cast,
19
+ )
20
+ from typing_extensions import override
21
+
22
+ import equinox as eqx
23
+ import jax
24
+ import jax.core
25
+ import numpy as np
26
+ import quax_blocks
27
+ import wadler_lindig as wl
28
+ from jax._src.numpy.array_methods import _IndexUpdateHelper, _IndexUpdateRef
29
+ from jaxtyping import Array, ArrayLike, Bool, ScalarLike, Shaped
30
+ from plum import add_promotion_rule, dispatch, type_nonparametric
31
+ from quax import ArrayValue
32
+
33
+ import quaxed.numpy as jnp
34
+ from dataclassish import field_items, replace
35
+
36
+ import unxt_api as uapi
37
+ from .mixins import AstropyQuantityCompatMixin, IPythonReprMixin, NumPyCompatMixin
38
+ from .value import StaticValue
39
+ from unxt.units import AbstractUnit
40
+
41
+ if TYPE_CHECKING:
42
+ from typing import Self
43
+
44
+
45
+ ArrayLikeSequence: TypeAlias = list[ScalarLike] | tuple[ScalarLike, ...]
46
+
47
+
48
+ class AbstractQuantity(
49
+ AstropyQuantityCompatMixin,
50
+ NumPyCompatMixin,
51
+ IPythonReprMixin,
52
+ ArrayValue,
53
+ quax_blocks.NumpyBinaryOpsMixin[Any, "AbstractQuantity"],
54
+ quax_blocks.NumpyComparisonMixin[Any, Bool[Array, "*shape"]], # TODO: shape hint
55
+ quax_blocks.NumpyUnaryMixin["AbstractQuantity"],
56
+ quax_blocks.NumpyRoundMixin["AbstractQuantity"],
57
+ quax_blocks.NumpyTruncMixin["AbstractQuantity"],
58
+ quax_blocks.NumpyFloorMixin["AbstractQuantity"],
59
+ quax_blocks.NumpyCeilMixin["AbstractQuantity"],
60
+ quax_blocks.LaxLenMixin,
61
+ quax_blocks.LaxLengthHintMixin,
62
+ ):
63
+ """Represents a Quantity with a unit.
64
+
65
+ Attributes
66
+ ----------
67
+ short_name : str | None
68
+ Optional short name for the class used in wadler-lindig printing when
69
+ ``use_short_name=True``. Defaults to ``None``.
70
+
71
+ Examples
72
+ --------
73
+ >>> import unxt as u
74
+
75
+ From an integer:
76
+
77
+ >>> u.Quantity(1, "m")
78
+ Quantity(Array(1, dtype=int32, ...), unit='m')
79
+
80
+ From a float:
81
+
82
+ >>> u.Quantity(1.0, "m")
83
+ Quantity(Array(1., dtype=float32, ...), unit='m')
84
+
85
+ From a list:
86
+
87
+ >>> u.Quantity([1, 2, 3], "m")
88
+ Quantity(Array([1, 2, 3], dtype=int32), unit='m')
89
+
90
+ From a tuple:
91
+
92
+ >>> u.Quantity((1, 2, 3), "m")
93
+ Quantity(Array([1, 2, 3], dtype=int32), unit='m')
94
+
95
+ From a `numpy.ndarray`:
96
+
97
+ >>> import numpy as np
98
+ >>> u.Quantity(np.array([1, 2, 3]), "m")
99
+ Quantity(Array([1, 2, 3], dtype=int32), unit='m')
100
+
101
+ From a `jax.Array`:
102
+
103
+ >>> import jax.numpy as jnp
104
+ >>> u.Quantity(jnp.array([1, 2, 3]), "m")
105
+ Quantity(Array([1, 2, 3], dtype=int32), unit='m')
106
+
107
+ The unit can also be given as a `astropy.units.Unit`:
108
+
109
+ >>> import astropy.units as apyu
110
+ >>> u.Quantity(1, apyu.m)
111
+ Quantity(Array(1, dtype=int32, ...), unit='m')
112
+
113
+ """
114
+
115
+ value: eqx.AbstractVar[Shaped[Array | StaticValue, "*shape"]]
116
+ """The value of the `AbstractQuantity`."""
117
+
118
+ unit: eqx.AbstractVar[AbstractUnit]
119
+ """The unit associated with this value."""
120
+
121
+ # ---------------------------------------------------------------
122
+ # Constructors
123
+
124
+ @classmethod
125
+ @dispatch.abstract
126
+ def from_(
127
+ cls: "type[AbstractQuantity]",
128
+ *args: Any,
129
+ **kwargs: Any,
130
+ ) -> "AbstractQuantity":
131
+ raise NotImplementedError # pragma: no cover
132
+
133
+ # See below for additional constructors
134
+
135
+ # ===============================================================
136
+ # Quantity API
137
+
138
+ def uconvert(self, u: Any, /) -> "AbstractQuantity":
139
+ """Convert the quantity to the given units.
140
+
141
+ See Also
142
+ --------
143
+ `unxt.uconvert` : convert a quantity to a new unit.
144
+
145
+ Examples
146
+ --------
147
+ >>> import unxt as u
148
+
149
+ >>> q = u.Quantity(1, "m")
150
+ >>> q.uconvert("cm")
151
+ Quantity(Array(100., dtype=float32, ...), unit='cm')
152
+
153
+ """
154
+ return uapi.uconvert(u, self)
155
+
156
+ def ustrip(self, u: Any, /) -> Array:
157
+ """Return the value in the given units.
158
+
159
+ See Also
160
+ --------
161
+ `unxt.ustrip` : strip the units from a quantity.
162
+
163
+ Examples
164
+ --------
165
+ >>> import unxt as u
166
+
167
+ >>> q = u.Quantity(1, "m")
168
+ >>> q.ustrip("cm")
169
+ Array(100., dtype=float32, weak_type=True)
170
+
171
+ """
172
+ return uapi.ustrip(u, self)
173
+
174
+ # ===============================================================
175
+ # Quax API
176
+
177
+ @property
178
+ def shape(self) -> tuple[int, ...]:
179
+ """Shape of the array."""
180
+ return self.value.shape
181
+
182
+ def materialise(self) -> NoReturn:
183
+ msg = f"Refusing to materialise `{type(self).__name__}`."
184
+ raise RuntimeError(msg)
185
+
186
+ def aval(self) -> jax.core.ShapedArray:
187
+ value = self.value
188
+ if isinstance(value, StaticValue):
189
+ value = jnp.asarray(value.array)
190
+ return jax.typeof(value)
191
+
192
+ def enable_materialise(self, _: bool = True) -> "Self": # noqa: FBT001, FBT002
193
+ return replace(self, value=self.value, unit=self.unit)
194
+
195
+ # ===============================================================
196
+ # Plum API
197
+
198
+ #: This tells `plum` that this type can be efficiently cached.
199
+ __faithful__: ClassVar[bool] = True
200
+
201
+ # ===============================================================
202
+ # Array API
203
+
204
+ def __array_namespace__(self, *, api_version: Any = None) -> ModuleType:
205
+ """Return the namespace for the array API.
206
+
207
+ Here we return the `quaxed.numpy` module, which is a drop-in replacement
208
+ for `jax.numpy`, but allows for array-ish objects to be used in place of
209
+ `jax` arrays. See `quax` for more information.
210
+
211
+ """
212
+ return jnp # quaxed.numpy
213
+
214
+ # ---------------------------------------------------------------
215
+ # attributes
216
+
217
+ @property
218
+ def dtype(self) -> jax.numpy.dtype:
219
+ """Data type of the array.
220
+
221
+ Examples
222
+ --------
223
+ >>> import unxt as u
224
+ >>> u.Quantity(1, "m").dtype
225
+ dtype('int32')
226
+
227
+ """
228
+ return self.value.dtype
229
+
230
+ @property
231
+ def device(self) -> jax.Device:
232
+ """Device where the array is located.
233
+
234
+ Examples
235
+ --------
236
+ >>> import unxt as u
237
+ >>> u.Quantity(1, "m").device
238
+ CpuDevice(id=0)
239
+
240
+ """
241
+ return self.value.devices().pop()
242
+
243
+ @property
244
+ def mT(self) -> "AbstractQuantity": # noqa: N802
245
+ """Matrix transpose of the array.
246
+
247
+ Examples
248
+ --------
249
+ >>> import unxt as u
250
+ >>> q = u.Quantity([[0, 1], [1, 2]], "m")
251
+ >>> q.mT
252
+ Quantity(Array([[0, 1],
253
+ [1, 2]], dtype=int32), unit='m')
254
+
255
+ """
256
+ return replace(self, value=jnp.matrix_transpose(self.value))
257
+
258
+ @property
259
+ def ndim(self) -> int:
260
+ """Number of dimensions.
261
+
262
+ Examples
263
+ --------
264
+ >>> import unxt as u
265
+ >>> q = u.Quantity([[1]], "m")
266
+ >>> q.ndim
267
+ 2
268
+
269
+ """
270
+ return self.value.ndim
271
+
272
+ @property
273
+ def size(self) -> int:
274
+ """Total number of elements.
275
+
276
+ Examples
277
+ --------
278
+ >>> import unxt as u
279
+ >>> q = u.Quantity([1, 2, 3], "m")
280
+ >>> q.size
281
+ 3
282
+
283
+ """
284
+ return self.value.size
285
+
286
+ @property
287
+ def T(self) -> "AbstractQuantity": # noqa: N802
288
+ """Transpose of the array.
289
+
290
+ Examples
291
+ --------
292
+ >>> import unxt as u
293
+ >>> q = u.Quantity([[0, 1], [1, 2]], "m")
294
+ >>> q.T
295
+ Quantity(Array([[0, 1],
296
+ [1, 2]], dtype=int32), unit='m')
297
+
298
+ """
299
+ return replace(self, value=self.value.T)
300
+
301
+ # required to override mixin methods
302
+ __eq__ = quax_blocks.NumpyEqMixin.__eq__ # type: ignore[assignment,unused-ignore]
303
+
304
+ # ---------------------------------------------------------------
305
+ # methods
306
+
307
+ def __bool__(self) -> bool:
308
+ """Convert a zero-dimensional array to a Python bool object.
309
+
310
+ Examples
311
+ --------
312
+ >>> import unxt as u
313
+ >>> q = u.Quantity([0, 1], "m")
314
+
315
+ >>> bool(q[0])
316
+ False
317
+
318
+ >>> bool(q[1])
319
+ True
320
+
321
+ """
322
+ return bool(self.value)
323
+
324
+ def __complex__(self) -> complex:
325
+ """Convert the array to a Python complex object.
326
+
327
+ Examples
328
+ --------
329
+ >>> import unxt as u
330
+ >>> q = u.Quantity(1, "")
331
+ >>> complex(q)
332
+ (1+0j)
333
+
334
+ """
335
+ return complex(uapi.ustrip("", self))
336
+
337
+ def __dlpack__(self, *args: Any, **kwargs: Any) -> Any:
338
+ """Export the array for consumption as a DLPack capsule."""
339
+ raise NotImplementedError
340
+
341
+ def __dlpack_device__(self, *args: Any, **kwargs: Any) -> Any:
342
+ """Return device type and device ID in DLPack format."""
343
+ raise NotImplementedError
344
+
345
+ def __float__(self) -> float:
346
+ """Convert the array to a Python float object.
347
+
348
+ Examples
349
+ --------
350
+ >>> import unxt as u
351
+ >>> q = u.Quantity(1, "")
352
+ >>> float(q)
353
+ 1.0
354
+
355
+ """
356
+ return float(uapi.ustrip("", self))
357
+
358
+ def __getitem__(self, key: Any) -> "AbstractQuantity":
359
+ """Get an item from the array.
360
+
361
+ This is a simple wrapper around the `__getitem__` method of the array,
362
+ calling `replace` to only update the value.
363
+
364
+ """
365
+ return replace(self, value=self.value[key])
366
+
367
+ def __index__(self) -> int:
368
+ """Convert a zero-dimensional integer array to a Python int object.
369
+
370
+ Examples
371
+ --------
372
+ >>> import unxt as u
373
+ >>> q = u.Quantity(1, "")
374
+ >>> q.__index__()
375
+ 1
376
+
377
+ """
378
+ return uapi.ustrip("", self).__index__()
379
+
380
+ def __int__(self) -> int:
381
+ """Convert a zero-dimensional array to a Python int object.
382
+
383
+ Examples
384
+ --------
385
+ >>> import unxt as u
386
+ >>> q = u.Quantity(1, "")
387
+ >>> int(q)
388
+ 1
389
+
390
+ """
391
+ return int(uapi.ustrip("", self))
392
+
393
+ def __setitem__(self, key: Any, value: Any) -> None:
394
+ """Set an item in the array.
395
+
396
+ This is a simple wrapper around the `__setitem__` method of the array.
397
+
398
+ Examples
399
+ --------
400
+ >>> import unxt as u
401
+ >>> q = u.Quantity([1, 2, 3], "m")
402
+ >>> try:
403
+ ... q[0] = 2
404
+ ... except Exception as e:
405
+ ... print("jax arrays do not support in-place item assignment")
406
+ jax arrays do not support in-place item assignment
407
+
408
+ """
409
+ self.value[key] = value
410
+
411
+ def to_device(self, device: None | jax.Device = None) -> "AbstractQuantity":
412
+ """Move the array to a new device.
413
+
414
+ Examples
415
+ --------
416
+ >>> import unxt as u
417
+ >>> q = u.Quantity(1, "m")
418
+ >>> q.to_device(None)
419
+ Quantity(Array(1, dtype=int32, weak_type=True), unit='m')
420
+
421
+ """
422
+ return replace(self, value=self.value.to_device(device))
423
+
424
+ # ===============================================================
425
+ # JAX API
426
+
427
+ def __iter__(self) -> Any:
428
+ """Iterate over the Quantity's value.
429
+
430
+ Examples
431
+ --------
432
+ >>> import unxt as u
433
+ >>> q = u.Quantity([1, 2, 3], "m")
434
+ >>> [x for x in q]
435
+ [Quantity(Array(1, dtype=int32), unit='m'),
436
+ Quantity(Array(2, dtype=int32), unit='m'),
437
+ Quantity(Array(3, dtype=int32), unit='m')]
438
+
439
+ """
440
+ yield from (self[i] for i in range(len(self.value)))
441
+
442
+ def argmax(self, *args: Any, **kwargs: Any) -> Array:
443
+ """Return the indices of the maximum value.
444
+
445
+ Examples
446
+ --------
447
+ >>> import unxt as u
448
+ >>> q = u.Quantity([1, 2, 3], "m")
449
+ >>> q.argmax()
450
+ Array(2, dtype=int32)
451
+
452
+ """
453
+ return self.value.argmax(*args, **kwargs)
454
+
455
+ def argmin(self, *args: Any, **kwargs: Any) -> Array:
456
+ """Return the indices of the minimum value.
457
+
458
+ Examples
459
+ --------
460
+ >>> import unxt as u
461
+ >>> q = u.Quantity([1, 2, 3], "m")
462
+ >>> q.argmin()
463
+ Array(0, dtype=int32)
464
+
465
+ """
466
+ return self.value.argmin(*args, **kwargs)
467
+
468
+ def astype(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
469
+ """Copy the array and cast to a specified dtype.
470
+
471
+ Examples
472
+ --------
473
+ >>> import unxt as u
474
+ >>> q = u.Quantity([1, 2, 3], "m")
475
+ >>> q.dtype
476
+ dtype('int32')
477
+
478
+ >>> q.astype(float)
479
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
480
+
481
+ """
482
+ return replace(self, value=self.value.astype(*args, **kwargs))
483
+
484
+ @ft.partial(property, doc=jax.Array.at.__doc__)
485
+ def at(self) -> "_QuantityIndexUpdateHelper":
486
+ return _QuantityIndexUpdateHelper(self) # type: ignore[arg-type]
487
+
488
+ def block_until_ready(self) -> "AbstractQuantity":
489
+ """Block until the array is ready.
490
+
491
+ Examples
492
+ --------
493
+ >>> import unxt as u
494
+ >>> q = u.Quantity(1, "m")
495
+ >>> q.block_until_ready() is q
496
+ True
497
+
498
+ """
499
+ _ = self.value.block_until_ready()
500
+ return self
501
+
502
+ def devices(self) -> set[jax.Device]:
503
+ """Return the devices where the array is located.
504
+
505
+ Examples
506
+ --------
507
+ >>> import unxt as u
508
+ >>> q = u.Quantity(1, "m")
509
+ >>> q.devices()
510
+ {CpuDevice(id=0)}
511
+
512
+ """
513
+ return self.value.devices()
514
+
515
+ def flatten(self) -> "AbstractQuantity":
516
+ """Return a flattened version of the array.
517
+
518
+ Examples
519
+ --------
520
+ >>> import unxt as u
521
+ >>> q = u.Quantity([[1, 2], [3, 4]], "m")
522
+ >>> q.flatten()
523
+ Quantity(Array([1, 2, 3, 4], dtype=int32), unit='m')
524
+
525
+ """
526
+ return replace(self, value=self.value.flatten())
527
+
528
+ def max(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
529
+ """Return the maximum value.
530
+
531
+ Examples
532
+ --------
533
+ >>> import unxt as u
534
+ >>> q = u.Quantity([1, 2, 3], "m")
535
+ >>> q.max()
536
+ Quantity(Array(3, dtype=int32), unit='m')
537
+
538
+ """
539
+ return replace(self, value=self.value.max(*args, **kwargs))
540
+
541
+ def mean(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
542
+ """Return the mean value.
543
+
544
+ Examples
545
+ --------
546
+ >>> import unxt as u
547
+ >>> q = u.Quantity([1, 2, 3], "m")
548
+ >>> q.mean()
549
+ Quantity(Array(2., dtype=float32), unit='m')
550
+
551
+ """
552
+ return replace(self, value=self.value.mean(*args, **kwargs))
553
+
554
+ def min(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
555
+ """Return the minimum value.
556
+
557
+ Examples
558
+ --------
559
+ >>> import unxt as u
560
+ >>> q = u.Quantity([1, 2, 3], "m")
561
+ >>> q.min()
562
+ Quantity(Array(1, dtype=int32), unit='m')
563
+
564
+ """
565
+ return replace(self, value=self.value.min(*args, **kwargs))
566
+
567
+ def ravel(self) -> "AbstractQuantity":
568
+ """Return a flattened version of the array.
569
+
570
+ Examples
571
+ --------
572
+ >>> import unxt as u
573
+ >>> q = u.Quantity([[1, 2], [3, 4]], "m")
574
+ >>> q.ravel()
575
+ Quantity(Array([1, 2, 3, 4], dtype=int32), unit='m')
576
+
577
+ """
578
+ return replace(self, value=self.value.ravel())
579
+
580
+ def reshape(self, *args: Any, order: str = "C") -> "AbstractQuantity":
581
+ """Return a reshaped version of the array.
582
+
583
+ Examples
584
+ --------
585
+ >>> import unxt as u
586
+ >>> q = u.Quantity([1, 2, 3, 4], "m")
587
+ >>> q.reshape(2, 2)
588
+ Quantity(Array([[1, 2],
589
+ [3, 4]], dtype=int32), unit='m')
590
+
591
+ """
592
+ __tracebackhide__ = True # pylint: disable=unused-variable
593
+ return replace(self, value=self.value.reshape(*args, order=order))
594
+
595
+ def round(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
596
+ """Round the array to the given number of decimals.
597
+
598
+ Examples
599
+ --------
600
+ >>> import unxt as u
601
+ >>> q = u.Quantity([1.1, 2.2, 3.3], "m")
602
+ >>> q.round(0)
603
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
604
+
605
+ """
606
+ return replace(self, value=self.value.round(*args, **kwargs))
607
+
608
+ @property
609
+ def sharding(self) -> Any:
610
+ """Return the sharding configuration of the array.
611
+
612
+ Examples
613
+ --------
614
+ >>> import unxt as u
615
+ >>> q = u.Quantity([1, 2, 3], "m")
616
+ >>> q.sharding
617
+ SingleDeviceSharding(device=..., memory_kind=...)
618
+
619
+ """
620
+ return self.value.sharding
621
+
622
+ def squeeze(self, *args: Any, **kwargs: Any) -> "AbstractQuantity":
623
+ """Return the array with all single-dimensional entries removed.
624
+
625
+ Examples
626
+ --------
627
+ >>> import unxt as u
628
+ >>> q = u.Quantity([[[1], [2], [3]]], "m")
629
+ >>> q.squeeze()
630
+ Quantity(Array([1, 2, 3], dtype=int32), unit='m')
631
+
632
+ """
633
+ return replace(self, value=self.value.squeeze(*args, **kwargs))
634
+
635
+ # ===============================================================
636
+ # Python stuff
637
+
638
+ def __hash__(self) -> int:
639
+ """Return the hash of the quantity.
640
+
641
+ This is the hash of the value and the unit, however since the value is
642
+ generally not hashable this will generally raise an exception.
643
+ Defining the `__hash__` method is required for the `AbstractQuantity` to
644
+ be considered immutable, e.g. by `dataclasses.dataclass`.
645
+
646
+ Examples
647
+ --------
648
+ >>> import unxt as u
649
+ >>> q = u.Quantity(1, "m")
650
+ >>> try:
651
+ ... hash(q)
652
+ ... except TypeError as e:
653
+ ... print(e)
654
+ unhashable type: 'jaxlib...ArrayImpl'
655
+
656
+ """
657
+ return hash((self.value, self.unit))
658
+
659
+ def __pdoc__(
660
+ self,
661
+ *,
662
+ named_unit: bool = True,
663
+ short_arrays: bool | Literal["compact"] = True,
664
+ use_short_name: bool = False,
665
+ **kwargs: Any,
666
+ ) -> wl.AbstractDoc:
667
+ """Return the Wadler-Lindig representation of this class.
668
+
669
+ This is used for the `__repr__` and `__str__` methods or when using the
670
+ `wadler_lindig` library.
671
+
672
+ Parameters
673
+ ----------
674
+ named_unit
675
+ If `True`, the unit is included in the representation as a named
676
+ argument. If `False`, the unit is included as a positional argument.
677
+ For example, ``Quantity(<array>, unit='m')`` versus
678
+ ``Quantity(<array>, 'm')``.
679
+ short_arrays
680
+ If 'compact', the array is printed in a compact form,
681
+ ``Quantity(Array([1., 2.], dtype=float32), unit='')`` to
682
+ ``Quantity([1., 2.], unit='')``. If `True` or `False` it uses the
683
+ default `wadler_lindig` behavior.
684
+ use_short_name
685
+ If `True` and the class has a ``short_name`` class variable, use the
686
+ short name instead of the full class name. For example, ``Q(...)``
687
+ instead of ``Quantity(...)``.
688
+ kwargs
689
+ Additional keyword arguments ``wadler_lindig.pdoc`` method for
690
+ formatting the value, stringified unit, and other fields.
691
+
692
+ Examples
693
+ --------
694
+ >>> import unxt as u
695
+ >>> import wadler_lindig as wl
696
+
697
+ >>> q = u.quantity.BareQuantity([1, 2, 3], "m")
698
+
699
+ The default pretty printing:
700
+
701
+ >>> wl.pprint(q)
702
+ BareQuantity(i32[3], unit='m')
703
+
704
+ The `str` method uses this as well:
705
+
706
+ >>> print(q)
707
+ BareQuantity(i32[3], unit='m')
708
+
709
+ Arrays can be printed in full:
710
+
711
+ >>> wl.pprint(q, short_arrays=False)
712
+ BareQuantity(Array([1, 2, 3], dtype=int32), unit='m')
713
+
714
+ The `repr` method uses this setting:
715
+
716
+ >>> print(repr(q))
717
+ BareQuantity(Array([1, 2, 3], dtype=int32), unit='m')
718
+
719
+ The units can be turned from a named argument to a positional argument
720
+ by setting `named_unit=False`:
721
+
722
+ >>> wl.pprint(q, named_unit=False)
723
+ BareQuantity(i32[3], 'm')
724
+
725
+ The arrays can be printed in a compact but informative form by setting
726
+ ``short_arrays="compact"``:
727
+
728
+ >>> wl.pprint(q, short_arrays="compact")
729
+ BareQuantity([1, 2, 3], unit='m')
730
+
731
+ The class short name can be used if available:
732
+
733
+ >>> wl.pprint(q, use_short_name=True)
734
+ BareQuantity(i32[3], unit='m')
735
+
736
+ """
737
+ # Class Name
738
+ if use_short_name and getattr(self, "short_name", None) is not None:
739
+ cls_name = wl.TextDoc(self.short_name)
740
+ else:
741
+ cls_name = wl.TextDoc(type_nonparametric(self).__name__)
742
+
743
+ # Object fields
744
+ fs = dict(field_items(self))
745
+ del fs["value"]
746
+ del fs["unit"]
747
+
748
+ # Customize value representation
749
+ # (backward compatibility for `compact_arrays` argument)
750
+ # TODO: remove in v2.0
751
+ if "compact_arrays" in kwargs:
752
+ short_arrays = kwargs.pop("compact_arrays")
753
+ warnings.warn(
754
+ "`compact_arrays` argument is deprecated; use "
755
+ "`short_arrays='compact'` instead.",
756
+ DeprecationWarning,
757
+ stacklevel=2,
758
+ )
759
+ match short_arrays:
760
+ case "compact":
761
+ kwargs["custom"] = custom_pdoc_noarray
762
+ case True:
763
+ kwargs["custom"] = custom_pdoc_no_kind
764
+ kwargs["short_arrays"] = True
765
+ case False:
766
+ kwargs["short_arrays"] = False
767
+
768
+ # Make the pdocs for the base and extra fields.
769
+ base_fields = [
770
+ wl.pdoc(self.value, **kwargs),
771
+ wl.TextDoc("unit=" if named_unit else "")
772
+ + wl.pdoc(self.unit.to_string(), **kwargs),
773
+ ]
774
+ extra_fields = wl.named_objs(tuple(fs.items()), **kwargs)
775
+
776
+ # Construct and return the Wadler-Lindig document.
777
+ return (
778
+ cls_name
779
+ + wl.TextDoc("(")
780
+ + wl.join(wl.comma, base_fields + extra_fields)
781
+ .group()
782
+ .nest(kwargs.get("indent", 4))
783
+ + wl.TextDoc(")")
784
+ )
785
+
786
+ def __repr__(self) -> str:
787
+ # TODO: make named_unit False?
788
+ return wl.pformat(self, short_arrays=False, named_unit=True, indent=4)
789
+
790
+ def __str__(self) -> str:
791
+ # TODO: make named_unit False?
792
+ return wl.pformat(self, short_arrays=True, named_unit=True, indent=4)
793
+
794
+
795
+ # -----------------------------------------------
796
+ # Register additional constructors
797
+
798
+
799
+ @AbstractQuantity.from_.dispatch
800
+ def from_(
801
+ cls: type[AbstractQuantity],
802
+ value: ArrayLike | ArrayLikeSequence,
803
+ unit: Any,
804
+ /,
805
+ *,
806
+ dtype: Any = None,
807
+ ) -> AbstractQuantity:
808
+ """Construct a `unxt.Quantity` from an array-like value and a unit.
809
+
810
+ :param value: The array-like value.
811
+ :param unit: The unit of the value.
812
+ :param dtype: The data type of the array (keyword-only).
813
+
814
+ Examples
815
+ --------
816
+ For this example we'll use the `Quantity` class. The same applies to
817
+ any subclass of `AbstractQuantity`.
818
+
819
+ >>> import jax.numpy as jnp
820
+ >>> import unxt as u
821
+
822
+ >>> x = jnp.array([1.0, 2, 3])
823
+ >>> u.Quantity.from_(x, "m")
824
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
825
+
826
+ >>> u.Quantity.from_([1.0, 2, 3], "m")
827
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
828
+
829
+ >>> u.Quantity.from_((1.0, 2, 3), "m")
830
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
831
+
832
+ """
833
+ # Dispatch on both arguments.
834
+ # Construct using the standard `__init__` method.
835
+ return cls(jnp.asarray(value, dtype=dtype), unit)
836
+
837
+
838
+ @AbstractQuantity.from_.dispatch
839
+ def from_(
840
+ cls: type[AbstractQuantity],
841
+ value: ArrayLike | ArrayLikeSequence,
842
+ /,
843
+ *,
844
+ unit: Any,
845
+ dtype: Any = None,
846
+ ) -> AbstractQuantity:
847
+ """Make a `unxt.AbstractQuantity` from an array-like value and a unit kwarg.
848
+
849
+ Examples
850
+ --------
851
+ For this example we'll use the `unxt.Quantity` class. The same applies
852
+ to any subclass of `unxt.AbstractQuantity`.
853
+
854
+ >>> import unxt as u
855
+ >>> u.Quantity.from_([1.0, 2, 3], unit="m")
856
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
857
+
858
+ """
859
+ # Dispatch on the `value` only. Dispatch to the full constructor.
860
+ return cls.from_(value, unit, dtype=dtype)
861
+
862
+
863
+ @AbstractQuantity.from_.dispatch
864
+ def from_(
865
+ cls: type[AbstractQuantity], *, value: Any, unit: Any, dtype: Any = None
866
+ ) -> AbstractQuantity:
867
+ """Construct a `AbstractQuantity` from value and unit kwargs.
868
+
869
+ Examples
870
+ --------
871
+ For this example we'll use the `Quantity` class. The same applies to
872
+ any subclass of `AbstractQuantity`.
873
+
874
+ >>> import unxt as u
875
+ >>> u.Quantity.from_(value=[1.0, 2, 3], unit="m")
876
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
877
+
878
+ """
879
+ # Dispatched on no argument. Dispatch to the full constructor.
880
+ return cls.from_(value, unit, dtype=dtype)
881
+
882
+
883
+ @AbstractQuantity.from_.dispatch
884
+ def from_(cls: type[AbstractQuantity], mapping: Mapping[str, Any]) -> AbstractQuantity:
885
+ """Construct a `Quantity` from a Mapping.
886
+
887
+ Examples
888
+ --------
889
+ For this example we'll use the `Quantity` class. The same applies to
890
+ any subclass of `AbstractQuantity`.
891
+
892
+ >>> import jax.numpy as jnp
893
+ >>> import unxt as u
894
+
895
+ >>> x = jnp.array([1.0, 2, 3])
896
+ >>> q = u.Quantity.from_({"value": x, "unit": "m"})
897
+ >>> q
898
+ Quantity(Array([1., 2., 3.], dtype=float32), unit='m')
899
+
900
+ >>> u.Quantity.from_({"value": q, "unit": "km"})
901
+ Quantity(Array([0.001, 0.002, 0.003], dtype=float32), unit='km')
902
+
903
+ """
904
+ # Dispatch on both arguments.
905
+ # Construct using the standard `__init__` method.
906
+ return cls.from_(**mapping)
907
+
908
+
909
+ @AbstractQuantity.from_.dispatch
910
+ def from_(
911
+ cls: type[AbstractQuantity],
912
+ value: AbstractQuantity,
913
+ unit: Any,
914
+ /,
915
+ *,
916
+ dtype: Any = None,
917
+ ) -> AbstractQuantity:
918
+ """Construct a `Quantity` from another `Quantity`.
919
+
920
+ The `value` is converted to the new `unit`.
921
+
922
+ Examples
923
+ --------
924
+ >>> import unxt as u
925
+
926
+ >>> q = u.Quantity(1, "m")
927
+ >>> u.Quantity.from_(q, "cm")
928
+ Quantity(Array(100., dtype=float32, ...), unit='cm')
929
+
930
+ """
931
+ value = jnp.asarray(uapi.uconvert(unit, value), dtype=dtype)
932
+ return cls(uapi.ustrip(unit, value), unit)
933
+
934
+
935
+ @AbstractQuantity.from_.dispatch
936
+ def from_(
937
+ cls: type[AbstractQuantity],
938
+ value: AbstractQuantity,
939
+ unit: None,
940
+ /,
941
+ *,
942
+ dtype: Any = None,
943
+ ) -> AbstractQuantity:
944
+ """Construct a `Quantity` from another `Quantity`.
945
+
946
+ The `value` is converted to the new `unit`.
947
+
948
+ Examples
949
+ --------
950
+ >>> import unxt as u
951
+
952
+ >>> q = u.Quantity(1, "m")
953
+ >>> u.Quantity.from_(q, None)
954
+ Quantity(Array(1, dtype=int32, ...), unit='m')
955
+
956
+ """
957
+ value = jnp.asarray(value, dtype=dtype)
958
+ unit = uapi.unit_of(value)
959
+ return cls(uapi.ustrip(unit, value), unit)
960
+
961
+
962
+ @AbstractQuantity.from_.dispatch
963
+ def from_(
964
+ cls: type[AbstractQuantity],
965
+ value: AbstractQuantity,
966
+ /,
967
+ *,
968
+ unit: Any | None = None,
969
+ dtype: Any = None,
970
+ ) -> AbstractQuantity:
971
+ """Construct a `Quantity` from another `Quantity`, with no unit change."""
972
+ unit = value.unit if unit is None else unit
973
+ value = jnp.asarray(uapi.uconvert(unit, value), dtype=dtype)
974
+ return cls(uapi.ustrip(unit, value), unit)
975
+
976
+
977
+ # -----------------------------------------------
978
+ # Promotion rules
979
+
980
+ add_promotion_rule(AbstractQuantity, AbstractQuantity, AbstractQuantity)
981
+
982
+
983
+ # ===============================================================
984
+ # Support for ``at``.
985
+
986
+
987
+ # `_QuantityIndexUpdateHelper` is defined up here because it is used in the
988
+ # runtime-checkable type annotation in `AbstractQuantity.at`.
989
+ # `_QuantityIndexUpdateRef` is defined after `AbstractQuantity` because it
990
+ # references `AbstractQuantity` in its runtime-checkable type annotations.
991
+ class _QuantityIndexUpdateHelper(_IndexUpdateHelper):
992
+ def __getitem__(self, index: Any) -> "_IndexUpdateRef":
993
+ return _QuantityIndexUpdateRef(self.array, index)
994
+
995
+ def __repr__(self) -> str:
996
+ """Return a string representation of the object.
997
+
998
+ Examples
999
+ --------
1000
+ >>> import unxt as u
1001
+ >>> q = u.Quantity([1, 2, 3, 4], "m")
1002
+ >>> q.at
1003
+ _QuantityIndexUpdateHelper(Quantity(Array([1, 2, 3, 4], dtype=int32), unit='m'))
1004
+
1005
+ """
1006
+ return f"_QuantityIndexUpdateHelper({self.array!r})"
1007
+
1008
+
1009
+ class _QuantityIndexUpdateRef(_IndexUpdateRef):
1010
+ # This is a subclass of `_IndexUpdateRef` that is used to implement the `at`
1011
+ # attribute of `AbstractQuantity`. See also `_QuantityIndexUpdateHelper`.
1012
+
1013
+ array: AbstractQuantity # type: ignore[assignment]
1014
+
1015
+ def __repr__(self) -> str:
1016
+ return super().__repr__().replace("_IndexUpdateRef", "_QuantityIndexUpdateRef")
1017
+
1018
+ @override
1019
+ def get( # type: ignore[override]
1020
+ self, *, fill_value: AbstractQuantity | None = None, **kw: Any
1021
+ ) -> AbstractQuantity:
1022
+ # TODO: by quaxified super
1023
+ value = self.array.value.at[self.index].get(
1024
+ fill_value=(
1025
+ fill_value
1026
+ if fill_value is None
1027
+ else uapi.ustrip(self.array.unit, fill_value)
1028
+ ),
1029
+ **kw,
1030
+ )
1031
+ return replace(self.array, value=value)
1032
+
1033
+ @override
1034
+ def set(self, values: AbstractQuantity, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1035
+ # TODO: by quaxified super
1036
+ value = self.array.value.at[self.index].set(
1037
+ uapi.ustrip(self.array.unit, values), **kw
1038
+ )
1039
+ return replace(self.array, value=value)
1040
+
1041
+ @override
1042
+ def apply(self, func: Any, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1043
+ raise NotImplementedError # TODO: by quaxified super
1044
+
1045
+ @override
1046
+ def add(self, values: AbstractQuantity, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1047
+ # TODO: by quaxified super
1048
+ value = self.array.value.at[self.index].add(
1049
+ uapi.ustrip(self.array.unit, values), **kw
1050
+ )
1051
+ return replace(self.array, value=value)
1052
+
1053
+ @override
1054
+ def multiply(self, values: ArrayLike, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1055
+ values = eqx.error_if(
1056
+ values, isinstance(values, AbstractQuantity), "values cannot be a Quantity"
1057
+ ) # TODO: can permit dimensionless quantities.
1058
+
1059
+ # TODO: by quaxified super
1060
+ value = self.array.value.at[self.index].multiply(values, **kw)
1061
+ return replace(self.array, value=value)
1062
+
1063
+ mul = multiply # type: ignore[assignment]
1064
+
1065
+ @override
1066
+ def divide(self, values: ArrayLike, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1067
+ values = eqx.error_if(
1068
+ values, isinstance(values, AbstractQuantity), "values cannot be a Quantity"
1069
+ ) # TODO: can permit dimensionless quantities.
1070
+
1071
+ # TODO: by quaxified super
1072
+ value = self.array.value.at[self.index].divide(values, **kw)
1073
+ return replace(self.array, value=value)
1074
+
1075
+ @override
1076
+ def power(self, values: ArrayLike, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1077
+ raise NotImplementedError
1078
+
1079
+ @override
1080
+ def min(self, values: AbstractQuantity, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1081
+ # TODO: by quaxified super
1082
+ value = self.array.value.at[self.index].min(
1083
+ uapi.ustrip(self.array.unit, values), **kw
1084
+ )
1085
+ return replace(self.array, value=value)
1086
+
1087
+ @override
1088
+ def max(self, values: AbstractQuantity, **kw: Any) -> AbstractQuantity: # type: ignore[override]
1089
+ # TODO: by quaxified super
1090
+ value = self.array.value.at[self.index].max(
1091
+ uapi.ustrip(self.array.unit, values), **kw
1092
+ )
1093
+ return replace(self.array, value=value)
1094
+
1095
+
1096
+ # This is public!
1097
+ def is_any_quantity(obj: Any, /) -> TypeGuard[AbstractQuantity]:
1098
+ """Check if an object is an instance of `unxt.quantity.AbstractQuantity`.
1099
+
1100
+ Examples
1101
+ --------
1102
+ >>> import unxt as u
1103
+
1104
+ >>> q = u.Quantity(1, "m")
1105
+ >>> is_any_quantity(q)
1106
+ True
1107
+
1108
+ """
1109
+ return isinstance(obj, AbstractQuantity)
1110
+
1111
+
1112
+ # TODO: replace with `equinox.internal.TreeWLCustom` when available.
1113
+ def custom_pdoc_no_kind(obj: Any) -> wl.AbstractDoc | None:
1114
+ """Return custom pdoc for ``AbstractQuantity`` objects."""
1115
+ if isinstance(obj, jax.Array):
1116
+ dtype = obj.dtype.name
1117
+ # Added in JAX 0.4.32 to `ShapeDtypeStruct`
1118
+ if getattr(obj, "weak_type", False):
1119
+ dtype = f"weak_{dtype}"
1120
+ return wl.array_summary(obj.shape, dtype, kind=None)
1121
+ return None
1122
+
1123
+
1124
+ def custom_pdoc_noarray(obj: Any) -> wl.AbstractDoc | None:
1125
+ """Return custom pdoc for ``AbstractQuantity`` objects."""
1126
+ if isinstance(obj, jax.Array):
1127
+ return wl.TextDoc(np.array2string(cast("np.ndarray", obj), separator=", "))
1128
+ return None
1129
+
1130
+
1131
+ @StaticValue.from_.dispatch
1132
+ def from_(cls: StaticValue, value: AbstractQuantity, /) -> StaticValue:
1133
+ """Disallow conversion of `AbstractQuantity` to a value."""
1134
+ msg = (
1135
+ f"Cannot convert '{type(value).__name__}' to a value. "
1136
+ "For a Quantity, use the `.from_` constructor instead."
1137
+ )
1138
+ raise TypeError(msg)
1139
+
1140
+
1141
+ @dispatch
1142
+ def convert_to_quantity_value(obj: AbstractQuantity, /) -> NoReturn:
1143
+ """Disallow conversion of `AbstractQuantity` to a value.
1144
+
1145
+ >>> import unxt as u
1146
+ >>> from unxt.quantity import convert_to_quantity_value
1147
+
1148
+ >>> try:
1149
+ ... convert_to_quantity_value(u.Quantity(1, "m"))
1150
+ ... except TypeError as e:
1151
+ ... print(e)
1152
+ Cannot convert 'Quantity[PhysicalType('length')]' to a value.
1153
+ For a Quantity, use the `.from_` constructor instead.
1154
+
1155
+ """
1156
+ msg = (
1157
+ f"Cannot convert '{type(obj).__name__}' to a value. "
1158
+ "For a Quantity, use the `.from_` constructor instead."
1159
+ )
1160
+ raise TypeError(msg)