Punpy as a standalone package#
In this section we give a short overview of some of the key capabilities of punpy for propagating uncertainties through a measurement function. When using punpy as a standalone tool, the input quantities and their uncertainties are manually specified. The code in this section is just as illustration and we refer to the the CoMet website examples for examples with all required information for running punpy. The punpy package can propagate various types of uncertainty through any python measurement function. In the next subsection we discuss how these python measurement functions are defined.
Measurement functions#
The measurement function can be written mathematically as:
where:
\(f\) is the measurment function;
\(y\) is the measurand;
\(x_{i}\) are the input quantities.
The measurand and input quantities are often vectors consisting of multiple numbers. E.g. in spectroscopy, the input quantities and measurand each have measurements for multiple wavelengths. These wavelengths are the same for the input quantities and the measurand. We refer to the Algorithm Theoretical Basis section for more details on the vocabulary used and the various types of uncertainty.
When using punpy, the measurement function can be any python function that takes a number of input quantities as arguments (as floats or numpy arrays of any shape) and returns a measurand (as a float or numpy array of any shape). For example:
def measurement_function(x1,x2,x3):
y=x1+x2-x3 # here any real measurement function can be implemented
return y
Any function that can be formatted to take only numpy arrays or floats/ints as its inputs and only returns one or more numpy arrays and or floats/ints can be used in punpy. Note that this measurement function can optionally be part of a class, which can allow to do an additional setup step to set further, non-numerical parameters:
class Func:
def __init__(self,option1,option2):
self.option1 = option1
self.option2 = option2
def measurement_function(self,x1,x2,x3):
if self.option1=="green":
y=x1+x2-x3 # here any real measurement function can be implemented
else:
y=self.option2
return y
Monte Carlo and Law of Propagation of Uncertainty methods#
Once this kind of measurement function is defined, we can use the various punpy methods to propagate uncertainties though this measurement function. In order to do this, we first create a prop object (object of punpy MCPropagation of LPUPropagation class):
import punpy
prop=punpy.MCPropagation(10000) # Here the number is how
# many MC samples will be generated
# Or if you have a measurement function that does not accept
# higher dimensional arrays as argument:
prop=punpy.MCPropagation(10000,parallel_cores=1)
# Alternatively it is possible to use LPU methods to
# propagate uncertainties
prop=punpy.LPUPropagation()
In order to do propagate uncertainties, punpy uses Monte Carlo (MC) methods (see Section Monte Carlo Method) or Law of Propagation of Uncertainties (LPU) methods (see Section Law of Propagation of Uncertainty Method). MC methods generate MC samples from the input quantities (which can be correlated to eachother or not), and then propagate these samples through the measurement function. The LPU methods implement the law of propagation of uncertainties from the GUM (Guide to the Expression of Uncertainty in Measurement) by calculating the Jacobian and using this to propagate the uncertainties.
For the MC method, the number of MC samples that is used is set as the first argument when creating the MCPropagation object (see example above). Two approaches can be followed to propagate the MC samples of the input quantities to the measurand, depending on whether the measurement function can be applied to numpy arrays of arbitrary size.
The most computationally efficient way is to pass an array consisting of all MC steps of an
input quantity instead of the input quantity themselves. Each of the input quantities will thus get an additional dimension in the MC sample.
If the measurement function can deal with these higher dimensional arrays by just performing numpy operations, this gives the most computationally efficient MC propagation.
This method is the default in punpy (which corresponds to setting the optional parallel_cores
keyword to 0).
However, this is not the case for every measurement function. If the inputs to the measurement
function are less flexible, and don’t support additional dimensions, it is possible to instead run the MC samples one by one.
In order to pass each MC sample individually to the measurement function, it is possible to set the optional
parallel_cores
keyword to 1. In Managing memory and processing speed in punpy, we will show how the same keyword can be used to do parallel processing for such measurement functions.
For the LPU methods, the numdifftools package (used within comet_maths) is used to calculate the Jacobian. This package automatically determines the stepsize in the numerical
differentiation, unless a manual stepsize is set. For some measurement functions, it can be necessary to set a manual stepsize (e.g. because of the limited
range of the input quantities). It is possible to set the stepsize to be passed to
the numdifftools jacobian method by setting the step
keyword when creating the propagation object:
prop = punpy.LPUPropagation(step=0.01)
Both the MC and LPU options also have the verbose
keyword to set the verbosity, which allows the user to get additional information such as the time the runs took and additional warnings:
prop=punpy.MCPropagation(10000,verbose=True)
Propagating random and systematic uncertainties#
Once a prop object has been made (see previous sections), a number of methods can be used to propagate uncertainties, depending on the kind of uncertainties that need to be propagated. We start by showing how to propagating random and systematic uncertainties. For random uncertainties, the errors associated with these uncertainties are entirely independent of each-other (errors for different elements of the input quantity are uncorrelated). For systematic uncertainties, the errors of the different elements (along the different dimensions of the input quantity) are entirely correlated. This typically means they are all affected by the same effect (e.g. if the calibration gain of a sensor is wrong, all its measurements will be wrong by the same factor).
For given values (arrays or numbers) of the input quantities xn, and their random (ur_xn) or systematic (us_xn) uncertainties, punpy can be used to propagate these uncertainties as follows:
y = measurement_function(x1, x2, x3)
ur_y = prop.propagate_random(measurement_function,
[x1, x2, x3], [ur_x1, ur_x2, ur_x3])
us_y = prop.propagate_systematic(measurement_function,
[x1, x2, x3], [us_x1, us_x2, us_x3])
In addition to returning the uncertainties, punpy can also be used to return the correlation matrix.
This is not particularly useful when the input quantities all have a random or all have a systematic error correlation as in this section, but is very relevant when dealing with input quantities that have other error correlations (see next section).
This is done by setting the return_corr
keyword to True:
ur_y, corr_y = prop.propagate_random(measurement_function,
[x1, x2, x3], [ur_x1, ur_x2, ur_x3], return_corr=True)
Here, the returned error correlation matrix will cover all dimensions of the associated uncertainty. If ur_y has shape (k,l), corr_y has shape (k*l,k*l).
The order of the correlation coefficient elements corresponds to the order for a flattened version of ur_y (ur_y.flatten()).
By default, the returned correlation matrices are forced to be positive-definite. This means they are sometimes slightly changed in order to accomodate this. If this is the case, a warning is shown.
To just return the unmodified correlation matrices, it is possible to set the PD_corr
keyword to False.
More advanced punpy input and output#
When returning the error correlation functions, rather than providing this full correlation matrix, it is also possible to get punpy to only calculate the error correlation matrix along one (or a list of) dimensions.
If return_corr
is set to True, the keyword corr_dims
can be used to indicate the dimension(s) for which the correlation should be calculated.
In this case the correlation coefficients are calculated using only the first index along all dimensions other than corr_dims
.
We note that corr_dims
should only be used when the error correlation matrices do not vary along the other dimension(s).
A warning is raised if any element of the correlation matrix varies by more than 0.05 between using only the first index along
all dimensions other than corr_dims
or using only the last index along all dimensions other than corr_dims
.
The corr_dims
keyword can be passed to any of the propagate_* functions:
ur_y, corr_y = prop.propagate_random(measurement_function,
[x1, x2, x3], [ur_x1, ur_x2, ur_x3], return_corr=True, corr_dims=0)
If ur_y again had shape (k,l), corr_y would now have shape (k,k).
For the MC method, it is also possible to return the generated samples by setting the optional return_samples
keyword to True:
prop = punpy.MCPropagation(10000)
ur_y, samplesr_y, samplesr_x = prop.propagate_random(
measurement_function, [x1, x2, x3], [ur_x1, ur_x2, ur_x3],
corr_between=corr_x1x2x3, return_samples=True)
ub_y, corr_y, samplesr_y, samplesr_x = prop.propagate_systematic(
measurement_function, [x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, return_samples=True)
It is also possible to pass a sample of input quantities rather than generating a new MC sample. This way, the exact same sample can be used as n a previous run, or one can generate a sample manually:
ub_y, corr_y = prop.propagate_systematic(
measurement_function, [x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, samples=samplesr_x)
For the LPU method, it is possible to additionally return the calculated Jacobian matrix by setting the return_Jacobian
keyword to True.
In addition, instead of calculating the Jacobian as part of the propagation, it is also possible to give a precomputed Jacobian matrix, by setting the Jx
keyword.
This allows to use the Jacobian matrix from a previous step or an analytical prescription, which results in much faster processing:
prop = punpy.LPUPropagation()
ur_y, Jac_x = prop.propagate_random(
measurement_function, [x1, x2, x3], [ur_x1, ur_x2, ur_x3],
corr_between=corr_x1x2x3, return_Jacobian=True)
ub_y, corr_y = prop.propagate_systematic(
measurement_function, [x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, Jx=Jac_x)
It is not uncommon to have measurment functions that take a number of input quantities, where each input quantity is a vector or array.
If the measurand and each of the input quantities all have the same shape, and the measurement function is applied independently to each
element in these arrays, then most of the elements in the Jacobian will be zero (all except the diagonal elements for each square Jacobian
matrix corresponding to each input quantity individually). Rather than calculating all these zeros, it is possible to set the Jx_diag
keyword
to True which will automatically ignore all the off-diagonal elements and result in faster processing:
prop = punpy.LPUPropagation()
ub_y, corr_y = prop.propagate_systematic(
measurement_function, [x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, Jx_diag=True)
Multiple measurands#
In some cases, the measurement function has multiple outputs (measurands):
def measurement_function(x1,x2,x3):
y1=x1+x2-x3 # here any real measurement function can be implemented
y2=x1-x2+x3 # here any real measurement function can be implemented
return y1,y2
These functions can still be handled by punpy, but require the output_vars
keyword to be set to the number of outputs:
us_y, corr_y, corr_out = prop.propagate_systematic(measurement_function,
[x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, corr_dims=0, output_vars=2)
Note that now the corr_dims
keyword is set to a list with the corr_dims for each output variable, and there is an additional output corr_out, which gives the correlation between the different output variables (in the above case a 2 by 2 matrix).
Here the correlation coefficients between the 2 variables are averaged over all measurements.
When there are multiple output variables, it is also possible to specify separate corr_dims for each measurand.
This is done by setting the separate_corr_dims
keyword to True, and passing a list of corr_dims:
us_y, corr_y, corr_out = prop.propagate_systematic(measurement_function,
[x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, corr_dims=[0,1],separate_corr_dims=True, output_vars=2)
It is also possible to set one of the separate corr_dims
to None if you do not want the error correlation to be calculated for that measurand. In that case None will be returned (as corr_y[1] in below example):
us_y, corr_y, corr_out = prop.propagate_systematic(measurement_function,
[x1, x2, x3], [us_x1, us_x2, us_x3],
return_corr=True, corr_dims=[0,None],separate_corr_dims=True, output_vars=2)
Different Probability Density Functions#
The standard probability density function in punpy is a Gaussian distribution. This means the generated MC samples will follow a gaussian distribution with the input quantity values as mean and uncertainties as standard deviation. However other probabilty density functions are also possible. Currently there are two additional options (with more to follow in the future).
The first alternative is a truncated Gaussian distribution. This distribution is just like the Gaussian one, except that there are no values outside a given minimum or maximum value. A typical use case of this distribution is when a certain input quantity can never be negative. In such a case the uncertainty propagation could be done like this:
ur_y = prop.propagate_random(measurement_function, [x1, x2, x3],
[ur_x1, ur_x2, ur_x3], corr_between = corr_x1x2x3, pdf_shape="truncated_gaussian", pdf_params={"min":0.})
When the alternative probability density functions require additional parameters, these can be passed in the optional pdf_params dictionary. For the truncated Gaussian example, this dictionary can contain a value for “min” and “max” for the minimum and maximum allowed values respectively.
The second alternative is a tophat distribution. In this case the MC sample will have a uniform probabilty distribution from the value of the input quantity minus its uncertainty to the value of the input quantity plus its uncertainty. We note that for these modified probability density functions, the standard deviation of the MC sample is not the same as the uncertainty anymore.
Handling errors in the measurement function#
There are cases where a measurement function occasionally runs into an error (e.g. if certain specific combinations of input quantities generated by the MC approach are invalid). This can completely mess up a long run even if it happens only occasionally. In cases where the measurement function does not raise an error but returns a measurand which has only non-finite values (np.nan or np.inf as one of the values), that MC sample of the measurand will automatically be ignored by punpy and not used when calculating the uncertainties or any of the other outputs.
In cases where an error is raised, one can catch this error using a try statement and instead return np.nan. punpy will ignore all these nan’s in the measurand MC sample, and will just calculate uncertainties and its other output without these nan samples:
import numpy as np
def function_fail(x1, x2):
zero_or_one=np.random.choice([0,1],p=[0.1,0.9])
with np.errstate(divide='raise'):
try:
return 2 * x1 - x2/zero_or_one
except:
return np.nan
prop = punpy.MCPropagation(1000)
ur_y = prop.propagate_random(function_fail, [x1, x2], [ur_x1, ur_x2])
Here the measurement will fail about 10% of the time (by raising a FloatingPointError due to division by zero). The resulting sample of valid measurands will thus have about 900 samples, which should still be enough to calculate the uncertainties.
By default, numpy will only ignore MC samples where all the values are non-finite.
However, it is also possible to ignore all MC samples where any of the values are non-finite.
This can be done by setting the allow_some_nans
keyword to False.
Shape of input quanties within the measurement function#
When setting parallel_cores to 1 or more, the shape of the input quantities used for each iteration in the measurement function matches the shape of the input quantities themselves.
However, when parallel_cores is set to 0, all iterations will be processed simultaneously and there will be an additional dimension for the MC iterations.
Generally within punpy, the MC dimension in the samples is the first one (i.e. internally as well as when MC samples are returned).
However, when processing all iterations simultaniously, in most cases it is more practical to have the MC dimension as the last dimension.
This is because we use numpy arrays and these are compatible when the last dimensions match following the numpy broadcasting rules.
So as default, the shape of the input quantities when using parallel_cores will have the MC iterations as its last dimension.
However, in some cases it is more helpful to have the MC iterations as the first dimension.
If this is the case, the MC iteration dimension can be made the first dimension by setting the MClastdim
keyword to False:
prop = punpy.MCPropagation(1000,MCdimlast=False)