python-units 0.2.0__tar.gz → 0.4.0__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 (36) hide show
  1. python_units-0.4.0/PKG-INFO +470 -0
  2. python_units-0.4.0/README.md +444 -0
  3. {python_units-0.2.0 → python_units-0.4.0}/pyproject.toml +1 -1
  4. {python_units-0.2.0 → python_units-0.4.0}/src/api/public.py +51 -0
  5. {python_units-0.2.0 → python_units-0.4.0}/src/api/si.py +51 -1
  6. {python_units-0.2.0 → python_units-0.4.0}/src/core/__init__.py +8 -0
  7. python_units-0.4.0/src/core/deprecations.py +62 -0
  8. python_units-0.4.0/src/core/quantity.py +469 -0
  9. python_units-0.4.0/src/core/unit_definitions.py +460 -0
  10. {python_units-0.2.0 → python_units-0.4.0}/src/models/dimension.py +27 -1
  11. python_units-0.4.0/src/python_units.egg-info/PKG-INFO +470 -0
  12. {python_units-0.2.0 → python_units-0.4.0}/src/python_units.egg-info/SOURCES.txt +1 -0
  13. {python_units-0.2.0 → python_units-0.4.0}/src/units/unit.py +16 -0
  14. python_units-0.2.0/PKG-INFO +0 -279
  15. python_units-0.2.0/README.md +0 -253
  16. python_units-0.2.0/src/core/quantity.py +0 -216
  17. python_units-0.2.0/src/core/unit_definitions.py +0 -215
  18. python_units-0.2.0/src/python_units.egg-info/PKG-INFO +0 -279
  19. {python_units-0.2.0 → python_units-0.4.0}/LICENSE +0 -0
  20. {python_units-0.2.0 → python_units-0.4.0}/setup.cfg +0 -0
  21. {python_units-0.2.0 → python_units-0.4.0}/setup.py +0 -0
  22. {python_units-0.2.0 → python_units-0.4.0}/src/adapters/__init__.py +0 -0
  23. {python_units-0.2.0 → python_units-0.4.0}/src/api/__init__.py +0 -0
  24. {python_units-0.2.0 → python_units-0.4.0}/src/core/errors.py +0 -0
  25. {python_units-0.2.0 → python_units-0.4.0}/src/models/__init__.py +0 -0
  26. {python_units-0.2.0 → python_units-0.4.0}/src/python_units.egg-info/dependency_links.txt +0 -0
  27. {python_units-0.2.0 → python_units-0.4.0}/src/python_units.egg-info/requires.txt +0 -0
  28. {python_units-0.2.0 → python_units-0.4.0}/src/python_units.egg-info/top_level.txt +0 -0
  29. {python_units-0.2.0 → python_units-0.4.0}/src/services/__init__.py +0 -0
  30. {python_units-0.2.0 → python_units-0.4.0}/src/units/__init__.py +0 -0
  31. {python_units-0.2.0 → python_units-0.4.0}/src/units/dimension.py +0 -0
  32. {python_units-0.2.0 → python_units-0.4.0}/src/units/errors.py +0 -0
  33. {python_units-0.2.0 → python_units-0.4.0}/src/units/quantity.py +0 -0
  34. {python_units-0.2.0 → python_units-0.4.0}/src/units/si.py +0 -0
  35. {python_units-0.2.0 → python_units-0.4.0}/src/utils/__init__.py +0 -0
  36. {python_units-0.2.0 → python_units-0.4.0}/src/utils/numbers.py +0 -0
