Materials Informatics Series 01 / 23
Python for Materials Science
Data Extraction
from Materials
Databases
APIs, Python libraries, and workflows for querying the world's largest materials databases – from the Materials Project to NOMAD.
Written for anyone newer to materials informatics: no assumed coding background. Every technical term is explained the first time it appears.
ToolsPython 3.10+
Librariespymatgen · matminer · mp-api
Materials Informatics Series 02 / 23

Why Use Databases?

Option A
Lab Synthesis
Weeks → months
Option B
DFT Calculation
Hours per material
Option C
Database Query
Milliseconds
Scale
Millions of precomputed entries – no lab, no HPC cluster needed to start research.
Reproducibility
Shared datasets with documented computational settings enable fair comparisons across studies.
ML-Ready Data
Structured, labelled entries – band gaps, formation energies, crystal structures – ready to featurize and train models on.
Materials Informatics Series 03 / 23

The Database Landscape

Materials Project
~154,000 materials

DFT with VASP. Band gaps, formation energies, elastic properties, phonons. Most widely used in ML-materials research.

AFLOW
~3.5 million compounds

Largest computational database. High-throughput DFT. Strong for alloys, intermetallics, and electronic structure.

OQMD
~1 million entries

Open Quantum Materials Database. Focus on formation enthalpies and thermodynamic stability. Built with VASP + PBE.

COD
~500,000 crystal structures

Crystallography Open Database. Experimental X-ray and neutron diffraction structures. CIF format. Open access.

NOMAD
~18 million calculations

Raw DFT calculation files from diverse codes (VASP, Quantum ESPRESSO, FHI-aims). Largest archive of calculation outputs.

ICSD
~280,000 structures

Inorganic Crystal Structure Database. Commercial but widely licensed. Most comprehensive experimental crystal structure collection.

Materials Informatics Series 04 / 23

VASP and the DFT Codes Behind These Databases

Every database on the previous slide is a pile of DFT calculations. Before we touch Python, it helps to know what actually produced those numbers: which code ran the calculation, and which settings it used. This is also what the "Reproducibility" point on the last slide was really about.

VASP
Vienna Ab initio Simulation Package

The most-used DFT code in materials databases. Paid licence (per institution). Plane-wave basis set with PAW pseudopotentials. Materials Project, AFLOW, and OQMD are all built on VASP.

Quantum ESPRESSO
Free, open-source alternative

Same plane-wave pseudopotential approach as VASP, but free to use. Popular in universities without a VASP licence. One of the 40+ codes NOMAD archives raw output from.

Other DFT codes
FHI-aims, ABINIT, CASTEP, CP2K

Each uses a different basis set (plane-wave vs. localised atomic orbitals). Results from different codes are not automatically comparable, even for the same material.

Pseudopotential
Speeds up every calculation

Core electrons barely affect chemistry but are expensive to simulate. A pseudopotential replaces them with a smooth effective field, leaving only the valence electrons to solve for explicitly.

Functional: GGA / PBE
Approximates exchange-correlation

DFT needs an approximation for electron-electron interaction. PBE (a GGA functional) is the near-universal default. It systematically underestimates band gaps by roughly 30 to 50 percent versus experiment.

+U correction & k-points
The fine-tuning knobs

+U corrects transition-metal oxides. k-point density and energy cutoff control the accuracy/speed trade-off.

Why this matters: Materials Project, AFLOW, and OQMD all mostly use VASP, but each fixes its own pseudopotential version, its own +U values, and its own k-point/cutoff convention. Same code, different settings, so the same material can genuinely get a different formation energy or band gap in each database. That is exactly why mixing data across databases needs careful alignment, not just a bigger download.
Materials Informatics Series 05 / 23

Where Do You Actually Write the Code?

Notebook Google Colab, Jupyter / JupyterLab

Code split into cells you run one at a time, with output shown directly below each cell. Built for exploring data step by step. Saved as .ipynb files.

Code Editor VS Code and others

One continuous file that runs top to bottom in a single pass, with no cells and no inline output. Built for larger projects and automation. Saved as .py files.

Google Colab
Free cloud notebook

Runs in your browser. Google provides the computer. Free GPU included. No install. Best starting point for this course.

Jupyter / JupyterLab
Local notebook server

Same cell format as Colab but runs on your own machine. Files stay local. JupyterLab is the modern version with a cleaner interface and file browser sidebar.

VS Code
Full IDE + notebooks

Open .ipynb notebooks or write .py scripts. Best for larger projects with multiple files.

GitHub
Code storage + version control

Not a place to run code. Google Drive for code. Tracks every change. All course notebooks live here so you can copy and run them in Colab with one command.

Kaggle
Competitions + free GPU notebooks

Free notebook environment with public datasets and ML competitions. Great for practising after this course on real materials and chemistry problems.

Hugging Face
AI model hub + Spaces

Download pre-trained materials AI models in one line. Spaces hosts runnable web apps so you can test models without writing any code at all.

Materials Informatics Series 06 / 23

CPU, GPU, TPU: Which Processor Do You Need?

CPU Central Processing Unit

The brain of your computer. Handles a few complex tasks at a time very quickly. Every laptop has one. Perfect for classical ML (Random Forest, ElasticNet, linear models) on tabular materials data. Also the standard for DFT calculations: VASP and Quantum ESPRESSO are CPU-optimised.

GPU Graphics Processing Unit

Handles thousands of tiny calculations simultaneously. Originally built for video game graphics: now the engine of modern AI. Essential for training GNNs on crystal structures, ML potentials (MACE, CHGNet), and any deep learning on materials. Free GPUs available on Colab and Kaggle.

TPU Tensor Processing Unit

Google's custom chip built specifically for matrix operations in neural networks. Faster than GPU for very large models (billions of parameters). Rarely used directly in materials informatics today: most materials models are small enough that a GPU is sufficient. Available free on Colab.

Materials Informatics: Which to Use
1
Tabular ML (ElasticNet, RF, XGBoost)
CPU is fine. Your laptop is enough. These models train in seconds on thousands of materials.
2
Graph Neural Networks (CGCNN, MEGNet)
Need GPU. Treat crystal structures as graphs. Use free Colab GPU to train.
3
ML Potentials (MACE, CHGNet, M3GNet)
Need GPU for training. Once trained, run MD simulations at DFT quality but 10⁲x faster.
4
DFT (VASP, Quantum ESPRESSO)
CPU-based, many cores. Runs on university HPC clusters, not your laptop.
Start here: free GPU in 30 seconds
Open Google Colab, go to Runtime → Change runtime type → Select GPU (T4). Free. No credit card. Gives you an NVIDIA GPU for up to 12 hours per session.
Materials Informatics Series 07 / 23

