unitx 0.8.3__tar.gz → 0.8.4__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 (73) hide show
  1. {unitx-0.8.3 → unitx-0.8.4}/CHANGELOG.md +25 -0
  2. {unitx-0.8.3 → unitx-0.8.4}/PKG-INFO +45 -22
  3. {unitx-0.8.3 → unitx-0.8.4}/README.md +43 -20
  4. {unitx-0.8.3 → unitx-0.8.4}/docs/numpy.md +70 -7
  5. unitx-0.8.4/examples/README.md +52 -0
  6. unitx-0.8.4/examples/blackbody.ipynb +143 -0
  7. unitx-0.8.4/examples/damped_oscillator.ipynb +153 -0
  8. unitx-0.8.4/examples/gaussian_wavepacket.ipynb +149 -0
  9. unitx-0.8.4/examples/optical_attenuation.ipynb +176 -0
  10. unitx-0.8.4/examples/pendulum_uncertainty.ipynb +188 -0
  11. {unitx-0.8.3 → unitx-0.8.4}/pyproject.toml +2 -2
  12. {unitx-0.8.3 → unitx-0.8.4}/pyrightconfig.json +3 -1
  13. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/_numpy.py +105 -12
  14. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/quantity.py +9 -22
  15. unitx-0.8.4/tests/_notebooks.py +11 -0
  16. {unitx-0.8.3 → unitx-0.8.4}/tests/numpy_typing/check_numpy.py +11 -0
  17. unitx-0.8.4/tests/test_examples.py +42 -0
  18. {unitx-0.8.3 → unitx-0.8.4}/tests/test_numpy.py +9 -6
  19. unitx-0.8.4/tests/test_numpy_integration.py +93 -0
  20. unitx-0.8.4/tests/test_numpy_products.py +296 -0
  21. {unitx-0.8.3 → unitx-0.8.4}/tests/test_typing.py +17 -9
  22. unitx-0.8.3/examples/blackbody.py +0 -20
  23. unitx-0.8.3/pyrightconfig.asarray.json +0 -5
  24. unitx-0.8.3/pyrightconfig.examples.json +0 -5
  25. unitx-0.8.3/pyrightconfig.numpy.json +0 -5
  26. unitx-0.8.3/pyrightconfig.scalar.json +0 -5
  27. {unitx-0.8.3 → unitx-0.8.4}/.github/workflows/ci.yml +0 -0
  28. {unitx-0.8.3 → unitx-0.8.4}/.github/workflows/release.yml +0 -0
  29. {unitx-0.8.3 → unitx-0.8.4}/.gitignore +0 -0
  30. {unitx-0.8.3 → unitx-0.8.4}/LICENSE +0 -0
  31. {unitx-0.8.3 → unitx-0.8.4}/conftest.py +0 -0
  32. {unitx-0.8.3 → unitx-0.8.4}/docs/images/blackbody.png +0 -0
  33. {unitx-0.8.3 → unitx-0.8.4}/docs/migration.md +0 -0
  34. {unitx-0.8.3 → unitx-0.8.4}/docs/scalars.md +0 -0
  35. {unitx-0.8.3 → unitx-0.8.4}/docs/units.md +0 -0
  36. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/__init__.py +0 -0
  37. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/_constnamespace.py +0 -0
  38. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/_numerics.py +0 -0
  39. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/_numpy_typing.py +0 -0
  40. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/_unicode.py +0 -0
  41. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/dimension.py +0 -0
  42. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/exceptions.py +0 -0
  43. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/factor.py +0 -0
  44. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/monomial.py +0 -0
  45. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/py.typed +0 -0
  46. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/si/__init__.py +0 -0
  47. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/si/bases.py +0 -0
  48. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/si/constants.py +0 -0
  49. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/si/units.py +0 -0
  50. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/singleunit.py +0 -0
  51. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/unit.py +0 -0
  52. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/unitparser.py +0 -0
  53. {unitx-0.8.3 → unitx-0.8.4}/src/unitx/unitregistry.py +0 -0
  54. {unitx-0.8.3 → unitx-0.8.4}/tests/__init__.py +0 -0
  55. {unitx-0.8.3 → unitx-0.8.4}/tests/example_typing/check_blackbody.py +0 -0
  56. {unitx-0.8.3 → unitx-0.8.4}/tests/test_builtin_definitions.py +0 -0
  57. {unitx-0.8.3 → unitx-0.8.4}/tests/test_dimension.py +0 -0
  58. {unitx-0.8.3 → unitx-0.8.4}/tests/test_factor.py +0 -0
  59. {unitx-0.8.3 → unitx-0.8.4}/tests/test_import.py +0 -0
  60. {unitx-0.8.3 → unitx-0.8.4}/tests/test_monomial.py +0 -0
  61. {unitx-0.8.3 → unitx-0.8.4}/tests/test_numeric.py +0 -0
  62. {unitx-0.8.3 → unitx-0.8.4}/tests/test_numpy_outputs.py +0 -0
  63. {unitx-0.8.3 → unitx-0.8.4}/tests/test_quantity.py +0 -0
  64. {unitx-0.8.3 → unitx-0.8.4}/tests/test_si.py +0 -0
  65. {unitx-0.8.3 → unitx-0.8.4}/tests/test_singleunit.py +0 -0
  66. {unitx-0.8.3 → unitx-0.8.4}/tests/test_unicode.py +0 -0
  67. {unitx-0.8.3 → unitx-0.8.4}/tests/test_unit.py +0 -0
  68. {unitx-0.8.3 → unitx-0.8.4}/tests/test_unit_definition.py +0 -0
  69. {unitx-0.8.3 → unitx-0.8.4}/tests/test_unitparser.py +0 -0
  70. {unitx-0.8.3 → unitx-0.8.4}/tests/test_unitregistry.py +0 -0
  71. {unitx-0.8.3 → unitx-0.8.4}/tests/test_warnings.py +0 -0
  72. {unitx-0.8.3 → unitx-0.8.4}/tests/typing/check_quantity.py +0 -0
  73. {unitx-0.8.3 → unitx-0.8.4}/tests/typing/check_si.py +0 -0
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.4] - 2026-10-20
4
+
5
+ ### Added
6
+
7
+ - Support prod and multiply.reduce with exact product units, scalar initial
8
+ values (including Quantities), boolean input masks, and staged output writes.
9
+ Require equal selected counts for dimensional outputs; normalize dimensionless
10
+ inputs before reduction. Document empty products and accumulator dtype rules.
11
+ - Support trapezoid integration with Quantity ordinates, coordinates, or scalar
12
+ spacing. Combine their units directly, preserving native coordinate order
13
+ and axis behavior without manual value extraction or unit reconstruction.
14
+
15
+ ### Improved
16
+
17
+ - Keep Python-comparison-to-ufunc mapping in the optional NumPy adapter and
18
+ replace redundant Fractionable type guards with direct isinstance checks.
19
+
20
+ ### Examples
21
+
22
+ - Replace the blackbody script with a notebook and add damped motion, free
23
+ Gaussian wave-packet propagation, optical attenuation, and correlated
24
+ measurement-uncertainty notebooks. Keep calculations as Quantities and derive
25
+ plot labels from their units. Check execution, typing, and output-free notebook
26
+ files without adding Jupyter as a library or test dependency.
27
+
3
28
  ## [0.8.3] - 2026-10-20
