Dimensional Analysis

Load DynamicQuantities.jl and check_units will tell you whether a model holds together dimensionally.

It returns a report and never throws. A unit mistake is something you want to be told about, not something that should stop you mid-derivation — which is what an earlier construction-time DimensionError did.

using Symbolics, SymbolicUncertainties, DynamicQuantities

@variables V I
check_units(V / I, Dict(V => u"V", I => u"A"))
# UnitReport: consistent

check_units(V + I, Dict(V => u"V", I => u"A"))
# UnitReport: 1 finding(s)
#   • + in `I + V` — dimensions differ (A vs m² kg s⁻³ A⁻¹)
UnitReport: 1 finding(s)
  • + in `I + V` — dimensions differ (A vs m² kg s⁻³ A⁻¹)

Units are an annotation of the symbol, supplied in a dictionary, never a value inside the expression. ModelingToolkit makes the same choice — it holds units in variable metadata — and the reason is practical: a quantity living inside the expression tree would be dragged through every simplification and every derivative.

Checking a measurement

Passing a SymbolicMeasurement also checks something no other tool does: that the combined uncertainty carries the dimension of the measurand. It is the most common unit error in a budget, and it is structurally invisible elsewhere because y and u live in different fields.

@variables σV σI
m = (V ± σV) / (I ± σI)
check_units(m, Dict(V => u"V", σV => u"V", I => u"A", σI => u"A"))
UnitReport: consistent

Dimension is not enough

Three cases defeat a plain dimension check. Each has its own annotation, because dimensional equality is necessary but never sufficient to decide that two quantities may be combined.

Affine scales — °C, °F

20 °C + 20 °C is not 40 °C. The zero of an affine scale is conventional, so absolute values do not add; only differences are proper intervals on the base unit. Both sides carry dimension Θ, so a dimension check passes it without a murmur.

using SymbolicUncertainties: Affine, Scaled, Kind
@variables T1 T2
celsius = Affine(u"K", :celsius, 273.15)

check_units(T1 + T2, Dict(T1 => celsius, T2 => celsius))  # reports
check_units(T1 - T2, Dict(T1 => celsius, T2 => celsius))  # consistent
UnitReport: consistent

The metrological corollary matters more than the arithmetic one: an uncertainty stated in °C is a kelvin interval, never an absolute temperature. A dispersion has no origin (JCGM 100:2008 §4.3.1).

Dimensionless is not one thing — %, ppm, dB

All three are dimensionless and none is interchangeable with another. dB is additionally logarithmic, so it does not even combine additively the way % and ppm do.

pct = Scaled(:percent, 1e-2)
ppm = Scaled(:ppm, 1e-6)
dB  = Scaled(:dB, nothing; logarithmic = true)
SymbolicUncertainties.Scaled(:dB, nothing, true)

Same dimension, different quantity

Torque and energy are both N·m. Activity and frequency are both s⁻¹. Adding a torque to an energy is a modelling error that dimensional analysis alone declares perfectly valid.

@variables M E
torque = Kind(u"N*m", :torque)
energy = Kind(u"N*m", :energy)

check_units(M + E, Dict(M => torque, E => energy))   # reports
UnitReport: 1 finding(s)
  • + in `E + M` — same dimension but different kind of quantity (energy [m² kg s⁻²] vs torque [m² kg s⁻²])

Per VIM §1.1 a quantity is not defined by its dimension; Kind records the part the dimension leaves out.

Getting the answer back with its unit

check_units verifies a model; evaluate uses the same walk to derive the unit of a result:

using SymbolicUncertainties, Symbolics, DynamicQuantities
@variables V I σV σI

r = (V ± σV) / (I ± σI)

evaluate(r, Dict(V => 5.0us"V", σV => 0.01us"V",
                 I => 0.5us"A", σI => 0.001us"A"))
(val = 10.0 A⁻¹ V, err = 0.0282842712474619 A⁻¹ V)

The unit on that answer was not written by anyone: it is what the model computes. A worked example that ends in a bare 10.0 and a # ohms comment records what its author believed the model produces, which is a different thing and is exactly the discrepancy dimensional analysis exists to expose.

The estimate and the uncertainty are annotated independently, so a u_c whose dimension has drifted from its own measurand (JCGM 100:2008 §4.3.1) shows up in the returned pair rather than hiding in a separate field. Where they agree — as they must in a correct model — the uncertainty is reported in the estimate's own unit.

Values may be us"..." quantities, u"..." quantities, or plain reals for dimensionless inputs like a gain. us"..." keeps unit names (A⁻¹ V) while u"..." reduces to SI base dimensions (m² kg s⁻³ A⁻²); the two are interchangeable and compare correctly against each other.

Every variable needs a value: evaluate raises rather than returning a half-substituted expression, since there would be no number to carry a unit. Use Symbolics.substitute for partial substitution.