Setting Up a Python Environment

A virtual environment is an isolated Python sandbox per project. Libraries installed in one project never conflict with another. Always use one.

Terminal (venv : built into Python)
# Step 1: create the environment
python -m venv matenv

# Step 2: activate it
# Windows:
matenv\Scriptsctivate
# Mac / Linux:
source matenv/bin/activate

# Step 3: install your packages
pip install mp-api pymatgen matminer

# Step 4: deactivate when done
deactivate
Terminal (conda : recommended for science)
# Create environment with a specific Python version
conda create -n matenv python=3.11

# Activate
conda activate matenv

# Install (conda first, then pip for extras)
conda install numpy pandas scikit-learn
pip install mp-api pymatgen matminer

# List all environments
conda env list
VS Code : step by step
1
Open the Command Palette
Press Ctrl+Shift+P (Windows) or Cmd+Shift+P (Mac)
2
Create virtual environment
Type Python: Create Environment, press Enter, choose Venv or Conda
3
Select the interpreter
VS Code detects the new env. Click the Python version in the bottom-left status bar and select your matenv environment.
4
Open the terminal and install
Terminal > New Terminal : the env is auto-activated. Run pip install normally.
5
For notebooks inside VS Code
Open a .ipynb file, click the kernel selector (top right), choose your matenv kernel.
venv vs conda : which one to use?
venv
Built into Python : no extra install needed.
Manages Python packages only. Does not touch system-level C libraries.
Lighter and faster to create. Good for simple scripts, web APIs, or when you are on a locked-down machine.
Can struggle with pymatgen or matminer on Windows because they need compiled C/Fortran extensions that pip cannot always build.
conda
Requires Anaconda or Miniconda installed first.
Manages Python packages AND non-Python dependencies: compiled C libraries, CUDA drivers, BLAS, HDF5.
Pre-built binaries mean numpy, pymatgen, and matminer install without needing a C compiler.
Recommended for materials informatics. Fewer headaches on Windows, Mac (Apple Silicon), and HPC clusters.
Rule of thumb: use conda when working with pymatgen, matminer, or anything that involves compiled science libraries. Use venv for lightweight pure-Python projects. You can always use pip inside a conda environment for packages not in the conda channel.
Materials Informatics Series 08 / 23

Installing Libraries with pip

What is pip?
Python's package manager – like an app store. You type the name of a library, pip finds it on the internet, downloads it, and sets it up so Python can use it. You only need to install each library once per environment.

ENV
Virtual Environments
Best practice: create a separate isolated Python environment per project so library versions never conflict across projects.
TIP
Install once, import anywhere
After installing, every Python file in that environment can use import library_name with no extra steps.

Terminal (Windows / Mac / Linux)

pip install mp-api pymatgen matminer aflow optimade-client requests pandas numpy

Google Colab cell

# ! = system command, not Python code
!pip install mp-api pymatgen matminer aflow optimade-client requests pandas numpy

Jupyter Notebook cell

!pip install mp-api pymatgen matminer aflow requests pandas numpy
# Safer: installs into the exact Python Jupyter is using:
import sys; !{sys.executable} -m pip install mp-api pymatgen

VS Code

# In the built-in terminal (Terminal → New Terminal):
pip install mp-api pymatgen matminer aflow requests pandas numpy
# Or use !pip inside a .ipynb notebook cell
Useful pip flags & commands
pip install --upgrade numpy
Update to latest version
Use when a library releases a bug fix or new feature you need. Without this flag, pip skips the download if any version is already installed.
pip install -r requirements.txt
Install from a file
requirements.txt lists every library and version your project needs, one per line. Running this one command sets up a fresh environment identically on any machine.
pip install --no-cache-dir pymatgen
Force fresh download
pip normally caches downloaded packages locally to speed up re-installs. Use this when a cached copy is corrupted or you suspect you are getting a stale version.
pip install -q matminer
Quiet: suppress output
Hides the progress bars and verbose logs. Useful in Colab notebooks where install output is long and clutters your notebook. Errors still show even with -q.
pip install -e .
Editable install
Links the package to your source folder directly. Every code change you save is reflected immediately without reinstalling. Used when you are developing your own materials analysis package.
pip show numpy
Inspect a package
Prints the installed version, location on disk, author, and dependencies. Run this first when debugging an import error to confirm the library is actually installed.
pip list
List all installed packages
Shows every library installed in the current environment with its version. Good for a quick sanity check that your environment has what you expect before running a notebook.
pip freeze > requirements.txt
Export exact versions
Writes every installed package with its exact pinned version into a file. Share this file with collaborators or add it to your GitHub repo so anyone can recreate your environment exactly.
Materials Informatics Series 09 / 23

Python Library Stack

Six words you will hear constantly in this lecture, explained once, in plain terms.

Client
The librarian

A database is a library: millions of records on shelves you cannot browse yourself. A client is the librarian software that sits between you and it. You ask in simple Python; it handles the technical back-and-forth. mp-api is the librarian for the Materials Project.

requests
The window and the note

Not every database has a librarian. Some just have a window in the wall: you slide a note through with your question, and a note comes back. requests is how Python writes and passes that note. Used for OQMD, COD, and NOMAD.

Structure
The floor plan

A crystal structure records exactly where every atom sits: positions, distances, angles, the repeating unit cell. Like a building's floor plan, but for atoms. It is the single most important thing a database hands back.

Object
The phone, not just the contacts

An object bundles data with the tools to use it, like a phone holding your contacts and also knowing how to call them. A Structure object holds atom positions and already knows how to compute density, volume, and symmetry.

File formats
The hospital form

An agreed way of writing structure data so any program can read it the same way, like every hospital filling in a patient form identically. CIF is that standard for crystal structures.

Environment
The workshop

Not the natural environment: a collection of tools that work together for one purpose, like a carpenter's workshop. ASE is a workshop for scientists who simulate atoms. Installing libraries is building your own.

These four sound alike. Here is how to tell them apart.

People new to this mix these four up constantly, because they all show up in the same sentence. The trick is to ask one question about each: what is it actually describing?

Structure is one kind of Object , saved to disk using a File format , and all of this happens inside an Environment

Structure answers "what material is this?": which atoms, where they sit. It is the real-world thing you are describing, not a piece of code. Object answers a completely different question, "how is this packaged in Python?": it is a general programming idea that has nothing to do with atoms specifically. A Structure happens to be one example of an object; so is a DataFrame, so is a trained model. File format answers "how is this written down and shared?": the exact same structure can be saved as a CIF, a POSCAR, or an XYZ file, the way the same recipe can be written in English or French. The format is not the thing itself, only one way of recording it. Environment answers a question that has nothing to do with atoms at all, "where am I typing this code?": Colab, VS Code, your own laptop. The same Structure, saved in the same CIF file, behaves identically no matter which environment opens it.

