unitx 0.14.2__tar.gz → 0.14.4__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 (157) hide show
  1. {unitx-0.14.2 → unitx-0.14.4}/.github/consumers/arrays.py +5 -0
  2. {unitx-0.14.2 → unitx-0.14.4}/.github/consumers/scalars.py +17 -1
  3. {unitx-0.14.2 → unitx-0.14.4}/.github/workflows/ci.yml +1 -0
  4. unitx-0.14.4/.github/workflows/pages.yml +52 -0
  5. {unitx-0.14.2 → unitx-0.14.4}/.github/workflows/release.yml +1 -0
  6. {unitx-0.14.2 → unitx-0.14.4}/CHANGELOG.md +33 -0
  7. {unitx-0.14.2 → unitx-0.14.4}/CONTRIBUTING.md +11 -3
  8. {unitx-0.14.2 → unitx-0.14.4}/PKG-INFO +3 -3
  9. {unitx-0.14.2 → unitx-0.14.4}/README.md +1 -1
  10. {unitx-0.14.2 → unitx-0.14.4}/conftest.py +1 -1
  11. {unitx-0.14.2 → unitx-0.14.4}/docs/architecture.md +6 -0
  12. {unitx-0.14.2 → unitx-0.14.4}/docs/logarithmic.md +37 -0
  13. {unitx-0.14.2 → unitx-0.14.4}/docs/typing.md +20 -1
  14. {unitx-0.14.2 → unitx-0.14.4}/examples/README.md +9 -0
  15. unitx-0.14.4/examples/impedance.md +66 -0
  16. unitx-0.14.4/examples/impedance.py +121 -0
  17. unitx-0.14.4/examples/smith_chart.ipynb +352 -0
  18. {unitx-0.14.2 → unitx-0.14.4}/mkdocs.yml +1 -0
  19. {unitx-0.14.2 → unitx-0.14.4}/pyproject.toml +2 -2
  20. {unitx-0.14.2 → unitx-0.14.4}/pyrightconfig.json +4 -3
  21. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/logunit.py +38 -0
  22. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/unit.py +19 -0
  23. unitx-0.14.4/tests/numpy_typing/check_asarray.py +13 -0
  24. {unitx-0.14.2 → unitx-0.14.4}/tests/numpy_typing/check_numpy.py +18 -27
  25. {unitx-0.14.2 → unitx-0.14.4}/tests/test_affine.py +48 -35
  26. {unitx-0.14.2 → unitx-0.14.4}/tests/test_affinequantity.py +25 -19
  27. {unitx-0.14.2 → unitx-0.14.4}/tests/test_catalog_collection.py +13 -10
  28. unitx-0.14.4/tests/test_decibel_call.py +158 -0
  29. {unitx-0.14.2 → unitx-0.14.4}/tests/test_definition_scope.py +29 -26
  30. {unitx-0.14.2 → unitx-0.14.4}/tests/test_display.py +36 -28
  31. {unitx-0.14.2 → unitx-0.14.4}/tests/test_examples.py +7 -3
  32. {unitx-0.14.2 → unitx-0.14.4}/tests/test_factor.py +6 -1
  33. {unitx-0.14.2 → unitx-0.14.4}/tests/test_immutable.py +4 -1
  34. unitx-0.14.4/tests/test_impedance_example.py +118 -0
  35. {unitx-0.14.2 → unitx-0.14.4}/tests/test_imperial.py +12 -10
  36. {unitx-0.14.2 → unitx-0.14.4}/tests/test_linear_boundaries.py +10 -7
  37. {unitx-0.14.2 → unitx-0.14.4}/tests/test_logarithmic_model.py +14 -11
  38. {unitx-0.14.2 → unitx-0.14.4}/tests/test_math.py +11 -9
  39. {unitx-0.14.2 → unitx-0.14.4}/tests/test_math_numpy.py +14 -7
  40. {unitx-0.14.2 → unitx-0.14.4}/tests/test_monomial.py +5 -1
  41. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy.py +65 -50
  42. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_affine.py +23 -14
  43. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_differentiation.py +43 -24
  44. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_integration.py +29 -11
  45. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_multi_outputs.py +30 -19
  46. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_mutation.py +52 -27
  47. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_outputs.py +36 -18
  48. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_products.py +67 -28
  49. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numpy_statistics.py +47 -18
  50. {unitx-0.14.2 → unitx-0.14.4}/tests/test_quantity.py +94 -57
  51. {unitx-0.14.2 → unitx-0.14.4}/tests/test_registry.py +32 -29
  52. {unitx-0.14.2 → unitx-0.14.4}/tests/test_registry_scope.py +19 -17
  53. {unitx-0.14.2 → unitx-0.14.4}/tests/test_serialization.py +60 -43
  54. {unitx-0.14.2 → unitx-0.14.4}/tests/test_si.py +7 -2
  55. {unitx-0.14.2 → unitx-0.14.4}/tests/test_siunitx_display.py +13 -8
  56. {unitx-0.14.2 → unitx-0.14.4}/tests/test_time.py +25 -23
  57. {unitx-0.14.2 → unitx-0.14.4}/tests/test_typing.py +25 -16
  58. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unicode_units.py +3 -1
  59. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unit.py +15 -8
  60. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unit_definition.py +56 -18
  61. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unitregistry.py +35 -13
  62. unitx-0.14.4/tests/typing/check_impedance_example.py +31 -0
  63. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_logarithmic.py +5 -0
  64. unitx-0.14.2/tests/example_typing/check_blackbody.py +0 -27
  65. {unitx-0.14.2 → unitx-0.14.4}/.github/consumers/pyrightconfig.json +0 -0
  66. {unitx-0.14.2 → unitx-0.14.4}/.gitignore +0 -0
  67. {unitx-0.14.2 → unitx-0.14.4}/.readthedocs.yaml +0 -0
  68. {unitx-0.14.2 → unitx-0.14.4}/LICENSE +0 -0
  69. {unitx-0.14.2 → unitx-0.14.4}/benchmarks/README.md +0 -0
  70. {unitx-0.14.2 → unitx-0.14.4}/benchmarks/numpy_writes.py +0 -0
  71. {unitx-0.14.2 → unitx-0.14.4}/benchmarks/scalars.py +0 -0
  72. {unitx-0.14.2 → unitx-0.14.4}/docs/README.md +0 -0
  73. {unitx-0.14.2 → unitx-0.14.4}/docs/affine.md +0 -0
  74. {unitx-0.14.2 → unitx-0.14.4}/docs/catalogues/README.md +0 -0
  75. {unitx-0.14.2 → unitx-0.14.4}/docs/catalogues/imperial.md +0 -0
  76. {unitx-0.14.2 → unitx-0.14.4}/docs/catalogues/si.md +0 -0
  77. {unitx-0.14.2 → unitx-0.14.4}/docs/custom-units.md +0 -0
  78. {unitx-0.14.2 → unitx-0.14.4}/docs/display.md +0 -0
  79. {unitx-0.14.2 → unitx-0.14.4}/docs/images/blackbody.svg +0 -0
  80. {unitx-0.14.2 → unitx-0.14.4}/docs/javascripts/mermaid.mjs +0 -0
  81. {unitx-0.14.2 → unitx-0.14.4}/docs/migration.md +0 -0
  82. {unitx-0.14.2 → unitx-0.14.4}/docs/numpy.md +0 -0
  83. {unitx-0.14.2 → unitx-0.14.4}/docs/requirements.txt +0 -0
  84. {unitx-0.14.2 → unitx-0.14.4}/docs/scalars.md +0 -0
  85. {unitx-0.14.2 → unitx-0.14.4}/docs/serialization.md +0 -0
  86. {unitx-0.14.2 → unitx-0.14.4}/docs/stdlib.md +0 -0
  87. {unitx-0.14.2 → unitx-0.14.4}/docs/units.md +0 -0
  88. {unitx-0.14.2 → unitx-0.14.4}/examples/arrhenius_fit.ipynb +0 -0
  89. {unitx-0.14.2 → unitx-0.14.4}/examples/blackbody.ipynb +0 -0
  90. {unitx-0.14.2 → unitx-0.14.4}/examples/damped_oscillator.ipynb +0 -0
  91. {unitx-0.14.2 → unitx-0.14.4}/examples/gaussian_wavepacket.ipynb +0 -0
  92. {unitx-0.14.2 → unitx-0.14.4}/examples/optical_attenuation.ipynb +0 -0
  93. {unitx-0.14.2 → unitx-0.14.4}/examples/pendulum_uncertainty.ipynb +0 -0
  94. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/__init__.py +0 -0
  95. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_constnamespace.py +0 -0
  96. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_conversion.py +0 -0
  97. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/__init__.py +0 -0
  98. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/affine.py +0 -0
  99. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/definition.py +0 -0
  100. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/logarithmic.py +0 -0
  101. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/logarithms.py +0 -0
  102. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/points.py +0 -0
  103. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_coordinates/resolve.py +0 -0
  104. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_display.py +0 -0
  105. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_immutable.py +0 -0
  106. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_numerics.py +0 -0
  107. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_numpy.py +0 -0
  108. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_numpy_typing.py +0 -0
  109. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_operands.py +0 -0
  110. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_registry/__init__.py +0 -0
  111. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_registry/builtin_units.py +0 -0
  112. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_registry/prefixes.py +0 -0
  113. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_registry/reconstruction.py +0 -0
  114. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_scaling.py +0 -0
  115. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/_unicode.py +0 -0
  116. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/affinequantity.py +0 -0
  117. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/affineunit.py +0 -0
  118. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/dimension.py +0 -0
  119. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/exceptions.py +0 -0
  120. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/factor.py +0 -0
  121. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/imperial/__init__.py +0 -0
  122. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/imperial/units.py +0 -0
  123. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/logquantity.py +0 -0
  124. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/math.py +0 -0
  125. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/monomial.py +0 -0
  126. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/py.typed +0 -0
  127. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/quantity.py +0 -0
  128. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/serialization.py +0 -0
  129. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/si/__init__.py +0 -0
  130. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/si/bases.py +0 -0
  131. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/si/constants.py +0 -0
  132. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/si/units.py +0 -0
  133. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/singleunit.py +0 -0
  134. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/time.py +0 -0
  135. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/unitcatalog.py +0 -0
  136. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/unitdefinition.py +0 -0
  137. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/unitparser.py +0 -0
  138. {unitx-0.14.2 → unitx-0.14.4}/src/unitx/unitregistry.py +0 -0
  139. {unitx-0.14.2 → unitx-0.14.4}/tests/__init__.py +0 -0
  140. {unitx-0.14.2 → unitx-0.14.4}/tests/_notebooks.py +0 -0
  141. {unitx-0.14.2 → unitx-0.14.4}/tests/numpy_typing/check_logarithmic.py +0 -0
  142. {unitx-0.14.2 → unitx-0.14.4}/tests/numpy_typing/check_math.py +0 -0
  143. {unitx-0.14.2 → unitx-0.14.4}/tests/test_builtin_definitions.py +0 -0
  144. {unitx-0.14.2 → unitx-0.14.4}/tests/test_dimension.py +0 -0
  145. {unitx-0.14.2 → unitx-0.14.4}/tests/test_import.py +0 -0
  146. {unitx-0.14.2 → unitx-0.14.4}/tests/test_numeric.py +0 -0
  147. {unitx-0.14.2 → unitx-0.14.4}/tests/test_singleunit.py +0 -0
  148. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unicode.py +0 -0
  149. {unitx-0.14.2 → unitx-0.14.4}/tests/test_unitparser.py +0 -0
  150. {unitx-0.14.2 → unitx-0.14.4}/tests/test_warnings.py +0 -0
  151. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_affine.py +0 -0
  152. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_imperial.py +0 -0
  153. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_math_time.py +0 -0
  154. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_quantity.py +0 -0
  155. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_registry.py +0 -0
  156. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_serialization.py +0 -0
  157. {unitx-0.14.2 → unitx-0.14.4}/tests/typing/check_si.py +0 -0