Why DynamicQuantities rather than Unitful

Unitful encodes units in the type parameter, so every distinct unit combination is a distinct concrete type. DynamicQuantities keeps dimensions in a runtime field, so one type covers V, A, Ω and W alike. That matters here because an uncertainty budget's rows are deliberately heterogeneous in unit.

It is also the system ModelingToolkit actually validates: its screen_unit accepts DynamicQuantities quantities and passes Unitful metadata through unchecked. Since the package ships an MTK extension, matching that avoids a backend switch mid-model.

Both packages export @u_str, so using them together makes u"V" ambiguous. That is not a reason to pick one — DynamicQuantities' own README shows the coexistence idiom, using DynamicQuantities; import Unitful, and a qualified Unitful.u"V" works fine. It is simply a thing to know if you keep both loaded.

API

SymbolicUncertainties.check_unitsFunction
check_units(expr, units::AbstractDict) -> UnitReport

Check an expression, or a SymbolicMeasurement, for dimensional consistency under the supplied symbol annotations.

Returns a report; never throws on an inconsistency. Annotations may be DynamicQuantities quantities or one of Affine, Scaled, Kind for the cases a bare dimension cannot express.

Requires DynamicQuantities.jl to be loaded.

Implements the methodology of JCGM 100:2008 §4.1 read with VIM §1.1. Traces REQ-211, REQ-212, REQ-213, REQ-214.

source
SymbolicUncertainties.evaluateFunction
evaluate(m::SymbolicMeasurement, values::AbstractDict) -> NamedTuple

Substitute unit-carrying values into a SymbolicMeasurement and return (val = ..., err = ...) as quantities.

The unit of the result is derived from the model, by the same dimensional walk check_units performs, rather than written out by the caller. A worked example therefore cannot claim a unit its own model does not produce — the failure a hand-written # ohms comment cannot catch.

Arguments

  • m — the measurement to evaluate.
  • values — one entry per symbol in the model. Values may be DynamicQuantities quantities or plain Reals, the latter read as dimensionless.

Returns

A NamedTuple with val and err. Both carry the dimension the model gives them, derived independently, so a u_c that has drifted from the dimension of its own measurand (JCGM 100:2008 §4.3.1) is visible in the result.

Quantities built with u"..." reduce to SI base dimensions; those built with us"..." keep their symbolic form, which usually reads better. The choice is the caller's and is preserved.

Errors

  • A variable left without a value → ArgumentError. There would be no number to carry a unit, and returning a partly symbolic result would defeat the call; use Symbolics.substitute for partial substitution.
  • A unit the walk cannot determine → ArgumentError.

A dimensional inconsistency is warned about, not thrown, and the result is still returned — the same posture as check_units, which should be called directly for the full report.

Requires DynamicQuantities.jl to be loaded.

Traces REQ-238.

source
SymbolicUncertainties.UnitReportType
UnitReport

Result of check_units: the findings, in the order they were encountered. An empty report means the expression is dimensionally sound under the supplied annotations.

A report is data, not an exception — inspect it, print it, or ignore it. Nothing in the package changes behaviour based on its contents.

The findings it carries are the three cases dimensional equality cannot decide on its own: affine scales, where an uncertainty in °C is a kelvin interval because a dispersion has no origin (JCGM 100:2008 §4.3.1); scaled dimensionless quantities (%, ppm, dB); and non-fungible homonyms, a quantity not being defined by its dimension (VIM §1.1).

Traces REQ-212.

source
SymbolicUncertainties.UnitFindingType
UnitFinding

One dimensional inconsistency: what names the offending subexpression, why explains the rule it breaks, and details carries the conflicting annotations.

source
SymbolicUncertainties.AffineType
Affine(base, name, offset)

An affine-scale quantity such as °C or °F: its zero is conventional, so absolute values do not add, and only differences are proper intervals on base.

20 °C + 20 °C is not 40 °C, yet both sides carry dimension Θ and a dimension check passes it. An uncertainty stated in °C is a kelvin interval, never an absolute temperature (JCGM 100:2008 §4.3.1: u is a dispersion, and a dispersion has no origin).

Traces REQ-214.

source
SymbolicUncertainties.ScaledType
Scaled(name, factor; logarithmic = false)

A dimensionless ratio carrying a scale: %, ppm, dB.

All are dimensionless and none is interchangeable with another, which dimensional equality cannot see. dB is additionally logarithmic, so it does not even combine additively the way % and ppm do.

Traces REQ-214.

source
SymbolicUncertainties.KindType
Kind(unit, kind)

A quantity distinguished by more than its dimension.

Torque and energy are both N·m; activity and frequency are both s⁻¹. Adding a torque to an energy is a modelling error that dimensional analysis alone declares valid. Per VIM §1.1 a quantity is not defined by its dimension, and Kind records the part the dimension omits.

Traces REQ-214.

source