Pythonで分子モデルの3Dビューアを作成してみた – RDKit, 3Dmol.jsから3Dモデルを生成

# 1. 開発した3Dビューア
今回は特定の分子ボタンをクリックすると３Dで観察可能な分子模型が表示可能なUIを構築した。マウス等で分子をドラッグ&ドロップする事で回転して観察可能となる。今後スケッチから分子をモデルして３D化する等の改良を加える。
![スクリーンショット 2024-12-31 16.54.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231863/9a344195-bbdd-adec-27b4-4bbba5f3ec66.png)



# 2. 使用した環境
プログラムの検証するにあたって使用した環境は以下の通りである。
#### 使用PC 
MacBook Pro 3-inch, 2017, Four Thunderbolt 3 Ports
#### Pythonの環境
Python 3.11.9

#### Pythonのライブラリ
- ##### PyQt5:
GUIアプリケーションを構築するために使用。
QApplication, QMainWindowなどのクラスでウィンドウやレイアウトを構築。
QWebEngineViewでHTMLコンテンツを表示。

- ##### rdkit:
Chemモジュールを使用して分子をSMILES形式から読み込む。
AllChemで分子の3D構造を生成し、エネルギー最適化を行う。

- ##### Bio.PDB:
分子構造をPDB形式で解析するためのライブラリ。

- ##### osとStringIO:
ファイル操作や文字列の入出力に使用。

#### その他使用したもの
3DレンダリングのためのJavaScriptライブラリ「3Dmol.js」を活用した。これは3D分子モデルの可視化を簡単に行うために設計されたオープンソースのJavaScriptライブラリ。分子シミュレーションの可視化や3Dレンダリングに利用される。主な特徴について以下に詳細を示す。

- ###### 公式ドキュメント
https://3dmol.csb.pitt.edu
- ###### 主な機能
- (a)  分子の可視化:
PDBやSDF、XYZファイルなどの化学データフォーマットをサポート。
表示スタイルの選択（リボン、ボール＆スティック、空間充填モデルなど）。
- (b)  インタラクティブな操作: 回転、ズーム、パン操作に対応。
選択された部分のハイライト表示や詳細表示。
- (c)  カスタマイズ可能:
ライティングや色、透明度の調整。
独自のスクリプトを使用して表示内容を動的に変更可能。
- (d)  WebGLを活用:
GPUのパフォーマンスを最大限に活用し、高速でリアルなレンダリングを実現。

# 3. コードの解説
### 1. 必要なライブラリのインポート
```python
from rdkit import Chem
from rdkit.Chem import AllChem
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl
from Bio.PDB import PDBParser
from io import StringIO
import os
```
### 2. アプリケーションのメインウィンドウクラス
```python
class MoleculeViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("3D Molecule Viewer")
        self.setGeometry(100, 100, 800, 600)
```
- #### MoleculeViewer:
    PyQt5のQMainWindowを継承して作成したカスタムクラス。
    アプリのメインウィンドウとして機能。
- #### setWindowTitleとsetGeometry:
    ウィンドウのタイトルを設定し、位置とサイズを指定。

### 3. Webエンジンビューとレイアウトの設定
```python
        self.browser = QWebEngineView()
        layout = QVBoxLayout()
```
- #### QWebEngineView:
    HTMLやJavaScriptを表示できるPyQt5のウィジェット。
    このアプリケーションでは、3Dmol.jsを使った分子モデルの可視化に利用。
- #### QVBoxLayout:
    垂直方向にウィジェットを配置するレイアウト。

### 4. ボタンの追加
```python
        button_layout = QHBoxLayout()
        self.buttons = {
            "メタン": "C",
            "ベンゼン": "C1=CC=CC=C1",
            "シクロヘキサン": "C1CCCCC1",
            "ブタジエン": "C=CC=C",
            "アンモニア": "N",
            "フェニルアラニン": "C1=CC=C(C=C1)CC(C(=O)O)N",
        }
        for label, smiles in self.buttons.items():
            button = QPushButton(label)
            button.clicked.connect(lambda _, s=smiles: self.load_html(s))
            button_layout.addWidget(button)
```
- #### QPushButton
    各分子のボタンを作成。
    ボタンを押すとself.load_html(smiles)が呼ばれるように設定。
- #### SMILES文字列
    3D表示したい分子モデルをSMILES文字列（分子構造の表記法）を辞書で管理。

### 5. レイアウトの組み立て
```python
        layout.addLayout(button_layout)
        layout.addWidget(self.browser)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)
        
        # 初期分子の表示
        self.load_html(self.buttons["ベンゼン"])
```
- #### レイアウト設定:
    ボタンを配置したレイアウトとQWebEngineViewを統合。
- #### 初期表示:
    起動時にベンゼンの3Dモデルを表示。

### 6. PDBデータをXYZ形式に変換
```python
    def pdb_to_xyz_string(self, pdb_content):
        parser = PDBParser(QUIET=True)
        structure = parser.get_structure('structure', StringIO(pdb_content))

        atoms = []
        for model in structure:
            for chain in model:
                for residue in chain:
                    for atom in residue:
                        atoms.append((atom.element, atom.coord))

        xyz_lines = [f"{len(atoms)}", "Converted from PDB content"]
        for element, coord in atoms:
            x, y, z = coord
            xyz_lines.append(f"{element} {x:.3f} {y:.3f} {z:.3f}")
    
        return "\n".join(xyz_lines)
```
- #### PDBパース:
    Bio.PDB.PDBParserでPDB形式のデータを解析し、原子の座標を抽出。
- #### XYZ形式変換:
    原子数と座標情報をXYZ形式（3Dmol.jsがサポート）に整形。
    PDBだと水素原子が消える問題が生じたためxyzに変換した。

