Dimensions & Units¶
Graphcal separates dimensions (compile-time types) from units (value-level scaling factors). This page covers the dimension algebra, unit definitions, conversion, and the prelude.
Dimensions¶
A dimension represents a physical quantity kind (e.g., length, time, mass). Dimensions form an algebra over base dimensions.
Base Dimensions¶
The prelude provides 8 base dimensions:
| Base Dimension | Symbol |
|---|---|
Length |
L |
Time |
T |
Mass |
M |
Temperature |
Θ |
ElectricCurrent |
I |
Amount |
N |
LuminousIntensity |
J |
Angle |
A |
Plus the special dimension Dimensionless (the identity element).
Derived Dimensions¶
The prelude already provides common derived dimensions such as Velocity,
Acceleration, Force, Energy, Power, Frequency, Pressure, Area,
and Volume. Define a derived dimension only when your model needs a
project-specific quantity kind:
Derived dimensions use algebraic expressions. The allowed operations are:
| Operation | Syntax | Example |
|---|---|---|
| Multiplication | A * B |
Mass * Length |
| Division | A / B |
Length / Time |
| Exponentiation | A^n |
Time^2 |
Exponents are non-zero integers (Time^2, Length^-1) or parenthesized rationals (Length^(1/2), m^(-3/2)); the same exponent grammar applies to dimension and unit expressions, including conversion targets.
User-Defined Base Dimensions¶
Declare a new base dimension with the base dim syntax:
Then build derived dimensions from it:
Dimension Algebra Rules¶
Internally, dimensions are represented as products of base dimensions with rational exponents. The compiler performs:
Length * Length = Length^2Length / Length = Dimensionless(Mass * Length / Time^2) * (Length / Time) = Mass * Length^2 / Time^3
Two expressions are dimensionally compatible if and only if they reduce to the same canonical form.
Units¶
Units are value-level scaling factors tied to a specific dimension. They define how a numeric value maps to the SI base.
Prelude Units¶
| Dimension | Units |
|---|---|
| Length | m, km (1000 m), cm (0.01 m), mm (0.001 m) |
| Time | s, min (60 s), h (3600 s) |
| Mass | kg, g (0.001 kg) |
| Temperature | K |
| ElectricCurrent | A |
| Amount | mol |
| LuminousIntensity | cd |
| Angle | rad, deg (π/180 rad) |
| Force | N (kg*m/s^2), kN (1000 N) |
| Energy | J (N*m), kJ (1000 J) |
| Power | W (J/s), kW (1000 W) |
| Pressure | Pa (N/m^2), kPa (1000 Pa), MPa (1e6 Pa) |
| Frequency | Hz (1/s) |
Defining Custom Units¶
const unit mile: Length = 1609.344 m;
const unit knot: Velocity = 0.514444 m/s;
base unit bit: Information; // canonical unit for a user-defined dimension
const unit byte: Information = 8.0 bit;
const unit kB: Information = 1000.0 byte;
A base unit declaration (base unit bit: Information; with no = ...) defines the one canonical unit for a user-defined base dimension. It is valid only when the dimension reduces to a single base dimension that does not already have a base unit. Prelude dimensions already have canonical units, and compound derived dimensions already have canonical representations, so declarations such as base unit furlong: Length; and base unit kt: Velocity; are rejected (D036). Define every additional unit with an explicit = ... scale instead; this prevents an accidental scale-1 alias from silently equating unrelated units. Non-base units must always carry an = ... body. Use const unit for compile-time scales and plain unit for runtime-dependent scales. The keyword determines constness: even a plain unit with a static-looking body is not usable from a const node. A const unit scale cannot contain @ references, and const node bodies may only use prelude units, base unit, and const unit declarations.
User unit definitions on bare Temperature are rejected (D014): the common temperature units (°C, °F) are affine scales with an offset, which a multiplicative unit definition cannot express — const unit C: Temperature = 1.0 K; would print 300 K as a meaningless 300 C. Keep absolute temperatures in K, or model offsets explicitly in expressions. Compound dimensions involving Temperature (e.g. Temperature / Time) still accept unit definitions, since offsets cancel in differences and rates.
The unit expression on the right-hand side must have exactly the declared dimension. For example, const unit wrong: Length = 1.0 h; is rejected instead of attaching an hour's scale to Length. This rule applies equally to static and runtime-dependent definitions, including compound and reciprocal unit expressions.
Unit scale factors must be positive and finite. Static unit definitions such as const unit z: Length = 0.0 m;, negative scales, and overflowing scales are rejected. Dynamic unit scales apply the same concrete-value check at evaluation time.
Unit Scoping¶
Static units follow the same scoping rules as every other imported category. A bare reference (@a -> mile) resolves against the file's own unit scope: the prelude's units, the file's own declarations, and selectively imported static units (import app.units::{ unit mile };). A module imported with an alias exposes its pub const unit declarations under that alias — import app.units as u; makes the static unit available as u::mile, and only as u::mile:
import app.units as u; // defines `pub const unit mile: Length = 1609.344 m;`
param a: Length = 3218.688 m;
node b: Length = @a -> u::mile; // 2 u::mile
Referencing an alias-imported unit by its bare name is an unknown-unit error (D003). A unit member uses the same explicit boundary as every other imported category: alias::unit, or alias.child::unit after dotted child-DAG traversal.
Because each alias scopes its own names, two modules may define the same unit name differently and both stay usable — ua::mile and ub::mile never collide. Selectively importing the same bare name from two modules is rejected as a duplicate import, like any other name clash.
Local const unit definitions can reference imported const units in their bodies, with either import form: const unit halfmile: Length = 0.5 u::mile; after import app.units as u;, or const unit halfmile: Length = 0.5 mile; after import app.units::{ unit mile };.
Dynamic Units¶
A unit's scale factor can depend on runtime values (params or nodes) by using a parenthesized expression with @-references:
base dim Money;
base unit USD: Money;
param usd_per_eur: Dimensionless = 1.08;
unit EUR: Money = (@usd_per_eur) USD;
Here, 1 EUR = usd_per_eur USD. The scale factor is evaluated at runtime, so
binding usd_per_eur at evaluation time (e.g., via
--param 'usd_per_eur=1.20') changes all EUR-denominated values accordingly.
Dynamic unit definitions are fully checked even when the unit is never used. The scale expression must have the scalar type Dimensionless: dimensioned quantities, Bool, Int, structs/unions, and indexed values are rejected (D032). Runtime params and nodes may be referenced; assertions and external plugin functions may not. The right-hand unit expression must still have exactly the declared dimension (D031).
Only the scale's concrete value is deferred until evaluation, after its referenced params and nodes have been computed. That value must be positive and finite; otherwise any declaration using the unit fails instead of receiving a fallback scale.
A plain unit belongs to the concrete runtime DAG instance whose params and nodes determine its scale. This capability follows the source marker, not constant folding: every plain unit is non-importable (M025), even when its right-hand side contains no @ reference. Use const unit when a blueprint-stable unit must cross an import boundary.
Each include or direct call uses its own bindings to determine dynamic unit scales, and its outputs retain those display units. A module include exposes a unit through its instance namespace (fx::EUR); a selective include can give it an alias (unit EUR as euros). Dimensions and base units used in the caller's annotations still require explicit import declarations.
Using Units¶
Attach a unit to a numeric literal:
param altitude: Length = 200.0 km;
param duration: Time = 1.5 h;
const node c: Velocity = 299792458.0 m/s;
Compound unit expressions are supported:
A unit suffix may also begin with the reciprocal shorthand 1/unit or a
parenthesized unit group:
Only the exact integer 1 is valid as a reciprocal numerator. The formatter
normalizes redundant grouping, so 3.0 (m/s) formats as 3.0 m/s.
The SI value produced by a quantity literal must remain finite. For example, a literal whose numeric value times its unit scale overflows is an error rather than inf.
Unit Conversion¶
The -> operator converts a value to a different unit of the same dimension:
param alt: Length = 200.0 km;
node alt_m: Length = @alt -> m; // 200000.0 m
node alt_cm: Length = @alt -> cm; // 20000000.0 cm
The source and target must share the same dimension. Attempting to convert between incompatible dimensions is a compile-time error.
-> also distributes element-wise over indexed values: @x -> km on a Length[R] (or multi-axis Length[R, C]) applies the display unit to every entry.
Compound targets support the 1/unit reciprocal shorthand, matching how unit labels are displayed: @f -> 1/min is equivalent to @f -> min^-1. Only a literal 1 is allowed as the numerator.
The conversion changes only the display unit, not the SI value used in calculations. References, field access, index access, and DAG outputs preserve the selected display unit. The same applies to timezone displays on Datetime values.
If a display conversion cannot be performed, Graphcal reports a presentation diagnostic and retains the valid SI value. This does not suppress computational, domain, or assertion failures. In particular, a quantity literal needs a valid unit scale to compute its SI value; an invalid scale makes that computation fail.
-> is non-chaining: an expression carries at most one conversion target. Both the bare chain @alt -> km -> m (a parse error) and the parenthesized form (@alt -> km) -> m (a D012 dimension-check error) are rejected — only the outermost target could ever take effect, so an inner conversion is either a typo or dead code.
A conversion is only allowed where its display effect can land — the top level of a declaration body, an if/match branch, a constructor field initializer, a map-literal entry, a for-comprehension body, or a scan/unfold init or body (and a scan source). Each recurrence step preserves its selected presentation; an unannotated computed step retains the initial display preference. Anywhere else (arithmetic operands, function arguments, comparisons, conditions, assert bodies) the conversion would be silently inert, so it is rejected (D013).
Result Dimension Computation¶
When you write an expression like @a + @b, the compiler computes the dimension of the result from the operands:
| Expression | Result Dimension |
|---|---|
a + b |
Same as a and b (must match) |
a - b |
Same as a and b (must match) |
a * b |
Product of dimensions |
a / b |
Quotient of dimensions |
a ^ n |
Dimension raised to power n |
sqrt(a) |
Dimension raised to power ½ |
For example, sqrt(Length^2 / Time^2) infers Length / Time (= Velocity).