ucon 0.5.1__py3-none-any.whl → 0.5.2__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.
@@ -0,0 +1,185 @@
1
+ # © 2026 The Radiativity Company
2
+ # Licensed under the Apache License, Version 2.0
3
+ # See the LICENSE file for details.
4
+
5
+ """
6
+ Tests for Vector with Fraction exponents.
7
+
8
+ Verifies backward compatibility with integer exponents and
9
+ correct behavior with fractional exponents for BasisTransform support.
10
+ """
11
+
12
+ import unittest
13
+ from fractions import Fraction
14
+
15
+ from ucon.algebra import Vector
16
+
17
+
18
+ class TestVectorIntegerBackwardCompatibility(unittest.TestCase):
19
+ """Verify existing integer-based code works unchanged."""
20
+
21
+ def test_integer_construction(self):
22
+ v = Vector(1, 0, -2, 0, 0, 0, 0, 0)
23
+ self.assertEqual(tuple(v), (1, 0, -2, 0, 0, 0, 0, 0))
24
+
25
+ def test_integer_addition(self):
26
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
27
+ v2 = Vector(0, 2, 0, 0, 0, 0, 0, 0)
28
+ result = v1 + v2
29
+ self.assertEqual(result, Vector(1, 2, 0, 0, 0, 0, 0, 0))
30
+
31
+ def test_integer_subtraction(self):
32
+ v1 = Vector(2, 1, 0, 0, 0, 0, 0, 0)
33
+ v2 = Vector(1, 1, 0, 0, 0, 0, 0, 0)
34
+ result = v1 - v2
35
+ self.assertEqual(result, Vector(1, 0, 0, 0, 0, 0, 0, 0))
36
+
37
+ def test_integer_scalar_multiplication(self):
38
+ v = Vector(1, -2, 0, 0, 0, 0, 3, 0)
39
+ result = v * 2
40
+ self.assertEqual(result, Vector(2, -4, 0, 0, 0, 0, 6, 0))
41
+
42
+ def test_integer_equality(self):
43
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
44
+ v2 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
45
+ self.assertEqual(v1, v2)
46
+
47
+ def test_integer_hash_consistency(self):
48
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
49
+ v2 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
50
+ self.assertEqual(hash(v1), hash(v2))
51
+ self.assertEqual(len({v1, v2}), 1)
52
+
53
+
54
+ class TestVectorFractionConstruction(unittest.TestCase):
55
+ """Test Vector construction with Fraction values."""
56
+
57
+ def test_fraction_construction(self):
58
+ v = Vector(Fraction(3, 2), Fraction(1, 2), Fraction(-1), 0, 0, 0, 0, 0)
59
+ self.assertEqual(v.T, Fraction(3, 2))
60
+ self.assertEqual(v.L, Fraction(1, 2))
61
+ self.assertEqual(v.M, Fraction(-1))
62
+
63
+ def test_mixed_int_fraction_construction(self):
64
+ v = Vector(1, Fraction(1, 2), 0, 0, 0, 0, 0, 0)
65
+ self.assertEqual(v.T, Fraction(1))
66
+ self.assertEqual(v.L, Fraction(1, 2))
67
+
68
+ def test_float_converted_to_fraction(self):
69
+ v = Vector(0.5, 0, 0, 0, 0, 0, 0, 0)
70
+ self.assertEqual(v.T, Fraction(1, 2))
71
+
72
+ def test_default_values_are_fraction_zero(self):
73
+ v = Vector()
74
+ self.assertEqual(v.T, Fraction(0))
75
+ self.assertEqual(v.L, Fraction(0))
76
+
77
+
78
+ class TestVectorFractionEquality(unittest.TestCase):
79
+ """Test equality across int and Fraction representations."""
80
+
81
+ def test_int_equals_fraction(self):
82
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
83
+ v2 = Vector(Fraction(1), Fraction(0), Fraction(0), Fraction(0),
84
+ Fraction(0), Fraction(0), Fraction(0), Fraction(0))
85
+ self.assertEqual(v1, v2)
86
+
87
+ def test_int_fraction_hash_equality(self):
88
+ v1 = Vector(1, 0, -2, 0, 0, 0, 0, 0)
89
+ v2 = Vector(Fraction(1), Fraction(0), Fraction(-2), Fraction(0),
90
+ Fraction(0), Fraction(0), Fraction(0), Fraction(0))
91
+ self.assertEqual(hash(v1), hash(v2))
92
+
93
+ def test_int_fraction_in_set(self):
94
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
95
+ v2 = Vector(Fraction(1), 0, 0, 0, 0, 0, 0, 0)
96
+ s = {v1, v2}
97
+ self.assertEqual(len(s), 1)
98
+
99
+
100
+ class TestVectorFractionArithmetic(unittest.TestCase):
101
+ """Test arithmetic with Fraction exponents."""
102
+
103
+ def test_fraction_addition(self):
104
+ v1 = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
105
+ v2 = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
106
+ result = v1 + v2
107
+ self.assertEqual(result.T, Fraction(1))
108
+
109
+ def test_fraction_subtraction(self):
110
+ v1 = Vector(Fraction(3, 2), 0, 0, 0, 0, 0, 0, 0)
111
+ v2 = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
112
+ result = v1 - v2
113
+ self.assertEqual(result.T, Fraction(1))
114
+
115
+ def test_fraction_scalar_multiply(self):
116
+ v = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
117
+ result = v * 2
118
+ self.assertEqual(result.T, Fraction(1))
119
+
120
+ def test_fraction_scalar_multiply_by_fraction(self):
121
+ v = Vector(1, 0, 0, 0, 0, 0, 0, 0)
122
+ result = v * Fraction(1, 2)
123
+ self.assertEqual(result.T, Fraction(1, 2))
124
+
125
+ def test_no_floating_point_drift(self):
126
+ # 1/3 * 3 should equal exactly 1, not 0.9999...
127
+ v = Vector(Fraction(1, 3), 0, 0, 0, 0, 0, 0, 0)
128
+ result = v * 3
129
+ self.assertEqual(result.T, Fraction(1))
130
+ self.assertEqual(result, Vector(1, 0, 0, 0, 0, 0, 0, 0))
131
+
132
+ def test_complex_fraction_arithmetic(self):
133
+ # CGS-ESU charge dimension: M^(1/2) · L^(3/2) · T^(-1)
134
+ esu_charge = Vector(
135
+ T=-1,
136
+ L=Fraction(3, 2),
137
+ M=Fraction(1, 2),
138
+ )
139
+ # Multiply by 2 (squaring the unit)
140
+ squared = esu_charge * 2
141
+ self.assertEqual(squared.T, Fraction(-2))
142
+ self.assertEqual(squared.L, Fraction(3))
143
+ self.assertEqual(squared.M, Fraction(1))
144
+
145
+
146
+ class TestVectorIterationWithFraction(unittest.TestCase):
147
+ """Test iteration and length with Fraction values."""
148
+
149
+ def test_iteration_returns_fractions(self):
150
+ v = Vector(Fraction(1, 2), 1, 0, 0, 0, 0, 0, 0)
151
+ components = list(v)
152
+ self.assertEqual(components[0], Fraction(1, 2))
153
+ self.assertEqual(components[1], Fraction(1))
154
+
155
+ def test_length_unchanged(self):
156
+ v = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
157
+ self.assertEqual(len(v), 8)
158
+
159
+
160
+ class TestVectorNegation(unittest.TestCase):
161
+ """Test vector negation with Fraction values."""
162
+
163
+ def test_negation_with_fractions(self):
164
+ v = Vector(Fraction(1, 2), Fraction(-3, 4), 0, 0, 0, 0, 0, 0)
165
+ neg = -v
166
+ self.assertEqual(neg.T, Fraction(-1, 2))
167
+ self.assertEqual(neg.L, Fraction(3, 4))
168
+
169
+
170
+ class TestVectorRightMultiply(unittest.TestCase):
171
+ """Test right multiplication (scalar * vector)."""
172
+
173
+ def test_rmul_integer(self):
174
+ v = Vector(Fraction(1, 2), 0, 0, 0, 0, 0, 0, 0)
175
+ result = 2 * v
176
+ self.assertEqual(result.T, Fraction(1))
177
+
178
+ def test_rmul_fraction(self):
179
+ v = Vector(1, 0, 0, 0, 0, 0, 0, 0)
180
+ result = Fraction(1, 2) * v
181
+ self.assertEqual(result.T, Fraction(1, 2))
182
+
183
+
184
+ if __name__ == "__main__":
185
+ unittest.main()
ucon/__init__.py CHANGED
@@ -38,19 +38,37 @@ 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, UnitFactor, UnitProduct, Number, Ratio
41
+ from ucon.core import (
42
+ BasisTransform,
43
+ Dimension,
44
+ DimensionNotCovered,
45
+ NonInvertibleTransform,
46
+ RebasedUnit,
47
+ Scale,
48
+ Unit,
49
+ UnitFactor,
50
+ UnitProduct,
51
+ UnitSystem,
52
+ Number,
53
+ Ratio,
54
+ )
42
55
  from ucon.graph import get_default_graph, using_graph
