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
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<style>div.output_scroll { height: 200ex; }</style>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "47b312dacc1c4db7a7421f644f792742",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(HTML(value='\\n<h2> Biology 3515/Chemistry 3515\\n<br>\\nBiological Chemistry Laboratory \\n<br>\\nU…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import pipette_cal_sim\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.0"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment