Selecting Default Parameter Ranges via Materials Project
In this notebook, we’ll go over how we selected parameter ranges for some hyperparameters of xtal2png, namely the lower and upper bounds of lattice parameter lengths (\(a\), \(b\), and \(c\)), cell volume, and site pairwise distances.
After we’ve downloaded the data from Materials Project (or loaded it if running the notebook again), we’ll extract the parameters from each of the compounds and do some exploratory data analysis. Based on the analysis, we choose to use a quantile as an upper bound on the parameter ranges in order to get rid of outliers. By removing the highest 1% in each parameter category, we retain 97% of the data with fewer than 52 sites. This gives us our final parameter ranges. Finally, we make publication-ready histogram figures and save these.
Setup
Let’s keep this notebook compatible both as a Google Colab notebook and running locally as a Jupyter notebook.
[1]:
from os import path
try:
import google.colab
IN_COLAB = True
base_dir = "/content/drive/MyDrive/"
except:
IN_COLAB = False
base_dir = path.join("data", "external")
[2]:
if IN_COLAB:
%pip install pymatgen kaleido
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting pymatgen
Downloading pymatgen-2022.0.17.tar.gz (40.6 MB)
|████████████████████████████████| 40.6 MB 1.3 MB/s
Installing build dependencies ... done
Getting requirements to build wheel ... done
Installing backend dependencies ... done
Preparing wheel metadata ... done
Collecting kaleido
Downloading kaleido-0.2.1-py2.py3-none-manylinux1_x86_64.whl (79.9 MB)
|████████████████████████████████| 79.9 MB 115 kB/s
Collecting uncertainties>=3.1.4
Downloading uncertainties-3.1.6-py2.py3-none-any.whl (98 kB)
|████████████████████████████████| 98 kB 7.9 MB/s
Requirement already satisfied: plotly>=4.5.0 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (5.5.0)
Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (from pymatgen) (1.3.5)
Collecting monty>=3.0.2
Downloading monty-2022.4.26-py3-none-any.whl (65 kB)
|████████████████████████████████| 65 kB 5.1 MB/s
Requirement already satisfied: matplotlib>=1.5 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (3.2.2)
Requirement already satisfied: palettable>=3.1.1 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (3.3.0)
Requirement already satisfied: numpy>=1.20.1 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (1.21.6)
Requirement already satisfied: typing-extensions>=3.7.4.3 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (4.1.1)
Requirement already satisfied: sympy in /usr/local/lib/python3.7/dist-packages (from pymatgen) (1.7.1)
Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from pymatgen) (2.23.0)
Collecting ruamel.yaml>=0.15.6
Downloading ruamel.yaml-0.17.21-py3-none-any.whl (109 kB)
|████████████████████████████████| 109 kB 50.9 MB/s
Requirement already satisfied: tabulate in /usr/local/lib/python3.7/dist-packages (from pymatgen) (0.8.9)
Requirement already satisfied: networkx>=2.2 in /usr/local/lib/python3.7/dist-packages (from pymatgen) (2.6.3)
Collecting spglib>=1.9.9.44
Downloading spglib-1.16.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (325 kB)
|████████████████████████████████| 325 kB 52.1 MB/s
Collecting scipy>=1.5.0
Downloading scipy-1.7.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (38.1 MB)
|████████████████████████████████| 38.1 MB 1.2 MB/s
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=1.5->pymatgen) (3.0.9)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=1.5->pymatgen) (1.4.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=1.5->pymatgen) (0.11.0)
Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib>=1.5->pymatgen) (2.8.2)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.7/dist-packages (from plotly>=4.5.0->pymatgen) (8.0.1)
Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from plotly>=4.5.0->pymatgen) (1.15.0)
Collecting ruamel.yaml.clib>=0.2.6
Downloading ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl (546 kB)
|████████████████████████████████| 546 kB 39.0 MB/s
Requirement already satisfied: future in /usr/local/lib/python3.7/dist-packages (from uncertainties>=3.1.4->pymatgen) (0.16.0)
Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.7/dist-packages (from pandas->pymatgen) (2022.1)
Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests->pymatgen) (1.24.3)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests->pymatgen) (2022.6.15)
Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->pymatgen) (3.0.4)
Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->pymatgen) (2.10)
Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy->pymatgen) (1.2.1)
Building wheels for collected packages: pymatgen
Building wheel for pymatgen (PEP 517) ... done
Created wheel for pymatgen: filename=pymatgen-2022.0.17-cp37-cp37m-linux_x86_64.whl size=41841032 sha256=d4c756fe06bd11325d77cca3e963e363c2389a7875320a893e5798a63d3bf3d5
Stored in directory: /root/.cache/pip/wheels/cf/f6/22/58a9be23c5f1b452770e02ff42047175eaf0f9c2f15219fc76
Successfully built pymatgen
Installing collected packages: ruamel.yaml.clib, uncertainties, spglib, scipy, ruamel.yaml, monty, pymatgen, kaleido
Attempting uninstall: scipy
Found existing installation: scipy 1.4.1
Uninstalling scipy-1.4.1:
Successfully uninstalled scipy-1.4.1
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
albumentations 0.1.12 requires imgaug<0.2.7,>=0.2.5, but you have imgaug 0.2.9 which is incompatible.
Successfully installed kaleido-0.2.1 monty-2022.4.26 pymatgen-2022.0.17 ruamel.yaml-0.17.21 ruamel.yaml.clib-0.2.6 scipy-1.7.3 spglib-1.16.5 uncertainties-3.1.6
Data
Materials Project API Key
Get your Materials Project API key from a file that you store in your Google Drive (see below) or current directory (.), or specify it manually by setting the api_key variable in the form field or by running in a local miniconda command prompt with an environment activated that has pymatgen installed: pmg config --add PMG_MAPI_KEY <USER_API_KEY>, e.g. pmg config --add PMG_MAPI_KEY 123abc456def. For the latter option, see the
`pymatgen docs <https://pymatgen.org/usage.html#setting-the-pmg-mapi-key-in-the-config-file>`__.
The file named mp-api-key.json placed directly in your MyDrive folder or in your current directory would look like the following:
{
"API_KEY": "YOUR_API_KEY"
}
Note that this file is not necessary locally if you use the pmg config option above.
[3]:
import json
if IN_COLAB:
from google.colab import drive
drive.mount('/content/drive')
apikey_fpath = "/content/drive/MyDrive/mp-api-key.json"
try:
# https://stackoverflow.com/a/68442279/13697228
with open(apikey_fpath, 'r') as f:
json_data = json.load(f)
api_key = json_data["API_KEY"]
except Exception as e:
print(e)
api_key = "" #@param {type:"string"}
if api_key == "":
print(f"Couldn't load API key from {apikey_fpath}, and user-input API key is also empty.")
print(f"defaulting to user-input API key {api_key}")
pass
else:
api_key = None
print("make sure that you have run `pmg config --add PMG_MAPI_KEY <USER_API_KEY>`")
Mounted at /content/drive
Download
Let’s either download the data directly from Materials Project using the MPRester API or load the data that’s been saved previously to your device as structures.pkl in your base_dir.
[4]:
import pandas as pd
import numpy as np
from tqdm import tqdm
import pickle
from pymatgen.ext.matproj import MPRester
[5]:
pkl_path = path.join(base_dir, "structures.pkl")
try:
with open(pkl_path, "rb") as f:
results = pickle.load(f)
except Exception as e:
print(e)
with MPRester(api_key) as m:
results = m.query(
{"nelements": {"$gte": 2},
"nsites": {"$lte": 52}},
properties=["structure"],
)
with open(pkl_path, "wb") as f:
pickle.dump(results, f)
pass
Extract Lattice and Distances
From here, we’ll loop through each of the structures and grab the lattice parameter lengths (a, b, and c) as well as the cell volume (volume) and pairwise distance matrices between each of the sites for a given structure (distance).
[6]:
a = []
b = []
c = []
volume = []
distance = []
num_sites = []
for s in tqdm(results):
s = s["structure"]
lattice = s.lattice
a.append(lattice.a)
b.append(lattice.b)
c.append(lattice.c)
volume.append(lattice.volume)
distance.append(s.distance_matrix)
num_sites.append(len(s.sites))
print('range of a is: ', min(a), '-', max(a))
print('range of b is: ', min(b), '-', max(b))
print('range of c is: ', min(c), '-', max(c))
print('range of volume is: ', min(volume), '-', max(volume))
print('range of num_sites is: ', min(num_sites), '-', max(num_sites))
dis_min_tmp = []
dis_max_tmp = []
for d in tqdm(range(len(distance))):
dis_min_tmp.append(min(distance[d][np.nonzero(distance[d])]))
dis_max_tmp.append(max(distance[d][np.nonzero(distance[d])]))
print('range of pair-wise distance is: ', min(dis_min_tmp), '-', max(dis_max_tmp))
100%|██████████| 106127/106127 [01:14<00:00, 1424.65it/s]
range of a is: 2.296021 - 66.29136774227022
range of b is: 2.258778 - 61.125585795588215
range of c is: 2.131537 - 130.453537
range of volume is: 11.91856931582488 - 20090.90640762975
range of num_sites is: 2 - 52
100%|██████████| 106127/106127 [00:12<00:00, 8679.68it/s]
range of pair-wise distance is: 0.7249349602879995 - 64.8913973530744
Exploratory Data Analysis
Setup
First, we store the data as a DataFrame to make it easier to visualize and apply operations to it.
[7]:
import plotly.express as px
df = pd.DataFrame(dict(a=a, b=b, c=c, volume=volume, num_sites=num_sites, min_distance=dis_min_tmp, max_distance=dis_max_tmp))
Min/Max
Next, we take a look at the minimum and maximum for each of the parameters.
[8]:
low_df = df.apply(np.min).drop("max_distance")
low_df
[8]:
a 2.296021
b 2.258778
c 2.131537
volume 11.918569
num_sites 2.000000
min_distance 0.724935
dtype: float64
[9]:
df.apply(np.max)
[9]:
a 66.291368
b 61.125586
c 130.453537
volume 20090.906408
num_sites 52.000000
min_distance 8.650650
max_distance 64.891397
dtype: float64
The maxima here can be pretty large, for example ~20000 cubic angstroms for the unit cell volume.
Histogram
Let’s take a quick look at one of the parameters involved, in this case the a lattice parameter length. If you’re seeing this notebook on GitHub and want to view the plotly figures, open the notebook using the Colab badge at the top of this notebook. It should display the image (even without rerunning the notebook).
[10]:
import plotly.express as px
px.histogram(df, x="a", marginal="rug")
Clearly, there are outliers.
Quantile Maximum
Since these are some pretty large ranges that will inflate the round-off error of xtal2png, let’s see if we can filter some of these further by considering only up to a certain percentile (q quantile) for the relevant parameters.
[11]:
q = 0.99
df.apply(lambda a: np.quantile(a, 1 - q)).drop("max_distance")
[11]:
a 2.921745
b 3.050439
c 3.347172
volume 39.162963
num_sites 2.000000
min_distance 0.981744
dtype: float64
[12]:
upp_df = df.apply(lambda a: np.quantile(a, q))
upp_df = upp_df.drop("min_distance")
upp_df
[12]:
a 15.292415
b 14.953414
c 35.792380
volume 1467.529411
num_sites 52.000000
max_distance 17.550941
dtype: float64
Data Retention
The ranges are a lot more reasonable now. Let’s see how many compounds are retained after applying an upper bound filtering step using this upper quantile.
[13]:
qstr = " and ".join([f"{lbl} < @upp_df.{lbl}" for lbl in upp_df.index]) # .drop(["volume", "max_distance"])
qstr
[13]:
'a < @upp_df.a and b < @upp_df.b and c < @upp_df.c and volume < @upp_df.volume and num_sites < @upp_df.num_sites and max_distance < @upp_df.max_distance'
[14]:
filt_df = df.query(qstr)
filt_df
[14]:
| a | b | c | volume | num_sites | min_distance | max_distance | |
|---|---|---|---|---|---|---|---|
| 0 | 5.189676 | 5.189676 | 5.189676 | 58.128751 | 3 | 2.906562 | 2.970818 |
| 1 | 5.388181 | 5.388181 | 5.388181 | 65.313995 | 3 | 3.018739 | 3.085060 |
| 2 | 3.300603 | 3.300603 | 3.300603 | 25.425237 | 2 | 2.333879 | 2.333879 |
| 3 | 3.498199 | 3.498199 | 3.498199 | 30.270418 | 2 | 2.142200 | 2.142200 |
| 4 | 3.510234 | 3.510234 | 3.510234 | 43.252200 | 2 | 3.039952 | 3.039952 |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 106122 | 8.466314 | 8.603384 | 8.606069 | 469.512423 | 40 | 1.502355 | 6.098565 |
| 106123 | 8.960343 | 8.960343 | 8.960342 | 466.892631 | 38 | 1.533764 | 6.157343 |
| 106124 | 6.874616 | 7.317851 | 8.159621 | 363.700541 | 36 | 0.996728 | 5.331704 |
| 106125 | 5.211291 | 7.406056 | 10.707537 | 370.569429 | 36 | 0.983919 | 5.761587 |
| 106126 | 5.397578 | 7.469631 | 10.283887 | 371.461746 | 36 | 0.978758 | 5.621589 |
101527 rows × 7 columns
[15]:
frac_retained = filt_df.shape[0] / df.shape[0]
print(f"{100*frac_retained:.1f}% retained")
95.7% retained
The ranges are much more reasonable now. Also, we have retained ~97% of the original compounds. The other 3% will be much less likely to be represented during generation (i.e. it’s been masked from the distribution), although as outliers to begin with it’s unclear if most generative models would generate these kinds of compounds anyway. This may be interesting as a topic of future study.
Selected Parameter Ranges
We’ll leave the lower bound as the minimum of all Materials Project entries (with fewer than 52 sites, that is). Alternatively, the lower bound could be set to 0 for each of these.
[16]:
low_df # i.e. minima
[16]:
a 2.296021
b 2.258778
c 2.131537
volume 11.918569
num_sites 2.000000
min_distance 0.724935
dtype: float64
[17]:
upp_df # based on `q` quantile
[17]:
a 15.292415
b 14.953414
c 35.792380
volume 1467.529411
num_sites 52.000000
max_distance 17.550941
dtype: float64
Plotting Histogram Distributions
Let’s plot and save the distributions for the parameters in upp_df. First, we define some helper functions to make the figures more compatible with academic publishing and to save them.
[18]:
from typing import Union
import plotly.graph_objs as go
from plotly import offline
def matplotlibify(
fig: go.Figure,
size: int = 24,
width_inches: Union[float, int] = 3.5,
height_inches: Union[float, int] = 3.5,
dpi: int = 142,
return_scale: bool = False,
) -> go.Figure:
"""Make plotly figures look more like matplotlib for academic publishing.
modified from: https://medium.com/swlh/formatting-a-plotly-figure-with-matplotlib-style-fa56ddd97539
Parameters
----------
fig : go.Figure
Plotly figure to be matplotlibified
size : int, optional
Font size for layout and axes, by default 24
width_inches : Union[float, int], optional
Width of matplotlib figure in inches, by default 3.5
height_inches : Union[float, int], optional
Height of matplotlib figure in Inches, by default 3.5
dpi : int, optional
Dots per inch (resolution) of matplotlib figure, by default 142. Leave as
default unless you're willing to verify nothing strange happens with the output.
return_scale : bool, optional
If true, then return `scale` which is a quantity that helps with creating a
high-resolution image at the specified absolute width and height in inches.
More specifically:
>>> width_default_px = fig.layout.width
>>> targ_dpi = 300
>>> scale = width_inches / (width_default_px / dpi) * (targ_dpi / dpi)
Feel free to ignore this parameter.
Returns
-------
fig : go.Figure
The matplotlibified plotly figure.
Examples
--------
>>> import plotly.express as px
>>> df = px.data.tips()
>>> fig = px.histogram(df, x="day")
>>> fig.show()
>>> fig = matplotlibify(fig, size=24, width_inches=3.5, height_inches=3.5, dpi=142)
>>> fig.show()
Note the difference between URL and URL.
"""
font_dict = dict(family="Arial", size=size, color="black")
# app = QApplication(sys.argv)
# screen = app.screens()[0]
# dpi = screen.physicalDotsPerInch()
# app.quit()
fig.update_layout(
font=font_dict,
plot_bgcolor="white",
width=width_inches * dpi,
height=height_inches * dpi,
margin=dict(r=40, t=20, b=10),
)
fig.update_yaxes(
showline=True, # add line at x=0
linecolor="black", # line color
linewidth=2.4, # line size
ticks="inside", # ticks outside axis
tickfont=font_dict, # tick label font
mirror="allticks", # add ticks to top/right axes
tickwidth=2.4, # tick width
tickcolor="black", # tick color
)
fig.update_xaxes(
showline=True,
showticklabels=True,
linecolor="black",
linewidth=2.4,
ticks="inside",
tickfont=font_dict,
mirror="allticks",
tickwidth=2.4,
tickcolor="black",
)
fig.update(layout_coloraxis_showscale=False)
width_default_px = fig.layout.width
targ_dpi = 300
scale = width_inches / (width_default_px / dpi) * (targ_dpi / dpi)
if return_scale:
return fig, scale
else:
return fig
def plot_and_save(fig_path, fig, mpl_kwargs={}, show=False, update_legend=False):
if show:
try:
fig.show()
except Exception as e:
print(e)
offline.plot(fig)
pass
fig.write_html(fig_path + ".html")
fig.to_json(fig_path + ".json")
if update_legend:
fig.update_layout(
legend=dict(
font=dict(size=16),
yanchor="bottom",
y=0.99,
xanchor="right",
x=0.99,
bgcolor="rgba(0,0,0,0)",
# orientation="h",
)
)
fig = matplotlibify(fig, **mpl_kwargs)
fig.write_image(fig_path + ".png")
From here, we just loop through the various parameters, plotting and saving histograms as we go. If running on Google Colab, these will be saved to the current directory which is temporary storage that will be purged after the session is closed.
[19]:
figs = []
for lbl in df.columns.drop("min_distance"):
fig = px.histogram(df, x=lbl, marginal="rug")
fig = matplotlibify(fig)
figs.append(fig)
plot_and_save(lbl+"_hist", fig, show=False)
Here’s an example of what the first figure looks like (compare with the histogram from an earlier section in terms of formatting).
[20]:
figs[0]