Units
Overview
Every number in a physics problem means something — a length, a force, a temperature — and a number with the wrong meaning is a silent disaster. The classic example is the 1999 Mars Climate Orbiter: one team reported a thruster impulse in pound-force·seconds while another read it in newton·seconds, and the spacecraft burned up because the two units never met.
Rasmah prevents this class of mistake by carrying physical dimensions on every quantity. A length is not just 0.5; it is 0.5 of a metre. The system then refuses to do something physically meaningless — you cannot add a length to a pressure — and it converts between units for you so you can work in millimetres, tonnes, and megapascals, or in inches and pounds-force, while the solver always sees clean SI values.
This chapter introduces the three ideas behind Rasmah's units:
- a dimension — the pattern of base-quantity exponents that tells you what a number measures;
- a quantity — a value tagged with a dimension, always stored in SI;
- a unit system — the choice of units used only at the boundary, to read bare numbers and to render results.
Dimensions: what a number measures
Dimensional analysis says that any mechanical quantity is built from a small handful of base quantities. Rasmah tracks the seven SI base quantities — length $L$, mass $M$, time $T$, electric current $I$, thermodynamic temperature $\Theta$, amount of substance $N$, and luminous intensity $J$ — plus plane angle. Every other quantity is a product of powers of these, so a dimension is just the list of exponents. Pressure is force per area,
\[\text{pressure} = \frac{M\,L\,T^{-2}}{L^{2}} = M\,L^{-1}\,T^{-2},\]
so its dimension is the exponent tuple $(L, M, T) = (-1, 1, -2)$. Rasmah lets those exponents be rational, which is what makes fractional dimensions such as fracture toughness ($\text{Pa}\cdot\sqrt{\text{m}} = M\,L^{-1/2}\,T^{-2}$) representable.
Rasmah.Units.Dimensions — Type
Dimensions(exps)The 8 base-quantity exponents that define a physical dimension, in the order (length, mass, time, electric current, thermodynamic temperature, amount of substance, luminous intensity, plane angle).
Dimensional analysis expresses every quantity as a product of base-quantity powers. A dimension is that pattern of powers: pressure is $M\,L^{-1}\,T^{-2}$, so its exponent tuple is $(L, M, T, I, \Theta, N, J, \text{angle}) = (-1, 1, -2, 0, 0, 0, 0, 0)$. The exponents are rational, so fractional dimensions (e.g. fracture toughness $\text{Pa}\cdot\sqrt{\text{m}} = M\,L^{-1/2}\,T^{-2}$) are representable. Plane angle is tracked only for display (radians/degrees); solvers treat it as a plain number.
Arguments
exps: an 8-tuple of integer (or rational) exponents, in the base-quantity order above.
Returns
A Dimensions tag. The common dimensions are pre-computed constants — Rasmah.D_LENGTH, Rasmah.D_MASS, Rasmah.D_PRESSURE, Rasmah.D_FORCE, Rasmah.D_ENERGY, … — and Rasmah.DIMENSIONLESS is the all-zero tuple.
Example
julia> Dimensions((-1, 1, -2, 0, 0, 0, 0, 0)) == Rasmah.D_PRESSURE
trueThe most common dimensions are pre-computed as constants — Rasmah.D_LENGTH, Rasmah.D_MASS, Rasmah.D_PRESSURE, Rasmah.D_FORCE, Rasmah.D_ENERGY, and so on — and Rasmah.DIMENSIONLESS is the all-zero tuple.
Rasmah.D_PRESSURE == Rasmah.D_MASS * Rasmah.D_LENGTH / Rasmah.D_TIME^2falseQuantities: a value with a dimension
A Quantity is a number carrying its dimension. The stored value is always in SI base units — the units you type are converted to SI the moment the quantity is built. This one invariant is what keeps the solver fast and the AD path clean: units are a facade, and the compute kernel only ever sees plain Float64.
Rasmah.Units.Quantity — Type
Quantity(x, dim::Dimensions)
Quantity(x, u::UnitDef)
Quantity(x::Real)A number tagged with physical dimensions. The stored value val is always in SI base units; dim is the Dimensions tag.
A physical quantity is a magnitude times a unit, $q = v \cdot [q]$. Rasmah splits the two: the magnitude v is kept in SI base units, and the unit's meaning — the pattern of base-quantity exponents — is carried by the dimension. Keeping the magnitude SI-canonical is what lets the solver and the AD path stay in plain Float64 while the boundary layer still knows what each number measures.
Construct with the string macro (10u"mm"), from a unit (Quantity(10, u"mm")), or from a raw SI value and a dimension (Quantity(0.5, Rasmah.D_LENGTH)).
Arguments
x: the magnitude. A bareRealis taken as SI; withuit is read inu's unit.dim::Dimensions: the physical dimension (a bareRealis dimensionless).u::UnitDef: the unit to interpretxin (converted to SI on entry).
Returns
A Quantity whose val is in SI base units.
Example
julia> 10u"mm"
0.01 m
julia> 2u"m" * 3u"s"
6.0 m·s
julia> ustrip(10u"mm")
0.01Quantities behave like the numbers underneath, with the dimensions doing the bookkeeping. Adding or subtracting requires equal dimensions; multiplying and dividing combine them; powers, sqrt, abs, and comparisons all propagate or check them:
2u"m" * 3u"s"6.0 m·s10u"m" / 2u"s"5.0 m/ssqrt(4u"m^2")2.0 m1u"m" + 2u"m"3.0 mThe same type checks protect you everywhere: an attempt to add a length to a pressure throws a DimensionError instead of producing a meaningless number.
Rasmah.Units.DimensionError — Type
DimensionErrorError thrown when an operation between quantities with incompatible dimensions is attempted (e.g. adding a length to a mass).
Unit literals and parsing
Units are written with the u"…" string macro, which understands the full SI prefix family and compound expressions:
Rasmah.Units.@u_str — Macro
@u_str(str)String macro for unit literals: u"mm" returns a UnitDef (magnitude 1); a value is attached by juxtaposition — 10u"mm" is 10 * u"mm", handled by *(Number, UnitDef), so affine temperature scales (25u"°C") convert correctly.
Example
julia> u"mm"
mm
julia> 25u"°C"
298.15 K10u"mm"0.01 m25u"°C"298.15 KThe affine temperature scales (°C, °F, °R) carry an offset as well as a scale, which is why 25u"°C" is 298.15 K — the same absolute temperature, not 25 of anything.
For programmatic use the same parser is exposed as uparse (returns the dimension, the SI factor, and the offset) and uparse_quantity (returns a Quantity):
Rasmah.Units.uparse — Function
uparse(str) -> (dim::Dimensions, factor::Float64, offset::Float64)Parse a unit expression ("mm", "kg/m^3", "N*m", "W/(m·K)", "°C") into its dimension, its SI magnitude (the factor a value in that unit is multiplied by to get the SI value), and an additive offset (nonzero only for the temperature scales °C/°F, which are only valid as standalone units).
Rasmah.Units.uparse_quantity — Function
uparse_quantity(x, str) -> QuantityParse the unit string str and return x as a Quantity expressed in that unit (e.g. uparse_quantity(10, "mm")).
uparse_quantity(10, "mm")0.01 mThe unit system
The UnitSystem decides two things: how bare numbers are interpreted when they cross the API boundary (a bare length is read in the system's length unit), and how quantities are rendered for display. Base units (length, mass, time, …) and derived display units (force, pressure, stress, energy, …) may be set independently, so a system can be non-consistent — millimetres for length with newtons for force and megapascals for stress, the way structural engineers actually work.
Rasmah.Units.UnitSystem — Type
UnitSystem(; length, mass, time, current, temperature, amount, luminous,
angle, force, pressure, stress, energy, power, velocity, density,
viscosity, frequency)The units used to interpret bare numbers at the API boundary and to render Quantity values for display. Base and derived units may be given as a UnitDef, a Quantity (u"mm"), or a unit name ("mm"). Derived display units (force, stress, …) are independent of the base units, so they may be a non-consistent set (e.g. mm + N + MPa). When a derived unit is omitted it is derived from the base units. SI is the canonical internal system regardless of these choices.
The active system is a global setting, queried and changed with:
Rasmah.Units.unitsystem — Function
unitsystem() -> UnitSystemThe currently active unit system.
Rasmah.Units.set_unitsystem! — Function
set_unitsystem!(us::UnitSystem) -> UnitSystem
set_unitsystem!(; length="mm", mass="tonne", ...)Set the active unit system. The keyword form keeps the current system's other units and changes only those named.
unitsystem().lengthmset_unitsystem!(MM_TONNE_S)
10u"mm"10.0 mmset_unitsystem!(SI_UNITS)
10u"mm"0.01 mThree ready-made systems cover the common cases:
Rasmah.Units.SI_UNITS — Constant
SI_UNITSThe SI unit system (m, kg, s, A, K, mol, cd, with degrees for angles).
Rasmah.Units.MM_TONNE_S — Constant
MM_TONNE_SThe millimetre–tonne–second unit system (structural FEA): length mm, mass tonne, force N, pressure/stress MPa, energy mJ.
Rasmah.Units.IN_LBF_S — Constant
IN_LBF_SThe US customary inch–pound–second unit system (structural FEA): length inch, mass lbm, force lbf, stress ksi, energy in·lbf, power hp, temperature °F.
Converting and stripping
Because the SI value never changes, converting is just a change of display:
Rasmah.Units.ustrip — Function
ustrip(q) -> Number
ustrip(u, q) -> NumberStrip the units from a Quantity (returning its SI value), or convert it to the value expressed in unit u. Bare numbers are returned unchanged.
This is the choke point into the solver: every geometry, material, and load entry point calls it on the way in, so the compute kernel and the AD path only ever see raw SI Float64.
Example
julia> ustrip(10u"mm")
0.01
julia> ustrip(u"mm", 10u"m")
10000.0Rasmah.Units.uconvert — Function
uconvert(u, q) -> NumberConvert a Quantity (or bare number) to the value expressed in unit u.
Rasmah.Units.udim — Function
udim(x) -> DimensionsThe Dimensions of a Quantity, UnitDef, or bare number (dimensionless).
ustrip(10u"mm")0.01uconvert(u"mm", 10u"m")10000.0udim(10u"mm") == Rasmah.D_LENGTHtrueustrip is also the choke point into the solver: every geometry, material, and load entry point strips its Quantity inputs to SI Float64 on the way in, so the compute kernel and the AD path never see a Quantity.
Display: units, names, and significant figures
Rendering a quantity in the active system's preferred unit is done by quantity_string; si_unit_string decomposes a raw dimension back into base units:
Rasmah.Units.quantity_string — Function
quantity_string(q) -> StringRender a quantity in the active unit system's preferred display unit.
Example
julia> quantity_string(2u"m")
"2.0 m"
julia> si_unit_string(Rasmah.D_PRESSURE)
"kg/(m·s^2)"Rasmah.Units.si_unit_string — Function
si_unit_string(d::Dimensions) -> StringThe SI base-unit string of a dimension (e.g. "kg/(m·s²)" for pressure, "" for dimensionless).
quantity_string(2u"m")"2.0 m"si_unit_string(Rasmah.D_PRESSURE)"kg/(m·s^2)"For a nicer scale, simplify re-expresses a quantity at the power-of-1000 prefix whose mantissa lies in $[1, 1000)$, so 1000 µm becomes 1.0 mm:
Rasmah.Units.simplify — Function
simplify(q::Quantity) -> ScaledQuantityRe-express q at the power-of-1000 SI prefix whose mantissa lies in [1, 1000), so simplify(1000u"µm") is 1.0 mm and simplify(1500u"m") is 1.5 km. Dimensions without a prefixable SI unit (angle, compound units) are returned in their base unit unchanged. Mass prefixes attach to the gram.
Example
julia> simplify(1000u"µm")
1.0 mm
julia> simplify(1500u"m")
1.5 km
julia> simplify(2500u"N")
2.5 kN
julia> simplify(1000u"kg")
1.0 MgRasmah.Units.ScaledQuantity — Type
ScaledQuantity(value, unit)A quantity re-expressed at a chosen unit scale, returned by simplify. ustrip gives the SI value; Quantity(::ScaledQuantity) converts back.
simplify(1000u"µm")1.0 mmsimplify(1500u"m")1.5 kmsimplify(2500u"N")2.5 kNsimplify(1000u"kg")1.0 MgMass is special: prefixes attach to the gram (the kilogram is the SI base unit but already carries a prefix), which is why 1000 kg simplifies to 1.0 Mg.
The number of significant digits used when rendering — and the full spelled-out name and one-line description of a unit — are exposed as:
Rasmah.Units.sigfigs — Function
sigfigs() -> Union{Nothing,Int}The number of significant digits used to render Quantity values (see quantity_string). nothing means full precision.
Rasmah.Units.set_sigfigs! — Function
set_sigfigs!(n)Set the number of significant digits used to render Quantity values. Pass nothing for full precision (the default), or a positive integer for significant-figure rounding (e.g. set_sigfigs!(6)).
Rasmah.Units.unit_name — Function
unit_name(x) -> StringThe full (spelled-out) name of a unit. Accepts a UnitDef, a unit Quantity (u"mm"), a Symbol, or a unit name String:
unit_name(u"mm") == "millimeter"
unit_name(u"GPa") == "gigapascal"
unit_name(u"kg/m^3") == "kilogram per cubic meter"
unit_name(u"W/(m·K)") == "watt per meter kelvin"
unit_name(u"°C") == "degree Celsius"Rasmah.Units.unit_info — Function
unit_info(x) -> StringA one-line description of a unit: its symbol, full name, and dimension.
unit_name(u"mm") == "millimeter"trueunit_name(u"GPa") == "gigapascal"trueunit_info(u"mm") == "mm — millimeter — dimension: m"trueSignificant figures are display-only — they never change the stored SI value — and they also absorb the tiny floating-point residue of imperial/affine conversions:
set_sigfigs!(6)
uconvert(u"°F", 100u"°C")211.99999999999994set_sigfigs!(nothing)Units through the pipeline
Because the invariant is SI inside, units at the boundary, units thread through the whole pipeline without ever reaching the solver. Geometry constructors accept Quantity arguments, and geometric measurements come back tagged:
volume(cylinder(1, 2))6.283185307179586 m^3volume(box(2u"mm", 3u"mm", 4u"mm"))2.4e-8 m^3Materials expose each property with its physical dimension:
Rasmah.Materials.quantity — Function
quantity(m, sym::Symbol) -> QuantityThe value of a material property as a Quantity with its SI unit (for display).
Rasmah.Materials.property_dimension — Function
property_dimension(sym::Symbol) -> DimensionsThe physical dimension of a material property accessor (e.g. property_dimension(:youngs_modulus) is pressure). Throws KeyError for an unknown property.
Rasmah.Materials.property_dimensions — Function
property_dimensions() -> Dict{Symbol,Dimensions}A copy of the physical-dimension table mapping each property accessor symbol to its Dimensions (e.g. :youngs_modulus → pressure).
si_unit_string(property_dimension(:youngs_modulus))"kg/(m·s^2)"si_unit_string(property_dimension(:density))"kg/m^3"And a solved field — or a load applied to it — knows its own dimension, so results and boundary conditions can be checked and rendered correctly:
Rasmah.field_dimension — Function
field_dimension(physics, field::Symbol) -> DimensionsThe physical dimension of a solved field (e.g. field_dimension(Elasticity(), :u) is length). Falls back to dimensionless for unknown fields.
Rasmah.load_dimension — Function
load_dimension(physics, field::Symbol) -> DimensionsThe physical dimension of a load/Neumann term on a field (a force for :u, a heat flux for :T, a charge density for :φ, …).
si_unit_string(field_dimension(Elasticity(), :u))"m"si_unit_string(load_dimension(Elasticity(), :u))"m·kg/s^2"si_unit_string(field_dimension(HeatConduction(), :T))"K"A solved field is returned as a QuantityVector (or QuantityMatrix), a vector of values sharing one dimension; ustrip gives back the raw SI vector:
Rasmah.Units.QuantityVector — Type
QuantityVector(data, dim)A vector of quantities sharing one dimension, returned by unit-aware entry points. ustrip(v) gives the raw SI Vector; v[i] gives a scalar Quantity.
Rasmah.Units.QuantityMatrix — Type
QuantityMatrix(data, dim)A matrix of quantities sharing one dimension (e.g. a solved vector-valued field, one column per degree of freedom). ustrip(m) gives the raw SI Matrix; m[i, j] gives a scalar Quantity.
v = QuantityVector([1.0, 2.0, 3.0], Rasmah.D_LENGTH)3-element QuantityVector{Float64}:
1.0 m
2.0 m
3.0 mustrip(v)3-element Vector{Float64}:
1.0
2.0
3.0A worked example: a pressure vessel
Let's put it together. Build a cylinder of radius $1\,\text{m}$ and height $2\,\text{m}$ and check its volume against $V = \pi r^{2} h$:
volume(cylinder(1, 2))6.283185307179586 m^3volume(cylinder(1, 2)) ≈ π * 1^2 * 2trueThe same solid in millimetres gives the same physical volume; the number just changes with the unit:
volume(cylinder(1000u"mm", 2000u"mm"))6.283185307179586 m^3ustrip(volume(cylinder(1000u"mm", 2000u"mm"))) ≈ 2πtruebecause $1000\,\text{mm} = 1\,\text{m}$ and $V = \pi \cdot 1^{2} \cdot 2 = 2\pi$ in SI, and $2\pi \cdot (\text{a unit of volume})$ is the same amount of material however you print it.
Finally, the safety net: a dimensional slip is caught at the boundary, not in the solver. Ask for the volume of a shape in newtons and Rasmah refuses to even build the quantity, because a volume is $L^{3}$ and a newton is $M\,L\,T^{-2}$.
Next steps
Units are the vocabulary the rest of Rasmah speaks in. Continue to Materials, where every property is a Quantity with a physical dimension, and to the FEM chapters, where boundary conditions and solved fields carry their units into visualization. If you are coming from the geometry chapters, head back to Modeling operations for the remaining geometry, or on to Meshing.
