Last active
October 27, 2021 19:10
-
-
Save dpgoldenberg/e8fef5127a71180ebfb2c59b86d481c1 to your computer and use it in GitHub Desktop.
Bufffer titration simulation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Gist title |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## 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 | |
## [email protected] | |
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:[email protected]" target="new">[email protected]</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) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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