unitx 0.13.4__tar.gz → 0.14.2__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 (156) hide show
  1. unitx-0.14.2/.github/consumers/arrays.py +35 -0
  2. unitx-0.14.2/.github/consumers/pyrightconfig.json +4 -0
  3. unitx-0.14.2/.github/consumers/scalars.py +90 -0
  4. unitx-0.14.2/.github/workflows/ci.yml +141 -0
  5. {unitx-0.13.4 → unitx-0.14.2}/.github/workflows/release.yml +17 -17
  6. {unitx-0.13.4 → unitx-0.14.2}/.gitignore +0 -3
  7. unitx-0.14.2/.readthedocs.yaml +14 -0
  8. {unitx-0.13.4 → unitx-0.14.2}/CHANGELOG.md +68 -0
  9. {unitx-0.13.4 → unitx-0.14.2}/CONTRIBUTING.md +33 -1
  10. {unitx-0.13.4 → unitx-0.14.2}/PKG-INFO +8 -5
  11. {unitx-0.13.4 → unitx-0.14.2}/README.md +7 -4
  12. unitx-0.14.2/benchmarks/README.md +30 -0
  13. unitx-0.14.2/benchmarks/scalars.py +68 -0
  14. {unitx-0.13.4 → unitx-0.14.2}/docs/README.md +20 -14
  15. unitx-0.14.2/docs/affine.md +153 -0
  16. unitx-0.14.2/docs/architecture.md +162 -0
  17. unitx-0.14.2/docs/catalogues/README.md +69 -0
  18. unitx-0.14.2/docs/catalogues/imperial.md +103 -0
  19. unitx-0.13.4/docs/catalogue.md → unitx-0.14.2/docs/catalogues/si.md +13 -12
  20. unitx-0.14.2/docs/custom-units.md +408 -0
  21. {unitx-0.13.4 → unitx-0.14.2}/docs/display.md +8 -8
  22. unitx-0.14.2/docs/javascripts/mermaid.mjs +16 -0
  23. unitx-0.14.2/docs/logarithmic.md +165 -0
  24. unitx-0.14.2/docs/migration.md +173 -0
  25. {unitx-0.13.4 → unitx-0.14.2}/docs/numpy.md +7 -5
  26. unitx-0.14.2/docs/requirements.txt +1 -0
  27. {unitx-0.13.4 → unitx-0.14.2}/docs/scalars.md +1 -1
  28. {unitx-0.13.4 → unitx-0.14.2}/docs/serialization.md +98 -32
  29. {unitx-0.13.4 → unitx-0.14.2}/docs/typing.md +1 -1
  30. {unitx-0.13.4 → unitx-0.14.2}/docs/units.md +19 -7
  31. unitx-0.14.2/mkdocs.yml +41 -0
  32. {unitx-0.13.4 → unitx-0.14.2}/pyproject.toml +1 -1
  33. {unitx-0.13.4 → unitx-0.14.2}/pyrightconfig.json +17 -0
  34. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/__init__.py +10 -1
  35. unitx-0.14.2/src/unitx/_conversion.py +28 -0
  36. unitx-0.14.2/src/unitx/_coordinates/__init__.py +1 -0
  37. unitx-0.14.2/src/unitx/_coordinates/affine.py +138 -0
  38. unitx-0.14.2/src/unitx/_coordinates/definition.py +42 -0
  39. unitx-0.14.2/src/unitx/_coordinates/logarithmic.py +175 -0
  40. unitx-0.14.2/src/unitx/_coordinates/logarithms.py +130 -0
  41. unitx-0.14.2/src/unitx/_coordinates/points.py +151 -0
  42. unitx-0.14.2/src/unitx/_coordinates/resolve.py +63 -0
  43. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_display.py +28 -34
  44. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_numpy.py +73 -84
  45. unitx-0.14.2/src/unitx/_operands.py +56 -0
  46. unitx-0.14.2/src/unitx/_registry/__init__.py +1 -0
  47. unitx-0.14.2/src/unitx/_registry/builtin_units.py +18 -0
  48. unitx-0.14.2/src/unitx/_registry/prefixes.py +107 -0
  49. unitx-0.14.2/src/unitx/_registry/reconstruction.py +221 -0
  50. unitx-0.14.2/src/unitx/_scaling.py +70 -0
  51. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/affinequantity.py +91 -227
  52. unitx-0.14.2/src/unitx/affineunit.py +343 -0
  53. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/dimension.py +50 -34
  54. unitx-0.14.2/src/unitx/imperial/__init__.py +8 -0
  55. unitx-0.14.2/src/unitx/imperial/units.py +347 -0
  56. unitx-0.14.2/src/unitx/logquantity.py +479 -0
  57. unitx-0.14.2/src/unitx/logunit.py +361 -0
  58. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/math.py +1 -1
  59. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/quantity.py +90 -86
  60. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/serialization.py +179 -72
  61. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/si/bases.py +3 -1
  62. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/si/units.py +62 -60
  63. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/singleunit.py +39 -27
  64. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/time.py +0 -3
  65. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/unit.py +164 -249
  66. unitx-0.14.2/src/unitx/unitcatalog.py +69 -0
  67. unitx-0.14.2/src/unitx/unitdefinition.py +301 -0
  68. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/unitparser.py +16 -7
  69. unitx-0.14.2/src/unitx/unitregistry.py +275 -0
  70. unitx-0.14.2/tests/numpy_typing/check_logarithmic.py +22 -0
  71. {unitx-0.13.4 → unitx-0.14.2}/tests/numpy_typing/check_numpy.py +2 -1
  72. {unitx-0.13.4 → unitx-0.14.2}/tests/test_affine.py +161 -99
  73. {unitx-0.13.4 → unitx-0.14.2}/tests/test_affinequantity.py +14 -13
  74. {unitx-0.13.4 → unitx-0.14.2}/tests/test_builtin_definitions.py +12 -9
  75. unitx-0.14.2/tests/test_catalog_collection.py +198 -0
  76. unitx-0.14.2/tests/test_definition_scope.py +328 -0
  77. {unitx-0.13.4 → unitx-0.14.2}/tests/test_dimension.py +3 -2
  78. {unitx-0.13.4 → unitx-0.14.2}/tests/test_display.py +29 -28
  79. {unitx-0.13.4 → unitx-0.14.2}/tests/test_examples.py +33 -1
  80. {unitx-0.13.4 → unitx-0.14.2}/tests/test_immutable.py +8 -10
  81. unitx-0.14.2/tests/test_imperial.py +118 -0
  82. unitx-0.14.2/tests/test_linear_boundaries.py +84 -0
  83. unitx-0.14.2/tests/test_logarithmic_model.py +221 -0
  84. {unitx-0.13.4 → unitx-0.14.2}/tests/test_math.py +2 -2
  85. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy.py +2 -2
  86. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_affine.py +24 -20
  87. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_mutation.py +1 -1
  88. {unitx-0.13.4 → unitx-0.14.2}/tests/test_quantity.py +12 -12
  89. unitx-0.14.2/tests/test_registry.py +414 -0
  90. unitx-0.14.2/tests/test_registry_scope.py +223 -0
  91. {unitx-0.13.4 → unitx-0.14.2}/tests/test_serialization.py +160 -62
  92. {unitx-0.13.4 → unitx-0.14.2}/tests/test_singleunit.py +4 -5
  93. {unitx-0.13.4 → unitx-0.14.2}/tests/test_siunitx_display.py +13 -13
  94. {unitx-0.13.4 → unitx-0.14.2}/tests/test_time.py +5 -5
  95. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unicode_units.py +10 -2
  96. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unit.py +49 -49
  97. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unit_definition.py +222 -128
  98. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unitregistry.py +148 -60
  99. {unitx-0.13.4 → unitx-0.14.2}/tests/typing/check_affine.py +4 -1
  100. unitx-0.14.2/tests/typing/check_imperial.py +13 -0
  101. unitx-0.14.2/tests/typing/check_logarithmic.py +25 -0
  102. {unitx-0.13.4 → unitx-0.14.2}/tests/typing/check_quantity.py +25 -25
  103. unitx-0.14.2/tests/typing/check_registry.py +56 -0
  104. {unitx-0.13.4 → unitx-0.14.2}/tests/typing/check_serialization.py +10 -2
  105. unitx-0.13.4/.github/workflows/ci.yml +0 -78
  106. unitx-0.13.4/docs/affine.md +0 -259
  107. unitx-0.13.4/docs/custom-units.md +0 -156
  108. unitx-0.13.4/docs/migration.md +0 -78
  109. unitx-0.13.4/src/unitx/_conversion.py +0 -228
  110. unitx-0.13.4/src/unitx/affineunit.py +0 -427
  111. unitx-0.13.4/src/unitx/unitregistry.py +0 -378
  112. {unitx-0.13.4 → unitx-0.14.2}/LICENSE +0 -0
  113. {unitx-0.13.4 → unitx-0.14.2}/benchmarks/numpy_writes.py +0 -0
  114. {unitx-0.13.4 → unitx-0.14.2}/conftest.py +0 -0
  115. {unitx-0.13.4 → unitx-0.14.2}/docs/images/blackbody.svg +0 -0
  116. {unitx-0.13.4 → unitx-0.14.2}/docs/stdlib.md +0 -0
  117. {unitx-0.13.4 → unitx-0.14.2}/examples/README.md +0 -0
  118. {unitx-0.13.4 → unitx-0.14.2}/examples/arrhenius_fit.ipynb +0 -0
  119. {unitx-0.13.4 → unitx-0.14.2}/examples/blackbody.ipynb +0 -0
  120. {unitx-0.13.4 → unitx-0.14.2}/examples/damped_oscillator.ipynb +0 -0
  121. {unitx-0.13.4 → unitx-0.14.2}/examples/gaussian_wavepacket.ipynb +0 -0
  122. {unitx-0.13.4 → unitx-0.14.2}/examples/optical_attenuation.ipynb +0 -0
  123. {unitx-0.13.4 → unitx-0.14.2}/examples/pendulum_uncertainty.ipynb +0 -0
  124. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_constnamespace.py +0 -0
  125. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_immutable.py +0 -0
  126. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_numerics.py +0 -0
  127. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_numpy_typing.py +0 -0
  128. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/_unicode.py +0 -0
  129. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/exceptions.py +0 -0
  130. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/factor.py +0 -0
  131. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/monomial.py +0 -0
  132. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/py.typed +0 -0
  133. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/si/__init__.py +0 -0
  134. {unitx-0.13.4 → unitx-0.14.2}/src/unitx/si/constants.py +0 -0
  135. {unitx-0.13.4 → unitx-0.14.2}/tests/__init__.py +0 -0
  136. {unitx-0.13.4 → unitx-0.14.2}/tests/_notebooks.py +0 -0
  137. {unitx-0.13.4 → unitx-0.14.2}/tests/example_typing/check_blackbody.py +0 -0
  138. {unitx-0.13.4 → unitx-0.14.2}/tests/numpy_typing/check_math.py +0 -0
  139. {unitx-0.13.4 → unitx-0.14.2}/tests/test_factor.py +0 -0
  140. {unitx-0.13.4 → unitx-0.14.2}/tests/test_import.py +0 -0
  141. {unitx-0.13.4 → unitx-0.14.2}/tests/test_math_numpy.py +0 -0
  142. {unitx-0.13.4 → unitx-0.14.2}/tests/test_monomial.py +0 -0
  143. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numeric.py +0 -0
  144. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_differentiation.py +0 -0
  145. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_integration.py +0 -0
  146. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_multi_outputs.py +0 -0
  147. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_outputs.py +0 -0
  148. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_products.py +0 -0
  149. {unitx-0.13.4 → unitx-0.14.2}/tests/test_numpy_statistics.py +0 -0
  150. {unitx-0.13.4 → unitx-0.14.2}/tests/test_si.py +0 -0
  151. {unitx-0.13.4 → unitx-0.14.2}/tests/test_typing.py +0 -0
  152. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unicode.py +0 -0
  153. {unitx-0.13.4 → unitx-0.14.2}/tests/test_unitparser.py +0 -0
  154. {unitx-0.13.4 → unitx-0.14.2}/tests/test_warnings.py +0 -0
  155. {unitx-0.13.4 → unitx-0.14.2}/tests/typing/check_math_time.py +0 -0
  156. {unitx-0.13.4 → unitx-0.14.2}/tests/typing/check_si.py +0 -0
