ucon 0.4.1__tar.gz → 0.4.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.
- {ucon-0.4.1 → ucon-0.4.2}/PKG-INFO +1 -1
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/test_quantity.py +60 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/core.py +18 -4
- {ucon-0.4.1 → ucon-0.4.2}/ucon.egg-info/PKG-INFO +1 -1
- {ucon-0.4.1 → ucon-0.4.2}/.github/workflows/publish.yaml +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/.github/workflows/tests.yaml +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/.gitignore +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/LICENSE +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/NOTICE +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/README.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ROADMAP.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/decisions/composable-unit-algebra.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/decisions/composite-units.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/decisions/unit-algebra-naming.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/decisions/unity-distance-metric-for-nearest-scale.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/explainers/exponent-scale-relationship.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/explainers/type-operation-matrix.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/explainers/why-algebraic-closure-matters.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/explainers/why-type-safety-matters.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/proposals/project_unified-algebraic-core.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/proposals/support-for-fractional-exponents.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/docs/proposals/unified-unit-presentation.md +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/noxfile.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/requirements.txt +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/setup.cfg +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/setup.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/__init__.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/__init__.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/conversion/__init__.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/conversion/test_graph.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/conversion/test_map.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/test_algebra.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/test_core.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/test_default_graph_conversions.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/tests/ucon/test_units.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/__init__.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/algebra.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/graph.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/maps.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/quantity.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon/units.py +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon.egg-info/SOURCES.txt +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon.egg-info/dependency_links.txt +0 -0
- {ucon-0.4.1 → ucon-0.4.2}/ucon.egg-info/top_level.txt +0 -0
|
@@ -364,6 +364,66 @@ class TestRatioEdgeCases(unittest.TestCase):
|
|
|
364
364
|
self.assertIn("/", rep)
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
class TestRatioExponentScaling(unittest.TestCase):
|
|
368
|
+
"""Tests for Ratio.evaluate() using Exponent-based scaling.
|
|
369
|
+
|
|
370
|
+
Ensures Ratio.evaluate() behaves consistently with Number.__truediv__
|
|
371
|
+
when units cancel to dimensionless results.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
def test_evaluate_dimensionless_with_different_scales(self):
|
|
375
|
+
"""Ratio of same unit with different scales should fold scales."""
|
|
376
|
+
kg = Scale.kilo * units.gram
|
|
377
|
+
# 500 g / 1 kg = 0.5 (dimensionless)
|
|
378
|
+
ratio = Ratio(units.gram(500), kg(1))
|
|
379
|
+
result = ratio.evaluate()
|
|
380
|
+
self.assertAlmostEqual(result.quantity, 0.5, places=10)
|
|
381
|
+
self.assertEqual(result.unit.dimension, Dimension.none)
|
|
382
|
+
|
|
383
|
+
def test_evaluate_matches_number_truediv(self):
|
|
384
|
+
"""Ratio.evaluate() should match Number.__truediv__ for dimensionless."""
|
|
385
|
+
kg = Scale.kilo * units.gram
|
|
386
|
+
num = units.gram(500)
|
|
387
|
+
den = kg(1)
|
|
388
|
+
|
|
389
|
+
ratio_result = Ratio(num, den).evaluate()
|
|
390
|
+
truediv_result = num / den
|
|
391
|
+
|
|
392
|
+
self.assertAlmostEqual(ratio_result.quantity, truediv_result.quantity, places=10)
|
|
393
|
+
self.assertEqual(ratio_result.unit.dimension, truediv_result.unit.dimension)
|
|
394
|
+
|
|
395
|
+
def test_evaluate_cross_base_scaling(self):
|
|
396
|
+
"""Binary and decimal prefixes should combine correctly."""
|
|
397
|
+
kibigram = Scale.kibi * units.gram # 1024 g
|
|
398
|
+
kg = Scale.kilo * units.gram # 1000 g
|
|
399
|
+
# 1 kibigram / 1 kg = 1024/1000 = 1.024
|
|
400
|
+
ratio = Ratio(kibigram(1), kg(1))
|
|
401
|
+
result = ratio.evaluate()
|
|
402
|
+
self.assertAlmostEqual(result.quantity, 1.024, places=10)
|
|
403
|
+
self.assertEqual(result.unit.dimension, Dimension.none)
|
|
404
|
+
|
|
405
|
+
def test_evaluate_dimensionful_preserves_scales(self):
|
|
406
|
+
"""Non-cancelling units should preserve symbolic scales."""
|
|
407
|
+
km = Scale.kilo * units.meter
|
|
408
|
+
# 100 km / 2 h = 50 km/h (scales preserved, not folded)
|
|
409
|
+
ratio = Ratio(km(100), units.hour(2))
|
|
410
|
+
result = ratio.evaluate()
|
|
411
|
+
self.assertAlmostEqual(result.quantity, 50.0, places=10)
|
|
412
|
+
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
413
|
+
self.assertIn("km", result.unit.shorthand)
|
|
414
|
+
|
|
415
|
+
def test_evaluate_complex_composition(self):
|
|
416
|
+
"""Composed ratios should maintain scale semantics."""
|
|
417
|
+
mL = Scale.milli * units.liter
|
|
418
|
+
# Density: 3.119 g/mL
|
|
419
|
+
density = Ratio(units.gram(3.119), mL(1))
|
|
420
|
+
# Volume: 2 mL
|
|
421
|
+
volume = Ratio(mL(2), Number())
|
|
422
|
+
# Mass = density * volume
|
|
423
|
+
result = (density * volume).evaluate()
|
|
424
|
+
self.assertAlmostEqual(result.quantity, 6.238, places=3)
|
|
425
|
+
|
|
426
|
+
|
|
367
427
|
class TestCallableUnits(unittest.TestCase):
|
|
368
428
|
"""Tests for the callable unit syntax: unit(quantity) -> Number."""
|
|
369
429
|
|
|
@@ -1091,13 +1091,27 @@ class Ratio:
|
|
|
1091
1091
|
return Ratio(numerator=self.denominator, denominator=self.numerator)
|
|
1092
1092
|
|
|
1093
1093
|
def evaluate(self) -> "Number":
|
|
1094
|
-
|
|
1095
|
-
|
|
1094
|
+
"""Evaluate the ratio to a Number.
|
|
1095
|
+
|
|
1096
|
+
Uses Exponent-derived arithmetic for scale handling:
|
|
1097
|
+
- If the result is dimensionless (units cancel), scales are folded
|
|
1098
|
+
into the magnitude using _canonical_magnitude.
|
|
1099
|
+
- If the result is dimensionful, raw quantities are divided and
|
|
1100
|
+
unit scales are preserved symbolically.
|
|
1096
1101
|
|
|
1097
|
-
|
|
1102
|
+
This matches the behavior of Number.__truediv__ for consistency.
|
|
1103
|
+
"""
|
|
1104
|
+
# Symbolic quotient in the unit algebra
|
|
1098
1105
|
unit = self.numerator.unit / self.denominator.unit
|
|
1099
1106
|
|
|
1100
|
-
#
|
|
1107
|
+
# Dimensionless result: fold all scale factors into magnitude
|
|
1108
|
+
if not unit.dimension:
|
|
1109
|
+
num = self.numerator._canonical_magnitude
|
|
1110
|
+
den = self.denominator._canonical_magnitude
|
|
1111
|
+
return Number(quantity=num / den, unit=_none)
|
|
1112
|
+
|
|
1113
|
+
# Dimensionful result: preserve user's chosen scales symbolically
|
|
1114
|
+
numeric = self.numerator.quantity / self.denominator.quantity
|
|
1101
1115
|
return Number(quantity=numeric, unit=unit)
|
|
1102
1116
|
|
|
1103
1117
|
def __mul__(self, another_ratio: 'Ratio') -> 'Ratio':
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ucon-0.4.1 → ucon-0.4.2}/NOTICE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|