Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Units & Quantities

A quantity is a physical measurement, a number paired with a unit: 45 kg, 42 km/h, 9.81 m/s^2, 10 N/m^2. Dolfin writes them as smart literals, exactly like the temporal values of Chapter 9: a keyword followed by human-friendly notation in parentheses.


fact rex a Dog
  weight quantity(45 kg)

Behind the keyword sits a small dimensional-analysis engine. It knows the SI system, resolves prefixes, does unit arithmetic, and reduces everything to the seven SI base dimensions so that quantities written in different units can still be compared.


The quantity(...) smart literal

The general form is a value, then a unit expression:

quantity( <number> <unit-expression> )
WrittenMeans
quantity(45 kg)45 kilograms
quantity(42 km/h)42 kilometres per hour
quantity(9.81 m/s^2)9.81 metres per second squared
quantity(10 N/m^2)10 newtons per square metre
quantity(1.609 km.s^(-1))1.609 kilometres per second
quantity(273.15 K)273.15 kelvin
quantity(1e3 Pa)1000 pascals

The literal is resolved at compile time: the value, the unit, and the dimension vector are computed once, when the file is parsed. A malformed quantity (unknown unit, impossible conversion) is a compile error, not a runtime surprise (see Errors).

Video 3.2.2 · Units and quantities, opening (0:04–0:25). Watch on PeerTube

Unit expressions

A unit expression is one or more unit tokens combined with . (multiply), / (divide), and ^ (power):


quantity(5 kg)             # a single unit
quantity(42 km/h)          # a quotient
quantity(9.81 m.s^(-2))    # a product with a negative power
quantity(10 N/m^2)         # 'm^2' is metre-squared
  • . multiplies the units on either side: N.m is a newton-metre.
  • / divides: km/h is kilometres per hour. A unit after / has its exponent negated, so km/h and km.h^(-1) mean the same thing.
  • ^n raises a unit to a power. Negative powers use ^(-n) or a bare ^-n: s^(-1), m^2, m.s^-2.

Prefixes

SI prefixes compose with any unit, so you do not enumerate km, cm, mm separately, they are k+m, c+m, m+m:

All 24 SI prefixes are recognised, from quetta (Q, 10³⁰) down to quecto (q, 10⁻³⁰):

PrefixQRYZEPTGMkhda
Factor10³⁰10²⁷10²⁴10²¹10¹⁸10¹⁵10¹²10⁹10⁶10³10²10¹
Prefixdcmµ / μ / unpfazyrq
Factor10⁻¹10⁻²10⁻³10⁻⁶10⁻⁹10⁻¹²10⁻¹⁵10⁻¹⁸10⁻²¹10⁻²⁴10⁻²⁷10⁻³⁰

kg, mg, MHz, kPa, cm all resolve compositionally. When a token is both a standalone unit and a prefix, the standalone unit wins: m is a metre (not milli-anything), T is a tesla (not tera), h is an hour, d is a day, kt is a knot, pt a pint. Prefix decomposition is only tried when a token is not a known unit on its own.

Some units never take a prefix: kg (prefix the gram: mg, not mkg), the tonne t (kt is the knot; there is no kilotonne), min, h, d, ha, u, and the non-metric units (ft, in, yd, mi, nmi, kt, oz, lb, ct, pt, atm, at).

Known units

The built-in registry covers the SI base units, the named derived units, and the common non-SI units:

LayerTokens (selection)
SI basem, kg, s, A, K, mol, cd
SI derivedN, Pa, J, W, Hz, V, Ohm/Ω, F, C, T
Mass (non-SI)g, t (tonne), oz, lb, ct (carat), u/Da (dalton)
Length (non-SI)mi, ft, in, yd, nmi (nautical mile)
Areaha (hectare)
Speedkt (knot)
Time (non-SI)min, h, d
VolumeL/l (litre), mL/ml, pt (US liquid pint)
Pressure/energybar, atm, at (technical atmosphere), cal, kcal
TemperatureK; °C/degC, °F/degF (standalone only)

An unknown token is a compile error.


User-defined units (unitdef)

The built-in registry above is fixed — it ships with dolfin-units and cannot be extended from a .dlf file. unitdef is the separate mechanism for a project to declare its own units: currencies, or units that aren’t physically commensurable at all (a bunch of carrots is not a length or a mass). It is a top-level declaration, like concept or property:


unitdef USD: scale 0.92 EUR
unitdef family vegetables
unitdef bunch_of_carrots: nominal of vegetables scale 2
unitdef cabbages: nominal of vegetables scale 1

Why unitdef and not unit. unit is an ordinary identifier, and older models still write qualified ranges such as has weight: unit.Mass (see legacy names). Reserving unit for this declaration would break them, so the declaration keyword is unitdef.

There are two independent kinds of unitdef, and they behave very differently. Picking the wrong one for what you’re modelling gets you either silently-wrong arithmetic or arithmetic dolfin unnecessarily refuses.

Derived units (currencies, and anything else on a real dimension)


unitdef USD: scale 0.92 EUR

This declares USD as an ordinary unit: 1 USD = 0.92 EUR. It behaves exactly like declaring km from m — dolfin-units ships EUR as its one built-in currency unit (an arbitrary base pick, scale 1.0, no other currency and no live exchange rate bundled), and a project adds any other currency itself, with whatever rate it wants to use.

Because a derived unit is a real dimension (dolfin-units has a currency axis alongside the seven SI ones), it composes and converts exactly like any other unit:


quantity(25 USD/h)              # composes fine — a salary rate
quantity(100 USD + 8 EUR)       # auto-converts and adds, like `1 km + 500 m`
quantity(100 USD as EUR)        # → 92 EUR

scale <factor> <reference> requires <reference> to already be a resolvable unit (built-in, or another unitdef — declared in any order, even across files: dolfin resolves references in dependency order, not declaration order). A reference that never resolves is a compile error naming the broken unitdef.

Nominal (incommensurable) units and families


unitdef family vegetables
unitdef bunch_of_carrots: nominal of vegetables scale 2
unitdef cabbages: nominal of vegetables scale 1

unitdef family <name> declares an abstract, dimensionless grouping — not a usable unit by itself, just something other unitdefs can belong to. unitdef <name>: nominal of <family> scale <factor> declares a unit that belongs to that family, with a per-unit scale used only when explicitly widening into the family (see below) — it is not a conversion rate between nominal units.

Unlike a derived unit, a nominal unit is not a real dimension: two different nominal units are incommensurable by design, even if they belong to the same family:


quantity(2 bunch_of_carrots + 3 bunch_of_carrots)   # OK: same unit, ordinary add → 5
quantity(2 bunch_of_carrots + 3 cabbages)            # error: incommensurable units

The only way to +/- different nominal units of the same family is an explicit as <family> cast, which widens each operand via its declared scale into the family’s dimensionless base and only then combines them:


quantity(2 bunch_of_carrots + 3 cabbages as vegetables)   # → 7 (2×2 + 3×1)
quantity(3 bunch_of_carrots as vegetables)                 # → 6 (single operand also widens)
quantity(3 bunch_of_carrots as fruit)                      # error: not a member of `fruit`

Casting to a family you aren’t a member of, or casting a non-nominal quantity to a family, is the same INVALID_QUANTITY-class compile error as casting 5 km as s.

*// follow a different rule than +/-: a nominal unit composes freely with an ordinary (non-nominal) unit, same as any other unit — a carrot-cutting machine’s rate is a perfectly good quantity:


quantity(10 bunch_of_carrots / 2 h)   # → 5 bunch_of_carrots/h, a rate

Two nominal units can also multiply/divide against each other, as long as they belong to the same family — the result is nominal-typed but no longer a plain member of the family (bunch_of_carrots * cabbages is a vegetables^2-shaped quantity, not a vegetables):


quantity(2 bunch_of_carrots * 3 cabbages)   # → 6 bunch_of_carrots.cabbages
quantity(6 bunch_of_carrots / 3 cabbages)   # → 2 bunch_of_carrots/cabbages

Multiplying/dividing nominal units from different families is still rejected — there’s no meaningful composite of bunch_of_carrots (vegetables) and some unit from an unrelated family:


