ansys-units 0.12.dev0__tar.gz → 0.13.dev0__tar.gz

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.
Files changed (28) hide show
  1. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/PKG-INFO +10 -9
  2. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/pyproject.toml +18 -16
  3. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/_constants.py +2 -0
  4. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/cfg.yaml +38 -0
  5. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/extensions/pandas_extension.py +1 -1
  6. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/unit.py +13 -2
  7. ansys_units-0.13.dev0/src/ansys/units/unit_registry.py +370 -0
  8. ansys_units-0.12.dev0/src/ansys/units/unit_registry.py +0 -188
  9. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/LICENSE +0 -0
  10. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/README.rst +0 -0
  11. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/__init__.py +0 -0
  12. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/base_dimensions.py +0 -0
  13. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/common.py +0 -0
  14. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/dimensions.py +0 -0
  15. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/docs/.gitignore +0 -0
  16. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/extensions/__init__.py +0 -0
  17. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/py.typed +0 -0
  18. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity.py +0 -0
  19. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity_dimensions.py +0 -0
  20. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity_tables/bt_table.yaml +0 -0
  21. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity_tables/cgs_table.yaml +0 -0
  22. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity_tables/keys.py +0 -0
  23. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/quantity_tables/si_table.yaml +0 -0
  24. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/systems.py +0 -0
  25. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/variable_descriptor/__init__.py +0 -0
  26. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/variable_descriptor/_generated_variable_descriptor.py +0 -0
  27. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/variable_descriptor/strategy.py +0 -0
  28. {ansys_units-0.12.dev0 → ansys_units-0.13.dev0}/src/ansys/units/variable_descriptor/variable_descriptor.py +0 -0
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansys-units
3
- Version: 0.12.dev0
3
+ Version: 0.13.dev0
4
4
  Summary: Pythonic interface for units, unit systems, and unit conversions.
5
5
  Author-email: "ANSYS, Inc." <pyansys.core@ansys.com>
6
6
  Maintainer-email: "ANSYS, Inc." <pyansys.core@ansys.com>
7
7
  Requires-Python: >=3.10,<3.15
8
8
  Description-Content-Type: text/x-rst
