{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Metal Organic Frameworks\n", "\n", "> ### In this tutorial we will cover:\n", "> - how we can build metal organic frameworks in BuildAMol from scratch" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Metal Organic Frameworks (MOF) are a class of materials consisting of metal ions and organic linker molecules that have been studied for purposes of catalysis or gas filtering. Actually, if you are reading this tutorial you likely know more about them than I do, so let's just jump in!\n", "\n", "In this tutorial we will build a pillard paddlewheel MOF as is shown in figure 1b of the review by [Deria et al. (2014)](https://doi.org/10.1039/C4CS00067F)\n", "![](https://pubs.rsc.org/image/article/2014/CS/c4cs00067f/c4cs00067f-f1.gif)\n", "\n", "Since BuildAMol does by default not have any trick up it's sleeve to create\n", " MOFs we will have to make the structure from hand. Also, the highly geometric nature of the overall structure means that we cannot really use the `attach` method to assemble our fragments since that method is designed to more freely find good spatial placements instead of maintaining a good orientation for macro-assembly. As a consequence we will be assembling the fragments using `merge` (which does not alter the placement of fragments in any way) and manually establish the placements. \n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sounds like a lot of work, right? Well, it is not as straightforward as calling some function a few times but hopefully it will be clear by the end. Admittedly, this tutorial will be a lengthy one but hopefully it will be very illustrative for cases where manual assembly is the only option to obtain the desired structure. If you make it through this tutorial you will surely be a master at using methods such as `align_to`, `superimpose_to_bond`, or `move`, all of which we will be using to get the right fragment in the right orientation at the right position. \n", "\n", "Let's go!" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import buildamol as bam\n", "import numpy as np" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Get the basic components\n", "\n", "The structure essentiall consists of two basic units, the metal complex and benzene rings. We can get the benzene rings from BuildAMol's built-in data but the metal complex is a more challenging bit. Spoilers ahead: we'll be just loading a file with an existing complex, which I prepared previously with the help of Stk, which is really good when it comes to metal complexes. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "bam.load_small_molecules()\n", "\n", "bnz = bam.molecule(\"BNZ\")\n", "paddle = bam.molecule(\"./files/paddlewheel.xml\")\n", "\n", "# here we also define the size of the total system we want to build\n", "X_STACK = 5 # number of molecules in the x direction\n", "Y_STACK = 5 # number of rows in the y direction\n", "Z_STACK = 5 # number of layers in the z direction\n", "\n", "# let's actually look at the metal complex component that we are going to start with\n", "paddle.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Make the basic stackable units\n", "\n", "### Make the planar linkers between metal complexes\n", "We can make the planar linkers by simply adding some benzenes together. Because I tried this out a bunch of times, I also know that we will have to `bend` the structure a little to make sure the geometries of the `paddle` metal complexes and the planar linkers will be stackable" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# make the planar linker of five benzenes\n", "planar_linker = bam.benzylate(bnz.copy(), [\"C1\", \"C2\", \"C4\", \"C5\"])\n", "\n", "v = planar_linker.py3dmol()\n", "\n", "# now bend the bonds that connect the peripheral benzenes a little\n", "angle = 11.5\n", "sign = 1\n", "for i, bond in enumerate(planar_linker.get_residue_connections(triplet=False)):\n", " planar_linker.bend_at_bond(*bond, angle, neighbor=\"H6\")\n", " if i == 1: # it's assymmetric otherwise...\n", " angle = -angle\n", "\n", "v += planar_linker.py3dmol(color=\"red\")\n", "v.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can add the metal complexes to the linkers:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "unit = planar_linker.copy()\n", "\n", "# we will add metal complexes to the peripheral benzenes\n", "# (the order itself is not so important but the whole downstream process\n", "# will be dependent on the order here, i.e. different \n", "# order = different residue numbers in the cells below!)\n", "for res in (2, 4, 3, 5):\n", " incoming = paddle.copy()\n", " pres = incoming.get_residue(1)\n", " ures = unit.get_residue(res)\n", " ref = (pres.get_atom(\"H1\"), pres.get_atom(\"C1\"))\n", " target = (ures.get_atom(\"C4\"), ures.get_atom(\"H4\"))\n", " \n", " # superimpose the icoming metal complex to the right position\n", " # then align it to the z_axis (the inversion term was necessary since one complex\n", " # would always align in the inverse orientation, I don't know why only this one tbh...)\n", " incoming.superimpose_to_bond(ref, target)\n", " incoming.align_to(bam.structural.z_axis * (-1 if res == 2 else 1))\n", " \n", " # now merge the incoming complex with the linker\n", " unit.remove_atoms(target[1])\n", " incoming.remove_atoms(ref[0])\n", " unit.merge(incoming)\n", " unit._add_bond(ref[1], target[0])\n", "\n", "# we want to make sure everything is one single \n", "# chain (since merge would normally add new chains)\n", "unit.squash_chains()\n", "\n", "# also we will remove all hydrogens since we don't care about them\n", "unit.remove_hydrogens()\n", "\n", "unit.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And there we have our basic `unit`. From this we can now make derivatives that we will stack to create our final supra-structure.\n", "\n", "## Stacking in the first direction\n", "\n", "We have our basic `unit`. From this will wil derivate a second molecule `row_tile` which we will stack in the y-direction. Our strategy is to first stack in one direction that use that \"full row\" to stack in the second direction and then use that \"full layer\" to stack in the third direction. \n", "\n", "### Making the Row-Tile\n", "To make our `row_tile` molecules we will selectively remove two metal complexes again from the `unit`. Like so:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# make the row_tile by removing the two \"top\" complexes\n", "row_tile = unit.copy()\n", "# remove the complexes (if you don't know how we know these are the right numbers\n", "# you can always call `unit.plotly().show()` to check again which residues are where)\n", "row_tile -= row_tile.get_residues(6, 8)\n", "\n", "row_tile.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to stack our `row_tile`s we will need to align each incoming tile to the metal complexes of the previous tile. Here is how we can do it: " ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "stacked = unit.copy()\n", "\n", "# first compute the distance between the stacked units\n", "# (empirically, I just tried out a few times until it looked good)\n", "diff = stacked.compute_length_along_axis(\"x\") - 0.4 * paddle.compute_length_along_axis(\"x\") + 1\n", "\n", "for i in range(Y_STACK):\n", "\n", " # get a new row_tile\n", " incoming = row_tile.copy()\n", "\n", " # move it to the right position\n", " incoming.move([0, (i+1) * diff, 0])\n", "\n", " # and merge into the structure\n", " stacked.merge(incoming)\n", "\n", "# finally we make sure everything is just one chain\n", "# and we infer the bonds between the stacked units\n", "stacked.squash_chains()\n", "stacked.infer_bonds(restrict_residues=False)\n", "stacked.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alrighty, there we have our first \"full row\" of stacked units! So, to summarize, we used the following method to get here: `superimpose_to_bond` in order to place incoming metal complexes in the right spot. We also used `align_to` to make sure we have a good orientation of metal complexes (i.e. vertical). We used `bend_at_bond` to slightly distort the `planar_linker` and we used `merge` to assemble our fragments into a single molecule. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Stacking in second direction\n", "\n", "Now we will go about stacking the \"full row\" in x-direction in order to get a \"full layer\". \n", "\n", "### Making the Plane-Tile\n", "\n", "The `plane_tile` will be derived from `stacked`. Just like for the `row_tile` we will be removing some metal complexes from the structure. However, this time we don't want to use residue numbers (since we don't know them and we don't want to look at the structure to get the numbers manually since there are so many residues now) but instead we will use a little geometric thinking to remove \"all metal complexes on one side\". Here's how:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plane_tile = stacked.copy()\n", "\n", "# remove all UNL's that are one one side\n", "# (well, we know those two still from before, \n", "# but you can always check with `plane_tile.plotly().show()`\n", "to_remove = plane_tile.get_residues(8, 7)\n", "\n", "# now we compute the vector between the two residue centers\n", "# which will give us the trajectory of the line where all \n", "# other metal complexes we want to remove will also be\n", "trajectory = bam.structural.vector_between(*to_remove)\n", "\n", "# now we iterate over all metal complexes (UNL residue name)\n", "for unl in plane_tile.get_residues(\"UNL\"):\n", " if unl in to_remove:\n", " continue\n", "\n", " # we compute again the vector from the residue to one of\n", " # our starting complexes (8 or 7 does not matter)\n", " vec = unl.coord - to_remove[0].coord\n", "\n", " # and compute the cosine similarity\n", " # if the similarity is close to 1 the metal complex \n", " # is on the line and we should remove it\n", " dot = np.dot(trajectory, vec)\n", "\n", " trajectory_magnitude = np.linalg.norm(trajectory)\n", " line_segment_magnitude = np.linalg.norm(vec)\n", "\n", " cosine_similarity = dot / (trajectory_magnitude * line_segment_magnitude)\n", "\n", " if cosine_similarity > 0.995: # Adjust the threshold as per your requirement\n", " to_remove.append(unl)\n", "\n", "# now remove all identified metal complexes\n", "plane_tile.remove_residues(*to_remove)\n", "plane_tile.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And there we have our `plane_tile` unit which we can now stack to get our \"full layer\". " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "full_layer = stacked.copy()\n", "\n", "# compute the distance we need to shift each incoming tile as the \"width\" of the x-stacked - \"width\" of the metal complexes\n", "diff = full_layer.compute_length_along_axis(\"x\") - paddle.compute_length_along_axis(\"x\") + 3.25\n", "\n", "for i in range(X_STACK):\n", " \n", " # get a new plane-tile and move it to the right spot\n", " incoming = plane_tile.copy()\n", " incoming.move([(i+1) * diff, 0, 0])\n", "\n", " # then merge into the structure\n", " full_layer.merge(incoming)\n", "\n", "full_layer.squash_chains()\n", "full_layer.infer_bonds(restrict_residues=False)\n", "\n", "full_layer.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Perfect, there is our `full_layer`! " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Stacking in third direction\n", "\n", "Now we can start to think about staking our `full_layer` in the z direction. To do so we first must get the `axial_linker`s however, which connect the different layers. \n", "\n", "### Making the axial linkers between layers\n", "The axial linkers are simply two benzene rings in our case so we can make them very easily using:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# this line will make a two-benzene molecule whith only one chain and residue named LNK\n", "axial_linker = bam.benzylate(bnz.copy(), \"C1\").squash(resname=\"LNK\").autolabel()\n", "axial_linker.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Making the Z-Tile\n", "The stackable unit we want is the `full_layer` with `axial_linker`s attached. So we will now distribute a linker on top of each metal complex in the layer. " ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "z_tile = full_layer.copy()\n", "\n", "for unl in z_tile.get_residues(\"UNL\"):\n", "\n", " incoming = axial_linker.copy()\n", "\n", " # we will align the incoming linker so that it's meta carbon on one ring is directly above the nitrogen in the metal complexes\n", " # (did I say we would not use superimpose anymore? well, guess I lied, here is one more superimpose)\n", " # (btw. how did I know about C5 being a meta carbon? I used `autolabel` so I knew this would be one... But if you are unsure\n", " # use `axial_linker.plotly().show()` to see for yourself)\n", " ref = (incoming.get_atom(\"C5\"), incoming.get_atom(\"H5\"))\n", " \n", " # now for the target nitrogen and iron atoms. The tricky bit is that even though we aligned to the Z-axis (at the beginning)\n", " # we cannot be sure whether NA or NB is actually going to be the \"upward\" nitrogen. So to be on the save side, we will use \n", " # this little snippet to get the right one in each case:\n", " N1 = unl.get_atom(\"NA\")\n", " N2 = unl.get_atom(\"NB\")\n", " if N1.coord[2] > N2.coord[2]:\n", " target = N1, unl.get_atom(\"FEA\")\n", " else:\n", " target = N2, unl.get_atom(\"FEB\")\n", "\n", " # now we can superimpose the linker\n", " incoming.superimpose_to_bond(ref, target)\n", "\n", " # now the linker is still a little too low (we aligned to N-FE, remember)\n", " # so we just push it up a little bit\n", " incoming.move([0, 0, 1.5])\n", " \n", " # and merge it into the structure (but without hydrogens)\n", " incoming.remove_hydrogens()\n", " z_tile.merge(incoming)\n", "\n", "z_tile.squash_chains()\n", "z_tile.infer_bonds(restrict_residues=False)\n", "z_tile.py3dmol().show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Great! Now we have our stackable unit for the z-direction! The only thing left to do is stack the whole thing to get our final structure!" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/3dmoljs_load.v0": "
\n

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n jupyter labextension install jupyterlab_3dmol

\n
\n", "text/html": [ "
\n", "

You appear to be running in JupyterLab (or JavaScript failed to load for some other reason). You need to install the 3dmol extension:
\n", " jupyter labextension install jupyterlab_3dmol

\n", "
\n", "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "final = z_tile.copy()\n", "\n", "# we again compute the distance beforehand so we can just use `move` to place \n", "diff = z_tile.compute_length_along_axis(\"z\") + 1\n", "\n", "for i in range(Z_STACK):\n", "\n", " # we want the top-most layer to be the one without axial linkers\n", " # so we distinguish which fragment is incoming\n", " if i == Z_STACK -1:\n", " incoming = full_layer.copy()\n", " else:\n", " incoming = z_tile.copy()\n", "\n", " incoming.move([0, 0, (i+1) * diff])\n", " final.merge(incoming)\n", "\n", "final.infer_bonds(restrict_residues=False)\n", "final.py3dmol().show() " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And there we have our final structure! Let's save it to a PDB file:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "final.to_pdb(\"./files/MOF.pdb\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So, let's recapitulate what we did in order to get here. We could not straightforwardly \"attach\" fragments together because we required them to be in a specific spatial arrangement in order to stack multiple molecules together into one regular large construct. Therefore we employed methods such as `superimpose_to_bond`, `align_to`, or `move` in order to obtain a desirable placement and orientation of our incoming fragments. Then we used `merge` to assemble larger structures. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The most difficult part was assembling the basic `unit`, where we neaded to superimpose, align, and also bend the structure. \n", "\n", "Especially the last part is not something that we could have known _a priori_ but we learned was necessary after having tried to assemble the `row_tile`. Therefore, even if this workflow looks very \"linear\" I did not write it from start to finish before actually pressing the play button! Developing your pipeline for creating the structures you need will involve experimentation, so be sure to run your code as often as possible and be sure to save your work regularly or work on **copies**! \n", "\n", "However, as you were hopefully able to see, creating a regular assembly of fragments is very straightforward in BuildAMol thanks to methos such as `move` or `align_to` that allow us to easily create assemblies of fragments in specific orientations by hand." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With that we have reached the end of this tutorial! Hopefully you now feel able to create your own MOFs! Thanks for checking out this tutorial and good luck with your project using BuildAMol!" ] } ], "metadata": { "kernelspec": { "display_name": "glyco2", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 2 }