Introductory MAZE Tutorial

These tutorials showcase the capabilities of the MAZE code, along with comments describing how to use the functions shown in the demos.

Verify Installation

Verify the installation by importing maze and making a simple empty Zeolite

import maze
maze.Zeolite()
Zeolite(symbols='', pbc=False)

Cif Fetching from the Database of Zeolite Structures

The database of zeolite structures is a useful resource for zeolite simulation experiments. It contains cif files for all synthesized zeolites, organized by their three letter zeolite code. Downloading them from the website is easy when working on a local machine, but challenging when working on a remote machine. To facilitate smoother workflows, a simple python function which downloads cif files from the database was created. An example of using this to download a few different cif files is shown below.

Note: Some users have had trouble using the cif_download function and the make method. The cause of this issue is typically latency issues with the IZA database’s website. If you encounter these issues you can download all of the CIF files in the database by going to this google drive link, downloading the zip folder and extracting the data folder. Place this data folder in your working directory. You can see your working directory by running the python command import os followed by print(os.getcwd()). After placing the data folder, which contains all of the CIF files from the IZA database, in your working directory, you will be able to run the make command without accessing the IZA database.

Note: The CIF reading capabilities of MAZE rely on those of ASE. Version 3.21.0 of ASE introduced a refactored CIF reader, which is not able to read all of the CIF files on the IZA database, thus most, but not all of the CIF files in the IZA database are readable with MAZE and the most recent version of ASE. This issue can be mitigated by using MAZE with any ASE version < 3.21.0, which is able to read and label the T-sites of all of the CIF files in the IZA database. This issuse might be fixed in future versions of ASE.

First, we import the MAZE package, the glob package, and the download_cif function from the maze.cif_download module.

>>> import maze
>>> from maze.cif_download import download_cif
>>> import glob

Next, we declare a helper function which prints out all of the directories in the current working directory. This will help us visualize the download_cif function’s behavior.

>>> def print_dirs():
...     print('dirs in cwd',  glob.glob('**/'))
...

We can view the directories names in our current directory using our helper function.

>>> print_dirs()
dirs in cwd []

Now, let’s download the GOO cif file, using the download_cif function. By default, the cif file is downloaded to the data directory; if this ‘data’ directory doesn’t exist, it is created.

>>> download_cif("GOO") # downloads "GOO.cif" to data/GOO.cif
>>> print_dirs()
dirs in cwd ['data/']
>>> print('files in data dir', glob.glob("data/*"))
files in data dir ['data/goo.cif']

We can download the cif file to a custom location by specifying the directory we want to use:

>>> download_cif("off", data_dir="my_other_data")
>>> print_dirs()
dirs in cwd ['my_other_data/', 'data/']
>>> print('files in my_other_data dir', glob.glob("my_other_data/*"))
files in my_other_data dir ['my_other_data/off.cif']

Making a Zeolite Using the Make Function

A cif file contains the crystallographic information that defines a zeolite structure. A downloaded cif from the iza-sc database of zeolite strucutres looks like this:

#**************************************************************************
#
# cif taken from the iza-sc database of zeolite structures
# ch. baerlocher and l.b. mccusker
# database of zeolite structures: http://www.iza-structure.org/databases/
#
# the atom coordinates and the cell parameters were optimized with dls76
# assuming a pure sio2 composition.
#
#**************************************************************************

_cell_length_a                  13.6750(0)
_cell_length_b                  13.6750(0)
_cell_length_c                  14.7670(0)
_cell_angle_alpha               90.0000(0)
_cell_angle_beta                90.0000(0)
_cell_angle_gamma              120.0000(0)

_symmetry_space_group_name_h-m     'r -3 m'
_symmetry_int_tables_number         166
_symmetry_cell_setting             trigonal

loop_
_symmetry_equiv_pos_as_xyz
'+x,+y,+z'
'2/3+x,1/3+y,1/3+z'
'1/3+x,2/3+y,2/3+z'
'-y,+x-y,+z'
... skipping all of this info for space
...

loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
    o1    o     0.9020    0.0980    0.1227
    o2    o     0.9767    0.3101    0.1667
    o3    o     0.1203    0.2405    0.1315
    o4    o     0.0000    0.2577    0.0000
    t1    si    0.9997    0.2264    0.1051

An important piece of information in each cif file is the _atom_site_label (01, 02, … t1, t2.. ect.) that is located in the first column near the atom position information. This information about the atoms identities is lost when the ase.io.read function is used to build an atoms object from a cif file. Because the identity of the T-sites is critical for zeolite simulation experiments, this issue inspired the creation of a custom constructor of the Zeolite object: make. This static method creates a Zeolite object, labels the unique atoms by tagging them, and then stores the mapping between the atom_site_label and the atom indices in the dictionaries site_to_atom_indices and atom_indices_to_site.

To demonstrate this feature, let us try building a Zeolite object from a cif file.

First, we import the MAZE package, the cif_download function and the Zeolite object

import maze
from maze.cif_download import download_cif

Next we import some ase packages to help us view the Zeolites we make.

import matplotlib.pyplot as plt
from ase.visualize.plot import plot_atoms

First we use the download_cif function to fetch the CHA.cif file from the IZA database.

download_cif('CHA', data_dir='data')

Now we can use the static make method to Zeolite with labeled atoms.

Our Zeolite object has been built. We can view it with the ase plot_atoms method (or view method). This works flawlessly because the Zeolite class is a subclass of the Atoms class.

plot_atoms(cha_zeolite)
../_images/output_17_1.png

The atom identity information is stored in two dictionaries. Let’s take a look at them:

print(cha_zeolite.site_to_atom_indices)
{'O1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], 'O2': [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35], 'O3': [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53], 'O4': [54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71], 'T1': [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107]}
print(cha_zeolite.atom_indices_to_sites)
{0: 'O1', 1: 'O1', 2: 'O1', 3: 'O1', 4: 'O1', 5: 'O1', 6: 'O1', 7: 'O1', 8: 'O1', 9: 'O1', 10: 'O1', 11: 'O1', 12: 'O1', 13: 'O1', 14: 'O1', 15: 'O1', 16: 'O1', 17: 'O1', 18: 'O2', 19: 'O2', 20: 'O2', 21: 'O2', 22: 'O2', 23: 'O2', 24: 'O2', 25: 'O2', 26: 'O2', 27: 'O2', 28: 'O2', 29: 'O2', 30: 'O2', 31: 'O2', 32: 'O2', 33: 'O2', 34: 'O2', 35: 'O2', 36: 'O3', 37: 'O3', 38: 'O3', 39: 'O3', 40: 'O3', 41: 'O3', 42: 'O3', 43: 'O3', 44: 'O3', 45: 'O3', 46: 'O3', 47: 'O3', 48: 'O3', 49: 'O3', 50: 'O3', 51: 'O3', 52: 'O3', 53: 'O3', 54: 'O4', 55: 'O4', 56: 'O4', 57: 'O4', 58: 'O4', 59: 'O4', 60: 'O4', 61: 'O4', 62: 'O4', 63: 'O4', 64: 'O4', 65: 'O4', 66: 'O4', 67: 'O4', 68: 'O4', 69: 'O4', 70: 'O4', 71: 'O4', 72: 'T1', 73: 'T1', 74: 'T1', 75: 'T1', 76: 'T1', 77: 'T1', 78: 'T1', 79: 'T1', 80: 'T1', 81: 'T1', 82: 'T1', 83: 'T1', 84: 'T1', 85: 'T1', 86: 'T1', 87: 'T1', 88: 'T1', 89: 'T1', 90: 'T1', 91: 'T1', 92: 'T1', 93: 'T1', 94: 'T1', 95: 'T1', 96: 'T1', 97: 'T1', 98: 'T1', 99: 'T1', 100: 'T1', 101: 'T1', 102: 'T1', 103: 'T1', 104: 'T1', 105: 'T1', 106: 'T1', 107: 'T1'}

Depending on the situation, one dictionary may be more useful than the other.

One Step Zeolite Construction

Downloading a cif file everytime you want to load a new Zeolite can be annoying. Thus, the make function automatically downloads the cif file from the IZA database if it cannot be located in the provided directory. It places the downloaded cif file in folder called data in the current working directory. Data paths can be specified with the data_dir optional argument.

from maze import Zeolite
bea_zeolite = Zeolite.make('BEA')  # Download the BEA cif file and build zeolite
plot_atoms(bea_zeolite)
../_images/output_23_1.png

Identifying Atom Types in a Zeolite Structure

The Zeotype class includes methods for identifying the different types of atoms in a zeolite structure. These methods will work on all Zeotype objects, even those where the atom_indices_to_site and site_to_atom_indices are not set.

Let’s import the Zeolite object and make a BEA structure and some tools to visualize the structures we create.

from maze import zeolite
import matplotlib.pyplot as plt
from ase.visualize.plot import plot_atoms
bea_zeolite = Zeolite.make('BEA')
plot_atoms(bea_zeolite)
../_images/output_30_1.png

To make things more interesting, let us replace the Si-T1 sites in the bea Zeolite with Aluminum.

for t1_index in bea_zeolite.site_to_atom_indices['T1']:
    bea_zeolite[t1_index].symbol = 'Al'
plot_atoms(bea_zeolite)
../_images/output_33_1.png

Making a replacement is that simple! Now we will use the count_elements method to get the count of each atom in the zeolite and the idenity of each atom.

atoms_indices, count = bea_zeolite.count_elements()
print(atoms_indices)
{'O': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127], 'Al': [128, 129, 130, 131, 132, 133, 134, 135], 'Si': [136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191]}
print(count)
{'O': 128, 'Al': 8, 'Si': 56}

