ucon 0.3.5__py3-none-any.whl → 0.3.5rc1__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 CHANGED
@@ -38,7 +38,7 @@ Design Philosophy
38
38
  """
39
39
  from ucon import units
40
40
  from ucon.algebra import Exponent
41
- from ucon.core import Dimension, Scale, Unit, UnitFactor, UnitProduct
41
+ from ucon.core import Dimension, Scale, Unit
42
42
  from ucon.quantity import Number, Ratio
43
43
 
44
44
 
@@ -49,7 +49,5 @@ __all__ = [
49
49
  'Ratio',
50
50
  'Scale',
51
51
  'Unit',
52
- 'UnitFactor',
53
- 'UnitProduct',
54
52
  'units',
55
53
  ]
ucon/core.py CHANGED
@@ -240,7 +240,8 @@ class Scale(Enum):
240
240
 
241
241
  def __mul__(self, other):
242
242
  # --- Case 1: applying Scale to simple Unit --------------------
243
- if isinstance(other, Unit):
243
+ if isinstance(other, Unit) and not isinstance(other, UnitProduct):
244
+ # Unit no longer has scale attribute - always safe to apply
244
245
  return UnitProduct({UnitFactor(unit=other, scale=self): 1})
245
246
 
246
247
  # --- Case 2: other cases are NOT handled here -----------------
@@ -342,6 +343,8 @@ class Unit:
342
343
  Unit * Unit -> UnitProduct
343
344
  Unit * UnitProduct -> UnitProduct
344
345
  """
346
+ from ucon.core import UnitProduct # local import to avoid circulars
347
+
345
348
  if isinstance(other, UnitProduct):
346
349
  # let UnitProduct handle merging
347
350
  return other.__rmul__(self)
@@ -353,13 +356,12 @@ class Unit:
353
356
 
354
357
  def __truediv__(self, other):
355
358
  """
356
- Unit / Unit or Unit / UnitProduct => UnitProduct
359
+ Unit / Unit:
360
+ - If same unit => dimensionless Unit()
361
+ - If denominator is dimensionless => self
362
+ - Else => UnitProduct
357
363
  """
358
- if isinstance(other, UnitProduct):
359
- combined = {self: 1.0}
360
- for u, exp in other.factors.items():
361
- combined[u] = combined.get(u, 0.0) - exp
362
- return UnitProduct(combined)
364
+ from ucon.core import UnitProduct # local import
363
365
 
364
366
  if not isinstance(other, Unit):
365
367
  return NotImplemented
@@ -383,13 +385,13 @@ class Unit:
383
385
  """
384
386
  Unit ** n => UnitProduct with that exponent.
385
387
  """
388
+ from ucon.core import UnitProduct # local import
389
+
386
390
  return UnitProduct({self: power})
387
391
 
388
392
  # ----------------- equality & hashing -----------------
389
393
 
390
394
  def __eq__(self, other):
391
- if isinstance(other, UnitProduct):
392
- return other.__eq__(self)
393
395
  if not isinstance(other, Unit):
394
396
  return NotImplemented