@@ -0,0 +1,470 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-units
3
+ Version: 0.4.0
4
+ Summary: Python library to represent numbers with units
5
+ Author-email: "Paul K. Korir, PhD" <paul.korir@gmail.com>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/sci2pro/python-units
8
+ Keywords: units
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: dev
20
+ Requires-Dist: build; extra == "dev"
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+ Requires-Dist: tox; extra == "dev"
24
+ Requires-Dist: twine; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # units
28
+
29
+ [![badge.fury.io](https://badge.fury.io/py/python-units.svg)](https://badge.fury.io/py/python-units)
30
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://pypi.org/project/python-units/)
31
+ [![Coverage 92%](https://img.shields.io/badge/coverage-92%25-brightgreen.svg)](/Users/paulkorir/PycharmProjects/python-units/tests/unit/test_units.py)
32
+
33
+ # The Price of Unitless Arithmetic
34
+
35
+ On September 23, 1999, flight controllers expected NASA's Mars Climate Orbiter
36
+ to pass behind Mars, fire its engine, and come back into radio contact after
37
+ orbit insertion. It never came back. When engineers reviewed the final hours of
38
+ flight data, the trajectory was not where the navigation system thought it was:
39
+ the spacecraft had approached Mars far lower than planned. The investigation
40
+ traced the loss to a unit boundary that software had failed to defend. One side
41
+ of the system handled "small forces" data in English units; the navigation side
42
+ expected metric units.
43
+
44
+ That is the kind of bug this package is meant to stop. Without units, the
45
+ mistake is just arithmetic:
46
+
47
+ ```python
48
+ # A navigation routine expects impulse in newton-seconds.
49
+ expected_impulse_ns = 120
50
+
51
+ # A supplier routine accidentally sends a value in a different force unit.
52
+ # The number is still just a number, so Python accepts it.
53
+ reported_impulse_other_units = 120
54
+
55
+ trajectory_impulse = expected_impulse_ns + reported_impulse_other_units
56
+ print(trajectory_impulse) # 240
57
+ ```
58
+
59
+ There is nothing in `240` that tells you a spacecraft trajectory may now be
60
+ wrong. With units attached, the mismatch stops at the boundary:
61
+
62
+ ```python
63
+ from units import CustomUnitBase
64
+ from units.dimension import DimensionSystem
65
+ from units.si import newton, second
66
+
67
+ class EnglishImpulseUnit(CustomUnitBase):
68
+ dimension_system = DimensionSystem("english-impulse", ("lbf_s",))
69
+
70
+ pound_force_second = EnglishImpulseUnit.define("lbf_s")
71
+
72
+ expected_impulse = 120 * newton * second
73
+ reported_impulse = 120 * pound_force_second
74
+
75
+ trajectory_impulse = expected_impulse + reported_impulse
76
+ # UnitCompatibilityError: units mismatch: m·kg·s^-1 and lbf_s
77
+ ```
78
+
79
+ That failure is the feature. A bug that would otherwise move through a program
80
+ as an ordinary number is stopped before it contaminates mission-critical
81
+ calculations.
82
+
83
+ Background: NASA/JPL describe the Mars Climate Orbiter loss as a navigation
84
+ error caused by a failure to translate English units to metric, sending the
85
+ spacecraft too close to Mars.
86
+
87
+ Source: https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
88
+
89
+ # About
90
+
91
+ `python-units` is a Python package for unit-aware arithmetic. It provides:
92
+ - a `Quantity` type that combines numeric values with unit information
93
+ - a registry of SI base and derived units
94
+ - algebraic unit manipulation and compatibility checks
95
+ - explicit multiplicative conversions between compatible units
96
+ - a public API that prioritizes scalar-by-unit construction and SI unit imports
97
+ - a migration path from the legacy `Unit` constructor and compatibility helpers
98
+ - a Python 3-only codebase with no Python 2 compatibility shims
99
+ - a project structure that separates public API, core logic, data models, and utilities
100
+ - comprehensive unit tests and documentation
101
+
102
+ Supported Python versions: 3.10+
103
+
104
+ Python 2 is not supported.
105
+
106
+ Project layout:
107
+
108
+ - public facade: `src/units`
109
+ - API exports: `src/api`
110
+ - business logic: `src/core`
111
+ - data models: `src/models`
112
+ - utilities: `src/utils`
113
+ - tests: `tests/unit` and `tests/integration`
114
+
115
+ Preferred API:
116
+
117
+ ```python
118
+ from units import Quantity
119
+ from units.si import metre, second, newton
120
+
121
+ distance = 10 * metre
122
+ time = 2 * second
123
+ speed = distance / time
124
+ force = 5 * newton
125
+ print(distance)
126
+ print(speed)
127
+ print(force)
128
+ ```
129
+
130
+ The preferred construction style is scalar-by-unit multiplication:
131
+
132
+ ```python
133
+ from units.si import metre, second
134
+
135
+ length = 3 * metre
136
+ time = 2 * second
137
+ speed = length / time
138
+ volume = 5 * metre ** 3
139
+ ```
140
+
141
+ Because `**` binds more tightly than `*`, `5 * metre ** 3` is interpreted as
142
+ `5 * (metre ** 3)`, which is the intended geometric-unit behavior.
143
+
144
+ The explicit constructor remains supported and is still the right low-level form
145
+ when you want to be fully explicit:
146
+
147
+ ```python
148
+ from units import Quantity
149
+ from units.si import metre
150
+
151
+ length = Quantity(3, metre)
152
+ ```
153
+
154
+ Legacy API compatibility:
155
+
156
+ ```python
157
+ import units as u
158
+ print(u.Unit(1, u.metre))
159
+ ```
160
+
161
+ The legacy `Unit` constructor remains available as a compatibility alias for
162
+ `Quantity` during the migration period. It is deprecated and scheduled for
163
+ removal in `1.0.0`, but it remains a true alias until then so existing type
164
+ checks keep working. New code should prefer `from units import Quantity` and
165
+ `from units.si import ...`.
166
+
167
+ The package is Python 3-only. Python 2 compatibility behavior is not part of the
168
+ supported interface.
169
+
170
+ # Migration guide
171
+
172
+ Old style:
173
+
174
+ ```python
175
+ import units as u
176
+ distance = u.Unit(3, u.metre)
177
+ time = u.Unit(2, u.second)
178
+ speed = distance / time
179
+ ```
180
+
181
+ New style:
182
+
183
+ ```python
184
+ from units.si import metre, second
185
+
186
+ distance = 3 * metre
187
+ time = 2 * second
188
+ speed = distance / time
189
+ volume = 5 * metre ** 3
190
+ ```
191
+
192
+ Still supported when you want the fully explicit constructor form:
193
+
194
+ ```python
195
+ from units import Quantity
196
+ from units.si import metre, second
197
+
198
+ distance = Quantity(3, metre)
199
+ time = Quantity(2, second)
200
+ speed = distance / time
201
+ ```
202
+
203
+ # Public API
204
+
205
+ Stable top-level imports:
206
+
207
+ * `Quantity`
208
+ * `Unit` (compatibility alias for `Quantity`)
209
+ * `convert`
210
+ * `value`
211
+ * `unit`
212
+ * `multiplier`
213
+ * `UnitsError`, `InvalidUnitError`, `InvalidValueError`,
214
+ `UnitCompatibilityError`, `UnitOperandError`
215
+
216
+ Canonical unit imports:
217
+
218
+ * `from units.si import metre, second, newton`
219
+ * prefixed and scaled units such as `kilometre`, `centimetre`, `gram`,
220
+ `minute`, `hour`, `kilowatt`, and `millivolt`
221
+
222
+ Legacy compatibility helpers:
223
+
224
+ * `Unit`
225
+ * `long_quantity`
226
+ * `int_unit`
227
+ * `float_unit`
228
+ * `long_unit`
229
+ * `complex_unit`
230
+
231
+ These names remain available during the migration period and emit
232
+ `DeprecationWarning` when called. `Unit` remains a true alias for `Quantity` and
233
+ does not emit a call-time warning, because preserving `Unit is Quantity` is part
234
+ of the pre-`1.0.0` compatibility contract. New code should prefer `Quantity`,
235
+ scalar-by-unit construction, and the `*_quantity` conversion helpers. The
236
+ deprecated compatibility paths are scheduled for removal in `1.0.0`.
237
+
238
+ # Notes on semantics
239
+
240
+ * Addition and subtraction require identical units.
241
+ * Multiplication and division combine units algebraically.
242
+ * Explicit scale-only conversions are available through `quantity.to(unit)` and
243
+ `convert(quantity, unit)`.
244
+ * Integer powers of units and unit-bearing quantities are supported.
245
+ * Unitless quantities are supported explicitly.
246
+ * Affine conversions, such as `degree_celcius <-> kelvin`, are intentionally not
247
+ implemented yet.
248
+ * The core quantity model allows signed values. Domain-specific constraints such
249
+ as non-negative lengths should be enforced by higher-level types or validators.
250
+
251
+ # Conversion foundations
252
+
253
+ `0.4.0` adds explicit multiplicative conversions. Conversion never happens
254
+ silently during addition or subtraction; you choose the target unit.
255
+
256
+ ```python
257
+ from units import convert, multiplier, unit, value
258
+ from units.si import gram, hour, kilogram, kilometre, metre, minute, second
259
+
260
+ distance = 1.5 * kilometre
261
+ print(distance.to(metre)) # 1500 m
262
+ print(convert(2500 * metre, kilometre)) # 2.5 km
263
+
264
+ duration = 2 * hour
265
+ print(duration.to(minute)) # 120 min
266
+ print((1500 * gram).to(kilogram)) # 1.5 kg
267
+
268
+ speed = (72 * kilometre) / (2 * hour)
269
+ print(speed) # 10.0 m·s^-1
270
+
271
+ print(value(distance)) # 1.5
272
+ print(unit(distance)) # km
273
+ print(multiplier(kilometre)) # 1000.0
274
+ ```
275
+
276
+ The conversion model is scale-only in this release. Celsius is a named
277
+ temperature unit, but converting between Celsius and kelvin requires an offset
278
+ and is reserved for a later affine-conversion release.
279
+
280
+ # Prefixed and scaled units
281
+
282
+ Common SI prefixes and practical time units are available from `units.si`:
283
+
284
+ ```python
285
+ from units.si import (
286
+ centimetre,
287
+ gram,
288
+ hour,
289
+ kiloampere,
290
+ kilometre,
291
+ kilovolt,
292
+ kilowatt,
293
+ megawatt,
294
+ micrometre,
295
+ microsecond,
296
+ milliampere,
297
+ milligram,
298
+ millimetre,
299
+ millisecond,
300
+ millivolt,
301
+ milliwatt,
302
+ minute,
303
+ nanometre,
304
+ nanosecond,
305
+ tonne,
306
+ )
307
+ ```
308
+
309
+ Scaled units participate correctly in multiplication, division, and powers:
310
+
311
+ ```python
312
+ from units.si import hour, kilometre, metre
313
+
314
+ area = (2 * kilometre) * (3 * metre)
315
+ print(area) # 6000 m^2
316
+
317
+ square = (2 * kilometre) ** 2
318
+ print(square) # 4000000 m^2
319
+
320
+ speed = (72 * kilometre) / (2 * hour)
321
+ print(speed) # 10.0 m·s^-1
322
+ ```
323
+
324
+ # Familiar composite units
325
+
326
+ Composite unit expressions such as `kilometre / hour` are algebraic unit
327
+ definitions. They carry the correct scale factor, but anonymous composite units
328
+ render in canonical SI base form:
329
+
330
+ ```python
331
+ from units.si import hour, kilometre
332
+
333
+ speed = 30 * kilometre / hour
334
+ print(speed) # 8.333333333333334 m·s^-1
335
+ ```
336
+
337
+ When you want a semantically familiar display unit, give that composite unit an
338
+ explicit name and convert to it:
339
+
340
+ ```python
341
+ from units import DerivedUnit, convert
342
+ from units.si import hour, kilometre
343
+
344
+ kilometres_per_hour = DerivedUnit.define("km·hr^-1", kilometre / hour)
345
+
346
+ speed = 30 * kilometre / hour
347
+ print(convert(speed, kilometres_per_hour)) # 30 km·hr^-1
348
+ print(30 * kilometres_per_hour) # 30 km·hr^-1
349
+ ```
350
+
351
+ This keeps the arithmetic deterministic while letting application code choose
352
+ domain-specific display names such as `km·hr^-1`, `N·m`, or any other familiar
353
+ derived unit form.
354
+
355
+ # Real-world examples
356
+
357
+ ## Electrical engineering: from resistance to power dissipation
358
+
359
+ ```python
360
+ from units.si import ampere, ohm, volt, watt
361
+
362
+ current = 12 * ampere
363
+ resistance = 8 * ohm
364
+ voltage = current * resistance
365
+ power = voltage * current
366
+
367
+ print(voltage) # 96 V
368
+ print(power) # 1152 W
369
+ ```
370
+
371
+ This works because the package canonicalizes unambiguous derived-unit assemblies:
372
+
373
+ - `ampere * ohm -> volt`
374
+ - `volt * ampere -> watt`
375
+
376
+ ## Pump sizing: hydraulic power from pressure rise and flow rate
377
+
378
+ ```python
379
+ from units.si import metre, second, kilogram, pascal, watt
380
+
381
+ density = 998 * (kilogram / metre ** 3)
382
+ flow_velocity = 2.5 * (metre / second)
383
+ pipe_area = 0.0314 * metre ** 2
384
+ pressure_rise = 180000 * pascal
385
+
386
+ volumetric_flow = flow_velocity * pipe_area
387
+ hydraulic_power = pressure_rise * volumetric_flow
388
+
389
+ print(volumetric_flow) # m^3·s^-1
390
+ print(hydraulic_power) # W
391
+ ```
392
+
393
+ This is a good example of a multi-step engineering computation that still renders
394
+ to intuitive derived units at the end of the chain.
395
+
396
+ ## Structural mechanics: work from force over distance
397
+
398
+ ```python
399
+ from units.si import metre, newton
400
+
401
+ force = 4200 * newton
402
+ displacement = 0.35 * metre
403
+ work = force * displacement
404
+
405
+ print(work) # J
406
+ ```
407
+
408
+ ## Geometric quantities: powers of units
409
+
410
+ ```python
411
+ from units.si import metre
412
+
413
+ volume = 5 * metre ** 3
414
+ area = (12 * metre) ** 2
415
+
416
+ print(volume) # 5 m^3
417
+ print(area) # 144 m^2
418
+ ```
419
+
420
+ The unit form is also valid on its own:
421
+
422
+ ```python
423
+ from units.si import metre
424
+
425
+ area_unit = metre ** 2
426
+ volume_unit = metre ** 3
427
+ ```
428
+
429
+ ## Fluid mechanics: dynamic pressure
430
+
431
+ ```python
432
+ from units.si import kilogram, metre, pascal, second
433
+
434
+ density = 1.225 * (kilogram / metre ** 3)
435
+ velocity = 68 * (metre / second)
436
+ dynamic_pressure = 0.5 * density * velocity * velocity
437
+
438
+ print(dynamic_pressure) # Pa
439
+ ```
440
+
441
+ ## Custom unit systems
442
+
443
+ Custom unit systems are supported, but they are intentionally separate from SI
444
+ canonicalization. Use them when you want the same algebra and formatting
445
+ behaviour without forcing your units into the SI registry.
446
+
447
+ ```python
448
+ from units import CustomUnitBase, DimensionSystem
449
+
450
+ class CommUnit(CustomUnitBase):
451
+ dimension_system = DimensionSystem('comm', ('b', 's', 'B'))
452
+
453
+ bit = CommUnit.define('b')
454
+ second = CommUnit.define('s')
455
+
456
+ data = 32 * bit
457
+ duration = 4 * second
458
+ rate = data / duration
459
+
460
+ print(rate) # 8.0 b·s^-1
461
+ ```
462
+
463
+ Custom systems inherit useful behaviour:
464
+
465
+ - dimensional algebra
466
+ - string rendering
467
+ - incompatibility checks within a system
468
+
469
+ They do not automatically simplify into SI-derived names such as `V`, `J`, or
470
+ `Pa`, and they cannot be mixed with SI units unless you build an explicit bridge.