ucon 0.5.1__py3-none-any.whl → 0.5.2__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/units.py CHANGED
@@ -28,7 +28,7 @@ Notes
28
28
  The design allows for future extensibility: users can register their own units,
29
29
  systems, or aliases dynamically, without modifying the core definitions.
30
30
  """
31
- from ucon.core import Dimension, Unit
31
+ from ucon.core import Dimension, Unit, UnitSystem
32
32
 
33
33
 
34
34
  none = Unit()
@@ -49,6 +49,7 @@ joule_per_kelvin = Unit(name='joule_per_kelvin', dimension=Dimension.entropy, al
49
49
  kelvin = Unit(name='kelvin', dimension=Dimension.temperature, aliases=('K',))
50
50
  kilogram = Unit(name='kilogram', dimension=Dimension.mass, aliases=('kg',))
51
51
  liter = Unit(name='liter', dimension=Dimension.volume, aliases=('L', 'l'))
52
+ candela = Unit(name='candela', dimension=Dimension.luminous_intensity, aliases=('cd',))
52
53
  lumen = Unit(name='lumen', dimension=Dimension.luminous_intensity, aliases=('lm',))
53
54
  lux = Unit(name='lux', dimension=Dimension.illuminance, aliases=('lx',))
54
55
  meter = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
@@ -141,6 +142,32 @@ basis_point = Unit(name='basis_point', dimension=Dimension.ratio, aliases=('bp',
141
142
  webers = weber
142
143
 
143
144
 
145
+ # -- Predefined Unit Systems -----------------------------------------------
146
+ si = UnitSystem(
147
+ name="SI",
148
+ bases={
149
+ Dimension.length: meter,
150
+ Dimension.mass: kilogram,
151
+ Dimension.time: second,
152
+ Dimension.temperature: kelvin,
153
+ Dimension.current: ampere,
154
+ Dimension.amount_of_substance: mole,
155
+ Dimension.luminous_intensity: candela,
156
+ }
157
+ )
158
+
159
+ imperial = UnitSystem(
160
+ name="Imperial",
161
+ bases={
162
+ Dimension.length: foot,
163
+ Dimension.mass: pound,
164
+ Dimension.time: second,
165
+ Dimension.temperature: fahrenheit,
166
+ }
167
+ )
168
+ # --------------------------------------------------------------------------
169
+
170
+
144
171
  def have(name: str) -> bool:
145
172
  assert name, "Must provide a unit name to check"
146
173
  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.5.1
3
+ Version: 0.5.2
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
@@ -92,6 +92,9 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
92
92
  | **`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). |
93
93
  | **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
94
94
  | **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
95
+ | **`UnitSystem`** | `ucon.core` | Named mapping from dimensions to base units (e.g., SI, Imperial). | Defining coherent unit systems; grouping base units by dimension. |
96
+ | **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
97
+ | **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
95
98
  | **`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.). |
96
99
 
97
100
  ### Under the Hood
@@ -235,6 +238,83 @@ length_ft = length.to(units.foot)
235
238
  print(length_ft) # <4.048... ± 0.0164... ft>
236
239
  ```
237
240
 
241
+ Unit systems and basis transforms enable conversions between incompatible dimensional structures.
242
+ This goes beyond simple unit conversion (meter → foot) into structural transformation:
243
+
244
+ ```python
245
+ from fractions import Fraction
246
+ from ucon import BasisTransform, Dimension, Unit, UnitSystem, units
247
+ from ucon.graph import ConversionGraph
248
+ from ucon.maps import LinearMap
249
+
250
+ # The realm of Valdris has three fundamental dimensions:
251
+ # - Aether (A): magical energy substrate
252
+ # - Resonance (R): vibrational frequency of magic
253
+ # - Substance (S): physical matter
254
+ #
255
+ # These combine into SI dimensions via a transformation matrix:
256
+ #
257
+ # | L | | 2 0 0 | | A |
258
+ # | M | = | 1 0 1 | × | R |
259
+ # | T | |-2 -1 0 | | S |
260
+ #
261
+ # Reading the columns:
262
+ # - 1 aether contributes: L², M, T⁻² (energy-like)
263
+ # - 1 resonance contributes: T⁻¹ (frequency-like)
264
+ # - 1 substance contributes: M (mass-like)
265
+
266
+ # Fantasy base units
267
+ mote = Unit(name='mote', dimension=Dimension.energy, aliases=('mt',))
268
+ chime = Unit(name='chime', dimension=Dimension.frequency, aliases=('ch',))
269
+ ite = Unit(name='ite', dimension=Dimension.mass, aliases=('it',))
270
+
271
+ valdris = UnitSystem(
272
+ name="Valdris",
273
+ bases={
274
+ Dimension.energy: mote,
275
+ Dimension.frequency: chime,
276
+ Dimension.mass: ite,
277
+ }
278
+ )
279
+
280
+ # The basis transform encodes how Valdris dimensions compose into SI
281
+ valdris_to_si = BasisTransform(
282
+ src=valdris,
283
+ dst=units.si,
284
+ src_dimensions=(Dimension.energy, Dimension.frequency, Dimension.mass),
285
+ dst_dimensions=(Dimension.energy, Dimension.frequency, Dimension.mass),
286
+ matrix=(
287
+ (2, 0, 0), # energy: 2 × aether
288
+ (1, 0, 1), # frequency: aether + substance
289
+ (-2, -1, 0), # mass: -2×aether - resonance
290
+ ),
291
+ )
292
+
293
+ # Physical calibration: how many SI units per fantasy unit
294
+ graph = ConversionGraph()
295
+ graph.connect_systems(
296
+ basis_transform=valdris_to_si,
297
+ edges={
298
+ (mote, units.joule): LinearMap(42), # 1 mote = 42 J
299
+ (chime, units.hertz): LinearMap(7), # 1 chime = 7 Hz
300
+ (ite, units.kilogram): LinearMap(Fraction(1, 2)), # 1 ite = 0.5 kg
301
+ }
302
+ )
303
+
304
+ # Game engine converts between physics systems
305
+ energy_map = graph.convert(src=mote, dst=units.joule)
306
+ energy_map(10) # 420 joules from 10 motes
307
+
308
+ # Inverse: display real-world values in game units
309
+ joule_to_mote = graph.convert(src=units.joule, dst=mote)
310
+ joule_to_mote(420) # 10 motes
311
+
312
+ # The transform is invertible with exact Fraction arithmetic
313
+ valdris_to_si.is_invertible # True
314
+ ```
315
+
316
+ This enables fantasy game physics, or any field where the dimensional structure differs from SI.
317
+
238
318
  ---
239
319
 
240
320
  ## Roadmap Highlights
@@ -245,8 +325,9 @@ print(length_ft) # <4.048... ± 0.0164... ft>
245
325
  | **0.4.x** | Conversion System | `ConversionGraph`, `Number.to()`, callable units | ✅ Complete |
246
326
  | **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
247
327
  | **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
248
- | **0.5.x** | Unit Systems | `BasisMap`, `UnitSystem` | 🚧 In Progress |
249
- | **0.7.x** | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
328
+ | **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | Complete |
329
+ | **0.6.x** | Pydantic Integration | Type-safe quantity validation | ⏳ Planned |
330
+ | **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
250
331
 
251
332
  See full roadmap: [ROADMAP.md](./ROADMAP.md)
252
333
 
@@ -1,24 +1,29 @@
1
1
  tests/ucon/__init__.py,sha256=9BAHYTs27Ed3VgqiMUH4XtVttAmOPgK0Zvj-dUNo7D8,119
2
2
  tests/ucon/test_algebra.py,sha256=NnoQOSMW8NJlOTnbr3M_5epvnDPXpVTgO21L2LcytRY,8503
3
+ tests/ucon/test_basis_transform.py,sha256=P7ccr9wgDPCwCmsj4dceu3V0A72qFbTavjzm9kB3xP8,16710
3
4
  tests/ucon/test_core.py,sha256=bmwSRWPlhwossy5NJ9rcPWujFmzBBPOeZzPAzN1acFg,32631
4
5
  tests/ucon/test_default_graph_conversions.py,sha256=rkcDcSV1_kZeuPf4ModHDpgfkOPZS32xcKq7KPDRN-0,15760
5
6
  tests/ucon/test_dimensionless_units.py,sha256=K6BrIPOFL9IO_ksR8t_oJUXmjTgqBUzMdgaV-hZc52w,8410
7
+ tests/ucon/test_graph_basis_transform.py,sha256=5-WglJaR1N_mJlqR6i8NuxLJ_FASqb5a8WoO_177smU,8249
6
8
  tests/ucon/test_quantity.py,sha256=md5nbmy0u2cFBdqNeu-ROhoj29vYrIlGm_AjlmCttgc,24519
9
+ tests/ucon/test_rebased_unit.py,sha256=n2mksEYSJ8UJJXXwlgaLKg3THaf7_LKzWB7kwjoaXEU,5150
7
10
  tests/ucon/test_uncertainty.py,sha256=KkJw2dJR0EToxPpBN24x735jr9fv6a2myxjvhOH4MPU,9649
11
+ tests/ucon/test_unit_system.py,sha256=gRc3fMvo9ded1tBUQWLKpcbpBepMAb7gPu8XvFzQZaM,5860
8
12
  tests/ucon/test_units.py,sha256=SILymDtDNDyxEhkYQubrfkakKCMexwEwjyHfhrkDrMI,869
13
+ tests/ucon/test_vector_fraction.py,sha256=fTgxlV9aSP15sA4gATTXBzIDbtKXwWqG1Ip0o-V2B4Y,6369
9
14
  tests/ucon/conversion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
15
  tests/ucon/conversion/test_graph.py,sha256=fs0aP6qNf8eE1uI7SoGSCW2XAkHYb7T9aaI-kzmO02c,16955
11
16
  tests/ucon/conversion/test_map.py,sha256=DVFQ3xwp16Nuy9EtZRjKlWbkXfRUcM1mOzFrS4HhOaw,13886
12
- ucon/__init__.py,sha256=jaFcNFZC5gxHKzM8OkS1pLxltp6ToLRVpuuhJQY9FKQ,2000
13
- ucon/algebra.py,sha256=4JiT_SHHep86Sv3tVkgKsRY95lBRASMkyH4vOUA-gfM,7459
14
- ucon/core.py,sha256=nuDmSuGiG3xUW_kRpOWu9FDhNmKE0aJPwpj30lMdrgo,49464
15
- ucon/graph.py,sha256=lPoYSvHNGBZxeZ-4dyZIu2OS5R1JTo0qPZ9wd0vg-s4,15566
17
+ ucon/__init__.py,sha256=M_sijIUYPvU87Jtnq_O2X7TS4x9RW1LHZJ8bbm3gfk0,2255
18
+ ucon/algebra.py,sha256=6QrPyD23L93XSrnIORcYEx2CLDv4WDcrh6H_hxeeOus,8668
19
+ ucon/core.py,sha256=j2Xw73-Xuh0CkaUYEv5ljsxjt-XdthiH-EbqUBgG1a8,63139
20
+ ucon/graph.py,sha256=Ec0Q2QiAGUm2RaxrKnpFHtwpNvTf4PYbvo62BWtGJG8,21159
16
21
  ucon/maps.py,sha256=tWP4ayYCEazJzf81EP1_fmtADhg18D1eHldudAMEY0U,5460
17
22
  ucon/quantity.py,sha256=GBxZ_96nocx-8F-usNWGbPvWHRhRgdZzqfH9Sx69iC4,465
18
- ucon/units.py,sha256=u1ILwGllzNiwGLadlg5jguKPyFV1u-CZSUMgUDWTen4,7509
19
- ucon-0.5.1.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
20
- ucon-0.5.1.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
21
- ucon-0.5.1.dist-info/METADATA,sha256=dkjXacaxxZbFq9vIBfJJ-2koy03T9Ozf_nimaNm2mwQ,13554
22
- ucon-0.5.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
- ucon-0.5.1.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
24
- ucon-0.5.1.dist-info/RECORD,,
23
+ ucon/units.py,sha256=MWCJhicK6jQb71fREyW_HSfGFKL8KEQej2JnySL_MjE,8285
24
+ ucon-0.5.2.dist-info/licenses/LICENSE,sha256=LtimSYBSw1L_X6n1-VEdZRdwuROzPumrMUNX21asFuI,11356
25
+ ucon-0.5.2.dist-info/licenses/NOTICE,sha256=bh4fBOItio3kM4hSNYhqfFpcaAvOoixjD7Du8im-sYA,1079
26
+ ucon-0.5.2.dist-info/METADATA,sha256=CTRgYp67awjnjy7ZzKL2opd81T2RK1feQ6AXMnXmSj4,17200
27
+ ucon-0.5.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
28
+ ucon-0.5.2.dist-info/top_level.txt,sha256=zZYRJiQrVUtN32ziJD2YEq7ClSvDmVYHYy5ArRAZGxI,11
29
+ ucon-0.5.2.dist-info/RECORD,,
File without changes