@@ -17,6 +17,11 @@ def check_arrays(values: NDArray[np.float64]) -> None:
17
17
  assert level.value is values
18
18
  np.testing.assert_allclose(level.to_quantity(si.W).value, [0.001, 0.1, 1.0])
19
19
 
20
+ voltage_level = LogQuantity(values, si.dB(si.mV))
21
+ assert_type(voltage_level, LogQuantity[NDArray[np.float64]])
22
+ assert voltage_level.value is values
23
+ np.testing.assert_allclose(voltage_level.to_quantity(si.mV).value, 10 ** (values / 20))
24
+
20
25
  distance = Quantity(values.copy(), si.m)
21
26
  assert_type(distance, Quantity[NDArray[np.float64]])
22
27
  assert_type(distance.unit, Unit)
@@ -12,7 +12,17 @@ from pathlib import Path
12
12
  from typing import Final, assert_type
13
13
 
14
14
  import unitx
15
- from unitx import AffineQuantity, LogQuantity, Quantity, Registry, Unit, UnitCatalog, math, time
15
+ from unitx import (
16
+ AffineQuantity,
17
+ LogQuantity,
18
+ LogUnit,
19
+ Quantity,
20
+ Registry,
21
+ Unit,
22
+ UnitCatalog,
23
+ math,
24
+ time,
25
+ )
16
26
  from unitx.factor import Factor
