ucon 0.3.5rc2__py3-none-any.whl → 0.4.1__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,409 @@
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
+ def test_matmul_with_affine(self):
71
+ lin = LinearMap(2.0)
72
+ aff = AffineMap(3.0, 5.0)
73
+ composed = lin @ aff
74
+ # lin(aff(x)) = 2 * (3x + 5) = 6x + 10
75
+ self.assertIsInstance(composed, AffineMap)
76
+ self.assertAlmostEqual(composed.a, 6.0)
77
+ self.assertAlmostEqual(composed.b, 10.0)
78
+ self.assertAlmostEqual(composed(1.0), 16.0)
79
+
80
+ def test_pow(self):
81
+ m = LinearMap(3.0)
82
+ squared = m ** 2
83
+ self.assertIsInstance(squared, LinearMap)
84
+ self.assertAlmostEqual(squared.a, 9.0)
85
+
86
+ def test_pow_negative(self):
87
+ m = LinearMap(4.0)
88
+ inv = m ** -1
89
+ self.assertIsInstance(inv, LinearMap)
90
+ self.assertAlmostEqual(inv.a, 0.25)
91
+
92
+ def test_pow_fractional(self):
93
+ m = LinearMap(4.0)
94
+ sqrt = m ** 0.5
95
+ self.assertIsInstance(sqrt, LinearMap)
96
+ self.assertAlmostEqual(sqrt.a, 2.0)
97
+
98
+ def test_is_identity_true(self):
99
+ m = LinearMap(1.0)
100
+ self.assertTrue(m.is_identity())
101
+
102
+ def test_is_identity_false(self):
103
+ m = LinearMap(2.0)
104
+ self.assertFalse(m.is_identity())
105
+
106
+ def test_is_identity_near_one(self):
107
+ m = LinearMap(1.0 + 1e-10)
108
+ self.assertTrue(m.is_identity())
109
+
110
+ def test_hash(self):
111
+ m1 = LinearMap(3.0)
112
+ m2 = LinearMap(3.0)
113
+ self.assertEqual(hash(m1), hash(m2))
114
+ self.assertEqual(len({m1, m2}), 1)
115
+
116
+
117
+ class TestAffineMap(unittest.TestCase):
118
+
119
+ def test_apply(self):
120
+ # Celsius to Fahrenheit: F = 1.8 * C + 32
121
+ c_to_f = AffineMap(1.8, 32.0)
122
+ self.assertAlmostEqual(c_to_f(0.0), 32.0)
123
+ self.assertAlmostEqual(c_to_f(100.0), 212.0)
124
+ self.assertAlmostEqual(c_to_f(-40.0), -40.0)
125
+
126
+ def test_inverse(self):
127
+ c_to_f = AffineMap(1.8, 32.0)
128
+ f_to_c = c_to_f.inverse()
129
+ self.assertIsInstance(f_to_c, AffineMap)
130
+ self.assertAlmostEqual(f_to_c(32.0), 0.0)
131
+ self.assertAlmostEqual(f_to_c(212.0), 100.0)
132
+
133
+ def test_inverse_zero_raises(self):
134
+ m = AffineMap(0, 5.0)
135
+ with self.assertRaises(ZeroDivisionError):
136
+ m.inverse()
137
+
138
+ def test_round_trip(self):
139
+ m = AffineMap(1.8, 32.0)
140
+ for x in [0.0, 100.0, -40.0, 37.5]:
141
+ self.assertAlmostEqual(m.inverse()(m(x)), x, places=10)
142
+
143
+ def test_compose_closed(self):
144
+ f = AffineMap(2.0, 3.0)
145
+ g = AffineMap(4.0, 5.0)
146
+ composed = f @ g
147
+ self.assertIsInstance(composed, AffineMap)
148
+ # f(g(x)) = 2*(4x+5)+3 = 8x+13
149
+ self.assertAlmostEqual(composed.a, 8.0)
150
+ self.assertAlmostEqual(composed.b, 13.0)
151
+
152
+ def test_compose_apply(self):
153
+ f = AffineMap(2.0, 3.0)
154
+ g = AffineMap(4.0, 5.0)
155
+ for x in [0.0, 1.0, -2.0]:
156
+ self.assertAlmostEqual((f @ g)(x), f(g(x)), places=10)
157
+
158
+ def test_invertible(self):
159
+ self.assertTrue(AffineMap(1.8, 32.0).invertible)
160
+ self.assertFalse(AffineMap(0, 32.0).invertible)
161
+
162
+ def test_eq(self):
163
+ self.assertEqual(AffineMap(1.8, 32.0), AffineMap(1.8, 32.0))
164
+ self.assertNotEqual(AffineMap(1.8, 32.0), AffineMap(1.8, 0.0))
165
+
166
+ def test_repr(self):
167
+ r = repr(AffineMap(1.8, 32.0))
168
+ self.assertIn("1.8", r)
169
+ self.assertIn("32.0", r)
170
+
171
+ def test_matmul_with_linear(self):
172
+ aff = AffineMap(2.0, 3.0)
173
+ lin = LinearMap(4.0)
174
+ composed = aff @ lin
175
+ # aff(lin(x)) = 2 * (4x) + 3 = 8x + 3
176
+ self.assertIsInstance(composed, AffineMap)
177
+ self.assertAlmostEqual(composed.a, 8.0)
178
+ self.assertAlmostEqual(composed.b, 3.0)
179
+ self.assertAlmostEqual(composed(1.0), 11.0)
180
+
181
+ def test_matmul_non_map_returns_not_implemented(self):
182
+ m = AffineMap(1.8, 32.0)
183
+ result = m.__matmul__("not a map")
184
+ self.assertIs(result, NotImplemented)
185
+
186
+ def test_pow_one(self):
187
+ m = AffineMap(1.8, 32.0)
188
+ result = m ** 1
189
+ self.assertIs(result, m)
190
+
191
+ def test_pow_negative_one(self):
192
+ m = AffineMap(1.8, 32.0)
193
+ result = m ** -1
194
+ inv = m.inverse()
195
+ self.assertEqual(result.a, inv.a)
196
+ self.assertEqual(result.b, inv.b)
197
+
198
+ def test_pow_invalid_raises(self):
199
+ m = AffineMap(1.8, 32.0)
200
+ with self.assertRaises(ValueError) as ctx:
201
+ m ** 2
202
+ self.assertIn("only supports exp=1 or exp=-1", str(ctx.exception))
203
+
204
+ def test_is_identity_true(self):
205
+ m = AffineMap(1.0, 0.0)
206
+ self.assertTrue(m.is_identity())
207
+
208
+ def test_is_identity_false_due_to_offset(self):
209
+ m = AffineMap(1.0, 5.0)
210
+ self.assertFalse(m.is_identity())
211
+
212
+ def test_is_identity_false_due_to_scale(self):
213
+ m = AffineMap(2.0, 0.0)
214
+ self.assertFalse(m.is_identity())
215
+
216
+ def test_hash(self):
217
+ m1 = AffineMap(1.8, 32.0)
218
+ m2 = AffineMap(1.8, 32.0)
219
+ self.assertEqual(hash(m1), hash(m2))
220
+ self.assertEqual(len({m1, m2}), 1)
221
+
222
+
223
+ class TestComposedMap(unittest.TestCase):
224
+
225
+ def test_heterogeneous_composition(self):
226
+ # LinearMap @ AffineMap now returns AffineMap (closed composition)
227
+ # Use ComposedMap directly to test the fallback behavior
228
+ lin = LinearMap(2.0)
229
+ aff = AffineMap(3.0, 1.0)
230
+ composed = ComposedMap(lin, aff)
231
+ # lin(aff(x)) = 2 * (3x + 1) = 6x + 2
232
+ self.assertAlmostEqual(composed(0.0), 2.0)
233
+ self.assertAlmostEqual(composed(1.0), 8.0)
234
+
235
+ def test_inverse(self):
236
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
237
+ for x in [0.0, 1.0, -3.0, 10.0]:
238
+ self.assertAlmostEqual(composed.inverse()(composed(x)), x, places=10)
239
+
240
+ def test_invertible(self):
241
+ self.assertTrue(ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0)).invertible)
242
+ self.assertFalse(ComposedMap(LinearMap(0), AffineMap(3.0, 1.0)).invertible)
243
+
244
+ def test_non_invertible_raises(self):
245
+ composed = ComposedMap(LinearMap(0), AffineMap(3.0, 1.0))
246
+ with self.assertRaises(ValueError):
247
+ composed.inverse()
248
+
249
+ def test_repr(self):
250
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
251
+ r = repr(composed)
252
+ self.assertIn("LinearMap", r)
253
+ self.assertIn("AffineMap", r)
254
+
255
+ def test_matmul(self):
256
+ c1 = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
257
+ c2 = LinearMap(5.0)
258
+ composed = c1 @ c2
259
+ self.assertIsInstance(composed, ComposedMap)
260
+ # c1(c2(x)) = c1(5x) = 2*(3*5x + 1) = 30x + 2
261
+ self.assertAlmostEqual(composed(1.0), 32.0)
262
+ self.assertAlmostEqual(composed(0.0), 2.0)
263
+
264
+ def test_matmul_non_map_returns_not_implemented(self):
265
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
266
+ result = composed.__matmul__(42)
267
+ self.assertIs(result, NotImplemented)
268
+
269
+ def test_pow_one(self):
270
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
271
+ result = composed ** 1
272
+ self.assertIs(result, composed)
273
+
274
+ def test_pow_negative_one(self):
275
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
276
+ result = composed ** -1
277
+ # Round-trip should be identity
278
+ for x in [0.0, 1.0, 5.0]:
279
+ self.assertAlmostEqual(result(composed(x)), x, places=10)
280
+
281
+ def test_pow_invalid_raises(self):
282
+ composed = ComposedMap(LinearMap(2.0), AffineMap(3.0, 1.0))
283
+ with self.assertRaises(ValueError) as ctx:
284
+ composed ** 2
285
+ self.assertIn("only supports exp=1 or exp=-1", str(ctx.exception))
286
+
287
+ def test_invertible_both_non_invertible(self):
288
+ composed = ComposedMap(LinearMap(0), LinearMap(0))
289
+ self.assertFalse(composed.invertible)
290
+
291
+ def test_invertible_inner_non_invertible(self):
292
+ composed = ComposedMap(LinearMap(2.0), LinearMap(0))
293
+ self.assertFalse(composed.invertible)
294
+
295
+ def test_is_identity(self):
296
+ composed = ComposedMap(LinearMap(1.0), LinearMap(1.0))
297
+ self.assertTrue(composed.is_identity())
298
+
299
+ def test_is_identity_false(self):
300
+ composed = ComposedMap(LinearMap(2.0), LinearMap(0.5))
301
+ self.assertTrue(composed.is_identity()) # 2 * 0.5 = 1
302
+
303
+ def test_is_identity_with_offset(self):
304
+ composed = ComposedMap(LinearMap(1.0), AffineMap(1.0, 5.0))
305
+ self.assertFalse(composed.is_identity())
306
+
307
+
308
+ class TestMapABC(unittest.TestCase):
309
+
310
+ def test_cannot_instantiate(self):
311
+ with self.assertRaises(TypeError):
312
+ Map()
313
+
314
+
315
+ class TestCrossTypeComposition(unittest.TestCase):
316
+ """Tests for composition between different Map types."""
317
+
318
+ def test_linear_at_affine_at_linear(self):
319
+ """Chain: LinearMap @ AffineMap @ LinearMap"""
320
+ l1 = LinearMap(2.0)
321
+ a = AffineMap(3.0, 1.0)
322
+ l2 = LinearMap(4.0)
323
+ # l1 @ a = AffineMap(6, 2)
324
+ # (l1 @ a) @ l2 = AffineMap(24, 2)
325
+ composed = l1 @ a @ l2
326
+ self.assertIsInstance(composed, AffineMap)
327
+ self.assertAlmostEqual(composed(1.0), 26.0)
328
+
329
+ def test_affine_at_linear_at_affine(self):
330
+ """Chain: AffineMap @ LinearMap @ AffineMap"""
331
+ a1 = AffineMap(2.0, 1.0)
332
+ l = LinearMap(3.0)
333
+ a2 = AffineMap(4.0, 5.0)
334
+ # l @ a2 = AffineMap(12, 15)
335
+ # a1 @ (l @ a2) = AffineMap(24, 31)
336
+ composed = a1 @ l @ a2
337
+ self.assertIsInstance(composed, AffineMap)
338
+ self.assertAlmostEqual(composed(1.0), 55.0)
339
+
340
+ def test_composed_preserves_semantics(self):
341
+ """Verify f @ g computes f(g(x)) correctly for all type combinations."""
342
+ maps = [
343
+ LinearMap(2.0),
344
+ LinearMap(0.5),
345
+ AffineMap(3.0, 1.0),
346
+ AffineMap(1.0, -5.0),
347
+ ]
348
+ for f in maps:
349
+ for g in maps:
350
+ composed = f @ g
351
+ for x in [0.0, 1.0, -2.0, 10.0]:
352
+ expected = f(g(x))
353
+ actual = composed(x)
354
+ self.assertAlmostEqual(actual, expected, places=10,
355
+ msg=f"Failed for {type(f).__name__} @ {type(g).__name__} at x={x}")
356
+
357
+
358
+ class TestMapEdgeCases(unittest.TestCase):
359
+ """Edge case tests for Map hierarchy."""
360
+
361
+ def test_linear_map_with_negative_scale(self):
362
+ m = LinearMap(-3.0)
363
+ self.assertAlmostEqual(m(5.0), -15.0)
364
+ self.assertAlmostEqual(m.inverse()(m(5.0)), 5.0)
365
+
366
+ def test_affine_map_with_negative_scale(self):
367
+ m = AffineMap(-2.0, 10.0)
368
+ self.assertAlmostEqual(m(5.0), 0.0)
369
+ self.assertAlmostEqual(m.inverse()(m(5.0)), 5.0)
370
+
371
+ def test_linear_map_very_small_scale(self):
372
+ m = LinearMap(1e-10)
373
+ self.assertAlmostEqual(m(1e10), 1.0)
374
+ self.assertTrue(m.invertible)
375
+
376
+ def test_linear_map_very_large_scale(self):
377
+ m = LinearMap(1e10)
378
+ self.assertAlmostEqual(m(1e-10), 1.0)
379
+ self.assertTrue(m.invertible)
380
+
381
+ def test_affine_identity(self):
382
+ """AffineMap(1, 0) should be identity."""
383
+ m = AffineMap(1.0, 0.0)
384
+ for x in [0.0, 1.0, -100.0, 1e6]:
385
+ self.assertAlmostEqual(m(x), x)
386
+
387
+ def test_linear_identity(self):
388
+ """LinearMap(1) should be identity."""
389
+ m = LinearMap(1.0)
390
+ for x in [0.0, 1.0, -100.0, 1e6]:
391
+ self.assertAlmostEqual(m(x), x)
392
+
393
+ def test_composed_map_deep_nesting(self):
394
+ """Test deeply nested ComposedMap."""
395
+ m = LinearMap(2.0)
396
+ for _ in range(5):
397
+ m = ComposedMap(m, LinearMap(1.5))
398
+ # 2 * 1.5^5 = 2 * 7.59375 = 15.1875
399
+ self.assertAlmostEqual(m(1.0), 2.0 * (1.5 ** 5))
400
+
401
+ def test_inverse_of_inverse(self):
402
+ """(m.inverse()).inverse() == m"""
403
+ m = LinearMap(7.0)
404
+ self.assertEqual(m.inverse().inverse(), m)
405
+
406
+ m2 = AffineMap(3.0, 5.0)
407
+ double_inv = m2.inverse().inverse()
408
+ self.assertAlmostEqual(double_inv.a, m2.a)
409
+ self.assertAlmostEqual(double_inv.b, m2.b)
@@ -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), 7) # always 7 components
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
@@ -332,14 +332,13 @@ class TestScaleMultiplicationAdditional(unittest.TestCase):
332
332
  self.assertIsInstance(result, Scale)