43
56
 
44
57
 
45
58
  __all__ = [
46
- 'Exponent',
59
+ 'BasisTransform',
47
60
  'Dimension',
61
+ 'DimensionNotCovered',
62
+ 'Exponent',
63
+ 'NonInvertibleTransform',
48
64
  'Number',
49
65
  'Ratio',
66
+ 'RebasedUnit',
50
67
  'Scale',
51
68
  'Unit',
52
69
  'UnitFactor',
53
70
  'UnitProduct',
71
+ 'UnitSystem',
54
72
  'get_default_graph',
55
73
  'using_graph',
56
74
  'units',
ucon/algebra.py CHANGED
@@ -20,13 +20,14 @@ Classes
20
20
  - :class:`Exponent` — Base/power pair supporting prefix arithmetic.
21
21
  """
22
22
  import math
23
- from dataclasses import dataclass
23
+ from dataclasses import dataclass, fields
24
+ from fractions import Fraction
24
25
  from functools import partial, reduce, total_ordering
25
26
  from operator import __sub__ as subtraction
26
27
  from typing import Callable, Iterable, Iterator, Tuple, Union
27
28
 
28
29
 
29
- diff: Callable[[Iterable], int] = partial(reduce, subtraction)
30
+ diff: Callable[[Iterable], Fraction] = partial(reduce, subtraction)
30
31
 
31
32
 
32
33
  @dataclass
@@ -43,22 +44,34 @@ class Vector:
43
44
  - Addition (`+`) → multiplication of quantities
44
45
  - Subtraction (`-`) → division of quantities
45
46
 
47
+ Components are stored as `Fraction` for exact arithmetic, enabling
48
+ fractional exponents (e.g., CGS-ESU charge: M^(1/2) · L^(3/2) · T^(-1)).
49
+ Integer and float inputs are automatically converted to Fraction.
50
+
46
51
  e.g.
47
52
  Vector(T=1, L=0, M=0, I=0, Θ=0, J=0, N=0, B=0) => "time"
48
53
  Vector(T=0, L=2, M=0, I=0, Θ=0, J=0, N=0, B=0) => "area"
49
54
  Vector(T=-2, L=1, M=1, I=0, Θ=0, J=0, N=0, B=0) => "force"
50
55
  Vector(T=0, L=0, M=0, I=0, Θ=0, J=0, N=0, B=1) => "information"
56
+ Vector(L=Fraction(3,2), M=Fraction(1,2), T=-1) => "esu_charge"
51
57
  """
52
- T: int = 0 # time
53
- L: int = 0 # length
54
- M: int = 0 # mass
55
- I: int = 0 # current
56
- Θ: int = 0 # temperature
57
- J: int = 0 # luminous intensity
58
- N: int = 0 # amount of substance
59
- B: int = 0 # information (bits)
60
-
61
- def __iter__(self) -> Iterator[int]:
58
+ T: Union[int, float, Fraction] = 0 # time
59
+ L: Union[int, float, Fraction] = 0 # length
60
+ M: Union[int, float, Fraction] = 0 # mass
61
+ I: Union[int, float, Fraction] = 0 # current
62
+ Θ: Union[int, float, Fraction] = 0 # temperature
63
+ J: Union[int, float, Fraction] = 0 # luminous intensity
64
+ N: Union[int, float, Fraction] = 0 # amount of substance
65
+ B: Union[int, float, Fraction] = 0 # information (bits)
66
+
67
+ def __post_init__(self):
68
+ """Convert all components to Fraction for exact arithmetic."""
69
+ for field in fields(self):
70
+ val = getattr(self, field.name)
71
+ if not isinstance(val, Fraction):
72
+ object.__setattr__(self, field.name, Fraction(val))
73
+
74
+ def __iter__(self) -> Iterator[Fraction]:
62
75
  yield self.T
63
76
  yield self.L
64
77
  yield self.M
@@ -90,7 +103,7 @@ class Vector:
90
103
  values = tuple(diff(pair) for pair in zip(tuple(self), tuple(vector)))
91
104
  return Vector(*values)
92
105
 
93
- def __mul__(self, scalar: Union[int, float]) -> 'Vector':
106
+ def __mul__(self, scalar: Union[int, float, Fraction]) -> 'Vector':
94
107
  """
95
108
  Scalar multiplication of the exponent vector.
96
109
 
@@ -99,9 +112,18 @@ class Vector:
99
112
  >>> Dimension.length ** 2 # area
100
113
  >>> Dimension.time ** -1 # frequency
101
114
  """
102
- values = tuple(component * scalar for component in tuple(self))
115
+ n = Fraction(scalar) if not isinstance(scalar, Fraction) else scalar
116
+ values = tuple(component * n for component in tuple(self))
103
117
  return Vector(*values)
104
118
 
119
+ def __rmul__(self, scalar: Union[int, float, Fraction]) -> 'Vector':
120
+ """Right multiplication: scalar * vector."""
121
+ return self.__mul__(scalar)
122
+
123
+ def __neg__(self) -> 'Vector':
124
+ """Negate the vector: -v."""
125
+ return Vector(*(-component for component in tuple(self)))
126
+
105
127
  def __eq__(self, other) -> bool:
106
128
  if not isinstance(other, Vector):
107
129
  return NotImplemented