Visualizing Structures with PLAMS
*********************************

Technical Visualization Molecular Dynamics

Render molecules and periodic systems directly from Python to quickly
inspect structures in scripted workflows.

**Requires:** AMS2026 or later

-[ Related examples ]-

* PLUMED Biasing in AMS Molecular Dynamics

* Automated Transition-State Workflow for a Diels-Alder Addition

* Basic MD Trajectory Analysis with PLAMS

* Building Packed Systems with Packmol

-[ Related tutorials ]-

* JupyterLab with AMS

* Building molecules tutorial

-[ Related documentation ]-

* ChemicalSystem overview

* Trajectory analysis utilities

[image]


Visualizing Structures with PLAMS
=================================


Rendering Images
----------------

PLAMS molecules can be simply passed to the "view" function to render
an image using AMSview. These images can then easily be displayed in a
notebook.

For example, below we create a molecule of caffeine from its SMILES
string, and view it.

   import scm.plams as plams
   from scm.base import ChemicalSystem
   import matplotlib.pyplot as plt

   caffeine = ChemicalSystem.from_smiles("CN1C=NC2=C1C(=O)N(C(=O)N2C)C")
   plams.view(caffeine)

   [image: image generated from notebook][image]

The size of the image can be controlled via the "height" and "width"
arguments, which specify the size in pixels. An additional "padding"
argument can also be used to control how much space (in Angstrom) is
added or trimmed from the edges of the image.

To save the image file, the "picture_path" argument can be supplied
which will also persist the image at the given location. Furthermore,
the molecule can also be opened interactively in AMSview using the
option "open_window = True".

Examples of all these options are demonstrated below for beta-
carotene.

   beta_carotene = ChemicalSystem.from_smiles(
       "CC1CCC/C(C)=C1/C=C/C(C)=C/C=C/C(C)=C/C=C/C=C(C)/C=C/C=C(C)/C=C/C2=C(C)/CCCC2(C)C",
   )

   plams.view(
       beta_carotene,
       width=1000,
       height=400,
       padding=-7,
       picture_path="beta-carotene.png",
       open_window=True,
   )

   [image: image generated from notebook][image]


View Directions
---------------

The "view" method aims to make it simple to view your system in the
most informative way. As such, the "direction" argument can be
provided to change the view angle.

The "direction", is composed of multiple parts and can be used to
view:

* **along** an axis

* with a **small_tilt**, normal **tilt** or **large_tilt**

* down a **corner**

In addition, the axis can be a:

* cartesian axis - **x**, **y**, **z**

* lattice vector - **a**, **b**, **c**

* principal component axis - **pca1**, **pca2**, **pca3**

Example directions are therefore:

* "along_z", view along the cartesian "z" axis (default value)

* "tilt_a", view along the "c" lattice vector with a tilt

* "corner_pca2", view down the corner of a principal component

Illustrations of these are provided below. Further examples can be
found in the *Periodic Systems* section.

   images = {
       d: plams.view(caffeine, direction=d, width=400) for d in ["along_x", "along_y", "along_z"]
   }

   plams.plot_image_grid(images, rows=1);

   [image: image generated from notebook][image]

   butane = ChemicalSystem.from_smiles("CCCC")

   images = {
       d: plams.view(butane, direction=d, width=400)
       for d in ["along_pca1", "along_pca2", "along_pca3"]
   }

   plams.plot_image_grid(images, rows=1);

   [image: image generated from notebook][image]

   images = {
       d: plams.view(butane, direction=d, width=400)
       for d in ["corner_pca1", "corner_pca2", "corner_pca3"]
   }

   plams.plot_image_grid(images, rows=1);

   [image: image generated from notebook][image]

If you want finer control over the view angle, this is possible
through the "ViewConfig" (note that more generally, all settings can
be passed in via the "ViewConfig", and a sub-set of the most
frequently used settings can be set directly via arguments).

Here, the "normal" option can be provided, which specifies the normal
to the view plane. The "normal_basis" can also be set to "xyz"
(cartesian), "abc" (lattice) or "pca" (principal components), which
defines which basis the normal vector is in.

   plams.view(
       butane, config=plams.ViewConfig(normal=(1, 0, 1), normal_basis="pca", width=300, height=300)
   )

   [image: image generated from notebook][image]


Regions and Labels
------------------

Atomic regions can be displayed by using the option
"show_regions=True". The regions then appear as color-coded
translucent spheres on the molecule.

For example, with our caffeine molecule, we can define QM and MM
regions and highlight these in the image.

   methyl_atoms = []

   for i, atom in enumerate(caffeine.atoms):
       if atom.symbol != "C":
           continue
       neighbor_idxs = caffeine.bonds.get_bonded_atoms(i)
       hydrogen_neighbor_idxs = [x for x in neighbor_idxs if caffeine.atoms[x].symbol == "H"]
       if len(hydrogen_neighbor_idxs) == 3:
           methyl_atoms.append(i)
           methyl_atoms.extend(hydrogen_neighbor_idxs)

   non_methyl_atoms = list(set(range(len(caffeine))) - set(methyl_atoms))
   caffeine.set_atoms_in_region(methyl_atoms, "MM")
   caffeine.set_atoms_in_region(non_methyl_atoms, "QM")
   plams.view(caffeine, show_regions=True)

   [image: image generated from notebook][image]

In addition, labels can also be displayed on the atoms, to indicate
the elements or atom names.

These are toggled through the "show_atom_labels" and "atom_label_type"
arguments. Further customization options are also available in the
"ViewConfig" object, which can be passed to the "config" argument.

See the example below which also allows tuning of the atom label size
and color:

   plams.view(
       caffeine,
       config=plams.ViewConfig(
           show_atom_labels=True,
           atom_label_color="#32CD32",
           atom_label_size=1.7,
       ),
   )

   [image: image generated from notebook][image]


Periodic Systems
================

The "view" function works equally well for periodic systems as for
molecular systems. For these systems, additional options are available
for viewing the lattice and unit cell.

We set up various systems here as examples:

* 1D carbon nanotube

* 2D platinum surface

* 3D crystal

* 3D water box

First, we set up and view the carbon nanotube. By using
"show_unit_cell_edges=False" argument we disable the default behaviour
for displaying the unit cell.

   from scm.utils.builder.nanotube import nanotube
   import numpy as np

   my_nanotube = nanotube(6, 0, length=4)
   my_nanotube.guess_bonds()
   plams.view(my_nanotube, padding=-7, show_unit_cell_edges=False)

   [image: image generated from notebook][image]

Next, we create the platinum surface, and use the option
"show_lattice_vectors=True" to add the lattice vectors onto the view.

   import ase.build as build

   pt_surface_ase = build.fcc111("Pt", size=(4, 4, 3), vacuum=5.0, orthogonal=True, periodic=True)
   pt_surface = ChemicalSystem.from_ase_atoms(pt_surface_ase)

   plams.view(pt_surface, show_unit_cell_edges=False, show_lattice_vectors=True)

   [image: image generated from notebook][image]

Next we create a non-orthorhombic crystal structure - the wurtzite
structure of zinc sulfide.

This demonstrates the further "direction" options that are available
for periodic structures. In this case, we can also choose to view
relative to the lattice vectors ("a", "b", "c") as well as the
cartesian axes ("x", "y", "z").

For example, below we choose to view along each of the lattice vectors
in turn using "along_a", "along_b" and "along_c".

Note that in this case we also set "fixed_atom_size=False" to scale
the atoms according to their radii.

   wurtzite_unit_cell = build.bulk("ZnS", crystalstructure="wurtzite", a=3.81)
   wurtzite = ChemicalSystem.from_ase_atoms(wurtzite_unit_cell).make_supercell([2, 2, 2])
   wurtzite.map_atoms(0)

   images = {
       d: plams.view(
           wurtzite, fixed_atom_size=False, show_lattice_vectors=True, direction=d, width=400
       )
       for d in ["along_a", "along_b", "along_c"]
   }

   plams.plot_image_grid(images, rows=1);

   [image: image generated from notebook][image]