333
333
  self.assertEqual(result.value.base, 10)
334
334
 
335
- @unittest.skip("TODO: revamp: Unit.scale is deprecated.")
336
335
  def test_scale_multiplication_with_unit(self):
337
- meter = UnitFactor('m', name='meter', dimension=Dimension.length)
338
- kilometer = Scale.kilo * meter
339
- self.assertIsInstance(kilometer, UnitFactor)
340
- self.assertEqual(kilometer.scale, Scale.kilo)
336
+ """Scale * Unit returns a UnitProduct with the scaled unit."""
337
+ kilometer = Scale.kilo * units.meter
338
+ self.assertIsInstance(kilometer, UnitProduct)
341
339
  self.assertEqual(kilometer.dimension, Dimension.length)
342
- self.assertIn('meter', kilometer.name)
340
+ self.assertEqual(kilometer.shorthand, "km")
341
+ self.assertAlmostEqual(kilometer.fold_scale(), 1000.0, places=10)
343
342
 
344
343
  def test_scale_multiplication_with_unit_returns_not_implemented_for_invalid_type(self):
345
344
  with self.assertRaises(TypeError):
@@ -451,7 +450,7 @@ class TestUnit(unittest.TestCase):
451
450
  unit_name = 'second'
452
451
  unit_type = 'time'
453
452
  unit_aliases = ('seconds', 'secs', 's', 'S')
454
- unit = Unit(*unit_aliases, name=unit_name, dimension=Dimension.time)
453
+ unit = Unit(name=unit_name, dimension=Dimension.time, aliases=unit_aliases)
455
454
 
456
455
  def test___repr__(self):
457
456
  self.assertEqual(f'<Unit {self.unit_aliases[0]}>', str(self.unit))
@@ -464,12 +463,12 @@ class TestUnit(unittest.TestCase):
464
463
 
465
464
  def test_unit_equality_alias_normalization(self):
466
465
  # ('',) should normalize to () under _norm
467
- u1 = Unit("", name="x", dimension=Dimension.length)
466
+ u1 = Unit(name="x", dimension=Dimension.length, aliases=("",))
468
467
  u2 = Unit(name="x", dimension=Dimension.length)
469
468
  self.assertEqual(u1, u2)
470
469
 
471
470
  def test_unit_invalid_eq_type(self):
472
- self.assertFalse(Unit("m", dimension=Dimension.length) == "meter")
471
+ self.assertFalse(Unit(name="meter", dimension=Dimension.length, aliases=("m",)) == "meter")
473
472
 
474
473
 
475
474
  class TestUnitProduct(unittest.TestCase):
@@ -544,7 +543,7 @@ class TestUnitEdgeCases(unittest.TestCase):
544
543
  self.assertEqual(repr(u), '<Unit>')
545
544
 
546
545
  def test_unit_with_aliases_and_name(self):
547
- u = Unit('m', 'M', name='meter', dimension=Dimension.length)
546
+ u = Unit(name='meter', dimension=Dimension.length, aliases=('m', 'M'))
548
547
  self.assertEqual(u.shorthand, 'm')
549
548
  self.assertIn('m', u.aliases)
550
549
  self.assertIn('M', u.aliases)
@@ -553,9 +552,9 @@ class TestUnitEdgeCases(unittest.TestCase):
553
552
  self.assertIn('<Unit m>', repr(u))
554
553
 
555
554
  def test_hash_and_equality_consistency(self):
556
- u1 = Unit('m', name='meter', dimension=Dimension.length)
557
- u2 = Unit('m', name='meter', dimension=Dimension.length)
558
- u3 = Unit('s', name='second', dimension=Dimension.time)
555
+ u1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
556
+ u2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
557
+ u3 = Unit(name='second', dimension=Dimension.time, aliases=('s',))
559
558
  self.assertEqual(u1, u2)
560
559
  self.assertEqual(hash(u1), hash(u2))
