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 fomightez/ab73fc7f666516c9f910d85400600109 to your computer and use it in GitHub Desktop.
Save fomightez/ab73fc7f666516c9f910d85400600109 to your computer and use it in GitHub Desktop.
Bufffer titration simulation
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: 100ex; }</style>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "23da79bd22e54d74b0be1591e334a4fa",
"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 buffer_titr"
]
},
{
"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.8.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
## Module to simulate titration of a weak acid or a weak base
## with a strong base or strong acid, respectively
## to prepare a buffer solution
## with widgets to allow interactive control in a Jupyter notebook
## Computations of pH during titration as described at:
## https://chem.libretexts.org/Bookshelves/Ancillary_Materials/Demos_Techniques_and_Experiments/General_Lab_Techniques/Titration/Titration_of_a_Weak_Base_with_a_Strong_Acid
## David P. Goldenberg, January 2021
## goldenberg@biology.utah.edu
import numpy as np
from scipy.optimize import fsolve
import ipywidgets as widgets
from IPython.display import display, HTML
from IPython.display import display, clear_output, HTML, FileLink
from time import sleep
import threading
import random as rand
display(HTML("<style>div.output_scroll { height: 300ex; }</style>"))
#----------Functions for calculating pH under different circumstances--------------#
def OHfunc(OH,base_conc,pKa):
'''Function to be solved to calculate OH concentration
for untitrated base'''
Ka = 10.0**(-pKa)
K = 1e-14/Ka
return OH**2 + OH*K - base_conc*K
def pH_base(pKa,base_conc):
'''Calculates pH for untitrated base'''
OHconc =fsolve(OHfunc,1e-7,(base_conc,pKa))
pH = float(-np.log10(1e-14/OHconc))
return pH
def Hfunc(H,acid_conc,pKa):
'''Function to solve to calculate H concentration
from dissociation of conjugate acid'''
Ka = 10.0**(-pKa)
return H**2 + Ka*H - Ka*acid_conc
def pH_acid(pKa,acid_conc):
'''Calculates pH for untitrated acid'''
Hconc = fsolve(Hfunc,1e-7,(acid_conc,pKa))
pH = -np.log10(Hconc)
return float(pH)
def pH_base_titr(pKa,base_moles,titr_moles):
'''Calculates pH when moles of added strong acid
are less than moles weak base'''
ba_ratio = (base_moles-titr_moles)/titr_moles
pH = pKa + np.log10(ba_ratio)
return pH
def pH_acid_titr(pKa,acid_moles,titr_moles):
'''Calculates pH when moles of added strong base
are less than moles weak acid'''
ba_ratio = titr_moles/(acid_moles-titr_moles)
pH = pKa + np.log10(ba_ratio)
return pH
def pH_post_eq_b(base_moles,titr_moles,volume):
'''Calculates pH when moles of added strong acid
are greater than moles weak base'''
Hconc = (titr_moles - base_moles)/volume
pH = -np.log10(Hconc)
return(pH)
def pH_post_eq_a(acid_moles,titr_moles,volume):
'''Calculates pH when moles of added strong base
are greater than moles weak acid'''
OHconc = (titr_moles - acid_moles)/volume
pH = np.log10(OHconc) + 14.0
return(pH)
def pH_full_titr_b(pKa,base_moles,v_init,titr_moles,titr_vol):
'''Calculates pH over full titration of a weak base
with a strong acid'''
vol_tot = v_init + titr_vol
if titr_moles == 0:
base_conc = base_moles/v_init
return pH_base(pKa,base_conc)
elif 0 < titr_moles < base_moles:
return pH_base_titr(pKa,base_moles,titr_moles)
elif titr_moles == base_moles:
conc = base_moles/vol_tot
return pH_acid(pKa,conc)
else:
return pH_post_eq_b(base_moles,titr_moles,vol_tot)
def pH_full_titr_a(pKa,acid_moles,v_init,titr_moles,titr_vol):
'''Calculates pH over full titration of a weak acid
with a strong base'''
vol_tot = v_init + titr_vol
if titr_moles == 0:
acid_conc = acid_moles/v_init
return pH_acid(pKa,acid_conc)
elif 0 < titr_moles < acid_moles:
return pH_acid_titr(pKa,acid_moles,titr_moles)
elif titr_moles == acid_moles:
conc = acid_moles/vol_tot
return pH_base(pKa,conc)
else:
return pH_post_eq_a(acid_moles,titr_moles,vol_tot)
def pH_calc(buffer,pKa,buff_moles,v_init,titr_moles,titr_vol):
'''Calculates pH over full titration of either a weak acid
or a weak base'''
if buffer == 'Weak acid':
ph = pH_full_titr_a(pKa,buff_moles,
v_init,titr_moles,titr_vol)
elif buffer == 'Weak base':
ph = pH_full_titr_b(pKa,buff_moles,
v_init,titr_moles,titr_vol)
else:
ph = float('nan')
return ph
#------------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 B: Buffer Preparation</h3>
<p style="font-size:16px">
This web page simulates a common laboratory protocol for preparing buffer solutions, in which a weak acid or base is titrated with a strong base or acid, respectively.
<br>
The top two rows of controls specify the starting parameters for the
titration: The buffer type (weak acid or weak base),the buffer
pK<sub>a</sub>, the number of moles of buffer and the initial solution
volume. The next row contains controls for specifying concentration
and volumes of individual titrant additions.
Titrant is added by clicking the green button. After each addition
the new pH is displayed, but it takes a little while to settle down!
The volumes of titrant are also updated. Click the orange button to
reset the titration.
<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>
''')
buffer = widgets.Dropdown(
options=['Weak acid','Weak base'],
value='Weak acid',
description='Buffer type:',
style=style,
layout=widgets.Layout(width='200px',margin='0px 100px 15px 0px')
)
pK = widgets.FloatSlider(
value = 7.0,
min = 3.0,
max = 11.0,
step = 0.1,
description = 'Buffer pKa:',
orientation = 'horizontal',
readout=True,
readout_format='.1f',
style=style,
layout=widgets.Layout(width='300px',margin='0px 0px 15px 0px')
)
moles = widgets.FloatText(
value = 0.5,
min = 0.01,
max = 5.0,
step = 0.01,
description = 'Moles buffer:',
style=style,
layout=widgets.Layout(width='200px',margin='0px 100px 15px 0px')
)
init_vol = widgets.FloatText(
value = 0.5,
min = 0.01,
max = 5.0,
step = 0.01,
description = 'Initial vol. (L):',
style=style,
layout=widgets.Layout(width='200px',margin='0px 0px 15px 0px')
)
hrule = widgets.HTML(
value = '<hr border-width:6px, color:black>'
)
titr_conc = widgets.Dropdown(
options=[('6 M',6.0),('1 M',1.0),('0.1 M',0.1)],
value = 6.0,
description = 'NaOH conc:',
style=style,
layout=widgets.Layout(width='200px',margin='10px 100px 15px 0px')
)
aliq_vol = widgets.Dropdown(
options = [('10 mL',10.0),('1.0 mL',1.0),('0.1 mL',0.1)],
value = 1.0,
description = 'NaOH vol:',
style=style,
layout=widgets.Layout(width='200px',margin='10px 100px 15px 0px')
)
add_titr_butt = widgets.Button(
description='Add NaOH',
button_style = 'success',
style=style,
layout=widgets.Layout(width='150px',margin='25px 150px 15px 70px')
)
reset_butt = widgets.Button(
description = 'Reset titration',
button_style ='warning',
style=style,
layout=widgets.Layout(width='150px',margin='25px 0px 15px 0px')
)
pH = widgets.HTML(
layout=widgets.Layout(width='150px',margin='25px 0px 15px 250px')
)
vol_header = widgets.HTML(
value = '<h3> NaOH volumes added: </h3>',
layout=widgets.Layout(width='80%',margin='10px 0% 0px 0%')
)
vol_add_6M = widgets.HTML(
value = '<h4> 6M: 0 </h4>',
layout=widgets.Layout(width='200px',margin='0px 25px 15px 0px')
)
vol_add_1M = widgets.HTML(
value = '<h4> 1M: 0 </h4>',
layout=widgets.Layout(width='200px',margin='0px 25px 15px 0px')
)
vol_add_0p1M = widgets.HTML(
value = '<h4> 1M: 0 </h4>',
layout=widgets.Layout(width='200px',margin='0px 25px 15px 0px')
)
#------------Widget Control Functions---------------------#
# some global variables. The shame!
global vol_6M, vol_1M, vol_0p1M, ph, titr_moles,titr_vol
titr_moles = 0.0
titr_vol =0.0
vol_6M = 0.0
vol_1M = 0.0
vol_0p1M = 0.0
def adjust_pH(start,end,t_tot,steps,sigma):
tau = 0.2*t_tot
for i in range(steps+1):
t = t_tot*i/steps
noise = rand.normalvariate(1,sigma)
pH = (start-end)*np.exp(-t/tau)*noise + end
sleep(t_tot/steps)
update_pH(pH)
def update_titr(b):
global vol_6M, vol_1M, vol_0p1M,ph, titr_moles
old_ph = ph
if titr_conc.value == 6.0:
vol_6M += aliq_vol.value
if titr_conc.value == 1.0:
vol_1M += aliq_vol.value
if titr_conc.value == 0.1:
vol_0p1M += aliq_vol.value
titr_moles = (6.0*vol_6M +
1.0*vol_1M +
0.1*vol_0p1M)/1000.0
titr_vol = vol_6M + vol_1M + vol_0p1M
ph = pH_calc(buffer.value,pK.value,moles.value,
init_vol.value,titr_moles,titr_vol)
update_vols(vol_6M,vol_1M,vol_0p1M)
adjust = threading.Thread(target=adjust_pH, args=(old_ph,ph,10,50,0.2))
adjust.start()
def update_pH(ph):
pH.value='<h1> pH: <span style="color:red;"> {:.2f}'.format(ph) + '</span></h1>'
def update_vols(vol_6M,vol_1M,vol_0p1M):
vol_add_6M.value = '<h4> 6M: ' + str(round(vol_6M,2)) + ' mL </h4>'
vol_add_1M.value = '<h4> 1M: ' + str(round(vol_1M,2)) + ' mL </h4>'
vol_add_0p1M.value = '<h4> 0.1M: ' + str(round(vol_0p1M,2)) + ' mL </h4>'
def on_reset(change):
global titr_moles,vol_6M,vol_1M, vol_0p1M,ph
titr_moles = 0
vol_6M = 0
vol_1M = 0
vol_0p1M = 0
ph = pH_calc(buffer.value,pK.value,moles.value,
init_vol.value,titr_moles,titr_vol)
if buffer.value=='Weak acid':
titr_conc.description = 'NaOH conc:'
aliq_vol.description = 'NaOH vol:'
add_titr_butt.description='Add NaOH'
vol_header.value = '<h3> NaOH volumes added: </h3>'
else:
titr_conc.description = 'HCl conc:'
aliq_vol.description = 'HCl vol:'
add_titr_butt.description='Add HCl'
vol_header.value = '<h3> HCl volumes added: </h3>'
update_vols(vol_6M,vol_1M,vol_0p1M)
update_pH(ph)
add_titr_butt.on_click(update_titr)
reset_butt.on_click(on_reset)
buffer.observe(on_reset,names='value')
pK.observe(on_reset,names='value')
moles.observe(on_reset,names='value')
init_vol.observe(on_reset,names='value')
#------------Display all of the widgets-----------------#
row1 = widgets.HBox([buffer,pK])
row2 = widgets.HBox([moles,init_vol])
row3 = widgets.HBox([titr_conc,aliq_vol])
row4 = widgets.HBox([add_titr_butt,reset_butt])
row5 = pH
row6 = vol_header
row7 = widgets.HBox([vol_add_6M,vol_add_1M,vol_add_0p1M])
display(widgets.VBox([header,row1,row2,hrule,row3,row4,row5,hrule,row6,row7,footer]))
on_reset(0)
name: buffer_sim
channels:
- conda-forge
- defaults
- conda-forge/label/broken
dependencies:
- numpy
- scipy
- ipywidgets
- voila
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment