Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dpgoldenberg/ed9789eee59dfb9e187b8ef406e5ad19 to your computer and use it in GitHub Desktop.
Save dpgoldenberg/ed9789eee59dfb9e187b8ef406e5ad19 to your computer and use it in GitHub Desktop.
Pipette Calibration Simulation
name: pipette_sim
channels:
- conda-forge
- defaults
- conda-forge/label/broken
dependencies:
- python=3.7.3
- numpy=1.16.4
- ipython=7.6.1
- ipywidgets=7.5.0
- voila = 0.1.21
## Module to simulate procedure for testing pipettes (and user)
## by weighing successive aliqots of water
## with widgets to allow interactive control in a Jupyter notebook
## David P. Goldenberg, December 2020
## goldenberg@biology.utah.edu
import numpy as np
import ipywidgets as widgets
from IPython.display import display, HTML
from time import sleep
import threading
import random as rand
display(HTML("<style>div.output_scroll { height: 200ex; }</style>"))
#---------Global variables-----------------#
# global variables
curr_grams = 0.0 # current grams displayed
p_delta = 0.0 # inaccuracy of pipette calibration, in uL
p_sigma = 0.0 # std dev used to calculate both normal distr of p_delta
# and error in individual pipette events
#--------Functions for simulating pipette errors ------------#
def pip_err(pip_size,sigma,p_fail):
'''Sets global variables, p_delta and p_sigma.
input arguments:
pip_size: maximum volume delivered by pipette, in uL
sigma: relative value of standard deviation, in percent
p_fail: probability of a significant pipette malfunction
'''
global p_delta,p_sigma
sigma = 0.01*sigma*pip_size
if rand.random() < p_fail:
# if pipette malfunction, p_delta is set to random value
# uniformly distributed between +/- 0.2*pip_size
p_delta = 0.4*pip_size*(rand.random()-0.5)
else:
# if no malfunction, p_delta is randomly distributed
# about 0
p_delta = rand.normalvariate(0,sigma)
if rand.random() < p_fail:
# if pipette malfunction, p_sigma is set to
# random value distributed uniformly between
# +/- 0.2*pip_size
p_sigma = 0.2*pip_size*rand.random()
else:
p_sigma = rand.normalvariate(0,sigma)
#print(p_delta,p_sigma)
def del_err(pip_size,delta,sigma):
# Calculates error for individual pipette operations
err = rand.normalvariate(delta, sigma)
return err
#---------------Widgets-----------------------#
header_html = """
<h2> Biology 3515/Chemistry 3515
<br>
Biological Chemistry Laboratory
<br>
University of Utah </h2>
<h3> Spring 2021 </h3>
<h3> Experiment 1, Part A: Pipette Calibration</h3>
<p style="font-size:16px">
This web page simulates the procedure for testing pipettes (and their use) by weighing aliquots of water
added successively to a balance. To carry out the procedure, first tare (zero) the balance. Then, choose a pipette size
(P1000: 1 mL, P200: 200 &micro;L, P20: 20 &micro;L) and set the volume to be delivered. Pipette 5 aliquots of water and
record the mass after each aliquot. Be sure to wait for the balance to reach equilibrium before recording the mass!
<br>
If you find that the pipette displays unacceptable errors, you should obtain a new one, by first selecting a different-sized pipette and then selecting the original size again.
<hr border-width:6px, color:black>
"""
style = {'description_width': 'initial'}
header = widgets.HTML(
value=header_html
)
footer = widgets.HTML(
value='''
<hr border-width:6px, color:black>
David P. Goldenberg, January 2021<br>
<a href="mailto:goldenberg@biology.utah.edu" target="new">goldenberg@biology.utah.edu</a><br>
School of Biological Sciences, University of Utah<br>
Salt Lake City, Utah 84112<br>
<a href="https://goldenberg.biology.utah.edu/courses/biol3515/index.shtml"
target="new">https://goldenberg.biology.utah.edu/courses/biol3550.</a>
''')
pipette = widgets.Dropdown(
# Choose pipette size
options = [('P1000',1000.0),('P200',200.0),('P20',20.0)],
style=style,
description = 'Pipette:',
layout=widgets.Layout(width='200px',margin='10px 100px 15px 0px')
)
volume = widgets.BoundedFloatText(
value = 1000.0,
min = 200.0,
max = 1000.0,
step = 10.0,
style=style,
description = 'Volume set (uL):',
layout=widgets.Layout(width='200px',margin='10px 100px 15px 0px')
)
grams = widgets.HTML(
# Grams display
value='<h1> gm: <span style="color:red;"> {:.4f}'.format(0) + '</span></h1>',
layout=widgets.Layout(width='250px',margin='25px 0px 15px 200px')
)
deliver_butt = widgets.Button(
# Button to deliver volume
description = 'Pipette!',
button_style = 'success',
style=style,
layout=widgets.Layout(width='150px',margin='25px 150px 15px 70px')
)
tare_butt = widgets.Button(
# Button to tare balance
description = 'Tare balance',
button_style = 'warning',
style=style,
layout=widgets.Layout(width='150px',margin='25px 0px 15px 0px')
)
#------------Functions to control widgets----------------#
def damp_sin(t,nu,tau):
'''damped sine function to simulate settling
of balance. Input arguments:
t: time
nu: sine-function frequency
tau: exponential-decay time constant
'''
d_sin = np.exp(-t/tau)*np.sin(t*nu*2*np.pi)
return d_sin
def adjust_grams(start,end,t_tot,steps):
'''Updates grams display after adding aliquot
Input arguments:
start: starting mass
end: mass after addition
t_tot: total time for balance to settle
steps: number of times to update balance display
as it settles.
'''
tau = 0.2*t_tot
nu = 5/t_tot
for i in range(steps+1):
t = t_tot*i/steps
gms = end + (end-start)*damp_sin(t,nu,tau)
sleep(t_tot/steps)
update_grams(gms)
def on_pipette_ch(change):
'''Changes volume and error parameters when pipette is changed.'''
volume.max = 1000.0
if pipette.value == 1000.0:
volume.min = 200.0
volume.max = 1000.0
volume.step = 10.0
elif pipette.value == 200.0:
volume.min = 20.0
volume.max = 200.0
volume.step = 1.0
elif pipette.value == 20.0:
volume.min = 1.0
volume.max = 20.0
volume.step = 0.1
volume.value = volume.max
p_fail = 0.1
rel_sigma = 1
pip_err(pipette.value,rel_sigma,p_fail)
def update_grams(gm):
'''Updates grams display'''
grams.value='<h1> gm: <span style="color:red;"> {:.4f}'.format(gm) + '</span></h1>'
def deliver(change):
# function for deliver button
# Updates curr_grams and calls adjust_grams
# to update grams display
global curr_grams
start = curr_grams
gram_incr = volume.value/1000.0
gram_incr += del_err(pipette.value,p_delta,p_sigma)/1000.0
curr_grams += gram_incr
t_tot =2
steps=25
adjust_grams(start,curr_grams,t_tot,steps)
def tare(change):
# Function for tare button.
# sets curr_grams to 0 and updates display
global curr_grams
curr_grams = 0
update_grams(curr_grams)
#----------Monitor widgets--------------#
pipette.observe(on_pipette_ch,names=['value'])
deliver_butt.on_click(deliver)
tare_butt.on_click(tare)
#----------Display widgets--------------#
update_grams(curr_grams)
row1 = widgets.HBox([pipette,volume])
row2 = widgets.HBox([deliver_butt,tare_butt])
display(widgets.VBox([header,row1,row2,grams,footer]))
#------------Start everything-----------#
on_pipette_ch(0)
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment