求解理想弱酸滴定
在强碱滴定过程中使用质量与电荷平衡,对照缓冲近似并检查物种分数。
目标与运行方法
本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。
学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。
环境与参数
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
from scipy.optimize import brentq
from IPython.display import display as _display
def display(value):
if isinstance(value,pd.DataFrame):
table=value.to_html(max_rows=12,escape=True)
style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
return _display(value)
RECIPE_ID='ch04-titration'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
读取内嵌快照
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'models': '48fd2e6e706650fe2ee0c0ad80b6d794a546a1dec78dd9d370ac09db3cc3adbf'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['models']
方法函数
def weak_acid_titration(acid_molar,acid_volume_L,base_molar,added_volume_L,pKa=4.76,Kw=1e-14):
"""Ideal dilute monoprotic-acid/strong-base charge balance, concentration model."""
volumes=np.asarray(added_volume_L,dtype=float)
if min(acid_molar,acid_volume_L,base_molar,Kw)<=0 or not np.isfinite(volumes).all() or np.any(volumes<0):
raise ValueError('Invalid titration concentrations or volumes')
Ka=10**(-pKa);pH=[];fractions=[];residuals=[]
for added in volumes:
total_volume=acid_volume_L+added;total_acid=acid_molar*acid_volume_L/total_volume;sodium=base_molar*added/total_volume
def balance(log_h):
H=10**log_h;anion=total_acid*Ka/(Ka+H)
return H+sodium-Kw/H-anion
log_h=brentq(balance,-16,1,xtol=1e-13)
H=10**log_h;pH.append(-log_h);fractions.append(Ka/(Ka+H));residuals.append(balance(log_h))
return dict(pH=np.asarray(pH),anion_fraction=np.asarray(fractions),charge_residual_mol_per_L=np.asarray(residuals))
print("Method ready: weak_acid_titration")
Method ready: weak_acid_titration
数据与模型边界
PubChem 性质与构象是数据库计算字段,不是新实验测量。本课 V2000 解析范围明确限定为原子、来源键型与形式电荷。滴定及校准使用已声明的理想/模拟教学假设;水蒸气压使用署名的 IAPWS 参考关联式。描述符、距离和视觉邻近不能证明生物活性或结合强度。
分步分析
1. 在完整滴定范围求解电荷平衡
单质子弱酸与强碱采用 H+[Na+]−Kw/H−[A−]=0,其中 [A−]=Ct Ka/(Ka+H)。这是声明温度下的理想浓度模型,未包含活度修正及真实仪器响应。
p=snapshot("models")["titration"]
added=np.linspace(0,.05,501)
result=weak_acid_titration(p["acid_initial_mol_per_L"],p["acid_volume_L"],p["base_mol_per_L"],added,p["pKa"],p["Kw"])
equivalence=p["acid_initial_mol_per_L"]*p["acid_volume_L"]/p["base_mol_per_L"]
fig,ax=plt.subplots(figsize=(8,4));ax.plot(1000*added,result["pH"])
ax.axvline(1000*equivalence,color="gray",linestyle="--",label="Stoichiometric equivalence")
ax.set(xlabel="Added base (mL)",ylabel="Ideal concentration-model pH",title="Weak-acid/strong-base titration");ax.legend();plt.tight_layout();plt.show()
2. 解读物种分数
绘制总酸以 HA 和 A− 存在的分数,其和因质量平衡等于一。当量体积附近的变化描述理想平衡计算,不是直接浓度测量。
fraction=result["anion_fraction"]
fig,ax=plt.subplots(figsize=(8,4));ax.plot(1000*added,1-fraction,label="HA fraction");ax.plot(1000*added,fraction,label="A− fraction")
ax.set(xlabel="Added base (mL)",ylabel="Fraction of total acid",ylim=(0,1.05),title="Ideal speciation fractions");ax.legend();plt.tight_layout();plt.show()
3. 在适用区间比较缓冲近似
Henderson–Hasselbalch 对照在 0<V<Veq 使用化学计量缓冲量,此形式在零添加量和当量处未定义。应画与完整电荷平衡的差,而不是跨整条曲线套用。
mask=(added>.002)&(added<equivalence-.002)
acid_moles=p["acid_initial_mol_per_L"]*p["acid_volume_L"];base_moles=p["base_mol_per_L"]*added[mask]
approximation=p["pKa"]+np.log10(base_moles/(acid_moles-base_moles))
fig,ax=plt.subplots(figsize=(8,4));ax.plot(1000*added[mask],approximation-result["pH"][mask])
ax.axhline(0,color="black",lw=1);ax.set(xlabel="Added base (mL), buffer interval",ylabel="Approximation − charge-balance pH",title="Approximation error is not electrode uncertainty");plt.tight_layout();plt.show()
4. 检查电中性并导出假设
核对摩尔电荷残差、滴定曲线单调性及半当量附近 pKa 行为。pKa=4.76 和 Kw 是教学输入;数值检查不能替代真实滴定或活度模型。
assert np.max(abs(result["charge_residual_mol_per_L"]))<1e-10
assert np.all(np.diff(result["pH"])>=-1e-9) and np.all((fraction>=0)&(fraction<=1))
half=weak_acid_titration(p["acid_initial_mol_per_L"],p["acid_volume_L"],p["base_mol_per_L"],np.array([equivalence/2]),p["pKa"],p["Kw"])
assert abs(half["pH"][0]-p["pKa"])<.01
OUTPUT_DIR.mkdir(parents=True,exist_ok=True)
pd.DataFrame({"base_volume_mL":added*1000,"ideal_pH":result["pH"],"anion_fraction":fraction,"charge_residual_mol_per_L":result["charge_residual_mol_per_L"]}).to_csv(OUTPUT_DIR/"ideal-titration.csv",index=False)
(OUTPUT_DIR/"assumptions.json").write_text(json.dumps(p,indent=2))
print("Mass/charge and half-equivalence checks passed; exports:",OUTPUT_DIR)
Mass/charge and half-equivalence checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ch04-titration
自主练习与参考答案
- 解释半当量比较。
- 化学计量缓冲近似在哪些区域失效?
参考解释。 理想弱酸半中和时 HA/A− 数量相近,pH 接近 pKa。起始/当量条件不能使用简单化学计量比值式。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
仅用 NumPy、SciPy、Matplotlib 比较 pKa=4.0、4.76、5.5,保持相同酸碱量。
提示词 2
使用同样的包增加电荷残差图,不裁剪 pH 曲线。
人工验收。 分数在 [0,1],电荷平衡通过,声明全部体积与浓度。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brentq.html',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
来源、快照与下一步
IAPWS saturation reference release — IAPWS SR1-86(1992): unrestricted publication allowed in all countries (cover page); retrieved 2026-10-09. SHA-256
a10d38d73339b0713c86cfac88f9adcba137202167ccdfa1cd58238c438361d7. Reference equations and numerical coefficients transcribed from release pages 2–3; pedagogical model values are not new experiments.Authored numerical teaching model parameters — Original teaching parameters/code MIT; reference equation coefficients attributed to IAPWS SR1-86(1992); retrieved 2026-10-09. SHA-256
4350c4305a83c772fa295a8be0ef86a247f2bfaae930a081720d3cd0c753abe0. Declared parameters and seed; generated trajectories and calibration observations must be labelled simulated.
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。