unitx 0.3.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.
- unitx-0.4.0/.github/workflows/ci.yml +50 -0
- unitx-0.4.0/.github/workflows/release.yml +113 -0
- {unitx-0.3.0 → unitx-0.4.0}/.gitignore +3 -3
- unitx-0.4.0/CHANGELOG.md +85 -0
- unitx-0.4.0/PKG-INFO +283 -0
- unitx-0.4.0/README.md +261 -0
- unitx-0.4.0/conftest.py +21 -0
- unitx-0.4.0/pyproject.toml +47 -0
- unitx-0.4.0/pyrightconfig.json +16 -0
- unitx-0.4.0/src/unitx/__init__.py +1 -0
- unitx-0.4.0/src/unitx/_numerics.py +289 -0
- unitx-0.4.0/src/unitx/_unicode.py +42 -0
- unitx-0.4.0/src/unitx/_unit_data.py +112 -0
- unitx-0.4.0/src/unitx/dimension.py +175 -0
- unitx-0.4.0/src/unitx/exceptions.py +51 -0
- unitx-0.4.0/src/unitx/factor.py +531 -0
- unitx-0.4.0/src/unitx/monomial.py +204 -0
- unitx-0.4.0/src/unitx/quantity.py +347 -0
- unitx-0.4.0/src/unitx/singleunit.py +209 -0
- unitx-0.4.0/src/unitx/unit.py +168 -0
- unitx-0.4.0/src/unitx/unitparser.py +348 -0
- unitx-0.4.0/tests/__init__.py +0 -0
- unitx-0.4.0/tests/test_dimension.py +286 -0
- unitx-0.4.0/tests/test_factor.py +1059 -0
- unitx-0.4.0/tests/test_import.py +5 -0
- unitx-0.4.0/tests/test_monomial.py +223 -0
- unitx-0.4.0/tests/test_numeric.py +244 -0
- unitx-0.4.0/tests/test_quantity.py +405 -0
- unitx-0.4.0/tests/test_singleunit.py +297 -0
- unitx-0.4.0/tests/test_typing.py +31 -0
- unitx-0.4.0/tests/test_unicode.py +38 -0
- unitx-0.4.0/tests/test_unit.py +325 -0
- unitx-0.4.0/tests/test_unit_data.py +90 -0
- unitx-0.4.0/tests/test_unitparser.py +587 -0
- unitx-0.4.0/tests/test_warnings.py +182 -0
- unitx-0.4.0/tests/typing/check_quantity.py +150 -0
- unitx-0.3.0/PKG-INFO +0 -46
- unitx-0.3.0/README.md +0 -2
- unitx-0.3.0/pyproject.toml +0 -45
- unitx-0.3.0/src/unitx/__init__.py +0 -7
- unitx-0.3.0/src/unitx/_data/prefix.py +0 -65
- unitx-0.3.0/src/unitx/_data/unit.py +0 -44
- unitx-0.3.0/src/unitx/dimension.py +0 -132
- unitx-0.3.0/src/unitx/exceptions.py +0 -30
- unitx-0.3.0/src/unitx/factor.py +0 -298
- unitx-0.3.0/src/unitx/monomial.py +0 -251
- unitx-0.3.0/src/unitx/quantity.py +0 -128
- unitx-0.3.0/src/unitx/singleunit.py +0 -116
- unitx-0.3.0/src/unitx/unit.py +0 -86
- unitx-0.3.0/src/unitx/unitparser.py +0 -184
- unitx-0.3.0/src/unitx/utils/number.py +0 -121
- unitx-0.3.0/src/unitx/utils/special_char.py +0 -41
- unitx-0.3.0/src/unitx/utils/valuetype.py +0 -15
- unitx-0.3.0/tests/test_dimension.py +0 -81
- unitx-0.3.0/tests/test_factor.py +0 -113
- unitx-0.3.0/tests/test_monomial.py +0 -113
- unitx-0.3.0/tests/test_quantity.py +0 -105
- unitx-0.3.0/tests/test_singleunit.py +0 -72
- unitx-0.3.0/tests/test_unit.py +0 -50
- unitx-0.3.0/tests/test_unitparser.py +0 -123
- {unitx-0.3.0 → unitx-0.4.0}/LICENSE +0 -0
- /unitx-0.3.0/src/unitx/utils/__init__.py → /unitx-0.4.0/src/unitx/py.typed +0 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Test Python ${{ matrix.python-version }} (${{ matrix.backend }})
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
python-version: ["3.12", "3.13", "3.14", "3.15"]
|
|
22
|
+
backend: ["builtin", "sympy"]
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout repository
|
|
26
|
+
uses: actions/checkout@v6
|
|
27
|
+
|
|
28
|
+
- name: Set up Python
|
|
29
|
+
uses: actions/setup-python@v7
|
|
30
|
+
with:
|
|
31
|
+
python-version: ${{ matrix.python-version }}
|
|
32
|
+
cache: pip
|
|
33
|
+
cache-dependency-path: pyproject.toml
|
|
34
|
+
|
|
35
|
+
- name: Install package and test dependencies
|
|
36
|
+
run: |
|
|
37
|
+
python -m pip install --upgrade pip
|
|
38
|
+
python -m pip install -e ".[test]"
|
|
39
|
+
|
|
40
|
+
- name: Install SymPy
|
|
41
|
+
if: matrix.backend == 'sympy'
|
|
42
|
+
run: python -m pip install sympy
|
|
43
|
+
|
|
44
|
+
- name: Run Ruff
|
|
45
|
+
run: |
|
|
46
|
+
ruff check .
|
|
47
|
+
ruff format --check .
|
|
48
|
+
|
|
49
|
+
- name: Run tests
|
|
50
|
+
run: pytest -q
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Configure a PyPI Trusted Publisher for DaitTHU/unitx, release.yml, and the
|
|
4
|
+
# GitHub environment named pypi.
|
|
5
|
+
# Manual runs validate distributions; publishing a GitHub release uploads them.
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
name: Validate and build distributions
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout repository
|
|
21
|
+
uses: actions/checkout@v6
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
uses: actions/setup-python@v7
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Check release version
|
|
31
|
+
id: version
|
|
32
|
+
env:
|
|
33
|
+
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
|
34
|
+
run: |
|
|
35
|
+
python - <<'PY'
|
|
36
|
+
import os
|
|
37
|
+
import tomllib
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
|
|
41
|
+
tag = os.environ.get('RELEASE_TAG', '')
|
|
42
|
+
if tag and tag != f'v{version}':
|
|
43
|
+
raise SystemExit(f'Release tag {tag!r} does not match project version {version!r}.')
|
|
44
|
+
with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
|
|
45
|
+
output.write(f'version={version}\n')
|
|
46
|
+
print(f'Building unitx {version}')
|
|
47
|
+
PY
|
|
48
|
+
|
|
49
|
+
- name: Install build and validation dependencies
|
|
50
|
+
run: python -m pip install -e ".[test]" build twine
|
|
51
|
+
|
|
52
|
+
- name: Validate source
|
|
53
|
+
run: |
|
|
54
|
+
python -m ruff check .
|
|
55
|
+
python -m ruff format --check .
|
|
56
|
+
python -m pytest -q
|
|
57
|
+
|
|
58
|
+
- name: Build wheel and source distribution
|
|
59
|
+
run: python -m build
|
|
60
|
+
|
|
61
|
+
- name: Check distribution metadata
|
|
62
|
+
run: python -m twine check --strict dist/*
|
|
63
|
+
|
|
64
|
+
- name: Install built wheel
|
|
65
|
+
run: python -m pip install --force-reinstall --no-deps dist/*.whl
|
|
66
|
+
|
|
67
|
+
- name: Check installed package
|
|
68
|
+
working-directory: ${{ runner.temp }}
|
|
69
|
+
env:
|
|
70
|
+
EXPECTED_VERSION: ${{ steps.version.outputs.version }}
|
|
71
|
+
run: |
|
|
72
|
+
python - <<'PY'
|
|
73
|
+
import os
|
|
74
|
+
from fractions import Fraction
|
|
75
|
+
from importlib.metadata import version
|
|
76
|
+
from importlib.resources import files
|
|
77
|
+
|
|
78
|
+
from unitx.quantity import Quantity
|
|
79
|
+
from unitx.unit import Unit
|
|
80
|
+
|
|
81
|
+
assert version('unitx') == os.environ['EXPECTED_VERSION']
|
|
82
|
+
assert files('unitx').joinpath('py.typed').is_file()
|
|
83
|
+
assert Quantity(1, 'cm').to('m').value == Fraction(1, 100)
|
|
84
|
+
assert 2 * Unit('m') == Quantity(200, 'cm')
|
|
85
|
+
PY
|
|
86
|
+
|
|
87
|
+
- name: Store distributions
|
|
88
|
+
uses: actions/upload-artifact@v5
|
|
89
|
+
with:
|
|
90
|
+
name: distributions
|
|
91
|
+
path: dist/*
|
|
92
|
+
if-no-files-found: error
|
|
93
|
+
|
|
94
|
+
publish:
|
|
95
|
+
name: Publish to PyPI
|
|
96
|
+
if: github.event_name == 'release'
|
|
97
|
+
needs: build
|
|
98
|
+
runs-on: ubuntu-latest
|
|
99
|
+
environment:
|
|
100
|
+
name: pypi
|
|
101
|
+
url: https://pypi.org/p/unitx
|
|
102
|
+
permissions:
|
|
103
|
+
id-token: write
|
|
104
|
+
|
|
105
|
+
steps:
|
|
106
|
+
- name: Download distributions
|
|
107
|
+
uses: actions/download-artifact@v6
|
|
108
|
+
with:
|
|
109
|
+
name: distributions
|
|
110
|
+
path: dist/
|
|
111
|
+
|
|
112
|
+
- name: Publish distributions
|
|
113
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -199,7 +199,7 @@ cython_debug/
|
|
|
199
199
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
200
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
201
|
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
-
|
|
202
|
+
.vscode/
|
|
203
203
|
# Temporary file for partial code execution
|
|
204
204
|
tempCodeRunnerFile.py
|
|
205
205
|
|
|
@@ -217,5 +217,5 @@ __marimo__/
|
|
|
217
217
|
# Streamlit
|
|
218
218
|
.streamlit/secrets.toml
|
|
219
219
|
|
|
220
|
-
#
|
|
221
|
-
.
|
|
220
|
+
# macOS
|
|
221
|
+
.DS_Store
|
unitx-0.4.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.4.0] - 2026-10-16
|
|
4
|
+
|
|
5
|
+
Replace the implementation previously published on PyPI as 0.3.0. The 0.0.x
|
|
6
|
+
entries below retain this repository's development milestones.
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- Require Python 3.12 or later; Python 3.11 is no longer supported.
|
|
11
|
+
- Use module imports for `Quantity`, `Unit`, `Dimension`, and `DIMENSIONLESS`; the former top-level exports and `unitx.utils` namespace are no longer provided.
|
|
12
|
+
- Remove `Unit` length and special truth-value behavior. Check formal identity with `unit == Unit('')`, or physical dimensions with `unit.is_dimensionless()`.
|
|
13
|
+
- Compare and hash scalar quantities using exact rational coefficients and remaining irrational unit factors. Finite floats retain their stored binary rational values for equality, without a tolerance.
|
|
14
|
+
- Convert finite Decimal magnitudes exactly to Fraction for scaling and arithmetic, independent of the Decimal precision context; numerical results can change type.
|
|
15
|
+
- Normalize quantity exponents once for both the magnitude and unit, with approximate native evaluation for nonintegral magnitude powers.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Interpret plain numbers as unscaled values when adding to or subtracting from scaled dimensionless quantities.
|
|
20
|
+
- Preserve the original magnitude and type when multiplying or dividing a quantity by a Unit.
|
|
21
|
+
- Cancel common irrational factors before evaluating quantity ordering, retaining exact ordering when the ratio is rational.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Support Fraction and Decimal scalar multiplication and division alongside int and float.
|
|
26
|
+
- Distribute inline type information with `py.typed` and check numerical and custom scalar contracts with Pyright.
|
|
27
|
+
- Add a release workflow that validates version tags, checks distributions and the installed wheel, and publishes GitHub releases through PyPI Trusted Publishing.
|
|
28
|
+
- Document construction, conversion, unit syntax, numerical rules, supported units, and migration from 0.3.0. NumPy integration remains planned and is not supported in this release.
|
|
29
|
+
|
|
30
|
+
## [0.0.7] - 2026-10-15
|
|
31
|
+
|
|
32
|
+
- Add `Unit` with string construction for compound unit expressions, including registered symbols, names, prefixes, and aliases.
|
|
33
|
+
- Support unit multiplication and division, rational powers, and cancellation while preserving exact rational exponents.
|
|
34
|
+
- Expose unit symbols, coherent SI scaling factors, physical dimensions, and dimensionless checks; dimensionless expressions can retain nontrivial scaling factors.
|
|
35
|
+
- Define equality and hashing by formal unit factors regardless of insertion order, preserving distinct prefixed unit identities.
|
|
36
|
+
- Provide readable Unicode display and reconstructible `repr` strings for compound units and rational powers.
|
|
37
|
+
- Add executable usage examples and regression tests for alias normalization, equality and hashing, exact and approximate factors, dimensions, power cancellation, conversion round trips, state preservation, and invalid inputs.
|
|
38
|
+
|
|
39
|
+
## [0.0.6] - 2026-10-15
|
|
40
|
+
|
|
41
|
+
- Add `UnitParser` to parse unit expressions into `Monomial[SingleUnit]`, resolving symbols, names, prefixes, and aliases through `SingleUnit`.
|
|
42
|
+
- Support explicit and implicit multiplication, division, parentheses, and exact rational powers using adjacent numbers, `^`, or `**`, including decimal exponents and parenthesized fractions.
|
|
43
|
+
- Support empty and dimensionless expressions, the literal identity `1` in products such as `m 1/s`, and single-factor leading reciprocals such as `/s` and `/(m s)`.
|
|
44
|
+
- Cache successful parse results so repeated calls return the same object, while allowing failed attempts to be retried.
|
|
45
|
+
- Add `UnitSyntaxError` with diagnostics for malformed expressions, invalid numeric bases, zero fraction denominators, and redundant suffixes.
|
|
46
|
+
- Use spaces for multiplication in ambiguous prefix warning suggestions, matching the supported expression syntax.
|
|
47
|
+
- Add grammar documentation, executable examples, and regression tests for exact parsing, whitespace, diagnostics, caching, retries, and warning behavior and attribution.
|
|
48
|
+
|
|
49
|
+
## [0.0.5] - 2026-10-15
|
|
50
|
+
|
|
51
|
+
- Add built-in SI prefix data and initial unit definitions for `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus the dimensionless unit, with exact factors, dimensions, and prefix restrictions.
|
|
52
|
+
- Add immutable, hashable `SingleUnit` with symbol and name parsing, canonical identity, dimension and factor queries, prefix inspection and removal, copying, and serialization.
|
|
53
|
+
- Support symbol and name aliases, including `meter`, micro prefix variants `u` and `μ`, and the uppercase `K` alias for kilo, while preserving canonical symbols and names.
|
|
54
|
+
- Add `UnitSymbolError` for invalid unit inputs and `UnitSymbolWarning` for potentially misleading prefix aliases, with warnings attributed to user call sites.
|
|
55
|
+
- Add `Factor` hashing consistent with equality to rational numbers and individual `SymbolicConstant` instances.
|
|
56
|
+
- Add regression tests for unit definitions, alias normalization, exact factors, immutability, serialization, and warning behavior, filtering, and attribution.
|
|
57
|
+
|
|
58
|
+
## [0.0.4] - 2026-10-15
|
|
59
|
+
|
|
60
|
+
- Add `Factor` for exact positive scaling expressions with rational exponents, multiplicative arithmetic, and exact rational or approximate float evaluation, along with predefined `ONE`, `TWO`, and `TEN` instances.
|
|
61
|
+
- Add immutable `SymbolicConstant` with symbolic identity and float approximations, along with the predefined `PI` constant.
|
|
62
|
+
- Support factor decomposition, compact formatting, and magnitude scaling that converts finite `Decimal` magnitudes exactly before multiplication.
|
|
63
|
+
- Provide built-in bounded integer factorization and primality testing, optional SymPy support, and `IncompleteFactorWarning` for unresolved factorization or unproven primality.
|
|
64
|
+
- Display negative `Monomial` powers as denominator terms, with parentheses around multiple denominator factors.
|
|
65
|
+
- Attribute numerical warnings to user call sites without manually passing warning stack levels.
|
|
66
|
+
- Raise the minimum supported Python version from 3.11 to 3.12.
|
|
67
|
+
- Extend CI to test both built-in and SymPy backends on Python 3.12–3.15.
|
|
68
|
+
|
|
69
|
+
## [0.0.3] - 2026-10-14
|
|
70
|
+
|
|
71
|
+
- Add immutable, hashable `Monomial` for formal products with rational exponents.
|
|
72
|
+
- Support multiplication, division, rational powers, inverses, and numerator/denominator decomposition.
|
|
73
|
+
- Provide explicit exponent queries, component views, Unicode formatting, and exact representation round trips.
|
|
74
|
+
- Add tests for monomial arithmetic, normalization, immutability, hashing, copying, and serialization.
|
|
75
|
+
|
|
76
|
+
## [0.0.2] - 2026-10-13
|
|
77
|
+
|
|
78
|
+
- Add immutable physical dimensions with rational exponents and dimensionless normalization.
|
|
79
|
+
- Support dimension arithmetic, exponent aliases, and exact representation round trips.
|
|
80
|
+
- Add numerical conversion utilities with inexact float warnings and Unicode exponent formatting.
|
|
81
|
+
- Add tests for dimensions, numerical conversion, formatting, copying, and serialization.
|
|
82
|
+
|
|
83
|
+
## [0.0.1] - 2026-10-13
|
|
84
|
+
|
|
85
|
+
- Project initialization and setup.
|
unitx-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: unitx
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
|
|
5
|
+
Project-URL: Homepage, https://github.com/DaitTHU/unitx
|
|
6
|
+
Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
|
|
7
|
+
Author: Dait
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pyright>=1.1.414; extra == 'test'
|
|
19
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
20
|
+
Requires-Dist: ruff>=0.8; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# unitx
|
|
24
|
+
|
|
25
|
+
Physical units, scalar quantities, and dimensional analysis for Python, with
|
|
26
|
+
exact rational unit scales and exponents.
|
|
27
|
+
|
|
28
|
+
unitx is in early development. Public APIs may change during the 0.x series.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Requires **Python 3.12 or later**, with no mandatory runtime dependencies.
|
|
33
|
+
|
|
34
|
+
Install a published release:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
python -m pip install unitx
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To use the current development version, run this from the repository root:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This README describes the current source tree, including unreleased changes.
|
|
47
|
+
For a published release, consult the README at its matching Git tag.
|
|
48
|
+
|
|
49
|
+
## Upgrading from 0.3.0
|
|
50
|
+
|
|
51
|
+
Version 0.4.0 replaces the implementation previously published on PyPI as 0.3.0.
|
|
52
|
+
It includes incompatible changes:
|
|
53
|
+
|
|
54
|
+
- Python 3.12 or later is required; Python 3.11 is no longer supported.
|
|
55
|
+
- Import classes from their modules. The former top-level exports and
|
|
56
|
+
`unitx.utils` namespace are no longer provided:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from unitx.dimension import DIMENSIONLESS, Dimension
|
|
60
|
+
from unitx.quantity import Quantity
|
|
61
|
+
from unitx.unit import Unit
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- `Unit` no longer defines `len()` or special truth-value behavior. Use
|
|
65
|
+
`unit == Unit('')` to check the formal identity, or `unit.is_dimensionless()`
|
|
66
|
+
to check physical dimensions; these are different questions.
|
|
67
|
+
- Conversion and arithmetic can produce different magnitude types, especially
|
|
68
|
+
for Decimal inputs. Review the [numerical behavior](#numerical-behavior)
|
|
69
|
+
before relying on a result's concrete type or approximate equality.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
Import the classes from their modules:
|
|
74
|
+
|
|
75
|
+
```pycon
|
|
76
|
+
>>> from unitx.quantity import Quantity
|
|
77
|
+
>>> from unitx.unit import Unit
|
|
78
|
+
>>> distance = 3 * Unit('m')
|
|
79
|
+
>>> elapsed = Quantity(2, 's')
|
|
80
|
+
>>> speed = distance / elapsed
|
|
81
|
+
>>> print(speed)
|
|
82
|
+
1.5 m/s
|
|
83
|
+
>>> print(speed.to('cm/s'))
|
|
84
|
+
150.0 cm/s
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`Quantity(value, unit)` accepts a unit expression or an existing `Unit` object.
|
|
89
|
+
Multiplying a scalar by a unit constructs a quantity; dividing a scalar by a
|
|
90
|
+
unit constructs a quantity with the reciprocal unit. Construction preserves the
|
|
91
|
+
original magnitude and its type.
|
|
92
|
+
|
|
93
|
+
## Conversion and arithmetic
|
|
94
|
+
|
|
95
|
+
Use `.to()` to convert between units with the same physical dimensions.
|
|
96
|
+
Addition and subtraction convert the right operand to the left operand's unit:
|
|
97
|
+
|
|
98
|
+
```pycon
|
|
99
|
+
>>> length = Quantity(1, 'm')
|
|
100
|
+
>>> print(length + Quantity(50, 'cm'))
|
|
101
|
+
3/2 m
|
|
102
|
+
>>> length.to('cm').value
|
|
103
|
+
100
|
|
104
|
+
>>> Quantity(1, 'cm').to('m').value
|
|
105
|
+
Fraction(1, 100)
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Multiplication and division combine units. Powers apply to both the magnitude
|
|
110
|
+
and the unit. Multiplication or division by a `Unit` alone preserves the
|
|
111
|
+
magnitude unchanged.
|
|
112
|
+
|
|
113
|
+
Conversion, addition, subtraction, and ordering across incompatible dimensions
|
|
114
|
+
raise `unitx.exceptions.DimensionError`. Equality across incompatible dimensions
|
|
115
|
+
returns `False`.
|
|
116
|
+
|
|
117
|
+
For repeated conversions, reuse a target `Unit` object to avoid parsing the
|
|
118
|
+
expression on every call.
|
|
119
|
+
|
|
120
|
+
## Unit expressions
|
|
121
|
+
|
|
122
|
+
Expressions support registered symbols, singular English names, SI prefixes,
|
|
123
|
+
multiplication, division, parentheses, and rational powers:
|
|
124
|
+
|
|
125
|
+
```pycon
|
|
126
|
+
>>> Unit('kg m/s2') == Unit('kg*m/s**2')
|
|
127
|
+
True
|
|
128
|
+
>>> Unit('m^(1/2)') ** 2 == Unit('m')
|
|
129
|
+
True
|
|
130
|
+
>>> Unit('micrometre') == Unit('um')
|
|
131
|
+
True
|
|
132
|
+
>>> Unit('ms') == Unit('m s')
|
|
133
|
+
False
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- Multiplication can use `*` or whitespace. Adjacent letters form one unit
|
|
138
|
+
token: `ms` is a millisecond, while `m s` is metre times second.
|
|
139
|
+
- Powers can use adjacent numbers, `^`, or `**`. Exponents written as ratios
|
|
140
|
+
require parentheses, as in `m^(1/2)`; decimal exponents such as `m^0.5` are exact.
|
|
141
|
+
- Multiplication and division associate left to right. Use `m/(s A)` to put
|
|
142
|
+
both `s` and `A` in the denominator; `m/s A` means `(m/s)*A`.
|
|
143
|
+
- Unit symbols and names are case-sensitive. `u`, `µ`, and `μ` are accepted
|
|
144
|
+
micro prefixes. Use lowercase `k` for kilo; `K` denotes kelvin.
|
|
145
|
+
|
|
146
|
+
The current unit table contains `s`, `m`, `g`, `A`, `K`, `mol`, and `cd`, plus
|
|
147
|
+
the dimensionless identity. Mass scales are referenced to kilograms: `g` has
|
|
148
|
+
factor `1/1000`, and `kg` has factor `1`. Named derived units such as `N` are
|
|
149
|
+
not registered yet; use expressions such as `kg*m/s2`.
|
|
150
|
+
|
|
151
|
+
## Units, dimensions, and equality
|
|
152
|
+
|
|
153
|
+
A `Unit` retains its formal factors. A `Quantity` compares physical values
|
|
154
|
+
across compatible units:
|
|
155
|
+
|
|
156
|
+
```pycon
|
|
157
|
+
>>> Unit('m') == Unit('cm')
|
|
158
|
+
False
|
|
159
|
+
>>> Quantity(1, 'm') == Quantity(100, 'cm')
|
|
160
|
+
True
|
|
161
|
+
>>> len({Quantity(1, 'm'), Quantity(100, 'cm')})
|
|
162
|
+
1
|
|
163
|
+
>>> from unitx.dimension import Dimension
|
|
164
|
+
>>> speed.dimension == Dimension(L=1, T=-1)
|
|
165
|
+
True
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Equal quantities have equal hashes when their magnitudes are hashable. Different
|
|
170
|
+
formal unit expressions can have the same physical dimension and scale without
|
|
171
|
+
being equal as `Unit` objects.
|
|
172
|
+
|
|
173
|
+
An expression can be dimensionless and still carry a scale. Plain numbers added
|
|
174
|
+
to or subtracted from dimensionless quantities represent unscaled values:
|
|
175
|
+
|
|
176
|
+
```pycon
|
|
177
|
+
>>> scaled = Quantity(1, 'km/m')
|
|
178
|
+
>>> scaled.is_dimensionless()
|
|
179
|
+
True
|
|
180
|
+
>>> scaled == 1000
|
|
181
|
+
True
|
|
182
|
+
>>> (scaled + 1).to('').value
|
|
183
|
+
Fraction(1001, 1)
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Numerical behavior
|
|
188
|
+
|
|
189
|
+
Exact unit algebra and numerical magnitude evaluation have different rules:
|
|
190
|
+
|
|
191
|
+
- Rational unit scales and exponents remain exact. Prefer `Fraction` or
|
|
192
|
+
`Decimal` for exact exponents; float exponents may be approximated with an
|
|
193
|
+
`InexactFloatWarning`.
|
|
194
|
+
- Conversion and arithmetic can change the magnitude's type. Finite `Decimal`
|
|
195
|
+
operands are converted exactly to `Fraction`, without Decimal context
|
|
196
|
+
rounding.
|
|
197
|
+
- Other magnitudes use native numerical arithmetic. In particular, integer
|
|
198
|
+
division produces a float, and nonintegral magnitude powers can round or
|
|
199
|
+
produce complex values.
|
|
200
|
+
- Equality is exact, without a tolerance. Finite floats are compared as their
|
|
201
|
+
stored binary rational values. Floating-point conversions and irrational
|
|
202
|
+
powers may round; compare their values approximately in a common unit when
|
|
203
|
+
appropriate.
|
|
204
|
+
|
|
205
|
+
```pycon
|
|
206
|
+
>>> from decimal import Decimal
|
|
207
|
+
>>> from fractions import Fraction
|
|
208
|
+
>>> Quantity(Decimal('0.1'), 'm').to('cm').value
|
|
209
|
+
10
|
|
210
|
+
>>> (Quantity(Fraction(1), 'm') / Quantity(3, 's')).value
|
|
211
|
+
Fraction(1, 3)
|
|
212
|
+
>>> Quantity(0.1, '') == Quantity(Fraction(1, 10), '')
|
|
213
|
+
False
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The built-in integer factorization backend is bounded. Unresolved factors emit
|
|
218
|
+
`IncompleteFactorWarning` and can limit simplification and comparisons. SymPy
|
|
219
|
+
is an optional, more capable factorization backend:
|
|
220
|
+
|
|
221
|
+
```sh
|
|
222
|
+
python -m pip install sympy
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Current scope
|
|
226
|
+
|
|
227
|
+
- **NumPy scalars, arrays, and NumPy operations are not supported yet. NumPy
|
|
228
|
+
integration is planned.** Structural compatibility with a magnitude protocol
|
|
229
|
+
does not imply NumPy support.
|
|
230
|
+
- Custom magnitudes must support scalar arithmetic, return a Python `bool`
|
|
231
|
+
from equality, and defer raw operations with `Unit` operands to its reflected
|
|
232
|
+
operators.
|
|
233
|
+
- Scalar multiplication and division of quantities currently accept `int`,
|
|
234
|
+
`float`, `Fraction`, and `Decimal`. Complex magnitudes are supported, including
|
|
235
|
+
results of fractional powers; ordering follows the magnitude's native rules.
|
|
236
|
+
- Unit conversion is multiplicative. Offset temperature scales such as Celsius
|
|
237
|
+
and Fahrenheit are not supported.
|
|
238
|
+
|
|
239
|
+
## Building blocks
|
|
240
|
+
|
|
241
|
+
| Class | Import module | Purpose |
|
|
242
|
+
| --- | --- | --- |
|
|
243
|
+
| `Quantity` | `unitx.quantity` | Scalar values paired with units, conversion, and arithmetic |
|
|
244
|
+
| `Unit` | `unitx.unit` | Compound units, scaling factors, and physical dimensions |
|
|
245
|
+
| `Dimension` | `unitx.dimension` | Seven physical dimensions with rational exponents |
|
|
246
|
+
| `SingleUnit` | `unitx.singleunit` | One registered unit with an optional prefix |
|
|
247
|
+
| `Factor` | `unitx.factor` | Positive scaling expressions and magnitude scaling |
|
|
248
|
+
| `UnitParser` | `unitx.unitparser` | Parsing unit expressions into formal products |
|
|
249
|
+
| `Monomial` | `unitx.monomial` | Immutable formal products with rational exponents |
|
|
250
|
+
|
|
251
|
+
The package includes inline type annotations and a `py.typed` marker. Construction
|
|
252
|
+
and operations with `Unit` preserve the magnitude type in type hints; conversion
|
|
253
|
+
and numerical arithmetic use result unions where the concrete type depends on
|
|
254
|
+
runtime values or units. Modules whose names start with `_` are internal.
|
|
255
|
+
|
|
256
|
+
## Development
|
|
257
|
+
|
|
258
|
+
Install the development version and test dependencies:
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
python -m pip install -e ".[test]"
|
|
262
|
+
python -m pytest -q
|
|
263
|
+
python -m ruff check .
|
|
264
|
+
python -m ruff format --check .
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The pytest suite includes doctests and Pyright checks for the typed API. CI runs
|
|
268
|
+
the suite with both the built-in and SymPy factorization backends.
|
|
269
|
+
|
|
270
|
+
## Releases
|
|
271
|
+
|
|
272
|
+
The [release workflow](.github/workflows/release.yml) validates the source,
|
|
273
|
+
builds a wheel and source distribution, checks their metadata, and tests the
|
|
274
|
+
installed wheel. Manual workflow runs store the distributions without uploading
|
|
275
|
+
them to PyPI.
|
|
276
|
+
|
|
277
|
+
Publishing a GitHub release runs the same checks and uploads the distributions
|
|
278
|
+
to PyPI. The release tag must match the project version, for example `v0.4.0`
|
|
279
|
+
for version `0.4.0`. Configure a PyPI Trusted Publisher for the repository,
|
|
280
|
+
workflow `release.yml`, and GitHub environment `pypi` before publishing.
|
|
281
|
+
|
|
282
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history. Licensed under the
|
|
283
|
+
[MIT License](LICENSE).
|