Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chemistry — molecules that know their own shape

The chemistry kit loads a real molecule from a real file. molecule3 reads an MDL structure file — an .sdf from PubChem’s Download → SDF, or a .mol from RDKit or Open Babel — and lays out its atoms and bonds as ordinary 3-D entities.

The point is that nothing about the shape is authored. The coordinates are the file’s, in Ångströms, so a bond angle on screen is the angle in the conformer someone computed. Hand-placing atoms gets a molecule that looks plausible; this gets one that is right.

canvas("16:9"); template("black");
camera3((0, -14, 4), (0, 0, 0), 40);

molecule3(caf, "asset:molecules/caffeine.sdf");
wait(4);

Three lines, and caffeine is on screen with its real geometry, turning slowly — because a shape only reads in three dimensions. Flat, methane is four lines from a dot.


The options

Everything past the path is optional:

molecule3(id, path, [center], [scale], [spec])
  • center — where to put it, default the origin. The molecule is centred on its own centroid first, so it lands in front of the camera whatever the file’s coordinates happened to be.
  • scale — world units per Ångström, default 1.5. A small molecule is then a few units across, which is the scale the rest of the 3-D kit works at.
  • spec — a bounded key=value string, below.
keyvaluesmeaning
styleball (default) · sticks · wireframe · spacefillhow it is drawn
hydrogens1 (default) · 00 drops them and their bonds
spindegrees per second, default 240 holds it still
axisz (default) · x · ywhich axis the turn is about
turn / tiltdegreesa static orientation, applied once
record1 (default)which molecule of a multi-record SDF
molecule3(caf, "asset:molecules/caffeine.sdf", (0,0,0), 0.9, "style=sticks hydrogens=0");
molecule3(bz,  "asset:molecules/benzene.sdf",  (0,0,0), 1.2, "spin=0 tilt=90");
molecule3(eth, "asset:molecules/alkanes.sdf",  (0,0,0), 2.0, "record=2");

The four styles answer four questions

  • ball & stick — what is bonded to what, and the shape. The textbook default.
  • sticks — the same connectivity with the spheres out of the way of the geometry. Bonds are two-tone, split at the midpoint and coloured by the atom each half touches, so a bond still says what it joins.
  • wireframe — thin lines only, for a large structure where anything solid hides it.
  • spacefill — full covalent radii, no bonds: the shape the molecule presents to the world, which is the one that decides what it fits into.

Hiding hydrogens

Half the atoms in an organic molecule are hydrogens, and often they are noise. hydrogens=0 removes them, removes the bonds that reached them (or the molecule sprouts sticks into empty space), and re-centres on what is left — so the molecule does not drift off the axis it spins about.

Animation is the default

The turn is on unless you switch it off, and it costs no timeline verb: it is a pure function of t, on the same per-frame path a cloud recomputes on. So it scrubs backwards exactly and records identically every run.

It rotates the molecule about its own axis, not around the world origin, and it writes the entity’s sync-owned home — which means shift3 and friends still work on top of it. A molecule can travel and turn at once, rigidly.


Structural formulae — the flat drawing

structure is the other half of the kit: the skeletal formula a chemist puts on paper, in 2-D.

canvas("16:9"); template("paper");
structure(glc, "asset:molecules/glucose-2d.sdf", (400, 360), 62);
wait(3);

That draws the pyranose ring with its five OH groups, its ring oxygen lettered, its carbons as bare vertices, and wedge and hash bonds where the file records stereochemistry — glucose’s 2-D record carries four of them, which is the difference between one sugar and another.

Every convention is derived from the file rather than authored:

what you seewhat justifies it
a carbon is a bare vertexthe element is C
hydrogens on carbon vanishthey are bonded to a C
an oxygen reads OHit has one explicit H neighbour
a nitrogen reads NH2it has two
a double bond leans into the ringthe crowded side, from the neighbours’ own coordinates — no ring detection needed
a bond is a solid wedgethe molfile’s fourth bond column says 1
a lone carbon reads CH4a single bare vertex is not a drawing of methane

