mcpField
M
mdtraj-trajectory-analysis

mdtraj-trajectory-analysis

github.com/jaechang-hitsデータ・データベース38447 回インストール6 日前に更新
リポジトリ

mdtraj-trajectory-analysis について

mdtraj Trajectory Analysisは、分子動力学シミュレーション後の軌跡を解析する研究者向けのPythonスキルです。一般的な軌跡形式を読み込み、構造、幾何、接触、水素結合、二次構造を解析できます。

主な機能

  1. 01一般的な軌跡形式の読み込みとストリーミング
  2. 02RMSD、Rg、RMSFの計算
  3. 03残基間接触と距離の解析
  4. 04ねじれ角と水素結合の計算
  5. 05SASAとDSSP二次構造の計算

活用シーン

  1. 01MD反復間の安定性の比較
  2. 02残基接触頻度マップの作成
  3. 03時間経過に伴う二次構造変化の追跡
  4. 04柔軟性と水素結合の持続性の解析

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill mdtraj-trajectory-analysis

インストール後、Claude Code で/mdtraj-trajectory-analysisを入力して読み込まれたか確認します。

詳細

mdtraj のトラジェクトリ解析

概要

mdtraj は、依存関係が少ない MD トラジェクトリ解析用の Python ライブラリです。DCD/XTC/TRR/NetCDF/H5/AMBER/GROMACS/CHARMM/OpenMM 形式を読み込んで、NumPy 配列に裏打ちされた Trajectory オブジェクトに格納し、その後、幾何学的解析、RMSD/Rg/RMSF/SASA、コンタクト、水素結合、ねじれ角、8 状態 DSSP を純粋な Python 関数として提供します。

単位:mdtraj の内部単位は nm と ps です。距離を Å にするには 10 を掛け、時間を ns にするには 1000 で割ります。ねじれ角の単位は ラジアン です。np.degrees() を使用してください。

使用する場面

  • MD レプリケート間の安定性を評価するための、時間経過に伴う RMSD、Rg、残基ごとの RMSF
  • トラジェクトリアンサンブルからの残基間コンタクト頻度マップ
  • ラマチャンドランプロット用のバックボーン φ/ψ(一般、Gly、Pro)
  • 二次構造の時系列用の、フレームごとの残基単位の 8 状態 DSSP
  • MDAnalysis のフルフレームワークを使うほどではない、軽量なアドホック解析
  • NMR 観測量(J カップリング、SHIFTX2 による化学シフト)
  • MDAnalysis の選択構文、AnalysisBase の並列処理、または LAMMPS/NAMD 固有のリーダーが必要な場合は、代わりに mdanalysis-trajectory を使用してください
  • シミュレーションの 実行 には OpenMM/GROMACS を使用してください。このスキルはシミュレーション後の解析専用です

前提条件

  • mdtraj、numpy、pandas、matplotlib
  • トポロジー(PDB/GRO/PRMTOP/PSF)+トラジェクトリ(DCD/XTC/TRR/NetCDF/H5)
  • Python 3.9 以降。conda-forge ビルドを推奨します

インストール前に確認してください。pixi/conda 環境では通常 mdtraj がインストールされています。

python3 -c "import mdtraj" 2>/dev/null || conda install -c conda-forge mdtraj numpy pandas matplotlib

クイックスタート

import mdtraj as md

traj = md.load("traj.xtc", top="topology.pdb")
ca = traj.topology.select("name CA")
traj.superpose(traj, frame=0, atom_indices=ca)

rmsd_ang = md.rmsd(traj, traj, frame=0, atom_indices=ca) * 10.0  # nm -> Å
print(f"Frames: {traj.n_frames}  RMSD: {rmsd_ang.min():.2f}–{rmsd_ang.max():.2f} Å")

コア API

モジュール 1:トラジェクトリの入出力

全体を読み込むことも、ストリーミングで読み込むこともできます。形式は拡張子から自動検出されます。

import mdtraj as md

traj = md.load("rep1.xtc", top="protein.pdb")

# Stream large trajectories — avoids OOM
for chunk in md.iterload("rep1.xtc", top="protein.pdb", chunk=500):
    rmsd_chunk = md.rmsd(chunk, chunk, frame=0)