### 7. 分子データの生成とHTMLの読み込み
```python
    def load_html(self, smiles):
        def generate_3d_pdb(smiles):
            mol = Chem.MolFromSmiles(smiles)
            mol = Chem.AddHs(mol, addCoords=True)
            AllChem.EmbedMolecule(mol)
            AllChem.MMFFOptimizeMolecule(mol)
            pdb_data = Chem.MolToPDBBlock(mol)
            return pdb_data
        
        pdb_data = generate_3d_pdb(smiles)
        pdb_data = self.pdb_to_xyz_string(pdb_data)

        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <script src="3Dmol-min.js"></script>
        </head>
        <body>
            <div id="viewer" style="width: 800px; height: 600px;"></div>
            <script>
                let viewer = $3Dmol.createViewer("viewer", {{ backgroundColor: "black" }});
                viewer.addModel(`{pdb_data}`, "xyz");
                viewer.setStyle({{}}, {{stick: {{radius: 0.2, hydrogen: true}}, sphere: {{scale: 0.3}}}});
                viewer.zoomTo();
                viewer.render();
            </script>
        </body>
        </html>
        """
        html_file = "molecule.html"
        with open(html_file, "w", encoding="utf-8") as f:
            f.write(html_content)
        full_path = os.path.abspath(html_file)
        self.browser.setUrl(QUrl.fromLocalFile(full_path))
```
- #### SMILESからPDBへの変換:
    RDKitを使ってSMILESから3D構造を生成。
    最適化後にPDB形式で出力。
- #### 3Dmol-min.jsのダウンロードと配置
	公式サイトまたはGitHubリポジトリから3Dmol-min.jsをダウンロードする。
    ダウンロードした3Dmol-min.jsを、Pythonスクリプト（例：molecule_viewer.py）と同じディレクトリに配置すること。
ディレクトリ構成の例：
project_directory/
├── molecule_viewer.py
├── 3Dmol-min.js 

- #### HTML生成:
    生成したXYZデータを埋め込んだHTMLファイルを作成。
    3Dmol.jsを使用して分子を可視化。

### 8. アプリケーションのエントリーポイント
```python
if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    viewer = MoleculeViewer()
    viewer.show()
    sys.exit(app.exec())
```
- #### アプリケーションの起動:
    QApplicationをインスタンス化し、MoleculeViewerを表示。

# 4. 全体プログラム
以下にプログラムの全体のコードを添付する。
```python
from rdkit import Chem
from rdkit.Chem import AllChem
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtCore import QUrl
from Bio.PDB import PDBParser
from io import StringIO
import os

class MoleculeViewer(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("3D Molecule Viewer")
        self.setGeometry(100, 100, 800, 600)

        # Webエンジンビューを作成
        self.browser = QWebEngineView()

        # レイアウト設定
        layout = QVBoxLayout()

        # ボタンを追加
        button_layout = QHBoxLayout()
        self.buttons = {
            "メタン": "C",
            "ベンゼン": "C1=CC=CC=C1",
            "シクロヘキサン": "C1CCCCC1",
            "ブタジエン": "C=CC=C",
            "アンモニア": "N",
            "フェニルアラニン": "C1=CC=C(C=C1)CC(C(=O)O)N",
        }
        for label, smiles in self.buttons.items():
            button = QPushButton(label)
            button.clicked.connect(lambda _, s=smiles: self.load_html(s))
            button_layout.addWidget(button)
        
        layout.addLayout(button_layout)
        layout.addWidget(self.browser)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)
        
        # 初期分子の表示
        self.load_html(self.buttons["ベンゼン"])

    def pdb_to_xyz_string(self, pdb_content):
        parser = PDBParser(QUIET=True)
        structure = parser.get_structure('structure', StringIO(pdb_content))

        atoms = []
        for model in structure:
            for chain in model:
                for residue in chain:
                    for atom in residue:
                        atoms.append((atom.element, atom.coord))

        xyz_lines = [f"{len(atoms)}", "Converted from PDB content"]
        for element, coord in atoms:
            x, y, z = coord
            xyz_lines.append(f"{element} {x:.3f} {y:.3f} {z:.3f}")
    
        return "\n".join(xyz_lines)

    def load_html(self, smiles):
        def generate_3d_pdb(smiles):
            mol = Chem.MolFromSmiles(smiles)  # 分子をSMILESから生成
            mol = Chem.AddHs(mol, addCoords=True)  # 水素を追加
            AllChem.EmbedMolecule(mol)  # 3D構造を生成
            AllChem.MMFFOptimizeMolecule(mol)  # エネルギー最適化
            pdb_data = Chem.MolToPDBBlock(mol)  # PDB形式でエクスポート
            return pdb_data
        
        pdb_data = generate_3d_pdb(smiles)  # 3D構造を生成
        pdb_data = self.pdb_to_xyz_string(pdb_data)

        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <script src="3Dmol-min.js"></script>
        </head>
        <body>
            <div id="viewer" style="width: 800px; height: 600px;"></div>
            <script>
                let viewer = $3Dmol.createViewer("viewer", {{ backgroundColor: "black" }});
                viewer.addModel(`{pdb_data}`, "xyz");
                viewer.setStyle({{}}, {{stick: {{radius: 0.2, hydrogen: true}}, sphere: {{scale: 0.3}}}});
                viewer.zoomTo();
                viewer.render();
            </script>
        </body>
        </html>
        """
        html_file = "molecule.html"
        with open(html_file, "w", encoding="utf-8") as f:
            f.write(html_content)
        full_path = os.path.abspath(html_file)
        self.browser.setUrl(QUrl.fromLocalFile(full_path))

if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    viewer = MoleculeViewer()
    viewer.show()
    sys.exit(app.exec())
```