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.
@@ -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.
@@ -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,]))
@@ -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.