Uncertainty Budget
The uncertainty budget is the per-variable breakdown of the combined standard uncertainty u_c(y). It is the EA-4/02 §7.3 deliverable of every calibration report — one row per input variable, showing how much each contributes and how sensitive the measurand is to it.
SymbolicUncertainties.jl provides four purely-symbolic helpers that compose cleanly: sensitivity_coefficient, uncertainty_contribution, relative_sensitivity, and the one-call uncertainty_budget that assembles all three into the EA-4/02 §7.3 table.
The governing normative sections are JCGM 100:2008 §5.1.3 equations (11a) and (11b), and EA-4/02 §7.3 for the row layout.
Per-variable helpers
sensitivity_coefficient(m, xᵢ) returns the symbolic cᵢ = ∂(m.val)/∂xᵢ via Symbolics.derivative. It returns Num(0) when the variable does not appear in the measurand.
using Symbolics
using SymbolicUncertainties
using DynamicQuantities
@variables Vin σVin R1 σR1 R2 σR2
Vout = propagate(
(vin, r1, r2) -> vin * r2 / (r1 + r2),
[Vin ± σVin, R1 ± σR1, R2 ± σR2],
)
sensitivity_coefficient(Vout, Vin) # R2/(R1+R2)
sensitivity_coefficient(Vout, R1) # -Vin·R2/(R1+R2)²
sensitivity_coefficient(Vout, R2) # Vin·R1/(R1+R2)²\[ \begin{equation} \frac{\mathtt{Vin}}{\mathtt{R1} + \mathtt{R2}} + \frac{ - \mathtt{R2} ~ \mathtt{Vin}}{\left( \mathtt{R1} + \mathtt{R2} \right)^{2}} \end{equation} \]
uncertainty_contribution(m, xᵢ, σᵢ) returns |cᵢ|·σᵢ — the individual uncertainty contribution of xᵢ.
relative_sensitivity(m, xᵢ, σᵢ) returns (cᵢ·σᵢ)² / u_c²(y) — the EA-4/02 §7.3 "percentage-of-variance" column. Summing the relative sensitivities across all uncorrelated input variables and applying Symbolics.simplify returns 1.
The one-call budget table — uncertainty_budget
uncertainty_budget(m) returns an UncertaintyBudget: a vector of BudgetRows following the EA-4/02 §7.3 layout, together with the three facts a bare vector of rows cannot carry — the measurand y the rows decompose, the u_c they recombine into, and whether declared correlations are in play.
The rows come from the quantity's own sources: m knows which independent measurements it descends from and with what sensitivity, so no variables / sigmas list is required (REQ-206). A source that cancelled produces no row at all. The three-argument form uncertainty_budget(m, variables, sigmas) remains available as a filter when only some inputs are of interest.
| Field | Meaning |
|---|---|
source | Identity of the independent source (source-derived rows) |
variable | The input variable xᵢ (variable-filtered rows) |
sigma | Its standard uncertainty u(xᵢ) |
sensitivity | cᵢ = ∂(m.val)/∂xᵢ (JCGM 100:2008 §5.1.3 eq (11b)) |
contribution | uᵢ(y) = abs(cᵢ)·σᵢ (JCGM 100:2008 §5.1.3 eq (11a)) |
relative | (cᵢ·σᵢ)² / u_c²(y) (EA-4/02 §7.3 "%-of-variance" column) |
budget = uncertainty_budget(Vout)Uncertainty budget (EA-4/02 §7.3) for y = (R2*Vin) / (R1 + R2)
Vin: u = σVin c = R2 / (R1 + R2) |c|u = abs(R2 / (R1 + R2))*σVin
R1: u = σR1 c = (-R2*Vin) / ((R1 + R2)^2) |c|u = abs((-R2*Vin) / ((R1 + R2)^2))*σR1
R2: u = σR2 c = (R1*Vin) / (R1^2 + 2R1*R2 + R2^2) |c|u = abs((R1*Vin) / (R1^2 + 2R1*R2 + R2^2))*σR2
u_c = sqrt((((-R2*Vin) / ((R1 + R2)^2))^2)*(σR1^2) + (((R1*Vin) / (R1^2 + 2R1*R2 + R2^2))^2)*(σR2^2) + ((R2 / (R1 + R2))^2)*(σVin^2))The budget prints as a table and still indexes and iterates as a vector of its rows, so every consumer written against the earlier Vector{NamedTuple} keeps working:
length(budget), budget.correlated, budget[1].contribution(3, false, abs(R2 / (R1 + R2))*σVin)Passing the variables explicitly filters the same table:
filtered = uncertainty_budget(
Vout,
[Vin, R1, R2],
[σVin, σR1, σR2],
)
# filtered[1].variable == Vin
# filtered[1].sensitivity == R2/(R1+R2)
# filtered[1].contribution == abs(R2/(R1+R2)) · σVin
# filtered[1].relative == (R2/(R1+R2))² · σVin² / Vout.err²Uncertainty budget (EA-4/02 §7.3) for y = (R2*Vin) / (R1 + R2)
Vin: u = σVin c = R2 / (R1 + R2) |c|u = abs(R2 / (R1 + R2))*σVin
R1: u = σR1 c = (-R2*Vin) / ((R1 + R2)^2) |c|u = abs((-R2*Vin) / ((R1 + R2)^2))*σR1
R2: u = σR2 c = Vin / (R1 + R2) + (-R2*Vin) / ((R1 + R2)^2) |c|u = abs(Vin / (R1 + R2) + (-R2*Vin) / ((R1 + R2)^2))*σR2
u_c = sqrt((((-R2*Vin) / ((R1 + R2)^2))^2)*(σR1^2) + (((R1*Vin) / (R1^2 + 2R1*R2 + R2^2))^2)*(σR2^2) + ((R2 / (R1 + R2))^2)*(σVin^2))Variance-decomposition invariant (SC-002)
For uncorrelated inputs, the sum of the relative sensitivities across the complete input list simplifies to 1:
Symbolics.simplify(sum(row.relative for row in filtered))
# → 1\[ \begin{equation} 1 \end{equation} \]
Under a declared correlation the column is no longer a decomposition: the JCGM 100:2008 §5.2.2 equation (13) cross terms belong to no single row, so the fractions no longer sum to 1 and a negative cross term can push one row past 100 %. That is what budget.correlated reports, and why it is on the budget rather than left for the reader to infer.
This is asserted by test/budget/test_variance_decomposition.jl for the linear, product, and voltage-divider cases, and by the exit-gate test test/examples/test_budget_voltage_divider.jl for the full three-input voltage divider.
Error handling
length(variables) != length(sigmas)raisesDimensionMismatch.- A variable that does not appear in
m.valproduces a row withsensitivity = 0,contribution = 0,relative = 0— not an error. - A measurand whose derivative the symbolic engine cannot resolve propagates the REQ-021
ArgumentErrorfromsensitivity_coefficient, directing the user at theapply(f, m; derivative = ...)escape hatch.
Dominant source — dominant_source
dominant_source(m, variables, sigmas; values=nothing) returns the input variable whose variance contribution dominates the budget — the EA-4/02 §7.3 convention for "where should I focus calibration effort?" Call without values to get a symbolic first-variable-wins result (with an @info diagnostic about the order-dependence), or pass a substitution dictionary for a numeric argmax:
dom = dominant_source(
Vout,
[Vin, R1, R2],
[σVin, σR1, σR2];
# Volts and ohms. `dominant_source` ranks plain numbers, so the
# values are stripped where it needs them; declaring them with
# units keeps the reader from having to guess the scale.
values = Dict(
k => ustrip(v) for (k, v) in Dict(
Vin => 5.0us"V", σVin => 0.01us"V",
R1 => 1_000.0us"Ω", σR1 => 10.0us"Ω",
R2 => 3_000.0us"Ω", σR2 => 10.0us"Ω",
)
),
)
# dom.index, dom.variable, dom.contribution, dom.ranked_by(index = 2, variable = R1, contribution = abs((-R2*Vin) / ((R1 + R2)^2))*σR1, ranked_by = :numeric)When every sensitivity coefficient is structurally zero (the measurand does not depend on any variable in the list), the function returns (index=0, …) and emits a @warn.
Rendering the budget
An UncertaintyBudget is an AbstractVector of its rows, so it composes with every Julia table-rendering library without the package depending on one. as = :dataframe renders through the DataFrames.jl extension:
using DataFrames, PrettyTables
pretty_table(uncertainty_budget(Vout; as = :dataframe))The SymbolicUncertainties.jl runtime has no hard dependency on either package.
API reference
SymbolicUncertainties.sensitivity_coefficient — Function
sensitivity_coefficient(m::SymbolicMeasurement, xᵢ::Num) -> NumReturn the symbolic sensitivity coefficient cᵢ = ∂(m.val)/∂xᵢ per JCGM 100:2008 §5.1.3 equation (11b).
If xᵢ does not appear in m.val, returns Num(0). If the symbolic engine cannot compute a closed-form derivative, raises ArgumentError directing the user at apply(f, m; derivative = ...) (consistent with the M2 propagate failure pathway — REQ-021).
Implements the methodology of JCGM 100:2008 §5.1.3 equation (11b). Traces REQ-040.
SymbolicUncertainties.uncertainty_contribution — Function
uncertainty_contribution(m::SymbolicMeasurement, xᵢ::Num, σᵢ::Num) -> NumReturn the symbolic uncertainty contribution uᵢ(y) = |cᵢ| · σᵢ of variable xᵢ to measurement m, per JCGM 100:2008 §5.1.3 equation (11a).
Presented as a row of an EA-4/02 §7.3 uncertainty-budget table. Traces REQ-041.
SymbolicUncertainties.relative_sensitivity — Function
relative_sensitivity(m::SymbolicMeasurement, xᵢ::Num, σᵢ::Num) -> NumReturn the fractional variance contribution (cᵢ · σᵢ)² / u_c²(y) — the EA-4/02 §7.3 "percentage-of-variance" column (JCGM 100:2008 §5.1.6).
For uncorrelated inputs, summing relative_sensitivity over the full input set simplifies to 1 under Symbolics.simplify — the variance-decomposition invariant (REQ-045 / REQ-152).
Traces REQ-042.
relative_sensitivity(m::SymbolicMeasurement, s::SourceId)Fraction of the combined variance contributed by the independent source s (JCGM 100:2008 §5.1.2).
This is the source-based form, and the one that stays meaningful under correlation: contributions are keyed by the independent measurements the quantity actually derives from, so with independent sources they sum to exactly 1.
The variable-based method relative_sensitivity(m, xᵢ, σᵢ) remains for models expressed in user symbols, but refuses to answer once correlations are declared, because (cᵢσᵢ)²/u_c² is then no longer a decomposition.
Traces REQ-042, REQ-152, REQ-206.
SymbolicUncertainties.uncertainty_budget — Function
uncertainty_budget(m; as = :budget) -> UncertaintyBudget
uncertainty_budget(m, variables, sigmas; as = :budget)Produce the EA-4/02 §7.3 uncertainty-budget table for the measurement m.
The one-argument form derives its rows from the quantity's own sources: m knows which independent measurements it descends from and with what sensitivity, so restating them would ask for information it already holds — and could not express a source with no user-facing symbol (REQ-206). A source that cancelled produces no row at all.
The three-argument form is a filter, kept for the case where only some inputs are of interest: rows follow variables, and a variable absent from m.val yields a zero row rather than an error.
Each BudgetRow carries the EA-4/02 §7.3 columns:
sigma— the standard uncertaintyu(xᵢ).sensitivity—cᵢ = ∂f/∂xᵢ(JCGM 100:2008 §5.1.3 eq. (11b)).contribution—uᵢ(y) = |cᵢ|·u(xᵢ)(§5.1.3 eq. (11a)).relative—uᵢ²(y)/u_c²(y), the percentage-of-variance column.
The UncertaintyBudget returned also carries the measurand, its u_c and whether declared correlations are in play; it indexes and iterates as a vector of rows.
Invariants:
- For independent sources
sum(row.relative for row in budget)simplifies to1(REQ-045 / REQ-152). Under declared correlation it does not, andbudget.correlatedsays so: the §5.2.2 eq. (13) cross terms belong to no single row.
Errors:
length(variables) != length(sigmas)raisesDimensionMismatch.- An unresolved symbolic differential in a per-row sensitivity propagates the REQ-021
ArgumentErrorfromsensitivity_coefficient.
Pass as = :dataframe to render the same budget as a DataFrame; that requires DataFrames.jl to be loaded (REQ-044).
Implements the methodology of JCGM 100:2008 §5.1.3 and the layout of EA-4/02 §7.3. Traces REQ-044, REQ-206, REQ-209.
SymbolicUncertainties.UncertaintyBudget — Type
UncertaintyBudget <: AbstractVector{BudgetRow}The EA-4/02 §7.3 uncertainty budget of a measurand: its rows, plus the three facts a bare vector of rows cannot carry.
measurand— the estimateythe rows decompose.uc— the combined standard uncertainty they recombine into (JCGM 100:2008 §5.1.2).rows— oneBudgetRowper contribution.correlated— whether the quantity carries declared covariances. It matters for reading the table: under §5.2.2 equation (13) the cross terms belong to no single row, so therelativecolumn stops summing to 1 and a negative cross term can push one row past 100 %.
A budget indexes and iterates as a vector of its rows, so it drops into DataFrames, PrettyTables or a plain for loop unchanged.
Traces REQ-209, REQ-044, REQ-206.
SymbolicUncertainties.BudgetRow — Type
BudgetRowOne line of an UncertaintyBudget: the EA-4/02 §7.3 columns for a single contribution.
source— theSourceIdof the independent measurement this row decomposes, ornothingfor a row produced by the variable-filtered form.variable— the input variablexᵢthis row decomposes, when the source was built from a bare input variable. It isnothingfor a source built from a numeric or derived estimate, which has no input symbol to name — which is whysourceandvariablecannot be one field.name— the source's display name.sigma— its standard uncertaintyu(xᵢ)(JCGM 100:2008 §4.1.5).sensitivity—cᵢ = ∂f/∂xᵢ(§5.1.3 equation (11b)).contribution—uᵢ(y) = |cᵢ|·u(xᵢ)(§5.1.3 equation (11a)).relative—uᵢ²(y)/u_c²(y), the EA-4/02 §7.3 percentage-of-variance column.
Traces REQ-209.
SymbolicUncertainties.dominant_source — Function
dominant_source(m::SymbolicMeasurement; values = nothing) -> SourceIdIdentify the independent source contributing the largest share of the combined variance, derived from the quantity's own structure (JCGM 100:2008 §5.1.2, EA-4/02 §7.3).
Pass values to substitute numerics before comparing; without it the comparison is only possible when the contributions are already concrete.
Traces REQ-043, REQ-206.
dominant_source(m, variables, sigmas; values=nothing) -> NamedTupleIdentify the variable whose variance contribution dominates.
The returned NamedTuple has fields (index, variable, contribution, ranked_by):
index::Int— 1-based position of the dominant variable invariables, or0when every sensitivity coefficient is structurally zero.variable::Num— the dominant variable itself.contribution::Num— its uncertainty contribution|cᵢ|·σᵢ.ranked_by::Symbol—:numericwhen avaluessubstitution dictionary was supplied,:symbolicotherwise.
Ranking strategy:
- With
values = Dict(...): each per-variable contribution is substituted and evaluated to aFloat64; the maximum by absolute value is the dominant source. - Without
values: the ranking is order-dependent (no canonical ordering exists on arbitraryNumexpressions). The first variable is returned and an@infomessage flags the order-dependence.
All-zero edge case: when every sensitivity coefficient is structurally 0, the record is (index=0, variable=Num(0), contribution=Num(0), ranked_by=:symbolic) and a @warn is emitted.
Errors:
length(variables) != length(sigmas)raisesDimensionMismatch.
Traces REQ-043. The ranking convention follows EA-4/02 §7.3 calibration-report practice; no single GUM section mandates it.