561
560
  self.assertNotEqual(u1, u3)
@@ -569,36 +568,36 @@ class TestUnitEdgeCases(unittest.TestCase):
569
568
  # --- arithmetic behavior ----------------------------------------------
570
569
 
571
570
  def test_multiplication_produces_composite_unit(self):
572
- m = Unit('m', name='meter', dimension=Dimension.length)
573
- s = Unit('s', name='second', dimension=Dimension.time)
571
+ m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
572
+ s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
574
573
  v = m / s
575
574
  self.assertIsInstance(v, UnitProduct)
576
575
  self.assertEqual(v.dimension, Dimension.velocity)
577
576
  self.assertIn('/', repr(v))
578
577
 
579
578
  def test_division_with_dimensionless_denominator_returns_self(self):
580
- m = Unit('m', name='meter', dimension=Dimension.length)
579
+ m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
581
580
  none = Unit(name='none', dimension=Dimension.none)
582
581
  result = m / none
583
582
  self.assertEqual(result, m)
584
583
 
585
584
  def test_division_of_identical_units_returns_dimensionless(self):
586
- m1 = Unit('m', name='meter', dimension=Dimension.length)
587
- m2 = Unit('m', name='meter', dimension=Dimension.length)
585
+ m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
586
+ m2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
588
587
  result = m1 / m2