@@ -0,0 +1,35 @@
1
+ """Check array runtime and inferred types using the installed wheel."""
2
+
3
+ from typing import assert_type
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from unitx import LogQuantity, LogUnit, Quantity, Unit
9
+ from unitx.exceptions import DimensionError
10
+ from unitx.si import si
11
+
12
+
13
+ def check_arrays(values: NDArray[np.float64]) -> None:
14
+ level = LogQuantity(values, si.dBm)
15
+ assert_type(level, LogQuantity[NDArray[np.float64]])
16
+ assert_type(level.unit, LogUnit)
17
+ assert level.value is values
18
+ np.testing.assert_allclose(level.to_quantity(si.W).value, [0.001, 0.1, 1.0])
19
+
20
+ distance = Quantity(values.copy(), si.m)
21
+ assert_type(distance, Quantity[NDArray[np.float64]])
22
+ assert_type(distance.unit, Unit)
23
+ np.testing.assert_allclose(distance.to(si.cm).value, values * 100)
24
+ saved = distance.value.copy()
25
+ try:
26
+ distance[:] = Quantity(values, si.s)
27
+ except DimensionError:
28
+ pass
29
+ else:
30
+ error_message = 'An incompatible assignment must fail.'
31
+ raise AssertionError(error_message)
32
+ np.testing.assert_array_equal(distance.value, saved)
33
+
34
+
35
+ check_arrays(np.asarray([0.0, 20.0, 30.0], dtype=np.float64))
@@ -0,0 +1,4 @@
1
+ {
2
+ "pythonVersion": "3.12",
3
+ "typeCheckingMode": "strict"
4
+ }
@@ -0,0 +1,90 @@
1
+ """Check the installed wheel before optional NumPy is installed."""
2
+
3
+ import os
4
+ import pickle
5
+ import sys
6
+ from datetime import datetime, timedelta, timezone
7
+ from decimal import Decimal
8
+ from fractions import Fraction
9
+ from importlib.metadata import distribution
10
+ from importlib.util import find_spec
11
+ from pathlib import Path
12
+ from typing import Final, assert_type
13
+
14
+ import unitx
15
+ from unitx import AffineQuantity, LogQuantity, Quantity, Registry, Unit, UnitCatalog, math, time
16
+ from unitx.factor import Factor
17
+ from unitx.imperial import imperial
18
+ from unitx.serialization import from_dict, to_dict
19
+ from unitx.si import si
20
+
21
+ installed = distribution('unitx')
22
+ package_root = Path(str(installed.locate_file('unitx')))
23
+ assert installed.version == os.environ['EXPECTED_VERSION']
24
+ assert Path(unitx.__file__).resolve() == (package_root / '__init__.py').resolve()
25
+ assert (package_root / 'py.typed').is_file()
26
+ assert find_spec('numpy') is None
27
+
28
+ distance = Quantity(Decimal('1.25'), si.m)
29
+ assert_type(distance.to(si.cm), Quantity[Decimal])
30
+ assert distance.value_in(si.cm) == Decimal(125)
31
+ assert Quantity(1, si.cm).value_in(si.m) == Fraction(1, 100)
32
+ assert 2 * Unit(si.m, scale=Factor(3)) == 6 * si.m
33
+
34
+ root = math.sqrt(9 * si.m**2)
35
+ assert_type(root, Quantity[float])
36
+ assert root == 3 * si.m
37
+ assert math.sin(90 * si.deg).value == 1.0
38
+ assert math.hypot(3 * si.m, 400 * si.cm) == 5 * si.m
39
+
40
+ stamp = time.time()
41
+ start = time.perf_counter()
42
+ elapsed = time.perf_counter() - start
43
+ assert_type(stamp, AffineQuantity[int])
44
+ assert_type(start, AffineQuantity[int])
45
+ assert_type(elapsed.unit, Unit)
46
+ assert type(stamp.value) is int and stamp.unit.delta_unit == si.ns
47
+ assert type(elapsed.value) is int and elapsed.unit == si.ns and elapsed.value >= 0
48
+ local_zone = timezone(timedelta(hours=8))
49
+ instant = datetime(2000, 1, 1, 12, 34, 56, 123456, tzinfo=local_zone)
50
+ point = time.from_datetime(instant)
51
+ assert_type(point, AffineQuantity[int])
52
+ assert_type(time.to_datetime(point), datetime)
53
+ utc_instant = time.to_datetime(point)
54
+ local_instant = time.to_datetime(point, tz=local_zone)
55
+ assert utc_instant == instant and utc_instant.tzinfo is timezone.utc
56
+ assert local_instant == instant and local_instant.tzinfo is local_zone
57
+ assert time.to_datetime(AffineQuantity(1500, si.ns)) == datetime(
58
+ 1970, 1, 1, 0, 0, 0, 2, tzinfo=timezone.utc
59
+ )
60
+ duration = time.from_timedelta(timedelta(microseconds=123456))
61
+ assert_type(duration, Quantity[int])
62
+ assert duration == 123456 * si.us
63
+ assert time.to_timedelta(Quantity(Decimal('0.0000015'), si.s)) == timedelta(microseconds=2)
64
+
65
+ celsius = 20 @ si.degC
66
+ level = 30 @ si.dBm
67
+ assert_type(celsius, AffineQuantity[int])
68
+ assert_type(level, LogQuantity[int])
69
+ assert celsius.to_quantity(si.K) == Fraction(5863, 20) * si.K
70
+ assert (32 @ imperial.degF).to(si.degC) == 0 @ si.degC
71
+ assert level.to_quantity(si.W) == 1 * si.W
72
+ assert LogQuantity.from_quantity(1 * si.W, si.dBm) == level
73
+
74
+ registry = Registry([si.m])
75
+ with registry.defining():
76
+ span = (2 * si.m).define('installedSpan')
77
+
78
+
79
+ class laboratory(UnitCatalog, units=(span,)):
80
+ length: Final = span
81
+
82
+
83
+ assert_type(laboratory.parse('installedSpan'), Unit)
84
+ assert laboratory.parse('installedSpan') == span
85
+ quantity = 3 * span
86
+ assert from_dict(to_dict(quantity), registry=registry) == quantity
87
+ for value in (quantity, celsius, level):
88
+ assert pickle.loads(pickle.dumps(value)) == value
89
+ assert 'numpy' not in sys.modules
90
+ assert 'sympy' not in sys.modules
@@ -0,0 +1,141 @@
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
+ docs:
15
+ name: Documentation
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ - uses: actions/setup-python@v7
20
+ with:
21
+ python-version: '3.12'
22
+ - name: Install documentation dependencies
23
+ run: python -m pip install -r docs/requirements.txt
24
+ - name: Build documentation with strict link checks
25
+ run: python -m mkdocs build --strict
26
+
27
+ test:
28
+ name: Test Python ${{ matrix.python-version }} (${{ matrix.backend }})
29
+ runs-on: ubuntu-latest
30
+
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ python-version: ["3.12", "3.13", "3.14", "3.15"]
35
+ backend: ["builtin", "sympy"]
36
+
37
+ steps:
38
+ - name: Checkout repository
39
+ uses: actions/checkout@v6
40
+
41
+ - name: Set up Python
42
+ uses: actions/setup-python@v7
43
+ with:
44
+ python-version: ${{ matrix.python-version }}
45
+ cache: pip
46
+ cache-dependency-path: pyproject.toml
47
+
48
+ - name: Install package and test dependencies
49
+ run: |
50
+ python -m pip install --upgrade pip
51
+ python -m pip install -e ".[test]"
52
+
53
+ - name: Install SymPy
54
+ if: matrix.backend == 'sympy'
55
+ run: python -m pip install sympy
56
+
57
+ - name: Run Ruff
58
+ run: |
59
+ ruff check .
60
+ ruff format --check .
61
+
62
+ - name: Run tests
63
+ run: |
64
+ pytest -q
65
+ python -m doctest README.md docs/*.md docs/catalogues/*.md
66
+
67
+ numpy:
68
+ name: NumPy ${{ matrix.numpy }} (Python ${{ matrix.python-version }})
69
+ runs-on: ubuntu-latest
70
+ strategy:
71
+ fail-fast: false
72
+ matrix:
73
+ include:
74
+ - python-version: '3.12'
75
+ numpy: '2.1.*'
76
+ - python-version: '3.14'
77
+ numpy: 'latest'
78
+ steps:
79
+ - uses: actions/checkout@v6
80
+ - uses: actions/setup-python@v7
81
+ with:
82
+ python-version: ${{ matrix.python-version }}
83
+ - name: Install package and dependencies
84
+ run: python -m pip install -e '.[test,numpy]' 'matplotlib>=3.9'
85
+ - name: Select minimum NumPy
86
+ if: matrix.numpy != 'latest'
87
+ run: python -m pip install 'numpy==${{ matrix.numpy }}'
88
+ - name: Run tests and typing
89
+ run: |
90
+ pytest -q
91
+ python -m doctest README.md docs/*.md docs/catalogues/*.md
92
+
93
+ installed:
94
+ name: Installed wheel (Python 3.12)
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v6
98
+ - uses: actions/setup-python@v7
99
+ with:
100
+ python-version: '3.12'
101
+
102
+ - name: Install build and type-check dependencies
103
+ run: python -m pip install build twine 'pyright>=1.1.414'
104
+
105
+ - name: Build and check distributions
106
+ id: version
107
+ run: |
108
+ python - <<'PY'
109
+ import os
110
+ import tomllib
111
+ from pathlib import Path
112
+
113
+ version = tomllib.loads(Path('pyproject.toml').read_text())['project']['version']
114
+ with Path(os.environ['GITHUB_OUTPUT']).open('a') as output:
115
+ output.write(f'version={version}\n')
116
+ PY
117
+ python -m build
118
+ python -m twine check --strict dist/*
119
+
120
+ - name: Install built wheel and copy consumers outside the checkout
121
+ run: |
122
+ python -m pip install --no-deps dist/*.whl
123
+ mkdir -p "$RUNNER_TEMP/unitx-consumers"
124
+ cp .github/consumers/*.py .github/consumers/pyrightconfig.json "$RUNNER_TEMP/unitx-consumers/"
125
+
126
+ - name: Check installed scalar runtime and types without NumPy
127
+ working-directory: ${{ runner.temp }}/unitx-consumers
128
+ env:
129
+ EXPECTED_VERSION: ${{ steps.version.outputs.version }}
130
+ run: |
131
+ python -I scalars.py
132
+ python -m pyright --project pyrightconfig.json --pythonpath "$(command -v python)" scalars.py
133
+
134
+ - name: Install optional NumPy
135
+ run: python -m pip install 'numpy>=2.1'
136
+
137
+ - name: Check installed NumPy runtime and types
138
+ working-directory: ${{ runner.temp }}/unitx-consumers
139
+ run: |
140
+ python -I arrays.py
141
+ python -m pyright --project pyrightconfig.json --pythonpath "$(command -v python)" arrays.py
@@ -54,7 +54,7 @@ jobs:
54
54
  python -m ruff check .
55
55
  python -m ruff format --check .
56
56
  python -m pytest -q
57
- python -m doctest README.md docs/*.md
57
+ python -m doctest README.md docs/*.md docs/catalogues/*.md
58
58
 
59
59
  - name: Build wheel and source distribution
60
60
  run: python -m build
@@ -62,28 +62,28 @@ jobs:
62
62
  - name: Check distribution metadata
63
63
  run: python -m twine check --strict dist/*
64
64
 
65
- - name: Install built wheel
66
- run: python -m pip install --force-reinstall --no-deps dist/*.whl
65
+ - name: Install built wheel and copy consumers outside the checkout
66
+ run: |
67
+ python -m pip install --force-reinstall --no-deps dist/*.whl
68
+ mkdir -p "$RUNNER_TEMP/unitx-consumers"
69
+ cp .github/consumers/*.py .github/consumers/pyrightconfig.json "$RUNNER_TEMP/unitx-consumers/"
67
70
 
68
- - name: Check installed package
69
- working-directory: ${{ runner.temp }}
71
+ - name: Check installed scalar runtime and types without NumPy
72
+ working-directory: ${{ runner.temp }}/unitx-consumers
70
73
  env:
71
74
  EXPECTED_VERSION: ${{ steps.version.outputs.version }}
72
75
  run: |
73
- python - <<'PY'
74
- import os
75
- from fractions import Fraction
76
- from importlib.metadata import version
77
- from importlib.resources import files
76
+ python -I scalars.py
77
+ python -m pyright --project pyrightconfig.json --pythonpath "$(command -v python)" scalars.py
78
78
 
79
- from unitx.quantity import Quantity
80
- from unitx.unit import Unit
79
+ - name: Install optional NumPy
80
+ run: python -m pip install 'numpy>=2.1'
81
81
 
82
- assert version('unitx') == os.environ['EXPECTED_VERSION']
83
- assert files('unitx').joinpath('py.typed').is_file()
84
- assert Quantity(1, 'cm').to('m').value == Fraction(1, 100)
85
- assert 2 * Unit('m') == Quantity(200, 'cm')
86
- PY
82
+ - name: Check installed NumPy runtime and types
83
+ working-directory: ${{ runner.temp }}/unitx-consumers
84
+ run: |
85
+ python -I arrays.py
86
+ python -m pyright --project pyrightconfig.json --pythonpath "$(command -v python)" arrays.py
87
87
 
88
88
  - name: Store distributions
89
89
  uses: actions/upload-artifact@v5
@@ -71,9 +71,6 @@ instance/
71
71
  # Sphinx documentation
72
72
  docs/_build/
73
73
 
74
- # Local design discussion, not public documentation
75
- /docs/design-affine-log.md
76
-
77
74
  # PyBuilder
78
75
  .pybuilder/
79
76
  target/
@@ -0,0 +1,14 @@
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-24.04
5
+ tools:
6
+ python: '3.12'
7
+
8
+ mkdocs:
9
+ configuration: mkdocs.yml
10
+ fail_on_warning: true
11
+
12
+ python:
13
+ install:
14
+ - requirements: docs/requirements.txt
@@ -2,6 +2,74 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.14.2] - 2026-10-24
6
+
7
+ ### Documentation
8
+
9
+ - Add a MkDocs site with pinned build requirements, Mermaid diagrams,
10
+ Read the Docs configuration and strict CI checks for internal links and anchors.
11
+ Hosting can use an available project name; no documentation domain is assumed.
12
+ - Fix the README's logarithmic-guide link for package-index rendering.
13
+
14
+ ### Internal
15
+
16
+ - Execute the README's Python code blocks directly and check the blackbody
17
+ plot's units, curve count and numerical data.
18
+ - Extend installed-wheel runtime and type consumers with mathematical functions,
19
+ integer clock readings, duration arithmetic and datetime precision checks.
20
+ - Add reproducible scalar benchmarks for parsing, conversion, comparison and
21
+ exact factors, recording the environment and timing parameters as JSON.
22
+
23
+ ## [0.14.1] - 2026-10-24
24
+
25
+ ### Documentation
26
+
27
+ - Clarify concurrent Registry reads, application synchronization during
28
+ registration, the absence of a freeze/snapshot API, and custom-definition
29
+ sharing within and across processes.
30
+
31
+ ## [0.14.0] - 2026-10-24
32
+
33
+ ### Added
34
+
35
+ - `Registry` provides independent parsing environments, atomic `include(*units)`
36
+ and explicit `with registry.defining():` blocks with configurable prefix defaults.
37
+ `UnitCatalog` collects read-only unit attributes and provides catalogue parsing.
38
+ - `unitx.imperial` supplies British Imperial, historical English and US customary
39
+ catalogues with independent spellings and exact documented scales, including
40
+ US survey feet, thermochemical Btu, circular mil and conventional water/mercury
41
+ pressure units. Importing a catalogue does not change default parsing.
42
+
43
+ ### Changed
44
+
45
+ - **Breaking:** `Unit` now stores its complete exact extra scale; `ScaledUnit` is
46
+ removed. Construct scaled units with `Unit(base, scale=...)` or Factor operands.
47
+ - **Breaking:** `.define()` creates standalone definitions outside an explicit
48
+ Registry block. Extend default parsing explicitly with `default_registry`.
49
+ Formal identity follows shared definition objects rather than symbols;
50
+ independent same-name definitions remain distinct. Import `UnitDefinition`
51
+ and `PrefixPolicy` from `unitx.unitdefinition`; registry table exports are removed.
52
+ - **Breaking:** `LogUnit` and `LogQuantity` replace logarithmic uses of AffineUnit
53
+ and AffineQuantity. Point `.to()` stays within its coordinate family; use
54
+ `.to_quantity()` and `LogQuantity.from_quantity()` at physical boundaries.
55
+ - **Breaking:** Dimension equality includes the internal logarithmic axis.
56
+ Use `.physical` for seven-axis physical comparisons; separate `log_degree`
57
+ attributes are removed.
58
+ - **Breaking:** Fahrenheit and Rankine move from SI/default parsing to
59
+ `imperial.degF` and `imperial.degR`.
60
+ - **Breaking:** Scalar JSON uses format 2 with definition IDs and an optional
61
+ receiving Registry; format 1 and older pickle payloads are unsupported.
62
+ Reconstruction requires existing definitions and checks their metadata.
63
+ - Logarithmic coordinates retain exact `reference` and `at` anchors; `origin`
64
+ is a derived numerical view. Affine defining readings that require an
65
+ unrepresentable symbolic origin sum are rejected; supply the origin at `at=0`.
66
+
67
+ ### Fixed
68
+
69
+ - Preserve private coordinate mappings through retained linear projections,
70
+ and reconstruct private coordinate/point reprs even when their symbols coincide
71
+ with default logarithmic difference units.
72
+
5
73
  ## [0.13.4] - 2026-10-23
6
74
 
7
75
  ### Fixed
@@ -23,11 +23,16 @@ Pytest collects runtime tests and source doctests. `tests/test_typing.py` runs
23
23
  Pyright against scalar and optional NumPy consumers, including scalar checks
24
24
  with NumPy absent. The [typing guide](docs/typing.md) explains the boundaries.
25
25
  CI covers Python 3.12–3.15, built-in/SymPy factorization and minimum/current NumPy.
26
+ An additional CI job builds the distributions and checks their wheel with the
27
+ consumers in `.github/consumers/`. It copies those scripts and their independent
28
+ Pyright configuration outside the checkout, first checking scalar runtime and
29
+ types without NumPy, then installing NumPy and checking array runtime and types.
30
+ The release workflow runs the same consumers before uploading distributions.
26
31
 
27
32
  Run the executable Markdown examples as well:
28
33
 
29
34
  ```sh
30
- python -m doctest README.md docs/*.md
35
+ python -m doctest README.md docs/*.md docs/catalogues/*.md
31
36
  ```
32
37
 
33
38
  Use tracked public guides for release validation; local ignored design notes
@@ -35,6 +40,33 @@ are not release documentation. Documentation examples should be reproducible
35
40
  and avoid modifying source files. Notebook source is checked and executed by
36
41
  the tests when its optional dependencies are present; see the
37
42
  [notebook policy](examples/README.md#keep-git-history-small).
43
+ With NumPy and Matplotlib installed, the tests also execute the README's
44
+ `python` code blocks in order and check the blackbody plot's numerical data.
45
+
46
+ ## Documentation site
47
+
48
+ Build or preview the existing Markdown guides with MkDocs:
49
+
50
+ ```sh
51
+ python -m pip install -r docs/requirements.txt
52
+ python -m mkdocs build --strict
53
+ python -m mkdocs serve
54
+ ```
55
+
56
+ The build writes to the ignored `site/` directory. CI checks the strict build,
57
+ including internal links and anchors. `.readthedocs.yaml` uses the same pinned
58
+ MkDocs requirement; connect the repository to a Read the Docs project to host
59
+ it. These configuration files alone do not create or publish a hosted site.
60
+ Mermaid diagrams load a pinned JavaScript module from jsDelivr in the browser;
61
+ building the HTML does not require that CDN. Repository files outside `docs/`
62
+ use GitHub links so they also work on a hosted documentation site.
63
+
64
+ ## Performance measurements
65
+
66
+ Run the [scalar and NumPy benchmarks](benchmarks/README.md) when investigating
67
+ performance changes. Their JSON reports record the environment and per-call
68
+ timings; compare runs under the same conditions rather than setting CI timing
69
+ thresholds.
38
70
 
39
71
  ## Keep changes reviewable
40
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.13.4
3
+ Version: 0.14.2
4
4
  Summary: Physical units, quantities, and unit conversion with exact unit algebra, type hints, and NumPy integration.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Documentation, https://github.com/DaitTHU/unitx/blob/main/docs/README.md
@@ -102,13 +102,14 @@ Use `.isclose()` for approximate comparisons.
102
102
  Use `@` for an absolute coordinate reading and `*` for a linear difference:
103
103
 
104
104
  ```pycon
105
+ >>> from unitx.imperial import imperial
105
106
  >>> difference = 20 * si.degC # A temperature difference of 20 degrees
106
107
  >>> room = 20 @ si.degC # A reading of 20 degrees Celsius
107
108
  >>> difference.value_in(si.K)
108
109
  20
109
110
  >>> room.value_in(si.K)
110
111
  Fraction(5863, 20)
111
- >>> print(room.to(si.degF))
112
+ >>> print(room.to(imperial.degF))
112
113
  68 °F
113
114
  >>> print(room + 5 * si.K)
114
115
  25 °C
@@ -119,13 +120,15 @@ True
119
120
 
120
121
  ```
121
122
 
122
- Points are `AffineQuantity` objects; differences are `Quantity` objects.
123
+ Linear affine points are `AffineQuantity` objects and logarithmic readings are
124
+ `LogQuantity` objects; differences are `Quantity` objects.
123
125
  Multiplying by `si.degC` constructs a difference; use `@` or the explicit
124
126
  `AffineQuantity(20, si.degC)` constructor for an absolute reading.
125
127
  Adding two points raises `TypeError`; subtract them to obtain a difference.
126
128
  Use `.to_quantity()` to turn a point into an ordinary physical quantity for
127
129
  further arithmetic. Read the [point and difference guide](https://github.com/DaitTHU/unitx/blob/main/docs/affine.md)
128
- for temperature, dBm/dBW/dBV, and logarithmic ratio rules.
130
+ for temperatures and [logarithmic quantities](https://github.com/DaitTHU/unitx/blob/main/docs/logarithmic.md) for
131
+ dBm/dBW/dBV and ratio rules.
129
132
 
130
133
  ## Mathematical functions and time
131
134
 
@@ -240,7 +243,7 @@ or go directly to a topic:
240
243
  | Topic | Guide |
241
244
  | --- | --- |
242
245
  | Construction, arithmetic, conversion and unit expressions | [Units and quantities](https://github.com/DaitTHU/unitx/blob/main/docs/units.md) |
243
- | Ready-to-use units, constants and spelling conventions | [Catalogue](https://github.com/DaitTHU/unitx/blob/main/docs/catalogue.md) |
246
+ | Ready-to-use units, constants and spelling conventions | [Catalogue](https://github.com/DaitTHU/unitx/blob/main/docs/catalogues/README.md) |
244
247
  | Exact extra scales and process-local registration | [Custom units](https://github.com/DaitTHU/unitx/blob/main/docs/custom-units.md) |
245
248
  | Fraction, Decimal, equality, powers and tolerances | [Scalar numerical behavior](https://github.com/DaitTHU/unitx/blob/main/docs/scalars.md) |
246
249
  | Notebook, text, LaTeX, HTML and siunitx output | [Display and formatting](https://github.com/DaitTHU/unitx/blob/main/docs/display.md) |
@@ -74,13 +74,14 @@ Use `.isclose()` for approximate comparisons.
74
74
  Use `@` for an absolute coordinate reading and `*` for a linear difference:
75
75
 
76
76
  ```pycon
77
+ >>> from unitx.imperial import imperial
77
78
  >>> difference = 20 * si.degC # A temperature difference of 20 degrees
78
79
  >>> room = 20 @ si.degC # A reading of 20 degrees Celsius
79
80
  >>> difference.value_in(si.K)
80
81
  20
81
82
  >>> room.value_in(si.K)
82
83
  Fraction(5863, 20)
83
- >>> print(room.to(si.degF))
84
+ >>> print(room.to(imperial.degF))
84
85
  68 °F
85
86
  >>> print(room + 5 * si.K)
86
87
  25 °C
@@ -91,13 +92,15 @@ True
91
92
 
92
93
  ```
93
94
 
94
- Points are `AffineQuantity` objects; differences are `Quantity` objects.
95
+ Linear affine points are `AffineQuantity` objects and logarithmic readings are
96
+ `LogQuantity` objects; differences are `Quantity` objects.
95
97
  Multiplying by `si.degC` constructs a difference; use `@` or the explicit
96
98
  `AffineQuantity(20, si.degC)` constructor for an absolute reading.
97
99
  Adding two points raises `TypeError`; subtract them to obtain a difference.
98
100
  Use `.to_quantity()` to turn a point into an ordinary physical quantity for
99
101
  further arithmetic. Read the [point and difference guide](https://github.com/DaitTHU/unitx/blob/main/docs/affine.md)
100
- for temperature, dBm/dBW/dBV, and logarithmic ratio rules.
102
+ for temperatures and [logarithmic quantities](https://github.com/DaitTHU/unitx/blob/main/docs/logarithmic.md) for
103
+ dBm/dBW/dBV and ratio rules.
101
104
 
102
105
  ## Mathematical functions and time
103
106
 
@@ -212,7 +215,7 @@ or go directly to a topic:
212
215
  | Topic | Guide |
213
216
  | --- | --- |
214
217
  | Construction, arithmetic, conversion and unit expressions | [Units and quantities](https://github.com/DaitTHU/unitx/blob/main/docs/units.md) |
215
- | Ready-to-use units, constants and spelling conventions | [Catalogue](https://github.com/DaitTHU/unitx/blob/main/docs/catalogue.md) |
218
+ | Ready-to-use units, constants and spelling conventions | [Catalogue](https://github.com/DaitTHU/unitx/blob/main/docs/catalogues/README.md) |
216
219
  | Exact extra scales and process-local registration | [Custom units](https://github.com/DaitTHU/unitx/blob/main/docs/custom-units.md) |
217
220
  | Fraction, Decimal, equality, powers and tolerances | [Scalar numerical behavior](https://github.com/DaitTHU/unitx/blob/main/docs/scalars.md) |
218
221
  | Notebook, text, LaTeX, HTML and siunitx output | [Display and formatting](https://github.com/DaitTHU/unitx/blob/main/docs/display.md) |
@@ -0,0 +1,30 @@
1
+ # Performance measurements
2
+
3
+ Install the checkout in the environment to be measured, then run from the
4
+ repository root:
5
+
6
+ ```sh
7
+ python -m pip install -e .
8
+ python benchmarks/scalars.py --repeat 7 --number 1000
9
+ ```
10
+
11
+ The scalar script measures fresh expression parsing, Fraction/Decimal
12
+ conversion, cross-unit equality and ordering, factor construction, rational
13
+ powers and symbolic cancellation. Input quantities and factors are prepared
14
+ outside the timed operations; each parsing call constructs a new parser.
15
+ The JSON output records median seconds per call, Python/platform, the selected
16
+ factorization backend, Decimal precision and timing parameters. SymPy is used
17
+ automatically when installed; compare backends in separate environments.
18
+
19
+ Array write measurements require the optional NumPy dependency:
20
+
21
+ ```sh
22
+ python -m pip install -e '.[numpy]'
23
+ python benchmarks/numpy_writes.py --size 1000000 --repeat 7 --number 20
24
+ ```
25
+
26
+ That script also reports peak traced allocations for each operation. Timing
27
+ and allocation tracing are separate measurements. Run baseline and candidate
28
+ with the same interpreter, dependencies, arguments and machine conditions.
29
+ These are development measurements, not performance guarantees; CI does not
30
+ enforce timing thresholds.
@@ -0,0 +1,68 @@
1
+ """Time scalar parsing, conversion, comparison and exact factor operations.
2
+
3
+ Run from an installed checkout: python benchmarks/scalars.py
4
+ Each parse creates a new UnitParser; conversion inputs are prepared beforehand.
5
+ Results describe this process and machine, not a cross-machine performance claim.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import platform
11
+ import statistics
12
+ import timeit
13
+ from decimal import Decimal, getcontext
14
+ from fractions import Fraction
15
+
16
+ from unitx import Quantity, Unit, pi
17
+ from unitx.factor import Factor, factorint
18
+ from unitx.si import si
19
+
20
+
21
+ def main() -> None:
22
+ parser = argparse.ArgumentParser(description=__doc__)
23
+ parser.add_argument('--repeat', type=int, default=7)
24
+ parser.add_argument('--number', type=int, default=1000)
25
+ options = parser.parse_args()
26
+ if options.repeat < 1 or options.number < 1:
27
+ parser.error('--repeat and --number must be positive integers.')
28
+
29
+ rational = Quantity(Fraction(1, 3), si.m)
30
+ decimal = Quantity(Decimal('1.25'), si.m)
31
+ equivalent = Quantity(Fraction(100, 3), si.cm)
32
+ greater = Quantity(Fraction(101, 3), si.cm)
33
+ base = Factor(2) * pi
34
+ radical = base ** Fraction(1, 3)
35
+ cases = {
36
+ 'parse_expression': lambda: Unit('kg m2/s3'),
37
+ 'convert_fraction': lambda: rational.value_in(si.cm),
38
+ 'convert_decimal': lambda: decimal.value_in(si.cm),
39
+ 'equal_cross_unit': lambda: rational == equivalent,
40
+ 'order_cross_unit': lambda: rational < greater,
41
+ 'factor_construct': lambda: Factor(Fraction(123456, 1001)),
42
+ 'factor_rational_power': lambda: base ** Fraction(1, 3),
43
+ 'factor_cancel': lambda: radical**3 / base,
44
+ }
45
+ results = {}
46
+ for name, operation in cases.items():
47
+ operation()
48
+ samples = timeit.repeat(operation, repeat=options.repeat, number=options.number)
49
+ results[name] = {'seconds': statistics.median(samples) / options.number}
50
+ backend = 'sympy' if factorint.__module__.startswith('sympy') else 'builtin'
51
+ print(
52
+ json.dumps(
53
+ {
54
+ 'python': platform.python_version(),
55
+ 'platform': platform.platform(),
56
+ 'factorization_backend': backend,
57
+ 'decimal_precision': getcontext().prec,
58
+ 'repeat': options.repeat,
59
+ 'number': options.number,
60
+ 'cases': results,
61
+ },
62
+ indent=2,
63
+ )
64
+ )
65
+
66
+
67
+ if __name__ == '__main__':
68
+ main()