quantity(2 bunch_of_carrots * 3 apples)   # error: incommensurable units (different families)
OperationSame unitSame family, no castSame family, as <family>Different family
+ / -✅❌ error✅ widens via scale❌ error
* / /✅✅ (nominal, family-typed)n/a (not a valid cast target for *//)❌ error
* / / with an ordinary (non-nominal) unit✅ composes into a rate, e.g. bunch_of_carrots/h———

Arithmetic and conversion

A quantity literal may contain a single arithmetic operation between two measurements, evaluated at parse time:


quantity(42 km / 30 min)   # a speed: 1.4 km/min
quantity(10 N / 1 m^2)     # a pressure: 10 N/m^2 (i.e. 10 Pa)

A trailing as converts the result to a target unit, or reduces it to SI base units. The target must be dimensionally compatible:


quantity(42 km/h as m/s)       # → 11.666… m/s
quantity(90 lb as kg)          # → 40.82… kg
quantity(42 km / 30 min as km/h)  # → 84 km/h
quantity(10 N/m^2 as Pa)       # → 10 Pa (named derived unit)
quantity(5 km as SI)           # → 5000 m (reduce to base units)

Converting across incompatible dimensions (quantity(5 km as s), a length to a time) is a compile error.

The dimensional rules are the ordinary ones:

OperationRequirement
+ / -both operands share the same dimension
* / /none (dimensions combine)
as / converttarget dimension equals source dimension

These are the rules for ordinary (physical, and derived/currency) units. A nominal unit follows a different rule for +/-: it requires the same specific unit (not merely the same dimension — nominal units are all dimensionless). *// compose freely with ordinary units (bunch_of_carrots/h) and with other nominal units of the same family (bunch_of_carrots * cabbages); different families still reject. See that section for the as <family> widening cast, which is the only way to +/- two different nominal units.


Dimensions

Every unit reduces to a vector over the seven SI base dimensions:

DimensionLetterSI base unit
LengthLm
MassMkg
TimeTs
Electric currentIA
TemperatureΘK
Amount of substanceNmol
Luminous intensityJcd

A quantity’s dimension is its real type. km/h and mi/h are different units but the same dimension (L1.T-1, a speed), and so they are comparable; kg and m are not. Two quantities are dimensionally compatible when their dimension vectors are equal.

There is an eighth, non-SI axis, currency, used only by EUR and any derived currency unitdef a project declares — it behaves exactly like the seven physical axes above (composes, converts, compares). Nominal units sit outside this vector entirely: they’re dimensionless (all-zero vector) but carry a separate family tag that ordinary dimension-vector equality doesn’t see, which is precisely why they need their own arithmetic rules instead of just being “another dimension”.


Quantities as values of a float field

There is no quantity primitive type: a quantity’s type is the dimension the value carries. The loosest way to hold one is a numeric range with quantity(...) values:


concept Animal:
  has weight: optional float   # the value supplies the unit

fact rex a Dog
  weight quantity(45 kg)

The declared float range says only “a number”. The unit lives in the value, so one attribute can hold quantity(45 kg) for one animal and quantity(90 lb) for another and still compare them correctly. float accepts any quantity (quantity(45 kg), quantity(3 s)) or a bare number alike — nothing checks the dimension.

Dimension-typed properties

To require a specific dimension, declare the range as a physical dimension instead of float. The dimensions are built-in lowercase quantity datatypes, like float or date; nothing needs to be declared (there is no concept Mass and no units file to import):

TypeDimensionQUDT quantity kind (rdfs:range)
lengthL1quantitykind:Length
massM1quantitykind:Mass
durationT1(xsd:duration, see below)
currentI1quantitykind:ElectricCurrent
temperatureTh1quantitykind:Temperature
amountN1quantitykind:AmountOfSubstance
luminosityJ1quantitykind:LuminousIntensity
speedL1.T-1quantitykind:Velocity
accelerationL1.T-2quantitykind:Acceleration
forceL1.M1.T-2quantitykind:Force
pressureL-1.M1.T-2quantitykind:Pressure
energyL2.M1.T-2quantitykind:Energy
powerL2.M1.T-3quantitykind:Power
frequencyT-1quantitykind:Frequency
areaL2quantitykind:Area
volumeL3quantitykind:Volume
densityL-3.M1quantitykind:Density
momentumL1.M1.T-1quantitykind:Momentum

These names are types only in type position. They are not keywords, so length, power or area stay valid field names: has length: length is fine. time is not in the list: it is the xsd:time clock-time type, and the time dimension is duration.


concept Animal:
  has weight: mass   # was: optional float

fact rex a Dog
  weight quantity(45 kg)      # OK — a mass

fact oops a Dog
  weight quantity(3 m)        # error: S008 dimension mismatch (length, not mass)

Analysis checks every quantity(...) value assigned to that property against the dimension — a mismatched dimension or a bare number (weight 45) is diagnostic S008 DIMENSION_MISMATCH. In Turtle, such a property is an owl:DatatypeProperty whose rdfs:range is the QUDT quantity kind (quantitykind:Mass), declared as an rdfs:Datatype; the value is a literal whose datatype is its unit (see Representation in Turtle).

duration is the time dimension

A duration field holds a time span, written either as a temporal duration(...) or as any time quantity. Both forms mean the same value:


concept Dog:
  has nap: optional duration

fact rex a Dog
  nap duration(20 min)     # → "PT20M"^^xsd:dayTimeDuration

fact bella a Dog
  nap quantity(20 min)     # → "PT20M"^^xsd:dayTimeDuration, the same value

In unit algebra duration is the T1 dimension, so quantity(3 km / 30 min) is a speed. Months and years have no fixed length, so mo and y are refused in quantity arithmetic and conversion (quantity(1 mo) is error S007 “Calendar unit ‘mo’ has variable length”). Calendar durations remain available as duration(1y 6mo), which compiles to "P1Y6M"^^xsd:duration.

Legacy capitalised names

Models written before the built-in types declared concept Mass (often in a shared units.dlf) and used it as a range (has weight: Mass or has weight: unit.Mass). Such models still compile to the same Turtle as has weight: mass, with two lint warnings, each with a quick-fix. The capitalised name only resolves while that declaration is there: Mass alone, with no concept Mass, is an unresolved type, so apply both fixes.

LintTriggered byQuick-fix
quantity/capitalized-dimension-typehas weight: Mass, unit.Massreplace with mass
quantity/builtin-dimension-conceptconcept Massdelete the declaration

A concept named after a built-in dimension is not written as an rdfs:Class. The legacy name Time maps to duration.


Comparisons

A comparison against a quantity, in a rule or query constraint block, is unit-aware:


rule flag_overweight_dog:
  match:
    ?dog a Dog
    ?dog weight [ > quantity(40 kg) ]
  then:
    ?dog a OverweightAnimal

The comparison compares physical magnitudes, not lexical numbers:

  • The two operands must share a dimension. A mismatch (weight in kilograms compared to quantity(3 m)) makes the comparison fail and the row is dropped, it never matches. In a query, when the property’s dimension is declared (has weight: mass), analysis reports this as an S008 warning ( `weight` is a Mass but is compared with a Length quantity, this query matches nothing). A plain float property carries no dimension, so a mismatch on it stays silent.
  • Compatible operands are compared by their SI magnitude (value × coefficient). So a dog whose weight was written quantity(90 lb) (≈ 40.8 kg) does trip a > quantity(40 kg) threshold, even though 90 and 40 alone would not suggest it.
Video 3.2.2 · Units and quantities, chapter “Compare across units” (0:24–1:08). Watch on PeerTube

For the unit-aware path to run, both sides must be quantities. A bare number (weight [ > 40.0 ]) is compared as a plain scalar, with no unit reasoning, so mixing a quantity(...) value with a bare-number threshold does not do what you want. Keep both the stored values and the threshold as quantity(...).

Use > < >= <=, not = / !=. After a quantity is normalised to its SI magnitude, two “equal” measurements almost never match to the bit, so exact (in)equality is unreliable. The linter warns on = / != against a quantity (rule semantic/quantity-exact-comparison). The ordering operators are the sound ones.


Representation in Turtle

A quantity compiles to a typed literal whose datatype is its unit, taken from QUDT, the shared vocabulary for units and quantity kinds. Each distinct unit used in the output gets one qudt: description:

@prefix qudt: <http://qudt.org/schema/qudt/> .
@prefix quantitykind: <http://qudt.org/vocab/quantitykind/> .

:weight a owl:DatatypeProperty ;
    rdfs:range quantitykind:Mass .

:rex   :weight "45"^^<http://qudt.org/vocab/unit/KiloGM> .
:bella :weight "90"^^<http://qudt.org/vocab/unit/LB> .
:rex   :nap    "PT20M"^^xsd:dayTimeDuration .

# one block per quantity kind used as a range, and per unit used in the data:
quantitykind:Mass a qudt:QuantityKind, rdfs:Datatype .
<http://qudt.org/vocab/unit/KiloGM> a qudt:Unit, rdfs:Datatype ;
    qudt:conversionMultiplier 1 ;
    qudt:hasDimensionVector <http://qudt.org/vocab/dimensionvector/A0E0L0I0M1H0T0D0> ;
    qudt:symbol "kg" ;
    qudt:hasQuantityKind quantitykind:Mass .
<http://qudt.org/vocab/unit/LB> a qudt:Unit, rdfs:Datatype ;
    qudt:conversionMultiplier 0.45359237 ;
    qudt:hasDimensionVector <http://qudt.org/vocab/dimensionvector/A0E0L0I0M1H0T0D0> ;
    qudt:symbol "lb" ;
    qudt:hasQuantityKind quantitykind:Mass .
  • Unit datatype: the QUDT unit IRI (kg → unit:KiloGM, km/h → unit:KiloM-PER-HR, lb → unit:LB), written as a full IRI (<http://qudt.org/vocab/unit/KiloGM>), not a prefixed name. The value is kept as written, in its own unit (no SI reduction).
  • Units QUDT lacks (a unitdef, an unusual compound such as km/min) get a datatype in the project namespace, <namespace>unit/<canonical> (km/min → …/unit/km.min-1, USD → …/unit/USD), described with the same qudt: triples.
  • qudt: terms: qudt:conversionMultiplier (factor to the SI base magnitude), qudt:hasDimensionVector (a QUDT qkdv: dimension vector), qudt:symbol (the human-readable unit), qudt:hasQuantityKind. A currency unit has no dimension vector; its kind is quantitykind:Currency.
  • Range: a property typed mass has range quantitykind:Mass, itself declared a qudt:QuantityKind, rdfs:Datatype. OWL reasoners do not treat a unit:KiloGM value as a member of that range: the dimension check is done by the SHACL shapes, which compare the value’s qudt:hasDimensionVector.
  • Time: any time quantity, and duration(...) without months or years, is an xsd:dayTimeDuration ("PT20M"), with no unit block. A duration field has range xsd:duration.
  • A dimensionless quantity carries no unit and degrades to a plain "<value>"^^xsd:double.
  • The qudt: and quantitykind: prefixes are only declared when the output has quantities.

How comparisons unfold

A quantity comparison in a query does not emit a bare scalar FILTER. It unfolds to standard SPARQL 1.1 that resolves the stored value’s unit and compares SI magnitudes, guarded by the dimension vector:

?s :topSpeed ?v .
BIND(datatype(?v) AS ?u)
?u <http://qudt.org/schema/qudt/conversionMultiplier> ?c .
?u <http://qudt.org/schema/qudt/hasDimensionVector> ?d .
FILTER(?d = <http://qudt.org/vocab/dimensionvector/A0E0L1I0M0H0T-1D0>
       && (xsd:double(str(?v)) * ?c) >= 16.666…)

The target magnitude (16.666…) and dimension vector are computed at compile time from the comparison literal. The unit descriptions must be in the queried dataset. A time target compares xsd:dayTimeDuration values directly (FILTER(?v >= "PT20M"^^xsd:dayTimeDuration)). Rules unfold the same way for the reasoner, using dedicated comparison predicates (dq:greaterThanOrEqual, …, in the https://dolfin.dev/quantity# namespace, since QUDT has no comparison functions) rather than overloaded math: builtins.

Displaying results

A result value keeps its unit datatype, so a client can reconstruct the unit for display: "45"^^unit:KiloGM renders as 45 kg, "42"^^unit:KiloM-PER-HR as 42 km/h. The datatype IRI maps back to a symbol via its qudt:symbol.


Errors

SituationResult
Unknown unit token (quantity(42 zonks))compile error INVALID_QUANTITY (S007)
Incompatible as conversion (5 km as s)compile error INVALID_QUANTITY (S007)
Wrong dimension on a dimension-typed property (has weight: mass, value quantity(3 m))compile error DIMENSION_MISMATCH (S008)
Bare number on a dimension-typed property (weight 45)compile error DIMENSION_MISMATCH (S008)
Two different nominal units combined without a cast (2 bunch_of_carrots + 3 cabbages)compile error, incommensurable units
*// between nominal units of different families (2 bunch_of_carrots * 3 apples)compile error, incommensurable units
as <family> on a unit that isn’t a member of that familycompile error INVALID_QUANTITY (S007)
Months or years in a quantity (quantity(1 mo), 1 y as s)compile error INVALID_QUANTITY (S007)
Dimension mismatch in a comparisonnot an error, the row is dropped at run time

The first four are reported by dolfin-analysis and surfaced in the editor. The last is a data-dependent runtime condition, so it is handled by dropping the non-matching row rather than by a diagnostic.

Video 3.2.2 · Units and quantities, chapter “Which units?” (1:07–1:34). Watch on PeerTube