395
397
  return (
@@ -496,7 +498,7 @@ class UnitFactor:
496
498
  return NotImplemented
497
499
 
498
500
 
499
- class UnitProduct:
501
+ class UnitProduct(Unit):
500
502
  """
501
503
  Represents a product or quotient of Units.
502
504
 
@@ -525,7 +527,8 @@ class UnitProduct:
525
527
  encountered UnitFactor (keeps user-intent scale).
526
528
  """
527
529
 
528
- self.name = ""
530
+ # UnitProduct always starts dimensionless
531
+ super().__init__(name="", dimension=Dimension.none)
529
532
  self.aliases = ()
530
533
 
531
534
  merged: dict[UnitFactor, float] = {}
@@ -703,16 +706,6 @@ class UnitProduct:
703
706
  result *= factor.scale.value.evaluated ** power
704
707
  return result
705
708
 
706
- # ------------- Helpers ---------------------------------------------------
707
-
708
- def _norm(self, aliases: tuple[str, ...]) -> tuple[str, ...]:
709
- """Normalize alias bag: drop empty/whitespace-only aliases."""
710
- return tuple(a for a in aliases if a.strip())
711
-
712
- def __pow__(self, power):
713
- """UnitProduct ** n => new UnitProduct with scaled exponents."""
714
- return UnitProduct({u: exp * power for u, exp in self.factors.items()})
715
-
716
709
  # ------------- Algebra ---------------------------------------------------
717
710
 
718
711
  def __mul__(self, other):
@@ -806,7 +799,7 @@ class UnitProduct:
806
799
  return f"<{self.__class__.__name__} {self.shorthand}>"
807
800
 
808
801
  def __eq__(self, other):
809
- if isinstance(other, Unit):
802
+ if isinstance(other, Unit) and not isinstance(other, UnitProduct):
810
803
  # Only equal to a plain Unit if we have exactly that unit^1
811
804
  # Here, the tuple comparison will invoke UnitFactor.__eq__(Unit)
812
805
  # on the key when factors are keyed by UnitFactor.
ucon/quantity.py CHANGED
@@ -43,7 +43,7 @@ class Number:
43
43
  <2.5 (m/s)>
44
44
  """
45
45
  quantity: Union[float, int] = 1.0
46
- unit: Union[Unit, UnitProduct] = units.none
46
+ unit: Unit = units.none
47
47
 
48
48
  @property
49
49
  def value(self) -> float:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.3.5
3
+ Version: 0.3.5rc1
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
@@ -55,8 +55,8 @@ Dynamic: summary
55
55
  It combines **units**, **scales**, and **dimensions** into a composable algebra that supports:
56
56
 
57
57
  - Dimensional analysis through `Number` and `Ratio`
58
- - Scale-aware arithmetic via `UnitFactor` and `UnitProduct`
59
- - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
58
+ - Scale-aware arithmetic and conversions
59
+ - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, ect.)
60
60
  - A clean foundation for physics, chemistry, data modeling, and beyond
61
61
 
62
62
  Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
@@ -70,22 +70,20 @@ The crux of this tiny library is to provide abstractions that simplify the answe
70
70
  To best answer this question, we turn to an age-old technique ([dimensional analysis](https://en.wikipedia.org/wiki/Dimensional_analysis)) which essentially allows for the solution to be written as a product of ratios. `ucon` comes equipped with some useful primitives:
71
71
  | Type | Defined In | Purpose | Typical Use Cases |
72
72
  | ----------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
73
- | **`Vector`** | `ucon.algebra` | Represents the exponent tuple of a physical quantity's base dimensions (e.g., T, L, M, I, Θ, J, N). | Internal representation of dimensional algebra; building derived quantities (e.g., area, velocity, force). |
74
- | **`Exponent`** | `ucon.algebra` | Represents base-power pairs (e.g., 10³, 2¹⁰) used by `Scale`. | Performing arithmetic on powers and bases; normalizing scales across conversions. |
75
- | **`Dimension`** | `ucon.core` | Encapsulates physical dimensions (e.g., length, time, mass) as algebraic combinations of vectors. | Enforcing dimensional consistency; defining relationships between quantities (e.g., length / time = velocity). |
73
+ | **`Vector`** | `ucon.dimension` | Represents the exponent tuple of a physical quantitys base dimensions (e.g., T, L, M, I, Θ, J, N). | Internal representation of dimensional algebra; building derived quantities (e.g., area, velocity, force). |
74
+ | **`Dimension`** | `ucon.dimension` | Encapsulates physical dimensions (e.g., length, time, mass) as algebraic combinations of vectors. | Enforcing dimensional consistency; defining relationships between quantities (e.g., length / time = velocity). |
75
+ | **`Unit`** | `ucon.unit` | Represents a named, dimensioned measurement unit (e.g., meter, second, joule). | Attaching human-readable units to quantities; defining or composing new units (`newton = kilogram * meter / second²`). |
76
76
  | **`Scale`** | `ucon.core` | Encodes powers of base magnitudes (binary or decimal prefixes like kilo-, milli-, mebi-). | Adjusting numeric scale without changing dimension (e.g., kilometer ↔ meter, byte ↔ kibibyte). |
77
- | **`Unit`** | `ucon.core` | An atomic, scale-free measurement symbol (e.g., meter, second, joule) with a `Dimension`. | Defining base units; serving as graph nodes for future conversions. |
78
- | **`UnitFactor`** | `ucon.core` | Pairs a `Unit` with a `Scale` (e.g., kilo + gram = kg). Used as keys inside `UnitProduct`. | Preserving user-provided scale prefixes through algebraic operations. |
79
- | **`UnitProduct`** | `ucon.core` | A product/quotient of `UnitFactor`s with exponent tracking and simplification. | Representing composite units like m/s, kg·m/s², kJ·h. |
80
- | **`Number`** | `ucon.quantity` | Combines a numeric quantity with a unit; the primary measurable type. | Performing arithmetic with units; representing physical quantities like 5 m/s. |
81
- | **`Ratio`** | `ucon.quantity` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
82
- | **`units` module** | `ucon.units` | Defines canonical unit instances (SI and common derived units). | Quick access to standard physical units (`units.meter`, `units.second`, `units.newton`, etc.). |
77
+ | **`Exponent`** | `ucon.core` | Represents base-power pairs (e.g., 10³, 2¹⁰) used by `Scale`. | Performing arithmetic on powers and bases; normalizing scales across conversions. |
78
+ | **`Number`** | `ucon.core` | Combines a numeric quantity with a unit and scale; the primary measurable type. | Performing arithmetic with units; converting between compatible units; representing physical quantities like 5 m/s. |
79
+ | **`Ratio`** | `ucon.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
80
+ | **`units` module** | `ucon.units` | Defines canonical unit instances (SI and common derived units). | Quick access to standard physical units (`units.meter`, `units.second`, `units.newton`, etc.). | |
83
81
 
84
82
  ### Under the Hood
85
83
 
86
84
  `ucon` models unit math through a hierarchy where each layer builds on the last:
87
85
 
88
- <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f24134c362829dc72e7dff18bfcaa24b9be01b54/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
86
+ <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/0c704737a52b9e4a87cda5c839e9aa40f7e5bb48/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
89
87
 
90
88
  ## Why `ucon`?
91
89
 
@@ -93,22 +91,24 @@ Python already has mature libraries for handling units and physical quantities
93
91
 
94
92
  | Library | Focus | Limitation |
95
93
  | --------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
96
- | **Pint** | Runtime unit conversion and compatibility checking | Treats quantities as decorated numbers — conversions work, but the algebra behind them isn't inspectable or type-safe. |
94
+ | **Pint** | Runtime unit conversion and compatibility checking | Treats quantities as decorated numbers — conversions work, but the algebra behind them isnt inspectable or type-safe. |
97
95
  | **SymPy** | Symbolic algebra and simplification of unit expressions | Excellent for symbolic reasoning, but not designed for runtime validation, conversion, or serialization. |
98
96
  | **Unum** | Unit-aware arithmetic and unit propagation | Tracks units through arithmetic but lacks explicit dimensional algebra, conversion taxonomy, or runtime introspection. |
99
97
 
100
98
  Together, these tools can _use_ units, but none can explicitly represent and verify the relationships between units and dimensions.
101
99
 
102
- That's the gap `ucon` fills.
100
+ Thats the gap `ucon` fills.
103
101
 
104
102
  It treats units, dimensions, and scales as first-class objects and builds a composable algebra around them.
105
103
  This allows you to:
106
104
  - Represent dimensional meaning explicitly (`Dimension`, `Vector`);
107
105
  - Compose and compute with type-safe, introspectable quantities (`Unit`, `Number`);
106
+ - Perform reversible, declarative conversions (standard, linear, affine, nonlinear);
107
+ - Serialize and validate measurements with Pydantic integration;
108
108
  - Extend the system with custom unit registries and conversion families.
109
109
 
110
110
  Where Pint, Unum, and SymPy focus on _how_ to compute with units,
111
- `ucon` focuses on why those computations make sense. Every operation checks the dimensional structure, _not just the unit labels_. This means ucon doesn't just track names: it enforces physics:
111
+ `ucon` focuses on why those computations make sense. Every operation checks the dimensional structure, _not just the unit labels_. This means ucon doesnt just track names: it enforces physics:
112
112
  ```python
113
113
  from ucon import Number, units
114
114
 
@@ -136,8 +136,7 @@ This sort of dimensional analysis:
136
136
  ```
137
137
  becomes straightforward when you define a measurement:
138
138
  ```python
139
- from ucon import Number, Scale, units
140
- from ucon.quantity import Ratio
139
+ from ucon import Number, Scale, Units, Ratio
141
140
 
142
141
  # Two milliliters of bromine
143
142
  mL = Scale.milli * units.liter
@@ -150,36 +149,27 @@ bromine_density = Ratio(
150
149
  )
151
150
 
152
151
  # Multiply to find mass
153
- grams_bromine = bromine_density.evaluate() * two_mL_bromine
154
- print(grams_bromine) # <6.238 g>
152
+ grams_bromine = two_mL_bromine * bromine_density
153
+ print(grams_bromine) # <6.238 gram>
155
154
  ```
156
155
 
157
- Scale prefixes compose naturally:
158
- ```python
159
- km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
160
- mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
161
-
162
- print(km.shorthand) # 'km'
163
- print(mg.shorthand) # 'mg'
156
+ Scale conversion is automatic and precise:
164
157
 
165
- # Scale arithmetic
166
- print(km.fold_scale()) # 1000.0
167
- print(mg.fold_scale()) # 0.001
158
+ ```python
159
+ grams_bromine.to(Scale.milli) # <6238.0 milligram>
160
+ grams_bromine.to(Scale.kibi) # <0.006091796875 kibigram>
168
161
  ```
169
162
 
170
- > **Note:** Unit _conversions_ (e.g., `number.to(units.inch)`) are planned for v0.4.x
171
- > via the `ConversionGraph` abstraction. See [ROADMAP.md](./ROADMAP.md).
172
-
173
163
  ---
174
164
 
175
165
  ## Roadmap Highlights
176
166
 
177
- | Version | Theme | Focus | Status |
178
- |----------|-------|--------|--------|
179
- | **0.3.5** | Dimensional Algebra | Unit/Scale separation, `UnitFactor`, `UnitProduct` | ✅ Complete |
180
- | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | `ConversionGraph`, `Number.to()` | 🚧 Up Next |
181
- | [**0.6.x**](https://github.com/withtwoemms/ucon/milestone/4) | Nonlinear / Specialized Units | Decibel, Percent, pH | ⏳ Planned |
182
- | [**0.8.x**](https://github.com/withtwoemms/ucon/milestone/6) | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
167
+ | Version | Theme | Focus |
168
+ |----------|-------|--------|
169
+ | [**0.3.x**](https://github.com/withtwoemms/ucon/milestone/1) | Primitive Type Refinement | Unified algebraic foundation |
170
+ | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | Linear & affine conversions |
171
+ | [**0.6.x**](https://github.com/withtwoemms/ucon/milestone/4) | Nonlinear / Specialized Units | Decibel, Percent, pH |
172
+ | [**0.8.x**](https://github.com/withtwoemms/ucon/milestone/6) | Pydantic Integration | Type-safe quantity validation |
183
173
 
184
174
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
185
175
 
@@ -192,13 +182,13 @@ Ensure `nox` is installed.
192
182
  ```
193
183
  pip install -r requirements.txt
194
184
  ```
195
- Then run the full test suite (against all supported python versions) before committing:
185
+ Then run the full test suite (agains all supported python versions) before committing:
196
186
 
197
187
  ```bash
198
188
  nox -s test
199
189
  ```
200
190
  ---
201
191
 
202
- > "If it can be measured, it can be represented.
192
+ > If it can be measured, it can be represented.
203
193
  If it can be represented, it can be validated.
204
- If it can be validated, it can be trusted."
194
+ If it can be validated, it can be trusted.”
@@ -0,0 +1,16 @@
1
+ tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
2
+ tests/ucon/test_algebra.py,sha256=0mxkiXibZfnzYtbscgVXPDcX1JelrVpcqNBcQe3cn3g,8330
3
+ tests/ucon/test_core.py,sha256=x5JLJAKuaTBkxQzYqTFnDaStVcIlnCviJNiN2OJ7KGQ,32435
4
+ tests/ucon/test_quantity.py,sha256=YLV78_t4AkZcJeEGu-lBvIXNLhTV_MLkTbIKV67rs4Y,14955
5
+ tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
6
+ ucon/__init__.py,sha256=vJ6A2BU6s2_3vW4fExhn3VEjbn8yrvgF2hYBfGrKPrY,1865
7
+ ucon/algebra.py,sha256=ZVF8B2kUOeSN20R-lTHJNJDxe_Zv7s8kJ70F2JOSYxk,7262
8
+ ucon/core.py,sha256=cjq0hc5L7iA3ObWUT9JetUnWN6-WWqoQJ7c9YWbS35Y,29597
9
+ ucon/quantity.py,sha256=rjJLx1bktnfddfgn80m3eyVEPq0LU9RmUVR0aOeopqM,7290
10
+ ucon/units.py,sha256=-CShNMLr9t7f3pyYsfmZv3wMCZU4lEnoe8r_9YQWjxA,3783
11
+ ucon-0.3.5rc1.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
12
+ ucon-0.3.5rc1.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
13
+ ucon-0.3.5rc1.dist-info/METADATA,sha256=EqRVt-FxtnB-C73U_-JZqzouAKJl_y3ofiBAKxxBVtM,10626
14
+ ucon-0.3.5rc1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ ucon-0.3.5rc1.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
16
+ ucon-0.3.5rc1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
2
- tests/ucon/test_algebra.py,sha256=0mxkiXibZfnzYtbscgVXPDcX1JelrVpcqNBcQe3cn3g,8330
3
- tests/ucon/test_core.py,sha256=x5JLJAKuaTBkxQzYqTFnDaStVcIlnCviJNiN2OJ7KGQ,32435
4
- tests/ucon/test_quantity.py,sha256=YLV78_t4AkZcJeEGu-lBvIXNLhTV_MLkTbIKV67rs4Y,14955
5
- tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
6
- ucon/__init__.py,sha256=Va7KJ5ImQCkf06TWPKs0r6kfziURsvGh5I63ipiYiLo,1927
7
- ucon/algebra.py,sha256=ZVF8B2kUOeSN20R-lTHJNJDxe_Zv7s8kJ70F2JOSYxk,7262
8
- ucon/core.py,sha256=Dx8HNwjGpF-RFlO_2Cz1BZikRFeLcYlMy7hscw106vo,29825
9
- ucon/quantity.py,sha256=skXge9RU-5dW1ULCV6kiE4jk-rMVzXpBa4hV4mVr_Eo,7310
10
- ucon/units.py,sha256=-CShNMLr9t7f3pyYsfmZv3wMCZU4lEnoe8r_9YQWjxA,3783
11
- ucon-0.3.5.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
12
- ucon-0.3.5.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
13
- ucon-0.3.5.dist-info/METADATA,sha256=unyLeaX8cl9Uf-EaX1NnylgaW5yUBLf_k22-cfCzaq0,11429
14
- ucon-0.3.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
- ucon-0.3.5.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
16
- ucon-0.3.5.dist-info/RECORD,,
File without changes