9
+ License-Expression: MIT
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Intended Audience :: Science/Research
11
12
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
12
- Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  License-File: LICENSE
@@ -21,20 +21,21 @@ Requires-Dist: pandas>=2.2,<3.0 ; extra == "additional" and ( python_version < '
21
21
  Requires-Dist: pandas-stubs>=2 ; extra == "additional"
22
22
  Requires-Dist: pydantic>=2.11.7 ; extra == "additional"
23
23
  Requires-Dist: pydantic_core>=2.33.2 ; extra == "additional"
24
+ Requires-Dist: basedpyright ; extra == "ci-types"
24
25
  Requires-Dist: matplotlib==3.10.8 ; extra == "ci-types"
25
26
  Requires-Dist: pandas>=3.0.0 ; extra == "ci-types" and ( python_version >= '3.11')
26
27
  Requires-Dist: pandas>=2.2,<3.0 ; extra == "ci-types" and ( python_version < '3.11')
27
28
  Requires-Dist: pandas-stubs>=2 ; extra == "ci-types"
28
29
  Requires-Dist: pydantic>=2.11.7 ; extra == "ci-types"
29
30
  Requires-Dist: pydantic_core>=2.33.2 ; extra == "ci-types"
30
- Requires-Dist: pytest==9.0.2 ; extra == "ci-types"
31
- Requires-Dist: pytest-cov==7.0.0 ; extra == "ci-types"
31
+ Requires-Dist: pytest==9.0.3 ; extra == "ci-types"
32
+ Requires-Dist: pytest-cov==7.1.0 ; extra == "ci-types"
32
33
  Requires-Dist: pytest-mock==3.15.1 ; extra == "ci-types"
33
34
  Requires-Dist: pint==0.24.4 ; extra == "ci-types"
34
- Requires-Dist: black==26.1.0 ; extra == "ci-types"
35
- Requires-Dist: ansys-sphinx-theme==1.6.4 ; extra == "doc"
35
+ Requires-Dist: black==26.3.1 ; extra == "ci-types"
36
+ Requires-Dist: ansys-sphinx-theme==1.7.2 ; extra == "doc"
36
37
  Requires-Dist: pdf2image==1.17.0 ; extra == "doc"
37
- Requires-Dist: quarto-cli==1.8.25 ; extra == "doc"
38
+ Requires-Dist: quarto-cli==1.9.37 ; extra == "doc"
38
39
  Requires-Dist: matplotlib==3.10.8 ; extra == "doc"
39
40
  Requires-Dist: numpydoc==1.10.0 ; extra == "doc"
40
41
  Requires-Dist: Sphinx==7.4.7 ; extra == "doc"
@@ -45,8 +46,8 @@ Requires-Dist: numpy>=1.24,<3 ; extra == "tests"
45
46
  Requires-Dist: pandas>=3.0.0 ; extra == "tests" and ( python_version >= '3.11')
46
47
  Requires-Dist: pandas>=2.2,<3.0 ; extra == "tests" and ( python_version < '3.11')
47
48
  Requires-Dist: pandas-stubs>=2 ; extra == "tests"
48
- Requires-Dist: pytest==9.0.2 ; extra == "tests"
49
- Requires-Dist: pytest-cov==7.0.0 ; extra == "tests"
49
+ Requires-Dist: pytest==9.0.3 ; extra == "tests"
50
+ Requires-Dist: pytest-cov==7.1.0 ; extra == "tests"
50
51
  Requires-Dist: pytest-mock==3.15.1 ; extra == "tests"
51
52
  Requires-Dist: pint==0.24.4 ; extra == "tests"
52
53
  Project-URL: Documentation, https://units.docs.pyansys.com/
@@ -5,48 +5,48 @@ build-backend = "flit_core.buildapi"
5
5
  [project]
6
6
  # Check https://flit.readthedocs.io/en/latest/pyproject_toml.html for all available sections
7
7
  name = "ansys-units"
8
- version = "0.12.dev0"
8
+ version = "0.13.dev0"
9
9
  description = "Pythonic interface for units, unit systems, and unit conversions."
10
10
  readme = "README.rst"
11
11
  requires-python = ">=3.10,<3.15"
12
- license = {file = "LICENSE"}
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
13
14
  authors = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}]
