Wednesday, 19 June 2019

Creating Chemical Structure Animations

I've just got back from the Eighth Joint Sheffield Conference on Chemoinfomatics where I presented about the technical details of subgraph isomorphism algorithms. It was a great conference (as usual) with good science, interesting posters and lots of fun. Noel was live tweeting the whole thing so check out the #Shef2019 hashtag if you want to catch up.

To help explain the algorithms in my talk I created some animations (as videos) that showed the backtracking search procedures. After the talk several delegates asked how I created these so I thought I do a quick blog post on it (and also to remind me in future how to do it again). I'd done something similar before to demonstrate the Sayle and Delany algorithm for tautomer enumeration. Here's the PDF (and PPTX with the videos) of my Sheffield talk.

CDK + ffmpeg


The general idea is to generate a bunch of PNGs and then stick them together with the ffmpeg command line tool. In older blog posts I used to generate a GIF but it turns out mp4 compresses better with higher quality.

Step 1: Generating the PNGs with CDK


The example code below loads the SMILES for indole and then loops around highlighting one atom. Other than some normal params we also set the symbol visibility. By default the depiction will add in symbols for highlighted carbons, we can turn this off by overriding the parameter.

String dest = "/tmp/example1";
IChemObjectBuilder bldr   = SilentChemObjectBuilder.getInstance();
SmilesParser       smipar = new SmilesParser(bldr);
IAtomContainer     mol    = smipar.parseSmiles("[nH]1ccc2c1cccc2");
DepictionGenerator dg = new DepictionGenerator().withZoom(5)
                                                .withSize(1280, 720)
                                                .withParam(StandardGenerator.Visibility.class,
                                                           SymbolVisibility.iupacRecommendationsWithoutTerminalCarbon())
                                                .withOuterGlowHighlight();
new File(dest).mkdirs();
for (int i=0; i<100; i++) {
    IAtom atm = mol.getAtom(i % mol.getAtomCount());
    dg.withHighlight(Collections.singleton(atm), Color.RED)
      .depict(mol)
      .writeTo(String.format("%s/frame.%03d.png", dest, i));
}

Step 2: Stick them all together


$ ffmpeg -r 12/1 -start_number 0 -i /tmp/example1/frame.%03d.png \
  -c:v libx264 -r 30 -pix_fmt yuv420p example1.mp4

The key parameters here are the output name (last arg) and input name template (-i /tmp/example1/frame.%03d.png). I zero padded the numbers so they will be ordered correctly when alphabetically sorted and match this in the argument. You don't need to zero-pad but it means they should then alphabetical in your OS file system which is handy. The first -r is used to set the input frame rate to 12/1 (12 per 1 second).

Okay so how did it turn out....

Download: example1.mp4

Not bad but the inter-frame alignment is off due to the different size caused by the moving outer-glow. In future I might have an anchor attribute that would lock the depiction in place relative to some atom but that's quite specialised. We can fix the shifting by highlighting all other atoms to the same as the background. This is possible with the high-level APIs but it's easy enough just to set the field on the atom:

for (int i=0; i<100; i++) {
    IAtom target = mol.getAtom(i % mol.getAtomCount());
    // reset all
    for (IAtom a : mol.atoms())
        a.setProperty(StandardGenerator.HIGHLIGHT_COLOR,
                      Color.WHITE);
    target.setProperty(StandardGenerator.HIGHLIGHT_COLOR,
                       Color.RED);
    dg.depict(mol)
      .writeTo(String.format("%s/frame.%03d.png", dest, i));
}


Download: example2.mp4

Add/Remove Atoms and Bonds?


Because of the alignment issue we need to use some tricks to add/remove atoms and bonds. Essentially you draw the whole thing and then hide the parts you don't want by setting them to the background color. For example:

int frameId = 0;
for (IAtom atom : mol.atoms())
    hide(atom);
for (IBond bond : mol.bonds())
    hide(bond);
// add bonds
for (int i = 0; i < mol.getBondCount(); i++) {
    IBond bnd = mol.getBond(i);
    show(bnd);
    show(bnd.getBegin());
    show(bnd.getEnd());
    dg.depict(mol)
      .writeTo(String.format("%s/frame.%03d.png", dest, ++frameId));
}
// hold for 12 frames
for (int i = 0; i < 12; i++)
    dg.depict(mol)
      .writeTo(String.format("%s/frame.%03d.png", dest, ++frameId));
// remove bonds
for (int i = 0; i < mol.getBondCount(); i++) {
    IBond bnd = mol.getBond(mol.getBondCount()-i-1);
    hide(bnd);
    if (!visible(bnd.getBegin()))
        hide(bnd.getBegin());
    if (!visible(bnd.getEnd()))
        hide(bnd.getEnd());
    dg.depict(mol)
      .writeTo(String.format("%s/frame.%03d.png", dest, ++frameId));
}

Where show/hide are:

private static void hide(IChemObject x) {
    x.setProperty(StandardGenerator.HIGHLIGHT_COLOR,
                  Color.WHITE);
}

private static void show(IChemObject x) {
    x.setProperty(StandardGenerator.HIGHLIGHT_COLOR,
                  Color.BLACK);
}

Producing:

Download: example3.mp4

That's just about it, the CDK depiction is quite configurable but not really intended to be a general purpose drawing/animation tool. However using some tricks you can work around some quirks and get some nice results. If you make animations let me know as I'd love to see them!