ucon 0.3.5rc1__py3-none-any.whl → 0.4.0__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/quantity.py CHANGED
@@ -4,193 +4,14 @@
4
4
 
5
5
  """
6
6
  ucon.quantity
7
- ==========
7
+ =============
8
8
 
9
- Implements the **quantitative core** of the *ucon* system — the machinery that
10
- defines how numeric values are coupled with units and scales to represent
11
- physical quantities.
9
+ Re-exports :class:`Number` and :class:`Ratio` from :mod:`ucon.core`
10
+ for backward compatibility.
12
11
 
13
- Classes
14
- -------
15
- - :class:`Number` — Couples a numeric value with a unit and scale.
16
- - :class:`Ratio` — Represents a ratio between two :class:`Number` objects.
17
-
18
- Together, these classes allow full arithmetic, conversion, and introspection
19
- of physical quantities with explicit dimensional semantics.
12
+ These classes now live in :mod:`ucon.core` to enable the callable
13
+ unit syntax: ``meter(5)`` returns a ``Number``.
20
14
  """
21
- from dataclasses import dataclass
22
- from typing import Union
23
-
24
- from ucon import units
25
- from ucon.core import Unit, UnitProduct, Scale
26
-
27
-
28
- Quantifiable = Union['Number', 'Ratio']
29
-
30
- @dataclass
31
- class Number:
32
- """
33
- Represents a **numeric quantity** with an associated :class:`Unit` and :class:`Scale`.
34
-
35
- Combines magnitude, unit, and scale into a single, composable object that
36
- supports dimensional arithmetic and conversion:
37
-
38
- >>> from ucon import core, units
39
- >>> length = core.Number(unit=units.meter, quantity=5)
40
- >>> time = core.Number(unit=units.second, quantity=2)
41
- >>> speed = length / time
42
- >>> speed
43
- <2.5 (m/s)>
44
- """
45
- quantity: Union[float, int] = 1.0
46
- unit: Unit = units.none
47
-
48
- @property
49
- def value(self) -> float:
50
- """Return the numeric magnitude as-expressed (no scale folding).
51
-
52
- Scale lives in the unit expression (e.g. kJ, mL) and is NOT
53
- folded into the returned value. Use ``unit.fold_scale()`` on a
54
- UnitProduct when you need the base-unit-equivalent magnitude.
55
- """
56
- return round(self.quantity, 15)
57
-
58
- @property
59
- def _canonical_magnitude(self) -> float:
60
- """Quantity folded to base-unit scale (internal use for eq/div)."""
61
- if isinstance(self.unit, UnitProduct):
62
- return self.quantity * self.unit.fold_scale()
63
- return self.quantity
64
-
65
- def simplify(self):
66
- """Return a new Number expressed in base scale (Scale.one)."""
67
- raise NotImplementedError("Unit simplification requires ConversionGraph; coming soon.")
68
-
69
- def to(self, new_scale: Scale):
70
- raise NotImplementedError("Unit conversion requires ConversionGraph; coming soon.")
71
-
72
- def as_ratio(self):
73
- return Ratio(self)
74
-
75
- def __mul__(self, other: Quantifiable) -> 'Number':
76
- if isinstance(other, Ratio):
77
- other = other.evaluate()
78
-
79
- if not isinstance(other, Number):
80
- return NotImplemented
81
-
82
- return Number(
83
- quantity=self.quantity * other.quantity,
84
- unit=self.unit * other.unit,
85
- )
86
-
87
- def __truediv__(self, other: Quantifiable) -> "Number":
88
- # Allow dividing by a Ratio (interpret as its evaluated Number)
89
- if isinstance(other, Ratio):
90
- other = other.evaluate()
91
-
92
- if not isinstance(other, Number):
93
- raise TypeError("Cannot divide Number by non-Number/Ratio type: {type(other)}")
94
-
95
- # Symbolic quotient in the unit algebra
96
- unit_quot = self.unit / other.unit
97
-
98
- # --- Case 1: Dimensionless result ----------------------------------
99
- # If the net dimension is none, we want a pure scalar:
100
- # fold *all* scale factors into the numeric magnitude.
101
- if not unit_quot.dimension:
102
- num = self._canonical_magnitude # quantity × scale
103
- den = other._canonical_magnitude
104
- return Number(quantity=num / den, unit=units.none)
105
-
106
- # --- Case 2: Dimensionful result -----------------------------------
107
- # For "real" physical results like g/mL, m/s², etc., preserve the
108
- # user's chosen unit scales symbolically. Only divide the raw quantities.
109
- new_quantity = self.quantity / other.quantity
110
- return Number(quantity=new_quantity, unit=unit_quot)
111
-
112
- def __eq__(self, other: Quantifiable) -> bool:
113
- if not isinstance(other, (Number, Ratio)):
114
- raise TypeError(
115
- f"Cannot compare Number to non-Number/Ratio type: {type(other)}"
116
- )
117
-
118
- # If comparing with a Ratio, evaluate it to a Number
119
- if isinstance(other, Ratio):
120
- other = other.evaluate()
121
-
122
- # Dimensions must match
123
- if self.unit.dimension != other.unit.dimension:
124
- return False
125
-
126
- # Compare magnitudes, scale-adjusted
127
- if abs(self._canonical_magnitude - other._canonical_magnitude) >= 1e-12:
128
- return False
129
-
130
- return True
131
-
132
- def __repr__(self):
133
- if not self.unit.dimension:
134
- return f"<{self.quantity}>"
135
- return f"<{self.quantity} {self.unit.shorthand}>"
136
-
137
-
138
- # TODO -- consider using a dataclass
139
- class Ratio:
140
- """
141
- Represents a **ratio of two Numbers**, preserving their unit semantics.
142
-
143
- Useful for expressing physical relationships like efficiency, density,
144
- or dimensionless comparisons:
145
-
146
- >>> ratio = Ratio(length, time)
147
- >>> ratio.evaluate()
148
- <2.5 (m/s)>
149
- """
150
- def __init__(self, numerator: Number = Number(), denominator: Number = Number()):
151
- self.numerator = numerator
152
- self.denominator = denominator
153
-
154
- def reciprocal(self) -> 'Ratio':
155
- return Ratio(numerator=self.denominator, denominator=self.numerator)
156
-
157
- def evaluate(self) -> "Number":
158
- # Pure arithmetic, no scale normalization.
159
- numeric = self.numerator.quantity / self.denominator.quantity
160
-
161
- # Pure unit division, with UnitFactor preservation.
162
- unit = self.numerator.unit / self.denominator.unit
163
-
164
- # DO NOT normalize, DO NOT fold scale.
165
- return Number(quantity=numeric, unit=unit)
166
-
167
- def __mul__(self, another_ratio: 'Ratio') -> 'Ratio':
168
- if self.numerator.unit == another_ratio.denominator.unit:
169
- factor = self.numerator / another_ratio.denominator
170
- numerator, denominator = factor * another_ratio.numerator, self.denominator
171
- elif self.denominator.unit == another_ratio.numerator.unit:
172
- factor = another_ratio.numerator / self.denominator
173
- numerator, denominator = factor * self.numerator, another_ratio.denominator
174
- else:
175
- factor = Number()
176
- another_number = another_ratio.evaluate()
177
- numerator, denominator = self.numerator * another_number, self.denominator
178
- return Ratio(numerator=numerator, denominator=denominator)
179
-
180
- def __truediv__(self, another_ratio: 'Ratio') -> 'Ratio':
181
- return Ratio(
182
- numerator=self.numerator * another_ratio.denominator,
183
- denominator=self.denominator * another_ratio.numerator,
184
- )
185
-
186
- def __eq__(self, another_ratio: 'Ratio') -> bool:
187
- if isinstance(another_ratio, Ratio):
188
- return self.evaluate() == another_ratio.evaluate()
189
- elif isinstance(another_ratio, Number):
190
- return self.evaluate() == another_ratio
191
- else:
192
- raise ValueError(f'"{another_ratio}" is not a Ratio or Number. Comparison not possible.')
15
+ from ucon.core import Number, Ratio, Quantifiable
193
16
 
194
- def __repr__(self):
195
- # TODO -- resolve int/float inconsistency
196
- return f'{self.evaluate()}' if self.numerator == self.denominator else f'{self.numerator} / {self.denominator}'
17
+ __all__ = ['Number', 'Ratio', 'Quantifiable']
ucon/units.py CHANGED
@@ -35,40 +35,83 @@ none = Unit()
35
35
 
36
36
 
37
37
  # -- International System of Units (SI) --------------------------------
38
- ampere = Unit('I', 'amp', name='ampere', dimension=Dimension.current)
39
- becquerel = Unit('Bq', name='becquerel', dimension=Dimension.frequency)
40
- celsius = Unit('°C', name='celsius', dimension=Dimension.temperature)
41
- coulomb = Unit('C', name='coulomb', dimension=Dimension.charge)
42
- farad = Unit('F', name='farad', dimension=Dimension.capacitance)
43
- gram = Unit('g', 'G', name='gram', dimension=Dimension.mass)
44
- gray = Unit('Gy', name='gray', dimension=Dimension.energy)
45
- henry = Unit('H', name='henry', dimension=Dimension.inductance)
46
- hertz = Unit('Hz', name='hertz', dimension=Dimension.frequency)
47
- hour = Unit('h', 'H', name='hour', dimension=Dimension.time)
48
- joule = Unit('J', name='joule', dimension=Dimension.energy)
49
- joule_per_kelvin = Unit('J/K', name='joule_per_kelvin', dimension=Dimension.entropy)
50
- kelvin = Unit('K', name='kelvin', dimension=Dimension.temperature)
51
- liter = Unit('L', 'l', name='liter', dimension=Dimension.volume)
52
- lumen = Unit('lm', name='lumen', dimension=Dimension.luminous_intensity)
53
- lux = Unit('lx', name='lux', dimension=Dimension.illuminance)
54
- meter = Unit('m', 'M', name='meter', dimension=Dimension.length)
55
- mole = Unit('mol', 'n', name='mole', dimension=Dimension.amount_of_substance)
56
- newton = Unit('N', name='newton', dimension=Dimension.force)
57
- ohm = Unit('Ω', name='ohm', dimension=Dimension.resistance)
58
- pascal = Unit('Pa', name='pascal', dimension=Dimension.pressure)
59
- radian = Unit('rad', name='radian', dimension=Dimension.none)
60
- second = Unit('s', 'sec', name='second', dimension=Dimension.time)
61
- sievert = Unit('Sv', name='sievert', dimension=Dimension.energy)
62
- siemens = Unit('S', name='siemens', dimension=Dimension.conductance)
63
- steradian = Unit('sr', name='steradian', dimension=Dimension.none)
64
- tesla = Unit('T', name='tesla', dimension=Dimension.magnetic_flux_density)
65
- volt = Unit('V', name='volt', dimension=Dimension.voltage)
66
- watt = Unit('W', name='watt', dimension=Dimension.power)
67
- webers = Unit('Wb', name='weber', dimension=Dimension.magnetic_flux)
68
- webers_per_meter = Unit('Wb/m', name='webers_per_meter', dimension=Dimension.magnetic_permeability)
38
+ ampere = Unit(name='ampere', dimension=Dimension.current, aliases=('I', 'amp'))
39
+ becquerel = Unit(name='becquerel', dimension=Dimension.frequency, aliases=('Bq',))
40
+ celsius = Unit(name='celsius', dimension=Dimension.temperature, aliases=('°C', 'degC'))
41
+ coulomb = Unit(name='coulomb', dimension=Dimension.charge, aliases=('C',))
42
+ farad = Unit(name='farad', dimension=Dimension.capacitance, aliases=('F',))
43
+ gram = Unit(name='gram', dimension=Dimension.mass, aliases=('g',))
44
+ gray = Unit(name='gray', dimension=Dimension.energy, aliases=('Gy',))
45
+ henry = Unit(name='henry', dimension=Dimension.inductance, aliases=('H',))
46
+ hertz = Unit(name='hertz', dimension=Dimension.frequency, aliases=('Hz',))
47
+ joule = Unit(name='joule', dimension=Dimension.energy, aliases=('J',))
48
+ joule_per_kelvin = Unit(name='joule_per_kelvin', dimension=Dimension.entropy, aliases=('J/K',))
49
+ kelvin = Unit(name='kelvin', dimension=Dimension.temperature, aliases=('K',))
50
+ kilogram = Unit(name='kilogram', dimension=Dimension.mass, aliases=('kg',))
51
+ liter = Unit(name='liter', dimension=Dimension.volume, aliases=('L', 'l'))
52
+ lumen = Unit(name='lumen', dimension=Dimension.luminous_intensity, aliases=('lm',))
53
+ lux = Unit(name='lux', dimension=Dimension.illuminance, aliases=('lx',))
54
+ meter = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
55
+ mole = Unit(name='mole', dimension=Dimension.amount_of_substance, aliases=('mol', 'n'))
56
+ newton = Unit(name='newton', dimension=Dimension.force, aliases=('N',))
57
+ ohm = Unit(name='ohm', dimension=Dimension.resistance, aliases=('Ω',))
58
+ pascal = Unit(name='pascal', dimension=Dimension.pressure, aliases=('Pa',))
59
+ radian = Unit(name='radian', dimension=Dimension.none, aliases=('rad',))
60
+ siemens = Unit(name='siemens', dimension=Dimension.conductance, aliases=('S',))
61
+ sievert = Unit(name='sievert', dimension=Dimension.energy, aliases=('Sv',))
62
+ steradian = Unit(name='steradian', dimension=Dimension.none, aliases=('sr',))
63
+ tesla = Unit(name='tesla', dimension=Dimension.magnetic_flux_density, aliases=('T',))
64
+ volt = Unit(name='volt', dimension=Dimension.voltage, aliases=('V',))
65
+ watt = Unit(name='watt', dimension=Dimension.power, aliases=('W',))
66
+ weber = Unit(name='weber', dimension=Dimension.magnetic_flux, aliases=('Wb',))
67
+ webers_per_meter = Unit(name='webers_per_meter', dimension=Dimension.magnetic_permeability, aliases=('Wb/m',))
69
68
  # ----------------------------------------------------------------------