17
27
  from unitx.imperial import imperial
18
28
  from unitx.serialization import from_dict, to_dict
@@ -70,6 +80,12 @@ assert celsius.to_quantity(si.K) == Fraction(5863, 20) * si.K
70
80
  assert (32 @ imperial.degF).to(si.degC) == 0 @ si.degC
71
81
  assert level.to_quantity(si.W) == 1 * si.W
72
82
  assert LogQuantity.from_quantity(1 * si.W, si.dBm) == level
83
+ assert_type(si.dB(si.V), LogUnit)
84
+ assert si.dB(si.mW) is si.dBm
85
+ assert Unit('decibel')(si.V) is si.dBV
86
+ millivolt_level = Decimal(20) @ si.dB(si.mV)
87
+ assert_type(millivolt_level, LogQuantity[Decimal])
88
+ assert millivolt_level.to_quantity(si.mV).value == Decimal(10)
73
89
 
74
90
  registry = Registry([si.m])
75
91
  with registry.defining():
@@ -57,6 +57,7 @@ jobs:
57
57
  - name: Run Ruff
58
58
  run: |
59
59
  ruff check .
60
+ ruff check tests conftest.py --select ANN --ignore ANN401
60
61
  ruff format --check .
61
62
 
62
63
  - name: Run tests
