Reconcile molecular formulas and calculated properties
Parse eight PubChem formulas, reconstruct approximate molar masses and compare elemental mass fractions and reported descriptors.
Goal and how to run
This lesson assumes basic Python knowledge. Read each method and caption before changing parameters. AI exercises are optional; no AI account is needed for the core workflow. Data lives in notebook metadata, so there is no companion data download. Keep the notebook saved in the active working folder.
Your goal is to understand the research question, execute transparent calculations, check the results, and state the limits of the conclusion.
Setup and parameters
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
import re
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='ch01-molecular-formulas'
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
Load the embedded snapshot
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={'molecules': '780c5f98bb3b7261465f8330d4b31589d4224cdf21f916b4a1788f11bcc00aac'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['molecules']
Read the method functions
def parse_simple_formula(formula):
"""Parse plain elemental formulas; refuse isotopes, groups and charge notation."""
tokens=re.findall(r'([A-Z][a-z]?)(\d*)',formula)
if not tokens or ''.join(element+count for element,count in tokens)!=formula:
raise ValueError('This teaching parser accepts plain elemental formulas only')
counts={}
for element,text in tokens:
count=int(text) if text else 1
if count<1:raise ValueError('Element counts must be positive')
counts[element]=counts.get(element,0)+count
return counts
print("Method ready: parse_simple_formula")
Method ready: parse_simple_formula
Data and model boundary
PubChem properties and conformers are calculated database fields, not new experimental measurements. V2000 parsing here is intentionally limited to atoms, reported bonds and formal charges. Titration and calibration use stated ideal/simulated teaching assumptions. Water pressure uses the attributed IAPWS reference correlation. Descriptors, distances and visual proximity do not establish biological activity or binding strength.
Steps
1. Load calculated fields and define the parser boundary
The source provides molecular formula, calculated molecular weight and descriptors for eight declared CIDs. The readable parser accepts ordinary element/count formulas, and rejects parentheses, isotope and charge notation rather than silently changing their meaning.
molecules=snapshot("molecules")
properties=pd.DataFrame(molecules["properties"]).set_index("CID")
assert len(properties)==8 and properties.index.is_unique
display(properties[["name","MolecularFormula","MolecularWeight","TPSA","Charge"]])
atomic_weights={"H":1.008,"C":12.011,"N":14.007,"O":15.999}
counts={int(cid):parse_simple_formula(row.MolecularFormula) for cid,row in properties.iterrows()}
assert all(set(count)<=set(atomic_weights) for count in counts.values())
| name | MolecularFormula | MolecularWeight | TPSA | Charge | |
|---|---|---|---|---|---|
| CID | |||||
| 702 | ethanol | C2H6O | 46.070 | 20.2 | 0 |
| 176 | acetic acid | C2H4O2 | 60.050 | 37.3 | 0 |
| 241 | benzene | C6H6 | 78.110 | 0.0 | 0 |
| 887 | methanol | CH4O | 32.042 | 20.2 | 0 |
| 180 | acetone | C3H6O | 58.080 | 17.1 | 0 |
| 2519 | caffeine | C8H10N4O2 | 194.190 | 58.4 | 0 |
| 2244 | aspirin | C9H8O4 | 180.160 | 63.6 | 0 |
| 750 | glycine | C2H5NO2 | 75.070 | 63.3 | 0 |
2. Reconstruct approximate molar masses
Use explicitly rounded average atomic weights in g/mol. This is not a monoisotopic mass calculation. Small differences from PubChem arise from declared rounding and representation; keep both original and reconstructed values.
calculated=np.array([sum(atomic_weights[element]*count for element,count in counts[int(cid)].items()) for cid in properties.index])
comparison=properties[["name","MolecularWeight"]].copy();comparison["reconstructed_g_per_mol"]=calculated
comparison["difference_g_per_mol"]=calculated-properties.MolecularWeight
fig,ax=plt.subplots(figsize=(8,4));ax.barh(properties.name,properties.MolecularWeight,label="PubChem")
ax.plot(calculated,np.arange(8),"kx",label="Rounded atomic-weight reconstruction")
ax.set(xlabel="Molar mass (g/mol)",title="Formula-based reconstruction of calculated source masses");ax.legend();plt.tight_layout();plt.show()
3. Read elemental mass fractions
Stacked bars give percent mass attributed to C, H, N and O under the same rounded-weight convention. A mass fraction is different from an atom-count fraction; hydrogen’s many atoms need not dominate total mass.
fraction=pd.DataFrame({element:[atomic_weights[element]*counts[int(cid)].get(element,0)/calculated[j] for j,cid in enumerate(properties.index)] for element in atomic_weights},index=properties.name)
fig,ax=plt.subplots(figsize=(9,4));(100*fraction).plot.bar(stacked=True,ax=ax)
ax.set(ylabel="Elemental mass (%)",xlabel="Declared compound",title="Mass fractions under the stated atomic-weight convention");plt.xticks(rotation=25,ha="right");plt.tight_layout();plt.show()
4. Compare a second calculated descriptor
TPSA is topological polar surface area in square ångströms, a computed descriptor. Its scatter against molar mass describes these eight molecules only, and does not estimate permeability, toxicity or drug efficacy.
fig,ax=plt.subplots(figsize=(8,5));ax.scatter(properties.MolecularWeight,properties.TPSA,s=40)
for row in properties.itertuples():ax.annotate(row.name,(row.MolecularWeight,row.TPSA),fontsize=8,xytext=(4,4),textcoords="offset points")
ax.set(xlabel="Calculated molar mass (g/mol)",ylabel="Calculated TPSA (Ų)",title="Reported descriptors, not a biological assay");plt.tight_layout();plt.show()
5. Check conservation and unsupported syntax
Fractions sum to one and reconstructed masses agree with the reported rounded masses within 0.03 g/mol. Verify that a grouped formula is rejected, then export calculations with the weight convention.
assert np.allclose(fraction.sum(axis=1),1)
assert np.max(abs(comparison.difference_g_per_mol))<.03
try:parse_simple_formula("Mg(OH)2")
except ValueError:print("Grouped formula rejected by the declared simple parser")
else:raise AssertionError("Unsupported syntax silently accepted")
OUTPUT_DIR.mkdir(parents=True,exist_ok=True);comparison.to_csv(OUTPUT_DIR/"molar-mass-reconciliation.csv")
fraction.to_csv(OUTPUT_DIR/"elemental-mass-fractions.csv")
(OUTPUT_DIR/"atomic-weights.json").write_text(json.dumps(atomic_weights,indent=2))
print("Mass and fraction checks passed; exports:",OUTPUT_DIR)
Grouped formula rejected by the declared simple parser Mass and fraction checks passed; exports: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads/results/ch01-molecular-formulas
Exercises and reference explanation
- Compare atom fraction and mass fraction for ethanol.
- Explain why this is not monoisotopic mass.
Reference explanation. Mass fractions weight each element count by the declared average atomic weight. The table uses rounded average weights, not individual isotope masses.
Keep a copy before changing parameters. Compare old and new figures and record which inputs, calculations and conclusions changed.
Optional AI coding exercises
Prompt 1
Using standard-library re plus NumPy/Pandas/Matplotlib, add a clear atom-fraction chart for the same eight CIDs.
Prompt 2
Using the same packages, annotate mass reconstruction differences and list unsupported formula syntax explicitly.
Human acceptance. Mass fractions sum to one, reconstruction uses the saved weight table and unsupported grouped/charged formulas raise errors.
Review the edited source, then manually run affected cells and subsequent checks. Ask the assistant to explain the purpose, packages and data provenance of its changes, and reconcile that explanation with actual output.
Optional online extension
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2519/property/MolecularFormula/JSON',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
Sources, snapshots and next steps
PubChem calculated molecular properties — NCBI public data; PubChem computed properties only; credit PubChem; no third-party narrative annotations included; retrieved 2026-10-09. SHA-256
b01b092cc8872e14fdab1fdc608c783669131a081c71d9ccee77c0bb9c6ff408. Keep the returned calculated fields for eight declared CIDs; molecular weight converted from API numeric string.PubChem 2D SDF 702 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
5962afc9ee8115dee9d5e63cefeef89cdc47641420b34afe156062d263c9caa7. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 176 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
f51353e1db0a0be8aa3aeb7bea28b6febb1e1f8afdf88fbf298405851f7ee092. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 241 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
3f4698bd6773f5e709184759048c116d106ac392e874676444b64af254e9a189. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 887 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
5cf6d180935f2fe77d20772a4bf2506711d4436762f6c6801157ac748f088530. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 180 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
6ecceacce326b6ecc292dedd946f589e879c999f43242395d995b59a8c8eb128. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 2519 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
47293391d630f44f2a32c3f7702d766744c8f4a4f16f6571d4ae09f04b3daaed. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 2244 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
0dfd04496745032c06410241f91f10fcec77cbc6828cdbede75f4c08da418fbb. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem 2D SDF 750 — NCBI public data; PubChem standardized structure; credit PubChem; retrieved 2026-10-09. SHA-256
e1c2ddd732cea1d052a9426c9a25fef86acd1309748bb2115cf2b56481ba288f. Preserve complete standardized SDF, including CID and explicit hydrogen/bond fields.PubChem caffeine 3D conformer — NCBI public data; PubChem computed conformer; credit PubChem; retrieved 2026-10-09. SHA-256
2a78d98d09d50768fe3f6e6114d7baf3dad9cf88e41835735f96abf5867f5092. Preserve calculated coordinates and conformer identifiers; not an experimental crystal structure.
Next steps. Transfer these methods to your own public or authorized data while retaining input checks, recorded parameters and result validation. Document the new experimental design and method limits; this example does not validate a new experiment. Full data licenses and generation instructions accompany the cookbook.