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.
Files changed (179) hide show
  1. {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/arrays.py +52 -1
  2. {unitx-0.16.2 → unitx-0.16.4}/CHANGELOG.md +20 -0
  3. {unitx-0.16.2 → unitx-0.16.4}/PKG-INFO +1 -1
  4. {unitx-0.16.2 → unitx-0.16.4}/docs/numpy.md +78 -2
  5. {unitx-0.16.2 → unitx-0.16.4}/docs/typing.md +7 -1
  6. {unitx-0.16.2 → unitx-0.16.4}/pyproject.toml +1 -1
  7. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/numpy.py +78 -4
  8. unitx-0.16.4/tests/test_numpy_array_manipulation.py +306 -0
  9. unitx-0.16.4/tests/test_numpy_shapes.py +217 -0
  10. {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/pyrightconfig.json +0 -0
  11. {unitx-0.16.2 → unitx-0.16.4}/.github/consumers/scalars.py +0 -0
  12. {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/ci.yml +0 -0
  13. {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/pages.yml +0 -0
  14. {unitx-0.16.2 → unitx-0.16.4}/.github/workflows/release.yml +0 -0
  15. {unitx-0.16.2 → unitx-0.16.4}/.gitignore +0 -0
  16. {unitx-0.16.2 → unitx-0.16.4}/.readthedocs.yaml +0 -0
  17. {unitx-0.16.2 → unitx-0.16.4}/CONTRIBUTING.md +0 -0
  18. {unitx-0.16.2 → unitx-0.16.4}/LICENSE +0 -0
  19. {unitx-0.16.2 → unitx-0.16.4}/README.md +0 -0
  20. {unitx-0.16.2 → unitx-0.16.4}/benchmarks/README.md +0 -0
  21. {unitx-0.16.2 → unitx-0.16.4}/benchmarks/numpy_writes.py +0 -0
  22. {unitx-0.16.2 → unitx-0.16.4}/benchmarks/scalars.py +0 -0
  23. {unitx-0.16.2 → unitx-0.16.4}/conftest.py +0 -0
  24. {unitx-0.16.2 → unitx-0.16.4}/docs/README.md +0 -0
  25. {unitx-0.16.2 → unitx-0.16.4}/docs/affine.md +0 -0
  26. {unitx-0.16.2 → unitx-0.16.4}/docs/architecture.md +0 -0
  27. {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/README.md +0 -0
  28. {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/cgs.md +0 -0
  29. {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/imperial.md +0 -0
  30. {unitx-0.16.2 → unitx-0.16.4}/docs/catalogues/si.md +0 -0
  31. {unitx-0.16.2 → unitx-0.16.4}/docs/custom-units.md +0 -0
  32. {unitx-0.16.2 → unitx-0.16.4}/docs/display.md +0 -0
  33. {unitx-0.16.2 → unitx-0.16.4}/docs/images/blackbody.svg +0 -0
  34. {unitx-0.16.2 → unitx-0.16.4}/docs/javascripts/mermaid.mjs +0 -0
  35. {unitx-0.16.2 → unitx-0.16.4}/docs/logarithmic.md +0 -0
  36. {unitx-0.16.2 → unitx-0.16.4}/docs/migration.md +0 -0
  37. {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/calculus.md +0 -0
  38. {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/selection.md +0 -0
  39. {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/statistics.md +0 -0
  40. {unitx-0.16.2 → unitx-0.16.4}/docs/numpy/writes.md +0 -0
  41. {unitx-0.16.2 → unitx-0.16.4}/docs/requirements.txt +0 -0
  42. {unitx-0.16.2 → unitx-0.16.4}/docs/scalars.md +0 -0
  43. {unitx-0.16.2 → unitx-0.16.4}/docs/serialization.md +0 -0
  44. {unitx-0.16.2 → unitx-0.16.4}/docs/stdlib.md +0 -0
  45. {unitx-0.16.2 → unitx-0.16.4}/docs/units.md +0 -0
  46. {unitx-0.16.2 → unitx-0.16.4}/examples/README.md +0 -0
  47. {unitx-0.16.2 → unitx-0.16.4}/examples/arrhenius_fit.ipynb +0 -0
  48. {unitx-0.16.2 → unitx-0.16.4}/examples/blackbody.ipynb +0 -0
  49. {unitx-0.16.2 → unitx-0.16.4}/examples/damped_oscillator.ipynb +0 -0
  50. {unitx-0.16.2 → unitx-0.16.4}/examples/gaussian_wavepacket.ipynb +0 -0
  51. {unitx-0.16.2 → unitx-0.16.4}/examples/impedance.md +0 -0
  52. {unitx-0.16.2 → unitx-0.16.4}/examples/impedance.py +0 -0
  53. {unitx-0.16.2 → unitx-0.16.4}/examples/optical_attenuation.ipynb +0 -0
  54. {unitx-0.16.2 → unitx-0.16.4}/examples/pendulum_uncertainty.ipynb +0 -0
  55. {unitx-0.16.2 → unitx-0.16.4}/examples/smith_chart.ipynb +0 -0
  56. {unitx-0.16.2 → unitx-0.16.4}/mkdocs.yml +0 -0
  57. {unitx-0.16.2 → unitx-0.16.4}/pyrightconfig.json +0 -0
  58. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/__init__.py +0 -0
  59. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_constnamespace.py +0 -0
  60. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/__init__.py +0 -0
  61. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/affine.py +0 -0
  62. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/definition.py +0 -0
  63. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/logarithmic.py +0 -0
  64. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/logarithms.py +0 -0
  65. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/points.py +0 -0
  66. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_coordinates/resolve.py +0 -0
  67. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/__init__.py +0 -0
  68. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/coefficients.py +0 -0
  69. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_electromagnetism/conversion.py +0 -0
  70. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/__init__.py +0 -0
  71. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/render.py +0 -0
  72. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_formatting/unicode.py +0 -0
  73. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_immutable.py +0 -0
  74. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/__init__.py +0 -0
  75. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/conversion.py +0 -0
  76. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/numpy_typing.py +0 -0
  77. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_numerics/scaling.py +0 -0
  78. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_operands.py +0 -0
  79. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/__init__.py +0 -0
  80. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/builtin_units.py +0 -0
  81. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/prefixes.py +0 -0
  82. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/_registry/reconstruction.py +0 -0
  83. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/affinequantity.py +0 -0
  84. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/affineunit.py +0 -0
  85. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/cgs/__init__.py +0 -0
  86. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/cgs/units.py +0 -0
  87. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/dimension.py +0 -0
  88. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/exceptions.py +0 -0
  89. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/factor.py +0 -0
  90. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/imperial/__init__.py +0 -0
  91. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/imperial/units.py +0 -0
  92. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/logquantity.py +0 -0
  93. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/logunit.py +0 -0
  94. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/math.py +0 -0
  95. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/monomial.py +0 -0
  96. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/py.typed +0 -0
  97. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/quantity.py +0 -0
  98. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/serialization.py +0 -0
  99. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/__init__.py +0 -0
  100. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/bases.py +0 -0
  101. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/constants.py +0 -0
  102. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/si/units.py +0 -0
  103. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/singleunit.py +0 -0
  104. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/time.py +0 -0
  105. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unit.py +0 -0
  106. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitcatalog.py +0 -0
  107. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitdefinition.py +0 -0
  108. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitparser.py +0 -0
  109. {unitx-0.16.2 → unitx-0.16.4}/src/unitx/unitregistry.py +0 -0
  110. {unitx-0.16.2 → unitx-0.16.4}/tests/__init__.py +0 -0
  111. {unitx-0.16.2 → unitx-0.16.4}/tests/_notebooks.py +0 -0
  112. {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_asarray.py +0 -0
  113. {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_cgs.py +0 -0
  114. {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_logarithmic.py +0 -0
  115. {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_math.py +0 -0
  116. {unitx-0.16.2 → unitx-0.16.4}/tests/numpy_typing/check_numpy.py +0 -0
  117. {unitx-0.16.2 → unitx-0.16.4}/tests/test_affine.py +0 -0
  118. {unitx-0.16.2 → unitx-0.16.4}/tests/test_affinequantity.py +0 -0
  119. {unitx-0.16.2 → unitx-0.16.4}/tests/test_builtin_definitions.py +0 -0
  120. {unitx-0.16.2 → unitx-0.16.4}/tests/test_catalog_collection.py +0 -0
  121. {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs.py +0 -0
  122. {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs_numpy.py +0 -0
  123. {unitx-0.16.2 → unitx-0.16.4}/tests/test_cgs_systems.py +0 -0
  124. {unitx-0.16.2 → unitx-0.16.4}/tests/test_decibel_call.py +0 -0
  125. {unitx-0.16.2 → unitx-0.16.4}/tests/test_definition_scope.py +0 -0
  126. {unitx-0.16.2 → unitx-0.16.4}/tests/test_dimension.py +0 -0
  127. {unitx-0.16.2 → unitx-0.16.4}/tests/test_display.py +0 -0
  128. {unitx-0.16.2 → unitx-0.16.4}/tests/test_examples.py +0 -0
  129. {unitx-0.16.2 → unitx-0.16.4}/tests/test_factor.py +0 -0
  130. {unitx-0.16.2 → unitx-0.16.4}/tests/test_immutable.py +0 -0
  131. {unitx-0.16.2 → unitx-0.16.4}/tests/test_impedance_example.py +0 -0
  132. {unitx-0.16.2 → unitx-0.16.4}/tests/test_imperial.py +0 -0
  133. {unitx-0.16.2 → unitx-0.16.4}/tests/test_import.py +0 -0
  134. {unitx-0.16.2 → unitx-0.16.4}/tests/test_linear_boundaries.py +0 -0
  135. {unitx-0.16.2 → unitx-0.16.4}/tests/test_logarithmic_model.py +0 -0
  136. {unitx-0.16.2 → unitx-0.16.4}/tests/test_math.py +0 -0
  137. {unitx-0.16.2 → unitx-0.16.4}/tests/test_math_numpy.py +0 -0
  138. {unitx-0.16.2 → unitx-0.16.4}/tests/test_monomial.py +0 -0
  139. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numeric.py +0 -0
  140. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy.py +0 -0
  141. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_affine.py +0 -0
  142. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_differentiation.py +0 -0
  143. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_integration.py +0 -0
  144. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_interpolation.py +0 -0
  145. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_multi_outputs.py +0 -0
  146. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_mutation.py +0 -0
  147. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_nan_reductions.py +0 -0
  148. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_nan_statistics.py +0 -0
  149. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_outputs.py +0 -0
  150. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_products.py +0 -0
  151. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_selection.py +0 -0
  152. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_statistics.py +0 -0
  153. {unitx-0.16.2 → unitx-0.16.4}/tests/test_numpy_weighted_statistics.py +0 -0
  154. {unitx-0.16.2 → unitx-0.16.4}/tests/test_quantity.py +0 -0
  155. {unitx-0.16.2 → unitx-0.16.4}/tests/test_registry.py +0 -0
  156. {unitx-0.16.2 → unitx-0.16.4}/tests/test_registry_scope.py +0 -0
  157. {unitx-0.16.2 → unitx-0.16.4}/tests/test_serialization.py +0 -0
  158. {unitx-0.16.2 → unitx-0.16.4}/tests/test_si.py +0 -0
  159. {unitx-0.16.2 → unitx-0.16.4}/tests/test_singleunit.py +0 -0
  160. {unitx-0.16.2 → unitx-0.16.4}/tests/test_siunitx_display.py +0 -0
  161. {unitx-0.16.2 → unitx-0.16.4}/tests/test_time.py +0 -0
  162. {unitx-0.16.2 → unitx-0.16.4}/tests/test_typing.py +0 -0
  163. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unicode.py +0 -0
  164. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unicode_units.py +0 -0
  165. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unit.py +0 -0
  166. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unit_definition.py +0 -0
  167. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unitparser.py +0 -0
  168. {unitx-0.16.2 → unitx-0.16.4}/tests/test_unitregistry.py +0 -0
  169. {unitx-0.16.2 → unitx-0.16.4}/tests/test_warnings.py +0 -0
  170. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_affine.py +0 -0
  171. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_cgs.py +0 -0
  172. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_impedance_example.py +0 -0
  173. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_imperial.py +0 -0
  174. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_logarithmic.py +0 -0
  175. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_math_time.py +0 -0
  176. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_quantity.py +0 -0
  177. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_registry.py +0 -0
  178. {unitx-0.16.2 → unitx-0.16.4}/tests/typing/check_serialization.py +0 -0
  179. {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.2
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`, reshape, and transpose follow
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 `where`, `clip`,
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.2'
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
- _QUANTITY_FUNCTIONS = {
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)