70
69
 
71
70
 
71
+ # -- Time Units --------------------------------------------------------
72
+ second = Unit(name='second', dimension=Dimension.time, aliases=('s', 'sec'))
73
+ minute = Unit(name='minute', dimension=Dimension.time, aliases=('min',))
74
+ hour = Unit(name='hour', dimension=Dimension.time, aliases=('h', 'hr'))
75
+ day = Unit(name='day', dimension=Dimension.time, aliases=('d',))
76
+ # ----------------------------------------------------------------------
77
+
78
+
79
+ # -- Imperial / US Customary Units -------------------------------------
80
+ # Length
81
+ foot = Unit(name='foot', dimension=Dimension.length, aliases=('ft',))
82
+ inch = Unit(name='inch', dimension=Dimension.length, aliases=('in',))
83
+ yard = Unit(name='yard', dimension=Dimension.length, aliases=('yd',))
84
+ mile = Unit(name='mile', dimension=Dimension.length, aliases=('mi',))
85
+
86
+ # Mass
87
+ pound = Unit(name='pound', dimension=Dimension.mass, aliases=('lb', 'lbs'))
88
+ ounce = Unit(name='ounce', dimension=Dimension.mass, aliases=('oz',))
89
+
90
+ # Temperature
91
+ fahrenheit = Unit(name='fahrenheit', dimension=Dimension.temperature, aliases=('°F', 'degF'))
92
+
93
+ # Volume
94
+ gallon = Unit(name='gallon', dimension=Dimension.volume, aliases=('gal',))
95
+
96
+ # Energy
97
+ calorie = Unit(name='calorie', dimension=Dimension.energy, aliases=('cal',))
98
+ btu = Unit(name='btu', dimension=Dimension.energy, aliases=('BTU',))
99
+
100
+ # Power
101
+ horsepower = Unit(name='horsepower', dimension=Dimension.power, aliases=('hp',))
102
+ # ----------------------------------------------------------------------
103
+
104
+
105
+ # -- Information Units -------------------------------------------------
106
+ bit = Unit(name='bit', dimension=Dimension.information, aliases=('b',))
107
+ byte = Unit(name='byte', dimension=Dimension.information, aliases=('B',))
108
+ # ----------------------------------------------------------------------
109
+
110
+
111
+ # Backward compatibility alias
112
+ webers = weber
113
+
114
+
72
115
  def have(name: str) -> bool:
73
116
  assert name, "Must provide a unit name to check"
74
117
  assert isinstance(name, str), "Unit name must be a string"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ucon
3
- Version: 0.3.5rc1
3
+ Version: 0.4.0
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,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.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 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
+ | **`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. |
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.core` | Combines a numeric quantity with a unit; the primary measurable type. | Performing arithmetic with units; representing physical quantities like 5 m/s. |
79
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). |
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.). | |
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.). |
81
85
 
82
86
  ### Under the Hood
83
87
 
84
88
  `ucon` models unit math through a hierarchy where each layer builds on the last:
85
89
 
86
- <img src=https://gist.githubusercontent.com/withtwoemms/429d2ca1f979865aa80a2658bf9efa32/raw/0c704737a52b9e4a87cda5c839e9aa40f7e5bb48/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/>
87
91
 
88
92
  ## Why `ucon`?
89
93
 
@@ -91,24 +95,22 @@ Python already has mature libraries for handling units and physical quantities
91
95
 
92
96
  | Library | Focus | Limitation |
93
97
  | --------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
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. |
98
+ | **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
99
  | **SymPy** | Symbolic algebra and simplification of unit expressions | Excellent for symbolic reasoning, but not designed for runtime validation, conversion, or serialization. |
96
100
  | **Unum** | Unit-aware arithmetic and unit propagation | Tracks units through arithmetic but lacks explicit dimensional algebra, conversion taxonomy, or runtime introspection. |