Analysis the finish line
pandas numpy scikit-learn matplotlib

Takes the numbers from the Features layer and does the actual work with them: builds tables, trains a prediction model, and draws the charts you see in results.

↑ consumes featurized data: rows of numbers, one row per material
Features the translator
matminer mendeleev

A machine learning model cannot read a crystal structure. This layer converts one into a list of plain numbers, describing it the way you might describe a house by its rooms, area, and age.

↑ receives structure objects: full atom-by-atom crystal data
Structures the floor plan
pymatgen ASE crystals

Turns the raw data a database sends back into a proper crystal structure object: exactly where every atom sits, ready for Python to work with.

↑ parses data from databases: raw records straight off the internet
Databases the starting point
mp-api aflow requests optimade-client

Every pipeline starts here: these tools fetch the raw data, band gaps, formation energies, atom positions, from a materials database over the internet and bring it into your Python code.

Read the stack from the bottom up: raw data comes in at the Databases layer, and each layer above turns it into something more useful, until Analysis produces an actual answer. Install everything at once:

pip install mp-api pymatgen matminer aflow requests pandas numpy
Materials Informatics Series 10 / 23

Essential Python Libraries for Materials Informatics

NumPy
Numerical arrays + linear algebra

Foundation of all scientific Python; every other library on this slide is built on it. Inside it: the ndarray type (a fixed-size, same-dtype grid of numbers), numpy.linalg for matrix operations, and numpy.random for sampling. In materials work it stores lattice matrices, atom position vectors, and arrays of property values.

# NumPy is the library; "np" is just a short nickname for it,
# so you don't have to type "numpy" every single time
import numpy as np

# store the three numbers 1.0, 2.0, 3.0 as one NumPy array,
# so maths on all three at once is fast
a = np.array([1.0, 2.0, 3.0])

# build a 3x3 grid of zeros: the same shape as a crystal's lattice matrix
m = np.zeros((3, 3))

# the average of the three numbers stored in "a"
np.mean(a)

# how spread out those numbers are, on average, from that mean
np.std(a)

# multiplies the grid "m" by the list "a": this is how you rotate
# or transform a set of atom coordinates using a lattice matrix
np.dot(m, a)
Pandas
Tabular data / DataFrames

Load, filter, merge, and export materials datasets: each row is one material, each column is one property. Inside it: the DataFrame (a full table) and Series (a single labelled column) classes, plus readers like read_csv, read_json, and read_excel.

# pandas is the table library; "pd" is its usual nickname
import pandas as pd

# open a spreadsheet-style CSV file and load it as a table (DataFrame)
df = pd.read_csv("band_gaps.csv")

# keep only the rows where the band_gap column is bigger than 1.0
df[df["band_gap"] > 1.0]

# show only the "formula" and "band_gap" columns, hide the rest
df[["formula", "band_gap"]]

# a quick summary: how many rows, their average, spread, min and max
df.describe()

# save this table back to disk as a new CSV file
df.to_csv("filtered.csv")
scikit-learn
Classical ML toolkit

Random Forest, ElasticNet, cross-validation, scaling: every model follows the same import, fit, predict pattern. Inside it: sklearn.ensemble (tree-based models), sklearn.linear_model (linear/regularised models), sklearn.model_selection (splitting and cross-validation), and sklearn.preprocessing (scaling and encoding).

# RandomForestRegressor is one specific model; scikit-learn has many
from sklearn.ensemble import RandomForestRegressor

# a helper that automatically trains and tests a model several times
from sklearn.model_selection import cross_val_score

# rescales your features so they are all on a similar numeric range,
# which many models need in order to learn well
from sklearn.preprocessing import StandardScaler

# create the model: 100 decision trees, not trained yet
model = RandomForestRegressor(n_estimators=100)

# show the model your training examples, so it can learn the pattern
model.fit(X_train, y_train)

# ask the trained model to guess values for materials it has not seen
model.predict(X_test)

# repeat train-then-test 5 times on different slices of the data,
# so you get 5 scores instead of trusting just one lucky split
cross_val_score(model, X, y, cv=5)
requests
HTTP queries to REST APIs

Query OQMD, COD, NOMAD: any database without its own Python client. Inside it: requests.get / requests.post for the two common HTTP verbs, and a Response object with .json(), .status_code, and .text. Every REST API here returns JSON, which .json() turns into a Python dictionary.

# requests is Python's library for talking to websites and APIs
import requests

# send a question to this web address and wait for the answer
r = requests.get(
  "https://oqmd.org/oqmdapi/formationenergy",
  # extra options attached to the question: at most 10 results,
  # and only send back these two named fields
  params={"limit": 10, "fields": "name,delta_e"}
)

# the number 200 means the request succeeded; other numbers mean an error
r.status_code

# turn the answer into a Python dictionary, then read its "data" part
data = r.json()["data"]
os + pathlib
Files, folders, environment vars

Read API keys securely, create output folders, build cross-platform file paths. Inside it: os.environ (a dictionary of environment variables), os.makedirs for folders, and pathlib.Path, an object representing a file path with methods like .exists(), .stem, .suffix, and .glob().

import os
from pathlib import Path

# read a secret key that was set outside the code, never type
# a real API key directly into a script anyone might see
key = os.environ.get("MP_API_KEY")

# create a folder called "results", and do nothing if it already exists
os.makedirs("results", exist_ok=True)

# builds a file path by joining "data" and "band_gaps.csv" correctly,
# whether you are on Windows, Mac, or Linux
p = Path("data") / "band_gaps.csv"

# check whether that file actually exists on disk yet
p.exists()

# pull out just the file name ("band_gaps") and the extension (".csv")
p.stem; p.suffix
warnings
Suppress deprecation noise

Silences pymatgen/matminer deprecation messages so your notebook output stays readable. Inside it: filterwarnings takes an action ("ignore", "error", "default") and an optional warning category to target only one kind of warning.

import warnings

# hide every warning message; blunt, but the most common approach
warnings.filterwarnings("ignore")

# a gentler option: hide only deprecation notices, so you still
# see warnings that might point to a real problem
warnings.filterwarnings(
  "ignore", category=DeprecationWarning
)
matplotlib
Plotting and visualisation

Band gap distributions, parity plots, learning curves, and it renders inline in notebooks. Inside it: matplotlib.pyplot is the state-based interface most people use day to day, sitting on top of Figure and Axes objects for finer control over multi-panel plots.

# the plotting toolkit; "plt" is its usual nickname
import matplotlib.pyplot as plt

