Visualizing Crystal Structures with Pymatgen and ASE#

In this tutorial, we will learn how to visualize and explore crystal structures using two powerful Python-based tools:

  • Pymatgen (Python Materials Genomics)
    A widely used library in materials science that allows us to:

    • Read, write, and manipulate structure files.

    • Access data from the Materials Project directly.

    • Perform symmetry analysis, generate supercells, and more.

  • ASE (Atomic Simulation Environment)
    A library designed for:

    • Representing atomic structures in a simple and standardized way.

    • Interfacing with many DFT codes.

    • Providing interactive 3D visualization tools.

Tip

Pymatgen is excellent for analyzing structures, while ASE is great for visualizing them. Together, they make a powerful duo.

We will start from the .cif file we downloaded in the previous tutorial from Materials Project, load it into Python, inspect its properties, and visualize it interactively.

Note

This tutorial assumes you have already completed the previous step where we downloaded a .cif file from Materials Project. If not, please revisit the tutorial: Exploring Materials Project


Step 1 - Setting up and Opening the Structure#

Before working with crystal data, we need to use Python libraries that help us read and analize it. One such library is pymatgen. We will install it and then import it.

What does “importing a library” mean?#

In Python, a library is a collection of pre-written code that provides useful functions. Instead of writing everything from scratch, we import a library and reuse its tools.

For this tutorial, we will import the Structure class from pymatgen, which let us work directly with crystallographic structures

# Install required packages (run this cell first)
!pip install pymatgen ase
Requirement already satisfied: pymatgen in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (2025.6.14)
Requirement already satisfied: ase in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (3.26.0)
Requirement already satisfied: bibtexparser>=1.4.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (1.4.3)
Requirement already satisfied: joblib>=1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (1.5.2)
Requirement already satisfied: matplotlib>=3.8 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (3.10.6)
Requirement already satisfied: monty>=2025.1.9 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (2025.3.3)
Requirement already satisfied: networkx>=2.7 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (3.5)
Requirement already satisfied: numpy<3,>=1.25.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (2.3.2)
Requirement already satisfied: orjson<4,>=3.10 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (3.11.3)
Requirement already satisfied: palettable>=3.3.3 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (3.3.3)
Requirement already satisfied: pandas>=2 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (2.3.2)
Requirement already satisfied: plotly>=5.0.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (6.3.0)
Requirement already satisfied: requests>=2.32 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (2.32.5)
Requirement already satisfied: ruamel.yaml>=0.17.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (0.18.15)
Requirement already satisfied: scipy>=1.13.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (1.16.1)
Requirement already satisfied: spglib>=2.5 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (2.6.0)
Requirement already satisfied: sympy>=1.3 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (1.14.0)
Requirement already satisfied: tabulate>=0.9 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (0.9.0)
Requirement already satisfied: tqdm>=4.60 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (4.67.1)
Requirement already satisfied: uncertainties>=3.1.4 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pymatgen) (3.2.3)
Requirement already satisfied: pyparsing>=2.0.3 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from bibtexparser>=1.4.0->pymatgen) (3.2.3)
Requirement already satisfied: contourpy>=1.0.1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (4.59.2)
Requirement already satisfied: kiwisolver>=1.3.1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (1.4.9)
Requirement already satisfied: packaging>=20.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (25.0)
Requirement already satisfied: pillow>=8 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (11.3.0)
Requirement already satisfied: python-dateutil>=2.7 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from matplotlib>=3.8->pymatgen) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pandas>=2->pymatgen) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from pandas>=2->pymatgen) (2025.2)
Requirement already satisfied: narwhals>=1.15.1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from plotly>=5.0.0->pymatgen) (2.3.0)
Requirement already satisfied: six>=1.5 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from python-dateutil>=2.7->matplotlib>=3.8->pymatgen) (1.17.0)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from requests>=2.32->pymatgen) (3.4.3)
Requirement already satisfied: idna<4,>=2.5 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from requests>=2.32->pymatgen) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from requests>=2.32->pymatgen) (2.5.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from requests>=2.32->pymatgen) (2025.8.3)
Requirement already satisfied: ruamel.yaml.clib>=0.2.7 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from ruamel.yaml>=0.17.0->pymatgen) (0.2.12)
Requirement already satisfied: typing-extensions>=4.9.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from spglib>=2.5->pymatgen) (4.15.0)
Requirement already satisfied: mpmath<1.4,>=1.1.0 in /home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages (from sympy>=1.3->pymatgen) (1.3.0)
from pymatgen.core import Structure

