ucon 0.3.2rc6__tar.gz → 0.3.3__tar.gz
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-0.3.2rc6 → ucon-0.3.3}/.github/workflows/publish.yaml +1 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/PKG-INFO +5 -32
- {ucon-0.3.2rc6 → ucon-0.3.3}/README.md +4 -31
- {ucon-0.3.2rc6 → ucon-0.3.3}/ROADMAP.md +3 -3
- ucon-0.3.3/docs/proposals/interface-unifying-the-value-layer.md +131 -0
- ucon-0.3.3/docs/proposals/support-for-fractional-exponents.md +144 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/ucon/test_core.py +54 -6
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon/core.py +74 -26
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon.egg-info/PKG-INFO +5 -32
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon.egg-info/SOURCES.txt +3 -1
- {ucon-0.3.2rc6 → ucon-0.3.3}/.github/workflows/tests.yaml +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/.gitignore +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/LICENSE +0 -0
- {ucon-0.3.2rc6/docs → ucon-0.3.3/docs/decisions}/unity-distance-metric-for-nearest-scale.md +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/noxfile.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/requirements.txt +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/setup.cfg +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/setup.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/__init__.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/ucon/__init__.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/ucon/test_dimension.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/ucon/test_unit.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/tests/ucon/test_units.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon/__init__.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon/dimension.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon/unit.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon/units.py +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon.egg-info/dependency_links.txt +0 -0
- {ucon-0.3.2rc6 → ucon-0.3.3}/ucon.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucon
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -34,17 +34,18 @@ Dynamic: maintainer
|
|
|
34
34
|
Dynamic: maintainer-email
|
|
35
35
|
Dynamic: summary
|
|
36
36
|
|
|
37
|
-
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="
|
|
37
|
+
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="200" />
|
|
38
38
|
|
|
39
39
|
# ucon
|
|
40
40
|
|
|
41
41
|
> Pronounced: _yoo · cahn_
|
|
42
|
-
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
43
42
|
|
|
44
43
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Atests)
|
|
45
44
|
[](https://codecov.io/gh/withtwoemms/ucon)
|
|
46
45
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Apublish)
|
|
47
46
|
|
|
47
|
+
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
48
|
+
|
|
48
49
|
---
|
|
49
50
|
|
|
50
51
|
## Overview
|
|
@@ -81,35 +82,7 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
81
82
|
|
|
82
83
|
`ucon` models unit math through a hierarchy where each layer builds on the last:
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
config:
|
|
87
|
-
layout: elk
|
|
88
|
-
elk:
|
|
89
|
-
mergeEdges: true # Combines parallel edges
|
|
90
|
-
nodePlacementStrategy: SIMPLE # Other options: SIMPLE, NETWORK_SIMPLEX, BRANDES_KOEPF (default)
|
|
91
|
-
---
|
|
92
|
-
flowchart LR
|
|
93
|
-
%% --- Algebraic substrate ---
|
|
94
|
-
subgraph "Algebraic Substrate"
|
|
95
|
-
A[Exponent] --> B[Scale]
|
|
96
|
-
end
|
|
97
|
-
%% --- Physical ontology ---
|
|
98
|
-
subgraph "Physical Ontology"
|
|
99
|
-
D[Dimension] --> E[Unit]
|
|
100
|
-
end
|
|
101
|
-
%% --- Value layer ---
|
|
102
|
-
subgraph "Value Layer"
|
|
103
|
-
F[Number]
|
|
104
|
-
G[Ratio]
|
|
105
|
-
end
|
|
106
|
-
%% --- Cross-layer relationships ---
|
|
107
|
-
E --> F
|
|
108
|
-
B --> F
|
|
109
|
-
%% Ratio composes Numbers and also evaluates to a Number
|
|
110
|
-
F --> G
|
|
111
|
-
G --> F
|
|
112
|
-
```
|
|
85
|
+
<img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f3518d37445301950026fc9ffd1bd062768005fe/ucon.data-model.png align="center" alt="ucon Data Model" width=600/>
|
|
113
86
|
|
|
114
87
|
## Why `ucon`?
|
|
115
88
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="
|
|
1
|
+
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="200" />
|
|
2
2
|
|
|
3
3
|
# ucon
|
|
4
4
|
|
|
5
5
|
> Pronounced: _yoo · cahn_
|
|
6
|
-
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
7
6
|
|
|
8
7
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Atests)
|
|
9
8
|
[](https://codecov.io/gh/withtwoemms/ucon)
|
|
10
9
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Apublish)
|
|
11
10
|
|
|
11
|
+
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
12
|
+
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
## Overview
|
|
@@ -45,35 +46,7 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
45
46
|
|
|
46
47
|
`ucon` models unit math through a hierarchy where each layer builds on the last:
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
config:
|
|
51
|
-
layout: elk
|
|
52
|
-
elk:
|
|
53
|
-
mergeEdges: true # Combines parallel edges
|
|
54
|
-
nodePlacementStrategy: SIMPLE # Other options: SIMPLE, NETWORK_SIMPLEX, BRANDES_KOEPF (default)
|
|
55
|
-
---
|
|
56
|
-
flowchart LR
|
|
57
|
-
%% --- Algebraic substrate ---
|
|
58
|
-
subgraph "Algebraic Substrate"
|
|
59
|
-
A[Exponent] --> B[Scale]
|
|
60
|
-
end
|
|
61
|
-
%% --- Physical ontology ---
|
|
62
|
-
subgraph "Physical Ontology"
|
|
63
|
-
D[Dimension] --> E[Unit]
|
|
64
|
-
end
|
|
65
|
-
%% --- Value layer ---
|
|
66
|
-
subgraph "Value Layer"
|
|
67
|
-
F[Number]
|
|
68
|
-
G[Ratio]
|
|
69
|
-
end
|
|
70
|
-
%% --- Cross-layer relationships ---
|
|
71
|
-
E --> F
|
|
72
|
-
B --> F
|
|
73
|
-
%% Ratio composes Numbers and also evaluates to a Number
|
|
74
|
-
F --> G
|
|
75
|
-
G --> F
|
|
76
|
-
```
|
|
49
|
+
<img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f3518d37445301950026fc9ffd1bd062768005fe/ucon.data-model.png align="center" alt="ucon Data Model" width=600/>
|
|
77
50
|
|
|
78
51
|
## Why `ucon`?
|
|
79
52
|
|
|
@@ -25,9 +25,9 @@ Stable baseline for:
|
|
|
25
25
|
- [x] Refactor `ucon.units` to use dimensional definitions
|
|
26
26
|
- [ ] Publish documentation for dimensional operations
|
|
27
27
|
- [x] Verify uniqueness and hashing correctness across all Dimensions
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
28
|
+
- [x] Redesign `Exponent` to support algebraic operations (`__mul__`, `__truediv__`, `to_base`, etc.)
|
|
29
|
+
- [x] Remove redundant evaluated caching in favor of property-based computation
|
|
30
|
+
- [x] Integrate `Scale` with Exponent for consistent prefix arithmetic
|
|
31
31
|
- [ ] Update `Number` and `Ratio` to use Exponent-driven scaling
|
|
32
32
|
- [ ] Add regression tests for prefix math (`kilo / milli → mega`, `2¹⁰ / 10³ → 1.024×`)
|
|
33
33
|
- [ ] Document Exponent/Scale relationship in developer guide
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Design Note: Quantifiable and the Ucon Algebra
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`Quantifiable` defines the core behavioral contract for all measurable entities in *ucon* —
|
|
6
|
+
including `Number`, `Ratio`, and any future algebraic quantity (e.g. `VectorQuantity`, `TensorQuantity`, or `UncertainQuantity`).
|
|
7
|
+
|
|
8
|
+
It establishes a unified interface for interacting with physical quantities that carry *unit*,
|
|
9
|
+
*scale*, and *magnitude*, providing both consistency and extensibility across the library.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why It Exists
|
|
14
|
+
|
|
15
|
+
Before `Quantifiable`, `Number` and `Ratio` implemented overlapping methods such as
|
|
16
|
+
`evaluated`, `simplify`, and `to`, but independently. This led to duplicated logic,
|
|
17
|
+
ad hoc comparisons, and specialized handling in conversions and arithmetic.
|
|
18
|
+
|
|
19
|
+
`Quantifiable` resolves this by formalizing the idea of “quantity-ness” as an algebraic
|
|
20
|
+
typeclass — a single interface for all objects that represent measurable magnitudes.
|
|
21
|
+
|
|
22
|
+
This abstraction brings:
|
|
23
|
+
- **Polymorphism**: shared behavior across `Number`, `Ratio`, and future types.
|
|
24
|
+
- **Code reuse**: common equality, serialization, and algebraic defaults.
|
|
25
|
+
- **Type safety**: all measurable types must define the same contract.
|
|
26
|
+
- **Simplified conversions**: the conversion graph can treat all quantities uniformly.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## The Quantifiable Contract
|
|
31
|
+
|
|
32
|
+
Every `Quantifiable` must define:
|
|
33
|
+
|
|
34
|
+
| Method | Purpose |
|
|
35
|
+
|---------|----------|
|
|
36
|
+
| `unit` | The dimensional anchor (e.g., meter, second, volt). |
|
|
37
|
+
| `scale` | The exponential prefix (e.g., kilo, milli, one). |
|
|
38
|
+
| `evaluated` | Numeric magnitude as `quantity × scale`. |
|
|
39
|
+
| `simplify()` | Collapse scale to base (Scale.one). |
|
|
40
|
+
| `to(target)` | Convert to another unit or scale. |
|
|
41
|
+
|
|
42
|
+
Optional methods (provided by defaults):
|
|
43
|
+
- `__eq__()` — Dimension-aware equality.
|
|
44
|
+
- `__repr__()` — Standardized developer representation.
|
|
45
|
+
- `as_dict()` — Serialization for persistence or validation.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## What It Enables
|
|
50
|
+
|
|
51
|
+
### 1. Unified Algebraic Operations
|
|
52
|
+
|
|
53
|
+
Functions can now operate generically on *any measurable thing*:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
def normalize(q: Quantifiable) -> Quantifiable:
|
|
57
|
+
return q.simplify()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Works for both `Number` and `Ratio` — no branching required.
|
|
61
|
+
|
|
62
|
+
### 2. Simplified Conversion Logic
|
|
63
|
+
|
|
64
|
+
The conversion system can be defined once:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def convert(q: Quantifiable, target_unit: Unit) -> Quantifiable:
|
|
68
|
+
factor = conversion_graph[q.unit, target_unit]
|
|
69
|
+
return q.__class__(
|
|
70
|
+
quantity=q.evaluated * factor,
|
|
71
|
+
unit=target_unit,
|
|
72
|
+
scale=q.scale
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Pydantic Integration
|
|
77
|
+
|
|
78
|
+
A single model can represent any measurable value:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
class Measurement(BaseModel):
|
|
82
|
+
value: Quantifiable
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This allows runtime validation of units, scales, and dimensions.
|
|
86
|
+
|
|
87
|
+
### 4. Cross-Type Extensibility
|
|
88
|
+
|
|
89
|
+
New classes like `VectorQuantity`, `MatrixQuantity`, or `UncertainQuantity`
|
|
90
|
+
can integrate by simply implementing the same interface.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
class VectorQuantity(Quantifiable):
|
|
94
|
+
def evaluated(self): ...
|
|
95
|
+
def simplify(self): ...
|
|
96
|
+
def to(self, target): ...
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 5. Formalization and Verification
|
|
100
|
+
|
|
101
|
+
The Quantifiable interface defines the algebraic properties of measurable quantities,
|
|
102
|
+
making it possible to express invariants in TLA+, Coq, or other formal tools:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
∀ q1, q2 ∈ Quantifiable :
|
|
106
|
+
q1.unit == q2.unit ⇒ simplify(q1 + q2) == simplify(q2 + q1)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This provides a pathway for formal verification of `ucon`'s correctness across languages.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Summary
|
|
114
|
+
|
|
115
|
+
| Benefit | Description |
|
|
116
|
+
|----------|--------------|
|
|
117
|
+
| **Polymorphism** | All measurable types share a common interface. |
|
|
118
|
+
| **Code Simplification** | Shared behavior eliminates duplication. |
|
|
119
|
+
| **Conversion Consistency** | The conversion system can treat all quantities uniformly. |
|
|
120
|
+
| **Type Safety** | Explicitly enforces dimensional and scale semantics. |
|
|
121
|
+
| **Formal Verifiability** | Provides a foundation for mathematical proofs of correctness. |
|
|
122
|
+
| **Extensibility** | Future types can plug into `ucon`'s algebra seamlessly. |
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Closing Thoughts
|
|
127
|
+
|
|
128
|
+
`Quantifiable` transforms *ucon* from a collection of data structures into a true **domain algebra** —
|
|
129
|
+
a framework that defines measurable quantities as first-class algebraic entities. It bridges
|
|
130
|
+
type safety, composability, and mathematical rigor, making future extensions — from vectorization
|
|
131
|
+
to distributed computation — both safe and elegant.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Discrete vs Continuous `Exponent` Comparison
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document compares the **discrete (integer-only)** `Exponent` implementation used in *ucon v0.3.2*
|
|
6
|
+
with the prospects of a new `Exponent` design that embraces fractional powers. It highlights how the
|
|
7
|
+
continuous model restores algebraic closure, improves precision, and simplifies higher-level `Scale`
|
|
8
|
+
operations.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Motivation
|
|
13
|
+
|
|
14
|
+
In *ucon v0.3.2*, `Exponent` was defined as an integer power over fixed bases (2 and 10).
|
|
15
|
+
This worked for standard prefixes like kilo or mebi but failed for mixed-base or non-integer scaling.
|
|
16
|
+
|
|
17
|
+
The continuous design instead treats `Exponent` as a **real-valued exponential** where a given exponent of base _b_ can be written in terms of another base, _a_:
|
|
18
|
+
|
|
19
|
+
$$
|
|
20
|
+
a^x \times b^y = a^{x + y \cdot \log_a(b)}
|
|
21
|
+
$$
|
|
22
|
+
|
|
23
|
+
This modification closes the algebra under all operations avoiding fallbacks or rounding while remaining fully compatible with integer-based scales.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Feature Comparison
|
|
28
|
+
|
|
29
|
+
| Property | Discrete Exponent | Continuous Exponent |
|
|
30
|
+
|-----------|------------------|---------------------|
|
|
31
|
+
| Power type | Integer | Float (real) |
|
|
32
|
+
| Closure | Partial | Complete |
|
|
33
|
+
| Mixed-base operations | Approximate or invalid | Exact |
|
|
34
|
+
| Reversibility (a×b)/b=a | Approximate | Exact |
|
|
35
|
+
| Fractional exponents | Not supported | Supported |
|
|
36
|
+
| Precision | Rounded to prefix | Full algebraic precision |
|
|
37
|
+
| Comparison | Magnitude-based | Power-difference-based |
|
|
38
|
+
| Scale interaction | Enum lookup | Continuous projection |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Example Comparisons
|
|
43
|
+
|
|
44
|
+
### Same-Base Operations
|
|
45
|
+
|
|
46
|
+
| Operation | Discrete | Continuous |
|
|
47
|
+
|------------|-----------|-------------|
|
|
48
|
+
| 10³ × 10² | `<10^5>` | `<10^5.00000>` |
|
|
49
|
+
| 2¹⁰ × 2⁵ | `<2^15>` | `<2^15.00000>` |
|
|
50
|
+
|
|
51
|
+
No change: _integer powers remain consistent._
|
|
52
|
+
|
|
53
|
+
### Mixed-Base Operations
|
|
54
|
+
|
|
55
|
+
| Operation | Discrete | Continuous |
|
|
56
|
+
|------------|-----------|-------------|
|
|
57
|
+
| 10³ × 2¹⁰ | `nearest(10⁶)` | `<10^6.01030>` |
|
|
58
|
+
| 2¹⁰ × 10³ | `nearest(2¹⁹)` | `<2^19.93157>` |
|
|
59
|
+
| 10⁶ ÷ 2¹⁰ | `nearest(10⁶)` | `<10^5.98970>` |
|
|
60
|
+
|
|
61
|
+
The continuous system captures **exact** mixed-base scaling.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 4. Simplified Scale Operations
|
|
66
|
+
|
|
67
|
+
The continuous `Exponent` model greatly simplifies `Scale` arithmetic.
|
|
68
|
+
|
|
69
|
+
In v0.3.2, `Scale.__mul__` and `Scale.__truediv__` had to juggle base mismatches, lookups, and fallbacks.
|
|
70
|
+
Now, `Scale` can delegate directly to its underlying `Exponent`:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
def __mul__(self, other: 'Scale') -> 'Scale':
|
|
74
|
+
result_exp = self.value * other.value # exact Exponent
|
|
75
|
+
return Scale.dynamic(result_exp)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
No more `nearest()` logic, no more rounding errors.
|
|
79
|
+
|
|
80
|
+
### Before
|
|
81
|
+
```python
|
|
82
|
+
# had to guess nearest scale name
|
|
83
|
+
return Scale.nearest(float(result), include_binary=include_binary)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### After
|
|
87
|
+
```python
|
|
88
|
+
# exact algebraic scaling
|
|
89
|
+
return Scale.dynamic(self.value * other.value)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
This removes three major branches of logic in the old implementation, simplifying the `Scale` layer to a thin symbolic wrapper around continuous exponents.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 5. Conceptual Model
|
|
97
|
+
|
|
98
|
+
| Level | Entity | Description |
|
|
99
|
+
|--------|---------|-------------|
|
|
100
|
+
| 1 | **Exponent** | Continuous exponential algebra (base^power) |
|
|
101
|
+
| 2 | **Scale** | Human-readable projection onto named prefixes |
|
|
102
|
+
| 3 | **Number** | Quantified magnitude combining Scale and Unit |
|
|
103
|
+
|
|
104
|
+
With closure restored at the core, everything beyond it becomes simpler and more expressive.
|
|
105
|
+
`Scale` no longer needs lookup heuristics or error handling.
|
|
106
|
+
It can always delegate cleanly to algebraic truth.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 6. Practical Outcomes
|
|
111
|
+
|
|
112
|
+
| Aspect | Benefit |
|
|
113
|
+
|---------|----------|
|
|
114
|
+
| Algebraic closure | No undefined or lossy operations |
|
|
115
|
+
| Extensibility | Future support for arbitrary user-defined bases |
|
|
116
|
+
| Precision | Exact mixed-base conversions (e.g. kilo × kibi) |
|
|
117
|
+
| Maintainability | Scale logic reduced to 1–2 lines |
|
|
118
|
+
| Mathematical integrity | Continuous and reversible model |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 7. Example
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from ucon.core import Exponent
|
|
126
|
+
|
|
127
|
+
a = Exponent(10, 3) # kilo
|
|
128
|
+
b = Exponent(2, 10) # kibi
|
|
129
|
+
|
|
130
|
+
print(a * b) # <10^6.01030>
|
|
131
|
+
print(a / b) # <10^-0.01030>
|
|
132
|
+
print((a * b) / b) # <10^3.00000>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Exact, reversible, and smooth: _no registry lookup or rounding required._
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 8. Closing Thoughts
|
|
140
|
+
|
|
141
|
+
This continuous approach:
|
|
142
|
+
- unifies all scaling operations algebraically,
|
|
143
|
+
- removes arbitrary lookup logic from the `Scale` layer, and
|
|
144
|
+
- lays the groundwork for reversibility across all scale relationships.
|
|
@@ -109,6 +109,15 @@ class TestScale(TestCase):
|
|
|
109
109
|
self.assertEqual(Scale.one, Scale.kibi / Scale.kibi)
|
|
110
110
|
self.assertEqual(Scale.one, Scale.kibi / Scale.kilo)
|
|
111
111
|
|
|
112
|
+
def test___mul__(self):
|
|
113
|
+
self.assertEqual(Scale.kilo, Scale.kilo * Scale.one)
|
|
114
|
+
self.assertEqual(Scale.kilo, Scale.one * Scale.kilo)
|
|
115
|
+
self.assertEqual(Scale.one, Scale.kilo * Scale.milli)
|
|
116
|
+
self.assertEqual(Scale.deca, Scale.hecto * Scale.deci)
|
|
117
|
+
self.assertEqual(Scale.mega, Scale.kilo * Scale.kibi)
|
|
118
|
+
self.assertEqual(Scale.giga, Scale.mega * Scale.kilo)
|
|
119
|
+
self.assertEqual(Scale.one, Scale.one * Scale.one)
|
|
120
|
+
|
|
112
121
|
def test___lt__(self):
|
|
113
122
|
self.assertLess(Scale.one, Scale.kilo)
|
|
114
123
|
|
|
@@ -121,6 +130,43 @@ class TestScale(TestCase):
|
|
|
121
130
|
self.assertIsInstance(Scale.all(), dict)
|
|
122
131
|
|
|
123
132
|
|
|
133
|
+
class TestScaleMultiplicationAdditional(TestCase):
|
|
134
|
+
|
|
135
|
+
def test_decimal_combinations(self):
|
|
136
|
+
self.assertEqual(Scale.kilo * Scale.centi, Scale.deca)
|
|
137
|
+
self.assertEqual(Scale.kilo * Scale.milli, Scale.one)
|
|
138
|
+
self.assertEqual(Scale.hecto * Scale.deci, Scale.deca)
|
|
139
|
+
|
|
140
|
+
def test_binary_combinations(self):
|
|
141
|
+
# kibi (2^10) * mebi (2^20) = 2^30 (should round to nearest known)
|
|
142
|
+
result = Scale.kibi * Scale.mebi
|
|
143
|
+
self.assertEqual(result.value.base, 2)
|
|
144
|
+
self.assertTrue(isinstance(result, Scale))
|
|
145
|
+
|
|
146
|
+
def test_mixed_base_combination(self):
|
|
147
|
+
self.assertEqual(Scale.mega, Scale.kilo * Scale.kibi)
|
|
148
|
+
|
|
149
|
+
def test_result_has_no_exact_match_fallbacks_to_nearest(self):
|
|
150
|
+
# Suppose the exponent product is not in Scale.all()
|
|
151
|
+
# e.g. kilo (10^3) * deci (10^-1) = 10^2 = hecto
|
|
152
|
+
result = Scale.kilo * Scale.deci
|
|
153
|
+
self.assertEqual(result, Scale.hecto)
|
|
154
|
+
|
|
155
|
+
def test_order_independence(self):
|
|
156
|
+
# Associativity of multiplication
|
|
157
|
+
self.assertEqual(Scale.kilo * Scale.centi, Scale.centi * Scale.kilo)
|
|
158
|
+
|
|
159
|
+
def test_non_scale_operand_returns_not_implemented(self):
|
|
160
|
+
with self.assertRaises(TypeError):
|
|
161
|
+
Scale.kilo * 2
|
|
162
|
+
|
|
163
|
+
def test_large_exponent_clamping(self):
|
|
164
|
+
# simulate a very large multiplication, should still resolve
|
|
165
|
+
result = Scale.mega * Scale.mega # 10^12, not defined -> nearest Scale
|
|
166
|
+
self.assertIsInstance(result, Scale)
|
|
167
|
+
self.assertEqual(result.value.base, 10)
|
|
168
|
+
|
|
169
|
+
|
|
124
170
|
class TestScaleDivisionAdditional(TestCase):
|
|
125
171
|
|
|
126
172
|
def test_division_same_base_large_gap(self):
|
|
@@ -255,7 +301,7 @@ class TestNumber(TestCase):
|
|
|
255
301
|
|
|
256
302
|
def test___eq__(self):
|
|
257
303
|
self.assertEqual(self.number, Ratio(self.number)) # 1 gram / 1
|
|
258
|
-
with self.assertRaises(
|
|
304
|
+
with self.assertRaises(TypeError):
|
|
259
305
|
self.number == 1
|
|
260
306
|
|
|
261
307
|
|
|
@@ -290,10 +336,12 @@ class TestRatio(TestCase):
|
|
|
290
336
|
self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
|
|
291
337
|
|
|
292
338
|
def test___mul__(self):
|
|
293
|
-
|
|
339
|
+
n1 = Number(unit=units.gram, quantity=3.119)
|
|
340
|
+
n2 = Number(unit=units.liter, scale=Scale.milli)
|
|
341
|
+
bromine_density = Ratio(n1, n2)
|
|
294
342
|
|
|
295
343
|
# How many grams of bromine are in 2 milliliters?
|
|
296
|
-
two_milliliters_bromine = Number(units.liter, Scale.milli, 2)
|
|
344
|
+
two_milliliters_bromine = Number(unit=units.liter, scale=Scale.milli, quantity=2)
|
|
297
345
|
ratio = two_milliliters_bromine.as_ratio() * bromine_density
|
|
298
346
|
answer = ratio.evaluate()
|
|
299
347
|
self.assertEqual(answer.unit.dimension, Dimension.mass)
|
|
@@ -319,7 +367,7 @@ class TestRatio(TestCase):
|
|
|
319
367
|
|
|
320
368
|
def test___repr__(self):
|
|
321
369
|
self.assertEqual(str(self.one_ratio), '<1.0 >')
|
|
322
|
-
self.assertEqual(str(self.two_ratio), '<2 > / <1 >')
|
|
370
|
+
self.assertEqual(str(self.two_ratio), '<2 > / <1.0 >')
|
|
323
371
|
self.assertEqual(str(self.two_ratio.evaluate()), '<2.0 >')
|
|
324
372
|
|
|
325
373
|
|
|
@@ -458,8 +506,8 @@ class TestNumberEdgeCases(TestCase):
|
|
|
458
506
|
|
|
459
507
|
def test_equality_with_non_number_raises_value_error(self):
|
|
460
508
|
n = Number()
|
|
461
|
-
with self.assertRaises(
|
|
462
|
-
|
|
509
|
+
with self.assertRaises(TypeError):
|
|
510
|
+
n == '5'
|
|
463
511
|
|
|
464
512
|
def test_equality_between_numbers_and_ratios(self):
|
|
465
513
|
n1 = Number(quantity=10)
|
|
@@ -15,6 +15,7 @@ Classes
|
|
|
15
15
|
Together, these classes allow full arithmetic, conversion, and introspection
|
|
16
16
|
of physical quantities with explicit dimensional semantics.
|
|
17
17
|
"""
|
|
18
|
+
from dataclasses import dataclass, field
|
|
18
19
|
from enum import Enum
|
|
19
20
|
from functools import lru_cache, reduce, total_ordering
|
|
20
21
|
from math import log2
|
|
@@ -33,6 +34,8 @@ class Exponent:
|
|
|
33
34
|
|
|
34
35
|
Provides comparison and division semantics used internally to represent
|
|
35
36
|
magnitude prefixes (e.g., kilo, mega, micro).
|
|
37
|
+
|
|
38
|
+
TODO (wittwemms): embrace fractional exponents for closure on multiplication/division.
|
|
36
39
|
"""
|
|
37
40
|
bases = {2: log2, 10: log10}
|
|
38
41
|
|
|
@@ -198,6 +201,31 @@ class Scale(Enum):
|
|
|
198
201
|
|
|
199
202
|
return min(candidates, key=distance)
|
|
200
203
|
|
|
204
|
+
def __mul__(self, other: 'Scale'):
|
|
205
|
+
"""
|
|
206
|
+
Multiply two Scales together.
|
|
207
|
+
|
|
208
|
+
Always returns a `Scale`, representing the resulting order of magnitude.
|
|
209
|
+
If no exact prefix match exists, returns the nearest known Scale.
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(other, Scale):
|
|
212
|
+
return NotImplemented
|
|
213
|
+
|
|
214
|
+
if self is Scale.one:
|
|
215
|
+
return other
|
|
216
|
+
if other is Scale.one:
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
result = self.value * other.value # delegates to Exponent.__mul__
|
|
220
|
+
include_binary = 2 in {self.value.base, other.value.base}
|
|
221
|
+
|
|
222
|
+
if isinstance(result, Exponent):
|
|
223
|
+
match = Scale.all().get(result.parts())
|
|
224
|
+
if match:
|
|
225
|
+
return Scale[match]
|
|
226
|
+
|
|
227
|
+
return Scale.nearest(float(result), include_binary=include_binary)
|
|
228
|
+
|
|
201
229
|
def __truediv__(self, other: 'Scale'):
|
|
202
230
|
"""
|
|
203
231
|
Divide one Scale by another.
|
|
@@ -210,7 +238,6 @@ class Scale(Enum):
|
|
|
210
238
|
|
|
211
239
|
if self == other:
|
|
212
240
|
return Scale.one
|
|
213
|
-
|
|
214
241
|
if other is Scale.one:
|
|
215
242
|
return self
|
|
216
243
|
|
|
@@ -241,7 +268,9 @@ class Scale(Enum):
|
|
|
241
268
|
return self.value == other.value
|
|
242
269
|
|
|
243
270
|
|
|
244
|
-
|
|
271
|
+
Quantifiable = Union['Number', 'Ratio']
|
|
272
|
+
|
|
273
|
+
@dataclass
|
|
245
274
|
class Number:
|
|
246
275
|
"""
|
|
247
276
|
Represents a **numeric quantity** with an associated :class:`Unit` and :class:`Scale`.
|
|
@@ -256,11 +285,14 @@ class Number:
|
|
|
256
285
|
>>> speed
|
|
257
286
|
<2.5 (m/s)>
|
|
258
287
|
"""
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
288
|
+
quantity: Union[float, int] = 1.0
|
|
289
|
+
unit: Unit = units.none
|
|
290
|
+
scale: Scale = field(default_factory=lambda: Scale.one)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def value(self) -> float:
|
|
294
|
+
"""Return numeric magnitude as quantity × scale factor."""
|
|
295
|
+
return round(self.quantity * self.scale.value.evaluated, 15)
|
|
264
296
|
|
|
265
297
|
def simplify(self):
|
|
266
298
|
return Number(unit=self.unit, quantity=self.value)
|
|
@@ -272,28 +304,44 @@ class Number:
|
|
|
272
304
|
def as_ratio(self):
|
|
273
305
|
return Ratio(self)
|
|
274
306
|
|
|
275
|
-
def __mul__(self,
|
|
307
|
+
def __mul__(self, other: Quantifiable) -> 'Number':
|
|
308
|
+
if not isinstance(other, (Number, Ratio)):
|
|
309
|
+
return NotImplemented
|
|
310
|
+
|
|
311
|
+
if isinstance(other, Ratio):
|
|
312
|
+
other = other.evaluate()
|
|
313
|
+
|
|
276
314
|
return Number(
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
315
|
+
quantity=self.quantity * other.quantity,
|
|
316
|
+
unit=self.unit * other.unit,
|
|
317
|
+
scale=self.scale * other.scale,
|
|
280
318
|
)
|
|
281
319
|
|
|
282
|
-
def __truediv__(self,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
def __truediv__(self, other: Quantifiable) -> 'Number':
|
|
321
|
+
if not isinstance(other, (Number, Ratio)):
|
|
322
|
+
return NotImplemented
|
|
323
|
+
|
|
324
|
+
if isinstance(other, Ratio):
|
|
325
|
+
other = other.evaluate()
|
|
326
|
+
|
|
327
|
+
return Number(
|
|
328
|
+
quantity=self.quantity / other.quantity,
|
|
329
|
+
unit=self.unit / other.unit,
|
|
330
|
+
scale=self.scale / other.scale,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def __eq__(self, other: Quantifiable) -> bool:
|
|
334
|
+
if not isinstance(other, (Number, Ratio)):
|
|
335
|
+
raise TypeError(f'Cannot compare Number to non-Number/Ratio type: {type(other)}')
|
|
336
|
+
|
|
337
|
+
elif isinstance(other, Ratio):
|
|
338
|
+
other = other.evaluate()
|
|
339
|
+
|
|
340
|
+
# Compare on evaluated numeric magnitude and exact unit
|
|
341
|
+
return (
|
|
342
|
+
self.unit == other.unit and
|
|
343
|
+
abs(self.value - other.value) < 1e-12
|
|
344
|
+
)
|
|
297
345
|
|
|
298
346
|
def __repr__(self):
|
|
299
347
|
return f'<{self.quantity} {"" if self.scale.name == "one" else self.scale.name}{self.unit.name}>'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucon
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -34,17 +34,18 @@ Dynamic: maintainer
|
|
|
34
34
|
Dynamic: maintainer-email
|
|
35
35
|
Dynamic: summary
|
|
36
36
|
|
|
37
|
-
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="
|
|
37
|
+
<img src="https://gist.githubusercontent.com/withtwoemms/0cb9e6bc8df08f326771a89eeb790f8e/raw/dde6c7d3b8a7d79eb1006ace03fb834e044cdebc/ucon-logo.png" align="left" width="200" />
|
|
38
38
|
|
|
39
39
|
# ucon
|
|
40
40
|
|
|
41
41
|
> Pronounced: _yoo · cahn_
|
|
42
|
-
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
43
42
|
|
|
44
43
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Atests)
|
|
45
44
|
[](https://codecov.io/gh/withtwoemms/ucon)
|
|
46
45
|
[](https://github.com/withtwoemms/ucon/actions?query=workflow%3Apublish)
|
|
47
46
|
|
|
47
|
+
> A lightweight, **unit-aware computation library** for Python — built on first-principles.
|
|
48
|
+
|
|
48
49
|
---
|
|
49
50
|
|
|
50
51
|
## Overview
|
|
@@ -81,35 +82,7 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
81
82
|
|
|
82
83
|
`ucon` models unit math through a hierarchy where each layer builds on the last:
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
config:
|
|
87
|
-
layout: elk
|
|
88
|
-
elk:
|
|
89
|
-
mergeEdges: true # Combines parallel edges
|
|
90
|
-
nodePlacementStrategy: SIMPLE # Other options: SIMPLE, NETWORK_SIMPLEX, BRANDES_KOEPF (default)
|
|
91
|
-
---
|
|
92
|
-
flowchart LR
|
|
93
|
-
%% --- Algebraic substrate ---
|
|
94
|
-
subgraph "Algebraic Substrate"
|
|
95
|
-
A[Exponent] --> B[Scale]
|
|
96
|
-
end
|
|
97
|
-
%% --- Physical ontology ---
|
|
98
|
-
subgraph "Physical Ontology"
|
|
99
|
-
D[Dimension] --> E[Unit]
|
|
100
|
-
end
|
|
101
|
-
%% --- Value layer ---
|
|
102
|
-
subgraph "Value Layer"
|
|
103
|
-
F[Number]
|
|
104
|
-
G[Ratio]
|
|
105
|
-
end
|
|
106
|
-
%% --- Cross-layer relationships ---
|
|
107
|
-
E --> F
|
|
108
|
-
B --> F
|
|
109
|
-
%% Ratio composes Numbers and also evaluates to a Number
|
|
110
|
-
F --> G
|
|
111
|
-
G --> F
|
|
112
|
-
```
|
|
85
|
+
<img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f3518d37445301950026fc9ffd1bd062768005fe/ucon.data-model.png align="center" alt="ucon Data Model" width=600/>
|
|
113
86
|
|
|
114
87
|
## Why `ucon`?
|
|
115
88
|
|
|
@@ -7,7 +7,9 @@ requirements.txt
|
|
|
7
7
|
setup.py
|
|
8
8
|
.github/workflows/publish.yaml
|
|
9
9
|
.github/workflows/tests.yaml
|
|
10
|
-
docs/unity-distance-metric-for-nearest-scale.md
|
|
10
|
+
docs/decisions/unity-distance-metric-for-nearest-scale.md
|
|
11
|
+
docs/proposals/interface-unifying-the-value-layer.md
|
|
12
|
+
docs/proposals/support-for-fractional-exponents.md
|
|
11
13
|
tests/__init__.py
|
|
12
14
|
tests/ucon/__init__.py
|
|
13
15
|
tests/ucon/test_core.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|