# draw a histogram: how many materials fall into each of 50 band-gap ranges
plt.hist(df["band_gap"], bins=50)

# one dot per material, comparing what actually happened to what
# the model predicted; dots on a diagonal line mean a good model
plt.scatter(y_test, y_pred)

# label the horizontal axis so readers know what it shows
plt.xlabel("Actual (eV)")

# label the vertical axis the same way
plt.ylabel("Predicted (eV)")

# actually draw the finished chart on screen
plt.show()
tqdm
Progress bars for loops

Wraps any loop and shows a live count, percentage, and time estimate; essential once you are featurizing thousands of structures. Inside it: tqdm() for scripts and the terminal, tqdm.notebook.tqdm() for an animated bar in Colab/Jupyter, and tqdm.pandas() to add a bar to a pandas .apply() call.

# this version is for scripts and the terminal
from tqdm import tqdm
features = []

# wrapping "structures" in tqdm() changes nothing about the loop
# itself, it just adds a progress bar while it runs
for s in tqdm(structures):
    # the bar's count ticks up by one each time this line runs
    features.append(featurize(s))

# inside a notebook (Colab, Jupyter), use this version instead,
# it draws a nicer, animated bar
from tqdm.notebook import tqdm
json + pickle
Save and load data / models

json for human-readable data files that any language can read; pickle for saving trained models so you never retrain from scratch. Inside it: json.dump/load handle text and only basic Python types; pickle.dump/load handle binary data and almost any Python object, including a fitted model, but should never load a file from an untrusted source.

import json, pickle

# save a dictionary or list to a text file anyone can open and read
json.dump(results, open("out.json", "w"))

# load that same file back in as a Python dictionary or list
json.load(open("out.json"))

# save your trained model to disk, so tomorrow you can reuse it
# instead of training it all over again; "wb" means write binary
pickle.dump(model, open("model.pkl", "wb"))

# load that saved model back in, ready to use; "rb" means read binary
model = pickle.load(open("model.pkl", "rb"))
mendeleev
Element property database

Periodic table data in Python: any element property, electronegativity, atomic radius, ionisation energy, electron configuration, density, complementing MAGPIE for hand-crafted materials features. Inside it: the element() function returns an Element object; that object exposes dozens of attributes, most as plain numbers, a few like .ionenergies as lists since an atom has more than one ionisation energy.

from mendeleev import element

# look up the element iron, using its periodic table symbol "Fe"
Fe = element("Fe")

# how strongly this element pulls on shared electrons; 1.83 for iron
Fe.electronegativity

# the typical size of one iron atom, in picometres: 126 pm
Fe.atomic_radius

# energy needed to remove one electron from an iron atom; an atom
# can lose more than one electron, so this is a list, not one number
Fe.ionenergies[1]

# how iron's electrons are arranged around its nucleus, as text
Fe.electron_configuration

# iron's density: 7.874 grams per cubic centimetre
Fe.density
Materials Informatics Series 11 / 23

How to Read a Line of Python

Before the next slide throws real code at you, here is every symbol in it, explained once, assuming you have never written a line of code before. Every example below is taken directly from the Materials Project code on the next slide.

import
Bring a toolbox into your code

Python starts nearly empty. If you want to use a toolbox someone else built, like the one for talking to the Materials Project, you have to say so first. Writing import is that request: "fetch this toolbox, and make everything inside it available to me from this point onward." Without it, Python has no idea the toolbox even exists.

from … import …
Take just one tool, not the whole box

A toolbox can hold dozens of tools you will never use. from mp_api.client import MPRester means: "reach inside the mp_api.client toolbox, and hand me only the one tool called MPRester. Leave everything else in the box." It is more precise, and faster, than importing everything.

the dot .
"Go one level deeper inside"

A dot always means the same thing: step inside. mp_api.client reads as "inside the mp_api toolbox, there is a smaller box called client." mpr.materials.summary.search repeats that idea three times in a row: inside mpr, find materials; inside materials, find summary; inside summary, find the tool called search.

as
Giving something a nickname

as mpr means "and from now on, call this thing mpr instead of typing its full name every time." Python does not care what nickname you pick, you could write as banana and it would still work. mpr is chosen because it reminds a human reader "this is my MP Rester connection."

round brackets ( )
"Run this, using what's inside"

Round brackets right after a name almost always mean "carry out this action, and here is what to do it with." MPRester("YOUR_API_KEY") means: run MPRester, and hand it your API key to work with. Whatever sits inside the brackets is called an argument, the input the action actually needs.

round brackets, second job ( , )
A short, fixed, ordered pair

Round brackets have a second, unrelated job: when they hold values separated by commas and are not attached to a name, like (2, 3), they build a small fixed group called a tuple. It is a strict, ordered pair: first the number 2, then the number 3, nothing can be added or removed later.

square brackets [ ]
An ordered list of items

Square brackets build a list: items kept in the exact order you wrote them, and you can add more later. ["Fe", "O"] is a list holding exactly two items, the text "Fe" and the text "O", in that order.

curly braces { }
A lookup table of label : value

Curly braces build a dictionary, a set of labels each pointing to a value, exactly like looking up a word in a real dictionary to find its meaning. You will meet these from the next database onward, in the shape {"limit": 10}: the label "limit" points to the value 10.

quotation marks " "
Literal text, called a string

Anything wrapped in quotation marks is a string: a literal piece of text, exactly as typed, not a number and not an instruction to run. "Fe" and "YOUR_API_KEY" are both strings. Without the quotes, Python would think you were referring to a variable named Fe, which does not exist.

the equals sign =
"Store this, under this name"

A single = does not mean "equals" the way it does in a maths class. It means "take whatever is on the right, and remember it under the name on the left." docs = mpr.materials.summary.search(...) means: run that search, then keep the answer, and call it docs from now on.

A variable, in one sentence

Both mpr and docs are variables: names you invented yourself to remember something by, exactly like writing a label on a box so you know what is inside without opening it every single time. Python places no meaning on the name itself, mpr could be called connection instead and work identically. Good names are chosen for the next human reader, not for the computer.

Materials Informatics Series 12 / 23

Materials Project mp-api

Get your API key
materialsproject.org → Dashboard → API Key

Inside mp_api.client: one class, MPRester, exposes many endpoint groups as attributes, mpr.materials.summary, .electronic_structure, .elasticity, .phonon, .thermo, each with its own .search(). A few common lookups, like fetching a structure by ID, get their own shortcut method directly on mpr.

Available properties:

band_gap formation_energy_per_atom structure density is_stable nsites total_magnetization elastic_tensor dos bandstructure
num_elements=(2, 3), word by word