# Save subset
ca = traj.topology.select("name CA")
traj.atom_slice(ca).save_dcd("ca_only.dcd")

原子の選択とフレームのスライス:

backbone = traj.topology.select("backbone")
chain_a  = traj.topology.select("chainid 0")

first_ns      = traj[:1000]
every_10th    = traj[::10]
last_half_bb  = traj[traj.n_frames // 2:].atom_slice(backbone)
モジュール 2:RMSD、Rg、RMSF

md.rmsd は内部で重ね合わせを行います。RMSF は、明示的に重ね合わせた後で手動計算します。

import mdtraj as md, numpy as np

traj = md.load("rep1.xtc", top="protein.pdb")
ca = traj.topology.select("name CA")

rmsd_ang = md.rmsd(traj, traj, frame=0, atom_indices=ca) * 10.0    # Å
rg_ang   = md.compute_rg(traj) * 10.0                              # Å
time_ns  = traj.time / 1000.0
print(f"<RMSD>={rmsd_ang.mean():.2f} Å,  <Rg>={rg_ang.mean():.2f} Å")
# Per-CA RMSF — average-structure reference
ca_traj = traj.atom_slice(ca)
ca_traj.superpose(ca_traj, frame=0)
diff     = ca_traj.xyz - ca_traj.xyz.mean(axis=0)
rmsf_ang = np.sqrt((diff ** 2).sum(axis=2).mean(axis=0)) * 10.0
res_ids  = [a.residue.resSeq for a in ca_traj.topology.atoms]
print(f"Max RMSF: residue {res_ids[np.argmax(rmsf_ang)]} = {rmsf_ang.max():.2f} Å")
モジュール 3:コンタクトと距離マップ

距離にしきい値を設定して、コンタクト頻度を取得します。

import mdtraj as md, numpy as np

traj = md.load("rep1.xtc", top="protein.pdb")
distances_nm, pairs = md.compute_contacts(traj, contacts="all", scheme="closest-heavy")
# distances_nm: (n_frames, n_pairs);  pairs: (n_pairs, 2) of residue indices

contact_freq = (distances_nm < 0.5).mean(axis=0)                  # 5 Å cutoff
n_res    = traj.n_residues
freq_map = np.zeros((n_res, n_res))
for (i, j), f in zip(pairs, contact_freq):
    freq_map[i, j] = freq_map[j, i] = f
print(f"Persistent contacts (>0.8): {(contact_freq > 0.8).sum()}")
モジュール 4:ねじれ角(ラマチャンドラン)

φ/ψ はラジアンで返されます。最初の残基には φ がなく、最後の残基には ψ がないため、残基で共通部分を取ります。

import mdtraj as md, numpy as np

traj = md.load("rep1.xtc", top="protein.pdb")
phi_ix, phi_rad = md.compute_phi(traj)
psi_ix, psi_rad = md.compute_psi(traj)

def res_of(indices): return np.array([traj.topology.atom(ix[1]).residue.index for ix in indices])
phi_res, psi_res = res_of(phi_ix), res_of(psi_ix)
common  = np.intersect1d(phi_res, psi_res)
phi_deg = np.degrees(phi_rad[:, np.isin(phi_res, common)])
psi_deg = np.degrees(psi_rad[:, np.isin(psi_res, common)])

Gly / Pro 残基に絞り込みます。

res_names = [traj.topology.residue(r).name for r in common]
gly_cols  = [i for i, n in enumerate(res_names) if n == "GLY"]
pro_cols  = [i for i, n in enumerate(res_names) if n == "PRO"]
phi_gly, psi_gly = phi_deg[:, gly_cols].ravel(), psi_deg[:, gly_cols].ravel()
phi_pro, psi_pro = phi_deg[:, pro_cols].ravel(), psi_deg[:, pro_cols].ravel()
モジュール 5:DSSP 二次構造(8 状態)

md.compute_dssp(traj, simplified=False) は、1 文字のコードからなる (n_frames, n_residues) を返します。

コード意味
Hα ヘリックス
Bβ ブリッジ
Eβ シート
G3_10 ヘリックス
Iπ ヘリックス
Tターン
Sベンド
(スペース)または Cコイル
import mdtraj as md

traj = md.load("rep1.xtc", top="protein.pdb")
dssp = md.compute_dssp(traj, simplified=False)
dssp[dssp == " "] = "C"
print(f"DSSP grid: {dssp.shape}  unique codes: {set(dssp.flatten())}")
モジュール 6:水素結合と SASA
import mdtraj as md
import pandas as pd

traj = md.load("rep1.xtc", top="protein.pdb")

# Baker–Hubbard: returns (n_hbonds, 3) of [donor, H, acceptor] atom indices
hbonds = md.baker_hubbard(traj, freq=0.5, periodic=False)
label  = lambda a: f"{traj.topology.atom(a).residue}-{traj.topology.atom(a).name}"
hbonds_df = pd.DataFrame({"donor": [label(d) for d, _, _ in hbonds],
                         "acceptor": [label(a) for _, _, a in hbonds]})

# SASA in nm^2 per residue per frame
sasa_nm2 = md.shrake_rupley(traj, mode="residue")
total_ang2 = sasa_nm2.sum(axis=1) * 100.0                # nm^2 -> Å^2
print(f"Persistent H-bonds: {len(hbonds_df)},  <SASA>: {total_ang2.mean():.0f} Ų")

重要な概念

単位(nm、ps、ラジアン)

traj.xyz は nm、traj.time は ps、ねじれ角はラジアン単位です。プロットする前に変換します。

import numpy as np
xyz_ang  = traj.xyz * 10.0
time_ns  = traj.time / 1000.0
phi_deg  = np.degrees(phi_rad)
ペアのインデックスと resSeq

md.compute_contacts(..., contacts="all") が返すのは 0 始まりの残基インデックス(鎖をまたいで連続)であり、PDB の resSeq ではありません。ヒートマップを作成する前に (n_res, n_res) に再形成します。ラベル付けの際に限り、traj.topology.residue(i).resSeq で対応付けます。

8 状態 DSSP と簡略化 DSSP

simplified=False → 8 状態です。simplified=True → 3 状態(H/E/C)となり、3_10 ヘリックス(G)、π ヘリックス(I)、β ブリッジ(B)、ベンド(S)、ターン(T)の情報が失われます。

よくあるワークフロー

ワークフロー 1:複数レプリケートの安定性+柔軟性+コンタクト+ラマチャンドラン+DSSP

目的:3 つの MD レプリケートについて、構造解析レポートを作成します。内容は、時間経過に伴う RMSD/Rg、残基ごとの RMSF、残基コンタクト頻度マップ(rep1)、ラマチャンドランプロット(一般+Gly+Pro、rep1)、DSSP 8 状態の時系列(rep1)です。プロットには matplotlib のデフォルト設定を使用します。配色は後続処理で利用する側が選択します。

import mdtraj as md, numpy as np, matplotlib.pyplot as plt
from pathlib import Path

replicas = {"rep1": "rep1.xtc", "rep2": "rep2.xtc", "rep3": "rep3.xtc"}
topology = "protein.pdb"
outdir = Path("figures"); outdir.mkdir(exist_ok=True)

trajs = {name: md.load(p, top=topology) for name, p in replicas.items()}
ca    = next(iter(trajs.values())).topology.select("name CA")

# 1. RMSD vs time + Rg (Å) for all replicates
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
for name, traj in trajs.items():
    traj.superpose(traj, frame=0, atom_indices=ca)
    ax1.plot(traj.time / 1000.0, md.rmsd(traj, traj, frame=0, atom_indices=ca) * 10.0, lw=1, label=name)
    ax2.plot(traj.time / 1000.0, md.compute_rg(traj) * 10.0, lw=1, label=name)
ax1.set(xlabel="Time (ns)", ylabel="RMSD (Å)", title="Backbone RMSD")
ax2.set(xlabel="Time (ns)", ylabel="Rg (Å)",   title="Radius of gyration")
for ax in (ax1, ax2): ax.legend(frameon=False)
fig.tight_layout(); fig.savefig(outdir / "01_rmsd_rg.png"); plt.close(fig)

# 2. Per-residue RMSF (Å) across replicates
fig, ax = plt.subplots(figsize=(10, 4))
for name, traj in trajs.items():
    ct = traj.atom_slice(ca); ct.superpose(ct, frame=0)
    rmsf = np.sqrt(((ct.xyz - ct.xyz.mean(axis=0)) ** 2).sum(axis=2).mean(axis=0)) * 10.0
    ax.plot([a.residue.resSeq for a in ct.topology.atoms], rmsf, lw=1, label=name)
ax.set(xlabel="Residue", ylabel="RMSF (Å)", title="Per-residue flexibility")
ax.legend(frameon=False); fig.tight_layout()
fig.savefig(outdir / "02_rmsf.png"); plt.close(fig)

# 3. Contact frequency map (rep1, 5 Å cutoff)
rep1 = trajs["rep1"]
dist_nm, pairs = md.compute_contacts(rep1, contacts="all", scheme="closest-heavy")
freq = (dist_nm < 0.5).mean(axis=0)
freq_map = np.zeros((rep1.n_residues, rep1.n_residues))
for (i, j), f in zip(pairs, freq):
    freq_map[i, j] = freq_map[j, i] = f
np.fill_diagonal(freq_map, 1.0)
fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(freq_map, vmin=0, vmax=1, origin="lower")
ax.set(xlabel="Residue", ylabel="Residue", title="Contact frequency (rep1)")
fig.colorbar(im, ax=ax, label="Fraction of frames in contact")
fig.tight_layout(); fig.savefig(outdir / "03_contact_map.png"); plt.close(fig)

# 4. Ramachandran (rep1) — phi/psi 2D density
phi_ix, phi_rad = md.compute_phi(rep1)
psi_ix, psi_rad = md.compute_psi(rep1)
def res_of(ix, t): return np.array([t.topology.atom(i[1]).residue.index for i in ix])
phi_res, psi_res = res_of(phi_ix, rep1), res_of(psi_ix, rep1)
common = np.intersect1d(phi_res, psi_res)
phi_deg = np.degrees(phi_rad[:, np.isin(phi_res, common)]).ravel()
psi_deg = np.degrees(psi_rad[:, np.isin(psi_res, common)]).ravel()
H, _, _ = np.histogram2d(phi_deg, psi_deg, bins=72, range=[[-180, 180], [-180, 180]])
fig, ax = plt.subplots(figsize=(5, 5))
ax.imshow(H.T, origin="lower", extent=[-180, 180, -180, 180], aspect="equal")
ax.set(xlabel=r"$\phi$ (°)", ylabel=r"$\psi$ (°)", title="Ramachandran (rep1)")
fig.tight_layout(); fig.savefig(outdir / "04_ramachandran_all.png"); plt.close(fig)

# 5. Glycine + proline Ramachandran
res_names = [rep1.topology.residue(r).name for r in common]
for label, cols, fname in [("Glycine", [i for i, n in enumerate(res_names) if n == "GLY"], "05_rama_gly.png"),
                           ("Proline", [i for i, n in enumerate(res_names) if n == "PRO"], "06_rama_pro.png")]:
    if not cols: continue
    phi = np.degrees(phi_rad[:, cols]).ravel()
    psi = np.degrees(psi_rad[:, cols]).ravel()
    H, _, _ = np.histogram2d(phi, psi, bins=60, range=[[-180, 180], [-180, 180]])
    fig, ax = plt.subplots(figsize=(5, 5))
    ax.imshow(H.T, origin="lower", extent=[-180, 180, -180, 180], aspect="equal")
    ax.set(xlabel=r"$\phi$ (°)", ylabel=r"$\psi$ (°)", title=f"{label} Ramachandran (rep1)")
    fig.tight_layout(); fig.savefig(outdir / fname); plt.close(fig)

# 6. DSSP 8-state time series (rep1)
dssp = md.compute_dssp(rep1, simplified=False); dssp[dssp == " "] = "C"
codes_order = ["C", "E", "B", "S", "T", "H", "I", "G"]
code_to_int = {c: i for i, c in enumerate(codes_order)}
dssp_int = np.vectorize(lambda c: code_to_int.get(c, 0))(dssp)
fig, ax = plt.subplots(figsize=(10, 5))
ax.imshow(dssp_int.T, aspect="auto", origin="lower", interpolation="nearest",
          extent=[rep1.time[0] / 1000.0, rep1.time[-1] / 1000.0, 0, rep1.n_residues])
ax.set(xlabel="Time (ns)", ylabel="Residue", title="DSSP 8-state (rep1)")
fig.tight_layout(); fig.savefig(outdir / "07_dssp_8state.png"); plt.close(fig)

# Dump DSSP code mapping so downstream code can pick its own legend/palette
np.savez(outdir / "07_dssp_8state.npz", dssp=dssp, dssp_int=dssp_int, codes_order=codes_order)
print("All figures written to figures/")
ワークフロー 2:レプリケート間の水素結合の持続性+SASA

目的:各レプリケートについて、持続的な水素結合(フレームの 50% 以上)と SASA 合計の時系列を取得します。

import mdtraj as md, numpy as np, pandas as pd

replicas = {"rep1": "rep1.xtc", "rep2": "rep2.xtc", "rep3": "rep3.xtc"}
top = "protein.pdb"
rows = []
for name, p in replicas.items():
    t = md.load(p, top=top)
    hbonds = md.baker_hubbard(t, freq=0.5, periodic=False)
    sasa_ang2 = (md.shrake_rupley(t, mode="residue").sum(axis=1) * 100.0)
    rows.append({"replica": name, "n_persistent_hbonds": len(hbonds),
                 "sasa_mean_ang2": sasa_ang2.mean(), "sasa_std_ang2": sasa_ang2.std()})
print(pd.DataFrame(rows).to_string(index=False))

主要なパラメーター

パラメーターモジュールデフォルト範囲 / オプション効果
chunk (md.iterload)I/O100100-5000ストリームチャンクあたりのフレーム数。速度と RAM のトレードオフになります
atom_indices (md.rmsd)安定性Noneint の配列重ね合わせと RMSD の対象を限定します(例:CA のみ)
frame (md.rmsd)安定性00-n_frames-1参照フレーム
contacts (md.compute_contacts)接触"all""all", [(i,j),...]すべての残基ペア、またはカスタムリスト
scheme (md.compute_contacts)接触"closest-heavy""ca", "closest", "closest-heavy", "sidechain", "sidechain-heavy"残基間距離を計算する原子のサブセット
cutoff(接触閾値、ユーザー設定)接触0.5 nm0.4-0.8 nm接触とみなす距離(CA の場合は 5 Å、closest-heavy の場合は 4 Å)
simplified (md.compute_dssp)DSSPTrueTrue/FalseFalse → 8 状態コード
freq (md.baker_hubbard)水素結合0.10.0-1.0報告する最小フレーム割合
mode (md.shrake_rupley)SASA"atom""atom", "residue"原子ごと、または残基ごとに合計
probe_radius (md.shrake_rupley)SASA0.14 nm0.12-0.20 nm溶媒プローブ(標準値は 1.4 Å)
periodic(幾何関数)幾何Trueboolボックス情報がある場合に最小イメージ規約を使用します

ベストプラクティス

  1. プロットの前に単位を変換する — nm → Å(×10)、ps → ns(÷1000)、rad → deg(np.degrees)。「RMSD が 10 分の 1 になる」という問題の主な原因です。
  2. RMSF の前に重ね合わせる — md.rmsd は内部で重ね合わせますが、RMSF は行いません。明示的に traj.superpose(...) を実行しないと、RMSF に剛体運動が反映されます。
  3. 大きなトラジェクトリには md.iterload を使う — 1 ps/frame の 1 µs のデータは数百万フレームになります。ストリーミングを使えば OOM を回避できます。
  4. 詳細な DSSP には simplified=False を使う — 3 状態では 3_10 / pi-helix / bend の遷移がまとめられます。
  5. resSeq ではなくペアのインデックスを使う — md.compute_contacts は 0 始まりの連続したインデックスを返します。ラベル付けには resSeq にマッピングしてください。
  6. 水分子は早めに取り除く — traj.atom_slice(traj.topology.select("protein")) を使うと、溶媒和系ではメモリと時間を 5~20 倍削減できます。

よく使うレシピ

レシピ:トラジェクトリ形式を変換する
import mdtraj as md

traj = md.load("input.nc", top="topology.parm7")
traj.save_xtc("output.xtc")
traj.save_pdb("output.pdb")
print(f"Converted {traj.n_frames} frames -> output.xtc")
レシピ:複数レプリケートにおける残基ごとの RMSF の平均 ± SD

柔軟性を考慮したドッキングやホモロジーリファインメントに用いる、アンサンブル柔軟性の入力データです。

import mdtraj as md, numpy as np, pandas as pd

paths = {"rep1": "rep1.xtc", "rep2": "rep2.xtc", "rep3": "rep3.xtc"}
top = "protein.pdb"
rmsf_by_rep = {}
for name, p in paths.items():
    t  = md.load(p, top=top)
    ct = t.atom_slice(t.topology.select("name CA")); ct.superpose(ct, frame=0)
    rmsf_by_rep[name] = np.sqrt(((ct.xyz - ct.xyz.mean(axis=0)) ** 2).sum(axis=2).mean(axis=0)) * 10.0

df = pd.DataFrame(rmsf_by_rep)
df["mean"], df["std"] = df.mean(axis=1), df.std(axis=1)
df.to_csv("rmsf_replicates.csv", index_label="residue_index")
レシピ:RMSD の閾値を超えるフレームを保存する

クラスタリングや後続の解析に使用する「開いた」コンフォメーションを選別します。

import mdtraj as md

traj = md.load("rep1.xtc", top="protein.pdb")
ca   = traj.topology.select("name CA")
traj.superpose(traj, frame=0, atom_indices=ca)
rmsd_ang = md.rmsd(traj, traj, frame=0, atom_indices=ca) * 10.0

open_state = traj[rmsd_ang > 4.0]
open_state.save_dcd("open_state.dcd")
print(f"Saved {open_state.n_frames}/{traj.n_frames} frames")
レシピ:レプリケート間のヘリックス割合
import mdtraj as md, numpy as np

paths = ["rep1.xtc", "rep2.xtc", "rep3.xtc"]
helix_fracs = [(md.compute_dssp(md.load(p, top="protein.pdb"), simplified=False) == "H").mean() for p in paths]
print(f"α-helix per replicate: {[f'{h:.2f}' for h in helix_fracs]}")
print(f"Mean ± std: {np.mean(helix_fracs):.2f} ± {np.std(helix_fracs):.2f}")

トラブルシューティング

問題原因解決策
RuntimeError: No topologyXTC/DCD/TRR にはトポロジーが含まれていませんmd.load に top="protein.pdb"(または .prmtop)を渡します
RMSD が 10 分の 1 になるmdtraj は nm を返します10.0 を掛けます
末端で RMSF が急増する末端固有の柔軟性であり、バグではありません末端をスライスで除外するか、その旨を明記します
DSSP が H/E/C だけを返すデフォルトは simplified=True ですsimplified=False を渡します
DSSP のセルが空、または ' ' になる空白はコイルを示し、欠損ではありませんdssp[dssp == " "] = "C"
大きなトラジェクトリで MemoryError が発生するトラジェクトリ全体を RAM に読み込んでいますmd.iterload(..., chunk=500) に切り替えます
phi/psi 配列で残基数が異なる最初の残基には phi がなく、最後の残基には psi がありませんペアにする前に残基インデックス集合の積集合を取ります
ラマチャンドランプロットが中央からずれる角度がラジアンのままですnp.degrees で変換し、range=[[-180, 180], [-180, 180]] を設定します
md.compute_contacts が NaN を返すclosest-heavy では、一部のペアに重原子がありません(例:グリシンの側鎖)scheme="closest" または "ca" を使い、NaN の列を削除します
接触マップが非対称になる上三角部分のみ入力されていますミラーリングします:freq_map[j, i] = freq_map[i, j]
traj.time がすべてゼロになる一部のライターは時刻を空のままにします再構築します:traj.time = np.arange(traj.n_frames) * dt_ps

関連スキル

  • mdanalysis-trajectory — より豊富な原子選択構文とAnalysisBaseフレームワーク
  • autodock-vina-docking / smina-molecular-docking — 代表的なMDフレームをドッキングターゲットとして入力します
  • chembl-database-bioactivity — MD由来の柔軟性マップにリガンドの生物活性を重ね合わせます

参考資料

コメント