unitx 0.16.0__tar.gz → 0.16.2__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 (177) hide show
  1. {unitx-0.16.0 → unitx-0.16.2}/.github/consumers/arrays.py +32 -0
  2. {unitx-0.16.0 → unitx-0.16.2}/CHANGELOG.md +23 -0
  3. {unitx-0.16.0 → unitx-0.16.2}/PKG-INFO +1 -1
  4. {unitx-0.16.0 → unitx-0.16.2}/docs/numpy/selection.md +18 -0
  5. {unitx-0.16.0 → unitx-0.16.2}/docs/numpy/statistics.md +85 -1
  6. {unitx-0.16.0 → unitx-0.16.2}/docs/numpy/writes.md +1 -1
  7. {unitx-0.16.0 → unitx-0.16.2}/docs/numpy.md +14 -10
  8. {unitx-0.16.0 → unitx-0.16.2}/docs/typing.md +2 -1
  9. {unitx-0.16.0 → unitx-0.16.2}/pyproject.toml +1 -1
  10. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_numerics/numpy.py +69 -28
  11. {unitx-0.16.0 → unitx-0.16.2}/tests/numpy_typing/check_numpy.py +16 -5
  12. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_affine.py +2 -0
  13. unitx-0.16.2/tests/test_numpy_nan_reductions.py +260 -0
  14. unitx-0.16.2/tests/test_numpy_nan_statistics.py +194 -0
  15. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_outputs.py +1 -1
  16. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_products.py +23 -23
  17. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_statistics.py +37 -21
  18. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_weighted_statistics.py +34 -18
  19. {unitx-0.16.0 → unitx-0.16.2}/.github/consumers/pyrightconfig.json +0 -0
  20. {unitx-0.16.0 → unitx-0.16.2}/.github/consumers/scalars.py +0 -0
  21. {unitx-0.16.0 → unitx-0.16.2}/.github/workflows/ci.yml +0 -0
  22. {unitx-0.16.0 → unitx-0.16.2}/.github/workflows/pages.yml +0 -0
  23. {unitx-0.16.0 → unitx-0.16.2}/.github/workflows/release.yml +0 -0
  24. {unitx-0.16.0 → unitx-0.16.2}/.gitignore +0 -0
  25. {unitx-0.16.0 → unitx-0.16.2}/.readthedocs.yaml +0 -0
  26. {unitx-0.16.0 → unitx-0.16.2}/CONTRIBUTING.md +0 -0
  27. {unitx-0.16.0 → unitx-0.16.2}/LICENSE +0 -0
  28. {unitx-0.16.0 → unitx-0.16.2}/README.md +0 -0
  29. {unitx-0.16.0 → unitx-0.16.2}/benchmarks/README.md +0 -0
  30. {unitx-0.16.0 → unitx-0.16.2}/benchmarks/numpy_writes.py +0 -0
  31. {unitx-0.16.0 → unitx-0.16.2}/benchmarks/scalars.py +0 -0
  32. {unitx-0.16.0 → unitx-0.16.2}/conftest.py +0 -0
  33. {unitx-0.16.0 → unitx-0.16.2}/docs/README.md +0 -0
  34. {unitx-0.16.0 → unitx-0.16.2}/docs/affine.md +0 -0
  35. {unitx-0.16.0 → unitx-0.16.2}/docs/architecture.md +0 -0
  36. {unitx-0.16.0 → unitx-0.16.2}/docs/catalogues/README.md +0 -0
  37. {unitx-0.16.0 → unitx-0.16.2}/docs/catalogues/cgs.md +0 -0
  38. {unitx-0.16.0 → unitx-0.16.2}/docs/catalogues/imperial.md +0 -0
  39. {unitx-0.16.0 → unitx-0.16.2}/docs/catalogues/si.md +0 -0
  40. {unitx-0.16.0 → unitx-0.16.2}/docs/custom-units.md +0 -0
  41. {unitx-0.16.0 → unitx-0.16.2}/docs/display.md +0 -0
  42. {unitx-0.16.0 → unitx-0.16.2}/docs/images/blackbody.svg +0 -0
  43. {unitx-0.16.0 → unitx-0.16.2}/docs/javascripts/mermaid.mjs +0 -0
  44. {unitx-0.16.0 → unitx-0.16.2}/docs/logarithmic.md +0 -0
  45. {unitx-0.16.0 → unitx-0.16.2}/docs/migration.md +0 -0
  46. {unitx-0.16.0 → unitx-0.16.2}/docs/numpy/calculus.md +0 -0
  47. {unitx-0.16.0 → unitx-0.16.2}/docs/requirements.txt +0 -0
  48. {unitx-0.16.0 → unitx-0.16.2}/docs/scalars.md +0 -0
  49. {unitx-0.16.0 → unitx-0.16.2}/docs/serialization.md +0 -0
  50. {unitx-0.16.0 → unitx-0.16.2}/docs/stdlib.md +0 -0
  51. {unitx-0.16.0 → unitx-0.16.2}/docs/units.md +0 -0
  52. {unitx-0.16.0 → unitx-0.16.2}/examples/README.md +0 -0
  53. {unitx-0.16.0 → unitx-0.16.2}/examples/arrhenius_fit.ipynb +0 -0
  54. {unitx-0.16.0 → unitx-0.16.2}/examples/blackbody.ipynb +0 -0
  55. {unitx-0.16.0 → unitx-0.16.2}/examples/damped_oscillator.ipynb +0 -0
  56. {unitx-0.16.0 → unitx-0.16.2}/examples/gaussian_wavepacket.ipynb +0 -0
  57. {unitx-0.16.0 → unitx-0.16.2}/examples/impedance.md +0 -0
  58. {unitx-0.16.0 → unitx-0.16.2}/examples/impedance.py +0 -0
  59. {unitx-0.16.0 → unitx-0.16.2}/examples/optical_attenuation.ipynb +0 -0
  60. {unitx-0.16.0 → unitx-0.16.2}/examples/pendulum_uncertainty.ipynb +0 -0
  61. {unitx-0.16.0 → unitx-0.16.2}/examples/smith_chart.ipynb +0 -0
  62. {unitx-0.16.0 → unitx-0.16.2}/mkdocs.yml +0 -0
  63. {unitx-0.16.0 → unitx-0.16.2}/pyrightconfig.json +0 -0
  64. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/__init__.py +0 -0
  65. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_constnamespace.py +0 -0
  66. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/__init__.py +0 -0
  67. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/affine.py +0 -0
  68. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/definition.py +0 -0
  69. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/logarithmic.py +0 -0
  70. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/logarithms.py +0 -0
  71. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/points.py +0 -0
  72. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_coordinates/resolve.py +0 -0
  73. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_electromagnetism/__init__.py +0 -0
  74. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_electromagnetism/coefficients.py +0 -0
  75. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_electromagnetism/conversion.py +0 -0
  76. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_formatting/__init__.py +0 -0
  77. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_formatting/render.py +0 -0
  78. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_formatting/unicode.py +0 -0
  79. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_immutable.py +0 -0
  80. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_numerics/__init__.py +0 -0
  81. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_numerics/conversion.py +0 -0
  82. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_numerics/numpy_typing.py +0 -0
  83. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_numerics/scaling.py +0 -0
  84. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_operands.py +0 -0
  85. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_registry/__init__.py +0 -0
  86. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_registry/builtin_units.py +0 -0
  87. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_registry/prefixes.py +0 -0
  88. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/_registry/reconstruction.py +0 -0
  89. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/affinequantity.py +0 -0
  90. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/affineunit.py +0 -0
  91. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/cgs/__init__.py +0 -0
  92. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/cgs/units.py +0 -0
  93. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/dimension.py +0 -0
  94. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/exceptions.py +0 -0
  95. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/factor.py +0 -0
  96. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/imperial/__init__.py +0 -0
  97. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/imperial/units.py +0 -0
  98. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/logquantity.py +0 -0
  99. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/logunit.py +0 -0
  100. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/math.py +0 -0
  101. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/monomial.py +0 -0
  102. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/py.typed +0 -0
  103. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/quantity.py +0 -0
  104. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/serialization.py +0 -0
  105. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/si/__init__.py +0 -0
  106. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/si/bases.py +0 -0
  107. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/si/constants.py +0 -0
  108. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/si/units.py +0 -0
  109. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/singleunit.py +0 -0
  110. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/time.py +0 -0
  111. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/unit.py +0 -0
  112. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/unitcatalog.py +0 -0
  113. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/unitdefinition.py +0 -0
  114. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/unitparser.py +0 -0
  115. {unitx-0.16.0 → unitx-0.16.2}/src/unitx/unitregistry.py +0 -0
  116. {unitx-0.16.0 → unitx-0.16.2}/tests/__init__.py +0 -0
  117. {unitx-0.16.0 → unitx-0.16.2}/tests/_notebooks.py +0 -0
  118. {unitx-0.16.0 → unitx-0.16.2}/tests/numpy_typing/check_asarray.py +0 -0
  119. {unitx-0.16.0 → unitx-0.16.2}/tests/numpy_typing/check_cgs.py +0 -0
  120. {unitx-0.16.0 → unitx-0.16.2}/tests/numpy_typing/check_logarithmic.py +0 -0
  121. {unitx-0.16.0 → unitx-0.16.2}/tests/numpy_typing/check_math.py +0 -0
  122. {unitx-0.16.0 → unitx-0.16.2}/tests/test_affine.py +0 -0
  123. {unitx-0.16.0 → unitx-0.16.2}/tests/test_affinequantity.py +0 -0
  124. {unitx-0.16.0 → unitx-0.16.2}/tests/test_builtin_definitions.py +0 -0
  125. {unitx-0.16.0 → unitx-0.16.2}/tests/test_catalog_collection.py +0 -0
  126. {unitx-0.16.0 → unitx-0.16.2}/tests/test_cgs.py +0 -0
  127. {unitx-0.16.0 → unitx-0.16.2}/tests/test_cgs_numpy.py +0 -0
  128. {unitx-0.16.0 → unitx-0.16.2}/tests/test_cgs_systems.py +0 -0
  129. {unitx-0.16.0 → unitx-0.16.2}/tests/test_decibel_call.py +0 -0
  130. {unitx-0.16.0 → unitx-0.16.2}/tests/test_definition_scope.py +0 -0
  131. {unitx-0.16.0 → unitx-0.16.2}/tests/test_dimension.py +0 -0
  132. {unitx-0.16.0 → unitx-0.16.2}/tests/test_display.py +0 -0
  133. {unitx-0.16.0 → unitx-0.16.2}/tests/test_examples.py +0 -0
  134. {unitx-0.16.0 → unitx-0.16.2}/tests/test_factor.py +0 -0
  135. {unitx-0.16.0 → unitx-0.16.2}/tests/test_immutable.py +0 -0
  136. {unitx-0.16.0 → unitx-0.16.2}/tests/test_impedance_example.py +0 -0
  137. {unitx-0.16.0 → unitx-0.16.2}/tests/test_imperial.py +0 -0
  138. {unitx-0.16.0 → unitx-0.16.2}/tests/test_import.py +0 -0
  139. {unitx-0.16.0 → unitx-0.16.2}/tests/test_linear_boundaries.py +0 -0
  140. {unitx-0.16.0 → unitx-0.16.2}/tests/test_logarithmic_model.py +0 -0
  141. {unitx-0.16.0 → unitx-0.16.2}/tests/test_math.py +0 -0
  142. {unitx-0.16.0 → unitx-0.16.2}/tests/test_math_numpy.py +0 -0
  143. {unitx-0.16.0 → unitx-0.16.2}/tests/test_monomial.py +0 -0
  144. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numeric.py +0 -0
  145. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy.py +0 -0
  146. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_differentiation.py +0 -0
  147. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_integration.py +0 -0
  148. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_interpolation.py +0 -0
  149. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_multi_outputs.py +0 -0
  150. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_mutation.py +0 -0
  151. {unitx-0.16.0 → unitx-0.16.2}/tests/test_numpy_selection.py +0 -0
  152. {unitx-0.16.0 → unitx-0.16.2}/tests/test_quantity.py +0 -0
  153. {unitx-0.16.0 → unitx-0.16.2}/tests/test_registry.py +0 -0
  154. {unitx-0.16.0 → unitx-0.16.2}/tests/test_registry_scope.py +0 -0
  155. {unitx-0.16.0 → unitx-0.16.2}/tests/test_serialization.py +0 -0
  156. {unitx-0.16.0 → unitx-0.16.2}/tests/test_si.py +0 -0
  157. {unitx-0.16.0 → unitx-0.16.2}/tests/test_singleunit.py +0 -0
  158. {unitx-0.16.0 → unitx-0.16.2}/tests/test_siunitx_display.py +0 -0
  159. {unitx-0.16.0 → unitx-0.16.2}/tests/test_time.py +0 -0
  160. {unitx-0.16.0 → unitx-0.16.2}/tests/test_typing.py +0 -0
  161. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unicode.py +0 -0
  162. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unicode_units.py +0 -0
  163. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unit.py +0 -0
  164. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unit_definition.py +0 -0
  165. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unitparser.py +0 -0
  166. {unitx-0.16.0 → unitx-0.16.2}/tests/test_unitregistry.py +0 -0
  167. {unitx-0.16.0 → unitx-0.16.2}/tests/test_warnings.py +0 -0
  168. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_affine.py +0 -0
  169. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_cgs.py +0 -0
  170. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_impedance_example.py +0 -0
  171. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_imperial.py +0 -0
  172. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_logarithmic.py +0 -0
  173. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_math_time.py +0 -0
  174. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_quantity.py +0 -0
  175. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_registry.py +0 -0
  176. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_serialization.py +0 -0
  177. {unitx-0.16.0 → unitx-0.16.2}/tests/typing/check_si.py +0 -0