Now we can use the Structure class to open our .cif file.

Loading a .cif File#

A .cif file (Crystallographic Information File) contains:

  • The shape and size of the unit cell (lattice parameters)

  • The atomic positions inside the crystal

  • Symmetry information and space group

Let us load the .cif file into Python:

Note

In Python, you can add comments to your code. That is, text that will not affect the functioning of the program but is useful to leave information about what it does. You will recognize a comment line because it always starts with #

Explanation:

  • We use Structure.from_file() from Pymatgen to read the .cif file.

  • The structure object (notice that this time is without capital S) now contains all this information in a format readable for Python.

# Replace this filename with your downloaded CIF file if different
structure = Structure.from_file("CaTiO3.cif")

# Display basic structure info
structure
Structure Summary
Lattice
    abc : 5.37204886 5.46279649 7.63635962
 angles : 90.0 90.0 90.0
 volume : 224.09973769300066
      A : np.float64(5.37204886) np.float64(0.0) np.float64(3.2894312206310936e-16)
      B : np.float64(8.784846766143028e-16) np.float64(5.46279649) np.float64(3.344998117935948e-16)
      C : np.float64(0.0) np.float64(0.0) np.float64(7.63635962)
    pbc : True True True
PeriodicSite: Ca0 (Ca2+) (2.735, 2.963, 5.727) [0.5091, 0.5424, 0.75]
PeriodicSite: Ca1 (Ca2+) (2.637, 2.5, 1.909) [0.4909, 0.4576, 0.25]
PeriodicSite: Ca2 (Ca2+) (5.323, 0.2316, 5.727) [0.9909, 0.0424, 0.75]
PeriodicSite: Ca3 (Ca2+) (0.04871, 5.231, 1.909) [0.009067, 0.9576, 0.25]
PeriodicSite: Ti4 (Ti4+) (4.392e-16, 2.731, 3.818) [0.0, 0.5, 0.5]
PeriodicSite: Ti5 (Ti4+) (4.392e-16, 2.731, 1.672e-16) [0.0, 0.5, 0.0]
PeriodicSite: Ti6 (Ti4+) (2.686, 0.0, 3.818) [0.5, 0.0, 0.5]
PeriodicSite: Ti7 (Ti4+) (2.686, 0.0, 1.645e-16) [0.5, 0.0, 0.0]
PeriodicSite: O8 (O2-) (0.4125, 2.629, 5.727) [0.07678, 0.4813, 0.75]
PeriodicSite: O9 (O2-) (4.96, 2.833, 1.909) [0.9232, 0.5187, 0.25]
PeriodicSite: O10 (O2-) (2.274, 5.361, 5.727) [0.4232, 0.9813, 0.75]
PeriodicSite: O11 (O2-) (3.098, 0.102, 1.909) [0.5768, 0.01867, 0.25]
PeriodicSite: O12 (O2-) (4.245, 4.313, 7.33) [0.7903, 0.7896, 0.9598]
PeriodicSite: O13 (O2-) (1.127, 1.149, 3.512) [0.2097, 0.2104, 0.4598]
PeriodicSite: O14 (O2-) (1.127, 1.149, 0.3067) [0.2097, 0.2104, 0.04016]
PeriodicSite: O15 (O2-) (4.245, 4.313, 4.125) [0.7903, 0.7896, 0.5402]
PeriodicSite: O16 (O2-) (3.813, 1.582, 7.33) [0.7097, 0.2896, 0.9598]
PeriodicSite: O17 (O2-) (1.559, 3.881, 3.512) [0.2903, 0.7104, 0.4598]
PeriodicSite: O18 (O2-) (1.559, 3.881, 0.3067) [0.2903, 0.7104, 0.04016]
PeriodicSite: O19 (O2-) (3.813, 1.582, 4.125) [0.7097, 0.2896, 0.5402]

Tip

Think of this step as opening a 3D model of your material in Python.
We’re now ready to explore its properties.


Step 2 - Exploring Structure Properites#

Once we have the structure loaded, we can ask Python to give us details about it. Let us check the formula, lattice parameters, symmetry, and atomic positions.

# Chemical formula
print("Formula:", structure.formula)

# Lattice parameters
print("Lattice parameters (a, b, c):", structure.lattice)
print("Lattice angles (α, β, γ):", structure.lattice.angles)

print("Volume:", structure.volume)
print("Density:", structure.density)
print("Space group:", structure.get_space_group_info())

# Number of atoms in the unit cell
print("Number of atomic sites:", len(structure.sites))

print("\nFractional atomic coordinates:")
structure.frac_coords
Formula: Ca4 Ti4 O12
Lattice parameters (a, b, c): 5.372049 0.000000 0.000000
0.000000 5.462796 0.000000
0.000000 0.000000 7.636360
Lattice angles (α, β, γ): (90.0, 90.0, 90.0)
Volume: 224.09973769300066
Density: 4.029259419540245 g cm^-3
Space group: ('Pnma', 62)
Number of atomic sites: 20

Fractional atomic coordinates:
array([[0.50906691, 0.54239755, 0.75      ],
       [0.49093309, 0.45760245, 0.25      ],
       [0.99093309, 0.04239755, 0.75      ],
       [0.00906691, 0.95760245, 0.25      ],
       [0.        , 0.5       , 0.5       ],
       [0.        , 0.5       , 0.        ],
       [0.5       , 0.        , 0.5       ],
       [0.5       , 0.        , 0.        ],
       [0.07678111, 0.48132762, 0.75      ],
       [0.92321889, 0.51867238, 0.25      ],
       [0.42321889, 0.98132762, 0.75      ],
       [0.57678111, 0.01867238, 0.25      ],
       [0.79029372, 0.78961378, 0.95984128],
       [0.20970628, 0.21038622, 0.45984128],
       [0.20970628, 0.21038622, 0.04015872],
       [0.79029372, 0.78961378, 0.54015872],
       [0.70970628, 0.28961378, 0.95984128],
       [0.29029372, 0.71038622, 0.45984128],
       [0.29029372, 0.71038622, 0.04015872],
       [0.70970628, 0.28961378, 0.54015872]])

Note

These properties are essential in materials science because they define how atoms are arranged in the material, which directly affects its physical and chemical properties.


Step 3 - Visualizing the Structure#

Seeing numbers is useful, but visualizing the structure makes it easier to understand.

Interactive Visualization with ASE#

ASE allows us to create an interactive 3D visualization of the structure directly inside the Jupyter notebook.

Explanation:

  • ASE’s read() function loads the .cif file into an ASE atoms object.

  • view() opens an interactive 3D viewer right inside the notebook when using viewer='x3d'.

  • You can rotate, zoom, and inspect the structure intuitively.

from ase.io import read
from ase.visualize import view

# Read the structure using ASE directly from the CIF file
ase_structure = read("CaTiO3.cif")

# Visualize using the built-in x3d viewer
view(ase_structure, viewer="x3d")
ASE atomic visualization

Step 4 - Generating a Supercell#

let’s take a moment to understand what we’re working with:

  • Crystal Lattice A crystalline material is made of repeating units called unit cells. These are defined by lattice vectors and angles.

  • Atomic Basis Inside each unit cell, we have a set of atoms at specific fractional coordinates.

  • Periodic Boundary Conditions In simulations, we assume the crystal repeats infinitely in space. When we visualize a .cif file, we typically only see the unit cell, which may look small.

However, sometimes we want to expand the structure to better see the periodicity, symmetry, and connectivity between atoms. That’s where supercells come in.

A supercell is a larger periodic representation of the crystal. Instead of visualizing just one unit cell, we replicate it along the lattice vectors.