num_elements is the name of one specific question the Materials Project lets you ask: "how many different elements is this material made of?" The = assigns your answer to that question. (2, 3) is a tuple, a fixed, two-part pair in round brackets (see the previous slide): the Materials Project reads a pair here as a range, minimum first, maximum second. So this line says "keep only materials made of at least 2 and at most 3 different elements." A material with exactly 2 elements is called binary (like Fe2O3: iron and oxygen); exactly 3 is called ternary. Writing 2 and 3 together, in one filter, keeps both kinds and excludes everything else: a single pure element (1) or anything with 4 or more.

# MPRester is the one class you need; it is the "front desk"
# for every query you send to the Materials Project
from mp_api.client import MPRester

# "with" opens the connection here, and automatically closes it
# again once you are done, even if something goes wrong partway through
with MPRester("YOUR_API_KEY") as mpr:

    # "summary" is one category of data the Materials Project offers;
    # .search() is how you ask it a question
    docs = mpr.materials.summary.search(

        # only return materials that contain both iron and oxygen
        elements=["Fe", "O"],

        # only materials made of exactly 2 or exactly 3 different elements
        num_elements=(2, 3),

        # of everything the database knows about each material, only
        # send back these five specific pieces of information
        fields=[
            "material_id",             # its unique ID, like "mp-19770"
            "formula_pretty",          # its chemical formula, like "Fe2O3"
            "band_gap",                # how good an insulator it is, in eV
            "formation_energy_per_atom",  # how stable it is, roughly
            "is_stable"                # true or false: is it actually stable
        ]
    )
    # "docs" now holds a list of matching materials, one entry each

    # a convenient shortcut for one common task: fetching the full
    # atom-by-atom structure of one specific material by its ID
    struct = mpr.get_structure_by_material_id(
        "mp-19770"  # this ID happens to be hematite, Fe2O3
    )
Materials Informatics Series 13 / 23

pymatgen – Structure Analysis

The backbone of computational materials analysis in Python. Handles crystal structures, phase diagrams, symmetry analysis, and file I/O.

What's inside: pymatgen.core holds the core objects (Structure, Lattice, Species, Composition); pymatgen.io has one submodule per file format (.cif, .vasp, .xyz); pymatgen.symmetry wraps the C library spglib for space groups; pymatgen.analysis covers phase diagrams, bonding, and structure matching.

So what exactly is .core?

Same rule as always: a dot means "step one level deeper inside." pymatgen is the big toolbox; core is the name pymatgen's authors gave to one specific smaller box inside it, the one holding the foundational, load-bearing tools that almost everything else in pymatgen depends on, most importantly the Structure class itself. Other smaller boxes inside pymatgen have different names for different jobs: io for reading and writing files, symmetry for space groups, analysis for higher-level calculations. "core" specifically signals "the basics live here."

Structure objects
Lattice, Species, Sites, full crystallographic information
File formats
CIF, POSCAR, XYZ, CSSR, JSON, read and write any format
Symmetry & analysis
Spacegroup detection, nearest neighbours, phase diagrams
# Structure is the central object: it represents one crystal,
# atoms, positions, and unit cell, all bundled together
from pymatgen.core import Structure

# open the file and build a Structure from it; pymatgen figures
# out it is a CIF file just from the ".cif" ending, automatically
struct = Structure.from_file("Fe2O3.cif")

# once you have a Structure, its properties are ready instantly,
# pymatgen already computed them when the file was read

# the lengths of the unit cell's three edges, in Ångstroms
print(struct.lattice.abc)

# the three angles between those edges, in degrees
print(struct.lattice.angles)

# the chemical formula, printed as text: "Fe2 O3"
print(struct.formula)

# the density: how much mass is packed into the unit cell's volume
print(struct.density)

# the volume of the unit cell itself
print(struct.volume)

# this tool figures out a crystal's symmetry; behind the scenes
# it hands the work off to spglib, a separate, well-trusted library
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer

# run the symmetry analysis on our structure, once, and keep the result
sga = SpacegroupAnalyzer(struct)

# the symmetry group's short name, e.g. "R-3c"
print(sga.get_space_group_symbol())

# the same symmetry group, but as a number from 1 to 230,
# since there are exactly 230 possible space groups
print(sga.get_space_group_number())



# rebuild the structure using the standard, textbook version of
# its unit cell shape, useful for comparing structures fairly
conv = sga.get_conventional_standard_structure()
Materials Informatics Series 14 / 23

matminer – Feature Extraction

Turns crystal structures and compositions into numerical feature vectors that machine learning models can consume. Includes built-in datasets.

What's inside: matminer.featurizers is organised by input type, .composition for formula-only featurizers like ElementProperty, .structure for full-structure ones, .site for per-atom descriptors, and .conversions for type converters like StrToComposition. matminer.datasets ships benchmark datasets ready to load in one call.

Two new patterns in this code

Arguments without a name. stc.featurize_dataframe(df, "structure") hands over two values with no name= in front of them. These are called positional arguments: Python matches them to what the tool expects purely by the order you list them in, first value to the first slot, second to the second. Compare that to col_id="composition" a few lines later, a keyword argument, where you say by name which slot it fills, so the order no longer matters.

Calling a tool on its own name. ElementProperty.from_preset("magpie") looks like the dot-and-parentheses pattern you already know, but notice there is no variable sitting before the dot yet, only the class name itself. This is a factory: instead of building an empty ElementProperty and configuring it yourself step by step, from_preset hands you back one ready-made, pre-tuned, exactly like ordering a preset meal instead of assembling one from raw ingredients.

ElementProperty (MAGPIE)
145 features from elemental properties: electronegativity, atomic radius, valence electrons, melting point…
SiteStatsFingerprint
Local structure descriptors – coordination environment, bond angles, Voronoi statistics
Built-in datasets
matbench_mp_gapmatbench_mp_e_formcastelli_perovskites
# load_dataset fetches a ready-made benchmark dataset in one call
from matminer.datasets import load_dataset

# ElementProperty is the tool that runs MAGPIE, matminer's most
# popular way of turning a formula into numbers
from matminer.featurizers.composition import \
    ElementProperty

# this converts a formula written as plain text, like "Fe2O3",
# into a proper Composition object matminer can work with
from matminer.featurizers.conversions import \
    StrToComposition

# download (only the first time) and load the Matbench band-gap
# dataset, over 106,000 materials, straight into a table
df = load_dataset("matbench_mp_gap")

# every featurizer needs a proper Composition object to work from,
# not a plain text string, so convert the column first
stc = StrToComposition()
df = stc.featurize_dataframe(df, "structure")

