0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIと量子計算で「夢の素材」を設計する——マテリアルズ・インフォマティクス完全実装ガイド

0
Posted at

AIと量子計算で「夢の素材」を設計する——マテリアルズ・インフォマティクス完全実装ガイド

元記事: 新素材の発見に「200年」かかっていた時代が終わる——AIと量子計算が「夢の物質」を設計する

本記事では上記 note 記事のコンセプトをベースに、実際に手元で動かせる実装コードを追加しています。


対象読者

  • 材料科学 × AI に興味がある研究者・エンジニア
  • GNN(グラフニューラルネットワーク)を素材設計に応用したい方
  • 量子化学シミュレーション(VQE)を試したい方
  • Materials Project / pymatgen でデータ駆動型研究をはじめたい方

なぜ素材発見にAIが必要なのか

118元素 × 無限の結晶構造の組み合わせ空間を人手で探索すれば 200年 かかる。GNoME(Google DeepMind)はGNNで 220万種以上 の安定結晶構造を予測し、従来比10倍以上のペースで候補を提示した。

従来のアプローチ:
  試行錯誤実験 → DFT計算 → 合成 → 評価(数年/素材)

マテリアルズ・インフォマティクス(MI):
  データベース → ML予測 → 逆設計 → DFT検証 → 合成(数週間/候補群)

システム全体アーキテクチャ

┌─────────────────────────────────────────────────────────────────┐
│              Materials Informatics Pipeline                     │
├──────────────┬──────────────┬──────────────┬───────────────────┤
│  Data Layer  │  ML Layer    │  Design Layer│  Validation Layer │
│              │              │              │                   │
│ Materials    │ GNN Property │ Inverse      │ DFT (VASP/        │
│ Project API  │ Predictor    │ Design Gen   │ Quantum ESPRESSO) │
│              │              │              │                   │
│ ICSD/MatNavi │ Formation    │ Diffusion-   │ VQE (Qiskit)      │
│ (NIMS)       │ Energy / Eg  │ based Gen    │ Quantum Chem      │
│              │              │              │                   │
│ AFLOW / OQMD │ Stability    │ Matlantis    │ ASE Relaxation    │
│              │ Classifier   │ MLFF         │                   │
└──────────────┴──────────────┴──────────────┴───────────────────┘
         ↓                ↓                ↓
    pymatgen          PyG / DGL      Crystal Diffusion VAE

環境構築

1. 基本パッケージのインストール

# Python 3.10+ 推奨
pip install pymatgen mp-api

# グラフニューラルネットワーク
pip install torch torch-geometric
pip install dgl dglgo -f https://data.dgl.ai/wheels/repo.html

# 量子化学シミュレーション
pip install qiskit qiskit-nature qiskit-aer
pip install pyscf  # ab initio計算ライブラリ

# 原子シミュレーション
pip install ase  # Atomic Simulation Environment

# 可視化・分析
pip install matminer  # 特徴量エンジニアリング
pip install nglview   # Jupyter上での結晶構造可視化
pip install matplotlib scikit-learn pandas numpy

# (オプション) NVIDIA GPU用 PyTorch Geometric 高速化
# pip install pyg-lib torch-scatter torch-sparse -f https://data.pyg.org/whl/torch-2.1.0+cu121.html

2. Materials Project API キーの取得

# https://materialsproject.org でアカウント作成後、API Keyを取得
export MP_API_KEY="your_api_key_here"

# または .env ファイルに記載
echo "MP_API_KEY=your_api_key_here" >> .env

Step 1: Materials Project からデータ取得

1-1. pymatgen で結晶構造データベースにアクセス

"""
materials_project_fetcher.py
Materials Project API から結晶データを取得・解析するモジュール
"""

import os
from mp_api.client import MPRester
import pandas as pd
import numpy as np
from pymatgen.core import Structure, Composition
from pymatgen.analysis.phase_diagram import PhaseDiagram, PDEntry
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer

class MaterialsDataFetcher:
    """Materials Project APIからデータを取得・整形するクラス"""
    
    def __init__(self, api_key: str = None):
        self.api_key = api_key or os.environ.get("MP_API_KEY")
        if not self.api_key:
            raise ValueError("MP_API_KEY environment variable not set")
    
    def fetch_materials_by_elements(
        self, 
        elements: list[str], 
        max_sites: int = 20,
        properties: list[str] = None
    ) -> pd.DataFrame:
        """
        指定元素を含む材料を取得
        
        Args:
            elements: 元素リスト (例: ["Li", "Fe", "O"])
            max_sites: 最大サイト数(構造の複雑さ制限)
            properties: 取得するプロパティ一覧
        
        Returns:
            材料データのDataFrame
        """
        default_properties = [
            "material_id", "formula_pretty", "energy_above_hull",
            "band_gap", "formation_energy_per_atom", "density",
            "volume", "nsites", "spacegroup", "is_stable",
            "theoretical", "structure"
        ]
        props = properties or default_properties
        
        with MPRester(self.api_key) as mpr:
            docs = mpr.materials.summary.search(
                elements=elements,
                num_sites=(1, max_sites),
                fields=props
            )
        
        records = []
        for doc in docs:
            record = {
                "material_id": doc.material_id,
                "formula": doc.formula_pretty,
                "energy_above_hull": doc.energy_above_hull,
                "band_gap": doc.band_gap,
                "formation_energy": doc.formation_energy_per_atom,
                "density": doc.density,
                "nsites": doc.nsites,
                "spacegroup_number": doc.spacegroup.number if doc.spacegroup else None,
                "is_stable": doc.is_stable,
            }
            records.append(record)
        
        df = pd.DataFrame(records)
        print(f"取得した材料数: {len(df)}")
        return df
    
    def get_structure(self, material_id: str) -> Structure:
        """特定材料の結晶構造を取得"""
        with MPRester(self.api_key) as mpr:
            structure = mpr.get_structure_by_material_id(material_id)
        return structure
    
    def analyze_structure(self, structure: Structure) -> dict:
        """結晶構造を解析して特徴量を抽出"""
        analyzer = SpacegroupAnalyzer(structure)
        
        return {
            "space_group_symbol": analyzer.get_space_group_symbol(),
            "space_group_number": analyzer.get_space_group_number(),
            "crystal_system": analyzer.get_crystal_system(),
            "lattice_type": analyzer.get_lattice_type(),
            "num_atoms": len(structure),
            "volume": structure.volume,
            "density": structure.density,
            "a": structure.lattice.a,
            "b": structure.lattice.b,
            "c": structure.lattice.c,
            "alpha": structure.lattice.alpha,
            "beta": structure.lattice.beta,
            "gamma": structure.lattice.gamma,
        }
    
    def fetch_battery_candidates(self) -> pd.DataFrame:
        """全固体電池候補材料(Li系酸化物・硫化物)を取得"""
        print("全固体電池候補を検索中...")
        # Li含有安定材料でバンドギャップが小さいもの
        with MPRester(self.api_key) as mpr:
            docs = mpr.materials.summary.search(
                elements=["Li"],
                energy_above_hull=(0, 0.05),  # 安定性閾値 50meV/atom
                fields=["material_id", "formula_pretty", "band_gap",
                        "formation_energy_per_atom", "energy_above_hull",
                        "is_stable", "density"]
            )
        
        records = [
            {
                "material_id": d.material_id,
                "formula": d.formula_pretty,
                "band_gap_eV": d.band_gap,
                "formation_energy": d.formation_energy_per_atom,
                "energy_above_hull": d.energy_above_hull,
                "density": d.density,
                "is_stable": d.is_stable,
            }
            for d in docs
        ]
        df = pd.DataFrame(records)
        print(f"候補材料: {len(df)}")
        return df.sort_values("energy_above_hull")


# 使用例
if __name__ == "__main__":
    fetcher = MaterialsDataFetcher()
    
    # Li-Fe-O系の材料を検索
    df = fetcher.fetch_materials_by_elements(["Li", "Fe", "O"])
    print("\n=== Li-Fe-O系材料(バンドギャップ上位10件)===")
    print(df.nlargest(10, "band_gap")[["formula", "band_gap", "energy_above_hull", "is_stable"]])
    
    # 全固体電池候補
    battery_df = fetcher.fetch_battery_candidates()
    print("\n=== 全固体電池候補(安定性上位10件)===")
    print(battery_df.head(10)[["formula", "band_gap_eV", "formation_energy", "density"]])

1-2. matminer で特徴量エンジニアリング

"""
feature_engineering.py
pymatgen 構造 → ML用特徴量ベクトルへの変換
"""

from matminer.featurizers.composition import (
    ElementProperty, Stoichiometry, ValenceOrbital, IonProperty
)
from matminer.featurizers.structure import (
    SiteStatsFingerprint, CoulombMatrix, RadialDistributionFunction
)
import pandas as pd
from pymatgen.core import Structure

class MaterialFeatureExtractor:
    """結晶構造から ML 用特徴量を抽出"""
    
    def __init__(self):
        # 組成ベース特徴量
        self.comp_featurizers = [
            ElementProperty.from_preset("magpie"),  # Magpie特徴量(279次元)
            Stoichiometry(),
            ValenceOrbital(props=["frac"]),
        ]
        # 構造ベース特徴量
        self.struct_featurizers = [
            SiteStatsFingerprint.from_preset("CrystalNNFingerprint_ops"),
        ]
    
    def extract_composition_features(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        組成式から特徴量を抽出
        df には 'composition' 列が必要(pymatgen.Composition オブジェクト)
        """
        from pymatgen.core import Composition
        
        if "composition" not in df.columns:
            df["composition"] = df["formula"].apply(Composition)
        
        for featurizer in self.comp_featurizers:
            featurizer.set_n_jobs(4)
            try:
                df = featurizer.featurize_dataframe(df, "composition", ignore_errors=True)
                print(f"  {featurizer.__class__.__name__}: OK")
            except Exception as e:
                print(f"  {featurizer.__class__.__name__}: SKIP ({e})")
        
        return df
    
    def extract_structure_features(self, structures: list[Structure]) -> pd.DataFrame:
        """結晶構造から構造的特徴量を抽出"""
        fingerprints = []
        featurizer = SiteStatsFingerprint.from_preset("CrystalNNFingerprint_ops")
        
        for i, struct in enumerate(structures):
            try:
                fp = featurizer.featurize(struct)
                fingerprints.append(fp)
                if i % 10 == 0:
                    print(f"  構造 {i+1}/{len(structures)} 処理中...")
            except Exception as e:
                fingerprints.append([None] * featurizer.feature_labels().__len__())
                print(f"  構造 {i+1} スキップ: {e}")
        
        cols = featurizer.feature_labels()
        return pd.DataFrame(fingerprints, columns=cols)

Step 2: GNN による結晶特性予測

2-1. 結晶構造をグラフに変換

"""
crystal_graph.py
結晶構造 → PyTorch Geometric グラフ変換
"""

import torch
import numpy as np
from torch_geometric.data import Data
from pymatgen.core import Structure
from pymatgen.analysis.graphs import StructureGraph
from pymatgen.analysis.local_env import CrystalNN

# 元素の原子番号 → one-hot または埋め込み用インデックス
ELEMENT_FEATURES = {
    "H": 1, "Li": 3, "Be": 4, "B": 5, "C": 6, "N": 7, "O": 8, "F": 9,
    "Na": 11, "Mg": 12, "Al": 13, "Si": 14, "P": 15, "S": 16, "Cl": 17,
    "K": 19, "Ca": 20, "Ti": 22, "V": 23, "Cr": 24, "Mn": 25, "Fe": 26,
    "Co": 27, "Ni": 28, "Cu": 29, "Zn": 30, "Ga": 31, "Ge": 32, "As": 33,
    # ... 必要に応じて追加
}
NUM_ELEMENTS = 118

def structure_to_graph(structure: Structure, cutoff: float = 6.0) -> Data:
    """
    pymatgen Structure → PyTorch Geometric Data オブジェクト
    
    ノード特徴量: 原子番号のone-hot + 電気陰性度 + 原子半径 + ...
    エッジ特徴量: 距離 + 方向ベクトル (Gaussian basis expansion)
    
    Args:
        structure: pymatgen の結晶構造
        cutoff: 結合を考慮する最大距離 [Å]
    
    Returns:
        PyTorch Geometric の Data オブジェクト
    """
    from pymatgen.core.periodic_table import Element
    
    # ノード特徴量(原子ごと)
    node_features = []
    for site in structure:
        elem = site.specie
        z = elem.Z  # 原子番号
        # 基本特徴量: 原子番号, 電気陰性度, 共有結合半径, イオン化エネルギー
        features = [
            z / 118.0,                          # 正規化原子番号
            float(elem.X) if elem.X else 0.0,   # 電気陰性度 (Pauling scale)
            float(elem.atomic_radius or 1.5) / 3.0,  # 原子半径
            float(elem.row) / 7.0,              # 周期
            float(elem.group) / 18.0,           # 族
        ]
        # one-hot encoding for atomic number (簡略版: 上位30元素)
        z_onehot = [0.0] * 30
        if z <= 30:
            z_onehot[z - 1] = 1.0
        features.extend(z_onehot)
        node_features.append(features)
    
    x = torch.tensor(node_features, dtype=torch.float)
    
    # エッジ(原子間結合)の構築
    edge_src, edge_dst, edge_attrs = [], [], []
    
    all_neighbors = structure.get_all_neighbors(cutoff)
    
    for i, neighbors in enumerate(all_neighbors):
        for neighbor in neighbors:
            j = neighbor.index
            dist = neighbor.nn_distance
            
            # Gaussian Basis Expansion で距離を多次元特徴量に
            gauss_centers = np.linspace(0, cutoff, 64)
            gauss_width = 0.5
            gauss_features = np.exp(-((dist - gauss_centers) ** 2) / (2 * gauss_width ** 2))
            
            edge_src.append(i)
            edge_dst.append(j)
            edge_attrs.append(gauss_features.tolist())
    
    if len(edge_src) == 0:
        # エッジなし(孤立原子)のフォールバック
        edge_index = torch.zeros((2, 0), dtype=torch.long)
        edge_attr = torch.zeros((0, 64), dtype=torch.float)
    else:
        edge_index = torch.tensor([edge_src, edge_dst], dtype=torch.long)
        edge_attr = torch.tensor(edge_attrs, dtype=torch.float)
    
    return Data(x=x, edge_index=edge_index, edge_attr=edge_attr)

2-2. GNN モデル定義(Crystal Graph Convolutional Neural Network)

"""
cgcnn_model.py
結晶グラフ畳み込みニューラルネットワーク(CGCNN)
バンドギャップ・形成エネルギーの予測に使用
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import (
    CGConv, global_mean_pool, global_add_pool, 
    NNConv, GATConv
)
from torch_geometric.data import Data, DataLoader
import numpy as np

class CrystalGNN(nn.Module):
    """
    Crystal Graph Neural Network
    
    参考: Xie & Grossman (2018) "Crystal Graph Convolutional Neural Networks
    for an Accurate and Interpretable Prediction of Material Properties"
    """
    
    def __init__(
        self,
        node_features: int = 35,   # ノード特徴量次元
        edge_features: int = 64,   # エッジ特徴量次元(Gaussian basis)
        hidden_dim: int = 128,
        num_conv_layers: int = 4,
        output_dim: int = 1,       # 予測値の次元(回帰: 1)
        dropout: float = 0.1,
    ):
        super().__init__()
        
        # 初期埋め込み層
        self.node_embedding = nn.Linear(node_features, hidden_dim)
        
        # Crystal Graph Convolution 層(CGConv は PyG に実装済み)
        self.conv_layers = nn.ModuleList([
            CGConv(
                channels=hidden_dim,
                dim=edge_features,
                batch_norm=True,
            )
            for _ in range(num_conv_layers)
        ])
        
        # グローバルプーリング後の全結合層
        self.fc_layers = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),
            nn.BatchNorm1d(hidden_dim),
            nn.SiLU(),  # Swish activation
            nn.Dropout(dropout),
            nn.Linear(hidden_dim, hidden_dim // 2),
            nn.SiLU(),
            nn.Linear(hidden_dim // 2, output_dim),
        )
    
    def forward(self, data: Data) -> torch.Tensor:
        x, edge_index, edge_attr = data.x, data.edge_index, data.edge_attr
        
        # ノード特徴量を埋め込み空間へ
        x = self.node_embedding(x)
        x = F.silu(x)
        
        # グラフ畳み込み
        for conv in self.conv_layers:
            x_new = conv(x, edge_index, edge_attr)
            x = x + x_new  # Residual connection
            x = F.silu(x)
        
        # グラフ全体を1ベクトルに集約(平均プーリング)
        x = global_mean_pool(x, data.batch)
        
        # 予測
        out = self.fc_layers(x)
        return out.squeeze(-1)


class MaterialsGNNTrainer:
    """GNNモデルの学習・評価を管理するクラス"""
    
    def __init__(
        self,
        model: CrystalGNN,
        target: str = "band_gap",  # 予測ターゲット
        device: str = "auto",
    ):
        self.model = model
        self.target = target
        
        if device == "auto":
            self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        else:
            self.device = torch.device(device)
        
        self.model.to(self.device)
        print(f"使用デバイス: {self.device}")
        print(f"モデルパラメータ数: {sum(p.numel() for p in model.parameters()):,}")
    
    def train_epoch(self, loader: DataLoader, optimizer, criterion) -> float:
        self.model.train()
        total_loss = 0.0
        
        for batch in loader:
            batch = batch.to(self.device)
            optimizer.zero_grad()
            
            pred = self.model(batch)
            target = batch.y.float()
            
            loss = criterion(pred, target)
            loss.backward()
            
            # Gradient clipping for stability
            torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
            optimizer.step()
            total_loss += loss.item()
        
        return total_loss / len(loader)
    
    @torch.no_grad()
    def evaluate(self, loader: DataLoader) -> dict:
        self.model.eval()
        preds, targets = [], []
        
        for batch in loader:
            batch = batch.to(self.device)
            pred = self.model(batch)
            preds.extend(pred.cpu().numpy())
            targets.extend(batch.y.cpu().numpy())
        
        preds = np.array(preds)
        targets = np.array(targets)
        
        mae = np.mean(np.abs(preds - targets))
        rmse = np.sqrt(np.mean((preds - targets) ** 2))
        
        return {"MAE": mae, "RMSE": rmse, "predictions": preds, "targets": targets}
    
    def train(
        self,
        train_loader: DataLoader,
        val_loader: DataLoader,
        epochs: int = 100,
        lr: float = 1e-3,
        patience: int = 20,
    ) -> dict:
        """学習ループ(Early Stopping付き)"""
        optimizer = torch.optim.Adam(self.model.parameters(), lr=lr)
        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
            optimizer, patience=10, factor=0.5, min_lr=1e-5
        )
        criterion = nn.MSELoss()
        
        history = {"train_loss": [], "val_mae": [], "val_rmse": []}
        best_val_mae = float("inf")
        patience_counter = 0
        
        for epoch in range(epochs):
            train_loss = self.train_epoch(train_loader, optimizer, criterion)
            val_metrics = self.evaluate(val_loader)
            
            scheduler.step(val_metrics["MAE"])
            
            history["train_loss"].append(train_loss)
            history["val_mae"].append(val_metrics["MAE"])
            history["val_rmse"].append(val_metrics["RMSE"])
            
            if epoch % 10 == 0:
                print(
                    f"Epoch {epoch:3d} | "
                    f"Train Loss: {train_loss:.4f} | "
                    f"Val MAE: {val_metrics['MAE']:.4f} eV | "
                    f"Val RMSE: {val_metrics['RMSE']:.4f} eV"
                )
            
            # Early Stopping
            if val_metrics["MAE"] < best_val_mae:
                best_val_mae = val_metrics["MAE"]
                torch.save(self.model.state_dict(), "best_gnn_model.pth")
                patience_counter = 0
            else:
                patience_counter += 1
                if patience_counter >= patience:
                    print(f"Early Stopping at epoch {epoch}")
                    break
        
        print(f"\n最良 Val MAE: {best_val_mae:.4f} eV")
        return history


# 学習実行例
if __name__ == "__main__":
    from torch_geometric.data import DataLoader
    from crystal_graph import structure_to_graph
    
    # ダミーデータで動作確認
    import random
    
    def create_dummy_dataset(n: int = 100):
        """動作確認用ダミーデータセット生成"""
        from torch_geometric.data import Data
        dataset = []
        for _ in range(n):
            n_atoms = random.randint(2, 10)
            x = torch.randn(n_atoms, 35)
            # ランダムエッジ
            n_edges = n_atoms * 3
            edge_index = torch.randint(0, n_atoms, (2, n_edges))
            edge_attr = torch.randn(n_edges, 64)
            y = torch.tensor([random.uniform(0, 5)])  # バンドギャップ (eV)
            dataset.append(Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y))
        return dataset
    
    dataset = create_dummy_dataset(200)
    train_data = dataset[:160]
    val_data = dataset[160:]
    
    train_loader = DataLoader(train_data, batch_size=32, shuffle=True)
    val_loader = DataLoader(val_data, batch_size=32)
    
    model = CrystalGNN(node_features=35, edge_features=64, hidden_dim=128, num_conv_layers=4)
    trainer = MaterialsGNNTrainer(model, target="band_gap")
    history = trainer.train(train_loader, val_loader, epochs=50)

Step 3: 逆設計(Inverse Design)— 目標特性から構造を生成

"""
inverse_design.py
目標特性(バンドギャップ・形成エネルギー)から結晶構造を逆設計
Crystal Diffusion Variational AutoEncoder (CDVAE) の簡易実装
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Optional

class CrystalVAE(nn.Module):
    """
    結晶構造の変分オートエンコーダー
    潜在空間から新しい結晶構造を生成
    
    参考: CDVAE (Xie et al., 2021), DiffCSP (Jiao et al., 2023)
    """
    
    def __init__(
        self,
        input_dim: int = 256,    # 入力特徴量次元(GNNの出力)
        latent_dim: int = 64,    # 潜在空間次元
        condition_dim: int = 4,  # 条件(バンドギャップ, 形成E, etc.)
    ):
        super().__init__()
        self.latent_dim = latent_dim
        
        # エンコーダー
        self.encoder = nn.Sequential(
            nn.Linear(input_dim + condition_dim, 512),
            nn.LayerNorm(512),
            nn.SiLU(),
            nn.Linear(512, 256),
            nn.SiLU(),
        )
        self.fc_mu = nn.Linear(256, latent_dim)
        self.fc_logvar = nn.Linear(256, latent_dim)
        
        # デコーダー(結晶構造パラメータを再構成)
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim + condition_dim, 256),
            nn.LayerNorm(256),
            nn.SiLU(),
            nn.Linear(256, 512),
            nn.SiLU(),
            nn.Linear(512, input_dim),  # 構造特徴量の再構成
        )
        
        # 格子定数予測ヘッド (a, b, c, α, β, γ)
        self.lattice_head = nn.Sequential(
            nn.Linear(latent_dim, 64),
            nn.SiLU(),
            nn.Linear(64, 6),
            nn.Softplus(),  # 正値を保証
        )
    
    def encode(self, x: torch.Tensor, condition: torch.Tensor):
        h = self.encoder(torch.cat([x, condition], dim=-1))
        return self.fc_mu(h), self.fc_logvar(h)
    
    def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
        """Reparameterization trick"""
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std
    
    def decode(self, z: torch.Tensor, condition: torch.Tensor) -> dict:
        zc = torch.cat([z, condition], dim=-1)
        recon = self.decoder(zc)
        lattice = self.lattice_head(z)  # [a, b, c, α, β, γ]
        return {"reconstruction": recon, "lattice": lattice}
    
    def forward(self, x: torch.Tensor, condition: torch.Tensor) -> dict:
        mu, logvar = self.encode(x, condition)
        z = self.reparameterize(mu, logvar)
        decoded = self.decode(z, condition)
        return {**decoded, "mu": mu, "logvar": logvar, "z": z}
    
    def vae_loss(self, recon: torch.Tensor, x: torch.Tensor,
                  mu: torch.Tensor, logvar: torch.Tensor,
                  beta: float = 1.0) -> torch.Tensor:
        """VAE損失 = 再構成誤差 + β * KLダイバージェンス"""
        recon_loss = F.mse_loss(recon, x, reduction="sum")
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        return recon_loss + beta * kl_loss


class PropertyGuidedSampler:
    """
    目標特性を指定して結晶構造を生成するサンプラー
    Bayesian Optimization + VAE latent space exploration
    """
    
    def __init__(self, vae: CrystalVAE, predictor: nn.Module):
        """
        Args:
            vae: 学習済み CrystalVAE
            predictor: バンドギャップ等を予測する GNN
        """
        self.vae = vae
        self.predictor = predictor
        self.device = next(vae.parameters()).device
    
    def sample_with_target(
        self, 
        target_band_gap: float,
        target_formation_energy: float = -2.0,
        n_samples: int = 100,
        n_iterations: int = 500,
        temperature: float = 1.0,
    ) -> list[dict]:
        """
        目標バンドギャップを持つ構造を逆設計
        
        Args:
            target_band_gap: 目標バンドギャップ [eV]
            target_formation_energy: 目標形成エネルギー [eV/atom]
            n_samples: 生成する候補数
            n_iterations: 最適化イテレーション数
            temperature: サンプリング温度(高いほど多様性UP)
        
        Returns:
            候補構造のリスト(予測特性付き)
        """
        self.vae.eval()
        
        condition = torch.tensor(
            [[target_band_gap, target_formation_energy, 0, 0]],
            device=self.device, dtype=torch.float
        ).expand(n_samples, -1)
        
        # ランダム初期化から最適化
        z = torch.randn(n_samples, self.vae.latent_dim, 
                        device=self.device, requires_grad=True)
        optimizer = torch.optim.Adam([z], lr=0.01)
        
        for i in range(n_iterations):
            optimizer.zero_grad()
            
            # デコード
            decoded = self.vae.decode(z * temperature, condition)
            lattice_params = decoded["lattice"]
            
            # 目標特性との差を損失に(プレースホルダー)
            # 実際は GNN predictor で予測して目標との差を最小化
            lattice_loss = torch.sum(
                (lattice_params[:, :3] - 5.0) ** 2  # 格子定数 ~5Å を目標
            )
            
            # 潜在空間の正則化
            reg_loss = 0.01 * torch.sum(z ** 2)
            
            loss = lattice_loss + reg_loss
            loss.backward()
            optimizer.step()
            
            if i % 100 == 0:
                print(f"  Iter {i}: loss={loss.item():.4f}, "
                      f"avg_a={lattice_params[:, 0].mean().item():.2f}Å")
        
        # 生成結果をまとめる
        results = []
        with torch.no_grad():
            decoded = self.vae.decode(z, condition)
            lattice = decoded["lattice"].cpu().numpy()
            
            for idx in range(n_samples):
                results.append({
                    "idx": idx,
                    "target_band_gap": target_band_gap,
                    "lattice_a": float(lattice[idx, 0]),
                    "lattice_b": float(lattice[idx, 1]),
                    "lattice_c": float(lattice[idx, 2]),
                    "lattice_alpha": float(lattice[idx, 3]),
                    "latent_vector": z[idx].detach().cpu().numpy().tolist(),
                })
        
        print(f"\n{n_samples} 件の候補構造を生成しました")
        return results


# 逆設計の実行例
if __name__ == "__main__":
    print("=== 逆設計デモ ===")
    print("目標: バンドギャップ 2.0 eV の半導体材料")
    
    vae = CrystalVAE(input_dim=128, latent_dim=64, condition_dim=4)
    
    # ダミー予測器(実際は学習済みGNNを使用)
    dummy_predictor = nn.Linear(128, 1)
    
    sampler = PropertyGuidedSampler(vae, dummy_predictor)
    candidates = sampler.sample_with_target(
        target_band_gap=2.0,
        n_samples=10,
        n_iterations=200,
    )
    
    print("\n生成された候補構造(上位3件):")
    for c in candidates[:3]:
        print(f"  候補 {c['idx']}: a={c['lattice_a']:.2f}Å, "
              f"b={c['lattice_b']:.2f}Å, c={c['lattice_c']:.2f}Å")

Step 4: ASE で構造の最適化・DFTとの連携

"""
structure_optimizer.py
ASE(Atomic Simulation Environment)で構造緩和・DFT計算の前処理
"""

from ase import Atoms
from ase.optimize import BFGS, LBFGS
from ase.io import write, read
from ase.build import bulk, surface, add_adsorbate
import numpy as np
from pymatgen.core import Structure
from pymatgen.io.ase import AseAtomsAdaptor

class StructureOptimizer:
    """ASE を使った結晶構造の前処理・最適化クラス"""
    
    def __init__(self):
        self.adaptor = AseAtomsAdaptor()
    
    def pymatgen_to_ase(self, structure: Structure) -> Atoms:
        """pymatgen Structure → ASE Atoms 変換"""
        return self.adaptor.get_atoms(structure)
    
    def ase_to_pymatgen(self, atoms: Atoms) -> Structure:
        """ASE Atoms → pymatgen Structure 変換"""
        return self.adaptor.get_structure(atoms)
    
    def create_battery_anode(self) -> Atoms:
        """
        シリコンアノード構造の生成例
        全固体電池の負極材料(Si は理論容量 3579 mAh/g)
        """
        # ダイヤモンド構造のシリコン
        si = bulk("Si", "diamond", a=5.43)  # 格子定数 5.43 Å
        si = si.repeat([2, 2, 2])  # 2×2×2 スーパーセル
        
        print(f"Si アノード構造: {len(si)} 原子, 体積={si.get_volume():.1f} ų")
        return si
    
    def create_perovskite(self, A: str = "Ba", B: str = "Ti", X: str = "O") -> Atoms:
        """
        ペロブスカイト構造 ABO3 を生成
        太陽電池・強誘電体材料として重要
        """
        from ase.build import bulk
        
        # BaTiO3 ペロブスカイト(立方晶)
        a = 4.01  # 格子定数 [Å]
        
        atoms = Atoms(
            symbols=[A, B, X, X, X],
            positions=[
                [0, 0, 0],          # A サイト(Ba)
                [a/2, a/2, a/2],    # B サイト(Ti)
                [a/2, a/2, 0],      # O1
                [a/2, 0, a/2],      # O2
                [0, a/2, a/2],      # O3
            ],
            cell=[a, a, a],
            pbc=True,
        )
        
        print(f"ペロブスカイト {A}{B}{X}3: {len(atoms)} 原子")
        return atoms
    
    def create_catalyst_surface(
        self, 
        element: str = "Pt", 
        facet: tuple = (1, 1, 1),
        n_layers: int = 4,
    ) -> Atoms:
        """
        触媒表面モデルの生成
        燃料電池・CO2還元触媒の設計に使用
        """
        slab = surface(element, facet, n_layers, vacuum=10.0)
        slab = slab.repeat([3, 3, 1])  # 3×3 スーパーセル
        
        # CO2 吸着分子を配置
        add_adsorbate(slab, "C", height=2.0, position=(slab.cell[0, 0]/2, slab.cell[1, 1]/2))
        
        print(f"{element}({facet[0]}{facet[1]}{facet[2]}) 触媒表面: {len(slab)} 原子")
        return slab
    
    def relax_structure_with_emt(self, atoms: Atoms, fmax: float = 0.05) -> Atoms:
        """
        EMT(Effective Medium Theory)ポテンシャルで構造緩和
        DFT の前処理・動作確認用(金属のみ対応)
        
        本番では VASP/Quantum ESPRESSO/CP2K を使用する
        """
        from ase.calculators.emt import EMT
        
        atoms.calc = EMT()
        
        # エネルギーと力の初期値
        e_initial = atoms.get_potential_energy()
        f_initial = np.max(np.abs(atoms.get_forces()))
        print(f"初期エネルギー: {e_initial:.4f} eV")
        print(f"初期最大力: {f_initial:.4f} eV/Å")
        
        # BFGS 法で構造最適化
        optimizer = BFGS(atoms, trajectory="relaxation.traj", logfile="relax.log")
        optimizer.run(fmax=fmax)
        
        e_final = atoms.get_potential_energy()
        print(f"最終エネルギー: {e_final:.4f} eV")
        print(f"エネルギー差: {e_final - e_initial:.4f} eV")
        
        return atoms
    
    def export_for_dft(self, atoms: Atoms, format: str = "vasp") -> str:
        """
        DFT計算ソフト向けに構造ファイルを出力
        
        対応形式:
            - "vasp": VASP の POSCAR ファイル
            - "espresso-in": Quantum ESPRESSO の入力ファイル
            - "cif": CIF 形式(汎用)
        """
        filename_map = {
            "vasp": "POSCAR",
            "espresso-in": "structure.in",
            "cif": "structure.cif",
        }
        filename = filename_map.get(format, "structure.cif")
        write(filename, atoms, format=format)
        print(f"構造ファイルを出力: {filename}")
        return filename


# 使用例
if __name__ == "__main__":
    optimizer = StructureOptimizer()
    
    # 全固体電池アノード(Si)の構造生成
    si_anode = optimizer.create_battery_anode()
    
    # ペロブスカイト太陽電池材料(BaTiO3)
    batio3 = optimizer.create_perovskite("Ba", "Ti", "O")
    
    # Pt(111) 触媒表面
    pt_surface = optimizer.create_catalyst_surface("Pt", (1, 1, 1))
    
    # DFT用ファイルに出力
    optimizer.export_for_dft(batio3, format="cif")
    
    # 簡易緩和(EMTポテンシャル、金属系のみ動作確認用)
    from ase.build import bulk
    au_bulk = bulk("Au", "fcc", a=4.08).repeat([2, 2, 2])
    relaxed_au = optimizer.relax_structure_with_emt(au_bulk)

Step 5: VQE による量子化学シミュレーション

"""
vqe_materials.py
Variational Quantum Eigensolver (VQE) で電子構造を量子計算
小分子・活性サイトのエネルギーを精密計算
"""

from qiskit import QuantumCircuit
from qiskit.primitives import Estimator
from qiskit_nature.second_q.drivers import PySCFDriver
from qiskit_nature.second_q.mappers import JordanWignerMapper, ParityMapper
from qiskit_nature.second_q.circuit.library import (
    UCCSD, HartreeFock, EvolvedOperatorAnsatz
)
from qiskit.algorithms.optimizers import COBYLA, L_BFGS_B, SPSA
from qiskit.algorithms.minimum_eigensolvers import VQE
import numpy as np

class QuantumChemistrySimulator:
    """
    VQE を使った量子化学計算
    触媒活性サイト・電解質分子のエネルギー精密計算に使用
    """
    
    def __init__(self):
        self.mapper = JordanWignerMapper()  # フェルミオン → クービット変換
    
    def compute_h2_ground_state(self, bond_length: float = 0.74) -> dict:
        """
        水素分子 H2 の基底状態エネルギーを VQE で計算
        最も基本的な量子化学ベンチマーク
        
        Args:
            bond_length: H-H 結合距離 [Å]
        
        Returns:
            {"energy": float, "hartree_fock_energy": float, "correlation_energy": float}
        """
        print(f"\n=== H2 VQE 計算 (bond_length={bond_length}Å) ===")
        
        # PySCF で分子軌道を計算
        driver = PySCFDriver(
            atom=f"H 0 0 0; H 0 0 {bond_length}",
            basis="sto3g",  # STO-3G 基底関数
        )
        problem = driver.run()
        
        # フェルミオン演算子 → クービット演算子
        fermionic_op = problem.hamiltonian.second_q_op()
        qubit_op = self.mapper.map(fermionic_op)
        
        print(f"量子ビット数: {qubit_op.num_qubits}")
        print(f"パウリ項数: {len(qubit_op)}")
        
        # Hartree-Fock 初期状態
        hf_state = HartreeFock(
            num_spatial_orbitals=problem.num_spatial_orbitals,
            num_particles=problem.num_particles,
            qubit_mapper=self.mapper,
        )
        
        # UCCSD アンザッツ(化学精度の VQE 標準)
        ansatz = UCCSD(
            num_spatial_orbitals=problem.num_spatial_orbitals,
            num_particles=problem.num_particles,
            qubit_mapper=self.mapper,
            initial_state=hf_state,
        )
        
        print(f"回路深さ: {ansatz.decompose().depth()}")
        print(f"パラメータ数: {ansatz.num_parameters}")
        
        # VQE 実行
        estimator = Estimator()
        optimizer = COBYLA(maxiter=500, tol=1e-6)
        
        vqe = VQE(estimator=estimator, ansatz=ansatz, optimizer=optimizer)
        result = vqe.compute_minimum_eigenvalue(qubit_op)
        
        # 核間反発エネルギーを加算して全エネルギーを求める
        nuclear_repulsion = problem.nuclear_repulsion_energy
        total_energy = result.eigenvalue.real + nuclear_repulsion
        
        # ハートリー-フォック エネルギー(参照値)
        hf_energy = problem.reference_energy
        correlation_energy = total_energy - hf_energy
        
        output = {
            "bond_length_angstrom": bond_length,
            "vqe_total_energy_hartree": total_energy,
            "hartree_fock_energy_hartree": hf_energy,
            "correlation_energy_hartree": correlation_energy,
            "n_qubits": qubit_op.num_qubits,
            "n_parameters": ansatz.num_parameters,
            "optimizer_iterations": result.optimizer_evals,
        }
        
        print(f"\nVQE 全エネルギー: {total_energy:.6f} Hartree")
        print(f"HF エネルギー:    {hf_energy:.6f} Hartree")
        print(f"相関エネルギー:   {correlation_energy:.6f} Hartree")
        
        return output
    
    def potential_energy_scan(
        self, 
        bond_lengths: list[float] = None,
    ) -> list[dict]:
        """
        H2 分子の結合距離 vs エネルギーカーブを計算
        解離エネルギーの評価・ポテンシャルエネルギー曲面の生成
        """
        if bond_lengths is None:
            bond_lengths = np.linspace(0.5, 2.5, 10).tolist()
        
        print(f"ポテンシャルエネルギーカーブを計算中 ({len(bond_lengths)} 点)")
        results = []
        
        for bl in bond_lengths:
            try:
                result = self.compute_h2_ground_state(bond_length=bl)
                results.append(result)
                print(f"  d={bl:.2f}Å: E={result['vqe_total_energy_hartree']:.6f} Ha")
            except Exception as e:
                print(f"  d={bl:.2f}Å: 計算失敗 ({e})")
        
        return results
    
    def simulate_lithium_hydride(self) -> dict:
        """
        LiH (リチウムハイドライド) の基底状態エネルギー計算
        全固体電池の電解質研究に関連
        """
        print("\n=== LiH VQE 計算 ===")
        
        driver = PySCFDriver(
            atom="Li 0 0 0; H 0 0 1.595",  # LiH 平衡結合距離
            basis="sto3g",
        )
        problem = driver.run()
        fermionic_op = problem.hamiltonian.second_q_op()
        
        # パリティマッパー(2量子ビット削減)を使用
        parity_mapper = ParityMapper(num_particles=problem.num_particles)
        qubit_op = parity_mapper.map(fermionic_op)
        
        print(f"量子ビット数(パリティマッパー): {qubit_op.num_qubits}")
        
        # UCCSD アンザッツ
        ansatz = UCCSD(
            num_spatial_orbitals=problem.num_spatial_orbitals,
            num_particles=problem.num_particles,
            qubit_mapper=parity_mapper,
            initial_state=HartreeFock(
                num_spatial_orbitals=problem.num_spatial_orbitals,
                num_particles=problem.num_particles,
                qubit_mapper=parity_mapper,
            ),
        )
        
        estimator = Estimator()
        vqe = VQE(
            estimator=estimator,
            ansatz=ansatz,
            optimizer=L_BFGS_B(maxiter=1000),
        )
        result = vqe.compute_minimum_eigenvalue(qubit_op)
        
        total_energy = result.eigenvalue.real + problem.nuclear_repulsion_energy
        print(f"LiH 基底状態エネルギー: {total_energy:.6f} Hartree")
        
        return {"molecule": "LiH", "ground_state_energy_hartree": total_energy}


# 量子化学デモ
if __name__ == "__main__":
    simulator = QuantumChemistrySimulator()
    
    # H2 の基底状態
    h2_result = simulator.compute_h2_ground_state(bond_length=0.74)
    
    # ポテンシャルエネルギーカーブ(5点)
    pes = simulator.potential_energy_scan(
        bond_lengths=[0.5, 0.7, 0.74, 1.0, 1.5]
    )
    
    print("\n=== ポテンシャルエネルギーカーブ ===")
    for r in pes:
        print(f"  d={r['bond_length_angstrom']:.2f}Å: "
              f"E={r['vqe_total_energy_hartree']:.6f} Ha")

Step 6: Matlantis API 連携(機械学習力場)

"""
matlantis_client.py
Preferred Networks の Matlantis(汎用機械学習力場)を使った高速構造緩和
DFT と同等精度で 1000倍以上高速
※ Matlantis の利用には有料サブスクリプションが必要
"""

import numpy as np

class MatlantisSimulator:
    """
    Matlantis MLFF(Machine Learning Force Field)ラッパー
    
    DFT比:
      精度: ほぼ同等(MAE ~0.1 eV/Å)
      速度: 1000-10000倍高速
      対応元素: 72 元素
    
    実際の使用には pfcc-extras パッケージとAPIキーが必要:
      pip install pfcc-extras
      export MATLANTIS_TOKEN=your_token
    """
    
    def relax_structure(self, atoms) -> dict:
        """
        構造緩和のデモ(Matlantis実際の呼び出し例)
        
        実際のコード(APIキー取得後):
        
        from pfcc_extras.structure.ase_interface import ASEInterface
        from ase.optimize import FIRE
        
        calc = ASEInterface(model_type="universal-fast")  # または "universal-accurate"
        atoms.calc = calc
        
        optimizer = FIRE(atoms, logfile="matlantis_relax.log")
        optimizer.run(fmax=0.03)  # 最大力 0.03 eV/Å で収束
        
        energy = atoms.get_potential_energy()
        forces = atoms.get_forces()
        stress = atoms.get_stress()
        
        return {
            "energy": energy,
            "forces": forces,
            "stress": stress,
            "n_steps": optimizer.get_number_of_steps(),
        }
        """
        # デモ用のダミー返値
        print("=== Matlantis 構造緩和(デモ) ===")
        n_atoms = 10
        return {
            "energy": -50.0 + np.random.normal(0, 0.1),
            "forces": np.random.randn(n_atoms, 3) * 0.01,
            "max_force": 0.025,
            "n_steps": 87,
            "converged": True,
        }
    
    def compute_phonons(self, atoms, supercell_matrix=None) -> dict:
        """
        フォノン分散計算(熱力学安定性の評価)
        
        実際のコード:
        from pfcc_extras.structure.ase_interface import ASEInterface
        from phonopy import Phonopy
        from phonopy.structure.atoms import PhonopyAtoms
        
        calc = ASEInterface(model_type="universal-accurate")
        # phonopy と連携してフォノン計算...
        """
        print("フォノン分散計算中... (デモ)")
        
        # 架空のフォノン分散(実際は Phonopy で計算)
        q_points = np.linspace(0, 1, 50)
        n_bands = 6  # 2原子セルのフォノン
        frequencies = np.abs(np.random.randn(len(q_points), n_bands) * 5 + 10)
        
        has_imaginary = np.any(frequencies < 0)
        
        return {
            "q_points": q_points.tolist(),
            "frequencies_THz": frequencies.tolist(),
            "has_imaginary_modes": has_imaginary,
            "dynamically_stable": not has_imaginary,
        }
    
    def screen_candidates(
        self, 
        structures: list,
        target_property: str = "band_gap",
        target_value: float = 2.0,
    ) -> list[dict]:
        """
        大量候補構造のハイスループットスクリーニング
        
        GNoME が生成した 220万構造を Matlantis でスクリーニングする流れ:
        1. Matlantis で全構造を高速緩和(DFTより1000倍速い)
        2. 安定性フィルタ(energy_above_hull < 50meV/atom)
        3. 目標特性フィルタ(バンドギャップ・弾性率等)
        4. フォノン計算で動的安定性確認
        5. DFT で最終確認(上位候補のみ)
        """
        print(f"\n=== ハイスループットスクリーニング ===")
        print(f"候補数: {len(structures)}")
        print(f"目標特性: {target_property}{target_value}")
        
        results = []
        for i, struct in enumerate(structures):
            # デモ用ダミー計算
            relax_result = self.relax_structure(struct)
            
            # 架空の予測バンドギャップ
            predicted_property = target_value + np.random.normal(0, 0.5)
            
            results.append({
                "idx": i,
                "energy": relax_result["energy"],
                "converged": relax_result["converged"],
                f"predicted_{target_property}": predicted_property,
                "property_error": abs(predicted_property - target_value),
                "pass_filter": abs(predicted_property - target_value) < 0.3,
            })
        
        passed = [r for r in results if r["pass_filter"]]
        print(f"フィルター通過: {len(passed)}/{len(results)}")
        
        return sorted(results, key=lambda x: x["property_error"])

Step 7: 統合パイプライン

"""
mi_pipeline.py
マテリアルズ・インフォマティクス統合パイプライン
データ取得 → 特徴量抽出 → GNN学習 → 逆設計 → スクリーニング → DFT検証
"""

import json
import pandas as pd
import numpy as np
from pathlib import Path

class MaterialsInformaticsPipeline:
    """
    MI の完全パイプライン統合管理
    
    フェーズ:
      1. データ収集(Materials Project / ICSD)
      2. 特徴量エンジニアリング(matminer)
      3. GNN 学習(CGCNN)
      4. 逆設計(Crystal VAE)
      5. 高速スクリーニング(Matlantis MLFF)
      6. DFT 検証(VASP / Quantum ESPRESSO)
    """
    
    def __init__(self, target_property: str = "band_gap", output_dir: str = "./mi_results"):
        self.target_property = target_property
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        
        print(f"MI パイプライン初期化")
        print(f"  目標特性: {target_property}")
        print(f"  出力ディレクトリ: {self.output_dir}")
    
    def phase1_data_collection(self, elements: list[str], n_max: int = 5000) -> pd.DataFrame:
        """フェーズ1: データ収集"""
        print(f"\n{'='*50}")
        print(f"フェーズ1: データ収集")
        print(f"  対象元素: {elements}")
        
        # 実際は MaterialsDataFetcher を使用
        # fetcher = MaterialsDataFetcher()
        # df = fetcher.fetch_materials_by_elements(elements)
        
        # デモ用ダミーデータ
        np.random.seed(42)
        n = min(n_max, 1000)
        df = pd.DataFrame({
            "material_id": [f"mp-{i:05d}" for i in range(n)],
            "formula": [f"{''.join(np.random.choice(elements, np.random.randint(2, 4)))}" for _ in range(n)],
            "band_gap": np.abs(np.random.randn(n) * 2.0),
            "formation_energy": -np.abs(np.random.randn(n) * 2.0),
            "energy_above_hull": np.abs(np.random.randn(n) * 0.1),
            "is_stable": np.random.random(n) > 0.3,
        })
        
        stable_df = df[df["is_stable"]]
        print(f"  取得件数: {len(df)}")
        print(f"  安定材料: {len(stable_df)}")
        
        df.to_csv(self.output_dir / "raw_materials.csv", index=False)
        return df
    
    def phase2_featurize(self, df: pd.DataFrame) -> pd.DataFrame:
        """フェーズ2: 特徴量エンジニアリング"""
        print(f"\n{'='*50}")
        print(f"フェーズ2: 特徴量エンジニアリング")
        
        # 実際は MaterialFeatureExtractor を使用
        # extractor = MaterialFeatureExtractor()
        # df = extractor.extract_composition_features(df)
        
        # デモ: ダミー特徴量
        n_features = 50
        feature_cols = [f"feature_{i}" for i in range(n_features)]
        feature_matrix = np.random.randn(len(df), n_features)
        feature_df = pd.DataFrame(feature_matrix, columns=feature_cols)
        
        df = pd.concat([df.reset_index(drop=True), feature_df], axis=1)
        print(f"  特徴量次元: {n_features}")
        
        df.to_csv(self.output_dir / "featurized_materials.csv", index=False)
        return df
    
    def phase3_train_gnn(self, df: pd.DataFrame) -> dict:
        """フェーズ3: GNN 学習"""
        print(f"\n{'='*50}")
        print(f"フェーズ3: GNN 学習 (target={self.target_property})")
        
        # 実際は CrystalGNN + MaterialsGNNTrainer を使用
        # trainer = MaterialsGNNTrainer(model, target=self.target_property)
        # history = trainer.train(train_loader, val_loader)
        
        # デモ: モック結果
        metrics = {
            "best_val_mae_eV": 0.12,
            "best_val_rmse_eV": 0.18,
            "epochs_trained": 87,
            "model_path": str(self.output_dir / "best_gnn_model.pth"),
        }
        
        print(f"  最良 Val MAE: {metrics['best_val_mae_eV']:.3f} eV")
        print(f"  最良 Val RMSE: {metrics['best_val_rmse_eV']:.3f} eV")
        
        with open(self.output_dir / "gnn_metrics.json", "w") as f:
            json.dump(metrics, f, indent=2)
        
        return metrics
    
    def phase4_inverse_design(
        self, 
        target_value: float,
        n_candidates: int = 1000,
    ) -> list[dict]:
        """フェーズ4: 逆設計"""
        print(f"\n{'='*50}")
        print(f"フェーズ4: 逆設計")
        print(f"  目標 {self.target_property}: {target_value}")
        
        # 実際は PropertyGuidedSampler を使用
        # sampler = PropertyGuidedSampler(vae, predictor)
        # candidates = sampler.sample_with_target(target_band_gap=target_value)
        
        # デモ: ダミー候補
        candidates = [
            {
                "idx": i,
                "predicted_band_gap": target_value + np.random.normal(0, 0.3),
                "predicted_formation_energy": -2.0 + np.random.normal(0, 0.5),
                "lattice_a": 4.0 + np.random.normal(0, 0.5),
            }
            for i in range(n_candidates)
        ]
        
        # 目標値に近い候補を選択
        candidates.sort(key=lambda x: abs(x["predicted_band_gap"] - target_value))
        top_candidates = candidates[:100]  # 上位100件
        
        print(f"  生成候補: {n_candidates}")
        print(f"  目標値付近(±0.3eV): {len([c for c in candidates if abs(c['predicted_band_gap'] - target_value) < 0.3])}")
        
        return top_candidates
    
    def phase5_screening(self, candidates: list[dict]) -> list[dict]:
        """フェーズ5: Matlantis でハイスループットスクリーニング"""
        print(f"\n{'='*50}")
        print(f"フェーズ5: ハイスループットスクリーニング")
        print(f"  候補数: {len(candidates)}")
        
        # 安定性・フォノン安定性でフィルタリング
        screened = []
        for c in candidates:
            # Matlantis で緩和・安定性チェック(デモ)
            energy_above_hull = np.abs(np.random.randn()) * 0.05
            dynamically_stable = np.random.random() > 0.3
            
            if energy_above_hull < 0.05 and dynamically_stable:
                c["energy_above_hull"] = energy_above_hull
                c["dynamically_stable"] = dynamically_stable
                screened.append(c)
        
        print(f"  スクリーニング通過: {len(screened)}/{len(candidates)}")
        return screened
    
    def phase6_dft_validation(self, candidates: list[dict], top_n: int = 10) -> list[dict]:
        """フェーズ6: DFT最終検証"""
        print(f"\n{'='*50}")
        print(f"フェーズ6: DFT 最終検証(上位 {top_n} 件)")
        
        # 実際は VASP/Quantum ESPRESSO ジョブを投入
        # ASEInterface + VASPCalculator 等を使用
        
        validated = []
        for c in candidates[:top_n]:
            # DFT 計算結果(デモ)
            c["dft_band_gap"] = c["predicted_band_gap"] + np.random.normal(0, 0.1)
            c["dft_formation_energy"] = c["predicted_formation_energy"] + np.random.normal(0, 0.05)
            c["dft_validated"] = True
            validated.append(c)
            print(f"  候補 {c['idx']}: "
                  f"予測Eg={c['predicted_band_gap']:.2f}eV, "
                  f"DFT Eg={c['dft_band_gap']:.2f}eV")
        
        return validated
    
    def run_full_pipeline(
        self,
        elements: list[str] = ["Li", "Na", "O", "S", "P"],
        target_value: float = 2.0,
    ) -> dict:
        """全フェーズを順次実行"""
        print("\n" + "="*60)
        print("マテリアルズ・インフォマティクス パイプライン 開始")
        print("="*60)
        
        df = self.phase1_data_collection(elements)
        df = self.phase2_featurize(df)
        metrics = self.phase3_train_gnn(df)
        candidates = self.phase4_inverse_design(target_value, n_candidates=500)
        screened = self.phase5_screening(candidates)
        validated = self.phase6_dft_validation(screened, top_n=5)
        
        summary = {
            "total_database_size": len(df),
            "gnn_val_mae": metrics["best_val_mae_eV"],
            "candidates_generated": len(candidates),
            "after_screening": len(screened),
            "dft_validated": len(validated),
            "top_candidates": validated,
        }
        
        print("\n" + "="*60)
        print("=== パイプライン完了 ===")
        print(f"  データベース: {summary['total_database_size']}")
        print(f"  GNN MAE:      {summary['gnn_val_mae']:.3f} eV")
        print(f"  逆設計候補:   {summary['candidates_generated']}")
        print(f"  スクリーニング後: {summary['after_screening']}")
        print(f"  DFT検証済み:  {summary['dft_validated']}")
        print("="*60)
        
        with open(self.output_dir / "pipeline_summary.json", "w", encoding="utf-8") as f:
            json.dump(summary, f, ensure_ascii=False, indent=2)
        
        return summary


# パイプライン実行
if __name__ == "__main__":
    pipeline = MaterialsInformaticsPipeline(
        target_property="band_gap",
        output_dir="./mi_battery_results"
    )
    
    # 全固体電池材料の探索
    result = pipeline.run_full_pipeline(
        elements=["Li", "Na", "O", "S", "P", "Si"],
        target_value=2.5,  # 目標バンドギャップ 2.5 eV(固体電解質向け)
    )

実装フェーズロードマップ

フェーズ 期間 内容 成果物
Phase 1 1〜2週 pymatgen + Materials Project API でデータ収集・可視化 材料DB CSV, 可視化ダッシュボード
Phase 2 2〜4週 matminer で特徴量エンジニアリング → ランダムフォレスト/XGBoost でベースライン構築 ベースライン予測モデル(MAE < 0.5eV)
Phase 3 1〜2ヶ月 CGCNN/MEGNET で GNN 実装 → Materials Project 全データで学習 GNN モデル(MAE < 0.2eV)
Phase 4 2〜3ヶ月 Crystal VAE / DiffCSP で逆設計パイプライン構築 新規候補構造の自動生成
Phase 5 3〜6ヶ月 Matlantis API でハイスループットスクリーニング 上位候補リスト(安定性・目標特性)
Phase 6 継続 DFT (VASP/QE) → 合成実験 → フィードバックループ 論文・特許

主要ライブラリ比較

ライブラリ 用途 特徴
pymatgen 結晶構造操作・解析 Materials Project 標準、豊富な解析機能
matminer 特徴量エンジニアリング Magpie/SOAP/ElementProperty 等 280+ 特徴量
PyTorch Geometric GNN 実装 CGConv/SchNet/DimeNet++ 内蔵
DGL GNN 実装(別系統) HeteroGraph 対応、MPNN/AttentiveFP
ASE 原子シミュレーション DFT コード(VASP, QE, CP2K)との I/O
Qiskit Nature 量子化学 VQE/QEOM、PySCF/GAMESS ドライバー
Matlantis ML力場(商用) 72元素対応、DFT比 1000x 高速
CHGNET ML力場(OSS) 磁気モーメント対応、無料
ALIGNN GNN(線グラフ) 角度情報を含む高精度モデル

まとめ

従来の材料探索(200年かかる)
  └── 試行錯誤 → DFT → 合成 → 評価 → 繰り返し

マテリアルズ・インフォマティクス(AIで加速)
  ├── GNN でバンドギャップ・形成エネルギーを予測(MAE < 0.1eV)
  ├── 逆設計で目標特性から構造を自動生成
  ├── Matlantis で DFT 比 1000倍速スクリーニング
  └── 量子コンピュータ(VQE)で難しい電子構造を精密計算

応用分野:
  全固体電池(Li固体電解質)  → Eg > 3eV、高イオン伝導率
  室温超伝導体                → 高転移温度 Tc
  太陽電池                    → Eg ≈ 1.3eV(Shockley-Queisser 最適)
  パワー半導体(GaN/Ga₂O₃)  → 広バンドギャップ、高耐圧
  CO₂還元触媒                 → 吸着エネルギー最適化

参考リンク


元記事: 新素材の発見に「200年」かかっていた時代が終わる——AIと量子計算が「夢の物質」を設計する by @airbticsquantum

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?