ucon 0.3.5rc2__tar.gz → 0.4.1__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.
Files changed (47) hide show
  1. {ucon-0.3.5rc2 → ucon-0.4.1}/PKG-INFO +28 -10
  2. {ucon-0.3.5rc2 → ucon-0.4.1}/README.md +26 -8
  3. {ucon-0.3.5rc2 → ucon-0.4.1}/ROADMAP.md +30 -16
  4. ucon-0.4.1/docs/explainers/exponent-scale-relationship.md +347 -0
  5. {ucon-0.3.5rc2 → ucon-0.4.1}/setup.py +1 -1
  6. ucon-0.4.1/tests/ucon/conversion/__init__.py +0 -0
  7. ucon-0.4.1/tests/ucon/conversion/test_graph.py +409 -0
  8. ucon-0.4.1/tests/ucon/conversion/test_map.py +409 -0
  9. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/ucon/test_algebra.py +34 -34
  10. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/ucon/test_core.py +25 -26
  11. ucon-0.4.1/tests/ucon/test_default_graph_conversions.py +443 -0
  12. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/ucon/test_quantity.py +246 -61
  13. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon/__init__.py +6 -2
  14. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon/algebra.py +9 -5
  15. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon/core.py +366 -53
  16. ucon-0.4.1/ucon/graph.py +423 -0
  17. ucon-0.4.1/ucon/maps.py +161 -0
  18. ucon-0.4.1/ucon/quantity.py +17 -0
  19. ucon-0.4.1/ucon/units.py +135 -0
  20. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon.egg-info/PKG-INFO +28 -10
  21. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon.egg-info/SOURCES.txt +7 -0
  22. ucon-0.3.5rc2/ucon/quantity.py +0 -196
  23. ucon-0.3.5rc2/ucon/units.py +0 -87
  24. {ucon-0.3.5rc2 → ucon-0.4.1}/.github/workflows/publish.yaml +0 -0
  25. {ucon-0.3.5rc2 → ucon-0.4.1}/.github/workflows/tests.yaml +0 -0
  26. {ucon-0.3.5rc2 → ucon-0.4.1}/.gitignore +0 -0
  27. {ucon-0.3.5rc2 → ucon-0.4.1}/LICENSE +0 -0
  28. {ucon-0.3.5rc2 → ucon-0.4.1}/NOTICE +0 -0
  29. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/decisions/composable-unit-algebra.md +0 -0
  30. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/decisions/composite-units.md +0 -0
  31. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/decisions/unit-algebra-naming.md +0 -0
  32. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/decisions/unity-distance-metric-for-nearest-scale.md +0 -0
  33. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/explainers/type-operation-matrix.md +0 -0
  34. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/explainers/why-algebraic-closure-matters.md +0 -0
  35. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/explainers/why-type-safety-matters.md +0 -0
  36. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
  37. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/proposals/project_unified-algebraic-core.md +0 -0
  38. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/proposals/support-for-fractional-exponents.md +0 -0
  39. {ucon-0.3.5rc2 → ucon-0.4.1}/docs/proposals/unified-unit-presentation.md +0 -0
  40. {ucon-0.3.5rc2 → ucon-0.4.1}/noxfile.py +0 -0
  41. {ucon-0.3.5rc2 → ucon-0.4.1}/requirements.txt +0 -0
  42. {ucon-0.3.5rc2 → ucon-0.4.1}/setup.cfg +0 -0
  43. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/__init__.py +0 -0
  44. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/ucon/__init__.py +0 -0
  45. {ucon-0.3.5rc2 → ucon-0.4.1}/tests/ucon/test_units.py +0 -0
  46. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon.egg-info/dependency_links.txt +0 -0
  47. {ucon-0.3.5rc2 → ucon-0.4.1}/ucon.egg-info/top_level.txt +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.3.5rc2
3
+ Version: 0.4.1
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
7
7
  Maintainer: Emmanuel I. Obi
8
8
  Maintainer-email: withtwoemms@gmail.com
9
9
  License: Apache-2.0
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Intended Audience :: Education
13
13
  Classifier: Intended Audience :: Science/Research
