unitforge 0.5.12__tar.gz → 0.5.13__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 (119) hide show
  1. {unitforge-0.5.12 → unitforge-0.5.13}/Cargo.lock +2 -2
  2. {unitforge-0.5.12 → unitforge-0.5.13}/Cargo.toml +2 -2
  3. {unitforge-0.5.12 → unitforge-0.5.13}/PKG-INFO +17 -1
  4. {unitforge-0.5.12 → unitforge-0.5.13}/Readme.md +11 -0
  5. {unitforge-0.5.12 → unitforge-0.5.13}/Readme_py.md +14 -0
  6. {unitforge-0.5.12 → unitforge-0.5.13}/install_hooks.sh +0 -0
  7. {unitforge-0.5.12 → unitforge-0.5.13}/src/impl_macros.rs +135 -176
  8. unitforge-0.5.13/src/internal.rs +684 -0
  9. {unitforge-0.5.12 → unitforge-0.5.13}/src/prelude.rs +2 -20
  10. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/acceleration.rs +2 -2
  11. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/angle.rs +3 -2
  12. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/area.rs +3 -3
  13. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/density.rs +4 -4
  14. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/distance.rs +12 -12
  15. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force.rs +2 -2
  16. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_distance.rs +2 -2
  17. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/inverse_distance.rs +4 -4
  18. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/mass.rs +6 -6
  19. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/rotational_stiffness.rs +4 -2
  20. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress.rs +1 -1
  21. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress_rate.rs +1 -1
  22. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/time.rs +11 -2
  23. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/velocity.rs +9 -2
  24. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/volume.rs +3 -3
  25. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities.template.rs +20 -3
  26. {unitforge-0.5.12 → unitforge-0.5.13}/src/relations.rs +0 -1
  27. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/bindings/matrix3_py.template.rs +31 -17
  28. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/bindings/vector3_py.template.rs +46 -58
  29. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/matrix2.rs +8 -11
  30. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/matrix2x3.rs +6 -4
  31. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/matrix3.rs +8 -10
  32. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/matrix3x2.rs +6 -3
  33. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/vector2.rs +4 -3
  34. {unitforge-0.5.12 → unitforge-0.5.13}/src/small_linalg/vector3.rs +5 -5
  35. {unitforge-0.5.12 → unitforge-0.5.13}/test.sh +0 -0
  36. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/constants.rs +2 -2
  37. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/construction.rs +38 -0
  38. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/normalization.rs +73 -2
  39. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/runtime_quantity.rs +46 -0
  40. unitforge-0.5.13/tests/quantity_core/scalar_edges.rs +126 -0
  41. unitforge-0.5.13/tests/quantity_core/unit_factors.rs +125 -0
  42. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core.rs +2 -0
  43. {unitforge-0.5.12 → unitforge-0.5.13}/tests/small_linalg_edge_cases.rs +75 -0
  44. {unitforge-0.5.12 → unitforge-0.5.13}/tests/test_quantities.py +26 -0
  45. {unitforge-0.5.12 → unitforge-0.5.13}/tests/test_small_linalg.py +50 -5
  46. {unitforge-0.5.12 → unitforge-0.5.13}/unitforge_macros/Cargo.toml +1 -1
  47. {unitforge-0.5.12 → unitforge-0.5.13}/unitforge_macros/src/lib.rs +108 -16
  48. unitforge-0.5.12/python/unitforge/__init__.pyi +0 -3548
  49. unitforge-0.5.12/python/unitforge/py.typed +0 -0
  50. unitforge-0.5.12/src/internal.rs +0 -331
  51. unitforge-0.5.12/tests/quantity_core/scalar_edges.rs +0 -46
  52. {unitforge-0.5.12 → unitforge-0.5.13}/.gitignore +0 -0
  53. {unitforge-0.5.12 → unitforge-0.5.13}/.gitlab-ci.yml +0 -0
  54. {unitforge-0.5.12 → unitforge-0.5.13}/pyproject.toml +0 -0
  55. {unitforge-0.5.12 → unitforge-0.5.13}/python/unitforge/__init__.py +0 -0
  56. {unitforge-0.5.12 → unitforge-0.5.13}/scripts/generate_python_stubs.py +0 -0
  57. {unitforge-0.5.12 → unitforge-0.5.13}/src/errors.rs +0 -0
  58. {unitforge-0.5.12 → unitforge-0.5.13}/src/lib.rs +0 -0
  59. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/angular_acceleration.rs +0 -0
  60. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/angular_velocity.rs +0 -0
  61. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/area_of_moment.rs +0 -0
  62. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/charge.rs +0 -0
  63. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/compliance.rs +0 -0
  64. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/density_rate.rs +0 -0
  65. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/density_temperature.rs +0 -0
  66. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/density_temperature_rate.rs +0 -0
  67. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_area.rs +0 -0
  68. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_div_distance_power_four.rs +0 -0
  69. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_per_volume.rs +0 -0
  70. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_stress.rs +0 -0
  71. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/force_volume.rs +0 -0
  72. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/frequency.rs +0 -0
  73. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/heat_capacity.rs +0 -0
  74. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/heat_flux.rs +0 -0
  75. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/inverse_angle.rs +0 -0
  76. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/inverse_area.rs +0 -0
  77. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/inverse_stress.rs +0 -0
  78. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/inverse_temperature.rs +0 -0
  79. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/mass_per_distance.rs +0 -0
  80. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/molar_mass.rs +0 -0
  81. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/power.rs +0 -0
  82. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/pressure_rate.rs +0 -0
  83. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/specific_energy.rs +0 -0
  84. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/specific_gas_constant.rs +0 -0
  85. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/specific_heat_capacity.rs +0 -0
  86. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stiffness.rs +0 -0
  87. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/strain.rs +0 -0
  88. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/strain_squared.rs +0 -0
  89. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress_cubed.rs +0 -0
  90. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress_per_temperature.rs +0 -0
  91. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress_rate_per_temperature.rs +0 -0
  92. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/stress_squared.rs +0 -0
  93. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/temperature.rs +0 -0
  94. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/temperature_gradient.rs +0 -0
  95. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/thermal_conductance.rs +0 -0
  96. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/thermal_conductivity.rs +0 -0
  97. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/universal_gas_constant.rs +0 -0
  98. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/velocity_squared.rs +0 -0
  99. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/voltage.rs +0 -0
  100. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/volumetric_energy_density.rs +0 -0
  101. {unitforge-0.5.12 → unitforge-0.5.13}/src/quantities/volumetric_heat_capacity.rs +0 -0
  102. {unitforge-0.5.12 → unitforge-0.5.13}/tests/common/mod.rs +0 -0
  103. {unitforge-0.5.12 → unitforge-0.5.13}/tests/matrix2.rs +0 -0
  104. {unitforge-0.5.12 → unitforge-0.5.13}/tests/matrix2x3.rs +0 -0
  105. {unitforge-0.5.12 → unitforge-0.5.13}/tests/matrix3.rs +0 -0
  106. {unitforge-0.5.12 → unitforge-0.5.13}/tests/matrix3_eigen.rs +0 -0
  107. {unitforge-0.5.12 → unitforge-0.5.13}/tests/matrix3x2.rs +0 -0
  108. {unitforge-0.5.12 → unitforge-0.5.13}/tests/ndarray_dot.rs +0 -0
  109. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantities.rs +0 -0
  110. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/enum_iter.rs +0 -0
  111. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/metadata.rs +0 -0
  112. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/plain_storage.rs +0 -0
  113. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_core/relations.rs +0 -0
  114. {unitforge-0.5.12 → unitforge-0.5.13}/tests/quantity_serde.rs +0 -0
  115. {unitforge-0.5.12 → unitforge-0.5.13}/tests/small_linalg_interfaces.rs +0 -0
  116. {unitforge-0.5.12 → unitforge-0.5.13}/tests/thermal_transient.rs +0 -0
  117. {unitforge-0.5.12 → unitforge-0.5.13}/tests/typecheck/mypy_smoke.py +0 -0
  118. {unitforge-0.5.12 → unitforge-0.5.13}/tests/vector2.rs +0 -0
  119. {unitforge-0.5.12 → unitforge-0.5.13}/tests/vector3.rs +0 -0
