Skip to main content

English | 简体中文

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 name quantity
  • 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

architecture: consumers, site, substrate

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 U vs B = 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

  • A means angstrom here; ampere is amp, and A is deliberately not prefixable so mA can never silently mean milli-ampere.
  • An exact symbol wins over a prefix interpretation (mol is not milli-ol).
  • J/(mol*K) is not J/mol*K.
  • % has factor 0.01, so Rwp = 8.5 % is the ratio 0.085.
  • A shared schema definition does not own the use-site unit.
  • quantity annotate proposes declarations from field names but never decides them: _ means division in Cv_J_per_mol_K and multiplication in sig_x_mJ_m2, and C11_GPa parses 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)

Source distribution for isq-quantity 1.2.0
File Size Uploaded
isq_quantity-1.2.0.tar.gz 63.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for isq-quantity 1.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page