As a final example of a periodic structure, we generate an
orthorhombic box of water.

Here we can also choose to view in a variety of different
orientations, which emphasise the periodicity of the system. For this
we set "direction = tilt_x", to show the full unit cell.

In addition, we pass a "ViewConfig" object which exposes additional
settings to fine-tune the image.

   water_box = plams.packmol(
       plams.from_smiles("O"),
       density=1.0,
       box_bounds=[0.0, 0.0, 0.0, 8.0, 12.0, 14.0],
   )  # a PLAMS Molecule, not ChemicalSystem

   config = plams.ViewConfig(unit_cell_edge_thickness=0.1)
   plams.view(water_box, config=config, direction="tilt_x", show_lattice_vectors=True)

   [image: image generated from notebook][image]


Backends
--------

By default, the "view" method will try and use AMSview to generate
images. However, if AMSview is not available, another backend will be
used. A specific backend can also be explicitly selected.

The available options are:

* "amsview": uses the AMSview program

* "amsview_xvfb": for use on a headless machine (no GUI), when the
  Xvfb program is available

* "ase_plot": uses ASE and matplotlib

Efforts have been made to ensure the view options when using different
backends are as close as possible, and so they can simply be switched.
For example, see the following images reproduced using the ASE
backend:

   plams.view(
       caffeine,
       width=350,
       height=350,
       show_regions=True,
       backend="ase_plot",
       show_atom_labels=True,
   )

   [image: image generated from notebook][image]

   plams.view(
       water_box,
       width=350,
       height=350,
       config=config,
       direction="tilt_z",
       show_lattice_vectors=True,
       backend="ase_plot",
   )

   [image: image generated from notebook][image]


See also
========


Related examples
----------------

* PLUMED Biasing in AMS Molecular Dynamics

* Automated Transition-State Workflow for a Diels-Alder Addition

* Basic MD Trajectory Analysis with PLAMS

* Building Packed Systems with Packmol


Related tutorials
-----------------

* JupyterLab with AMS

* Building molecules tutorial


Related documentation
---------------------

* ChemicalSystem overview

* Trajectory analysis utilities


