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.
- 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 +366 -53
- ucon/graph.py +415 -0
- ucon/maps.py +161 -0
- ucon/quantity.py +7 -186
- ucon/units.py +74 -31
- {ucon-0.3.5rc2.dist-info → ucon-0.4.0.dist-info}/METADATA +27 -9
- ucon-0.4.0.dist-info/RECORD +21 -0
- ucon-0.3.5rc2.dist-info/RECORD +0 -16
- {ucon-0.3.5rc2.dist-info → ucon-0.4.0.dist-info}/WHEEL +0 -0
- {ucon-0.3.5rc2.dist-info → ucon-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.3.5rc2.dist-info → ucon-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {ucon-0.3.5rc2.dist-info → ucon-0.4.0.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# © 2026 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from ucon import units
|
|
8
|
+
from ucon.core import Dimension, Scale, Unit, UnitFactor, UnitProduct
|
|
9
|
+
from ucon.graph import (
|
|
10
|
+
ConversionGraph,
|
|
11
|
+
DimensionMismatch,
|
|
12
|
+
ConversionNotFound,
|
|
13
|
+
CyclicInconsistency,
|
|
14
|
+
)
|
|
15
|
+
from ucon.maps import LinearMap, AffineMap
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestConversionGraphEdgeManagement(unittest.TestCase):
|
|
19
|
+
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.graph = ConversionGraph()
|
|
22
|
+
self.meter = units.meter
|
|
23
|
+
self.foot = Unit(name='foot', dimension=Dimension.length, aliases=('ft',))
|
|
24
|
+
self.inch = Unit(name='inch', dimension=Dimension.length, aliases=('in',))
|
|
25
|
+
self.gram = units.gram
|
|
26
|
+
|
|
27
|
+
def test_add_and_retrieve_edge(self):
|
|
28
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
29
|
+
m = self.graph.convert(src=self.meter, dst=self.foot)
|
|
30
|
+
self.assertAlmostEqual(m(1), 3.28084, places=4)
|
|
31
|
+
|
|
32
|
+
def test_inverse_auto_stored(self):
|
|
33
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
34
|
+
m = self.graph.convert(src=self.foot, dst=self.meter)
|
|
35
|
+
self.assertAlmostEqual(m(1), 0.3048, places=4)
|
|
36
|
+
|
|
37
|
+
def test_dimension_mismatch_rejected(self):
|
|
38
|
+
with self.assertRaises(DimensionMismatch):
|
|
39
|
+
self.graph.add_edge(src=self.meter, dst=self.gram, map=LinearMap(1))
|
|
40
|
+
|
|
41
|
+
def test_cyclic_consistency_check(self):
|
|
42
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
43
|
+
# Adding inconsistent reverse should raise
|
|
44
|
+
with self.assertRaises(CyclicInconsistency):
|
|
45
|
+
self.graph.add_edge(src=self.foot, dst=self.meter, map=LinearMap(0.5)) # wrong!
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestConversionGraphUnitConversion(unittest.TestCase):
|
|
49
|
+
|
|
50
|
+
def setUp(self):
|
|
51
|
+
self.graph = ConversionGraph()
|
|
52
|
+
self.meter = units.meter
|
|
53
|
+
self.foot = Unit(name='foot', dimension=Dimension.length, aliases=('ft',))
|
|
54
|
+
self.inch = Unit(name='inch', dimension=Dimension.length, aliases=('in',))
|
|
55
|
+
self.yard = Unit(name='yard', dimension=Dimension.length, aliases=('yd',))
|
|
56
|
+
|
|
57
|
+
def test_identity_conversion(self):
|
|
58
|
+
m = self.graph.convert(src=self.meter, dst=self.meter)
|
|
59
|
+
self.assertTrue(m.is_identity())
|
|
60
|
+
|
|
61
|
+
def test_direct_edge(self):
|
|
62
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
63
|
+
m = self.graph.convert(src=self.meter, dst=self.foot)
|
|
64
|
+
self.assertAlmostEqual(m(1), 3.28084, places=4)
|
|
65
|
+
|
|
66
|
+
def test_composed_path(self):
|
|
67
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
68
|
+
self.graph.add_edge(src=self.foot, dst=self.inch, map=LinearMap(12))
|
|
69
|
+
m = self.graph.convert(src=self.meter, dst=self.inch)
|
|
70
|
+
self.assertAlmostEqual(m(1), 39.37008, places=3)
|
|
71
|
+
|
|
72
|
+
def test_multi_hop_path(self):
|
|
73
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
74
|
+
self.graph.add_edge(src=self.foot, dst=self.inch, map=LinearMap(12))
|
|
75
|
+
self.graph.add_edge(src=self.inch, dst=self.yard, map=LinearMap(1/36))
|
|
76
|
+
m = self.graph.convert(src=self.meter, dst=self.yard)
|
|
77
|
+
self.assertAlmostEqual(m(1), 1.0936, places=3)
|
|
78
|
+
|
|
79
|
+
def test_no_path_raises(self):
|
|
80
|
+
mile = Unit(name='mile', dimension=Dimension.length, aliases=('mi',))
|
|
81
|
+
with self.assertRaises(ConversionNotFound):
|
|
82
|
+
self.graph.convert(src=self.meter, dst=mile)
|
|
83
|
+
|
|
84
|
+
def test_dimension_mismatch_on_convert(self):
|
|
85
|
+
with self.assertRaises(DimensionMismatch):
|
|
86
|
+
self.graph.convert(src=self.meter, dst=units.second)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TestConversionGraphProductConversion(unittest.TestCase):
|
|
90
|
+
|
|
91
|
+
def setUp(self):
|
|
92
|
+
self.graph = ConversionGraph()
|
|
93
|
+
self.meter = units.meter
|
|
94
|
+
self.second = units.second
|
|
95
|
+
self.mile = Unit(name='mile', dimension=Dimension.length, aliases=('mi',))
|
|
96
|
+
self.hour = Unit(name='hour', dimension=Dimension.time, aliases=('h',))
|
|
97
|
+
|
|
98
|
+
# Register unit conversions
|
|
99
|
+
self.graph.add_edge(src=self.meter, dst=self.mile, map=LinearMap(0.000621371))
|
|
100
|
+
self.graph.add_edge(src=self.second, dst=self.hour, map=LinearMap(1/3600))
|
|
101
|
+
|
|
102
|
+
def test_factorwise_velocity_conversion(self):
|
|
103
|
+
m_per_s = UnitProduct({
|
|
104
|
+
UnitFactor(self.meter, Scale.one): 1,
|
|
105
|
+
UnitFactor(self.second, Scale.one): -1,
|
|
106
|
+
})
|
|
107
|
+
mi_per_hr = UnitProduct({
|
|
108
|
+
UnitFactor(self.mile, Scale.one): 1,
|
|
109
|
+
UnitFactor(self.hour, Scale.one): -1,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
m = self.graph.convert(src=m_per_s, dst=mi_per_hr)
|
|
113
|
+
# 1 m/s = 2.23694 mi/h
|
|
114
|
+
self.assertAlmostEqual(m(1), 2.237, places=2)
|
|
115
|
+
|
|
116
|
+
def test_scale_ratio_in_factorwise(self):
|
|
117
|
+
km = UnitProduct({UnitFactor(self.meter, Scale.kilo): 1})
|
|
118
|
+
m = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
119
|
+
|
|
120
|
+
conversion = self.graph.convert(src=km, dst=m)
|
|
121
|
+
self.assertAlmostEqual(conversion(1), 1000, places=6)
|
|
122
|
+
|
|
123
|
+
def test_direct_product_edge(self):
|
|
124
|
+
# Define joule and watt_hour as UnitProducts
|
|
125
|
+
g = units.gram
|
|
126
|
+
joule = UnitProduct({
|
|
127
|
+
UnitFactor(g, Scale.one): 1,
|
|
128
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
129
|
+
UnitFactor(self.second, Scale.one): -2,
|
|
130
|
+
})
|
|
131
|
+
watt = UnitProduct({
|
|
132
|
+
UnitFactor(g, Scale.one): 1,
|
|
133
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
134
|
+
UnitFactor(self.second, Scale.one): -3,
|
|
135
|
+
})
|
|
136
|
+
watt_hour = watt * UnitProduct({UnitFactor(self.hour, Scale.one): 1})
|
|
137
|
+
|
|
138
|
+
# Register direct edge
|
|
139
|
+
self.graph.add_edge(src=joule, dst=watt_hour, map=LinearMap(1/3600))
|
|
140
|
+
|
|
141
|
+
m = self.graph.convert(src=joule, dst=watt_hour)
|
|
142
|
+
self.assertAlmostEqual(m(7200), 2.0, places=6)
|
|
143
|
+
|
|
144
|
+
def test_product_identity(self):
|
|
145
|
+
m_per_s = UnitProduct({
|
|
146
|
+
UnitFactor(self.meter, Scale.one): 1,
|
|
147
|
+
UnitFactor(self.second, Scale.one): -1,
|
|
148
|
+
})
|
|
149
|
+
m = self.graph.convert(src=m_per_s, dst=m_per_s)
|
|
150
|
+
self.assertTrue(m.is_identity())
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestConversionGraphTemperature(unittest.TestCase):
|
|
154
|
+
|
|
155
|
+
def setUp(self):
|
|
156
|
+
self.graph = ConversionGraph()
|
|
157
|
+
self.celsius = Unit(name='celsius', dimension=Dimension.temperature, aliases=('°C',))
|
|
158
|
+
self.kelvin = Unit(name='kelvin', dimension=Dimension.temperature, aliases=('K',))
|
|
159
|
+
self.fahrenheit = Unit(name='fahrenheit', dimension=Dimension.temperature, aliases=('°F',))
|
|
160
|
+
|
|
161
|
+
def test_celsius_to_kelvin(self):
|
|
162
|
+
self.graph.add_edge(src=self.celsius, dst=self.kelvin, map=AffineMap(1, 273.15))
|
|
163
|
+
m = self.graph.convert(src=self.celsius, dst=self.kelvin)
|
|
164
|
+
self.assertAlmostEqual(m(0), 273.15, places=2)
|
|
165
|
+
self.assertAlmostEqual(m(100), 373.15, places=2)
|
|
166
|
+
|
|
167
|
+
def test_celsius_to_fahrenheit_via_kelvin(self):
|
|
168
|
+
# C → K: K = C + 273.15
|
|
169
|
+
self.graph.add_edge(src=self.celsius, dst=self.kelvin, map=AffineMap(1, 273.15))
|
|
170
|
+
# F → K: K = (F - 32) * 5/9 + 273.15 = 5/9 * F + 255.372
|
|
171
|
+
self.graph.add_edge(src=self.fahrenheit, dst=self.kelvin, map=AffineMap(5/9, 255.372))
|
|
172
|
+
|
|
173
|
+
m = self.graph.convert(src=self.celsius, dst=self.fahrenheit)
|
|
174
|
+
self.assertAlmostEqual(m(0), 32, places=0)
|
|
175
|
+
self.assertAlmostEqual(m(100), 212, places=0)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from ucon.maps import AffineMap, ComposedMap, LinearMap, Map
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestLinearMap(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
def test_apply(self):
|
|
13
|
+
m = LinearMap(39.37)
|
|
14
|
+
self.assertAlmostEqual(m(1.0), 39.37)
|
|
15
|
+
self.assertAlmostEqual(m(0.0), 0.0)
|
|
16
|
+
self.assertAlmostEqual(m(2.5), 98.425)
|
|
17
|
+
|
|
18
|
+
def test_inverse(self):
|
|
19
|
+
m = LinearMap(39.37)
|
|
20
|
+
inv = m.inverse()
|
|
21
|
+
self.assertIsInstance(inv, LinearMap)
|
|
22
|
+
self.assertAlmostEqual(inv.a, 1.0 / 39.37)
|
|
23
|
+
|
|
24
|
+
def test_inverse_zero_raises(self):
|
|
25
|
+
m = LinearMap(0)
|
|
26
|
+
with self.assertRaises(ZeroDivisionError):
|
|
27
|
+
m.inverse()
|
|
28
|
+
|
|
29
|
+
def test_round_trip(self):
|
|
30
|
+
m = LinearMap(39.37)
|
|
31
|
+
for x in [0.0, 1.0, -5.5, 1000.0]:
|
|
32
|
+
self.assertAlmostEqual(m.inverse()(m(x)), x, places=10)
|
|
33
|
+
|
|
34
|
+
def test_compose_closed(self):
|
|
35
|
+
f = LinearMap(39.37)
|
|
36
|
+
g = LinearMap(1.0 / 12.0)
|
|
37
|
+
composed = f @ g
|
|
38
|
+
self.assertIsInstance(composed, LinearMap)
|
|
39
|
+
self.assertAlmostEqual(composed.a, 39.37 / 12.0)
|
|
40
|
+
|
|
41
|
+
def test_compose_apply(self):
|
|
42
|
+
f = LinearMap(2.0)
|
|
43
|
+
g = LinearMap(3.0)
|
|
44
|
+
# (f @ g)(x) = f(g(x)) = 2 * (3 * x) = 6x
|
|
45
|
+
self.assertAlmostEqual((f @ g)(5.0), 30.0)
|
|
46
|
+
|
|
47
|
+
def test_identity(self):
|
|
48
|
+
ident = LinearMap.identity()
|
|
49
|
+
self.assertAlmostEqual(ident(42.0), 42.0)
|
|
50
|
+
m = LinearMap(7.0)
|
|
51
|
+
self.assertEqual(m @ ident, m)
|
|
52
|
+
self.assertEqual(ident @ m, m)
|
|
53
|
+
|
|
54
|
+
def test_invertible(self):
|
|
55
|
+
self.assertTrue(LinearMap(5.0).invertible)
|
|
56
|
+
self.assertFalse(LinearMap(0).invertible)
|
|
57
|
+
|
|
58
|
+
def test_eq(self):
|
|
59
|
+
self.assertEqual(LinearMap(3.0), LinearMap(3.0))
|
|
60
|
+
self.assertNotEqual(LinearMap(3.0), LinearMap(4.0))
|
|
61
|
+
|
|
62
|
+
def test_repr(self):
|
|
63
|
+
self.assertIn("39.37", repr(LinearMap(39.37)))
|
|
64
|
+
|
|
65
|
+
def test_matmul_non_map_returns_not_implemented(self):
|
|
66
|
+
m = LinearMap(2.0)
|
|
67
|
+
result = m.__matmul__(42)
|
|
68
|
+
self.assertIs(result, NotImplemented)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestAffineMap(unittest.TestCase):
|
|
72
|
+
|
|
73
|
+
def test_apply(self):
|
|
74
|
+
# Celsius to Fahrenheit: F = 1.8 * C + 32
|
|
75
|
+
c_to_f = AffineMap(1.8, 32.0)
|
|
76
|
+
self.assertAlmostEqual(c_to_f(0.0), 32.0)
|
|
77
|
+
self.assertAlmostEqual(c_to_f(100.0), 212.0)
|
|
78
|
+
self.assertAlmostEqual(c_to_f(-40.0), -40.0)
|
|
79
|
+
|
|
80
|
+
def test_inverse(self):
|
|
81
|
+
c_to_f = AffineMap(1.8, 32.0)
|
|
82
|
+
f_to_c = c_to_f.inverse()
|
|
83
|
+
self.assertIsInstance(f_to_c, AffineMap)
|
|
84
|
+
self.assertAlmostEqual(f_to_c(32.0), 0.0)
|
|
85
|
+
self.assertAlmostEqual(f_to_c(212.0), 100.0)
|
|
86
|
+
|
|
87
|
+
def test_inverse_zero_raises(self):
|
|
88
|
+
m = AffineMap(0, 5.0)
|
|
89
|
+
with self.assertRaises(ZeroDivisionError):
|
|
90
|
+
m.inverse()
|
|
91
|
+
|
|
92
|
+
def test_round_trip(self):
|
|
93
|
+
m = AffineMap(1.8, 32.0)
|
|
94
|
+
for x in [0.0, 100.0, -40.0, 37.5]:
|
|
95
|
+
self.assertAlmostEqual(m.inverse()(m(x)), x, places=10)
|
|
96
|
+
|
|
97
|
+
def test_compose_closed(self):
|
|
98
|
+
f = AffineMap(2.0, 3.0)
|
|
99
|
+
g = AffineMap(4.0, 5.0)
|
|
100
|
+
composed = f @ g
|
|
101
|
+
self.assertIsInstance(composed, AffineMap)
|
|
102
|
+
# f(g(x)) = 2*(4x+5)+3 = 8x+13
|
|
103
|
+
self.assertAlmostEqual(composed.a, 8.0)
|
|
104
|
+
self.assertAlmostEqual(composed.b, 13.0)
|
|
105
|
+
|
|
106
|
+
def test_compose_apply(self):
|
|
107
|
+
f = AffineMap(2.0, 3.0)
|
|
108
|
+
g = AffineMap(4.0, 5.0)
|
|
109
|
+
for x in [0.0, 1.0, -2.0]:
|
|
110
|
+
self.assertAlmostEqual((f @ g)(x), f(g(x)), places=10)
|
|
111
|
+
|
|
112
|
+
def test_invertible(self):
|
|
113
|
+
self.assertTrue(AffineMap(1.8, 32.0).invertible)
|
|
114
|
+
self.assertFalse(AffineMap(0, 32.0).invertible)
|
|
115
|
+
|
|
116
|
+
def test_eq(self):
|
|
117
|
+
self.assertEqual(AffineMap(1.8, 32.0), AffineMap(1.8, 32.0))
|
|
118
|
+
self.assertNotEqual(AffineMap(1.8, 32.0), AffineMap(1.8, 0.0))
|
|
119
|
+
|
|
120
|
+
def test_repr(self):
|
|
121
|
+
r = repr(AffineMap(1.8, 32.0))
|
|
122
|
+
self.assertIn("1.8", r)
|
|
123
|
+
self.assertIn("32.0", r)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestComposedMap(unittest.TestCase):
|
|
127
|
+
|
|
128
|
+
def test_heterogeneous_composition(self):
|
|
129
|
+
# LinearMap @ AffineMap now returns AffineMap (closed composition)
|
|
130
|
+
# Use ComposedMap directly to test the fallback behavior
|
|
131
|
+
lin = LinearMap(2.0)
|
|
132
|
+
aff = AffineMap(3.0, 1.0)
|
|
133
|
+
composed = ComposedMap(lin, aff)
|
|
134
|
+
# lin(aff(x)) = 2 * (3x + 1) = 6x + 2
|
|
135
|
+
self.assertAlmostEqual(composed(0.0), 2.0)
|
|
136
|
+
self.assertAlmostEqual(composed(1.0), 8.0)
|
|
137
|
+
|
|
138
|
+
def test_inverse(self):
|
|
139
|
+
composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
|
|
140
|
+
for x in [0.0, 1.0, -3.0, 10.0]:
|
|
141
|
+
self.assertAlmostEqual(composed.inverse()(composed(x)), x, places=10)
|
|
142
|
+
|
|
143
|
+
def test_invertible(self):
|
|
144
|
+
self.assertTrue(ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0)).invertible)
|
|
145
|
+
self.assertFalse(ComposedMap(LinearMap(0), AffineMap(3.0, 1.0)).invertible)
|
|
146
|
+
|
|
147
|
+
def test_non_invertible_raises(self):
|
|
148
|
+
composed = ComposedMap(LinearMap(0), AffineMap(3.0, 1.0))
|
|
149
|
+
with self.assertRaises(ValueError):
|
|
150
|
+
composed.inverse()
|
|
151
|
+
|
|
152
|
+
def test_repr(self):
|
|
153
|
+
composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
|
|
154
|
+
r = repr(composed)
|
|
155
|
+
self.assertIn("LinearMap", r)
|
|
156
|
+
self.assertIn("AffineMap", r)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TestMapABC(unittest.TestCase):
|
|
160
|
+
|
|
161
|
+
def test_cannot_instantiate(self):
|
|
162
|
+
with self.assertRaises(TypeError):
|
|
163
|
+
Map()
|
tests/ucon/test_algebra.py
CHANGED
|
@@ -11,41 +11,41 @@ from ucon.algebra import Exponent, Vector
|
|
|
11
11
|
class TestVector(TestCase):
|
|
12
12
|
|
|
13
13
|
def test_vector_iteration_and_length(self):
|
|
14
|
-
v = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
15
|
-
self.assertEqual(tuple(v), (1, 0, 0, 0, 0, 0, 0))
|
|
16
|
-
self.assertEqual(len(v),
|
|
14
|
+
v = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
15
|
+
self.assertEqual(tuple(v), (1, 0, 0, 0, 0, 0, 0, 0))
|
|
16
|
+
self.assertEqual(len(v), 8) # 7 SI + 1 information
|
|
17
17
|
|
|
18
18
|
def test_vector_addition(self):
|
|
19
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
20
|
-
v2 = Vector(0, 2, 0, 0, 0, 0, 0)
|
|
19
|
+
v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
20
|
+
v2 = Vector(0, 2, 0, 0, 0, 0, 0, 0)
|
|
21
21
|
result = v1 + v2
|
|
22
|
-
self.assertEqual(result, Vector(1, 2, 0, 0, 0, 0, 0))
|
|
22
|
+
self.assertEqual(result, Vector(1, 2, 0, 0, 0, 0, 0, 0))
|
|
23
23
|
|
|
24
24
|
def test_vector_subtraction(self):
|
|
25
|
-
v1 = Vector(2, 1, 0, 0, 0, 0, 0)
|
|
26
|
-
v2 = Vector(1, 1, 0, 0, 0, 0, 0)
|
|
27
|
-
self.assertEqual(v1 - v2, Vector(1, 0, 0, 0, 0, 0, 0))
|
|
25
|
+
v1 = Vector(2, 1, 0, 0, 0, 0, 0, 0)
|
|
26
|
+
v2 = Vector(1, 1, 0, 0, 0, 0, 0, 0)
|
|
27
|
+
self.assertEqual(v1 - v2, Vector(1, 0, 0, 0, 0, 0, 0, 0))
|
|
28
28
|
|
|
29
29
|
def test_vector_scalar_multiplication_by_integer(self):
|
|
30
|
-
v = Vector(1, -2, 0, 0, 0, 0, 3)
|
|
30
|
+
v = Vector(1, -2, 0, 0, 0, 0, 3, 0)
|
|
31
31
|
scaled = v * 2
|
|
32
|
-
self.assertEqual(scaled, Vector(2, -4, 0, 0, 0, 0, 6))
|
|
33
|
-
self.assertEqual(v, Vector(1, -2, 0, 0, 0, 0, 3)) # original unchanged
|
|
32
|
+
self.assertEqual(scaled, Vector(2, -4, 0, 0, 0, 0, 6, 0))
|
|
33
|
+
self.assertEqual(v, Vector(1, -2, 0, 0, 0, 0, 3, 0)) # original unchanged
|
|
34
34
|
|
|
35
35
|
def test_vector_scalar_multiplication_by_float(self):
|
|
36
|
-
v = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
36
|
+
v = Vector(0, 1, 0, 0, 0, 0, 0, 0)
|
|
37
37
|
scaled = v * 0.5
|
|
38
|
-
self.assertEqual(scaled, Vector(0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0))
|
|
38
|
+
self.assertEqual(scaled, Vector(0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0))
|
|
39
39
|
|
|
40
40
|
def test_vector_scalar_multiplication_by_zero(self):
|
|
41
|
-
v = Vector(1, 2, 3, 4, 5, 6, 7)
|
|
41
|
+
v = Vector(1, 2, 3, 4, 5, 6, 7, 8)
|
|
42
42
|
zeroed = v * 0
|
|
43
|
-
self.assertEqual(zeroed, Vector(0, 0, 0, 0, 0, 0, 0))
|
|
43
|
+
self.assertEqual(zeroed, Vector(0, 0, 0, 0, 0, 0, 0, 0))
|
|
44
44
|
|
|
45
45
|
def test_vector_equality_and_hash(self):
|
|
46
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
47
|
-
v2 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
48
|
-
v3 = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
46
|
+
v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
47
|
+
v2 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
48
|
+
v3 = Vector(0, 1, 0, 0, 0, 0, 0, 0)
|
|
49
49
|
self.assertTrue(v1 == v2)
|
|
50
50
|
self.assertFalse(v1 == v3)
|
|
51
51
|
self.assertEqual(hash(v1), hash(v2))
|
|
@@ -56,17 +56,17 @@ class TestVectorEdgeCases(TestCase):
|
|
|
56
56
|
|
|
57
57
|
def test_zero_vector_equality_and_additivity(self):
|
|
58
58
|
zero = Vector()
|
|
59
|
-
self.assertEqual(zero, Vector(0, 0, 0, 0, 0, 0, 0))
|
|
59
|
+
self.assertEqual(zero, Vector(0, 0, 0, 0, 0, 0, 0, 0))
|
|
60
60
|
# Adding or subtracting zero should yield same vector
|
|
61
|
-
v = Vector(1, 2, 3, 4, 5, 6, 7)
|
|
61
|
+
v = Vector(1, 2, 3, 4, 5, 6, 7, 8)
|
|
62
62
|
self.assertEqual(v + zero, v)
|
|
63
63
|
self.assertEqual(v - zero, v)
|
|
64
64
|
|
|
65
65
|
def test_vector_with_negative_exponents(self):
|
|
66
|
-
v1 = Vector(1, -2, 3, 0, 0, 0, 0)
|
|
67
|
-
v2 = Vector(-1, 2, -3, 0, 0, 0, 0)
|
|
66
|
+
v1 = Vector(1, -2, 3, 0, 0, 0, 0, 0)
|
|
67
|
+
v2 = Vector(-1, 2, -3, 0, 0, 0, 0, 0)
|
|
68
68
|
result = v1 + v2
|
|
69
|
-
self.assertEqual(result, Vector(0, 0, 0, 0, 0, 0, 0))
|
|
69
|
+
self.assertEqual(result, Vector(0, 0, 0, 0, 0, 0, 0, 0))
|
|
70
70
|
self.assertEqual(v1 - v1, Vector()) # perfect cancellation
|
|
71
71
|
|
|
72
72
|
def test_vector_equality_with_non_vector(self):
|
|
@@ -77,27 +77,27 @@ class TestVectorEdgeCases(TestCase):
|
|
|
77
77
|
v == None
|
|
78
78
|
|
|
79
79
|
def test_hash_consistency_for_equal_vectors(self):
|
|
80
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
81
|
-
v2 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
80
|
+
v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
81
|
+
v2 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
82
82
|
self.assertEqual(hash(v1), hash(v2))
|
|
83
83
|
self.assertEqual(len({v1, v2}), 1)
|
|
84
84
|
|
|
85
85
|
def test_iter_length_order_consistency(self):
|
|
86
|
-
v = Vector(1, 2, 3, 4, 5, 6, 7)
|
|
86
|
+
v = Vector(1, 2, 3, 4, 5, 6, 7, 8)
|
|
87
87
|
components = list(v)
|
|
88
88
|
self.assertEqual(len(components), len(v))
|
|
89
|
-
# Ensure order of iteration is fixed (T→L→M→I→Θ→J→N)
|
|
90
|
-
self.assertEqual(components, [1, 2, 3, 4, 5, 6, 7])
|
|
89
|
+
# Ensure order of iteration is fixed (T→L→M→I→Θ→J→N→B)
|
|
90
|
+
self.assertEqual(components, [1, 2, 3, 4, 5, 6, 7, 8])
|
|
91
91
|
|
|
92
92
|
def test_vector_arithmetic_does_not_mutate_operands(self):
|
|
93
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
94
|
-
v2 = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
93
|
+
v1 = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
94
|
+
v2 = Vector(0, 1, 0, 0, 0, 0, 0, 0)
|
|
95
95
|
_ = v1 + v2
|
|
96
|
-
self.assertEqual(v1, Vector(1, 0, 0, 0, 0, 0, 0))
|
|
97
|
-
self.assertEqual(v2, Vector(0, 1, 0, 0, 0, 0, 0))
|
|
96
|
+
self.assertEqual(v1, Vector(1, 0, 0, 0, 0, 0, 0, 0))
|
|
97
|
+
self.assertEqual(v2, Vector(0, 1, 0, 0, 0, 0, 0, 0))
|
|
98
98
|
|
|
99
99
|
def test_invalid_addition_type_raises(self):
|
|
100
|
-
v = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
100
|
+
v = Vector(1, 0, 0, 0, 0, 0, 0, 0)
|
|
101
101
|
with self.assertRaises(TypeError):
|
|
102
102
|
_ = v + "length"
|
|
103
103
|
with self.assertRaises(TypeError):
|
tests/ucon/test_core.py
CHANGED
|
@@ -451,7 +451,7 @@ class TestUnit(unittest.TestCase):
|
|
|
451
451
|
unit_name = 'second'
|
|
452
452
|
unit_type = 'time'
|
|
453
453
|
unit_aliases = ('seconds', 'secs', 's', 'S')
|
|
454
|
-
unit = Unit(
|
|
454
|
+
unit = Unit(name=unit_name, dimension=Dimension.time, aliases=unit_aliases)
|
|
455
455
|
|
|
456
456
|
def test___repr__(self):
|
|
457
457
|
self.assertEqual(f'<Unit {self.unit_aliases[0]}>', str(self.unit))
|
|
@@ -464,12 +464,12 @@ class TestUnit(unittest.TestCase):
|
|
|
464
464
|
|
|
465
465
|
def test_unit_equality_alias_normalization(self):
|
|
466
466
|
# ('',) should normalize to () under _norm
|
|
467
|
-
u1 = Unit(
|
|
467
|
+
u1 = Unit(name="x", dimension=Dimension.length, aliases=("",))
|
|
468
468
|
u2 = Unit(name="x", dimension=Dimension.length)
|
|
469
469
|
self.assertEqual(u1, u2)
|
|
470
470
|
|
|
471
471
|
def test_unit_invalid_eq_type(self):
|
|
472
|
-
self.assertFalse(Unit("
|
|
472
|
+
self.assertFalse(Unit(name="meter", dimension=Dimension.length, aliases=("m",)) == "meter")
|
|
473
473
|
|
|
474
474
|
|
|
475
475
|
class TestUnitProduct(unittest.TestCase):
|
|
@@ -544,7 +544,7 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
544
544
|
self.assertEqual(repr(u), '<Unit>')
|
|
545
545
|
|
|
546
546
|
def test_unit_with_aliases_and_name(self):
|
|
547
|
-
u = Unit(
|
|
547
|
+
u = Unit(name='meter', dimension=Dimension.length, aliases=('m', 'M'))
|
|
548
548
|
self.assertEqual(u.shorthand, 'm')
|
|
549
549
|
self.assertIn('m', u.aliases)
|
|
550
550
|
self.assertIn('M', u.aliases)
|
|
@@ -553,9 +553,9 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
553
553
|
self.assertIn('<Unit m>', repr(u))
|
|
554
554
|
|
|
555
555
|
def test_hash_and_equality_consistency(self):
|
|
556
|
-
u1 = Unit(
|
|
557
|
-
u2 = Unit(
|
|
558
|
-
u3 = Unit(
|
|
556
|
+
u1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
557
|
+
u2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
558
|
+
u3 = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
559
559
|
self.assertEqual(u1, u2)
|
|
560
560
|
self.assertEqual(hash(u1), hash(u2))
|
|
561
561
|
self.assertNotEqual(u1, u3)
|
|
@@ -569,36 +569,36 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
569
569
|
# --- arithmetic behavior ----------------------------------------------
|
|
570
570
|
|
|
571
571
|
def test_multiplication_produces_composite_unit(self):
|
|
572
|
-
m = Unit(
|
|
573
|
-
s = Unit(
|
|
572
|
+
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
573
|
+
s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
574
574
|
v = m / s
|
|
575
575
|
self.assertIsInstance(v, UnitProduct)
|
|
576
576
|
self.assertEqual(v.dimension, Dimension.velocity)
|
|
577
577
|
self.assertIn('/', repr(v))
|
|
578
578
|
|
|
579
579
|
def test_division_with_dimensionless_denominator_returns_self(self):
|
|
580
|
-
m = Unit(
|
|
580
|
+
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
581
581
|
none = Unit(name='none', dimension=Dimension.none)
|
|
582
582
|
result = m / none
|
|
583
583
|
self.assertEqual(result, m)
|
|
584
584
|
|
|
585
585
|
def test_division_of_identical_units_returns_dimensionless(self):
|
|
586
|
-
m1 = Unit(
|
|
587
|
-
m2 = Unit(
|
|
586
|
+
m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
587
|
+
m2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
588
588
|
result = m1 / m2
|
|
589
589
|
self.assertEqual(result.dimension, Dimension.none)
|
|
590
590
|
self.assertEqual(result.name, '')
|
|
591
591
|
|
|
592
592
|
def test_multiplying_with_dimensionless_returns_self(self):
|
|
593
|
-
m = Unit(
|
|
593
|
+
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
594
594
|
none = Unit(name='none', dimension=Dimension.none)
|
|
595
595
|
result = m * none
|
|
596
596
|
self.assertEqual(result.dimension, Dimension.length)
|
|
597
597
|
self.assertEqual('m', result.shorthand)
|
|
598
598
|
|
|
599
599
|
def test_invalid_dimension_combinations_raise_value_error(self):
|
|
600
|
-
m = Unit(
|
|
601
|
-
c = Unit(
|
|
600
|
+
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
601
|
+
c = Unit(name='coulomb', dimension=Dimension.charge, aliases=('C',))
|
|
602
602
|
# The result of combination gives CompositeUnit
|
|
603
603
|
self.assertIsInstance(m / c, UnitProduct)
|
|
604
604
|
self.assertIsInstance(m * c, UnitProduct)
|
|
@@ -606,16 +606,16 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
606
606
|
# --- equality, hashing, immutability ----------------------------------
|
|
607
607
|
|
|
608
608
|
def test_equality_with_non_unit(self):
|
|
609
|
-
self.assertFalse(Unit(
|
|
609
|
+
self.assertFalse(Unit(name='meter', dimension=Dimension.length, aliases=('m',)) == 'meter')
|
|
610
610
|
|
|
611
611
|
def test_hash_stability_in_collections(self):
|
|
612
|
-
m1 = Unit(
|
|
612
|
+
m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
613
613
|
s = set([m1])
|
|
614
|
-
self.assertIn(Unit(
|
|
614
|
+
self.assertIn(Unit(name='meter', dimension=Dimension.length, aliases=('m',)), s)
|
|
615
615
|
|
|
616
616
|
def test_operations_do_not_mutate_operands(self):
|
|
617
|
-
m = Unit(
|
|
618
|
-
s = Unit(
|
|
617
|
+
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
618
|
+
s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
619
619
|
_ = m / s
|
|
620
620
|
self.assertEqual(m.dimension, Dimension.length)
|
|
621
621
|
self.assertEqual(s.dimension, Dimension.time)
|