@@ -93,6 +93,38 @@ def check_arrays(values: NDArray[np.float64]) -> None:
93
93
  assert_type(median_target.value.dtype, np.dtype[np.float64])
94
94
  np.testing.assert_allclose(median_target.value, 2000)
95
95
 
96
+ gapped = Quantity(np.array([1.0, np.nan, 3]), si.m)
97
+ for operation in (
98
+ np.nanmean,
99
+ np.nanmedian,
100
+ np.nanstd,
101
+ np.nanvar,
102
+ np.nanmin,
103
+ np.nanmax,
104
+ np.nansum,
105
+ ):
106
+ result = cast(Quantity[Array], operation(gapped))
107
+ assert isinstance(result, Quantity)
108
+ assert result.unit == (si.m**2 if operation is np.nanvar else si.m)
109
+ np.testing.assert_allclose(result.value, operation(gapped.value))
110
+ product = cast(Quantity[Array], np.nanprod(gapped))
111
+ assert isinstance(product, Quantity) and product.unit == si.m**2
112
+ np.testing.assert_allclose(product.value, 3)
113
+ for operation in (np.nanargmin, np.nanargmax):
114
+ indices = np.empty((), dtype=np.intp)
115
+ assert operation(gapped, out=indices) is indices
116
+ assert_type(indices.dtype, np.dtype[np.intp])
117
+ np.testing.assert_array_equal(indices, operation(gapped.value))
118
+ for operation, probability in ((np.nanquantile, 0.5), (np.nanpercentile, 50)):
119
+ result = cast(Quantity[Array], operation(gapped, probability))
120
+ assert isinstance(result, Quantity) and result.unit == si.m
121
+ np.testing.assert_allclose(result.value, 2)
122
+ gapped.__array_function__(
123
+ operation, (Quantity,), (gapped, probability), {'out': median_target}
124
+ )
125
+ np.testing.assert_allclose(median_target.value, 200)
126
+ assert_type(median_target.value.dtype, np.dtype[np.float64])
127
+
96
128
  sample_time = Quantity(np.array([0.0, 1, 3]), si.s)
97
129
  sample_current = Quantity(np.array([0.0, 2, 6]), si.A)
98
130
  grid = Quantity(np.array([0.0, 500, 1000, 2000, 3000]), si.ms)
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.16.2] - 2026-10-25
6
+
7
+ ### Added
8
+
9
+ - Support NumPy `nanmin`, `nanmax`, `nanargmin` and `nanargmax`, preserving
10
+ units or point coordinates for extrema and returning bare indices. Retain
11
+ NumPy's all-NaN warnings/errors and stage output writes before mutation.
12
+ - Support `nansum` with the existing sum unit, initial and mask rules, and
13
+ `nanprod` with unit powers determined by selected non-NaN element counts.
14
+ Dimensional product slices require equal counts; dimensionless products
15
+ normalize to coherent values and may have different counts.
16
+
17
+ ## [0.16.1] - 2026-10-25
18
+
19
+ ### Added
20
+
21
+ - Support NumPy `nanmean`, `nanmedian`, `nanstd`, `nanvar`, `nanquantile` and
22
+ `nanpercentile`, retaining the corresponding statistical unit and coordinate
23
+ rules. Omit NaN samples with NumPy's dtype, mask, weight and warning behavior;
24
+ preserve staged output writes, including when warnings become exceptions.
25
+ Weighted NaN quantiles support one integer axis or `axis=None`, with full-shape
26
+ weights for multidimensional samples, due to NumPy 2.1/2.5 kernel limitations.
27
+
5
28
  ## [0.16.0] - 2026-10-25
6
29
 
7
30
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.16.0
3
+ Version: 0.16.2
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/
@@ -50,9 +50,27 @@ Empty reductions, axes, NaNs and `keepdims` follow NumPy. These reductions
50
50
  write the exact result shape to `out`; elementwise selection and clipping
51
51
  allow the ufunc output broadcasting rules.
52
52
 