@@ -351,7 +351,7 @@ checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
351
351
 
352
352
  [[package]]
353
353
  name = "unitforge"
354
- version = "0.5.12"
354
+ version = "0.5.13"
355
355
  dependencies = [
356
356
  "ndarray",
357
357
  "num-traits",
@@ -367,7 +367,7 @@ dependencies = [
367
367
 
368
368
  [[package]]
369
369
  name = "unitforge_macros"
370
- version = "0.5.12"
370
+ version = "0.5.13"
371
371
  dependencies = [
372
372
  "regex",
373
373
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "unitforge"
3
- version = "0.5.12"
3
+ version = "0.5.13"
4
4
  edition = "2021"
5
5
  authors = ["Dr.Q team <info@askdrq.com>", "Henrik Stromberg <henrik@askdrq.com>"]
6
6
  description = "A library for unit and quantity consistent computations in Rust"
@@ -20,7 +20,7 @@ pyo3 = { version = "0.28.3", features = ["extension-module", "auto-initialize",
20
20
  numpy = { version = "0.28.0", optional = true }
21
21
  strum = { version = "0.28.0", optional = true }
22
22
  strum_macros = { version = "0.28.0", optional = true }
23
- unitforge_macros = { version = "0.5.12", path = "unitforge_macros" }
23
+ unitforge_macros = { version = "0.5.13", path = "unitforge_macros" }
24
24
 
25
25
  [features]
26
26
  default = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitforge
3
- Version: 0.5.12
3
+ Version: 0.5.13
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Rust
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -9,7 +9,9 @@ Classifier: Topic :: Scientific/Engineering :: Physics
9
9
  Requires-Dist: numpy>=2.2.0
10
10
  Summary: A library for unit and quantity consistent computations in Rust
11
11
  Keywords: units,quantities,simulation,physics
12
+ Author: Dr.Q team <info@askdrq.com>, Henrik Stromberg <henrik@askdrq.com>
12
13
  Author-email: "Dr.Q team" <info@askdrq.com>, Henrik Stromberg <henrik@askdrq.com>
14
+ License: MIT
13
15
  Requires-Python: >=3.7
14
16
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
17
  Project-URL: Repository, https://gitlab.com/henrikjstromberg/unitforge
@@ -72,6 +74,20 @@ f_sum = f1 + f2
72
74
  print(f_sum.to(ForceUnit.N)) # → 8.0
73
75
  ```
74
76
 
77
+ ### Comparing Quantities
78
+
79
+ `==` is tolerance-based: two quantities are equal when they differ by at most `1e-9` of the larger
80
+ magnitude. An infinity equals only itself, and NaN equals nothing. `<`, `<=`, `>` and `>=` compare
81
+ exactly, so for nearly equal values `a == b` and `a < b` can both be true. For explicit tolerances
82
+ use `a.close_abs(b, threshold)` (absolute, `threshold` is a quantity) or
83
+ `a.close_rel(b, threshold)` (relative to the mean magnitude). Quantities are not hashable.
84
+
85
+ ```python
86
+ a = Force(1.0, ForceUnit.N)
87
+ b = Force(1.0 + 1e-12, ForceUnit.N)
88
+ print(a == b, a < b) # → True True
89
+ ```
90
+
75
91
  ---
76
92
 
77
93
  ## Vector and Matrix Support
@@ -15,6 +15,17 @@
15
15
  - **Serialization**: Optional support for serde using `features = ["serde"]`; combine with `ndarray` for ndarray serialization support.
16
16
  - **python interface** Optional Python interface when building with flag pyo3; also available on pypi
17
17
 
18
+ ## Comparing quantities
19
+
20
+ `==` is tolerance-based: two quantities are equal when they differ by at most `1e-9` of the larger
21
+ magnitude. An infinity equals only itself, and NaN equals nothing. Ordering (`<`, `<=`, `>`, `>=`)
22
+ compares the stored values exactly. As a consequence:
23
+
24
+ - `==` is not transitive, and for nearly equal values `a == b` and `a < b` can both be true.
25
+ - `Hash` hashes the exact stored value, so quantities that compare equal can hash differently. Do
26
+ not rely on hash-based collections to merge nearly equal quantities.
27
+ - For an explicit, absolute tolerance use `a.is_close(&b, &tolerance)`.
28
+
18
29
  ## Contribute
19
30
 
20
31
  All contributions are welcome! Feel free to implement new quantities or define relations using `impl_macros.rs`. 🚀
@@ -56,6 +56,20 @@ f_sum = f1 + f2
56
56
  print(f_sum.to(ForceUnit.N)) # → 8.0
57
57
  ```
58
58
 
59
+ ### Comparing Quantities
60
+
61
+ `==` is tolerance-based: two quantities are equal when they differ by at most `1e-9` of the larger
62
+ magnitude. An infinity equals only itself, and NaN equals nothing. `<`, `<=`, `>` and `>=` compare
63
+ exactly, so for nearly equal values `a == b` and `a < b` can both be true. For explicit tolerances
64
+ use `a.close_abs(b, threshold)` (absolute, `threshold` is a quantity) or
65
+ `a.close_rel(b, threshold)` (relative to the mean magnitude). Quantities are not hashable.
66
+
67
+ ```python
68
+ a = Force(1.0, ForceUnit.N)
69
+ b = Force(1.0 + 1e-12, ForceUnit.N)
70
+ print(a == b, a < b) # → True True
71
+ ```
72
+
59
73
  ---
60
74
 
61
75
  ## Vector and Matrix Support
File without changes
@@ -56,6 +56,9 @@ macro_rules! impl_quantity {
56
56
  pub(crate) kind_id: u32,
57
57
  }
58
58
 
59
+ // Hashes the stored value: canonical parts (which every constructor produces
60
+ // deterministically) or the plain float with `-0.0` folded into `0.0`. `==` allows a
61
+ // relative tolerance, so values that are merely close can still hash differently.
59
62
  impl core::hash::Hash for $type {
60
63
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
61
64
  fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
@@ -64,11 +67,26 @@ macro_rules! impl_quantity {
64
67
  }
65
68
  #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
66
69
  fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
67
- core::hash::Hash::hash(&self.value.to_bits(), state);
70
+ core::hash::Hash::hash(
71
+ &$crate::internal::canonical_float_bits(f64::from(self.value)),
72
+ state,
73
+ );
68
74
  }
69
75
  }
70
76
 
71
77
  impl $type {
78
+ /// Whether both hold exactly the same value (never true for NaN).
79
+ fn same_value(&self, other: &Self) -> bool {
80
+ #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
81
+ {
82
+ self.multiplier == other.multiplier && self.power == other.power
83
+ }
84
+ #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
85
+ {
86
+ self.value == other.value
87
+ }
88
+ }
89
+
72
90
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
73
91
  fn from_parts(multiplier: f64, power: i32) -> Self {
74
92
  let (multiplier, power) =
@@ -82,8 +100,7 @@ macro_rules! impl_quantity {
82
100
 
83
101
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
84
102
  pub(crate) fn from_base_value(value: f64) -> Self {
85
- let (multiplier, power) = <Self as $crate::PhysicsQuantity>::split_value(value);
86
- Self::from_parts(multiplier, power)
103
+ Self::from_parts(value, 0)
87
104
  }
88
105
 
89
106
  #[cfg(feature = "storage-f64")]
@@ -160,8 +177,9 @@ macro_rules! impl_quantity {
160
177
  );
161
178
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
162
179
  {
180
+ // Same normalization as `from_raw`, so both agree bit for bit.
163
181
  let (multiplier, power) =
164
- $crate::internal::value_to_quantity_parts_const(value);
182
+ $crate::internal::canonicalize_quantity_parts_const(value, 0);
165
183
  let (multiplier, power) = $crate::internal::canonicalize_quantity_parts_const(
166
184
  multiplier * unit_multiplier,
167
185
  $crate::internal::saturating_power_add_const(power, unit_power),
@@ -246,10 +264,7 @@ macro_rules! impl_quantity {
246
264
  fn as_f64(&self) -> f64 {
247
265
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
248
266
  {
249
- if self.multiplier == 0.0 {
250
- return 0.0;
251
- }
252
- self.multiplier * 10_f64.powi(self.power)
267
+ $crate::internal::quantity_parts_to_value(self.multiplier, self.power)
253
268
  }
254
269
  #[cfg(feature = "storage-f64")]
255
270
  {
@@ -269,29 +284,12 @@ macro_rules! impl_quantity {
269
284
 
270
285
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
271
286
  fn split_value(v: f64) -> (f64, i32) {
272
- if v.is_zero() {
273
- (0.0, 0)
274
- } else if v.is_nan() {
275
- (f64::NAN, 0)
276
- } else if v.is_infinite() {
277
- if v > 0.0 {
278
- (f64::INFINITY, 0)
279
- } else {
280
- (f64::NEG_INFINITY, 0)
281
- }
282
- } else {
283
- let power = v.abs().log10().floor() as i32;
284
- let multiplier = v / 10f64.powi(power);
285
- (multiplier, power)
286
- }
287
+ $crate::internal::split_decimal(v)
287
288
  }
288
289
 
289
290
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
290
291
  fn get_value(&self) -> f64 {
291
- if self.multiplier == 0.0 {
292
- return 0.0;
293
- }
294
- self.multiplier * 10_f64.powi(self.power)
292
+ self.as_f64()
295
293
  }
296
294
 
297
295
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
@@ -314,12 +312,11 @@ macro_rules! impl_quantity {
314
312
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
315
313
  {
316
314
  if offset == 0.0 {
317
- if self.multiplier == 0.0 {
318
- return 0.0;
319
- }
320
315
  let (unit_multiplier, unit_power) = unit.base_per_x();
321
- return self.multiplier / unit_multiplier
322
- * $crate::internal::pow10_delta(self.power, unit_power);
316
+ return $crate::internal::quantity_parts_to_value(
317
+ self.multiplier / unit_multiplier,
318
+ $crate::internal::saturating_power_sub(self.power, unit_power),
319
+ );
323
320
  }
324
321
  // Affine units (non-zero offset) go through `as_f64()`
325
322
  // instead of the mantissa/power fast path above, since
@@ -394,7 +391,8 @@ macro_rules! impl_quantity {
394
391
  }
395
392
 
396
393
  fn is_close(&self, other: &Self, tolerance: &Self) -> bool {
397
- (self.as_f64() - other.as_f64()).abs() <= tolerance.as_f64().abs()
394
+ // Identical values, equal infinities included, are always close.
395
+ self.same_value(other) || (*self - *other).abs() <= tolerance.abs()
398
396
  }
399
397
 
400
398
  fn optimize(&mut self) {
@@ -540,24 +538,18 @@ macro_rules! impl_quantity {
540
538
  }
541
539
  #[cfg(not(feature = "no_std"))]
542
540
  {
543
- use std::collections::HashMap;
544
- let mut groups: HashMap<String, Vec<String>> = HashMap::new();
545
-
541
+ // Units showing the same rounded value share an entry, in display-unit order.
542
+ let mut groups: Vec<(String, Vec<String>)> = Vec::new();
546
543
  for base_unit in $display_units {
547
- let value = self.to(base_unit);
548
- let rounded = if value.is_zero() {
549
- value
550
- } else {
551
- let digits = value.abs().log10().ceil() as i32;
552
- let rounding_multiplier = 10_f64.powi(3 - digits);
553
- (value * rounding_multiplier).round() / rounding_multiplier
554
- };
555
-
556
- let value = format!("{}", rounded);
557
- groups
558
- .entry(value)
559
- .or_default()
560
- .push(base_unit.name().to_string());
544
+ let value = format!(
545
+ "{}",
546
+ $crate::internal::round_to_significant_digits(self.to(base_unit), 3)
547
+ );
548
+ let name = base_unit.name().to_string();
549
+ match groups.iter_mut().find(|(existing, _)| *existing == value) {
550
+ Some((_, units)) => units.push(name),
551
+ None => groups.push((value, vec![name])),
552
+ }
561
553
  }
562
554
 
563
555
  let mut parts = Vec::new();
@@ -599,30 +591,45 @@ macro_rules! impl_quantity {
599
591
  where
600
592
  $type: PhysicsQuantity,
601
593
  {
594
+ /// Equal up to `RELATIVE_EQUALITY_TOLERANCE` (1e-9) of the larger magnitude.
595
+ /// Symmetric; an infinity equals only itself and NaN equals nothing.
602
596
  fn eq(&self, other: &Self) -> bool {
603
597
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
604
598
  {
605
- self.is_close(
606
- other,
607
- &Self::from_parts(
608
- self.multiplier,
609
- $crate::internal::saturating_power_sub(self.power, 9),
610
- ),
599
+ $crate::internal::quantity_parts_approx_eq(
600
+ self.multiplier,
601
+ self.power,
602
+ other.multiplier,
603
+ other.power,
611
604
  )
612
605
  }
613
606
  #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
614
607
  {
615
- self.is_close(other, &Self::from_base_value(self.as_f64().abs() * 1.0e-9))
608
+ $crate::internal::float_approx_eq(self.as_f64(), other.as_f64())
616
609
  }
617
610
  }
618
611
  }
619
612
 
613
+ /// Exact ordering of the stored values (unlike the tolerance-based `==`), so sorting
614
+ /// stays a strict weak ordering.
620
615
  impl PartialOrd for $type
621
616
  where
622
617
  $type: PhysicsQuantity,
623
618
  {
624
619
  fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
625
- self.as_f64().partial_cmp(&other.as_f64())
620
+ #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
621
+ {
622
+ $crate::internal::compare_quantity_parts(
623
+ self.multiplier,
624
+ self.power,
625
+ other.multiplier,
626
+ other.power,
627
+ )
628
+ }
629
+ #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
630
+ {
631
+ self.as_f64().partial_cmp(&other.as_f64())
632
+ }
626
633
  }
627
634
  }
628
635
 
@@ -646,11 +653,16 @@ macro_rules! impl_quantity {
646
653
  fn add(self, other: Self) -> Self {
647
654
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
648
655
  {
656
+ // Align to the larger power: its multiplier stays exact and the smaller
657
+ // operand can only underflow, never overflow.
649
658
  let common_power = self.power.max(other.power);
650
- let multiplier = self.multiplier
651
- * $crate::internal::pow10_delta(self.power, common_power)
652
- + other.multiplier
653
- * $crate::internal::pow10_delta(other.power, common_power);
659
+ let multiplier = $crate::internal::mul_pow10(
660
+ self.multiplier,
661
+ $crate::internal::saturating_power_sub(self.power, common_power),
662
+ ) + $crate::internal::mul_pow10(
663
+ other.multiplier,
664
+ $crate::internal::saturating_power_sub(other.power, common_power),
665
+ );
654
666
  Self::from_parts(multiplier, common_power)
655
667
  }
656
668
  #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
@@ -666,20 +678,8 @@ macro_rules! impl_quantity {
666
678
  fn sub(self, other: Self) -> Self {
667
679
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
668
680
  {
669
- if self.multiplier.is_infinite()
670
- && other.multiplier.is_infinite()
671
- && self.multiplier.is_sign_positive() == other.multiplier.is_sign_positive()
672
- {
673
- return Self::zero();
674
- }
675
-
676
- let common_power = $crate::internal::average_power(self.power, other.power);
677
- let multiplier = self.multiplier
678
- * $crate::internal::pow10_delta(self.power, common_power)
679
- - other.multiplier
680
- * $crate::internal::pow10_delta(other.power, common_power);
681
-
682
- Self::from_parts(multiplier, common_power)
681
+ // Negation is exact, so this rounds exactly like a direct subtraction.
682
+ self + (-other)
683
683
  }
684
684
  #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
685
685
  {
@@ -762,45 +762,13 @@ macro_rules! impl_quantity {
762
762
 
763
763
  impl AddAssign for $type {
764
764
  fn add_assign(&mut self, other: Self) {
765
- #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
766
- {
767
- let common_power = $crate::internal::average_power(self.power, other.power);
768
- let multiplier = self.multiplier
769
- * $crate::internal::pow10_delta(self.power, common_power)
770
- + other.multiplier
771
- * $crate::internal::pow10_delta(other.power, common_power);
772
- *self = Self::from_parts(multiplier, common_power);
773
- }
774
- #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
775
- {
776
- *self = Self::from_base_value(self.as_f64() + other.as_f64());
777
- }
765
+ *self = *self + other;
778
766
  }
779
767
  }
780
768
 
781
769
  impl SubAssign for $type {
782
770
  fn sub_assign(&mut self, other: Self) {
783
- #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
784
- {
785
- if self.multiplier.is_infinite()
786
- && other.multiplier.is_infinite()
787
- && self.multiplier.is_sign_positive() == other.multiplier.is_sign_positive()
788
- {
789
- *self = Self::zero();
790
- return;
791
- }
792
-
793
- let common_power = $crate::internal::average_power(self.power, other.power);
794
- let multiplier = self.multiplier
795
- * $crate::internal::pow10_delta(self.power, common_power)
796
- - other.multiplier
797
- * $crate::internal::pow10_delta(other.power, common_power);
798
- *self = Self::from_parts(multiplier, common_power);
799
- }
800
- #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
801
- {
802
- *self = Self::from_base_value(self.as_f64() - other.as_f64());
803
- }
771
+ *self = *self - other;
804
772
  }
805
773
  }
806
774
 
@@ -966,12 +934,15 @@ macro_rules! impl_quantity {
966
934
  }
967
935
 
968
936
  fn close_abs(&self, other: PyRef<Self>, threshold: Self) -> bool {
969
- (self.clone() - other.clone()).abs() <= threshold
937
+ self.is_close(&other, &threshold)
970
938
  }
971
939
 
972
940
  fn close_rel(&self, other: PyRef<Self>, threshold: f64) -> bool {
973
- let mean = (self.clone() + other.clone()) / 2.;
974
- (self.clone() - other.clone()).abs() <= mean * threshold
941
+ if self.same_value(&other) {
942
+ return true;
943
+ }
944
+ let mean = (*self + *other) / 2.;
945
+ (*self - *other).abs() <= mean.abs() * threshold
975
946
  }
976
947
 
977
948
  fn __richcmp__(&self, other: PyRef<Self>, op: CompareOp) -> bool {
@@ -1021,74 +992,36 @@ macro_rules! impl_quantity {
1021
992
  self.kind_id
1022
993
  }
1023
994
 
995
+ /// The unit in which this value's magnitude is closest to one, i.e. the one that
996
+ /// minimizes `|log10(|self.to(unit)|)|`. Zero and non-finite values have no
997
+ /// meaningful magnitude; for them the unit whose own size is closest to the base
998
+ /// unit wins. `None` only if `choices` is empty.
1024
999
  pub fn optimal_unit<I>(&self, choices: I) -> Option<$unit>
1025
1000
  where
1026
1001
  I: IntoIterator<Item = $unit>,
1027
1002
  {
1028
- #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
1029
- {
1030
- let mut min_value = i64::MAX;
1031
- let mut best_unit = None;
1032
- for unit in choices {
1033
- let difference =
1034
- (i64::from(self.power) - i64::from(unit.base_per_x().1)).abs();
1035
- if difference < min_value {
1036
- min_value = difference;
1037
- best_unit = Some(unit);
1038
- }
1039
- }
1040
- best_unit
1041
- }
1042
- #[cfg(all(
1043
- feature = "no_std",
1044
- any(feature = "storage-f64", feature = "storage-f32")
1045
- ))]
1046
- {
1047
- let mut min_value = f64::INFINITY;
1048
- let mut best_unit = None;
1049
- for unit in choices {
1050
- let (unit_multiplier, unit_power) = unit.base_per_x();
1051
- let unit_scale =
1052
- $crate::internal::quantity_parts_to_value(unit_multiplier, unit_power);
1053
- let converted = (self.as_f64() / unit_scale).abs();
1054
- let difference = if converted == 0.0 {
1055
- f64::INFINITY
1056
- } else if converted < 1.0 {
1057
- 1.0 / converted
1058
- } else {
1059
- converted
1060
- };
1061
- if difference < min_value {
1062
- min_value = difference;
1063
- best_unit = Some(unit);
1064
- }
1065
- }
1066
- best_unit
1067
- }
1068
- #[cfg(all(
1069
- not(feature = "no_std"),
1070
- any(feature = "storage-f64", feature = "storage-f32")
1071
- ))]
1072
- {
1073
- let mut min_value = f64::INFINITY;
1074
- let mut best_unit = None;
1075
- for unit in choices {
1003
+ let value = self.as_f64();
1004
+ let has_magnitude = value.is_finite() && value != 0.0;
1005
+ let mut best: Option<($unit, f64)> = None;
1006
+ for unit in choices {
1007
+ let magnitude = if has_magnitude {
1008
+ self.to(unit).abs()
1009
+ } else {
1076
1010
  let (unit_multiplier, unit_power) = unit.base_per_x();
1077
- let unit_scale =
1078
- $crate::internal::quantity_parts_to_value(unit_multiplier, unit_power);
1079
- let converted = (self.as_f64() / unit_scale).abs();
1080
- let difference = if converted == 0.0 {
1081
- f64::INFINITY
1082
- } else {
1083
- converted.log10().abs()
1084
- };
1085
- if difference < min_value {
1086
- min_value = difference;
1087
- best_unit = Some(unit);
1088
- }
1011
+ $crate::internal::quantity_parts_to_value(unit_multiplier, unit_power).abs()
1012
+ };
1013
+ // max(m, 1/m) orders like |log10(m)| without needing a logarithm.
1014
+ let difference = if magnitude.is_finite() && magnitude > 0.0 {
1015
+ magnitude.max(1.0 / magnitude)
1016
+ } else {
1017
+ f64::INFINITY
1018
+ };
1019
+ match best {
1020
+ Some((_, best_difference)) if difference >= best_difference => {}
1021
+ _ => best = Some((unit, difference)),
1089
1022
  }
1090
- best_unit
1091
1023
  }
1024
+ best.map(|(unit, _)| unit)
1092
1025
  }
1093
1026
  }
1094
1027
  };
@@ -1158,8 +1091,10 @@ macro_rules! impl_div_with_self_to_f64 {
1158
1091
  fn div(self, rhs: Self) -> Self::Output {
1159
1092
  #[cfg(not(any(feature = "storage-f64", feature = "storage-f32")))]
1160
1093
  {
1161
- (self.multiplier / rhs.multiplier)
1162
- * $crate::internal::pow10_delta(self.power, rhs.power)
1094
+ $crate::internal::quantity_parts_to_value(
1095
+ self.multiplier / rhs.multiplier,
1096
+ $crate::internal::saturating_power_sub(self.power, rhs.power),
1097
+ )
1163
1098
  }
1164
1099
  #[cfg(any(feature = "storage-f64", feature = "storage-f32"))]
1165
1100
  {
@@ -1238,7 +1173,13 @@ macro_rules! impl_mul {
1238
1173
  impl MulArray1<$rhs> for Array1<$lhs> {
1239
1174
  type Output = Array1<$result>;
1240
1175
 
1176
+ #[track_caller]
1241
1177
  fn mul_array1(self, rhs: Array1<$rhs>) -> Array1<$result> {
1178
+ assert_eq!(
1179
+ self.len(),
1180
+ rhs.len(),
1181
+ "mul_array1: operands must have the same length"
1182
+ );
1242
1183
  self.into_iter()
1243
1184
  .zip(rhs.into_iter())
1244
1185
  .map(|(force, distance)| force * distance)
@@ -1296,7 +1237,13 @@ macro_rules! impl_mul {
1296
1237
  impl MulArray1<$lhs> for Array1<$rhs> {
1297
1238
  type Output = Array1<$result>;
1298
1239
 
1240
+ #[track_caller]
1299
1241
  fn mul_array1(self, rhs: Array1<$lhs>) -> Array1<$result> {
1242
+ assert_eq!(
1243
+ self.len(),
1244
+ rhs.len(),
1245
+ "mul_array1: operands must have the same length"
1246
+ );
1300
1247
  self.into_iter()
1301
1248
  .zip(rhs.into_iter())
1302
1249
  .map(|(force, distance)| force * distance)
@@ -1377,7 +1324,13 @@ macro_rules! impl_mul_with_self {
1377
1324
  impl MulArray1<$lhs> for Array1<$lhs> {
1378
1325
  type Output = Array1<$result>;
1379
1326
 
1327
+ #[track_caller]
1380
1328
  fn mul_array1(self, rhs: Array1<$lhs>) -> Array1<$result> {
1329
+ assert_eq!(
1330
+ self.len(),
1331
+ rhs.len(),
1332
+ "mul_array1: operands must have the same length"
1333
+ );
1381
1334
  self.into_iter()
1382
1335
  .zip(rhs.into_iter())
1383
1336
  .map(|(force, distance)| force * distance)
@@ -1485,7 +1438,13 @@ macro_rules! impl_div {
1485
1438
  impl DivArray1<$rhs> for Array1<$lhs> {
1486
1439
  type Output = Array1<$result>;
1487
1440
 
1441
+ #[track_caller]
1488
1442
  fn div_array1(self, rhs: Array1<$rhs>) -> Array1<$result> {
1443
+ assert_eq!(
1444
+ self.len(),
1445
+ rhs.len(),
1446
+ "div_array1: operands must have the same length"
1447
+ );
1489
1448
  self.into_iter()
1490
1449
  .zip(rhs.into_iter())
1491
1450
  .map(|(force, distance)| force / distance)