97
101
 
98
102
  Together, these tools can _use_ units, but none can explicitly represent and verify the relationships between units and dimensions.
99
103
 
100
- Thats the gap `ucon` fills.
104
+ That's the gap `ucon` fills.
101
105
 
102
106
  It treats units, dimensions, and scales as first-class objects and builds a composable algebra around them.
103
107
  This allows you to:
104
108
  - Represent dimensional meaning explicitly (`Dimension`, `Vector`);
105
109
  - 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
110
  - Extend the system with custom unit registries and conversion families.
109
111
 
110
112
  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:
113
+ `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
114
  ```python
113
115
  from ucon import Number, units
114
116
 
@@ -136,7 +138,8 @@ This sort of dimensional analysis:
136
138
  ```
137
139
  becomes straightforward when you define a measurement:
138
140
  ```python
139
- from ucon import Number, Scale, Units, Ratio
141
+ from ucon import Number, Scale, units
142
+ from ucon.quantity import Ratio
140
143
 
141
144
  # Two milliliters of bromine
142
145
  mL = Scale.milli * units.liter
@@ -149,27 +152,52 @@ bromine_density = Ratio(
149
152
  )
150
153
 
151
154
  # Multiply to find mass
152
- grams_bromine = two_mL_bromine * bromine_density
153
- print(grams_bromine) # <6.238 gram>
155
+ grams_bromine = bromine_density.evaluate() * two_mL_bromine
156
+ print(grams_bromine) # <6.238 g>
154
157
  ```
155
158
 
156
- Scale conversion is automatic and precise:
159
+ Scale prefixes compose naturally:
160
+ ```python
161
+ km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
162
+ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
163
+
164
+ print(km.shorthand) # 'km'
165
+ print(mg.shorthand) # 'mg'
166
+
167
+ # Scale arithmetic
168
+ print(km.fold_scale()) # 1000.0
169
+ print(mg.fold_scale()) # 0.001
170
+ ```
157
171
 
172
+ Units are callable for ergonomic quantity construction:
158
173
  ```python
