Table of Contents¶
- getting started
- tutorial
- expressing physical quantities
- propagating uncertainty
- Monte-Carlo uncertainty propagation
- expanded uncertainty
- an uncertainty budget
- units and mathematical operations
- built-in and user defined units
- physical constants
- curve fitting
- applying numerical functions to gummys
- functions with multiple inputs and outputs
- creating correlated gummys
- complex numbers with uncertainty
- formatting
getting started¶
MetroloPy is a pure python package and requires Python 3.6 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.
tutorial¶
expressing physical quantities¶
A gummy object represents a physical quantity with an uncertainty and (or) a unit:
import metrolopy as uc
distance = uc.gummy(342.195,u=0.023,unit='m')
time = uc.gummy(1.00235,u=0.00044,unit='s')
distance/time
The uncertainty in the velocity is automatically calculated using first order uncertainty propagation. 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, here is another example:
side_1 = uc.gummy(1.23456,u=0.00234,unit='cm',utype='B')
side_1
side_1.style='+-'
side_1
You can set the default style for gummys by setting gummy.style
at the class level (e.g. uc.gummy.style = '+-'
).
Notice that the displayed value is rounded to two significant figures in the uncertainty. You can change number of significant
figures in the uncertainty by setting the gummy.nsig
property (either at the instance or class level).
We can express the uncertainty in different units:
side_1.uunit = 'um' # the uunit property sets the units on the uncertainty
side_1
or express the uncertainty as a relative uncertainty:
side_1.uunit = '%'
side_1
side_1.uunit = 'mm/m'
side_1
The gummy side_1
was defined with a utype of 'B'. This is just an arbitrary string that we can attach to gummys to classify
the uncertainty type (how you want classify uncertainties or the labels that you use is up to you).
Gummy instances can also track degrees of freedom and propagate them using the Welch-Satterthwaite approximation.
Say we measured side 2 six times. We can create a gummy instance and use the dof
keywork (e.g.
uc.gummy(3.059,u=0.020,dof=5,unit='mm')
but here we will use the MetroloPy mean
function:
measurements = [3.011,3.125,2.995,3.055,3.067,3.101]
side_2 = uc.mean(measurements,unit='mm',utype='A')
side_2
(By default, the number of degrees of freedom is printed when it is less than 10. This can be turned off by setting
side_b.show_dof = False
. Set it to None
to get back the default behavior. The global settings default for many
properties of newly created gummy instances can be set by setting the class properties, e.g. uc.gummy.style
and
uc.gummy.show_dof
).
Up to this point we have been dealing with standard uncertainties (sometimes referred to roughly as 1-sigma uncertaities).
But often uncertainties are expressed as expanded uncertainties. For example, if we want the uncerainty interval at a
95% level of confidence we can set the gummy.p
property to 0.95 then the value and expanded uncertainty, calculated
using a Student's t distribution with 5 degrees of freedom is:
side_2.p = 0.95
side_2
The expanded uncertainty can be accessed with the U
property:
side_2.U
0.05268125101927068
While the standard uncertianty is still available with the lower case u
property:
side_2.u
0.02049390153191918
The .u
property always gives the standard uncertainty in the same units as the value (.x
).
The .k
property gives the coverage factor, which is the ratio $ U/u $. Note that we can also
set the .k
property and then get the level of confidence corresponding that $ k $ value by
reading the .p
property.
side_2.show_k = True
side_2
Here are the gummy properties that we have used so far:
side_2.x # value in units of side_2.unit, read-only
3.0589999999999997
side_2.u # standard uncertainty in units of side_2.unit, read-only
0.02049390153191918
# expanded uncertainty in units of side_2.uunit is side_2.uunit is set,
# units of side_2.unit if side_2.uunit is not set, read-only
side_2.U
0.05268125101927068
side_2.dof # degrees of freedom, can be float('inf'), read-only
5
# setting this value will affect side_2.x, side_2.u, and, if side_2 uunit is not
# set, side_2.U. This property returns a Unit instance, but can be set with a
# string or Unit instance.
side_2.unit
# level of confidence for the expanded uncertainty.
# Setting this will affect side_2.k and side_2.U.
side_2.p
0.95
# Coverage factor for the expanded uncertainty.
# Setting this will affect side_2.p and side_2.U.
side_2.k
2.570581835636314
# Units for the expanded uncertianty. If this is set to None, then the
# expanded uncertainty is in units of side_2.unit.
print(side_2.uunit)
None
# Read-only. This may be a string, None, or if the gummy is the product
# of operations between gummys with different utypes, a list containing
# strings and None.
side_2.utype
'A'
Note that we can set the .unit
property for an in-place unit conversion, but we can also use the .convert
method to return a copy of the gummy (e.g. side_2_copy = side_2.convert('m')
).
We can also create a gummy based on an expanded uncertainty and/or relative uncertainty by using k, p and uunit keywords in the initializer:
g = uc.gummy(3.4457995,u=1.24,dof=7,p=0.95,unit='kg',uunit='ppm')
g
Even when .p
or .k
are set .u
property gives the standard uncertainty in kg:
g.u # this is equal to g.x*g.U*1e-6/g.k
1.8069642046180892e-06
propagating uncertainty¶
Many numpy functions will work with gummys:
import numpy as np
angle = np.arctan(side_2/side_1)
angle
angle.convert('degree')
Or another example:
The gummys keep track of any correlations between values for example if we define:
area = side_1*side_2
area
then there is a correlation between the area and the angle:
area.correlation(angle)
0.851780140482482
which can also be expressed as a covariance:
area.covariance(angle)
3.6403257452426167e-06
uc.gummy.correlation_matrix([angle,area])
[[1, 0.851780140482482], [0.851780140482482, 1]]
uc.gummy.covariance_matrix([angle,area])
[[2.641868561782402e-06, 3.6403257452426167e-06], [3.6403257452426167e-06, 6.913759482755993e-06]]
MetroloPy takes into account these correlations when propagating uncertainties.
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.934566,half_width=0.0096),unit='N',utype='B')
force
And we can calculate:
pressure = force/area
pressure
pressure.unit = 'kPa'
pressure
We can get the uncertainty contribution from specific sources. Here is the combined standard uncertainty from the type B uncertainty sources:
pressure.ufrom('B')
0.1540769887232122
or we can seperate the type A and type B uncertainties when the gummy is printed:
pressure.ubreakdown = ['A','B']
pressure
ufrom
and ubreakdown
can also take gummys or lists of gummys as
parameters. There is also a .doffrom
method.
Monte-Carlo uncertainty propagation¶
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.ubreakdown = None # ubreakdown was set previously, clear it now
pressure.p = 0.95
pressure.name = 'P'
pressure.hist()
The Monte-Carlo uncertainty above is pretty close (though not exactly the same as) the result from the first order uncertainty propagation:
pressure
There are a number of gummy properties summarize that the Monte-Carlo results:
pressure.usim # The standard deviation of the simulated data
0.2637010595930153
pressure.xsim # The mean of the simulated data
24.747366771406362
# The confidence interval based on the level of confidence that
# we set with the pressure.p property:
pressure.cisim
[24.243127333334265, 25.266092192961985]
By default the the confidence interval is calculated as the shortest inverval corresponding
to the desired level of confidence. However by setting the pressure.cimethod
from the
string "shortest" to "symmetric", the interval can be changed to one that leaves and equal
fraction of simulated values below and above the interval (i.e. the interval between the (1-p)/2
and (1+p)/2 quantiles).
pressure.Usim # this is [pressure.cisim[1] - pressure.xsim,pressure.xsim - pressure.cisim[0]]
(0.5187254215556223, 0.5042394380720978)
# the standard deviation of the Monte-Carlo data if we hold all but the
# type B variables constant.
pressure.ufromsim('B')
0.15414402889691253
The full Monte-Carlo data can be accessed with the pressure.simdata
and pressure.simsorted
properties. Note that we could generate Monte-Carlo data for pressure
only by calling
pressure.sim()
. However when ever the .sim
or gummy.simulate
methods are called, all
previous Monte-Carlo data for all gummys is erased, so use the gummy.simulate([list of guumys])
method when you want data from more than one gummy available at one time.
an uncertainty budget¶
Let's summarize the uncertainty budget in a table:
side_1.name = 'Side 1' # define names for the table listing
side_2.name = 'Side 2'
pressure.budget([side_1,side_2,force])
By default the table is ordered with the most significant contributors to the uncertainty at the top.
Maybe you prefer relative uncertainties in the table:
pressure.budget([side_1,side_2,force],uunit='%')
units and mathematical operations¶
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
'degree Celsius interval'
difference.convert('K')
This is different from, say t1
defined above:
t1.unit.name
'degree Celsius'
t1.convert('K')
built-in and user defined units¶
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
- angstrom, 1 Å = 1 × 10-10 m, alias: Å
- astronomical unit, 1 au = 1.495 978 707 × 1011 m, aliases: au, ua
- bohr, 1 a0 = 5.291 772 105 41(80) × 10-11, aliases: a0, a(0)
- cable, 1 cb = 120 ftm, alias: cb
- chain, 1 ch = 4 rd, alias: ch
- fathom, 1 ftm = 2 yd, alias: ftm
- foot, 1 ft = 12 in, alias: ft
- furlong, 1 fur = 10 ch, alias: fur
- hand, 1 hand = 4 in
- inch (1 prefix), 1 in = 0.0254 m, alias: in
- league, 1 lea = 3 mi, alias: lea
- light hour, 1 light-hour = 1 c h, alias: light-hour
- light minute, 1 light-minute = 1 c min, alias: light-minute
- light second, 1 light-second = 1 c s, alias: light-second
- light year, 1 ly = 1 c a, alias: ly
- link, 1 li = 0.66 ft, alias: li
- metre (20 prefixes), symbol: m, aliases: m, meter
- mile, 1 mi = 1760 yd, alias: mi
- nautical mile, 1 M = 1852 m, aliases: M, Nm, NM, nmi
- parsec (3 prefixes), 1 pc = 206 264.806 247 096 363 2(97) au, alias: pc
- pica, 1 P/ = 1/6 in, alias: P/
- Planck length, 1 lP = 1.616 255(18) × 10-35 m, alias: l(P)
- point, 1 p = 1/12 P/, alias: p
- rack unit, 1 U = 1.75 in
- reduced Compton wavelength, 1 ƛC = 1 ℏ/(me c), aliases: lambda(C), ƛ(C)
- rod, 1 rd = 25 ft, alias: rd
- siriometer, 1 Sm = 1 000 000 au
- survey foot, 1 ft = 1200/3937 m
- survey mile, 1 mi = 8 fur, alias: statute mile
- thousandth of an inch, 1 mil = 0.001 in, aliases: mil, thou, thousandth
- yard, 1 yd = 3 ft, alias: yd
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')
physical constants¶
MetroloPy includes some built-in constants, for example:
uc.constant('G')
uc.constant('G').description
'Newtonian constant of gravitation, CODATA 2022'
Correlations between constants are correctly handled. For example the proton and electron masses are know to about 0.3 ppb, however the ratio of the masses has a smaller uncertainty.
me = uc.constant('m(e)')
me.uunit = 'ppb'
me
mp = uc.constant('m(p)')
mp.uunit = 'ppb'
mp
r = mp/me
r.uunit = 'ppb'
r
Constants can be found with search_constants
function:
uc.search_constants('electron')
- classical electron radius rr = 2.817 940 320 4(13) × 10-15 m, alias: r(e)
- Compton wavelength λC = 2.426 310 235 35(73) × 10-12 m, aliases: lambda(C), λ(C)
- electron g factor ge- = -2.002 319 304 360 92(36), alias: g(e-)
- electron gyromagnetic ratio γe = 1.760 859 627 82(53) × 1011 1/(T s), aliases: gamma(e), γ(e)
- electron magnetic moment μe = -9.284 764 691 6(28) × 10-24 J/T, aliases: mu(e), μ(e)
- electron magnetic moment anomaly ae = 0.001 159 652 180 46(18), alias: a(e)
- electron mass me = 9.109 383 714 0 × 10-31 kg ± 0.30 ppb, alias: m(e)
The constants can be referenced by name or alias. To list all constants, call the search_constants
function with no arguments.
Users can also define constants to be added to the constant library:
uc.GummyConstant(1.75,unit='in',name='rack constant',symbol='rk',description='see also "rack unit"',add_symbol=True)
uc.constant('rk')
curve fitting¶
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
best fit parameters:
p1 = 1.7(19) m
p2 = -1.9(20) m/s
p3 = 1.37(43) m/s2
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
There are several other fit classes:
x = np.linspace(0,10,100)
y = np.sin(x + 0.88) + 1.3 + np.random.normal(scale=0.23,size=100)
sinfit = uc.SinFit(x,y,xunit='t',yunit='m')
sinfit
best fit parameters:
p1 = 1.076(33) m
p2 = 0.993(12) 1/t
p3 = 0.922(70)
p4 = 1.253(25) m
sinfit.plot(cip=0.95,clp=0.95)
# cip is the level of confidence for the fit uncertainty band in green
# clp (or clk) sets control limits in red, the region where the data points
# arelikely to lie
The predicted value is a gummy (and is correlated with the fit parameter gummys):
sinfit.ypred(5.2)
User defined functions can be fit with the Fit
class, either by passing the function
to the constructor of Fit
or by sub-classing Fit
.
applying numerical functions to gummys¶
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:
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 20 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 = 20
functions with multiple inputs and outputs¶
The gummy.apply
and gummy.napply
methods work with functions that have a
list or array output, returning a list of correlated gummys:
def f(x,y):
return [x + y, x - y, x*y]
x = uc.gummy(3.664,u=0.012)
y = uc.gummy(2.229,u=0.022)
z = uc.gummy.napply(f,x,y)
z
[5.893(25), 1.435(25), 8.167(85)]
uc.gummy.correlation_matrix(z) # or uc.gummy.covariance_matrix(z)
[[1, -0.5414012738853412, 0.9840308675421242], [-0.5414012738853412, 1, -0.682409848973878], [0.9840308675421242, -0.682409848973878, 1]]
creating correlated gummys¶
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
[1.10(30), 2.20(10), 3.30(40)]
g[0].covariance(g[2])
0.05999999999999998
The covariance_matrix
key word can be used in the create
method in place of u
and correlation_matrix
.
Note that mathematical operations between gummys will also create correlations between the input and output gummys.
complex numbers with uncertainty¶
The jummy
class can be used to represent complex numbers with uncertainties:
c = uc.jummy(real=1.334,imag=0.2345,cov=[[0.0127,0.0055],[0.0055,0.0334]])
c
Polar coordinates can also be used:
c.style='polar'
c
d = uc.jummy(r=2.445,phi=4.556,cov=[[0.022,0],[0,0.044]])
d
The .real
, .imag
, .r
and .phi
properties return gummy values:
e = c + d**2
e
e.real
e.imag
e.r
e.phi
formatting¶
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()
1.37(43) m/s**2
acceleration.unicode()
1.37(43) m/s²
Use the tolatex() or tohtml() methods to get a string with the LaTeX or HTML encoding for the gummy.
acceleration.tolatex()
'1.37(43)\\:\\mathrm{m}/\\mathrm{s}^{2}'
acceleration.tohtml()
'<span>1.37(43) m/s<sup>2</sup></span>'
It is also possible to change the default output to LaTeX:
uc.gummy.printer = 'latex'
acceleration
$ 1.37(43)\:\mathrm{m}/\mathrm{s}^{2} $
or unicode:
uc.gummy.printer = 'unicode'
acceleration
1.37(43) m/s²
or output using only ASCII characters:
uc.gummy.printer = 'ascii'
acceleration
1.37(43) m/s**2
or back to HTML:
uc.gummy.printer = 'html'
acceleration
Uncertainty budgets and fits can also be rendered using LaTeX:
budget = pressure.budget([side_1,side_2,force],uunit='%')
budget.latex()
budget.tolatex()
'\\begin{array}{ c c c c c c c c }\n\\text{Component} & \\text{Unit} & \\text{Value} & u\\,/\\,\\mathrm{\\%} & \\nu_{eff} & \\text{Type} & \\left\\lvert\\frac{\\partial y}{\\partial x} \\right\\rvert & s \\\\\n\\hline\n\\text{Side 2} & \\mathrm{mm} & 3.059 & 0.67 & 5 & \\text{A} & 8.1 & 0.54 \\\\\n\\text{Force} & \\mathrm{N} & 0.9346 & 0.59 & \\infty & \\text{B} & 26 & 0.42 \\\\\n\\text{Side 1} & \\mathrm{cm} & 1.2346 & 0.19 & \\infty & \\text{B} & 20 & 0.04 \\\\\n\\hline\nu_{c} \\text{ type A} & \\mathrm{kPa} & & 0.67 & 5.0 & \\text{A} & & 0.54 \\\\\nu_{c} \\text{ type B} & \\mathrm{kPa} & & 0.62 & \\infty & \\text{B} & & 0.46 \\\\\n\\hline\nP & \\mathrm{kPa} & 24.75 & 0.23 & 17.4 & \\text{} & & \\\\\n\\hline\nU_c \\text{ at } k = 2.1 & \\mathrm{kPa} & & 1.9 & & \\text{} & & \\\\\n\\end{array}'
As can the ouput of the fit from the curve fitting section:
fit.latex()