unitx 0.7.0__tar.gz → 0.8.0__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 (53) hide show
  1. {unitx-0.7.0 → unitx-0.8.0}/.github/workflows/ci.yml +24 -0
  2. {unitx-0.7.0 → unitx-0.8.0}/CHANGELOG.md +62 -0
  3. {unitx-0.7.0 → unitx-0.8.0}/PKG-INFO +44 -20
  4. {unitx-0.7.0 → unitx-0.8.0}/README.md +41 -19
  5. {unitx-0.7.0 → unitx-0.8.0}/conftest.py +4 -0
  6. unitx-0.8.0/docs/numpy.md +133 -0
  7. {unitx-0.7.0 → unitx-0.8.0}/pyproject.toml +2 -1
  8. {unitx-0.7.0 → unitx-0.8.0}/pyrightconfig.json +1 -0
  9. unitx-0.8.0/pyrightconfig.numpy.json +5 -0
  10. unitx-0.8.0/src/unitx/_constnamespace.py +33 -0
  11. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/_numerics.py +47 -14
  12. unitx-0.8.0/src/unitx/_numpy.py +374 -0
  13. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/factor.py +2 -2
  14. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/quantity.py +488 -40
  15. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/si/bases.py +9 -7
  16. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/si/constants.py +14 -22
  17. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/si/units.py +125 -70
  18. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/unit.py +38 -11
  19. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/unitregistry.py +4 -4
  20. unitx-0.8.0/tests/numpy_typing/check_numpy.py +61 -0
  21. unitx-0.8.0/tests/test_numpy.py +423 -0
  22. {unitx-0.7.0 → unitx-0.8.0}/tests/test_si.py +62 -1
  23. {unitx-0.7.0 → unitx-0.8.0}/tests/test_singleunit.py +6 -2
  24. {unitx-0.7.0 → unitx-0.8.0}/tests/test_typing.py +25 -0
  25. {unitx-0.7.0 → unitx-0.8.0}/tests/test_unitregistry.py +28 -4
  26. {unitx-0.7.0 → unitx-0.8.0}/tests/typing/check_quantity.py +34 -30
  27. {unitx-0.7.0 → unitx-0.8.0}/tests/typing/check_si.py +9 -1
  28. unitx-0.7.0/src/unitx/_constclass.py +0 -34
  29. {unitx-0.7.0 → unitx-0.8.0}/.github/workflows/release.yml +0 -0
  30. {unitx-0.7.0 → unitx-0.8.0}/.gitignore +0 -0
  31. {unitx-0.7.0 → unitx-0.8.0}/LICENSE +0 -0
  32. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/__init__.py +0 -0
  33. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/_unicode.py +0 -0
  34. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/dimension.py +0 -0
  35. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/exceptions.py +0 -0
  36. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/monomial.py +0 -0
  37. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/py.typed +0 -0
  38. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/si/__init__.py +0 -0
  39. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/singleunit.py +0 -0
  40. {unitx-0.7.0 → unitx-0.8.0}/src/unitx/unitparser.py +0 -0
  41. {unitx-0.7.0 → unitx-0.8.0}/tests/__init__.py +0 -0
  42. {unitx-0.7.0 → unitx-0.8.0}/tests/test_builtin_definitions.py +0 -0
  43. {unitx-0.7.0 → unitx-0.8.0}/tests/test_dimension.py +0 -0
  44. {unitx-0.7.0 → unitx-0.8.0}/tests/test_factor.py +0 -0
  45. {unitx-0.7.0 → unitx-0.8.0}/tests/test_import.py +0 -0
  46. {unitx-0.7.0 → unitx-0.8.0}/tests/test_monomial.py +0 -0
  47. {unitx-0.7.0 → unitx-0.8.0}/tests/test_numeric.py +0 -0
  48. {unitx-0.7.0 → unitx-0.8.0}/tests/test_quantity.py +0 -0
  49. {unitx-0.7.0 → unitx-0.8.0}/tests/test_unicode.py +0 -0
  50. {unitx-0.7.0 → unitx-0.8.0}/tests/test_unit.py +0 -0
  51. {unitx-0.7.0 → unitx-0.8.0}/tests/test_unit_definition.py +0 -0
  52. {unitx-0.7.0 → unitx-0.8.0}/tests/test_unitparser.py +0 -0
  53. {unitx-0.7.0 → unitx-0.8.0}/tests/test_warnings.py +0 -0
@@ -48,3 +48,27 @@ jobs:
48
48
 
49
49
  - name: Run tests
50
50
  run: pytest -q
51
+
52
+ numpy:
53
+ name: NumPy ${{ matrix.numpy }} (Python ${{ matrix.python-version }})
54
+ runs-on: ubuntu-latest
55
+ strategy:
56
+ fail-fast: false
57
+ matrix:
58
+ include:
59
+ - python-version: '3.12'
60
+ numpy: '2.1.*'
61
+ - python-version: '3.14'
62
+ numpy: 'latest'
63
+ steps:
64
+ - uses: actions/checkout@v6
65
+ - uses: actions/setup-python@v7
66
+ with:
67
+ python-version: ${{ matrix.python-version }}
68
+ - name: Install package and dependencies
69
+ run: python -m pip install -e '.[test,numpy]'
70
+ - name: Select minimum NumPy
71
+ if: matrix.numpy != 'latest'
72
+ run: python -m pip install 'numpy==${{ matrix.numpy }}'
73
+ - name: Run tests and typing
74
+ run: pytest -q
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0] - 2026-10-19
4
+
5
+ ### Added
6
+
7
+ - Add optional NumPy >= 2.1 support through `unitx[numpy]`, with one Quantity
8
+ wrapping an entire plain numeric ndarray or NumPy numeric scalar. Preserve
9
+ input objects in both `array * unit` and `unit * array` construction.
10
+ - Support unit-aware conversion, broadcasting arithmetic and comparisons,
11
+ matrix multiplication, indexing, iteration, array metadata, transpose, and
12
+ flattening. Preserve NumPy scalar indexing and view/copy behavior.
13
+ - Implement a documented set of NumPy ufuncs and functions, including roots,
14
+ dimensionless transcendental functions, concatenation, statistics, predicates,
15
+ and position queries. Keep exact unit exponents and ScaledUnit factors.
16
+ - Extend Quantity.isclose with symmetric array comparisons and broadcasting
17
+ tolerances; implement NumPy isclose/allclose with their own defaults and
18
+ asymmetric formula. Validate tolerance dimensions, finiteness, and signs.
19
+ - Add runtime and installed-consumer typing coverage, import-order and optional
20
+ dependency checks, and CI jobs for minimum and current NumPy versions.
21
+
22
+ ### Changed
23
+
24
+ - Define `Scalar`, `Array`, and `Magnitude = Scalar | Array` in `_numerics`.
25
+ Preserve concrete magnitude types at construction and in unit-only operations;
26
+ use conservative result types for array numerical operations.
27
+ - Keep Python scalar arithmetic, Decimal behavior, and exact scalar comparisons
28
+ independent of the optional NumPy adapter. Array conversion follows the
29
+ documented NumPy promotion, rounding, and overflow rules.
30
+
31
+ ### NumPy scope
32
+
33
+ - Document the supported function/keyword matrix and precision/storage rules in
34
+ `docs/numpy.md`. Other array backends, ndarray subclasses, object/boolean
35
+ magnitudes, and implicit list/tuple conversion are unsupported.
36
+ - Reject implicit unit removal by `np.asarray(q)` or `np.array(q)`; use `.value`
37
+ or `.value_in(unit)` explicitly. NumPy magnitudes produce unhashable Quantities.
38
+ - Reject writable out, item assignment, and operations that would require
39
+ heterogeneous units. Python augmented assignment creates and rebinds a result.
40
+
41
+ ## [0.7.1] - 2026-10-19
42
+
43
+ ### Added
44
+
45
+ - Accept a single exact spelling string for `prefixable`, equivalent to a
46
+ one-element tuple, with the same spelling validation before registration.
47
+ - Expand the `si` namespace's prefixed shortcuts and common units, including
48
+ percent, permille, permyriad, rotational frequency (`rpm`), speed of light
49
+ (`c`), gram-force, volt-ampere, ampere-hour, mho, and nit. Enable Torr prefixes.
50
+ - Add `SI.kappa` and the wavelength and frequency Wien displacement constants,
51
+ `SI.b` and `SI.b_`.
52
+
53
+ ### Changed
54
+
55
+ - Use fixed Decimal approximations with about 50 significant digits for pi and
56
+ the positive roots in the Wien displacement laws. Derived constants still use
57
+ the Decimal context active at import.
58
+ - Treat `u` and `Da` as spellings of one formal unit, with prefixes enabled only
59
+ on `Da`. Rebuild stored units when migrating from the separate `u` identity;
60
+ the mass conversion scale is unchanged. Use `kDa` rather than `kilodalton`.
61
+ - Keep ASCII micro spellings such as `um` and `us` convenient to type without
62
+ warnings. The explicit product `u m` retains its separate mass-times-length
63
+ meaning.
64
+
3
65
  ## [0.7.0] - 2026-10-19
4
66
 
5
67
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
@@ -14,6 +14,8 @@ Classifier: Programming Language :: Python :: 3.14
14
14
  Classifier: Programming Language :: Python :: 3.15
