unitx 0.8.2__tar.gz → 0.8.3__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.
- {unitx-0.8.2 → unitx-0.8.3}/CHANGELOG.md +12 -0
- {unitx-0.8.2 → unitx-0.8.3}/PKG-INFO +1 -1
- {unitx-0.8.2 → unitx-0.8.3}/docs/numpy.md +40 -15
- {unitx-0.8.2 → unitx-0.8.3}/pyproject.toml +1 -1
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/_numpy.py +51 -30
- {unitx-0.8.2 → unitx-0.8.3}/tests/numpy_typing/check_numpy.py +6 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_numpy.py +9 -8
- unitx-0.8.3/tests/test_numpy_outputs.py +229 -0
- {unitx-0.8.2 → unitx-0.8.3}/.github/workflows/ci.yml +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/.github/workflows/release.yml +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/.gitignore +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/LICENSE +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/README.md +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/conftest.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/docs/images/blackbody.png +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/docs/migration.md +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/docs/scalars.md +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/docs/units.md +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/examples/blackbody.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/pyrightconfig.asarray.json +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/pyrightconfig.examples.json +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/pyrightconfig.json +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/pyrightconfig.numpy.json +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/pyrightconfig.scalar.json +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/__init__.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/_constnamespace.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/_numerics.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/_numpy_typing.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/_unicode.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/dimension.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/exceptions.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/factor.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/monomial.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/py.typed +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/quantity.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/si/__init__.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/si/bases.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/si/constants.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/si/units.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/singleunit.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/unit.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/unitparser.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/src/unitx/unitregistry.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/__init__.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/example_typing/check_blackbody.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_builtin_definitions.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_dimension.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_factor.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_import.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_monomial.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_numeric.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_quantity.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_si.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_singleunit.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_typing.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_unicode.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_unit.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_unit_definition.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_unitparser.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_unitregistry.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/test_warnings.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/typing/check_quantity.py +0 -0
- {unitx-0.8.2 → unitx-0.8.3}/tests/typing/check_si.py +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.3] - 2026-10-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Support unit-aware `out` for sum, mean, median, std, var, cumsum,
|
|
8
|
+
concatenate, stack, and add.reduce/accumulate. Require the exact result shape,
|
|
9
|
+
preserve target identity, unit, and dtype, and stage computation, conversion,
|
|
10
|
+
and casting before writing. Argmin/argmax accept bare integer ndarray outputs.
|
|
11
|
+
- Document function/reduction output casting and add regressions for scaled
|
|
12
|
+
units, positional outputs, overlapping/strided buffers, and unchanged outputs
|
|
13
|
+
on failure, plus installed-consumer type checks.
|
|
14
|
+
|
|
3
15
|
## [0.8.2] - 2026-10-20
|
|
4
16
|
|
|
5
17
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
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
|
|
@@ -79,7 +79,7 @@ methods, mixed array types, and keywords fail explicitly.
|
|
|
79
79
|
| power, float_power / `**` | Dimensional bases require a finite real scalar exponent; dimensionless bases/exponents normalize their scales |
|
|
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
|
-
| add.reduce, add.accumulate | Preserve unit; accept axis
|
|
82
|
+
| add.reduce, add.accumulate | Preserve unit; accept axis, dtype, out, plus keepdims for reduce |
|
|
83
83
|
|
|
84
84
|
Ufunc `__call__` accepts `where`, `dtype`, `casting`, `order`, `subok`, and
|
|
85
85
|
single-output `out` as described below. Each kernel validates its own options.
|
|
@@ -95,25 +95,29 @@ functions, including exp/log. Python scalar numerical semantics remain native.
|
|
|
95
95
|
| NumPy function | Unit/result rule | Supported options |
|
|
96
96
|
| --- | --- | --- |
|
|
97
97
|
| reshape, transpose | Preserve unit | shape/order/copy; axes |
|
|
98
|
-
| concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting |
|
|
99
|
-
| sum, mean | Preserve unit | axis, dtype, keepdims |
|
|
100
|
-
| median | Preserve unit | axis, keepdims; overwrite_input=False |
|
|
101
|
-
| std, var | Preserve unit / square unit | axis, dtype, ddof, keepdims |
|
|
102
|
-
| cumsum | Preserve unit | axis, dtype |
|
|
103
|
-
| argmin, argmax | Bare indices | axis, keepdims |
|
|
98
|
+
| concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting, out; dtype and out are mutually exclusive |
|
|
99
|
+
| sum, mean | Preserve unit | axis, dtype, keepdims, out |
|
|
100
|
+
| median | Preserve unit | axis, keepdims, out; overwrite_input=False |
|
|
101
|
+
| std, var | Preserve unit / square unit | axis, dtype, ddof, keepdims, out |
|
|
102
|
+
| cumsum | Preserve unit | axis, dtype, out |
|
|
103
|
+
| argmin, argmax | Bare indices | axis, keepdims, out |
|
|
104
104
|
| isclose, allclose | Bare mask / Python bool | rtol, atol, equal_nan |
|
|
105
105
|
|
|
106
106
|
Supply the first magnitude argument positionally; concatenate/stack take a
|
|
107
107
|
sequence. Options follow each supported signature, including positional out
|
|
108
|
-
checks. Function and reduction
|
|
109
|
-
|
|
108
|
+
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.
|
|
110
111
|
There is no `__setitem__`; `q += other` creates a result and rebinds q.
|
|
111
112
|
|
|
112
|
-
##
|
|
113
|
+
## Output buffers
|
|
113
114
|
|
|
114
|
-
Supported single-output ufunc calls
|
|
115
|
+
Supported single-output ufunc calls, add.reduce/accumulate, and the numeric
|
|
116
|
+
functions listed with `out` above accept a Quantity whose magnitude is a
|
|
115
117
|
writable plain numeric ndarray. `out=target`, `out=(target,)`, and positional
|
|
116
|
-
outputs return that same target
|
|
118
|
+
outputs for ufuncs return that same target; functions accept `out=target` or
|
|
119
|
+
the positional output argument in their NumPy signature, without a tuple.
|
|
120
|
+
The target's magnitude object, dtype, and unit stay
|
|
117
121
|
fixed. Zero-dimensional, empty, and noncontiguous output arrays are supported.
|
|
118
122
|
NumPy scalars and Python scalars cannot serve as output buffers.
|
|
119
123
|
|
|
@@ -127,7 +131,10 @@ converts that result to the target unit and casts it into a staging buffer.
|
|
|
127
131
|
The numerical kernel uses the input dtypes and any explicit `dtype`; the
|
|
128
132
|
target dtype is used for the final cast. This is not a promise of NumPy's
|
|
129
133
|
output-dependent kernel selection or allocation-free execution. Both the
|
|
130
|
-
result and `where` mask must broadcast to the target shape
|
|
134
|
+
result and `where` mask must broadcast to the target shape for ufunc calls.
|
|
135
|
+
Function and reduction outputs must have exactly the ordinary result's shape,
|
|
136
|
+
including axes retained by `keepdims`; they do not broadcast into a larger
|
|
137
|
+
target. Only true mask
|
|
131
138
|
positions are written; false positions retain their original target values.
|
|
132
139
|
Unit conversion also skips false positions. Input/output overlap and shared
|
|
133
140
|
slices are handled through temporaries; a mask sharing output storage is copied.
|
|
@@ -138,6 +145,8 @@ length target. `ScaledUnit` factors participate in the conversion. Every numeric
|
|
|
138
145
|
result, including a coherent or scaled dimensionless result, requires a Quantity
|
|
139
146
|
target; writing it to a bare ndarray would discard its unit. Boolean predicates
|
|
140
147
|
and comparisons instead accept only a bare boolean ndarray output.
|
|
148
|
+
`argmin` and `argmax` accept only a bare integer ndarray output, with the
|
|
149
|
+
same exact shape requirement as other function outputs.
|
|
141
150
|
|
|
142
151
|
Final casting uses `casting='same_kind'` by default, or the explicitly requested
|
|
143
152
|
NumPy casting policy. It does not silently enable `unsafe`; nonintegral unit
|
|
@@ -148,8 +157,24 @@ The final copy is not a general transaction against concurrent changes or
|
|
|
148
157
|
external buffer failures. Temporaries cost memory proportional to the result
|
|
149
158
|
and target; `out` provides controlled writes, not a zero-allocation guarantee.
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
Functions and add.reduce/accumulate use `same_kind` for the final cast and do
|
|
161
|
+
not accept a new `casting` keyword. Concatenate and stack use their existing
|
|
162
|
+
`casting` option for the final write as well as the kernel; their `dtype` and
|
|
163
|
+
`out` options cannot be combined. Index outputs also use `same_kind`, including
|
|
164
|
+
its allowance for integer narrowing. These policies intentionally do not
|
|
165
|
+
reproduce every function's native output casting rule. An explicit `dtype`
|
|
166
|
+
controls the temporary numerical computation for supported reductions; the
|
|
167
|
+
target dtype does not select the accumulator or kernel dtype.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
totals = Quantity(np.empty(3), 'cm')
|
|
171
|
+
assert np.sum(q, axis=0, out=totals) is totals
|
|
172
|
+
indices = np.empty(3, dtype=np.intp)
|
|
173
|
+
assert np.argmax(q, axis=0, out=indices) is indices
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`out=None` and ufunc `out=(None,)` allocate ordinary results.
|
|
177
|
+
Multiple-output ufuncs and product reductions remain unsupported.
|
|
153
178
|
Output writes preserve the magnitude's type and dtype, so the covariant
|
|
154
179
|
`Quantity[T]` contract remains valid; no setter or arbitrary replacement of T
|
|
155
180
|
is introduced. NumPy's own stubs do not accept Quantity outputs or infer wrapped
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
An identity scale retains storage. Integral scales use a Python int multiplier;
|
|
4
4
|
nonintegral rational scales use one Python float multiplier, and irrational
|
|
5
5
|
scales use Factor's approximation. NumPy then controls promotion and errors.
|
|
6
|
-
No Fraction/Decimal object arrays are produced. Explicit
|
|
6
|
+
No Fraction/Decimal object arrays are produced. Explicit outputs write
|
|
7
7
|
only after computing, converting, and casting a temporary result.
|
|
8
8
|
"""
|
|
9
9
|
|
|
@@ -96,7 +96,11 @@ def _validate_where(kwargs: dict[str, Any]) -> None:
|
|
|
96
96
|
def dispatch_ufunc(ufunc: Any, method: str, inputs: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
|
97
97
|
outputs = kwargs.get('out', ())
|
|
98
98
|
if any(x is not None for x in outputs):
|
|
99
|
-
if
|
|
99
|
+
if (
|
|
100
|
+
method not in ('__call__', 'reduce', 'accumulate')
|
|
101
|
+
or ufunc.nout != 1
|
|
102
|
+
or len(outputs) != 1
|
|
103
|
+
):
|
|
100
104
|
return NotImplemented
|
|
101
105
|
target = outputs[0]
|
|
102
106
|
predicates = (
|
|
@@ -120,33 +124,56 @@ def dispatch_ufunc(ufunc: Any, method: str, inputs: tuple[Any, ...], kwargs: dic
|
|
|
120
124
|
result = _dispatch_ufunc(ufunc, method, inputs, options)
|
|
121
125
|
if result is NotImplemented:
|
|
122
126
|
return NotImplemented
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
return _write_output(
|
|
128
|
+
result,
|
|
129
|
+
target,
|
|
130
|
+
where=options.get('where', True),
|
|
131
|
+
casting=options.get('casting', 'same_kind'),
|
|
132
|
+
broadcast=method == '__call__',
|
|
133
|
+
)
|
|
128
134
|
return _dispatch_ufunc(ufunc, method, inputs, kwargs)
|
|
129
135
|
|
|
130
136
|
|
|
131
|
-
def
|
|
137
|
+
def _write_output(
|
|
138
|
+
result: Any,
|
|
139
|
+
target: Any,
|
|
140
|
+
*,
|
|
141
|
+
where: Any = True,
|
|
142
|
+
casting: Any = 'same_kind',
|
|
143
|
+
broadcast: bool = False,
|
|
144
|
+
) -> Any:
|
|
132
145
|
"""Stage a single output without replacing its magnitude, dtype, or unit."""
|
|
133
|
-
|
|
146
|
+
if target is None:
|
|
147
|
+
return result
|
|
134
148
|
if isinstance(result, Quantity):
|
|
149
|
+
if not isinstance(target, Quantity) or type(target.value) is not np.ndarray:
|
|
150
|
+
raise TypeError('Quantity out requires a plain numeric ndarray buffer.')
|
|
135
151
|
if result.dimension != target.dimension:
|
|
136
152
|
raise DimensionError('out has an incompatible dimension.')
|
|
137
|
-
|
|
153
|
+
factor = result.unit.factor / target.unit.factor
|
|
138
154
|
buffer = target.value
|
|
139
155
|
else:
|
|
140
|
-
|
|
156
|
+
kinds = 'b' if np.asarray(result).dtype.kind == 'b' else 'iu'
|
|
157
|
+
if type(target) is not np.ndarray or target.dtype.kind not in kinds:
|
|
158
|
+
raise TypeError('Unitless out requires a plain ndarray with the matching result kind.')
|
|
159
|
+
factor = ONE
|
|
160
|
+
buffer = target
|
|
161
|
+
if not buffer.flags.writeable:
|
|
162
|
+
raise ValueError('out buffer is read-only.')
|
|
163
|
+
value = result.value if isinstance(result, Quantity) else result
|
|
164
|
+
if not broadcast and np.shape(value) != buffer.shape:
|
|
165
|
+
raise ValueError('out must have exactly the result shape.')
|
|
166
|
+
if isinstance(result, Quantity):
|
|
167
|
+
value = scale_magnitude(value, factor, where=where)
|
|
141
168
|
# Validate broadcasting and casting before touching shared storage. Casting
|
|
142
169
|
# also runs in temporary storage so e.g. float narrowing errors cannot
|
|
143
170
|
# partially overwrite the caller's buffer.
|
|
144
171
|
value = np.broadcast_to(value, buffer.shape)
|
|
145
|
-
mask = np.broadcast_to(
|
|
172
|
+
mask = np.broadcast_to(where, buffer.shape)
|
|
146
173
|
if np.may_share_memory(mask, buffer):
|
|
147
174
|
mask = mask.copy()
|
|
148
175
|
staged = np.empty(buffer.shape, dtype=buffer.dtype)
|
|
149
|
-
np.copyto(staged, value, where=mask, casting=
|
|
176
|
+
np.copyto(staged, value, where=mask, casting=casting)
|
|
150
177
|
np.copyto(buffer, staged, where=mask, casting='no')
|
|
151
178
|
return target
|
|
152
179
|
|
|
@@ -306,11 +333,6 @@ def isclose(
|
|
|
306
333
|
return result
|
|
307
334
|
|
|
308
335
|
|
|
309
|
-
def _reject_out(out: Any) -> None:
|
|
310
|
-
if out is not None:
|
|
311
|
-
raise TypeError('Writable out is not supported for Quantity.')
|
|
312
|
-
|
|
313
|
-
|
|
314
336
|
def dispatch_function(
|
|
315
337
|
func: Any, types: tuple[type, ...], args: tuple[Any, ...], kwargs: dict[str, Any]
|
|
316
338
|
) -> Any:
|
|
@@ -364,7 +386,8 @@ def _join_arrays(
|
|
|
364
386
|
dtype: Any = None,
|
|
365
387
|
casting: str = 'same_kind',
|
|
366
388
|
) -> Any:
|
|
367
|
-
|
|
389
|
+
if out is not None and dtype is not None:
|
|
390
|
+
raise TypeError('Cannot provide both dtype and out for concatenate/stack.')
|
|
368
391
|
arrays = tuple(arrays)
|
|
369
392
|
if not arrays or not all(_is_supported_operand(x) for x in arrays):
|
|
370
393
|
return NotImplemented
|
|
@@ -379,7 +402,8 @@ def _join_arrays(
|
|
|
379
402
|
values.append(scale_magnitude(raw, unit.factor / first.unit.factor))
|
|
380
403
|
if dtype is not None and np.dtype(dtype).kind not in 'iufc':
|
|
381
404
|
raise TypeError('Only numeric output dtypes are supported.')
|
|
382
|
-
|
|
405
|
+
result = Quantity(func(values, axis=axis, dtype=dtype, casting=casting), first.unit)
|
|
406
|
+
return _write_output(result, out, casting=casting)
|
|
383
407
|
|
|
384
408
|
|
|
385
409
|
def _reshape(q: Any, shape: Any, order: Literal['C', 'F', 'A'] = 'C', *, copy: Any = None) -> Any:
|
|
@@ -393,31 +417,28 @@ def _transpose(q: Any, axes: Any = None) -> Any:
|
|
|
393
417
|
def _argmin_max(
|
|
394
418
|
func: Any, q: Any, axis: Any = None, out: Any = None, *, keepdims: bool = False
|
|
395
419
|
) -> Any:
|
|
396
|
-
|
|
397
|
-
return func(q.value, axis=axis, keepdims=keepdims)
|
|
420
|
+
return _write_output(func(q.value, axis=axis, keepdims=keepdims), out)
|
|
398
421
|
|
|
399
422
|
|
|
400
423
|
def _median(
|
|
401
424
|
q: Any, axis: Any = None, out: Any = None, overwrite_input: bool = False, keepdims: bool = False
|
|
402
425
|
) -> Any:
|
|
403
|
-
_reject_out(out)
|
|
404
426
|
if overwrite_input:
|
|
405
427
|
raise TypeError('overwrite_input is not supported for Quantity.')
|
|
406
|
-
return Quantity(np.median(q.value, axis=axis, keepdims=keepdims), q.unit)
|
|
428
|
+
return _write_output(Quantity(np.median(q.value, axis=axis, keepdims=keepdims), q.unit), out)
|
|
407
429
|
|
|
408
430
|
|
|
409
431
|
def _cumsum(q: Any, axis: Any = None, dtype: Any = None, out: Any = None) -> Any:
|
|
410
|
-
_reject_out(out)
|
|
411
432
|
_validate_dtype(dtype)
|
|
412
|
-
return Quantity(np.cumsum(q.value, axis=axis, dtype=dtype), q.unit)
|
|
433
|
+
return _write_output(Quantity(np.cumsum(q.value, axis=axis, dtype=dtype), q.unit), out)
|
|
413
434
|
|
|
414
435
|
|
|
415
436
|
def _sum_mean(
|
|
416
437
|
func: Any, q: Any, axis: Any = None, dtype: Any = None, out: Any = None, keepdims: bool = False
|
|
417
438
|
) -> Any:
|
|
418
|
-
_reject_out(out)
|
|
419
439
|
_validate_dtype(dtype)
|
|
420
|
-
|
|
440
|
+
result = Quantity(func(q.value, axis=axis, dtype=dtype, keepdims=keepdims), q.unit)
|
|
441
|
+
return _write_output(result, out)
|
|
421
442
|
|
|
422
443
|
|
|
423
444
|
def _std_var(
|
|
@@ -429,10 +450,10 @@ def _std_var(
|
|
|
429
450
|
ddof: Any = 0,
|
|
430
451
|
keepdims: bool = False,
|
|
431
452
|
) -> Any:
|
|
432
|
-
_reject_out(out)
|
|
433
453
|
_validate_dtype(dtype)
|
|
434
454
|
unit = q.unit**2 if func is np.var else q.unit
|
|
435
|
-
|
|
455
|
+
result = Quantity(func(q.value, axis=axis, dtype=dtype, ddof=ddof, keepdims=keepdims), unit)
|
|
456
|
+
return _write_output(result, out)
|
|
436
457
|
|
|
437
458
|
|
|
438
459
|
def _validate_dtype(dtype: Any) -> None:
|
|
@@ -78,6 +78,12 @@ def output_contracts(a: npt.NDArray[np.float64]) -> None:
|
|
|
78
78
|
# never the magnitude object or dtype described by the narrower type.
|
|
79
79
|
# NumPy's own out stubs accept ndarray only; check the protocol entry here.
|
|
80
80
|
q.__array_ufunc__(np.add, '__call__', q, q, out=(widened,))
|
|
81
|
+
q.__array_ufunc__(np.add, 'accumulate', q, out=(widened,))
|
|
82
|
+
q.__array_function__(np.cumsum, (Quantity,), (q,), {'out': widened})
|
|
83
|
+
scalar_target = Quantity(np.empty(()), 'cm')
|
|
84
|
+
q.__array_ufunc__(np.add, 'reduce', q, out=(scalar_target,))
|
|
85
|
+
q.__array_function__(np.sum, (Quantity,), (q,), {'out': scalar_target})
|
|
86
|
+
assert_type(scalar_target.value.dtype, np.dtype[np.float64])
|
|
81
87
|
assert_type(widened.value, Array)
|
|
82
88
|
assert_type(q.value, npt.NDArray[np.float64])
|
|
83
89
|
assert_type(q * Unit('s'), Quantity[npt.NDArray[np.float64]])
|
|
@@ -427,13 +427,14 @@ def test_unsupported_outputs_do_not_mutate():
|
|
|
427
427
|
target = Quantity(buffer, 'm')
|
|
428
428
|
with pytest.raises(TypeError):
|
|
429
429
|
np.add(q, q, out=buffer)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
430
|
+
with pytest.raises(TypeError):
|
|
431
|
+
np.sum(q, out=buffer)
|
|
432
|
+
with pytest.raises(ValueError, match='result shape'):
|
|
433
|
+
np.sum(q, None, None, target)
|
|
434
|
+
with pytest.raises(TypeError):
|
|
435
|
+
np.concatenate((q, q), out=buffer)
|
|
436
|
+
with pytest.raises(ValueError, match='result shape'):
|
|
437
|
+
np.concatenate((q, q), out=target)
|
|
437
438
|
np.testing.assert_array_equal(buffer, [-123] * 3)
|
|
438
439
|
result = np.add(q, q, where=np.array([True, False, True]))
|
|
439
440
|
np.testing.assert_array_equal(result.value[[0, 2]], [0, 4])
|
|
@@ -642,7 +643,7 @@ def test_ufunc_out_protocol():
|
|
|
642
643
|
|
|
643
644
|
assert np.add(q, q, out=Foreign()) == 'foreign dispatched'
|
|
644
645
|
target = Quantity(np.full(3, -123.0), 'm')
|
|
645
|
-
for operation in (np.
|
|
646
|
+
for operation in (np.multiply.reduce, np.multiply.accumulate, np.modf):
|
|
646
647
|
with pytest.raises(TypeError):
|
|
647
648
|
operation(q, out=(target, None)) if operation is np.modf else operation(q, out=target)
|
|
648
649
|
np.testing.assert_array_equal(target.value, [-123] * 3)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Unit-aware function and reduction outputs, including shared storage."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
np = pytest.importorskip('numpy')
|
|
6
|
+
|
|
7
|
+
from unitx import Quantity, Unit # noqa: E402
|
|
8
|
+
from unitx.exceptions import DimensionError # noqa: E402
|
|
9
|
+
from unitx.factor import Factor # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
'operation',
|
|
14
|
+
[np.sum, np.mean, np.median, np.std, np.var, np.cumsum, np.add.reduce, np.add.accumulate],
|
|
15
|
+
)
|
|
16
|
+
def test_numpy_function_out_conversion(operation):
|
|
17
|
+
unit = Factor(2) * Unit('m')
|
|
18
|
+
q = Quantity(np.arange(6.0).reshape(2, 3), unit)
|
|
19
|
+
natural_unit = unit**2 if operation is np.var else unit
|
|
20
|
+
target_unit = Unit('cm2' if operation is np.var else 'cm')
|
|
21
|
+
expected = Quantity(operation(q.value, axis=0), natural_unit).value_in(target_unit)
|
|
22
|
+
target = Quantity(np.full(np.shape(expected), -123.0), target_unit)
|
|
23
|
+
buffer = target.value
|
|
24
|
+
assert operation(q, axis=0, out=target) is target
|
|
25
|
+
assert target.value is buffer and target.unit is target_unit
|
|
26
|
+
np.testing.assert_allclose(buffer, expected)
|
|
27
|
+
np.testing.assert_array_equal(q.value, np.arange(6.0).reshape(2, 3))
|
|
28
|
+
ordinary = operation(q, axis=0, out=None)
|
|
29
|
+
assert ordinary.unit == natural_unit
|
|
30
|
+
np.testing.assert_allclose(ordinary.value, operation(q.value, axis=0))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize('operation', [np.sum, np.mean, np.median, np.std, np.var, np.add.reduce])
|
|
34
|
+
def test_numpy_reduction_out_axes(operation):
|
|
35
|
+
q = Quantity(np.arange(24.0).reshape(2, 3, 4), 'm')
|
|
36
|
+
unit = 'm2' if operation is np.var else 'm'
|
|
37
|
+
expected = operation(q.value, axis=(0, -1), keepdims=True)
|
|
38
|
+
target = Quantity(np.empty((1, 3, 1)), unit)
|
|
39
|
+
assert operation(q, axis=(0, -1), keepdims=True, out=target) is target
|
|
40
|
+
np.testing.assert_allclose(target.value, expected)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_numpy_function_out_positional():
|
|
44
|
+
q = Quantity(np.array([1.0, 2.0, 3.0]), 'm')
|
|
45
|
+
target = Quantity(np.array(-123.0), 'cm')
|
|
46
|
+
for operation in (np.sum, np.mean, np.std, np.var):
|
|
47
|
+
actual = Quantity(target.value, 'cm2') if operation is np.var else target
|
|
48
|
+
assert operation(q, None, None, actual) is actual
|
|
49
|
+
assert np.median(q, None, target) is target
|
|
50
|
+
vector = Quantity(np.empty(3), 'cm')
|
|
51
|
+
assert np.cumsum(q, None, None, vector) is vector
|
|
52
|
+
assert np.add.reduce(q, None, None, target) is target
|
|
53
|
+
assert np.add.reduce(q, out=(target,)) is target
|
|
54
|
+
assert np.add.accumulate(q, out=(vector,)) is vector
|
|
55
|
+
assert np.add.reduce(q, out=(None,)).value == 6
|
|
56
|
+
index = np.array(-1, dtype=np.intp)
|
|
57
|
+
assert np.argmax(q, None, index) is index
|
|
58
|
+
assert index == 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize('operation', [np.concatenate, np.stack])
|
|
62
|
+
def test_numpy_join_out_conversion(operation):
|
|
63
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
64
|
+
other = Quantity(np.arange(3.0) * 100, 'cm')
|
|
65
|
+
expected = operation((q.value, q.value)) * 1000
|
|
66
|
+
storage = np.full((*expected.shape, 2), -123.0)
|
|
67
|
+
target = Quantity(storage[..., 0], 'mm')
|
|
68
|
+
assert operation((q, other), out=target) is target
|
|
69
|
+
np.testing.assert_array_equal(target.value, expected)
|
|
70
|
+
np.testing.assert_array_equal(storage[..., 1], -123)
|
|
71
|
+
assert operation((q, other), 0, target) is target
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_numpy_join_out_alias():
|
|
75
|
+
storage = np.arange(6.0)
|
|
76
|
+
q = Quantity(storage, 'm')
|
|
77
|
+
assert np.concatenate((q[3:], q[:3]), out=q) is q
|
|
78
|
+
np.testing.assert_array_equal(storage, [3, 4, 5, 0, 1, 2])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize('operation', [np.argmin, np.argmax])
|
|
82
|
+
def test_numpy_index_out(operation):
|
|
83
|
+
q = Quantity(np.array([[3, 1, 4], [2, 7, 0]]), 'm')
|
|
84
|
+
storage = np.full((3, 2), -123, dtype=np.intp)
|
|
85
|
+
target = storage[:, 0]
|
|
86
|
+
assert operation(q, axis=0, out=target) is target
|
|
87
|
+
np.testing.assert_array_equal(target, operation(q.value, axis=0))
|
|
88
|
+
np.testing.assert_array_equal(storage[:, 1], -123)
|
|
89
|
+
kept = np.empty((1, 3), dtype=np.intp)
|
|
90
|
+
assert operation(q, axis=0, out=kept, keepdims=True) is kept
|
|
91
|
+
np.testing.assert_array_equal(kept, operation(q.value, axis=0, keepdims=True))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_numpy_reduction_out_storage():
|
|
95
|
+
storage = np.arange(6.0).reshape(2, 3)
|
|
96
|
+
q = Quantity(storage, 'm')
|
|
97
|
+
target = Quantity(storage[0], 'cm')
|
|
98
|
+
assert np.sum(q, axis=0, out=target) is target
|
|
99
|
+
np.testing.assert_array_equal(storage, [[300, 500, 700], [3, 4, 5]])
|
|
100
|
+
assert np.cumsum(q, axis=1, out=q) is q
|
|
101
|
+
np.testing.assert_array_equal(storage, [[300, 800, 1500], [3, 7, 12]])
|
|
102
|
+
assert np.add.accumulate(q, axis=0, out=q) is q
|
|
103
|
+
np.testing.assert_array_equal(storage, [[300, 800, 1500], [303, 807, 1512]])
|
|
104
|
+
q.value.flags.writeable = False
|
|
105
|
+
separate = Quantity(np.empty(3), 'm')
|
|
106
|
+
assert np.sum(q, axis=0, out=separate) is separate
|
|
107
|
+
np.testing.assert_array_equal(separate.value, [603, 1607, 3012])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_numpy_function_out_empty():
|
|
111
|
+
q = Quantity(np.empty((0, 3)), 'm')
|
|
112
|
+
target = Quantity(np.full(3, -123.0), 'cm')
|
|
113
|
+
assert np.sum(q, axis=0, out=target) is target
|
|
114
|
+
np.testing.assert_array_equal(target.value, 0)
|
|
115
|
+
empty = Quantity(np.empty((0, 3)), 'cm')
|
|
116
|
+
assert np.cumsum(q, axis=0, out=empty) is empty
|
|
117
|
+
assert np.add.accumulate(q, axis=0, out=empty) is empty
|
|
118
|
+
scalar = Quantity(np.array(2.0), 'km/m')
|
|
119
|
+
coherent = Quantity(np.array(-123.0), '')
|
|
120
|
+
assert np.sum(scalar, out=coherent) is coherent
|
|
121
|
+
assert coherent.value == 2000
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize('operation', [np.sum, np.add.reduce, np.cumsum, np.add.accumulate])
|
|
125
|
+
def test_numpy_function_out_invalid_dimension(operation):
|
|
126
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
127
|
+
target = Quantity(np.full(np.shape(operation(q.value)), -123.0), 's')
|
|
128
|
+
with pytest.raises(DimensionError, match='out has an incompatible dimension'):
|
|
129
|
+
operation(q, out=target)
|
|
130
|
+
np.testing.assert_array_equal(target.value, -123)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize('shape', [(1,), (2, 3), (0,)])
|
|
134
|
+
def test_numpy_reduction_out_invalid_shape(shape):
|
|
135
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
136
|
+
for operation in (np.sum, np.add.reduce, np.argmax):
|
|
137
|
+
buffer = np.full(shape, -123, dtype=np.intp)
|
|
138
|
+
out = buffer if operation is np.argmax else Quantity(buffer, 'm')
|
|
139
|
+
with pytest.raises(ValueError, match='result shape'):
|
|
140
|
+
operation(q, out=out)
|
|
141
|
+
np.testing.assert_array_equal(buffer, -123)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_numpy_function_out_invalid_buffer():
|
|
145
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
146
|
+
for target in (np.array(-123.0), Quantity(1, 'm'), Quantity(np.float64(1), 'm')):
|
|
147
|
+
with pytest.raises(TypeError, match='plain numeric ndarray'):
|
|
148
|
+
np.sum(q, out=target)
|
|
149
|
+
target = Quantity(np.array(-123.0), 'm')
|
|
150
|
+
target.value.flags.writeable = False
|
|
151
|
+
with pytest.raises(ValueError, match='read-only'):
|
|
152
|
+
np.sum(q, out=target)
|
|
153
|
+
assert target.value == -123
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@pytest.mark.parametrize(
|
|
157
|
+
'target', [np.array(-123.0), np.array(False), Quantity(np.array(-123), '')]
|
|
158
|
+
)
|
|
159
|
+
def test_numpy_index_out_invalid_buffer(target):
|
|
160
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
161
|
+
with pytest.raises(TypeError, match='Unitless out'):
|
|
162
|
+
np.argmax(q, out=target)
|
|
163
|
+
assert (target.value if isinstance(target, Quantity) else target) == (
|
|
164
|
+
False if isinstance(target, np.ndarray) and target.dtype.kind == 'b' else -123
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_numpy_function_out_invalid_cast():
|
|
169
|
+
q = Quantity(np.array([1.0, 2.5]), 'cm')
|
|
170
|
+
target = Quantity(np.array(-123), 'm')
|
|
171
|
+
for operation in (np.sum, np.mean, np.median, np.add.reduce):
|
|
172
|
+
with pytest.raises(TypeError):
|
|
173
|
+
operation(q, out=target)
|
|
174
|
+
assert target.value == -123
|
|
175
|
+
narrow = Quantity(np.array(-123, dtype=np.float32), 'm')
|
|
176
|
+
assert np.sum(q, dtype=np.float64, out=narrow) is narrow
|
|
177
|
+
assert narrow.value.dtype == np.dtype(np.float32)
|
|
178
|
+
np.testing.assert_allclose(narrow.value, 0.035)
|
|
179
|
+
with np.errstate(over='raise'):
|
|
180
|
+
with pytest.raises(FloatingPointError):
|
|
181
|
+
np.sum(Quantity(np.array([1e100]), 'm'), out=narrow)
|
|
182
|
+
np.testing.assert_allclose(narrow.value, 0.035)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_numpy_join_out_invalid_options():
|
|
186
|
+
q = Quantity(np.array([1.0]), 'cm')
|
|
187
|
+
target = Quantity(np.full(2, -123), 'm')
|
|
188
|
+
with pytest.raises(TypeError):
|
|
189
|
+
np.concatenate((q, q), out=target)
|
|
190
|
+
np.testing.assert_array_equal(target.value, -123)
|
|
191
|
+
assert np.concatenate((q, q), out=target, casting='unsafe') is target
|
|
192
|
+
np.testing.assert_array_equal(target.value, 0)
|
|
193
|
+
for operation in (np.concatenate, np.stack):
|
|
194
|
+
with pytest.raises(TypeError, match='both dtype and out'):
|
|
195
|
+
operation((q, q), out=target, dtype=float)
|
|
196
|
+
with pytest.raises(DimensionError):
|
|
197
|
+
np.concatenate((q, Quantity(np.array([1.0]), 's')), out=target)
|
|
198
|
+
np.testing.assert_array_equal(target.value, 0)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_numpy_function_out_invalid_kernel():
|
|
202
|
+
q = Quantity(np.array([np.inf, np.inf]), 'm')
|
|
203
|
+
target = Quantity(np.array(-123.0), 'm2')
|
|
204
|
+
with np.errstate(invalid='raise'):
|
|
205
|
+
with pytest.raises(FloatingPointError):
|
|
206
|
+
np.var(q, out=target)
|
|
207
|
+
assert target.value == -123
|
|
208
|
+
with pytest.raises(TypeError):
|
|
209
|
+
np.median(q, out=target, overwrite_input=True)
|
|
210
|
+
assert target.value == -123
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_numpy_reduction_out_protocol():
|
|
214
|
+
q = Quantity(np.arange(3.0), 'm')
|
|
215
|
+
|
|
216
|
+
class Foreign:
|
|
217
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
218
|
+
return 'foreign dispatched'
|
|
219
|
+
|
|
220
|
+
def __array_function__(self, func, types, args, kwargs):
|
|
221
|
+
return 'foreign dispatched'
|
|
222
|
+
|
|
223
|
+
assert np.add.reduce(q, out=Foreign()) == 'foreign dispatched'
|
|
224
|
+
assert np.sum(q, out=Foreign()) == 'foreign dispatched'
|
|
225
|
+
target = Quantity(np.array(-123.0), 'm')
|
|
226
|
+
for options in ({'where': True}, {'initial': 1}, {'casting': 'unsafe'}):
|
|
227
|
+
with pytest.raises(TypeError):
|
|
228
|
+
np.add.reduce(q, out=target, **options)
|
|
229
|
+
assert target.value == -123
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|