ucon 0.3.3rc2__py3-none-any.whl → 0.3.5__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/__init__.py +3 -0
- tests/ucon/test_algebra.py +239 -0
- tests/ucon/test_core.py +607 -362
- tests/ucon/test_quantity.py +370 -0
- tests/ucon/test_units.py +7 -3
- ucon/__init__.py +9 -3
- ucon/algebra.py +216 -0
- ucon/core.py +703 -286
- ucon/quantity.py +196 -0
- ucon/units.py +5 -2
- {ucon-0.3.3rc2.dist-info → ucon-0.3.5.dist-info}/METADATA +49 -37
- ucon-0.3.5.dist-info/RECORD +16 -0
- {ucon-0.3.3rc2.dist-info → ucon-0.3.5.dist-info}/WHEEL +1 -1
- ucon-0.3.5.dist-info/licenses/LICENSE +202 -0
- ucon-0.3.5.dist-info/licenses/NOTICE +28 -0
- tests/ucon/test_dimension.py +0 -206
- tests/ucon/test_unit.py +0 -143
- ucon/dimension.py +0 -172
- ucon/unit.py +0 -92
- ucon-0.3.3rc2.dist-info/RECORD +0 -15
- ucon-0.3.3rc2.dist-info/licenses/LICENSE +0 -21
- {ucon-0.3.3rc2.dist-info → ucon-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
ucon — A dimensional analysis and unit algebra library
|
|
2
|
+
© 2025 The Radiativity Company
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0.
|
|
5
|
+
You may not use this project except in compliance with the License.
|
|
6
|
+
A copy of the License is included in the LICENSE file or at:
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
This NOTICE file is part of the ucon distribution.
|
|
11
|
+
|
|
12
|
+
ucon implements:
|
|
13
|
+
• A compositional unit algebra (UnitFactor, UnitProduct, UnitForm)
|
|
14
|
+
• A dimension algebra based on vector-space operations
|
|
15
|
+
• Expression-level scale separation for unit provenance
|
|
16
|
+
• A foundation for ConversionGraph-based unit transformations
|
|
17
|
+
|
|
18
|
+
Portions of this software may incorporate or depend upon
|
|
19
|
+
third-party libraries. Attribution notices for those components,
|
|
20
|
+
if required, are included here or in the accompanying documentation.
|
|
21
|
+
|
|
22
|
+
The Radiativity Company retains all trademark rights to the names:
|
|
23
|
+
• "ucon"
|
|
24
|
+
• "The Radiativity Company"
|
|
25
|
+
• "Project Calico"
|
|
26
|
+
|
|
27
|
+
This file is for attribution purposes only and does not modify
|
|
28
|
+
the terms of the Apache License, Version 2.0.
|
tests/ucon/test_dimension.py
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from ucon.dimension import Vector, Dimension
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class TestVector(unittest.TestCase):
|
|
6
|
-
|
|
7
|
-
def test_vector_iteration_and_length(self):
|
|
8
|
-
v = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
9
|
-
self.assertEqual(tuple(v), (1, 0, 0, 0, 0, 0, 0))
|
|
10
|
-
self.assertEqual(len(v), 7) # always 7 components
|
|
11
|
-
|
|
12
|
-
def test_vector_addition(self):
|
|
13
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
14
|
-
v2 = Vector(0, 2, 0, 0, 0, 0, 0)
|
|
15
|
-
result = v1 + v2
|
|
16
|
-
self.assertEqual(result, Vector(1, 2, 0, 0, 0, 0, 0))
|
|
17
|
-
|
|
18
|
-
def test_vector_subtraction(self):
|
|
19
|
-
v1 = Vector(2, 1, 0, 0, 0, 0, 0)
|
|
20
|
-
v2 = Vector(1, 1, 0, 0, 0, 0, 0)
|
|
21
|
-
self.assertEqual(v1 - v2, Vector(1, 0, 0, 0, 0, 0, 0))
|
|
22
|
-
|
|
23
|
-
def test_vector_equality_and_hash(self):
|
|
24
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
25
|
-
v2 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
26
|
-
v3 = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
27
|
-
self.assertTrue(v1 == v2)
|
|
28
|
-
self.assertFalse(v1 == v3)
|
|
29
|
-
self.assertEqual(hash(v1), hash(v2))
|
|
30
|
-
self.assertNotEqual(hash(v1), hash(v3))
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class TestDimension(unittest.TestCase):
|
|
34
|
-
|
|
35
|
-
def test_basic_dimensions_are_unique(self):
|
|
36
|
-
seen = set()
|
|
37
|
-
for dim in Dimension:
|
|
38
|
-
self.assertNotIn(dim.value, seen, f'Duplicate vector found for {dim.name}')
|
|
39
|
-
seen.add(dim.value)
|
|
40
|
-
|
|
41
|
-
def test_multiplication_adds_exponents(self):
|
|
42
|
-
self.assertEqual(
|
|
43
|
-
Dimension.mass * Dimension.acceleration,
|
|
44
|
-
Dimension.force,
|
|
45
|
-
)
|
|
46
|
-
self.assertEqual(
|
|
47
|
-
Dimension.length * Dimension.length,
|
|
48
|
-
Dimension.area,
|
|
49
|
-
)
|
|
50
|
-
self.assertEqual(
|
|
51
|
-
Dimension.length * Dimension.length * Dimension.length,
|
|
52
|
-
Dimension.volume,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
def test_division_subtracts_exponents(self):
|
|
56
|
-
self.assertEqual(
|
|
57
|
-
Dimension.length / Dimension.time,
|
|
58
|
-
Dimension.velocity,
|
|
59
|
-
)
|
|
60
|
-
self.assertEqual(
|
|
61
|
-
Dimension.force / Dimension.area,
|
|
62
|
-
Dimension.pressure,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
def test_none_dimension_behaves_neutrally(self):
|
|
66
|
-
base = Dimension.mass
|
|
67
|
-
self.assertEqual(base * Dimension.none, base)
|
|
68
|
-
self.assertEqual(base / Dimension.none, base)
|
|
69
|
-
self.assertEqual(Dimension.none * base, base)
|
|
70
|
-
with self.assertRaises(ValueError) as exc:
|
|
71
|
-
Dimension.none / base
|
|
72
|
-
assert type(exc.exception) == ValueError
|
|
73
|
-
assert str(exc.exception).endswith('is not a valid Dimension')
|
|
74
|
-
|
|
75
|
-
def test_hash_and_equality_consistency(self):
|
|
76
|
-
d1 = Dimension.mass
|
|
77
|
-
d2 = Dimension.mass
|
|
78
|
-
d3 = Dimension.length
|
|
79
|
-
self.assertEqual(d1, d2)
|
|
80
|
-
self.assertNotEqual(d1, d3)
|
|
81
|
-
self.assertEqual(hash(d1), hash(d2))
|
|
82
|
-
self.assertNotEqual(hash(d1), hash(d3))
|
|
83
|
-
|
|
84
|
-
def test_composite_quantities_examples(self):
|
|
85
|
-
# Energy = Force * Length
|
|
86
|
-
self.assertEqual(
|
|
87
|
-
Dimension.force * Dimension.length,
|
|
88
|
-
Dimension.energy,
|
|
89
|
-
)
|
|
90
|
-
# Power = Energy / Time
|
|
91
|
-
self.assertEqual(
|
|
92
|
-
Dimension.energy / Dimension.time,
|
|
93
|
-
Dimension.power,
|
|
94
|
-
)
|
|
95
|
-
# Pressure = Force / Area
|
|
96
|
-
self.assertEqual(
|
|
97
|
-
Dimension.force / Dimension.area,
|
|
98
|
-
Dimension.pressure,
|
|
99
|
-
)
|
|
100
|
-
# Charge = Current * Time
|
|
101
|
-
self.assertEqual(
|
|
102
|
-
Dimension.current * Dimension.time,
|
|
103
|
-
Dimension.charge,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
def test_vector_equality_reflects_dimension_equality(self):
|
|
107
|
-
self.assertEqual(Dimension.mass.value, Dimension.mass.value)
|
|
108
|
-
self.assertNotEqual(Dimension.mass.value, Dimension.time.value)
|
|
109
|
-
self.assertEqual(Dimension.mass, Dimension.mass)
|
|
110
|
-
self.assertNotEqual(Dimension.mass, Dimension.time)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class TestVectorEdgeCases(unittest.TestCase):
|
|
114
|
-
|
|
115
|
-
def test_zero_vector_equality_and_additivity(self):
|
|
116
|
-
zero = Vector()
|
|
117
|
-
self.assertEqual(zero, Vector(0, 0, 0, 0, 0, 0, 0))
|
|
118
|
-
# Adding or subtracting zero should yield same vector
|
|
119
|
-
v = Vector(1, 2, 3, 4, 5, 6, 7)
|
|
120
|
-
self.assertEqual(v + zero, v)
|
|
121
|
-
self.assertEqual(v - zero, v)
|
|
122
|
-
|
|
123
|
-
def test_vector_with_negative_exponents(self):
|
|
124
|
-
v1 = Vector(1, -2, 3, 0, 0, 0, 0)
|
|
125
|
-
v2 = Vector(-1, 2, -3, 0, 0, 0, 0)
|
|
126
|
-
result = v1 + v2
|
|
127
|
-
self.assertEqual(result, Vector(0, 0, 0, 0, 0, 0, 0))
|
|
128
|
-
self.assertEqual(v1 - v1, Vector()) # perfect cancellation
|
|
129
|
-
|
|
130
|
-
def test_vector_equality_with_non_vector(self):
|
|
131
|
-
v = Vector()
|
|
132
|
-
with self.assertRaises(AssertionError):
|
|
133
|
-
v == "not a vector"
|
|
134
|
-
with self.assertRaises(AssertionError):
|
|
135
|
-
v == None
|
|
136
|
-
|
|
137
|
-
def test_hash_consistency_for_equal_vectors(self):
|
|
138
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
139
|
-
v2 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
140
|
-
self.assertEqual(hash(v1), hash(v2))
|
|
141
|
-
self.assertEqual(len({v1, v2}), 1)
|
|
142
|
-
|
|
143
|
-
def test_iter_length_order_consistency(self):
|
|
144
|
-
v = Vector(1, 2, 3, 4, 5, 6, 7)
|
|
145
|
-
components = list(v)
|
|
146
|
-
self.assertEqual(len(components), len(v))
|
|
147
|
-
# Ensure order of iteration is fixed (T→L→M→I→Θ→J→N)
|
|
148
|
-
self.assertEqual(components, [1, 2, 3, 4, 5, 6, 7])
|
|
149
|
-
|
|
150
|
-
def test_vector_arithmetic_does_not_mutate_operands(self):
|
|
151
|
-
v1 = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
152
|
-
v2 = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
153
|
-
_ = v1 + v2
|
|
154
|
-
self.assertEqual(v1, Vector(1, 0, 0, 0, 0, 0, 0))
|
|
155
|
-
self.assertEqual(v2, Vector(0, 1, 0, 0, 0, 0, 0))
|
|
156
|
-
|
|
157
|
-
def test_invalid_addition_type_raises(self):
|
|
158
|
-
v = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
159
|
-
with self.assertRaises(TypeError):
|
|
160
|
-
_ = v + "length"
|
|
161
|
-
with self.assertRaises(TypeError):
|
|
162
|
-
_ = v - 5
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
class TestDimensionEdgeCases(unittest.TestCase):
|
|
166
|
-
|
|
167
|
-
def test_invalid_multiplication_type(self):
|
|
168
|
-
with self.assertRaises(TypeError):
|
|
169
|
-
Dimension.length * 5
|
|
170
|
-
with self.assertRaises(TypeError):
|
|
171
|
-
"mass" * Dimension.time
|
|
172
|
-
|
|
173
|
-
def test_invalid_division_type(self):
|
|
174
|
-
with self.assertRaises(TypeError):
|
|
175
|
-
Dimension.time / "length"
|
|
176
|
-
with self.assertRaises(TypeError):
|
|
177
|
-
5 / Dimension.mass
|
|
178
|
-
|
|
179
|
-
def test_equality_with_non_dimension(self):
|
|
180
|
-
with self.assertRaises(TypeError):
|
|
181
|
-
Dimension.mass == "mass"
|
|
182
|
-
|
|
183
|
-
def test_enum_uniqueness_and_hash(self):
|
|
184
|
-
# Hashes should be unique per distinct dimension
|
|
185
|
-
hashes = {hash(d) for d in Dimension}
|
|
186
|
-
self.assertEqual(len(hashes), len(Dimension))
|
|
187
|
-
# All Dimension.value entries must be distinct Vectors
|
|
188
|
-
values = [d.value for d in Dimension]
|
|
189
|
-
self.assertEqual(len(values), len(set(values)))
|
|
190
|
-
|
|
191
|
-
def test_combined_chained_operations(self):
|
|
192
|
-
# (mass * acceleration) / area = pressure
|
|
193
|
-
result = (Dimension.mass * Dimension.acceleration) / Dimension.area
|
|
194
|
-
self.assertEqual(result, Dimension.pressure)
|
|
195
|
-
|
|
196
|
-
def test_dimension_round_trip_equality(self):
|
|
197
|
-
# Multiplying and dividing by the same dimension returns self
|
|
198
|
-
d = Dimension.energy
|
|
199
|
-
self.assertEqual((d * Dimension.none) / Dimension.none, d)
|
|
200
|
-
self.assertEqual(d / Dimension.none, d)
|
|
201
|
-
self.assertEqual(Dimension.none * d, d)
|
|
202
|
-
|
|
203
|
-
def test_enum_is_hashable_and_iterable(self):
|
|
204
|
-
seen = {d for d in Dimension}
|
|
205
|
-
self.assertIn(Dimension.mass, seen)
|
|
206
|
-
self.assertEqual(len(seen), len(Dimension))
|
tests/ucon/test_unit.py
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from unittest import TestCase
|
|
4
|
-
|
|
5
|
-
from ucon.dimension import Dimension
|
|
6
|
-
from ucon.unit import Unit
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestUnit(TestCase):
|
|
10
|
-
|
|
11
|
-
unit_name = 'second'
|
|
12
|
-
unit_type = 'time'
|
|
13
|
-
unit_aliases = ('seconds', 'secs', 's', 'S')
|
|
14
|
-
unit = Unit(*unit_aliases, name=unit_name, dimension=Dimension.time)
|
|
15
|
-
|
|
16
|
-
def test___repr__(self):
|
|
17
|
-
self.assertEqual(f'<{self.unit_type} | {self.unit_name}>', str(self.unit))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TestUnitEdgeCases(TestCase):
|
|
21
|
-
|
|
22
|
-
# --- Initialization & representation -----------------------------------
|
|
23
|
-
|
|
24
|
-
def test_default_unit_is_dimensionless(self):
|
|
25
|
-
u = Unit()
|
|
26
|
-
self.assertEqual(u.dimension, Dimension.none)
|
|
27
|
-
self.assertEqual(u.name, '')
|
|
28
|
-
self.assertEqual(u.aliases, ())
|
|
29
|
-
self.assertEqual(u.shorthand, '')
|
|
30
|
-
self.assertEqual(repr(u), '<none>')
|
|
31
|
-
|
|
32
|
-
def test_unit_with_aliases_and_name(self):
|
|
33
|
-
u = Unit('m', 'M', name='meter', dimension=Dimension.length)
|
|
34
|
-
self.assertEqual(u.shorthand, 'm')
|
|
35
|
-
self.assertIn('m', u.aliases)
|
|
36
|
-
self.assertIn('M', u.aliases)
|
|
37
|
-
self.assertIn('length', repr(u))
|
|
38
|
-
self.assertIn('meter', repr(u))
|
|
39
|
-
|
|
40
|
-
def test_hash_and_equality_consistency(self):
|
|
41
|
-
u1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
42
|
-
u2 = Unit('m', name='meter', dimension=Dimension.length)
|
|
43
|
-
u3 = Unit('s', name='second', dimension=Dimension.time)
|
|
44
|
-
self.assertEqual(u1, u2)
|
|
45
|
-
self.assertEqual(hash(u1), hash(u2))
|
|
46
|
-
self.assertNotEqual(u1, u3)
|
|
47
|
-
self.assertNotEqual(hash(u1), hash(u3))
|
|
48
|
-
|
|
49
|
-
def test_units_with_same_name_but_different_dimension_not_equal(self):
|
|
50
|
-
u1 = Unit(name='amp', dimension=Dimension.current)
|
|
51
|
-
u2 = Unit(name='amp', dimension=Dimension.time)
|
|
52
|
-
self.assertNotEqual(u1, u2)
|
|
53
|
-
|
|
54
|
-
# --- generate_name edge cases -----------------------------------------
|
|
55
|
-
|
|
56
|
-
def test_generate_name_both_have_shorthand(self):
|
|
57
|
-
u1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
58
|
-
u2 = Unit('s', name='second', dimension=Dimension.time)
|
|
59
|
-
result = u1.generate_name(u2, '*')
|
|
60
|
-
self.assertEqual(result, '(m*s)')
|
|
61
|
-
|
|
62
|
-
def test_generate_name_missing_left_shorthand(self):
|
|
63
|
-
u1 = Unit(name='unitless', dimension=Dimension.none)
|
|
64
|
-
u2 = Unit('s', name='second', dimension=Dimension.time)
|
|
65
|
-
self.assertEqual(u1.generate_name(u2, '/'), 'second')
|
|
66
|
-
|
|
67
|
-
def test_generate_name_missing_right_shorthand(self):
|
|
68
|
-
u1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
69
|
-
u2 = Unit(name='none', dimension=Dimension.none)
|
|
70
|
-
self.assertEqual(u1.generate_name(u2, '*'), 'meter')
|
|
71
|
-
|
|
72
|
-
def test_generate_name_no_aliases_on_either_side(self):
|
|
73
|
-
u1 = Unit(name='foo', dimension=Dimension.length)
|
|
74
|
-
u2 = Unit(name='bar', dimension=Dimension.time)
|
|
75
|
-
self.assertEqual(u1.generate_name(u2, '*'), '(foo*bar)')
|
|
76
|
-
|
|
77
|
-
# --- arithmetic behavior ----------------------------------------------
|
|
78
|
-
|
|
79
|
-
def test_multiplication_produces_composite_unit(self):
|
|
80
|
-
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
81
|
-
s = Unit('s', name='second', dimension=Dimension.time)
|
|
82
|
-
v = m / s
|
|
83
|
-
self.assertIsInstance(v, Unit)
|
|
84
|
-
self.assertEqual(v.dimension, Dimension.velocity)
|
|
85
|
-
self.assertIn('/', v.name)
|
|
86
|
-
|
|
87
|
-
def test_division_with_dimensionless_denominator_returns_self(self):
|
|
88
|
-
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
89
|
-
none = Unit(name='none', dimension=Dimension.none)
|
|
90
|
-
result = m / none
|
|
91
|
-
self.assertEqual(result, m)
|
|
92
|
-
|
|
93
|
-
def test_division_of_identical_units_returns_dimensionless(self):
|
|
94
|
-
m1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
95
|
-
m2 = Unit('m', name='meter', dimension=Dimension.length)
|
|
96
|
-
result = m1 / m2
|
|
97
|
-
self.assertEqual(result.dimension, Dimension.none)
|
|
98
|
-
self.assertEqual(result.name, '')
|
|
99
|
-
|
|
100
|
-
def test_multiplying_with_dimensionless_returns_self(self):
|
|
101
|
-
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
102
|
-
none = Unit(name='none', dimension=Dimension.none)
|
|
103
|
-
result = m * none
|
|
104
|
-
self.assertEqual(result.dimension, Dimension.length)
|
|
105
|
-
self.assertIn('m', result.name)
|
|
106
|
-
|
|
107
|
-
def test_invalid_dimension_combinations_raise_value_error(self):
|
|
108
|
-
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
109
|
-
c = Unit('C', name='coulomb', dimension=Dimension.charge)
|
|
110
|
-
# The result of dividing these is undefined (no such Dimension)
|
|
111
|
-
with self.assertRaises(ValueError):
|
|
112
|
-
_ = m / c
|
|
113
|
-
with self.assertRaises(ValueError):
|
|
114
|
-
_ = c * m
|
|
115
|
-
|
|
116
|
-
# --- equality, hashing, immutability ----------------------------------
|
|
117
|
-
|
|
118
|
-
def test_equality_with_non_unit(self):
|
|
119
|
-
with self.assertRaises(TypeError):
|
|
120
|
-
Unit('m', name='meter', dimension=Dimension.length) == 'meter'
|
|
121
|
-
|
|
122
|
-
def test_hash_stability_in_collections(self):
|
|
123
|
-
m1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
124
|
-
s = set([m1])
|
|
125
|
-
self.assertIn(Unit('m', name='meter', dimension=Dimension.length), s)
|
|
126
|
-
|
|
127
|
-
def test_operations_do_not_mutate_operands(self):
|
|
128
|
-
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
129
|
-
s = Unit('s', name='second', dimension=Dimension.time)
|
|
130
|
-
_ = m / s
|
|
131
|
-
self.assertEqual(m.dimension, Dimension.length)
|
|
132
|
-
self.assertEqual(s.dimension, Dimension.time)
|
|
133
|
-
|
|
134
|
-
# --- operator edge cases ----------------------------------------------
|
|
135
|
-
|
|
136
|
-
def test_generate_name_handles_empty_names_and_aliases(self):
|
|
137
|
-
a = Unit()
|
|
138
|
-
b = Unit()
|
|
139
|
-
self.assertEqual(a.generate_name(b, '*'), '')
|
|
140
|
-
|
|
141
|
-
def test_repr_contains_dimension_name_even_without_name(self):
|
|
142
|
-
u = Unit(dimension=Dimension.force)
|
|
143
|
-
self.assertIn('force', repr(u))
|
ucon/dimension.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ucon.dimension
|
|
3
|
-
===============
|
|
4
|
-
|
|
5
|
-
Defines the algebra of **physical dimensions**--the foundation of all unit
|
|
6
|
-
relationships and dimensional analysis in *ucon*.
|
|
7
|
-
|
|
8
|
-
Each :class:`Dimension` represents a physical quantity (time, mass, length, etc.)
|
|
9
|
-
expressed as a 7-element exponent vector following the SI base system:
|
|
10
|
-
|
|
11
|
-
(T, L, M, I, Θ, J, N) :: (s * m * kg * A * K * cd * mol)
|
|
12
|
-
time, length, mass, current, temperature, luminous intensity, substance
|
|
13
|
-
|
|
14
|
-
Derived dimensions are expressed as algebraic sums or differences of these base
|
|
15
|
-
vectors (e.g., `velocity = length / time`, `force = mass * acceleration`).
|
|
16
|
-
|
|
17
|
-
Classes
|
|
18
|
-
-------
|
|
19
|
-
- :class:`Vector` — Represents the exponent vector of a physical quantity.
|
|
20
|
-
- :class:`Dimension` — Enum of known physical quantities, each with a `Vector`
|
|
21
|
-
value and operator overloads for dimensional algebra.
|
|
22
|
-
"""
|
|
23
|
-
from dataclasses import dataclass
|
|
24
|
-
from enum import Enum
|
|
25
|
-
from functools import partial, reduce
|
|
26
|
-
from operator import __sub__ as subtraction
|
|
27
|
-
from typing import Callable, Iterable, Iterator
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
diff: Callable[[Iterable], int] = partial(reduce, subtraction)
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class Vector:
|
|
34
|
-
"""
|
|
35
|
-
Represents the **exponent vector** of a physical quantity.
|
|
36
|
-
|
|
37
|
-
Each component corresponds to the power of a base dimension in the SI system:
|
|
38
|
-
time (T), length (L), mass (M), current (I), temperature (Θ),
|
|
39
|
-
luminous intensity (J), and amount of substance (N).
|
|
40
|
-
|
|
41
|
-
Arithmetic operations correspond to dimensional composition:
|
|
42
|
-
- Addition (`+`) → multiplication of quantities
|
|
43
|
-
- Subtraction (`-`) → division of quantities
|
|
44
|
-
|
|
45
|
-
e.g.
|
|
46
|
-
Vector(T=1, L=0, M=0, I=0, Θ=0, J=0, N=0) => "time"
|
|
47
|
-
Vector(T=0, L=2, M=0, I=0, Θ=0, J=0, N=0) => "area"
|
|
48
|
-
Vector(T=-2, L=1, M=1, I=0, Θ=0, J=0, N=0) => "force"
|
|
49
|
-
"""
|
|
50
|
-
T: int = 0 # time
|
|
51
|
-
L: int = 0 # length
|
|
52
|
-
M: int = 0 # mass
|
|
53
|
-
I: int = 0 # current
|
|
54
|
-
Θ: int = 0 # temperature
|
|
55
|
-
J: int = 0 # luminous intensity
|
|
56
|
-
N: int = 0 # amount of substance
|
|
57
|
-
|
|
58
|
-
def __iter__(self) -> Iterator[int]:
|
|
59
|
-
yield self.T
|
|
60
|
-
yield self.L
|
|
61
|
-
yield self.M
|
|
62
|
-
yield self.I
|
|
63
|
-
yield self.Θ
|
|
64
|
-
yield self.J
|
|
65
|
-
yield self.N
|
|
66
|
-
|
|
67
|
-
def __len__(self) -> int:
|
|
68
|
-
return sum(tuple(1 for x in self))
|
|
69
|
-
|
|
70
|
-
def __add__(self, vector: 'Vector') -> 'Vector':
|
|
71
|
-
"""
|
|
72
|
-
Addition, here, comes from the multiplication of base quantities
|
|
73
|
-
|
|
74
|
-
e.g. F = m * a
|
|
75
|
-
F =
|
|
76
|
-
(s^-2 * m^1 * kg * A * K * cd * mol) +
|
|
77
|
-
(s * m * kg^1 * A * K * cd * mol)
|
|
78
|
-
"""
|
|
79
|
-
values = tuple(sum(pair) for pair in zip(tuple(self), tuple(vector)))
|
|
80
|
-
return Vector(*values)
|
|
81
|
-
|
|
82
|
-
def __sub__(self, vector: 'Vector') -> 'Vector':
|
|
83
|
-
"""
|
|
84
|
-
Subtraction, here, comes from the division of base quantities
|
|
85
|
-
"""
|
|
86
|
-
values = tuple(diff(pair) for pair in zip(tuple(self), tuple(vector)))
|
|
87
|
-
return Vector(*values)
|
|
88
|
-
|
|
89
|
-
def __eq__(self, vector: 'Vector') -> bool:
|
|
90
|
-
assert isinstance(vector, Vector), "Can only compare Vector to another Vector"
|
|
91
|
-
return tuple(self) == tuple(vector)
|
|
92
|
-
|
|
93
|
-
def __hash__(self) -> int:
|
|
94
|
-
# Hash based on the string because tuples have been shown to collide
|
|
95
|
-
# Not the most performant, but effective
|
|
96
|
-
return hash(str(tuple(self)))
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
class Dimension(Enum):
|
|
100
|
-
"""
|
|
101
|
-
Represents a **physical dimension** defined by a :class:`Vector`.
|
|
102
|
-
|
|
103
|
-
Each dimension corresponds to a distinct combination of base exponents.
|
|
104
|
-
Dimensions are algebraically composable via multiplication and division:
|
|
105
|
-
|
|
106
|
-
>>> Dimension.length / Dimension.time
|
|
107
|
-
<Dimension.velocity: Vector(T=-1, L=1, M=0, I=0, Θ=0, J=0, N=0)>
|
|
108
|
-
|
|
109
|
-
This algebra forms the foundation for unit compatibility and conversion.
|
|
110
|
-
"""
|
|
111
|
-
none = Vector()
|
|
112
|
-
|
|
113
|
-
# -- BASIS ---------------------------------------
|
|
114
|
-
time = Vector(1, 0, 0, 0, 0, 0, 0)
|
|
115
|
-
length = Vector(0, 1, 0, 0, 0, 0, 0)
|
|
116
|
-
mass = Vector(0, 0, 1, 0, 0, 0, 0)
|
|
117
|
-
current = Vector(0, 0, 0, 1, 0, 0, 0)
|
|
118
|
-
temperature = Vector(0, 0, 0, 0, 1, 0, 0)
|
|
119
|
-
luminous_intensity = Vector(0, 0, 0, 0, 0, 1, 0)
|
|
120
|
-
amount_of_substance = Vector(0, 0, 0, 0, 0, 0, 1)
|
|
121
|
-
# ------------------------------------------------
|
|
122
|
-
|
|
123
|
-
acceleration = Vector(-2, 1, 0, 0, 0, 0, 0)
|
|
124
|
-
angular_momentum = Vector(-1, 2, 1, 0, 0, 0, 0)
|
|
125
|
-
area = Vector(0, 2, 0, 0, 0, 0, 0)
|
|
126
|
-
capacitance = Vector(4, -2, -1, 2, 0, 0, 0)
|
|
127
|
-
charge = Vector(1, 0, 0, 1, 0, 0, 0)
|
|
128
|
-
conductance = Vector(3, -2, -1, 2, 0, 0, 0)
|
|
129
|
-
conductivity = Vector(3, -3, -1, 2, 0, 0, 0)
|
|
130
|
-
density = Vector(0, -3, 1, 0, 0, 0, 0)
|
|
131
|
-
electric_field_strength = Vector(-3, 1, 1, -1, 0, 0, 0)
|
|
132
|
-
energy = Vector(-2, 2, 1, 0, 0, 0, 0)
|
|
133
|
-
entropy = Vector(-2, 2, 1, 0, -1, 0, 0)
|
|
134
|
-
force = Vector(-2, 1, 1, 0, 0, 0, 0)
|
|
135
|
-
frequency = Vector(-1, 0, 0, 0, 0, 0, 0)
|
|
136
|
-
gravitation = Vector(-2, 3, -1, 0, 0, 0, 0)
|
|
137
|
-
illuminance = Vector(0, -2, 0, 0, 0, 1, 0)
|
|
138
|
-
inductance = Vector(-2, 2, 1, -2, 0, 0, 0)
|
|
139
|
-
magnetic_flux = Vector(-2, 2, 1, -1, 0, 0, 0)
|
|
140
|
-
magnetic_flux_density = Vector(-2, 0, 1, -1, 0, 0, 0)
|
|
141
|
-
magnetic_permeability = Vector(-2, 1, 1, -2, 0, 0, 0)
|
|
142
|
-
molar_mass = Vector(0, 0, 1, 0, 0, 0, -1)
|
|
143
|
-
molar_volume = Vector(0, 3, 0, 0, 0, 0, -1)
|
|
144
|
-
momentum = Vector(-1, 1, 1, 0, 0, 0, 0)
|
|
145
|
-
permittivity = Vector(4, -3, -1, 2, 0, 0, 0)
|
|
146
|
-
power = Vector(-3, 2, 1, 0, 0, 0, 0)
|
|
147
|
-
pressure = Vector(-2, -1, 1, 0, 0, 0, 0)
|
|
148
|
-
resistance = Vector(-3, 2, 1, -2, 0, 0, 0)
|
|
149
|
-
resistivity = Vector(-3, 3, 1, -2, 0, 0, 0)
|
|
150
|
-
specific_heat_capacity = Vector(-2, 2, 0, 0, -1, 0, 0)
|
|
151
|
-
thermal_conductivity = Vector(-3, 1, 1, 0, -1, 0, 0)
|
|
152
|
-
velocity = Vector(-1, 1, 0, 0, 0, 0, 0)
|
|
153
|
-
voltage = Vector(-3, 2, 1, -1, 0, 0, 0)
|
|
154
|
-
volume = Vector(0, 3, 0, 0, 0, 0, 0)
|
|
155
|
-
|
|
156
|
-
def __truediv__(self, dimension: 'Dimension') -> 'Dimension':
|
|
157
|
-
if not isinstance(dimension, Dimension):
|
|
158
|
-
raise TypeError(f"Cannot divide Dimension by non-Dimension type: {type(dimension)}")
|
|
159
|
-
return Dimension(self.value - dimension.value)
|
|
160
|
-
|
|
161
|
-
def __mul__(self, dimension: 'Dimension') -> 'Dimension':
|
|
162
|
-
if not isinstance(dimension, Dimension):
|
|
163
|
-
raise TypeError(f"Cannot multiply Dimension by non-Dimension type: {type(dimension)}")
|
|
164
|
-
return Dimension(self.value + dimension.value)
|
|
165
|
-
|
|
166
|
-
def __eq__(self, dimension) -> bool:
|
|
167
|
-
if not isinstance(dimension, Dimension):
|
|
168
|
-
raise TypeError(f"Cannot compare Dimension with non-Dimension type: {type(dimension)}")
|
|
169
|
-
return self.value == dimension.value
|
|
170
|
-
|
|
171
|
-
def __hash__(self) -> int:
|
|
172
|
-
return hash(self.value)
|
ucon/unit.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
ucon.unit
|
|
3
|
-
==========
|
|
4
|
-
|
|
5
|
-
Defines the **Unit** abstraction — the symbolic and algebraic representation of
|
|
6
|
-
a measurable quantity associated with a :class:`ucon.dimension.Dimension`.
|
|
7
|
-
|
|
8
|
-
A :class:`Unit` pairs a human-readable name and aliases with its underlying
|
|
9
|
-
dimension.
|
|
10
|
-
|
|
11
|
-
Units are composable:
|
|
12
|
-
|
|
13
|
-
>>> from ucon import units
|
|
14
|
-
>>> units.meter / units.second
|
|
15
|
-
<velocity | (m/s)>
|
|
16
|
-
|
|
17
|
-
They can be multiplied or divided to form compound units, and their dimensional
|
|
18
|
-
relationships are preserved algebraically.
|
|
19
|
-
"""
|
|
20
|
-
from ucon.dimension import Dimension
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class Unit:
|
|
24
|
-
"""
|
|
25
|
-
Represents a **unit of measure** associated with a :class:`Dimension`.
|
|
26
|
-
|
|
27
|
-
Parameters
|
|
28
|
-
----------
|
|
29
|
-
*aliases : str
|
|
30
|
-
Optional shorthand symbols (e.g., "m", "sec").
|
|
31
|
-
name : str
|
|
32
|
-
Canonical name of the unit (e.g., "meter").
|
|
33
|
-
dimension : Dimension
|
|
34
|
-
The physical dimension this unit represents.
|
|
35
|
-
|
|
36
|
-
Notes
|
|
37
|
-
-----
|
|
38
|
-
Units participate in algebraic operations that produce new compound units:
|
|
39
|
-
|
|
40
|
-
>>> density = units.gram / units.liter
|
|
41
|
-
>>> density.dimension
|
|
42
|
-
<Dimension.density: Vector(T=0, L=-3, M=1, I=0, Θ=0, J=0, N=0)>
|
|
43
|
-
|
|
44
|
-
The combination rules follow the same algebra as :class:`Dimension`.
|
|
45
|
-
"""
|
|
46
|
-
def __init__(self, *aliases: str, name: str = '', dimension: Dimension = Dimension.none):
|
|
47
|
-
self.dimension = dimension
|
|
48
|
-
self.name = name
|
|
49
|
-
self.aliases = aliases
|
|
50
|
-
self.shorthand = aliases[0] if aliases else self.name
|
|
51
|
-
|
|
52
|
-
def __repr__(self):
|
|
53
|
-
addendum = f' | {self.name}' if self.name else ''
|
|
54
|
-
return f'<{self.dimension.name}{addendum}>'
|
|
55
|
-
|
|
56
|
-
# TODO -- limit `operator` param choices
|
|
57
|
-
def generate_name(self, unit: 'Unit', operator: str):
|
|
58
|
-
if (self.dimension is Dimension.none) and not (unit.dimension is Dimension.none):
|
|
59
|
-
return unit.name
|
|
60
|
-
if not (self.dimension is Dimension.none) and (unit.dimension is Dimension.none):
|
|
61
|
-
return self.name
|
|
62
|
-
|
|
63
|
-
if not self.shorthand and not unit.shorthand:
|
|
64
|
-
name = ''
|
|
65
|
-
elif self.shorthand and not unit.shorthand:
|
|
66
|
-
name = f'({self.shorthand}{operator}?)'
|
|
67
|
-
elif not self.shorthand and unit.shorthand:
|
|
68
|
-
name = f'(?{operator}{unit.shorthand})'
|
|
69
|
-
else:
|
|
70
|
-
name = f'({self.shorthand}{operator}{unit.shorthand})'
|
|
71
|
-
return name
|
|
72
|
-
|
|
73
|
-
def __truediv__(self, unit: 'Unit') -> 'Unit':
|
|
74
|
-
# TODO -- define __eq__ for simplification, here
|
|
75
|
-
if (self.name == unit.name) and (self.dimension == unit.dimension):
|
|
76
|
-
return Unit()
|
|
77
|
-
|
|
78
|
-
if (unit.dimension is Dimension.none):
|
|
79
|
-
return self
|
|
80
|
-
|
|
81
|
-
return Unit(name=self.generate_name(unit, '/'), dimension=self.dimension / unit.dimension)
|
|
82
|
-
|
|
83
|
-
def __mul__(self, unit: 'Unit') -> 'Unit':
|
|
84
|
-
return Unit(name=self.generate_name(unit, '*'), dimension=self.dimension * unit.dimension)
|
|
85
|
-
|
|
86
|
-
def __eq__(self, unit: 'Unit') -> bool:
|
|
87
|
-
if not isinstance(unit, Unit):
|
|
88
|
-
raise TypeError(f'Cannot compare Unit to non-Unit type: {type(unit)}')
|
|
89
|
-
return (self.name == unit.name) and (self.dimension == unit.dimension)
|
|
90
|
-
|
|
91
|
-
def __hash__(self) -> int:
|
|
92
|
-
return hash(tuple([self.name, self.dimension,]))
|
ucon-0.3.3rc2.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
tests/ucon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
tests/ucon/test_core.py,sha256=a48nfTqg-WJ_jzjVT6KtsBb6E8oPUZaqCmscKDzlG_g,22871
|
|
3
|
-
tests/ucon/test_dimension.py,sha256=JyA9ySFvohs2l6oK77ehCQ7QvvFVqB_9t0iC7CUErjw,7296
|
|
4
|
-
tests/ucon/test_unit.py,sha256=vEPOeSxFBqcRBAUczCN9KPo_dTmLk4LQExPSt6UGVa4,5712
|
|
5
|
-
tests/ucon/test_units.py,sha256=248JZbo8RVvG_q3T0IhKG43vxM4F_2Xgf4_RjGZNsFM,704
|
|
6
|
-
ucon/__init__.py,sha256=ZWWLodIiG17OgCfoAm532wpwmJzdRXlUGX3w6OBxFeQ,1743
|
|
7
|
-
ucon/core.py,sha256=QI0aayUm0rgggdD7_zvdrmV26dbEARCJ6Yj5gn5PitI,13729
|
|
8
|
-
ucon/dimension.py,sha256=uUP05bPE8r15oFeD36DrclNIfBsugV7uFhvtJRYy4qI,6598
|
|
9
|
-
ucon/unit.py,sha256=KxOBcQNxciljGskhZCfktLhRF5u-rWgrTg565Flo3eI,3213
|
|
10
|
-
ucon/units.py,sha256=e1j7skYMghlMZi7l94EAgxq4_lNRDC7FcSooJoE_U50,3689
|
|
11
|
-
ucon-0.3.3rc2.dist-info/licenses/LICENSE,sha256=-Djjiq2wM8Cc6fzTsdMbr_T2_uaX6Yorxcemr3GGkqc,1072
|
|
12
|
-
ucon-0.3.3rc2.dist-info/METADATA,sha256=ngEwpM1VYJIqUwDilCUwNavOqt0ol55k4zFNDb7Xcpg,10606
|
|
13
|
-
ucon-0.3.3rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
ucon-0.3.3rc2.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
|
|
15
|
-
ucon-0.3.3rc2.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2020 Emmanuel I. Obi
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
File without changes
|