@@ -0,0 +1,52 @@
1
+ name: Documentation site
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'docs/**'
8
+ - 'mkdocs.yml'
9
+ - '.github/workflows/pages.yml'
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+ pages: read
15
+
16
+ concurrency:
17
+ group: github-pages
18
+ cancel-in-progress: false
19
+
20
+ jobs:
21
+ build:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v6
25
+ - uses: actions/setup-python@v7
26
+ with:
27
+ python-version: '3.12'
28
+ - name: Configure GitHub Pages
29
+ uses: actions/configure-pages@v5
30
+ - name: Install documentation dependencies
31
+ run: python -m pip install -r docs/requirements.txt
32
+ - name: Build documentation
33
+ run: python -m mkdocs build --strict
34
+ - name: Upload site
35
+ uses: actions/upload-pages-artifact@v4
36
+ with:
37
+ path: site
38
+
39
+ deploy:
40
+ needs: build
41
+ if: github.ref == 'refs/heads/main'
42
+ runs-on: ubuntu-latest
43
+ permissions:
44
+ pages: write
45
+ id-token: write
46
+ environment:
47
+ name: github-pages
48
+ url: ${{ steps.deployment.outputs.page_url }}
49
+ steps:
50
+ - name: Deploy GitHub Pages
51
+ id: deployment
52
+ uses: actions/deploy-pages@v4
@@ -52,6 +52,7 @@ jobs:
52
52
  - name: Validate source
53
53
  run: |
54
54
  python -m ruff check .
55
+ python -m ruff check tests conftest.py --select ANN --ignore ANN401
55
56
  python -m ruff format --check .
56
57
  python -m pytest -q