# "magpie" is a named, ready-tuned recipe: 22 elemental properties,
# each summarised 6 different statistical ways
ep = ElementProperty.from_preset("magpie")

# run that recipe on the "composition" column; this is the step
# that actually adds 145 new numeric columns to the table
df = ep.featurize_dataframe(
    df, col_id="composition"
)

# check the table's size: same number of rows, many more columns
print(df.shape)
Materials Informatics Series 15 / 23

AFLOW – 3.5 Million Compounds

AFLOW (Automatic Flow) is the largest high-throughput DFT database. Access via the aflow Python package or directly via REST.

What's inside the aflow package: aflow.search() starts a query; aflow.K is a namespace listing every AFLOW property as an attribute, so aflow.K.Egap is the band gap keyword, not a string you have to remember correctly; .filter(), .select(), and .orderby() chain onto the query object.

Four new patterns in this code

Method chaining. .filter(...).filter(...).select(...) stacks several dots-and-parentheses in a row. Each call finishes and hands back an updated version of the same query, which the next dot immediately acts on again, like giving a series of instructions to the same assistant one after another: "narrow this, narrow it again, now pick these columns."

Comparison symbols. aflow.K.Egap > 0 is a question, not an instruction: "is this value greater than 0?" It resolves to True or False. < means less than, and == (two equals signs, not one) means "is equal to." Two equals signs are used here deliberately, because a single = already means something else: storing a value, not asking a question.

The for … in loop. for entry in results[:20]: means "one at a time, take each item out of results, temporarily call it entry, and run the indented lines below using it." It repeats until every item has had its turn.

Slicing with a colon. results[:20] means "take items from the start, up to but not including position 20," a slice of the full list rather than the whole thing. Leaving the number before the colon blank means "start from the very beginning."

Key properties
Egapenergy_atomspacegroup_relaxvolume_atomnatomsnspecies
Filter syntax
Chain .filter() calls to narrow results by any property.
pip install aflow
import aflow

# "icsd" restricts the search to materials that were first found
# experimentally, and later re-computed with DFT for consistency
results = (
    aflow.search(catalog="icsd")

        # only keep materials with a band gap above zero, which
        # rules out metals (a metal's band gap is exactly 0)
        .filter(aflow.K.Egap > 0)

        # and below 3.5 eV, which rules out very wide-gap insulators,
        # keeping us in the semiconductor range
        .filter(aflow.K.Egap < 3.5)

        # only keep materials built from exactly two elements
        .filter(aflow.K.nspecies == 2)

        # of everything AFLOW knows, only send back these four fields
        .select(
            aflow.K.compound,             # the formula, e.g. "GaAs"
            aflow.K.Egap,                 # the band gap, in eV
            aflow.K.spacegroup_relax,     # its symmetry group
            aflow.K.energy_atom           # energy per atom, a stability clue
        )

        # sort the results from smallest band gap to largest
        .orderby(aflow.K.Egap)
)
# nothing has actually been sent to AFLOW's servers yet: this whole
# query is just a description of what you want, built step by step

# the request only fires once you actually try to use the results,
# which is what this loop does; here we look at the first 20
for entry in results[:20]:
    # each result lets you read its fields by name, like this
    print(entry.compound, entry.Egap)

# if the aflow package cannot do what you need, you can always
# fall back to AFLOW's plain REST API directly, using requests
import requests
url = "http://aflow.org/API/aflowlib.php"
r = requests.get(url, params={
    "species": "Fe,O",      # the material must contain these elements
    "format": "json"       # ask for a computer-readable answer, not a webpage
})
Materials Informatics Series 16 / 23

OQMD – REST API

Open Quantum Materials Database, 1 million DFT-computed formation enthalpies and stability data. No Python client needed: pure REST.

What's inside the response: every OQMD API call returns a JSON object with a "data" key holding a list of records, and separate "meta" info about pagination. Each record is a flat dictionary, one key per requested field, which is exactly the shape pd.DataFrame() expects.

The letter f before a string

You have seen strings in quotation marks before, but f"{BASE}/formationenthalpy" has an f sitting right before the opening quote. That letter turns on a feature called an f-string: anything inside curly braces gets swapped out for the current value of that variable before the string is used. If BASE holds the text "https://oqmd.org/oqmdapi", then f"{BASE}/formationenthalpy" becomes "https://oqmd.org/oqmdapi/formationenthalpy", built fresh, without you having to glue the pieces together by hand. Also new here: <= means "less than or equal to," the same idea as > and < from the AFLOW slide, just inclusive of the boundary itself.

Base URL
https://oqmd.org/oqmdapi/
Key endpoints
/formationenthalpy/structure/calculation
No API key required
Fully open access – just use requests directly.
import requests
import pandas as pd

# every question we ask OQMD starts with this same web address
BASE = "https://oqmd.org/oqmdapi"

# "/formationenthalpy" is one specific section of OQMD's API,
# focused on how stable a material is
r = requests.get(
    f"{BASE}/formationenthalpy",

    # these extra options narrow down exactly what comes back
    params={
        "composition": "Fe-O",       # any material made of iron and oxygen
        "limit": 100,                 # send back at most 100 results
        "offset": 0,                  # start counting from the very first result
        "fields": "name,delta_e,stability,band_gap"  # only these 4 pieces of info
    }
)
# turn the raw answer into a Python dictionary we can work with
data = r.json()

# the actual list of materials lives inside data, under "data"
df = pd.DataFrame(data["data"])

# a quick look at the first 5 rows, just to sanity-check the data
print(df.head())

# "stability" measures how far above the most stable possible
# combination a material sits; zero or below means it is stable
stable = df[df["stability"] <= 0.0]

# print how many materials passed that stability check
print(f"Stable phases: {len(stable)}")
Materials Informatics Series 17 / 23

COD – Experimental Structures

The Crystallography Open Database contains experimental structures from X-ray and neutron diffraction, not DFT. Use when you need real measured structures.

What's inside this code: pymatgen.io.cif.CifParser reads raw CIF text and builds one or more Structure objects, since a CIF file can describe several structures at once; Python's built-in io.StringIO wraps a text string so it can be read like a file, without ever writing anything to disk.

Two new patterns in this code

Brackets stacked back to back. entries[0]["file"] reads right to left, one step at a time. entries[0] means "take the first item out of the entries list" (list positions start counting from 0, not 1, so [0] is the very first one). That first item turns out to itself be a dictionary, so the second bracket, ["file"], immediately looks up the value stored under the label "file" inside it.

A property with no parentheses. requests.get(cif_url).text ends in .text with no round brackets after it. That absence is meaningful: parentheses mean "run an action," so leaving them off means this is not an action to run, it is a value already sitting there waiting to be read, in this case the response's raw text content.

