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")

---

Go deeper with CFSGPT

Get AI-powered deep-dives, save terms, and run advanced simulations — free account.

Create Free Account