Uncertainty Sources

A SymbolicMeasurement records which independent measurements it derives from, and with what sensitivity, rather than carrying a bare uncertainty number. Every combined uncertainty, sensitivity coefficient, budget and covariance is derived from that one structure.

This page documents the representation. It is the foundation the propagation operators are being migrated onto; until that migration completes, the binary operators keep their earlier behaviour.

Why provenance is recorded

An uncertainty number alone cannot tell you where it came from. Two consequences follow, and both are visible to users:

  • x - x cannot be known to be exactly zero, because the two operands are indistinguishable once reduced to a pair of numbers;
  • two quantities that are correlated because they derive from a common upstream measurement cannot be recognised as such, which is precisely the case where a user cannot supply a covariance matrix by hand.

Recording the sources answers both from the same structure.

Sources are carried, not registered

The source descriptors and any declared covariances live in the quantity, not in module-level state.

This is deliberate. With a global registry, Symbolics.substitute would have nothing local to substitute into: replacing σx with 0.7 would have to reach shared state, and without that a fully substituted measurement would no longer yield a number. Carrying the descriptors keeps substitution purely local — the estimate, each sensitivity, each source uncertainty, each covariance.

The only global state is a counter that hands out fresh identifiers, which makes ± impure in exactly the way gensym is: bounded, with no leakage between sessions, and safe to call from several threads.

using Symbolics, SymbolicUncertainties
@variables V σV

m = V ± σV
SymbolicUncertainties.terms_of(m)     # one source, sensitivity 1
SymbolicUncertainties.sources_of(m)   # its standard uncertainty: σV
Dict{SymbolicUncertainties.SourceId, SymbolicUncertainties.Source} with 1 entry:
  SourceId(0x0000000000000094) => Source(σV, nothing, :V, V)

Each ± mints a fresh source. Two syntactically identical constructions are two independent measurements — the same σ symbol may perfectly well describe two different resistors of equal tolerance.

Write each input once, then reuse it

The flip side of that rule is a trap. Identity comes from the measurement object, never from the symbol name, so writing R2 ± σR2 twice in one model declares two independent resistors that happen to share a tolerance symbol — not one resistor used twice.

@variables Vin σVin R1 σR1 R2 σR2

# WRONG: `R2 ± σR2` appears twice, so R2 is two sources
wrong = (Vin ± σVin) * (R2 ± σR2) / ((R1 ± σR1) + (R2 ± σR2))
length(uncertainty_budget(wrong))
4
# RIGHT: one object per physical input, reused
v, r1, r2 = Vin ± σVin, R1 ± σR1, R2 ± σR2
right = v * r2 / (r1 + r2)
length(uncertainty_budget(right))
3

Four budget rows for three inputs is the visible symptom. The invisible one is the number: at Vin = 5 V, R1 = 1 kΩ, R2 = 3 kΩ with u = (0.01, 10, 20), the first form reports u_c = 0.0335 V and the second 0.0135 V. The correct value is 0.0135 — the first double-counts R2 instead of combining its two sensitivities, and nothing warns, because two sources sharing a standard uncertainty is exactly what two nominally identical instruments look like.

The rule is one line: bind each physical input to a variable once, then use that variable everywhere it appears in the model.

Combined standard uncertainty

The combined standard uncertainty is the quadratic form over the source covariance structure. With independent sources it reduces to JCGM 100:2008 §5.1.2 equation (10); when covariances are declared, the cross terms of §5.2.2 equation (13) appear. The two GUM formulas are two regimes of a single expression rather than two code paths.

m.err is therefore a computed property, not a stored field:

m.err     # σV

\[ \begin{equation} \mathtt{{\sigma}V} \end{equation} \]

Correlation

Two quantities that derive from a common source are correlated, and that correlation is propagated with no matrix supplied by anyone:

@variables V σV R σR
v = V ± σV
r = R ± σR

p = v * v / r    # power
i = v / r        # current

p / i            # exactly V ± σV — R cancels, V behaves correctly

\[V \pm σV\]

This is the case that matters in practice, and the one a user-supplied covariance matrix serves worst: the correlation exists because both quantities descend from the same voltage measurement, so the person writing the model is precisely the person who does not know the covariance.

Correlation between sources that are not shared — two instruments sharing a reference standard, say — is declared explicitly:

@variables x σx y σy ρ
a = x ± σx
b = y ± σy

a2, b2 = declare_correlated(a, b, ρ)
(a2 + b2).err        # carries the 2ρσₓσᵧ cross term of eq. (13)

