unitx 0.8.0__tar.gz → 0.8.2__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 (66) hide show
  1. {unitx-0.8.0 → unitx-0.8.2}/.github/workflows/ci.yml +7 -3
  2. {unitx-0.8.0 → unitx-0.8.2}/.github/workflows/release.yml +1 -0
  3. {unitx-0.8.0 → unitx-0.8.2}/CHANGELOG.md +35 -0
  4. unitx-0.8.2/PKG-INFO +268 -0
  5. unitx-0.8.2/README.md +244 -0
  6. {unitx-0.8.0 → unitx-0.8.2}/conftest.py +4 -1
  7. unitx-0.8.2/docs/images/blackbody.png +0 -0
  8. unitx-0.8.2/docs/migration.md +16 -0
  9. unitx-0.8.2/docs/numpy.md +225 -0
  10. unitx-0.8.2/docs/scalars.md +106 -0
  11. unitx-0.8.0/README.md → unitx-0.8.2/docs/units.md +7 -274
  12. unitx-0.8.2/examples/blackbody.py +20 -0
  13. {unitx-0.8.0 → unitx-0.8.2}/pyproject.toml +1 -1
  14. unitx-0.8.2/pyrightconfig.asarray.json +5 -0
  15. unitx-0.8.2/pyrightconfig.examples.json +5 -0
  16. {unitx-0.8.0 → unitx-0.8.2}/pyrightconfig.json +1 -0
  17. unitx-0.8.2/pyrightconfig.scalar.json +5 -0
  18. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/_numerics.py +20 -17
  19. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/_numpy.py +85 -10
  20. unitx-0.8.2/src/unitx/_numpy_typing.py +21 -0
  21. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/quantity.py +57 -44
  22. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/unit.py +3 -2
  23. unitx-0.8.2/tests/example_typing/check_blackbody.py +27 -0
  24. unitx-0.8.2/tests/numpy_typing/check_numpy.py +127 -0
  25. {unitx-0.8.0 → unitx-0.8.2}/tests/test_numpy.py +313 -6
  26. unitx-0.8.2/tests/test_typing.py +82 -0
  27. unitx-0.8.0/PKG-INFO +0 -651
  28. unitx-0.8.0/docs/numpy.md +0 -133
  29. unitx-0.8.0/tests/numpy_typing/check_numpy.py +0 -61
  30. unitx-0.8.0/tests/test_typing.py +0 -56
  31. {unitx-0.8.0 → unitx-0.8.2}/.gitignore +0 -0
  32. {unitx-0.8.0 → unitx-0.8.2}/LICENSE +0 -0
  33. {unitx-0.8.0 → unitx-0.8.2}/pyrightconfig.numpy.json +0 -0
  34. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/__init__.py +0 -0
  35. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/_constnamespace.py +0 -0
  36. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/_unicode.py +0 -0
  37. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/dimension.py +0 -0
  38. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/exceptions.py +0 -0
  39. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/factor.py +0 -0
  40. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/monomial.py +0 -0
  41. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/py.typed +0 -0
  42. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/si/__init__.py +0 -0
  43. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/si/bases.py +0 -0
  44. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/si/constants.py +0 -0
  45. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/si/units.py +0 -0
  46. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/singleunit.py +0 -0
  47. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/unitparser.py +0 -0
  48. {unitx-0.8.0 → unitx-0.8.2}/src/unitx/unitregistry.py +0 -0
  49. {unitx-0.8.0 → unitx-0.8.2}/tests/__init__.py +0 -0
  50. {unitx-0.8.0 → unitx-0.8.2}/tests/test_builtin_definitions.py +0 -0
  51. {unitx-0.8.0 → unitx-0.8.2}/tests/test_dimension.py +0 -0
  52. {unitx-0.8.0 → unitx-0.8.2}/tests/test_factor.py +0 -0
  53. {unitx-0.8.0 → unitx-0.8.2}/tests/test_import.py +0 -0
  54. {unitx-0.8.0 → unitx-0.8.2}/tests/test_monomial.py +0 -0
  55. {unitx-0.8.0 → unitx-0.8.2}/tests/test_numeric.py +0 -0
  56. {unitx-0.8.0 → unitx-0.8.2}/tests/test_quantity.py +0 -0
  57. {unitx-0.8.0 → unitx-0.8.2}/tests/test_si.py +0 -0
  58. {unitx-0.8.0 → unitx-0.8.2}/tests/test_singleunit.py +0 -0
  59. {unitx-0.8.0 → unitx-0.8.2}/tests/test_unicode.py +0 -0
  60. {unitx-0.8.0 → unitx-0.8.2}/tests/test_unit.py +0 -0
  61. {unitx-0.8.0 → unitx-0.8.2}/tests/test_unit_definition.py +0 -0
  62. {unitx-0.8.0 → unitx-0.8.2}/tests/test_unitparser.py +0 -0
  63. {unitx-0.8.0 → unitx-0.8.2}/tests/test_unitregistry.py +0 -0
  64. {unitx-0.8.0 → unitx-0.8.2}/tests/test_warnings.py +0 -0
  65. {unitx-0.8.0 → unitx-0.8.2}/tests/typing/check_quantity.py +0 -0
  66. {unitx-0.8.0 → unitx-0.8.2}/tests/typing/check_si.py +0 -0
