|
## 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) |
|
|