ucon 0.3.5rc1__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- tests/ucon/conversion/__init__.py +0 -0
- tests/ucon/conversion/test_graph.py +175 -0
- tests/ucon/conversion/test_map.py +163 -0
- tests/ucon/test_algebra.py +34 -34
- tests/ucon/test_core.py +20 -20
- tests/ucon/test_quantity.py +205 -14
- ucon/__init__.py +6 -2
- ucon/algebra.py +9 -5
- ucon/core.py +388 -68
- ucon/graph.py +415 -0
- ucon/maps.py +161 -0
- ucon/quantity.py +7 -186
- ucon/units.py +74 -31
- {ucon-0.3.5rc1.dist-info → ucon-0.4.0.dist-info}/METADATA +58 -30
- ucon-0.4.0.dist-info/RECORD +21 -0
- ucon-0.3.5rc1.dist-info/RECORD +0 -16
- {ucon-0.3.5rc1.dist-info → ucon-0.4.0.dist-info}/WHEEL +0 -0
- {ucon-0.3.5rc1.dist-info → ucon-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.3.5rc1.dist-info → ucon-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {ucon-0.3.5rc1.dist-info → ucon-0.4.0.dist-info}/top_level.txt +0 -0
tests/ucon/test_quantity.py
CHANGED
|
@@ -19,18 +19,15 @@ class TestNumber(unittest.TestCase):
|
|
|
19
19
|
self.assertEqual(ratio.numerator, self.number)
|
|
20
20
|
self.assertEqual(ratio.denominator, Number())
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
decagram =
|
|
25
|
-
kibigram = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.kibi)
|
|
26
|
-
|
|
22
|
+
def test_simplify_scaled_unit(self):
|
|
23
|
+
"""Test simplify() removes scale prefix and adjusts quantity."""
|
|
24
|
+
decagram = Scale.deca * units.gram
|
|
27
25
|
ten_decagrams = Number(unit=decagram, quantity=10)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
self.assertEqual(
|
|
33
|
-
self.assertEqual(Number(unit=units.gram, quantity=2048), two_kibigrams.simplify())
|
|
26
|
+
result = ten_decagrams.simplify()
|
|
27
|
+
# 10 decagrams = 100 grams
|
|
28
|
+
self.assertAlmostEqual(result.quantity, 100.0, places=10)
|
|
29
|
+
# Unit should be base gram (Scale.one)
|
|
30
|
+
self.assertEqual(result.unit.shorthand, "g")
|
|
34
31
|
|
|
35
32
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
36
33
|
def test_to(self):
|
|
@@ -232,7 +229,7 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
232
229
|
self.assertTrue(r == Number())
|
|
233
230
|
|
|
234
231
|
def test_repr_includes_scale_and_unit(self):
|
|
235
|
-
kV = Scale.kilo * Unit(
|
|
232
|
+
kV = Scale.kilo * Unit(name='volt', dimension=Dimension.voltage, aliases=('V',))
|
|
236
233
|
n = Number(unit=kV, quantity=5)
|
|
237
234
|
rep = repr(n)
|
|
238
235
|
self.assertIn("kV", rep)
|
|
@@ -269,7 +266,7 @@ class TestRatio(unittest.TestCase):
|
|
|
269
266
|
self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
|
|
270
267
|
|
|
271
268
|
def test___mul__(self):
|
|
272
|
-
mL = Scale.milli * Unit(
|
|
269
|
+
mL = Scale.milli * Unit(name='liter', dimension=Dimension.volume, aliases=('L',))
|
|
273
270
|
n1 = Number(unit=units.gram, quantity=3.119)
|
|
274
271
|
n2 = Number(unit=mL)
|
|
275
272
|
bromine_density = Ratio(n1, n2)
|
|
@@ -289,7 +286,7 @@ class TestRatio(unittest.TestCase):
|
|
|
289
286
|
|
|
290
287
|
# How many Wh from 20 kJ?
|
|
291
288
|
twenty_kilojoules = Number(
|
|
292
|
-
unit=Scale.kilo * Unit(
|
|
289
|
+
unit=Scale.kilo * Unit(name='joule', dimension=Dimension.energy, aliases=('J',)),
|
|
293
290
|
quantity=20
|
|
294
291
|
)
|
|
295
292
|
ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
|
|
@@ -368,3 +365,197 @@ class TestRatioEdgeCases(unittest.TestCase):
|
|
|
368
365
|
r = Ratio(n1, n2)
|
|
369
366
|
rep = repr(r)
|
|
370
367
|
self.assertIn("/", rep)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class TestCallableUnits(unittest.TestCase):
|
|
371
|
+
"""Tests for the callable unit syntax: unit(quantity) -> Number."""
|
|
372
|
+
|
|
373
|
+
def test_unit_callable_returns_number(self):
|
|
374
|
+
result = units.meter(5)
|
|
375
|
+
self.assertIsInstance(result, Number)
|
|
376
|
+
self.assertEqual(result.quantity, 5)
|
|
377
|
+
|
|
378
|
+
def test_unit_callable_shorthand(self):
|
|
379
|
+
result = units.meter(5)
|
|
380
|
+
self.assertIn("m", result.unit.shorthand)
|
|
381
|
+
|
|
382
|
+
def test_unit_product_callable_returns_number(self):
|
|
383
|
+
velocity = units.meter / units.second
|
|
384
|
+
result = velocity(10)
|
|
385
|
+
self.assertIsInstance(result, Number)
|
|
386
|
+
self.assertEqual(result.quantity, 10)
|
|
387
|
+
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
388
|
+
|
|
389
|
+
def test_scaled_unit_callable_returns_number(self):
|
|
390
|
+
km = Scale.kilo * units.meter
|
|
391
|
+
result = km(5)
|
|
392
|
+
self.assertIsInstance(result, Number)
|
|
393
|
+
self.assertEqual(result.quantity, 5)
|
|
394
|
+
self.assertIn("km", result.unit.shorthand)
|
|
395
|
+
|
|
396
|
+
def test_composite_scaled_unit_callable(self):
|
|
397
|
+
mph = units.mile / units.hour
|
|
398
|
+
result = mph(60)
|
|
399
|
+
self.assertIsInstance(result, Number)
|
|
400
|
+
self.assertEqual(result.quantity, 60)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class TestScaledUnitConversion(unittest.TestCase):
|
|
404
|
+
"""Tests for conversions involving scaled units.
|
|
405
|
+
|
|
406
|
+
Regression tests for bug where scale was applied twice during conversion.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
def test_km_to_mile_conversion(self):
|
|
410
|
+
"""5 km should be approximately 3.10686 miles."""
|
|
411
|
+
km = Scale.kilo * units.meter
|
|
412
|
+
result = km(5).to(units.mile)
|
|
413
|
+
# 5 km = 5000 m = 5000 / 1609.34 miles ≈ 3.10686
|
|
414
|
+
self.assertAlmostEqual(result.quantity, 3.10686, places=4)
|
|
415
|
+
|
|
416
|
+
def test_km_to_meter_conversion(self):
|
|
417
|
+
"""1 km should be 1000 meters."""
|
|
418
|
+
km = Scale.kilo * units.meter
|
|
419
|
+
result = km(1).to(units.meter)
|
|
420
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=6)
|
|
421
|
+
|
|
422
|
+
def test_meter_to_mm_conversion(self):
|
|
423
|
+
"""1 meter should be 1000 millimeters."""
|
|
424
|
+
mm = Scale.milli * units.meter
|
|
425
|
+
result = units.meter(1).to(mm)
|
|
426
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=6)
|
|
427
|
+
|
|
428
|
+
def test_mm_to_inch_conversion(self):
|
|
429
|
+
"""25.4 mm should be approximately 1 inch."""
|
|
430
|
+
mm = Scale.milli * units.meter
|
|
431
|
+
result = mm(25.4).to(units.inch)
|
|
432
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=4)
|
|
433
|
+
|
|
434
|
+
def test_scaled_velocity_conversion(self):
|
|
435
|
+
"""1 km/h should be approximately 0.27778 m/s."""
|
|
436
|
+
km_per_h = (Scale.kilo * units.meter) / units.hour
|
|
437
|
+
m_per_s = units.meter / units.second
|
|
438
|
+
result = km_per_h(1).to(m_per_s)
|
|
439
|
+
# 1 km/h = 1000m / 3600s = 0.27778 m/s
|
|
440
|
+
self.assertAlmostEqual(result.quantity, 0.27778, places=4)
|
|
441
|
+
|
|
442
|
+
def test_mph_to_m_per_s_conversion(self):
|
|
443
|
+
"""60 mph should be approximately 26.8224 m/s."""
|
|
444
|
+
mph = units.mile / units.hour
|
|
445
|
+
m_per_s = units.meter / units.second
|
|
446
|
+
result = mph(60).to(m_per_s)
|
|
447
|
+
# 60 mph = 60 * 1609.34 / 3600 m/s ≈ 26.8224
|
|
448
|
+
self.assertAlmostEqual(result.quantity, 26.8224, places=2)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class TestNumberSimplify(unittest.TestCase):
|
|
452
|
+
"""Tests for Number.simplify() method."""
|
|
453
|
+
|
|
454
|
+
def test_simplify_kilo_prefix(self):
|
|
455
|
+
"""5 km simplifies to 5000 m."""
|
|
456
|
+
km = Scale.kilo * units.meter
|
|
457
|
+
result = km(5).simplify()
|
|
458
|
+
self.assertAlmostEqual(result.quantity, 5000.0, places=10)
|
|
459
|
+
self.assertEqual(result.unit.shorthand, "m")
|
|
460
|
+
|
|
461
|
+
def test_simplify_milli_prefix(self):
|
|
462
|
+
"""500 mg simplifies to 0.5 g."""
|
|
463
|
+
mg = Scale.milli * units.gram
|
|
464
|
+
result = mg(500).simplify()
|
|
465
|
+
self.assertAlmostEqual(result.quantity, 0.5, places=10)
|
|
466
|
+
self.assertEqual(result.unit.shorthand, "g")
|
|
467
|
+
|
|
468
|
+
@unittest.skip("Requires Dimension.information and units.byte (see user story)")
|
|
469
|
+
def test_simplify_binary_prefix(self):
|
|
470
|
+
"""2 kibibytes simplifies to 2048 bytes."""
|
|
471
|
+
kibibyte = Scale.kibi * units.byte
|
|
472
|
+
result = kibibyte(2).simplify()
|
|
473
|
+
self.assertAlmostEqual(result.quantity, 2048.0, places=10)
|
|
474
|
+
self.assertEqual(result.unit.shorthand, "B")
|
|
475
|
+
|
|
476
|
+
def test_simplify_composite_unit(self):
|
|
477
|
+
"""1 km/h simplifies to base scales."""
|
|
478
|
+
km_per_h = (Scale.kilo * units.meter) / units.hour
|
|
479
|
+
result = km_per_h(1).simplify()
|
|
480
|
+
# 1 km/h = 1000 m / 1 h (hour stays hour since it's base unit)
|
|
481
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=10)
|
|
482
|
+
self.assertEqual(result.unit.shorthand, "m/h")
|
|
483
|
+
|
|
484
|
+
def test_simplify_plain_unit_unchanged(self):
|
|
485
|
+
"""Plain unit without scale returns equivalent Number."""
|
|
486
|
+
result = units.meter(5).simplify()
|
|
487
|
+
self.assertAlmostEqual(result.quantity, 5.0, places=10)
|
|
488
|
+
self.assertEqual(result.unit.shorthand, "m")
|
|
489
|
+
|
|
490
|
+
def test_simplify_preserves_dimension(self):
|
|
491
|
+
"""Simplified Number has same dimension."""
|
|
492
|
+
km = Scale.kilo * units.meter
|
|
493
|
+
original = km(5)
|
|
494
|
+
simplified = original.simplify()
|
|
495
|
+
self.assertEqual(original.unit.dimension, simplified.unit.dimension)
|
|
496
|
+
|
|
497
|
+
def test_simplify_idempotent(self):
|
|
498
|
+
"""Simplifying twice gives same result."""
|
|
499
|
+
km = Scale.kilo * units.meter
|
|
500
|
+
result1 = km(5).simplify()
|
|
501
|
+
result2 = result1.simplify()
|
|
502
|
+
self.assertAlmostEqual(result1.quantity, result2.quantity, places=10)
|
|
503
|
+
self.assertEqual(result1.unit.shorthand, result2.unit.shorthand)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class TestInformationDimension(unittest.TestCase):
|
|
507
|
+
"""Tests for Dimension.information and information units (bit, byte)."""
|
|
508
|
+
|
|
509
|
+
def test_dimension_information_exists(self):
|
|
510
|
+
"""Dimension.information should be a valid dimension."""
|
|
511
|
+
self.assertEqual(Dimension.information.name, 'information')
|
|
512
|
+
self.assertNotEqual(Dimension.information, Dimension.none)
|
|
513
|
+
|
|
514
|
+
def test_bit_unit_exists(self):
|
|
515
|
+
"""units.bit should have Dimension.information."""
|
|
516
|
+
self.assertEqual(units.bit.dimension, Dimension.information)
|
|
517
|
+
self.assertIn('b', units.bit.aliases)
|
|
518
|
+
|
|
519
|
+
def test_byte_unit_exists(self):
|
|
520
|
+
"""units.byte should have Dimension.information."""
|
|
521
|
+
self.assertEqual(units.byte.dimension, Dimension.information)
|
|
522
|
+
self.assertIn('B', units.byte.aliases)
|
|
523
|
+
|
|
524
|
+
def test_byte_to_bit_conversion(self):
|
|
525
|
+
"""1 byte should be 8 bits."""
|
|
526
|
+
result = units.byte(1).to(units.bit)
|
|
527
|
+
self.assertAlmostEqual(result.quantity, 8.0, places=10)
|
|
528
|
+
|
|
529
|
+
def test_bit_to_byte_conversion(self):
|
|
530
|
+
"""8 bits should be 1 byte."""
|
|
531
|
+
result = units.bit(8).to(units.byte)
|
|
532
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
533
|
+
|
|
534
|
+
@unittest.skip("Requires Number.simplify() from ucon#93-numbers-can-be-simplified")
|
|
535
|
+
def test_kibibyte_simplify(self):
|
|
536
|
+
"""1 kibibyte simplifies to 1024 bytes."""
|
|
537
|
+
kibibyte = Scale.kibi * units.byte
|
|
538
|
+
result = kibibyte(1).simplify()
|
|
539
|
+
self.assertAlmostEqual(result.quantity, 1024.0, places=10)
|
|
540
|
+
self.assertEqual(result.unit.shorthand, "B")
|
|
541
|
+
|
|
542
|
+
@unittest.skip("Requires Number.simplify() from ucon#93-numbers-can-be-simplified")
|
|
543
|
+
def test_kilobyte_simplify(self):
|
|
544
|
+
"""1 kilobyte simplifies to 1000 bytes."""
|
|
545
|
+
kilobyte = Scale.kilo * units.byte
|
|
546
|
+
result = kilobyte(1).simplify()
|
|
547
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=10)
|
|
548
|
+
self.assertEqual(result.unit.shorthand, "B")
|
|
549
|
+
|
|
550
|
+
def test_data_rate_dimension(self):
|
|
551
|
+
"""bytes/second should have information/time dimension."""
|
|
552
|
+
data_rate = units.byte / units.second
|
|
553
|
+
expected_dim = Dimension.information / Dimension.time
|
|
554
|
+
self.assertEqual(data_rate.dimension, expected_dim)
|
|
555
|
+
|
|
556
|
+
def test_information_orthogonal_to_physical(self):
|
|
557
|
+
"""Information dimension should be orthogonal to physical dimensions."""
|
|
558
|
+
# byte * meter should have both information and length
|
|
559
|
+
composite = units.byte * units.meter
|
|
560
|
+
self.assertNotEqual(composite.dimension, Dimension.information)
|
|
561
|
+
self.assertNotEqual(composite.dimension, Dimension.length)
|
ucon/__init__.py
CHANGED
|
@@ -38,8 +38,8 @@ Design Philosophy
|
|
|
38
38
|
"""
|
|
39
39
|
from ucon import units
|
|
40
40
|
from ucon.algebra import Exponent
|
|
41
|
-
from ucon.core import Dimension, Scale, Unit
|
|
42
|
-
from ucon.
|
|
41
|
+
from ucon.core import Dimension, Scale, Unit, UnitFactor, UnitProduct, Number, Ratio
|
|
42
|
+
from ucon.graph import get_default_graph, using_graph
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
__all__ = [
|
|
@@ -49,5 +49,9 @@ __all__ = [
|
|
|
49
49
|
'Ratio',
|
|
50
50
|
'Scale',
|
|
51
51
|
'Unit',
|
|
52
|
+
'UnitFactor',
|
|
53
|
+
'UnitProduct',
|
|
54
|
+
'get_default_graph',
|
|
55
|
+
'using_graph',
|
|
52
56
|
'units',
|
|
53
57
|
]
|
ucon/algebra.py
CHANGED
|
@@ -34,18 +34,20 @@ class Vector:
|
|
|
34
34
|
"""
|
|
35
35
|
Represents the **exponent vector** of a physical quantity.
|
|
36
36
|
|
|
37
|
-
Each component corresponds to the power of a base dimension in the SI system
|
|
37
|
+
Each component corresponds to the power of a base dimension in the SI system
|
|
38
|
+
plus information (B) as an orthogonal non-SI dimension:
|
|
38
39
|
time (T), length (L), mass (M), current (I), temperature (Θ),
|
|
39
|
-
luminous intensity (J),
|
|
40
|
+
luminous intensity (J), amount of substance (N), and information (B).
|
|
40
41
|
|
|
41
42
|
Arithmetic operations correspond to dimensional composition:
|
|
42
43
|
- Addition (`+`) → multiplication of quantities
|
|
43
44
|
- Subtraction (`-`) → division of quantities
|
|
44
45
|
|
|
45
46
|
e.g.
|
|
46
|
-
Vector(T=1, L=0, M=0, I=0, Θ=0, J=0, N=0) => "time"
|
|
47
|
-
Vector(T=0, L=2, M=0, I=0, Θ=0, J=0, N=0) => "area"
|
|
48
|
-
Vector(T=-2, L=1, M=1, I=0, Θ=0, J=0, N=0) => "force"
|
|
47
|
+
Vector(T=1, L=0, M=0, I=0, Θ=0, J=0, N=0, B=0) => "time"
|
|
48
|
+
Vector(T=0, L=2, M=0, I=0, Θ=0, J=0, N=0, B=0) => "area"
|
|
49
|
+
Vector(T=-2, L=1, M=1, I=0, Θ=0, J=0, N=0, B=0) => "force"
|
|
50
|
+
Vector(T=0, L=0, M=0, I=0, Θ=0, J=0, N=0, B=1) => "information"
|
|
49
51
|
"""
|
|
50
52
|
T: int = 0 # time
|
|
51
53
|
L: int = 0 # length
|
|
@@ -54,6 +56,7 @@ class Vector:
|
|
|
54
56
|
Θ: int = 0 # temperature
|
|
55
57
|
J: int = 0 # luminous intensity
|
|
56
58
|
N: int = 0 # amount of substance
|
|
59
|
+
B: int = 0 # information (bits)
|
|
57
60
|
|
|
58
61
|
def __iter__(self) -> Iterator[int]:
|
|
59
62
|
yield self.T
|
|
@@ -63,6 +66,7 @@ class Vector:
|
|
|
63
66
|
yield self.Θ
|
|
64
67
|
yield self.J
|
|
65
68
|
yield self.N
|
|
69
|
+
yield self.B
|
|
66
70
|
|
|
67
71
|
def __len__(self) -> int:
|
|
68
72
|
return sum(tuple(1 for x in self))
|