mp_20_pxrdnet / visualization /visualize_materials.py
2090741942justin's picture
Upload mp_20 PXRDNet workspace
39c21b2 verified
Raw
History Blame Contribute Delete
17.1 kB
import torch
import plotly.graph_objects as go
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
from pymatgen.core.periodic_table import Element
from pymatgen.core.structure import Structure
from pymatgen.core.lattice import Lattice
from pymatgen.analysis.diffraction.xrd import XRDCalculator, WAVELENGTHS
from scipy.ndimage import gaussian_filter1d
from scripts.gen_xrd import create_xrd_tensor
from scripts.eval_utils import get_crystals_list
import warnings
import os
import argparse
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
# Thanks ChatGPT!
# Thanks https://www.umass.edu/microbio/chime/pe_beta/pe/shared/cpk-rgb.htm
CPK_COLORS = {
"C": [200, 200, 200], # Carbon
"O": [240, 0, 0], # Oxygen
"H": [248, 248, 248], # Hydrogen
"N": [143, 143, 255], # Nitrogen
"S": [255, 200, 50], # Sulphur
"Cl": [0, 255, 0], # Chlorine
"B": [0, 255, 0], # Boron
"P": [255, 165, 0], # Phosphorus
"Fe": [255, 165, 0], # Iron
"Ba": [255, 165, 0], # Barium
"Na": [0, 0, 255], # Sodium
"Mg": [34, 139, 34], # Magnesium
"Zn": [165, 42, 42], # Zinc
"Cu": [165, 42, 42], # Copper
"Ni": [165, 42, 42], # Nickel
"Br": [165, 42, 42], # Bromine
"Ca": [128, 128, 144], # Calcium
"Mn": [128, 128, 144], # Manganese
"Al": [128, 128, 144], # Aluminum
"Ti": [128, 128, 144], # Titanium
"Cr": [128, 128, 144], # Chromium
"Ag": [128, 128, 144], # Silver
"F": [218, 165, 32], # Fluorine
"Si": [218, 165, 32], # Silicon
"Au": [218, 165, 32], # Gold
"I": [160, 32, 240], # Iodine
"Li": [178, 34, 34], # Lithium
"He": [255, 192, 203], # Helium
}
DEFAULT_COLOR = [255, 20, 147] # Default
DEFAULT_RADIUS = 0.1
def create_materials(args, frac_coords, num_atoms, atom_types, lengths, angles, create_xrd=False, symprec=0.01):
# wavelength
curr_wavelength = WAVELENGTHS[args.wave_source]
# Create the XRD calculator
xrd_calc = XRDCalculator(wavelength=curr_wavelength)
# get the crystals
crystals_list = get_crystals_list(frac_coords=frac_coords, atom_types=atom_types, lengths=lengths, angles=angles, num_atoms=num_atoms)
# ret vals
all_coords = list()
all_atom_types = list()
all_xrds = list()
# loop through and process the crystals
for i in tqdm(range(min(args.num_materials, len(crystals_list)))):
curr_crystal = crystals_list[i]
curr_structure = Structure(
lattice=Lattice.from_parameters(
*(curr_crystal['lengths'].tolist() + curr_crystal['angles'].tolist())),
species=curr_crystal['atom_types'], coords=curr_crystal['frac_coords'], coords_are_cartesian=False)
curr_coords = list()
curr_atom_types = list()
for site in curr_structure:
curr_coords.append([site.x, site.y, site.z])
curr_atom_types.append(Element(site.species_string))
if create_xrd:
try:
sga = SpacegroupAnalyzer(curr_structure, symprec=symprec)
conventional_structure = sga.get_conventional_standard_structure()
except:
warnings.warn(f"Failed to get conventional standard structure for material {i}")
conventional_structure = curr_structure
# Calculate the XRD pattern
try:
pattern = xrd_calc.get_pattern(conventional_structure, two_theta_range=(args.min_theta, args.max_theta))
# Create the XRD tensor
xrd_tensor = create_xrd_tensor(args, pattern)
except:
warnings.warn(f"Failed to get XRD pattern for material {i}")
xrd_tensor = torch.zeros(args.xrd_vector_dim)
all_xrds.append(xrd_tensor)
all_coords.append(np.array(curr_coords))
all_atom_types.append(curr_atom_types)
truncated_crystals_list = crystals_list[:args.num_materials]
assert len(all_coords) == len(all_atom_types)
assert len(all_coords) == min(len(num_atoms), args.num_materials)
assert len(truncated_crystals_list) == len(all_coords)
if create_xrd:
assert len(all_coords) == len(all_xrds)
all_xrds = torch.stack(all_xrds, dim=0).numpy()
assert all_xrds.shape == (len(all_coords), args.xrd_vector_dim)
return all_coords, all_atom_types, all_xrds, truncated_crystals_list
def sinc_filter(x, sinc_filt):
filtered = np.convolve(x, sinc_filt, mode='same')
return filtered
def gaussian_filter(x, n_presubsample, horizontal_noise_range):
filtered = gaussian_filter1d(x,
sigma=np.random.uniform(
low=n_presubsample * horizontal_noise_range[0],
high=n_presubsample * horizontal_noise_range[1]
),
mode='constant', cval=0)
return filtered
def sample(x, n_postsubsample):
x_subsample = [np.max(chunk) for chunk in np.array_split(x, n_postsubsample)]
return np.array(x_subsample)
def augment_xrdStrip(curr_xrdStrip, sinc_filt, n_presubsample=4096, n_postsubsample=512, horizontal_noise_range=(1e-2, 1.1e-2), vertical_noise=1e-3, xrd_filter='both'):
"""
Augments curr_xrdStrip via:
-> Adding peak broadening (horizontal)
-> Adding small Gaussian perturbations to peaks (vertical)
"""
xrd = curr_xrdStrip.numpy()
assert xrd.shape == (n_presubsample,)
# Peak broadening
if xrd_filter == 'both':
sinc_filtered = sinc_filter(xrd, sinc_filt)
filtered = gaussian_filter(sinc_filtered, n_presubsample, horizontal_noise_range)
assert filtered.shape == xrd.shape
elif xrd_filter == 'sinc':
filtered = sinc_filter(xrd)
assert filtered.shape == xrd.shape
else:
raise ValueError("Invalid filter requested")
# scale
filtered = filtered / np.max(filtered)
filtered = np.maximum(filtered, np.zeros_like(filtered))
# sample it
assert filtered.shape == (n_presubsample,)
assert filtered.shape == curr_xrdStrip.shape
filtered = sample(filtered)
# convert to torch
filtered = torch.from_numpy(filtered)
assert filtered.shape == (n_postsubsample,)
# Perturbation
perturbed = filtered + torch.normal(mean=0, std=vertical_noise, size=filtered.size())
perturbed = torch.maximum(perturbed, torch.zeros_like(perturbed))
perturbed = torch.minimum(perturbed, torch.ones_like(perturbed)) # band-pass filter
return perturbed
# Thanks ChatGPT!
# Function to generate sphere coordinates
def generate_sphere_coordinates(center, radius, n_points=100):
phi = np.linspace(0, 2 * np.pi, n_points)
theta = np.linspace(0, np.pi, n_points)
phi, theta = np.meshgrid(phi, theta)
x = center[0] + radius * np.sin(theta) * np.cos(phi)
y = center[1] + radius * np.sin(theta) * np.sin(phi)
z = center[2] + radius * np.cos(theta)
return x, y, z
# https://stackoverflow.com/a/71053527
def ms(center, radius, n_points=20):
x, y, z = center
"""Return the coordinates for plotting a sphere centered at (x,y,z)"""
u, v = np.mgrid[0:2*np.pi:n_points*2j, 0:np.pi:n_points*1j]
X = radius * np.cos(u)*np.sin(v) + x
Y = radius * np.sin(u)*np.sin(v) + y
Z = radius * np.cos(v) + z
return (X, Y, Z)
def plot_materials(args, the_coords, atom_types, output_dir, batch_idx):
for i in range(min(len(the_coords), args.num_materials)):
curr_coords = the_coords[i]
curr_atom_types = atom_types[i]
plot_material_single(curr_coords, curr_atom_types, output_dir, idx=i, batch_idx=batch_idx)
return
def plot_xrds(args, xrds, output_dir):
for i in range(min(args.num_materials, xrds.shape[0])):
curr_xrd = xrds[i]
assert curr_xrd.shape == (512,)
thetas = [pos * 180 / len(curr_xrd) for pos in range(len(curr_xrd))]
plt.plot(thetas, curr_xrd)
plt.savefig(os.path.join(output_dir, f'material{i}.png'))
plt.close()
return
def plot_xrd_single(args, curr_xrd, output_dir, idx, filename=None, x_axis=None, x_label='2 Theta (degrees)'):
plt.figure()
assert curr_xrd.shape == (512,)
if x_axis is None:
x_axis = [pos * 180 / len(curr_xrd) for pos in range(len(curr_xrd))]
plt.figure()
plt.plot(x_axis, curr_xrd)
plt.xlabel(x_label)
plt.ylabel('Scaled Intensity')
filename = filename if filename is not None else f'material{idx}.png'
img_path = os.path.join(output_dir, filename)
plt.savefig(img_path)
plt.savefig(img_path.replace('.png', '.pdf'))
plt.close()
return img_path
def plot_material_single(curr_coords, curr_atom_types, output_dir, idx=0, batch_idx=0, filename=None):
assert len(curr_atom_types) == len(curr_coords)
assert len(curr_coords.shape) == 2 and curr_coords.shape[1] == 3
x = curr_coords[:,0].tolist()
y = curr_coords[:,1].tolist()
z = curr_coords[:,2].tolist()
plot_data = list()
shown_elements = set()
elemental_names = [el.symbol for el in curr_atom_types]
atomic_radii = [float(el.atomic_radius) if el.atomic_radius else DEFAULT_RADIUS for el in curr_atom_types]
curr_coords = curr_coords.tolist()
for i in range(len(curr_coords)):
curr_center = curr_coords[i]
x, y, z = ms(center=curr_center, radius=atomic_radii[i], n_points=25)
curr_color = tuple(CPK_COLORS[elemental_names[i]] if elemental_names[i] in CPK_COLORS else DEFAULT_COLOR)
plot_data.append(
go.Surface(
x=x, y=y, z=z,
opacity=1,
lighting=dict(ambient=0.9, diffuse=0.5, roughness = 0.5, specular=0.1, fresnel=3),
showscale=False,
colorscale=[[0, f'rgb{curr_color}'],[1, f'rgb{curr_color}']],
name=elemental_names[i],
showlegend=elemental_names[i] not in shown_elements
)
)
# Plot spheres using Mesh3d
fig = go.Figure(
data=plot_data
)
camera = dict(
up=dict(x=0, y=0, z=1),
center=dict(x=0, y=0, z=0),
eye=dict(x=1.5, y=1.5, z=1.5)
)
# Customize layout
fig.update_layout(
title=os.path.split(output_dir)[-1],
scene=dict(
xaxis_title='X (Å)',
yaxis_title='Y (Å)',
zaxis_title='Z (Å)',
),
margin=dict(l=10, r=10, t=30, b=10),
scene_camera=camera,
scene_aspectmode='data'
)
filename = filename if filename is not None else f'material{idx}_sample{batch_idx}.png'
img_path = os.path.join(output_dir, filename)
fig.write_image(img_path)
fig.write_image(img_path.replace('.png', '.pdf'))
return img_path
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Generate XRD patterns from CIF descriptions')
parser.add_argument('--filepath', type=str, help='the file with the predictions from evaluate.py',
default='/home/gabeguo/hydra/singlerun/2024-03-01/perov_smoothScaledXRD/eval_recon.pt')
parser.add_argument('--results_folder', type=str, help='where to save the visualizations',
default='material_vis')
parser.add_argument('--xrd_vector_dim', type=int, help='what dimension are the xrds? (should be 512)',
default=512)
parser.add_argument('--min_theta', type=int,
default=0)
parser.add_argument('--max_theta', type=int,
default=180)
parser.add_argument('--num_materials', type=int, help='how many materials to visualize?',
default=10)
parser.add_argument('--wave_source', type=str, help='What is the wave source?',
default='CuKa')
parser.add_argument('--task', choices=['recon', 'gen', 'opt'], help='What is the task?',
default='recon')
args = parser.parse_args()
results = torch.load(args.filepath)
if args.task == 'recon':
print([x for x in results])
for the_dataset, the_name in zip([results, results['input_data_batch']],
['pred', 'gt']):
is_pred = 'pred' in the_name
batched_frac_coords = the_dataset['frac_coords']
batched_num_atoms = the_dataset['num_atoms']
batched_atom_types = the_dataset['atom_types']
batched_lengths = the_dataset['lengths']
batched_angles = the_dataset['angles']
if not is_pred:
batched_frac_coords = batched_frac_coords.unsqueeze(0)
batched_num_atoms = batched_num_atoms.unsqueeze(0)
batched_atom_types = batched_atom_types.unsqueeze(0)
batched_lengths = batched_lengths.unsqueeze(0)
batched_angles = batched_angles.unsqueeze(0)
num_batches = batched_frac_coords.shape[0]
assert num_batches == batched_num_atoms.shape[0]
curr_folder = os.path.join(args.results_folder, the_name)
os.makedirs(curr_folder, exist_ok=True)
for i in range(num_batches):
frac_coords = batched_frac_coords[i]
num_atoms = batched_num_atoms[i]
atom_types = batched_atom_types[i]
lengths = batched_lengths[i]
angles = batched_angles[i]
the_coords, atom_types, generated_xrds, crystals_list = create_materials(args,
frac_coords, num_atoms, atom_types, lengths, angles, create_xrd=True)
plot_materials(args, the_coords, atom_types, curr_folder, i)
xrd_folder = os.path.join(args.results_folder, f"{'pred' if is_pred else 'gt'}_xrds")
os.makedirs(xrd_folder, exist_ok=True)
if i == 0:
plot_xrds(args, generated_xrds, xrd_folder)
elif args.task == 'opt':
the_dataset = results
# fetch base truth materials
base_truth = the_dataset['data']
base_truth_frac_coords = base_truth['frac_coords']
base_truth_num_atoms = base_truth['num_atoms']
base_truth_atom_types = base_truth['atom_types']
base_truth_lengths = base_truth['lengths']
base_truth_angles = base_truth['angles']
# fetch the optimized materials
batched_frac_coords = the_dataset['frac_coords']
batched_num_atoms = the_dataset['num_atoms']
batched_atom_types = the_dataset['atom_types']
batched_lengths = the_dataset['lengths']
batched_angles = the_dataset['angles']
num_batches = batched_frac_coords.shape[0]
assert num_batches == batched_num_atoms.shape[0]
print('num_batches', num_batches)
# create the folders - optimization
opt_materials_folder = os.path.join(args.results_folder, 'opt_materials')
os.makedirs(opt_materials_folder, exist_ok=True)
pred_xrd_folder = os.path.join(args.results_folder, 'opt_xrds')
os.makedirs(pred_xrd_folder, exist_ok=True)
# create the folders - base truth
base_truth_folder = os.path.join(args.results_folder, 'base_truth_materials')
os.makedirs(base_truth_folder, exist_ok=True)
base_truth_xrd_folder = os.path.join(args.results_folder, 'base_truth_xrds')
os.makedirs(base_truth_xrd_folder, exist_ok=True)
xrds = results['xrds'].cpu().squeeze().numpy()
os.makedirs(base_truth_xrd_folder, exist_ok=True)
plot_xrds(args, xrds, base_truth_xrd_folder)
for i in range(num_batches):
frac_coords = batched_frac_coords[i]
num_atoms = batched_num_atoms[i]
atom_types = batched_atom_types[i]
lengths = batched_lengths[i]
angles = batched_angles[i]
# predictions
the_coords, atom_types, generated_xrds, crystals_list = create_materials(args,
frac_coords, num_atoms, atom_types, lengths, angles, create_xrd=True)
plot_materials(args, the_coords, atom_types, opt_materials_folder, i)
# apply gaussian smoothing to the XRDs (to match base-truth gaussian smoothed xrds)
smoothed_xrds = list()
for i in range(generated_xrds.shape[0]):
smoothed_xrd = augment_xrdStrip(torch.tensor(generated_xrds[i,:]))
smoothed_xrds.append(smoothed_xrd)
generated_xrds = torch.stack(smoothed_xrds, dim=0).numpy()
plot_xrds(args, generated_xrds, pred_xrd_folder)
# ground truth
the_coords, atom_types, generated_xrds, crystals_list = create_materials(args,
base_truth_frac_coords, base_truth_num_atoms, base_truth_atom_types, base_truth_lengths, base_truth_angles, create_xrd=True)
plot_materials(args, the_coords, atom_types, base_truth_folder, i)