xpint 0.1.0b1__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.
- xpint-0.1.0b1/.gitignore +17 -0
- xpint-0.1.0b1/CHANGELOG.md +65 -0
- xpint-0.1.0b1/LICENSE +29 -0
- xpint-0.1.0b1/PKG-INFO +410 -0
- xpint-0.1.0b1/README.md +388 -0
- xpint-0.1.0b1/examples/README.md +30 -0
- xpint-0.1.0b1/examples/affine_temperature.py +35 -0
- xpint-0.1.0b1/examples/angle_dimensions.py +177 -0
- xpint-0.1.0b1/examples/custom_semantics.py +142 -0
- xpint-0.1.0b1/examples/designated_solutions.py +36 -0
- xpint-0.1.0b1/examples/pint_style_basics.py +23 -0
- xpint-0.1.0b1/examples/semantic_arrays.py +38 -0
- xpint-0.1.0b1/pyproject.toml +119 -0
- xpint-0.1.0b1/src/xpint/__init__.py +32 -0
- xpint-0.1.0b1/src/xpint/_adapters.py +155 -0
- xpint-0.1.0b1/src/xpint/_formatter.py +38 -0
- xpint-0.1.0b1/src/xpint/_matplotlib.py +27 -0
- xpint-0.1.0b1/src/xpint/_measurement.py +73 -0
- xpint-0.1.0b1/src/xpint/_policies.py +160 -0
- xpint-0.1.0b1/src/xpint/designations.py +305 -0
- xpint-0.1.0b1/src/xpint/py.typed +1 -0
- xpint-0.1.0b1/src/xpint/registry.py +3148 -0
- xpint-0.1.0b1/src/xpint/units.py +74 -0
xpint-0.1.0b1/.gitignore
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable public changes to xpint are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.0b1] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
First public beta. No upgrade from a previous public version is required.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Pint-backed semantic quantities with dimension-constrained kinds, explicit
|
|
12
|
+
classification/removal methods, and `compatible_kinds()` on quantities, units,
|
|
13
|
+
and the registry to discover kinds sharing a dimensionality.
|
|
14
|
+
- Factor-level, case-sensitive designations for substances and compartments,
|
|
15
|
+
including concentration aliases and designation-preserving conversions.
|
|
16
|
+
- Strict Absolute, Delta, and Generic temperature roles, including Celsius
|
|
17
|
+
construction and affine arithmetic across temperature scales.
|
|
18
|
+
Absolute temperature units construct quantities only from magnitudes; multiplying
|
|
19
|
+
one by a quantity or unit raises, including through NumPy.
|
|
20
|
+
- Offset-aware equality: absolute temperatures in different scales are compared
|
|
21
|
+
after conversion, so `0 * degC == 0 * degF` is false, unlike in ordinary Pint,
|
|
22
|
+
and equality agrees with hashing.
|
|
23
|
+
- `define_kind()` rejects new kinds over thermodynamic-temperature dimensions;
|
|
24
|
+
the built-in `Temperature` and `TemperatureDifference` kinds carry affine roles,
|
|
25
|
+
and conversion would otherwise replace a custom kind silently.
|
|
26
|
+
- Strict stored-kind matching: addition, subtraction, assignments, fills,
|
|
27
|
+
searches, and additive-reduction quantity initials reject differing kinds,
|
|
28
|
+
including a missing kind. Use `as_kind()` or `as_unkinded()` to align them.
|
|
29
|
+
`clip()` permits an unkinded bound.
|
|
30
|
+
- Optional per-registry angle dimensions with corresponding rotational kinds.
|
|
31
|
+
- A documented subset of NumPy, masked-array, Dask, xarray, and pint-xarray
|
|
32
|
+
behavior that preserves semantic state or explicitly rejects undefined operations.
|
|
33
|
+
- Registry ownership checks raise `ValueError` for values from another registry,
|
|
34
|
+
including ordinary Pint quantities and a pickle whose `angle_dimension` differs
|
|
35
|
+
from the application registry; unpickling an undefined kind raises a `KeyError`
|
|
36
|
+
naming `set_application_registry()`.
|
|
37
|
+
- Registry ownership checks, scoped application-registry pickle reconstruction,
|
|
38
|
+
and guarded Pint decorators, plotting, and measurement adapters. `wraps`
|
|
39
|
+
preserves classified values under `None` variadic declarations when Pint binds
|
|
40
|
+
them correctly, and keeps a protective guard on converters that do not.
|
|
41
|
+
- `define_kind()` and `define_designation_slot()` to extend a registry's kind
|
|
42
|
+
catalogue and designation syntax beyond the built-in definitions.
|
|
43
|
+
- An `xpint.units` module exporting curated unit symbols and resolving any other
|
|
44
|
+
Pint unit name or prefix through the shared registry on attribute access.
|
|
45
|
+
- Typed APIs, runnable examples, and the BSD 3-Clause license.
|
|
46
|
+
|
|
47
|
+
### Compatibility and limitations
|
|
48
|
+
|
|
49
|
+
- Python 3.12–3.14 and Pint 0.25.3 are tested. The dependency range remains
|
|
50
|
+
`pint>=0.25.3,<0.26`; Pint 0.26 and later are not supported yet.
|
|
51
|
+
- Scalar use requires only Pint and its dependencies. Arrays and ecosystem
|
|
52
|
+
adapters need their corresponding optional packages; NumPy 2.5.2 is tested.
|
|
53
|
+
- Registry `check` and `wraps` require fixed signatures. Inherited variadic and
|
|
54
|
+
keyword-only validation remains defective on Pint 0.25.3, so xpint guards those
|
|
55
|
+
declarations rather than trusting them.
|
|
56
|
+
- Measurement adapters reject kinds, designations, and affine roles. To use an
|
|
57
|
+
ordinary measurement in semantic arithmetic, explicitly construct an xpint
|
|
58
|
+
Quantity from its magnitude and units first.
|
|
59
|
+
- Designated values support xpint `str()` and the documented `format()` specs.
|
|
60
|
+
Direct Pint/Babel formatter APIs reject them rather than omit their labels.
|
|
61
|
+
- Native designated-string parsing, pandas, arbitrary Dask graph transformations,
|
|
62
|
+
and classified Measurement objects remain unsupported.
|
|
63
|
+
- The API may change during beta. See the
|
|
64
|
+
[README](https://github.com/matthiasschabel/python-xpint/blob/main/README.md)
|
|
65
|
+
for installation and detailed behavior.
|
xpint-0.1.0b1/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Matthias C. Schabel
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
xpint-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xpint
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: xpint extends Pint with quantity kinds and unit designations: semantic information that keeps energy distinct from torque and mL[blood]/mL[tissue] distinct from a plain ratio.
|
|
5
|
+
Project-URL: Source, https://github.com/matthiasschabel/python-xpint
|
|
6
|
+
Project-URL: Documentation, https://github.com/matthiasschabel/python-xpint/blob/main/README.md
|
|
7
|
+
Project-URL: Issues, https://github.com/matthiasschabel/python-xpint/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/matthiasschabel/python-xpint/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Matthias C. Schabel
|
|
10
|
+
License-Expression: BSD-3-Clause
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: pint<0.26,>=0.25.3
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# xpint
|
|
24
|
+
|
|
25
|
+
xpint extends [Pint](https://pint.readthedocs.io/) with quantity kinds and unit
|
|
26
|
+
designations: semantic information that keeps energy distinct from torque and
|
|
27
|
+
mL[blood]/mL[tissue] distinct from a plain ratio.
|
|
28
|
+
|
|
29
|
+
> **Status:** 0.1.0b1 is the first public beta. The API and the supported
|
|
30
|
+
> interoperability surface may change during beta.
|
|
31
|
+
|
|
32
|
+
Pint checks physical dimensions. Many mistakes pass that check: energy and
|
|
33
|
+
torque are both newton-metres, an oxygen concentration and a carbon dioxide
|
|
34
|
+
concentration are both mol/L, and 20 °C plus 20 °C is not 40 °C. xpint adds
|
|
35
|
+
three kinds of meaning on top of Pint and checks them before Pint can erase the
|
|
36
|
+
distinction:
|
|
37
|
+
|
|
38
|
+
- **Kinds** say what a quantity is (`Energy`, `Torque`, `Frequency`).
|
|
39
|
+
- **Designations** say which referent a unit factor belongs to (`mmol["O2"]`,
|
|
40
|
+
`mL["blood"]`).
|
|
41
|
+
- **Temperature roles** separate an absolute temperature (a point on a scale)
|
|
42
|
+
from a temperature difference.
|
|
43
|
+
|
|
44
|
+
Pint remains the unit engine: definitions, conversions, contexts, and NumPy
|
|
45
|
+
support are Pint's own. xpint values are Pint quantities, and importing xpint
|
|
46
|
+
does not change Pint's application registry.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install "xpint==0.1.0b1"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
xpint requires Python 3.12 or later and Pint `>=0.25.3,<0.26`. The Pint range is
|
|
55
|
+
deliberately narrow because xpint hooks into Pint internals; Pint 0.26 is not
|
|
56
|
+
supported yet. Scalars need only Pint. Install NumPy for arrays (2.5.2 is
|
|
57
|
+
tested). Matplotlib, Dask, xarray, pint-xarray, Babel, and uncertainties are
|
|
58
|
+
optional and are not installed by xpint.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
Unit symbols come from `xpint.units`, which binds them to one shared registry.
|
|
63
|
+
The module exports common symbols and resolves any other Pint unit name or
|
|
64
|
+
prefix on import, for example `from xpint.units import kg, microgram`.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from xpint.units import J, L, N, degC, degF, delta_degC, m, mL, mmol
|
|
68
|
+
|
|
69
|
+
# Kinds: same dimensions, different meaning
|
|
70
|
+
energy = (10 * J).as_kind("Energy")
|
|
71
|
+
torque = (10 * N * m).as_kind("Torque")
|
|
72
|
+
energy + torque # TypeError: cannot add quantity kinds 'Energy' and 'Torque'
|
|
73
|
+
energy.to("kWh") # the kind survives conversion
|
|
74
|
+
(N * m).compatible_kinds() # ('Energy', 'Torque')
|
|
75
|
+
|
|
76
|
+
# Designations: same units, different referent
|
|
77
|
+
oxygen = 5 * mmol["O2"] / L
|
|
78
|
+
oxygen + 5 * mmol["CO2"] / L # TypeError: different factor designations
|
|
79
|
+
oxygen.to("mol/L") # 0.005 mole[O2] / liter
|
|
80
|
+
|
|
81
|
+
ratio = 0.6 * mL["blood"] / mL["tissue"]
|
|
82
|
+
ratio.dimensionless # True: physically a ratio
|
|
83
|
+
ratio.is_semantic_unity # False: the designations do not cancel
|
|
84
|
+
float(ratio) # TypeError: remove the designations explicitly first
|
|
85
|
+
|
|
86
|
+
# Temperature roles: points and differences
|
|
87
|
+
room = 20 * degC
|
|
88
|
+
room + 5 * delta_degC # 25 degC
|
|
89
|
+
room + room # TypeError: cannot add two absolute temperatures
|
|
90
|
+
25 * degC - 68 * degF # about 5 delta_degC
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Quantity kinds
|
|
94
|
+
|
|
95
|
+
A kind is optional metadata naming what a quantity is. It is checked against the
|
|
96
|
+
quantity's dimensions when attached, and only against other kinds afterwards:
|
|
97
|
+
|
|
98
|
+
- `as_kind(name)` classifies a value without converting it; `as_unkinded()`
|
|
99
|
+
removes the classification. Both return new quantities. You can also pass
|
|
100
|
+
`kind=` to `ureg.Quantity(...)`.
|
|
101
|
+
- Addition, subtraction, assignment into arrays, and fills require equal kinds,
|
|
102
|
+
including both being unkinded. A kinded value plus an unkinded one is rejected
|
|
103
|
+
rather than silently adopting the kind; align them with `as_kind()` or
|
|
104
|
+
`as_unkinded()`.
|
|
105
|
+
- Equality and ordering require exactly the same semantic state, so a kinded
|
|
106
|
+
quantity is unequal to an otherwise identical unkinded one.
|
|
107
|
+
- Multiplying or dividing by a plain number or a plain dimensionless value keeps
|
|
108
|
+
the kind. Other products, quotients, and powers drop it: xpint does not guess
|
|
109
|
+
that force times length is torque. `energy / (2 * s)` is an unkinded
|
|
110
|
+
`J / s`.
|
|
111
|
+
- Conversion keeps the kind. A context conversion that would change a kinded
|
|
112
|
+
quantity's dimensions, such as wavelength to energy, requires `as_unkinded()`
|
|
113
|
+
first.
|
|
114
|
+
- `compatible_kinds()` on a unit, quantity, or registry lists the registered
|
|
115
|
+
kinds that match the dimensions. It is a lookup, not inference.
|
|
116
|
+
|
|
117
|
+
The built-in catalogue covers the SI base and derived quantities and the common
|
|
118
|
+
same-dimension collisions (energy/torque, frequency/angular
|
|
119
|
+
velocity/radioactivity, absorbed/equivalent dose, entropy/heat capacity, and
|
|
120
|
+
others). See `ureg.kinds` for the full list.
|
|
121
|
+
|
|
122
|
+
## Designations
|
|
123
|
+
|
|
124
|
+
A designation is an opaque, case-sensitive label attached to one factor of a
|
|
125
|
+
unit, such as the substance in `mmol["O2"] / L`:
|
|
126
|
+
|
|
127
|
+
- Matching designations cancel; different ones stay in the quantity's
|
|
128
|
+
designation signature even when the physical units cancel, as in
|
|
129
|
+
`mL["blood"] / mL["tissue"]`. `dimensionless` keeps Pint's physical meaning;
|
|
130
|
+
`is_semantic_unity` is true only when a value is dimensionless *and* has no
|
|
131
|
+
designation left. It does not look at kinds.
|
|
132
|
+
- Addition and ordering require matching designations, and equality with a
|
|
133
|
+
mismatch is false. Scalar conversion (`float()`, `int()`) and undefined NumPy
|
|
134
|
+
operations reject any retained designation.
|
|
135
|
+
- Converting to a plain unit keeps the source's designations:
|
|
136
|
+
`oxygen.to("mol/L")` is `0.005 mole[O2] / liter`, and `oxygen.m_as("mol/L")` is
|
|
137
|
+
`0.005`. A designated target must match exactly, so `oxygen.to(mol["CO2"] / L)`
|
|
138
|
+
is rejected. The same rules apply to `ito()`, compact and preferred
|
|
139
|
+
conversions, construction from another quantity, and registry `convert()`.
|
|
140
|
+
- `with_designation(label)` labels a value whose unit is a single factor, such as
|
|
141
|
+
`(5 * mmol).with_designation("O2")`; `as_undesignated()` removes every label.
|
|
142
|
+
- Unit reduction keeps designations and physical scale:
|
|
143
|
+
`1 m["a"] / (1 cm["b"])` reduces to `100 m[a] / m[b]`.
|
|
144
|
+
- Compound units do not guess which factor a label belongs to. `mM["O2"]` works
|
|
145
|
+
because the registry declares that molar concentration designates its
|
|
146
|
+
substance; otherwise write the factor form, `mmol["O2"] / L`.
|
|
147
|
+
|
|
148
|
+
Designations are entered programmatically; there is no parser for strings such
|
|
149
|
+
as `"mmol[O2]/L"` yet.
|
|
150
|
+
|
|
151
|
+
## Temperature roles
|
|
152
|
+
|
|
153
|
+
Thermodynamic temperatures have one of three roles:
|
|
154
|
+
|
|
155
|
+
| Role | Comes from | Meaning |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| Absolute | offset units such as `degC`, `degF`, and Réaumur; Rankine; or kind `Temperature` | a point on a scale |
|
|
158
|
+
| Delta | delta units (`delta_degC`, ...), or kind `TemperatureDifference` | a difference |
|
|
159
|
+
| Generic | plain Kelvin and other linear units, unclassified | ordinary Pint arithmetic |
|
|
160
|
+
|
|
161
|
+
| Operands | Addition | Subtraction |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| Absolute, Absolute | rejected | Delta, in the left operand's scale |
|
|
164
|
+
| Absolute, Delta | Absolute | Absolute |
|
|
165
|
+
| Delta, Absolute | Absolute | rejected |
|
|
166
|
+
| Delta, Delta | Delta | Delta |
|
|
167
|
+
| Generic, Generic | Pint's rules | Pint's rules |
|
|
168
|
+
| Generic with Absolute or Delta | rejected | rejected |
|
|
169
|
+
|
|
170
|
+
- The role survives conversion: `(20 * degC).to(K)` is an absolute temperature
|
|
171
|
+
of kind `Temperature`, so adding Generic `5 * K` to it is rejected.
|
|
172
|
+
- Absolute temperatures reject multiplication, division, powers, negation, and
|
|
173
|
+
`abs()`. To use ratio-scale algebra, convert to Kelvin and remove the role:
|
|
174
|
+
`(20 * degC).to(K).as_unkinded() ** 4`.
|
|
175
|
+
- `20 * degC` and `ureg.Quantity(20, "degC")` construct an absolute temperature.
|
|
176
|
+
String parsing such as `ureg("20 degC")` multiplies by the unit and is rejected
|
|
177
|
+
with a pointer to these spellings; `"5 delta_degC"` and `"300 kelvin"` parse
|
|
178
|
+
normally.
|
|
179
|
+
- Equality between offset scales converts first. Unlike ordinary Pint,
|
|
180
|
+
`0 * degC == 0 * degF` is false.
|
|
181
|
+
- Custom kinds cannot be defined over temperature dimensions, because the
|
|
182
|
+
`Temperature` and `TemperatureDifference` kinds carry the role.
|
|
183
|
+
|
|
184
|
+
The strict roles apply to thermodynamic temperature only, not to gauge pressure
|
|
185
|
+
or other offset scales.
|
|
186
|
+
|
|
187
|
+
## Optional angle dimensions
|
|
188
|
+
|
|
189
|
+
By default plane angle is dimensionless, as in SI and Pint. A registry can
|
|
190
|
+
instead make it a base dimension, with solid angle derived as `rad ** 2`:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from xpint import SemanticRegistry
|
|
194
|
+
|
|
195
|
+
u = SemanticRegistry(angle_dimension=True)
|
|
196
|
+
|
|
197
|
+
u.Quantity(1, "radian") + u.Quantity(1, "steradian") # DimensionalityError
|
|
198
|
+
u.get_dimensionality("radian / second") == u.get_dimensionality("hertz") # False
|
|
199
|
+
((1 * u.mrad) * (1 * u.mrad)).to("microsteradian") # 1.0 microsteradian
|
|
200
|
+
torque = 5 * u.J / u.rad # not interchangeable with energy
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Rotational quantities then take their conjugate forms (`joule / radian` for
|
|
204
|
+
torque), and leaving the angle world is explicit: extract a magnitude with
|
|
205
|
+
`theta.m_as("radian")`, or multiply by `eta = 1 / u.radian`. Pint's constants
|
|
206
|
+
carry no angle dimension, so formulas such as `hbar * omega` need one `eta`. The
|
|
207
|
+
mode is fixed when the registry is created, and values from angle and ordinary
|
|
208
|
+
registries cannot be mixed. See
|
|
209
|
+
[examples/angle_dimensions.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/angle_dimensions.py).
|
|
210
|
+
|
|
211
|
+
In either mode, `float()`, `int()`, and functions that call them, such as
|
|
212
|
+
`math.sin`, reject a kinded value even when it is dimensionless, because the
|
|
213
|
+
conversion would drop the kind. Use `math.sin(angle.m_as("radian"))`; NumPy
|
|
214
|
+
trigonometric functions accept such angles directly and return unkinded results.
|
|
215
|
+
|
|
216
|
+
## Extending a registry
|
|
217
|
+
|
|
218
|
+
The built-in catalogue is a starting point. A registry can declare its own kinds
|
|
219
|
+
and designation slots:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from xpint import SemanticRegistry
|
|
223
|
+
|
|
224
|
+
u = SemanticRegistry()
|
|
225
|
+
|
|
226
|
+
u.define_kind("SpecificActivity", "becquerel / gram")
|
|
227
|
+
activity = u.Quantity(4.2, "becquerel / gram", kind="SpecificActivity")
|
|
228
|
+
|
|
229
|
+
u.Quantity(1.0, "becquerel").as_kind("SpecificActivity")
|
|
230
|
+
# ValueError: kind 'SpecificActivity' requires dimensions 1 / [time] / [mass], ...
|
|
231
|
+
|
|
232
|
+
u.define_designation_slot("katal", "[substance]")
|
|
233
|
+
3 * u.katal["trypsin"] # 3 katal[trypsin]
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
`define_kind()` constrains a name to the dimensions of a reference unit.
|
|
237
|
+
`define_designation_slot()` names the single factor that a label designates on a
|
|
238
|
+
one-token derived unit. Both affect only that registry. See
|
|
239
|
+
[examples/custom_semantics.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/custom_semantics.py).
|
|
240
|
+
|
|
241
|
+
## Working with Pint, NumPy, and other libraries
|
|
242
|
+
|
|
243
|
+
### Registries
|
|
244
|
+
|
|
245
|
+
Every xpint value belongs to one `SemanticRegistry`. Combining values from two
|
|
246
|
+
registries, or an xpint value with an ordinary Pint quantity, raises
|
|
247
|
+
`ValueError`; equality between two xpint registries is simply false. One caveat comes from Pint: `pint_quantity == xpint_quantity`
|
|
248
|
+
(ordinary Pint on the left) compares physically and can return true.
|
|
249
|
+
|
|
250
|
+
### What behaves like ordinary Pint
|
|
251
|
+
|
|
252
|
+
Values without kinds, designations, or a temperature role go through Pint's own
|
|
253
|
+
code paths, with two guards added: registry ownership checks, and rejection of
|
|
254
|
+
Pint 0.25.3's unit-blind NumPy `out=` and quantity-valued `initial=` handling
|
|
255
|
+
(including `ndarray += quantity`, which would strip the unit). A bare scalar
|
|
256
|
+
`initial=` is a magnitude in the quantity's own unit.
|
|
257
|
+
|
|
258
|
+
Celsius, Fahrenheit, and Rankine values are classified by their unit, so they
|
|
259
|
+
follow the semantic rules below rather than Pint's full NumPy surface. Functions
|
|
260
|
+
outside the list, such as `np.sort`, `np.median`, `np.percentile`, `np.isclose`,
|
|
261
|
+
and `np.interp`, raise for them. Convert explicitly with
|
|
262
|
+
`temperatures.to("kelvin").as_unkinded()` to use them as ordinary linear values,
|
|
263
|
+
or operate on `.magnitude`.
|
|
264
|
+
|
|
265
|
+
### NumPy arrays
|
|
266
|
+
|
|
267
|
+
For classified arrays the supported surface is deliberately bounded, and
|
|
268
|
+
anything outside it raises rather than silently dropping metadata:
|
|
269
|
+
|
|
270
|
+
- Arithmetic and comparisons follow the kind, designation, and temperature rules
|
|
271
|
+
above.
|
|
272
|
+
- `mean`/`nanmean`, `max`/`min`/`amax`/`amin`, shape-only methods, `zeros_like`,
|
|
273
|
+
`concatenate`, `stack`, and `where` preserve metadata, including absolute
|
|
274
|
+
temperatures. Combined operands must have matching semantic state.
|
|
275
|
+
`nanmax`/`nanmin` are not supported yet.
|
|
276
|
+
- `sum`/`nansum` and `cumsum`/`nancumsum` preserve kinds and designations and
|
|
277
|
+
reject absolute temperatures, which cannot be added together.
|
|
278
|
+
- `sqrt`, `square`, `cbrt`, and `reciprocal` follow power rules: kinds are
|
|
279
|
+
dropped and designation exponents scale.
|
|
280
|
+
- `std`/`nanstd` preserve units, kinds, and designations; `var`/`nanvar` square
|
|
281
|
+
the units and drop the kind. Both reject absolute temperatures, and these and
|
|
282
|
+
the cumulative sums reject output targets.
|
|
283
|
+
- `diff` preserves metadata, except that absolute temperatures produce
|
|
284
|
+
temperature differences; `diff(n=0)` returns the input unchanged.
|
|
285
|
+
- `argmax`, `argmin`, `argsort`, and `nonzero` return plain indices.
|
|
286
|
+
- `clip()` keeps the input's classification and allows an unkinded bound.
|
|
287
|
+
- Reductions validate `out` and `initial` whether passed by position or keyword.
|
|
288
|
+
A quantity `initial` must match the input's semantic state.
|
|
289
|
+
- Undefined operations, discrete arithmetic such as modulo, `dot`, `prod`, and
|
|
290
|
+
implicit conversion to `ndarray` (including `np.asarray`) raise. Use
|
|
291
|
+
`.magnitude` when you intend to leave xpint's checks.
|
|
292
|
+
|
|
293
|
+
In-place powers, unit conversions (`ito()`), temperature-role addition and
|
|
294
|
+
subtraction, and classified in-place multiplication and division stage a
|
|
295
|
+
temporary NumPy copy, so a failure in those operations leaves values and metadata
|
|
296
|
+
unchanged. This costs one extra full-size buffer, sometimes two. Other in-place
|
|
297
|
+
operations, including ordinary `+=` on a kinded array, write through Pint
|
|
298
|
+
directly, as in Pint, and a floating-point error raised midway can leave the
|
|
299
|
+
array partly updated. The staging covers NumPy arrays only. Classified quantities expose array metadata such as `shape`,
|
|
300
|
+
`dtype`, and `mask`, but forwarded magnitude methods such as `view()` raise.
|
|
301
|
+
|
|
302
|
+
### Other array libraries
|
|
303
|
+
|
|
304
|
+
- Masked arrays: `filled()` and `compressed()` preserve classified values.
|
|
305
|
+
- Dask: `compute()`, `persist()`, and `rechunk()` preserve metadata; classified
|
|
306
|
+
`map_blocks()` raises because the callback could change values or dimensions.
|
|
307
|
+
- xarray: reductions, concatenation, `where`, transpose and indexing, Dask-backed
|
|
308
|
+
computation, and multiplication by xpint units are tested.
|
|
309
|
+
- pint-xarray: ordinary units work with `SemanticRegistry(force_ndarray_like=True)`;
|
|
310
|
+
designated round trips need a designation parser and are unsupported.
|
|
311
|
+
- pandas and arbitrary Dask graph transformations are not supported.
|
|
312
|
+
|
|
313
|
+
### Comparison and hashing
|
|
314
|
+
|
|
315
|
+
Scalar quantities can be dictionary keys. Hashes include the registry and the
|
|
316
|
+
semantic state, so equal values hash equally; arrays are unhashable.
|
|
317
|
+
`is_compatible_with()` has two meanings. With a unit or unit string it asks
|
|
318
|
+
whether converting to that unit would be accepted, so
|
|
319
|
+
`(20 * degC).is_compatible_with(K)` is true while `K.is_compatible_with(degC)` is
|
|
320
|
+
false. With a quantity it also requires the same kind, designations, and
|
|
321
|
+
temperature role, so an `Energy` value is not compatible with a `Torque` value
|
|
322
|
+
even though it converts to its units. A quantity used as a conversion target
|
|
323
|
+
supplies units and designations only; it never reclassifies the value.
|
|
324
|
+
|
|
325
|
+
### Formatting
|
|
326
|
+
|
|
327
|
+
`str()` and `format()` include designations and accept numeric specs such as
|
|
328
|
+
`f"{oxygen:.2f}"` and the abbreviated unit spec `~`; other unit specs (`P`, `L`,
|
|
329
|
+
`H`, `C`, `D`) are rejected for designated values. `repr()` shows the kind,
|
|
330
|
+
`kind='Energy'`, which `str()` omits. Pint's formatter and Babel APIs reject
|
|
331
|
+
designated values rather than drop their labels.
|
|
332
|
+
|
|
333
|
+
### Pickling
|
|
334
|
+
|
|
335
|
+
Quantities and units from any registry can be pickled. Unpickling reconstructs
|
|
336
|
+
against `xpint.get_application_registry()`, by default the registry behind
|
|
337
|
+
`xpint.units`. Registry definitions are not pickled: to unpickle values that use
|
|
338
|
+
custom units or kinds, or an angle-dimension registry, call
|
|
339
|
+
`xpint.set_application_registry(registry)` first, also in worker processes such
|
|
340
|
+
as multiprocessing or Dask distributed. A missing kind or mismatched angle mode
|
|
341
|
+
raises an error naming that function. Neither function changes Pint's
|
|
342
|
+
application registry.
|
|
343
|
+
|
|
344
|
+
`to_tuple()` and `to_timedelta()` reject kinds and designations, which those
|
|
345
|
+
representations cannot hold; remove them explicitly first.
|
|
346
|
+
|
|
347
|
+
### Registry adapters
|
|
348
|
+
|
|
349
|
+
- `ureg.check("[length]")` checks physical dimensions only and passes the
|
|
350
|
+
original quantity through with its metadata.
|
|
351
|
+
- `ureg.wraps(...)` converts arguments to magnitudes for a numerical function.
|
|
352
|
+
Converted arguments must be unclassified, including the implicit roles of
|
|
353
|
+
Celsius and delta units; return units may construct those roles but cannot
|
|
354
|
+
carry designations. With a fixed parameter list, a `None` argument
|
|
355
|
+
specification passes the value through unchanged, metadata included.
|
|
356
|
+
- Use fixed parameter lists with both. Pint 0.25.3 binds variadic and
|
|
357
|
+
keyword-only parameters incorrectly
|
|
358
|
+
([pint#2254](https://github.com/hgrecco/pint/issues/2254),
|
|
359
|
+
[pint#2426](https://github.com/hgrecco/pint/pull/2426)). For `wraps`, xpint
|
|
360
|
+
rejects classified values in slots Pint might convert by mistake, which can
|
|
361
|
+
also reject a `None`-specified variadic argument; `check` inherits Pint's
|
|
362
|
+
behavior unchanged.
|
|
363
|
+
- `ureg.pi_theorem({...})` and `ureg.setup_matplotlib()` accept unclassified
|
|
364
|
+
values. For temperature data, convert to Kelvin and call `as_unkinded()` before
|
|
365
|
+
plotting.
|
|
366
|
+
- Measurements (`plus_minus()`, `ureg.Measurement()`, which need the
|
|
367
|
+
`uncertainties` package) accept only unclassified values. To combine
|
|
368
|
+
uncertainty with semantic checks, construct
|
|
369
|
+
`ureg.Quantity(measurement.magnitude, measurement.units)`.
|
|
370
|
+
|
|
371
|
+
## Examples
|
|
372
|
+
|
|
373
|
+
Runnable scripts that start from Pint's tutorial style and add xpint's checks:
|
|
374
|
+
|
|
375
|
+
| Example | Shows |
|
|
376
|
+
| --- | --- |
|
|
377
|
+
| [pint_style_basics.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/pint_style_basics.py) | Construction, arithmetic, and conversion with imported symbols |
|
|
378
|
+
| [designated_solutions.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/designated_solutions.py) | Concentrations, analyte mismatches, and compartment ratios |
|
|
379
|
+
| [affine_temperature.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/affine_temperature.py) | Absolute, Delta, and Generic temperature roles |
|
|
380
|
+
| [semantic_arrays.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/semantic_arrays.py) | Metadata through NumPy and where it stops |
|
|
381
|
+
| [angle_dimensions.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/angle_dimensions.py) | The two angle modes side by side |
|
|
382
|
+
| [custom_semantics.py](https://github.com/matthiasschabel/python-xpint/blob/main/examples/custom_semantics.py) | Custom kinds and designation slots |
|
|
383
|
+
|
|
384
|
+
## Reporting issues
|
|
385
|
+
|
|
386
|
+
Use the [issue tracker](https://github.com/matthiasschabel/python-xpint/issues)
|
|
387
|
+
and include a minimal reproducer, your Python, Pint, and NumPy versions, and the
|
|
388
|
+
expected and actual behavior.
|
|
389
|
+
|
|
390
|
+
## Development
|
|
391
|
+
|
|
392
|
+
From a source checkout:
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
uv sync --locked
|
|
396
|
+
uv run ruff check .
|
|
397
|
+
uv run ruff format --check .
|
|
398
|
+
uv run mypy src tests examples benchmarks tools/check_minimal_install.py
|
|
399
|
+
uv run pytest
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The full suite needs the development dependency group, which installs every
|
|
403
|
+
optional dependency the tests use. CI tests the installed wheel on Python 3.12–3.14 and a
|
|
404
|
+
separate install without optional dependencies. Maintainer documentation is in
|
|
405
|
+
[docs/](https://github.com/matthiasschabel/python-xpint/blob/main/docs/README.md).
|
|
406
|
+
|
|
407
|
+
## License
|
|
408
|
+
|
|
409
|
+
[BSD 3-Clause](https://github.com/matthiasschabel/python-xpint/blob/main/LICENSE),
|
|
410
|
+
matching Pint's license. Copyright (c) 2026 Matthias C. Schabel.
|