589
588
  self.assertEqual(result.dimension, Dimension.none)
590
589
  self.assertEqual(result.name, '')
591
590
 
592
591
  def test_multiplying_with_dimensionless_returns_self(self):
593
- m = Unit('m', name='meter', dimension=Dimension.length)
592
+ m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
594
593
  none = Unit(name='none', dimension=Dimension.none)
595
594
  result = m * none
596
595
  self.assertEqual(result.dimension, Dimension.length)
597
596
  self.assertEqual('m', result.shorthand)
598
597
 
599
598
  def test_invalid_dimension_combinations_raise_value_error(self):
600
- m = Unit('m', name='meter', dimension=Dimension.length)
601
- c = Unit('C', name='coulomb', dimension=Dimension.charge)
599
+ m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
600
+ c = Unit(name='coulomb', dimension=Dimension.charge, aliases=('C',))
602
601
  # The result of combination gives CompositeUnit
603
602
  self.assertIsInstance(m / c, UnitProduct)
604
603
  self.assertIsInstance(m * c, UnitProduct)
@@ -606,16 +605,16 @@ class TestUnitEdgeCases(unittest.TestCase):
606
605
  # --- equality, hashing, immutability ----------------------------------
607
606
 
608
607
  def test_equality_with_non_unit(self):
609
- self.assertFalse(Unit('m', name='meter', dimension=Dimension.length) == 'meter')
608
+ self.assertFalse(Unit(name='meter', dimension=Dimension.length, aliases=('m',)) == 'meter')
610
609
 
611
610
  def test_hash_stability_in_collections(self):
612
- m1 = Unit('m', name='meter', dimension=Dimension.length)
611
+ m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
613
612
  s = set([m1])
614
- self.assertIn(Unit('m', name='meter', dimension=Dimension.length), s)
613
+ self.assertIn(Unit(name='meter', dimension=Dimension.length, aliases=('m',)), s)
615
614
 
616
615
  def test_operations_do_not_mutate_operands(self):
617
- m = Unit('m', name='meter', dimension=Dimension.length)
618
- s = Unit('s', name='second', dimension=Dimension.time)
616
+ m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
617
+ s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
619
618
  _ = m / s
620
619
  self.assertEqual(m.dimension, Dimension.length)
621
620
  self.assertEqual(s.dimension, Dimension.time)