unitx 0.16.2__tar.gz → 0.16.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.16.2 → unitx-0.16.3}/.github/consumers/arrays.py +21 -1
- {unitx-0.16.2 → unitx-0.16.3}/CHANGELOG.md +8 -0
- {unitx-0.16.2 → unitx-0.16.3}/PKG-INFO +1 -1
- {unitx-0.16.2 → unitx-0.16.3}/docs/numpy.md +34 -2
- {unitx-0.16.2 → unitx-0.16.3}/docs/typing.md +6 -1
- {unitx-0.16.2 → unitx-0.16.3}/pyproject.toml +1 -1
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_numerics/numpy.py +36 -0
- unitx-0.16.3/tests/test_numpy_shapes.py +217 -0
- {unitx-0.16.2 → unitx-0.16.3}/.github/consumers/pyrightconfig.json +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.github/consumers/scalars.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.github/workflows/ci.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.github/workflows/pages.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.github/workflows/release.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.gitignore +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/.readthedocs.yaml +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/CONTRIBUTING.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/LICENSE +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/benchmarks/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/benchmarks/numpy_writes.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/benchmarks/scalars.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/conftest.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/affine.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/architecture.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/catalogues/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/catalogues/cgs.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/catalogues/imperial.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/catalogues/si.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/custom-units.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/display.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/images/blackbody.svg +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/javascripts/mermaid.mjs +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/logarithmic.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/migration.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/numpy/calculus.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/numpy/selection.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/numpy/statistics.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/numpy/writes.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/requirements.txt +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/scalars.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/serialization.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/stdlib.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/docs/units.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/README.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/arrhenius_fit.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/blackbody.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/damped_oscillator.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/gaussian_wavepacket.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/impedance.md +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/impedance.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/optical_attenuation.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/pendulum_uncertainty.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/examples/smith_chart.ipynb +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/mkdocs.yml +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/pyrightconfig.json +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_constnamespace.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/definition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/logarithms.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/points.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_coordinates/resolve.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_electromagnetism/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_electromagnetism/coefficients.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_electromagnetism/conversion.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_formatting/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_formatting/render.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_formatting/unicode.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_immutable.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_numerics/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_numerics/conversion.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_numerics/numpy_typing.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_numerics/scaling.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_operands.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_registry/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_registry/builtin_units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_registry/prefixes.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/_registry/reconstruction.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/affinequantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/affineunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/cgs/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/cgs/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/dimension.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/exceptions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/factor.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/imperial/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/imperial/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/logquantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/logunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/monomial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/py.typed +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/si/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/si/bases.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/si/constants.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/si/units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/singleunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/unit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/unitcatalog.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/unitdefinition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/unitparser.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/src/unitx/unitregistry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/__init__.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/_notebooks.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/numpy_typing/check_asarray.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/numpy_typing/check_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/numpy_typing/check_logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/numpy_typing/check_math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/numpy_typing/check_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_affinequantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_builtin_definitions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_catalog_collection.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_cgs_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_cgs_systems.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_decibel_call.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_definition_scope.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_dimension.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_display.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_examples.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_factor.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_immutable.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_impedance_example.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_imperial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_import.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_linear_boundaries.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_logarithmic_model.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_math.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_math_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_monomial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numeric.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_differentiation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_integration.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_interpolation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_multi_outputs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_mutation.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_nan_reductions.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_nan_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_outputs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_products.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_selection.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_numpy_weighted_statistics.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_registry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_registry_scope.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_si.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_singleunit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_siunitx_display.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_typing.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unicode.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unicode_units.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unit.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unit_definition.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unitparser.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_unitregistry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/test_warnings.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_affine.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_cgs.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_impedance_example.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_imperial.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_logarithmic.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_math_time.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_quantity.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_registry.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/tests/typing/check_serialization.py +0 -0
- {unitx-0.16.2 → unitx-0.16.3}/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,24 @@ 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
|
+
|
|
50
70
|
bounded = cast(Quantity[Array], np.clip(distance, Quantity(500, si.cm), Quantity(25, si.m)))
|
|
51
71
|
assert isinstance(bounded, Quantity) and bounded.unit == si.m
|
|
52
72
|
np.testing.assert_allclose(bounded.value, [5, 20, 25])
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.16.3] - 2026-10-25
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Support NumPy `squeeze`, `expand_dims`, `moveaxis`, `swapaxes`, `rollaxis`
|
|
10
|
+
and `ravel`, preserving complete units or point coordinates. Retain NumPy's
|
|
11
|
+
axis rules, dtype, shared views, read-only storage and necessary copies.
|
|
12
|
+
|
|
5
13
|
## [0.16.2] - 2026-10-25
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unitx
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.3
|
|
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,9 @@ 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 |
|
|
118
121
|
| concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting, out; dtype and out are mutually exclusive |
|
|
119
122
|
| sum, nansum | Preserve unit | axis, dtype, keepdims, initial, where, out |
|
|
120
123
|
| mean, nanmean | Preserve unit or point coordinate | axis, dtype, keepdims, where, out |
|
|
@@ -146,6 +149,35 @@ Indexed assignment and augmented arithmetic on ndarrays follow the fixed-target
|
|
|
146
149
|
rules below. Python and NumPy scalar magnitudes retain ordinary result/rebinding
|
|
147
150
|
behavior for augmented arithmetic.
|
|
148
151
|
|
|
152
|
+
## Shape transformations
|
|
153
|
+
|
|
154
|
+
`squeeze`, `expand_dims`, `moveaxis`, `swapaxes`, `rollaxis`, and `ravel`
|
|
155
|
+
operate on magnitudes without conversion. Like reshape and transpose, they
|
|
156
|
+
retain the complete unit or point coordinate and return the same family:
|
|
157
|
+
Quantity, AffineQuantity, or LogQuantity. Only the NumPy function forms are
|
|
158
|
+
supported; these functions have no `out` option.
|
|
159
|
+
|
|
160
|
+
Axis rules and errors follow NumPy, including negative and tuple axes where
|
|
161
|
+
supported. Squeezing an ndarray whose axes all have length one produces a
|
|
162
|
+
zero-dimensional ndarray magnitude. Views share storage and its writeability
|
|
163
|
+
with the input.
|
|
164
|
+
`ravel` returns a contiguous one-dimensional magnitude, copying only when needed;
|
|
165
|
+
it can copy a noncontiguous input even when reshape could return a view.
|
|
166
|
+
Its C/F/A/K orders follow NumPy. Prefer `moveaxis` for new code; `rollaxis` is
|
|
167
|
+
also supported for compatibility with existing NumPy code.
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import numpy as np
|
|
171
|
+
from unitx import Quantity
|
|
172
|
+
|
|
173
|
+
samples = Quantity(np.arange(6).reshape(1, 2, 3), 'cm')
|
|
174
|
+
matrix = np.squeeze(samples, axis=0)
|
|
175
|
+
assert matrix.shape == (2, 3)
|
|
176
|
+
assert np.moveaxis(matrix, 0, -1).shape == (3, 2)
|
|
177
|
+
assert matrix.unit == samples.unit
|
|
178
|
+
assert np.shares_memory(matrix.value, samples.value)
|
|
179
|
+
```
|
|
180
|
+
|
|
149
181
|
## Selection and clipping
|
|
150
182
|
|
|
151
183
|
See [selection and clipping](numpy/selection.md) for `where`, `clip`,
|
|
@@ -94,11 +94,16 @@ 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
|
+
`where`, `clip`,
|
|
98
99
|
`minimum/maximum`, `average`, `quantile`, `percentile`, the supported `nan*`
|
|
99
100
|
statistics, and `interp`: runtime results
|
|
100
101
|
preserve the documented Quantity/point wrappers, but NumPy may annotate them
|
|
101
102
|
as ndarrays or Any. NumPy's `out` annotations also accept only ndarrays.
|
|
103
|
+
The `moveaxis` and `rollaxis` input annotations likewise require ndarrays
|
|
104
|
+
and can reject a Quantity/point argument during static checking, despite
|
|
105
|
+
working at runtime. Their installed type consumers check the dispatch protocol
|
|
106
|
+
directly; runtime tests use ordinary NumPy calls.
|
|
102
107
|
Runtime support and installed-consumer checks cover
|
|
103
108
|
these calls; unitx does not replace NumPy's stubs.
|
|
104
109
|
|
|
@@ -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.3'
|
|
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'
|
|
@@ -604,6 +604,12 @@ def dispatch_function(
|
|
|
604
604
|
np.allclose,
|
|
605
605
|
np.reshape,
|
|
606
606
|
np.transpose,
|
|
607
|
+
np.squeeze,
|
|
608
|
+
np.expand_dims,
|
|
609
|
+
np.moveaxis,
|
|
610
|
+
np.swapaxes,
|
|
611
|
+
np.rollaxis,
|
|
612
|
+
np.ravel,
|
|
607
613
|
np.argmin,
|
|
608
614
|
np.argmax,
|
|
609
615
|
np.nanargmin,
|
|
@@ -754,6 +760,30 @@ def _transpose(q: Any, axes: Any = None) -> Any:
|
|
|
754
760
|
return _wrap(np.transpose(q.value, axes), q.unit)
|
|
755
761
|
|
|
756
762
|
|
|
763
|
+
def _squeeze(q: Any, axis: Any = None) -> Any:
|
|
764
|
+
return _wrap(np.squeeze(q.value, axis=axis), q.unit)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _expand_dims(q: Any, axis: Any) -> Any:
|
|
768
|
+
return _wrap(np.expand_dims(q.value, axis), q.unit)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _moveaxis(q: Any, source: Any, destination: Any) -> Any:
|
|
772
|
+
return _wrap(np.moveaxis(q.value, source, destination), q.unit)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _swapaxes(q: Any, axis1: Any, axis2: Any) -> Any:
|
|
776
|
+
return _wrap(np.swapaxes(q.value, axis1, axis2), q.unit)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def _rollaxis(q: Any, axis: Any, start: Any = 0) -> Any:
|
|
780
|
+
return _wrap(np.rollaxis(q.value, axis, start), q.unit)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def _ravel(q: Any, order: Literal['C', 'F', 'A', 'K'] = 'C') -> Any:
|
|
784
|
+
return _wrap(np.ravel(q.value, order=order), q.unit)
|
|
785
|
+
|
|
786
|
+
|
|
757
787
|
def _interp(
|
|
758
788
|
x: Any, xp: Any, fp: Any, left: Any = None, right: Any = None, period: Any = None
|
|
759
789
|
) -> Any:
|
|
@@ -1216,6 +1246,12 @@ def _extremum(
|
|
|
1216
1246
|
_QUANTITY_FUNCTIONS = {
|
|
1217
1247
|
np.reshape: _reshape,
|
|
1218
1248
|
np.transpose: _transpose,
|
|
1249
|
+
np.squeeze: _squeeze,
|
|
1250
|
+
np.expand_dims: _expand_dims,
|
|
1251
|
+
np.moveaxis: _moveaxis,
|
|
1252
|
+
np.swapaxes: _swapaxes,
|
|
1253
|
+
np.rollaxis: _rollaxis,
|
|
1254
|
+
np.ravel: _ravel,
|
|
1219
1255
|
np.argmin: partial(_argmin_max, np.argmin),
|
|
1220
1256
|
np.argmax: partial(_argmin_max, np.argmax),
|
|
1221
1257
|
np.nanargmin: partial(_argmin_max, np.nanargmin),
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Shape transformations retain units, coordinates and NumPy storage behavior."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from unitx import AffineQuantity, LogQuantity, Quantity, Unit
|
|
11
|
+
from unitx._numerics import Array
|
|
12
|
+
from unitx.factor import Factor, pi
|
|
13
|
+
|
|
14
|
+
np = pytest.importorskip('numpy')
|
|
15
|
+
|
|
16
|
+
ArrayQuantity = Quantity[Array] | AffineQuantity[Array] | LogQuantity[Array]
|
|
17
|
+
|
|
18
|
+
_TRANSFORMS = [
|
|
19
|
+
pytest.param(np.squeeze, (), {}, id='squeeze'),
|
|
20
|
+
pytest.param(np.squeeze, (), {'axis': (0, -2)}, id='squeeze-tuple'),
|
|
21
|
+
pytest.param(np.expand_dims, (0,), {}, id='expand-dims'),
|
|
22
|
+
pytest.param(np.expand_dims, (), {'axis': (0, -1)}, id='expand-dims-tuple'),
|
|
23
|
+
pytest.param(np.moveaxis, (0, -1), {}, id='moveaxis'),
|
|
24
|
+
pytest.param(np.moveaxis, (), {'source': (0, 3), 'destination': (3, 0)}, id='moveaxis-tuple'),
|
|
25
|
+
pytest.param(np.swapaxes, (), {'axis1': 1, 'axis2': -1}, id='swapaxes'),
|
|
26
|
+
pytest.param(np.rollaxis, (3,), {}, id='rollaxis'),
|
|
27
|
+
pytest.param(np.rollaxis, (), {'axis': 0, 'start': 4}, id='rollaxis-end'),
|
|
28
|
+
pytest.param(np.rollaxis, (3, -3), {}, id='rollaxis-negative-start'),
|
|
29
|
+
pytest.param(np.ravel, (), {}, id='ravel'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture(params=['linear', 'affine', 'logarithmic'])
|
|
34
|
+
def sample(request: pytest.FixtureRequest) -> ArrayQuantity:
|
|
35
|
+
values = np.arange(24, dtype=np.int16).reshape(1, 2, 1, 12)[..., ::4]
|
|
36
|
+
if request.param == 'linear':
|
|
37
|
+
return Quantity(values, Factor(2) * pi * Unit('cm'))
|
|
38
|
+
if request.param == 'affine':
|
|
39
|
+
return AffineQuantity(values, 'degC')
|
|
40
|
+
return LogQuantity(values, 'dBm')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
44
|
+
@pytest.mark.parametrize('readonly', [False, True])
|
|
45
|
+
def test_numpy_shape_transform_storage(
|
|
46
|
+
sample: ArrayQuantity,
|
|
47
|
+
operation: Callable[..., Any],
|
|
48
|
+
args: tuple[object, ...],
|
|
49
|
+
kwargs: dict[str, object],
|
|
50
|
+
readonly: bool,
|
|
51
|
+
) -> None:
|
|
52
|
+
sample.value.flags.writeable = not readonly
|
|
53
|
+
before = sample.value.copy()
|
|
54
|
+
expected = operation(sample.value, *args, **kwargs)
|
|
55
|
+
result = operation(sample, *args, **kwargs)
|
|
56
|
+
assert type(result) is type(sample)
|
|
57
|
+
assert result.unit == sample.unit
|
|
58
|
+
assert type(result.value) is type(expected)
|
|
59
|
+
assert result.value.dtype == expected.dtype
|
|
60
|
+
assert result.value.shape == expected.shape
|
|
61
|
+
assert result.value.strides == expected.strides
|
|
62
|
+
assert result.value.flags.writeable == expected.flags.writeable
|
|
63
|
+
assert np.shares_memory(result.value, sample.value) == np.shares_memory(expected, sample.value)
|
|
64
|
+
np.testing.assert_array_equal(result.value, expected)
|
|
65
|
+
np.testing.assert_array_equal(sample.value, before)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
69
|
+
def test_numpy_shape_transform_empty(
|
|
70
|
+
sample: ArrayQuantity,
|
|
71
|
+
operation: Callable[..., Any],
|
|
72
|
+
args: tuple[object, ...],
|
|
73
|
+
kwargs: dict[str, object],
|
|
74
|
+
) -> None:
|
|
75
|
+
q = type(sample)(np.empty((1, 0, 1, 3), dtype=np.float32), sample.unit)
|
|
76
|
+
expected = operation(q.value, *args, **kwargs)
|
|
77
|
+
result = operation(q, *args, **kwargs)
|
|
78
|
+
assert type(result) is type(q) and result.unit == q.unit
|
|
79
|
+
assert result.value.shape == expected.shape
|
|
80
|
+
assert result.value.dtype == expected.dtype
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize(
|
|
84
|
+
'operation,args',
|
|
85
|
+
[(np.squeeze, ()), (np.expand_dims, (-1,)), (np.moveaxis, ((), ())), (np.ravel, ())],
|
|
86
|
+
)
|
|
87
|
+
@pytest.mark.parametrize('value', [2, np.float32(2), np.array(2, dtype=np.int16)])
|
|
88
|
+
def test_numpy_shape_transform_scalar(
|
|
89
|
+
sample: ArrayQuantity, operation: Callable[..., Any], args: tuple[object, ...], value: Any
|
|
90
|
+
) -> None:
|
|
91
|
+
q = type(sample)(value, sample.unit)
|
|
92
|
+
expected = operation(value, *args)
|
|
93
|
+
result = operation(q, *args)
|
|
94
|
+
assert type(result) is type(q) and result.unit == q.unit
|
|
95
|
+
assert type(result.value) is type(expected)
|
|
96
|
+
assert result.value.shape == expected.shape
|
|
97
|
+
assert result.value.dtype == expected.dtype
|
|
98
|
+
np.testing.assert_array_equal(result.value, expected)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_numpy_squeeze_zero_dimensional_result(sample: ArrayQuantity) -> None:
|
|
102
|
+
q = type(sample)(np.array([[2]], dtype=np.int16), sample.unit)
|
|
103
|
+
result = np.squeeze(q)
|
|
104
|
+
assert type(result) is type(q) and result.unit == q.unit
|
|
105
|
+
assert type(result.value) is np.ndarray and result.value.shape == ()
|
|
106
|
+
assert result.value.item() == 2
|
|
107
|
+
assert np.shares_memory(result.value, q.value)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
111
|
+
def test_numpy_shape_transform_shared_write(
|
|
112
|
+
sample: ArrayQuantity,
|
|
113
|
+
operation: Callable[..., Any],
|
|
114
|
+
args: tuple[object, ...],
|
|
115
|
+
kwargs: dict[str, object],
|
|
116
|
+
) -> None:
|
|
117
|
+
q = type(sample)(np.arange(6.0).reshape(1, 2, 1, 3), sample.unit)
|
|
118
|
+
result = operation(q, *args, **kwargs)
|
|
119
|
+
assert np.shares_memory(result.value, q.value)
|
|
120
|
+
result.value.flat[0] = 42
|
|
121
|
+
assert q.value.flat[0] == 42
|
|
122
|
+
assert q.unit == sample.unit
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize('order', ['C', 'F', 'A', 'K'])
|
|
126
|
+
@pytest.mark.parametrize('layout', ['C', 'F', 'transpose', 'strided', 'negative'])
|
|
127
|
+
def test_numpy_ravel_order(sample: ArrayQuantity, order: str, layout: str) -> None:
|
|
128
|
+
values = np.arange(12, dtype=np.int16).reshape(3, 4)
|
|
129
|
+
if layout == 'F':
|
|
130
|
+
values = np.asfortranarray(values)
|
|
131
|
+
elif layout == 'transpose':
|
|
132
|
+
values = values.T
|
|
133
|
+
elif layout == 'strided':
|
|
134
|
+
values = values[:, ::2]
|
|
135
|
+
elif layout == 'negative':
|
|
136
|
+
values = values[:, ::-1]
|
|
137
|
+
q = type(sample)(values, sample.unit)
|
|
138
|
+
before = values.copy()
|
|
139
|
+
expected = np.ravel(values, order)
|
|
140
|
+
result = np.ravel(q, order)
|
|
141
|
+
assert type(result) is type(q) and result.unit == q.unit
|
|
142
|
+
assert result.value.ndim == 1 and result.value.flags.c_contiguous
|
|
143
|
+
assert np.shares_memory(result.value, values) == np.shares_memory(expected, values)
|
|
144
|
+
np.testing.assert_array_equal(result.value, expected)
|
|
145
|
+
if not np.shares_memory(result.value, values):
|
|
146
|
+
result.value[0] = -1
|
|
147
|
+
np.testing.assert_array_equal(values, before)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.mark.parametrize(
|
|
151
|
+
'operation,args',
|
|
152
|
+
[
|
|
153
|
+
(np.squeeze, (4,)),
|
|
154
|
+
(np.expand_dims, (-6,)),
|
|
155
|
+
(np.moveaxis, (4, 0)),
|
|
156
|
+
(np.swapaxes, (0, -5)),
|
|
157
|
+
(np.rollaxis, (0, 5)),
|
|
158
|
+
],
|
|
159
|
+
)
|
|
160
|
+
def test_numpy_shape_transform_invalid_axis(
|
|
161
|
+
sample: ArrayQuantity, operation: Callable[..., Any], args: tuple[object, ...]
|
|
162
|
+
) -> None:
|
|
163
|
+
before = sample.value.copy()
|
|
164
|
+
with pytest.raises(np.exceptions.AxisError):
|
|
165
|
+
operation(sample, *args)
|
|
166
|
+
np.testing.assert_array_equal(sample.value, before)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_numpy_squeeze_invalid_axis_size(sample: ArrayQuantity) -> None:
|
|
170
|
+
with pytest.raises(ValueError, match='size not equal to one'):
|
|
171
|
+
np.squeeze(sample, axis=1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_numpy_squeeze_invalid_repeated_axis(sample: ArrayQuantity) -> None:
|
|
175
|
+
with pytest.raises(ValueError, match='duplicate value'):
|
|
176
|
+
np.squeeze(sample, axis=(0, 0))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.parametrize(
|
|
180
|
+
'operation,args',
|
|
181
|
+
[
|
|
182
|
+
(np.expand_dims, ((0, 0),)),
|
|
183
|
+
(np.moveaxis, ((0, 0), (1, 2))),
|
|
184
|
+
(np.moveaxis, ((0, 1), (2, 2))),
|
|
185
|
+
],
|
|
186
|
+
)
|
|
187
|
+
def test_numpy_shape_transform_invalid_repeated_axis(
|
|
188
|
+
sample: ArrayQuantity, operation: Callable[..., Any], args: tuple[object, ...]
|
|
189
|
+
) -> None:
|
|
190
|
+
with pytest.raises(ValueError, match='repeated axis'):
|
|
191
|
+
operation(sample, *args)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_numpy_moveaxis_invalid_axis_count(sample: ArrayQuantity) -> None:
|
|
195
|
+
with pytest.raises(ValueError, match='same number of elements'):
|
|
196
|
+
np.moveaxis(sample, (0, 1), (2,))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.parametrize('operation,args,kwargs', _TRANSFORMS)
|
|
200
|
+
def test_numpy_shape_transform_invalid_keyword(
|
|
201
|
+
sample: ArrayQuantity,
|
|
202
|
+
operation: Callable[..., Any],
|
|
203
|
+
args: tuple[object, ...],
|
|
204
|
+
kwargs: dict[str, object],
|
|
205
|
+
) -> None:
|
|
206
|
+
with pytest.raises(TypeError, match='unexpected keyword'):
|
|
207
|
+
operation(sample, *args, **kwargs, out=sample)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_numpy_ravel_invalid_order(sample: ArrayQuantity) -> None:
|
|
211
|
+
with pytest.raises(ValueError, match='order'):
|
|
212
|
+
np.ravel(sample, order='invalid')
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_numpy_ravel_invalid_copy(sample: ArrayQuantity) -> None:
|
|
216
|
+
with pytest.raises(TypeError, match='unexpected keyword'):
|
|
217
|
+
np.ravel(sample, copy=True)
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|