15
15
  Classifier: Topic :: Scientific/Engineering :: Physics
16
16
  Requires-Python: >=3.12
17
+ Provides-Extra: numpy
18
+ Requires-Dist: numpy>=2.1; extra == 'numpy'
17
19
  Provides-Extra: test
18
20
  Requires-Dist: pyright>=1.1.414; extra == 'test'
19
21
  Requires-Dist: pytest>=8; extra == 'test'
@@ -22,11 +24,15 @@ Description-Content-Type: text/markdown
22
24
 
23
25
  # unitx
24
26
 
25
- Write Python formulas with ready-to-use units and physical constants. unitx
26
- handles scalar quantities, unit conversion, and dimensional analysis, with
27
- exact rational unit scales and exponents.
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)
28
30
 
29
- unitx is in early development. Public APIs may change during the 0.x series.
31
+ Write Python formulas with ready-to-use units and physical constants. `unitx`
32
+ handles scalar and optional NumPy quantities, unit conversion, and dimensional
33
+ analysis, with exact rational unit scales and exponents.
34
+
35
+ `unitx` is in early development. Public APIs may change during the 0.x series.
30
36
 
31
37
  ## Installation
32
38
 
@@ -101,15 +107,17 @@ astronomical units are included. Units and constants can be combined directly:
101
107
 
102
108
  Both namespaces are read-only and do not need to be instantiated. `si` exposes
103
109
  a selection of convenient attributes; other registered prefix combinations
104
- remain available through `Unit`, for example `Unit('Tm')`. Importing unitx
110
+ remain available through `Unit`, for example `Unit('Tm')`. Importing `unitx`
105
111
  initializes the predefined unit catalogue. Defining a custom unit registers its
106
112
  text spellings without adding an attribute to `si`.
107
113
 
108
114
  SI defining constants retain their exact decimal values. Measured constants
109
115
  are stored nominal values; measurement uncertainties are not propagated.
110
116
  Derived Decimal constants are evaluated in the Decimal context active at
111
- import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
112
- additional Decimal precision does not add precision to pi.
117
+ import. Pi and the positive roots used by the Wien displacement constants
118
+ (`SI.b` for wavelength and `SI.b_` for frequency) use fixed Decimal
119
+ approximations with about 50 significant digits. Raising the context precision
120
+ does not add accuracy beyond those stored approximations.
113
121
 
114
122
  ## Constructing units and quantities
115
123
 
@@ -267,7 +275,7 @@ meaning rather than acting as string delimiters. Bare compound names are
267
275
  not guessed from adjacent words, and ordinary parentheses still group products.
268
276
 
269
277
  The unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the
270
- dimensionless identity. Importing unitx also initializes the common units in
278
+ dimensionless identity. Importing `unitx` also initializes the common units in
271
279
  `unitx.si`, including named derived units such as `N`. Mass scales are referenced
272
280
  to kilograms: `g` has factor `1/1000`, and `kg` has factor `1`.
273
281
 
@@ -337,8 +345,9 @@ Bare multiword names are recognized only as complete inputs. For example,
337
345
  as a factor; use `[astronomical unit] / arcsec`, symbols, or Unit objects instead.
338
346
 
339
347
  SI prefixes are enabled by default. `prefixable: PrefixPolicy = True` accepts
340
- `True` for all spellings, `False` for none, or a tuple of exact symbols, names,
341
- and aliases supplied in that definition. With a tuple, only the listed
348
+ `True` for all spellings, `False` for none, a single spelling string, or a tuple
349
+ of exact symbols, names, and aliases supplied in that definition. A string such
350
+ as `prefixable='Da'` is equivalent to `prefixable=('Da',)`. Only the selected
342
351
  spellings accept prefixes; an empty tuple disables prefixes. Unknown spellings
343
352
  and non-string entries are errors. For example,
344
353
  `prefixable=('arcsec', 'arcsecond')` enables all SI prefixes on those two
@@ -347,7 +356,7 @@ spellings, while other spellings of that unit remain unprefixable.
347
356
  The tuple selects **unit spellings**, not prefix symbols. Prefix names attach
348
357
  directly to the first word of a multiword name, as in `kiloastronomical unit`;
349
358
  a separate `kilo astronomical unit` is not accepted. `PrefixPolicy` is available
350
- from `unitx.unitregistry` for annotations and means `bool | tuple[str, ...]`.
359
+ from `unitx.unitregistry` for annotations and means `bool | str | tuple[str, ...]`.
351
360
 