Bonds stop short of a letter, so no line runs under text.

structure(id, source, [center], [unit], [spec])

source is either a file or a SMILES string — see From a SMILES string below.

unit is pixels per bond (default 64) — a 2-D record is laid out on a 1.0-per-bond grid, so it means exactly what it says.

keyvaluesmeaning
carbons0 (default) · 1letter every carbon too — the explicit style a first lesson uses
hydrogens0 (default) · 1draw them as their own vertices instead of folding them in
labelstext (default) · latexhow the letters are set
colorany palette name or #rrggbbthe bond and carbon colour
record1 (default)which molecule of a multi-record file

Typeset labels

labels=latex sets each label through the same LaTeX engine as equation, so a subscript is a real subscript and a charge is raised:

structure(gly, "asset:molecules/glycine-2d.sdf", (400, 360), 74, "labels=latex");

NH2 becomes NH₂, SO4^2- becomes SO₄²⁻, and element symbols stay upright (\mathrm{}) because an element is not a variable — italic O is wrong in a way a chemist notices at once. Element colours carry through the tint.

The cost is one rasterised image per distinct label, not per label: glucose has six lettered atoms but only two distinct strings (OH and O), so it costs two. Plain text stays the default because it is free and legible at any size — reach for LaTeX when the frame is going in front of an audience.

It needs the 2-D record

PubChem serves both, and they are not interchangeable:

…/compound/cid/5793/record/SDF?record_type=2d     ← for `structure`
…/compound/cid/5793/record/SDF?record_type=3d     ← for `molecule3`

The 2-D coordinates are laid out for depiction — a clean grid, regular hexagons, nothing overlapping. A 3-D conformer flattened is a tangle of crossing bonds, so structure refuses one and names the fix. The bundled 2-D files are asset:molecules/<name>-2d.sdf.

From a SMILES string

A file is not always what you have. A reaction record, a paper, a database export gives you SMILES — a molecule as a line of text — and structure takes one directly:

structure(eth, "CCO", (300, 360), 70);            // ethanol
structure(asp, "CC(=O)Oc1ccccc1C(=O)O", (800, 360), 54);   // aspirin

There is no second builtin and no conversion step: a string that does not look like a file — no asset:, no /, no .sdf or .mol — is read as SMILES. So the two forms never collide, and a scene can mix them freely.

The difference is where the coordinates come from. A file carries its depiction; a SMILES string does not, so the layout is computed:

what you getthe rule behind it
a chain zig-zagseach bond turns 60° from the last, so the interior angle is the tetrahedral 120°
a ring is a regular polygonclosed on its own circumcircle, whatever its size
a fused ring reflects across the shared bondso naphthalene reads as naphthalene, not as two rings on top of each other
a substituent points out of its ringradially, away from the ring centre — toluene’s methyl never points inward
an aromatic ring gets real double bondsthe lowercase c1ccccc1 form is Kekulé-ised across the whole aromatic system

The layout is checked before it is drawn, and this is the whole safety argument. Overlapping atoms, crossing bonds and a stretched bond are refused — with the 2-D-file fix named — rather than drawn as a tangle:

structure(m, "CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5");
error: the layout for `CN1CC[C@]23c4c5ccc(O)c4O[C@H]2[C@@H](O)C=C[C@H]3[C@H]1C5` is
not clean enough to draw: the bond between atoms 20 and 21 came out 4.00 bond-lengths
long. Laying a molecule out from SMILES is reliable for chains, single rings and
simply fused systems; a complex polycyclic needs a real depiction. Use a 2-D structure
file instead — asset:molecules/<name>-2d.sdf if it is bundled, or a PubChem download
with record_type=2d

That is morphine: five fused rings including a bridge, which is exactly what growing a layout outward cannot place. Fused systems that grow outward are fine, and that covers more than it sounds like — a steroid’s four rings, a substituted phenanthroline, a linear or angular aromatic all draw correctly. A bridged or caged skeleton does not, and says so.

