Note
Go to the end to download the full example code.
Basics of RME Propagation¶
This is a basic example demonstrating how to propagate RMEMeas objects through functions.
This example generates a hypothetical voltage and current measurement each with a Monte Carlo distribution and single linear uncertainty mechanism.
We define a propagator and a propagation function to calculate power and perform the uncertainty analysis.
Importing packages¶
import rmellipse as rme
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
Creating a propagator¶
The first step is to define a propagator. This is the object that inspects functions for uncertainty objects, propagating them according to its settings.
myprop = rme.RMEProp(montecarlo_sims=1000, sensitivity=True)
Creating RMEMeas objects¶
RMEMeas objects (Rocky Mountain Ellipse Measurements) carry along with them 2 key attributes .cov and RMEMeas.mc, along attributes for propagating metadata - RMEMeas.covcats and RMEMeas.covdofs.
The`RMEMeas.cov`is an xarray.DataArray object that stores both the nominal value of the measurement, and copies of the nominal value perturbed by 1 standard deviation for each uncertainty mechanism. The perturbed copies are stored along the first dimension of the`RMEMeas.cov`attribute, which is always called ‘umech_id’. Along that dimension the first index is always labeled ‘nominal’ and holds the nominal value, the remaining indexes store the names of the linear uncertainty mechanisms. These are labelled to be unique. Importantly, if two RMEMeas objects being propagated share an uncertainty mechanism with the same umech_id (i.e. they both have a perturbed copy of the nominal with the same index label), then they are assume to be fully correlated.
The RMEMeas.mc attribute stores Monte Carlo data in an xarray.DataArray. Its first dimension is called sample_id; every row is a stochastic sample, with integer labels starting at zero. The independent nominal value remains in the first row of RMEMeas.cov.
In this case, we will create a voltage and current measurement, both drawing from a gaussian distribution. Because we are creating a measurement using a float, the resulting`RMEMeas.cov`attribute will be shape (2,) (index 0 for the nominal value and index 1 representing the uncertainty associated with the Gaussian we sampled from). The RMEMeas.mc attribute will have shape (1000,), with every index representing a sample from the Gaussian distribution.
def make_measurement(d1_coords, std=0.1):
"""Make a sample measurement with length 4 coordinate set."""
nom = xr.DataArray(
np.ones((4, 2)),
dims=('d1', 'd2'),
coords={'d1': d1_coords, 'd2': np.arange(2)},
)
meas = rme.RMEMeas.from_nom(name='meas', nom=nom)
meas.add_umech(
name='mymechanisms',
value=meas.nom + np.ones(meas.nom.shape) * std,
dof=np.inf,
category={'Type': 'B', 'Origin': 'Data Sheet'},
add_uid=True,
)
for i in range(1000):
meas.add_mc_sample(meas.nom + np.random.normal(loc=0, scale=std, size=None))
return meas
V = make_measurement([0, 1, 2, 3])
I = make_measurement([0, 1.1, 2, 2.9])
print(V)
print(' dimensions of V.cov :', V.cov.dims, V.cov.shape)
print(' dimensions of V.mc :', V.mc.dims, V.mc.shape)
print('V linear uncertainty mechanisms :', V.umech_id)
Propagating Through a Function¶
Writing a function to be propagated requires a little knowledge about how the propagator works, but in trade enables you to take advantage of vectorized operations for significantly faster computation.
First we define a function and wrap it the myprop.propagator decorator. This is equivalent to calling power = myprop.propagate(power) after defining the function power. This will tell the propagator to inspect the arguments of power when it is called and propagate any RMEMeas objects that are positional or keyword arguments.
The propagator will call the function twice. Once for the linear sensitivity analysis where it passes in the`RMEMeas.cov`attribute, and once for the monte-carlo analysis where it passes in the`RMEMeas.mc` attribute.
In the case of the sensitivity analysis, the propagator will align the linear uncertainty mechanisms of each RMEMeas object so that they can be used in math operations. In this example this means that the shape of the v and i arguments will have shape (3,) each, with the first axis aligning the uncertainty mechanisms that existed in both v and i. Uncertainty mechanisms that don’t exist in a variable are filled with a copy of the nominal in the newly aligned dimension.
For the Monte Carlo analysis, the propagator uses samples from the data stored in the RMEMeas.mc attribute. We defined 1000 Monte Carlo simulations in our propagator, so v and i will have shape (1000,) when passed through our power function.
Note that while the propagator is design to encourage vectorization for efficiency, if that is not possible simply you can turn off vectorization with the vectorize setting. See the example on non-vectorized propagation for more details.
The propagator will name your RMEMeas object using the name of the function you used to propagate it.
@myprop.propagate
def power(v, i):
print(v.dims, v.shape, i.shape)
return v * i
# when we call this, note that the power function is called twice, and the shapes
# of the v and i arguments are changed from the original definitions for the
# `RMEMeas.cov`attribute, now (3,) after the uncertainty mechanisms were aligned.
p = power(V, I)
Evaluating Uncertainty¶
We can calculate the standard uncertainty of a RMEMeas object easily!
covunc, mcunc = p.stdunc(k=1)
print('sensitivity analysis uncertainty:', covunc)
print('monte-carlo uncertainty:', mcunc)
We can easily calculate the lower/upper uncertainty bounds for a given expansion factor on the uncertainty, degrees of freedom based on the linear sensitivity analysis, and confidence intervals based on the linear sensitivity analysis.
# k = 2 uncertainty bounds estimated by
# linear uncertainty analysis and monte carlo analysis
cov_k2, mc_k2 = p.uncbounds(k=2)
# degrees of freedom
# on the linear uncertaint analysis
dof = p.dof()
# 95% confidence interval
# of the linear uncertainty analysis
lower_ci, upper_ci = p.confint(0.95)