Why use COD?
Experimental data for validation, training on real measurements, structures not in DFT databases.
Output: CIF files
Standard Crystallographic Information File – directly readable by pymatgen, ASE, VESTA.
Important note
COD contains experimental data – DFT vs COD values differ due to temperature, pressure, functional.
import requests

# CifParser reads raw CIF text and turns it into one or more
# pymatgen Structure objects
from pymatgen.io.cif import CifParser

# StringIO is a small trick: it lets Python treat a plain piece
# of text as if it were an open file, without saving anything to disk
from io import StringIO

COD = "https://www.crystallography.net/cod"

# Step 1: search for materials with this formula; this only
# gives you a list of matches, not the actual structures yet
r = requests.get(
    f"{COD}/result",
    params={"formula": "TiO2", "format": "json"}
)
entries = r.json()

# COD often stores several different measurements of the same
# material, so a search like this can return many entries
print(f"Found {len(entries)} entries")

# Step 2: each entry has its own ID, and the actual CIF file for
# it always lives at the same kind of web address
cod_id = entries[0]["file"]
cif_url = f"{COD}/{cod_id}.cif"

# download that CIF file's contents as plain text
cif_text = requests.get(cif_url).text

# Step 3: hand that text straight to pymatgen, using StringIO,
# so there is no need to save a temporary file first
parser = CifParser(StringIO(cif_text))

# a single CIF file can describe more than one structure,
# so this returns a list; here we just take the first one
struct = parser.get_structures()[0]

# from here on, "struct" is a normal Structure object, exactly
# like the one we got from the Materials Project earlier
print(struct.formula, struct.density)
Materials Informatics Series 18 / 23

NOMAD – Raw Calculation Archive

NOMAD stores the raw output files of DFT calculations, not just extracted properties. Useful for accessing calculation metadata and diverse code outputs.

What's inside the query: the "query" block filters which entries match, "pagination" controls how many come back per request, and "required" tells NOMAD which sections of each entry to actually send, a "*" means all fields in that section, without pulling the full raw calculation files.

A safer way to look something up: .get()

You already know ["key"] looks something up inside a dictionary, but if that key does not exist, Python stops the whole program with an error. mat.get("symmetry", {}) is a gentler version of the same idea: "look up symmetry, and if it is not there, quietly hand me an empty dictionary instead of crashing." The .get("space_group_number") chained straight after it then does the same safe lookup one level deeper. This matters here because not every NOMAD entry records every field, so the code needs to keep going even when something is missing.

Supports 40+ DFT codes
VASP, Quantum ESPRESSO, FHI-aims, ABINIT, CP2K and more – all in one place.
Query with POST
NOMAD uses a JSON body for complex queries – filter by elements, code, functional, upload date.

Base URL:

https://nomad-lab.eu/prod/v1/api/v1/
import requests

URL = "https://nomad-lab.eu/prod/v1/api/v1"

# unlike the other databases on this slide, NOMAD wants its
# question written out as a structured dictionary, sent as POST
payload = {
    # the actual filter: which materials qualify
    "query": {
        "results.material.elements": {
            "all": ["Fe", "Ni"]   # must contain both iron and nickel
        }
    },

    # how many results to send back at once, like a page size
    "pagination": {"page_size": 20},

    # of everything NOMAD could tell you about each match, only
    # send back these two categories, not the full raw calculation
    "required": {
        "results": {
            "material": "*",          # everything about the material itself
            "method": "*"             # everything about how it was calculated
        }
    }
}

# "post" (rather than "get") is used because the question is too
# complex to fit neatly into a web address; it travels as the payload
r = requests.post(
    f"{URL}/entries/query",
    json=payload      # requests turns this dictionary into JSON automatically
)

# pull the list of matching entries out of the response
hits = r.json()["data"]

for h in hits:
    # the material's details sit a few levels deep in the response
    mat = h["results"]["material"]

    # print its formula, and its space group number if one was
    # recorded; .get() quietly returns nothing instead of crashing
    # when a field happens to be missing
    print(mat["chemical_formula_reduced"],
          mat.get("symmetry", {}).get("space_group_number"))
Materials Informatics Series 19 / 23

The Feature Layer – Turning Crystals into Numbers

A machine learning model cannot read a crystal structure – it only understands lists of numbers. Featurization is the process of converting a crystal into those numbers. Each number is called a feature.

Analogy – Describing a House

You can't feed a house photo into a price-prediction model. But you can describe the house with numbers: rooms, floor area (m²), distance to school (km), age (years). Each of these is a feature. Crystal featurization works the same way: instead of a house, you describe a crystal.

MAGPIE preset – Ward et al. 2016

