ucon 0.5.0__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.
- tests/ucon/test_basis_transform.py +521 -0
- tests/ucon/test_graph_basis_transform.py +263 -0
- tests/ucon/test_rebased_unit.py +184 -0
- tests/ucon/test_uncertainty.py +264 -0
- tests/ucon/test_unit_system.py +174 -0
- tests/ucon/test_vector_fraction.py +185 -0
- ucon/__init__.py +20 -2
- ucon/algebra.py +36 -14
- ucon/core.py +558 -12
- ucon/graph.py +167 -10
- ucon/maps.py +22 -1
- ucon/units.py +28 -1
- {ucon-0.5.0.dist-info → ucon-0.5.2.dist-info}/METADATA +105 -3
- ucon-0.5.2.dist-info/RECORD +29 -0
- ucon-0.5.0.dist-info/RECORD +0 -23
- {ucon-0.5.0.dist-info → ucon-0.5.2.dist-info}/WHEEL +0 -0
- {ucon-0.5.0.dist-info → ucon-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.5.0.dist-info → ucon-0.5.2.dist-info}/licenses/NOTICE +0 -0
- {ucon-0.5.0.dist-info → ucon-0.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
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 UnitSystem.
|
|
7
|
+
|
|
8
|
+
Verifies construction, validation, and query methods for named
|
|
9
|
+
unit system definitions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
from ucon import units
|
|
15
|
+
from ucon.core import Dimension, Unit, UnitSystem, DimensionNotCovered
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestUnitSystemConstruction(unittest.TestCase):
|
|
19
|
+
"""Test UnitSystem construction and validation."""
|
|
20
|
+
|
|
21
|
+
def test_valid_construction(self):
|
|
22
|
+
system = UnitSystem(
|
|
23
|
+
name="SI",
|
|
24
|
+
bases={
|
|
25
|
+
Dimension.length: units.meter,
|
|
26
|
+
Dimension.mass: units.kilogram,
|
|
27
|
+
Dimension.time: units.second,
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
self.assertEqual(system.name, "SI")
|
|
31
|
+
self.assertEqual(len(system.bases), 3)
|
|
32
|
+
|
|
33
|
+
def test_single_base_allowed(self):
|
|
34
|
+
system = UnitSystem(
|
|
35
|
+
name="length-only",
|
|
36
|
+
bases={Dimension.length: units.meter}
|
|
37
|
+
)
|
|
38
|
+
self.assertEqual(len(system.bases), 1)
|
|
39
|
+
|
|
40
|
+
def test_empty_name_rejected(self):
|
|
41
|
+
with self.assertRaises(ValueError) as ctx:
|
|
42
|
+
UnitSystem(name="", bases={Dimension.length: units.meter})
|
|
43
|
+
self.assertIn("name", str(ctx.exception).lower())
|
|
44
|
+
|
|
45
|
+
def test_empty_bases_rejected(self):
|
|
46
|
+
with self.assertRaises(ValueError) as ctx:
|
|
47
|
+
UnitSystem(name="empty", bases={})
|
|
48
|
+
self.assertIn("base", str(ctx.exception).lower())
|
|
49
|
+
|
|
50
|
+
def test_mismatched_dimension_rejected(self):
|
|
51
|
+
# meter has Dimension.length, but we declare it as mass
|
|
52
|
+
with self.assertRaises(ValueError) as ctx:
|
|
53
|
+
UnitSystem(
|
|
54
|
+
name="bad",
|
|
55
|
+
bases={Dimension.mass: units.meter}
|
|
56
|
+
)
|
|
57
|
+
self.assertIn("dimension", str(ctx.exception).lower())
|
|
58
|
+
|
|
59
|
+
def test_partial_system_allowed(self):
|
|
60
|
+
# Imperial doesn't need mole or candela
|
|
61
|
+
system = UnitSystem(
|
|
62
|
+
name="Imperial",
|
|
63
|
+
bases={
|
|
64
|
+
Dimension.length: units.foot,
|
|
65
|
+
Dimension.mass: units.pound,
|
|
66
|
+
Dimension.time: units.second,
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
self.assertEqual(len(system.bases), 3)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestUnitSystemQueries(unittest.TestCase):
|
|
73
|
+
"""Test UnitSystem query methods."""
|
|
74
|
+
|
|
75
|
+
def setUp(self):
|
|
76
|
+
self.si = UnitSystem(
|
|
77
|
+
name="SI",
|
|
78
|
+
bases={
|
|
79
|
+
Dimension.length: units.meter,
|
|
80
|
+
Dimension.mass: units.kilogram,
|
|
81
|
+
Dimension.time: units.second,
|
|
82
|
+
Dimension.temperature: units.kelvin,
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_covers_returns_true_for_covered(self):
|
|
87
|
+
self.assertTrue(self.si.covers(Dimension.length))
|
|
88
|
+
self.assertTrue(self.si.covers(Dimension.mass))
|
|
89
|
+
self.assertTrue(self.si.covers(Dimension.time))
|
|
90
|
+
|
|
91
|
+
def test_covers_returns_false_for_uncovered(self):
|
|
92
|
+
self.assertFalse(self.si.covers(Dimension.current))
|
|
93
|
+
self.assertFalse(self.si.covers(Dimension.luminous_intensity))
|
|
94
|
+
|
|
95
|
+
def test_base_for_returns_correct_unit(self):
|
|
96
|
+
self.assertEqual(self.si.base_for(Dimension.length), units.meter)
|
|
97
|
+
self.assertEqual(self.si.base_for(Dimension.mass), units.kilogram)
|
|
98
|
+
self.assertEqual(self.si.base_for(Dimension.time), units.second)
|
|
99
|
+
|
|
100
|
+
def test_base_for_raises_for_uncovered(self):
|
|
101
|
+
with self.assertRaises(DimensionNotCovered) as ctx:
|
|
102
|
+
self.si.base_for(Dimension.current)
|
|
103
|
+
self.assertIn("current", str(ctx.exception).lower())
|
|
104
|
+
|
|
105
|
+
def test_dimensions_property(self):
|
|
106
|
+
dims = self.si.dimensions
|
|
107
|
+
self.assertIsInstance(dims, set)
|
|
108
|
+
self.assertEqual(len(dims), 4)
|
|
109
|
+
self.assertIn(Dimension.length, dims)
|
|
110
|
+
self.assertIn(Dimension.mass, dims)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestUnitSystemEquality(unittest.TestCase):
|
|
114
|
+
"""Test UnitSystem equality and hashing."""
|
|
115
|
+
|
|
116
|
+
def test_same_systems_equal(self):
|
|
117
|
+
s1 = UnitSystem(
|
|
118
|
+
name="SI",
|
|
119
|
+
bases={Dimension.length: units.meter}
|
|
120
|
+
)
|
|
121
|
+
s2 = UnitSystem(
|
|
122
|
+
name="SI",
|
|
123
|
+
bases={Dimension.length: units.meter}
|
|
124
|
+
)
|
|
125
|
+
self.assertEqual(s1, s2)
|
|
126
|
+
|
|
127
|
+
def test_different_names_not_equal(self):
|
|
128
|
+
s1 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
129
|
+
s2 = UnitSystem(name="CGS", bases={Dimension.length: units.meter})
|
|
130
|
+
self.assertNotEqual(s1, s2)
|
|
131
|
+
|
|
132
|
+
def test_different_bases_not_equal(self):
|
|
133
|
+
s1 = UnitSystem(name="test", bases={Dimension.length: units.meter})
|
|
134
|
+
s2 = UnitSystem(name="test", bases={Dimension.length: units.foot})
|
|
135
|
+
self.assertNotEqual(s1, s2)
|
|
136
|
+
|
|
137
|
+
def test_hashable(self):
|
|
138
|
+
s1 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
139
|
+
s2 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
140
|
+
self.assertEqual(hash(s1), hash(s2))
|
|
141
|
+
self.assertEqual(len({s1, s2}), 1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestUnitSystemImmutability(unittest.TestCase):
|
|
145
|
+
"""Test that UnitSystem is immutable."""
|
|
146
|
+
|
|
147
|
+
def test_frozen_dataclass(self):
|
|
148
|
+
system = UnitSystem(
|
|
149
|
+
name="SI",
|
|
150
|
+
bases={Dimension.length: units.meter}
|
|
151
|
+
)
|
|
152
|
+
with self.assertRaises(AttributeError):
|
|
153
|
+
system.name = "changed"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestPredefinedSystems(unittest.TestCase):
|
|
157
|
+
"""Test predefined unit systems in ucon.units."""
|
|
158
|
+
|
|
159
|
+
def test_si_system_exists(self):
|
|
160
|
+
from ucon.units import si
|
|
161
|
+
self.assertEqual(si.name, "SI")
|
|
162
|
+
self.assertTrue(si.covers(Dimension.length))
|
|
163
|
+
self.assertTrue(si.covers(Dimension.mass))
|
|
164
|
+
self.assertTrue(si.covers(Dimension.time))
|
|
165
|
+
|
|
166
|
+
def test_imperial_system_exists(self):
|
|
167
|
+
from ucon.units import imperial
|
|
168
|
+
self.assertEqual(imperial.name, "Imperial")
|
|
169
|
+
self.assertTrue(imperial.covers(Dimension.length))
|
|
170
|
+
self.assertTrue(imperial.covers(Dimension.mass))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
unittest.main()
|
|
@@ -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
|
|
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
|
-
'
|
|
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],
|
|
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
|
|
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
|
-
|
|
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
|