Bufffer titration simulation
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
## 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: | |
- python=3.7.3 | |
- numpy=1.16.4 | |
- scipy | |
- ipython=7.6.1 | |
- ipywidgets=7.5.0 | |
- voila = 0.1.21 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment