Physical Units¶
Dimensional analysis via Quantity(value, dims) terms, with full arithmetic,
named-unit predicates, SI prefix constants, imperial unit vectors, and
syntactic sugar for writing measurements inline.
Quick start¶
-import_from(py.units, [m, s, newton, kilo, has_units, strip_units])
-import_from(py.imperial, [foot, inch])
# SI sugar: n(Unit), where Unit is an SI unit predicate
speed(V) <- (
eval_(100(m), D), # 100 (metre)
eval_(9.58(s), T), # 9.58 (second)
eval_(D / T, V) # 10.438... (metre / second)
)
# SI prefix: a plain number, multiplied in a ++ escape
big_force(F) <- (F is ++(5 * kilo * newton(1))) # 5000 (kilogram * metre / second ** 2)
# Imperial: a unit-vector Quantity, multiplied in a ++ escape
height(H) <- (H is ++(6 * foot + 2 * inch)) # 1.8796... (metre)
# Check a dimension
is_speed(V) <- has_units(V, m/s)
# Extract the numeric component
force_value(V) <- strip_units(9.8(newton), V) # V = 9.8
def main():
for V in --speed(V):
print(V) # 10.438413361169102 (metre / second)
for V in --(speed(V), is_speed(V)):
print("a speed:", V)
main runs the queries from Python with a goal-position --; call it once the file has
loaded (a query at module top level runs before the predicates are registered).
Naming¶
Unit names are lowercase identifiers — metre, kilogram, newton,
kilometre, byte — like the currency names (euro) and the symbol forms
(m, kg, s). Multi-word names use an underscore (julian_year), and
the physical constants are snake_case (speed_of_light, planck_constant).
The length family is spelled like metre throughout: kilometre,
centimetre, millimetre, micrometre, nanometre. The printed label of
a Quantity follows the identifier: str(5(metre)) is 5 (metre), and
9.8(newton) prints in base units as 9.8 (kilogram * metre / second ** 2)
(a scaled unit such as kilometre has no label of its own — 5(kilometre)
prints as 5000 (metre)).
The spellings these replaced — TitleCase Metre, Second, Newton,
SpeedOfLight, … and the American kilometer, centimeter, … — are
deprecated aliases. They still resolve — -import_from(py.units, [Metre])
imports metre under the old name, and units.Metre in Python returns
units.metre — but each warns with a ClausalDeprecatedSpellingWarning:
once per file from the -import_from list (naming every rename in it), once
per process per name from Python attribute access. Rename Metre -> metre,
kilometer -> kilometre, SpeedOfLight -> speed_of_light; the old
spellings will be removed in a future release.
Printed labels changed with the rename, and there is no alias for a
label. A Quantity prints its dimension keys by their unit name, so text
that came out of write/1, str(), a UnitsMismatch message or the
predicate repr changed spelling in the same release — before:
9.8 Kilogram·Metre·Second^-2, Unit mismatch for add: Metre vs Second,
units.Metre/[]; after (today's format): 9.8 (kilogram * metre / second ** 2),
Unit mismatch for add: metre vs second, units.metre/[]. Anything that
parses or compares those strings must expect the lowercase form; the values,
dimension keys and arithmetic are unchanged.
Two syntactic styles¶
n(Unit) sugar — SI predicates only¶
-import_from(py.units, [m, s, newton])
a(X) <- eval_(5(m), X) # 5 (metre)
b(X) <- eval_(9.8(newton), X) # 9.8 (kilogram * metre / second ** 2)
c(X) <- eval_(-3(s), X) # -3 (second)
d(X) <- eval_(5(m/s), X) # 5 (metre / second)
e(X) <- eval_(10(m**2), X) # 10 (metre ** 2)
The argument inside the parentheses must be an SI unit predicate (or a
compound expression built from SI unit predicates using *, /, **).
Imperial and other scaled-unit Quantity values (inch, foot, byte, …)
also work — 5(inch) is 5 * inch — because a Quantity is callable and
scales itself. SI prefix names (kilo, milli, …) are plain numbers, not
units; 5(kilo) raises a TypeError ("SI prefixes cannot be used as units").
Multiply a prefix in instead: ++(5 * kilo * m(1)) — call the unit as m(1)
to get a multipliable Quantity; the bare predicate m cannot appear on the
right of * (5 * kilo * m raises a TypeError).
For unusual constructions, use ++() directly:
-import_from(py.units, [kilogram, metre, second])
custom(X) <- (X is ++(kilogram(1) * metre(1) / second(1)**2 * 9.8)) # same as 9.8(newton)
* unit style — SI prefixes and imperial¶
SI prefixes are plain numbers; imperial/non-SI units are Quantity unit
vectors. Both are used via multiplication inside a ++() escape:
-import_from(py.units, [kilo, nano, giga, newton, second, hertz])
-import_from(py.imperial, [inch, mph])
force(X) <- (X is ++(5 * kilo * newton(1))) # 5 kN
tick(X) <- (X is ++(100 * nano * second(1))) # 100 ns
clock(X) <- (X is ++(2.4 * giga * hertz(1))) # 2.4 GHz
span(X) <- (X is ++(20 * inch)) # 20 inches → 0.508 (metre)
pace(X) <- (X is ++(60 * mph)) # 60 mph → 26.8224 (metre / second)
n() — dimensionless literal¶
An empty-argument call on any numeric literal produces a dimensionless
Quantity(n, {}):
X(Unit) — construction from a runtime value¶
when the callee is a logic variable, MY_VAL(Unit) desugars to
++(Quantity(MY_VAL, Unit)):
-import_from(py.units, [newton])
force_of(N, F) <- eval_(N(newton), F) # force_of(9.8, F): F = 9.8 (kilogram * metre / second ** 2)
has_units(X, Unit) — dimension constraint / check¶
-import_from(py.units, [m, s, newton, has_units])
is_force(F) <- has_units(F, newton) # check or constrain: F must have newton dims
is_velocity(V) <- has_units(V, m/s) # velocity check/constraint
is_acceleration(A) <- has_units(A, m/s**2) # acceleration
# an unbound F is constrained first, then checked when it is bound
newton_ok(F) <- (has_units(F, newton), eval_(9.8(newton), F)) # succeeds
metre_bad(F) <- (has_units(F, newton), eval_(9.8(m), F)) # fails
has_units/2 posts an AttVar constraint on F if it is unbound. Compound unit
expressions work directly — the transformer auto-wraps them.
has_units cannot appear inside an eval_/2 expression or on the RHS of ==.
Quantity term¶
from clausal.terms import Quantity, UnitsMismatch
d = Quantity(10.0, {metre: 1, second: -1}) # 10 m/s
d.value # 10.0
d.dims # MappingProxyType({<metre>: 1, <second>: -1})
Arithmetic¶
| Operation | Behaviour |
|---|---|
a + b |
Requires identical dims; raises UnitsMismatch otherwise |
a - b |
Same as addition |
a * b |
Merges dims by addition (exponents add) |
a / b |
Merges dims by subtraction (exponents subtract) |
a ** n |
Multiplies all exponents by integer n |
-a |
Negates value; preserves dims |
abs(a) |
Absolute value; preserves dims |
a * k |
Scales value by plain number; preserves dims |
k / a |
Inverts dims and scales |
These are the Python operators on a Quantity. In a clause, compute with
eval_/2: + - * ** behave as above, and division is
exact. Two integer magnitudes divide to a Fraction, not a float —
eval_(7(m) / 2, X) gives X a magnitude of Fraction(7, 2) — unlike a bare
7 / 2 between plain numbers, which is 3.5 (see Operators). A float
magnitude stays a float (eval_(9.58(s) * 2, X)), and a zero divisor raises
evaluation_error(zero_divisor) in eval_/2 and fails inside a constraint
(X == 1(m) / 0). A currency amount is stricter: a float factor beside
its exact Decimal magnitude is refused.
Modules¶
py.units — SI units and prefixes¶
Contains: SI base unit predicates, scaled SI unit predicates, named derived SI
unit predicates, SI prefix constants, IEC binary prefix constants, SI
abbreviations, SI unit vectors, digital information units, physical constants,
and utility predicates (has_units, strip_units, dimension_of, make_quantity).
py.imperial — imperial and non-SI unit vectors¶
Contains: imperial and non-SI Quantity unit vectors for length, mass, force,
volume, pressure, energy, power, speed, and temperature differences. All
values are stored in SI base units; has_units checks work without changes.
SI base unit predicates¶
Used with n(Unit) sugar.
| Alias | Full name | Dims | SI symbol |
|---|---|---|---|
m |
metre |
{metre: 1} |
m |
kg |
kilogram |
{kilogram: 1} |
kg |
s |
second |
{second: 1} |
s |
mol |
mole |
{mole: 1} |
mol |
cd |
candela |
{candela: 1} |
cd |
| — | ampere |
{ampere: 1} |
A (clash) |
| — | kelvin |
{kelvin: 1} |
K (clash) |
A and K are omitted as aliases — single uppercase letters are logic
variables in Clausal Prolog.
Digital information base unit (IEC 80000-13):
| Alias | Full name | Dims | IEC symbol |
|---|---|---|---|
| — | bit |
{bit: 1} |
bit |
bit uses itself as the dimension key, exactly like the SI base units.
Scaled SI unit predicates¶
These scale on the way in and store as SI base units. Use with n(Unit) sugar.
length (stored as metres)¶
kilometre (km), centimetre (cm), millimetre (mm),
micrometre (um), nanometre (nm)
Mass (stored as kilograms)¶
gram (mg for milli, ug for micro), milligram, microgram, tonne
Time (stored as seconds)¶
millisecond (ms), microsecond (us), nanosecond (ns),
minute (min), hour (hr), day, week, julian_year
Digital information (stored as bits)¶
bit is the base unit (IEC 80000-13). All values are normalised to bits.
-import_from(py.units, [bit, byte, kilobyte, gigabyte, kibibyte, gibibyte,
kilobit, megabit, kibi, mebi, gibi, tebi])
disk(SIZE) <- eval_(4(gibibyte), SIZE) # 34359738368 (bit)
link(RATE) <- eval_(100(megabit), RATE) # 100000000 (bit)
buffer(X) <- (X is ++(512 * mebi * byte(1))) # 512 MiB via binary prefix: 4294967296 (bit)
Decimal (SI-prefixed) byte multiples:
| Predicate | Stored as bits | Alias |
|---|---|---|
byte |
8 | — |
kilobyte |
8 × 10³ | — |
megabyte |
8 × 10⁶ | — |
gigabyte |
8 × 10⁹ | — |
terabyte |
8 × 10¹² | — |
Decimal bit multiples:
| Predicate | Stored as bits |
|---|---|
kilobit |
10³ |
megabit |
10⁶ |
gigabit |
10⁹ |
Binary (IEC-prefixed) byte multiples:
| Predicate | Stored as bits |
|---|---|
kibibyte |
8 × 2¹⁰ |
mebibyte |
8 × 2²⁰ |
gibibyte |
8 × 2³⁰ |
tebibyte |
8 × 2⁴⁰ |
Binary bit multiples:
| Predicate | Stored as bits |
|---|---|
kibibit |
2¹⁰ |
mebibit |
2²⁰ |
gibibit |
2³⁰ |
Named derived SI units¶
| Predicate | Quantity | Dims (SI base) |
|---|---|---|
newton |
force | {kg:1, m:1, s:-2} |
joule |
energy | {kg:1, m:2, s:-2} |
watt |
power | {kg:1, m:2, s:-3} |
pascal |
pressure | {kg:1, m:-1, s:-2} |
hertz |
frequency | {s:-1} |
volt |
voltage | {kg:1, m:2, s:-3, A:-1} |
coulomb |
charge | {A:1, s:1} |
farad |
capacitance | {kg:-1, m:-2, s:4, A:2} |
ohm |
resistance | {kg:1, m:2, s:-3, A:-2} |
siemens |
conductance | {kg:-1, m:-2, s:3, A:2} |
weber |
magnetic flux | {kg:1, m:2, s:-2, A:-1} |
tesla |
magnetic flux density | {kg:1, s:-2, A:-1} |
henry |
inductance | {kg:1, m:2, s:-2, A:-2} |
lumen |
luminous flux | {cd:1} |
lux |
illuminance | {cd:1, m:-2} |
katal |
catalytic activity | {mol:1, s:-1} |
gray |
absorbed dose | {m:2, s:-2} |
sievert |
dose equivalent | {m:2, s:-2} |
Scaled variants: bar, millibar, atmosphere, electronvolt, kilowatt
SI prefix constants¶
Plain Python numbers — not predicates. Use inside ++() by multiplying
against a unit vector:
-import_from(py.units, [kilo, nano, giga, mega, newton, second, hertz, joule])
prefixed(X) <- (X is ++(5 * kilo * newton(1))) # 5 kN
prefixed(X) <- (X is ++(100 * nano * second(1))) # 100 ns
prefixed(X) <- (X is ++(2.4 * giga * hertz(1))) # 2.4 GHz
prefixed(X) <- (X is ++(1 * mega * joule(1))) # 1 MJ
| Name | Value | SI symbol | Note |
|---|---|---|---|
yotta |
1e24 | Y (clash) | uppercase = logic var |
zetta |
1e21 | Z (clash) | |
exa |
1e18 | E (clash) | |
peta |
1e15 | P (clash) | |
tera |
1e12 | T (clash) | |
giga |
1e9 | G (clash) | |
mega |
1e6 | M (clash) | |
kilo |
1e3 | k → k |
|
hecto |
1e2 | h → h |
|
deca |
1e1 | da → da |
|
deci |
1e-1 | d → d |
|
centi |
1e-2 | c → c |
|
milli |
1e-3 | m (clash with metre alias) | use milli |
micro |
1e-6 | μ (not a valid identifier) | use micro |
nano |
1e-9 | n → n |
|
pico |
1e-12 | p → p |
|
femto |
1e-15 | f → f |
|
atto |
1e-18 | a → a |
|
zepto |
1e-21 | z → z |
(rarely needed) |
yocto |
1e-24 | y → y |
(rarely needed) |
Single-letter abbreviations (k, h, da, d, c, n, p, f, a)
are available but must be imported explicitly.
milli has no safe single-letter alias: m is already the metre predicate.
micro has no safe alias: μ is not a valid Python identifier. The
per-unit abbreviations ms, mg, mm, us, um encode both prefix and
unit together.
IEC binary prefix constants¶
Plain Python numbers — use inside ++() by multiplying against a unit vector:
-import_from(py.units, [gibi, mebi, kibi, byte, bit])
binary(X) <- (X is ++(4 * gibi * byte(1))) # 4 GiB → 34359738368 (bit)
binary(X) <- (X is ++(512 * mebi * byte(1))) # 512 MiB
binary(X) <- (X is ++(100 * kibi * bit(1))) # 100 Kib
| Name | Value | IEC symbol |
|---|---|---|
kibi |
2¹⁰ | Ki |
mebi |
2²⁰ | Mi |
gibi |
2³⁰ | Gi |
tebi |
2⁴⁰ | Ti |
pebi |
2⁵⁰ | Pi |
exbi |
2⁶⁰ | Ei |
The IEC symbol abbreviations (Ki, Mi, Gi, …) start with an uppercase
letter and are not provided as aliases — in Clausal Prolog an identifier starting with
an uppercase letter is a logic variable.
Imperial and non-SI unit vectors (py.imperial)¶
Plain Quantity values — not predicates. Import from py.imperial and
use by multiplying a scalar inside a ++() escape:
-import_from(py.imperial, [inch, pound_mass, mph, kilowatt_hour])
imperial(LEN, MASS, SPD, E) <- (
LEN is ++(20 * inch), # 0.508 (metre)
MASS is ++(150 * pound_mass), # 68.0388555 (kilogram)
SPD is ++(60 * mph), # 26.8224 (metre / second)
E is ++(1 * kilowatt_hour) # 3600000.0 (kilogram * metre ** 2 / second ** 2)
)
All values are stored in SI base units; dimensions are the same as their SI
equivalents so has_units checks work without any changes:
-import_from(py.imperial, [inch])
-import_from(py.units, [metre, has_units])
is_length() <- has_units(++(20 * inch), metre) # succeeds — both have {metre: 1}
length (stored as metres)¶
| Name | Value (m) | Abbrev |
|---|---|---|
inch |
0.0254 | — |
foot |
0.3048 | ft |
yard |
0.9144 | yd |
mile |
1 609.344 | mi |
nautical_mile |
1 852.0 | nmi |
light_year |
9.461 × 10¹⁵ | ly |
astronomical_unit |
1.496 × 10¹¹ | au |
Mass (stored as kilograms)¶
| Name | Value (kg) | Abbrev |
|---|---|---|
pound_mass |
0.453 592 37 | lb, lbm |
ounce_mass |
0.028 349 52 | oz |
stone |
6.350 293 18 | — |
short_ton |
907.184 74 | — |
long_ton |
1 016.046 909 | — |
Force (stored as Newtons = kg·m/s²)¶
| Name | Value (N) | Abbrev |
|---|---|---|
pound_force |
4.448 221 615 | lbf |
Volume (stored as cubic metres)¶
| Name | Value (m³) | Abbrev |
|---|---|---|
litre |
1 × 10⁻³ | l |
millilitre |
1 × 10⁻⁶ | ml |
gallon_us |
3.785 × 10⁻³ | — |
quart_us |
9.464 × 10⁻⁴ | — |
pint_us |
4.732 × 10⁻⁴ | — |
fluid_ounce_us |
2.957 × 10⁻⁵ | — |
gallon_uk |
4.546 × 10⁻³ | — |
pint_uk |
5.683 × 10⁻⁴ | — |
fluid_ounce_uk |
2.841 × 10⁻⁵ | — |
Pressure (stored as Pascals = kg/(m·s²))¶
| Name | Value (Pa) | Abbrev |
|---|---|---|
psi |
6 894.757 | — |
Energy (stored as Joules = kg·m²/s²)¶
| Name | Value (J) | Abbrev |
|---|---|---|
calorie |
4.184 | — |
kilocalorie |
4 184.0 | — |
btu |
1 055.056 | — |
kilowatt_hour |
3 600 000.0 | — |
Power (stored as Watts = kg·m²/s³)¶
| Name | Value (W) | Abbrev |
|---|---|---|
horsepower |
745.699 87 | — |
Speed (stored as m/s)¶
| Name | Value (m/s) | Abbrev |
|---|---|---|
mph |
0.447 04 | — |
kph |
0.277 7̄ | — |
knot |
0.514 4̄ | — |
Temperature differences (stored as kelvin — ratio scale only)¶
| Name | Value (K) |
|---|---|
rankine |
5/9 |
Absolute offset scales (Celsius, Fahrenheit) are unsupported — they are not ratio scales.
Utility predicates¶
| Predicate | Description |
|---|---|
dimension_of(D, Dims) |
Unify Dims with a DictTerm of the dimension dict (a bare number is dimensionless: {}) |
strip_units(D, V) |
Unify V with the numeric component |
make_quantity(V, Dims, D) |
Construct Quantity from value V and DictTerm dims |
has_units/2¶
Explicit dimension check/constraint predicate. Succeeds if D is a ground
Quantity whose dims match UnitPred._dims, or if D is an unbound Var
(posts an AttVar constraint).
Physical constants¶
| Name | Value (SI) | Dims |
|---|---|---|
speed_of_light |
2.998 × 10⁸ m/s | {m:1, s:-1} |
planck_constant |
6.626 × 10⁻³⁴ J·s | {kg:1, m:2, s:-1} |
boltzmann_constant |
1.381 × 10⁻²³ J/K | {kg:1, m:2, s:-2, K:-1} |
standard_gravity |
9.806 65 m/s² | {m:1, s:-2} |
elementary_charge |
1.602 × 10⁻¹⁹ C | {A:1, s:1} |
gravitational_constant |
6.674 × 10⁻¹¹ m³/(kg·s²) | {m:3, kg:-1, s:-2} |
Uninstantiated dimensioned slots (AttVar)¶
An uninstantiated slot that will eventually hold a measurement uses a plain
Var with a "units" AttVar constraint — not Quantity(Var, dims).
from clausal.logic.variables import Var, Trail
from clausal.logic.units_constraint import constrain_var_dims
from clausal.modules.py.units import newton
trail = Trail()
v = Var()
constrain_var_dims(v, newton._dims, trail) # post constraint
from clausal.logic.variables import unify
unify(v, newton(9.8), trail) # fires hook → checks dims → binds v
Such a variable, or a ground quantity, may take part in a CLP constraint:
the CLP(FD) comparators and in_domain/3, CLP(Q) ({C} in a .pl
file, clpq.rational/1, in_q/3, the objectives) and, for physical
quantities only, CLP(R). The solver works on
the exact magnitude in the dimension's base unit and the answer comes back
as a quantity; dimensions that disagree raise
error(system_error(units_mismatch), Ctx). See clpq.md
and the design in docs/superpowers/specs/2026-09-12-clp-units-side-channel-design.md.
Catching unit errors¶
UnitsMismatch is a Python exception class, so a module imports it and catches it with a
++ catcher (see catch/3); the instance form binds the message:
-import_from(py.units, [metre, second])
-import_from(clausal.terms, [UnitsMismatch])
mismatch_message(MSG) <- catch(
_ is ++(metre(3) + second(2)),
++UnitsMismatch(MSG),
true
)
# MSG = "Unit mismatch for add: metre vs second"
A comparison across dimensions (X > 0 with X a length) is not a UnitsMismatch
exception but the ISO error term error(system_error(units_mismatch), (>)/2).
Program verification with has_units¶
has_units goals are runtime assertions about dimensional types. They compose
freely with all Clausal Prolog constructs: negation-as-failure, catch/3,
backtracking, constraint solving.
The intended workflow:
- Development: annotate inputs and outputs with
has_unitscalls. - Verification: once tests pass with assertions active, dimensional invariants are confirmed on those paths.
- Production: strip
has_unitsgoals for zero overhead.
Dimensional analysis with SciPy predicates¶
SciPy wrapper predicates are quantity-aware: when Quantity inputs are
passed, units are stripped before calling SciPy, and the result is re-wrapped
with correctly propagated dimensions. when plain inputs are passed, SciPy is
called directly with zero overhead.
Each SciPy predicate falls into one of four categories (see individual module docs for details: scipy.linalg, scipy.special, scipy.fft, scipy.differentiate, scipy.integrate, scipy.interpolate):
| Category | Behaviour | Examples |
|---|---|---|
| Require dimensionless | Raises UnitsMismatch if any input has non-empty dims |
scipy_special (Gamma, Erf, Bessel, ...) |
| Pass-through | Output dims = input dims | scipy_fft (FFT, IFFT, ...) |
| Algebraic propagation | Output dims computed from input dims by a fixed rule | scipy_linalg (Solve, Norm, Det, ...), scipy_differentiate (Derivative, Jacobian, Hessian) |
| Intrinsically dimensionless | Inputs stripped, output is always plain | scipy_stats test statistics, scipy_cluster labels |
Modules with quantity support¶
| Module | Status | Notes |
|---|---|---|
scipy_linalg |
Supported | Full algebraic propagation for all predicates |
scipy_special |
Supported | Requires dimensionless inputs |
scipy_fft |
Supported | Pass-through (output dims = input dims) |
scipy_differentiate |
Supported | df dims = f_dims - x_dims; callable probing detects f output dims |
scipy_integrate |
Supported | Array quadrature: y_dims + x_dims; callable quadrature: probes f, f_dims + x_dims |
scipy_interpolate |
Supported | Dims stored in handle; eval/integral/derivative propagate algebraically |
See each module's documentation for details.
Design notes¶
- No offset scales:
Celsius/Fahrenheitare unsupported. - Integer-only exponents in
Pow:area ** 0.5raisesUnitsMismatch. - Dimension keys are predicate objects: the seven SI base unit predicates
are the keys in
dims. AandKaliases omitted: single uppercase letters are logic variables in Clausal Prolog.- SI prefixes are plain numbers:
kilo = 1e3,milli = 1e-3, etc. They cannot appear insiden(Unit)parentheses; use multiplication in a++()escape instead. - IEC binary prefixes are plain numbers:
kibi = 2¹⁰,mebi = 2²⁰, etc. Same rules as SI prefixes — multiply against a unit vector in++(). bitis the information base unit (IEC 80000-13): all byte and prefixed-bit predicates store internally in bits, so arithmetic between them works without conversion.- Imperial units are Quantity unit vectors:
inch,foot,pound_mass, etc. Multiply by a scalar in a++()escape.has_unitschecks work normally since the dimensions are identical to their SI equivalents.
See also: Currency — exact-decimal money built on this units machinery · Arithmetic — numeric operations in Clausal Prolog · Python Interop — ++() escape for direct Pint operations.