MetroloPy is a pure python package and requires Python 3.5 or later and the SciPy stack (NumPy, SciPy and Pandas). It looks best in a Jupyter Notebook like the one used to create this tutorial.
Install MetroloPy with pip:
$ pip install metrolopy
or with conda from conda-forge:
$ conda install -c conda-forge metrolopy
See the MetroloPy home page for the full documentation.
A gummy object represents a physical quantity with an uncertainty and (or) a unit:
import metrolopy as uc
side_a = uc.gummy(1.2345,u=0.0234,unit='cm')
side_a
By default concise notation is used when a gummy is printed, where the numbers in parentheses represent the uncertainty in the last digits of the value. There are many other ways that the uncertainty can be notated, for example:
side_a.style='+-'
side_a
or:
side_a.uunit = 'um' # the uunit property sets the units on the uncertainty
side_a
or:
side_a.uunit = '%'
side_a
Gummys can be used in arbitrary formulas, propagating the uncertainties:
side_b = uc.gummy(3.034,u=0.174,unit='mm')
side_b
angle = uc.arctan(side_b/side_a)
angle
angle.convert('degree')
Or another example:
area = side_a*side_b
area
The gummys keep track of any correlations between values:
area.correlation(angle)
Up to now we have assumed that the uncertainties are best modeled with a Normal probability distribution, but we can define a gummy with, say, a uniform probability distribution:
force = uc.gummy(uc.UniformDist(center=0.9345,half_width=0.096),unit='N')
force
And we can calculate:
pressure = force/area
pressure
pressure.unit = 'kPa'
pressure
Above, gummy just took the standard deviation of the force probability distribution and then forgot about the shape and propagated the uncertainties assuming everything was Gaussian. However we can also do a Monte-Carlo simulation using the actual probability distributions:
%matplotlib inline
uc.gummy.simulate([area,force,pressure]) # generate Monte-Carlo data, default is 1e6 samples for each gummy
area.name = 'Area' #give the variables names for the plot
area.p = 0.95 # put the reference lines in the histograms at a 95% confidence interval
area.hist() # create the histogram for a (we could have used a.hist(p=0.95) instead of a.p = 0.95 above)
force.name = 'Force'
force.p = 0.95
force.cimethod = 'symmetric' # The default method for calculating confidence intervals is
# cimethod = 'shortest', the shortest interval with the desired
# confidence level. But this doesn't work well with a uniform distribution.
force.hist()
pressure.name = 'Pressure'
pressure.p = 0.95
pressure.name = 'P'
pressure.hist()
When we did the first uncertainty propagation for pressure we just got the standard (1-sigma) uncertainty. Lets convert that to an uncertainty with a 95% level of confidence (again assuming that everything is Normally distributed):
pressure.p = 0.95 # pressure.k = 2 gives about the same result
pressure
So we do get a pretty good answer just assuming that everything is Normally distributed, but it is nice be able to check to be sure.
Let's summarize the uncertainty budget in a table:
side_a.name = 'Side A' # define names for the table listing
side_b.name = 'Side B'
pressure.budget([side_a,side_b,force])
Maybe you prefer relative uncertainties in the table:
pressure.budget([side_a,side_b,force],uunit='%')
gummy can also keep track of the effective number of degrees of freedom that the uncertainty is based on and group uncertainties into different types:
# dof is degrees of freedom and
# utype is an arbitrary lable for the uncertainty type
distance = uc.gummy(3.4567,0.12,dof=6,unit='m',utype='A',name='distance')
distance
time_B = uc.gummy(4.4444,0.05,unit='s',utype='B',name='time')
time_A = uc.gummy(0,0.22,dof=4,unit='s',utype='A',name='time')
time = time_A + time_B
velocity = distance/time
velocity.name = 'velocity'
velocity
velocity.ubreakdown=['A','B']
velocity.show_dof = False
velocity
velocity.budget([distance,time_A,time_B])
During mathematical operations, gummys will automatically convert units if necessary:
x = uc.gummy(1.2,unit='cm')
y = uc.gummy(2.1,unit='in')
x + y
x*y
Use the c flag to control the unit conversion. Put the .c property on the unit that you want converted:
x.c + y
x.c*y
Nonlinear units (e.g. the decibel) or units with a offset origin may affect the way gummys behave under mathematical operations:
t1 = uc.gummy(27,unit='degC')
t2 = uc.gummy(19,unit='degC')
difference = t1 - t2 # t1 + t2 will raise an exception
difference
The difference looks like it has units degree Celsius, however gummy understands that it represents a temperature interval:
difference.unit.name
difference.convert('K')
This is different from, say t1
defined above:
t1.unit.name
t1.convert('K')
A number of units are built into gummy. Use the search_units function to search them:
uc.search_units('length',fmt='html')
# uc.search_units() with no argument displays all loaded units
You can also create custom units. Creating an instance of the Unit class automatically loads the unit definition into the unit library so it can be accessed by its string name. The Unit class has two required parameters, the unit name and the unit symbol.
uc.Unit('weird meter','wm',conversion=uc.Conversion('m',0.9144),add_symbol=True)
The optional conversion defines a unit conversion, in this case 1 wm = 0.9144 m. Because add_symbol was set to True when creating the unit, we can access the unit using its symbol 'wm' as well as by its name '[weird meter]' ( the brackets are required because there is a space in the name):
w = uc.gummy(1,unit='wm')
w
w.convert('m')
A number of mathematical functions that can be used with gummys are included with the gummy package:
a = uc.gummy(1.233,0.022)
uc.sin(a)
Many NumPy functions can also be used with gummys (however this only works with NumPy version 1.13 or later):
import numpy as np
np.sin(a)
The apply
static method can also be used to apply a arbitrary numerical function to a gummy or several gummys. The apply method takes as its first parameter the function, which must take one or more float parameters and return a float or list or numpy.ndarray of floats. The second parameter is another function which gives the derivative of the first function. The remaining parameters are the gummy(s) or float(s) to which the function will be applied. We also demonstrate here that gummy can be used with the mpmath package to work with extended precision floating point types:
# the mpmath package can be installed with pip or conda
from mpmath import sin, cos, mpf, mp
mp.dps = 50
# set mpmath to a precision of 50 digits
uc.gummy.max_digits = 50
# by default gummy doesn't display more than 15 digits;
# this option does not affect the working precision only
# the display
a = uc.gummy(mpf('1/3'),mpf('2.2e-45'))
uc.gummy.apply(sin,cos,a)
The napply static method is similar to the apply method except that the derivatives are calculated numerically and do not need to be applied:
uc.gummy.napply(sin,a)
#set the max digits back to its original value
uc.gummy.max_digits = 15
The gummy package also includes several classes for fitting functions, for example:
y0 = uc.gummy(0.11,2.2,unit='m')
y1 = uc.gummy(2.12,1.2,unit='m')
y2 = uc.gummy(3.02,1.3,unit='m')
y3 = uc.gummy(5.55,2.3,unit='m')
y4 = uc.gummy(16.22,1.2,unit='m')
fit = uc.PolyFit([0,1,2,3,4],[y0,y1,y2,y3,y4],deg=2,xunit='s')
fit
We can plot the fit along with the standard uncertainty in the fit at any point:
fit.plot(cik=1,xlabel='time',ylabel=distance)
# cik is the coverage factor for the fit uncertainty band,
# alterately the cip parameter can be set to give a
# probability level for the band
gummy keeps track of the correlation between the fit parameters:
uc.gummy.correlation_matrix(fit.p)
uc.gummy.covariance_matrix(fit.p)
Use the create static method to generate a list of correlated gummys:
g = uc.gummy.create([1.1,2.2,3.3],u=[0.3,0.1,0.4],correlation_matrix=[[1,0,0.5],[0,1,0],[0.5,0,1]])
g
g[0].covariance(g[2])
Note that mathematical operations between gummys will also create correlations between the input and output gummys.
By default in a Jupyter notebook, gummy output is rendered using HTML:
acceleration = fit.p[2]
acceleration
But they can also be displayed using LaTeX:
acceleration.latex()
The latex() method is like a print command but rendering the output with LaTeX. There is also a html() method. Output can also be printed using only ASCII or unicode characters:
acceleration.ascii()
acceleration.unicode()
Use the tolatex() or tohtml() methods to get a string with the LaTeX or HTML encoding for the gummy.
acceleration.tolatex()
acceleration.tohtml()
It is also possible to change the default output to LaTeX:
uc.gummy.printer = 'latex'
acceleration
or unicode:
uc.gummy.printer = 'unicode'
acceleration
or output using only ASCII characters:
uc.gummy.printer = 'ascii'
acceleration
or back to HTML:
uc.gummy.printer = 'html'
acceleration
Uncertainty budgets and fits can also be rendered using LaTeX:
budget = pressure.budget([side_a,side_b,force],uunit='%')
budget.latex()
budget.tolatex()
As can the ouput of the fit from the curve fitting section:
fit.latex()
The gummy object has a setting to automatically include uncertainty due to floating point rounding errors:
uc.gummy.rounding_u = True
uc.gummy(1.23)
uc.gummy(np.float32(1/3))
uc.gummy(3.30000000000001) - uc.gummy(3.3)
The rounding_u
option simply adds and an uncertainty proportional to the machine epsilon whenever a gummy is created with a floating point data type and then propagates this uncertainty like any other uncertainty. This feature is experimental and perhaps can give some idea of the magnitude of the floating point errors, but is not a substitute for a full numerical error analysis.
The gummy object recognizes that integer and Faction values do not need an uncertainty component to account for rounding:
uc.gummy(3)
from fractions import Fraction
uc.gummy(Fraction(1,3))
uc.gummy(3)/uc.gummy(7)
uc.gummy.rounding_u = False