159
- grams_bromine.to(Scale.milli) # <6238.0 milligram>
160
- grams_bromine.to(Scale.kibi) # <0.006091796875 kibigram>
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>
161
189
  ```
162
190
 
163
191
  ---
164
192
 
165
193
  ## Roadmap Highlights
166
194
 
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 |
195
+ | Version | Theme | Focus | Status |
196
+ |----------|-------|--------|--------|
197
+ | **0.3.5** | Dimensional Algebra | Unit/Scale separation, `UnitFactor`, `UnitProduct` | ✅ Complete |
198
+ | [**0.4.x**](https://github.com/withtwoemms/ucon/milestone/2) | Conversion System | `ConversionGraph`, `Number.to()`, callable units | 🚧 In Progress |
199
+ | [**0.6.x**](https://github.com/withtwoemms/ucon/milestone/4) | Nonlinear / Specialized Units | Decibel, Percent, pH | ⏳ Planned |
200
+ | [**0.8.x**](https://github.com/withtwoemms/ucon/milestone/6) | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
173
201
 
174
202
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
175
203
 
@@ -182,13 +210,13 @@ Ensure `nox` is installed.
182
210
  ```
183
211
  pip install -r requirements.txt
184
212
  ```
185
- Then run the full test suite (agains all supported python versions) before committing:
213
+ Then run the full test suite (against all supported python versions) before committing:
186
214
 
187
215
  ```bash
188
216
  nox -s test
189
217
  ```
190
218
  ---
191
219
 
192
- > If it can be measured, it can be represented.
220
+ > "If it can be measured, it can be represented.
193
221
  If it can be represented, it can be validated.
194
- If it can be validated, it can be trusted.”
222
+ If it can be validated, it can be trusted."
@@ -0,0 +1,21 @@
1
+ tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
2
+ tests/ucon/test_algebra.py,sha256=Esm38M1QBNsV7vdfoFgqRUluyvWX7yccB0RwZXk4DpA,8433
3
+ tests/ucon/test_core.py,sha256=NqmyKMkrF3T4ekucsFzVsxbkErdB6assbkhBAYH2fug,32664
4
+ tests/ucon/test_quantity.py,sha256=qJ-dXOPgIp0SKelASu45IC6KquTjlcRwHVkSKicsrXw,22754
5
+ tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
6
+ tests/ucon/conversion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ tests/ucon/conversion/test_graph.py,sha256=R3u-0FCpgffoUXvJmT7aBJCUDUpzmBYROh5I0WLHA_Y,7256
8
+ tests/ucon/conversion/test_map.py,sha256=2_bxXCuga-lTGxU5rGferiziJmjzrltksA-6JJseEQ0,5359
9
+ ucon/__init__.py,sha256=jaFcNFZC5gxHKzM8OkS1pLxltp6ToLRVpuuhJQY9FKQ,2000
10
+ ucon/algebra.py,sha256=wGl4jJVMd8SXQ4sYDBOxV00ymAzRWfDhea1o4t2kVp4,7482
11
+ ucon/core.py,sha256=o3Q4posUOYoIhQVHl6bANCIcGKgGOpNZsnqGZw9ujYk,41523
12
+ ucon/graph.py,sha256=lgueyVdBI73pgqsydwRtOY9sxt02G-gy8-Lf7993RNw,14224
13
+ ucon/maps.py,sha256=yyZ7RqnohO2joTUvvKh40in7E6SKMQIQ8jkECO0-_NA,4753
14
+ ucon/quantity.py,sha256=GBxZ_96nocx-8F-usNWGbPvWHRhRgdZzqfH9Sx69iC4,465
15
+ ucon/units.py,sha256=PJFAqUoEq_0--Zo7JDHezZBPHPbAlS_5eArIVCLemxA,5852
16
+ ucon-0.4.0.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
17
+ ucon-0.4.0.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
18
+ ucon-0.4.0.dist-info/METADATA,sha256=_hZdjrgY1lvorBac-JPQQVzNVuNet-QY2zPP0G8-hik,12349
19
+ ucon-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ ucon-0.4.0.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
21
+ ucon-0.4.0.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=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,,
File without changes