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.
- unxt/__init__.py +79 -0
- unxt/_interop/__init__.py +18 -0
- unxt/_interop/optional_deps.py +13 -0
- unxt/_interop/unxt_interop_astropy/__init__.py +5 -0
- unxt/_interop/unxt_interop_astropy/custom_types.py +12 -0
- unxt/_interop/unxt_interop_astropy/dimensions.py +53 -0
- unxt/_interop/unxt_interop_astropy/quantity.py +308 -0
- unxt/_interop/unxt_interop_astropy/units.py +111 -0
- unxt/_interop/unxt_interop_gala/__init__.py +5 -0
- unxt/_interop/unxt_interop_gala/unitsystems.py +108 -0
- unxt/_interop/unxt_interop_mpl/__init__.py +81 -0
- unxt/_src/__init__.py +6 -0
- unxt/_src/dimensions.py +400 -0
- unxt/_src/experimental.py +234 -0
- unxt/_src/quantity/__init__.py +24 -0
- unxt/_src/quantity/angle.py +58 -0
- unxt/_src/quantity/base.py +1160 -0
- unxt/_src/quantity/base_angle.py +89 -0
- unxt/_src/quantity/base_parametric.py +290 -0
- unxt/_src/quantity/flag.py +137 -0
- unxt/_src/quantity/mixins.py +238 -0
- unxt/_src/quantity/quantity.py +123 -0
- unxt/_src/quantity/register_api.py +383 -0
- unxt/_src/quantity/register_conversions.py +95 -0
- unxt/_src/quantity/register_dispatches.py +246 -0
- unxt/_src/quantity/register_primitives.py +5075 -0
- unxt/_src/quantity/static_quantity.py +90 -0
- unxt/_src/quantity/unchecked.py +45 -0
- unxt/_src/quantity/value.py +289 -0
- unxt/_src/units.py +103 -0
- unxt/_src/unitsystems/__init__.py +40 -0
- unxt/_src/unitsystems/base.py +258 -0
- unxt/_src/unitsystems/builtin.py +190 -0
- unxt/_src/unitsystems/builtin_dimensions.py +21 -0
- unxt/_src/unitsystems/compare.py +13 -0
- unxt/_src/unitsystems/core.py +307 -0
- unxt/_src/unitsystems/flags.py +80 -0
- unxt/_src/unitsystems/realizations.py +73 -0
- unxt/_src/unitsystems/utils.py +95 -0
- unxt/_src/utils.py +112 -0
- unxt/_version.py +34 -0
- unxt/_version.pyi +2 -0
- unxt/dims.py +19 -0
- unxt/py.typed +0 -0
- unxt/quantity.py +172 -0
- unxt/setup_package.py +60 -0
- unxt/units.py +19 -0
- unxt/unitsystems.py +67 -0
- unxt-0.1.dev463.dist-info/METADATA +533 -0
- unxt-0.1.dev463.dist-info/RECORD +52 -0
- unxt-0.1.dev463.dist-info/WHEEL +4 -0
- unxt-0.1.dev463.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Unitsystem compatibility."""
|
|
2
|
+
# TODO: move to a compatibility module
|
|
3
|
+
|
|
4
|
+
__all__: tuple[str, ...] = ()
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from astropy.units import UnitBase as AstropyUnit
|
|
9
|
+
from gala.units import ( # pylint: disable=import-error
|
|
10
|
+
DimensionlessUnitSystem as GalaDimensionlessUnitSystem,
|
|
11
|
+
UnitSystem as GalaUnitSystem,
|
|
12
|
+
)
|
|
13
|
+
from plum import conversion_method, convert, dispatch
|
|
14
|
+
|
|
15
|
+
from unxt.unitsystems import AbstractUnitSystem, DimensionlessUnitSystem, dimensionless
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dispatch
|
|
19
|
+
def unitsystem(value: GalaUnitSystem, /) -> AbstractUnitSystem:
|
|
20
|
+
"""Return a `gala.units.UnitSystem` as a `unxt.AbstractUnitSystem`.
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
--------
|
|
24
|
+
>>> import unxt as u
|
|
25
|
+
>>> import gala.units as gu
|
|
26
|
+
>>> import astropy.units as apyu
|
|
27
|
+
|
|
28
|
+
>>> usys = gu.UnitSystem(apyu.km, apyu.s, apyu.Msun, apyu.radian)
|
|
29
|
+
|
|
30
|
+
>>> u.unitsystem(usys)
|
|
31
|
+
unitsystem(km, s, solMass, rad)
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
# Create a new unit system instance, and possibly class.
|
|
35
|
+
return unitsystem(*value._core_units) # noqa: SLF001
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dispatch
|
|
39
|
+
def unitsystem(_: GalaDimensionlessUnitSystem, /) -> DimensionlessUnitSystem:
|
|
40
|
+
"""Return a `gala.units.DimensionlessUnitSystem` as a `unxt.DimensionlessUnitSystem`.
|
|
41
|
+
|
|
42
|
+
Examples
|
|
43
|
+
--------
|
|
44
|
+
>>> import unxt as u
|
|
45
|
+
>>> from gala.units import DimensionlessUnitSystem
|
|
46
|
+
|
|
47
|
+
>>> usys = DimensionlessUnitSystem()
|
|
48
|
+
|
|
49
|
+
>>> u.unitsystem(usys)
|
|
50
|
+
DimensionlessUnitSystem()
|
|
51
|
+
|
|
52
|
+
""" # noqa: E501
|
|
53
|
+
return dimensionless
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Convert
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@conversion_method(type_from=GalaUnitSystem, type_to=AbstractUnitSystem) # type: ignore[arg-type]
|
|
61
|
+
def convert_gala_unitsystem_to_unxt_unitsystem(
|
|
62
|
+
usys: GalaUnitSystem, /
|
|
63
|
+
) -> AbstractUnitSystem:
|
|
64
|
+
"""Convert a `gala.units.UnitSystem` to a `unxt.AbstractUnitSystem`.
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> import unxt as u
|
|
69
|
+
>>> import gala.units as gu
|
|
70
|
+
>>> import astropy.units as apyu
|
|
71
|
+
|
|
72
|
+
>>> usys = gu.UnitSystem(apyu.km, apyu.s, apyu.Msun, apyu.radian)
|
|
73
|
+
>>> usys
|
|
74
|
+
<UnitSystem (km, s, solMass, rad)>
|
|
75
|
+
|
|
76
|
+
>>> convert(usys, u.AbstractUnitSystem)
|
|
77
|
+
unitsystem(km, s, solMass, rad)
|
|
78
|
+
|
|
79
|
+
"""
|
|
80
|
+
return unitsystem(usys)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@conversion_method(type_from=AbstractUnitSystem, type_to=GalaUnitSystem) # type: ignore[arg-type]
|
|
84
|
+
def convert_unxt_unitsystem_to_gala_unitsystem(
|
|
85
|
+
usys: AbstractUnitSystem, /
|
|
86
|
+
) -> GalaUnitSystem:
|
|
87
|
+
"""Convert a `unxt.AbstractUnitSystem` to a `gala.units.UnitSystem`.
|
|
88
|
+
|
|
89
|
+
Examples
|
|
90
|
+
--------
|
|
91
|
+
>>> import unxt as u
|
|
92
|
+
>>> import gala.units as gu
|
|
93
|
+
>>> import astropy.units as apyu
|
|
94
|
+
>>> from plum import convert
|
|
95
|
+
|
|
96
|
+
>>> usys = u.unitsystem(apyu.km, apyu.s, apyu.Msun, apyu.radian)
|
|
97
|
+
>>> usys
|
|
98
|
+
unitsystem(km, s, solMass, rad)
|
|
99
|
+
|
|
100
|
+
>>> convert(usys, gu.UnitSystem)
|
|
101
|
+
<UnitSystem (km, s, solMass, rad)>
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def convert_unit_to_apyu(u: Any) -> AstropyUnit:
|
|
106
|
+
return convert(u, AstropyUnit)
|
|
107
|
+
|
|
108
|
+
return GalaUnitSystem(*map(convert_unit_to_apyu, usys.base_units))
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Interoperability with `matplotlib`."""
|
|
2
|
+
# pylint: disable=import-error
|
|
3
|
+
|
|
4
|
+
# This module was adapted from both the `astropy` and `pint` package's
|
|
5
|
+
# implementations.
|
|
6
|
+
|
|
7
|
+
__all__ = ("UnxtConverter", "setup_matplotlib_support_for_unxt")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Sized
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import matplotlib.units
|
|
15
|
+
from jaxtyping import Array
|
|
16
|
+
from matplotlib.axes import Axes
|
|
17
|
+
|
|
18
|
+
from zeroth import zeroth
|
|
19
|
+
|
|
20
|
+
from unxt.quantity import AbstractQuantity, BareQuantity, ustrip
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class UnxtConverter(matplotlib.units.ConversionInterface): # type: ignore[misc]
|
|
25
|
+
"""Support `unxt` in `matplotlib`'s unit conversion framework.
|
|
26
|
+
|
|
27
|
+
This class is a subclass of `matplotlib.units.ConversionInterface`
|
|
28
|
+
and is used to convert `unxt.Quantity` instances for use with
|
|
29
|
+
`matplotlib`.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
axisinfo_kw: dict[str, Any] = field(
|
|
34
|
+
default_factory=lambda: {"format": "latex_inline"}
|
|
35
|
+
)
|
|
36
|
+
"""Keyword arguments to use when making the :meth:`matplotlib.units.AxisInfo`."""
|
|
37
|
+
|
|
38
|
+
def convert(self, obj: Any, unit: Any, axis: Axes) -> Array | list[Array]:
|
|
39
|
+
"""Convert *obj* using *unit* for the specified *axis*."""
|
|
40
|
+
# Hot-path Quantity
|
|
41
|
+
if isinstance(obj, AbstractQuantity):
|
|
42
|
+
return ustrip(unit, obj)
|
|
43
|
+
# Need to recurse (singly) into iterables
|
|
44
|
+
if isinstance(obj, Iterable):
|
|
45
|
+
return [self._convert_value(v, unit, axis) for v in obj]
|
|
46
|
+
|
|
47
|
+
return self._convert_value(obj, unit, axis)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _convert_value(obj: Any, unit: Any, axis: Axes) -> Array:
|
|
51
|
+
"""Handle converting using attached unit or falling back to axis units."""
|
|
52
|
+
if isinstance(obj, AbstractQuantity):
|
|
53
|
+
return ustrip(unit, obj)
|
|
54
|
+
|
|
55
|
+
return BareQuantity.from_(obj, axis.get_units()).ustrip(unit)
|
|
56
|
+
|
|
57
|
+
def axisinfo(self, unit: Any, _: Axes) -> matplotlib.units.AxisInfo:
|
|
58
|
+
"""Return axis information for this particular unit."""
|
|
59
|
+
fmt = self.axisinfo_kw.get("format", "latex_inline")
|
|
60
|
+
return matplotlib.units.AxisInfo(label=unit.to_string(fmt))
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def default_units(x: Any, _: Axes) -> Any:
|
|
64
|
+
"""Get the default unit to use for the given combination of unit and axis."""
|
|
65
|
+
if hasattr(x, "unit"):
|
|
66
|
+
return x.unit
|
|
67
|
+
if isinstance(x, Iterable) and isinstance(x, Sized):
|
|
68
|
+
x = zeroth(x)
|
|
69
|
+
return getattr(x, "units", None)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def setup_matplotlib_support_for_unxt(*, enable: bool = True) -> None:
|
|
73
|
+
"""Set up matplotlib's unit support for `unxt`.
|
|
74
|
+
|
|
75
|
+
:param enable: Whether support should be enabled or disabled.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
if enable:
|
|
79
|
+
matplotlib.units.registry[AbstractQuantity] = UnxtConverter()
|
|
80
|
+
else:
|
|
81
|
+
matplotlib.units.registry.pop(AbstractQuantity, None)
|
unxt/_src/__init__.py
ADDED
unxt/_src/dimensions.py
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Dimensions module.
|
|
2
|
+
|
|
3
|
+
This is the private implementation of the dimensions module.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__all__ = ("AbstractDimension", "dimension", "dimension_of")
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, NoReturn, TypeAlias
|
|
13
|
+
|
|
14
|
+
import astropy.units as apyu
|
|
15
|
+
from packaging.version import Version, parse as parse_version
|
|
16
|
+
from plum import dispatch
|
|
17
|
+
|
|
18
|
+
import unxt_api as uapi
|
|
19
|
+
|
|
20
|
+
AbstractDimension: TypeAlias = apyu.PhysicalType
|
|
21
|
+
|
|
22
|
+
# Regex pattern to detect PEMD operators (Parentheses, Exponentiation,
|
|
23
|
+
# Multiplication, Division). We match: ( ) * / ** but NOT space, +, or -
|
|
24
|
+
_PEMD_PATTERN = re.compile(r"[()*/]|\*\*")
|
|
25
|
+
|
|
26
|
+
# Regex pattern to match parenthesized dimension names that may contain spaces
|
|
27
|
+
_PAREN_DIM_PATTERN = re.compile(r"\(([^()]+)\)")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ===================================================================
|
|
31
|
+
# Construct the dimensions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _preprocess_dimension_string(expr: str, /) -> tuple[str, dict[str, str]]:
|
|
35
|
+
"""Preprocess dimension string to handle multi-word dimension names.
|
|
36
|
+
|
|
37
|
+
Converts (dimension name) to _dimN for parsing, then stores the mapping.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
expr : str
|
|
42
|
+
The expression string that may contain parenthesized dimension names.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
str
|
|
47
|
+
The preprocessed expression with valid Python identifiers.
|
|
48
|
+
dict[str, str]
|
|
49
|
+
Mapping from temporary identifiers to original dimension names.
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
dim_mapping: dict[str, str] = {}
|
|
53
|
+
counter = 0
|
|
54
|
+
|
|
55
|
+
def replace_paren_dim(match: re.Match[str], /) -> str:
|
|
56
|
+
nonlocal counter
|
|
57
|
+
# Strip whitespace from the captured dimension name to handle cases like
|
|
58
|
+
# "( amount of substance )" where users might include extra spaces
|
|
59
|
+
dim_name = match.group(1).strip()
|
|
60
|
+
temp_id = f"_dim{counter}"
|
|
61
|
+
dim_mapping[temp_id] = dim_name
|
|
62
|
+
counter += 1
|
|
63
|
+
return temp_id
|
|
64
|
+
|
|
65
|
+
preprocessed = _PAREN_DIM_PATTERN.sub(replace_paren_dim, expr)
|
|
66
|
+
return preprocessed, dim_mapping
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _eval_dimension_node( # noqa: C901
|
|
70
|
+
node: ast.AST,
|
|
71
|
+
/,
|
|
72
|
+
*,
|
|
73
|
+
dim_mapping: dict[str, str] | None = None,
|
|
74
|
+
) -> AbstractDimension:
|
|
75
|
+
"""Recursively evaluate AST nodes into dimensions or numeric values.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
node : ast.AST
|
|
80
|
+
AST node to evaluate.
|
|
81
|
+
dim_mapping : dict[str, str] | None
|
|
82
|
+
Mapping from temporary identifiers to original dimension names.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
AbstractDimension
|
|
87
|
+
Evaluated dimension.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
if dim_mapping is None:
|
|
91
|
+
dim_mapping = {}
|
|
92
|
+
|
|
93
|
+
if isinstance(node, ast.Expression):
|
|
94
|
+
return _eval_dimension_node(node.body, dim_mapping=dim_mapping)
|
|
95
|
+
|
|
96
|
+
if isinstance(node, ast.BinOp):
|
|
97
|
+
left = _eval_dimension_node(node.left, dim_mapping=dim_mapping)
|
|
98
|
+
|
|
99
|
+
if isinstance(node.op, ast.Pow):
|
|
100
|
+
# For powers, evaluate the exponent
|
|
101
|
+
# It can be a Constant or a UnaryOp (for negative exponents like **-1)
|
|
102
|
+
if isinstance(node.right, ast.Constant):
|
|
103
|
+
right = node.right.value
|
|
104
|
+
elif isinstance(node.right, ast.UnaryOp) and isinstance(
|
|
105
|
+
node.right.op, ast.USub
|
|
106
|
+
):
|
|
107
|
+
# Handle negative exponents like **-1
|
|
108
|
+
if isinstance(node.right.operand, ast.Constant):
|
|
109
|
+
right = -node.right.operand.value
|
|
110
|
+
else:
|
|
111
|
+
msg = "Power exponent must be a number"
|
|
112
|
+
raise TypeError(msg)
|
|
113
|
+
else:
|
|
114
|
+
msg = "Power exponent must be a number"
|
|
115
|
+
raise TypeError(msg)
|
|
116
|
+
|
|
117
|
+
if not isinstance(right, int | float):
|
|
118
|
+
msg = f"Power exponent must be a number, got: {type(right).__name__}"
|
|
119
|
+
raise TypeError(msg)
|
|
120
|
+
return left**right
|
|
121
|
+
|
|
122
|
+
# For other operators, evaluate right side normally
|
|
123
|
+
right = _eval_dimension_node(node.right, dim_mapping=dim_mapping)
|
|
124
|
+
|
|
125
|
+
if isinstance(node.op, ast.Mult):
|
|
126
|
+
return left * right
|
|
127
|
+
if isinstance(node.op, ast.Div):
|
|
128
|
+
return left / right
|
|
129
|
+
|
|
130
|
+
msg = f"Unsupported operator: {node.op.__class__.__name__}"
|
|
131
|
+
raise ValueError(msg)
|
|
132
|
+
|
|
133
|
+
if isinstance(node, ast.UnaryOp):
|
|
134
|
+
operand = _eval_dimension_node(node.operand, dim_mapping=dim_mapping)
|
|
135
|
+
if isinstance(node.op, ast.USub):
|
|
136
|
+
return operand**-1
|
|
137
|
+
msg = f"Unsupported unary operator: {node.op.__class__.__name__}"
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
|
|
140
|
+
if isinstance(node, ast.Name):
|
|
141
|
+
# Check if this is a temporary identifier that maps to a dimension name
|
|
142
|
+
dim_name = dim_mapping.get(node.id, node.id)
|
|
143
|
+
# This is a dimension name - recursively call dimension()
|
|
144
|
+
return uapi.dimension(dim_name)
|
|
145
|
+
|
|
146
|
+
if isinstance(node, ast.Constant):
|
|
147
|
+
# Handle numeric constants (for exponents)
|
|
148
|
+
return node.value
|
|
149
|
+
|
|
150
|
+
msg = f"Unsupported AST node: {node.__class__.__name__}"
|
|
151
|
+
raise ValueError(msg)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _parse_dimension_string(expr: str, /) -> AbstractDimension:
|
|
155
|
+
"""Parse a dimension string with mathematical operations.
|
|
156
|
+
|
|
157
|
+
Supports *, /, and ** operators following PEMDAS. Dimension names can
|
|
158
|
+
be parenthesized and may contain spaces, e.g., "(amount of substance)".
|
|
159
|
+
|
|
160
|
+
Parameters
|
|
161
|
+
----------
|
|
162
|
+
expr : str
|
|
163
|
+
Mathematical expression like "length / time**2" or
|
|
164
|
+
"(amount of substance) / (time)"
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
AbstractDimension
|
|
169
|
+
The resulting physical type from evaluating the expression.
|
|
170
|
+
|
|
171
|
+
Examples
|
|
172
|
+
--------
|
|
173
|
+
>>> _parse_dimension_string("length / time**2")
|
|
174
|
+
PhysicalType(...)
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
# Normalize whitespace
|
|
178
|
+
expr = expr.strip()
|
|
179
|
+
|
|
180
|
+
# Preprocess to handle multi-word dimension names in parentheses
|
|
181
|
+
preprocessed, dim_mapping = _preprocess_dimension_string(expr)
|
|
182
|
+
|
|
183
|
+
# Parse the expression into an AST
|
|
184
|
+
try:
|
|
185
|
+
tree = ast.parse(preprocessed, mode="eval")
|
|
186
|
+
except SyntaxError as e:
|
|
187
|
+
msg = f"Invalid dimension expression: {expr}"
|
|
188
|
+
raise ValueError(msg) from e
|
|
189
|
+
|
|
190
|
+
return _eval_dimension_node(tree, dim_mapping=dim_mapping)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dispatch
|
|
194
|
+
def dimension(obj: AbstractDimension, /) -> AbstractDimension:
|
|
195
|
+
"""Construct dimension from a dimension object.
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
>>> import unxt as u
|
|
200
|
+
>>> import astropy.units as apyu
|
|
201
|
+
|
|
202
|
+
>>> length = apyu.get_physical_type("length")
|
|
203
|
+
>>> length
|
|
204
|
+
PhysicalType('length')
|
|
205
|
+
|
|
206
|
+
>>> u.dimension(length) is length
|
|
207
|
+
True
|
|
208
|
+
|
|
209
|
+
"""
|
|
210
|
+
return obj
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dispatch
|
|
214
|
+
def dimension(obj: str, /) -> AbstractDimension:
|
|
215
|
+
"""Construct dimension from a string.
|
|
216
|
+
|
|
217
|
+
The string can be:
|
|
218
|
+
1. A simple dimension name (e.g., "length", "time", "mass")
|
|
219
|
+
2. A multi-word dimension name (e.g., "amount of substance", "absement")
|
|
220
|
+
3. A mathematical expression using *, /, and ** operators
|
|
221
|
+
|
|
222
|
+
Mathematical Expressions:
|
|
223
|
+
|
|
224
|
+
Expressions are evaluated using operator precedence (PEMDAS):
|
|
225
|
+
- ** (exponentiation, highest precedence)
|
|
226
|
+
- * and / (multiplication and division, equal precedence, left-to-right)
|
|
227
|
+
|
|
228
|
+
Parentheses are supported for grouping and for dimension names with spaces.
|
|
229
|
+
|
|
230
|
+
Operators Supported:
|
|
231
|
+
- `*` : Multiplication (e.g., "length * time")
|
|
232
|
+
- `/` : Division (e.g., "length / time")
|
|
233
|
+
- `**` : Exponentiation (e.g., "length**2")
|
|
234
|
+
|
|
235
|
+
Unsupported Operators:
|
|
236
|
+
- `+` and `-` are NOT supported as operators since dimensions are invariant
|
|
237
|
+
under addition and subtraction. They are treated as part of dimension names.
|
|
238
|
+
|
|
239
|
+
Rules for Dimension Names in Expressions:
|
|
240
|
+
- Single-word names don't need parentheses: "length * time"
|
|
241
|
+
- Multi-word names MUST be parenthesized: "(amount of substance) * time"
|
|
242
|
+
- Parenthesized single-word names are allowed: "(length) / (time)"
|
|
243
|
+
- Whitespace is flexible: "length / time", "length/time", "length / time**2"
|
|
244
|
+
|
|
245
|
+
Examples
|
|
246
|
+
--------
|
|
247
|
+
>>> from unxt.dims import dimension
|
|
248
|
+
|
|
249
|
+
**Simple dimension names:**
|
|
250
|
+
|
|
251
|
+
>>> dimension("length")
|
|
252
|
+
PhysicalType('length')
|
|
253
|
+
|
|
254
|
+
>>> dimension("time")
|
|
255
|
+
PhysicalType('time')
|
|
256
|
+
|
|
257
|
+
>>> dimension("mass")
|
|
258
|
+
PhysicalType('mass')
|
|
259
|
+
|
|
260
|
+
**Multi-word dimension names:**
|
|
261
|
+
|
|
262
|
+
>>> dimension("amount of substance")
|
|
263
|
+
PhysicalType('amount of substance')
|
|
264
|
+
|
|
265
|
+
**Mathematical expressions with single-word names:**
|
|
266
|
+
|
|
267
|
+
>>> dimension("length / time")
|
|
268
|
+
PhysicalType({'speed', 'velocity'})
|
|
269
|
+
|
|
270
|
+
>>> dimension("length**2")
|
|
271
|
+
PhysicalType('area')
|
|
272
|
+
|
|
273
|
+
>>> dimension("length * mass / time**2")
|
|
274
|
+
PhysicalType('force')
|
|
275
|
+
|
|
276
|
+
**Parenthesized expressions:**
|
|
277
|
+
|
|
278
|
+
>>> dimension("(length) / (time)")
|
|
279
|
+
PhysicalType({'speed', 'velocity'})
|
|
280
|
+
|
|
281
|
+
**Expressions with multi-word dimension names:**
|
|
282
|
+
|
|
283
|
+
>>> dimension("(amount of substance) / (time)")
|
|
284
|
+
PhysicalType('catalytic activity')
|
|
285
|
+
|
|
286
|
+
**Mixed expressions (multi-word with parentheses, single-word without):**
|
|
287
|
+
|
|
288
|
+
>>> dimension("length * (amount of substance)")
|
|
289
|
+
PhysicalType('unknown')
|
|
290
|
+
|
|
291
|
+
>>> dimension("(absement) / (time)")
|
|
292
|
+
PhysicalType('length')
|
|
293
|
+
|
|
294
|
+
See Also
|
|
295
|
+
--------
|
|
296
|
+
dimension_of : Get the dimension of an object
|
|
297
|
+
unxt.units : Unit specifications can also use dimension expressions
|
|
298
|
+
|
|
299
|
+
"""
|
|
300
|
+
# Check if the string contains PEMD operators using regex
|
|
301
|
+
# We only consider (), *, /, ** as operators - not space, +, or -
|
|
302
|
+
if _PEMD_PATTERN.search(obj):
|
|
303
|
+
return _parse_dimension_string(obj)
|
|
304
|
+
|
|
305
|
+
# Simple dimension name - use astropy directly
|
|
306
|
+
return apyu.get_physical_type(obj)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ===================================================================
|
|
310
|
+
# Get the dimension
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dispatch
|
|
314
|
+
def dimension_of(obj: Any, /) -> None:
|
|
315
|
+
"""Most objects have no dimension.
|
|
316
|
+
|
|
317
|
+
Examples
|
|
318
|
+
--------
|
|
319
|
+
>>> from unxt.dims import dimension_of
|
|
320
|
+
|
|
321
|
+
>>> print(dimension_of(1))
|
|
322
|
+
None
|
|
323
|
+
|
|
324
|
+
>>> print(dimension_of("length"))
|
|
325
|
+
None
|
|
326
|
+
|
|
327
|
+
"""
|
|
328
|
+
return None # noqa: RET501
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@dispatch
|
|
332
|
+
def dimension_of(obj: AbstractDimension, /) -> AbstractDimension:
|
|
333
|
+
"""Return the dimension of the given units.
|
|
334
|
+
|
|
335
|
+
Examples
|
|
336
|
+
--------
|
|
337
|
+
>>> from unxt.dims import dimension, dimension_of
|
|
338
|
+
|
|
339
|
+
>>> dimension_of(dimension("length"))
|
|
340
|
+
PhysicalType('length')
|
|
341
|
+
|
|
342
|
+
"""
|
|
343
|
+
return obj
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@dispatch
|
|
347
|
+
def dimension_of(obj: type, /) -> NoReturn:
|
|
348
|
+
"""Get the dimension of a type.
|
|
349
|
+
|
|
350
|
+
Examples
|
|
351
|
+
--------
|
|
352
|
+
>>> import unxt as u
|
|
353
|
+
|
|
354
|
+
>>> try:
|
|
355
|
+
... u.dimension_of(u.quantity.BareQuantity)
|
|
356
|
+
... except ValueError as e:
|
|
357
|
+
... print(e)
|
|
358
|
+
Cannot get the dimension of <class 'unxt._src.quantity.unchecked.BareQuantity'>.
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
msg = f"Cannot get the dimension of {obj}."
|
|
362
|
+
raise ValueError(msg)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# ===================================================================
|
|
366
|
+
# COMPAT
|
|
367
|
+
|
|
368
|
+
ASTROPY_LT_71 = parse_version(importlib.metadata.version("astropy")) < Version("7.1")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@dispatch
|
|
372
|
+
def name_of(dim: AbstractDimension, /) -> str:
|
|
373
|
+
"""Name of a dimension.
|
|
374
|
+
|
|
375
|
+
Examples
|
|
376
|
+
--------
|
|
377
|
+
>>> import unxt as u
|
|
378
|
+
|
|
379
|
+
>>> name_of(u.dimension("length"))
|
|
380
|
+
'length'
|
|
381
|
+
|
|
382
|
+
>>> name_of(u.dimension("speed"))
|
|
383
|
+
'speed'
|
|
384
|
+
|
|
385
|
+
>>> name_of(u.dimension("mass density"))
|
|
386
|
+
'mass density'
|
|
387
|
+
|
|
388
|
+
"""
|
|
389
|
+
if dim == "unknown":
|
|
390
|
+
ptid = dim._unit._physical_type_id # noqa: SLF001
|
|
391
|
+
name = " ".join(
|
|
392
|
+
f"{unit}{power}" if power != 1 else unit for unit, power in ptid
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
elif ASTROPY_LT_71:
|
|
396
|
+
name = dim._name_string_as_ordered_set().split("'")[1] # noqa: SLF001
|
|
397
|
+
else:
|
|
398
|
+
name = dim._physical_type[0] # noqa: SLF001
|
|
399
|
+
|
|
400
|
+
return name
|