ucon 0.3.5rc1__py3-none-any.whl → 0.3.5rc2__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/core.py CHANGED
@@ -240,8 +240,7 @@ 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) and not isinstance(other, UnitProduct):
244
- # Unit no longer has scale attribute - always safe to apply
243
+ if isinstance(other, Unit):
245
244
  return UnitProduct({UnitFactor(unit=other, scale=self): 1})
246
245
 
247
246
  # --- Case 2: other cases are NOT handled here -----------------
@@ -343,8 +342,6 @@ class Unit:
343
342
  Unit * Unit -> UnitProduct
344
343
  Unit * UnitProduct -> UnitProduct
345
344
  """
346
- from ucon.core import UnitProduct # local import to avoid circulars
347
-
348
345
  if isinstance(other, UnitProduct):
349
346
  # let UnitProduct handle merging
350
347
  return other.__rmul__(self)
@@ -356,12 +353,13 @@ class Unit:
356
353
 
357
354
  def __truediv__(self, other):
358
355
  """
359
- Unit / Unit:
360
- - If same unit => dimensionless Unit()
361
- - If denominator is dimensionless => self
362
- - Else => UnitProduct
356
+ Unit / Unit or Unit / UnitProduct => UnitProduct
363
357
  """
364
- from ucon.core import UnitProduct # local import
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)
365
363
 
366
364
  if not isinstance(other, Unit):
367
365
  return NotImplemented
@@ -385,13 +383,13 @@ class Unit:
385
383
  """
386
384
  Unit ** n => UnitProduct with that exponent.
387
385
  """
388
- from ucon.core import UnitProduct # local import
389
-
390
386
  return UnitProduct({self: power})
391
387
 
392
388
  # ----------------- equality & hashing -----------------
393
389
 
394
390
  def __eq__(self, other):
391
+ if isinstance(other, UnitProduct):
392
+ return other.__eq__(self)
395
393
  if not isinstance(other, Unit):
396
394
  return NotImplemented
397
395
  return (
@@ -498,7 +496,7 @@ class UnitFactor:
498
496
  return NotImplemented
499
497
 
500
498
 
501
- class UnitProduct(Unit):
499
+ class UnitProduct:
502
500
  """
503
501
  Represents a product or quotient of Units.
504
502
 
@@ -527,8 +525,7 @@ class UnitProduct(Unit):
527
525
  encountered UnitFactor (keeps user-intent scale).
