.. _mda-label: MDAnalysis tutorials ******************** .. container:: hatnote Perform post-mortem analysis using Python and MDAnalysis .. container:: justify There are two main ways to analyze data from a molecular dynamics simulation: (1) on-the-fly analysis, for instance using *fix ave/time* command, and (2) post-mortem analysis, using the trajectories dumped in the *lammpstrj* file. .. container:: justify The main advantage of post-mortem analysis is that there is no need to know what we want to measure before starting the simulation. .. container:: justify In these short tutorials, several trajectories are imported into Python using MDAnalysis and different information is extracted. All the trajectories required for these tutorials are provided below and were generated from one of the LAMMPS tutorials. .. include:: ../../non-tutorials/needhelp.rst .. figure:: ../figures/level2/polymer-in-water/PEG-dark.webp :alt: Movie of a peg molecule in water as simulated with LAMMPS :height: 250 :align: right :class: only-dark .. figure:: ../figures/level2/polymer-in-water/PEG-light.webp :alt: Movie of a peg molecule in water as simulated with LAMMPS :height: 250 :align: right :class: only-light Simple trajectory import ======================== .. container:: justify Here, we re-use a trajectory generated during the :ref:`all-atoms-label` tutorial. Download the |dump_all_atom| and the |data_all_atom| files to continue with this tutorial. .. |dump_all_atom| raw:: html dump .. |data_all_atom| raw:: html data Create a Universe ----------------- .. container:: justify Open a new Jupyter notebook and call it *simple_import.ipynb*. First, let us import both *MDAnalysis* and *NumPy* by copying the following lines into *simple_import.ipynb*. .. code-block:: python import MDAnalysis as mda import numpy as np .. container:: justify Then, let us create a MDAnalysis *universe* using the LAMMPS data file *mix.data* as topology, and the *dump.lammpstrj* file as trajectory. Add the following lines into the notebook (adapt the *path_to_data* accordingly): .. code-block:: python path_to_data = "./" u = mda.Universe(path_to_data + "mix.data", path_to_data + "dump.lammpstrj", topology_format="data", format="lammpsdump") Read topology information ------------------------- .. container:: justify From the :ref:`all-atoms-label` tutorial, we know that atom types 1 to 7 are from the PEG atoms, and atom types 8 and 9 are from the water molecules. .. container:: justify One can create atom groups using the atom types with the *select_atoms* option of MDAnalysis: .. code-block:: python peg = u.select_atoms("type 1 2 3 4 5 6 7") h2o = u.select_atoms("type 8 9") .. container:: justify Let us print the number of atoms in each group: .. code-block:: python print("atoms in peg:", peg.atoms.n_atoms) print("atoms in h2o:", h2o.atoms.n_atoms) .. code-block:: bw atoms in peg: 101 atoms in h2o: 3045 .. container:: justify Atom groups are atom containers, from which information about the atoms can be read. For instance, one can loop over the 6 first atoms from the peg group, and extract their IDs, types, masses, and charges: .. code-block:: python for atom in peg[:6]: id = atom.id type = atom.type mass = atom.mass charge = np.round(atom.charge,2) print("Atom id:", id, "type:", type, "mass:", mass, "g/mol charge:", charge, "e") Atom id: 3151 type: 4 mass: 1.008 g/mol charge: 0.19 e Atom id: 3152 type: 6 mass: 15.9994 g/mol charge: -0.31 e Atom id: 3153 type: 5 mass: 12.011 g/mol charge: 0.06 e Atom id: 3154 type: 3 mass: 1.008 g/mol charge: 0.05 e Atom id: 3155 type: 3 mass: 1.008 g/mol charge: 0.05 e Atom id: 3156 type: 2 mass: 12.011 g/mol charge: 0.02 e Extract temporal evolution -------------------------- .. container:: justify Let us extract the position of the first atom of the peg group (i.e. the hydrogen of type 4), and store its coordinates in each frame into a list: .. code-block:: python atom1 = peg[0] position_vs_time = [] for ts in u.trajectory: x, y, z = atom1.position position_vs_time.append([ts.frame, x, y, z]) .. container:: justify Here, the for loop runs over all the frames, and the x, y, and z coordinates of the atom named *atom1* are read. Here *ts.frame* is the id of the frame, it goes from 0 to 300, i.e. the total number of frames. The *position_vs_time* list contains 301 items, each item being the frame id, and the corresponding coordinates of *atom1*. .. container:: justify One can use Matplotlib Pyplot to visualize all the x and y coordinates occupied by *atom1* during the simulation. .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/position-atom-dark.png :alt: plot of the position-atom :class: only-dark .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/position-atom-light.png :alt: plot of the position-atom :class: only-light .. container:: figurelegend Figure: Position of the *atom1* along time. The size of the disks is proportional to the frame ID. .. figure:: ../figures/level1/breaking-a-carbon-nanotube/CNT_dark.webp :alt: carbon nanotube image in vacuum :height: 250 :align: right :class: only-dark .. figure:: ../figures/level1/breaking-a-carbon-nanotube/CNT_light.webp :alt: carbon nanotube image in vacuum :height: 250 :align: right :class: only-light Counting the bonds of a CNT =========================== .. container:: justify Here, we re-use the trajectory generated during the second part *Breakable bonds* of the :ref:`carbon-nanotube-label` tutorial. It is recommended that you follow this tutorial first, but you can also directly download the |dump_cnt| file and the |data_cnt| file and continue with this MDA tutorial. .. |dump_cnt| raw:: html dump .. |data_cnt| raw:: html data Create a Universe ----------------- .. container:: justify Open a new Jupyter Notebook and call it *measure_bond_evolution.ipynb*. First, let us import both *MDAnalysis* and *NumPy* by copying the following lines into *measure_bond_evolution.ipynb*. .. code-block:: python import MDAnalysis as mda import numpy as np .. container:: justify Then, let us create a MDAnalysis *universe* using the LAMMPS data file *cnt_atom.data* as topology, and the *lammpstrj* file as trajectory. Add the following lines into *measure_bond_evolution.ipynb*: .. code-block:: python path_to_data = "./" u = mda.Universe(path_to_data + "cnt_deformed.data", path_to_data + "dump.lammpstrj", topology_format="data", format="lammpsdump", atom_style='id type xs ys zs', guess_bonds=True, vdwradii={'1':1.7}) .. container:: justify Since the *.data* file does not contain any bond information the original bonds are guessed using the bond guesser of MDAnalysis using *guess_bonds=True*. .. container:: justify Note that the bond guesser of MDAnalysis will not update the list of bond over time, so we will need to use a few tricks to extract the evolution of the number of bonds with time. .. container:: justify Let us create a single-atom group named *cnt* and containing all the carbon atoms, i.e. all the atoms of type 1, by adding the following lines into *measure_bond_evolution.ipynb*. .. code-block:: python cnt = u.select_atoms("type 1") Some basics of MDAnalysis ------------------------- .. container:: justify MDAnalysis allows us to easily access information concerning the simulation, such as the number of atoms, or the number of frames in the trajectory: .. code-block:: python print("Number of atoms =", cnt.n_atoms) print("Number of frames =", u.trajectory.n_frames) Number of atoms = 690 Number of frames = 286 .. container:: justify It is also possible to access the indexes of the atoms that are considered as bonded by the bond guesser of MDAnalysis: .. code-block:: python print(cnt.atoms.bonds.indices) [[ 0 2] [ 0 23] [ 0 56] (...) [686 687] [686 689] [688 689]] .. container:: justify MDAnalysis also offers the possibility to loop over all the frame of the trajectory using: .. code-block:: python for ts in u.trajectory: print(ts.frame) 0 1 2 3 (...) 283 284 285 .. container:: justify The positions of the atoms can also be obtained using: .. code-block:: python u.atoms.positions array([[ 75.14728 , 78.17872 , 95.61408 ], [ 75.33008 , 77.751114, 93.20232 ], [ 75.550476, 77.34152 , 94.54224 ], ..., [ 84.66992 , 82.24888 , 143.84988 ], [ 84.66992 , 82.24888 , 147.60156 ], [ 84.85272 , 81.82128 , 146.26175 ]], dtype=float32) .. container:: justify where the three columns of the array are the *x*, *y*, and *z* coordinates of the atoms. Counting the bonds ------------------ .. container:: justify In order to measure the evolution of the number of bonds over time, let us loop over the trajectory and manually extract the inter-atomic distance over time. .. container:: justify To do so, for every step of the trajectory, let us loop over the indexes of the atoms that were initially detected as bonded, and calculate the distance between the two atoms, which can be done using: .. code-block:: python for ts in u.trajectory: for id1, id2 in cnt.atoms.bonds.indices: # detect positions pos1 = u.atoms.positions[u.atoms.indices == id1][0] pos2 = u.atoms.positions[u.atoms.indices == id2][0] r = np.sqrt(np.sum((pos1-pos2)**2)) .. container:: justify Then, let us assume that if :math:`r` is larger that a certain cut-off value of, let's say, :math:`1.8\,Å`, the bond is broken: .. code-block:: python for ts in u.trajectory: for id1, id2 in cnt.atoms.bonds.indices: pos1 = u.atoms.positions[u.atoms.indices == id1][0] pos2 = u.atoms.positions[u.atoms.indices == id2][0] r = np.sqrt(np.sum((pos1-pos2)**2)) if r < 1.8: print("the bond has a length", r, "Å") else: print("the bond is broken") .. container:: justify Finally, let us store both the mean length of the bonds and the total number of bonds in lists. .. code-block:: python lbond_vs_frame = [] nbond_vs_frame = [] for ts in u.trajectory: frame = ts.frame all_bonds_ts = [] # temporary list to store bond length for id1, id2 in cnt.atoms.bonds.indices: pos1 = u.atoms.positions[u.atoms.indices == id1] pos2 = u.atoms.positions[u.atoms.indices == id2] r = np.sqrt(np.sum((pos1-pos2)**2)) if r < 1.8: all_bonds_ts.append(r) mean_length_bonds = np.mean(all_bonds_ts) number_of_bond = len(all_bonds_ts)/2 # divide by 2 to avoid counting twice lbond_vs_frame.append([frame, mean_length_bonds]) nbond_vs_frame.append([frame, number_of_bond]) .. container:: justify The data can then be saved to files: .. code-block:: python np.savetxt("number_bond_vs_time.dat", nbond_vs_frame) np.savetxt("length_bond_vs_time.dat", lbond_vs_frame) .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/bond-dark.png :alt: plot of the bond length and distance versus time :class: only-dark .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/bond-light.png :alt: plot of the bond length and distance versus time :class: only-light .. container:: figurelegend Figure: Evolution of the average bond length (a) and bond number (b) as a function of time. Bond length distributions ------------------------- .. container:: justify Using a similar script, let us extract the bond length distribution at the beginning of the simulation (let us say the 20 first frame), as well as near the maximum deformation of the CNT: .. code-block:: python bond_length_distributions = [] for ts in u.trajectory: all_bonds_ts = [] for id1, id2 in cnt.atoms.bonds.indices: pos1 = u.atoms.positions[u.atoms.indices == id1] pos2 = u.atoms.positions[u.atoms.indices == id2] r = np.sqrt(np.sum((pos1-pos2)**2)) if r < 1.8: all_bonds_ts.append(r) if frame > 0: # ignore the first frame histo, r_val = np.histogram(all_bonds_ts, bins=50, range=(1.3, 1.65)) r_val = (r_val[1:]+r_val[:-1])/2 bond_length_distributions.append(np.vstack([r_val, histo])) .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/bond-distribution-dark.png :alt: plot of the bond distribution :class: only-dark .. figure:: ../figures/mdanalysis/mdanalysis-tutorial/bond-distribution-light.png :alt: plot of the bond distribution :class: only-light .. container:: figurelegend Figure: Distribution in bond length near the start of the simulation, as well as near the maximum deformation of the CNT.