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.

FieldMeaning
sourceIdentity of the independent source (source-derived rows)
variableThe input variable xᵢ (variable-filtered rows)
sigmaIts standard uncertainty u(xᵢ)
sensitivitycᵢ = ∂(m.val)/∂xᵢ (JCGM 100:2008 §5.1.3 eq (11b))
contributionuᵢ(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) raises DimensionMismatch.
  • A variable that does not appear in m.val produces a row with sensitivity = 0, contribution = 0, relative = 0 — not an error.
  • A measurand whose derivative the symbolic engine cannot resolve propagates the REQ-021 ArgumentError from sensitivity_coefficient, directing the user at the apply(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_coefficientFunction
sensitivity_coefficient(m::SymbolicMeasurement, xᵢ::Num) -> Num

Return 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.

source
SymbolicUncertainties.uncertainty_contributionFunction
uncertainty_contribution(m::SymbolicMeasurement, xᵢ::Num, σᵢ::Num) -> Num

Return 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.

source
SymbolicUncertainties.relative_sensitivityFunction
relative_sensitivity(m::SymbolicMeasurement, xᵢ::Num, σᵢ::Num) -> Num

Return 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.

source
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.

source
SymbolicUncertainties.uncertainty_budgetFunction
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 uncertainty u(xᵢ).
  • sensitivitycᵢ = ∂f/∂xᵢ (JCGM 100:2008 §5.1.3 eq. (11b)).
  • contributionuᵢ(y) = |cᵢ|·u(xᵢ) (§5.1.3 eq. (11a)).
  • relativeuᵢ²(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 to 1 (REQ-045 / REQ-152). Under declared correlation it does not, and budget.correlated says so: the §5.2.2 eq. (13) cross terms belong to no single row.

Errors:

  • length(variables) != length(sigmas) raises DimensionMismatch.
  • An unresolved symbolic differential in a per-row sensitivity propagates the REQ-021 ArgumentError from sensitivity_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.

source
SymbolicUncertainties.UncertaintyBudgetType
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 estimate y the rows decompose.
  • uc — the combined standard uncertainty they recombine into (JCGM 100:2008 §5.1.2).
  • rows — one BudgetRow per 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 the relative column 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.

source
SymbolicUncertainties.BudgetRowType
BudgetRow

One line of an UncertaintyBudget: the EA-4/02 §7.3 columns for a single contribution.

  • source — the SourceId of the independent measurement this row decomposes, or nothing for a row produced by the variable-filtered form.
  • variable — the input variable xᵢ this row decomposes, when the source was built from a bare input variable. It is nothing for a source built from a numeric or derived estimate, which has no input symbol to name — which is why source and variable cannot be one field.
  • name — the source's display name.
  • sigma — its standard uncertainty u(xᵢ) (JCGM 100:2008 §4.1.5).
  • sensitivitycᵢ = ∂f/∂xᵢ (§5.1.3 equation (11b)).
  • contributionuᵢ(y) = |cᵢ|·u(xᵢ) (§5.1.3 equation (11a)).
  • relativeuᵢ²(y)/u_c²(y), the EA-4/02 §7.3 percentage-of-variance column.

Traces REQ-209.

source
SymbolicUncertainties.dominant_sourceFunction
dominant_source(m::SymbolicMeasurement; values = nothing) -> SourceId

Identify 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.

source
dominant_source(m, variables, sigmas; values=nothing) -> NamedTuple

Identify 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 in variables, or 0 when every sensitivity coefficient is structurally zero.
  • variable::Num — the dominant variable itself.
  • contribution::Num — its uncertainty contribution |cᵢ|·σᵢ.
  • ranked_by::Symbol:numeric when a values substitution dictionary was supplied, :symbolic otherwise.

Ranking strategy:

  • With values = Dict(...): each per-variable contribution is substituted and evaluated to a Float64; the maximum by absolute value is the dominant source.
  • Without values: the ranking is order-dependent (no canonical ordering exists on arbitrary Num expressions). The first variable is returned and an @info message 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) raises DimensionMismatch.

Traces REQ-043. The ranking convention follows EA-4/02 §7.3 calibration-report practice; no single GUM section mandates it.

source