Metadata-Version: 2.4
Name: isq-quantity
Version: 1.2.0
Summary: A zero-dependency physical-value substrate: ISQ dimensions, exact unit algebra, CODATA constants, and declared model boundaries.
Author: Xiaoxi Zeng
License: MIT License
        
        Copyright (c) 2026 Xiaoxi Zeng
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/BrookZeng2023/isq-quantity
Project-URL: Source, https://github.com/BrookZeng2023/isq-quantity
Keywords: units,dimensional-analysis,ISQ,ISO-80000,CODATA,physical-quantities,materials-science,scientific-computing
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

[English](https://github.com/BrookZeng2023/isq-quantity/blob/main/README.md) | [简体中文](https://github.com/BrookZeng2023/isq-quantity/blob/main/README.zh-CN.md)

# 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.

```python
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](https://raw.githubusercontent.com/BrookZeng2023/isq-quantity/main/docs/architecture.svg)

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

```bash
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:

```bash
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

```python
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:

```mermaid
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
```

```mermaid
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:

```bash
QUANTITY_SITE=examples/site python3 examples/run.py
QUANTITY_SITE=examples/site quantity site
QUANTITY_SITE=examples/site quantity graph
```

## Extending

Start one:

```bash
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.

```json
{
  "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:

```json
{
  "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.

```python
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:

```json
{
  "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

```bash
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:

```python
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:

```python
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

```bash
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](https://github.com/BrookZeng2023/isq-quantity/blob/main/LICENSE).