The point is that nothing draws wrong. It either draws, or it tells you what to do instead — because a plausible-but-incorrect structural formula is worse than no drawing at all.

One consequence is worth knowing, because it will look like a bug otherwise: the layout grows along the string, so two SMILES for the same molecule need not behave the same. The 4,7-dimethoxy-1,10-phenanthroline in examples/reaction-ord-screen.manic is the case in point:

structure(l, "COC1=CC=NC2=C3N=CC=C(OC)C3=CC=C12");   // refused — a bond 3.6 long
structure(l, "COC1=CC=NC2=C1C=CC1=C2N=CC=C1OC");     // the same molecule, draws cleanly

Where the ring closures fall decides the order the rings get placed in, and one order paints the layout into a corner. If a string you did not write is refused, a canonical form of it — from PubChem, RDKit or Open Babel — is worth trying before reaching for a file. But when the exact string matters to the scene, use the depiction file and say why; that is what the escape hatch is for.

Atoms are numbered in written order from zero, which is often the most useful handle you could ask for. CC(=O)O writes its hydroxyl oxygen last, at a3:

structure(acid, "CC(=O)O", (190, 348), 76);
recolor(acid.a3, crimson, 0.6);   // the OH that is about to leave

Only lettered atoms get a label entity, so a{i} addresses heteroatoms — which is usually what a lesson wants to point at anyway.

See examples/reaction-esterification.manic, which builds A + B → C entirely from SMILES and marks the atoms that become the water.

Writing it on

Because bonds are paths and labels are text, the handwriting effect is a core-kit idiom — nothing chemistry-specific:

structure(caf, "asset:molecules/caffeine-2d.sdf", (350, 372), 54);
untraced(caf.bonds);
hidden(caf.labels);

stagger(0.05) { draw(caf.bonds, 0.4); }   // bond by bond
show(caf.labels, 0.6);

Address {id}.bonds, not {id}.b0…: a skeletal drawing skips every C–H bond, so the surviving ids keep the file’s numbering with gaps in it.

And since these are 2-D entities, framebox and brace work on them — which they do not on molecule3.

A molecule that couldn’t exist is refused

structure(x, "C(C)(C)(C)(C)C");
error: `C(C)(C)(C)(C)C`: this is not a molecule that can exist: atom 1 is C with
5 bonds, but C takes 4. If it is meant to be an ion, say so — `[C+]` or `[C-]` —
since a charge changes what a valence is allowed to be.

This is a separate check from the layout one, and it has to be. That carbon with five bonds lays out perfectly cleanly — no crossings, no overlaps, every bond the right length — so the geometry gate passes it and draws a confident picture of something impossible. Asking “is this readable?” and asking “is there anything to draw?” are different questions.

The rule is bond-order sum against the element’s allowed valences, adjusted for formal charge, which is what keeps it useful rather than pedantic:

structure(a, "O(C)(C)C");         // refused — three bonds on a neutral oxygen
structure(b, "[N+](C)(C)(C)C");   // fine — that is an ammonium ion

It runs on files too, not only SMILES strings — a hand-edited or carelessly converted file is exactly where a bad valence turns up.

What it does not catch, stated plainly: it is a capacity check, asking whether the atom could accommodate that many bonds. Nitrogen’s allowed valences are 3 and 5, so a neutral four-bonded nitrogen slips through — 5 could hold it, even though such a nitrogen is really an ammonium missing its charge.

The stricter alternative — requiring the count to equal an allowed valence — was measured against the bundled library and rejected. It refuses nitric oxide (a genuine radical, and a molecule any biology lesson wants), ferrocene and cyanocobalamin. Rejecting those to catch a four-bonded nitrogen is a bad trade, so the looser rule stands. Hypervalent sulfur and phosphorus, metal complexes and aromatics all pass either way.

What it’s called

A structure carries its own names, as two captions under the drawing:

structure(caf, "asset:molecules/caffeine-2d.sdf", (400, 300), 56);

show(caf.name, 0.4);    // caffeine
show(caf.iupac, 0.4);   // 1,3,7-trimethylpurine-2,6-dione

The gap between those two lines is the nomenclature lesson. Reveal the word, then reveal the systematic name, and the point makes itself — no narration needed.

Both start hidden, so show is the entire opt-in and no existing scene changes.

The two names come from different places, and the difference is worth understanding:

sourceavailable for
{id}.iupacthe record’s own name, or generatedbundled files, your files, and SMILES within a narrow scope
{id}.namethe curated asset:molecules/ filenamebundled molecules only

The systematic name is read straight from the record, which makes it authoritative and complete — stereodescriptors included, since that is most of what makes a real name hard. PubChem writes those with markup ((8<I>R</I>,9<I>S</I>)-…), so the tags are stripped before anything reaches the screen. For a SMILES string there is no record, so the name is generated; outside a narrow scope the generator declines, and then there is simply no iupac entity rather than a guess.

The everyday name is stranger, and worth explaining because the obvious approach fails. Nothing inside a record carries it. The title line is the bare compound id, and PubChem’s PUBCHEM_IUPAC_TRADITIONAL_NAME field — which looks exactly like the field for it — is semi-systematic as often as not:

caffeine       -> caffeine                          ✓
dopamine       -> 4-(2-aminoethyl)pyrocatechol      ✗
serotonin      -> 3-(2-aminoethyl)-1H-indol-5-ol    ✗
testosterone   -> (8R,9S,10R,13S,14S,17S)-17-hyd…   ✗

No length or shape rule separates the two, and a common name is a fact about usage rather than structure, so it cannot be computed at all. The one place Manic genuinely holds it is the library’s own filenames — every file under asset:molecules/ was fetched by resolving a common name to a compound id, so the slug is the name. That is why {id}.name exists for asset:molecules/dopamine-2d.sdf and not for your own file: anyone’s thing-2d.sdf could be called anything.

Long names wrap rather than running off the canvas — 25 of the bundled molecules have systematic names over 100 characters, and oxytocin’s is 306.

Functional groups are addressable

A structure is also tagged by the functional groups in it, perceived from the structure itself:

structure(asp, "CC(=O)Oc1ccccc1C(=O)O", (400, 360), 62);

color(asp.carboxyl, crimson);    // the acid: C, =O, OH, and the bonds between them
color(asp.ester, indigo);        // the acetyl group, including its linking O
color(asp.aromatic, crimson);    // the whole benzene ring

This is the same mechanism as the element tags ({id}.O, {id}.N) pointed at a different fact about an atom — so every core verb already works on it. Nothing chemistry-specific was added to make pulse(glc.hydroxyl) or framebox(box, asp.aromatic) work.

The twenty groups:

hydroxylcarbonylcarboxylaldehyde
ketoneesteramideprimary_amine
secondary_aminetertiary_aminenitronitrile
thiolsulfidesulfoxidesulfone
halogenetherepoxidearomatic

Four things worth knowing about how they behave:

A tag covers the bonds, not only the letters. Colouring a carboxyl that left its C=O line black would not be what you asked for, so a bond with both ends inside the group carries the tag too.

Every repeat shares one tag. Open-chain glucose has five hydroxyls, and color(glc.hydroxyl, indigo) colours all five — which is what the singular reads as.

Groups overlap on purpose. Acetic acid reports carboxyl, and also carbonyl and hydroxyl, because each of those is true of it and a lesson might point at any one. Deciding a carboxyl is “not really” a carbonyl would be the engine inventing chemistry. An ester, on the other hand, reports ester and not carboxyl — there is no free acid in it.

A file and a string give the same answer. That takes a little work behind the scenes: PubChem writes benzene in Kekulé form, with alternating single and double bonds and no aromatic flag anywhere, so aromaticity is perceived (Hückel) rather than taken on trust. Without that, asp.aromatic would find a ring in CC(=O)Oc1ccccc1C(=O)O and miss the identical ring in aspirin-2d.sdf.

A group that is not present is simply not tagged, and addressing it is an error that names the id — butane.hydroxyl does not silently match nothing.

Addressing the chemistry

Every piece is a plain tagged entity, so the core kit does the presentation:

addresswhat it is
{id}.a{i}the i-th atom, in file order
{id}.b{k}the k-th bond (.{n} per line of a multiple bond, .h{0,1} per half)
{id}.atoms · {id}.bondsthe two groups
{id}.multibondsonly the double and triple bonds
{id}.<element>every atom of one element{id}.O, {id}.N, {id}.Fe

That last one is the useful one, because it is how a lesson actually talks:

color(caf.N, cyan);       // "the nitrogens"
pulse(caf.O);             // "watch the oxygens"
view3(caf.N, "front");    // fly the camera to frame them
opacity(caf.H, 0.25);     // push the hydrogens back

show, fade, hidden, color, hue, opacity, pulse, to, draw + untraced, shift3, rotate3, orbit3, camera3, finish3 and view3 all work on a molecule, a group or one element.

Two limits worth knowing. framebox and brace measure 2-D screen bounds and do not see 3-D entities at all — that is true of model3 and sphere3 as much as a molecule; view3 is the 3-D framing verb. And draw traces the bonds on (a bond is a path); an atom is a sphere, so it swells rather than draws.


The bundled molecules

192 molecules, 392 files under asset:molecules/ — byte-exact PubChem downloads, most with both a 3-D conformer (<name>.sdf) and a 2-D depiction (<name>-2d.sdf). Enough to teach from without downloading anything.

Grouped the way a syllabus is: inorganics and gases · hydrocarbons · aromatics and heterocycles · alcohols, carbonyls, acids and solvents · sugars · all twenty amino acids · nucleobases and nucleotides · vitamins · lipids, steroids and hormones · neurotransmitters · medicines · flavours and famous molecules · industrial monomers.

The full table with compound ids is assets/molecules/INDEX.md.

Some have no 3-D conformer — mostly salts, metal complexes and the largest molecules — so only the -2d file exists. And three names are deliberately absent: cellulose, starch and haemoglobin are polymers or proteins rather than compounds, so PubChem has no single record for them.

Three honest caveats:

  1. They are computed conformers, not measured structures. PubChem optimises with MMFF94, so a bond length sits within about 0.05 Å of the experimental value rather than on it. Right for teaching shape; not a source for a number you intend to quote.
  2. Some have no 3-D conformer at all — mostly the inorganics and salts — so the 2-D depiction is bundled and every z is zero. Such a molecule lies flat, and tilting it will show it edge-on.
  3. PubChem writes benzene in Kekulé form, alternating single and double, rather than as a delocalised ring. That is the file being explicit, not the reader guessing.

Your own file

Any .sdf or .mol works — pass a path instead of an asset: URI. From PubChem:

https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/2519/record/SDF?record_type=3d

A V3000 molfile is refused with the command to convert it, rather than mis-parsed: obabel in.sdf -O out.sdf --outputV2000.


An example end to end

Why molecules have shapes — four molecules, four geometries, and not one coordinate in the script:

canvas("16:9"); template("black");
camera3((0, -15, 8.5), (0, 0, 0), 40);

molecule3(w,  "asset:molecules/water.sdf",          (-6.4, 0, 0), 1.5);
molecule3(am, "asset:molecules/ammonia.sdf",        (-2.1, 0, 0), 1.5);
molecule3(me, "asset:molecules/methane.sdf",        ( 2.2, 0, 0), 1.5);
molecule3(co, "asset:molecules/carbon-dioxide.sdf", ( 6.6, 0, 0), 1.5);

text(t1, (196, 566), "bent");        text(t3, (760, 566), "tetrahedral");
text(t2, (452, 566), "pyramidal");   text(t4, (1060, 566), "linear");
wait(6);

The labels are shape classes, not measurements — the geometry on screen is the file’s, and there is no number in the script to disagree with it.

See the chemistry gallery for the full scenes.