For example, to generate a 2×2×2 supercell:

# Create a 2x2x2 supercell using Pymatgen
supercell = structure * (2, 2, 2)

# Save it as a CIF file
supercell.to(fmt="cif", filename="CaTiO3_supercell.cif")

# Load into ASE and visualize
ase_supercell = read("CaTiO3_supercell.cif")
view(ase_supercell, viewer="x3d")
/home/paladin/anaconda3/envs/matdata/lib/python3.11/site-packages/pymatgen/core/structure.py:2949: UserWarning: Site labels are not unique, which is not compliant with the CIF spec (https://www.iucr.org/__data/iucr/cifdic_html/1/cif_core.dic/Iatom_site_label.html):`['Ca0', 'Ca0', 'Ca0', 'Ca0', 'Ca0', 'Ca0', 'Ca0', 'Ca0', 'Ca1', 'Ca1', 'Ca1', 'Ca1', 'Ca1', 'Ca1', 'Ca1', 'Ca1', 'Ca2', 'Ca2', 'Ca2', 'Ca2', 'Ca2', 'Ca2', 'Ca2', 'Ca2', 'Ca3', 'Ca3', 'Ca3', 'Ca3', 'Ca3', 'Ca3', 'Ca3', 'Ca3', 'Ti4', 'Ti4', 'Ti4', 'Ti4', 'Ti4', 'Ti4', 'Ti4', 'Ti4', 'Ti5', 'Ti5', 'Ti5', 'Ti5', 'Ti5', 'Ti5', 'Ti5', 'Ti5', 'Ti6', 'Ti6', 'Ti6', 'Ti6', 'Ti6', 'Ti6', 'Ti6', 'Ti6', 'Ti7', 'Ti7', 'Ti7', 'Ti7', 'Ti7', 'Ti7', 'Ti7', 'Ti7', 'O8', 'O8', 'O8', 'O8', 'O8', 'O8', 'O8', 'O8', 'O9', 'O9', 'O9', 'O9', 'O9', 'O9', 'O9', 'O9', 'O10', 'O10', 'O10', 'O10', 'O10', 'O10', 'O10', 'O10', 'O11', 'O11', 'O11', 'O11', 'O11', 'O11', 'O11', 'O11', 'O12', 'O12', 'O12', 'O12', 'O12', 'O12', 'O12', 'O12', 'O13', 'O13', 'O13', 'O13', 'O13', 'O13', 'O13', 'O13', 'O14', 'O14', 'O14', 'O14', 'O14', 'O14', 'O14', 'O14', 'O15', 'O15', 'O15', 'O15', 'O15', 'O15', 'O15', 'O15', 'O16', 'O16', 'O16', 'O16', 'O16', 'O16', 'O16', 'O16', 'O17', 'O17', 'O17', 'O17', 'O17', 'O17', 'O17', 'O17', 'O18', 'O18', 'O18', 'O18', 'O18', 'O18', 'O18', 'O18', 'O19', 'O19', 'O19', 'O19', 'O19', 'O19', 'O19', 'O19']`.
  writer: Any = CifWriter(self, **kwargs)
ASE atomic visualization

Why Are Supercells Useful?#

Supercells are not just for nicer pictures — they are essential in computational materials science:

  • Defect Modeling Introducing vacancies, dopants, or interstitial atoms requires a larger cell to avoid artificial interactions between defects.

  • Phonon Calculations Vibrational properties often require sampling multiple periodic images.

  • Surface and Interface Studies Modeling surfaces needs enough periodicity to simulate slabs.

By visualizing both the unit cell and supercell, you gain a better understanding of the 3D periodic nature of crystalline materials.


Step 6 - Summary#

In this tutorial, we learned how to:

  • Load .cif files with Pymatgen

  • Visualize structures interactively using ASE

  • Understand the concept of unit cells and periodicity

  • Generate and visualize supercells

  • Recognize why supercells are important for real simulations

This workflow bridges structure visualization with practical simulation needs.

Next steps#

Next, we will learn how to query material data directly from the Materials Project API using Pymatgen — no manual downloads needed!