ucon 0.3.5rc2__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.
@@ -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
- @unittest.skip("Requires ConversionGraph implementation")
23
- def test_simplify(self):
24
- decagram = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.deca)
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
- point_one_decagrams = Number(unit=decagram, quantity=0.1)
29
- two_kibigrams = Number(unit=kibigram, quantity=2)
30
-
31
- self.assertEqual(Number(unit=units.gram, quantity=100), ten_decagrams.simplify())
32
- self.assertEqual(Number(unit=units.gram, quantity=1), point_one_decagrams.simplify())
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('V', name='volt', dimension=Dimension.voltage)
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('L', name='liter', dimension=Dimension.volume)
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('J', name='joule', dimension=Dimension.energy),
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.quantity import Number, Ratio
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), and amount of substance (N).
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))