57
58
  python -m doctest README.md docs/*.md docs/catalogues/*.md
@@ -2,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.14.4] - 2026-10-24
6
+
7
+ ### Fixed
8
+
9
+ - Correct the Impedance example's mixed-addition result annotations and retain
10
+ its specialized scalar `+=` result types. Document the subclass boundary and
11
+ check it with runtime and static consumers.
12
+
13
+ ### Documentation
14
+
15
+ - Host the MkDocs guides on GitHub Pages with automatic deployment from main.
16
+ Keep Read the Docs deployment pending transfer of the project name.
17
+
18
+ ## [0.14.3] - 2026-10-24
19
+
20
+ ### Added
21
+
22
+ - Allow `si.dB(reference, kind=...)` to construct a logarithmic coordinate.
23
+ Named watts and volts, including SI prefixes, supply conventional defaults;
24
+ common references reuse dBm, dBW and dBV. Other units require an explicit kind.
25
+ - Add a standalone Impedance subclass example with series addition, parallel
26
+ `//` composition, unit conversion and a complex RLC circuit calculation,
27
+ plus a notebook that traces its frequency response on a Smith chart.
28
+
29
+ ### Internal
30
+
31
+ - Consolidate static consumers under scalar and NumPy typing fixtures, checking
32
+ actual notebook source instead of duplicate blackbody examples. Keep the
33
+ NumPy 2.5 asarray regression separately gated and exclude static fixtures from
34
+ pytest collection.
35
+ - Complete runtime-test signature annotations and check them in the existing CI
36
+ and release lint steps.
37
+
5
38
  ## [0.14.2] - 2026-10-24
6
39
 
7
40
  ### Documentation
@@ -15,6 +15,7 @@ without NumPy and the built-in factorization path without SymPy.
15
15
  ```sh
16
16
  python -m pytest -q
17
17
  python -m ruff check .
18
+ python -m ruff check tests conftest.py --select ANN --ignore ANN401
18
19
  python -m ruff format --check .
19
20
  python -m pyright --pythonpath /path/to/your/python
20
21
  ```
@@ -28,6 +29,9 @@ consumers in `.github/consumers/`. It copies those scripts and their independent
28
29
  Pyright configuration outside the checkout, first checking scalar runtime and
29
30
  types without NumPy, then installing NumPy and checking array runtime and types.
30
31
  The release workflow runs the same consumers before uploading distributions.
32
+ Test functions and helpers have annotated parameters and return values. The
33
+ dedicated Ruff command checks those signatures; static inference regressions
34
+ use the separate Pyright consumers rather than annotating expected results.
31
35
 
32
36
  Run the executable Markdown examples as well:
33
37
 
@@ -54,9 +58,13 @@ python -m mkdocs serve
54
58
  ```
55
59
 
56
60
  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.
61
+ including internal links and anchors. The [documentation site](https://daitthu.github.io/unitx/)
62
+ is hosted on GitHub Pages. `.github/workflows/pages.yml` builds and deploys
63
+ changes to `docs/`, `mkdocs.yml` or the workflow on main; it also supports
64
+ manual runs. Repository Settings → Pages must use GitHub Actions as its source.
65
+ `.readthedocs.yaml` retains the same pinned MkDocs requirement for a future
66
+ Read the Docs deployment. Do not connect or publish that project until
67
+ ownership of `unitx.readthedocs.io` has transferred to the project owner.
60
68
  Mermaid diagrams load a pinned JavaScript module from jsDelivr in the browser;
61
69
  building the HTML does not require that CDN. Repository files outside `docs/`
62
70
  use GitHub links so they also work on a hosted documentation site.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.14.2
3
+ Version: 0.14.4
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
- Project-URL: Documentation, https://github.com/DaitTHU/unitx/blob/main/docs/README.md
6
+ Project-URL: Documentation, https://daitthu.github.io/unitx/
7
7
  Project-URL: Source, https://github.com/DaitTHU/unitx
8
8
  Project-URL: Changelog, https://github.com/DaitTHU/unitx/blob/main/CHANGELOG.md
9
9
  Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
@@ -237,7 +237,7 @@ plt.show()
237
237
 
238
238
  ## Read more
239
239
 
240
- Start at the [documentation index](https://github.com/DaitTHU/unitx/blob/main/docs/README.md),
240
+ Start at the [documentation site](https://daitthu.github.io/unitx/),
241
241
  or go directly to a topic:
242
242
 
243
243
  | Topic | Guide |
@@ -209,7 +209,7 @@ plt.show()
209
209
 
210
210
  ## Read more
211
211
 
212
- Start at the [documentation index](https://github.com/DaitTHU/unitx/blob/main/docs/README.md),
212
+ Start at the [documentation site](https://daitthu.github.io/unitx/),
213
213
  or go directly to a topic:
214
214
 
215
215
  | Topic | Guide |
@@ -7,7 +7,7 @@ from unitx.exceptions import InexactFloatWarning
7
7
  from unitx.factor import Factor
8
8
 
9
9
  # Static-only fixtures run through Pyright in tests/test_typing.py.
10
- collect_ignore = ['tests/example_typing', 'tests/numpy_typing']
10
+ collect_ignore = ['tests/typing', 'tests/numpy_typing']
11
11
  if find_spec('numpy') is None:
12
12
  collect_ignore.append('src/unitx/_numpy.py')
13
13
 
@@ -75,6 +75,12 @@ a physical Quantity; `LogQuantity.from_quantity(quantity, coordinate)` enters a
75
75
  logarithmic coordinate. These boundaries prevent an implicit exponential or
76
76
  logarithm from entering ordinary Quantity conversions.
77
77
 
78
+ `Unit.__call__` is a narrow convenience boundary for the formal dB unit. It
79
+ delegates lazily to the logarithmic module, which constructs an ordinary
80
+ LogUnit or reuses a named descriptor. Default conventions recognize watt and
81
+ volt definition identities with their prefixes; they are not inferred from
82
+ dimensions or symbol strings. Calling another Unit raises TypeError.
83
+
78
84
  The difference-unit factor uses the neper scale. The mapping coefficient is
79
85
  `ln(10)/2` for power and `ln(10)` for amplitude, so a reading displacement `x - at`
80
86
  represents the ratio `10**((x - at) * delta.factor / coefficient)` relative to
@@ -54,6 +54,43 @@ True
54
54
 
55
55
  ## Defining logarithmic coordinates
56
56
 
57
+ For a zero-reading reference, `si.dB(reference, kind=...)` is a shorthand for
58
+ `LogUnit(reference, delta_unit=si.dB, kind=...)`. A Unit argument means one of
59
+ that unit; a Quantity argument supplies the full reference value. Watts and
60
+ volts, including their SI prefixes and extra exact scales, default to
61
+ `kind='power'` and `kind='amplitude'`, respectively. This convention follows
62
+ the named unit definition, not physical dimension: other units and composite
63
+ expressions require an explicit `kind`. An explicit `kind` always takes precedence.
64
+
65
+ ```pycon
66
+ >>> si.dB(si.mW) is si.dBm
67
+ True
68
+ >>> si.dB(si.W) is si.dBW
69
+ True
70
+ >>> si.dB(si.V) is si.dBV
71
+ True
72
+ >>> millivolt_level = si.dB(si.mV)
73
+ >>> (20 @ millivolt_level).to_quantity(si.mV).value
74
+ 10
75
+ >>> (0 @ millivolt_level).to(si.dBV).value
76
+ -60
77
+ >>> pressure_level = si.dB(20 * si.uPa, kind='amplitude')
78
+ >>> (0 @ pressure_level).to_quantity(si.uPa).value
79
+ 20
80
+
81
+ ```
82
+
83
+ References with value one and unit mW, W or V, using their usual conventions,
84
+ return the existing named descriptors. Other references return
85
+ anonymous LogUnits without registering new names. The millivolt reference
86
+ corresponds to dBmV; dBm refers to a milliwatt. Voltage-to-power conversion
87
+ requires additional physical information such as impedance.
88
+
89
+ Only the formal dB unit supports this call. Reconstructed dB units behave the
90
+ same way; other Units raise TypeError when called. As a consequence of the
91
+ shared `Unit.__call__` method, `callable(unit)` is true for all Unit objects
92
+ and does not test whether this shorthand is supported.
93
+
57
94
  `LogUnit(reference, /, *, delta_unit, kind, at=0)` means reading `at` represents
58
95
  `reference`. A reference is a physical Quantity or LogQuantity. Both `delta_unit`
59
96
  and `kind='power'|'amplitude'` are required. References must be positive finite
@@ -36,6 +36,20 @@ seconds. An `Annotated` label alone does not teach a type checker dimensional
36
36
  algebra. unitx does not currently supply dimension-parameterized quantities
37
37
  or a call-validation decorator.
38
38
 
39
+ ## Quantity subclasses
40
+
41
+ Preserving the magnitude parameter T does not preserve a Quantity subclass.
42
+ Quantity's ordinary binary arithmetic, unary operators and `.to()` construct
43
+ ordinary Quantity objects. A subclass must explicitly implement the operations
44
+ that preserve its own invariants. Scalar augmented operators delegate to the
45
+ corresponding binary operation; their annotations must describe the subclass's
46
+ results too.
47
+ For example, the [Impedance extension](https://github.com/DaitTHU/unitx/blob/main/examples/impedance.md)
48
+ preserves its type for addition of two Impedances, parallel composition and
49
+ conversion. Adding an ordinary Quantity returns an ordinary Quantity in either
50
+ operand order. Its local overrides specialize the base class's wider input
51
+ domain; they are not a template for preserving every array or coordinate overload.
52
+
39
53
  ## NumPy magnitude types
40
54
 
41
55
  `_numerics.py` defines `Scalar`, `Array`, and `Magnitude = Array | Scalar`
@@ -93,7 +107,12 @@ in static analysis. Result dtype and shape remain determined by NumPy.
93
107
 
94
108
  The test suite runs strict scalar fixtures with and without NumPy and checks
95
109
  array consumers separately when NumPy is installed. `tests/typing` and
96
- `tests/numpy_typing` are static fixtures, not runtime test modules.
110
+ `tests/numpy_typing` are static fixtures excluded from pytest collection.
111
+ The small `check_asarray.py` regression requires NumPy >= 2.5 stubs; other array
112
+ fixtures cover the full supported NumPy range. Notebook checks extract the
113
+ actual code and add selected type assertions, rather than maintaining copies
114
+ of scientific examples. Runtime test annotations describe fixtures and test
115
+ data; they do not replace static `assert_type()` checks of public inference.
97
116
 
98
117
  ```sh
99
118
  python -m pyright --pythonpath /path/to/your/python
@@ -1,5 +1,10 @@
1
1
  # Scientific examples
2
2
 
3
+ For a small extension without optional dependencies, see
4
+ [Extending Quantity: complex impedances](impedance.md). Its standalone Python
5
+ script uses a Quantity subclass for series `+` and parallel `//` composition
6
+ and calculates an RLC circuit with complex current and power.
7
+
3
8
  These notebooks use unitx for dimensional calculations and NumPy for numerical
4
9
  work. Each contains a model, assumptions, plots, and numerical checks. The
5
10
  material parameters and measurement draws are illustrative, not experimental data.
@@ -12,6 +17,7 @@ material parameters and measurement draws are illustrative, not experimental dat
12
17
  | [Optical attenuation](optical_attenuation.ipynb) | Transmission through absorbing layers | prod/multiply.reduce, input masks, Quantity initial, fixed-unit out |
13
18
  | [Pendulum uncertainty](pendulum_uncertainty.ipynb) | Correlated input uncertainties and Monte Carlo propagation | Mean/std/var units, mixed measurement units, explicit quantiles |
14
19
  | [Arrhenius fit](arrhenius_fit.ipynb) | Estimate activation energy from synthetic reaction rates | Absolute temperatures, regression boundary, parameter units and model-based standard errors |
20
+ | [Smith chart](smith_chart.ipynb) | Series/parallel RLC impedance and reflection trajectories | Quantity subclass, complex arithmetic, explicit normalization and plotting boundary |
15
21
 
16
22
  ## Run
17
23
 
@@ -25,11 +31,14 @@ jupyter lab examples/
25
31
 
26
32
  Open a notebook and run all cells from the beginning. Use the current checkout
27
33
  so that all demonstrated APIs are available.
34
+ Keep `smith_chart.ipynb` beside its companion `impedance.py` module.
28
35
  The examples do not download datasets or write files. Keep physical quantities in calculations;
29
36
  plot their `.value` and derive labels from their `.unit`. Convert only to choose
30
37
  a useful display scale or to align units at a numerical boundary. Quantiles use
31
38
  an explicit numerical boundary and reattach the input unit, without naming a
32
39
  variable after its unit. Display equations use `$$...$$`.
40
+ Generate physical parameter labels from Quantity formatting or `to_latex()`;
41
+ do not repeat their numerical values and unit spellings in plot strings.
33
42
 
34
43
  ## Keep Git history small
35
44
 
@@ -0,0 +1,66 @@
1
+ # Extending Quantity: complex impedances
2
+
3
+ [impedance.py](impedance.py) is a standalone example, available from this checkout
4
+ rather than the installed unitx API. Run it without optional dependencies:
5
+
6
+ ```sh
7
+ python examples/impedance.py
8
+ ```
9
+
10
+ Open [the Smith-chart notebook](smith_chart.ipynb) alongside this module to sweep
11
+ an RLC network over frequency and plot its reflection coefficient. Keep both
12
+ files in the same directory; the notebook imports the class from `impedance.py`.
13
+
14
+ `Impedance` subclasses `Quantity` to represent the complex impedance of an ideal
15
+ two-terminal circuit at a given angular frequency. A real value describes an
16
+ ordinary resistor. The class adds `//` for parallel composition, while `+`
17
+ represents series composition. Both return another Impedance and keep the left
18
+ operand's unit. Conversion with `.to()` also returns an Impedance, allowing
19
+ `(z1 + z2).to(si.kohm) // z3` and `z1 // z2 // z3`.
20
+
21
+ For the convention $e^{i\omega t}$, ideal components have impedances
22
+
23
+ $$Z_R=R, \qquad Z_L=i\omega L, \qquad Z_C=\frac{1}{i\omega C}.$$
24
+
25
+ Series and parallel combinations follow
26
+
27
+ $$Z_{\mathrm{series}}=Z_1+Z_2, \qquad
28
+ Z_{\mathrm{parallel}}=\frac{Z_1Z_2}{Z_1+Z_2}.$$
29
+
30
+ All impedances being combined must correspond to the **same frequency**. This
31
+ class stores a value and unit, without recording or checking frequency. It does
32
+ not model a frequency-dependent component or preserve the circuit topology.
33
+ The script evaluates a 100 ohm, 10 mH, 1 microfarad series branch at 1 kHz,
34
+ puts a 300 ohm resistor in parallel and applies a 12 V RMS voltage phasor.
35
+ It calculates the current and complex power $S=VI^*$, retaining quantities
36
+ throughout the circuit calculation. $\operatorname{Re}S$ is active power and
37
+ $\operatorname{Im}S$ is reactive power; the latter retains its sign.
38
+
39
+ The example accepts finite scalar `int`, `float`, `Fraction` and `complex`
40
+ magnitudes, preserving their types at construction. Fraction inputs keep purely
41
+ rational calculations exact; float and complex calculations follow Python's
42
+ rounding rules. Decimal and arrays are outside this small model. Negative real
43
+ and imaginary parts are accepted; the class imposes no passivity assumption.
44
+
45
+ Parallel composition handles zero impedance explicitly, including two shorts.
46
+ It rearranges the formula to avoid multiplying large magnitudes. Nonzero
47
+ impedances with cancelling admittances, such as `100j` and `-100j` ohm at ideal
48
+ parallel resonance, produce an infinite equivalent impedance. Infinity is
49
+ outside this finite-value example, so that operation raises ZeroDivisionError.
50
+ Use parentheses when mixing `+` and `//`: Python gives `//` higher precedence.
51
+
52
+ Inherited multiplication and division return ordinary Quantity: voltage,
53
+ current, admittance and power have their usual units. Ordinary operations such
54
+ as subtraction also retain the base class's result behavior. Only addition of
55
+ two Impedances, parallel composition and conversion preserve the extension
56
+ type. Quantity's general subclass and serialization contracts stay unchanged.
57
+ Mixing an Impedance with an ordinary Quantity in addition returns an ordinary
58
+ Quantity in either operand order. For example, `z + 50 * si.ohm` loses the
59
+ extension's `//` operation; construct `Impedance(50)` to compose circuit elements.
60
+ The type annotations distinguish this mixed result from `z1 + z2`.
61
+ The scalar `+=` operation follows the same rule, returning a new result rather
62
+ than changing the original object. `//=` follows parallel composition.
63
+ The example deliberately narrows the inherited addition and conversion
64
+ signatures to its scalar model and str/Unit targets. Local Pyright annotations
65
+ mark that specialization; it is not a general template for preserving all of
66
+ Quantity's array and coordinate overloads.
@@ -0,0 +1,121 @@
1
+ """A scalar Quantity extension for series and parallel complex impedances.
2
+
3
+ Run `python examples/impedance.py` from the checkout. See `impedance.md` for the
4
+ fixed-frequency model. Fraction inputs retain exact rational arithmetic;
5
+ float and complex calculations follow ordinary Python numerical rules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import cmath
11
+ import math
12
+ from fractions import Fraction
13
+ from typing import cast, overload
14
+
15
+ from unitx import Quantity, Unit
16
+ from unitx._numerics import Scalar
17
+ from unitx.exceptions import DimensionError
18
+ from unitx.si import si
19
+
20
+ type Number = int | float | Fraction | complex
21
+
22
+
23
+ class Impedance(Quantity[Number]):
24
+ """A finite scalar impedance with `+` series and `//` parallel composition.
25
+
26
+ Values being combined must describe the same angular frequency. This class
27
+ stores the impedance value and unit, without frequency or circuit topology.
28
+ Open circuits (infinity), Decimal and arrays are outside this small model.
29
+ """
30
+
31
+ __slots__ = ()
32
+
33
+ def __init__(self, value: Number, unit: str | Unit = si.ohm) -> None:
34
+ if type(value) not in (int, float, Fraction, complex):
35
+ error_message = 'Impedance requires an int, float, Fraction or complex scalar.'
36
+ raise TypeError(error_message)
37
+ if isinstance(value, (float, complex)) and not cmath.isfinite(value):
38
+ error_message = 'Impedance must be finite.'
39
+ raise ValueError(error_message)
40
+ super().__init__(value, unit)
41
+ if self.dimension != si.ohm.dimension:
42
+ error_message = 'Impedance requires an impedance unit.'
43
+ raise DimensionError(error_message)
44
+
45
+ # This small extension intentionally specializes the broader Quantity
46
+ # signatures to scalar impedances and str/Unit conversion targets.
47
+ def value_in(self, unit: str | Unit, /) -> Number: # pyright: ignore[reportIncompatibleMethodOverride]
48
+ # Scaling the explicitly supported numeric domain stays in that domain.
49
+ return cast(Number, super().value_in(unit))
50
+
51
+ @overload
52
+ def __add__(self, other: Impedance, /) -> Impedance: ...
53
+
54
+ @overload
55
+ def __add__(self, other: Quantity[Scalar] | Scalar, /) -> Quantity[Scalar]: ...
56
+
57
+ def __add__(self, other: object, /) -> Quantity[Scalar]: # pyright: ignore[reportIncompatibleMethodOverride]
58
+ if not isinstance(other, Impedance):
59
+ return NotImplemented
60
+ return Impedance(self.value + other.value_in(self.unit), self.unit)
61
+
62
+ @overload
63
+ def __iadd__(self, other: Impedance, /) -> Impedance: ...
64
+
65
+ @overload
66
+ def __iadd__(self, other: Quantity[Scalar] | Scalar, /) -> Quantity[Scalar]: ...
67
+
68
+ def __iadd__(self, other: Quantity[Scalar] | Scalar, /) -> Quantity[Scalar]: # pyright: ignore[reportIncompatibleMethodOverride]
69
+ return self + other
70
+
71
+ def __floordiv__(self, other: object, /) -> Impedance:
72
+ if not isinstance(other, Impedance):
73
+ return NotImplemented
74
+ left, right = self.value, other.value_in(self.unit)
75
+ if left == 0 or right == 0:
76
+ return Impedance(0, self.unit)
77
+ if left + right == 0:
78
+ error_message = 'Parallel admittances cancel; the equivalent impedance is infinite.'
79
+ raise ZeroDivisionError(error_message)
80
+ # Z1*Z2/(Z1+Z2), avoiding a large product. Complex values use modulus.
81
+ smaller, larger = (left, right) if abs(left) <= abs(right) else (right, left)
82
+ return Impedance(smaller / (1 + smaller / larger), self.unit)
83
+
84
+ def to(self, unit: str | Unit, /) -> Impedance: # pyright: ignore[reportIncompatibleMethodOverride]
85
+ return Impedance(self.value_in(unit), unit)
86
+
87
+
88
+ def main() -> None:
89
+ """Evaluate an RLC branch in parallel with a resistor, using RMS phasors."""
90
+ first = Impedance(Fraction(100))
91
+ second = Impedance(Fraction(1, 5), si.kohm)
92
+ parallel_resistors = first // second
93
+ assert parallel_resistors == Fraction(200, 3) * si.ohm
94
+ print(f'({first}) // ({second.to(first.unit)}) = {parallel_resistors}')
95
+
96
+ frequency = 1 * si.kHz
97
+ omega = 2 * math.pi * frequency
98
+ inductance = 10 * si.mH
99
+ capacitance = 1 * si.uF
100
+ inductor = Impedance(1j * (omega * inductance).value_in(si.ohm))
101
+ capacitor = Impedance(-1j * (1 / (omega * capacitance)).value_in(si.ohm))
102
+ series = first + inductor + capacitor
103
+ shunt = Impedance(300)
104
+ network = series // shunt
105
+ voltage = 12 * si.V # Real RMS voltage phasor; the current may be complex.
106
+ current = voltage / network
107
+ current_value = current.value
108
+ assert isinstance(current_value, complex)
109
+ complex_power = voltage * Quantity(current_value.conjugate(), current.unit)
110
+
111
+ assert network.dimension == si.ohm.dimension
112
+ assert current.dimension == si.A.dimension
113
+ assert complex_power.dimension == si.W.dimension
114
+ assert current_value.real > 0 and current_value.imag > 0 # Net capacitive branch.
115
+ print(f'At {frequency}: series RLC = {series.to(si.ohm):.3f}')
116
+ print(f'With a {shunt} parallel branch: Z = {network.to(si.ohm):.3f}')
117
+ print(f'At {voltage} RMS: I = {current.to(si.mA):.3f}, S = {complex_power.to(si.W):.3f}')
118
+
119
+
120
+ if __name__ == '__main__':
121
+ main()