53
+ `np.nanmin/max` share the same unit, point, mask, initial and output rules,
54
+ while omitting NaNs. An all-NaN slice returns NaN and emits RuntimeWarning;
55
+ an empty reduction still needs an initial value. A warning promoted to an
56
+ exception leaves `out` unchanged.
57
+
58
+ `np.argmin/argmax` and `np.nanargmin/nanargmax` return bare indices and accept
59
+ one integer axis or `axis=None`, `keepdims`, and a bare integer ndarray `out`
60
+ of exactly the result shape. They also accept affine and logarithmic points
61
+ and operate on stored readings. The NaN variants omit NaNs and raise
62
+ ValueError for all-NaN slices before writing `out`. NumPy's caveat remains:
63
+ `nanargmin` results cannot be trusted for slices containing only NaNs and
64
+ positive infinity; the corresponding `nanargmax` case is NaNs and negative
65
+ infinity. unitx delegates these numerical choices to NumPy.
66
+
53
67
  See NumPy's [where](https://numpy.org/doc/stable/reference/generated/numpy.where.html),
54
68
  [clip](https://numpy.org/doc/stable/reference/generated/numpy.clip.html) and
55
69
  [minimum](https://numpy.org/doc/stable/reference/generated/numpy.minimum.html)
56
70
  references for the underlying numerical behavior.
71
+ See also [nanmin](https://numpy.org/doc/stable/reference/generated/numpy.nanmin.html),
72
+ [nanargmin](https://numpy.org/doc/stable/reference/generated/numpy.nanargmin.html)
73
+ and [nanargmax](https://numpy.org/doc/stable/reference/generated/numpy.nanargmax.html)
74
+ for NaN and infinity behavior.
57
75
 
58
76
  [NumPy overview and support matrix](../numpy.md) · [Documentation index](../README.md)
@@ -106,7 +106,8 @@ with the additional single-unit and explicit-output rules described here.
106
106
  their first, positional argument. Results retain its complete unit, including
107
107
  an exact extra scale or a scaled dimensionless unit. AffineQuantity and
108
108
  LogQuantity points are unsupported; convert explicitly before using these
109
- functions. The `nan*` variants remain unsupported.
109
+ functions. `nanquantile` and `nanpercentile` share these boundaries and omit
110
+ NaN samples as described [below](#nan-statistics).
110
111
 
111
112
  Weights and the `q` parameter must be dimensionless: use bare numeric scalars,
112
113
  plain numeric ndarrays, numeric lists/tuples, or dimensionless Quantities.
@@ -188,4 +189,87 @@ See [NumPy average](https://numpy.org/doc/stable/reference/generated/numpy.avera
188
189
  for the numerical definitions. unitx adds the unit, parameter and output rules
189
190
  described here and uses the installed NumPy kernel.
190
191
 
192
+ ## NaN statistics
193
+
194
+ `nansum` omits NaN samples and otherwise shares `sum`'s unit, `axis`, `dtype`,
195
+ `keepdims`, `where`, scalar `initial` and staged `out` rules. All-NaN, empty
196
+ and fully masked slices return zero in the input unit, or the converted
197
+ initial when supplied. Affine and logarithmic point sums remain undefined.
198
+
199
+ `nanprod` shares `prod`'s options and initial-unit rules. For dimensional
200
+ input U, its unit is `U**N * initial.unit`, where N counts elements selected
201
+ by `where` that are **not NaN**. Zero and infinity still count. All result
202
+ slices must have equal N, even when a zero initial makes their magnitudes
203
+ equal; otherwise DimensionError is raised before writing `out`. All-NaN,
204
+ empty and fully masked slices have N = 0, with value 1 and unit `Unit()`
205
+ before applying initial. When the output has no elements, use `prod`'s
206
+ nominal-extent convention. Dimensionless inputs normalize to coherent values
207
+ first, so unequal non-NaN counts are allowed. Point products remain undefined.
208
+ NaN initial values are passed through to NumPy; they are not omitted.
209
+
210
+ `nanmean`, `nanmedian`, `nanstd`, `nanvar`, `nanquantile`, and `nanpercentile`
211
+ omit NaN samples. Their units, supported options, parameter normalization and
212
+ staged `out` writes match the corresponding functions above. Sample units,
213
+ including exact extra scales and scaled dimensionless units, stay unchanged;
214
+ `nanvar` squares the complete input unit.
215
+
216
+ `nanmean` and `nanmedian` also accept AffineQuantity and LogQuantity points,
217
+ returning points in the same coordinate. `nanstd` returns the coordinate's
218
+ full difference unit, and `nanvar` its square. Logarithmic point statistics
219
+ operate on stored readings; convert to physical quantities explicitly for
220
+ power or amplitude statistics. `nanquantile` and `nanpercentile` accept only
221
+ linear Quantities, following quantile and percentile.
222
+
223
+ `nanmean`, `nanstd`, and `nanvar` accept bare boolean `where` masks; NaN samples
224
+ are omitted from the selected elements. For `nanstd` and `nanvar`, N in
225
+ `N - ddof` is the selected non-NaN count. Precomputed `mean`, `correction` and
226
+ `initial` remain unsupported. `nanmedian`, `nanquantile` and `nanpercentile`
227
+ reject `overwrite_input=True`, and do not accept `where` or `dtype`.
228
+ Weighted NaN quantiles require `method="inverted_cdf"`; a NaN sample and its
229
+ associated weight are omitted together. Weights themselves are not cleaned.
230
+ Weighted NaN quantiles accept one integer axis or `axis=None`; tuple axes
231
+ are unsupported. For multidimensional samples, supply weights with the full
232
+ sample shape; reduced-axis weight shapes are rejected. NumPy 2.1 mishandles
233
+ weighted tuple axes, and NumPy 2.1 and 2.5's NaN kernels do not reliably handle
234
+ reduced-axis weights. Broadcast weights explicitly to the full sample shape
235
+ before calling. One-dimensional samples accept a matching weight vector as
236
+ usual. Unweighted NaN quantiles and ordinary quantile/percentile retain tuple
237
+ axis support; ordinary quantile/percentile also retain reduced-axis weights.
238
+
239
+ NumPy determines dtype, complex-sample support, Inf behavior, empty/all-NaN
240
+ results and RuntimeWarnings. unitx does not suppress these warnings. If a
241
+ warning is promoted to an exception, the staged output target stays unchanged.
242
+ Ordinary statistics continue to propagate NaNs.
243
+
244
+ ```python
245
+ gapped = Quantity(np.array([[1.0, np.nan, 3], [np.nan, 5, 7]]), 'm')
246
+ means = np.nanmean(gapped, axis=1)
247
+ assert means.unit == gapped.unit
248
+ np.testing.assert_allclose(means.value, [2, 6])
249
+ variances = np.nanvar(gapped, axis=1, ddof=1)
250
+ assert variances.unit == Unit('m2')
251
+ np.testing.assert_allclose(variances.value, [2, 2])
252
+ medians = np.nanpercentile(gapped, 50, axis=1)
253
+ np.testing.assert_allclose(medians.value, [2, 6])
254
+ totals = np.nansum(gapped, axis=1)
255
+ assert totals.unit == Unit('m')
256
+ np.testing.assert_array_equal(totals.value, [4, 12])
257
+ areas = np.nanprod(gapped, axis=1)
258
+ assert areas.unit == Unit('m2')
259
+ np.testing.assert_array_equal(areas.value, [3, 35])
260
+ minima = np.nanmin(gapped, axis=1)
261
+ assert minima.unit == Unit('m')
262
+ np.testing.assert_array_equal(minima.value, [1, 5])
263
+ np.testing.assert_array_equal(np.nanargmax(gapped, axis=1), [2, 2])
264
+ ```
265
+
266
+ See [NumPy nanmean](https://numpy.org/doc/stable/reference/generated/numpy.nanmean.html),
267
+ [nanstd](https://numpy.org/doc/stable/reference/generated/numpy.nanstd.html) and
268
+ [nanquantile](https://numpy.org/doc/stable/reference/generated/numpy.nanquantile.html)
269
+ for the numerical definitions and warnings.
270
+ The [nansum](https://numpy.org/doc/stable/reference/generated/numpy.nansum.html)
271
+ and [nanprod](https://numpy.org/doc/stable/reference/generated/numpy.nanprod.html)
272
+ kernels determine NaN identities and accumulator behavior, with unitx's
273
+ additional single-unit rules above.
274
+
191
275
  [NumPy overview and support matrix](../numpy.md) · [Documentation index](../README.md)
@@ -125,7 +125,7 @@ length target. `Unit` factors participate in the conversion. Every numeric
125
125
  result, including a coherent or scaled dimensionless result, requires a Quantity
126
126
  target; writing it to a bare ndarray would discard its unit. Boolean predicates
127
127
  and comparisons instead accept only a bare boolean ndarray output.
128
- `argmin` and `argmax` accept only a bare integer ndarray output, with the
128
+ `argmin`, `argmax`, `nanargmin` and `nanargmax` accept only a bare integer ndarray output, with the
129
129
  same exact shape requirement as other function outputs.
130
130
 
131
131
  Final casting uses `casting='same_kind'` by default, or the explicitly requested
@@ -116,18 +116,18 @@ functions, including exp/log. Python scalar numerical semantics remain native.
116
116
  | --- | --- | --- |
117
117
  | reshape, transpose | Preserve unit | shape/order/copy; axes |
118
118
  | concatenate, stack | Convert to first Quantity's unit; bare arrays remain dimensionless | axis, dtype, casting, out; dtype and out are mutually exclusive |
119
- | sum | Preserve unit | axis, dtype, keepdims, initial, where, out |
120
- | mean | Preserve unit | axis, dtype, keepdims, where, out |
121
- | min/amin, max/amax | Preserve unit or point coordinate | axis, keepdims, initial, where, out |
119
+ | sum, nansum | Preserve unit | axis, dtype, keepdims, initial, where, out |
120
+ | mean, nanmean | Preserve unit or point coordinate | axis, dtype, keepdims, where, out |
121
+ | min/amin/nanmin, max/amax/nanmax | Preserve unit or point coordinate | axis, keepdims, initial, where, out |
122
122
  | where | Select between compatible quantities or points | condition, x, y, all positional; no out |
123
123
  | clip | Preserve input unit or point coordinate | a_min/a_max or min/max, out; ufunc call options |
124
- | prod | Product unit rules below | axis, dtype, keepdims, initial, where, out |
125
- | median | Preserve unit | axis, keepdims, out; overwrite_input=False |
126
- | std, var | Preserve unit / square unit | axis, dtype, ddof, keepdims, where, out |
124
+ | prod, nanprod | Product unit rules below; nanprod counts selected non-NaN elements | axis, dtype, keepdims, initial, where, out |
125
+ | median, nanmedian | Preserve unit or point coordinate | axis, keepdims, out; overwrite_input=False |
126
+ | std/nanstd, var/nanvar | Difference unit / square difference unit | axis, dtype, ddof, keepdims, where, out |
127
127
  | average | Preserve sample unit; returned weight sum is a dimensionless Quantity | axis, weights, returned, keepdims; linear Quantity only |
128
- | quantile, percentile | Preserve sample unit; dimensionless q and weights | axis, out, method, keepdims, weights; overwrite_input=False; linear Quantity only |
128
+ | quantile, percentile, nanquantile, nanpercentile | Preserve sample unit; dimensionless q and weights | axis, out, method, keepdims, weights; overwrite_input=False; linear Quantity only |
129
129
  | cumsum | Preserve unit | axis, dtype, out |
130
- | argmin, argmax | Bare indices | axis, keepdims, out |
130
+ | argmin/nanargmin, argmax/nanargmax | Bare indices | axis, keepdims, out |
131
131
  | interp | Align x to xp's unit; retain fp's unit | finite strictly increasing 1-D xp; scalar left/right, period=None; no out |
132
132
  | trapezoid | Multiply ordinate and coordinate units | x, scalar dx, axis; no out |
133
133
  | diff | Preserve input unit | n, axis, prepend, append; no out |
@@ -138,8 +138,9 @@ Supply the first magnitude argument positionally; concatenate/stack take a
138
138
  sequence. Options follow each supported signature, including positional out
139
139
  checks. Function and reduction outputs follow the rules below;
140
140
  overwrite_input=True is rejected. Reduction/statistical `where` is supported
141
- for sum, mean, std, var, add.reduce, prod, multiply.reduce, and min/max; `initial` is
142
- supported for sum/add.reduce, prod/multiply.reduce, and min/max. Precomputed `mean`,
141
+ for sum/nansum, mean/nanmean, std/nanstd, var/nanvar, add.reduce, prod/nanprod,
142
+ multiply.reduce, and min/max/nanmin/nanmax; `initial` is supported for
143
+ sum/nansum/add.reduce, prod/nanprod/multiply.reduce, and min/max/nanmin/nanmax. Precomputed `mean`,
143
144
  `correction`, and other unsupported keywords raise TypeError.
144
145
  Indexed assignment and augmented arithmetic on ndarrays follow the fixed-target
145
146
  rules below. Python and NumPy scalar magnitudes retain ordinary result/rebinding
@@ -149,6 +150,7 @@ behavior for augmented arithmetic.
149
150
 
150
151
  See [selection and clipping](numpy/selection.md) for `where`, `clip`,
151
152
  `minimum/maximum`, and `min/max`, including coordinate and bound rules.
153
+ The NaN extrema and index functions share those coordinate rules and omit NaNs.
152
154
 
153
155
  ## Indexed assignment and augmented arithmetic
154
156
 
@@ -164,6 +166,8 @@ See [integration, differences and gradients](numpy/calculus.md) for
164
166
 
165
167
  See [reductions and statistics](numpy/statistics.md#masked-sums-and-statistics)
166
168
  for sum, mean, standard deviation, variance, initial values and input masks.
169
+ The [NaN statistics](numpy/statistics.md#nan-statistics) omit NaN samples using
170
+ the same unit and output rules as their ordinary counterparts.
167
171
 
168
172
  ## Product reductions
169
173
 
@@ -95,7 +95,8 @@ numerical boundary, use `np.expm1(np.asarray(q.value_in(si.one)))` after
95
95
  checking dimensions and normalizing the scale.
96
96
 
97
97
  The same NumPy stub limitation applies to `where`, `clip`,
98
- `minimum/maximum`, `average`, `quantile`, `percentile`, and `interp`: runtime results
98
+ `minimum/maximum`, `average`, `quantile`, `percentile`, the supported `nan*`
99
+ statistics, and `interp`: runtime results
99
100
  preserve the documented Quantity/point wrappers, but NumPy may annotate them
100
101
  as ndarrays or Any. NumPy's `out` annotations also accept only ndarrays.
101
102
  Runtime support and installed-consumer checks cover
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'unitx'
7
- version = '0.16.0'
7
+ version = '0.16.2'
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'
@@ -403,7 +403,7 @@ def _dispatch_ufunc(
403
403
  }:
404
404
  return NotImplemented
405
405
  handler = _sum if ufunc is np.add else _prod
406
- return handler(inputs[0], **{'axis': 0, **kwargs})
406
+ return handler(ufunc.reduce, inputs[0], **{'axis': 0, **kwargs})
407
407
  if method == 'accumulate' and ufunc is np.add:
408
408
  if kwargs.keys() - {'axis', 'dtype'}:
409
409
  return NotImplemented
@@ -606,14 +606,22 @@ def dispatch_function(
606
606
  np.transpose,
607
607
  np.argmin,
608
608
  np.argmax,
609
+ np.nanargmin,
610
+ np.nanargmax,
609
611
  np.median,
610
612
  np.mean,
611
613
  np.std,
612
614
  np.var,
615
+ np.nanmedian,
616
+ np.nanmean,
617
+ np.nanstd,
618
+ np.nanvar,
613
619
  np.min,
614
620
  np.max,
615
621
  np.amin,
616
622
  np.amax,
623
+ np.nanmin,
624
+ np.nanmax,
617
625
  ):
618
626
  raise TypeError(
619
627
  'This NumPy function is undefined for affine points; convert explicitly.'
@@ -854,11 +862,17 @@ def _argmin_max(
854
862
 
855
863
 
856
864
  def _median(
857
- q: Any, axis: Any = None, out: Any = None, overwrite_input: bool = False, keepdims: bool = False
865
+ func: Any,
866
+ q: Any,
867
+ axis: Any = None,
868
+ out: Any = None,
869
+ overwrite_input: bool = False,
870
+ keepdims: bool = False,
858
871
  ) -> Any:
859
872
  if overwrite_input:
860
- raise TypeError('overwrite_input is not supported for Quantity.')
861
- return _write_output(_wrap(np.median(q.value, axis=axis, keepdims=keepdims), q.unit), out)
873
+ error_message = 'overwrite_input is not supported for Quantity.'
874
+ raise TypeError(error_message)
875
+ return _write_output(_wrap(func(q.value, axis=axis, keepdims=keepdims), q.unit), out)
862
876
 
863
877
 
864
878
  def _cumsum(q: Any, axis: Any = None, dtype: Any = None, out: Any = None) -> Any:
@@ -867,6 +881,7 @@ def _cumsum(q: Any, axis: Any = None, dtype: Any = None, out: Any = None) -> Any
867
881
 
868
882
 
869
883
  def _mean(
884
+ func: Any,
870
885
  q: Any,
871
886
  axis: Any = None,
872
887
  dtype: Any = None,
@@ -877,7 +892,7 @@ def _mean(
877
892
  ) -> Any:
878
893
  _validate_dtype(dtype)
879
894
  _validate_where({'where': where})
880
- result = _wrap(np.mean(q.value, axis=axis, dtype=dtype, keepdims=keepdims, where=where), q.unit)
895
+ result = _wrap(func(q.value, axis=axis, dtype=dtype, keepdims=keepdims, where=where), q.unit)
881
896
  return _write_output(result, out)
882
897
 
883
898
 
@@ -931,15 +946,26 @@ def _quantile(
931
946
  error_message = 'overwrite_input is not supported for Quantity.'
932
947
  raise TypeError(error_message)
933
948
  q = _statistic_parameter(q, 'q')
949
+ value = _numeric_value(a.value)
934
950
  if weights is not None:
935
951
  weights = _statistic_parameter(weights, 'weights')
936
- result = func(
937
- _numeric_value(a.value), q, axis=axis, method=method, keepdims=keepdims, weights=weights
938
- )
952
+ if func in (np.nanquantile, np.nanpercentile):
953
+ # NumPy 2.1 mishandles tuple axes with weights; its NaN kernels
954
+ # also index reduced-axis weights without broadcasting in 2.1/2.5.
955
+ if isinstance(axis, tuple):
956
+ error_message = 'Weighted NaN quantiles require one integer axis or axis=None.'
957
+ raise TypeError(error_message)
958
+ if np.ndim(value) > 1 and axis is not None and np.shape(weights) != np.shape(value):
959
+ error_message = (
960
+ 'NaN quantiles require full-shape weights for multidimensional samples.'
961
+ )
962
+ raise ValueError(error_message)
963
+ result = func(value, q, axis=axis, method=method, keepdims=keepdims, weights=weights)
939
964
  return _write_output(Quantity(result, a.unit), out)
940
965
 
941
966
 
942
967
  def _sum(
968
+ func: Any,
943
969
  q: Any,
944
970
  axis: Any = None,
945
971
  dtype: Any = None,
@@ -954,33 +980,34 @@ def _sum(
954
980
  initial_value = None
955
981
  if initial is not None:
956
982
  if not _is_supported_operand(initial):
957
- raise TypeError('initial must be a numeric scalar or scalar Quantity.')
983
+ error_message = 'initial must be a numeric scalar or scalar Quantity.'
984
+ raise TypeError(error_message)
958
985
  initial_value, initial_unit = _linear_parts(initial)
959
986
  if np.ndim(initial_value) != 0:
960
- raise TypeError('initial must be a numeric scalar or scalar Quantity.')
987
+ error_message = 'initial must be a numeric scalar or scalar Quantity.'
988
+ raise TypeError(error_message)
961
989
  if not compatible(initial_unit, unit):
962
990
  # Bare zero is the additive identity in any dimension. A Quantity
963
991
  # zero still has its declared dimension and must be compatible.
964
992
  if isinstance(initial, (Quantity, _Point)) or initial != 0:
965
- raise DimensionError('initial has an incompatible dimension.')
993
+ error_message = 'initial has an incompatible dimension.'
994
+ raise DimensionError(error_message)
966
995
  else:
967
996
  initial_value = scale_magnitude(initial_value, initial_unit.factor / unit.factor)
968
997
  result = _wrap(
969
- np.add.reduce(
970
- value, axis=axis, dtype=dtype, keepdims=keepdims, initial=initial_value, where=where
971
- ),
998
+ func(value, axis=axis, dtype=dtype, keepdims=keepdims, initial=initial_value, where=where),
972
999
  unit,
973
1000
  )
974
1001
  return _write_output(result, out)
975
1002
 
976
1003
 
977
1004
  def _prod(
1005
+ func: Any,
978
1006
  q: Any,
979
1007
  axis: Any = None,
980
1008
  dtype: Any = None,
981
1009
  out: Any = None,
982
1010
  keepdims: bool = False,
983
- *,
984
1011
  initial: Any = 1,
985
1012
  where: Any = True,
986
1013
  ) -> Any:
@@ -989,10 +1016,12 @@ def _prod(
989
1016
  initial_value, initial_unit = None, Unit()
990
1017
  if initial is not None:
991
1018
  if not _is_supported_operand(initial):
992
- raise TypeError('initial must be a numeric scalar or scalar Quantity.')
1019
+ error_message = 'initial must be a numeric scalar or scalar Quantity.'
1020
+ raise TypeError(error_message)
993
1021
  initial_value, initial_unit = _linear_parts(initial)
994
1022
  if np.ndim(initial_value) != 0:
995
- raise TypeError('initial must be a numeric scalar or scalar Quantity.')
1023
+ error_message = 'initial must be a numeric scalar or scalar Quantity.'
1024
+ raise TypeError(error_message)
996
1025
 
997
1026
  value, unit = _linear_parts(q)
998
1027
  mask = np.broadcast_to(where, np.shape(value))
@@ -1000,7 +1029,7 @@ def _prod(
1000
1029
  if unit.dimension == DIMENSIONLESS:
1001
1030
  value = scale_magnitude(value, unit.factor, where=mask)
1002
1031
  unit = Unit()
1003
- result = np.multiply.reduce(
1032
+ result = func(
1004
1033
  value, axis=axis, dtype=dtype, keepdims=keepdims, initial=initial_value, where=where
1005
1034
  )
1006
1035
  if unit.dimension != DIMENSIONLESS:
@@ -1008,16 +1037,17 @@ def _prod(
1008
1037
  axes = range(len(shape)) if axis is None else axis if isinstance(axis, tuple) else (axis,)
1009
1038
  # NumPy has already validated axes, including its scalar 0/-1 special case.
1010
1039
  count = math.prod(shape[i] for i in axes) if shape else 1
1040
+ if func is np.nanprod:
1041
+ mask = mask & ~np.isnan(value)
1011
1042
  if np.ndim(where) == 0:
1012
1043
  count = count if bool(where) else 0
1013
- else:
1044
+ if np.ndim(where) != 0 or func is np.nanprod:
1014
1045
  counts = np.sum(mask, axis=axis)
1015
1046
  if np.size(counts):
1016
1047
  count = int(np.asarray(counts).flat[0])
1017
1048
  if np.any(counts != count):
1018
- raise DimensionError(
1019
- 'Product reductions require equal element counts per result.'
1020
- )
1049
+ error_message = 'Product reductions require equal element counts per result.'
1050
+ raise DimensionError(error_message)
1021
1051
  # With no output elements, retain the nominal reduction extent.
1022
1052
  unit = unit**count
1023
1053
  assert isinstance(unit, Unit) and isinstance(initial_unit, Unit)
@@ -1038,7 +1068,7 @@ def _std_var(
1038
1068
  _validate_dtype(dtype)
1039
1069
  _validate_where({'where': where})
1040
1070
  delta = q.unit.delta_unit if isinstance(q.unit, _Coordinate) else q.unit
1041
- unit = delta**2 if func is np.var else delta
1071
+ unit = delta**2 if func in (np.var, np.nanvar) else delta
1042
1072
  result = _wrap(
1043
1073
  func(q.value, axis=axis, dtype=dtype, ddof=ddof, keepdims=keepdims, where=where), unit
1044
1074
  )
@@ -1170,7 +1200,6 @@ def _extremum(
1170
1200
  axis: Any = None,
1171
1201
  out: Any = None,
1172
1202
  keepdims: bool = False,
1173
- *,
1174
1203
  initial: Any = _UNSET,
1175
1204
  where: Any = True,
1176
1205
  ) -> Any:
@@ -1189,18 +1218,30 @@ _QUANTITY_FUNCTIONS = {
1189
1218
  np.transpose: _transpose,
1190
1219
  np.argmin: partial(_argmin_max, np.argmin),
1191
1220
  np.argmax: partial(_argmin_max, np.argmax),
1192
- np.median: _median,
1221
+ np.nanargmin: partial(_argmin_max, np.nanargmin),
1222
+ np.nanargmax: partial(_argmin_max, np.nanargmax),
1223
+ np.median: partial(_median, np.median),
1224
+ np.nanmedian: partial(_median, np.nanmedian),
1193
1225
  np.cumsum: _cumsum,
1194
- np.prod: _prod,
1226
+ np.prod: partial(_prod, np.multiply.reduce),
1227
+ np.nanprod: partial(_prod, np.nanprod),
1195
1228
  np.std: partial(_std_var, np.std),
1196
1229
  np.var: partial(_std_var, np.var),
1197
- np.sum: _sum,
1198
- np.mean: _mean,
1230
+ np.nanstd: partial(_std_var, np.nanstd),
1231
+ np.nanvar: partial(_std_var, np.nanvar),
1232
+ np.sum: partial(_sum, np.add.reduce),
1233
+ np.nansum: partial(_sum, np.nansum),
1234
+ np.mean: partial(_mean, np.mean),
1235
+ np.nanmean: partial(_mean, np.nanmean),
1199
1236
  np.average: _average,
1200
1237
  np.quantile: partial(_quantile, np.quantile),
1201
1238
  np.percentile: partial(_quantile, np.percentile),
1239
+ np.nanquantile: partial(_quantile, np.nanquantile),
1240
+ np.nanpercentile: partial(_quantile, np.nanpercentile),
1202
1241
  np.min: partial(_extremum, np.min),
1203
1242
  np.max: partial(_extremum, np.max),
1204
1243
  np.amin: partial(_extremum, np.amin),
1205
1244
  np.amax: partial(_extremum, np.amax),
1245
+ np.nanmin: partial(_extremum, np.nanmin),
1246
+ np.nanmax: partial(_extremum, np.nanmax),
1206
1247
  }
@@ -119,17 +119,27 @@ def output_contracts(a: npt.NDArray[np.float64]) -> None:
119
119
  scalar_target = Quantity(np.empty(()), 'cm')
120
120
  q.__array_ufunc__(np.add, 'reduce', q, out=(scalar_target,))
121
121
  q.__array_function__(np.sum, (Quantity,), (q,), {'out': scalar_target})
122
- for operation in (np.quantile, np.percentile):
122
+ for operation in (np.quantile, np.percentile, np.nanquantile, np.nanpercentile):
123
123
  q.__array_function__(operation, (Quantity,), (q, 0.5), {'out': scalar_target})
124
124
  mask = np.array([True, False, True])
125
125
  q.__array_ufunc__(
126
126
  np.add, 'reduce', q, out=(scalar_target,), where=mask, initial=Quantity(2, 'cm')
127
127
  )
128
- for operation in (np.sum, np.mean, np.std):
128
+ for operation in (np.sum, np.nansum, np.mean, np.std, np.nanmean, np.nanstd):
129
129
  q.__array_function__(operation, (Quantity,), (q,), {'where': mask, 'out': scalar_target})
130
- q.__array_function__(
131
- np.var, (Quantity,), (q,), {'where': mask, 'out': Quantity(np.empty(()), 'm2')}
132
- )
130
+ for operation in (np.nanmin, np.nanmax):
131
+ q.__array_function__(
132
+ operation, (Quantity,), (q,), {'initial': Quantity(0, 'cm'), 'out': scalar_target}
133
+ )
134
+ for operation in (np.nanargmin, np.nanargmax):
135
+ indices = np.empty((), dtype=np.intp)
136
+ q.__array_function__(operation, (Quantity,), (q,), {'out': indices})
137
+ assert_type(indices.dtype, np.dtype[np.intp])
138
+ q.__array_function__(np.nanmedian, (Quantity,), (q,), {'out': scalar_target})
139
+ for operation in (np.var, np.nanvar):
140
+ q.__array_function__(
141
+ operation, (Quantity,), (q,), {'where': mask, 'out': Quantity(np.empty(()), 'm2')}
142
+ )
133
143
  q.__array_function__(np.diff, (Quantity,), (q,), {'prepend': Quantity(2, 'cm')})
134
144
  q.__array_function__(np.gradient, (Quantity,), (q, Quantity(2, 's')), {'axis': 0})
135
145
  product_target = Quantity(np.empty(()), 'm2')
@@ -142,6 +152,7 @@ def output_contracts(a: npt.NDArray[np.float64]) -> None:
142
152
  (q,),
143
153
  {'initial': Quantity(2, 's'), 'out': Quantity(np.empty(()), 'm2 s')},
144
154
  )
155
+ q.__array_function__(np.nanprod, (Quantity,), (q,), {'where': mask, 'out': product_target})
145
156
  assert_type(product_target.value.dtype, np.dtype[np.float64])
146
157
  assert_type(scalar_target.value.dtype, np.dtype[np.float64])
147
158
  assert_type(widened.value, Array)
@@ -71,8 +71,10 @@ def test_numpy_add_sub_and_comparison() -> None:
71
71
  'func',
72
72
  [
73
73
  np.sum,
74
+ np.nansum,
74
75
  np.cumsum,
75
76
  np.prod,
77
+ np.nanprod,
76
78
  np.exp,
77
79
  np.log,
78
80
  np.sin,