\[ \begin{equation} \sqrt{\mathtt{{\sigma}x}^{2} + \mathtt{{\sigma}y}^{2} + 2 ~ \rho ~ \mathtt{{\sigma}x} ~ \mathtt{{\sigma}y}} \end{equation} \]

declare_correlated returns new quantities and mutates nothing, so two models in the same session can carry different and equally valid hypotheses. The declared covariance enters as the cross term of JCGM 100:2008 §5.2.2 equation (13); with ρ = 0 it collapses onto equation (10). There is one implementation, with two regimes.

Covariance between measurands

Two quantities that share sources have a covariance fixed by their linear forms, which JCGM 102:2011 §6 requires for a vector-valued measurand — a user combining two outputs further needs to know how they move together:

covariance(a2, b2)
correlation(a2, b2)

\[ \begin{equation} \frac{\rho ~ \mathtt{{\sigma}x} ~ \mathtt{{\sigma}y}}{\sqrt{\mathtt{{\sigma}x}^{2}} ~ \sqrt{\mathtt{{\sigma}y}^{2}}} \end{equation} \]

This once could not be answered: propagate_vector returned marginal uncertainties and left cross-output covariance out of scope, because nothing recorded which inputs an output derived from.

API

SymbolicUncertainties.SourceIdType
SourceId

Identity of one independent uncertainty source, per JCGM 100:2008 §4.1 (input quantities Xᵢ whose estimates are the xᵢ).

Two quantities sharing a SourceId derive from the same physical measurement, which is what makes their covariance computable without a user-supplied matrix.

Traces REQ-200.

source
SymbolicUncertainties.SourceType
Source

Description of one independent source: its symbolic standard uncertainty u (JCGM 100:2008 §2.3.1), its optional degrees of freedom (§G.3, used by Welch-Satterthwaite), a display name, and the input variable it perturbs.

variable is the input the source was declared on — the V of V ± σV. It is nothing when the estimate is numeric or is already a function of an input, such as (V + 1) ± σ, which is not itself an input of the measurement model. It is a provenance label rather than a value, so it survives substitution: a fully numeric budget can still say which input each contribution came from.

Traces REQ-200, REQ-208, REQ-235.

source
SymbolicUncertainties.declare_correlatedFunction
declare_correlated(m1, m2, ρ) -> (SymbolicMeasurement, SymbolicMeasurement)

Declare a correlation coefficient ρ between the sources of m1 and m2, returning new quantities carrying that hypothesis.

Nothing is mutated. A correlation hypothesis is a property of the measurement model, so it travels with the quantities it concerns rather than acting at a distance on shared state — two models in the same session can hold different, equally valid assumptions.

The declared covariance u(xᵢ,xⱼ) = ρ · u(xᵢ) · u(xⱼ) enters the combined uncertainty as the cross term of JCGM 100:2008 §5.2.2 equation (13). With ρ = 0 equation (13) collapses onto equation (10), so the independent case is a special case rather than a separate code path.

Both quantities must derive from a single source each; correlations between composite quantities are declared between their underlying sources.

Traces REQ-203, REQ-205.

source
SymbolicUncertainties.covarianceFunction
covariance(m1, m2) -> Symbolics.Num

Covariance between two measurands, derived from the sources they share.

JCGM 102:2011 §6 asks for the covariance matrix of a vector-valued measurand, not merely its marginal uncertainties: a user who combines two outputs further needs to know how they move together. Before M11 that could not be answered — propagate_vector returned marginal uncertainties and left cross-output covariance out of scope — because nothing recorded which inputs an output derived from.

It follows from the linear forms:

cov(y₁, y₂) = Σᵢ Σⱼ (∂y₁/∂sᵢ)(∂y₂/∂sⱼ) · cov(sᵢ, sⱼ)

with cov(sᵢ, sᵢ) = u²(sᵢ) and off-diagonal terms taken from the declared covariances (zero unless declared). covariance(m, m) is therefore the variance, and quantities sharing no source have covariance zero.

Traces REQ-033, REQ-203, REQ-205.

source
SymbolicUncertainties.correlationFunction
correlation(m1, m2) -> Symbolics.Num

Correlation coefficient r(y₁, y₂) = cov(y₁, y₂) / (u(y₁)·u(y₂)), per JCGM 100:2008 §5.2.2 equation (14).

Returns zero when either quantity carries no uncertainty, since the coefficient is then undefined rather than infinite.

Traces REQ-033, REQ-203.

source