quantity — a physical-value substrate
A physical number is not a float. It is a value plus a unit, an ISQ
dimension, and, at a model boundary, a stable physical-quantity
identity. quantity keeps those properties attached while model inputs and
outputs cross component boundaries, with no third-party dependencies and no
runtime cost you have not asked for.
It exists because of a specific failure. Two implementations of the same model
each typed 1 MV/cm = 1e-3 V/A; the correct factor is 1e-2. Homogeneity
checks, exponent checks and spread checks all stayed green for ten days,
because E_c ∝ σ³/E² cancelled two decade errors against each other.
Scale-invariant tests cannot see a uniform scale error. Declared boundaries
can.
import quantity
sigma = quantity.Q(8.93, "meV/A^2", "interface_energy_density")
field = quantity.Q([0.5, 2.5], "MV/cm", "electric_field")
float(sigma) # UnitError: refusing to erase meV/A^2
sigma.m_as("eV/A^2") # 0.00893 — the only way out
field.m_as("V/A") # [0.005, 0.025]
- Distribution name
isq-quantity· import namequantity - Python ≥ 3.9, standard library only, MIT licensed.
- ISO 80000-1 for the seven ISQ base dimensions, the SI Brochure (9th ed.) for coherent SI, prefixes and accepted non-SI units, CODATA 2022 for measured constants.
How it fits together
The substrate is what is true everywhere: the dimensional algebra, the
symbols of coherent SI, the CODATA constants, Q, and the model boundary. A
site is what is true for one deployment: the units and quantity identities
your field needs, the models your components implement, and the contracts
your components hand each other. Consumers import one package and get both.
Install
pip install git+https://github.com/BrookZeng2023/isq-quantity.git
# or, from a checkout
pip install .
Either way you get import quantity and a quantity command.
The package has no install-time build step and no dependencies, so a checkout also works untouched:
git clone https://github.com/BrookZeng2023/isq-quantity.git isq-quantity
cd isq-quantity
./quantity units 'J/(mol*K)' # the source-checkout CLI binds ./src
./quantity where # executable, import, repo, commit, remote, sites
apply.sh automates that second form for a whole machine: it symlinks the CLI
into ~/bin and writes a .pth into user-site so every interpreter of that
user can import quantity with no PYTHONPATH at all. apply.sh --remote HOST [PYTHON...] installs the substrate and the sibling site directory on
another machine and verifies both with each interpreter named there — a login
shell's python3 is usually not the interpreter a component actually runs
under.
Q is the model boundary
import quantity
field = quantity.Q([0.3, 0.45, 0.9], "MV/cm", "electric_field")
energy = quantity.Q(2.0, "eV", "energy")
area = quantity.Q(4.0, "A^2", "area")
# Arithmetic propagates dimensions; derived identities are never guessed.
sigma = (energy / area).with_quantity("interface_energy_density")
# A bare value exists only at a named numeric-kernel boundary.
field_V_A = field.m_as("V/A")
sigma_meV_A2 = sigma.m_as("meV/A^2")
# Constants stay physical values, so k_B T is an energy, not a number.
kbt = (quantity.constant("boltzmann")
* quantity.Q(300, "K", "temperature")).m_as("eV")
A value's whole journey, and what each step refuses:
flowchart TD
P["producer — DFT, instrument, upstream JSON"] -->|"value + unit + identity"| Q(["Q"])
Q --> BIND["model.bind_inputs()"]
BIND --> MAG["magnitudes() — the one float boundary"]
MAG --> KERNEL["numeric kernel — plain floats, one declared unit system"]
KERNEL --> OUT["model.bind_outputs()"]
OUT --> VER["verify_result()"]
VER --> CONTRACT["contract JSON — x-quantity and x-unit at every number"]
Q -. refuses .-> R0["float(q) · a unit that contradicts the identity · adding across dimensions"]
BIND -. refuses .-> R1["a bare float · the right dimension under the wrong identity · a missing or extra role"]
VER -. refuses .-> R2["a missing selector · a non-numeric result · a null that was not declared nullable"]
CONTRACT -. refuses .-> R3["an undeclared number · a declaration in the wrong dimension"]
float(q) always fails. Addition, subtraction and comparison require matching
dimensions. Arrays are scaled by duck typing; the package itself does not
import numpy. Nothing falls back: a missing table, an unknown unit, an unknown
quantity or a bare float at a model boundary is an exception, never a typed
default.
What ships with the substrate
etc/units.json unit symbols -> dimension + factor to coherent SI
etc/quantities.json stable physical-quantity identities
etc/constants.json generated CODATA 2022 constants
etc/codata-2022.txt the vendored NIST machine-readable listing
lib/dimension.py zero-dependency unit-expression algebra
lib/value.py Q: physical values with explicit unwrapping
lib/model.py model ports, boundaries, result selectors, graph edges
lib/registry.py quantity identities and JSON-contract checks
lib/site.py where a deployment's own declarations come from
cli.py the CLI, which is a view over the same objects
tools/gen-constants regenerates etc/constants.json from the vendored
listing and checks each dimension, each derivation and an independent set of
computed rows. Units whose factor is a physical constant (eV, Ha, Ry,
u, e) refer to that generated constant with si_from; they never carry a
second typed copy of it.
Sites
A site directory holds everything that is true for one deployment and not for the world:
<site>/site.json REQUIRED manifest: name, description, requires_substrate
<site>/units.json unit symbols this deployment adds
<site>/quantities.json quantity identities this deployment adds
<site>/models/*.json declared physical models
<site>/contracts.json the roster `quantity check` walks
<site>/check.sh optional; run by apply.sh --check
flowchart TD
A{"QUANTITY_SITE set?"} -->|yes| B["every path in it, in order"]
A -->|no| C{"sibling directory quantity-site exists?"}
C -->|yes| D["that one"]
C -->|no| E["no site — a legitimate state"]
B --> F{"manifest present? name unique? requirement satisfied?"}
D --> F
F -->|no| G["error naming the file, and both sources on a collision"]
F -->|yes| H["units and quantities composed into one table;<br/>models and roster loaded"]
E --> I["Q, conversion and constants still work;<br/>quantity check exits 2 — no roster to report on"]
The sibling default exists because an environment variable is the first thing to fall off when code runs inside a sealed runtime — a conda env, a wrapped interpreter, a scheduler job body. A path next to the package survives being copied; nothing has to remember to export anything.
No site is a legitimate state. What the substrate will not do is report "every contract is clean" when it has no roster to check.
examples/site is a complete, runnable site:
QUANTITY_SITE=examples/site python3 examples/run.py
QUANTITY_SITE=examples/site quantity site
QUANTITY_SITE=examples/site quantity graph
Extending
Start one:
quantity site --init ./my-site # manifest, models/, contracts.json, check.sh
Add a unit in <site>/units.json. A site may only ADD; redeclaring a
symbol the substrate already defines is an error naming both files, because
moving eV would move every number ever written in it. SI prefixes are not a
deployment's to redefine.
{
"units": {
"D": {"dim": {"L": 1, "T": 1, "I": 1}, "si": 3.33564e-30, "aliases": ["debye"]}
}
}
Add a quantity identity in <site>/quantities.json, same rule:
{
"quantities": {
"electric_dipole_moment": {
"symbol": "p", "unit": "D",
"description": "electric dipole moment of a molecule or a unit cell",
"note": "solid-state codes report e*A, quantum-chemistry codes report D; the two differ by 4.8032"
}
}
}
Add a model in <site>/models/*.json: a stable id and implementation,
consumes and produces roles, a stable quantity identity and external unit
for every role, the numeric kernel's explicit internal_unit, result selectors
for produced ports, and any assumptions, validity limits and independent
computation paths.
model = quantity.model("example.coercive_field")
bound = model.bind_inputs({
"sigma": quantity.Q(8.9, "meV/A^2", "interface_energy_density"),
"areal_polarisation": quantity.Q(0.0272, "e/A^2", "areal_polarisation"),
"film_thickness": quantity.Q(9.0, "nm", "length"),
})
kernel_inputs = bound.magnitudes() # eV/A^2, e/A^2 and A — the one float boundary
Loading fails on a duplicate role or model id, an unknown quantity, a
wrong-dimensional unit, a cross-dimensional internal unit, a missing
implementation or a missing result selector. A port requires an exact
quantity_id, so an A^2 mean-square displacement cannot enter a
geometric-area port merely because both are L^2.
The result-selector language is deliberately small: dotted dict keys, integer
list indices, and * over a dict or list. It is not JSONPath. A produced port
may declare nullable when an undefined sample is physical; strings and other
non-numeric values still fail. Model graph edges are derived from produced and
consumed quantity identities, not from role names — this is a model-level
graph, not an artifact-level provenance graph.
Add a contract to <site>/contracts.json. Every number declares both
identity and unit at its use site:
{
"type": "number",
"x-quantity": "interface_energy_density",
"x-unit": "eV/A^2"
}
A roster entry may carry an undeclared list: an exact ratchet, where a new
undeclared field is a regression and a listed field that now passes is a
stale tolerance. Both are exit 1, because a tolerance nobody removes is a
tolerance that grows. A roster path is resolved against the file that declares
it, so a site directory can be copied to another machine unchanged.
Sites compose. QUANTITY_SITE takes a list; each site keeps its own name,
and any collision — two sites claiming one unit symbol, one quantity id, one
model id, one contract name, or even one site name — is an error naming both.
Two things this deliberately does not do. A site cannot add constants: every
value in etc/constants.json is generated from the vendored CODATA listing and
re-derived by tools/gen-constants --check, and a hand-added constant would
turn that guarantee into a qualified one — a site unit can state its si factor
directly instead. And there is no plugin/entry-point discovery: a site is a
directory you can ls, rsync and diff, which is the property that makes it
survive the sealed runtimes it has to reach.
CLI
quantity list # every registered quantity
quantity show interface_energy_density
quantity units 'J/(mol*K)' 'eV/A^2'
quantity convert 1 MV/cm V/A
quantity constants
quantity constant boltzmann eV/K
quantity site # substrate version, sites, and what each contributes
quantity site --init DIR # scaffold a new site
quantity models
quantity model example.coercive_field
quantity graph --to electric_field
quantity verify example.wall_energy result.json
quantity check # every contract on the roster
quantity check path/to.schema.json
quantity annotate path/to.schema.json
quantity selftest # positive controls for the instrument itself
Exit status is 0 for a clean check, 1 for a finding, and 2 for usage, an
unreadable input, or a question that could not be asked at all.
convert prints a magnitude for inspection, but model code uses Q and
.m_as() instead. verify checks a model's declared result ports; it does not
claim full result-schema coverage.
Relation to pint, astropy.units and unyt
Those libraries are mature, widely used and good at what they do, and for most
work they are the right answer: pint has the largest ecosystem, astropy.units
is the standard in astronomy, unyt carries units through numpy arrays.
quantity is not a competitor to their unit algebra. It differs in two places,
and both are deliberate rather than missing features.
There is no implicit way out. pint gives you .magnitude, which returns a
bare number in whatever unit the value happens to be carrying at that moment.
quantity refuses:
float(sigma) # UnitError
sigma.m_as("eV/A^2") # 0.00893 — you must name the unit
A number leaves the system only at a place where the code says which unit it is leaving in. That is a constraint on the caller, not a convenience, and it is the whole point: the failure this library was written for was a value that crossed a boundary carrying the wrong scale, in code where every check was green.
A dimension is not an identity. Energy and torque are both N·m. Young's
modulus and stress are both Pa. A dimension check passes on all four, so it
cannot tell you that a stress was handed to something expecting a modulus.
quantity carries a stable physical-quantity identity alongside the unit and
checks it at declared model boundaries:
quantity.Q(8.93, "meV/A^2", "interface_energy_density")
Two further practical differences: quantity has no third-party
dependencies (pint pulls numpy), and its tables are pinned to their sources --
ISO 80000-1 for the ISQ base dimensions, the SI Brochure 9th edition for
coherent SI and prefixes, CODATA 2022 for measured constants.
If you want units and conversions, use pint. If you want a boundary that refuses to let an unlabelled number through, this is what that looks like.
What dimension checks cannot prove
A schema that claims eV/A^2 while its producer emits meV/A^2 is internally
consistent and still 1000× wrong. The same limitation applies to conventions
that share a dimension and differ in meaning:
- per mole of atoms vs per mole of formula units;
- radians vs degrees;
- displacement
UvsB = 8π²U; - a geometric area vs a mean-square displacement;
e·Åvs debye — the same dipole moment, a factor of 4.8 apart;- a tally count vs a dimensionless ratio.
Those need a quantity identity, an explicit basis/convention field, a
conversion benchmark, or an independent computation path. A self-consistent
checker is not an independent witness — which is why quantity selftest is a
set of positive and negative controls on the checker itself, and why the test
suite plants defects in a fixture and requires that they be caught.
Traps worth knowing
Ameans angstrom here; ampere isamp, andAis deliberately not prefixable somAcan never silently mean milli-ampere.- An exact symbol wins over a prefix interpretation (
molis not milli-ol). J/(mol*K)is notJ/mol*K.%has factor0.01, soRwp = 8.5 %is the ratio0.085.- A shared schema definition does not own the use-site unit.
quantity annotateproposes declarations from field names but never decides them:_means division inCv_J_per_mol_Kand multiplication insig_x_mJ_m2, andC11_GPaparses as coulomb to a power. Read the producer, not the field name.
Development
for t in tests/test_*.py; do python3 "$t"; done # no pytest required
pytest tests -v # but it works under pytest too
python3 tools/gen-constants --check # constants still reproduce
./quantity selftest # instrument controls
QUANTITY_SITE=examples/site ./quantity selftest # including the model layer
bash apply.sh --check # everything above, plus each site's own gate
CI runs the suite from a bare checkout on Python 3.9–3.13, from an installed
wheel, and asserts two things that are easy to get backwards: that quantity check with no roster fails rather than reporting a clean run, and that a
site produced by quantity site --init is one the substrate accepts.
License
MIT — see LICENSE.
Metadata
Release files for isq-quantity 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| isq_quantity-1.2.0.tar.gz | 63.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| isq_quantity-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 118.9 kB
Release files / isq_quantity-1.2.0.tar.gz
| Download URL | isq_quantity-1.2.0.tar.gz |
|---|---|
| Size | 63.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
64eefa22f6888c4337c84f19d0f8de495d2a9f4f6fb61bbe6169343b956c250a
|
|
BLAKE2b-256 checksum How to use checksums |
106f6733bf55495791e0e0254d223cec0bb6e60b781aa403b85ebcd02d1be57b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency logRelease files / isq_quantity-1.2.0-py3-none-any.whl
| Download URL | isq_quantity-1.2.0-py3-none-any.whl |
|---|---|
| Size | 55.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a4c4ac80f282da8c484ff84b61971ee6a7353c5196ebc087c11beca67d098412
|
|
BLAKE2b-256 checksum How to use checksums |
b2c44d88e64d20e8c2abb9ea72901590de3acd4de9e305201907c945591db468
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.
Transparency log