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.
@@ -0,0 +1,17 @@
1
+ .DS_Store
2
+ .coverage
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ .venv-minimal/
8
+ __pycache__/
9
+ *.egg-info/
10
+ *.py[cod]
11
+ build/
12
+ dist/
13
+ htmlcov/
14
+
15
+ # Agent guidance is local to each maintainer.
16
+ AGENTS.md
17
+ CLAUDE.md
@@ -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.