352
361
  ```pycon
353
362
  >>> tick = si.s.define(
@@ -390,10 +399,24 @@ form; `760 * si.mmHg` converts to `si.atm` with value 1 under the library's
390
399
  existing pressure convention. Prefixes do not stack: `mmmHg` and `kmmHg` are
391
400
  invalid. `kt` retains its meaning as a knot alias; tonne remains unprefixable.
392
401
 
393
- The newly registered `u` denotes the unified atomic mass unit. ASCII micro
394
- spellings such as `um` still mean micrometres, but now issue `UnitSymbolWarning`
395
- because `u m` has a separate meaning. Use `µm`, `μm`, or `micrometre` to avoid
396
- this warning.
402
+ `si.mas` and `si.uas` provide milli- and microarcseconds as namespace shortcuts;
403
+ their registered expressions are `marcsec` and `µarcsec`. The strings `mas`
404
+ and `uas` are not registered aliases. `si.Da` and `si.u` now share one formal
405
+ unit identity; only the spelling `Da` accepts prefixes, such as `kDa`.
406
+ `si.mTorr` is available, and Torr accepts SI prefixes. Tonne uses `t` or
407
+ `tonne`; the ambiguous spelling `ton` is not registered.
408
+
409
+ `si.rpm` represents rotational frequency: `60 * si.rpm` converts to `si.Hz`
410
+ with value 1. For angular velocity, multiply that frequency by `2 * PI` and
411
+ convert to `si.rad / si.s`; directly converting frequency to `rad/s` does
412
+ not insert the physical factor of 2π. `si.c` is a speed unit equal to the
413
+ speed of light, while `SI.c` is the corresponding physical quantity.
414
+
415
+ The spelling `u` denotes the unified atomic mass unit when used on its own.
416
+ ASCII micro spellings such as `um` and `us` remain supported without warnings,
417
+ so entering non-ASCII characters is optional. `um`, `µm`, `μm`, and
418
+ `micrometre` all mean micrometres; the explicit product `u m` instead means
419
+ unified atomic mass unit times metre.
397
420
 
398
421
  Registration raises `UnitRegistryError` if a new spelling, including a generated
399
422
  prefixed form, already resolves to a unit or conflicts with another spelling
@@ -549,9 +572,10 @@ python -m pip install sympy
549
572
 
550
573
  ## Current scope
551
574
 
552
- - **NumPy scalars, arrays, and NumPy operations are not supported yet. NumPy
553
- integration is planned.** Structural compatibility with a magnitude protocol
554
- does not imply NumPy support.
575
+ - Optional NumPy support accepts plain numeric ndarrays and NumPy numeric
576
+ scalars. Install `unitx[numpy]`; see the [support matrix and numerical
577
+ contract](https://github.com/DaitTHU/unitx/blob/v0.8.0/docs/numpy.md). Array operations preserve units, comparisons return
578
+ masks, and implicit unit stripping and writable `out` are rejected.
555
579
  - Custom magnitudes must support scalar arithmetic, return a Python `bool`
556
580
  from equality, and defer raw operations with `Unit` operands to its reflected
557
581
  operators.
@@ -567,7 +591,7 @@ python -m pip install sympy
567
591
  | Class | Import module | Purpose |
568
592
  | --- | --- | --- |
569
593
  | `si` / `SI` | `unitx.si` | Ready-to-use units and physical constants |
570
- | `Quantity` | `unitx.quantity` | Scalar values paired with units, conversion, and arithmetic |
594
+ | `Quantity` | `unitx.quantity` | Scalar or NumPy magnitudes paired with units, conversion, and arithmetic |
571
595
  | `Unit` | `unitx.unit` | Compound units, scaling factors, and physical dimensions |
572
596
  | `ScaledUnit` | `unitx.unit` | Formal unit products with an additional exact scale |
573
597
  | `Dimension` | `unitx.dimension` | Seven physical dimensions with rational exponents |
@@ -1,10 +1,14 @@
1
1
  # unitx
2
2
 
3
- Write Python formulas with ready-to-use units and physical constants. unitx
4
- handles scalar quantities, unit conversion, and dimensional analysis, with
5
- exact rational unit scales and exponents.
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
6
 
7
- unitx is in early development. Public APIs may change during the 0.x series.
7
+ Write Python formulas with ready-to-use units and physical constants. `unitx`
8
+ handles scalar and optional NumPy quantities, unit conversion, and dimensional
9
+ analysis, with exact rational unit scales and exponents.
10
+
11
+ `unitx` is in early development. Public APIs may change during the 0.x series.
8
12
 
9
13
  ## Installation
10
14
 
@@ -79,15 +83,17 @@ astronomical units are included. Units and constants can be combined directly:
79
83
 
80
84
  Both namespaces are read-only and do not need to be instantiated. `si` exposes
81
85
  a selection of convenient attributes; other registered prefix combinations
82
- remain available through `Unit`, for example `Unit('Tm')`. Importing unitx
86
+ remain available through `Unit`, for example `Unit('Tm')`. Importing `unitx`
83
87
  initializes the predefined unit catalogue. Defining a custom unit registers its
84
88
  text spellings without adding an attribute to `si`.
85
89
 
86
90
  SI defining constants retain their exact decimal values. Measured constants
87
91
  are stored nominal values; measurement uncertainties are not propagated.
88
92
  Derived Decimal constants are evaluated in the Decimal context active at
89
- import. Formulas involving pi use `Decimal(str(math.pi))` as an approximation;
90
- additional Decimal precision does not add precision to pi.
93
+ import. Pi and the positive roots used by the Wien displacement constants
94
+ (`SI.b` for wavelength and `SI.b_` for frequency) use fixed Decimal
95
+ approximations with about 50 significant digits. Raising the context precision
96
+ does not add accuracy beyond those stored approximations.
91
97
 
92
98
  ## Constructing units and quantities
93
99
 
@@ -245,7 +251,7 @@ meaning rather than acting as string delimiters. Bare compound names are
245
251
  not guessed from adjacent words, and ordinary parentheses still group products.
246
252
 
247
253
  The unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the
248
- dimensionless identity. Importing unitx also initializes the common units in
254
+ dimensionless identity. Importing `unitx` also initializes the common units in
249
255
  `unitx.si`, including named derived units such as `N`. Mass scales are referenced
250
256
  to kilograms: `g` has factor `1/1000`, and `kg` has factor `1`.
251
257
 
@@ -315,8 +321,9 @@ Bare multiword names are recognized only as complete inputs. For example,
315
321
  as a factor; use `[astronomical unit] / arcsec`, symbols, or Unit objects instead.
316
322
 
317
323
  SI prefixes are enabled by default. `prefixable: PrefixPolicy = True` accepts
318
- `True` for all spellings, `False` for none, or a tuple of exact symbols, names,
319
- and aliases supplied in that definition. With a tuple, only the listed
324
+ `True` for all spellings, `False` for none, a single spelling string, or a tuple
325
+ of exact symbols, names, and aliases supplied in that definition. A string such
326
+ as `prefixable='Da'` is equivalent to `prefixable=('Da',)`. Only the selected
320
327
  spellings accept prefixes; an empty tuple disables prefixes. Unknown spellings
321
328
  and non-string entries are errors. For example,
322
329
  `prefixable=('arcsec', 'arcsecond')` enables all SI prefixes on those two
@@ -325,7 +332,7 @@ spellings, while other spellings of that unit remain unprefixable.
325
332
  The tuple selects **unit spellings**, not prefix symbols. Prefix names attach
326
333
  directly to the first word of a multiword name, as in `kiloastronomical unit`;
327
334
  a separate `kilo astronomical unit` is not accepted. `PrefixPolicy` is available
328
- from `unitx.unitregistry` for annotations and means `bool | tuple[str, ...]`.
335
+ from `unitx.unitregistry` for annotations and means `bool | str | tuple[str, ...]`.
329
336
 
330
337
  ```pycon