4
29
 
5
30
  ### Added
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.8.3
4
- Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
3
+ Version: 0.8.4
4
+ Summary: A modern, intuitive unit library, with exact unit algebra, expressive type hints, and NumPy integration.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
7
7
  Author: Dait
@@ -28,31 +28,37 @@ Description-Content-Type: text/markdown
28
28
  [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
29
29
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
30
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.
31
+ A modern, intuitive unit library for Python 3.12+.
32
+ Exact unit algebra, expressive type hints, and NumPy integration.
33
+
34
+ Write physical formulas with units and constants in Python. `unitx` provides
35
+ unit conversion and dimensional analysis for Python scalars and integrates
36
+ with NumPy array arithmetic, broadcasting, and supported functions.
37
+ Unit scales and rational exponents retain their exact structure.
34
38
 
35
39
  The library is in early development; public APIs may change during the 0.x series.
36
40
 
37
41
  ## Install
38
42
 
39
- Python **3.12 or later** is required. The scalar library has no mandatory
40
- runtime dependencies:
43
+ Python **3.12 or later** is required. Install unitx with:
41
44
 
42
45
  ```sh
43
- python -m pip install unitx
46
+ pip install unitx
44
47
  ```
45
48
 
46
- For arrays, install NumPy support. Add Matplotlib to run the plotting example:
49
+ There are no mandatory runtime dependencies. If you already use NumPy **2.1
50
+ or later**, this installation also lets you use units in your NumPy calculations.
51
+ For NumPy work in a new environment or with an older NumPy version, use the
52
+ following command to install unitx and a compatible NumPy version:
47
53
 
48
54
  ```sh
49
- python -m pip install 'unitx[numpy]' matplotlib
55
+ pip install 'unitx[numpy]'
50
56
  ```
51
57
 
52
- For development, install into the same interpreter that runs your code:
58
+ For development, install into your active Python environment:
53
59
 
54
60
  ```sh
55
- python -m pip install -e '.[test,numpy]'
61
+ pip install -e '.[test,numpy]'
56
62
  ```
57
63
 
58
64
  This README describes the current source tree. For an older release, consult
@@ -60,8 +66,17 @@ its matching Git tag.
60
66
 
61
67
  ## Start with units and constants
62
68
 
63
- `si` provides units; `SI` provides physical constants. Both are read-only
64
- namespaces in `unitx.si`:
69
+ The `unitx.si` submodule provides ready-to-use units and physical constants
70
+ based on the International System of Units (SI), through two read-only
71
+ namespaces:
72
+
73
+ - `si` contains units such as `si.m`, `si.s`, and `si.MeV`, including common
74
+ prefixes and non-SI units.
75
+ - `SI` contains physical constants expressed in SI units, such as the speed
76
+ of light `SI.c` and the Planck constant `SI.h`.
77
+
78
+ In the import below, `unitx.si` is the module path; the imported `si` and `SI`
79
+ are the namespaces you use directly in formulas:
65
80
 
66
81
  ```pycon
67
82
  >>> from unitx.si import SI, si
@@ -85,10 +100,13 @@ division combine units; addition and subtraction align compatible quantities
85
100
  to the left operand's unit. Incompatible conversions and arithmetic raise
86
101
  `DimensionError`.
87
102
 
88
- ## NumPy example: blackbody radiation
103
+ ## Units in NumPy: blackbody radiation
89
104
 
90
- Broadcast a column of wavelengths against three temperatures, then plot
91
- Planck's spectral radiance:
105
+ Attach units to NumPy arrays, then use broadcasting and NumPy functions to
106
+ evaluate a physical formula. Here, a column of wavelengths and three
107
+ temperatures broadcast to give Planck's spectral radiance.
108
+
109
+ Add `matplotlib` to your environment to plot the result:
92
110
 
93
111
  ```python
94
112
  import numpy as np
@@ -121,7 +139,7 @@ values in the catalogue and enter NumPy calculations as floats.
121
139
  `.value_in(unit)` also converts it to a chosen unit. Implicit unit removal with
122
140
  `np.asarray(quantity)` or `np.array(quantity)` raises TypeError.
123
141
 
124
- The [runnable example](examples/blackbody.py) uses `np.asarray([...]) * unit`
142
+ The [blackbody notebook](examples/blackbody.ipynb) uses `np.asarray([...]) * unit`
125
143
  without an explicit dtype. This automatic construction inference is checked
126
144
  with NumPy 2.5 stubs and Pyright; the example also runs on NumPy 2.1. Older
127
145
  NumPy stubs may still infer Any for this expression. NumPy's own stubs can
@@ -131,6 +149,11 @@ infer different types for `np.array` and `np.asarray`;
131
149
  shape/dtype inference and wrapped return types for every NumPy function are not
132
150
  promised. See the [supported operations, storage rules, and typing limits](docs/numpy.md).
133
151
 
152
+ The [physics notebooks](examples/README.md) also cover damped motion, Gaussian
153
+ wave-packet spreading, optical attenuation, and correlated measurement uncertainty.
154
+ They include explanations and numerical checks; plots are generated when run,
155
+ keeping saved notebooks small.
156
+
134
157
  ## Values and conversion
135
158
 
136
159
  Use `Quantity` and `Unit` directly when constructing values from strings:
@@ -219,7 +242,7 @@ copying, and serialization.
219
242
 
220
243
  ## Scope and types
221
244
 
222
- - Supported magnitudes are Python scalars and optional plain numeric NumPy
245
+ - Supported magnitudes are Python scalars and plain numeric NumPy
223
246
  arrays/scalars. Other array backends, ndarray subclasses, object arrays,
224
247
  and implicit list/tuple magnitudes are unsupported.
225
248
  - Conversion uses multiplication by a scale. Offset temperature scales such
@@ -250,9 +273,9 @@ with `_` are internal.
250
273
  Run the checks from the repository root:
251
274
 
252
275
  ```sh
253
- python -m pytest -q
254
- python -m ruff check .
255
- python -m ruff format --check .
276
+ pytest -q
277
+ ruff check .
278
+ ruff format --check .
256
279
  python -m doctest README.md docs/units.md docs/scalars.md
257
280
  ```
258
281
 
@@ -4,31 +4,37 @@
4
4
  [![Python versions](https://img.shields.io/pypi/pyversions/unitx)](https://pypi.org/project/unitx/)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
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.
7
+ A modern, intuitive unit library for Python 3.12+.
8
+ Exact unit algebra, expressive type hints, and NumPy integration.
9
+
10
+ Write physical formulas with units and constants in Python. `unitx` provides
11
+ unit conversion and dimensional analysis for Python scalars and integrates
12
+ with NumPy array arithmetic, broadcasting, and supported functions.
13
+ Unit scales and rational exponents retain their exact structure.
10
14
 
11
15
  The library is in early development; public APIs may change during the 0.x series.
12
16
 
13
17
  ## Install
14
18
 
15
- Python **3.12 or later** is required. The scalar library has no mandatory
16
- runtime dependencies:
19
+ Python **3.12 or later** is required. Install unitx with:
17
20
 
18
21
  ```sh
19
- python -m pip install unitx
22
+ pip install unitx
20
23
  ```
21
24
 
22
- For arrays, install NumPy support. Add Matplotlib to run the plotting example:
25
+ There are no mandatory runtime dependencies. If you already use NumPy **2.1
26
+ or later**, this installation also lets you use units in your NumPy calculations.
27
+ For NumPy work in a new environment or with an older NumPy version, use the
28
+ following command to install unitx and a compatible NumPy version:
23
29
 
24
30
  ```sh
25
- python -m pip install 'unitx[numpy]' matplotlib
31
+ pip install 'unitx[numpy]'
26
32
  ```
27
33
 
28
- For development, install into the same interpreter that runs your code:
34
+ For development, install into your active Python environment:
29
35
 
30
36
  ```sh
31
- python -m pip install -e '.[test,numpy]'
37
+ pip install -e '.[test,numpy]'
32
38
  ```
33
39
 
34
40
  This README describes the current source tree. For an older release, consult
@@ -36,8 +42,17 @@ its matching Git tag.
36
42
 
37
43
  ## Start with units and constants
38
44
 
39
- `si` provides units; `SI` provides physical constants. Both are read-only
40
- namespaces in `unitx.si`:
45
+ The `unitx.si` submodule provides ready-to-use units and physical constants
46
+ based on the International System of Units (SI), through two read-only
47
+ namespaces:
48
+
49
+ - `si` contains units such as `si.m`, `si.s`, and `si.MeV`, including common
50
+ prefixes and non-SI units.
51
+ - `SI` contains physical constants expressed in SI units, such as the speed
52
+ of light `SI.c` and the Planck constant `SI.h`.
53
+
54
+ In the import below, `unitx.si` is the module path; the imported `si` and `SI`
55
+ are the namespaces you use directly in formulas:
41
56
 
42
57
  ```pycon
43
58
  >>> from unitx.si import SI, si
@@ -61,10 +76,13 @@ division combine units; addition and subtraction align compatible quantities
61
76
  to the left operand's unit. Incompatible conversions and arithmetic raise
62
77
  `DimensionError`.
63
78
 
64
- ## NumPy example: blackbody radiation
79
+ ## Units in NumPy: blackbody radiation
65
80
 
66
- Broadcast a column of wavelengths against three temperatures, then plot
67
- Planck's spectral radiance:
81
+ Attach units to NumPy arrays, then use broadcasting and NumPy functions to
82
+ evaluate a physical formula. Here, a column of wavelengths and three
83
+ temperatures broadcast to give Planck's spectral radiance.
84
+
85
+ Add `matplotlib` to your environment to plot the result:
68
86
 
69
87
  ```python
70
88
  import numpy as np
@@ -97,7 +115,7 @@ values in the catalogue and enter NumPy calculations as floats.
97
115
  `.value_in(unit)` also converts it to a chosen unit. Implicit unit removal with
98
116
  `np.asarray(quantity)` or `np.array(quantity)` raises TypeError.
99
117
 
100
- The [runnable example](examples/blackbody.py) uses `np.asarray([...]) * unit`
118
+ The [blackbody notebook](examples/blackbody.ipynb) uses `np.asarray([...]) * unit`
101
119
  without an explicit dtype. This automatic construction inference is checked
102
120
  with NumPy 2.5 stubs and Pyright; the example also runs on NumPy 2.1. Older
103
121
  NumPy stubs may still infer Any for this expression. NumPy's own stubs can
@@ -107,6 +125,11 @@ infer different types for `np.array` and `np.asarray`;
107
125
  shape/dtype inference and wrapped return types for every NumPy function are not
108
126
  promised. See the [supported operations, storage rules, and typing limits](docs/numpy.md).
109
127
 
128
+ The [physics notebooks](examples/README.md) also cover damped motion, Gaussian
129
+ wave-packet spreading, optical attenuation, and correlated measurement uncertainty.
130
+ They include explanations and numerical checks; plots are generated when run,
131
+ keeping saved notebooks small.
132
+
110
133
  ## Values and conversion
111
134
 
112
135
  Use `Quantity` and `Unit` directly when constructing values from strings:
@@ -195,7 +218,7 @@ copying, and serialization.
195
218
 
196
219
  ## Scope and types
197
220
 
198
- - Supported magnitudes are Python scalars and optional plain numeric NumPy
221
+ - Supported magnitudes are Python scalars and plain numeric NumPy
199
222
  arrays/scalars. Other array backends, ndarray subclasses, object arrays,
200
223
  and implicit list/tuple magnitudes are unsupported.
201
224
  - Conversion uses multiplication by a scale. Offset temperature scales such
@@ -226,9 +249,9 @@ with `_` are internal.
226
249
  Run the checks from the repository root:
227
250
 
228
251
  ```sh
229
- python -m pytest -q
230
- python -m ruff check .
231
- python -m ruff format --check .
252
+ pytest -q
253
+ ruff check .
254
+ ruff format --check .
232
255
  python -m doctest README.md docs/units.md docs/scalars.md
233
256
  ```
234
257
 
@@ -80,13 +80,14 @@ methods, mixed array types, and keywords fail explicitly.
80
80
  | sin, cos, tan, exp, expm1, log, log2, log10, log1p | Require coherent dimensionless values; return dimensionless Quantity |
81
81
  | isfinite, isnan, isinf, signbit | Bare boolean results |
82
82
  | add.reduce, add.accumulate | Preserve unit; accept axis, dtype, out, plus keepdims for reduce |
83
+ | multiply.reduce | Product unit rules below; accepts axis, dtype, keepdims, initial, where, out |
83
84
 
84
85
  Ufunc `__call__` accepts `where`, `dtype`, `casting`, `order`, `subok`, and
85
86
  single-output `out` as described below. Each kernel validates its own options.
86
87
  `where` is a bare boolean scalar
87
88
  or plain boolean ndarray; masked-out positions without an output buffer follow
88
89
  NumPy's uninitialized-result behavior. Numeric outputs must stay in the
89
- supported dtype domain. Product reductions, at, outer, and reduceat are
90
+ supported dtype domain. Product accumulation, at, outer, and reduceat are
90
91
  unsupported. Python `**` also accepts Fraction/Decimal exponents, retaining
91
92
  exact unit exponents while passing a float exponent to the NumPy kernel.
92
93
  Angles retain unitx's dimensionless model: deg/rad normalize before these
@@ -97,22 +98,82 @@ functions, including exp/log. Python scalar numerical semantics remain native.
97
98
  | reshape, transpose | Preserve unit | shape/order/copy; axes |
98
99
  | concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting, out; dtype and out are mutually exclusive |
99
100
  | sum, mean | Preserve unit | axis, dtype, keepdims, out |
101
+ | prod | Product unit rules below | axis, dtype, keepdims, initial, where, out |
100
102
  | median | Preserve unit | axis, keepdims, out; overwrite_input=False |
101
103
  | std, var | Preserve unit / square unit | axis, dtype, ddof, keepdims, out |
102
104
  | cumsum | Preserve unit | axis, dtype, out |
103
105
  | argmin, argmax | Bare indices | axis, keepdims, out |
106
+ | trapezoid | Multiply ordinate and coordinate units | x, scalar dx, axis; no out |
104
107
  | isclose, allclose | Bare mask / Python bool | rtol, atol, equal_nan |
105
108
 
106
109
  Supply the first magnitude argument positionally; concatenate/stack take a
107
110
  sequence. Options follow each supported signature, including positional out
108
111
  checks. Function and reduction outputs follow the rules below;
109
- overwrite_input=True is rejected. Unsupported initial, reduction/statistical
110
- where, mean, correction, and other keywords raise TypeError.
112
+ overwrite_input=True is rejected. Reduction/statistical `where` and `initial`
113
+ are supported only for prod/multiply.reduce. Unsupported mean, correction,
114
+ and other keywords raise TypeError.
111
115
  There is no `__setitem__`; `q += other` creates a result and rebinds q.
112
116
 
117
+ `np.trapezoid(y, x=x)` accepts a Quantity for y, x, or both and returns a
118
+ Quantity with unit `y.unit * x.unit`; bare inputs are dimensionless. With
119
+ `x=None`, scalar `dx` supplies the coordinate unit instead (default: dimensionless
120
+ 1). NumPy dispatches this function on y and x, so at least one of those must be
121
+ a Quantity to enter unitx; a Quantity supplied only as dx is insufficient.
122
+ Explicit x makes dx irrelevant. Numeric arrays, scalar spacing, axis handling,
123
+ coordinate order (including reversed or nonmonotonic x), broadcasting, and
124
+ empty intervals follow [NumPy trapezoid](https://numpy.org/doc/stable/reference/generated/numpy.trapezoid.html).
125
+ No coordinate sorting, interpolation, dtype option, or output buffer is added.
126
+
127
+ ## Product reductions
128
+
129
+ `np.prod(q)` reduces all axes by default; `np.multiply.reduce(q)` defaults to
130
+ axis 0, following NumPy. Both accept scalar, negative, or tuple axes, `axis=None`,
131
+ `axis=()`, and `keepdims`. For dimensional input with unit U, each result has
132
+ unit `U**N`, where N is the number of selected input elements in that slice.
133
+ Zero-valued elements still count. All output slices must have the same N;
134
+ otherwise unitx raises DimensionError because a single Quantity cannot hold
135
+ different dimensions. `where` accepts the same bare boolean masks as ufunc
136
+ calls and broadcasts to the input shape. It selects input elements; every
137
+ output position is written when an output buffer is supplied.
138
+
139
+ An empty or fully masked product has value 1 and unit `U**0`, before applying
140
+ `initial`. If the output itself has no elements, the unit uses the nominal
141
+ reduced-axis extent; scalar `where=False` sets that extent to zero.
142
+ Dimensionless inputs, including deg and km/m, first convert to coherent
143
+ dimensionless values and produce unit `Unit()`. Their masked slices may have
144
+ different counts because the result still has a single coherent unit.
145
+
146
+ `initial` is a bare numeric scalar or a scalar Quantity, including NumPy
147
+ scalars and zero-dimensional arrays. A bare initial is dimensionless; a
148
+ Quantity initial contributes its own unit once, so the result unit is
149
+ `U**N * initial.unit`. An explicit zero still contributes its declared unit.
150
+ The default is dimensionless 1. `initial=None` uses NumPy's first-element
151
+ semantics and fails for empty reductions. NumPy controls the accumulator dtype,
152
+ including casting the initial value to that dtype; initial does not select
153
+ a wider numerical domain. Decimal/Fraction initial values enter NumPy as floats.
154
+
155
+ ```python
156
+ lengths = Quantity(np.array([[2.0, 3, 4], [5, 6, 7]]), 'm')
157
+ selected = np.array([True, False, True])
158
+ areas = np.prod(lengths, axis=1, where=selected)
159
+ assert areas.unit == Unit('m2')
160
+ np.testing.assert_array_equal(areas.value, [8, 35])
161
+ weighted = np.multiply.reduce(lengths, axis=1, where=selected, initial=Quantity(2, 's'))
162
+ assert weighted.unit == Unit('m2 s')
163
+ ```
164
+
165
+ Both entry points support the output rules below, including exact result
166
+ shape and unchanged targets on computation, unit, shape, or casting errors.
167
+ `cumprod` and `multiply.accumulate` remain unsupported: partial products
168
+ generally require different units at different positions. The supported
169
+ axis, empty-product, initial, and mask behavior follows
170
+ [NumPy prod](https://numpy.org/doc/stable/reference/generated/numpy.prod.html)
171
+ and [ufunc.reduce](https://numpy.org/doc/stable/reference/generated/numpy.ufunc.reduce.html),
172
+ with the additional single-unit and explicit-output rules described here.
173
+
113
174
  ## Output buffers
114
175
 
115
- Supported single-output ufunc calls, add.reduce/accumulate, and the numeric
176
+ Supported single-output ufunc calls, add.reduce/accumulate, multiply.reduce, and the numeric
116
177
  functions listed with `out` above accept a Quantity whose magnitude is a
117
178
  writable plain numeric ndarray. `out=target`, `out=(target,)`, and positional
118
179
  outputs for ufuncs return that same target; functions accept `out=target` or
@@ -157,7 +218,7 @@ The final copy is not a general transaction against concurrent changes or
157
218
  external buffer failures. Temporaries cost memory proportional to the result
158
219
  and target; `out` provides controlled writes, not a zero-allocation guarantee.
159
220
 
160
- Functions and add.reduce/accumulate use `same_kind` for the final cast and do
221
+ Functions and supported reductions/accumulations use `same_kind` for the final cast and do
161
222
  not accept a new `casting` keyword. Concatenate and stack use their existing
162
223
  `casting` option for the final write as well as the kernel; their `dtype` and
163
224
  `out` options cannot be combined. Index outputs also use `same_kind`, including
@@ -174,7 +235,7 @@ assert np.argmax(q, axis=0, out=indices) is indices
174
235
  ```
175
236
 
176
237
  `out=None` and ufunc `out=(None,)` allocate ordinary results.
177
- Multiple-output ufuncs and product reductions remain unsupported.
238
+ Multiple-output ufuncs and product accumulations remain unsupported.
178
239
  Output writes preserve the magnitude's type and dtype, so the covariant
179
240
  `Quantity[T]` contract remains valid; no setter or arbitrary replacement of T
180
241
  is introduced. NumPy's own stubs do not accept Quantity outputs or infer wrapped
@@ -221,7 +282,9 @@ NumPy's many arithmetic overloads against Scalar's own return protocol.
221
282
  `_numpy_typing.py` imports concrete NumPy types only for static analysis; core
222
283
  runtime imports still do not load NumPy. Strict scalar-consumer checks also
223
284
  run with NumPy absent. Checking the full library's optional NumPy annotations
224
- requires NumPy; `pyrightconfig.scalar.json` checks scalar consumers without it.
285
+ requires NumPy; `pyright tests/typing` checks scalar consumers without it.
286
+ All checks share `pyrightconfig.json`; the test runner selects the input
287
+ directories and skips optional cases when their dependencies are unavailable.
225
288
 
226
289
  NumPy's left-operand and function stubs do not generally describe every
227
290
  unit-aware dispatch. With NumPy 2.5 stubs and Pyright 1.1.414, the documented
@@ -0,0 +1,52 @@
1
+ # Physics notebooks
2
+
3
+ These notebooks use unitx for dimensional calculations and NumPy for numerical
4
+ work. Each contains a model, assumptions, plots, and numerical checks. The
5
+ material parameters and measurement draws are illustrative, not experimental data.
6
+
7
+ | Notebook | Topic | unitx features |
8
+ | --- | --- | --- |
9
+ | [Blackbody radiation](blackbody.ipynb) | Planck spectra and Wien peak checks | Constants, broadcasting, spectral-density conversion |
10
+ | [Damped oscillator](damped_oscillator.ipynb) | Release from rest and energy loss | Dimensionless ufunc arguments, energy units, comparisons |
11
+ | [Gaussian wave packet](gaussian_wavepacket.ipynb) | Free electron propagation and uncertainty | Probability-density units, quantum constants, unit-aware integration |
12
+ | [Optical attenuation](optical_attenuation.ipynb) | Transmission through absorbing layers | prod/multiply.reduce, input masks, Quantity initial, fixed-unit out |
13
+ | [Pendulum uncertainty](pendulum_uncertainty.ipynb) | Correlated input uncertainties and Monte Carlo propagation | Mean/std/var units, mixed measurement units, explicit quantiles |
14
+
15
+ ## Run
16
+
17
+ Use Python 3.12 or newer. From the repository root, install the current checkout
18
+ and notebook tools in the environment you will select as the Jupyter kernel:
19
+
20
+ ```sh
21
+ python -m pip install -e '.[numpy]' matplotlib jupyterlab
22
+ jupyter lab examples/
23
+ ```
24
+
25
+ Open a notebook and run all cells from the beginning. These notebooks require
26
+ unitx 0.8.4 or newer for product reductions and unit-aware integration.
27
+ The examples do not download datasets or write files. Keep physical quantities in calculations;
28
+ plot their `.value` and derive labels from their `.unit`. Convert only to choose
29
+ a useful display scale or to align units at a numerical boundary. Quantiles use
30
+ an explicit numerical boundary and reattach the input unit, without naming a
31
+ variable after its unit. Display equations use `$$...$$`.
32
+
33
+ ## Keep Git history small
34
+
35
+ Commit only notebook source and minimal kernel metadata. Clear all outputs and
36
+ execution counts before committing; do not embed figures, widget state, or
37
+ attachments. Figures are regenerated when cells run. The test suite checks this
38
+ policy and executes the numerical examples when NumPy and Matplotlib are present.
39
+
40
+ To clear a notebook after running it, use JupyterLab's **Clear Outputs of All
41
+ Cells**, then save it. For a command-line cleanup using nbconvert:
42
+
43
+ ```sh
44
+ python -m pip install nbconvert
45
+ jupyter nbconvert --clear-output --inplace examples/*.ipynb
46
+ ```
47
+
48
+ The notebooks use ordinary Python code cells. Their code is also extracted for
49
+ the existing Pyright example check, without committing duplicate `.py` exports.
50
+ Assertions on NumPy results also check that dispatch retains the Quantity
51
+ wrapper. NumPy's stubs cannot describe every wrapped result; the optical
52
+ example documents local suppressions for its Quantity `initial` and `out` call.
@@ -0,0 +1,143 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "blackbody-00",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Blackbody spectral radiance\n",
9
+ "\n",
10
+ "Compare thermal spectra at three temperatures with wavelength on one array\n",
11
+ "axis and temperature on the other. This is the notebook version of the\n",
12
+ "README example. Run the setup in [examples/README.md](README.md) first.\n",
13
+ "\n",
14
+ "For an ideal blackbody, the spectral radiance per unit wavelength is\n",
15
+ "$$\n",
16
+ "B_\\lambda(T)=\\frac{2hc^2}{\\lambda^5}\n",
17
+ "\\left[\\exp\\!\\left(\\frac{hc}{\\lambda k_B T}\\right)-1\\right]^{-1}.\n",
18
+ "$$\n",
19
+ "Here $h$ is Planck's constant, $c$ is the speed of light, $k_B$ is Boltzmann's\n",
20
+ "constant, and $T$ is absolute temperature. The exponential argument is\n",
21
+ "dimensionless. Radiance is per unit solid angle; it is not the hemispheric\n",
22
+ "radiant exitance, which has an additional factor of $\\pi$ for this model.\n",
23
+ "\n",
24
+ "Reference: [CIE, Planck's law](https://cie.co.at/eilvterm/17-24-005)."
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "execution_count": null,
30
+ "id": "blackbody-01",
31
+ "metadata": {},
32
+ "outputs": [],
33
+ "source": [
34
+ "import numpy as np\n",
35
+ "from matplotlib import pyplot as plt\n",
36
+ "\n",
37
+ "from unitx.si import SI, si"
38
+ ]
39
+ },
40
+ {
41
+ "cell_type": "markdown",
42
+ "id": "blackbody-02",
43
+ "metadata": {},
44
+ "source": [
45
+ "## Broadcast over wavelengths and temperatures\n",
46
+ "\n",
47
+ "The wavelength array has shape `(100, 1)` and the temperature array `(3,)`.\n",
48
+ "Each expression below produces a `(100, 3)` Quantity with one uniform unit.\n",
49
+ "`expm1` evaluates $\\exp(x)-1$ accurately near zero. The selected range also\n",
50
+ "keeps the short-wavelength exponential within floating-point range."
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "id": "blackbody-03",
57
+ "metadata": {},
58
+ "outputs": [],
59
+ "source": [
60
+ "lam = np.linspace(0.01, 3, 100)[:, None] * si.um # wavelengths in microns\n",
61
+ "T = np.asarray([3000, 4000, 5000]) * si.K # temperatures in Kelvin\n",
62
+ "\n",
63
+ "nu = SI.c / lam\n",
64
+ "radiance = 2 * SI.h * SI.c**2 / (lam**5 * np.expm1(SI.h * nu / (SI.kB * T)))\n",
65
+ "radiance = radiance.to('kW/sr/m2/nm') # spectral radiance per unit wavelength\n",
66
+ "\n",
67
+ "print('Radiance shape:', radiance.shape)\n",
68
+ "print('Radiance unit:', radiance.unit)"
69
+ ]
70
+ },
71
+ {
72
+ "cell_type": "markdown",
73
+ "id": "blackbody-04",
74
+ "metadata": {},
75
+ "source": [
76
+ "## Plot with explicit numerical exits\n",
77
+ "\n",
78
+ "Matplotlib receives values in the units written on the axes. The spectral\n",
79
+ "density conversion includes the change from per metre to per nanometre."
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "id": "blackbody-05",
86
+ "metadata": {},
87
+ "outputs": [],
88
+ "source": [
89
+ "fig, ax = plt.subplots(figsize=(7, 4), layout='constrained')\n",
90
+ "ax.plot(lam.value, radiance.value)\n",
91
+ "ax.set(\n",
92
+ " xlabel=f'Wavelength [{lam.unit}]',\n",
93
+ " ylabel=f'Spectral radiance [{radiance.unit}]',\n",
94
+ " title='Blackbody radiation',\n",
95
+ ")\n",
96
+ "ax.legend([f'T = {temperature}' for temperature in T])\n",
97
+ "ax.grid(True)\n",
98
+ "plt.show()"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "markdown",
103
+ "id": "blackbody-06",
104
+ "metadata": {},
105
+ "source": [
106
+ "## Check the wavelength peaks\n",
107
+ "\n",
108
+ "Wien's displacement law gives $\\lambda_{\\max}T=b$, with `SI.b` provided by\n",
109
+ "unitx. A grid maximum has finite wavelength resolution; compare it to the\n",
110
+ "continuous prediction with an absolute tolerance tied to the grid spacing.\n",
111
+ "This is the peak of $B_\\lambda$, not the peak of $B_\\nu$ mapped to wavelength."
112
+ ]
113
+ },
114
+ {
115
+ "cell_type": "code",
116
+ "execution_count": null,
117
+ "id": "blackbody-07",
118
+ "metadata": {},
119
+ "outputs": [],
120
+ "source": [
121
+ "assert radiance.shape == (100, 3)\n",
122
+ "assert np.all(np.isfinite(radiance.value)) and np.all(radiance.value >= 0)\n",
123
+ "lambda_peak = lam[np.argmax(radiance.value, axis=0), 0]\n",
124
+ "lambda_formula = SI.b / T\n",
125
+ "assert np.all(lambda_peak.isclose(lambda_formula, rel_tol=0, abs_tol=0.02 * si.um))\n",
126
+ "print('Grid peaks:', lambda_peak)\n",
127
+ "print('Wien prediction:', lambda_formula.to(lambda_peak.unit))"
128
+ ]
129
+ }
130
+ ],
131
+ "metadata": {
132
+ "kernelspec": {
133
+ "display_name": "Python 3",
134
+ "language": "python",
135
+ "name": "python3"
136
+ },
137
+ "language_info": {
138
+ "name": "python"
139
+ }
140
+ },
141
+ "nbformat": 4,
142
+ "nbformat_minor": 5
143
+ }