This module provides tools for representing and manipulating Gaussian random variables numerically. It can deal with individual variables or arbitrarily large sets of variables, correlated or uncorrelated. It also supports complicated (Python) functions of Gaussian variables.
A Gaussian variable is a random variable that represents a typical random number drawn from a particular Gaussian (or normal) probability distribution; more precisely, it represents the entire probability distribution. A given Gaussian variable x is therefore completely characterized by its mean x.mean and standard deviation x.sdev.
A mathematical function of a Gaussian variable can be defined as the probability distribution of function values obtained by evaluating the function for random numbers drawn from the original distribution. The distribution of function values is itself approximately Gaussian provided the standard deviation of the Gaussian variable is sufficiently small. Thus we can define a function f of a Gaussian variable x to be a Gaussian variable itself, with
f(x).mean = f(x.mean)
f(x).sdev = x.sdev |f'(x.mean)|,
which follows from linearizing the x dependence of f(x) about point x.mean. (This obviously fails at an extremum of f(x), where f'(x)=0.)
The last formula, together with its multidimensional generalization, leads to a full calculus for Gaussian random variables that assigns Gaussian-variable values to arbitrary arithmetic expressions and functions involving Gaussian variables. A multidimensional collection x[i] of Gaussian variables is characterized by the means x[i].mean for each variable, together with a covariance matrix cov[i, j]. Diagonal elements of cov specify the standard deviations of different variables: x[i].sdev = cov[i, i]**0.5. Nonzero off-diagonal elements imply correlations between different variables:
cov[i, j] = <x[i]*x[j]> - <x[i]> * <x[j]>
where <y> denotes the expectation value or mean for a random variable y.
Gaussian variables are represented in gvar by objects of type gvar.GVar. gvar.GVars are particularly useful for error propagation because the errors (i.e., standard deviations) associated with them propagate through arbitrarily complex Python functions. This is true of errors from individual gvar.GVars, and also of the correlations between different gvar.GVars. In the following sections we demonstrate how to create gvar.GVars and how to manipulate them to solve common problems in error propagation. They are also very useful for creating correlated Gaussian priors for Bayesian analyses.
Objects of type gvar.GVar are of two types: 1) primary gvar.GVars that are created from means and covariances using gvar.gvar(); and 2) derived gvar.GVars that result from arithmetic expressions or functions involving gvar.GVars (primary or derived). A single (primary) gvar.GVar is created from its mean xmean and standard deviation xsdev using:
x = gvar.gvar(xmean, xsdev).
This function can also be used to convert strings like "-72.374(22)" or "511.2 +- 0.3" into gvar.GVars: for example,
>>> import gvar
>>> x = gvar.gvar(3.1415, 0.0002)
>>> print(x)
3.14150(20)
>>> x = gvar.gvar("3.1415(2)")
>>> print(x)
3.14150(20)
>>> x = gvar.gvar("3.1415 +- 0.0002")
>>> print(x)
3.14150(20)
Note that x = gvar.gvar(x) is useful when you are unsure whether x is a gvar.GVar or a string representing a gvar.GVar.
gvar.GVars are usually more interesting when used to describe multidimensional distributions, especially if there are correlations between different variables. Such distributions are represented by collections of gvar.GVars in one of two standard formats: 1) numpy arrays of gvar.GVars (any shape); or, more flexibly, 2) Python dictionaries whose values are gvar.GVars or arrays of gvar.GVars. Most functions in gvar that handle multiple gvar.GVars work with either format, and if they return multidimensional results do so in the same format as the inputs (that is, arrays or dictionaries). Any dictionary is converted internally into a specialized (ordered) dictionary of type gvar.BufferDict, and dictionary-valued results are also gvar.BufferDicts.
To create an array of gvar.GVars with mean values specified by array xmean and covariance matrix xcov, use
x = gvar.gvar(xmean, xcov)
where array x has the same shape as xmean (and xcov.shape = xmean.shape+xmean.shape). Then each element x[i] of a one-dimensional array, for example, is a gvar.GVar where:
x[i].mean = xmean[i] # mean of x[i]
x[i].val = xmean[i] # same as x[i].mean
x[i].sdev = xcov[i, i]**0.5 # std deviation of x[i]
x[i].var = xcov[i, i] # variance of x[i]
As an example,
>>> x, y = gvar.gvar([0.1, 10.], [[0.015625, 0.24], [0.24, 4.]])
>>> print('x =', x, ' y =', y)
x = 0.10(13) y = 10.0(2.0)
makes x and y gvar.GVars with standard deviations sigma_x=0.125 and sigma_y=2, and a fairly strong statistical correlation:
>>> print(gvar.evalcov([x, y])) # covariance matrix
[[ 0.015625 0.24 ]
[ 0.24 4. ]]
>>> print(gvar.evalcorr([x, y])) # correlation matrix
[[ 1. 0.96]
[ 0.96 1. ]]
Here functions gvar.evalcov() and gvar.evalcorr() compute the covariance and correlation matrices, respectively, of the list of gvar.GVars in their arguments.
gvar.gvar() can also be used to convert strings or tuples stored in arrays or dictionaries into gvar.GVars: for example,
>>> garray = gvar.gvar(['2(1)', '10+-5', (99, 3), gvar.gvar(0, 2)])
>>> print(garray)
[2.0(1.0) 10.0(5.0) 99.0(3.0) 0.0(2.0)]
>>> gdict = gvar.gvar(dict(a='2(1)', b=['10+-5', (99, 3), gvar.gvar(0, 2)]))
>>> print(gdict)
{'a': 2.0(1.0),'b': array([10.0(5.0), 99.0(3.0), 0.0(2.0)], dtype=object)}
If the covariance matrix in gvar.gvar is diagonal, it can be replaced by an array of standard deviations (square roots of diagonal entries in cov). The example above without correlations, therefore, would be:
>>> x, y = gvar.gvar([0.1, 10.], [0.125, 2.])
>>> print('x =', x, ' y =', y)
x = 0.10(12) y = 10.0(2.0)
>>> print(gvar.evalcov([x, y])) # covariance matrix
[[ 0.015625 0. ]
[ 0. 4. ]]
>>> print(gvar.evalcorr([x, y])) # correlation matrix
[[ 1. 0.]
[ 0. 1.]]
The gvar.GVars discussed in the previous section are all primary gvar.GVars since they were created by specifying their means and covariances explicitly, using gvar.gvar(). What makes gvar.GVars particularly useful is that they can be used in arithemtic expressions (and numeric pure-Python functions), just like Python floats. Such expressions result in new, derived gvar.GVars whose means, standard deviations, and correlations are determined from the covariance matrix of the primary gvar.GVars. The automatic propagation of correlations through arbitrarily complicated arithmetic is an especially useful feature of gvar.GVars.
As an example, again define
>>> x, y = gvar.gvar([0.1, 10.], [0.125, 2.])
and set
>>> f = x + y
>>> print('f =', f)
f = 10.1(2.0)
Then f is a (derived) gvar.GVar whose variance f.var equals
df/dx cov[0, 0] df/dx + 2 df/dx cov[0, 1] df/dy + ... = 2.0039**2
where cov is the original covariance matrix used to define x and y (in gvar.gvar). Note that while f and y separately have 20% uncertainties in this example, the ratio f/y has much smaller errors:
>>> print(f / y)
1.010(13)
This happens, of course, because the errors in f and y are highly correlated — the error in f comes mostly from y. gvar.GVars automatically track correlations even through complicated arithmetic expressions and functions: for example, the following more complicated ratio has a still smaller error, because of stronger correlations between numerator and denominator:
>>> print(gvar.sqrt(f**2 + y**2) / f)
1.4072(87)
>>> print(gvar.evalcorr([f, y]))
[[ 1. 0.99805258]
[ 0.99805258 1. ]]
>>> print(gvar.evalcorr([gvar.sqrt(f**2 + y**2), f]))
[[ 1. 0.9995188]
[ 0.9995188 1. ]]
The gvar module defines versions of the standard Python mathematical functions that work with gvar.GVar arguments. These include: exp, log, sqrt, sin, cos, tan, arcsin, arccos, arctan, sinh, cosh, tanh, arcsinh, arccosh, arctanh. Numeric functions defined entirely in Python (i.e., pure-Python functions) will likely also work with gvar.GVars.
Numeric functions implemented by modules using low-level languages like C will not work with gvar.GVars. Such functions must be replaced by equivalent code written directly in Python. In some cases it is possible to construct a gvar.GVar-capable function from low-level code for the function and its derivative. For example, the following code defines a new version of the standard Python error function that accepts either floats or gvar.GVars as its argument:
import math
import gvar
def erf(x):
if isinstance(x, gvar.GVar):
f = math.erf(x.mean)
dfdx = 2. * math.exp(- x.mean ** 2) / math.sqrt(math.pi)
return gvar.gvar_function(x, f, dfdx)
else:
return math.erf(x)
Here function gvar.gvar_function() creates the gvar.GVar for a function with mean value f and derivative dfdx at point x.
Some sample numerical analysis codes, adapted for use with gvar.GVars, are described in Numerical Analysis Modules in gvar.
Arithmetic operators + - * / ** == != <> += -= *= /= are all defined for gvar.GVars. Comparison operators are also supported: == != > >= < <=. They are applied to the mean values of gvar.GVars: for example, gvar.gvar(1,1) == gvar.var(1,2) is true, as is gvar.gvar(1,1) > 0. Logically x>y for gvar.GVars should evaluate to a boolean-valued random variable, but such variables are beyond the scope of this module. Comparison operators that act only on the mean values make it easier to implement pure-Python functions that work with either gvar.GVars or floats as arguments.
Implementation Notes: Each gvar.GVar keeps track of three pieces of information: 1) its mean value; 2) its derivatives with respect to the primary gvar.GVars (created by gvar.gvar()); and 3) the location of the covariance matrix for the primary gvar.GVars. The derivatives and covariance matrix allow one to compute the standard deviation of the gvar.GVar, as well as correlations between it and any other function of the primary gvar.GVars. The derivatives for derived gvar.GVars are computed automatically, using automatic differentiation.
The derivative of a gvar.GVar f with respect to a primary gvar.GVar x is obtained from f.deriv(x). A list of derivatives with respect to all primary gvar.GVars is given by f.der, where the order of derivatives is the same as the order in which the primary gvar.GVars were created.
A gvar.GVar can be constructed at a very low level by supplying all the three essential pieces of information — for example,
f = gvar.gvar(fmean, fder, cov)
where fmean is the mean, fder is an array where fder[i] is the derivative of f with respect to the i-th primary gvar.GVar (numbered in the order in which they were created using gvar.gvar()), and cov is the covariance matrix for the primary gvar.GVars (easily obtained from an existing gvar.GVar x using x.cov).
It is sometimes useful to know how much of the uncertainty in a derived quantity is due to a particular input uncertainty. Continuing the example above, for example, we might want to know how much of fs standard deviation is due to the standard deviation of x and how much comes from y. This is easily computed:
>>> x, y = gvar.gvar([0.1, 10.], [0.125, 2.])
>>> f = x + y
>>> print(f.partialsdev(x)) # uncertainty in f due to x
0.125
>>> print(f.partialsdev(y)) # uncertainty in f due to y
2.0
>>> print(f.partialsdev(x, y)) # uncertainty in f due to x and y
2.00390244274
>>> print(f.sdev) # should be the same
2.00390244274
This shows, for example, that most (2.0) of the uncertainty in f (2.0039) is from y.
gvar provides a useful tool for compiling an “error budget” for derived gvar.GVars relative to the primary gvar.GVars from which they were constructed: continuing the example above,
>>> outputs = {'f':f, 'f/y':f/y}
>>> inputs = {'x':x, 'y':y}
>>> print(gvar.fmt_values(outputs))
Values:
f/y: 1.010(13)
f: 10.1(2.0)
>>> print(gvar.fmt_errorbudget(outputs=outputs, inputs=inputs))
Partial % Errors:
f/y f
----------------------------------------
y: 0.20 19.80
x: 1.24 1.24
----------------------------------------
total: 1.25 19.84
This shows y is responsible for 19.80% of the 19.84% uncertainty in f, but only 0.2% of the 1.25% uncertainty in f/y. The total uncertainty in each case is obtained by adding the x and y contributions in quadrature.
Storing gvar.GVars in a file for later use is complicated by the need to capture the covariances between different gvar.GVars as well as their means. To save an array or dictionary g of gvar.GVars, for example, we might use
>>> gmean = gvar.mean(g)
>>> gcov = gvar.evalcov(g)
to extract the means and covariance matrix into arrays/dictionaries of floats which then can be saved using standard methods. To reassemble the gvar.GVars we use:
>>> g = gvar.gvar(gmean, gcov)
This procedure preserves the correlations between different elements of the original array/dictionary g, but ignores their correlations with any other gvar.GVars. So it is important to include all gvar.GVars of interest in a single array or dictionary (g) before saving them.
This process is simplified if the gvar.GVars that need saving are all in a gvar.BufferDict since these can be serialized and saved to a file using Python’s standard pickle module. So if g is a gvar.BufferDict containing gvar.GVars (and/or arrays of gvar.GVars),
>>> import pickle
>>> with open('gfile.p', 'wb') as ofile:
... pickle.dump(g, ofile)
saves the contents of g to a file named gfile.p. The gvar.GVars are retrieved using:
>>> with open('gfile.p', 'rb') as ifile:
... g = pickle.load(ifile)
gvar.BufferDicts also have methods that allow saving their contents using Python’s json module rather than pickle.
gvar.GVars represent probability distributions. It is possible to use them to generate random numbers from those distributions. For example, in
>>> z = gvar.gvar(2.0, 0.5)
>>> print(z())
2.29895701465
>>> print(z())
3.00633184275
>>> print(z())
1.92649199321
calls to z() generate random numbers from a Gaussian random number generator with mean z.mean=2.0 and standard deviation z.sdev=0.5.
To obtain random arrays from an array g of gvar.GVars use giter=gvar.raniter(g) (see gvar.raniter()) to create a random array generator giter. Each call to next(giter) generates a new array of random numbers. The random number arrays have the same shape as the array g of gvar.GVars and have the distribution implied by those random variables (including correlations). For example,
>>> a = gvar.gvar(1.0, 1.0)
>>> da = gvar.gvar(0.0, 0.1)
>>> g = [a, a+da]
>>> giter = gvar.raniter(g)
>>> print(next(giter))
[ 1.51874589 1.59987422]
>>> print(next(giter))
[-1.39755111 -1.24780937]
>>> print(next(giter))
[ 0.49840244 0.50643312]
Note how the two random numbers separately vary over the region 1±1 (approximately), but the separation between the two is rarely more than 0±0.1. This is as expected given the strong correlation between a and a+da.
gvar.raniter(g) also works when g is a dictionary (or gvar.BufferDict) whose entries g[k] are gvar.GVars or arrays of gvar.GVars. In such cases the iterator returns a dictionary with the same layout:
>>> g = dict(a=gvar.gvar(0, 1), b=[gvar.gvar(0, 100), gvar.gvar(10, 1e-3)])
>>> print(g)
{'a': 0.0(1.0), 'b': [0(100), 10.0000(10)]}
>>> giter = gvar.raniter(g)
>>> print(next(giter))
{'a': -0.88986130981173306, 'b': array([-67.02994213, 9.99973707])}
>>> print(next(giter))
{'a': 0.21289976681277872, 'b': array([ 29.9351328 , 10.00008606])}
One use for such random number generators is dealing with situations where the standard deviations are too large to justify the linearization assumed in defining functions of Gaussian variables. Consider, for example,
>>> x = gvar.gvar(1., 3.)
>>> print(cos(x))
0.5(2.5)
The standard deviation for cos(x) is obviously wrong since cos(x) can never be larger than one. To obtain the real mean and standard deviation, we generate a large number of random numbers xi from x, compute cos(xi) for each, and compute the mean and standard deviation for the resulting distribution (or any other statistical quantity, particularly if the resulting distribution is not Gaussian):
# estimate mean,sdev from 1000 random x's
>>> ran_x = numpy.array([x() for in range(1000)])
>>> ran_cos = numpy.cos(ran_x)
>>> print('mean =', ran_cos.mean(), ' std dev =', ran_cos.std())
mean = 0.0350548954142 std dev = 0.718647118869
# check by doing more (and different) random numbers
>>> ran_x = numpy.array([x() for in range(100000)])
>>> ran_cos = numpy.cos(ran_x)
>>> print('mean =', ran_cos.mean(), ' std dev =', ran_cos.std())
mean = 0.00806276057656 std dev = 0.706357174056
This procedure generalizes trivially for multidimensional analyses, using arrays or dictionaries with gvar.raniter().
Finally note that bootstrap copies of gvar.GVars are easily created. A bootstrap copy of gvar.GVar x ± dx is another gvar.GVar with the same width but where the mean value is replaced by a random number drawn from the original distribution. Bootstrap copies of a data set, described by a collection of gvar.GVars, can be used as new (fake) data sets having the same statistical errors and correlations:
>>> g = gvar.gvar([1.1, 0.8], [[0.01, 0.005], [0.005, 0.01]])
>>> print(g)
[1.10(10) 0.80(10)]
>>> print(gvar.evalcov(g)) # print covariance matrix
[[ 0.01 0.005]
[ 0.005 0.01 ]]
>>> gbs_iter = gvar.bootstrap_iter(g)
>>> gbs = next(gbs_iter) # bootstrap copy of f
>>> print(gbs)
[1.14(10) 0.90(10)] # different means
>>> print(gvar.evalcov(gbs))
[[ 0.01 0.005] # same covariance matrix
[ 0.005 0.01 ]]
Such fake data sets are useful for analyzing non-Gaussian behavior, for example, in nonlinear fits.
The most fundamental limitation of this module is that the calculus of Gaussian variables that it assumes is only valid when standard deviations are small (compared to the distances over which the functions of interest change appreciably). One way of dealing with this limitation is described above in the section on Random Number Generators.
Another potential issue is roundoff error, which can become problematic if there is a wide range of standard deviations among correlated modes. For example, the following code works as expected:
>>> from gvar import gvar, evalcov
>>> tiny = 1e-4
>>> a = gvar(0., 1.)
>>> da = gvar(tiny, tiny)
>>> a, ada = gvar([a.mean, (a+da).mean], evalcov([a, a+da])) # = a,a+da
>>> print(ada-a) # should be da again
0.00010(10)
Reducing tiny, however, leads to problems:
>>> from gvar import gvar, evalcov
>>> tiny = 1e-8
>>> a = gvar(0., 1.)
>>> da = gvar(tiny, tiny)
>>> a, ada = gvar([a.mean, (a+da).mean], evalcov([a, a+da])) # = a, a+da
>>> print(ada-a) # should be da again
1(0)e-08
Here the call to gvar.evalcov() creates a new covariance matrix for a and ada = a+da, but the matrix does not have enough numerical precision to encode the size of da‘s variance, which gets set, in effect, to zero. The problem arises here for values of tiny less than about 2e-8 (with 64-bit floating point numbers — tiny**2 is what appears in the covariance matrix).
When there are lots of primary gvar.GVars, the number of derivatives stored for each derived gvar.GVar can become rather large, potentially (though not necessarily) leading to slower calculations. One way to alleviate this problem, should it arise, is to separate the primary variables into groups that are never mixed in calculations and to use different gvar.gvar()s when generating the variables in different groups. New versions of gvar.gvar() are obtained using gvar.switch_gvar(): for example,
import gvar
...
x = gvar.gvar(...)
y = gvar.gvar(...)
z = f(x, y)
... other manipulations involving x and y ...
gvar.switch_gvar()
a = gvar(...)
b = gvar(...)
c = g(a, b)
... other manipulations involving a and b (but not x and y) ...
Here the gvar.gvar() used to create a and b is a different function than the one used to create x and y. A derived quantity, like c, knows about its derivatives with respect to a and b, and about their covariance matrix; but it carries no derivative information about x and y. Absent the switch_gvar line, c would have information about its derivatives with respect to x and y (zero derivative in both cases) and this would make calculations involving c slightly slower than with the switch_gvar line. Usually the difference is negligible — it used to be more important, in earlier implementations of gvar.GVar before sparse matrices were introduced to keep track of covariances. Note that the previous gvar.gvar() can be restored using gvar.restore_gvar().
The function used to create Gaussian variable objects is:
Create one or more new gvar.GVars.
Each of the following creates new gvar.GVars:
Returns a gvar.GVar with mean x and standard deviation xsdev. Returns an array of gvar.GVars if x and xsdev are arrays with the same shape; the shape of the result is the same as the shape of x. Returns a gvar.BufferDict if x and xsdev are dictionaries with the same keys and layout; the result has the same keys and layout as x.
Returns an array of gvar.GVars with means given by array x and a covariance matrix given by array xcov, where xcov.shape = 2*x.shape; the result has the same shape as x. Returns a gvar.BufferDict if x and xcov are dictionaries, where the keys in xcov are (k1,k2) for any keys k1 and k2 in x. The layout for xcov is compatible with that produced by gvar.evalcov() with a dictionary argument.
Returns a gvar.GVar with mean x and standard deviation xsdev.
Returns a gvar.GVar corresponding to string xstr which is either of the form "xmean +- xsdev" or "x(xerr)" (see GVar.fmt()).
Returns gvar.GVar xgvar unchanged.
Returns a dictionary (BufferDict) b where b[k] = gvar(xdict[k]) for every key in dictionary xdict. The values in xdict, therefore, can be strings, tuples or gvar.GVars (see above), or arrays of these.
Returns an array a having the same shape as xarray where every element a[i...] = gvar(xarray[i...]). The values in xarray, therefore, can be strings, tuples or gvar.GVars (see above).
gvar.gvar is actually an object of type gvar.GVarFactory.
The following function is useful for constructing new functions that can accept gvar.GVars as arguments:
Create a gvar.GVar for function f(x) given f and df/dx at x.
This function creates a gvar.GVar corresponding to a function of gvar.GVars x whose value is f and whose derivatives with respect to each x are given by dfdx. Here x can be a single gvar.GVar, an array of gvar.GVars (for a multidimensional function), or a dictionary whose values are gvar.GVars or arrays of gvar.GVars, while dfdx must be a float, an array of floats, or a dictionary whose values are floats or arrays of floats, respectively.
This function is useful for creating functions that can accept gvar.GVars as arguments. For example,
import math
import gvar as gv
def sin(x):
if isinstance(x, gv.GVar):
f = math.sin(x.mean)
dfdx = math.cos(x.mean)
return gv.gvar_function(x, f, dfdx)
else:
return math.sin(x)
creates a version of sin(x) that works with either floats or gvar.GVars as its argument. This particular function is unnecessary since it is already provided by gvar.
Parameters: | |
---|---|
Returns: | A gvar.GVar representing the function’s value at x. |
Means, standard deviations, variances, formatted strings, covariance matrices and correlation/comparison information can be extracted from arrays (or dictionaries) of gvar.GVars using:
Extract means from gvar.GVars in g.
g can be a gvar.GVar, an array of gvar.GVars, or a dictionary containing gvar.GVars or arrays of gvar.GVars. Result has the same layout as g.
g is returned unchanged if it contains something other than gvar.GVars.
Extract standard deviations from gvar.GVars in g.
g can be a gvar.GVar, an array of gvar.GVars, or a dictionary containing gvar.GVars or arrays of gvar.GVars. Result has the same layout as g.
Extract variances from gvar.GVars in g.
g can be a gvar.GVar, an array of gvar.GVars, or a dictionary containing gvar.GVars or arrays of gvar.GVars. Result has the same layout as g.
Format gvar.GVars in g.
g can be a gvar.GVar, an array of gvar.GVars, or a dictionary containing gvar.GVars or arrays of gvar.GVars. Each gvar.GVar gi in g is replaced by the string generated by gi.fmt(ndecimal,sep). Result has same structure as g.
Compute covariance matrix for elements of array/dictionary g.
If g is an array of gvar.GVars, evalcov returns the covariance matrix as an array with shape g.shape+g.shape. If g is a dictionary whose values are gvar.GVars or arrays of gvar.GVars, the result is a doubly-indexed dictionary where cov[k1,k2] is the covariance for g[k1].flat and g[k2].flat.
Compute correlation matrix for elements of array/dictionary g.
If g is an array of gvar.GVars, evalcorr returns the correlation matrix as an array with shape g.shape+g.shape. If g is a dictionary whose values are gvar.GVars or arrays of gvar.GVars, the result is a doubly-indexed dictionary where corr[k1,k2] is the correlation for g[k1] and g[k2].
The correlation matrix is related to the covariance matrix by:
corr[i,j] = cov[i,j] / (cov[i,i] * cov[j,j]) ** 0.5
Return True if gvar.GVars in g1 uncorrelated with those in g2.
g1 and g2 can be gvar.GVars, arrays of gvar.GVars, or dictionaries containing gvar.GVars or arrays of gvar.GVars. Returns True if either of g1 or g2 is None.
Compute chi**2 of g1-g2.
chi**2 is a measure of whether the multi-dimensional Gaussian distributions g1 and g2 (dictionaries or arrays) agree with each other — that is, do their means agree within errors for corresponding elements. The probability is high if chi2(g1,g2)/chi2.dof is of order 1 or smaller.
Usually g1 and g2 are dictionaries with the same keys, where g1[k] and g2[k] are gvar.GVars or arrays of gvar.GVars having the same shape. Alternatively g1 and g2 can be gvar.GVars, or arrays of gvar.GVars having the same shape.
One of g1 or g2 can contain numbers instead of gvar.GVars, in which case chi**2 is a measure of the likelihood that the numbers came from the distribution specified by the other argument.
One or the other of g1 or g2 can be missing keys, or missing elements from arrays. Only the parts of g1 and g2 that overlap are used. Also setting g2=None is equivalent to replacing its elements by zeros.
chi**2 is computed from the inverse of the covariance matrix of g1-g2. The matrix inversion can be sensitive to roundoff errors. In such cases, SVD cuts can be applied by setting parameters svdcut and svdnum. See the documentation for gvar.SVD for information about these parameters.
The return value is the chi**2. Extra data is stored in chi2 itself:
Number of degrees of freedom (that is, the number of variables compared).
The probability that the chi**2 could have been larger, by chance, even if g1 and g2 agree. Values smaller than 0.1 or so suggest that they do not agree. Also called the p-value.
If argument fmt==True, then a string is returned containing the chi**2 per degree of freedom, the number of degrees of freedom, and Q.
Return string containing chi**2/dof, dof and Q from f.
Assumes f has attributes chi2, dof and Q. The logarithm of the Bayes factor will also be printed if f has attribute logGBF.
gvar.GVars contain information about derivatives with respect to the independent gvar.GVars from which they were constructed. This information can be extracted using:
Compute first derivatives wrt x of gvar.GVars in g.
g can be a gvar.GVar, an array of gvar.GVars, or a dictionary containing gvar.GVars or arrays of gvar.GVars. Result has the same layout as g.
x must be an primary gvar.GVar, which is a gvar.GVar created by a call to gvar.gvar() (e.g., x = gvar.gvar(xmean, xsdev)) or a function f(x) of such a gvar.GVar. (More precisely, x.der must have only one nonzero entry.)
The following function creates an iterator that generates random arrays from the distribution defined by array (or dictionary) g of gvar.GVars. The random numbers incorporate any correlations implied by the gs.
Return iterator for random samples from distribution g
The gaussian variables (gvar.GVar objects) in array (or dictionary) g collectively define a multidimensional gaussian distribution. The iterator defined by raniter() generates an array (or dictionary) containing random numbers drawn from that distribution, with correlations intact.
The layout for the result is the same as for g. So an array of the same shape is returned if g is an array. When g is a dictionary, individual entries g[k] may be gvar.GVars or arrays of gvar.GVars, with arbitrary shapes.
raniter() also works when g is a single gvar.GVar, in which case the resulting iterator returns random numbers drawn from the distribution specified by g.
Parameters: |
|
---|---|
Returns: | An iterator that returns random arrays or dictionaries with the same shape as g drawn from the gaussian distribution defined by g. |
Return iterator for bootstrap copies of g.
The gaussian variables (gvar.GVar objects) in array (or dictionary) g collectively define a multidimensional gaussian distribution. The iterator created by bootstrap_iter() generates an array (or dictionary) of new gvar.GVars whose covariance matrix is the same as g‘s but whose means are drawn at random from the original g distribution. This is a bootstrap copy of the original distribution. Each iteration of the iterator has different means (but the same covariance matrix).
bootstrap_iter() also works when g is a single gvar.GVar, in which case the resulting iterator returns bootstrap copies of the g.
Parameters: |
|
---|---|
Returns: | An iterator that returns bootstrap copies of g. |
Seed random number generators with tuple seed.
Argument seed is a tuple of integers that is used to seed the random number generators used by numpy and random (and therefore by gvar). Reusing the same seed results in the same set of random numbers.
ranseed generates its own seed when called without an argument or with seed=None. This seed is stored in ranseed.seed and also returned by the function. The seed can be used to regenerate the same set of random numbers at a later time.
Parameters: | seed (tuple or None) – A tuple of integers. Generates a random tuple if None. |
---|---|
Returns: | The seed. |
Two functions that are useful for tabulating results and for analyzing where the errors in a gvar.GVar constructed from other gvar.GVars come from:
Tabulate error budget for outputs[ko] due to inputs[ki].
For each output outputs[ko], fmt_errorbudget computes the contributions to outputs[ko]‘s standard deviation coming from the gvar.GVars collected in inputs[ki]. This is done for each key combination (ko,ki) and the results are tabulated with columns and rows labeled by ko and ki, respectively. If a gvar.GVar in inputs[ki] is correlated with other gvar.GVars, the contribution from the others is included in the ki contribution as well (since contributions from correlated gvar.GVars cannot be resolved). The table is returned as a string.
Parameters: |
|
---|---|
Returns: | A table (str) containing the error budget. Output variables are labeled by the keys in outputs (columns); sources of uncertainty are labeled by the keys in inputs (rows). |
Tabulate gvar.GVars in outputs.
Parameters: |
|
---|---|
Returns: | A table (str) containing values and standard deviations for variables in outputs, labeled by the keys in outputs. |
The following functions creates new functions that generate gvar.GVars (to replace gvar.gvar()):
Switch gvar.gvar() to new gvar.GVarFactory.
Returns: | New gvar.gvar(). |
---|
Restore previous gvar.gvar().
Returns: | Previous gvar.gvar(). |
---|
Return new function for creating gvar.GVars (to replace gvar.gvar()).
If cov is specified, it is used as the covariance matrix for new gvar.GVars created by the function returned by gvar_factory(cov). Otherwise a new covariance matrix is created internally.
gvar.GVars created by different functions cannot be combined in arithmetic expressions (the error message “Incompatible GVars.” results).
The following function can be used to rebuild collections of gvar.GVars, ignoring all correlations with other variables. It can also be used to introduce correlations between uncorrelated variables.
Rebuild g stripping correlations with variables not in g.
g is either an array of gvar.GVars or a dictionary containing gvar.GVars and/or arrays of gvar.GVars. rebuild(g) creates a new collection gvar.GVars with the same layout, means and covariance matrix as those in g, but discarding all correlations with variables not in g.
If corr is nonzero, rebuild will introduce correlations wherever there aren’t any using
cov[i,j] -> corr * sqrt(cov[i,i]*cov[j,j])
wherever cov[i,j]==0.0 initially. Positive values for corr introduce positive correlations, negative values anti-correlations.
Parameter gvar specifies a function for creating new gvar.GVars that replaces gvar.gvar() (the default).
Parameters: |
|
---|---|
Returns: | Array or dictionary (gvar.BufferDict) of gvar.GVars (same layout as g) where all correlations with variables other than those in g are erased. |
Finally there is a utility function and a class for implementing an svd analysis of a covariance or other symmetric, positive matrix:
Apply svd cuts to collection of gvar.GVars in g.
g is an array of gvar.GVars or a dictionary containing gvar.GVars and/or arrays of gvar.GVars. svd(g,...) returns a copy of g whose gvar.GVars have been modified so that their covariance matrix is less singular than for the original g (the gvar.GVar means are unchanged). This is done using an svd algorithm which is controlled by three parameters: svdcut, svdnum and rescale (see gvar.SVD for more details). svd cuts are not applied when the covariance matrix is diagonal (that is, when there are no correlations between different elements of g).
The input parameters are :
Parameters: |
|
---|---|
Returns: | A copy of g with the same means but with a covariance matrix modified by svd cuts. |
Data from the svd analysis of g‘s covariance matrix is stored in svd itself:
Eigenvalues of the covariance matrix after svd cuts (and after rescaling if rescale=True); the eigenvalues are ordered, with the smallest first.
Eigenvectors of the covariance matrix after svd cuts (and after rescaling if rescale=True), where svd.vec[i] is the vector corresponding to svd.val[i].
Ratio of the smallest to largest eigenvalue before svd cuts are applied (but after rescaling if rescale=True).
Diagonal of matrix used to rescale the covariance matrix before applying svd cuts (cuts are applied to D*cov*D) if rescale=True; svd.D is None if rescale=False.
Logarithm of the determinant of the covariance matrix after svd cuts are applied.
Vector of the svd corrections to g.flat;
The sum of the outer product of vectors inv_wgt[i] with themselves equals the inverse of the covariance matrix after svd cuts. Only computed if compute_inv=True. The order of the vectors is reversed relative to svd.val and svd.vec
The fundamental class for representing Gaussian variables is:
The basic attributes are:
Mean value.
Standard deviation.
Variance.
Two methods allow one to isolate the contributions to the variance or standard deviation coming from other gvar.GVars:
Compute partial variance due to gvar.GVars in args.
This method computes the part of self.var due to the gvar.GVars in args. If args[i] is correlated with other gvar.GVars, the variance coming from these is included in the result as well. (This last convention is necessary because variances associated with correlated gvar.GVars cannot be disentangled into contributions corresponding to each variable separately.)
Parameters: | args[i] (gvar.GVar or array/dictionary of gvar.GVars) – Variables contributing to the partial variance. |
---|---|
Returns: | Partial variance due to all of args. |
Compute partial standard deviation due to gvar.GVars in args.
This method computes the part of self.sdev due to the gvar.GVars in args. If args[i] is correlated with other gvar.GVars, the standard deviation coming from these is included in the result as well. (This last convention is necessary because variances associated with correlated gvar.GVars cannot be disentangled into contributions corresponding to each variable separately.)
Parameters: | args[i] (gvar.GVar or array/dictionary of gvar.GVars) – Variables contributing to the partial standard deviation. |
---|---|
Returns: | Partial standard deviation due to args. |
Partial derivatives of the gvar.GVar with respect to the independent gvar.GVars from which it was constructed are given by:
Derivative of self with respest to primary gvar.GVar x.
All gvar.GVars are constructed from primary gvar.GVars. self.deriv(x) returns the partial derivative of self with respect to primary gvar.GVar x, holding all of the other primary gvar.GVars constant.
Parameters: | x – A primary gvar.GVar (or a function of a single primary gvar.GVar). |
---|---|
Returns: | The derivative of self with respect to x. |
There are two methods for converting self into a string, for printing:
Return string representation of self.
The representation is designed to show at least one digit of the mean and two digits of the standard deviation. For cases where mean and standard deviation are not too different in magnitude, the representation is of the form 'mean(sdev)'. When this is not possible, the string has the form 'mean +- sdev'.
Convert to string with format: mean(sdev).
Leading zeros in the standard deviation are omitted: for example, 25.67 +- 0.02 becomes 25.67(2). Parameter ndecimal specifies how many digits follow the decimal point in the mean. Parameter sep is a string that is inserted between the mean and the (sdev). If ndecimal is None (default), it is set automatically to the larger of int(2-log10(self.sdev)) or 0; this will display at least two digits of error. Very large or very small numbers are written with exponential notation when ndecimal is None.
Setting ndecimal < 0 returns mean +- sdev.
Two attributes and a method make reference to the original variables from which self is derived:
Return the dot product of self.der and v.
The following class is a specialized form of an ordered dictionary for holding gvar.GVars (or other scalars) and arrays of gvar.GVars (or other scalars) that supports Python pickling:
Dictionary whose data is packed into a 1-d buffer (numpy.array).
A gvar.BufferDict object is a dictionary-like object whose values must either be scalars or arrays (like numpy arrays, with arbitrary shapes). The scalars and arrays are assembled into different parts of a single one-dimensional buffer. The various scalars and arrays are retrieved using keys, as in a dictionary: e.g.,
>>> a = BufferDict()
>>> a['scalar'] = 0.0
>>> a['vector'] = [1.,2.]
>>> a['tensor'] = [[3.,4.],[5.,6.]]
>>> print(a.flatten()) # print a's buffer
[ 0. 1. 2. 3. 4. 5. 6.]
>>> for k in a: # iterate over keys in a
... print(k,a[k])
scalar 0.0
vector [ 1. 2.]
tensor [[ 3. 4.]
[ 5. 6.]]
>>> a['vector'] = a['vector']*10 # change the 'vector' part of a
>>> print(a.flatten())
[ 0. 10. 20. 3. 4. 5. 6.]
The first four lines here could have been collapsed to one statement:
a = BufferDict(scalar=0.0,vector=[1.,2.],tensor=[[3.,4.],[5.,6.]])
or
a = BufferDict([('scalar',0.0),('vector',[1.,2.]),
('tensor',[[3.,4.],[5.,6.]])])
where in the second case the order of the keys is preserved in a (that is, BufferDict is an ordered dictionary).
The keys and associated shapes in a gvar.BufferDict can be transferred to a different buffer, creating a new gvar.BufferDict: e.g., using a from above,
>>> buf = numpy.array([0.,10.,20.,30.,40.,50.,60.])
>>> b = BufferDict(a,buf=buf) # clone a but with new buffer
>>> print(b['tensor'])
[[ 30. 40.]
[ 50. 60.]]
>>> b['scalar'] += 1
>>> print(buf)
[ 1. 10. 20. 30. 40. 50. 60.]
Note how b references buf and can modify it. One can also replace the buffer in the original gvar.BufferDict using, for example, a.buf = buf:
>>> a.buf = buf
>>> print(a['tensor'])
[[ 30. 40.]
[ 50. 60.]]
>>> a['tensor'] *= 10.
>>> print(buf)
[ 1. 10. 20. 300. 400. 500. 600.]
a.buf is the numpy array used for a‘s buffer. It can be used to access and change the buffer directly. In a.buf = buf, the new buffer buf must be a numpy array of the correct shape. The buffer can also be accessed through iterator a.flat (in analogy with numpy arrays), and through a.flatten() which returns a copy of the buffer.
When creating a gvar.BufferDict from a dictionary (or another gvar.BufferDict), the keys included and their order can be specified using a list of keys: for example,
>>> d = dict(a=0.0,b=[1.,2.],c=[[3.,4.],[5.,6.]],d=None)
>>> print(d)
{'a': 0.0, 'c': [[3.0, 4.0], [5.0, 6.0]], 'b': [1.0, 2.0], 'd': None}
>>> a = BufferDict(d, keys=['d', 'b', 'a'])
>>> for k in a:
... print(k, a[k])
d None
b [1.0 2.0]
a 0.0
A gvar.BufferDict functions like a dictionary except: a) items cannot be deleted once inserted; b) all values must be either scalars or arrays of scalars, where the scalars can be any noniterable type that works with numpy arrays; and c) any new value assigned to an existing key must have the same size and shape as the original value.
Note that gvar.BufferDicts can be pickled and unpickled even when they store gvar.GVars (which themselves cannot be pickled separately).
The main attributes are:
Size of buffer array.
Buffer array iterator.
Data type of buffer array elements.
The (1d) buffer array. Allows direct access to the buffer: for example, self.buf[i] = new_val sets the value of the i-th element in the buffer to value new_val. Setting self.buf = nbuf replaces the old buffer by new buffer nbuf. This only works if nbuf is a one-dimensional numpy array having the same length as the old buffer, since nbuf itself is used as the new buffer (not a copy).
Always equal to None. This attribute is included since gvar.BufferDicts share several attributes with numpy arrays to simplify coding that might support either type. Being dictionaries they do not have shapes in the sense of numpy arrays (hence the shape is None).
The main methods are:
Copy of buffer array.
Return slice/index in self.flat corresponding to key k.
Return True if self[k] is scalar else False.
Add contents of dictionary d to self.
Load serialized gvar.BufferDict from file object fobj. Uses pickle unless use_json is True, in which case it uses json (obvioulsy).
Load serialized gvar.BufferDict from string object s. Uses pickle unless use_json is True, in which case it uses json (obvioulsy).
Serialize gvar.BufferDict in file object fobj.
Uses pickle unless use_json is True, in which case it uses json (obviously). json does not handle non-string valued keys very well. This attempts a workaround, but it will only work in simpler cases. Serialization only works when pickle (or json) knows how to serialize the data type stored in the gvar.BufferDict‘s buffer (or for gvar.GVars).
Serialize gvar.BufferDict into string.
Uses pickle unless use_json is True, in which case it uses json (obviously). json does not handle non-string valued keys very well. This attempts a workaround, but it will only work in simpler cases (e.g., integers, tuples of integers, etc.). Serialization only works when pickle (or json) knows how to serialize the data type stored in the gvar.BufferDict‘s buffer (or for gvar.GVars).
SVD analysis is handled by the following class:
SVD decomposition of a pos. sym. matrix.
SVD is a function-class that computes the eigenvalues and eigenvectors of a positive symmetric matrix mat. Eigenvalues that are small (or negative, because of roundoff) can be eliminated or modified using svd cuts. Typical usage is:
>>> mat = [[1.,.25],[.25,2.]]
>>> s = SVD(mat)
>>> print(s.val) # eigenvalues
[ 0.94098301 2.05901699]
>>> print(s.vec[0]) # 1st eigenvector (for s.val[0])
[ 0.97324899 -0.22975292]
>>> print(s.vec[1]) # 2nd eigenvector (for s.val[1])
[ 0.22975292 0.97324899]
>>> s = SVD(mat,svdcut=0.6) # force s.val[i]>=s.val[-1]*0.6
>>> print(s.val)
[ 1.2354102 2.05901699]
>>> print(s.vec[0]) # eigenvector unchanged
[ 0.97324899 -0.22975292]
>>> s = SVD(mat)
>>> w = s.decomp(-1) # decomposition of inverse of mat
>>> invmat = sum(numpy.outer(wj,wj) for wj in w)
>>> print(numpy.dot(mat,invmat)) # should be unit matrix
[[ 1.00000000e+00 2.77555756e-17]
[ 1.66533454e-16 1.00000000e+00]]
Input parameters are:
Parameters: |
|
---|
The results are accessed using:
An ordered array containing the eigenvalues or mat. Note that val[i]<=val[i+1].
Eigenvectors vec[i] corresponding to the eigenvalues val[i].
The diagonal matrix used to precondition the input matrix if rescale==True. The matrix diagonalized is D M D where M is the input matrix. D is stored as a one-dimensional vector of diagonal elements. D is None if rescale==False.
The first nmod eigenvalues in self.val were modified by the SVD cut (equals 0 unless svdcut > 0).
Ratio of the smallest to the largest eigenvector in the unconditioned matrix (after rescaling if rescale=True)
A vector of gvars whose means are zero and whose covariance matrix is what was added to mat to condition its eigenvalues. Is None if svdcut<0 or compute_delta==False.
Vector decomposition of input matrix raised to power n.
Computes vectors w[i] such that
mat**n = sum_i numpy.outer(w[i],w[i])
where mat is the original input matrix to svd. This decomposition cannot be computed if the input matrix was rescaled (rescale=True) except for n=1 and n=-1.
Parameters: | n (number) – Power of input matrix. |
---|---|
Returns: | Array w of vectors. |