ucon 0.4.0__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.
- tests/ucon/conversion/test_graph.py +234 -0
- tests/ucon/conversion/test_map.py +246 -0
- tests/ucon/test_core.py +5 -6
- tests/ucon/test_default_graph_conversions.py +443 -0
- tests/ucon/test_quantity.py +43 -49
- ucon/graph.py +8 -0
- ucon/units.py +5 -0
- {ucon-0.4.0.dist-info → ucon-0.4.1.dist-info}/METADATA +2 -2
- ucon-0.4.1.dist-info/RECORD +22 -0
- ucon-0.4.0.dist-info/RECORD +0 -21
- {ucon-0.4.0.dist-info → ucon-0.4.1.dist-info}/WHEEL +0 -0
- {ucon-0.4.0.dist-info → ucon-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.4.0.dist-info → ucon-0.4.1.dist-info}/licenses/NOTICE +0 -0
- {ucon-0.4.0.dist-info → ucon-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -11,6 +11,10 @@ from ucon.graph import (
|
|
|
11
11
|
DimensionMismatch,
|
|
12
12
|
ConversionNotFound,
|
|
13
13
|
CyclicInconsistency,
|
|
14
|
+
get_default_graph,
|
|
15
|
+
set_default_graph,
|
|
16
|
+
reset_default_graph,
|
|
17
|
+
using_graph,
|
|
14
18
|
)
|
|
15
19
|
from ucon.maps import LinearMap, AffineMap
|
|
16
20
|
|
|
@@ -173,3 +177,233 @@ class TestConversionGraphTemperature(unittest.TestCase):
|
|
|
173
177
|
m = self.graph.convert(src=self.celsius, dst=self.fahrenheit)
|
|
174
178
|
self.assertAlmostEqual(m(0), 32, places=0)
|
|
175
179
|
self.assertAlmostEqual(m(100), 212, places=0)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestConversionGraphProductEdgeManagement(unittest.TestCase):
|
|
183
|
+
"""Tests for UnitProduct edge management."""
|
|
184
|
+
|
|
185
|
+
def setUp(self):
|
|
186
|
+
self.graph = ConversionGraph()
|
|
187
|
+
self.meter = units.meter
|
|
188
|
+
self.second = units.second
|
|
189
|
+
self.gram = units.gram
|
|
190
|
+
|
|
191
|
+
def test_add_product_edge(self):
|
|
192
|
+
"""Test adding edge between two UnitProducts with different scales."""
|
|
193
|
+
# Create two energy-like products with different scales
|
|
194
|
+
energy_g = UnitProduct({
|
|
195
|
+
UnitFactor(self.gram, Scale.one): 1,
|
|
196
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
197
|
+
UnitFactor(self.second, Scale.one): -2,
|
|
198
|
+
})
|
|
199
|
+
energy_kg = UnitProduct({
|
|
200
|
+
UnitFactor(self.gram, Scale.kilo): 1,
|
|
201
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
202
|
+
UnitFactor(self.second, Scale.one): -2,
|
|
203
|
+
})
|
|
204
|
+
# Register direct edge: 1 g·m²/s² = 0.001 kg·m²/s²
|
|
205
|
+
self.graph.add_edge(src=energy_g, dst=energy_kg, map=LinearMap(0.001))
|
|
206
|
+
m = self.graph.convert(src=energy_g, dst=energy_kg)
|
|
207
|
+
self.assertAlmostEqual(m(1), 0.001, places=6)
|
|
208
|
+
|
|
209
|
+
def test_add_mixed_unit_and_product_edge(self):
|
|
210
|
+
"""Test adding edge with Unit on one side, UnitProduct on other."""
|
|
211
|
+
m_prod = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
212
|
+
self.graph.add_edge(src=self.meter, dst=m_prod, map=LinearMap(1.0))
|
|
213
|
+
m = self.graph.convert(src=self.meter, dst=m_prod)
|
|
214
|
+
self.assertTrue(m.is_identity())
|
|
215
|
+
|
|
216
|
+
def test_product_edge_dimension_mismatch(self):
|
|
217
|
+
"""Test that dimension mismatch raises for UnitProduct edges."""
|
|
218
|
+
length_prod = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
219
|
+
time_prod = UnitProduct({UnitFactor(self.second, Scale.one): 1})
|
|
220
|
+
with self.assertRaises(DimensionMismatch):
|
|
221
|
+
self.graph.add_edge(src=length_prod, dst=time_prod, map=LinearMap(1))
|
|
222
|
+
|
|
223
|
+
def test_product_edge_cyclic_consistency(self):
|
|
224
|
+
"""Test cyclic consistency check for UnitProduct edges."""
|
|
225
|
+
energy1 = UnitProduct({
|
|
226
|
+
UnitFactor(self.gram, Scale.one): 1,
|
|
227
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
228
|
+
UnitFactor(self.second, Scale.one): -2,
|
|
229
|
+
})
|
|
230
|
+
energy2 = UnitProduct({
|
|
231
|
+
UnitFactor(self.gram, Scale.kilo): 1,
|
|
232
|
+
UnitFactor(self.meter, Scale.one): 2,
|
|
233
|
+
UnitFactor(self.second, Scale.one): -2,
|
|
234
|
+
})
|
|
235
|
+
self.graph.add_edge(src=energy1, dst=energy2, map=LinearMap(0.001))
|
|
236
|
+
# Adding inconsistent reverse should raise
|
|
237
|
+
with self.assertRaises(CyclicInconsistency):
|
|
238
|
+
self.graph.add_edge(src=energy2, dst=energy1, map=LinearMap(500)) # wrong!
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestConversionGraphFactorwiseEdgeCases(unittest.TestCase):
|
|
242
|
+
"""Tests for factorwise conversion edge cases."""
|
|
243
|
+
|
|
244
|
+
def setUp(self):
|
|
245
|
+
self.graph = ConversionGraph()
|
|
246
|
+
self.meter = units.meter
|
|
247
|
+
self.second = units.second
|
|
248
|
+
self.foot = Unit(name='foot', dimension=Dimension.length, aliases=('ft',))
|
|
249
|
+
|
|
250
|
+
def test_factorwise_exponent_mismatch(self):
|
|
251
|
+
"""Test that exponent mismatch raises ConversionNotFound."""
|
|
252
|
+
# m^2 vs m^1 - different exponents for same dimension
|
|
253
|
+
m_squared = UnitProduct({UnitFactor(self.meter, Scale.one): 2})
|
|
254
|
+
m_single = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
255
|
+
# These have different dimensions, so dimension check fails first
|
|
256
|
+
with self.assertRaises(DimensionMismatch):
|
|
257
|
+
self.graph.convert(src=m_squared, dst=m_single)
|
|
258
|
+
|
|
259
|
+
def test_factorwise_misaligned_structures(self):
|
|
260
|
+
"""Test that misaligned factor structures raise ConversionNotFound."""
|
|
261
|
+
# m/s vs m (missing time dimension in target)
|
|
262
|
+
m_per_s = UnitProduct({
|
|
263
|
+
UnitFactor(self.meter, Scale.one): 1,
|
|
264
|
+
UnitFactor(self.second, Scale.one): -1,
|
|
265
|
+
})
|
|
266
|
+
m_only = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
267
|
+
with self.assertRaises(DimensionMismatch):
|
|
268
|
+
self.graph.convert(src=m_per_s, dst=m_only)
|
|
269
|
+
|
|
270
|
+
def test_convert_unit_to_product(self):
|
|
271
|
+
"""Test conversion from plain Unit to UnitProduct."""
|
|
272
|
+
self.graph.add_edge(src=self.meter, dst=self.foot, map=LinearMap(3.28084))
|
|
273
|
+
m_prod = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
274
|
+
ft_prod = UnitProduct({UnitFactor(self.foot, Scale.one): 1})
|
|
275
|
+
m = self.graph.convert(src=m_prod, dst=ft_prod)
|
|
276
|
+
self.assertAlmostEqual(m(1), 3.28084, places=4)
|
|
277
|
+
|
|
278
|
+
def test_convert_product_dimension_mismatch(self):
|
|
279
|
+
"""Test dimension mismatch in _convert_products."""
|
|
280
|
+
length = UnitProduct({UnitFactor(self.meter, Scale.one): 1})
|
|
281
|
+
time = UnitProduct({UnitFactor(self.second, Scale.one): 1})
|
|
282
|
+
with self.assertRaises(DimensionMismatch):
|
|
283
|
+
self.graph.convert(src=length, dst=time)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestConversionGraphBFSEdgeCases(unittest.TestCase):
|
|
287
|
+
"""Tests for BFS path-finding edge cases."""
|
|
288
|
+
|
|
289
|
+
def setUp(self):
|
|
290
|
+
self.graph = ConversionGraph()
|
|
291
|
+
|
|
292
|
+
def test_no_edges_for_dimension(self):
|
|
293
|
+
"""Test ConversionNotFound when dimension has no edges at all."""
|
|
294
|
+
custom_unit1 = Unit(name='custom1', dimension=Dimension.length)
|
|
295
|
+
custom_unit2 = Unit(name='custom2', dimension=Dimension.length)
|
|
296
|
+
with self.assertRaises(ConversionNotFound):
|
|
297
|
+
self.graph.convert(src=custom_unit1, dst=custom_unit2)
|
|
298
|
+
|
|
299
|
+
def test_source_has_no_outgoing_edges(self):
|
|
300
|
+
"""Test when source exists in graph but has no path to target."""
|
|
301
|
+
a = Unit(name='a', dimension=Dimension.length)
|
|
302
|
+
b = Unit(name='b', dimension=Dimension.length)
|
|
303
|
+
c = Unit(name='c', dimension=Dimension.length)
|
|
304
|
+
# Only add a→b, no connection to c
|
|
305
|
+
self.graph.add_edge(src=a, dst=b, map=LinearMap(2.0))
|
|
306
|
+
with self.assertRaises(ConversionNotFound):
|
|
307
|
+
self.graph.convert(src=a, dst=c)
|
|
308
|
+
|
|
309
|
+
def test_disconnected_subgraphs(self):
|
|
310
|
+
"""Test when units are in same dimension but disconnected subgraphs."""
|
|
311
|
+
a = Unit(name='a', dimension=Dimension.length)
|
|
312
|
+
b = Unit(name='b', dimension=Dimension.length)
|
|
313
|
+
c = Unit(name='c', dimension=Dimension.length)
|
|
314
|
+
d = Unit(name='d', dimension=Dimension.length)
|
|
315
|
+
# Subgraph 1: a ↔ b
|
|
316
|
+
self.graph.add_edge(src=a, dst=b, map=LinearMap(2.0))
|
|
317
|
+
# Subgraph 2: c ↔ d
|
|
318
|
+
self.graph.add_edge(src=c, dst=d, map=LinearMap(3.0))
|
|
319
|
+
# Cannot convert between subgraphs
|
|
320
|
+
with self.assertRaises(ConversionNotFound):
|
|
321
|
+
self.graph.convert(src=a, dst=c)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class TestDefaultGraphManagement(unittest.TestCase):
|
|
325
|
+
"""Tests for default graph management functions."""
|
|
326
|
+
|
|
327
|
+
def setUp(self):
|
|
328
|
+
reset_default_graph()
|
|
329
|
+
|
|
330
|
+
def tearDown(self):
|
|
331
|
+
reset_default_graph()
|
|
332
|
+
|
|
333
|
+
def test_get_default_graph_returns_graph(self):
|
|
334
|
+
"""Test that get_default_graph returns a ConversionGraph."""
|
|
335
|
+
graph = get_default_graph()
|
|
336
|
+
self.assertIsInstance(graph, ConversionGraph)
|
|
337
|
+
|
|
338
|
+
def test_get_default_graph_is_cached(self):
|
|
339
|
+
"""Test that get_default_graph returns same instance."""
|
|
340
|
+
graph1 = get_default_graph()
|
|
341
|
+
graph2 = get_default_graph()
|
|
342
|
+
self.assertIs(graph1, graph2)
|
|
343
|
+
|
|
344
|
+
def test_set_default_graph(self):
|
|
345
|
+
"""Test that set_default_graph replaces the default."""
|
|
346
|
+
custom = ConversionGraph()
|
|
347
|
+
set_default_graph(custom)
|
|
348
|
+
self.assertIs(get_default_graph(), custom)
|
|
349
|
+
|
|
350
|
+
def test_reset_default_graph(self):
|
|
351
|
+
"""Test that reset_default_graph clears the cached graph."""
|
|
352
|
+
graph1 = get_default_graph()
|
|
353
|
+
custom = ConversionGraph()
|
|
354
|
+
set_default_graph(custom)
|
|
355
|
+
reset_default_graph()
|
|
356
|
+
graph2 = get_default_graph()
|
|
357
|
+
self.assertIsNot(graph2, custom)
|
|
358
|
+
# Should be a fresh standard graph
|
|
359
|
+
self.assertIsInstance(graph2, ConversionGraph)
|
|
360
|
+
|
|
361
|
+
def test_using_graph_context_manager(self):
|
|
362
|
+
"""Test using_graph provides scoped override."""
|
|
363
|
+
default = get_default_graph()
|
|
364
|
+
custom = ConversionGraph()
|
|
365
|
+
|
|
366
|
+
with using_graph(custom) as g:
|
|
367
|
+
self.assertIs(g, custom)
|
|
368
|
+
self.assertIs(get_default_graph(), custom)
|
|
369
|
+
|
|
370
|
+
# After context, back to default
|
|
371
|
+
self.assertIs(get_default_graph(), default)
|
|
372
|
+
|
|
373
|
+
def test_using_graph_nested(self):
|
|
374
|
+
"""Test nested using_graph contexts."""
|
|
375
|
+
default = get_default_graph()
|
|
376
|
+
custom1 = ConversionGraph()
|
|
377
|
+
custom2 = ConversionGraph()
|
|
378
|
+
|
|
379
|
+
with using_graph(custom1):
|
|
380
|
+
self.assertIs(get_default_graph(), custom1)
|
|
381
|
+
with using_graph(custom2):
|
|
382
|
+
self.assertIs(get_default_graph(), custom2)
|
|
383
|
+
self.assertIs(get_default_graph(), custom1)
|
|
384
|
+
|
|
385
|
+
self.assertIs(get_default_graph(), default)
|
|
386
|
+
|
|
387
|
+
def test_using_graph_exception_safety(self):
|
|
388
|
+
"""Test using_graph restores graph even on exception."""
|
|
389
|
+
default = get_default_graph()
|
|
390
|
+
custom = ConversionGraph()
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
with using_graph(custom):
|
|
394
|
+
self.assertIs(get_default_graph(), custom)
|
|
395
|
+
raise ValueError("test error")
|
|
396
|
+
except ValueError:
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
self.assertIs(get_default_graph(), default)
|
|
400
|
+
|
|
401
|
+
def test_default_graph_has_standard_conversions(self):
|
|
402
|
+
"""Test that default graph includes expected conversions."""
|
|
403
|
+
graph = get_default_graph()
|
|
404
|
+
# Test a few known conversions
|
|
405
|
+
m = graph.convert(src=units.meter, dst=units.foot)
|
|
406
|
+
self.assertAlmostEqual(m(1), 3.28084, places=4)
|
|
407
|
+
|
|
408
|
+
m = graph.convert(src=units.kilogram, dst=units.pound)
|
|
409
|
+
self.assertAlmostEqual(m(1), 2.20462, places=4)
|
|
@@ -67,6 +67,52 @@ class TestLinearMap(unittest.TestCase):
|
|
|
67
67
|
result = m.__matmul__(42)
|
|
68
68
|
self.assertIs(result, NotImplemented)
|
|
69
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
|
+
|
|
70
116
|
|
|
71
117
|
class TestAffineMap(unittest.TestCase):
|
|
72
118
|
|
|
@@ -122,6 +168,57 @@ class TestAffineMap(unittest.TestCase):
|
|
|
122
168
|
self.assertIn("1.8", r)
|
|
123
169
|
self.assertIn("32.0", r)
|
|
124
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
|
+
|
|
125
222
|
|
|
126
223
|
class TestComposedMap(unittest.TestCase):
|
|
127
224
|
|
|
@@ -155,9 +252,158 @@ class TestComposedMap(unittest.TestCase):
|
|
|
155
252
|
self.assertIn("LinearMap", r)
|
|
156
253
|
self.assertIn("AffineMap", r)
|
|
157
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
|
+
|
|
158
307
|
|
|
159
308
|
class TestMapABC(unittest.TestCase):
|
|
160
309
|
|
|
161
310
|
def test_cannot_instantiate(self):
|
|
162
311
|
with self.assertRaises(TypeError):
|
|
163
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)
|
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
|
-
|
|
338
|
-
kilometer = Scale.kilo * meter
|
|
339
|
-
self.assertIsInstance(kilometer,
|
|
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.
|
|
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):
|
|
@@ -0,0 +1,443 @@
|
|
|
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 the default ConversionGraph with temperature, pressure, and base SI conversions.
|
|
7
|
+
|
|
8
|
+
These tests verify that Number.to() works correctly with the default graph for:
|
|
9
|
+
- Temperature conversions (Celsius, Kelvin, Fahrenheit) using AffineMap
|
|
10
|
+
- Pressure conversions (Pascal, Bar, PSI, Atmosphere)
|
|
11
|
+
- Base SI conversions (length, mass, time, volume, energy, power, information)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
from ucon import units
|
|
17
|
+
from ucon.graph import get_default_graph, reset_default_graph
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestTemperatureConversions(unittest.TestCase):
|
|
21
|
+
"""Tests for temperature conversions using AffineMap in the default graph."""
|
|
22
|
+
|
|
23
|
+
def setUp(self):
|
|
24
|
+
reset_default_graph()
|
|
25
|
+
|
|
26
|
+
def test_celsius_to_kelvin_freezing(self):
|
|
27
|
+
"""0°C = 273.15 K"""
|
|
28
|
+
result = units.celsius(0).to(units.kelvin)
|
|
29
|
+
self.assertAlmostEqual(result.quantity, 273.15, places=2)
|
|
30
|
+
|
|
31
|
+
def test_celsius_to_kelvin_boiling(self):
|
|
32
|
+
"""100°C = 373.15 K"""
|
|
33
|
+
result = units.celsius(100).to(units.kelvin)
|
|
34
|
+
self.assertAlmostEqual(result.quantity, 373.15, places=2)
|
|
35
|
+
|
|
36
|
+
def test_kelvin_to_celsius_absolute_zero(self):
|
|
37
|
+
"""0 K = -273.15°C"""
|
|
38
|
+
result = units.kelvin(0).to(units.celsius)
|
|
39
|
+
self.assertAlmostEqual(result.quantity, -273.15, places=2)
|
|
40
|
+
|
|
41
|
+
def test_kelvin_to_celsius_room_temp(self):
|
|
42
|
+
"""293.15 K = 20°C"""
|
|
43
|
+
result = units.kelvin(293.15).to(units.celsius)
|
|
44
|
+
self.assertAlmostEqual(result.quantity, 20, places=1)
|
|
45
|
+
|
|
46
|
+
def test_fahrenheit_to_celsius_freezing(self):
|
|
47
|
+
"""32°F = 0°C"""
|
|
48
|
+
result = units.fahrenheit(32).to(units.celsius)
|
|
49
|
+
self.assertAlmostEqual(result.quantity, 0, places=1)
|
|
50
|
+
|
|
51
|
+
def test_fahrenheit_to_celsius_boiling(self):
|
|
52
|
+
"""212°F = 100°C"""
|
|
53
|
+
result = units.fahrenheit(212).to(units.celsius)
|
|
54
|
+
self.assertAlmostEqual(result.quantity, 100, places=1)
|
|
55
|
+
|
|
56
|
+
def test_celsius_to_fahrenheit_freezing(self):
|
|
57
|
+
"""0°C = 32°F"""
|
|
58
|
+
result = units.celsius(0).to(units.fahrenheit)
|
|
59
|
+
self.assertAlmostEqual(result.quantity, 32, places=1)
|
|
60
|
+
|
|
61
|
+
def test_celsius_to_fahrenheit_boiling(self):
|
|
62
|
+
"""100°C = 212°F"""
|
|
63
|
+
result = units.celsius(100).to(units.fahrenheit)
|
|
64
|
+
self.assertAlmostEqual(result.quantity, 212, places=1)
|
|
65
|
+
|
|
66
|
+
def test_fahrenheit_to_kelvin_absolute_zero(self):
|
|
67
|
+
"""-459.67°F = 0 K (approximately)"""
|
|
68
|
+
result = units.fahrenheit(-459.67).to(units.kelvin)
|
|
69
|
+
self.assertAlmostEqual(result.quantity, 0, places=0)
|
|
70
|
+
|
|
71
|
+
def test_kelvin_to_fahrenheit_boiling(self):
|
|
72
|
+
"""373.15 K = 212°F"""
|
|
73
|
+
result = units.kelvin(373.15).to(units.fahrenheit)
|
|
74
|
+
self.assertAlmostEqual(result.quantity, 212, places=0)
|
|
75
|
+
|
|
76
|
+
def test_temperature_round_trip_celsius(self):
|
|
77
|
+
"""Round-trip: C → K → C"""
|
|
78
|
+
original = 25.0
|
|
79
|
+
via_kelvin = units.celsius(original).to(units.kelvin)
|
|
80
|
+
back = via_kelvin.to(units.celsius)
|
|
81
|
+
self.assertAlmostEqual(back.quantity, original, places=10)
|
|
82
|
+
|
|
83
|
+
def test_temperature_round_trip_fahrenheit(self):
|
|
84
|
+
"""Round-trip: F → C → F"""
|
|
85
|
+
original = 98.6 # body temperature
|
|
86
|
+
via_celsius = units.fahrenheit(original).to(units.celsius)
|
|
87
|
+
back = via_celsius.to(units.fahrenheit)
|
|
88
|
+
self.assertAlmostEqual(back.quantity, original, places=10)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestPressureConversions(unittest.TestCase):
|
|
92
|
+
"""Tests for pressure conversions in the default graph."""
|
|
93
|
+
|
|
94
|
+
def setUp(self):
|
|
95
|
+
reset_default_graph()
|
|
96
|
+
|
|
97
|
+
def test_pascal_to_bar(self):
|
|
98
|
+
"""100000 Pa = 1 bar"""
|
|
99
|
+
result = units.pascal(100000).to(units.bar)
|
|
100
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=5)
|
|
101
|
+
|
|
102
|
+
def test_bar_to_pascal(self):
|
|
103
|
+
"""1 bar = 100000 Pa"""
|
|
104
|
+
result = units.bar(1).to(units.pascal)
|
|
105
|
+
self.assertAlmostEqual(result.quantity, 100000, places=0)
|
|
106
|
+
|
|
107
|
+
def test_pascal_to_psi(self):
|
|
108
|
+
"""6894.76 Pa ≈ 1 psi"""
|
|
109
|
+
result = units.pascal(6894.76).to(units.psi)
|
|
110
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
111
|
+
|
|
112
|
+
def test_psi_to_pascal(self):
|
|
113
|
+
"""1 psi ≈ 6894.76 Pa"""
|
|
114
|
+
result = units.psi(1).to(units.pascal)
|
|
115
|
+
self.assertAlmostEqual(result.quantity, 6894.76, places=0)
|
|
116
|
+
|
|
117
|
+
def test_atmosphere_to_pascal(self):
|
|
118
|
+
"""1 atm = 101325 Pa"""
|
|
119
|
+
result = units.atmosphere(1).to(units.pascal)
|
|
120
|
+
self.assertAlmostEqual(result.quantity, 101325, places=0)
|
|
121
|
+
|
|
122
|
+
def test_pascal_to_atmosphere(self):
|
|
123
|
+
"""101325 Pa = 1 atm"""
|
|
124
|
+
result = units.pascal(101325).to(units.atmosphere)
|
|
125
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=5)
|
|
126
|
+
|
|
127
|
+
def test_atmosphere_to_bar(self):
|
|
128
|
+
"""1 atm ≈ 1.01325 bar"""
|
|
129
|
+
result = units.atmosphere(1).to(units.bar)
|
|
130
|
+
self.assertAlmostEqual(result.quantity, 1.01325, places=4)
|
|
131
|
+
|
|
132
|
+
def test_bar_to_atmosphere(self):
|
|
133
|
+
"""1 bar ≈ 0.986923 atm"""
|
|
134
|
+
result = units.bar(1).to(units.atmosphere)
|
|
135
|
+
self.assertAlmostEqual(result.quantity, 0.986923, places=4)
|
|
136
|
+
|
|
137
|
+
def test_atmosphere_to_psi(self):
|
|
138
|
+
"""1 atm ≈ 14.696 psi"""
|
|
139
|
+
result = units.atmosphere(1).to(units.psi)
|
|
140
|
+
self.assertAlmostEqual(result.quantity, 14.696, places=2)
|
|
141
|
+
|
|
142
|
+
def test_psi_to_atmosphere(self):
|
|
143
|
+
"""14.696 psi ≈ 1 atm"""
|
|
144
|
+
result = units.psi(14.696).to(units.atmosphere)
|
|
145
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
146
|
+
|
|
147
|
+
def test_pressure_round_trip(self):
|
|
148
|
+
"""Round-trip: Pa → bar → Pa"""
|
|
149
|
+
original = 250000
|
|
150
|
+
via_bar = units.pascal(original).to(units.bar)
|
|
151
|
+
back = via_bar.to(units.pascal)
|
|
152
|
+
self.assertAlmostEqual(back.quantity, original, places=5)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestBaseSILengthConversions(unittest.TestCase):
|
|
156
|
+
"""Tests for length conversions in the default graph."""
|
|
157
|
+
|
|
158
|
+
def setUp(self):
|
|
159
|
+
reset_default_graph()
|
|
160
|
+
|
|
161
|
+
def test_meter_to_foot(self):
|
|
162
|
+
"""1 m ≈ 3.28084 ft"""
|
|
163
|
+
result = units.meter(1).to(units.foot)
|
|
164
|
+
self.assertAlmostEqual(result.quantity, 3.28084, places=4)
|
|
165
|
+
|
|
166
|
+
def test_foot_to_meter(self):
|
|
167
|
+
"""1 ft ≈ 0.3048 m"""
|
|
168
|
+
result = units.foot(1).to(units.meter)
|
|
169
|
+
self.assertAlmostEqual(result.quantity, 0.3048, places=4)
|
|
170
|
+
|
|
171
|
+
def test_foot_to_inch(self):
|
|
172
|
+
"""1 ft = 12 in"""
|
|
173
|
+
result = units.foot(1).to(units.inch)
|
|
174
|
+
self.assertAlmostEqual(result.quantity, 12, places=10)
|
|
175
|
+
|
|
176
|
+
def test_inch_to_foot(self):
|
|
177
|
+
"""12 in = 1 ft"""
|
|
178
|
+
result = units.inch(12).to(units.foot)
|
|
179
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
180
|
+
|
|
181
|
+
def test_meter_to_inch(self):
|
|
182
|
+
"""1 m ≈ 39.37 in (via foot)"""
|
|
183
|
+
result = units.meter(1).to(units.inch)
|
|
184
|
+
self.assertAlmostEqual(result.quantity, 39.37, places=1)
|
|
185
|
+
|
|
186
|
+
def test_foot_to_yard(self):
|
|
187
|
+
"""3 ft = 1 yd"""
|
|
188
|
+
result = units.foot(3).to(units.yard)
|
|
189
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
190
|
+
|
|
191
|
+
def test_yard_to_foot(self):
|
|
192
|
+
"""1 yd = 3 ft"""
|
|
193
|
+
result = units.yard(1).to(units.foot)
|
|
194
|
+
self.assertAlmostEqual(result.quantity, 3.0, places=10)
|
|
195
|
+
|
|
196
|
+
def test_mile_to_foot(self):
|
|
197
|
+
"""1 mi = 5280 ft"""
|
|
198
|
+
result = units.mile(1).to(units.foot)
|
|
199
|
+
self.assertAlmostEqual(result.quantity, 5280, places=0)
|
|
200
|
+
|
|
201
|
+
def test_foot_to_mile(self):
|
|
202
|
+
"""5280 ft = 1 mi"""
|
|
203
|
+
result = units.foot(5280).to(units.mile)
|
|
204
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
205
|
+
|
|
206
|
+
def test_meter_to_mile(self):
|
|
207
|
+
"""1609.34 m ≈ 1 mi (multi-hop: m → ft → mi)"""
|
|
208
|
+
result = units.meter(1609.34).to(units.mile)
|
|
209
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
210
|
+
|
|
211
|
+
def test_meter_to_yard(self):
|
|
212
|
+
"""1 m ≈ 1.094 yd (multi-hop: m → ft → yd)"""
|
|
213
|
+
result = units.meter(1).to(units.yard)
|
|
214
|
+
self.assertAlmostEqual(result.quantity, 1.094, places=2)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class TestBaseSIMassConversions(unittest.TestCase):
|
|
218
|
+
"""Tests for mass conversions in the default graph."""
|
|
219
|
+
|
|
220
|
+
def setUp(self):
|
|
221
|
+
reset_default_graph()
|
|
222
|
+
|
|
223
|
+
def test_kilogram_to_gram(self):
|
|
224
|
+
"""1 kg = 1000 g"""
|
|
225
|
+
result = units.kilogram(1).to(units.gram)
|
|
226
|
+
self.assertAlmostEqual(result.quantity, 1000, places=10)
|
|
227
|
+
|
|
228
|
+
def test_gram_to_kilogram(self):
|
|
229
|
+
"""1000 g = 1 kg"""
|
|
230
|
+
result = units.gram(1000).to(units.kilogram)
|
|
231
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
232
|
+
|
|
233
|
+
def test_kilogram_to_pound(self):
|
|
234
|
+
"""1 kg ≈ 2.20462 lb"""
|
|
235
|
+
result = units.kilogram(1).to(units.pound)
|
|
236
|
+
self.assertAlmostEqual(result.quantity, 2.20462, places=4)
|
|
237
|
+
|
|
238
|
+
def test_pound_to_kilogram(self):
|
|
239
|
+
"""1 lb ≈ 0.453592 kg"""
|
|
240
|
+
result = units.pound(1).to(units.kilogram)
|
|
241
|
+
self.assertAlmostEqual(result.quantity, 0.453592, places=4)
|
|
242
|
+
|
|
243
|
+
def test_pound_to_ounce(self):
|
|
244
|
+
"""1 lb = 16 oz"""
|
|
245
|
+
result = units.pound(1).to(units.ounce)
|
|
246
|
+
self.assertAlmostEqual(result.quantity, 16, places=10)
|
|
247
|
+
|
|
248
|
+
def test_ounce_to_pound(self):
|
|
249
|
+
"""16 oz = 1 lb"""
|
|
250
|
+
result = units.ounce(16).to(units.pound)
|
|
251
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
252
|
+
|
|
253
|
+
def test_gram_to_pound(self):
|
|
254
|
+
"""453.592 g ≈ 1 lb (via kg)"""
|
|
255
|
+
result = units.gram(453.592).to(units.pound)
|
|
256
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
257
|
+
|
|
258
|
+
def test_kilogram_to_ounce(self):
|
|
259
|
+
"""1 kg ≈ 35.274 oz (multi-hop: kg → lb → oz)"""
|
|
260
|
+
result = units.kilogram(1).to(units.ounce)
|
|
261
|
+
self.assertAlmostEqual(result.quantity, 35.274, places=1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TestBaseSITimeConversions(unittest.TestCase):
|
|
265
|
+
"""Tests for time conversions in the default graph."""
|
|
266
|
+
|
|
267
|
+
def setUp(self):
|
|
268
|
+
reset_default_graph()
|
|
269
|
+
|
|
270
|
+
def test_second_to_minute(self):
|
|
271
|
+
"""60 s = 1 min"""
|
|
272
|
+
result = units.second(60).to(units.minute)
|
|
273
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
274
|
+
|
|
275
|
+
def test_minute_to_second(self):
|
|
276
|
+
"""1 min = 60 s"""
|
|
277
|
+
result = units.minute(1).to(units.second)
|
|
278
|
+
self.assertAlmostEqual(result.quantity, 60, places=10)
|
|
279
|
+
|
|
280
|
+
def test_minute_to_hour(self):
|
|
281
|
+
"""60 min = 1 hr"""
|
|
282
|
+
result = units.minute(60).to(units.hour)
|
|
283
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
284
|
+
|
|
285
|
+
def test_hour_to_minute(self):
|
|
286
|
+
"""1 hr = 60 min"""
|
|
287
|
+
result = units.hour(1).to(units.minute)
|
|
288
|
+
self.assertAlmostEqual(result.quantity, 60, places=10)
|
|
289
|
+
|
|
290
|
+
def test_hour_to_day(self):
|
|
291
|
+
"""24 hr = 1 day"""
|
|
292
|
+
result = units.hour(24).to(units.day)
|
|
293
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
294
|
+
|
|
295
|
+
def test_day_to_hour(self):
|
|
296
|
+
"""1 day = 24 hr"""
|
|
297
|
+
result = units.day(1).to(units.hour)
|
|
298
|
+
self.assertAlmostEqual(result.quantity, 24, places=10)
|
|
299
|
+
|
|
300
|
+
def test_second_to_hour(self):
|
|
301
|
+
"""3600 s = 1 hr (multi-hop: s → min → hr)"""
|
|
302
|
+
result = units.second(3600).to(units.hour)
|
|
303
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
304
|
+
|
|
305
|
+
def test_second_to_day(self):
|
|
306
|
+
"""86400 s = 1 day (multi-hop: s → min → hr → day)"""
|
|
307
|
+
result = units.second(86400).to(units.day)
|
|
308
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
309
|
+
|
|
310
|
+
def test_day_to_second(self):
|
|
311
|
+
"""1 day = 86400 s"""
|
|
312
|
+
result = units.day(1).to(units.second)
|
|
313
|
+
self.assertAlmostEqual(result.quantity, 86400, places=0)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TestBaseSIVolumeConversions(unittest.TestCase):
|
|
317
|
+
"""Tests for volume conversions in the default graph."""
|
|
318
|
+
|
|
319
|
+
def setUp(self):
|
|
320
|
+
reset_default_graph()
|
|
321
|
+
|
|
322
|
+
def test_liter_to_gallon(self):
|
|
323
|
+
"""1 L ≈ 0.264172 gal"""
|
|
324
|
+
result = units.liter(1).to(units.gallon)
|
|
325
|
+
self.assertAlmostEqual(result.quantity, 0.264172, places=5)
|
|
326
|
+
|
|
327
|
+
def test_gallon_to_liter(self):
|
|
328
|
+
"""1 gal ≈ 3.78541 L"""
|
|
329
|
+
result = units.gallon(1).to(units.liter)
|
|
330
|
+
self.assertAlmostEqual(result.quantity, 3.78541, places=3)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class TestBaseSIEnergyConversions(unittest.TestCase):
|
|
334
|
+
"""Tests for energy conversions in the default graph."""
|
|
335
|
+
|
|
336
|
+
def setUp(self):
|
|
337
|
+
reset_default_graph()
|
|
338
|
+
|
|
339
|
+
def test_joule_to_calorie(self):
|
|
340
|
+
"""4.184 J = 1 cal"""
|
|
341
|
+
result = units.joule(4.184).to(units.calorie)
|
|
342
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=3)
|
|
343
|
+
|
|
344
|
+
def test_calorie_to_joule(self):
|
|
345
|
+
"""1 cal = 4.184 J"""
|
|
346
|
+
result = units.calorie(1).to(units.joule)
|
|
347
|
+
self.assertAlmostEqual(result.quantity, 4.184, places=3)
|
|
348
|
+
|
|
349
|
+
def test_joule_to_btu(self):
|
|
350
|
+
"""1055.06 J ≈ 1 BTU"""
|
|
351
|
+
result = units.joule(1055.06).to(units.btu)
|
|
352
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
353
|
+
|
|
354
|
+
def test_btu_to_joule(self):
|
|
355
|
+
"""1 BTU ≈ 1055.06 J"""
|
|
356
|
+
result = units.btu(1).to(units.joule)
|
|
357
|
+
self.assertAlmostEqual(result.quantity, 1055.06, places=0)
|
|
358
|
+
|
|
359
|
+
def test_calorie_to_btu(self):
|
|
360
|
+
"""252 cal ≈ 1 BTU (multi-hop via joule)"""
|
|
361
|
+
result = units.calorie(252).to(units.btu)
|
|
362
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestBaseSIPowerConversions(unittest.TestCase):
|
|
366
|
+
"""Tests for power conversions in the default graph."""
|
|
367
|
+
|
|
368
|
+
def setUp(self):
|
|
369
|
+
reset_default_graph()
|
|
370
|
+
|
|
371
|
+
def test_watt_to_horsepower(self):
|
|
372
|
+
"""745.7 W ≈ 1 hp"""
|
|
373
|
+
result = units.watt(745.7).to(units.horsepower)
|
|
374
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=2)
|
|
375
|
+
|
|
376
|
+
def test_horsepower_to_watt(self):
|
|
377
|
+
"""1 hp ≈ 745.7 W"""
|
|
378
|
+
result = units.horsepower(1).to(units.watt)
|
|
379
|
+
self.assertAlmostEqual(result.quantity, 745.7, places=0)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class TestInformationConversions(unittest.TestCase):
|
|
383
|
+
"""Tests for information unit conversions in the default graph."""
|
|
384
|
+
|
|
385
|
+
def setUp(self):
|
|
386
|
+
reset_default_graph()
|
|
387
|
+
|
|
388
|
+
def test_byte_to_bit(self):
|
|
389
|
+
"""1 B = 8 b"""
|
|
390
|
+
result = units.byte(1).to(units.bit)
|
|
391
|
+
self.assertAlmostEqual(result.quantity, 8, places=10)
|
|
392
|
+
|
|
393
|
+
def test_bit_to_byte(self):
|
|
394
|
+
"""8 b = 1 B"""
|
|
395
|
+
result = units.bit(8).to(units.byte)
|
|
396
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
397
|
+
|
|
398
|
+
def test_kilobyte_to_bit(self):
|
|
399
|
+
"""1 KB = 8000 b (using Scale.kilo)"""
|
|
400
|
+
from ucon.core import Scale
|
|
401
|
+
kilobyte = Scale.kilo * units.byte
|
|
402
|
+
result = kilobyte(1).to(units.bit)
|
|
403
|
+
self.assertAlmostEqual(result.quantity, 8000, places=0)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class TestConversionRoundTrips(unittest.TestCase):
|
|
407
|
+
"""Tests verifying round-trip conversion accuracy."""
|
|
408
|
+
|
|
409
|
+
def setUp(self):
|
|
410
|
+
reset_default_graph()
|
|
411
|
+
|
|
412
|
+
def test_length_round_trip(self):
|
|
413
|
+
"""meter → foot → meter"""
|
|
414
|
+
original = 42.5
|
|
415
|
+
via_foot = units.meter(original).to(units.foot)
|
|
416
|
+
back = via_foot.to(units.meter)
|
|
417
|
+
self.assertAlmostEqual(back.quantity, original, places=8)
|
|
418
|
+
|
|
419
|
+
def test_mass_round_trip(self):
|
|
420
|
+
"""kilogram → pound → kilogram"""
|
|
421
|
+
original = 75.0
|
|
422
|
+
via_pound = units.kilogram(original).to(units.pound)
|
|
423
|
+
back = via_pound.to(units.kilogram)
|
|
424
|
+
self.assertAlmostEqual(back.quantity, original, places=8)
|
|
425
|
+
|
|
426
|
+
def test_time_round_trip(self):
|
|
427
|
+
"""second → day → second"""
|
|
428
|
+
original = 172800 # 2 days
|
|
429
|
+
via_day = units.second(original).to(units.day)
|
|
430
|
+
back = via_day.to(units.second)
|
|
431
|
+
self.assertAlmostEqual(back.quantity, original, places=5)
|
|
432
|
+
|
|
433
|
+
def test_pressure_multi_hop_round_trip(self):
|
|
434
|
+
"""pascal → atmosphere → bar → pascal"""
|
|
435
|
+
original = 500000
|
|
436
|
+
via_atm = units.pascal(original).to(units.atmosphere)
|
|
437
|
+
via_bar = via_atm.to(units.bar)
|
|
438
|
+
back = via_bar.to(units.pascal)
|
|
439
|
+
self.assertAlmostEqual(back.quantity, original, places=2)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
if __name__ == '__main__':
|
|
443
|
+
unittest.main()
|
tests/ucon/test_quantity.py
CHANGED
|
@@ -29,25 +29,28 @@ class TestNumber(unittest.TestCase):
|
|
|
29
29
|
# Unit should be base gram (Scale.one)
|
|
30
30
|
self.assertEqual(result.unit.shorthand, "g")
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
def test_to_converts_between_units(self):
|
|
33
|
+
"""Test Number.to() converts between compatible units."""
|
|
34
|
+
# 1 gram to kilogram
|
|
35
|
+
kg = Scale.kilo * units.gram
|
|
36
|
+
result = self.number.to(kg)
|
|
37
|
+
self.assertAlmostEqual(result.quantity, 0.001, places=10)
|
|
38
|
+
|
|
39
|
+
# 1 gram to milligram
|
|
40
|
+
mg = Scale.milli * units.gram
|
|
41
|
+
result = self.number.to(mg)
|
|
42
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=10)
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# 1 kilogram to gram
|
|
45
|
+
one_kg = Number(unit=kg, quantity=1)
|
|
46
|
+
result = one_kg.to(units.gram)
|
|
47
|
+
self.assertAlmostEqual(result.quantity, 1000.0, places=10)
|
|
45
48
|
|
|
46
|
-
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
47
49
|
def test___repr__(self):
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.assertIn(self.number.
|
|
50
|
+
"""Test Number repr contains quantity and unit shorthand."""
|
|
51
|
+
repr_str = repr(self.number)
|
|
52
|
+
self.assertIn(str(self.number.quantity), repr_str)
|
|
53
|
+
self.assertIn(self.number.unit.shorthand, repr_str)
|
|
51
54
|
|
|
52
55
|
def test___truediv__(self):
|
|
53
56
|
dal = Scale.deca * units.gram
|
|
@@ -151,32 +154,32 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
151
154
|
assert evaluated.unit == units.gram
|
|
152
155
|
assert abs(evaluated.quantity - 6.238) < 1e-12
|
|
153
156
|
|
|
154
|
-
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
155
157
|
def test_default_number_is_dimensionless_one(self):
|
|
158
|
+
"""Default Number() is dimensionless with quantity=1."""
|
|
156
159
|
n = Number()
|
|
157
160
|
self.assertEqual(n.unit, units.none)
|
|
158
|
-
self.assertEqual(n.unit.scale, Scale.one)
|
|
159
161
|
self.assertEqual(n.quantity, 1)
|
|
160
162
|
self.assertAlmostEqual(n.value, 1.0)
|
|
161
163
|
self.assertIn("1", repr(n))
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
n = Number(quantity=
|
|
167
|
-
converted = n.to(
|
|
168
|
-
|
|
169
|
-
self.
|
|
165
|
+
def test_to_different_scale_changes_quantity(self):
|
|
166
|
+
"""Converting to a different scale changes the quantity."""
|
|
167
|
+
km = Scale.kilo * units.meter
|
|
168
|
+
n = Number(quantity=5, unit=km) # 5 km
|
|
169
|
+
converted = n.to(units.meter) # convert to meters
|
|
170
|
+
# quantity changes: 5 km = 5000 m
|
|
171
|
+
self.assertNotEqual(n.quantity, converted.quantity)
|
|
172
|
+
self.assertAlmostEqual(converted.quantity, 5000.0, places=10)
|
|
170
173
|
|
|
171
|
-
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
172
|
-
@unittest.skip("Requires ConversionGraph implementation")
|
|
173
174
|
def test_simplify_uses_value_as_quantity(self):
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
"""Simplify converts scaled quantity to base scale quantity."""
|
|
176
|
+
km = Scale.kilo * units.meter
|
|
177
|
+
n = Number(quantity=2, unit=km) # 2 km
|
|
176
178
|
simplified = n.simplify()
|
|
177
|
-
|
|
178
|
-
self.
|
|
179
|
-
|
|
179
|
+
# simplified.quantity should be the canonical magnitude (2 * 1000 = 2000)
|
|
180
|
+
self.assertAlmostEqual(simplified.quantity, 2000.0, places=10)
|
|
181
|
+
# canonical magnitude (physical quantity) is preserved
|
|
182
|
+
self.assertAlmostEqual(simplified._canonical_magnitude, n._canonical_magnitude, places=10)
|
|
180
183
|
|
|
181
184
|
def test_multiplication_combines_units_and_quantities(self):
|
|
182
185
|
n1 = Number(unit=units.joule, quantity=2)
|
|
@@ -185,14 +188,13 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
185
188
|
self.assertEqual(result.quantity, 6)
|
|
186
189
|
self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
|
|
187
190
|
|
|
188
|
-
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
189
|
-
@unittest.skip("Requires ConversionGraph implementation")
|
|
190
191
|
def test_division_combines_units_scales_and_quantities(self):
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
"""Division creates composite unit with preserved scales."""
|
|
193
|
+
km = Scale.kilo * units.meter
|
|
194
|
+
n1 = Number(unit=km, quantity=1000) # 1000 km
|
|
193
195
|
n2 = Number(unit=units.second, quantity=2)
|
|
194
196
|
|
|
195
|
-
result = n1 / n2
|
|
197
|
+
result = n1 / n2 # should yield <500 km/s>
|
|
196
198
|
|
|
197
199
|
cu = result.unit
|
|
198
200
|
self.assertIsInstance(cu, UnitProduct)
|
|
@@ -203,18 +205,13 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
203
205
|
# --- dimension check ---
|
|
204
206
|
self.assertEqual(cu.dimension, Dimension.velocity)
|
|
205
207
|
|
|
206
|
-
# --- scale check: km/s should have a kilo-scaled meter in the numerator ---
|
|
207
|
-
# find the meter-like unit in the components
|
|
208
|
-
meter_like = next(u for u, exp in cu.components.items() if u.dimension == Dimension.length)
|
|
209
|
-
self.assertEqual(meter_like.scale, Scale.kilo)
|
|
210
|
-
self.assertEqual(cu.components[meter_like], 1) # exponent = 1 in numerator
|
|
211
|
-
|
|
212
208
|
# --- symbolic shorthand ---
|
|
213
209
|
self.assertEqual(cu.shorthand, "km/s")
|
|
214
210
|
|
|
215
|
-
# ---
|
|
216
|
-
|
|
217
|
-
|
|
211
|
+
# --- convert to base units (m/s) ---
|
|
212
|
+
m_per_s = units.meter / units.second
|
|
213
|
+
canonical = result.to(m_per_s)
|
|
214
|
+
self.assertAlmostEqual(canonical.quantity, 500000, places=5)
|
|
218
215
|
self.assertEqual(canonical.unit.shorthand, "m/s")
|
|
219
216
|
|
|
220
217
|
def test_equality_with_non_number_raises_value_error(self):
|
|
@@ -465,7 +462,6 @@ class TestNumberSimplify(unittest.TestCase):
|
|
|
465
462
|
self.assertAlmostEqual(result.quantity, 0.5, places=10)
|
|
466
463
|
self.assertEqual(result.unit.shorthand, "g")
|
|
467
464
|
|
|
468
|
-
@unittest.skip("Requires Dimension.information and units.byte (see user story)")
|
|
469
465
|
def test_simplify_binary_prefix(self):
|
|
470
466
|
"""2 kibibytes simplifies to 2048 bytes."""
|
|
471
467
|
kibibyte = Scale.kibi * units.byte
|
|
@@ -531,7 +527,6 @@ class TestInformationDimension(unittest.TestCase):
|
|
|
531
527
|
result = units.bit(8).to(units.byte)
|
|
532
528
|
self.assertAlmostEqual(result.quantity, 1.0, places=10)
|
|
533
529
|
|
|
534
|
-
@unittest.skip("Requires Number.simplify() from ucon#93-numbers-can-be-simplified")
|
|
535
530
|
def test_kibibyte_simplify(self):
|
|
536
531
|
"""1 kibibyte simplifies to 1024 bytes."""
|
|
537
532
|
kibibyte = Scale.kibi * units.byte
|
|
@@ -539,7 +534,6 @@ class TestInformationDimension(unittest.TestCase):
|
|
|
539
534
|
self.assertAlmostEqual(result.quantity, 1024.0, places=10)
|
|
540
535
|
self.assertEqual(result.unit.shorthand, "B")
|
|
541
536
|
|
|
542
|
-
@unittest.skip("Requires Number.simplify() from ucon#93-numbers-can-be-simplified")
|
|
543
537
|
def test_kilobyte_simplify(self):
|
|
544
538
|
"""1 kilobyte simplifies to 1000 bytes."""
|
|
545
539
|
kilobyte = Scale.kilo * units.byte
|
ucon/graph.py
CHANGED
|
@@ -399,6 +399,14 @@ def _build_standard_graph() -> ConversionGraph:
|
|
|
399
399
|
# F → C: C = (F - 32) * 5/9
|
|
400
400
|
graph.add_edge(src=units.fahrenheit, dst=units.celsius, map=AffineMap(5/9, -32 * 5/9))
|
|
401
401
|
|
|
402
|
+
# --- Pressure ---
|
|
403
|
+
# 1 Pa = 0.00001 bar, so 1 bar = 100000 Pa
|
|
404
|
+
graph.add_edge(src=units.pascal, dst=units.bar, map=LinearMap(1/100000))
|
|
405
|
+
# 1 Pa = 0.000145038 psi
|
|
406
|
+
graph.add_edge(src=units.pascal, dst=units.psi, map=LinearMap(0.000145038))
|
|
407
|
+
# 1 atm = 101325 Pa
|
|
408
|
+
graph.add_edge(src=units.atmosphere, dst=units.pascal, map=LinearMap(101325))
|
|
409
|
+
|
|
402
410
|
# --- Volume ---
|
|
403
411
|
graph.add_edge(src=units.liter, dst=units.gallon, map=LinearMap(0.264172))
|
|
404
412
|
|
ucon/units.py
CHANGED
|
@@ -99,6 +99,11 @@ btu = Unit(name='btu', dimension=Dimension.energy, aliases=('BTU',))
|
|
|
99
99
|
|
|
100
100
|
# Power
|
|
101
101
|
horsepower = Unit(name='horsepower', dimension=Dimension.power, aliases=('hp',))
|
|
102
|
+
|
|
103
|
+
# Pressure
|
|
104
|
+
bar = Unit(name='bar', dimension=Dimension.pressure, aliases=('bar',))
|
|
105
|
+
psi = Unit(name='psi', dimension=Dimension.pressure, aliases=('lbf/in²',))
|
|
106
|
+
atmosphere = Unit(name='atmosphere', dimension=Dimension.pressure, aliases=('atm',))
|
|
102
107
|
# ----------------------------------------------------------------------
|
|
103
108
|
|
|
104
109
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucon
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: a tool for dimensional analysis: a "Unit CONverter"
|
|
5
5
|
Home-page: https://github.com/withtwoemms/ucon
|
|
6
6
|
Author: Emmanuel I. Obi
|
|
7
7
|
Maintainer: Emmanuel I. Obi
|
|
8
8
|
Maintainer-email: withtwoemms@gmail.com
|
|
9
9
|
License: Apache-2.0
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Education
|
|
13
13
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
|
|
2
|
+
tests/ucon/test_algebra.py,sha256=Esm38M1QBNsV7vdfoFgqRUluyvWX7yccB0RwZXk4DpA,8433
|
|
3
|
+
tests/ucon/test_core.py,sha256=bmwSRWPlhwossy5NJ9rcPWujFmzBBPOeZzPAzN1acFg,32631
|
|
4
|
+
tests/ucon/test_default_graph_conversions.py,sha256=rkcDcSV1_kZeuPf4ModHDpgfkOPZS32xcKq7KPDRN-0,15760
|
|
5
|
+
tests/ucon/test_quantity.py,sha256=7rFg4cpdmGV7vyCwg72Bt4EFBgkyM3cPFoRbRn0bqEI,21943
|
|
6
|
+
tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
|
|
7
|
+
tests/ucon/conversion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
tests/ucon/conversion/test_graph.py,sha256=fs0aP6qNf8eE1uI7SoGSCW2XAkHYb7T9aaI-kzmO02c,16955
|
|
9
|
+
tests/ucon/conversion/test_map.py,sha256=DVFQ3xwp16Nuy9EtZRjKlWbkXfRUcM1mOzFrS4HhOaw,13886
|
|
10
|
+
ucon/__init__.py,sha256=jaFcNFZC5gxHKzM8OkS1pLxltp6ToLRVpuuhJQY9FKQ,2000
|
|
11
|
+
ucon/algebra.py,sha256=wGl4jJVMd8SXQ4sYDBOxV00ymAzRWfDhea1o4t2kVp4,7482
|
|
12
|
+
ucon/core.py,sha256=o3Q4posUOYoIhQVHl6bANCIcGKgGOpNZsnqGZw9ujYk,41523
|
|
13
|
+
ucon/graph.py,sha256=EH0Zi4yj8SI6o27V4uo4muucaqV5nCoL6S3syb-IfXc,14587
|
|
14
|
+
ucon/maps.py,sha256=yyZ7RqnohO2joTUvvKh40in7E6SKMQIQ8jkECO0-_NA,4753
|
|
15
|
+
ucon/quantity.py,sha256=GBxZ_96nocx-8F-usNWGbPvWHRhRgdZzqfH9Sx69iC4,465
|
|
16
|
+
ucon/units.py,sha256=2hisCuB_kTDcNlG6tzze2ZNVpmsnEeEFGWhfbrbomzk,6096
|
|
17
|
+
ucon-0.4.1.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
|
|
18
|
+
ucon-0.4.1.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
|
|
19
|
+
ucon-0.4.1.dist-info/METADATA,sha256=bHAexnROmQ4BUaxkY83fLe6ptkCGwt7aVEckFrflg3U,12348
|
|
20
|
+
ucon-0.4.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
ucon-0.4.1.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
|
|
22
|
+
ucon-0.4.1.dist-info/RECORD,,
|
ucon-0.4.0.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
|
|
2
|
-
tests/ucon/test_algebra.py,sha256=Esm38M1QBNsV7vdfoFgqRUluyvWX7yccB0RwZXk4DpA,8433
|
|
3
|
-
tests/ucon/test_core.py,sha256=NqmyKMkrF3T4ekucsFzVsxbkErdB6assbkhBAYH2fug,32664
|
|
4
|
-
tests/ucon/test_quantity.py,sha256=qJ-dXOPgIp0SKelASu45IC6KquTjlcRwHVkSKicsrXw,22754
|
|
5
|
-
tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
|
|
6
|
-
tests/ucon/conversion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
-
tests/ucon/conversion/test_graph.py,sha256=R3u-0FCpgffoUXvJmT7aBJCUDUpzmBYROh5I0WLHA_Y,7256
|
|
8
|
-
tests/ucon/conversion/test_map.py,sha256=2_bxXCuga-lTGxU5rGferiziJmjzrltksA-6JJseEQ0,5359
|
|
9
|
-
ucon/__init__.py,sha256=jaFcNFZC5gxHKzM8OkS1pLxltp6ToLRVpuuhJQY9FKQ,2000
|
|
10
|
-
ucon/algebra.py,sha256=wGl4jJVMd8SXQ4sYDBOxV00ymAzRWfDhea1o4t2kVp4,7482
|
|
11
|
-
ucon/core.py,sha256=o3Q4posUOYoIhQVHl6bANCIcGKgGOpNZsnqGZw9ujYk,41523
|
|
12
|
-
ucon/graph.py,sha256=lgueyVdBI73pgqsydwRtOY9sxt02G-gy8-Lf7993RNw,14224
|
|
13
|
-
ucon/maps.py,sha256=yyZ7RqnohO2joTUvvKh40in7E6SKMQIQ8jkECO0-_NA,4753
|
|
14
|
-
ucon/quantity.py,sha256=GBxZ_96nocx-8F-usNWGbPvWHRhRgdZzqfH9Sx69iC4,465
|
|
15
|
-
ucon/units.py,sha256=PJFAqUoEq_0--Zo7JDHezZBPHPbAlS_5eArIVCLemxA,5852
|
|
16
|
-
ucon-0.4.0.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
|
|
17
|
-
ucon-0.4.0.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
|
|
18
|
-
ucon-0.4.0.dist-info/METADATA,sha256=_hZdjrgY1lvorBac-JPQQVzNVuNet-QY2zPP0G8-hik,12349
|
|
19
|
-
ucon-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
20
|
-
ucon-0.4.0.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
|
|
21
|
-
ucon-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|