@@ -47,7 +47,9 @@ jobs:
47
47
  ruff format --check .
48
48
 
49
49
  - name: Run tests
50
- run: pytest -q
50
+ run: |
51
+ pytest -q
52
+ python -m doctest README.md docs/units.md docs/scalars.md
51
53
 
52
54
  numpy:
53
55
  name: NumPy ${{ matrix.numpy }} (Python ${{ matrix.python-version }})
@@ -66,9 +68,11 @@ jobs:
66
68
  with:
67
69
  python-version: ${{ matrix.python-version }}
68
70
  - name: Install package and dependencies
69
- run: python -m pip install -e '.[test,numpy]'
71
+ run: python -m pip install -e '.[test,numpy]' 'matplotlib>=3.9'
70
72
  - name: Select minimum NumPy
71
73
  if: matrix.numpy != 'latest'
72
74
  run: python -m pip install 'numpy==${{ matrix.numpy }}'
73
75
  - name: Run tests and typing
74
- run: pytest -q
76
+ run: |
77
+ pytest -q
78
+ python -m doctest README.md docs/units.md docs/scalars.md
@@ -54,6 +54,7 @@ jobs:
54
54
  python -m ruff check .
55
55
  python -m ruff format --check .
56
56
  python -m pytest -q
57
+ python -m doctest README.md docs/units.md docs/scalars.md
57
58
 
58
59
  - name: Build wheel and source distribution
59
60
  run: python -m build
@@ -1,5 +1,40 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.2] - 2026-10-20
4
+
5
+ ### Fixed
6
+
7
+ - Support Decimal and Fraction scalar operands in NumPy calculations, including
8
+ SI constants such as h and kB. Convert them to Python floats at the NumPy
9
+ boundary without changing stored constants or Python scalar arithmetic.
10
+ Keep exact Fraction/Decimal unit exponents in dimensional NumPy powers.
11
+ - Remove recursive arithmetic return constraints from the Scalar protocol to
12
+ avoid excessive type-checker work against NumPy overloads. Preserve scalar
13
+ boolean equality and public Quantity result overloads.
14
+
15
+ ### Improved
16
+
17
+ - Describe array arithmetic, conversion, indexing, and iteration with concrete
18
+ ndarray/NumPy-numeric-scalar result unions. Preserve the magnitude type and
19
+ dtype for transpose, expose NumPy dtype metadata, and type flatten as ndarray.
20
+ - Add strict NumPy method-completion and no-NumPy scalar-consumer checks,
21
+ installed-wheel validation, and a runnable blackbody example using asarray.
22
+ - Rewrite the README as a concise user guide, retaining detailed unit, scalar,
23
+ and migration documentation under docs/. Document NumPy stub limitations and
24
+ validate the example's supported type-inference path without an explicit dtype.
25
+
26
+ ## [0.8.1] - 2026-10-19
27
+
28
+ ### Added
29
+
30
+ - Support `out` for covered single-output NumPy ufunc calls. Numeric results
31
+ write to compatible Quantity ndarray buffers after unit conversion; boolean
32
+ comparisons and predicates write to bare boolean ndarrays. Preserve target
33
+ identity, dtype, unit, and masked-out values, including overlapping views.
34
+ - Stage computation, unit conversion, and casting before writing; validation
35
+ and temporary computation failures leave the target unchanged. Function and
36
+ reduction outputs, item assignment, and multi-output ufuncs remain unsupported.
37
+
3
38
  ## [0.8.0] - 2026-10-19