@@ -70,22 +70,24 @@ 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). |
73
+ | **`Vector`** | `ucon.algebra` | Represents the 8-component exponent tuple of a physical quantity's base dimensions (T, L, M, I, Θ, J, N, B). | Internal representation of dimensional algebra; building derived quantities (e.g., area, velocity, force). |
74
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
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
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
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
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.). |
80
+ | **`Number`** | `ucon.core` | 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.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
82
+ | **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
83
+ | **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
84
+ | **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
83
85
 
84
86
  ### Under the Hood
85
87
 
86
88
  `ucon` models unit math through a hierarchy where each layer builds on the last:
87
89
 
88
- <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f24134c362829dc72e7dff18bfcaa24b9be01b54/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
90
+ <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/5df6a7fb2a6426ee6804096c092c10bed1b30b6f/ucon.data-model_v040.png align="center" alt="ucon Data Model" width=600/>
89
91
 
90
92
  ## Why `ucon`?
91
93
 
@@ -167,8 +169,24 @@ print(km.fold_scale()) # 1000.0
167
169
  print(mg.fold_scale()) # 0.001
168
170
  ```
169
171
 
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
+ Units are callable for ergonomic quantity construction:
173
+ ```python
174
+ from ucon import units, Scale
175
+
176
+ # Callable syntax: unit(quantity) → Number
177
+ height = units.meter(1.8)
178
+ speed = (units.mile / units.hour)(60)
179
+
180
+ # Convert between units
181
+ height_ft = height.to(units.foot)
182
+ print(height_ft) # <5.905... ft>
183
+
184
+ # Scaled units work too
185
+ km = Scale.kilo * units.meter
186
+ distance = km(5)
187
+ distance_mi = distance.to(units.mile)
188
+ print(distance_mi) # <3.107... mi>
189
+ ```
172
190
 
173
191
  ---
174
192
 
@@ -177,7 +195,7 @@ print(mg.fold_scale()) # 0.001
177
195
  | Version | Theme | Focus | Status |
178
196
  |----------|-------|--------|--------|
179
197
  | **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 |
198
+ | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | `ConversionGraph`, `Number.to()`, callable units | 🚧 In Progress |
181
199
  | [**0.6.x**](https://github.com/withtwoemms/ucon/milestone/4) | Nonlinear / Specialized Units | Decibel, Percent, pH | ⏳ Planned |
182
200
  | [**0.8.x**](https://github.com/withtwoemms/ucon/milestone/6) | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
183
201
 
@@ -33,22 +33,24 @@ The crux of this tiny library is to provide abstractions that simplify the answe
33
33
  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:
34
34
  | Type | Defined In | Purpose | Typical Use Cases |
35
35
  | ----------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
36
- | **`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). |
36
+ | **`Vector`** | `ucon.algebra` | Represents the 8-component exponent tuple of a physical quantity's base dimensions (T, L, M, I, Θ, J, N, B). | Internal representation of dimensional algebra; building derived quantities (e.g., area, velocity, force). |
37
37
  | **`Exponent`** | `ucon.algebra` | Represents base-power pairs (e.g., 10³, 2¹⁰) used by `Scale`. | Performing arithmetic on powers and bases; normalizing scales across conversions. |
38
38
  | **`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). |
39
39
  | **`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). |
40
40
  | **`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. |
41
41
  | **`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. |
42
42
  | **`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. |
43
- | **`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. |
44
- | **`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). |
45
- | **`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.). |
43
+ | **`Number`** | `ucon.core` | Combines a numeric quantity with a unit; the primary measurable type. | Performing arithmetic with units; representing physical quantities like 5 m/s. |
44
+ | **`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). |
45
+ | **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
46
+ | **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
47
+ | **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
46
48
 
47
49
  ### Under the Hood
48
50
 
49
51
  `ucon` models unit math through a hierarchy where each layer builds on the last:
50
52
 
51
- <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/f24134c362829dc72e7dff18bfcaa24b9be01b54/ucon.data-model_v035.png align="center" alt="ucon Data Model" width=600/>
53
+ <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/5df6a7fb2a6426ee6804096c092c10bed1b30b6f/ucon.data-model_v040.png align="center" alt="ucon Data Model" width=600/>
52
54
 
53
55
  ## Why `ucon`?
54
56
 
@@ -130,8 +132,24 @@ print(km.fold_scale()) # 1000.0
130
132
  print(mg.fold_scale()) # 0.001
131
133
  ```
132
134
 