528
526
  """
529
527
 
530
- # UnitProduct always starts dimensionless
531
- super().__init__(name="", dimension=Dimension.none)
528
+ self.name = ""
532
529
  self.aliases = ()
533
530
 
534
531
  merged: dict[UnitFactor, float] = {}
@@ -706,6 +703,16 @@ class UnitProduct(Unit):
706
703
  result *= factor.scale.value.evaluated ** power
707
704
  return result
708
705
 
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
+
709
716
  # ------------- Algebra ---------------------------------------------------
710
717
 
711
718
  def __mul__(self, other):
@@ -799,7 +806,7 @@ class UnitProduct(Unit):
799
806
  return f"<{self.__class__.__name__} {self.shorthand}>"
800
807
 
801
808
  def __eq__(self, other):
802
- if isinstance(other, Unit) and not isinstance(other, UnitProduct):
809
+ if isinstance(other, Unit):
803
810
  # Only equal to a plain Unit if we have exactly that unit^1
804
811
  # Here, the tuple comparison will invoke UnitFactor.__eq__(Unit)
805
812
  # 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: Unit = units.none
46
+ unit: Union[Unit, UnitProduct] = 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.5rc1
3
+ Version: 0.3.5rc2
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 and conversions
59
- - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, ect.)
58
+ - Scale-aware arithmetic via `UnitFactor` and `UnitProduct`
59
+ - Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
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,20 +70,22 @@ 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.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²`). |
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). |
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
- | **`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.). | |
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.). |
81
83
 
82
84
  ### Under the Hood
83
85
 
84
86
  `ucon` models unit math through a hierarchy where each layer builds on the last:
85
87
 
86
- <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/0c704737a52b9e4a87cda5c839e9aa40f7e5bb48/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
88
+ <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f24134c362829dc72e7dff18bfcaa24b9be01b54/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
87
89
 
88
90
  ## Why `ucon`?
89
91
 
@@ -91,24 +93,22 @@ Python already has mature libraries for handling units and physical quantities
91
93
 
92
94
  | Library | Focus | Limitation |
93
95
  | --------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
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. |
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. |
95
97
  | **SymPy** | Symbolic algebra and simplification of unit expressions | Excellent for symbolic reasoning, but not designed for runtime validation, conversion, or serialization. |
96
98
  | **Unum** | Unit-aware arithmetic and unit propagation | Tracks units through arithmetic but lacks explicit dimensional algebra, conversion taxonomy, or runtime introspection. |
97
99
 
98
100
  Together, these tools can _use_ units, but none can explicitly represent and verify the relationships between units and dimensions.
99
101
 
100
- Thats the gap `ucon` fills.
102
+ That's the gap `ucon` fills.
101
103
 
102
104
  It treats units, dimensions, and scales as first-class objects and builds a composable algebra around them.
103
105
  This allows you to:
104
106
  - Represent dimensional meaning explicitly (`Dimension`, `Vector`);
105
107
  - 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 doesnt 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 doesn't just track names: it enforces physics:
112
112
  ```python
113
113
  from ucon import Number, units
114
114
 
@@ -136,7 +136,8 @@ 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, Ratio
139
+ from ucon import Number, Scale, units
140
+ from ucon.quantity import Ratio
140
141
 
141
142
  # Two milliliters of bromine
142
143
  mL = Scale.milli * units.liter
@@ -149,27 +150,36 @@ bromine_density = Ratio(
149
150
  )
150
151
 
151
152
  # Multiply to find mass
152
- grams_bromine = two_mL_bromine * bromine_density
153
- print(grams_bromine) # <6.238 gram>
153
+ grams_bromine = bromine_density.evaluate() * two_mL_bromine
154
+ print(grams_bromine) # <6.238 g>
154
155
  ```
155
156
 
156
- Scale conversion is automatic and precise:
157
-
157
+ Scale prefixes compose naturally:
158
158
  ```python
159
- grams_bromine.to(Scale.milli) # <6238.0 milligram>
160
- grams_bromine.to(Scale.kibi) # <0.006091796875 kibigram>
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'
164
+
165
+ # Scale arithmetic
166
+ print(km.fold_scale()) # 1000.0
167
+ print(mg.fold_scale()) # 0.001
161
168
  ```
162
169
 
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
+
163
173
  ---
164
174
 
165
175
  ## Roadmap Highlights
166
176
 
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 |
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 |
173
183
 
174
184
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
175
185
 
@@ -182,13 +192,13 @@ Ensure `nox` is installed.
182
192
  ```
183
193
  pip install -r requirements.txt
184
194
  ```
185
- Then run the full test suite (agains all supported python versions) before committing:
195
+ Then run the full test suite (against all supported python versions) before committing:
186
196
 
187
197
  ```bash
188
198
  nox -s test
189
199
  ```
190
200
  ---
191
201
 
192
- > If it can be measured, it can be represented.
202
+ > "If it can be measured, it can be represented.
193
203
  If it can be represented, it can be validated.
194
- If it can be validated, it can be trusted.”
204
+ If it can be validated, it can be trusted."
@@ -5,12 +5,12 @@ tests/ucon/test_quantity.py,sha256=YLV78_t4AkZcJeEGu-lBvIXNLhTV_MLkTbIKV67rs4Y,1
5
5
  tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
6
6
  ucon/__init__.py,sha256=vJ6A2BU6s2_3vW4fExhn3VEjbn8yrvgF2hYBfGrKPrY,1865
7
7
  ucon/algebra.py,sha256=ZVF8B2kUOeSN20R-lTHJNJDxe_Zv7s8kJ70F2JOSYxk,7262
8
- ucon/core.py,sha256=cjq0hc5L7iA3ObWUT9JetUnWN6-WWqoQJ7c9YWbS35Y,29597
9
- ucon/quantity.py,sha256=rjJLx1bktnfddfgn80m3eyVEPq0LU9RmUVR0aOeopqM,7290
8
+ ucon/core.py,sha256=Dx8HNwjGpF-RFlO_2Cz1BZikRFeLcYlMy7hscw106vo,29825
9
+ ucon/quantity.py,sha256=skXge9RU-5dW1ULCV6kiE4jk-rMVzXpBa4hV4mVr_Eo,7310
10
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,,
11
+ ucon-0.3.5rc2.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
12
+ ucon-0.3.5rc2.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
13
+ ucon-0.3.5rc2.dist-info/METADATA,sha256=I6f-KMRaLq9rm4q5OxcFLlK5bKOPvKlWMZHnNpTL_d8,11432
14
+ ucon-0.3.5rc2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ ucon-0.3.5rc2.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
16
+ ucon-0.3.5rc2.dist-info/RECORD,,