unitx 0.16.2__tar.gz → 0.16.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.
- {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/arrays.py +52 -1
- {unitx-0.16.2 → unitx-0.16.4}/CHANGELOG.md +20 -0
- {unitx-0.16.2 → unitx-0.16.4}/PKG-INFO +1 -1
- {unitx-0.16.2 → unitx-0.16.4}/docs/numpy.md +78 -2
- {unitx-0.16.2 → unitx-0.16.4}/docs/typing.md +7 -1
- {unitx-0.16.2 → unitx-0.16.4}/pyproject.toml +1 -1
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/numpy.py +78 -4
- unitx-0.16.4/tests/test_numpy_array_manipulation.py +306 -0
- unitx-0.16.4/tests/test_numpy_shapes.py +217 -0
- {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/pyrightconfig.json +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/scalars.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/ci.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/pages.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/release.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.gitignore +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/.readthedocs.yaml +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/CONTRIBUTING.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/LICENSE +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/benchmarks/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/benchmarks/numpy_writes.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/benchmarks/scalars.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/conftest.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/affine.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/architecture.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/cgs.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/imperial.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/si.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/custom-units.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/display.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/images/blackbody.svg +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/javascripts/mermaid.mjs +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/logarithmic.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/migration.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/calculus.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/selection.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/statistics.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/writes.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/requirements.txt +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/scalars.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/serialization.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/stdlib.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/docs/units.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/arrhenius_fit.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/blackbody.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/damped_oscillator.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/gaussian_wavepacket.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/impedance.md +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/impedance.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/optical_attenuation.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/pendulum_uncertainty.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/examples/smith_chart.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/mkdocs.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/pyrightconfig.json +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_constnamespace.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/definition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/logarithms.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/points.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/resolve.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/coefficients.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/conversion.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/render.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/unicode.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_immutable.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/conversion.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/numpy_typing.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/scaling.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_operands.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/builtin_units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/prefixes.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/reconstruction.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/affinequantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/affineunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/cgs/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/cgs/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/dimension.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/exceptions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/factor.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/imperial/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/imperial/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/logquantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/logunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/monomial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/py.typed +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/bases.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/constants.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/singleunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitcatalog.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitdefinition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitparser.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitregistry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/_notebooks.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_asarray.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_affinequantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_builtin_definitions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_catalog_collection.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs_systems.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_decibel_call.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_definition_scope.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_dimension.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_display.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_examples.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_factor.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_immutable.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_impedance_example.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_imperial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_import.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_linear_boundaries.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_logarithmic_model.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_math_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_monomial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numeric.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_differentiation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_integration.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_interpolation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_multi_outputs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_mutation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_nan_reductions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_nan_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_outputs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_products.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_selection.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_weighted_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_registry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_registry_scope.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_si.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_singleunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_siunitx_display.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_typing.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unicode.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unicode_units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unit_definition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unitparser.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_unitregistry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/test_warnings.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_impedance_example.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_imperial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_math_time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_registry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_si.py +0 -0
|
@@ -5,12 +5,14 @@ from typing import assert_type, cast
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from numpy.typing import NDArray
|
|
7
7
|
|
|
8
|
-
from unitx import LogQuantity, LogUnit, Quantity, Unit
|
|
8
|
+
from unitx import AffineQuantity, LogQuantity, LogUnit, Quantity, Unit
|
|
9
9
|
from unitx._numerics import Array
|
|
10
10
|
from unitx.cgs import emu, esu, gaussian, hl
|
|
11
11
|
from unitx.exceptions import DimensionError
|
|
12
12
|
from unitx.si import si
|
|
13
13
|
|
|
14
|
+
type ArrayQuantity = Quantity[Array] | AffineQuantity[Array] | LogQuantity[Array]
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def check_arrays(values: NDArray[np.float64]) -> None:
|
|
16
18
|
charge = gaussian.convert(Quantity(values, si.C), gaussian.statC)
|
|
@@ -47,6 +49,55 @@ def check_arrays(values: NDArray[np.float64]) -> None:
|
|
|
47
49
|
|
|
48
50
|
# NumPy's stubs lose the wrapper on these returns. State the supported
|
|
49
51
|
# array result domain explicitly and verify the wrapper at runtime.
|
|
52
|
+
for sample in (distance, AffineQuantity(values, si.degC), level):
|
|
53
|
+
expanded = cast(ArrayQuantity, np.expand_dims(sample, (0, -1)))
|
|
54
|
+
# moveaxis/rollaxis stubs require ndarray input despite runtime dispatch.
|
|
55
|
+
moved = cast(
|
|
56
|
+
ArrayQuantity,
|
|
57
|
+
expanded.__array_function__(np.moveaxis, (type(expanded),), (expanded, 0, -1), {}),
|
|
58
|
+
)
|
|
59
|
+
swapped = cast(ArrayQuantity, np.swapaxes(moved, 0, 1))
|
|
60
|
+
rolled = cast(
|
|
61
|
+
ArrayQuantity,
|
|
62
|
+
swapped.__array_function__(np.rollaxis, (type(swapped),), (swapped, 2, 0), {}),
|
|
63
|
+
)
|
|
64
|
+
squeezed = np.squeeze(rolled)
|
|
65
|
+
result = cast(ArrayQuantity, np.ravel(squeezed))
|
|
66
|
+
assert type(result) is type(sample) and result.unit == sample.unit
|
|
67
|
+
np.testing.assert_array_equal(result.value, values)
|
|
68
|
+
assert np.shares_memory(result.value, sample.value)
|
|
69
|
+
|
|
70
|
+
for actual, expected in (
|
|
71
|
+
(np.atleast_1d(sample), np.atleast_1d(values)),
|
|
72
|
+
(np.atleast_2d(sample), np.atleast_2d(values)),
|
|
73
|
+
(np.atleast_3d(sample), np.atleast_3d(values)),
|
|
74
|
+
(np.broadcast_to(sample, (2, 3)), np.broadcast_to(values, (2, 3))),
|
|
75
|
+
(np.flip(sample), np.flip(values)),
|
|
76
|
+
(np.flipud(sample), np.flipud(values)),
|
|
77
|
+
(np.roll(sample, 1), np.roll(values, 1)),
|
|
78
|
+
(np.repeat(sample, 2), np.repeat(values, 2)),
|
|
79
|
+
(np.tile(sample, (2, 1)), np.tile(values, (2, 1))),
|
|
80
|
+
(np.copy(sample), np.copy(values)),
|
|
81
|
+
):
|
|
82
|
+
manipulated = cast(ArrayQuantity, actual)
|
|
83
|
+
assert type(manipulated) is type(sample) and manipulated.unit == sample.unit
|
|
84
|
+
np.testing.assert_array_equal(manipulated.value, expected)
|
|
85
|
+
assert_type(np.shape(sample), tuple[int, ...])
|
|
86
|
+
assert_type(np.ndim(sample), int)
|
|
87
|
+
assert_type(np.size(sample), int)
|
|
88
|
+
assert np.shape(sample) == (3,) and np.ndim(sample) == 1 and np.size(sample) == 3
|
|
89
|
+
for operation in (np.matrix_transpose, np.linalg.matrix_transpose, np.fliplr, np.rot90):
|
|
90
|
+
manipulated = cast(ArrayQuantity, operation(np.atleast_2d(sample)))
|
|
91
|
+
assert type(manipulated) is type(sample) and manipulated.unit == sample.unit
|
|
92
|
+
np.testing.assert_array_equal(manipulated.value, operation(np.atleast_2d(values)))
|
|
93
|
+
broadcasted, bare = cast(
|
|
94
|
+
tuple[ArrayQuantity, NDArray[np.float64]],
|
|
95
|
+
np.broadcast_arrays(sample, np.ones((2, 1))),
|
|
96
|
+
)
|
|
97
|
+
assert type(broadcasted) is type(sample) and broadcasted.unit == sample.unit
|
|
98
|
+
np.testing.assert_array_equal(broadcasted.value, np.broadcast_to(values, (2, 3)))
|
|
99
|
+
np.testing.assert_array_equal(bare, np.ones((2, 3)))
|
|
100
|
+
|
|
50
101
|
bounded = cast(Quantity[Array], np.clip(distance, Quantity(500, si.cm), Quantity(25, si.m)))
|
|
51
102
|
assert isinstance(bounded, Quantity) and bounded.unit == si.m
|
|
52
103
|
np.testing.assert_allclose(bounded.value, [5, 20, 25])
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.16.4] - 2026-10-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Support NumPy `atleast_1d/2d/3d`, `broadcast_to`, `broadcast_arrays`,
|
|
10
|
+
`matrix_transpose` (including `linalg.matrix_transpose`), `flip/fliplr/flipud`,
|
|
11
|
+
`rot90`, `roll`, `repeat`, `tile` and `copy`. Preserve complete units or point
|
|
12
|
+
coordinates, NumPy's views/copies, writeability and warnings. Multiple inputs
|
|
13
|
+
retain their own units or coordinates independently; bare inputs stay bare.
|
|
14
|
+
- Support NumPy `shape`, `ndim` and `size` as bare array metadata, including
|
|
15
|
+
scalar quantities. Document and verify the existing `permute_dims` alias.
|
|
16
|
+
|
|
17
|
+
## [0.16.3] - 2026-10-25
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Support NumPy `squeeze`, `expand_dims`, `moveaxis`, `swapaxes`, `rollaxis`
|
|
22
|
+
and `ravel`, preserving complete units or point coordinates. Retain NumPy's
|
|
23
|
+
axis rules, dtype, shared views, read-only storage and necessary copies.
|
|
24
|
+
|
|
5
25
|
## [0.16.2] - 2026-10-25
|
|
6
26
|
|
|
7
27
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.4
|
|
4
4
|
Summary: Physical units, quantities, and unit conversion with exact unit algebra, type hints, and NumPy integration.
|
|
5
5
|
Project-URL: Homepage, https://github.com/DaitTHU/unitx
|
|
6
6
|
Project-URL: Documentation, https://daitthu.github.io/unitx/
|
|
@@ -38,8 +38,8 @@ use `np.asarray(q.value_in(target_unit))`.
|
|
|
38
38
|
|
|
39
39
|
Construction, unit-only multiplication/division, and an identity conversion
|
|
40
40
|
preserve the magnitude object. Factor/SymbolicConstant operands change the
|
|
41
|
-
exact unit scale. Indexing, iteration, `.T`,
|
|
42
|
-
NumPy's scalar/view/copy rules; `.flatten(order='C')` copies. Zero-dimensional
|
|
41
|
+
exact unit scale. Indexing, iteration, `.T`, and the supported shape transformations
|
|
42
|
+
follow NumPy's scalar/view/copy rules; `.flatten(order='C')` copies. Zero-dimensional
|
|
43
43
|
arrays cannot be iterated. `.shape`, `.ndim`, `.dtype`, and `.size` are available
|
|
44
44
|
for NumPy magnitudes. Python scalars do not acquire array attributes. Exposed
|
|
45
45
|
values and views may share writable storage.
|
|
@@ -115,6 +115,15 @@ functions, including exp/log. Python scalar numerical semantics remain native.
|
|
|
115
115
|
| NumPy function | Unit/result rule | Supported options |
|
|
116
116
|
| --- | --- | --- |
|
|
117
117
|
| reshape, transpose | Preserve unit | shape/order/copy; axes |
|
|
118
|
+
| squeeze, expand_dims | Preserve unit or point coordinate | axis |
|
|
119
|
+
| moveaxis, swapaxes, rollaxis | Preserve unit or point coordinate | source/destination; axis1/axis2; axis/start |
|
|
120
|
+
| ravel | Preserve unit or point coordinate | order: C, F, A, K |
|
|
121
|
+
| matrix_transpose, linalg.matrix_transpose | Swap the last two axes; preserve unit or point coordinate | At least two dimensions; transpose's permute_dims alias is also supported |
|
|
122
|
+
| atleast_1d, atleast_2d, atleast_3d | Preserve each input's unit or point coordinate independently | One or more positional numeric scalars/ndarrays, quantities or points |
|
|
123
|
+
| broadcast_to, broadcast_arrays | Preserve each input's unit or point coordinate independently | shape, subok; broadcasting does not align units |
|
|
124
|
+
| flip, fliplr, flipud, rot90, roll | Preserve unit or point coordinate | axis; k/axes for rot90; shift/axis for roll |
|
|
125
|
+
| repeat, tile, copy | Preserve unit or point coordinate | repeats/axis; reps; order/subok for copy |
|
|
126
|
+
| shape, ndim, size | Bare shape tuple or integer | size accepts axis |
|
|
118
127
|
| concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting, out; dtype and out are mutually exclusive |
|
|
119
128
|
| sum, nansum | Preserve unit | axis, dtype, keepdims, initial, where, out |
|
|
120
129
|
| mean, nanmean | Preserve unit or point coordinate | axis, dtype, keepdims, where, out |
|
|
@@ -146,6 +155,73 @@ Indexed assignment and augmented arithmetic on ndarrays follow the fixed-target
|
|
|
146
155
|
rules below. Python and NumPy scalar magnitudes retain ordinary result/rebinding
|
|
147
156
|
behavior for augmented arithmetic.
|
|
148
157
|
|
|
158
|
+
## Shape transformations
|
|
159
|
+
|
|
160
|
+
`squeeze`, `expand_dims`, `moveaxis`, `swapaxes`, `rollaxis`, and `ravel`
|
|
161
|
+
operate on magnitudes without conversion. Like reshape and transpose, they
|
|
162
|
+
retain the complete unit or point coordinate and return the same family:
|
|
163
|
+
Quantity, AffineQuantity, or LogQuantity. Only the NumPy function forms are
|
|
164
|
+
supported; these functions have no `out` option.
|
|
165
|
+
|
|
166
|
+
Axis rules and errors follow NumPy, including negative and tuple axes where
|
|
167
|
+
supported. Squeezing an ndarray whose axes all have length one produces a
|
|
168
|
+
zero-dimensional ndarray magnitude. Views share storage and its writeability
|
|
169
|
+
with the input.
|
|
170
|
+
`ravel` returns a contiguous one-dimensional magnitude, copying only when needed;
|
|
171
|
+
it can copy a noncontiguous input even when reshape could return a view.
|
|
172
|
+
Its C/F/A/K orders follow NumPy. Prefer `moveaxis` for new code; `rollaxis` is
|
|
173
|
+
also supported for compatibility with existing NumPy code.
|
|
174
|
+
|
|
175
|
+
`matrix_transpose` and `linalg.matrix_transpose` swap the last two axes of an
|
|
176
|
+
array with at least two dimensions. `permute_dims` is NumPy's alias of
|
|
177
|
+
`transpose` and supports the same axes argument.
|
|
178
|
+
|
|
179
|
+
`atleast_1d/2d/3d` and `broadcast_arrays` accept multiple positional inputs.
|
|
180
|
+
Each Quantity or point retains its own complete unit or coordinate; bare
|
|
181
|
+
numeric scalars/ndarrays produce bare ndarrays. Inputs may have different
|
|
182
|
+
dimensions or coordinate families because these functions only rearrange
|
|
183
|
+
storage. Bare list/tuple inputs and unsupported array types remain unsupported.
|
|
184
|
+
One input to `atleast_*` returns one result; multiple inputs return NumPy's
|
|
185
|
+
sequence container. `broadcast_arrays` always returns a sequence, even for one
|
|
186
|
+
input. Shape incompatibilities follow NumPy.
|
|
187
|
+
|
|
188
|
+
`broadcast_to` returns a read-only view. `broadcast_arrays` retains NumPy's
|
|
189
|
+
view flags and warnings; broadcast elements may alias the same storage, so
|
|
190
|
+
use `copy` before writing. `flip`, `fliplr`, `flipud`, and `rot90` retain
|
|
191
|
+
NumPy's views, including negative strides. `roll`, `repeat`, and `tile` follow
|
|
192
|
+
NumPy's allocation rules. `copy(order='K', subok=False)` creates independent,
|
|
193
|
+
writable storage, including when the source is read-only. `subok` on copy and
|
|
194
|
+
broadcasting is forwarded to NumPy for the plain supported magnitudes; it does
|
|
195
|
+
not enable ndarray subclasses.
|
|
196
|
+
|
|
197
|
+
`shape`, `ndim`, and `size` inspect the magnitude and return bare metadata,
|
|
198
|
+
including for scalar quantities. All these function forms preserve the wrapper
|
|
199
|
+
family when returning unit-bearing values and have no `out` option.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import numpy as np
|
|
203
|
+
from unitx import Quantity
|
|
204
|
+
|
|
205
|
+
samples = Quantity(np.arange(6).reshape(1, 2, 3), 'cm')
|
|
206
|
+
matrix = np.squeeze(samples, axis=0)
|
|
207
|
+
assert matrix.shape == (2, 3)
|
|
208
|
+
assert np.moveaxis(matrix, 0, -1).shape == (3, 2)
|
|
209
|
+
assert matrix.unit == samples.unit
|
|
210
|
+
assert np.shares_memory(matrix.value, samples.value)
|
|
211
|
+
|
|
212
|
+
times = Quantity(np.array([0.0, 1.0, 2.0]), 's')
|
|
213
|
+
distance_grid, time_grid = np.broadcast_arrays(matrix, times)
|
|
214
|
+
assert distance_grid.unit == matrix.unit and time_grid.unit == times.unit
|
|
215
|
+
assert time_grid.shape == (2, 3)
|
|
216
|
+
readonly = np.broadcast_to(times, (2, 3))
|
|
217
|
+
assert not readonly.value.flags.writeable
|
|
218
|
+
independent = np.copy(readonly)
|
|
219
|
+
assert independent.value.flags.writeable
|
|
220
|
+
assert not np.shares_memory(independent.value, times.value)
|
|
221
|
+
assert np.shape(np.flip(independent, axis=-1)) == (2, 3)
|
|
222
|
+
assert np.ndim(independent) == 2 and np.size(independent, axis=0) == 2
|
|
223
|
+
```
|
|
224
|
+
|
|
149
225
|
## Selection and clipping
|
|
150
226
|
|
|
151
227
|
See [selection and clipping](numpy/selection.md) for `where`, `clip`,
|
|
@@ -94,11 +94,17 @@ ufunc stubs preserve the wrapper at every intermediate call. For an explicit
|
|
|
94
94
|
numerical boundary, use `np.expm1(np.asarray(q.value_in(si.one)))` after
|
|
95
95
|
checking dimensions and normalizing the scale.
|
|
96
96
|
|
|
97
|
-
The same NumPy stub limitation applies to
|
|
97
|
+
The same NumPy stub limitation applies to the supported shape transformations,
|
|
98
|
+
broadcasting, rearrangement, repetition and copying,
|
|
99
|
+
`where`, `clip`,
|
|
98
100
|
`minimum/maximum`, `average`, `quantile`, `percentile`, the supported `nan*`
|
|
99
101
|
statistics, and `interp`: runtime results
|
|
100
102
|
preserve the documented Quantity/point wrappers, but NumPy may annotate them
|
|
101
103
|
as ndarrays or Any. NumPy's `out` annotations also accept only ndarrays.
|
|
104
|
+
The `moveaxis` and `rollaxis` input annotations likewise require ndarrays
|
|
105
|
+
and can reject a Quantity/point argument during static checking, despite
|
|
106
|
+
working at runtime. Their installed type consumers check the dispatch protocol
|
|
107
|
+
directly; runtime tests use ordinary NumPy calls.
|
|
102
108
|
Runtime support and installed-consumer checks cover
|
|
103
109
|
these calls; unitx does not replace NumPy's stubs.
|
|
104
110
|
|
|
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'unitx'
|
|
7
|
-
version = '0.16.
|
|
7
|
+
version = '0.16.4'
|
|
8
8
|
description = 'Physical units, quantities, and unit conversion with exact unit algebra, type hints, and NumPy integration.'
|
|
9
9
|
readme = 'README.md'
|
|
10
10
|
requires-python = '>=3.12'
|
|
@@ -598,12 +598,12 @@ def dispatch_function(
|
|
|
598
598
|
return _clip(*args, **kwargs)
|
|
599
599
|
if func is np.interp:
|
|
600
600
|
return _interp(*args, **kwargs)
|
|
601
|
+
if func in (np.atleast_1d, np.atleast_2d, np.atleast_3d, np.broadcast_arrays):
|
|
602
|
+
return _multiple_arrays(func, *args, **kwargs)
|
|
601
603
|
if args and isinstance(args[0], (Quantity, _Point)) and isinstance(args[0].unit, _Coordinate):
|
|
602
|
-
if func not in (
|
|
604
|
+
if func not in _ARRAY_FUNCTIONS and func not in (
|
|
603
605
|
np.isclose,
|
|
604
606
|
np.allclose,
|
|
605
|
-
np.reshape,
|
|
606
|
-
np.transpose,
|
|
607
607
|
np.argmin,
|
|
608
608
|
np.argmax,
|
|
609
609
|
np.nanargmin,
|
|
@@ -754,6 +754,56 @@ def _transpose(q: Any, axes: Any = None) -> Any:
|
|
|
754
754
|
return _wrap(np.transpose(q.value, axes), q.unit)
|
|
755
755
|
|
|
756
756
|
|
|
757
|
+
def _squeeze(q: Any, axis: Any = None) -> Any:
|
|
758
|
+
return _wrap(np.squeeze(q.value, axis=axis), q.unit)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
def _expand_dims(q: Any, axis: Any) -> Any:
|
|
762
|
+
return _wrap(np.expand_dims(q.value, axis), q.unit)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _moveaxis(q: Any, source: Any, destination: Any) -> Any:
|
|
766
|
+
return _wrap(np.moveaxis(q.value, source, destination), q.unit)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def _swapaxes(q: Any, axis1: Any, axis2: Any) -> Any:
|
|
770
|
+
return _wrap(np.swapaxes(q.value, axis1, axis2), q.unit)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _rollaxis(q: Any, axis: Any, start: Any = 0) -> Any:
|
|
774
|
+
return _wrap(np.rollaxis(q.value, axis, start), q.unit)
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _ravel(q: Any, order: Literal['C', 'F', 'A', 'K'] = 'C') -> Any:
|
|
778
|
+
return _wrap(np.ravel(q.value, order=order), q.unit)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def _array_transform(func: Any, q: Any, *args: Any, **kwargs: Any) -> Any:
|
|
782
|
+
"""Transform storage without changing values, units or coordinates."""
|
|
783
|
+
return _wrap(func(q.value, *args, **kwargs), q.unit)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _array_metadata(func: Any, q: Any, *args: Any, **kwargs: Any) -> Any:
|
|
787
|
+
return func(q.value, *args, **kwargs)
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _multiple_arrays(func: Any, *arrays: Any, **kwargs: Any) -> Any:
|
|
791
|
+
"""Rearrange each operand independently, retaining bare and wrapped inputs."""
|
|
792
|
+
if not all(_is_supported_operand(x) for x in arrays):
|
|
793
|
+
return NotImplemented
|
|
794
|
+
values = tuple(
|
|
795
|
+
x.value if isinstance(x, (Quantity, _Point)) else _numeric_value(x) for x in arrays
|
|
796
|
+
)
|
|
797
|
+
result = func(*values, **kwargs)
|
|
798
|
+
if func is not np.broadcast_arrays and len(arrays) == 1:
|
|
799
|
+
source = arrays[0]
|
|
800
|
+
return _wrap(result, source.unit) if isinstance(source, (Quantity, _Point)) else result
|
|
801
|
+
return type(result)(
|
|
802
|
+
_wrap(raw, source.unit) if isinstance(source, (Quantity, _Point)) else raw
|
|
803
|
+
for raw, source in zip(result, arrays)
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
757
807
|
def _interp(
|
|
758
808
|
x: Any, xp: Any, fp: Any, left: Any = None, right: Any = None, period: Any = None
|
|
759
809
|
) -> Any:
|
|
@@ -1213,9 +1263,33 @@ def _extremum(
|
|
|
1213
1263
|
return _write_output(_wrap(func(q.value, **kwargs), q.unit), out)
|
|
1214
1264
|
|
|
1215
1265
|
|
|
1216
|
-
|
|
1266
|
+
_ARRAY_FUNCTIONS = {
|
|
1217
1267
|
np.reshape: _reshape,
|
|
1218
1268
|
np.transpose: _transpose,
|
|
1269
|
+
np.squeeze: _squeeze,
|
|
1270
|
+
np.expand_dims: _expand_dims,
|
|
1271
|
+
np.moveaxis: _moveaxis,
|
|
1272
|
+
np.swapaxes: _swapaxes,
|
|
1273
|
+
np.rollaxis: _rollaxis,
|
|
1274
|
+
np.ravel: _ravel,
|
|
1275
|
+
np.matrix_transpose: partial(_array_transform, np.matrix_transpose),
|
|
1276
|
+
np.linalg.matrix_transpose: partial(_array_transform, np.linalg.matrix_transpose),
|
|
1277
|
+
np.broadcast_to: partial(_array_transform, np.broadcast_to),
|
|
1278
|
+
np.flip: partial(_array_transform, np.flip),
|
|
1279
|
+
np.fliplr: partial(_array_transform, np.fliplr),
|
|
1280
|
+
np.flipud: partial(_array_transform, np.flipud),
|
|
1281
|
+
np.rot90: partial(_array_transform, np.rot90),
|
|
1282
|
+
np.roll: partial(_array_transform, np.roll),
|
|
1283
|
+
np.repeat: partial(_array_transform, np.repeat),
|
|
1284
|
+
np.tile: partial(_array_transform, np.tile),
|
|
1285
|
+
np.copy: partial(_array_transform, np.copy),
|
|
1286
|
+
np.shape: partial(_array_metadata, np.shape),
|
|
1287
|
+
np.ndim: partial(_array_metadata, np.ndim),
|
|
1288
|
+
np.size: partial(_array_metadata, np.size),
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
_QUANTITY_FUNCTIONS = {
|
|
1292
|
+
**_ARRAY_FUNCTIONS,
|
|
1219
1293
|
np.argmin: partial(_argmin_max, np.argmin),
|
|
1220
1294
|
np.argmax: partial(_argmin_max, np.argmax),
|
|
1221
1295
|
np.nanargmin: partial(_argmin_max, np.nanargmin),
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""Array manipulation preserves each operand's unit and native storage rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from unitx import AffineQuantity, LogQuantity, Quantity, Unit
|
|
12
|
+
from unitx._numerics import Array
|
|
13
|
+
from unitx.factor import Factor, pi
|
|
14
|
+
|
|
15
|
+
np = pytest.importorskip('numpy')
|
|
16
|
+
|
|
17
|
+
ArrayQuantity = Quantity[Array] | AffineQuantity[Array] | LogQuantity[Array]
|
|
18
|
+
|
|
19
|
+
_TRANSFORMS = [
|
|
20
|
+
(np.matrix_transpose, (), {}),
|
|
21
|
+
(np.linalg.matrix_transpose, (), {}),
|
|
22
|
+
(np.permute_dims, (), {'axes': (1, 0)}),
|
|
23
|
+
(np.broadcast_to, ((4, 2, 3),), {'subok': True}),
|
|
24
|
+
(np.flip, (), {}),
|
|
25
|
+
(np.flip, (), {'axis': (0, -1)}),
|
|
26
|
+
(np.fliplr, (), {}),
|
|
27
|
+
(np.flipud, (), {}),
|
|
28
|
+
(np.rot90, (), {'k': -1, 'axes': (1, 0)}),
|
|
29
|
+
(np.rot90, (2,), {}),
|
|
30
|
+
(np.roll, (2,), {}),
|
|
31
|
+
(np.roll, (), {'shift': (1, -2), 'axis': (0, -1)}),
|
|
32
|
+
(np.repeat, (2,), {}),
|
|
33
|
+
(np.repeat, (), {'repeats': [0, 2, 1], 'axis': -1}),
|
|
34
|
+
(np.tile, ((2, 1, 3),), {}),
|
|
35
|
+
(np.copy, (), {}),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture(params=['linear', 'affine', 'logarithmic'])
|
|
40
|
+
def sample(request: pytest.FixtureRequest) -> ArrayQuantity:
|
|
41
|
+
values = np.arange(12, dtype=np.int16).reshape(2, 6)[:, ::2]
|
|
42
|
+
if request.param == 'linear':
|
|
43
|
+
return Quantity(values, Factor(2) * pi * Unit('cm'))
|
|
44
|
+
if request.param == 'affine':
|
|
45
|
+
return AffineQuantity(values, 'degC')
|
|
46
|
+
return LogQuantity(values, 'dBm')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _writeable_flag(value: Any) -> tuple[bool, tuple[tuple[type[Warning], str], ...]]:
|
|
50
|
+
# NumPy may warn when inspecting a broadcast view's writeability. Verify
|
|
51
|
+
# that wrapping retains those warnings instead of filtering them globally.
|
|
52
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
53
|
+
warnings.simplefilter('always')
|
|
54
|
+
writeable = value.flags.writeable
|
|
55
|
+
return writeable, tuple((item.category, str(item.message)) for item in caught)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _check_result(result: Any, expected: Any, source: ArrayQuantity) -> None:
|
|
59
|
+
assert type(result) is type(source) and result.unit == source.unit
|
|
60
|
+
assert type(result.value) is type(expected)
|
|
61
|
+
assert result.value.dtype == expected.dtype
|
|
62
|
+
assert result.value.shape == expected.shape
|
|
63
|
+
assert result.value.strides == expected.strides
|
|
64
|
+
assert _writeable_flag(result.value) == _writeable_flag(expected)
|
|
65
|
+
assert np.shares_memory(result.value, source.value) == np.shares_memory(expected, source.value)
|
|
66
|
+
np.testing.assert_array_equal(result.value, expected)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
70
|
+
@pytest.mark.parametrize('readonly', [False, True])
|
|
71
|
+
def test_numpy_array_transform_storage(
|
|
72
|
+
sample: ArrayQuantity,
|
|
73
|
+
operation: Callable[..., Any],
|
|
74
|
+
args: tuple[object, ...],
|
|
75
|
+
kwargs: dict[str, object],
|
|
76
|
+
readonly: bool,
|
|
77
|
+
) -> None:
|
|
78
|
+
sample.value.flags.writeable = not readonly
|
|
79
|
+
before = sample.value.copy()
|
|
80
|
+
expected = operation(sample.value, *args, **kwargs)
|
|
81
|
+
_check_result(operation(sample, *args, **kwargs), expected, sample)
|
|
82
|
+
np.testing.assert_array_equal(sample.value, before)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.parametrize(
|
|
86
|
+
'operation,args',
|
|
87
|
+
[
|
|
88
|
+
(np.matrix_transpose, ()),
|
|
89
|
+
(np.flip, ()),
|
|
90
|
+
(np.roll, (2,)),
|
|
91
|
+
(np.repeat, (0,)),
|
|
92
|
+
(np.tile, ((2, 1),)),
|
|
93
|
+
(np.copy, ()),
|
|
94
|
+
(np.broadcast_to, ((2, 0, 3),)),
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
def test_numpy_array_transform_empty(
|
|
98
|
+
sample: ArrayQuantity, operation: Callable[..., Any], args: tuple[object, ...]
|
|
99
|
+
) -> None:
|
|
100
|
+
q = type(sample)(np.empty((0, 3), dtype=np.float32), sample.unit)
|
|
101
|
+
_check_result(operation(q, *args), operation(q.value, *args), q)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize(
|
|
105
|
+
'operation,args',
|
|
106
|
+
[
|
|
107
|
+
(np.flip, ()),
|
|
108
|
+
(np.roll, (2,)),
|
|
109
|
+
(np.repeat, (3,)),
|
|
110
|
+
(np.tile, ((2, 1),)),
|
|
111
|
+
(np.copy, ()),
|
|
112
|
+
(np.broadcast_to, ((2, 3),)),
|
|
113
|
+
(np.atleast_1d, ()),
|
|
114
|
+
(np.atleast_2d, ()),
|
|
115
|
+
(np.atleast_3d, ()),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
@pytest.mark.parametrize('value', [2, np.float32(2), np.array(2, dtype=np.int16)])
|
|
119
|
+
def test_numpy_array_transform_scalar(
|
|
120
|
+
sample: ArrayQuantity, operation: Callable[..., Any], args: tuple[object, ...], value: Any
|
|
121
|
+
) -> None:
|
|
122
|
+
q = type(sample)(value, sample.unit)
|
|
123
|
+
_check_result(operation(q, *args), operation(value, *args), q)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize('operation', [np.atleast_1d, np.atleast_2d, np.atleast_3d])
|
|
127
|
+
def test_numpy_atleast_single(sample: ArrayQuantity, operation: Callable[..., Any]) -> None:
|
|
128
|
+
_check_result(operation(sample), operation(sample.value), sample)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pytest.mark.parametrize(
|
|
132
|
+
'operation', [np.atleast_1d, np.atleast_2d, np.atleast_3d, np.broadcast_arrays]
|
|
133
|
+
)
|
|
134
|
+
@pytest.mark.parametrize('bare_first', [False, True])
|
|
135
|
+
def test_numpy_multiple_arrays_independent_units(
|
|
136
|
+
operation: Callable[..., Any], bare_first: bool
|
|
137
|
+
) -> None:
|
|
138
|
+
distance = Quantity(np.array([1, 2, 3]), Factor(2) * pi * Unit('cm'))
|
|
139
|
+
temperature = AffineQuantity(np.array([[10.0], [20]]), 'degC')
|
|
140
|
+
level = LogQuantity(np.array(30.0), 'dBm')
|
|
141
|
+
bare = np.array([4.0, 5, 6])
|
|
142
|
+
arrays = (
|
|
143
|
+
(bare, distance, temperature, level, 2)
|
|
144
|
+
if bare_first
|
|
145
|
+
else (distance, temperature, level, bare, 2)
|
|
146
|
+
)
|
|
147
|
+
values = tuple(
|
|
148
|
+
x.value if isinstance(x, (Quantity, AffineQuantity, LogQuantity)) else x for x in arrays
|
|
149
|
+
)
|
|
150
|
+
expected = operation(*values)
|
|
151
|
+
result = operation(*arrays)
|
|
152
|
+
assert type(result) is type(expected) and len(result) == len(arrays)
|
|
153
|
+
for actual, raw, source in zip(result, expected, arrays):
|
|
154
|
+
if isinstance(source, (Quantity, AffineQuantity, LogQuantity)):
|
|
155
|
+
_check_result(actual, raw, source)
|
|
156
|
+
else:
|
|
157
|
+
assert type(actual) is type(raw) and actual.dtype == raw.dtype
|
|
158
|
+
np.testing.assert_array_equal(actual, raw)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@pytest.mark.parametrize('subok', [False, True])
|
|
162
|
+
def test_numpy_broadcast_arrays_single(sample: ArrayQuantity, subok: bool) -> None:
|
|
163
|
+
expected = np.broadcast_arrays(sample.value, subok=subok)
|
|
164
|
+
result = np.broadcast_arrays(sample, subok=subok)
|
|
165
|
+
assert type(result) is type(expected) and len(result) == 1
|
|
166
|
+
_check_result(result[0], expected[0], sample)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_numpy_broadcast_to_readonly(sample: ArrayQuantity) -> None:
|
|
170
|
+
result = np.broadcast_to(sample, (4, 2, 3))
|
|
171
|
+
assert np.shares_memory(result.value, sample.value)
|
|
172
|
+
with pytest.raises(ValueError, match='read-only'):
|
|
173
|
+
result[0, 0, 0] = type(sample)(1, sample.unit)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.parametrize(
|
|
177
|
+
'operation',
|
|
178
|
+
[
|
|
179
|
+
np.flip,
|
|
180
|
+
np.fliplr,
|
|
181
|
+
np.flipud,
|
|
182
|
+
np.rot90,
|
|
183
|
+
np.matrix_transpose,
|
|
184
|
+
np.linalg.matrix_transpose,
|
|
185
|
+
np.atleast_1d,
|
|
186
|
+
np.atleast_2d,
|
|
187
|
+
np.atleast_3d,
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
def test_numpy_array_transform_shared_write(
|
|
191
|
+
sample: ArrayQuantity, operation: Callable[..., Any]
|
|
192
|
+
) -> None:
|
|
193
|
+
result = operation(sample)
|
|
194
|
+
assert np.shares_memory(result.value, sample.value)
|
|
195
|
+
result.value[...] = 42
|
|
196
|
+
np.testing.assert_array_equal(sample.value, np.full(sample.shape, 42))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize('order', ['C', 'F', 'A', 'K'])
|
|
200
|
+
@pytest.mark.parametrize('layout', ['C', 'F', 'transpose', 'strided', 'negative'])
|
|
201
|
+
def test_numpy_copy_order(sample: ArrayQuantity, order: str, layout: str) -> None:
|
|
202
|
+
value = np.arange(12, dtype=np.int16).reshape(3, 4)
|
|
203
|
+
if layout == 'F':
|
|
204
|
+
value = np.asfortranarray(value)
|
|
205
|
+
elif layout == 'transpose':
|
|
206
|
+
value = value.T
|
|
207
|
+
elif layout == 'strided':
|
|
208
|
+
value = value[:, ::2]
|
|
209
|
+
elif layout == 'negative':
|
|
210
|
+
value = value[:, ::-1]
|
|
211
|
+
value.flags.writeable = False
|
|
212
|
+
q = type(sample)(value, sample.unit)
|
|
213
|
+
result = np.copy(q, order=order, subok=True)
|
|
214
|
+
_check_result(result, np.copy(value, order=order, subok=True), q)
|
|
215
|
+
assert result.value.flags.writeable and not np.shares_memory(result.value, value)
|
|
216
|
+
result.value[...] = -1
|
|
217
|
+
assert np.all(value >= 0)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.mark.parametrize('value', [2, np.float32(2), np.array(2), np.empty((2, 0, 3))])
|
|
221
|
+
def test_numpy_array_metadata(sample: ArrayQuantity, value: Any) -> None:
|
|
222
|
+
q = type(sample)(value, sample.unit)
|
|
223
|
+
assert np.shape(q) == np.shape(value)
|
|
224
|
+
assert np.ndim(q) == np.ndim(value)
|
|
225
|
+
assert np.size(q) == np.size(value)
|
|
226
|
+
assert type(np.shape(q)) is tuple
|
|
227
|
+
assert type(np.ndim(q)) is int
|
|
228
|
+
assert type(np.size(q)) is int
|
|
229
|
+
if np.ndim(value):
|
|
230
|
+
assert np.size(q, axis=-1) == np.size(value, axis=-1)
|
|
231
|
+
assert np.size(q, 0) == np.size(value, 0)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.parametrize(
|
|
235
|
+
'operation,args,error',
|
|
236
|
+
[
|
|
237
|
+
(np.broadcast_to, ((3, 2),), ValueError),
|
|
238
|
+
(np.flip, (3,), np.exceptions.AxisError),
|
|
239
|
+
(np.rot90, (1, (0, 0)), ValueError),
|
|
240
|
+
(np.roll, (1, 3), np.exceptions.AxisError),
|
|
241
|
+
(np.repeat, (-1,), ValueError),
|
|
242
|
+
(np.repeat, ([1, 2], -1), ValueError),
|
|
243
|
+
(np.tile, ((-1, 2),), ValueError),
|
|
244
|
+
(np.copy, ('invalid',), ValueError),
|
|
245
|
+
(np.size, (3,), IndexError),
|
|
246
|
+
],
|
|
247
|
+
)
|
|
248
|
+
def test_numpy_array_transform_invalid(
|
|
249
|
+
sample: ArrayQuantity,
|
|
250
|
+
operation: Callable[..., Any],
|
|
251
|
+
args: tuple[object, ...],
|
|
252
|
+
error: type[Exception],
|
|
253
|
+
) -> None:
|
|
254
|
+
before = sample.value.copy()
|
|
255
|
+
with pytest.raises(error):
|
|
256
|
+
operation(sample, *args)
|
|
257
|
+
np.testing.assert_array_equal(sample.value, before)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@pytest.mark.parametrize('operation', [np.matrix_transpose, np.linalg.matrix_transpose, np.fliplr])
|
|
261
|
+
def test_numpy_matrix_transform_invalid_rank(
|
|
262
|
+
sample: ArrayQuantity, operation: Callable[..., Any]
|
|
263
|
+
) -> None:
|
|
264
|
+
q = type(sample)(np.array([1, 2]), sample.unit)
|
|
265
|
+
with pytest.raises(ValueError):
|
|
266
|
+
operation(q)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_numpy_broadcast_arrays_invalid_shape(sample: ArrayQuantity) -> None:
|
|
270
|
+
with pytest.raises(ValueError, match='shape mismatch'):
|
|
271
|
+
np.broadcast_arrays(sample, np.ones(4))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.parametrize(
|
|
275
|
+
'operation', [np.atleast_1d, np.atleast_2d, np.atleast_3d, np.broadcast_arrays]
|
|
276
|
+
)
|
|
277
|
+
@pytest.mark.parametrize(
|
|
278
|
+
'value', [[1, 2, 3], np.array([object()], dtype=object), np.ma.array([1, 2, 3])]
|
|
279
|
+
)
|
|
280
|
+
def test_numpy_multiple_arrays_invalid_operand(
|
|
281
|
+
sample: ArrayQuantity, operation: Callable[..., Any], value: Any
|
|
282
|
+
) -> None:
|
|
283
|
+
with pytest.raises(TypeError):
|
|
284
|
+
operation(sample, value)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
288
|
+
def test_numpy_array_transform_invalid_keyword(
|
|
289
|
+
sample: ArrayQuantity,
|
|
290
|
+
operation: Callable[..., Any],
|
|
291
|
+
args: tuple[object, ...],
|
|
292
|
+
kwargs: dict[str, object],
|
|
293
|
+
) -> None:
|
|
294
|
+
with pytest.raises(TypeError, match='unexpected keyword'):
|
|
295
|
+
operation(sample, *args, **kwargs, out=sample)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.parametrize(
|
|
299
|
+
'operation',
|
|
300
|
+
[np.atleast_1d, np.atleast_2d, np.atleast_3d, np.broadcast_arrays, np.shape, np.ndim, np.size],
|
|
301
|
+
)
|
|
302
|
+
def test_numpy_array_function_invalid_keyword(
|
|
303
|
+
sample: ArrayQuantity, operation: Callable[..., Any]
|
|
304
|
+
) -> None:
|
|
305
|
+
with pytest.raises(TypeError, match='unexpected keyword'):
|
|
306
|
+
operation(sample, out=sample)
|