Looks at the chemical formula. For each element (e.g. Fe, O in Fe₂O₃, looks up 22 elemental properties – electronegativity, atomic radius, valence electrons, melting point … Then computes statistics (mean, range, std) across elements. Result: 145 numbers from just the formula.

matminer in action

from matminer.datasets import load_dataset
from matminer.featurizers.composition import ElementProperty
from matminer.featurizers.conversions import StrToComposition

# load a ready-made set of 106,113 materials, each already
# labelled with its real, DFT-computed band gap
df = load_dataset("matbench_mp_gap")

# turn the formula text, like "Fe2O3", into a proper Composition
# object, which is the input every featurizer expects
stc = StrToComposition()
df = stc.featurize_dataframe(df, "structure")

# MAGPIE is the featurizer: it turns each composition into 145
# separate numbers describing that material
ep = ElementProperty.from_preset("magpie")
df = ep.featurize_dataframe(df, col_id="composition")

# the table now has 147+ columns instead of 2: it is ready to
# feed straight into a machine learning model
print(df.shape)
145
features per material
22
elemental properties
106K
matbench entries
Materials Informatics Series 20 / 23

The Analysis Layer – Four Tools, Four Jobs

Think of building a house – you need different tradespeople for different jobs. These four libraries are your four tradespeople for data analysis.

pandas – the spreadsheet worker
Table management

Gives you a table (DataFrame), like a very powerful Excel, but controlled by Python. Each row is one material, each column is one property or feature. Filter rows, sort, merge tables with .merge(), group and summarise with .groupby(). Every database result goes into a pandas DataFrame first.

numpy – the calculator
Fast maths on arrays

Does mathematical operations on thousands of values at once in a single line. Divide 100,000 band gaps by 2, one instruction, via numpy.linalg for matrix algebra and numpy.random for sampling. The scientific calculator that works on entire tables simultaneously. pandas, scikit-learn, and matminer are all built on numpy internally.

scikit-learn – the model builder
Machine learning

Contains dozens of ML algorithms across sklearn.ensemble (Random Forest), sklearn.linear_model (Linear Regression, ElasticNet), and sklearn.svm. Give it your feature table and target property, it trains a model that learns the relationship. Once trained, it predicts properties of materials it has never seen before.

matplotlib – the plotter
Visualization

Draws graphs and charts from your results, predicted vs actual band gaps, error histograms, learning curves, through the pyplot interface, itself built on Figure and Axes objects for multi-panel layouts. Takes numbers and turns them into pictures you can understand and share. Every result in a paper started as a matplotlib figure.

Materials Informatics Series 21 / 23

Complete ML Pipeline

Step 1 : Fetch data
mp-api: query Materials Project
filter by band gap (0.1 to 5.0 eV): returns Structure objects + values
↓ list of Structure objects
Step 2 : Parse structures
pymatgen: Structure to Composition
crystal structure → chemical formula → Composition object
↓ Composition objects (formula + element breakdown)
Step 3 : Featurize
matminer: MAGPIE ElementProperty preset
formula → 145 numerical features per material in a pandas DataFrame
↓ DataFrame: 145 feature columns + band_gap target
Step 4 : Train and evaluate
scikit-learn: Random Forest + 5-fold cross-validation
CV R² ~0.87 on Materials Project band gap data

Same four libraries as the rest of this lecture, now chained: mp_api.client fetches, matminer.featurizers.conversions and .composition featurize, sklearn.ensemble and .model_selection train and score.

The one genuinely new pattern: building a list in one line

[{"structure": d.structure, "band_gap": d.band_gap} for d in docs] looks dense, but it is the for … in loop you already met on the AFLOW slide, just written inside square brackets instead of spread across several indented lines. Read it as: "for every d in docs, build a small dictionary out of it, and collect all of those dictionaries into one list." This is called a list comprehension, a compact way to write "loop over this, and build a new list from what you find," when the whole loop only does one simple thing per item.

Formatting a number inside an f-string. f"R² = {scores.mean():.3f}" adds one more trick to the f-strings from the OQMD slide: the :.3f right after the value is a formatting instruction meaning "show this as a decimal number, rounded to exactly 3 digits after the point," so 0.8724193 becomes the much more readable 0.872.

from mp_api.client import MPRester                     # step 1
from matminer.featurizers.composition import ElementProperty    # step 3
from matminer.featurizers.conversions import StructureToComposition  # step 2
import pandas as pd                              # carries the data through every step
from sklearn.ensemble import RandomForestRegressor       # step 4
from sklearn.model_selection import cross_val_score       # step 4

# STEP 1, FETCH: ask the Materials Project for semiconductors,
# meaning a band gap somewhere between 0.1 and 5.0 eV: this range
# excludes metals on one side and very wide-gap insulators on the other
with MPRester("YOUR_API_KEY") as mpr:
    docs = mpr.materials.summary.search(
        fields=["structure", "band_gap"],
        band_gap=(0.1, 5.0)
    )
# "docs" is now a list, one entry per matching material

# build a table with one row per material, keeping just the two
# things we actually need: its structure and its band gap
df = pd.DataFrame([{
    "structure": d.structure,
    "band_gap": d.band_gap        # this is the value we want the model to learn to predict
} for d in docs])

# STEP 2, PARSE: pull the chemical composition, e.g. "Fe2O3", out
# of each full structure, since MAGPIE only needs the formula
stc = StructureToComposition()
df = stc.featurize_dataframe(df, "structure")

# STEP 3, FEATURIZE: turn each composition into 145 numbers using
# MAGPIE, the same preset explained back on slide 13
ep = ElementProperty.from_preset("magpie")
df = ep.featurize_dataframe(df, "composition")

# STEP 4, TRAIN: separate the 145 feature columns (the inputs)
# from the band_gap column (what we are trying to predict)
X = df[ep.feature_labels()]
y = df["band_gap"]

# build a Random Forest model made of 100 individual decision
# trees; at this point it has not learned anything yet
rf = RandomForestRegressor(n_estimators=100)

# train and test the model 5 separate times, on 5 different
# slices of the data, so one lucky or unlucky split cannot fool us
scores = cross_val_score(rf, X, y, scoring="r2", cv=5)

# print the average score across all 5 tests: closer to 1.0 means
# the model's predictions closely match the real band gaps
print(f"R² = {scores.mean():.3f}")
Materials Informatics Series 22 / 23

Model Context Protocol (MCP)

What is MCP?

MCP is an open standard that lets AI models connect to external data sources and tools through a single, unified interface. Think of it as USB-C for AI: instead of writing a custom Python script for every database, you connect once and any AI agent can query any MCP-compatible source in plain language.

Announced by Anthropic in November 2024 and released as open-source. Now maintained as a community standard with SDKs in Python, TypeScript, Java, Kotlin, C#, and Swift.

Why it was created

Before MCP, every AI tool had its own custom connector for every data source: one integration for Slack, another for GitHub, another for a database. Each one was written from scratch. The result was an M × N problem: M tools each needing N connectors. MCP collapses this to M + N: each tool implements MCP once, each data source exposes an MCP server once, and everything works together.

Materials databases with MCP servers
MP
Materials Project
Community MCP servers wrap the mp-api so an AI agent can query band gaps, structures, and phase diagrams in plain language, no Python needed.
NMD
NOMAD
NOMAD exposes its REST API, which can be wrapped as an MCP server for conversational access to its 14M+ calculations.
AFW
AFLOW + OQMD
Both have REST APIs that follow the same MCP server pattern: expose endpoints as tools, and any MCP-compatible AI client can call them.
PY
pymatgen + matminer as local MCP tools
You can wrap any Python function as an MCP tool. An agent can call featurize_composition() or get_structure() as naturally as asking a question.
Why this matters for materials informatics
Today you write Python to query databases. With MCP, an AI agent can query Materials Project, parse the structure with pymatgen, featurize it with matminer, and train a model, all from a single natural language instruction. This is the direction the field is moving: agentic materials science.
Materials Informatics Series 23 / 23

Library Cheatsheet

Library / Tool Purpose Access method Best for
mp-api Materials Project client Python client DFT band gaps, formation energies, structures
aflow AFLOW database client Python client Alloys, largest compound space, electronic structure
requests Generic HTTP client REST API OQMD, COD, NOMAD – any database with an API
pymatgen Structure analysis Library Parsing CIF/POSCAR, symmetry, phase diagrams
matminer Feature extraction Library Turning structures into ML-ready feature vectors
ASE Atomic Simulation Env. Library MD/DFT setup, file conversion, visualisation
Next Lecture

Hands-on: querying the Materials Project, building a band gap prediction dataset, and training your first ElasticNet model.