Extracting, Adding and Capping Clusters

One of the most useful features of the MAZE package is the ability to add and remove atoms from a Zeolite object. To demonstrate this, we will extract a cluster from an Zeolite object, change some of the atoms, and then integrate it back into the main Zeolite.

First, we import the zeolite object and the plot_atoms function.

from maze import zeolite
import matplotlib.pyplot as plt
from ase.visualize.plot import plot_atoms

Then we make a bea_zeolite object

bea_zeolite = Zeolite.make('BEA')
plot_atoms(bea_zeolite)
../_images/output_44_1.png

The next step is to pick a T-site and then use one of the static methods in the Cluster class to select indices to build the cluster.

The atom 154 is right in the middle of the zeolite, which will make viewing the cluster creation easy. One could also use the site_to_atom_indices dictionary to select a specific T site.

The Zeolite object uses a ClusterMaker object to select out certain groups of atoms from the Zeolite.

By default a DefaultClusterMaker object is used. This DefaultClusterMaker object has a get_cluster_indices method which uses get_oh_cluster_indices function to selects the indices of central t atom, and surrounding oxygens and hydrogens. There are other cluster functions avalible in the source code of DefaultClusterMaker. If you need some different functionality, simply make your own ClusterMaker object and set the Zeolite.cluster_maker attribute to your custom cluster maker.

Let us call our bea_zeolite’s ClusterMaker object’s get_cluster_indices fun, to see what indices it will select.

site = 154
cluster_indices = bea_zeolite.cluster_maker.get_cluster_indices(bea_zeolite, site)
print(cluster_indices)
[2, 66, 74, 138, 77, 82, 146, 22, 154, 30, 38, 102, 186, 42, 174, 50, 114, 117, 118, 58, 126]

These are the indices of the atoms that will make up the resulting cluster and the indices of the atoms that will be absent in the open defect zeolite.

We can now make the cluster and open defefect zeolites by using the get_cluster method.

cluster, od = bea_zeolite.get_cluster(154)

The cluster looks like this

plot_atoms(cluster)
../_images/output_50_1.png

the open defect looks like this

plot_atoms(od)
../_images/output_52_1.png

Both the open defect and the cluster are Zeolite objects, yet they have a different ztype attribute

display(type(bea_zeolite))
display(type(od))
display(type(cluster))
maze.zeolite.Zeolite
maze.zeolite.Zeolite
maze.zeolite.Zeolite
display(bea_zeolite.ztype)
display(od.ztype)
display(cluster.ztype)
'Zeolite'
'Open Defect'
'Cluster'

Next we want to cap the cluster and changes some of the atoms its structure. Capping involves adding hydrogens and oxygens to the cluster. The built-in cap_atoms() method returns a new cluster object that has hydrogen caps added to it.

capped_cluster = cluster.cap_atoms()
plot_atoms(capped_cluster)
../_images/output_58_1.png

In a typical zeolite workflow, this cluster would be optimized. Configuring an optimizer can be tricky, and so for this tutorial we instead replace the oxygen atoms with aluminum atoms.

for atom in capped_cluster:
    if atom.symbol == 'O':
        capped_cluster[atom.index].symbol = 'Al'
plot_atoms(capped_cluster)
../_images/output_61_1.png

The next stage is removing the caps from the atom and reintegrating it back into the original zeolite.

To remove caps, we have to find the name of the caps in additions dictionary

dict(capped_cluster.additions)
{'h_caps': ['h_caps_34']}

or we can just select the last h_caps added using pythons list methods

additon_category = 'h_caps'
addition_name = capped_cluster.additions[additon_category][-1]
display(addition_name)
'h_caps_34'

Next we call the remove_addition method

uncapped_cluster = capped_cluster.remove_addition(addition_name, additon_category)
plot_atoms(uncapped_cluster)
../_images/output_68_1.png

The caps have been removed. We can now integrate the cluster back into the original zeolite.

bea_zeolite_with_al = bea_zeolite.integrate(uncapped_cluster)
plot_atoms(bea_zeolite_with_al)
../_images/output_71_1.png

The changes have been made. An important thing to notice is that none of the structural manipulation features of MAZE have side-effects. The bea_zeolite remains unchanged by this integration and a new bea_zeolite_with_al is created. Along with leading to cleaner code with fewew bugs, this style of programming also allows for method chanining.

This demo showed the power of the MAZE code to extract and add clusters to zeotypes. This is one of the most useful features in the MAZE code.