Display, Substitution, and Safety
Three layers sit on top of the symbolic pipeline: a one-call bridge from symbolic measurements to numeric values, LaTeX rendering for Jupyter and Pluto notebooks, and two runtime safety warnings (division-by-zero and sqrt/log domain).
substitute(m, dict) — the symbolic-to-numeric bridge
SymbolicUncertainties.jl extends Symbolics.substitute with a method that accepts a SymbolicMeasurement and a replacement dictionary. Every field (val, err, and — when set — dof) is rewritten via the underlying Symbolics.substitute engine:
using Symbolics
using DynamicQuantities
using SymbolicUncertainties
@variables Vin σVin R1 σR1 R2 σR2
Vout = propagate(
(vin, r1, r2) -> vin * r2 / (r1 + r2),
[Vin ± σVin, R1 ± σR1, R2 ± σR2],
)
divider = Dict(
Vin => 5.0us"V", σVin => 0.01us"V",
R1 => 1_000.0us"Ω", σR1 => 1.0us"Ω",
R2 => 3_000.0us"Ω", σR2 => 1.0us"Ω",
)
# `substitute` works on plain numbers, so the values are stripped here;
# [`evaluate`](@ref) is the call that keeps the units all the way to the
# answer.
Vout_numeric = substitute(Vout, Dict(k => ustrip(v) for (k, v) in divider))\[3.75 \pm sqrt(5.7226562500000006e-5)\]
Behaviour:
- Silent ignore of extra keys — any
Dictkey that does not appear in any of the three fields is silently dropped (REQ-122). - Partial substitution — a
Dictcovering only a subset of variables leaves the rest symbolic in the returned measurement. dof === nothingpreserved — no accidental upgrade to a substituted zero.- No simplification pass — users who want Giac-level canonicalisation should call
Symbolics.simplifyon the returned fields themselves. - No REQ-005 re-check — a substituted
errthat simplifies to a concrete negativeRealis returned silently. The REQ-005 negativity guard lives in the numeric constructor;substituteis a rewriting operation, not a construction.
Extracting numeric values
Symbolics.substitute does not numerically evaluate expressions like sqrt(0.05) — they remain symbolic (documented upstream and mirrored in upstream-bugs.md UB-001). To obtain a concrete Float64, use the standard toexpr + eval round-trip:
val_float = Float64(eval(Symbolics.toexpr(Vout_numeric.val)))
err_float = Float64(eval(Symbolics.toexpr(Vout_numeric.err)))0.007564824023068878The same pattern lives in the AsFloat snippet the test suite uses throughout.
LaTeX rendering for Jupyter / Pluto
Base.show(io, MIME"text/latex", m) emits a LaTeX-math representation wrapped in inline math delimiters $...$:
$3.75 \pm 0.00625$Jupyter and Pluto recognise the text/latex MIME type and render the output as math automatically — no caller-side wrapping required. The plain-text text/plain display (val ± err in Unicode) is what every REPL and non-notebook consumer sees.
The in-library formatter is intentionally minimal: for concrete numeric values it prints the Float64 directly; for symbolic expressions it defers to Base.string(expr). Users who need production-grade LaTeX for complex expressions should load Latexify.jl and call latexify on the individual m.val / m.err fields. A dedicated SymbolicUncertaintiesLatexifyExt package extension may land in a later milestone.
Safety warnings (division-by-zero and sqrt / log domain)
Two runtime warnings surface potential correctness issues at build time:
- Division-by-zero (REQ-140) fires when a division's denominator cannot be proven nonzero, i.e. when the denominator's
.valis not a concrete numericReal. - Domain (REQ-141) fires when
sqrt,log,log2, orlog10is applied to a measurement whose.valcannot be proven strictly positive.
Both fire uniformly, through the binary operators and through propagate alike. Neither halts the computation — the returned SymbolicMeasurement is valid.
@variables a σa b σb
a_m = a ± σa
b_m = b ± σb
a_m / b_m
# ┌ Warning: Division by `y` whose `.val` is symbolic — the
# │ denominator cannot be proven nonzero at build time
# │ (JCGM 100:2008 §5.1, REQ-140). ...
# └
sqrt(a_m)
# ┌ Warning: sqrt applied to a measurement whose `.val` is
# │ symbolic — the argument cannot be proven strictly positive
# │ at build time (JCGM 100:2008 §5.1, REQ-141). ...
# └
propagate((x, y) -> x / y, [a_m, b_m])
# Fires REQ-140 via the propagate syntactic walker.
propagate((x,) -> log(x), [a_m])
# Fires REQ-141 via the same walker.\[log(a) \pm sqrt(((1 / a)^2)*(σa^2))\]
When no warning fires
The library uses a simple, deterministic rule: a value is "provably safe" iff its .val unwraps via Symbolics.value to a plain Julia Real passing the appropriate test (nonzero for division, strictly positive for sqrt/log/log2/log10).
The quantities here are deliberately dimensionless: the guard tests the number in .val, and a ratio scaling a signal or the argument of a square root carries no unit of its own.
one_m = 1.0 ± 0.0
a_m / one_m # no warning — denominator is provably 1.0
four_m = 4.0 ± 0.1
SymbolicUncertainties._warn_domain(:sqrt, four_m)
# no warning — argument is provably 4.0 > 0Why the library errs on the side of over-warning
Symbolics.jl does not expose an assumptions framework (assume / additionally) — see upstream-bugs.md UB-003. Without an upstream way to declare "this symbol is known to be positive", the library cannot distinguish a user who knows σ > 0 from one who genuinely allows any sign. The conservative choice is to warn whenever the value cannot be proven safe, rather than silently skip a genuine domain violation. An in-library assume_positive! / assume_nonzero! layer is recorded in UB-003 as the long-term fix; it is not implemented.
Silencing the warnings
Three supported options:
- Substitute first. Call
substitute(m, Dict(...))with concrete numeric values for the problematic variables; the.valthen unwraps to aRealand the warning does not fire on subsequent operations. - Julia stdlib toggle. In a REPL or notebook session,
import Logging; Logging.disable_logging(Logging.Warn)suppresses all warnings, not only this package's. - Test context. Wrap the offending call in
Test.@test_logs (:warn, r"REQ-140") expr(orREQ-141) to assert the warning is expected. This is how the package's own test suite captures the warnings it emits.
There is no in-library global silencing switch by design — the constitution's purely-symbolic principle and the Julia-ecosystem aversion to mutable global state make a toggle discouraged.
API reference
SymbolicUtils.substitute — Method
Symbolics.substitute(m::SymbolicMeasurement, dict::AbstractDict)Rewrite each of m.val, m.err, and m.dof (when not nothing) by applying Symbolics.substitute with the user-supplied replacement dictionary.
Behaviour:
- Extra dict keys not present in any of the three fields are silently ignored (REQ-122).
- Partial substitution is supported — unsubstituted variables remain symbolic in the returned measurement.
m.dof === nothingis preserved (not upgraded to a substituted zero).- No simplification pass is applied after substitution; users who want Giac-level canonicalisation should call
Symbolics.simplifyon the returned fields themselves. - The REQ-005 negativity guard on the numeric constructor is not re-applied. A substituted
errthat simplifies to a concrete negativeRealis returned silently —substituteis a rewriting operation, not a constructor.
Implements the methodology of JCGM 100:2008 §5.1 (the measurement-model substitution step) and §7.2.2 (the transition to the calibration-certificate result). Traces REQ-120, REQ-122, REQ-123, REQ-142.