ucon 0.5.2__py3-none-any.whl → 0.6.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.
@@ -1,521 +0,0 @@
1
- # (c) 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 BasisTransform.
7
-
8
- Verifies matrix-based dimensional basis transformations,
9
- invertibility detection, and exact Fraction arithmetic.
10
- """
11
-
12
- import unittest
13
- from fractions import Fraction
14
-
15
- from ucon.core import (
16
- BasisTransform,
17
- Dimension,
18
- NonInvertibleTransform,
19
- Unit,
20
- UnitSystem,
21
- )
22
- from ucon import units
23
-
24
-
25
- class TestBasisTransformConstruction(unittest.TestCase):
26
- """Test BasisTransform construction and validation."""
27
-
28
- def setUp(self):
29
- self.si = UnitSystem(
30
- name="SI",
31
- bases={
32
- Dimension.length: units.meter,
33
- Dimension.mass: units.kilogram,
34
- Dimension.time: units.second,
35
- }
36
- )
37
- self.custom = UnitSystem(
38
- name="Custom",
39
- bases={
40
- Dimension.length: units.foot,
41
- Dimension.mass: units.pound,
42
- Dimension.time: units.second,
43
- }
44
- )
45
-
46
- def test_valid_construction(self):
47
- bt = BasisTransform(
48
- src=self.custom,
49
- dst=self.si,
50
- src_dimensions=(Dimension.length,),
51
- dst_dimensions=(Dimension.length,),
52
- matrix=((1,),),
53
- )
54
- self.assertEqual(bt.src, self.custom)
55
- self.assertEqual(bt.dst, self.si)
56
-
57
- def test_matrix_converted_to_fraction(self):
58
- bt = BasisTransform(
59
- src=self.custom,
60
- dst=self.si,
61
- src_dimensions=(Dimension.length, Dimension.mass),
62
- dst_dimensions=(Dimension.length, Dimension.mass),
63
- matrix=((1, 0), (0, 1)),
64
- )
65
- self.assertIsInstance(bt.matrix[0][0], Fraction)
66
- self.assertIsInstance(bt.matrix[1][1], Fraction)
67
-
68
- def test_dimension_count_mismatch_rows(self):
69
- with self.assertRaises(ValueError) as ctx:
70
- BasisTransform(
71
- src=self.custom,
72
- dst=self.si,
73
- src_dimensions=(Dimension.length,),
74
- dst_dimensions=(Dimension.length, Dimension.mass), # 2 dims
75
- matrix=((1,),), # only 1 row
76
- )
77
- self.assertIn("row", str(ctx.exception).lower())
78
-
79
- def test_dimension_count_mismatch_cols(self):
80
- with self.assertRaises(ValueError) as ctx:
81
- BasisTransform(
82
- src=self.custom,
83
- dst=self.si,
84
- src_dimensions=(Dimension.length, Dimension.mass), # 2 dims
85
- dst_dimensions=(Dimension.length,),
86
- matrix=((1,),), # only 1 column
87
- )
88
- self.assertIn("column", str(ctx.exception).lower())
89
-
90
-
91
- class TestBasisTransformSquareMatrix(unittest.TestCase):
92
- """Test BasisTransform with square matrices."""
93
-
94
- def setUp(self):
95
- self.si = UnitSystem(
96
- name="SI",
97
- bases={
98
- Dimension.length: units.meter,
99
- Dimension.mass: units.kilogram,
100
- Dimension.time: units.second,
101
- }
102
- )
103
- self.custom = UnitSystem(
104
- name="Custom",
105
- bases={
106
- Dimension.length: units.foot,
107
- Dimension.mass: units.pound,
108
- Dimension.time: units.second,
109
- }
110
- )
111
-
112
- def test_1x1_identity_transform(self):
113
- bt = BasisTransform(
114
- src=self.custom,
115
- dst=self.si,
116
- src_dimensions=(Dimension.length,),
117
- dst_dimensions=(Dimension.length,),
118
- matrix=((1,),),
119
- )
120
- self.assertTrue(bt.is_square)
121
- self.assertTrue(bt.is_invertible)
122
-
123
- def test_2x2_identity_transform(self):
124
- bt = BasisTransform(
125
- src=self.custom,
126
- dst=self.si,
127
- src_dimensions=(Dimension.length, Dimension.mass),
128
- dst_dimensions=(Dimension.length, Dimension.mass),
129
- matrix=((1, 0), (0, 1)),
130
- )
131
- self.assertTrue(bt.is_square)
132
- self.assertTrue(bt.is_invertible)
133
-
134
- def test_2x2_singular_not_invertible(self):
135
- bt = BasisTransform(
136
- src=self.custom,
137
- dst=self.si,
138
- src_dimensions=(Dimension.length, Dimension.mass),
139
- dst_dimensions=(Dimension.length, Dimension.mass),
140
- matrix=((1, 2), (2, 4)), # Rows are linearly dependent
141
- )
142
- self.assertTrue(bt.is_square)
143
- self.assertFalse(bt.is_invertible)
144
-
145
- def test_3x3_invertible(self):
146
- bt = BasisTransform(
147
- src=self.custom,
148
- dst=self.si,
149
- src_dimensions=(Dimension.length, Dimension.mass, Dimension.time),
150
- dst_dimensions=(Dimension.length, Dimension.mass, Dimension.time),
151
- matrix=(
152
- (1, 0, 0),
153
- (0, 1, 0),
154
- (0, 0, 1),
155
- ),
156
- )
157
- self.assertTrue(bt.is_square)
158
- self.assertTrue(bt.is_invertible)
159
-
160
- def test_determinant_2x2(self):
161
- bt = BasisTransform(
162
- src=self.custom,
163
- dst=self.si,
164
- src_dimensions=(Dimension.length, Dimension.mass),
165
- dst_dimensions=(Dimension.length, Dimension.mass),
166
- matrix=((2, 3), (1, 4)), # det = 2*4 - 3*1 = 5
167
- )
168
- # Access internal _determinant method for testing
169
- self.assertEqual(bt._determinant(), Fraction(5))
170
-
171
-
172
- class TestBasisTransformInverse(unittest.TestCase):
173
- """Test BasisTransform inverse computation."""
174
-
175
- def setUp(self):
176
- self.si = UnitSystem(
177
- name="SI",
178
- bases={
179
- Dimension.length: units.meter,
180
- Dimension.mass: units.kilogram,
181
- Dimension.time: units.second,
182
- }
183
- )
184
- self.custom = UnitSystem(
185
- name="Custom",
186
- bases={
187
- Dimension.length: units.foot,
188
- Dimension.mass: units.pound,
189
- Dimension.time: units.second,
190
- }
191
- )
192
-
193
- def test_1x1_inverse(self):
194
- bt = BasisTransform(
195
- src=self.custom,
196
- dst=self.si,
197
- src_dimensions=(Dimension.length,),
198
- dst_dimensions=(Dimension.length,),
199
- matrix=((2,),), # scale by 2
200
- )
201
- inv = bt.inverse()
202
- self.assertEqual(inv.matrix, ((Fraction(1, 2),),))
203
- self.assertEqual(inv.src, self.si)
204
- self.assertEqual(inv.dst, self.custom)
205
-
206
- def test_2x2_inverse(self):
207
- bt = BasisTransform(
208
- src=self.custom,
209
- dst=self.si,
210
- src_dimensions=(Dimension.length, Dimension.mass),
211
- dst_dimensions=(Dimension.length, Dimension.mass),
212
- matrix=((1, 0), (0, 1)),
213
- )
214
- inv = bt.inverse()
215
- self.assertEqual(inv.matrix, ((Fraction(1), Fraction(0)), (Fraction(0), Fraction(1))))
216
-
217
- def test_inverse_of_inverse_equals_original(self):
218
- bt = BasisTransform(
219
- src=self.custom,
220
- dst=self.si,
221
- src_dimensions=(Dimension.length, Dimension.mass),
222
- dst_dimensions=(Dimension.length, Dimension.mass),
223
- matrix=((2, 1), (1, 1)), # det = 2*1 - 1*1 = 1
224
- )
225
- inv = bt.inverse()
226
- inv_inv = inv.inverse()
227
- self.assertEqual(bt.matrix, inv_inv.matrix)
228
-
229
- def test_non_invertible_raises(self):
230
- bt = BasisTransform(
231
- src=self.custom,
232
- dst=self.si,
233
- src_dimensions=(Dimension.length, Dimension.mass),
234
- dst_dimensions=(Dimension.length, Dimension.mass),
235
- matrix=((1, 2), (2, 4)), # Singular
236
- )
237
- with self.assertRaises(NonInvertibleTransform):
238
- bt.inverse()
239
-
240
-
241
- class TestBasisTransformNonSquare(unittest.TestCase):
242
- """Test BasisTransform with non-square (surjective) matrices."""
243
-
244
- def setUp(self):
245
- self.si = UnitSystem(
246
- name="SI",
247
- bases={
248
- Dimension.length: units.meter,
249
- Dimension.mass: units.kilogram,
250
- Dimension.time: units.second,
251
- }
252
- )
253
- self.reduced = UnitSystem(
254
- name="Reduced",
255
- bases={
256
- Dimension.length: units.meter,
257
- Dimension.mass: units.kilogram,
258
- }
259
- )
260
-
261
- def test_2x3_not_square(self):
262
- bt = BasisTransform(
263
- src=self.si,
264
- dst=self.reduced,
265
- src_dimensions=(Dimension.length, Dimension.mass, Dimension.time),
266
- dst_dimensions=(Dimension.length, Dimension.mass),
267
- matrix=(
268
- (1, 0, 0),
269
- (0, 1, 0),
270
- ),
271
- )
272
- self.assertFalse(bt.is_square)
273
- self.assertFalse(bt.is_invertible)
274
-
275
- def test_non_square_inverse_raises(self):
276
- bt = BasisTransform(
277
- src=self.si,
278
- dst=self.reduced,
279
- src_dimensions=(Dimension.length, Dimension.mass, Dimension.time),
280
- dst_dimensions=(Dimension.length, Dimension.mass),
281
- matrix=(
282
- (1, 0, 0),
283
- (0, 1, 0),
284
- ),
285
- )
286
- with self.assertRaises(NonInvertibleTransform):
287
- bt.inverse()
288
-
289
-
290
- class TestBasisTransformTransform(unittest.TestCase):
291
- """Test the transform() method for vector transformation."""
292
-
293
- def setUp(self):
294
- self.si = UnitSystem(
295
- name="SI",
296
- bases={
297
- Dimension.length: units.meter,
298
- Dimension.mass: units.kilogram,
299
- Dimension.time: units.second,
300
- }
301
- )
302
- self.custom = UnitSystem(
303
- name="Custom",
304
- bases={
305
- Dimension.length: units.foot,
306
- Dimension.mass: units.pound,
307
- Dimension.time: units.second,
308
- }
309
- )
310
-
311
- def test_identity_transform_vector(self):
312
- bt = BasisTransform(
313
- src=self.custom,
314
- dst=self.si,
315
- src_dimensions=(Dimension.length,),
316
- dst_dimensions=(Dimension.length,),
317
- matrix=((1,),),
318
- )
319
- # Transform the length dimension vector
320
- src_vector = Dimension.length.value
321
- result = bt.transform(src_vector)
322
- self.assertEqual(result, Dimension.length.value)
323
-
324
- def test_scaling_transform_vector(self):
325
- bt = BasisTransform(
326
- src=self.custom,
327
- dst=self.si,
328
- src_dimensions=(Dimension.length,),
329
- dst_dimensions=(Dimension.length,),
330
- matrix=((2,),),
331
- )
332
- src_vector = Dimension.length.value
333
- result = bt.transform(src_vector)
334
- # Length component should be doubled
335
- self.assertEqual(result.L, Fraction(2))
336
-
337
- def test_mixed_transform_vector(self):
338
- # Transform where mass includes contributions from multiple src dimensions
339
- bt = BasisTransform(
340
- src=self.custom,
341
- dst=self.si,
342
- src_dimensions=(Dimension.length, Dimension.mass),
343
- dst_dimensions=(Dimension.length, Dimension.mass),
344
- matrix=(
345
- (1, 0),
346
- (1, 1), # dst_mass = src_length + src_mass
347
- ),
348
- )
349
- # Input: pure length (L=1, M=0)
350
- src_vector = Dimension.length.value
351
- result = bt.transform(src_vector)
352
- self.assertEqual(result.L, Fraction(1))
353
- self.assertEqual(result.M, Fraction(1)) # Contribution from length
354
-
355
-
356
- class TestBasisTransformValidateEdge(unittest.TestCase):
357
- """Test validate_edge() for cross-basis edge validation."""
358
-
359
- def setUp(self):
360
- self.si = UnitSystem(
361
- name="SI",
362
- bases={
363
- Dimension.length: units.meter,
364
- Dimension.mass: units.kilogram,
365
- Dimension.time: units.second,
366
- }
367
- )
368
- self.custom = UnitSystem(
369
- name="Custom",
370
- bases={
371
- Dimension.length: units.foot,
372
- Dimension.mass: units.pound,
373
- Dimension.time: units.second,
374
- }
375
- )
376
-
377
- def test_valid_edge_same_dimension(self):
378
- bt = BasisTransform(
379
- src=self.custom,
380
- dst=self.si,
381
- src_dimensions=(Dimension.length,),
382
- dst_dimensions=(Dimension.length,),
383
- matrix=((1,),),
384
- )
385
- # foot (length) -> meter (length) should be valid
386
- self.assertTrue(bt.validate_edge(units.foot, units.meter))
387
-
388
- def test_invalid_edge_different_dimension(self):
389
- bt = BasisTransform(
390
- src=self.custom,
391
- dst=self.si,
392
- src_dimensions=(Dimension.length,),
393
- dst_dimensions=(Dimension.length,),
394
- matrix=((1,),),
395
- )
396
- # foot (length) -> kilogram (mass) should be invalid
397
- self.assertFalse(bt.validate_edge(units.foot, units.kilogram))
398
-
399
-
400
- class TestBasisTransformFractionArithmetic(unittest.TestCase):
401
- """Test that Fraction arithmetic is exact throughout."""
402
-
403
- def setUp(self):
404
- self.si = UnitSystem(
405
- name="SI",
406
- bases={
407
- Dimension.length: units.meter,
408
- Dimension.mass: units.kilogram,
409
- Dimension.time: units.second,
410
- }
411
- )
412
- self.custom = UnitSystem(
413
- name="Custom",
414
- bases={
415
- Dimension.length: units.foot,
416
- Dimension.mass: units.pound,
417
- Dimension.time: units.second,
418
- }
419
- )
420
-
421
- def test_fraction_matrix_entries(self):
422
- bt = BasisTransform(
423
- src=self.custom,
424
- dst=self.si,
425
- src_dimensions=(Dimension.length, Dimension.mass),
426
- dst_dimensions=(Dimension.length, Dimension.mass),
427
- matrix=(
428
- (Fraction(1, 3), Fraction(2, 3)),
429
- (Fraction(1, 2), Fraction(1, 2)),
430
- ),
431
- )
432
- self.assertEqual(bt.matrix[0][0], Fraction(1, 3))
433
- self.assertEqual(bt.matrix[0][1], Fraction(2, 3))
434
-
435
- def test_inverse_preserves_fractions(self):
436
- bt = BasisTransform(
437
- src=self.custom,
438
- dst=self.si,
439
- src_dimensions=(Dimension.length, Dimension.mass),
440
- dst_dimensions=(Dimension.length, Dimension.mass),
441
- matrix=((3, 0), (0, 2)),
442
- )
443
- inv = bt.inverse()
444
- self.assertEqual(inv.matrix[0][0], Fraction(1, 3))
445
- self.assertEqual(inv.matrix[1][1], Fraction(1, 2))
446
-
447
- def test_no_floating_point_drift(self):
448
- # 1/3 inverse should give exactly 3, not 2.9999...
449
- bt = BasisTransform(
450
- src=self.custom,
451
- dst=self.si,
452
- src_dimensions=(Dimension.length,),
453
- dst_dimensions=(Dimension.length,),
454
- matrix=((Fraction(1, 3),),),
455
- )
456
- inv = bt.inverse()
457
- self.assertEqual(inv.matrix[0][0], Fraction(3))
458
- # Double inverse should be exactly original
459
- inv_inv = inv.inverse()
460
- self.assertEqual(inv_inv.matrix[0][0], Fraction(1, 3))
461
-
462
-
463
- class TestBasisTransformHashEquality(unittest.TestCase):
464
- """Test BasisTransform equality and hashing."""
465
-
466
- def setUp(self):
467
- self.si = UnitSystem(
468
- name="SI",
469
- bases={
470
- Dimension.length: units.meter,
471
- Dimension.mass: units.kilogram,
472
- Dimension.time: units.second,
473
- }
474
- )
475
- self.custom = UnitSystem(
476
- name="Custom",
477
- bases={
478
- Dimension.length: units.foot,
479
- Dimension.mass: units.pound,
480
- Dimension.time: units.second,
481
- }
482
- )
483
-
484
- def test_equal_transforms(self):
485
- bt1 = BasisTransform(
486
- src=self.custom,
487
- dst=self.si,
488
- src_dimensions=(Dimension.length,),
489
- dst_dimensions=(Dimension.length,),
490
- matrix=((1,),),
491
- )
492
- bt2 = BasisTransform(
493
- src=self.custom,
494
- dst=self.si,
495
- src_dimensions=(Dimension.length,),
496
- dst_dimensions=(Dimension.length,),
497
- matrix=((1,),),
498
- )
499
- self.assertEqual(bt1, bt2)
500
-
501
- def test_hashable(self):
502
- bt1 = BasisTransform(
503
- src=self.custom,
504
- dst=self.si,
505
- src_dimensions=(Dimension.length,),
506
- dst_dimensions=(Dimension.length,),
507
- matrix=((1,),),
508
- )
509
- bt2 = BasisTransform(
510
- src=self.custom,
511
- dst=self.si,
512
- src_dimensions=(Dimension.length,),
513
- dst_dimensions=(Dimension.length,),
514
- matrix=((1,),),
515
- )
516
- self.assertEqual(hash(bt1), hash(bt2))
517
- self.assertEqual(len({bt1, bt2}), 1)
518
-
519
-
520
- if __name__ == "__main__":
521
- unittest.main()