4
39
 
5
40
  ### Added
unitx-0.8.2/PKG-INFO ADDED
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.5
2
+ Name: unitx
3
+ Version: 0.8.2
4
+ Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
5
+ Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
+ Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
7
+ Author: Dait
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Programming Language :: Python :: 3.15
15
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
+ Requires-Python: >=3.12
17
+ Provides-Extra: numpy
18
+ Requires-Dist: numpy>=2.1; extra == 'numpy'
19
+ Provides-Extra: test
20
+ Requires-Dist: pyright>=1.1.414; extra == 'test'
21
+ Requires-Dist: pytest>=8; extra == 'test'
22
+ Requires-Dist: ruff>=0.8; extra == 'test'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # unitx
26
+
27
+ [![PyPI version](https://img.shields.io/pypi/v/unitx)](https://pypi.org/project/unitx/)
28
+ [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
30
+
31
+ Write physical formulas with units and constants. `unitx` combines unit
32
+ conversion and dimensional analysis with Python scalar arithmetic and optional
33
+ NumPy arrays. Unit scales and rational exponents retain their exact structure.
34
+
35
+ The library is in early development; public APIs may change during the 0.x series.
36
+
37
+ ## Install
38
+
39
+ Python **3.12 or later** is required. The scalar library has no mandatory
40
+ runtime dependencies:
41
+
42
+ ```sh
43
+ python -m pip install unitx
44
+ ```
45
+
46
+ For arrays, install NumPy support. Add Matplotlib to run the plotting example:
47
+
48
+ ```sh
49
+ python -m pip install 'unitx[numpy]' matplotlib
50
+ ```
51
+
52
+ For development, install into the same interpreter that runs your code:
53
+
54
+ ```sh
55
+ python -m pip install -e '.[test,numpy]'
56
+ ```
57
+
58
+ This README describes the current source tree. For an older release, consult
59
+ its matching Git tag.
60
+
61
+ ## Start with units and constants
62
+
63
+ `si` provides units; `SI` provides physical constants. Both are read-only
64
+ namespaces in `unitx.si`:
65
+
66
+ ```pycon
67
+ >>> from unitx.si import SI, si
68
+ >>> distance = 3 * si.m
69
+ >>> elapsed = 2 * si.s
70
+ >>> speed = distance / elapsed
71
+ >>> print(speed)
72
+ 1.5 m/s
73
+ >>> print(speed.to(si.cm / si.s))
74
+ 150.0 cm/s
75
+ >>> print(SI.c)
76
+ 299792458 m/s
77
+ >>> rest_energy = (SI.me * SI.c**2).to(si.MeV)
78
+ >>> round(float(rest_energy.value), 6)
79
+ 0.510999
80
+
81
+ ```
82
+
83
+ Multiplying a number by a unit constructs a Quantity. Multiplication and
84
+ division combine units; addition and subtraction align compatible quantities
85
+ to the left operand's unit. Incompatible conversions and arithmetic raise
86
+ `DimensionError`.
87
+
88
+ ## NumPy example: blackbody radiation
89
+
90
+ Broadcast a column of wavelengths against three temperatures, then plot
91
+ Planck's spectral radiance:
92
+
93
+ ```python
94
+ import numpy as np
95
+ from matplotlib import pyplot as plt
96
+
97
+ from unitx.si import SI, si
98
+
99
+ lam = np.linspace(0.01, 3, 100)[:, None] * si.um # wavelength
100
+ T = np.asarray([3000, 4000, 5000]) * si.K # temperature
101
+
102
+ nu = SI.c / lam # frequency
103
+ radiance = 2 * SI.h * SI.c**2 / (lam**5 * np.expm1(SI.h * nu / (SI.kB * T)))
104
+ radiance = radiance.to('kW/sr/m2/nm') # spectral radiance per unit wavelength
105
+
106
+ plt.plot(lam.value, radiance.value)
107
+ plt.xlabel(f'Wavelength [{lam.unit}]')
108
+ plt.ylabel(f'Spectral radiance [{radiance.unit}]')
109
+ plt.legend([f'T = {t}' for t in T])
110
+ plt.title('Blackbody radiation')
111
+ plt.show()
112
+ ```
113
+
114
+ ![Blackbody spectral radiance](docs/images/blackbody.png)
115
+
116
+ One Quantity wraps the entire ndarray with one unit. NumPy handles broadcasting;
117
+ unitx checks dimensions and combines units. `SI.h` and `SI.kB` retain Decimal
118
+ values in the catalogue and enter NumPy calculations as floats.
119
+
120
+ `.to()` returns a new Quantity. `.value` exposes the numerical data for plotting;
121
+ `.value_in(unit)` also converts it to a chosen unit. Implicit unit removal with
122
+ `np.asarray(quantity)` or `np.array(quantity)` raises TypeError.
123
+
124
+ The [runnable example](examples/blackbody.py) uses `np.asarray([...]) * unit`
125
+ without an explicit dtype. This automatic construction inference is checked
126
+ with NumPy 2.5 stubs and Pyright; the example also runs on NumPy 2.1. Older
127
+ NumPy stubs may still infer Any for this expression. NumPy's own stubs can
128
+ infer different types for `np.array` and `np.asarray`;
129
+ `np.array([...]) * unit` can become Any. `unit * array` and
130
+ `Quantity(array, unit)` provide alternative construction forms. Arbitrary
131
+ shape/dtype inference and wrapped return types for every NumPy function are not
132
+ promised. See the [supported operations, storage rules, and typing limits](docs/numpy.md).
133
+
134
+ ## Values and conversion
135
+
136
+ Use `Quantity` and `Unit` directly when constructing values from strings:
137
+
138
+ ```pycon
139
+ >>> from unitx import Quantity, Unit
140
+ >>> length = Quantity(1, 'm')
141
+ >>> print(length + Quantity(50, 'cm'))
142
+ 3/2 m
143
+ >>> length.value_in('cm')
144
+ 100
145
+ >>> Quantity(1, 'cm').value_in('m')
146
+ Fraction(1, 100)
147
+ >>> Quantity(1, 'm') == Quantity(100, 'cm')
148
+ True
149
+ >>> Unit('m') == Unit('cm')
150
+ False
151
+
152
+ ```
153
+
154
+ A quantity's `.value` is paired with its `.unit`. Units retain their formal
155
+ identity: metre and centimetre are different units, although quantities can
156
+ compare equal after accounting for their scales.
157
+
158
+ Python scalar operations follow the magnitude's numerical domain. Decimal
159
+ operations retain Decimal context behavior; Fraction magnitudes provide exact
160
+ rational arithmetic. Scalar equality compares exact stored values, so binary
161
+ float `0.1` is distinct from Fraction(1, 10). Use symmetric `.isclose()` for
162
+ approximate comparisons:
163
+
164
+ ```pycon
165
+ >>> Quantity(0.1 + 0.2, 'm').isclose(Quantity(0.3, 'm'))
166
+ True
167
+ >>> a, b = Quantity(1, 'm'), Quantity(100.05, 'cm')
168
+ >>> a.isclose(b, rel_tol=0, abs_tol=1 * si.mm)
169
+ True
170
+
171
+ ```
172
+
173
+ NumPy numerical operations instead follow the documented array conversion,
174
+ promotion, rounding, and overflow rules. Array comparisons return boolean
175
+ masks, and NumPy-backed quantities are unhashable.
176
+
177
+ Read the [scalar numerical contract](docs/scalars.md) for Decimal conversion,
178
+ exact comparisons, powers, tolerances, and optional SymPy factorization.
179
+
180
+ ## Expressions and custom units
181
+
182
+ Parse symbols, registered names, prefixes, products, division, and rational
183
+ powers:
184
+
185
+ ```pycon
186
+ >>> Unit('kg m/s2') == Unit('kg*m/s**2')
187
+ True
188
+ >>> Unit('m^(1/2)') ** 2 == si.m
189
+ True
190
+ >>> Unit('ms') == Unit('m s')
191
+ False
192
+ >>> Unit('[astronomical unit] / s') == si.au / si.s
193
+ True
194
+
195
+ ```
196
+
197
+ Multiplication and division associate left to right: write `m/(s A)` for a
198
+ shared denominator. Multiword names can stand alone; use `[registered name]`
199
+ inside expressions. Readable `str()` output is not a guaranteed parser input.
200
+
201
+ Define a unit from an existing unit or a positive finite quantity:
202
+
203
+ ```pycon
204
+ >>> stride = (2 * si.m).define('demoStride', 'demo stride', prefixable=False)
205
+ >>> (3 * stride).value_in('m')
206
+ 6
207
+ >>> Unit('demo stride') == stride
208
+ True
209
+
210
+ ```
211
+
212
+ Definitions register spellings in the current process and return a new Unit.
213
+ Use `Factor` or `SymbolicConstant` with a unit when an additional exact unit
214
+ scale is intended; ordinary numbers construct quantities.
215
+
216
+ See the [unit and quantity guide](docs/units.md) for ScaledUnit, dynamic
217
+ registration, spelling-specific prefix permissions, catalogue conventions,
218
+ copying, and serialization.
219
+
220
+ ## Scope and types
221
+
222
+ - Supported magnitudes are Python scalars and optional plain numeric NumPy
223
+ arrays/scalars. Other array backends, ndarray subclasses, object arrays,
224
+ and implicit list/tuple magnitudes are unsupported.
225
+ - Conversion uses multiplication by a scale. Offset temperature scales such
226
+ as Celsius and Fahrenheit are unsupported.
227
+ - Covered single-output NumPy ufuncs support unit-aware `out` buffers.
228
+ Item assignment, true in-place operators, function/reduction `out`, and
229
+ heterogeneous-unit reductions remain outside the current support matrix.
230
+ - The package ships inline annotations and `py.typed`. Construction and
231
+ unit-only operations preserve the magnitude type; `.T` also retains it.
232
+ Array calculations and conversion use concrete ndarray/NumPy-scalar result
233
+ unions, making native `.value` methods available without claiming an
234
+ unchanged dtype. Scalar protocols avoid recursive arithmetic return types
235
+ so editor analysis remains responsive.
236
+
237
+ The package root exports `Quantity`, `Unit`, `Dimension`, `DIMENSIONLESS`,
238
+ `ONE`, `TEN`, and `PI`; `SI` and `si` are imported from `unitx.si`.
239
+ Other building blocks are available from their modules. Modules beginning
240
+ with `_` are internal.
241
+
242
+ ## Documentation and development
243
+
244
+ - [Unit and quantity guide](docs/units.md)
245
+ - [Scalar numerical behavior](docs/scalars.md)
246
+ - [NumPy support matrix and typing](docs/numpy.md)
247
+ - [Migration from 0.3.0](docs/migration.md)
248
+ - [Changelog](CHANGELOG.md)
249
+
250
+ Run the checks from the repository root:
251
+
252
+ ```sh
253
+ python -m pytest -q
254
+ python -m ruff check .
255
+ python -m ruff format --check .
256
+ python -m doctest README.md docs/units.md docs/scalars.md
257
+ ```
258
+
259
+ Pytest includes runtime tests, source doctests, and Pyright checks. CI covers
260
+ Python 3.12–3.15, optional SymPy, and minimum/current NumPy versions. Strict
261
+ scalar consumers are also checked without NumPy installed.
262
+
263
+ Publishing a GitHub Release triggers the [release workflow](.github/workflows/release.yml)
264
+ to validate and upload wheel/sdist distributions to PyPI. Manual workflow runs
265
+ validate and store distributions without uploading them. Release tags must
266
+ match the project version.
267
+
268
+ Licensed under the [MIT License](LICENSE).
unitx-0.8.2/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # unitx
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/unitx)](https://pypi.org/project/unitx/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Write physical formulas with units and constants. `unitx` combines unit
8
+ conversion and dimensional analysis with Python scalar arithmetic and optional
9
+ NumPy arrays. Unit scales and rational exponents retain their exact structure.
10
+
11
+ The library is in early development; public APIs may change during the 0.x series.
12
+
13
+ ## Install
14
+
15
+ Python **3.12 or later** is required. The scalar library has no mandatory
16
+ runtime dependencies:
17
+
18
+ ```sh
19
+ python -m pip install unitx
20
+ ```
21
+
22
+ For arrays, install NumPy support. Add Matplotlib to run the plotting example:
23
+
24
+ ```sh
25
+ python -m pip install 'unitx[numpy]' matplotlib
26
+ ```
27
+
28
+ For development, install into the same interpreter that runs your code:
29
+
30
+ ```sh
31
+ python -m pip install -e '.[test,numpy]'
32
+ ```
33
+
34
+ This README describes the current source tree. For an older release, consult
35
+ its matching Git tag.
36
+
37
+ ## Start with units and constants
38
+
39
+ `si` provides units; `SI` provides physical constants. Both are read-only
40
+ namespaces in `unitx.si`:
41
+
42
+ ```pycon
43
+ >>> from unitx.si import SI, si
44
+ >>> distance = 3 * si.m
45
+ >>> elapsed = 2 * si.s
46
+ >>> speed = distance / elapsed
47
+ >>> print(speed)
48
+ 1.5 m/s
49
+ >>> print(speed.to(si.cm / si.s))
50
+ 150.0 cm/s
51
+ >>> print(SI.c)
52
+ 299792458 m/s
53
+ >>> rest_energy = (SI.me * SI.c**2).to(si.MeV)
54
+ >>> round(float(rest_energy.value), 6)
55
+ 0.510999
56
+
57
+ ```
58
+
59
+ Multiplying a number by a unit constructs a Quantity. Multiplication and
60
+ division combine units; addition and subtraction align compatible quantities
61
+ to the left operand's unit. Incompatible conversions and arithmetic raise
62
+ `DimensionError`.
63
+
64
+ ## NumPy example: blackbody radiation
65
+
66
+ Broadcast a column of wavelengths against three temperatures, then plot
67
+ Planck's spectral radiance:
68
+
69
+ ```python
70
+ import numpy as np
71
+ from matplotlib import pyplot as plt
72
+
73
+ from unitx.si import SI, si
74
+
75
+ lam = np.linspace(0.01, 3, 100)[:, None] * si.um # wavelength
76
+ T = np.asarray([3000, 4000, 5000]) * si.K # temperature
77
+
78
+ nu = SI.c / lam # frequency
79
+ radiance = 2 * SI.h * SI.c**2 / (lam**5 * np.expm1(SI.h * nu / (SI.kB * T)))
80
+ radiance = radiance.to('kW/sr/m2/nm') # spectral radiance per unit wavelength
81
+
82
+ plt.plot(lam.value, radiance.value)
83
+ plt.xlabel(f'Wavelength [{lam.unit}]')
84
+ plt.ylabel(f'Spectral radiance [{radiance.unit}]')
85
+ plt.legend([f'T = {t}' for t in T])
86
+ plt.title('Blackbody radiation')
87
+ plt.show()
88
+ ```
89
+
90
+ ![Blackbody spectral radiance](docs/images/blackbody.png)
91
+
92
+ One Quantity wraps the entire ndarray with one unit. NumPy handles broadcasting;
93
+ unitx checks dimensions and combines units. `SI.h` and `SI.kB` retain Decimal
94
+ values in the catalogue and enter NumPy calculations as floats.
95
+
96
+ `.to()` returns a new Quantity. `.value` exposes the numerical data for plotting;
97
+ `.value_in(unit)` also converts it to a chosen unit. Implicit unit removal with
98
+ `np.asarray(quantity)` or `np.array(quantity)` raises TypeError.
99
+
100
+ The [runnable example](examples/blackbody.py) uses `np.asarray([...]) * unit`
101
+ without an explicit dtype. This automatic construction inference is checked
102
+ with NumPy 2.5 stubs and Pyright; the example also runs on NumPy 2.1. Older
103
+ NumPy stubs may still infer Any for this expression. NumPy's own stubs can
104
+ infer different types for `np.array` and `np.asarray`;
105
+ `np.array([...]) * unit` can become Any. `unit * array` and
106
+ `Quantity(array, unit)` provide alternative construction forms. Arbitrary
107
+ shape/dtype inference and wrapped return types for every NumPy function are not
108
+ promised. See the [supported operations, storage rules, and typing limits](docs/numpy.md).
109
+
110
+ ## Values and conversion
111
+
112
+ Use `Quantity` and `Unit` directly when constructing values from strings:
113
+
114
+ ```pycon
115
+ >>> from unitx import Quantity, Unit
116
+ >>> length = Quantity(1, 'm')
117
+ >>> print(length + Quantity(50, 'cm'))
118
+ 3/2 m
119
+ >>> length.value_in('cm')
120
+ 100
121
+ >>> Quantity(1, 'cm').value_in('m')
122
+ Fraction(1, 100)
123
+ >>> Quantity(1, 'm') == Quantity(100, 'cm')
124
+ True
125
+ >>> Unit('m') == Unit('cm')
126
+ False
127
+
128
+ ```
129
+
130
+ A quantity's `.value` is paired with its `.unit`. Units retain their formal
131
+ identity: metre and centimetre are different units, although quantities can
132
+ compare equal after accounting for their scales.
133
+
134
+ Python scalar operations follow the magnitude's numerical domain. Decimal
135
+ operations retain Decimal context behavior; Fraction magnitudes provide exact
136
+ rational arithmetic. Scalar equality compares exact stored values, so binary
137
+ float `0.1` is distinct from Fraction(1, 10). Use symmetric `.isclose()` for
138
+ approximate comparisons:
139
+
140
+ ```pycon
141
+ >>> Quantity(0.1 + 0.2, 'm').isclose(Quantity(0.3, 'm'))
142
+ True
143
+ >>> a, b = Quantity(1, 'm'), Quantity(100.05, 'cm')
144
+ >>> a.isclose(b, rel_tol=0, abs_tol=1 * si.mm)
145
+ True
146
+
147
+ ```
148
+
149
+ NumPy numerical operations instead follow the documented array conversion,
150
+ promotion, rounding, and overflow rules. Array comparisons return boolean
151
+ masks, and NumPy-backed quantities are unhashable.
152
+
153
+ Read the [scalar numerical contract](docs/scalars.md) for Decimal conversion,
154
+ exact comparisons, powers, tolerances, and optional SymPy factorization.
155
+
156
+ ## Expressions and custom units
157
+
158
+ Parse symbols, registered names, prefixes, products, division, and rational
159
+ powers:
160
+
161
+ ```pycon
162
+ >>> Unit('kg m/s2') == Unit('kg*m/s**2')
163
+ True
164
+ >>> Unit('m^(1/2)') ** 2 == si.m
165
+ True
166
+ >>> Unit('ms') == Unit('m s')
167
+ False
168
+ >>> Unit('[astronomical unit] / s') == si.au / si.s
169
+ True
170
+
171
+ ```
172
+
173
+ Multiplication and division associate left to right: write `m/(s A)` for a
174
+ shared denominator. Multiword names can stand alone; use `[registered name]`
175
+ inside expressions. Readable `str()` output is not a guaranteed parser input.
176
+
177
+ Define a unit from an existing unit or a positive finite quantity:
178
+
179
+ ```pycon
180
+ >>> stride = (2 * si.m).define('demoStride', 'demo stride', prefixable=False)
181
+ >>> (3 * stride).value_in('m')
182
+ 6
183
+ >>> Unit('demo stride') == stride
184
+ True
185
+
186
+ ```
187
+
188
+ Definitions register spellings in the current process and return a new Unit.
189
+ Use `Factor` or `SymbolicConstant` with a unit when an additional exact unit
190
+ scale is intended; ordinary numbers construct quantities.
191
+
192
+ See the [unit and quantity guide](docs/units.md) for ScaledUnit, dynamic
193
+ registration, spelling-specific prefix permissions, catalogue conventions,
194
+ copying, and serialization.
195
+
196
+ ## Scope and types
197
+
198
+ - Supported magnitudes are Python scalars and optional plain numeric NumPy
199
+ arrays/scalars. Other array backends, ndarray subclasses, object arrays,
200
+ and implicit list/tuple magnitudes are unsupported.
201
+ - Conversion uses multiplication by a scale. Offset temperature scales such
202
+ as Celsius and Fahrenheit are unsupported.
203
+ - Covered single-output NumPy ufuncs support unit-aware `out` buffers.
204
+ Item assignment, true in-place operators, function/reduction `out`, and
205
+ heterogeneous-unit reductions remain outside the current support matrix.
206
+ - The package ships inline annotations and `py.typed`. Construction and
207
+ unit-only operations preserve the magnitude type; `.T` also retains it.
208
+ Array calculations and conversion use concrete ndarray/NumPy-scalar result
209
+ unions, making native `.value` methods available without claiming an
210
+ unchanged dtype. Scalar protocols avoid recursive arithmetic return types
211
+ so editor analysis remains responsive.
212
+
213
+ The package root exports `Quantity`, `Unit`, `Dimension`, `DIMENSIONLESS`,
214
+ `ONE`, `TEN`, and `PI`; `SI` and `si` are imported from `unitx.si`.
215
+ Other building blocks are available from their modules. Modules beginning
216
+ with `_` are internal.
217
+
218
+ ## Documentation and development
219
+
220
+ - [Unit and quantity guide](docs/units.md)
221
+ - [Scalar numerical behavior](docs/scalars.md)
222
+ - [NumPy support matrix and typing](docs/numpy.md)
223
+ - [Migration from 0.3.0](docs/migration.md)
224
+ - [Changelog](CHANGELOG.md)
225
+
226
+ Run the checks from the repository root:
227
+
228
+ ```sh
229
+ python -m pytest -q
230
+ python -m ruff check .
231
+ python -m ruff format --check .
232
+ python -m doctest README.md docs/units.md docs/scalars.md
233
+ ```
234
+
235
+ Pytest includes runtime tests, source doctests, and Pyright checks. CI covers
236
+ Python 3.12–3.15, optional SymPy, and minimum/current NumPy versions. Strict
237
+ scalar consumers are also checked without NumPy installed.
238
+
239
+ Publishing a GitHub Release triggers the [release workflow](.github/workflows/release.yml)
240
+ to validate and upload wheel/sdist distributions to PyPI. Manual workflow runs
241
+ validate and store distributions without uploading them. Release tags must
242
+ match the project version.
243
+
244
+ Licensed under the [MIT License](LICENSE).
@@ -6,7 +6,10 @@ from unitx._numerics import as_fraction
6
6
  from unitx.exceptions import InexactFloatWarning
7
7
  from unitx.factor import Factor
8
8
 
9
- collect_ignore = ['src/unitx/_numpy.py'] if find_spec('numpy') is None else []
9
+ # Static-only fixtures run through Pyright in tests/test_typing.py.
10
+ collect_ignore = ['tests/example_typing']
11
+ if find_spec('numpy') is None:
12
+ collect_ignore.append('src/unitx/_numpy.py')
10
13
 
11
14
  _DOCTEST_FILTER: dict[str, tuple[pytest.MarkDecorator, ...]] = {
12
15
  f'{obj.__module__}.{obj.__qualname__}': tuple(
Binary file
@@ -0,0 +1,16 @@
1
+ # Upgrading from 0.3.0
2
+
3
+ Version 0.4.0 replaced the implementation previously published on PyPI as 0.3.0.
4
+ It introduced incompatible changes:
5
+
6
+ - Python 3.12 or later is required; Python 3.11 is no longer supported.
7
+ - The former `unitx.utils` namespace was removed. Classes can be imported from
8
+ their modules, as shown above; the current source tree also exports `Unit`,
9
+ `Quantity`, and `Dimension` at the package root.
10
+ - `Unit` no longer defines `len()` or special truth-value behavior. Use
11
+ `unit == Unit('')` to check the formal identity, or `unit.is_dimensionless()`
12
+ to check physical dimensions.
13
+ - Conversion and arithmetic can produce different magnitude types, especially
14
+ for Decimal inputs. Review the [scalar numerical behavior](scalars.md)
15
+ before relying on a result's concrete type or approximate equality.
16
+