Crystal Structure Prediction via Machine Learning
# Crystal Structure Prediction via Machine Learning
## Introduction & Motivation
Predicting stable crystal structures from composition is a grand challenge in materials science. ML models accelerate discovery by predicting likely structures, enabling high-throughput screening and rational material design for applications in electronics, energy, and catalysis.
Motivation: Predict stable crystal structures for unknown compositions.
Applications: Structure discovery, stability prediction, phase diagram mapping, material design.
---
## Core Concepts & Theory
### Crystal Systems
Seven crystal systems and space group classifications.
### Lattice Parameters
Unit cell dimensions and atomic positions.
### Stability Criteria
Formation energy and thermodynamic stability.
### Structure Similarity
Comparison metrics and clustering.
---
## Mathematical Formulation
Formation Energy:
$$E_{form} = E_{total} - \sum_i n_i \mu_i$$
Structure Distance:
$$d_{struct} = \min_{ ext{mappings}} \|\mathbf{r}_1 - \mathbf{r}_2\|$$
Probability Distribution:
$$P(structure|composition) = \frac{e^{-E_{form}/k_B T}}{\sum_s e^{-E_s/k_B T}}$$
---
## Advanced Theory & Extensions
### Polymorph Prediction
Multiple structures for same composition.
### Thermodynamic Stability
Competing phases and convex hull.
### Kinetic Barriers
Activation energies for phase transformations.
---
## Computational Considerations
DFT Calculations: O(N³) for N atoms.
Structure Optimization: O(I·N³) for I iterations.
Screening Database: O(M·D²) for M structures, D descriptors.
---
## Practical Implementation Strategies
### Composition Representation
Elemental fractions and statistics.
### Structure Descriptors
Radial distribution functions and symmetry.
### Energy Estimation
Machine learning surrogates for DFT.
---
## Benchmark Datasets & Evaluation
ICSD: Inorganic Crystal Structure Database.
Materials Project: Computed structures and properties.
OQMD: Open Quantum Materials Database.
---
## Key Challenges & Limitations
### Structure Space
Enormous number of possible structures.
### Rare Structures
Limited training data for novel materials.
### Thermodynamic Uncertainty
Temperature and pressure dependencies.
---
## Hyperparameter Tuning
Feature scaling: Normalization by elemental properties.
Model depth: 3-5 hidden layers for neural networks.
Training data: 10K-100K structures.
---
## Real-World Applications & Case Studies
Perovskites: Solar cell materials discovery.
Intermetallics: High-strength alloys.
Layered Materials: 2D electronics.
---
## Integration with Other Methods
Crystal prediction + DFT validation; + experimental synthesis; + high-throughput testing.
---
## Summary & Key Takeaways
ML accelerates crystal structure prediction for materials discovery.
Principles:
1. Composition: Encode elemental information.
2. Descriptors: Extract structural features.
3. Training: Learn from known structures.
4. Prediction: Forecast unknown structures.
5. Validation: Experimental or computational verification.
---
## Appendix: Practical Labs
### Lab 1: Composition-Based Features
import numpy as np
# Simplified elemental property table
ELEMENTAL_PROPERTIES = {
'Al': {'atomic_num': 13, 'radius': 1.43, 'electronegativity': 1.61},
'Cu': {'atomic_num': 29, 'radius': 1.32, 'electronegativity': 1.90},
'Ni': {'atomic_num': 28, 'radius': 1.24, 'electronegativity': 1.91},
'O': {'atomic_num': 8, 'radius': 0.66, 'electronegativity': 3.44},
'N': {'atomic_num': 7, 'radius': 0.71, 'electronegativity': 3.04},
}
def composition_to_descriptor(composition_dict):
"""Convert composition to ML descriptor"""
# composition_dict: {'Al': 0.5, 'O': 0.5}
descriptor = np.zeros(10)
# Weighted elemental properties
total_frac = sum(composition_dict.values())
for elem, frac in composition_dict.items():
if elem in ELEMENTAL_PROPERTIES:
props = ELEMENTAL_PROPERTIES[elem]
weight = frac / total_frac if total_frac > 0 else 0
descriptor[0] += weight * props['atomic_num'] # Mean atomic number
descriptor[1] += weight * props['radius'] # Mean radius
descriptor[2] += weight * props['electronegativity'] # Mean EN
# Variance of properties
descriptor[3] = np.std([ELEMENTAL_PROPERTIES[e]['radius'] for e in composition_dict])
return descriptor
# Test
comp = {'Al': 0.5, 'O': 0.5} # Al2O3 stoichiometry
descriptor = composition_to_descriptor(comp)
print(f"✓ Composition descriptor computed")
print(f" Mean atomic number: {descriptor[0]:.1f}")
print(f" Mean ionic radius: {descriptor[1]:.2f}")
print(f" Mean electronegativity: {descriptor[2]:.2f}")### Lab 2: Formation Energy Estimation
import numpy as np
class FormationEnergyPredictor:
def __init__(self, reference_energies=None):
if reference_energies is None:
# Reference energies for elements (eV/atom, simplified)
self.reference_energies = {
'Al': -3.28, 'Cu': -3.51, 'O': -2.42,
'N': -5.10, 'Ni': -5.81
}
else:
self.reference_energies = reference_energies
self.model_weights = np.random.randn(10) * 0.1
def predict_formation_energy(self, composition_descriptor, total_atoms):
"""Estimate formation energy"""
# Simplified model: composition descriptor → E_form
E_form_ml = np.dot(composition_descriptor, self.model_weights)
return E_form_ml
def chemical_potential_correction(self, composition_dict):
"""Compute reference energy contribution"""
correction = 0
for elem, fraction in composition_dict.items():
if elem in self.reference_energies:
correction += fraction * self.reference_energies[elem]
return correction
def total_formation_energy(self, descriptor, composition_dict):
"""E_form = E_ML + reference correction"""
E_ml = self.predict_formation_energy(descriptor, sum(composition_dict.values()))
E_ref = self.chemical_potential_correction(composition_dict)
E_total = E_ml + E_ref
return E_total
predictor = FormationEnergyPredictor()
comp = {'Al': 0.4, 'O': 0.6}
descriptor = np.array([15, 1.1, 2.5, 0.5, 0, 0, 0, 0, 0, 0])
E_form = predictor.total_formation_energy(descriptor, comp)
print(f"✓ Formation energy estimate: {E_form:.2f} eV/atom")### Lab 3: Structure Similarity Metric
import numpy as np
def structure_distance(positions1, positions2, max_dist=5.0):
"""Compute similarity between two structures"""
# Simplified: compare atomic position distributions
if len(positions1) != len(positions2):
return float('inf')
# Compute pairwise distances
dists = []
for p1 in positions1:
# Find nearest atom in structure 2
distances = np.linalg.norm(positions2 - p1, axis=1)
min_dist = np.min(distances)
if min_dist < max_dist:
dists.append(min_dist)
if len(dists) == 0:
return float('inf')
return np.mean(dists)
class StructureCluster:
def __init__(self, n_structures=100, n_dims=3):
self.structures = [np.random.randn(5, n_dims) for _ in range(n_structures)]
def hierarchical_clustering(self, threshold=0.5):
"""Cluster similar structures"""
clusters = []
used = set()
for i, struct_i in enumerate(self.structures):
if i in used:
continue
cluster = [i]
used.add(i)
for j in range(i+1, len(self.structures)):
if j not in used:
dist = structure_distance(struct_i, self.structures[j])
if dist < threshold:
cluster.append(j)
used.add(j)
clusters.append(cluster)
return clusters
clustering = StructureCluster(n_structures=20)
clusters = clustering.hierarchical_clustering(threshold=0.8)
print(f"✓ Found {len(clusters)} structure clusters")
for i, cluster in enumerate(clusters):
print(f" Cluster {i}: {len(cluster)} structures")### Lab 4: Crystal Structure Prediction System
import numpy as np
class CrystalStructurePredictionSystem:
def __init__(self, n_structures_known=1000):
self.n_known = n_structures_known
# Known structures database (simplified)
self.known_compositions = np.random.randn(n_structures_known, 10)
self.known_structures = [np.random.randn(5, 3) for _ in range(n_structures_known)]
self.formation_energies = np.random.randn(n_structures_known)
def predict_structure_for_composition(self, new_composition, k=5):
"""Find k nearest known structures for composition"""
# Compute distances to known compositions
distances = np.linalg.norm(self.known_compositions - new_composition, axis=1)
# Find k nearest
nearest_indices = np.argsort(distances)[:k]
return nearest_indices, distances[nearest_indices]
def estimate_formation_energy(self, new_composition, k=5):
"""Estimate formation energy via k-nearest neighbors"""
indices, distances = self.predict_structure_for_composition(new_composition, k)
# Weight by inverse distance
weights = 1.0 / (distances + 1e-6)
weights /= weights.sum()
E_form_est = np.average(self.formation_energies[indices], weights=weights)
return E_form_est
def stability_ranking(self, compositions_to_screen):
"""Rank compositions by predicted stability"""
energies = []
for comp in compositions_to_screen:
E = self.estimate_formation_energy(comp)
energies.append(E)
# Sort by energy (lower = more stable)
ranking = np.argsort(energies)
return ranking, np.array(energies)
# Create system and test
system = CrystalStructurePredictionSystem(n_structures_known=100)
# Screen some new compositions
new_compositions = np.random.randn(20, 10)
ranking, energies = system.stability_ranking(new_compositions)
print(f"✓ Predicted formation energies (top 5 most stable):")
for i in ranking[:5]:
print(f" Composition {i}: E_form = {energies[i]:.2f} eV/atom")---