import sympy as sp
from IPython.display import Math, displayGaussian Error Propagation with SymPy
Physikalisches Fortgeschrittenen-Praktikum Heidelberg, Klaus Reygers
This notebook shows how to use symbolic derivatives to propagate independent measurement uncertainties. The example is the volume of a cylinder,
\[V = \pi r^2 h,\]
where the measured radius \(r\) and height \(h\) have uncertainties \(\sigma_r\) and \(\sigma_h\).
Gaussian error propagation
For a quantity \(f(x_1, x_2, \ldots)\) that depends on independent measured variables, the variance is approximated by
\[ \sigma_f^2 = \sum_i \left(\frac{\partial f}{\partial x_i}\sigma_{x_i}\right)^2. \]
The following helper implements this formula symbolically with SymPy.
def gaussian_uncertainty(expr, variables):
"""Return the Gaussian uncertainty of expr for independent variables.
Parameters
----------
expr : sympy expression
Quantity calculated from the measured variables.
variables : sequence of (sympy.Symbol, sympy.Symbol)
Pairs of measured variables and their standard uncertainties,
e.g. [(x, sigma_x), (y, sigma_y)].
"""
if not variables:
raise ValueError("At least one variable-uncertainty pair is required.")
variance = sp.S.Zero
for variable, uncertainty in variables:
derivative = sp.diff(expr, variable)
variance += derivative**2 * uncertainty**2
return sp.sqrt(sp.simplify(variance))Example: volume of a cylinder
First define the symbols and the formula. The symbols are declared positive so that SymPy can simplify square roots in a physically meaningful way.
r, h, sigma_r, sigma_h = sp.symbols("r h sigma_r sigma_h", positive=True)
volume = sp.pi * r**2 * h
volume\(\displaystyle \pi h r^{2}\)
Now apply the propagation formula. The uncertainty is calculated from the derivatives with respect to \(r\) and \(h\).
sigma_volume = gaussian_uncertainty(volume, [(r, sigma_r), (h, sigma_h)])
display(Math(rf"\sigma_V = {sp.latex(sigma_volume)}"))\(\displaystyle \sigma_V = \pi r \sqrt{4 h^{2} \sigma_{r}^{2} + r^{2} \sigma_{h}^{2}}\)
It is often useful to look at the relative uncertainty. For the cylinder volume, the radius uncertainty enters with a factor of two because \(V\) depends on \(r^2\).
relative_uncertainty = sp.simplify(sigma_volume / volume)
display(Math(rf"\frac{{\sigma_V}}{{V}} = {sp.latex(relative_uncertainty)}"))\(\displaystyle \frac{\sigma_V}{V} = \frac{\sqrt{4 h^{2} \sigma_{r}^{2} + r^{2} \sigma_{h}^{2}}}{h r}\)
Insert measured values
Use a dictionary for substitutions. This keeps the numerical calculation readable and reduces the chance of mixing up a variable and its uncertainty.
measurements = {
r: 3.0, # cm
sigma_r: 0.1, # cm
h: 5.0, # cm
sigma_h: 0.1, # cm
}
volume_value = float(volume.subs(measurements))
sigma_value = float(sigma_volume.subs(measurements))
display(Math(rf"V = ({volume_value:.1f} \pm {sigma_value:.1f})\,\mathrm{{cm}}^3"))\(\displaystyle V = (141.4 \pm 9.8)\,\mathrm{cm}^3\)
Which measurement matters most?
The variance is a sum of squared contributions. Inspecting the contributions is a good habit: it tells you where a more precise measurement would improve the final result most.
contributions = {
"radius": (sp.diff(volume, r) * sigma_r)**2,
"height": (sp.diff(volume, h) * sigma_h)**2,
}
numeric_contributions = {
name: float(term.subs(measurements))
for name, term in contributions.items()
}
total_variance = sum(numeric_contributions.values())
for name, variance_part in numeric_contributions.items():
fraction = variance_part / total_variance
print(f"{name:>6}: {100 * fraction:5.1f}% of the variance")radius: 91.7% of the variance
height: 8.3% of the variance