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.
- ucon/__init__.py +4 -1
- ucon/core.py +7 -2
- ucon/mcp/__init__.py +8 -0
- ucon/mcp/server.py +250 -0
- ucon/pydantic.py +199 -0
- ucon/units.py +259 -11
- {ucon-0.5.2.dist-info → ucon-0.6.0.dist-info}/METADATA +73 -97
- ucon-0.6.0.dist-info/RECORD +17 -0
- ucon-0.6.0.dist-info/entry_points.txt +2 -0
- ucon-0.6.0.dist-info/top_level.txt +1 -0
- tests/ucon/__init__.py +0 -3
- tests/ucon/conversion/__init__.py +0 -0
- tests/ucon/conversion/test_graph.py +0 -409
- tests/ucon/conversion/test_map.py +0 -409
- tests/ucon/test_algebra.py +0 -239
- tests/ucon/test_basis_transform.py +0 -521
- tests/ucon/test_core.py +0 -827
- tests/ucon/test_default_graph_conversions.py +0 -443
- tests/ucon/test_dimensionless_units.py +0 -248
- tests/ucon/test_graph_basis_transform.py +0 -263
- tests/ucon/test_quantity.py +0 -615
- tests/ucon/test_rebased_unit.py +0 -184
- tests/ucon/test_uncertainty.py +0 -264
- tests/ucon/test_unit_system.py +0 -174
- tests/ucon/test_units.py +0 -25
- tests/ucon/test_vector_fraction.py +0 -185
- ucon-0.5.2.dist-info/RECORD +0 -29
- ucon-0.5.2.dist-info/top_level.txt +0 -2
- {ucon-0.5.2.dist-info → ucon-0.6.0.dist-info}/WHEEL +0 -0
- {ucon-0.5.2.dist-info → ucon-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.5.2.dist-info → ucon-0.6.0.dist-info}/licenses/NOTICE +0 -0
tests/ucon/test_rebased_unit.py
DELETED
|
@@ -1,184 +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 RebasedUnit.
|
|
7
|
-
|
|
8
|
-
Verifies that units transformed by a BasisTransform preserve
|
|
9
|
-
provenance while exposing the rebased dimension.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import unittest
|
|
13
|
-
from fractions import Fraction
|
|
14
|
-
|
|
15
|
-
from ucon.core import (
|
|
16
|
-
BasisTransform,
|
|
17
|
-
Dimension,
|
|
18
|
-
RebasedUnit,
|
|
19
|
-
Unit,
|
|
20
|
-
UnitSystem,
|
|
21
|
-
)
|
|
22
|
-
from ucon import units
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TestRebasedUnitConstruction(unittest.TestCase):
|
|
26
|
-
"""Test RebasedUnit construction."""
|
|
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
|
-
self.bt = BasisTransform(
|
|
46
|
-
src=self.custom,
|
|
47
|
-
dst=self.si,
|
|
48
|
-
src_dimensions=(Dimension.length,),
|
|
49
|
-
dst_dimensions=(Dimension.length,),
|
|
50
|
-
matrix=((1,),),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
def test_valid_construction(self):
|
|
54
|
-
rebased = RebasedUnit(
|
|
55
|
-
original=units.foot,
|
|
56
|
-
rebased_dimension=Dimension.length,
|
|
57
|
-
basis_transform=self.bt,
|
|
58
|
-
)
|
|
59
|
-
self.assertEqual(rebased.original, units.foot)
|
|
60
|
-
self.assertEqual(rebased.rebased_dimension, Dimension.length)
|
|
61
|
-
self.assertEqual(rebased.basis_transform, self.bt)
|
|
62
|
-
|
|
63
|
-
def test_dimension_property(self):
|
|
64
|
-
rebased = RebasedUnit(
|
|
65
|
-
original=units.foot,
|
|
66
|
-
rebased_dimension=Dimension.length,
|
|
67
|
-
basis_transform=self.bt,
|
|
68
|
-
)
|
|
69
|
-
self.assertEqual(rebased.dimension, Dimension.length)
|
|
70
|
-
|
|
71
|
-
def test_name_property(self):
|
|
72
|
-
rebased = RebasedUnit(
|
|
73
|
-
original=units.foot,
|
|
74
|
-
rebased_dimension=Dimension.length,
|
|
75
|
-
basis_transform=self.bt,
|
|
76
|
-
)
|
|
77
|
-
self.assertEqual(rebased.name, "foot")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
class TestRebasedUnitEquality(unittest.TestCase):
|
|
81
|
-
"""Test RebasedUnit equality and hashing."""
|
|
82
|
-
|
|
83
|
-
def setUp(self):
|
|
84
|
-
self.si = UnitSystem(
|
|
85
|
-
name="SI",
|
|
86
|
-
bases={
|
|
87
|
-
Dimension.length: units.meter,
|
|
88
|
-
Dimension.mass: units.kilogram,
|
|
89
|
-
Dimension.time: units.second,
|
|
90
|
-
}
|
|
91
|
-
)
|
|
92
|
-
self.custom = UnitSystem(
|
|
93
|
-
name="Custom",
|
|
94
|
-
bases={
|
|
95
|
-
Dimension.length: units.foot,
|
|
96
|
-
Dimension.mass: units.pound,
|
|
97
|
-
Dimension.time: units.second,
|
|
98
|
-
}
|
|
99
|
-
)
|
|
100
|
-
self.bt = BasisTransform(
|
|
101
|
-
src=self.custom,
|
|
102
|
-
dst=self.si,
|
|
103
|
-
src_dimensions=(Dimension.length,),
|
|
104
|
-
dst_dimensions=(Dimension.length,),
|
|
105
|
-
matrix=((1,),),
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
def test_equal_rebased_units(self):
|
|
109
|
-
r1 = RebasedUnit(
|
|
110
|
-
original=units.foot,
|
|
111
|
-
rebased_dimension=Dimension.length,
|
|
112
|
-
basis_transform=self.bt,
|
|
113
|
-
)
|
|
114
|
-
r2 = RebasedUnit(
|
|
115
|
-
original=units.foot,
|
|
116
|
-
rebased_dimension=Dimension.length,
|
|
117
|
-
basis_transform=self.bt,
|
|
118
|
-
)
|
|
119
|
-
self.assertEqual(r1, r2)
|
|
120
|
-
|
|
121
|
-
def test_hashable(self):
|
|
122
|
-
r1 = RebasedUnit(
|
|
123
|
-
original=units.foot,
|
|
124
|
-
rebased_dimension=Dimension.length,
|
|
125
|
-
basis_transform=self.bt,
|
|
126
|
-
)
|
|
127
|
-
r2 = RebasedUnit(
|
|
128
|
-
original=units.foot,
|
|
129
|
-
rebased_dimension=Dimension.length,
|
|
130
|
-
basis_transform=self.bt,
|
|
131
|
-
)
|
|
132
|
-
self.assertEqual(hash(r1), hash(r2))
|
|
133
|
-
self.assertEqual(len({r1, r2}), 1)
|
|
134
|
-
|
|
135
|
-
def test_different_original_not_equal(self):
|
|
136
|
-
r1 = RebasedUnit(
|
|
137
|
-
original=units.foot,
|
|
138
|
-
rebased_dimension=Dimension.length,
|
|
139
|
-
basis_transform=self.bt,
|
|
140
|
-
)
|
|
141
|
-
r2 = RebasedUnit(
|
|
142
|
-
original=units.inch,
|
|
143
|
-
rebased_dimension=Dimension.length,
|
|
144
|
-
basis_transform=self.bt,
|
|
145
|
-
)
|
|
146
|
-
self.assertNotEqual(r1, r2)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class TestRebasedUnitImmutability(unittest.TestCase):
|
|
150
|
-
"""Test that RebasedUnit is immutable."""
|
|
151
|
-
|
|
152
|
-
def setUp(self):
|
|
153
|
-
self.si = UnitSystem(
|
|
154
|
-
name="SI",
|
|
155
|
-
bases={
|
|
156
|
-
Dimension.length: units.meter,
|
|
157
|
-
}
|
|
158
|
-
)
|
|
159
|
-
self.custom = UnitSystem(
|
|
160
|
-
name="Custom",
|
|
161
|
-
bases={
|
|
162
|
-
Dimension.length: units.foot,
|
|
163
|
-
}
|
|
164
|
-
)
|
|
165
|
-
self.bt = BasisTransform(
|
|
166
|
-
src=self.custom,
|
|
167
|
-
dst=self.si,
|
|
168
|
-
src_dimensions=(Dimension.length,),
|
|
169
|
-
dst_dimensions=(Dimension.length,),
|
|
170
|
-
matrix=((1,),),
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
def test_frozen_dataclass(self):
|
|
174
|
-
rebased = RebasedUnit(
|
|
175
|
-
original=units.foot,
|
|
176
|
-
rebased_dimension=Dimension.length,
|
|
177
|
-
basis_transform=self.bt,
|
|
178
|
-
)
|
|
179
|
-
with self.assertRaises(AttributeError):
|
|
180
|
-
rebased.original = units.meter
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if __name__ == "__main__":
|
|
184
|
-
unittest.main()
|
tests/ucon/test_uncertainty.py
DELETED
|
@@ -1,264 +0,0 @@
|
|
|
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 v0.5.x uncertainty propagation.
|
|
7
|
-
|
|
8
|
-
Tests Number construction with uncertainty, display formatting,
|
|
9
|
-
arithmetic propagation, and conversion propagation.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import math
|
|
13
|
-
import unittest
|
|
14
|
-
|
|
15
|
-
from ucon import units, Scale
|
|
16
|
-
from ucon.core import Number, UnitProduct
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestUncertaintyConstruction(unittest.TestCase):
|
|
20
|
-
"""Test constructing Numbers with uncertainty."""
|
|
21
|
-
|
|
22
|
-
def test_number_with_uncertainty(self):
|
|
23
|
-
n = units.meter(1.234, uncertainty=0.005)
|
|
24
|
-
self.assertEqual(n.value, 1.234)
|
|
25
|
-
self.assertEqual(n.uncertainty, 0.005)
|
|
26
|
-
|
|
27
|
-
def test_number_without_uncertainty(self):
|
|
28
|
-
n = units.meter(1.234)
|
|
29
|
-
self.assertEqual(n.value, 1.234)
|
|
30
|
-
self.assertIsNone(n.uncertainty)
|
|
31
|
-
|
|
32
|
-
def test_unit_product_callable_with_uncertainty(self):
|
|
33
|
-
km = Scale.kilo * units.meter
|
|
34
|
-
n = km(5.0, uncertainty=0.1)
|
|
35
|
-
self.assertEqual(n.value, 5.0)
|
|
36
|
-
self.assertEqual(n.uncertainty, 0.1)
|
|
37
|
-
|
|
38
|
-
def test_composite_unit_with_uncertainty(self):
|
|
39
|
-
mps = units.meter / units.second
|
|
40
|
-
n = mps(10.0, uncertainty=0.5)
|
|
41
|
-
self.assertEqual(n.value, 10.0)
|
|
42
|
-
self.assertEqual(n.uncertainty, 0.5)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class TestUncertaintyDisplay(unittest.TestCase):
|
|
46
|
-
"""Test display formatting with uncertainty."""
|
|
47
|
-
|
|
48
|
-
def test_repr_with_uncertainty(self):
|
|
49
|
-
n = units.meter(1.234, uncertainty=0.005)
|
|
50
|
-
r = repr(n)
|
|
51
|
-
self.assertIn("1.234", r)
|
|
52
|
-
self.assertIn("±", r)
|
|
53
|
-
self.assertIn("0.005", r)
|
|
54
|
-
self.assertIn("m", r)
|
|
55
|
-
|
|
56
|
-
def test_repr_without_uncertainty(self):
|
|
57
|
-
n = units.meter(1.234)
|
|
58
|
-
r = repr(n)
|
|
59
|
-
self.assertIn("1.234", r)
|
|
60
|
-
self.assertNotIn("±", r)
|
|
61
|
-
|
|
62
|
-
def test_repr_dimensionless_with_uncertainty(self):
|
|
63
|
-
n = Number(quantity=0.5, uncertainty=0.01)
|
|
64
|
-
r = repr(n)
|
|
65
|
-
self.assertIn("0.5", r)
|
|
66
|
-
self.assertIn("±", r)
|
|
67
|
-
self.assertIn("0.01", r)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class TestAdditionSubtractionPropagation(unittest.TestCase):
|
|
71
|
-
"""Test uncertainty propagation through addition and subtraction."""
|
|
72
|
-
|
|
73
|
-
def test_addition_both_uncertain(self):
|
|
74
|
-
a = units.meter(10.0, uncertainty=0.3)
|
|
75
|
-
b = units.meter(5.0, uncertainty=0.4)
|
|
76
|
-
c = a + b
|
|
77
|
-
self.assertAlmostEqual(c.value, 15.0, places=9)
|
|
78
|
-
# sqrt(0.3² + 0.4²) = sqrt(0.09 + 0.16) = sqrt(0.25) = 0.5
|
|
79
|
-
self.assertAlmostEqual(c.uncertainty, 0.5, places=9)
|
|
80
|
-
|
|
81
|
-
def test_subtraction_both_uncertain(self):
|
|
82
|
-
a = units.meter(10.0, uncertainty=0.3)
|
|
83
|
-
b = units.meter(5.0, uncertainty=0.4)
|
|
84
|
-
c = a - b
|
|
85
|
-
self.assertAlmostEqual(c.value, 5.0, places=9)
|
|
86
|
-
# sqrt(0.3² + 0.4²) = 0.5
|
|
87
|
-
self.assertAlmostEqual(c.uncertainty, 0.5, places=9)
|
|
88
|
-
|
|
89
|
-
def test_addition_one_uncertain(self):
|
|
90
|
-
a = units.meter(10.0, uncertainty=0.3)
|
|
91
|
-
b = units.meter(5.0) # no uncertainty
|
|
92
|
-
c = a + b
|
|
93
|
-
self.assertAlmostEqual(c.value, 15.0, places=9)
|
|
94
|
-
self.assertAlmostEqual(c.uncertainty, 0.3, places=9)
|
|
95
|
-
|
|
96
|
-
def test_addition_neither_uncertain(self):
|
|
97
|
-
a = units.meter(10.0)
|
|
98
|
-
b = units.meter(5.0)
|
|
99
|
-
c = a + b
|
|
100
|
-
self.assertAlmostEqual(c.value, 15.0, places=9)
|
|
101
|
-
self.assertIsNone(c.uncertainty)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class TestMultiplicationPropagation(unittest.TestCase):
|
|
105
|
-
"""Test uncertainty propagation through multiplication."""
|
|
106
|
-
|
|
107
|
-
def test_multiplication_both_uncertain(self):
|
|
108
|
-
a = units.meter(10.0, uncertainty=0.2) # 2% relative
|
|
109
|
-
b = units.meter(5.0, uncertainty=0.15) # 3% relative
|
|
110
|
-
c = a * b
|
|
111
|
-
self.assertAlmostEqual(c.value, 50.0, places=9)
|
|
112
|
-
# relative: sqrt((0.2/10)² + (0.15/5)²) = sqrt(0.0004 + 0.0009) = sqrt(0.0013) ≈ 0.0361
|
|
113
|
-
# absolute: 50 * 0.0361 ≈ 1.803
|
|
114
|
-
expected = 50.0 * math.sqrt((0.2/10)**2 + (0.15/5)**2)
|
|
115
|
-
self.assertAlmostEqual(c.uncertainty, expected, places=6)
|
|
116
|
-
|
|
117
|
-
def test_multiplication_one_uncertain(self):
|
|
118
|
-
a = units.meter(10.0, uncertainty=0.2)
|
|
119
|
-
b = units.meter(5.0) # no uncertainty
|
|
120
|
-
c = a * b
|
|
121
|
-
self.assertAlmostEqual(c.value, 50.0, places=9)
|
|
122
|
-
# Only a contributes: |c| * (δa/a) = 50 * 0.02 = 1.0
|
|
123
|
-
expected = 50.0 * (0.2/10)
|
|
124
|
-
self.assertAlmostEqual(c.uncertainty, expected, places=9)
|
|
125
|
-
|
|
126
|
-
def test_multiplication_neither_uncertain(self):
|
|
127
|
-
a = units.meter(10.0)
|
|
128
|
-
b = units.meter(5.0)
|
|
129
|
-
c = a * b
|
|
130
|
-
self.assertAlmostEqual(c.value, 50.0, places=9)
|
|
131
|
-
self.assertIsNone(c.uncertainty)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class TestDivisionPropagation(unittest.TestCase):
|
|
135
|
-
"""Test uncertainty propagation through division."""
|
|
136
|
-
|
|
137
|
-
def test_division_both_uncertain(self):
|
|
138
|
-
a = units.meter(10.0, uncertainty=0.2) # 2% relative
|
|
139
|
-
b = units.second(2.0, uncertainty=0.04) # 2% relative
|
|
140
|
-
c = a / b
|
|
141
|
-
self.assertAlmostEqual(c.value, 5.0, places=9)
|
|
142
|
-
# relative: sqrt((0.2/10)² + (0.04/2)²) = sqrt(0.0004 + 0.0004) = sqrt(0.0008) ≈ 0.0283
|
|
143
|
-
# absolute: 5 * 0.0283 ≈ 0.1414
|
|
144
|
-
expected = 5.0 * math.sqrt((0.2/10)**2 + (0.04/2)**2)
|
|
145
|
-
self.assertAlmostEqual(c.uncertainty, expected, places=6)
|
|
146
|
-
|
|
147
|
-
def test_division_one_uncertain(self):
|
|
148
|
-
a = units.meter(10.0, uncertainty=0.2)
|
|
149
|
-
b = units.second(2.0) # no uncertainty
|
|
150
|
-
c = a / b
|
|
151
|
-
self.assertAlmostEqual(c.value, 5.0, places=9)
|
|
152
|
-
expected = 5.0 * (0.2/10)
|
|
153
|
-
self.assertAlmostEqual(c.uncertainty, expected, places=9)
|
|
154
|
-
|
|
155
|
-
def test_division_neither_uncertain(self):
|
|
156
|
-
a = units.meter(10.0)
|
|
157
|
-
b = units.second(2.0)
|
|
158
|
-
c = a / b
|
|
159
|
-
self.assertAlmostEqual(c.value, 5.0, places=9)
|
|
160
|
-
self.assertIsNone(c.uncertainty)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
class TestScalarOperations(unittest.TestCase):
|
|
164
|
-
"""Test uncertainty propagation with scalar multiplication/division."""
|
|
165
|
-
|
|
166
|
-
def test_scalar_multiply_number(self):
|
|
167
|
-
a = units.meter(10.0, uncertainty=0.2)
|
|
168
|
-
c = a * 3 # scalar multiplication
|
|
169
|
-
self.assertAlmostEqual(c.value, 30.0, places=9)
|
|
170
|
-
self.assertAlmostEqual(c.uncertainty, 0.6, places=9)
|
|
171
|
-
|
|
172
|
-
def test_scalar_divide_number(self):
|
|
173
|
-
a = units.meter(10.0, uncertainty=0.2)
|
|
174
|
-
c = a / 2 # scalar division
|
|
175
|
-
self.assertAlmostEqual(c.value, 5.0, places=9)
|
|
176
|
-
self.assertAlmostEqual(c.uncertainty, 0.1, places=9)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
class TestConversionPropagation(unittest.TestCase):
|
|
180
|
-
"""Test uncertainty propagation through unit conversion."""
|
|
181
|
-
|
|
182
|
-
def test_linear_conversion(self):
|
|
183
|
-
# meter to foot: factor ≈ 3.28084
|
|
184
|
-
length = units.meter(1.0, uncertainty=0.01)
|
|
185
|
-
length_ft = length.to(units.foot)
|
|
186
|
-
self.assertAlmostEqual(length_ft.value, 3.28084, places=4)
|
|
187
|
-
# uncertainty scales by same factor
|
|
188
|
-
self.assertAlmostEqual(length_ft.uncertainty, 0.01 * 3.28084, places=4)
|
|
189
|
-
|
|
190
|
-
def test_affine_conversion(self):
|
|
191
|
-
# Celsius to Kelvin: K = C + 273.15
|
|
192
|
-
# Derivative is 1, so uncertainty unchanged
|
|
193
|
-
temp = units.celsius(25.0, uncertainty=0.5)
|
|
194
|
-
temp_k = temp.to(units.kelvin)
|
|
195
|
-
self.assertAlmostEqual(temp_k.value, 298.15, places=9)
|
|
196
|
-
self.assertAlmostEqual(temp_k.uncertainty, 0.5, places=9)
|
|
197
|
-
|
|
198
|
-
def test_fahrenheit_to_celsius(self):
|
|
199
|
-
# F to C: C = (F - 32) * 5/9
|
|
200
|
-
# Derivative is 5/9 ≈ 0.5556
|
|
201
|
-
temp = units.fahrenheit(100.0, uncertainty=1.0)
|
|
202
|
-
temp_c = temp.to(units.celsius)
|
|
203
|
-
self.assertAlmostEqual(temp_c.value, 37.7778, places=3)
|
|
204
|
-
self.assertAlmostEqual(temp_c.uncertainty, 1.0 * (5/9), places=6)
|
|
205
|
-
|
|
206
|
-
def test_conversion_without_uncertainty(self):
|
|
207
|
-
length = units.meter(1.0)
|
|
208
|
-
length_ft = length.to(units.foot)
|
|
209
|
-
self.assertIsNone(length_ft.uncertainty)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class TestEdgeCases(unittest.TestCase):
|
|
213
|
-
"""Test edge cases for uncertainty handling."""
|
|
214
|
-
|
|
215
|
-
def test_zero_uncertainty(self):
|
|
216
|
-
a = units.meter(10.0, uncertainty=0.0)
|
|
217
|
-
b = units.meter(5.0, uncertainty=0.0)
|
|
218
|
-
c = a + b
|
|
219
|
-
self.assertAlmostEqual(c.uncertainty, 0.0, places=9)
|
|
220
|
-
|
|
221
|
-
def test_very_small_uncertainty(self):
|
|
222
|
-
a = units.meter(10.0, uncertainty=1e-15)
|
|
223
|
-
b = units.meter(5.0, uncertainty=1e-15)
|
|
224
|
-
c = a * b
|
|
225
|
-
self.assertIsNotNone(c.uncertainty)
|
|
226
|
-
self.assertGreater(c.uncertainty, 0)
|
|
227
|
-
|
|
228
|
-
def test_uncertainty_preserved_through_simplify(self):
|
|
229
|
-
km = Scale.kilo * units.meter
|
|
230
|
-
n = km(5.0, uncertainty=0.1)
|
|
231
|
-
simplified = n.simplify()
|
|
232
|
-
self.assertAlmostEqual(simplified.value, 5000.0, places=9)
|
|
233
|
-
# uncertainty also scales
|
|
234
|
-
self.assertAlmostEqual(simplified.uncertainty, 100.0, places=9)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
class TestMapDerivative(unittest.TestCase):
|
|
238
|
-
"""Test Map.derivative() implementations."""
|
|
239
|
-
|
|
240
|
-
def test_linear_map_derivative(self):
|
|
241
|
-
from ucon.maps import LinearMap
|
|
242
|
-
m = LinearMap(3.28084)
|
|
243
|
-
self.assertAlmostEqual(m.derivative(0), 3.28084, places=9)
|
|
244
|
-
self.assertAlmostEqual(m.derivative(100), 3.28084, places=9)
|
|
245
|
-
|
|
246
|
-
def test_affine_map_derivative(self):
|
|
247
|
-
from ucon.maps import AffineMap
|
|
248
|
-
m = AffineMap(5/9, -32 * 5/9) # F to C
|
|
249
|
-
self.assertAlmostEqual(m.derivative(0), 5/9, places=9)
|
|
250
|
-
self.assertAlmostEqual(m.derivative(100), 5/9, places=9)
|
|
251
|
-
|
|
252
|
-
def test_composed_map_derivative(self):
|
|
253
|
-
from ucon.maps import LinearMap, AffineMap
|
|
254
|
-
# Compose: first scale by 2, then add 10
|
|
255
|
-
inner = LinearMap(2)
|
|
256
|
-
outer = AffineMap(1, 10)
|
|
257
|
-
composed = outer @ inner
|
|
258
|
-
# d/dx [1*(2x) + 10] = 2
|
|
259
|
-
self.assertAlmostEqual(composed.derivative(0), 2, places=9)
|
|
260
|
-
self.assertAlmostEqual(composed.derivative(5), 2, places=9)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if __name__ == "__main__":
|
|
264
|
-
unittest.main()
|
tests/ucon/test_unit_system.py
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# © 2026 The Radiativity Company
|
|
2
|
-
# Licensed under the Apache License, Version 2.0
|
|
3
|
-
# See the LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
"""
|
|
6
|
-
Tests for UnitSystem.
|
|
7
|
-
|
|
8
|
-
Verifies construction, validation, and query methods for named
|
|
9
|
-
unit system definitions.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import unittest
|
|
13
|
-
|
|
14
|
-
from ucon import units
|
|
15
|
-
from ucon.core import Dimension, Unit, UnitSystem, DimensionNotCovered
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class TestUnitSystemConstruction(unittest.TestCase):
|
|
19
|
-
"""Test UnitSystem construction and validation."""
|
|
20
|
-
|
|
21
|
-
def test_valid_construction(self):
|
|
22
|
-
system = UnitSystem(
|
|
23
|
-
name="SI",
|
|
24
|
-
bases={
|
|
25
|
-
Dimension.length: units.meter,
|
|
26
|
-
Dimension.mass: units.kilogram,
|
|
27
|
-
Dimension.time: units.second,
|
|
28
|
-
}
|
|
29
|
-
)
|
|
30
|
-
self.assertEqual(system.name, "SI")
|
|
31
|
-
self.assertEqual(len(system.bases), 3)
|
|
32
|
-
|
|
33
|
-
def test_single_base_allowed(self):
|
|
34
|
-
system = UnitSystem(
|
|
35
|
-
name="length-only",
|
|
36
|
-
bases={Dimension.length: units.meter}
|
|
37
|
-
)
|
|
38
|
-
self.assertEqual(len(system.bases), 1)
|
|
39
|
-
|
|
40
|
-
def test_empty_name_rejected(self):
|
|
41
|
-
with self.assertRaises(ValueError) as ctx:
|
|
42
|
-
UnitSystem(name="", bases={Dimension.length: units.meter})
|
|
43
|
-
self.assertIn("name", str(ctx.exception).lower())
|
|
44
|
-
|
|
45
|
-
def test_empty_bases_rejected(self):
|
|
46
|
-
with self.assertRaises(ValueError) as ctx:
|
|
47
|
-
UnitSystem(name="empty", bases={})
|
|
48
|
-
self.assertIn("base", str(ctx.exception).lower())
|
|
49
|
-
|
|
50
|
-
def test_mismatched_dimension_rejected(self):
|
|
51
|
-
# meter has Dimension.length, but we declare it as mass
|
|
52
|
-
with self.assertRaises(ValueError) as ctx:
|
|
53
|
-
UnitSystem(
|
|
54
|
-
name="bad",
|
|
55
|
-
bases={Dimension.mass: units.meter}
|
|
56
|
-
)
|
|
57
|
-
self.assertIn("dimension", str(ctx.exception).lower())
|
|
58
|
-
|
|
59
|
-
def test_partial_system_allowed(self):
|
|
60
|
-
# Imperial doesn't need mole or candela
|
|
61
|
-
system = UnitSystem(
|
|
62
|
-
name="Imperial",
|
|
63
|
-
bases={
|
|
64
|
-
Dimension.length: units.foot,
|
|
65
|
-
Dimension.mass: units.pound,
|
|
66
|
-
Dimension.time: units.second,
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
self.assertEqual(len(system.bases), 3)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class TestUnitSystemQueries(unittest.TestCase):
|
|
73
|
-
"""Test UnitSystem query methods."""
|
|
74
|
-
|
|
75
|
-
def setUp(self):
|
|
76
|
-
self.si = UnitSystem(
|
|
77
|
-
name="SI",
|
|
78
|
-
bases={
|
|
79
|
-
Dimension.length: units.meter,
|
|
80
|
-
Dimension.mass: units.kilogram,
|
|
81
|
-
Dimension.time: units.second,
|
|
82
|
-
Dimension.temperature: units.kelvin,
|
|
83
|
-
}
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
def test_covers_returns_true_for_covered(self):
|
|
87
|
-
self.assertTrue(self.si.covers(Dimension.length))
|
|
88
|
-
self.assertTrue(self.si.covers(Dimension.mass))
|
|
89
|
-
self.assertTrue(self.si.covers(Dimension.time))
|
|
90
|
-
|
|
91
|
-
def test_covers_returns_false_for_uncovered(self):
|
|
92
|
-
self.assertFalse(self.si.covers(Dimension.current))
|
|
93
|
-
self.assertFalse(self.si.covers(Dimension.luminous_intensity))
|
|
94
|
-
|
|
95
|
-
def test_base_for_returns_correct_unit(self):
|
|
96
|
-
self.assertEqual(self.si.base_for(Dimension.length), units.meter)
|
|
97
|
-
self.assertEqual(self.si.base_for(Dimension.mass), units.kilogram)
|
|
98
|
-
self.assertEqual(self.si.base_for(Dimension.time), units.second)
|
|
99
|
-
|
|
100
|
-
def test_base_for_raises_for_uncovered(self):
|
|
101
|
-
with self.assertRaises(DimensionNotCovered) as ctx:
|
|
102
|
-
self.si.base_for(Dimension.current)
|
|
103
|
-
self.assertIn("current", str(ctx.exception).lower())
|
|
104
|
-
|
|
105
|
-
def test_dimensions_property(self):
|
|
106
|
-
dims = self.si.dimensions
|
|
107
|
-
self.assertIsInstance(dims, set)
|
|
108
|
-
self.assertEqual(len(dims), 4)
|
|
109
|
-
self.assertIn(Dimension.length, dims)
|
|
110
|
-
self.assertIn(Dimension.mass, dims)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class TestUnitSystemEquality(unittest.TestCase):
|
|
114
|
-
"""Test UnitSystem equality and hashing."""
|
|
115
|
-
|
|
116
|
-
def test_same_systems_equal(self):
|
|
117
|
-
s1 = UnitSystem(
|
|
118
|
-
name="SI",
|
|
119
|
-
bases={Dimension.length: units.meter}
|
|
120
|
-
)
|
|
121
|
-
s2 = UnitSystem(
|
|
122
|
-
name="SI",
|
|
123
|
-
bases={Dimension.length: units.meter}
|
|
124
|
-
)
|
|
125
|
-
self.assertEqual(s1, s2)
|
|
126
|
-
|
|
127
|
-
def test_different_names_not_equal(self):
|
|
128
|
-
s1 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
129
|
-
s2 = UnitSystem(name="CGS", bases={Dimension.length: units.meter})
|
|
130
|
-
self.assertNotEqual(s1, s2)
|
|
131
|
-
|
|
132
|
-
def test_different_bases_not_equal(self):
|
|
133
|
-
s1 = UnitSystem(name="test", bases={Dimension.length: units.meter})
|
|
134
|
-
s2 = UnitSystem(name="test", bases={Dimension.length: units.foot})
|
|
135
|
-
self.assertNotEqual(s1, s2)
|
|
136
|
-
|
|
137
|
-
def test_hashable(self):
|
|
138
|
-
s1 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
139
|
-
s2 = UnitSystem(name="SI", bases={Dimension.length: units.meter})
|
|
140
|
-
self.assertEqual(hash(s1), hash(s2))
|
|
141
|
-
self.assertEqual(len({s1, s2}), 1)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class TestUnitSystemImmutability(unittest.TestCase):
|
|
145
|
-
"""Test that UnitSystem is immutable."""
|
|
146
|
-
|
|
147
|
-
def test_frozen_dataclass(self):
|
|
148
|
-
system = UnitSystem(
|
|
149
|
-
name="SI",
|
|
150
|
-
bases={Dimension.length: units.meter}
|
|
151
|
-
)
|
|
152
|
-
with self.assertRaises(AttributeError):
|
|
153
|
-
system.name = "changed"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class TestPredefinedSystems(unittest.TestCase):
|
|
157
|
-
"""Test predefined unit systems in ucon.units."""
|
|
158
|
-
|
|
159
|
-
def test_si_system_exists(self):
|
|
160
|
-
from ucon.units import si
|
|
161
|
-
self.assertEqual(si.name, "SI")
|
|
162
|
-
self.assertTrue(si.covers(Dimension.length))
|
|
163
|
-
self.assertTrue(si.covers(Dimension.mass))
|
|
164
|
-
self.assertTrue(si.covers(Dimension.time))
|
|
165
|
-
|
|
166
|
-
def test_imperial_system_exists(self):
|
|
167
|
-
from ucon.units import imperial
|
|
168
|
-
self.assertEqual(imperial.name, "Imperial")
|
|
169
|
-
self.assertTrue(imperial.covers(Dimension.length))
|
|
170
|
-
self.assertTrue(imperial.covers(Dimension.mass))
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if __name__ == "__main__":
|
|
174
|
-
unittest.main()
|
tests/ucon/test_units.py
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
# © 2025 The Radiativity Company
|
|
3
|
-
# Licensed under the Apache License, Version 2.0
|
|
4
|
-
# See the LICENSE file for details.
|
|
5
|
-
|
|
6
|
-
from unittest import TestCase
|
|
7
|
-
|
|
8
|
-
from ucon import units
|
|
9
|
-
from ucon.core import Dimension
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestUnits(TestCase):
|
|
13
|
-
|
|
14
|
-
def test_has_expected_basic_units(self):
|
|
15
|
-
expected_basic_units = {'none', 'volt', 'liter', 'gram', 'second', 'kelvin', 'mole', 'coulomb'}
|
|
16
|
-
missing = {name for name in expected_basic_units if not units.have(name)}
|
|
17
|
-
assert not missing, f"Missing expected units: {missing}"
|
|
18
|
-
|
|
19
|
-
def test___truediv__(self):
|
|
20
|
-
self.assertEqual(units.none, units.gram / units.gram)
|
|
21
|
-
self.assertEqual(units.gram, units.gram / units.none)
|
|
22
|
-
|
|
23
|
-
composite_unit = units.gram / units.liter
|
|
24
|
-
self.assertEqual("g/L", composite_unit.shorthand)
|
|
25
|
-
self.assertEqual(Dimension.density, composite_unit.dimension)
|