Python Script
=============

   #!/usr/bin/env python
   # coding: utf-8

   # ## Visualizing Structures with PLAMS

   # ### Rendering Images

   # PLAMS molecules can be simply passed to the `view` function to render an image using AMSview. These images can then easily be displayed in a notebook. 
   # 
   # For example, below we create a molecule of caffeine from its SMILES string, and view it.

   import scm.plams as plams
   from scm.base import ChemicalSystem
   import matplotlib.pyplot as plt

   caffeine = ChemicalSystem.from_smiles("CN1C=NC2=C1C(=O)N(C(=O)N2C)C")
   plams.view(caffeine, picture_path="picture1.png")


   # The size of the image can be controlled via the `height` and `width` arguments, which specify the size in pixels. An additional `padding` argument can also be used to control how much space (in Angstrom) is added or trimmed from the edges of the image.
   # 
   # To save the image file, the `picture_path` argument can be supplied which will also persist the image at the given location. Furthermore, the molecule can also be opened interactively in AMSview using the option `open_window = True`.
   # 
   # Examples of all these options are demonstrated below for beta-carotene.

   beta_carotene = ChemicalSystem.from_smiles(
       "CC1CCC/C(C)=C1/C=C/C(C)=C/C=C/C(C)=C/C=C/C=C(C)/C=C/C=C(C)/C=C/C2=C(C)/CCCC2(C)C",
   )

   plams.view(
       beta_carotene,
       width=1000,
       height=400,
       padding=-7,
       picture_path="beta-carotene.png",
       open_window=True,
   )


   # ### View Directions

   # The `view` method aims to make it simple to view your system in the most informative way. As such, the `direction` argument can be provided to change the view angle.
   # 
   # The `direction`, is composed of multiple parts and can be used to view:
   # 
   # - __along__ an axis
   # - with a __small_tilt__, normal __tilt__ or __large_tilt__
   # - down a __corner__
   # 
   # In addition, the axis can be a:
   # 
   # - cartesian axis - __x__, __y__, __z__
   # - lattice vector - __a__, __b__, __c__
   # - principal component axis - __pca1__, __pca2__, __pca3__
   # 
   # Example directions are therefore:
   # 
   # - `along_z`, view along the cartesian `z` axis (default value)
   # - `tilt_a`, view along the `c` lattice vector with a tilt
   # - `corner_pca2`, view down the corner of a principal component
   # 
   # Illustrations of these are provided below. Further examples can be found in the _Periodic Systems_ section.

   images = {
       d: plams.view(caffeine, direction=d, width=400) for d in ["along_x", "along_y", "along_z"]
   }

   plams.plot_image_grid(images, rows=1, save_path="picture2.png");


   butane = ChemicalSystem.from_smiles("CCCC")

   images = {
       d: plams.view(butane, direction=d, width=400)
       for d in ["along_pca1", "along_pca2", "along_pca3"]
   }

   plams.plot_image_grid(images, rows=1, save_path="picture3.png");


   images = {
       d: plams.view(butane, direction=d, width=400)
       for d in ["corner_pca1", "corner_pca2", "corner_pca3"]
   }

   plams.plot_image_grid(images, rows=1, save_path="picture4.png");


   # If you want finer control over the view angle, this is possible through the `ViewConfig` (note that more generally, all settings can be passed in via the `ViewConfig`, and a sub-set of the most frequently used settings can be set directly via arguments).
   # 
   # Here, the `normal` option can be provided, which specifies the normal to the view plane. The `normal_basis` can also be set to `xyz` (cartesian), `abc` (lattice) or `pca` (principal components), which defines which basis the normal vector is in.

   plams.view(
       butane, config=plams.ViewConfig(normal=(1, 0, 1), normal_basis="pca", width=300, height=300)
   , picture_path="picture5.png")


   # ### Regions and Labels

   # Atomic regions can be displayed by using the option `show_regions=True`. The regions then appear as color-coded translucent spheres on the molecule.
   # 
   # For example, with our caffeine molecule, we can define QM and MM regions and highlight these in the image.

   methyl_atoms = []

   for i, atom in enumerate(caffeine.atoms):
       if atom.symbol != "C":
           continue
       neighbor_idxs = caffeine.bonds.get_bonded_atoms(i) 
       hydrogen_neighbor_idxs = [x for x in neighbor_idxs if caffeine.atoms[x].symbol == "H"]
       if len(hydrogen_neighbor_idxs) == 3:
           methyl_atoms.append(i)
           methyl_atoms.extend(hydrogen_neighbor_idxs)

   non_methyl_atoms = list(set(range(len(caffeine))) - set(methyl_atoms))
   caffeine.set_atoms_in_region(methyl_atoms, "MM")
   caffeine.set_atoms_in_region(non_methyl_atoms, "QM")
   plams.view(caffeine, show_regions=True, picture_path="picture6.png")


   # In addition, labels can also be displayed on the atoms, to indicate the elements or atom names.
   # 
   # These are toggled through the `show_atom_labels` and `atom_label_type` arguments. Further customization options are also available in the `ViewConfig` object, which can be passed to the `config` argument. 
   # 
   # See the example below which also allows tuning of the atom label size and color:

   plams.view(
       caffeine,
       config=plams.ViewConfig(
           show_atom_labels=True,
           atom_label_color="#32CD32",
           atom_label_size=1.7,
       ),
    picture_path="picture7.png")


   # ## Periodic Systems

   # The `view` function works equally well for periodic systems as for molecular systems. For these systems, additional options are available for viewing the lattice and unit cell.
   # 
   # We set up various systems here as examples:
   # 
   # - 1D carbon nanotube
   # - 2D platinum surface
   # - 3D crystal
   # - 3D water box

   # First, we set up and view the carbon nanotube. By using `show_unit_cell_edges=False` argument we disable the default behaviour for displaying the unit cell.

   from scm.utils.builder.nanotube import nanotube
   import numpy as np

   my_nanotube = nanotube(6, 0, length=4)
   my_nanotube.guess_bonds()
   plams.view(my_nanotube, padding=-7, show_unit_cell_edges=False, picture_path="picture8.png")


   # Next, we create the platinum surface, and use the option `show_lattice_vectors=True` to add the lattice vectors onto the view.

   import ase.build as build

   pt_surface_ase = build.fcc111("Pt", size=(4, 4, 3), vacuum=5.0, orthogonal=True, periodic=True)
   pt_surface = ChemicalSystem.from_ase_atoms(pt_surface_ase)

   plams.view(pt_surface, show_unit_cell_edges=False, show_lattice_vectors=True, picture_path="picture9.png")


   # Next we create a non-orthorhombic crystal structure - the wurtzite structure of zinc sulfide.
   # 
   # This demonstrates the further `direction` options that are available for periodic structures. In this case, we can also choose to view relative to the lattice vectors (`a`, `b`, `c`) as well as the cartesian axes (`x`, `y`, `z`).
   # 
   # For example, below we choose to view along each of the lattice vectors in turn using `along_a`, `along_b` and `along_c`.
   # 
   # Note that in this case we also set `fixed_atom_size=False` to scale the atoms according to their radii.

   wurtzite_unit_cell = build.bulk("ZnS", crystalstructure="wurtzite", a=3.81)
   wurtzite = ChemicalSystem.from_ase_atoms(wurtzite_unit_cell).make_supercell([2, 2, 2])
   wurtzite.map_atoms(0)

   images = {
       d: plams.view(
           wurtzite, fixed_atom_size=False, show_lattice_vectors=True, direction=d, width=400
       )
       for d in ["along_a", "along_b", "along_c"]
   }

   plams.plot_image_grid(images, rows=1, save_path="picture10.png");


   # As a final example of a periodic structure, we generate an orthorhombic box of water.
   # 
   # Here we can also choose to view in a variety of different orientations, which emphasise the periodicity of the system. For this we set `direction = tilt_x`, to show the full unit cell. 
   # 
   # In addition, we pass a `ViewConfig` object which exposes additional settings to fine-tune the image.

   water_box = plams.packmol(
       plams.from_smiles("O"),
       density=1.0,
       box_bounds=[0.0, 0.0, 0.0, 8.0, 12.0, 14.0],
   )  # a PLAMS Molecule, not ChemicalSystem

   config = plams.ViewConfig(unit_cell_edge_thickness=0.1)
   plams.view(water_box, config=config, direction="tilt_x", show_lattice_vectors=True, picture_path="picture11.png")


   # ### Backends

   # By default, the `view` method will try and use AMSview to generate images. However, if AMSview is not available, another backend will be used. A specific backend can also be explicitly selected.
   # 
   # The available options are:
   # 
   # - `amsview`: uses the AMSview program
   # - `amsview_xvfb`: for use on a headless machine (no GUI), when the [Xvfb](https://www.x.org/archive/X11R7.7/doc/man/man1/Xvfb.1.xhtml) program is available
   # - `ase_plot`: uses ASE and matplotlib
   # 
   # Efforts have been made to ensure the view options when using different backends are as close as possible, and so they can simply be switched. For example, see the following images reproduced using the ASE backend:

   plams.view(
       caffeine,
       width=350,
       height=350,
       show_regions=True,
       backend="ase_plot",
       show_atom_labels=True,
    picture_path="picture12.png")


   plams.view(
       water_box,
       width=350,
       height=350,
       config=config,
       direction="tilt_z",
       show_lattice_vectors=True,
       backend="ase_plot",
    picture_path="picture13.png")