331
338
  >>> tick = si.s.define(
@@ -368,10 +375,24 @@ form; `760 * si.mmHg` converts to `si.atm` with value 1 under the library's
368
375
  existing pressure convention. Prefixes do not stack: `mmmHg` and `kmmHg` are
369
376
  invalid. `kt` retains its meaning as a knot alias; tonne remains unprefixable.
370
377
 
371
- The newly registered `u` denotes the unified atomic mass unit. ASCII micro
372
- spellings such as `um` still mean micrometres, but now issue `UnitSymbolWarning`
373
- because `u m` has a separate meaning. Use `µm`, `μm`, or `micrometre` to avoid
374
- this warning.
378
+ `si.mas` and `si.uas` provide milli- and microarcseconds as namespace shortcuts;
379
+ their registered expressions are `marcsec` and `µarcsec`. The strings `mas`
380
+ and `uas` are not registered aliases. `si.Da` and `si.u` now share one formal
381
+ unit identity; only the spelling `Da` accepts prefixes, such as `kDa`.
382
+ `si.mTorr` is available, and Torr accepts SI prefixes. Tonne uses `t` or
383
+ `tonne`; the ambiguous spelling `ton` is not registered.
384
+
385
+ `si.rpm` represents rotational frequency: `60 * si.rpm` converts to `si.Hz`
386
+ with value 1. For angular velocity, multiply that frequency by `2 * PI` and
387
+ convert to `si.rad / si.s`; directly converting frequency to `rad/s` does
388
+ not insert the physical factor of 2π. `si.c` is a speed unit equal to the
389
+ speed of light, while `SI.c` is the corresponding physical quantity.
390
+
391
+ The spelling `u` denotes the unified atomic mass unit when used on its own.
392
+ ASCII micro spellings such as `um` and `us` remain supported without warnings,
393
+ so entering non-ASCII characters is optional. `um`, `µm`, `μm`, and
394
+ `micrometre` all mean micrometres; the explicit product `u m` instead means
395
+ unified atomic mass unit times metre.
375
396
 
376
397
  Registration raises `UnitRegistryError` if a new spelling, including a generated
377
398
  prefixed form, already resolves to a unit or conflicts with another spelling
@@ -527,9 +548,10 @@ python -m pip install sympy
527
548
 
528
549
  ## Current scope
529
550
 
530
- - **NumPy scalars, arrays, and NumPy operations are not supported yet. NumPy
531
- integration is planned.** Structural compatibility with a magnitude protocol
532
- does not imply NumPy support.
551
+ - Optional NumPy support accepts plain numeric ndarrays and NumPy numeric
552
+ scalars. Install `unitx[numpy]`; see the [support matrix and numerical
553
+ contract](https://github.com/DaitTHU/unitx/blob/v0.8.0/docs/numpy.md). Array operations preserve units, comparisons return
554
+ masks, and implicit unit stripping and writable `out` are rejected.
533
555
  - Custom magnitudes must support scalar arithmetic, return a Python `bool`
534
556
  from equality, and defer raw operations with `Unit` operands to its reflected
535
557
  operators.
@@ -545,7 +567,7 @@ python -m pip install sympy
545
567
  | Class | Import module | Purpose |
546
568
  | --- | --- | --- |
547
569
  | `si` / `SI` | `unitx.si` | Ready-to-use units and physical constants |
548
- | `Quantity` | `unitx.quantity` | Scalar values paired with units, conversion, and arithmetic |
570
+ | `Quantity` | `unitx.quantity` | Scalar or NumPy magnitudes paired with units, conversion, and arithmetic |
549
571
  | `Unit` | `unitx.unit` | Compound units, scaling factors, and physical dimensions |
550
572
  | `ScaledUnit` | `unitx.unit` | Formal unit products with an additional exact scale |
551
573
  | `Dimension` | `unitx.dimension` | Seven physical dimensions with rational exponents |
@@ -1,9 +1,13 @@
1
+ from importlib.util import find_spec
2
+
1
3
  import pytest
2
4
 
3
5
  from unitx._numerics import as_fraction
4
6
  from unitx.exceptions import InexactFloatWarning
5
7
  from unitx.factor import Factor
6
8
 
9
+ collect_ignore = ['src/unitx/_numpy.py'] if find_spec('numpy') is None else []
10
+
7
11
  _DOCTEST_FILTER: dict[str, tuple[pytest.MarkDecorator, ...]] = {
8
12
  f'{obj.__module__}.{obj.__qualname__}': tuple(
9
13
  pytest.mark.filterwarnings(f'ignore::{warning.__module__}.{warning.__qualname__}')
@@ -0,0 +1,133 @@
1
+ # NumPy support
2
+
3
+ Install `unitx[numpy]` for NumPy >= 2.1. Core imports and Python scalar
4
+ arithmetic do not import NumPy.
5
+
6
+ ```python
7
+ import numpy as np
8
+ from unitx import Quantity, Unit
9
+
10
+ metre = Unit('m')
11
+ a = np.arange(6.0).reshape(2, 3)
12
+ q = a * metre
13
+ assert q.value is a and (metre * a).value is a
14
+ speed = q / Quantity(2, 's')
15
+ centimetres = q.value_in('cm')
16
+ column_means = np.mean(q, axis=0)
17
+ mask = q > Quantity(100, 'cm')
18
+ ```
19
+
20
+ One Quantity holds one unit for its entire magnitude. Supported magnitudes
21
+ are plain ndarrays and NumPy numeric scalars with integer, floating, or complex
22
+ dtype. Object, boolean, string, structured, datetime, masked arrays, ndarray
23
+ subclasses, and other backends are unsupported. Boolean arrays are accepted
24
+ as masks and returned by predicates. Construct ndarrays explicitly: list/tuple
25
+ magnitudes and implicit Fraction/Decimal object arrays are rejected.
26
+
27
+ ## Values, storage, and precision
28
+
29
+ `.value`, `.value_in(unit)`, and `.to(unit).value` are explicit numerical exits.
30
+ `np.asarray(q)` and `np.array(q)` raise TypeError even for dimensionless q;
31
+ use `np.asarray(q.value_in(target_unit))`.
32
+
33
+ Construction, unit-only multiplication/division, and an identity conversion
34
+ preserve the magnitude object. Factor/SymbolicConstant operands change the
35
+ exact unit scale. Indexing, iteration, `.T`, reshape, and transpose follow
36
+ NumPy's scalar/view/copy rules; `.flatten(order='C')` copies. Zero-dimensional
37
+ arrays cannot be iterated. `.shape`, `.ndim`, `.dtype`, and `.size` are available
38
+ for NumPy magnitudes. Python scalars do not acquire array attributes. Exposed
39
+ values and views may share writable storage.
40
+
41
+ Unit algebra stays exact. Nonidentity conversion uses `np.multiply`: integral
42
+ rational ratios become Python int multipliers; nonintegral rational ratios
43
+ become one Python float multiplier; irrational ratios use Factor's approximate
44
+ value. NumPy controls dtype promotion, rounding, and overflow. Conversion
45
+ allocates and never writes to the input. Integer overflow, multiplier
46
+ conversion errors, and float overflow/underflow remain possible, including for
47
+ extended precision inputs. Conversion never creates Fraction/Decimal arrays.
48
+
49
+ Comparisons broadcast and return bare boolean results. Equal unit factors
50
+ compare native values; otherwise both operands convert to coherent SI values.
51
+ Different dimensions yield False equality / True inequality masks; ordering
52
+ raises DimensionError. Bare numbers/arrays are dimensionless. These numerical
53
+ rules are distinct from Python scalar Quantity's exact comparison and hash
54
+ rules. All NumPy magnitudes, including scalars and zero-dimensional arrays,
55
+ produce unhashable Quantities. `bool(q)` delegates to `bool(q.value)`.
56
+
57
+ ## Support matrix
58
+
59
+ Unit-bearing results are Quantities, including dimensionless values and scalar
60
+ reductions. Predicates and indices are bare results. Unsupported functions,
61
+ methods, mixed array types, and keywords fail explicitly.
62
+
63
+ | ufunc / Python operation | Result rule |
64
+ | --- | --- |
65
+ | add, subtract / `+`, `-` | Align compatible dimensions to the first Quantity's unit |
66
+ | multiply, divide/true_divide, matmul / `*`, `/`, `@` | Combine units |
67
+ | equal, not_equal, less, less_equal, greater, greater_equal / comparisons | Bare boolean results |
68
+ | positive, negative, absolute, fabs, conjugate / unary `+`, `-`, `abs` | Preserve unit |
69
+ | square, sqrt, cbrt, reciprocal | Exact unit exponents 2, 1/2, 1/3, -1 |
70
+ | power, float_power / `**` | Dimensional bases require a finite real scalar exponent; dimensionless bases/exponents normalize their scales |
71
+ | sin, cos, tan, exp, expm1, log, log2, log10, log1p | Require coherent dimensionless values; return dimensionless Quantity |
72
+ | isfinite, isnan, isinf, signbit | Bare boolean results |
73
+ | add.reduce, add.accumulate | Preserve unit; accept axis and dtype, plus keepdims for reduce |
74
+
75
+ Ufunc `__call__` accepts `where`, `dtype`, `casting`, `order`, `subok`, and empty
76
+ `out`. Each kernel validates its own options. `where` is a bare boolean scalar
77
+ or plain boolean ndarray; masked-out positions without an output buffer follow
78
+ NumPy's uninitialized-result behavior. Numeric outputs must stay in the
79
+ supported dtype domain. Product reductions, at, outer, and reduceat are
80
+ unsupported. Python `**` also accepts Fraction/Decimal exponents, retaining
81
+ exact unit exponents while passing a float exponent to the NumPy kernel.
82
+ Angles retain unitx's dimensionless model: deg/rad normalize before these
83
+ functions, including exp/log. Python scalar numerical semantics remain native.
84
+
85
+ | NumPy function | Unit/result rule | Supported options |
86
+ | --- | --- | --- |
87
+ | reshape, transpose | Preserve unit | shape/order/copy; axes |
88
+ | concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting |
89
+ | sum, mean | Preserve unit | axis, dtype, keepdims |
90
+ | median | Preserve unit | axis, keepdims; overwrite_input=False |
91
+ | std, var | Preserve unit / square unit | axis, dtype, ddof, keepdims |
92
+ | cumsum | Preserve unit | axis, dtype |
93
+ | argmin, argmax | Bare indices | axis, keepdims |
94
+ | isclose, allclose | Bare mask / Python bool | rtol, atol, equal_nan |
95
+
96
+ Supply the first magnitude argument positionally; concatenate/stack take a
97
+ sequence. Options follow each supported signature, including positional out
98
+ checks. Writable `out` and overwrite_input=True are rejected. Unsupported
99
+ initial, statistical where, mean, correction, and other keywords raise TypeError.
100
+ There is no `__setitem__`; `q += other` creates a result and rebinds q.
101
+
102
+ ## Close comparisons and typing
103
+
104
+ `q.isclose(other, rel_tol=1e-9, abs_tol=0)` uses the symmetric formula in
105
+ coherent SI coordinates:
106
+ `abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)`.
107
+ `np.isclose` instead uses `abs(a-b) <= atol + rtol*abs(b)` with defaults
108
+ rtol=1e-5 and atol=1e-8; `np.allclose` reduces that mask. Tolerances broadcast
109
+ and must be finite, nonnegative, and real. Relative tolerance is dimensionless;
110
+ absolute tolerance matches the operands' dimension. Nonzero bare absolute
111
+ tolerance is dimensionless, so dimensional `np.isclose` calls need a Quantity
112
+ atol or zero. Matching infinities are close; NaNs require equal_nan=True.
113
+ The NumPy path enters at least float64 before subtraction, preserving
114
+ longdouble when present; large integers may lose precision. Mixed NumPy
115
+ operations require supported arrays/scalars or Python int/float/complex.
116
+
117
+ ```python
118
+ np.isclose(q, Quantity(a * 100, 'cm'), atol=Quantity(1e-8, 'm'))
119
+ q.isclose(Quantity(a * 100, 'cm'), abs_tol=Quantity(1e-8, 'm'))
120
+ ```
121
+
122
+ `_numerics.py` defines `Scalar`, `Array`, and `Magnitude = Scalar | Array`
123
+ without importing NumPy. Array is a typing protocol, not a guarantee of other
124
+ backend support. Construction and unit-only operators preserve Quantity[T];
125
+ numerical array results conservatively use Array. NumPy's left-operand and
126
+ function stubs cannot generally infer wrapped unitx results; use
127
+ Quantity(array, unit) or unit * array for precise construction types. Arbitrary
128
+ shape/dtype inference is not promised. NumPy-free scalar consumers retain
129
+ working type hints.
130
+
131
+ References: [NumPy dispatch](https://numpy.org/doc/stable/reference/arrays.classes.html),
132
+ [promotion](https://numpy.org/doc/stable/reference/arrays.promotion.html),
133
+ [isclose](https://numpy.org/doc/stable/reference/generated/numpy.isclose.html).
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'unitx'
7
- version = '0.7.0'
7
+ version = '0.8.0'
8
8
  description = 'A modern, intuitive Python library for units, quantities, and dimensional analysis.'
9
9
  readme = 'README.md'
10
10
  requires-python = '>=3.12'
@@ -23,6 +23,7 @@ classifiers = [
23
23
 
24
24
  [project.optional-dependencies]
25
25
  test = ['pytest>=8', 'ruff>=0.8', 'pyright>=1.1.414']
26
+ numpy = ['numpy>=2.1']
26
27
 
27
28
  [project.urls]
28
29
  Homepage = 'https://github.com/DaitTHU/unitx'
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "include": [
3
3
  "src/unitx/quantity.py",
4
+ "src/unitx/_numpy.py",
4
5
  "src/unitx/_numerics.py",
5
6
  "src/unitx/factor.py",
6
7
  "src/unitx/unit.py",
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./pyrightconfig.json",
3
+ "include": ["tests/numpy_typing"],
4
+ "strict": ["tests/numpy_typing"]
5
+ }
@@ -0,0 +1,33 @@
1
+ """Class-based namespaces for constants accessed directly as class attributes.
2
+
3
+ Namespaces cannot be instantiated, and their attributes cannot be rebound or
4
+ deleted. Objects referenced by those attributes are not recursively frozen.
5
+ """
6
+
7
+ from typing import Any, NoReturn
8
+
9
+ __all__ = ['ConstNamespace']
10
+
11
+
12
+ class _ConstNamespaceMeta(type):
13
+ def __call__(metacls, *args: Any, **kwds: Any) -> NoReturn:
14
+ """
15
+ This namespace cannot be instantiated; access constants via class attributes.
16
+ """
17
+ raise TypeError(metacls.__call__.__doc__)
18
+
19
+ def __setattr__(cls, name, value) -> NoReturn:
20
+ """
21
+ Constant namespace attributes cannot be assigned.
22
+ """
23
+ raise AttributeError(cls.__class__.__setattr__.__doc__)
24
+
25
+ def __delattr__(cls, name) -> NoReturn:
26
+ """
27
+ Constant namespace attributes cannot be deleted.
28
+ """
29
+ raise AttributeError(cls.__class__.__delattr__.__doc__)
30
+
31
+
32
+ class ConstNamespace(metaclass=_ConstNamespaceMeta):
33
+ """Base for constant namespaces with protected class attribute bindings."""