14
15
  maintainers = [{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"}]
15
16
  classifiers = [
16
17
  "Development Status :: 4 - Beta",
17
18
  "Intended Audience :: Science/Research",
18
19
  "Topic :: Scientific/Engineering :: Information Analysis",
19
- "License :: OSI Approved :: MIT License",
20
20
  "Operating System :: OS Independent",
21
- "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3"
22
22
  ]
23
23
 
24
24
  dependencies = [
25
25
  "pyyaml>=6.0",
26
- "typing-extensions>=4.5.0",
26
+ "typing-extensions>=4.5.0"
27
27
  ]
28
28
 
29
29
  [project.optional-dependencies]
30
30
  doc = [
31
- "ansys-sphinx-theme==1.6.4",
31
+ "ansys-sphinx-theme==1.7.2",
32
32
  "pdf2image==1.17.0",
33
- "quarto-cli==1.8.25",
33
+ "quarto-cli==1.9.37",
34
34
  "matplotlib==3.10.8",
35
35
  "numpydoc==1.10.0",
36
36
  "Sphinx==7.4.7",
37
37
  "sphinx-copybutton==0.5.2",
38
38
  "sphinx-gallery==0.20.0",
39
- "sphinx-toggleprompt==0.6.0",
39
+ "sphinx-toggleprompt==0.6.0"
40
40
  ]
41
41
  tests = [
42
42
  "numpy>=1.24,<3",
43
43
  "pandas>=3.0.0; python_version >= '3.11'",
44
44
  "pandas>=2.2,<3.0; python_version < '3.11'",
45
45
  "pandas-stubs>=2",
46
- "pytest==9.0.2",
47
- "pytest-cov==7.0.0",
46
+ "pytest==9.0.3",
47
+ "pytest-cov==7.1.0",
48
48
  "pytest-mock==3.15.1",
49
- "pint==0.24.4",
49
+ "pint==0.24.4"
50
50
  ]
51
51
  additional = [
52
52
  "numpy>=1.24,<3",
@@ -54,21 +54,22 @@ additional = [
54
54
  "pandas>=2.2,<3.0; python_version < '3.11'",
55
55
  "pandas-stubs>=2",
56
56
  "pydantic>=2.11.7",
57
- "pydantic_core>=2.33.2",
57
+ "pydantic_core>=2.33.2"
58
58
  ]
59
59
  # getting versions of matplotlib and numpy that are compatible is hard so we have this for CI
60
60
  ci_types = [
61
+ "basedpyright",
61
62
  "matplotlib==3.10.8",
62
63
  "pandas>=3.0.0; python_version >= '3.11'",
63
64
  "pandas>=2.2,<3.0; python_version < '3.11'",
64
65
  "pandas-stubs>=2",
65
66
  "pydantic>=2.11.7",
66
67
  "pydantic_core>=2.33.2",
67
- "pytest==9.0.2",
68
- "pytest-cov==7.0.0",
68
+ "pytest==9.0.3",
69
+ "pytest-cov==7.1.0",
69
70
  "pytest-mock==3.15.1",
70
71
  "pint==0.24.4",
71
- "black==26.1.0",
72
+ "black==26.3.1"
72
73
  ]
73
74
 
74
75
  [project.urls]
@@ -84,7 +85,7 @@ name = "ansys.units"
84
85
  testpaths = ["tests"]
85
86
  addopts = "-v --cov=ansys.units --cov-report html:.cov/html --cov-report xml:.cov/xml --cov-report term --durations 0 --show-capture=all"
86
87
  markers = [
87
- "developer_only: tests that must not run on github",
88
+ "developer_only: tests that must not run on github"
88
89
  ]
89
90
 
90
91
  [tool.black]
@@ -125,4 +126,5 @@ reportUnknownMemberType = false
125
126
  reportExplicitAny = false
126
127
  reportPrivateUsage = false
127
128
  reportUnusedCallResult = false
129
+ reportUnknownArgumentType = false
128
130
  ignore = ["doc"]
@@ -66,6 +66,8 @@ class _DerivedUnitInfo(TypedDict):
66
66
 
67
67
  _derived_units: dict[str, _DerivedUnitInfo] = qc_data["derived_units"]
68
68
 
69
+ _aliases: dict[str, str] = qc_data.get("aliases", {})
70
+
69
71
  table_path = os.path.join(file_dir, "quantity_tables/si_table.yaml")
70
72
 
71
73
  with open(table_path, "r") as table:
@@ -284,3 +284,41 @@ derived_units:
284
284
  tonne:
285
285
  composition: kg
286
286
  factor: 1000
287
+
288
+ # Aliases
289
+ # -------
290
+ # Alternative names that map to existing base or derived units.
291
+ # Each alias resolves to its canonical unit name during parsing.
292
+ #
293
+ # <alias>: <canonical unit name>
294
+
295
+ aliases:
296
+ deg: degree
297
+ rad: radian
298
+ sec: s
299
+ lb_mass: lbm
300
+ pound: lb
301
+ meter: m
302
+ metre: m
303
+ kilogram: kg
304
+ gram: g
305
+ second: s
306
+ ampere: A
307
+ kelvin: K
308
+ candela: cd
309
+ steradian: sr
310
+ newton: N
311
+ pascal: Pa
312
+ watt: W
313
+ joule: J
314
+ volt: V
315
+ henry: H
316
+ siemens: S
317
+ weber: Wb
318
+ tesla: T
319
+ hertz: Hz
320
+ liter: l
321
+ litre: l
322
+ gallon: gal
323
+ foot: ft
324
+ feet: ft
@@ -516,7 +516,7 @@ class QuantityArray(ExtensionArray):
516
516
 
517
517
  @staticmethod
518
518
  def _create_arithmetic_method(
519
- op: Callable[..., Any], reflected: bool = False
519
+ op: Callable[[Any, Any], Any], reflected: bool = False
520
520
  ) -> Callable[..., Any]:
521
521
  """
522
522
  Create arithmetic method.
@@ -28,6 +28,7 @@ import os
28
28
  from typing import TYPE_CHECKING, Any, overload
29
29
 
30
30
  from ansys.units._constants import (
31
+ _aliases,
31
32
  _base_units,
32
33
  _BaseUnitInfo,
33
34
  _derived_units,
@@ -544,8 +545,13 @@ def _multiplier_check(unit_term: str) -> bool:
544
545
  ``True`` if the unit term contains a multiplier, ``False`` otherwise.
545
546
  """
546
547
  # Check if the unit term is not an existing base or derived unit.
547
- return unit_term and not (
548
- (unit_term in _base_units) or (unit_term in _derived_units)
548
+ return bool(
549
+ unit_term
550
+ and not (
551
+ (unit_term in _base_units)
552
+ or (unit_term in _derived_units)
553
+ or (unit_term in _aliases)
554
+ )
549
555
  )
550
556
 
551
557
 
@@ -638,6 +644,11 @@ def _filter_unit_term(unit_term: str) -> tuple[str, str, float]:
638
644
 
639
645
  base = unit_term
640
646
 
647
+ # Resolve alias to canonical unit name before multiplier detection
648
+ while unit_term in _aliases:
649
+ unit_term = _aliases[unit_term]
650
+ base = unit_term
651
+
641
652
  # strip multiplier and base from unit term
642
653
  has_multiplier = _multiplier_check(unit_term)
643
654
  if has_multiplier:
@@ -0,0 +1,370 @@
1
+ # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+ """Provides the ``UnitRegistry`` class and instance-scoped unit registration."""
23
+
24
+ from collections.abc import Generator, Mapping, Sequence
25
+ import math
26
+ import os
27
+ from typing import TYPE_CHECKING, Any
28
+
29
+ import yaml
30
+
31
+ from ansys.units._constants import (
32
+ _BaseUnitInfo,
33
+ )
34
+ from ansys.units._constants import (
35
+ _DerivedUnitInfo,
36
+ )
37
+ from ansys.units._constants import _aliases as _CONST_ALIASES
38
+ from ansys.units._constants import _base_units as _CONST_BASE_UNITS
39
+ from ansys.units._constants import _derived_units as _CONST_DERIVED_UNITS
40
+ from ansys.units.unit import Unit
41
+
42
+ if TYPE_CHECKING:
43
+ from ansys.units.quantity import Quantity # noqa: F401
44
+
45
+
46
+ class UnitRegistry:
47
+ """
48
+ A representation of valid ``Unit`` instances.
49
+
50
+ All base and derived units loaded from the configuration file, `cfg.yaml`,
51
+ on package initialization are provided by default.
52
+
53
+ Parameters
54
+ ----------
55
+ config: str, optional
56
+ Path of a ``YAML`` configuration file, which can be a custom file, and
57
+ defaults to the provided file, ``cfg.yaml``. Custom configuration files
58
+ must match the format of the default configuration file.
59
+ other: dict, optional
60
+ Dictionary for additional units (uses config file format).
61
+ custom_units : list of dict, optional
62
+ List of custom units to register during construction. Each dict must
63
+ have keys ``"unit"``, ``"composition"``, and ``"factor"`` matching
64
+ the :meth:`register_unit` signature.
65
+
66
+ Examples
67
+ --------
68
+ >>> from ansys.units import UnitRegistry, Unit
69
+ >>> ureg = UnitRegistry()
70
+ >>> assert ureg.kg == Unit(units="kg")
71
+ >>> fps = Unit("ft s^-1")
72
+ >>> ureg.foot_per_sec = fps
73
+
74
+ Register custom units at construction:
75
+
76
+ >>> ur = UnitRegistry(custom_units=[
77
+ ... {"unit": "micron", "composition": "m", "factor": 1e-6},
78
+ ... {"unit": "inch", "composition": "m", "factor": 0.0254},
79
+ ... ])
80
+ >>> q = ur.Quantity(10, "micron")
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ config: str = "cfg.yaml",
86
+ other: Mapping[
87
+ str, Mapping[str, Any]
88
+ ] = {}, # pyright: ignore[reportCallInDefaultInitializer]
89
+ custom_units: Sequence[Mapping[str, Any]] | None = None,
90
+ ):
91
+ unitdict = dict(other)
92
+
93
+ if config:
94
+ file_dir = os.path.dirname(__file__)
95
+ qc_path = os.path.join(file_dir, config)
96
+
97
+ with open(qc_path, "r") as qc_yaml:
98
+ qc_data = yaml.safe_load(qc_yaml)
99
+ _base_units: dict[str, _BaseUnitInfo] = qc_data["base_units"]
100
+ _derived_units: dict[str, _DerivedUnitInfo] = qc_data["derived_units"]
101
+
102
+ unitdict |= _base_units | _derived_units
103
+
104
+ for unit_name in unitdict:
105
+ # Prevent overriding attributes already present on this instance
106
+ if hasattr(self, unit_name):
107
+ raise UnitNameAlreadyRegistered(unit_name)
108
+
109
+ cfg = unitdict[unit_name]
110
+ if unit_name in _CONST_BASE_UNITS or unit_name in _CONST_DERIVED_UNITS:
111
+ object.__setattr__(self, unit_name, Unit(unit_name, cfg))
112
+ else:
113
+ # For dynamically registered units not present in constants, build
114
+ # from their composition so dimensions/si data are correct, then
115
+ # override the name to the desired symbol and attach config.
116
+ if "composition" in cfg:
117
+ composed = Unit(units=str(cfg["composition"]))
118
+ obj = Unit(copy_from=composed)
119
+ obj._name = unit_name
120
+ object.__setattr__(self, unit_name, obj)
121
+ else:
122
+ object.__setattr__(self, unit_name, Unit(unit_name, cfg))
123
+
124
+ # Register custom units using the same logic as register_unit
125
+ if custom_units:
126
+ for cu in custom_units:
127
+ self.register_unit(
128
+ unit=cu["unit"],
129
+ composition=cu["composition"],
130
+ factor=cu["factor"],
131
+ )
132
+
133
+ def __str__(self):
134
+ returned_string = ""
135
+ attrs = self.__dict__
136
+ for key in attrs:
137
+ returned_string += f"{key}, "
138
+ return returned_string
139
+
140
+ if TYPE_CHECKING:
141
+
142
+ def __getattr__(self, name: str) -> Unit: ...
143
+
144
+ def __setattr__(self, name: str, unit: Any) -> None:
145
+ if hasattr(self, name):
146
+ raise UnitNameAlreadyRegistered(name)
147
+ self.__dict__[name] = unit
148
+
149
+ def __iter__(self) -> Generator[str]:
150
+ yield from self.__dict__
151
+
152
+ def get_unit(self, name: str) -> Unit:
153
+ """
154
+ Look up a unit by name from this registry.
155
+
156
+ Checks instance-registered units first, then falls back to built-in
157
+ units and compound unit strings. This allows string-based lookup of
158
+ custom registered units as well as standard unit expressions.
159
+
160
+ Parameters
161
+ ----------
162
+ name : str
163
+ The unit name or compound unit string to look up (e.g., "micron",
164
+ "m", "kg mol^-1").
165
+
166
+ Returns
167
+ -------
168
+ Unit
169
+ The unit object.
170
+
171
+ Raises
172
+ ------
173
+ AttributeError
174
+ If the unit is not found in this registry or cannot be parsed.
175
+ """
176
+ # First check instance-registered units
177
+ if name in self.__dict__:
178
+ return self.__dict__[name]
179
+ # Fall back to creating a Unit (handles built-ins and compound strings)
180
+ try:
181
+ return Unit(name)
182
+ except Exception as e:
183
+ raise AttributeError(f"Unit `{name}` not found in this registry.") from e
184
+
185
+ def Quantity(
186
+ self,
187
+ value: "int | float | Sequence[float]",
188
+ units: "str | Unit",
189
+ ) -> "Quantity":
190
+ """
191
+ Create a Quantity using this registry's units.
192
+
193
+ This method allows creating quantities with instance-registered units
194
+ using string names.
195
+
196
+ Parameters
197
+ ----------
198
+ value : int, float, or sequence
199
+ The numeric value.
200
+ units : str or Unit
201
+ The unit name (looked up in this registry) or a Unit object.
202
+
203
+ Returns
204
+ -------
205
+ Quantity
206
+ The created quantity.
207
+
208
+ Examples
209
+ --------
210
+ >>> ur = UnitRegistry()
211
+ >>> ur.register_unit(unit="micron", composition="m", factor=1e-6)
212
+ >>> q = ur.Quantity(1, "micron")
213
+ >>> print(q)
214
+ 1.0 micron
215
+ """
216
+ from ansys.units.quantity import Quantity as _Quantity
217
+
218
+ if isinstance(units, str):
219
+ units = self.get_unit(units)
220
+ return _Quantity(value, units)
221
+
222
+ def register_unit(
223
+ self,
224
+ *,
225
+ unit: str,
226
+ composition: str,
227
+ factor: float,
228
+ ) -> Unit:
229
+ """
230
+ Register a new derived unit on this ``UnitRegistry`` instance.
231
+
232
+ This is instance-scoped: it affects only this registry and does not
233
+ mutate global state or other registries. The registered unit can be
234
+ accessed as an attribute (e.g., ``ur.micron``) or via
235
+ :meth:`get_unit` for string-based lookup.
236
+
237
+ Parameters
238
+ ----------
239
+ unit: str
240
+ The symbol/name of the new unit (e.g., "micron").
241
+ composition: str
242
+ A valid unit composition using existing configured units (e.g., "m").
243
+ factor: float
244
+ Scale factor that relates the composition to this unit.
245
+
246
+ Returns
247
+ -------
248
+ Unit
249
+ The registered unit attached on this instance.
250
+
251
+ Raises
252
+ ------
253
+ UnitNameAlreadyRegistered
254
+ If a unit with the same name already exists on this instance or
255
+ as a built-in. This is a name-only check and does not detect
256
+ equivalent definitions with different names.
257
+ ValueError
258
+ If ``unit`` is empty or ``factor`` is not finite.
259
+
260
+ Notes
261
+ -----
262
+ The name collision check is superficial (name-only). Two units with
263
+ different names but equivalent definitions (same composition and factor)
264
+ can both be registered without error.
265
+
266
+ Examples
267
+ --------
268
+ >>> ur = UnitRegistry()
269
+ >>> ur.register_unit(unit="micron", composition="m", factor=1e-6)
270
+ >>> q = ur.Quantity(1, "micron") # Use registry's Quantity method
271
+ """
272
+ unit = unit.strip()
273
+ if not unit:
274
+ raise ValueError("`unit` must be a non-empty string.")
275
+ f = float(factor)
276
+ if not math.isfinite(f):
277
+ raise ValueError("`factor` must be a finite number.")
278
+
279
+ # Name-only collision check against built-ins and this instance
280
+ if (
281
+ unit in _CONST_BASE_UNITS
282
+ or unit in _CONST_DERIVED_UNITS
283
+ or hasattr(self, unit)
284
+ ):
285
+ raise UnitNameAlreadyRegistered(unit)
286
+
287
+ composed = Unit(units=str(composition))
288
+ obj = Unit(copy_from=composed)
289
+
290
+ obj._si_scaling_factor *= f
291
+ obj._name = unit
292
+ object.__setattr__(self, unit, obj)
293
+ return obj
294
+
295
+ def register_alias(
296
+ self,
297
+ alias: str,
298
+ canonical: str,
299
+ ) -> None:
300
+ """
301
+ Register a new alias for an existing unit.
302
+
303
+ The alias is added to the global alias table so that it can be used
304
+ anywhere a unit string is accepted (``Unit()``, ``Quantity()``, etc.).
305
+
306
+ Parameters
307
+ ----------
308
+ alias : str
309
+ The alias name (e.g., ``"deg"``).
310
+ canonical : str
311
+ The canonical unit name the alias resolves to
312
+ (e.g., ``"degree"``). Must be a configured base or derived unit,
313
+ or an existing alias.
314
+
315
+ Raises
316
+ ------
317
+ AliasAlreadyRegistered
318
+ If ``alias`` is already registered as a unit or alias.
319
+ ValueError
320
+ If ``canonical`` is not a known unit or alias.
321
+ """
322
+ alias = alias.strip()
323
+ canonical = canonical.strip()
324
+ if not alias:
325
+ raise ValueError("`alias` must be a non-empty string.")
326
+ if not canonical:
327
+ raise ValueError("`canonical` must be a non-empty string.")
328
+
329
+ # Prevent shadowing existing units
330
+ if (
331
+ alias in _CONST_BASE_UNITS
332
+ or alias in _CONST_DERIVED_UNITS
333
+ or alias in _CONST_ALIASES
334
+ ):
335
+ raise AliasAlreadyRegistered(alias)
336
+
337
+ # Validate the canonical target exists
338
+ if not (
339
+ canonical in _CONST_BASE_UNITS
340
+ or canonical in _CONST_DERIVED_UNITS
341
+ or canonical in _CONST_ALIASES
342
+ ):
343
+ raise ValueError(
344
+ f"`{canonical}` is not a configured unit or existing alias."
345
+ )
346
+
347
+ _CONST_ALIASES[alias] = canonical
348
+
349
+
350
+ class AliasAlreadyRegistered(ValueError):
351
+ """Raised when an alias conflicts with an existing unit or alias."""
352
+
353
+ def __init__(self, name: str):
354
+ super().__init__(
355
+ f"Unable to register alias `{name}`: it already exists as a unit or alias."
356
+ )
357
+
358
+
359
+ class UnitNameAlreadyRegistered(ValueError):
360
+ """
361
+ Raised when a unit name conflicts with an existing name.
362
+
363
+ This is a name-only check. Units with different names but equivalent definitions
364
+ (same composition and factor) are not detected.
365
+ """
366
+
367
+ def __init__(self, name: str):
368
+ super().__init__(
369
+ f"Unable to register `{name}`: a unit with this name already exists."
370
+ )
@@ -1,188 +0,0 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
2
- # SPDX-License-Identifier: MIT
3
- #
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
22
- """Provides the ``UnitRegistry`` class and instance-scoped unit registration."""
23
-
24
- from collections.abc import Generator, Mapping
25
- import math
26
- import os
27
- from typing import TYPE_CHECKING, Any
28
-
29
- import yaml
30
-
31
- from ansys.units._constants import (
32
- _BaseUnitInfo,
33
- )
34
- from ansys.units._constants import (
35
- _DerivedUnitInfo,
36
- )
37
- from ansys.units._constants import _base_units as _CONST_BASE_UNITS
38
- from ansys.units._constants import _derived_units as _CONST_DERIVED_UNITS
39
- from ansys.units.unit import Unit
40
-
41
-
42
- class UnitRegistry:
43
- """
44
- A representation of valid ``Unit`` instances.
45
-
46
- All base and derived units loaded from the configuration file, `cfg.yaml`,
47
- on package initialization are provided by default.
48
-
49
- Parameters
50
- ----------
51
- config: str, optional
52
- Path of a ``YAML`` configuration file, which can be a custom file, and
53
- defaults to the provided file, ``cfg.yaml``. Custom configuration files
54
- must match the format of the default configuration file.
55
- other: dict, optional
56
- Dictionary for additional units.
57
-
58
- Examples
59
- --------
60
- >>> from ansys.units import UnitRegistry, Unit
61
- >>> ureg = UnitRegistry()
62
- >>> assert ureg.kg == Unit(units="kg")
63
- >>> fps = Unit("ft s^-1")
64
- >>> ureg.foot_per_sec = fps
65
- """
66
-
67
- def __init__(
68
- self,
69
- config: str = "cfg.yaml",
70
- other: Mapping[
71
- str, Mapping[str, Any]
72
- ] = {}, # pyright: ignore[reportCallInDefaultInitializer]
73
- ):
74
- unitdict = dict(other)
75
-
76
- if config:
77
- file_dir = os.path.dirname(__file__)
78
- qc_path = os.path.join(file_dir, config)
79
-
80
- with open(qc_path, "r") as qc_yaml:
81
- qc_data = yaml.safe_load(qc_yaml)
82
- _base_units: dict[str, _BaseUnitInfo] = qc_data["base_units"]
83
- _derived_units: dict[str, _DerivedUnitInfo] = qc_data["derived_units"]
84
-
85
- unitdict |= _base_units | _derived_units
86
-
87
- for unit_name in unitdict:
88
- # Prevent overriding attributes already present on this instance
89
- if hasattr(self, unit_name):
90
- raise UnitAlreadyRegistered(unit_name)
91
-
92
- cfg = unitdict[unit_name]
93
- if unit_name in _CONST_BASE_UNITS or unit_name in _CONST_DERIVED_UNITS:
94
- object.__setattr__(self, unit_name, Unit(unit_name, cfg))
95
- else:
96
- # For dynamically registered units not present in constants, build
97
- # from their composition so dimensions/si data are correct, then
98
- # override the name to the desired symbol and attach config.
99
- if "composition" in cfg:
100
- composed = Unit(units=str(cfg["composition"]))
101
- obj = Unit(copy_from=composed)
102
- obj._name = unit_name
103
- object.__setattr__(self, unit_name, obj)
104
- else:
105
- object.__setattr__(self, unit_name, Unit(unit_name, cfg))
106
-
107
- def __str__(self):
108
- returned_string = ""
109
- attrs = self.__dict__
110
- for key in attrs:
111
- returned_string += f"{key}, "
112
- return returned_string
113
-
114
- if TYPE_CHECKING:
115
-
116
- def __getattr__(self, name: str) -> Unit: ...
117
-
118
- def __setattr__(self, name: str, unit: Any) -> None:
119
- if hasattr(self, name):
120
- raise UnitAlreadyRegistered(name)
121
- self.__dict__[name] = unit
122
-
123
- def __iter__(self) -> Generator[str]:
124
- yield from self.__dict__
125
-
126
- def register_unit(
127
- self,
128
- *,
129
- unit: str,
130
- composition: str,
131
- factor: float,
132
- ) -> Unit:
133
- """
134
- Register a new derived unit on this ``UnitRegistry`` instance.
135
-
136
- This is instance-scoped: it affects only this registry and does not
137
- mutate global state or other registries.
138
-
139
- Parameters
140
- ----------
141
- unit: str
142
- The symbol/name of the new unit (e.g., "Q"). Preferred keyword.
143
- composition: str
144
- A valid unit composition using existing configured units (e.g., "N m").
145
- factor: float
146
- Scale factor that relates the composition to this unit.
147
-
148
- Returns
149
- -------
150
- Unit
151
- The registered unit attached on this instance.
152
-
153
- Raises
154
- ------
155
- UnitAlreadyRegistered
156
- If a unit with the same name already exists on this instance or is built-in.
157
- ValueError
158
- If ``unit`` is empty or ``factor`` is not finite.
159
- """
160
- unit = unit.strip()
161
- if not unit:
162
- raise ValueError("`unit` must be a non-empty string.")
163
- f = float(factor)
164
- if not math.isfinite(f):
165
- raise ValueError("`factor` must be a finite number.")
166
-
167
- # Prevent overriding built-ins or existing attributes on this instance
168
- if (
169
- unit in _CONST_BASE_UNITS
170
- or unit in _CONST_DERIVED_UNITS
171
- or hasattr(self, unit)
172
- ):
173
- raise UnitAlreadyRegistered(unit)
174
-
175
- composed = Unit(units=str(composition))
176
- obj = Unit(copy_from=composed)
177
-
178
- obj._si_scaling_factor *= f
179
- obj._name = unit
180
- object.__setattr__(self, unit, obj)
181
- return obj
182
-
183
-
184
- class UnitAlreadyRegistered(ValueError):
185
- """Raised when a unit has previously been registered."""
186
-
187
- def __init__(self, name: str):
188
- super().__init__(f"Unable to override `{name}` it has already been registered.")
File without changes