133
- > **Note:** Unit _conversions_ (e.g., `number.to(units.inch)`) are planned for v0.4.x
134
- > via the `ConversionGraph` abstraction. See [ROADMAP.md](./ROADMAP.md).
135
+ Units are callable for ergonomic quantity construction:
136
+ ```python
137
+ from ucon import units, Scale
138
+
139
+ # Callable syntax: unit(quantity) → Number
140
+ height = units.meter(1.8)
141
+ speed = (units.mile / units.hour)(60)
142
+
143
+ # Convert between units
144
+ height_ft = height.to(units.foot)
145
+ print(height_ft) # <5.905... ft>
146
+
147
+ # Scaled units work too
148
+ km = Scale.kilo * units.meter
149
+ distance = km(5)
150
+ distance_mi = distance.to(units.mile)
151
+ print(distance_mi) # <3.107... mi>
152
+ ```
135
153
 
136
154
  ---
137
155
 
@@ -140,7 +158,7 @@ print(mg.fold_scale()) # 0.001
140
158
  | Version | Theme | Focus | Status |
141
159
  |----------|-------|--------|--------|
142
160
  | **0.3.5** | Dimensional Algebra | Unit/Scale separation, `UnitFactor`, `UnitProduct` | ✅ Complete |
143
- | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | `ConversionGraph`, `Number.to()` | 🚧 Up Next |
161
+ | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | `ConversionGraph`, `Number.to()`, callable units | 🚧 In Progress |
144
162
  | [**0.6.x**](https://github.com/withtwoemms/ucon/milestone/4) | Nonlinear / Specialized Units | Decibel, Percent, pH | ⏳ Planned |
145
163
  | [**0.8.x**](https://github.com/withtwoemms/ucon/milestone/6) | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
146
164
 
@@ -4,13 +4,16 @@
4
4
 
5
5
  ---
6
6
 
7
- ## 🪜 Current Version: **v0.3.5**
7
+ ## 🪜 Current Version: **v0.4.0** (in progress)
8
8
 
9
- Stable baseline for:
10
- - `ucon.core` (`Dimension`, `Scale`, `Unit`, `UnitFactor`, `UnitProduct`)
11
- - `ucon.quantity` (`Number`, `Ratio`)
12
- - `ucon.units` (canonical SI definitions)
13
- - Initial CI, testing, and packaging
9
+ Building on v0.3.5 baseline:
10
+ - `ucon.core` (`Dimension`, `Scale`, `Unit`, `UnitFactor`, `UnitProduct`, `Number`, `Ratio`)
11
+ - `ucon.maps` (`Map`, `LinearMap`, `AffineMap`, `ComposedMap`)
12
+ - `ucon.graph` (`ConversionGraph`, default graph, `get_default_graph()`, `using_graph()`)
13
+ - `ucon.units` (SI + imperial + information units, callable syntax)
14
+ - Callable unit API: `meter(5)`, `(mile / hour)(60)`
15
+ - `Number.simplify()` for base-scale normalization
16
+ - `Dimension.information` with `units.bit`, `units.byte`
14
17
 
15
18
  ---
16
19
 
@@ -46,25 +49,36 @@ Stable baseline for:
46
49
 
47
50
  ---
48
51
 
49
- ## ⚙️ v0.4.x — Conversion System Foundations (Up Next)
52
+ ## ⚙️ v0.4.x — Conversion System Foundations (In Progress)
50
53
 
51
54
  ### 🔹 Summary
52
55
  > Implements unified conversion engine for standard, linear, and affine conversions.
56
+ > Introduces callable unit API for ergonomic quantity construction.
53
57
 
54
58
  ### ✅ Goals
55
- - [ ] Introduce `ConversionGraph` registry keyed by `Dimension`
56
- - [ ] Add support for `standard`, `linear`, and `affine` conversion types
57
- - [ ] Implement `Number.to(target_unit)` and `Number.simplify()`
58
- - [ ] Scale-only conversions short-circuit without graph lookup
59
- - [ ] Composite-to-composite conversion via per-component decomposition
60
- - [ ] Round-trip validation for reversible conversions
61
- - [ ] Extend tests to include temperature, pressure, and base SI conversions
62
- - [ ] Document Exponent/Scale relationship in developer guide
59
+ - [x] Introduce `ConversionGraph` registry keyed by `Dimension`
60
+ - [x] Add support for `standard`, `linear`, and `affine` conversion types
61
+ - [x] Implement `Number.to(target_unit)` conversion API
62
+ - [x] Scale-only conversions short-circuit without graph lookup
63
+ - [x] Composite-to-composite conversion via per-component decomposition
64
+ - [x] Round-trip validation for reversible conversions (inverse maps)
65
+ - [x] Callable unit syntax: `meter(5)`, `(mile / hour)(60)`
66
+ - [x] Default graph with common SI and imperial conversions
67
+ - [x] Imperial units: `foot`, `mile`, `yard`, `inch`, `pound`, `ounce`, `fahrenheit`, `gallon`
68
+ - [x] `Number.simplify()` — Express in base scale
69
+ - [x] `Dimension.information` with `units.bit` and `units.byte`
70
+ - [x] `Vector` extended to 8 components (added B for information)
71
+ - [x] Information unit conversions in default graph (byte ↔ bit)
72
+ - [x] Extend tests to include temperature, pressure, and base SI conversions
73
+ - [x] Document Exponent/Scale relationship in developer guide
63
74
 
64
75
  ### 🧩 Outcomes
65
76
  - Unified conversion taxonomy
66
77
  - Reversible, dimension-checked conversions
67
78
  - Scale-aware graph that leverages the `Unit`/`UnitFactor` separation from v0.3.x
79
+ - Ergonomic API: units are callable, returning `Number` instances
80
+ - Information dimension support (bit, byte) with binary prefix compatibility
81
+ - `Number.simplify()` for expressing quantities in base scale
68
82
  - Forms the basis for nonlinear and domain-specific conversion families
69
83
 
70
84
  ---
@@ -202,7 +216,7 @@ Stable baseline for:
202
216
  | Version | Theme | Key Focus | Status |
203
217
  |----------|--------|------------|---------|
204
218
  | **0.3.5** | Dimensional Algebra | Unit/Scale separation, `UnitFactor`, `UnitProduct` | ✅ Complete |
205
- | **0.4.0** | Conversion Engine | `ConversionGraph`, `Number.to()`, scale-aware lookup | 🚧 Up Next |
219
+ | **0.4.0** | Conversion Engine | `ConversionGraph`, `Number.to()`, callable units | 🚧 In Progress |
206
220
  | **0.5.0** | Unit Systems & Registries | Extensible registry system | ⏳ Planned |
207
221
  | **0.6.0** | Nonlinear Conversions | Logarithmic / exponential families | ⏳ Planned |
208
222
  | **0.7.0** | Testing & API Polish | Coverage, ergonomics, stability | ⏳ Planned |
@@ -0,0 +1,347 @@
1
+ # Developer Guide: Exponent and Scale Relationship
2
+
3
+ > Understanding the algebraic foundation of magnitude prefixes in ucon.
4
+
5
+ ---
6
+
7
+ ## Overview
8
+
9
+ The `Exponent` and `Scale` classes form the algebraic foundation for handling magnitude prefixes (kilo, milli, mebi, etc.) in ucon. This guide explains their relationship and how they work together.
10
+
11
+ ```
12
+ ┌─────────────────────────────────────────────────────────────┐
13
+ │ Scale (Enum) │
14
+ │ kilo, milli, mega, kibi, mebi, etc. │
15
+ │ │ │
16
+ │ ▼ │
17
+ │ ScaleDescriptor (dataclass) │
18
+ │ shorthand: "k", alias: "kilo" │
19
+ │ │ │
20
+ │ ▼ │
21
+ │ Exponent (class) │
22
+ │ base: 10, power: 3 │
23
+ │ evaluated: 1000.0 │
24
+ └─────────────────────────────────────────────────────────────┘
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Exponent: The Algebraic Primitive
30
+
31
+ `Exponent` (`ucon.algebra`) represents a base-power pair like 10³ or 2¹⁰.
32
+
33
+ ### Structure
34
+
35
+ ```python
36
+ class Exponent:
37
+ base: int # 2 or 10 only
38
+ power: int | float
39
+
40
+ @property
41
+ def evaluated(self) -> float:
42
+ return self.base ** self.power
43
+ ```
44
+
45
+ ### Supported Bases
46
+
47
+ Only two bases are supported, chosen for their prevalence in scientific and computing contexts:
48
+
49
+ | Base | Use Case | Examples |
50
+ |------|----------|----------|
51
+ | **10** | SI prefixes (metric) | kilo (10³), milli (10⁻³), mega (10⁶) |
52
+ | **2** | Binary prefixes (IEC) | kibi (2¹⁰), mebi (2²⁰), gibi (2³⁰) |
53
+
54
+ ### Arithmetic Operations
55
+
56
+ Exponent supports algebraic operations that mirror the rules of exponents:
57
+
58
+ ```python
59
+ from ucon.algebra import Exponent
60
+
61
+ kilo = Exponent(10, 3) # 10³ = 1000
62
+ milli = Exponent(10, -3) # 10⁻³ = 0.001
63
+
64
+ # Multiplication: add powers (same base)
65
+ kilo * milli # → Exponent(10, 0) = 1
66
+
67
+ # Division: subtract powers (same base)
68
+ kilo / milli # → Exponent(10, 6) = 1,000,000
69
+
70
+ # Exponentiation: multiply power
71
+ kilo ** 2 # → Exponent(10, 6) = 1,000,000
72
+
73
+ # Cross-base operations return float
74
+ kibi = Exponent(2, 10) # 2¹⁰ = 1024
75
+ kilo / kibi # → 0.9765625 (float, not Exponent)
76
+ ```
77
+
78
+ ### Base Conversion
79
+
80
+ Convert between bases while preserving numeric value:
81
+
82
+ ```python
83
+ kibi = Exponent(2, 10) # 1024
84
+ kibi.to_base(10) # → Exponent(10, 3.0103...) ≈ 1024
85
+ ```
86
+
87
+ ---
88
+
89
+ ## ScaleDescriptor: Adding Human-Readable Labels
90
+
91
+ `ScaleDescriptor` (`ucon.core`) wraps an `Exponent` with display information:
92
+
93
+ ```python
94
+ @dataclass(frozen=True)
95
+ class ScaleDescriptor:
96
+ exponent: Exponent
97
+ shorthand: str # "k", "M", "Ki"
98
+ alias: str # "kilo", "mega", "kibi"
99
+ ```
100
+
101
+ ### Properties
102
+
103
+ ```python
104
+ desc = ScaleDescriptor(Exponent(10, 3), "k", "kilo")
105
+
106
+ desc.evaluated # 1000.0 (delegates to exponent)
107
+ desc.base # 10
108
+ desc.power # 3
109
+ desc.parts() # (10, 3)
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Scale: The User-Facing Enum
115
+
116
+ `Scale` (`ucon.core`) is an enum where each member's value is a `ScaleDescriptor`.
117
+
118
+ ### Available Scales
119
+
120
+ ```python
121
+ class Scale(Enum):
122
+ # Binary (base 2)
123
+ gibi = ScaleDescriptor(Exponent(2, 30), "Gi", "gibi") # 2³⁰
124
+ mebi = ScaleDescriptor(Exponent(2, 20), "Mi", "mebi") # 2²⁰
125
+ kibi = ScaleDescriptor(Exponent(2, 10), "Ki", "kibi") # 2¹⁰
126
+
127
+ # Decimal (base 10)
128
+ peta = ScaleDescriptor(Exponent(10, 15), "P", "peta")
129
+ tera = ScaleDescriptor(Exponent(10, 12), "T", "tera")
130
+ giga = ScaleDescriptor(Exponent(10, 9), "G", "giga")
131
+ mega = ScaleDescriptor(Exponent(10, 6), "M", "mega")
132
+ kilo = ScaleDescriptor(Exponent(10, 3), "k", "kilo")
133
+ hecto = ScaleDescriptor(Exponent(10, 2), "h", "hecto")
134
+ deca = ScaleDescriptor(Exponent(10, 1), "da", "deca")
135
+ one = ScaleDescriptor(Exponent(10, 0), "", "") # identity
136
+ deci = ScaleDescriptor(Exponent(10, -1), "d", "deci")
137
+ centi = ScaleDescriptor(Exponent(10, -2), "c", "centi")
138
+ milli = ScaleDescriptor(Exponent(10, -3), "m", "milli")
139
+ micro = ScaleDescriptor(Exponent(10, -6), "µ", "micro")
140
+ nano = ScaleDescriptor(Exponent(10, -9), "n", "nano")
141
+ pico = ScaleDescriptor(Exponent(10, -12),"p", "pico")
142
+ femto = ScaleDescriptor(Exponent(10, -15),"f", "femto")
143
+ ```
144
+
145
+ ### Scale Arithmetic
146
+
147
+ Scale operations delegate to Exponent arithmetic, then resolve back to a Scale:
148
+
149
+ ```python
150
+ from ucon.core import Scale
151
+
152
+ # Multiplication
153
+ Scale.kilo * Scale.kilo # → Scale.mega (10³ × 10³ = 10⁶)
154
+ Scale.kilo * Scale.milli # → Scale.one (10³ × 10⁻³ = 10⁰)
155
+
156
+ # Division
157
+ Scale.mega / Scale.kilo # → Scale.kilo (10⁶ / 10³ = 10³)
158
+ Scale.kilo / Scale.mega # → Scale.milli (10³ / 10⁶ = 10⁻³)
159
+
160
+ # Exponentiation
161
+ Scale.kilo ** 2 # → Scale.mega (10³)² = 10⁶
162
+ Scale.milli ** -1 # → Scale.kilo (10⁻³)⁻¹ = 10³
163
+ ```
164
+
165
+ ### Nearest Scale Resolution
166
+
167
+ When arithmetic produces a non-standard power, `Scale.nearest()` finds the closest match:
168
+
169
+ ```python
170
+ # Non-exact result resolves to nearest scale
171
+ Scale.kilo * Scale.kibi # Cross-base: 1000 × 1024 = 1,024,000
172
+ # → resolves to Scale.mega (closest)
173
+
174
+ # Manual nearest lookup
175
+ Scale.nearest(5000) # → Scale.kilo (10³ = 1000 is closest)
176
+ Scale.nearest(500) # → Scale.one (undershoot bias)
177
+ ```
178
+
179
+ The `undershoot_bias` parameter (default 0.75) penalizes scales smaller than the value, preferring slight overestimation for cleaner display.
180
+
181
+ ### Scale × Unit → UnitProduct
182
+
183
+ The primary use case: applying a scale to a unit:
184
+
185
+ ```python
186
+ from ucon.core import Scale
187
+ from ucon import units
188
+
189
+ km = Scale.kilo * units.meter # → UnitProduct with kilo-scaled meter
190
+ mg = Scale.milli * units.gram # → UnitProduct with milli-scaled gram
191
+
192
+ print(km.shorthand) # "km"
193
+ print(mg.shorthand) # "mg"
194
+
195
+ # Used in Number construction
196
+ distance = km(5) # 5 kilometers
197
+ mass = mg(250) # 250 milligrams
198
+ ```
199
+
200
+ ---
201
+
202
+ ## The Complete Stack
203
+
204
+ Here's how it all fits together:
205
+
206
+ ```
207
+ User writes: km = Scale.kilo * units.meter
208
+
209
+
210
+ Scale.kilo ScaleDescriptor(Exponent(10, 3), "k", "kilo")
211
+
212
+
213
+ Scale.__mul__(Unit) Returns UnitProduct({UnitFactor(meter, kilo): 1})
214
+
215
+
216
+ UnitProduct Stores the unit with its scale prefix
217
+
218
+
219
+ Number(5, unit=km) <5 km> with quantity=5, preserving "kilo" scale
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Key Design Decisions
225
+
226
+ ### Why Separate Exponent from Scale?
227
+
228
+ 1. **Algebraic Closure**: Exponent handles the pure math of base-power pairs without display concerns
229
+ 2. **Cross-Base Operations**: 2¹⁰ / 10³ returns a float because no single base can represent it
230
+ 3. **Nearest Resolution**: Scale can find the "closest" human-readable prefix for arbitrary values
231
+
232
+ ### Why Only Base 2 and 10?
233
+
234
+ These are the only bases with standardized prefix names:
235
+ - **Base 10**: SI prefixes (kilo, mega, giga, etc.)
236
+ - **Base 2**: IEC binary prefixes (kibi, mebi, gibi, etc.)
237
+
238
+ Other bases would require inventing new prefix names.
239
+
240
+ ### Why ScaleDescriptor?
241
+
242
+ Separates concerns:
243
+ - `Exponent`: Pure numeric computation
244
+ - `ScaleDescriptor`: Adds shorthand ("k") and alias ("kilo") for display
245
+ - `Scale`: Enum providing named access and class methods like `nearest()`
246
+
247
+ ---
248
+
249
+ ## Common Patterns
250
+
251
+ ### Creating Scaled Units
252
+
253
+ ```python
254
+ from ucon.core import Scale
255
+ from ucon import units
256
+
257
+ # Standard metric prefixes
258
+ km = Scale.kilo * units.meter
259
+ MHz = Scale.mega * units.hertz
260
+ ns = Scale.nano * units.second
261
+
262
+ # Binary prefixes (for information units)
263
+ KiB = Scale.kibi * units.byte
264
+ GiB = Scale.gibi * units.byte
265
+ ```
266
+
267
+ ### Scale Arithmetic in Practice
268
+
269
+ ```python
270
+ # Velocity: km/h involves scale
271
+ km = Scale.kilo * units.meter
272
+ speed = (km / units.hour)(100) # 100 km/h
273
+
274
+ # Energy: kJ
275
+ kJ = Scale.kilo * units.joule
276
+ energy = kJ(4.184) # 4.184 kJ = 1 kcal
277
+ ```
278
+
279
+ ### Folding Scale into Quantity
280
+
281
+ ```python
282
+ km = Scale.kilo * units.meter
283
+ distance = km(5)
284
+
285
+ # Get the scale factor
286
+ distance.unit.fold_scale() # 1000.0
287
+
288
+ # Canonical magnitude (quantity × scale)
289
+ distance._canonical_magnitude # 5000.0
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Testing Scale/Exponent Relationships
295
+
296
+ From `tests/ucon/test_core.py`:
297
+
298
+ ```python
299
+ def test_scale_multiplication_same_base(self):
300
+ self.assertEqual(Scale.kilo * Scale.kilo, Scale.mega)
301
+ self.assertEqual(Scale.kilo * Scale.milli, Scale.one)
302
+
303
+ def test_scale_division(self):
304
+ self.assertEqual(Scale.mega / Scale.kilo, Scale.kilo)
305
+ self.assertEqual(Scale.kilo / Scale.mega, Scale.milli)
306
+
307
+ def test_scale_exponentiation(self):
308
+ self.assertEqual(Scale.kilo ** 2, Scale.mega)
309
+ self.assertEqual(Scale.milli ** -1, Scale.kilo)
310
+ ```
311
+
312
+ From `tests/ucon/test_algebra.py`:
313
+
314
+ ```python
315
+ def test_exponent_multiplication(self):
316
+ kibibyte = Exponent(2, 10)
317
+ mebibyte = Exponent(2, 20)
318
+ product = kibibyte * mebibyte
319
+ self.assertEqual(product.power, 30) # 2³⁰
320
+
321
+ def test_exponent_division_same_base(self):
322
+ thousand = Exponent(10, 3)
323
+ thousandth = Exponent(10, -3)
324
+ ratio = thousand / thousandth
325
+ self.assertEqual(ratio.power, 6) # 10⁶
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Summary
331
+
332
+ | Class | Module | Purpose |
333
+ |-------|--------|---------|
334
+ | `Exponent` | `ucon.algebra` | Base-power arithmetic (10³, 2¹⁰) |
335
+ | `ScaleDescriptor` | `ucon.core` | Wraps Exponent with shorthand/alias |
336
+ | `Scale` | `ucon.core` | Enum of named prefixes with algebra |
337
+
338
+ The relationship flows downward:
339
+ - `Scale.kilo` → `ScaleDescriptor` → `Exponent(10, 3)`
340
+
341
+ Arithmetic flows upward:
342
+ - `Exponent` math → resolve to `Scale` via lookup or `nearest()`
343
+
344
+ This layered design provides:
345
+ - **Type safety**: Scale operations return Scale, not raw numbers
346
+ - **Algebraic closure**: `kilo * kilo = mega`, not just `1000000`
347
+ - **Human readability**: Quantities display with appropriate prefixes
@@ -23,7 +23,7 @@ setup(
23
23
  maintainer_email='withtwoemms@gmail.com',
24
24
  url='https://github.com/withtwoemms/ucon',
25
25
  classifiers=[
26
- 'Development Status :: 3 - Alpha',
26
+ 'Development Status :: 4 - Beta',
27
27
  'Intended Audience :: Developers',
28
28
  'Intended Audience :: Education',
29
29
  'Intended Audience :: Science/Research',
File without changes