magattacaのブログ

日付以外誤報

オルト位置換基で絞り込む話

前回の記事で、SMARTS記法の扱い方がなんとなくわかりました。本題にもどって、ライブラリをオルト位に置換基の入ったビフェニルで絞り込みたいと思います。

その前に、現在残っている化合物数の確認です。

Enamine_Premium Enamie_Advenced Enamine_HTS UOS_HTS total
指標でのフィルタリング後 4060 37431 414562 106948 563001
ビフェニル有 12 329 3182 697 4220
(0.7%)
ビフェニル無 4048 37102 411380 106251 558781
(99.3%)

合計4000個程度残っています。このうちオルト位に置換基が入ったものはいくつあるでしょうか?

オルト位置換基による絞り込み

PandasToolsでSDFをよみこむ

4種類のライブラリから抽出した、ビフェニルを部分構造として含む化合物のリストを連結(extend()を使った)し、SDFファイル「biphenyl_library.sdf」として保存しました。こちらをRDKitのPandasToolsをもちいてDataFrameでよみこみます。

PandasToolsの使い方に関しては化学の新しいカタチさんの記事「RDKitのPandasToolsでデータ分析を加速する」を参考にさせていただきました。

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw
from rdkit.Chem import PandasTools
import pandas as pd

biphenyl_df = PandasTools.LoadSDF('./biphenyl_library.sdf')
# 数を確認
len(biphenyl_df)
# 4220

こんな感じになりました。

f:id:magattaca:20190216220559p:plain

Pandas上で部分構造検索

PandasToolsを使うと「>=(ge比較演算子)」で部分構造検索が行えるとのことですので試しに利用して見ます

ortho_biphenyl= Chem.MolFromSmarts('c1ccc(c(*)c1)c1ccccc1')
ortho_df = biphenyl_df[biphenyl_df['ROMol'] >= ortho_biphenyl]

len(ortho_df)
# 2353

オルト位に置換基を持つ化合物は約半数程度の2353個でした。

一致検索後のDataFrameでは、Molオブジェクトを格納したカラムにおいて、一致した部分構造が赤色でハイライトされて表示されています。

f:id:magattaca:20190216220822p:plain

忘れずにSDFに書き出しておきます。SDFに残すプロパティを指定するため、DataFrameのカラム名をリストとして取得します。

property_names = list(biphenyl_df.columns)
print(property_names)
# ['ID', 'MW', 'MolLogP', 'NumHAcceptors', 'NumHDonors', 'NumRotatableBonds', 'ROMol', 'TPSA', 'original_id']

「ROMol」は不要なのでリストから除きます。

property_names.remove('ROMol')
print(property_names)
# ['ID', 'MW', 'MolLogP', 'NumHAcceptors', 'NumHDonors', 'NumRotatableBonds', 'TPSA', 'original_id']

出力します。

PandasTools.WriteSDF(ortho_df, 'ortho_biphenyl_library.sdf', properties = property_names)

以上で、オルト位に置換基をもつ部分構造での絞り込みが完了しました。

オルト位の置換基の構造的意義

なぜ「オルト位に置換基を持つ」ということを、絞り込みの基準として重要視するのか?ついでなので「オルト位に置換基を持つ」ということの意味について考えたいと思います。

・・・といっても立体障害でビフェニル骨格が捻れるというだけなのですが、、、、

3次元構造を眺める

オルト位の置換基の有無が3次元構造(コンフォメーション)にどのような影響を与えるか?ということを眺めて見ます。3次元構造の生成は「化学の新しいカタチ」さんの記事「RDKitによる3次元構造の生成」を参考にしました。

まずは構造を準備。無置換、オルト位にメチル基を1つもつもの、2つ持つものの3つを作りました。

biPh = Chem.MolFromSmiles('c1ccc(cc1)c1ccccc1')
ortho_Me_biPh = Chem.MolFromSmiles('c1ccc(c(C)c1)c1ccccc1')
ortho_diMe_biPh = Chem.MolFromSmiles('c1cc(C)c(c(C)c1)c1ccccc1')

biPh_list = [biPh, ortho_Me_biPh, ortho_diMe_biPh]
Draw.MolsToGridImage(biPh_list)

f:id:magattaca:20190216221249p:plain

水素原子を付加し、ETKDG法で3次元構造を生成します。

biPh_3D_list = []

for mol in biPh_list:
    mol_H = AllChem.AddHs(mol)
    AllChem.EmbedMolecule(mol_H, AllChem.ETKDG())
    biPh_3D_list.append(mol_H)

生成した構造を眺めます。

from rdkit.Chem.Draw import IPythonConsole
import py3Dmol

v = py3Dmol.view(width=600, height=300, linked=False, viewergrid=(1,3))

for m, i in zip(biPh_3D_list, [(0,0), (0,1), (0,2)]):
    mb = Chem.MolToMolBlock(m)
    v.addModel(mb, 'sdf', viewer=i)

v.setBackgroundColor('0xeeeeee')
v.setStyle({'stick':{}})
v.zoomTo()
v.show()

無置換、モノ置換、ジ置換の順にねじれ角度が大きくなっているのがわかります。

f:id:magattaca:20190216221523p:plain

もっとわかりやすくするために、重ねて見ます。

アラインメントをとって重ねる

RDKitブログの記事を参考にして3つとも重ねて見ます。

rdMolAlign.AlignMolを使って部分構造のアラインメントを取り3次元構造を重ね合わせます。重ねたい部分構造(template)を明示的に与えるため引数atomMapに無置換のベンゼン環に相当するatom numberを与えます。

こちらや、こちらを参考にエラーに対処していたらやたらとlist()を使うことになってしまった・・・

from rdkit.Chem import rdMolAlign

# 無置換フェニルのテンプレートをSmartsから作成
template = Chem.MolFromSmarts('[cH1]1[cH1][cH1][cH1][cH1]c1-c')

# テンプレートに一致する無置換ビフェニルの原子を基準にする
# タプルのタプルで得られる 
template_atoms = biPh_3D_list[0].GetSubstructMatches(template)

# アラインメントを取る
for m in biPh_3D_list:
    matched_atoms = m.GetSubstructMatches(template)
    rdMolAlign.AlignMol(m, biPh_3D_list[0], atomMap = list(zip(list(matched_atoms[0]), list(template_atoms[0]))))  
# 描く
v = py3Dmol.view(width=300, height=300)

for m in biPh_3D_list:
    mb = Chem.MolToMolBlock(m)
    v.addModel(mb, 'sdf')

v.setBackgroundColor('0xeeeeee')
v.setStyle({'stick':{}})
v.zoomTo()
v.show()

f:id:magattaca:20190216222114p:plainf:id:magattaca:20190216222117p:plain

静止画ではわかりにくいですが、無置換、モノ置換、ジ置換の順に二面角(Torsion Angle)が大きくなっているのがわかります。

二面角を計算してみる

それぞれ一つずつコンフォマーを取得して二面角を計算して見ます。

まず二面角を計算するために、目的の部分のatom numberを確認します。無置換ビフェニルの場合・・・

Draw.MolToImage(biPh_3D_list[0], includeAtomNumbers = True)

f:id:magattaca:20190216222309p:plain

atom numberがわかったので目的の4原子を指定して、2面角を求めます。

from rdkit.Chem import rdMolTransforms
conf=biPh_3D_list[0].GetConformer(0)
rdMolTransforms.GetDihedralDeg(conf, 2,3,6,7)
# 148.88976772600805

モノ置換、ジ置換も同様に計算し、0° - 90°の範囲に値を修正すると以下のようになります。

無置換 モノ置換 ジ置換
二面角 31° 50° 81°

順に二面角が大きくなり、特にジ置換ではほぼ直角近くになっています。

フィンガープリントではどう認識される?

上記の二面角の差異がフィンガープリントとしてはどのように認識できるか、少し見てみたいと思います。Atom PairsTopological Torsionsでそれぞれ類似性を比較した際にどのようになるか試してみます。

この二つを選んだ理由は名前の雰囲気です・・・(計算方法を理解していません。すみません)。

from rdkit import DataStructs

from rdkit.Chem.AtomPairs import Pairs
pair_fps = [Pairs.GetAtomPairFingerprint(mol) for mol in biPh_3D_list]

from rdkit.Chem.AtomPairs import Torsions
tts_fps = [Torsions.GetTopologicalTorsionFingerprintAsIntVect(mol) for mol in biPh_3D_list]

# Tanimoto係数、Dice係数をそれぞれの組み合わせで計算
pair_Dice_0_1 = DataStructs.DiceSimilarity(pair_fps[0], pair_fps[1])
pair_Tani_0_1 = DataStructs.TanimotoSimilarity(pair_fps[0], pair_fps[1])

# 残りの組み合わせ、TorsionFingerprintについても同じ計算をしたので省略

結果をまとめると以下になりました。

無置換-モノ置換 無置換-ジ置換 モノ置換-ジ置換
AtomPair_Tanimoto係数 0.76 0.59 0.77
AtomPair_Dice係数 0.86 0.75 0.87
Torsion_Tanimoto係数 0.87 0.77 0.88
Torsion_Dice係数 0.93 0.86 0.94

Torsionの方が差異が大きくなると面白いと思っていたのですが、今回の結果ではAtom Pairの方が置換の数の差を反映しているようにも見えます。いずれにせよ、メチルの数が1つ増えるか2つ増えるかで類似度が下がっていくことに代わりわなさそうです。

オルト位置換を創薬の話に繋げようとする

オルト位に置換基を加えることで分子がねじれ、立体的な構造が変化するということがわかりました。では、このねじれを医薬品に活用した事例は???ということでFasiglifam(TAK-875)の構造を眺めて見たいと思います。2型糖尿病を適応として開発が行われていた武田薬品の化合物で、GPCRの一つGPR40の作動薬です。残念ながら臨床試験第三相で肝臓における安全性の懸念から開発中止となり、上市にはいたらなかったそうです。

Fasiglifam(TAK-875)の構造

なぜこちらを取り上げるのか? 構造を見ていただくとお分かりいただけると思います。 印象的なビフェニル、オルト-ジ置換構造を持っています。

Fasiglifam_mol = Chem.MolFromSmiles('CC1=CC(=CC(=C1C2=CC(=CC=C2)COC3=CC4=C(C=C3)[C@@H](CO4)CC(=O)O)C)OCCCS(=O)(=O)C')

from rdkit.Chem.Draw import rdMolDraw2D
from IPython.display import SVG

highlights = Fasiglifam_mol.GetSubstructMatch(ortho_diMe_biPh)
view = rdMolDraw2D.MolDraw2DSVG(500,500)
pre_mol = rdMolDraw2D.PrepareMolForDrawing(Fasiglifam_mol)
view.DrawMolecule(pre_mol, highlightAtoms=highlights)
view.FinishDrawing()
svg = view.GetDrawingText()

SVG(svg.replace('svg:,',''))

f:id:magattaca:20190216222942p:plain

Fasiglifamを見出した構造活性相関の研究に関していくつかの論文が出されています。ずいぶんと以前に読んだ論文のため引用しようにも忘れてしまいました(おそらくJ.Med.ChemやACS Med. Chem. Lett.だったような・・・)。その中でビフェニル構造に置換基を加えることで2面角が変化すること、その構造の変化が活性に大きな影響を与えたことが議論されていたように思います。(いろいろぼんやりとした記憶で不確かですみません・・・)

では立体的にはどのような構造なのか?眺めて見ます。PubChemのID「cid: 24857286」をつかってpy3Dmolで描画して見ます。

化学の新しいカタチさんの記事「py3Dmolを使って化学構造をJupyter上で美しく表示する」を参考にさせていただきました。

view = py3Dmol.view(width= 800, height=300, viewergrid=(1,3), query='cid:24857286')
view.setStyle({'cartoon': {}}, viewer=(0,0))
view.setStyle({'stick': {}}, viewer=(0,1))
view.setStyle({'sphere': {}}, viewer=(0,2))
view.show()

f:id:magattaca:20190216223132p:plain

ビフェニル構造がほぼ直角にねじれています。

受容体との共結晶構造

FasiglifamとGPR40との共結晶構造が解かれています(PDB id: 4PHU*1)。ついでに眺めて見ます。 *2*3

view = py3Dmol.view(width=680, height=480, query='pdb:4PHU', viewergrid=(1,1))
view.setStyle({'cartoon': {'color': 'spectrum'}}, viewer=(0,0))
view.setStyle({'resn': '2YB'}, {'sphere': {'colorsheme': 'default'}}, viewer=(0,0))
view.addSurface(py3Dmol.SES, {'opacity': 0.6, 'color': '#FADBD8'}, viewer=(0,0))
view.show()

f:id:magattaca:20190216223554p:plain

とても興味深いことに、Fasiglifamはその構造の半分程度がGPR40のヘリックスの合間から外に向かって飛び出しています。

上の図では少しわかりにくいのでRCSD PDBのビューワーでリガンドと受容体の相互作用部位を拡大して眺めます。

こんな感じ・・・

f:id:magattaca:20190216224125p:plain

ちょうど直角にねじれたビフェニル構造の部分が受容体内-外の境目に来ています。最近ではGPCRの構造が多数解かれるようになってきますが、この構造が公開された当時(2014年)はまだそこまで多くなく、さらにリガンドの結合様式も見たことがないものでこんな結合の仕方あるのか!!!と、とても驚いたのを覚えています。

まとめ

以上、今回はオルト位に置換基を含む化合物のライブラリからの抽出と、その構造的意義についてざっと眺めて見ました。オルト位に置換基を加えることでライブラリ化合物は2400弱にまで絞り込むことができました。後半、古い記憶を辿ってFasiglifamの話などをし始めたのは、ライブラリスクリーニングの方法に行き詰まったからです・・・困難から逃走するタイプ・・・

*1:Crystal structure of Human GPR40 bound to allosteric agonist TAK-875 DOI: 10.2210/pdb4PHU/pdb

*2:High-resolution structure of the human GPR40 receptor bound to allosteric agonist TAK-875. (2014) Nature 513: 124-127

*3:AS Rose et al. (2018) NGL viewer: web-based molecular graphics for large complexes. Bioinformatics dio:10.1093/bioinformatics/bty419

SMARTS記法とRDKitの化学反応で遊んだ話

前回の記事で、ビフェニル部分構造によるフィルタリングを行いましたが、オルト位に置換基の入ったビフェニルについてはうまく実行することができませんでした。@iwatobipen先生、@mojaie先生にSMARTSを使えば良いというご指摘をいただきました。ありがとうございました!

まずは前回失敗した処理を再掲します。

活性化合物のデータセット共闘用シェアデータ )から取り出した以下の分子に対し、

f:id:magattaca:20190210183530p:plain

RDKitの部分構造検索(HasSubstructMatch)を用い、オルト位に置換基の入った構造を取り出そうとしました。

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw

ortho_biphenyl = Chem.MolFromSmiles('c1ccc(c(*)c1)c1ccccc1')
Draw.MolToImage(ortho_biphenyl)

f:id:magattaca:20190210184157p:plain

検索したい部分構造は上のものです。(* はワイルドカード

test_mol.HasSubstructMatch(ortho_biphenyl)
# False

上記のようにFalseとなってしまうというのが前回の失敗でした。

まずは@iwatobipen先生に教えていただいたSMARTSを用いた方法を試します。

ortho_biphenyl_2 = Chem.MolFromSmarts('c1cccc([!H])c1-c1ccccc1')
Draw.MolToImage(ortho_biphenyl_2)

f:id:magattaca:20190210183650p:plain

SMARTSから構築したMOlオブジェクトは、先に作成したものとは異なる形で認識されているようです。(見た感じ・・・)
こちらを使って部分構造の一致を検証します。

test_mol.HasSubstructMatch(ortho_biphenyl_2)
# True

True! うまく構造の一致を認識できました!

次に@mojaie先生にご指摘いただいた事項。
私が失敗したコードでは、Chem.MolFromSmiles()としてしまっているから上手くいかないのではないか、ということでした。「ワイルドカードの*は検索条件なので、本来はSMARTSの表記」のため、Chem.MolFromSmarts()とするのが適切、ということだそうです。

ortho_biphenyl_3 = Chem.MolFromSmarts('c1ccc(c(*)c1)c1ccccc1')
test_mol.HasSubstructMatch(ortho_biphenyl_3)
# True

True! こちらでもうまく認識できました!

先生方ありがとうございました!

今回失敗した原因は、SMILESとSMARTSの違いを理解せずに適当に使ってしまっていたことでした。

RDKitのドキュメンテーションsubstructure_searchingによると、部分構造検索のクエリ分子はSMILESからでもSMARTSからでも良いが、二つの記法で違いがあるから注意してください、といった指摘がなされていました。

そこで、両者の違いについて少し勉強してみることとしました。

SMILESとSMARTSの違い

SMARTSとは?

そもそもSMARTSって何の略なんだろう?と思っていたのですが、株式会社MOLSISさんのページにわかりやすくまとめられていたので転記させていただきます。 (同社はTwitterで何度かお見かけしたドッキングに使われていたソフト、MOEも製品として取り扱われているそうです)

  • SMILES(Simplified Molecular Input Line Entry System) : 分子記述言語

    • 分子の二次元構造を文字列として記述
    • 情報をコンパクトに保存
    • 原子座標の羅列と違い、ユーザーにも理解しやすい
  • SMARTS(SMiles ARbitrary Target Specification): パターン記述言語

    • SMILES言語を検索クエリ用に拡張したパターン記述言語
    • ワイルドカードや条件指定といった構造検索条件を表記可能

つまり、SMILESを構造検索に用いるため発展させたもの、ということのようです。

Daylight社 の解説ページでSMILES / SMARTSを勉強

SMARTSの詳細については、SMILESの考案者Dr. David Weiningerの設立したDaylight C.I.S. 社のページ を参照してください、と「化学の新しいカタチ」さんのこちらの記事(SMILES記法は化学構造の線形表記法)でオススメされていたので早速眺めて見たいと思います。

  • 部分構造検索とは?

    • 分子(グラフ)から特定のパターン(サブグラフ)を見つけ出すプロセス
  • 部分構造検索におけるSMILESの特徴

    • 2つの基本的な記号「原子(atom)」と「結合(bond)」を使って分子グラフを特定できる
    • グラフの構成要素に対してラベル(label)を割り当てることができる
    • atomのタイプがグラフのノード(node)の、bondのタイプがグラフのエッジ(edge)のラベルに対応
  • SMILESを直接拡張したSMARTSではさらに・・・・

    • 論理演算子(logical operator)
    • より一般的な原子、結合を表現するための特別な原子記号(atomic symbol)、結合記号(bond symbol)が使える

ようになっているそうです。

基本的にはSMILESの拡張のため、SMARTSでもSMILESの表現が使えるそうですが、SMILESは分子記述SMARTSはパターン記述という違いから同じ文字列でも意味合いが異なってくることに注意が必要、とのこと。Daylight社のページでは以下の具体例が挙げられていました。

  • 例1: 分子とパターン

    • SMILESを検索クエリに使うと、SMILESから構築される分子が検索対象となる
    • SMARTSの場合はパターンが検索対象になる
      • SMARTS "C1=CC=CC=C1"は「脂肪族炭素が単結合、2重結合交互に6個繋がった環構造」というパターンでベンゼン環とマッチしない
  • 例2: 原子を括弧 () を使わないで指定した場合

    • SMILESではデフォルトの値が使われる
      • SMILES "O" は電荷ゼロ、水素原子2つの脂肪族酸素原子(aliphatic oxygen)、つまり水(H2O)
    • SMARTSではパターンが定義されていないとして認識される
      • SMARTS "O" は他の条件一切無くaliphatic oxygen そのもの。なのでエタノールやアセトンの"O"ともマッチする

記法のルールを写経

具体的にはどのような記法になるのか、ざっくりテーブルを日本語にします。順番など少し入れ替えています。

SMILES記法同様、角括弧「」は原子の境界を、丸括弧「()」は分岐構造を示します。

原子

記号 意味 デフォルト
* ワイルドカード(どの原子でも良い)
a 芳香属性原子
A 脂肪族原子
D<数字> 明示的な結合の数(級数:degree)が<数字> ちょうど1
H<数字> 結合している水素原子が<数字>個 ちょうど1
h<数字> 暗に示された水素原子が<数字>個 少なくとも1
R<数字> <数字>のSSSR環構造に含まれる(ring membership) 環の構成原子どれでも
r<数字> <数字>のサイズのSSSR環構造に含まれる(ring size) 環の構成原子どれでも
v<数字> 結合次数の総数が<数字> ちょうど1
X<数字> 結合の総数が<数字> ちょうど1
x<数字> 環構造の結合の総数が<数字>(ring connectivity) 少なくとも1
-<数字> マイナス <数字> の電荷 -1 電荷、--なら-2のチャージ
+<数字> プラス <数字> の形式電荷 +1 電荷、++なら+2のチャージ
#<数字> 原子番号<数字>
@ 不斉、反時計回り(@@は時計回り)
<数字> 明示的な原子量

結合

記号 意味
- 単結合(脂肪族)
= 2重結合
# 3重結合
: 芳香族性結合
~ ワイルドカード(どのような結合形式でも良い)
@ 環結合ならなんでもよい
/ 方向性のある結合 アップ
|方向性性のある結合 ダウン
/? アップあるいは指定されていない
\? ダウンあるいは指定されていない

二重結合の幾何異性体は、例えば「 /C=C/ : trans型2重結合(E)」「/C=C\ : cis型2重結合(Z)」と言ったように表すことができるそうです(SMILES記法の場合)。

論理演算子

記号 意味
! not
& and
, or
; and

andを意味するのに「&」と「;」の2つがありますが、「&」が優先されるそうです。
論理演算子を使って原子のパターンを記述するには、角括弧「[]」内部にその原子と条件指定全体を含めることに注意が必要とのことです。

Reaction SMILES / SMARTS

SMILESには他にも拡張として、化学反応を記述できるようにしたReaction SMILESというものもあるそうです。 どのような拡張がなされているのか、「化学の新しいカタチ」さんのこちらの記事RDKitで化学反応:ケモインフォマティクスにおける反応式の扱い方 にわかりやすく記載されているのでご参照ください。

折角なので遊んで見たいと思います。取り上げる反応はC-H結合活性化!(・・・私が学生の頃とても流行ってた)

Murai reaction

まずは村井眞二先生らによる世界初の実用的触媒的C-H結合活性化型反応から*1*2

f:id:magattaca:20190210185353p:plain

「原料・試薬 >> 生成物」とのことなのでReaction SMILESは以下のようになります。*3

O=C-C1=CC=CC=C1.CCO[Si](OCC)(OCC)C=C>>CCO[Si](CCC1=C(C=O)C=CC=C1)(OCC)OCC

二つの原料はSMILES同士が「.」でつながれています。

肝心の触媒を忘れました。

RuH2(CO)(PPh3)3を二つの「>」の間に挿入しなければいけません。PubChemによるとこの触媒のSMILESは「[C-]#[O+].C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.[RhH2]」だそうです。長すぎる・・・

O=C-C1=CC=CC=C1.CCO[Si](OCC)(OCC)C=C>[C-]#[O+].C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.C1=CC=C(C=C1)P(C2=CC=CC=C2)C3=CC=CC=C3.[RhH2]>CCO[Si](CCC1=C(C=O)C=CC=C1)(OCC)OCC

訳が分からなくなってしまいました。
RDKitの反応に使うにはReaction SMARTSにするとのことなので触媒を省略した上で以下のように書いてみました。

[O:1]=[C:2]-[c:3][c:4].[C:5]=[C:6]-[Si:7] >> [O:1]=[C:2]-[c:3][c:4]-[C:5]=[C:6]-[Si:7]

では実際に反応を試します。反応させるMolオブジェクト、反応パターン(Reaction SMARTS)を用意した上でRunReactantsを実行します。

reactant_1 = Chem.MolFromSmiles('O=C-C1=CC=CC=C1')
reactant_2 = Chem.MolFromSmiles('CCO[Si](OCC)(OCC)C=C')
reaction_pattern = '[O:1]=[C:2]-[c:3][c:4].[C:5]=[C:6]-[Si:7] >> [O:1]=[C:2]-[c:3][c:4]-[C:5]=[C:6]-[Si:7]'
rxn = AllChem.ReactionFromSmarts(reaction_pattern)
x = rxn.RunReactants([reactant_1, reactant_2])
print(x)
# ((<rdkit.Chem.rdchem.Mol object at 0x115f19fa8>,), (<rdkit.Chem.rdchem.Mol object at 0x115f19e48>,))

結果はMolオブジェクトを要素とするタプルのタプルとして与えられるようです。外側のタプルはとりうる反応点の組み合わせに相当し、内部のタプルは一つの反応点の組み合わせあたりに複数の生成物ができる場合(加水分解など)に生成物を格納するためにあるようです。

今回は同一の分子が2つ生成されていました。一方を取り出して見ます。

x_0 = x[0][0]
Draw.MolToImage(x_0)

f:id:magattaca:20190210190221p:plain

アルデヒドの酸素原子が水になっています!どうしてこうなった。

RDKitオンラインドキュメンテーションChemical-reactionsによると、生成される分子はサニタイゼーションされていないので、サニタイゼーションが必要とのことです。

Chem.SanitizeMol(x_0)
# rdkit.Chem.rdmolops.SanitizeFlags.SANITIZE_NONE と返ってきた
Draw.MolToImage(x_0)

f:id:magattaca:20190210190344p:plain

今度は酸素原子がうまく認識されています。よかった。

Fagnouの添加剤

だいたい使い方がわかってきました。ちょっと添加剤にもこだわってみましょう。

Keith Fagnouらによって見出された条件、ピバル酸の添加を行ってみます*4

こちらの論文ではピパル酸を添加することで、触媒的C-H活性化反応が加速するということを見出しています。メカニズムとして、パラジウム触媒に配位し、協奏的メタル化-脱プロトン化反応機構を加速するのではないかということが提唱されています。

f:id:magattaca:20190210190545p:plain

上記のReaction SMILESは以下のようになります。
C1=CC=CC=C1.CC1=CC=C(Br)C=C1>CC(C)(C)C(O)=O.[Pd]>CC1=CC=C(C=C1)C1=CC=CC=C1

手抜きして酢酸パラジウム(II)をパラジウム単体にしてしまっていますがご容赦ください。

reactant_1 = Chem.MolFromSmiles('C1=CC=CC=C1')
reactant_2 = Chem.MolFromSmiles('CC1=CC=C(Br)C=C1')
reaction_pattern = '[c:1].[c:2]-[Br:3] >CC(C)(C)C(O)=O.[Pd]> [c:1]-[c:2]'
rxn = AllChem.ReactionFromSmarts(reaction_pattern)
x = rxn.RunReactants([reactant_1, reactant_2])
len(x)
# 6

・・・添加剤とか記載しなくても結果変わらなかった気がする。まあ、いいや。
ベンゼンを原料としてのC-H活性化反応なので6つの化合物が生成されました。重複を取り除きたいと思います。

手順としては、

  1. 空の辞書を作成
  2. サニタイゼーションしてからSMILESに変換(あとで描画したいのでサニタイゼーションもここで行なった)
  3. 辞書にSMILESをkeyとして、valueにMolオブジェクトを渡す。

これで重複した分子は、keyのSMILESが一致するため複数格納されることはなく、ユニークな分子のみとなります。

uniqps = {}
for p in x:
    Chem.SanitizeMol(p[0])
    smi = Chem.MolToSmiles(p[0])
    uniqps[smi] = p[0]

print(uniqps)
# {'Cc1ccc(-c2ccccc2)cc1': <rdkit.Chem.rdchem.Mol object at 0x115f28138>}

分子一つだけが残りました。生成物を確認します。

Draw.MolToImage(uniqps['Cc1ccc(-c2ccccc2)cc1'])

f:id:magattaca:20190210191545p:plain

団子・・・

Baranの溶媒

今度は溶媒にもこだわってみましょう。C-H活性化の溶媒と言えば・・・ウーロン茶ですよね!

さまざまなインパクトのある全合成、反応開発を行い、近年では電気化学の合成反応への応用にも力を入れている様子のPhil S. Baranらにより2012年Natureに報告された反応です。*5

f:id:magattaca:20190210192001p:plain

上記のReaction SMILESは以下のようになります。
CN1C=NC2=C1C(=O)N(C)C(=O)N2C.FC(F)(F)S(=O)O[Zn]OS(=O)C(F)(F)F>>CN1C(=NC2=C1C(=O)N(C)C(=O)N2C)C(F)(F)F

まずは溶媒を指定せずに行います。

reaction_pattern = '[n:1][cH1:2][n:3]>>[n:1][c:2](C(F)(F)F)[n:3]'
reactant = Chem.MolFromSmiles('CN1C=NC2=C1C(=O)N(C)C(=O)N2C')
rxn = AllChem.ReactionFromSmarts(reaction_pattern)
x = rxn.RunReactants([reactant,])

uniqps = {}
for p in x:
    Chem.SanitizeMol(p[0])
    smi = Chem.MolToSmiles(p[0])
    uniqps[smi] = p[0]
 
print(uniqps)
# {'Cc1ccc(-c2ccccc2)cc1': <rdkit.Chem.rdchem.Mol object at 0x115f28138>}
Draw.MolToImage(uniqps['Cn1c(=O)c2c(nc(C(F)(F)F)n2C)n(C)c1=O'])

f:id:magattaca:20190210192300p:plain

うまくカフェインのトリフルオロメチル化ができました。
それではウーロン茶(Oolong tea)を溶媒として反応を仕込みましょう。

reaction_pattern = '[n:1][cH1:2][n:3]>Oolong tea>[n:1][c:2](C(F)(F)F)[n:3]'
rxn = AllChem.ReactionFromSmarts(reaction_pattern)
x = rxn.RunReactants(reactant)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-150-2ccb3852d288> in <module>()
----> 1 rxn = AllChem.ReactionFromSmarts(reaction_pattern)
      2 x = rxn.RunReactants(reactant)

ValueError: ChemicalReactionParserException: Problems constructing agent from SMARTS: Oolong tea

怒られました。そんなSMARTSは無い!!とのこと・・・ふざけてすみません。

MacMillanによるLate-Stage Trifluoromethylation

少し真面目な話に戻して、、、

C-H活性化反応ですが、合成終盤に分子の多様性を増加させるLate-Stage Functionalizationとしての使い方、とくに医薬品低分子への適用が、その有用性を示すデモンストレーションとして行われることが多いです。*6創薬化学で多用される部分構造であるトリフルオロメチル基を、医薬品分子そのものに1ステップで直接導入できれば、新しいプロファイルの有用な分子をできるのではないないか?それに複数の生成物を一回で作ることができれば、有望な分子のバリエーションを増やせて良いよね!」というのが、彼らの主張です。

反応形式としては芳香環のC-H結合のトリフルオロメチル基への変換です。

f:id:magattaca:20190210192812p:plain

光触媒としてRu(phen)3Cl2が用いられていますが、SMILESに直すと大変そうなので省いてあります。

上記のReaction SMILESは以下の通り。
FC(F)(F)S(Cl)(=O)=O.C1=CC=CC=C1>>FC(F)(F)C1=CC=CC=C1

Reaction SMARTSは [c:1][cH1:2][c:3]>>[c:1][c:2](C(F)(F)F)[c:3] としておけば良さそうです。

論文中ではLipitor(Atrovastatin)を基質として反応を行い、トリフルオロメチル化されたCF3-Lipitorの合成を行なっています。
基質のLipitorを描くのが面倒なのでSMILESをPubChemから取得します。

import pubchempy as pcp
import pandas as pd
Lipitor_df = pcp.get_properties('canonical_smiles', 'Lipitor', 'name', as_dataframe=True)

f:id:magattaca:20190210193635p:plain

Lipitorのフリー体はPubChemの「CID : 60823」のようなのでこちらのSMILESを使います。

Lipitor_smi = Lipitor_df.loc[60823, 'CanonicalSMILES']
Lipitor_mol = Chem.MolFromSmiles(Lipitor_smi)
Draw.MolToImage(Lipitor_mol)

f:id:magattaca:20190210193720p:plain

Lipitorの構造は上記のようなものです。
Lipitorには3つのベンゼン環があり、反応点としては多数ありますが、MacMillanらは3つの主要なトリフルオロメチル化生成物を、1:1:1の位置異性体混合物(収率74%)で取得しています。

反応させましょう。

CF3_reaction = '[c:1][cH1:2][c:3]>>[c:1][c:2](C(F)(F)F)[c:3]'
rxn = AllChem.ReactionFromSmarts(CF3_reaction)
x = rxn.RunReactants([Lipitor_mol,])

# ユニークな分子を取り出す
CF3_Lipitors = {}
for p in x:
    Chem.SanitizeMol(p[0])
    smi = Chem.MolToSmiles(p[0])
    CF3_Lipitors[smi] = p[0]

print(len(CF3_Lipitors))
# 8個の分子が生成された

# 辞書をリストに変換したうえで描画
CF3_Lipitor_list = list(CF3_Lipitors.values())
Draw.MolsToGridImage(CF3_Lipitor_list, molsPerRow=4, subImgSize=(200,200))

f:id:magattaca:20190210194212p:plain

このままではどこにCF3が入ったか分からないので、Lipitorの骨格でアラインメントを取ります。

AllChem.Compute2DCoords(Lipitor_mol)
for m in CF3_Lipitor_list:
    AllChem.GenerateDepictionMatching2DStructure(m, Lipitor_mol)

ついでにCF3をハイライトするため、部分構造検索と一致した原子の番号を取得します。

CF3_list = []
CF3_subst = Chem.MolFromSmarts('C(F)(F)F')
for mol in CF3_Lipitor_list:
    matched_atoms = mol.GetSubstructMatches(CF3_subst)
    #取得した原子番号はタプルのタプルの型となっているのでindex[0]で最初のものを取り出す
    CF3_list.append(matched_atoms[0])

レジェンドをつけて描画します。

#レジェンドはintではダメだったのでstringに変換
legends = [str(i) for i in range(8)]
Draw.MolsToGridImage(CF3_Lipitor_list, molsPerRow=4, subImgSize=(200,200), legends=legends, highlightAtomLists=CF3_list)

f:id:magattaca:20190210194454p:plain

以上のようになりました。整列してハイライトするととてもわかりやすくなった気がします。

上の構造のうち、実際に論文中で生成が確認された3つの分子は4番(4'-CF3-Lipitor, 単離収率22%)、5番(2-CF3-Lipitor, 単離収率25%)、7(4-CF3-Lipitor, 単離収率27%)です。

フルオロフェニルで反応が進行していないこと、アニリンでの反応位置をみると、このトリフルオロメチル化の反応の選択性は、電子豊富な環、立体的に空いている位置が優先するとなりそうです。

まとめ

以上、今回はSMILESとSMARTSの違いを勉強するということから始めて、SMILESのもう一つの拡張、Reaction SMILES/SMARTS を用いたRDKitの化学反応を使用してみました。通常、有機合成反応では高い選択性かつクリーンな反応が好まれますが、Late-Stage Functionalizationの観点からはむしろ多様な分子を作り出すため、選択性があまりでないことが重要となっています。このような反応形式を検証するにあたって、生成物としてできうる構造をルールに従って全て書き出すことができる、というのはとても便利だなという印象を受けました。反応開発や副生成物の解析(不純物のプロファイリング)といった用途にも色々と応用できそうです。

*1:Chem-stationの記事

*2:Murai reaction(Wikipedia)

*3:反応の構造式、Reaction SMILESともにMarvinSketch 18.24.0を用いて作成

*4:Lafrance, M. and Fagnou, K. J.Am.Chem.Soc.,2006,128,16496

*5:Nature 2012(492)95

*6:最近では、Late-Stage Diversificationともいうのでしょうか?(合成後期多様化法)))

そこで最後に2011年David W. C. MacMillanらがNature誌に報告した光酸化還元触媒(Photoredox Catalyst)を用いたトリフルオロメチル化反応を眺めて見たいと思います。((Nature volume2012(480)224

ファクトフルネスについてのファクトレスな読書感想文

長距離バスにのってお出かけしなければならない事態におちいったため、道中、話題の書籍「ファクトフルネス」を読みました。(往復6時間、要件30分・・・)

 

FACTFULNESS(ファクトフルネス) 10の思い込みを乗り越え、データを基に世界を正しく見る習慣

FACTFULNESS(ファクトフルネス) 10の思い込みを乗り越え、データを基に世界を正しく見る習慣

 

 

Twitterなどで以下のグラフをご覧になった方も多いと思います。世界各国の経済状況・公衆衛生の状況が過去200年以上にわたってどのように推移してきたかを示すものです(横軸に収入、縦軸に平均余命をとり、各国の年次推移が動的なバブルチャートとしてあらわされています*1

 

 

ファクトフルネスは上のグラフを作成・公開しているギャップマインダー財団(Gapminder)の設立者、ハンス・ロリング、オーラ・ロスリング、そしてアンナ・ロスリング・ロランドの3名によって書かれた書籍です。

 

医師・公衆衛生学者として"途上国"における貧困と疫病の研究に長年携わり、その実態と現状を伝える啓蒙活動にも取り組まれたハンス・ロスリング博士(1948 - 2017)の集大成とも言える内容となっています。

 

オーラ・ロスリング氏、アンナ・ロスリング・ロランド氏はハンス・ロスリング博士のご子息夫妻とのことで、博士の培った知識・経験をよりわかりやすく伝えるべく、データ解析と分析、動的なバブルチャートの開発といった明快なプレゼンテーションの作成に携わってこられたとのことです。

 

時にはぶつかり合いながら、異なる視点・アイデアからの意見を戦わせつつ仕事をすすめてきた、というお三方の成果がいかに素晴らしいものかは以下のTEDトーク動画をみていただければ一目瞭然だと思います。

 


How not to be ignorant about the world | Hans and Ola Rosling

 

上のTED  動画冒頭と同じく、本書は読者に対するクイズから始まります。

 

質問1

現在、低所得国に暮らす女子の何割が、初等教育を終了するでしょう?

A. 20%

B. 40%

C. 60% 

 (A ・・・かな?と思った方は私と同類です(答えは書籍をご確認ください))

 

驚くことに、私のような一般人(以下?)は言わずもがな、世界情勢について関心を抱き政策立案に携わるような要人やリンダウ・ノーベル賞受賞者会議に出席する科学者たちに対して同様の質問をなげかけた際にも、その正答率は惨憺たるものだったそうで、ロスリング氏は「これならチンパンジーにランダムに選ばせた方が正答率が高い!!」と、世間一般の現状認識のズレに対して警鐘を鳴らしています。

 

「どうしてそんなズレが生じてしまうのか??」

 

ロリング博士は、社会情勢に関して学校で学んだ知識が「最新の情報にアップデートされていない」からだ、と考え、様々な機関・場所で講演会・啓蒙活動を行い、「現在の世界の実情」についての正しい理解広めることに努めてこられました。

 

「あーなるほど、OK!世界の状況こんな風に変わってたのかー。知らなかったわー。チャート見たしTEDもみたし、だいたい分かったからこれで大丈夫!」と思ったあなた!!! そんなあなたこそ、本書をきちんと読む必要があります。

 

なぜなら本書は、ロリング博士が情報のアップデート・発信に努めてこられたにも関わらず、一向に世間一般の認識に改善が見られない。 それはなぜか??? その背景には我々が物事を把握する際に知らず知らず、現実(ファクト)から離れ、思い込みによって「理解 / 解釈」してしまうという本能の傾向があるからではないか? という問題を投げかけることこそが主題となっているからです。

 

我々が陥ってしまう10個の思い込み、そしてそれを乗り越え「データをもとに世界を正しく見る習慣」を身につけるためにはどうすれば良いのか? ネタバレになってしまうので詳細は書きませんが、時間のない方のために・・・

 

書店で手に取っていただき目次を眺めた後、324-325ページをご覧ください。「最後にひとこと」と題する見開きとなると思います。このページを進める時点で書評としては失格となると思いますが、こちらに事実(データ)に基づいて現実を認識するためのファクトフルネスの10個のポイントがイラストで示されています。 プレゼンテーションのプロフェッショナルでもある、著者3名の技術が遺憾無く発揮されたページでもあると思いますが、こちらをご覧になって一つでもひっかかる言葉があれば、その章だけでもお読みになることをお勧めします。

 

さてさて、本書を褒めるのはこれくらいにして、、、

アフィリエイトではないので上のアマゾンのリンクをクリックしても大丈夫!(・・・何が???))

 

なぜ本書の感想文をこちらで書いてみようかなと思ったかというと、本書の主題が「データを現実の理解に如何に生かすか?」であり、インフォマティクス に関連する話題なのではないかな???と思ったからです。

 

きっかけはファクトネスの「ルール3:直線本能を抑えるには・・・」です。この章では、我々が「あるデータの時系列変化」を追っていると、その変化がそのまま「直線的に継続」して進行すると思い込んでしまいがちである、という問題点が指摘されています。

直線的ではない変化の例として、著者が実際に遭遇した2014年のエボラ出血熱予想感染者数のデータが挙げられています。感染者数が直線的ではなく倍々(指数関数的!)に増加していると気づいたことから事態の深刻さ、一刻も早く対処しなければならないことに気づいたという経験が語られています。この経験に関連づけられて、データの推移はさまざまな変化のパターン(S字カーブ、滑り台型、コブ型…)がありうるので気をつけなければならないといったことが述べられています。

 

この章を読んだ際にこれって久保先生の「データ解析のための統計モデリング入門」で語られていたことにつながるのでは???という感想を抱きました。 

 

 久保先生の本ではデータをうまく説明し予測を可能とするモデルをどう構築するか、モデルを作る上での分野の知識(ドメイン知識、特徴量選別・抽出)の重要性が強調されたように思います(・・・・浅い理解ですみません)。一方、ファクトフルネスでは世界の人口変化の予測、とくにその年齢層の分布の変化予測のモデルが取り上げられていますが、あわせてハンス・ロスリング博士が人口調査のプロ・専門家による予測を如何に重視・信用しているかという点についても語られています。データに基づき、予測・意思決定を行うことの重要性は疑うまでもないことですが、そのデータの持つ意味、解釈にあったって、深い知識・見識をもつ専門家の意見(とそのモデル)に敬意を払うことは、すこしでも科学に関わった身(末端、ドロップアウト組ですが・・・)としては肝に命じておきたいことだという感想を抱きました。

 

その他にも、「世界で起こっていることを理解するため確かなデータが手に入るようになったことが重要だった」といった統計データの正確性の重要性や、「なぜ、先進国では科学に逆行するかのようにワクチン使用・化学物質に対する過剰な嫌悪が生じるのか」といった現在の日本のニュースを見ていると身につまされるような話題も挙げられているのですが、もっとも印象に残ったエピソードをあげさせていただきます。

 

若き日のロスリング博士がモザンピークで医療に携わっていたころ、治療環境・現場スタッフのレベルといった種々の制約の中で、日々は運び込まれる乳幼児・子供の治療に対して、博士は先進国における治療の方針から判断すれば不適切ともとれるような簡易の治療手段をとることを余儀なくされます。ある日訪れた別の地域で働く友人医師(同様に貧困地域の医療に携わる医師ではあるが、ロスリング博士よりも少しマシな地域の担当だった)には、博士のとる治療手段が手抜きのようにみえ、「今、その場、目の前にいる患者に対して最善、最良の医療を施す努力をしないのは医師として失格なのではないか」、という叱責を受けることになります。しかしながらロスリング博士は「自分の使命は、病院に運び込まれる子供を助けることだけではなく、より多数の病院にすらくることもできないような子供も救うことができるような地域全体の医療を改善することだ。そのためには、病院での治療が簡易のものとなったとしても、地域の公衆衛生全体を改善することにより多くの時間を割くべきだ」という苦渋の決断を下すこととなります。

 

ロスリング博士の下した決断は、個々の命を「統計的に処理」し、全体としての最適な状態を目指すためには、「それぞれの命のかけがえのなさ」といったものが、ある種「軽視された」ように見えてしまう、という倫理的な問題を孕んだものとなります。このエピソードを読んだ際に私は学部生の頃にうけた生命倫理の講義における問いを思い出しました。「生政治の哲学」を扱ったこの講義は、私の学生生活の中で最も素晴らしく、現在に至るまで深い影響を受けたものなのですが、そこにおいて出された問いは以下のようなものです。

 

阪神淡路大震災以降トリアージという言葉が医療の現場で注目を浴びた(注:東日本大震災が起こる前でした)。災害時にはその場で治療によって救うことができるかを判断し、救うことができないのと判断したならば直ちに治療を中止し、優先順位をつけて救うことができる患者の治療に時間を割くよう判断を下すのが適切であるとする概念だ。確かに全体で見たときの結果としては適切な判断で最良の結果をもたらすだろう。そうだとして君達は今、目の前にいる患者を救える可能性が低いからといって切り捨てる決断が本当に下せるのか?」

 

といったものでした。この問いを受けた時に自分の論理的な弱さ、論理に基づいて決断をくだすということの難しさに気付かされたのは、非常に忘れがたい思い出です。

 

(学生生活最高の講義であったといいつつ、課題も提出できず単位を落としたクズ学生・・・。最近、先生の訃報を目にし非常にショックを受けました・・・。幾分、悲観的すぎるかとも思いましたが、とても誠実な講義をされる先生でした。)

 

落ちこぼれの思い出話はおいておいて、「ファクトフルネス」は我々が気づかぬまに陥っている思い込みを指摘し、如何に事実(データ)にもとづき判断する習慣を身につけるべきか、その重要性を指摘するだけではなく、一人の優れた医学者・公衆衛生学者が世界でおきている状況を改善すべく奮闘してきたか、生涯をかけた仕事の集大成ともいえるべき内容を、軽妙な語り口で重くなりすぎず誠実に語った良書だと思います。

 

さてさて、本記事の冒頭、「"途上国"」とあえてクォーテーションで囲った記載を行いました。なぜそのようなことをしたのか疑問に思った方、「先進国/ 途上国」という切り分けに違和感を抱かなかった方は是非是非「ファクトフルネス」を手にとっていただければと思います。知らぬ間に前提として受け入れていた世界の見方が如何にアップデートできるか、きっと楽しめると思います。 

 

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

注)

ギャップマインダーの統計データは引用自由なものとして公開されています。

 

 

 

 

 

*1: Gapminder Tools

より)

部分構造で絞り込む話

Twitterで拝見した以下の処理の手順を参考に、指標を用いた粗い絞り込みまでを実施しました。

処理の手順

処理 意図
分子量の範囲を設定 粗い絞り込み
部分構造で絞り込み 活性化合物を参照したLBVS(?)
粗いドッキング タンパク質との相互作用を予測するSBVS(?)
ファーマコフォアを指定したより詳細なドッキング SBVS 2回目(?)

指標による絞り込み基準

指標 分子量 LogP 水素結合供与体数 水素結合受容体数 回転可能結合数 極性表面積
Rule of 5 ≦500 ≦5 ≦5 ≦10
基準 >300 >3 >3 >3 >60

今回は指標による絞り込みの次の段階、部分構造での絞り込みを行いたいと思います。
参考にさせていただいている処理の方法では、オルト位に置換基の入ったビフェニルで絞り込んだとのことでしたが、いきなり置換基なども含めた処理を行うのは難しいので、まずはビフェニル構造での絞り込みを行いたいと思います。

活性化合物のデータセットでビフェニルを探す

そもそもなぜビフェニルなのか? 活性化合物のデータセット共闘用シェアデータ )中の分子(マクロサイクル型を除く)が、ビフェニル構造をもつのか検証してみます。

「化学の新しいカタチ」さんのこちらの記事「 RDKitを用いた部分構造検索とMCSアルゴリズム 」 を参考にRDKitの部分構造検索メソッドを利用します。

まずは必要なものをimport・・・

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw

RDKitで部分構造検索を行う際、検索構造(query)はMolオブジェクトにする必要があるということなので、まずはビフェニル構造のMolオブジェクトをSMILESから作成します。

biphenyl = Chem.MolFromSmiles('c1ccc(cc1)c1ccccc1')
Draw.MolToImage(biphenyl)

f:id:magattaca:20190203191836p:plain

きちんとビフェニル構造のMolオブジェクトが作られていそうです。こちらをテンプレートとして用い、部分構造検索を行います。

#以前取り出した鎖状分子のSDFを使用する
chain_compounds_suppl = Chem.ForwardSDMolSupplier('chain_compounds.sdf')

mols = [mol for mol in chain_compounds_suppl if mol is not None]   

HasSubstructMatchをもちいて、ビフェニル構造を持つ分子と持たない分子を区別し、それぞれ別々のリストに入れていきます。

#ビフェニルありのリスト
biphenyl_compounds = []
#ビフェニルなしのリスト
w_o_biphenyl = []

for mol in mols:
    if mol.HasSubstructMatch(biphenyl):
        biphenyl_compounds.append(mol)
    else:
        w_o_biphenyl.append(mol)

print('Number of compounds with biphenyl', len(biphenyl_compounds))
# Number of compounds with biphenyl 29
print('Number of compounds without biphenyl', len(w_o_biphenyl))
# Number of compounds without biphenyl 10

39個の分子のうち、29個にビフェニルが含まれました。確かに活性化合物は多くがビフェニル構造を持つようです。

最初の一つの分子を取り出して、ビフェニル構造の確認を行います。

GetSubstructMatchesでマッチした複数の原子インデックスを取得できるそうです。

test_mol = biphenyl_compounds[0]
matched_atoms = test_mol.GetSubstructMatches(biphenyl)
print(matched_atoms)
# ((23, 17, 18, 20, 21, 22, 24, 25, 26, 27, 28, 29),)

原子インデックスのタプルのタプルとして得られました。

原子インデックスを描画してみます。

RDKitのメーリングリストディスカッション によると、オプションを「Draw.DrawingOptions.includeAtomNumbers=True」と設定することで、表示できるようです。少し書き方を変えて以下のようにしました。

Draw.MolToImage(test_mol, includeAtomNumbers = True)

f:id:magattaca:20190203192126p:plain

構造式の上の方に、認識された原子インデックスがあるのがわかります。

こちらをハイライトしてみます。

#タプルのタプルなのでインデックス[0]が必要
Draw.MolToImage(test_mol, highlightAtoms = matched_atoms[0]) 

f:id:magattaca:20190203192244p:plain

順番に眺めます。

from ipywidgets import interact,fixed,IntSlider
import ipywidgets

def biphenyl_viewer(idx):
    mol = biphenyl_compounds[idx]
    matched_atoms = mol.GetSubstructMatches(biphenyl)
    return(Draw.MolToImage(mol, highlightAtoms = matched_atoms[0]))

interact(biphenyl_viewer, idx=ipywidgets.IntSlider(min=0,max=len(biphenyl_compounds)-1, step=1));

f:id:magattaca:20190203192518g:plain

ビフェニル構造は末端近くにあるものが多いようですが、いくつかは分子の中央部分にもあります。共通構造といっても分子の中で占める位置が異なるものもあるようです。
中央部分にビフェニルを含むものは、ビフェニルを中心とした対称構造のようにもみえるので、結合様式等(?)の知見を反映してデザインされた化合物なのかもしれません。

ライブラリ化合物で部分構造による絞り込み

それでは、ライブラリ化合物の中からビフェニル構造を持つ化合物を取り出してみたいと思います。

まずは指標を計算済みのEnamine_Premium_collectionについて、指標でのフィルタリングを実施します。

EPc_suppl = Chem.ForwardSDMolSupplier('./Enamine_Premium_collection.sdf')

EPc_mols_filt = []

for x in EPc_suppl:
    if x is not None:
        mol = x
    MW_value = mol.GetDoubleProp('MW')
    MolLogP_value = mol.GetDoubleProp('MolLogP')
    NumHDonors_value = mol.GetDoubleProp('NumHDonors')
    NumHAcceptors_value = mol.GetDoubleProp('NumHAcceptors')
    NumRotatableBonds_value = mol.GetDoubleProp('NumRotatableBonds')
    TPSA_value = mol.GetDoubleProp('TPSA')
    
    #フィルタリング基準を満たす場合のみリストに追加する
    if MW_value > 300 and MW_value <= 500 \
    and MolLogP_value > 3 and MolLogP_value <=5\
    and NumHDonors_value <= 5 \
    and NumHAcceptors_value > 3 and NumHAcceptors_value <= 10 \
    and NumRotatableBonds_value > 3 \
    and TPSA_value > 60 :
        filtered_mols.append(mol)

フィルタリングを満たした化合物のリストを、活性化合物群に対して行なったのと同様にビフェニルの有無で別々のリストにします。

biphenyl = Chem.MolFromSmiles('c1ccc(cc1)c1ccccc1')
biphenyl_compounds = []
w_o_biphenyl = []
<200b>
for mol in filtered_mols:
    if mol.HasSubstructMatch(biphenyl):
        biphenyl_compounds.append(mol)
    else:
        w_o_biphenyl.append(mol)

print(len(biphenyl_compounds))  
#12
print(len(w_o_biphenyl))
#4048

Enamine_Premium_collectionで条件を満たすものは12個となりました。 構造を眺めてみます。

Draw.MolsToGridImage(biphenyl_compounds, molsPerRow=4, subImgSize=(200,200))

f:id:magattaca:20190203193038p:plain

なかなかPremium感のあるいい感じの化合物が残っている気がします。

他のライブラリに適用した結果とあわせてまとめます。

Enamine_Premium Enamie_Advenced Enamine_HTS UOS_HTS total
指標でのフィルタリング後 4060 37431 414562 106948 563001
ビフェニル有 12 329 3182 697 4220
(0.7%)
ビフェニル無 4048 37102 411380 106251 558781
(99.3%)

全体を合わせて 4220 個の分子が残りました。まだまだ多いですが、かなり数を絞り込めてきているように思います。

部分構造検索をKNIMEで

Twitter話題のKNIME・・・、@sumatat_ さんのブログ非プログラマーのためのインフォマティクス 入門。(仮) のコンテンツが素晴らしいのでKNIMEブームに便乗することにしました。

(ブログタイトルがまさにこれを求めていた!!って感じです)

今回行った流れは、

  1. 活性化合物群をビフェニルの有無で分ける
  2. フィンガープリントを計算し、PCAで次元圧縮したうえでプロット
  3. インタラクティブに遊ぶ

です。

以下の記事を参考にさせていただきました

  1. @Mochimasa さんの記事 化合物をベクトルにして比較しプロットする
  2. @sumatat_ さんの記事 KNIMEで化合物をクラスタリング&可視化してみよう
  3. @iwatobipen先生の記事 Make interactive plot with Knime
  4. @PKさんの記事 【KNIME】CellProfilerの画像解析結果を可視化する

早速ワークフロー全体です。2通りの可視化を行うため、プロットのためののメタノード(灰色)が2つあります。

f:id:magattaca:20190203193203p:plain

KNIMEの視認性の高さはすごいですね!何となくやっていることがわかります。 Jupyter NoteBookは3日経つと自分の操作の意味すらわからなくなります(私だけ?)が、これなら思い出しやすそうです。

一応ワークフローの説明をすると

  1. SDFの読み込み(SDF Reader)
  2. SDFの構造ををRDKitのMolオブジェクトに変換(RDKit To Molecule)
  3. ビフェニル構造の有無で2つに分割(RDKit Substructure Filter)
  4. 分割したテーブルにクラス名のカラムを追加(Constant Value Column)
  5. 2つのテーブルを一つに再結合(Concatenate)
  6. フィンガープリント(Morgan/ 1024ビット/ 半径2)を計算(RDKit Fingerprint)
  7. フィンガープリントのビットベクトルをPCAに渡すため整数値のカラムに分割(Expand Bit Vector
  8. PCA(PCA)
  9. 分子の構造を描画のためSVGに変換(Renderer to Image)
  10. クラスに応じた色分け(Color Manager)
  11. プロット

プロットでは、ビフェニルの有無での色分けと、各化合物の出元(特許の出願人(applicant))での色分けの2通りを行っています。 前者ではTile View (KNIME 3.7.1 ではCard Viewの名称が変更となったそうです)、後者ではTable Viewを使用しました。

メタノードの中身はこちら

f:id:magattaca:20190203193246p:plainf:id:magattaca:20190203193252p:plain

実行した結果はこのようになります。 まずはビフェニルの有無で色分けした場合・・・

f:id:magattaca:20190203193456g:plain

ビフェニルを含まない構造(赤色)の多くは、プロットの右下に集まっています。これらは全てapplicantAurigeneとなっています。他のapplicantとは系統の異なる化合物を報告しているようです。 また、ビフェニルを含む構造(緑色)でも左下には中国医学科学院の化合物群、中央上部にはIncyteの化合物群が集まっているように見えます。興味深いことに、Morganフィンガープリントにもとづくプロットで化合物の出所がうまく分かれているようです。

applicantによる色分けを眺めてみます。

f:id:magattaca:20190203201707g:plain

先のプロットで想定した通り、applicantごとにプロット上でうまく分かれていそうです。

見よう見まねでも何となく使えてしまうとはKNIMEの完成度高い!

オルト位置換の認識に失敗

元々の部分構造フィルタリングの目標は、オルト位に置換基の入ったビフェニルだったので、SMILESのワイルドカード(*)をつかって、以下のコードを実施しました。

ortho_biphenyl = Chem.MolFromSmiles('c1ccc(c(*)c1)c1ccccc1')
Draw.MolToImage(ortho_biphenyl)

f:id:magattaca:20190203200615p:plain

オルト位置換(なんでも良い)が再現できていそうです。 以下のテスト分子で試します。

f:id:magattaca:20190203200739p:plain

test_mol.HasSubstructMatch(ortho_biphenyl)
# Flase

・・・ダメでした。置換基あるのに・・・

まとめ

今回、「ビフェニル構造を部分構造としてもつか否か」を基準としてライブラリのさらなる絞り込みを実施しました。その結果、約4000 個にまで化合物数を絞り込むことができました。また、KNIMEをもちいた可視化を実施し、フィンガープリントを持ちいることで化合物群を出所(出願者)に応じてうまく識別できる可能性があることがわかりました。

残念ながら、オルト位に置換基の入ったビフェニル、というフィルタリングはうまくできませんでした。私、応用力無さすぎ・・・

Lipinski先生に従うことにした話

以前の記事 でライブラリの指標の計算を行いました。その結果、さらに前の記事 で活性化合物群から求めた指標の閾値では、全く化合物の数が絞れないことがわかりました。

やはり、素人が付け焼き刃で基準値を考えるのは無理がある・・・ここは偉大な先人の後についていくしかない・・・ということでまずは、以下の2つのフィルタリングで化合物数が絞り込めるか試してみたいと思います。

  1. LipinskiのRule of 5を満たすもの
  2. フラグメント指標 Rule of Threeを元にしたフラグメントの削除

1.のLipinskiのRule of 5 に加えて、2.の基準を加えた理由は、
「今回の目標はフラグメントレベルではなくある程度の活性を達成できるような化合物を見出すことなので、むしろフラグメント以下の分子は除いてしまった方が良い」
ということです。

LipinskiのRule of 5の適用

Lipinskiの法則に関しては「化学の新しいカタチ」さんのこちらの記事( RDKitにおける記述子の扱い方をリピンスキーの法則を通して学ぶ )を参考にさせていただきました。

指標 分子量 LogP 水素結合供与体数 水素結合受容体数
Rule of 5 ≦500 ≦5 ≦5 ≦10

指標の計算値のみを集めたcsvファイルを出力しておいたのでこちらを利用して、Rule of 5を満たす化合物の数を取得します。

まずはPandasのDataFrameで読み込みます。

df_EAc = pd.read_csv('./Enamine_Advanced_collection_desc.csv')

Lipinskiの法則を判断する関数を作成し、DataFrameにapplyで適用しようとしたのですが、どうしてもうまくできませんでした。また、mapでは遅いという記事を見かけたので、こちら(pandasで複数カラムを参照して高速に1行1行値を調整する際のメモ)を参考に処理を行いました。

PandasのDataFrameにapplyで関数を適用すると、Seriesとして1行ずつ処理することになるようですが、こちらのSeriesへのアクセスが遅いのが問題とのことです。解決策として、Pythonの辞書へのアクセスが高速であることを利用すれば良い、とのことでした。

# Lipinskiの判別に用いる指標を辞書として取り出す。(to_dict関数)
MW_dict = df_EAc['MW'].to_dict()
MolLogP_dict = df_EAc['MolLogP'].to_dict()
NumHDonors_dict = df_EAc['NumHDonors'].to_dict()
NumHAcceptors_dict = df_EAc['NumHAcceptors'].to_dict()

# 辞書のKeyをDataFrameのindexの値としているので、indexの値を格納するカラムをDataFrameに追加する。
df_EAc['index_val'] = df_EAc.index

# Lipinskiのルールを判別する関数を作成
def lipinski(index):
    # indexの分子の指標を取り出す
    MW = MW_dict[index]
    MolLogP = MolLogP_dict[index]
    NumHDonors = NumHDonors_dict[index]
    NumHAcceptors = NumHAcceptors_dict[index]
    
    # Lipinskiのルールに合致するならTrue、しないならFalseとする
    if  MW <= 500 \
        and MolLogP <=5\
        and NumHDonors <=5 \
        and NumHAcceptors <=10:
            return True
                          
    else:
        return False

# 上記関数を適用し、新しいカラム(Lipinski)に当てはめる
df_EAc['Lipinski'] = df_EAc['index_val'].apply(lipinski)

以上で、Lipinskiのルールの判別が完了しました。あとはTrueの数を数えれば合致した分子の数がわかります。

True1False0なので、そのままsum()を実行することで条件を満たす要素の数が得られるそうです。(参考記事

EAc_Lipinski_True = df_EAc['Lipinski'].sum()
print(EAc_Lipinski_True)

483858となりました。Enamine_Advanced_collectionは元々が486322だったので、2千個程度Lipinskiの法則を満たさないものが含まれているようです。

他のライブラリの計算結果と合わせると下記の通りです。

Lipinski Enamine_Premium
(分子量300以上)
Enamine_Advenced Enamine_HTS UOS_HTS
総数 109602 486322 1921489 516664
True 109590 483858 1843248 437821
False 12 2464 78241 78843

Enamine_Premium_collectionのみFalseとなった数が12と少ないのが興味深い結果です。Premiumだけに何らかのプレミアムな基準で選ばれた優良な化合物たちなのでしょうか??

Enamine_Premium_collectionだけでも11万個の化合物があるので、こちらのみから絞り込むのでも良い気がしてきました・・・

フラグメントライクな化合物

Lipinskiの基準は、ある閾値以下の化合物を選抜するものです。前回参照したSAR News Np.19の記事には、フラグメント指標「Rule of Three」が記載されていました。

指標 分子量 LogP 水素結合供与体数 水素結合受容体数 回転可能結合数 極性表面積
基準 ≦300 ≦3 ≦3 ≦3 ≦3 ≦60

こちらを頼りにしてフラグメントライクな分子を取り除こうと思います。

以下に、一つずつの指標でフィルタリングした場合と、全てを満たす場合の数をまとめました。

指標 総分子数 分子量 LogP 水素結合供与体数 水素結合受容体数 回転可能結合数 極性表面積 すべて満たすもの
基準 >300 >3 >3 >3 >3 >60
Enamine_Premium
(分子量300以上)
109602 109602
(100%)
13327
(12%)
49
(0.04%)
99362
(91%)
78633
(72%)
84269
(77%)
0
Enamine_Advenced 486322 305230
(63%)
129679
(27%)
902
(0.2%)
325234
(67%)
305416
(63%)
277665
(57%)
64
(0.01%)
Enamine_HTS 1921489 1609119
(84%)
904582
(47%)
6847
(0.4%)
1492627
(78%)
1547785
(81%)
1319649
(69%)
87
(0.05%)
UOS_HTS 516664 439751
(85%)
284727
(55%)
2461
(0.5%)
400416
(78%)
433231
(84%)
351715
(68%)
746
(0.14%)

6つの指標すべてでフラグメント指標 Rule of Threeよりも大きいものの和集合、という基準(一番右の列)にしてしまうと、ほとんどの分子が除外されてしまいます。

特にプレミアム感のある期待のライブラリEnamine_Premium_collection がすべてなくなってしまいます。これではちょっとやりすぎ感があります。

上記の表を見ると、特に水素結合供与体数(>3)が削減率が高く、ついで LogP(>3) で削られているものも多そうです。

計算方法が間違っているといけないので、以下にコードを転記しておきます。(かなり冗長です)

#一つずつ計算する場合
df_MW_300 = df_EAc['MW'] > 300
df_MolLogP_3 = df_EAc['MolLogP'] > 3
df_NumHD_3 = df_EAc['NumHDonors'] > 3
df_NumHA_3 = df_EAc['NumHAcceptors'] > 3
df_NumRB_3 = df_EAc['NumRotatableBonds'] > 3
df_TPSA_60 = df_EAc['TPSA'] > 60

print(df_MW_300.sum())
print((df_MW_300.sum() / len(df_EAc))*100)

print(df_MolLogP_3.sum())
print((df_MolLogP_3.sum() / len(df_EAc))*100)

print(df_NumHD_3.sum())
print((df_NumHD_3.sum() / len(df_EAc))*100)

print(df_NumHA_3.sum())
print((df_NumHA_3.sum() / len(df_EAc))*100)

print(df_NumRB_3.sum())
print((df_NumRB_3.sum() / len(df_EAc))*100)

print(df_TPSA_60.sum())
print((df_TPSA_60.sum() / len(df_EAc))*100)

# すべての基準の和集合
df_all =((df_EAc['MW'] > 300) & \
         (df_EAc['MolLogP'] > 3) & \
         (df_EAc['NumHDonors'] > 3) & \
         (df_EAc['NumHAcceptors'] > 3) & \
         (df_EAc['NumRotatableBonds'] > 3) & \
         (df_EAc['TPSA'] > 60))

print(df_all.sum())
print((df_all.sum() / len(df_EAc)) * 100)

PCAを用いた次元圧縮

フラグメントライブラリの指標をすべて逆転して用いてしまうと、化合物数を減らしすぎてしまう・・・でもどの基準を残せば良いかわからない・・・。

指標を選択する基準はないか???ということでPCAを用いた次元圧縮を行ってみたいと思います。 (たぶん使い方間違ってる)

まずはすべてのDataframeを統合します。Enamine_Advanced_collectionのMolLogPの計算値には欠損値(NaN)があるのでこれは除いておきます。

# NaNを含む列の削除
df_EAc_w_o_NaN = df_EAc.dropna()

# 確認(isnullでTrueとなる数をカウントして合計が0ならNaNは無い)
print(df_EAc_w_o_NaN.isnull().values.sum() == 0) 
# True

# 4つのライブラリを統合
df_all = pd.concat([df_EPc, df_EAc_w_o_NaN, df_EHc, df_UH]) 

# 化合物総数
print(len(df_all))
#3034076

全部で約 300万個あります。

これにPCAを行ってみます。

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

sc = StandardScaler()

# 指標の値をPandasからnumpyのndarrayに変換する (values)
X = df_all[descriptors].values
# 標準化
X_std = sc.fit_transform(X)
# PCA(2成分)
pca = PCA(n_components =2)
X_pca = pca.fit_transform(X_std)
#可視化
plt.figure()
plt.scatter(X_pca[:, 0], X_std[:, 1])
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.show()

#次元ごとの寄与率
print(pca.explained_variance_ratio_)

f:id:magattaca:20190127232211p:plain

コピペしてみたが、何がおきているかよくわからない!!

とりあえず第1主成分と、第2主成分をあわせても累積寄与率(?)は7割以下みたいです。

因子負荷量の計算

よくわからないが理解するのを待っていたらいつまでかかるかわからない!突き進むしかない!

各主成分に対してどの指標がどの程度相関しているかを眺めるため、因子負荷量を計算します。

import numpy as np

# 因子負荷量を計算
pca_components = pca.components_*np.c_[np.sqrt(pca.explained_variance_)]

# 眺めるための新しいDataFrameを作成
column_names = descriptors
PCA_df = pd.DataFrame([pca_components[0], pca_components[1]], columns=column_names) 

以下のようになりました。

f:id:magattaca:20190127232658p:plain

ついでにグラフ化しておきます。(グラフでは絶対値にしています)

labels = ['MW', 'MolLogP', 'NumHD', 'NumHA', 'NumRB', 'TPSA']

# NumPyのfabsを使って絶対値に修正
PCA_df_abs = np.fabs(PCA_df)

height_0=PCA_df_abs.loc[0]
height_1=PCA_df_abs.loc[1]
left= np.arange(len(height_1))  

width=0.3
plt.bar(left, height_0, color='r', width =width)
plt.bar(left+width, height_1, color='b', width =width)

plt.xticks(left, labels)
plt.show()

f:id:magattaca:20190127232841p:plain

第1主成分赤色)は、分子量(MW)、水素結合受容体数(NumHA)、回転可能結合数(NumRB)、極性表面積(TPSA)との相関が、第2主成分青色)はLogP(MolLogP)との相関が強そうです。

水素結合供与体数(NumHD)は第1主成分よりも第2主成分との相関が強そうですが、MolLogPと比べると見劣りがします。

もう、NumHDを削ってしまっても良さそうな気がしてきました。(・・・無理やりこじつけた)

# NumHDonor以外の基準の和集合
df_all_w_o_NumHD =((df_all['MW'] > 300) & \
                   (df_all['MolLogP'] > 3) & \
                   (df_all['NumHAcceptors'] > 3) & \
                   (df_all['NumRotatableBonds'] > 3) & \
                   (df_all['TPSA'] > 60))

print(df_all_w_o_NumHD.sum())
print((df_all_w_o_NumHD.sum() / len(df_all)) * 100)

フラグメント指標から水素結合供与体数を除いた残りの5つを使って、5つ全てを満たす化合物数を求めると645704個(21%)となりました。

これならまだマシそうなので、こちらを用いたいと思います。

二つの基準を満たす分子の数

以上、見てきた内容をまとめると適用する基準は下記となります。

指標 分子量 LogP 水素結合供与体数 水素結合受容体数 回転可能結合数 極性表面積
Lipinskiより ≦500 ≦5 ≦5 ≦10
フラグメント指標より >300 >3 >3 >3 >60

2つの基準を満たす分子はどの程度あるでしょうか?

df_all_both = ((df_all['MW'] > 300) & (df_all['MW'] <= 500) & \
               (df_all['MolLogP'] > 3) & (df_all['MolLogP'] <= 5) &\
               (df_all['NumHDonors'] <= 5) & \
               (df_all['NumHAcceptors'] > 3) & (df_all['NumHAcceptors'] <= 10) &\   # 5以下になっていたので10以下に修正(01/28)
               (df_all['NumRotatableBonds'] > 3) & \
               (df_all['TPSA'] > 60))

print(len(df_all))   # 3034076
print(df_all_both.sum())  # 563000
print((df_all_both.sum() / len(df_all)) * 100)   # 18.55589642447981

元々の分子の総数 3034076、両基準を満たす分子の数563000(19%)となりました。
(最初の投稿では NumHA 10以下ではなく間違って5以下としていたため、このさらに半分の約30万個にまで減っていました。(01/28修正))

両基準で絞り込んだ化合物数は約56万個で、 もともと300万個の化合物があったので、1/5程度にまで減らすことができたことになります。

かなり無茶苦茶な話をしているので怒られてしまいそうですが、とりあえずの結果としてこの基準をもとに進めていきたいと思います。

解釈や使用方法等に誤りがたくさんあると思うのでご指摘いただければ幸いです。

SSSRを使って環状化合物と鎖状化合物を区別しようとする話

以前の記事で、創薬レイドバトルの共闘用シェアデータを2つにクラスタリングした後、環状ペプチドとそれ以外の分子の集まりとに手作業で分類しました。これでは数増えた時に大変ですし、見落としもありそうです。そこで今回はRDKit オンラインドキュメンテーションに記載のあったSSSRという考え方を頼りに同様の分類できるかどうか試してみたいと思います。

RDKitオンラインドキュメンテーションの該当箇所は下記です。
1. Ring Information
2. Ring Finding and SSSR

SSSR(smallest set of smallest rings; SSSR)については @mojaie さんのこちらの記事 化学構造情報とグラフ理論の説明が非常にわかりやいのでご参照ください。

とにかく、分子に含まれる環構造のうち「環の個数と環の大きさが最小になるような組み合わせ」だそうです。(ふわっとした理解)

環の情報がわかるなら、大員環とそれ以外の分子と区別することもできるのでは??? というのが今回の記事の目的です。

まずはデータの準備

from rdkit import rdBase, Chem
from rdkit.Chem import AllChem, Draw, Descriptors, PandasTools
import pandas as pd
 

df = pd.read_csv('../pd1_inhibitor_dataset-master/PD1_inhibitor_dataset.csv')
PandasTools.AddMoleculeColumnToFrame(df, 'smiles')

GetSSSR関数

RDKitのドキュメンテーションによると、SSSRに関してはGetSSSRとGetSymmSSSRの2つの関数があるそうです。

まずはGetSSSR関数を用いて見ます。こちらはSSSRの数を取得することができる関数です。

データセットの最初の分子をテスト化合物としてどのような結果が出るのか眺めて見ます。

最初の分子の構造はこのようなものです。

test_mol = df.loc[0, 'ROMol']
Draw.MolToImage(test_mol)

f:id:magattaca:20190127102613p:plain

この分子のSSSRの数は???

test_SSSR = Chem.GetSSSR(test_mol)
print(test_SSSR)
# 8

8個だそうです!

・・・わからん。

SSSRの数だけではよくわかりません。

GetSymmSSSR関数

もう一方の関数、GetSymmSSSRを使うと、認識された各環構造の情報を取得することもできるそうです。

test_SymmSSSR = Chem.GetSymmSSSR(test_mol)
print(type(test_SymmSSSR))
# <class 'rdkit.rdBase._vectNSt3__16vectorIiNS_9allocatorIiEEEE'>

print(type(test_SSSR))
# <class 'int'>

GetSSSRで得た結果は整数値(int)ですが、GetSymmSSSRで得た結果はrdkitのオブジェクトでSSSRについてより詳細な情報をもっていそうです。

GetSymmSSSRの結果から、SSSRの数を取得するには長さを求めれば良いそうです。

print(len(test_SymmSSSR))
#8

先ほどの結果と一致しました。

最大の環構造を探す

ドキュメンテーションの説明をみる限り、GetSymmSSSRで得られるものは、認識された環構造のリストとなっており、リストの要素は各環構造に含まれる原子(atom index)のリストとなっているようなオブジェクトのようです(リストのリスト)。

8個の各環構造のサイズを求めてみます。

for i in range(len(test_SymmSSSR)):
    print('{}番目の環のサイズ:{}'.format(i, len(test_SymmSSSR[i])))

"""
出力結果
0番目の環のサイズ:45
1番目の環のサイズ:5
2番目の環のサイズ:5
3番目の環のサイズ:6
4番目の環のサイズ:5
5番目の環のサイズ:6
6番目の環のサイズ:5
7番目の環のサイズ:6
"""

0番目が環員数 45となっています。ぼんやりと眺めていましたが環状ペプチドの長さはこんなにも長いものなんですね。具体的な数値を見るとあらためてびっくりしました。

この最大の環構造を構成する原子の番号(atom index)を取得して見ます。

largest_ring_member = list(test_SymmSSSR[0])
print(largest_ring_member)

# [4, 5, 7, 8, 9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 22, 23, 24, 26, 27, 28, 30, 34, 35, 37, 38, 39, 41, 42, 43, 45, 46, 47, 49, 50, 51, 53, 54, 55, 57, 58, 59, 61, 62, 63, 65]

構成要素の原子がわかったので、この情報を用いて分子のどの原子が該当の環構造に含まれるのか、ハイライトして描画して見ました。

f:id:magattaca:20190127103859p:plain

期待通り、環状ペプチドの骨格となる構造を認識できていそうです。

環構造を順番に眺める

ipywidgetsを利用して各環構造を順番に眺めて見ます。

# 描画用
from rdkit.Chem.Draw import rdMolDraw2D
from IPython.display import SVG
from matplotlib.colors import ColorConverter

# インタラクティブなビューワー
from ipywidgets import interact,fixed,IntSlider
import ipywidgets

# ビューワーに与える関数を作成
def SSSR_viwer(idx):
    ring_member_atoms = list(test_SymmSSSR[idx])
    prepro_test_mol = rdMolDraw2D.PrepareMolForDrawing(test_mol)
    target_ring_atoms = list(test_SymmSSSR[idx])
    
    #デフォルトの設定は見づらいので色を変える
    color_dict = {}
    radius_dict = {}
    for i in target_ring_atoms:
        color_dict[i] = ColorConverter().to_rgb('khaki')
        radius_dict[i] = 0.7
    
    #コンテナとなるオブジェクトの作成
    view = rdMolDraw2D.MolDraw2DSVG(600,600)
    
    #コンテナの描画設定
    option = view.drawOptions()
    option.circleAtoms=False
    option.continuousHighlights=False
    
    #最大の環構造に含まれる原子をハイライトに設定
    view.DrawMolecule(prepro_test_mol, 
                      highlightAtoms=target_ring_atoms,
                      highlightAtomColors= color_dict,
                      highlightAtomRadii= radius_dict,
                      highlightBonds=[])

    #コンテナをファイナライズ
    view.FinishDrawing()

    #コンテナに書き込んだデータを取り出す
    svg = view.GetDrawingText()

    #データを描画
    return(SVG(svg.replace('svg:,','')))

#ビューワーを実行
interact(SSSR_viwer, idx=ipywidgets.IntSlider(min=0,max=len(test_SymmSSSR)-1, step=1));

f:id:magattaca:20190127104409g:plain

順番に表示させることでSSSRがどのような環構造を認識するのか、非常にわかりやすくなりました。

SSSRを利用して大員環を区別

大体SSSRでどのような情報が得られるのかわかったので、これを用いてマクロサイクル型の化合物とそれ以外を区別できるか検証して見たいと思います。

Wikipediaによると大員環化合物(macrocyclic compound)は「概ね10個以上の原子からなる環状構造を持つ有機化合物」ということなので、これを基準として用います。SSSRで得た環構造のうち、最大の大きさの環構造を構成する原子数が10以上の場合、大員環化合物とすることとしたいと思います。

まずは、各化合物のSSSRをもとめ、最大の環構造の情報を取得します。

関数を作成し、Molオブジェクトを含むカラムに適用し、得た値を新しいカラムへと格納します。

def largest_ring_member_count(mol):
    SymmSSSR = Chem.GetSymmSSSR(mol)

    size_list = []
    
    # 各環の大きさを順番にリストに追加
    for i in range(len(SymmSSSR)):
        size_list.append(len(SymmSSSR[i]))
    
    # リストの最大値をとりだす
    max_size = max(size_list)
                   
    return(max_size)

# 最大の環の情報をもつ新しいカラムを作成
df['largest_ring_size'] = df['ROMol'].map(largest_ring_member_count)

環のサイズが10以上の時はマクロサイクル型(macro)、それより小さい場合は鎖型(chain)として分類してみます。

#classifierとか一回言ってみたかった
def macro_classifier(size):
    if size >= 10:
        return('macro')
    elif size < 10:
        return('chain')

df['macro_chain'] = df['largest_ring_size'].map(macro_classifier)

どのような分類になったか数を確認してみます。

df['macro_chain'].value_counts()

"""
chain    39
macro    14
Name: macro_chain, dtype: int64
"""

クロサイクル型 14個、鎖状型39個、となりました。

前回の分類では、外れ値としたマクロサイクル型化合物1つの除いた、合計52個を、マクロサイクル型13個と鎖状型39個とに分けました。数を見る限り同様の結果が得られていそうです。

それぞれ別のDataFrameに分けたあと、構造を確認します。

クロサイクル型・・・

df_macro = df[df['macro_chain']== 'macro']
macro_list = list(df_macro['ROMol'])

def macro_viewer(idx):
    mol = macro_list[idx]
    return(display(Draw.MolToImage(mol)))
    
interact(class_1_viewer, idx=ipywidgets.IntSlider(min=0,max=len(macro_list)-1, step=1));

f:id:magattaca:20190127105231g:plain

鎖状型・・・

df_chain = df[df['macro_chain']== 'chain']
chain_list = list(df_chain['ROMol'])

def chain_viewer(idx):
    mol = chain_list[idx]
    return(display(Draw.MolToImage(mol)))
    
interact(chain_viewer, idx=ipywidgets.IntSlider(min=0,max=len(chain_list)-1, step=1));

f:id:magattaca:20190127105411g:plain

しっかりと望み通りの分類ができていそうです。

まとめ

今回はSSSRを使って、大員環化合物とそれ以外の化合物を分類できるか?ということを行ってみました。
RDKitのドキュメンテーションを読んでいる時は、どのような目的で使えば良いものかさっぱりわからず、どうして The SSSR Problem という節を設けるほど解説してあるのか疑問に思っていたのですが、実際に使って見るととても使い勝手の良い重要な考え方ということがわかりました。

かなり適当に使ってしまったので、概念の理解の誤り等ございましたらご指摘いただければ幸いです。

ライブラリの指標計算結果まとめ

前回の記事で、活性化合物群の情報をもとにライブラリから候補化合物を絞り込むため、指標の閾値(フィルター)を決めようと試みました。とりあえずの結果として、以下の値を求めました。

指標 small_molecules
min
small_molecules
max
フィルタリング目安
分子量 MW 215 835 > 300
cLogP MolLogP -2.5 9.5 -3 ~ 10
水素結合供与体数 NumHDonoes 1 7 < 10
水素結合受容体数 NumHAcceptors 3 9 < 10
回転可能結合数 NumRotatableBonds 2 15 < 15
極性表面積 TPSA 48 250 < 250

こちらのフィルタリング目安を、前処理済みのライブラリEnamine_Premium_collection(分子量300より小さいものは除去済み)に適用すべく、各化合物の指標を計算、その統計値を求めました。

結果は以下の通り・・・

f:id:magattaca:20190126163131p:plain

なんと、欲張って幅を広くとりすぎたためか、分子量以外の指標では化合物の数をうまく減らせそうにありません。

絞り込み対象のライブラリにおける指標の値の分布を検証せずに、手抜きして活性化合物群のみから閾値を決めようとしたことに原因があるかもしれません。

まずは、相手を知る必要がありそうです。

そこで各指標値の値を残りのライブラリ3つ(Enamine_Advanced_collection、Enamine_HTS_collection、UOS_HTS)に対しても求め分布を把握してみることにしました。 これらのライブラリに対しては分子量 300以下を除く処理はしていません。

まずは結果から・・・

① Enamine_Advanced_collection

f:id:magattaca:20190126163312p:plain

② Enamine_HTS_collection

f:id:magattaca:20190126163415p:plain

③ UOS_HTS

f:id:magattaca:20190126163429p:plain

分子量300以下を除く処理をしていないためか、分子量の最小値の小ささにすこしびっくりしました。

このままではよくわからないので、値のばらつきを見るため箱ひげ図を用いてばらつきを可視化しました。 以下は、Enamine_Advanced_collectionの場合です。

f:id:magattaca:20190126163614p:plain

こちらを見ると先日設定したフィルタリング目安は、最大値よりも大きく、外れ値とも言えそうな化合物も全て残してしまう、ということがわかります。

先にこっちを見ておくべきだった!!

以下に、上記を求めた際につまづいた点などを記載しておきます。

MolLogPの計算

Enamine_Premium_collectionを計算する際には問題とならなかったのですが、Enamine_Advanced_collectionを処理する際に、MolLogPの計算でエラーが生じました。指標の要約統計量を確認するとEnamine_Advanced_collectionのMolLogPのみ、count(値の総数:486321)が分子の総数(486322)よりも1小さくなっています。

エラーの内容は以下の通り。

---> 42   MolLogP_value = Chem.Descriptors.MolLogP(mol_neu)
     43   mol_neu.SetDoubleProp('MolLogP', MolLogP_value)
     44   MolLogP_list.append(MolLogP_value)

/root/miniconda/lib/python3.6/site-packages/rdkit/Chem/Crippen.py in <lambda>(*x, **y)
    168 _pyMolMR.version = "1.1.0"
    169 
--> 170 MolLogP = lambda *x, **y: rdMolDescriptors.CalcCrippenDescriptors(*x, **y)[0]
    171 MolLogP.version = rdMolDescriptors._CalcCrippenDescriptors_version
    172 MolLogP.__doc__ = """ Wildman-Crippen LogP value

ValueError: Sanitization error: Explicit valence for atom # 6 B, 5, is greater than permitted

Value Error と書かれたエラー内容から推測すると、この分子はホウ素(B : 原子番号 6)を含み、その原子に対する結合の数が、通常よりも多いため分子としてうまく認識できなくなっているようです。構造は以下の通り。

f:id:magattaca:20190126163852p:plain

上記のようにホウ素に3つのフッ素原子とピロールが結合した分子のカリウム塩となっています。4本の結合+負電荷で 「valence # 6 B, 5」となりエラーとなったようです。どうやらこのライブラリにはクロスカップリング反応の試薬(有機トリフルオロボレート塩 (Chem-Station さんより))となるような化合物も含まれているようです。

前処理のコード全体

上記エラーのためforループが途中でとまってしまい、前処理ができなくなってしまいました。そこで、MolLogPの計算については例外処理(try…except…)を追加しました。前処理のコード全体としては下記となります。

from rdkit import Chem
import gzip
from rdkit.Chem import Descriptors, MolStandardize

EAc_gz = gzip.open('Enamine_Advanced_collection.sdf.gz')
EAc_suppl = Chem.ForwardSDMolSupplier(EAc_gz) 

#Molオブジェクト、および各指標を計算し格納するためのからのリストを作成
EAc_mols = []
MW_list = []
MolLogP_list = []
NumHDonors_list = []
NumHAcceptors_list = []
NumRotatableBonds_list = []
TPSA_list = []

for x in EAc_suppl:
  if x is not None:
    mol = x
    
  #処理の前にidnumberを取り出しておく
  ID = mol.GetProp('idnumber') 

    
  #構造の標準化
  normalizer =MolStandardize.normalize.Normalizer()
  mol_norm = normalizer.normalize(mol)
  
  #一番大きいサイズのフラグメントのみ残す
  lfc = MolStandardize.fragment.LargestFragmentChooser()
  mol_desalt = lfc.choose(mol_norm)
  
  #電荷の中和
  uc = MolStandardize.charge.Uncharger()
  mol_neu = uc.uncharge(mol_desalt)
  
  #処理後のMolオブジェクトに、取り出しておいた元々のidnumberをoriginal_idとして付与
  mol_neu.SetProp('original_id', ID)
  
  #分子量を計算
  MW_value = Chem.Descriptors.MolWt(mol_neu)

  #分子量をプロパティとして持たせる。(小数点を含むfloat型)
  mol_neu.SetDoubleProp('MW', MW_value)
  
  #値のみのリストにも追加する
  MW_list.append(MW_value)
  
  #MolLogPを計算(MolLogPは対応しない元素を含むとエラーを返すので例外処理として記入)
  try: 
    MolLogP_value = Chem.Descriptors.MolLogP(mol_neu)
    mol_neu.SetDoubleProp('MolLogP', MolLogP_value)
    MolLogP_list.append(MolLogP_value)
  except:
    MolLogP_list.append(None)
   
  #NumHDonorsでも同じことをする。
  NumHDonors_value = Chem.Descriptors.NumHDonors(mol_neu)

  #非負の整数値(int型)のプロパティなので SetUnsignedIntProp でも良いが面倒なのでSetDoublePropにする。
  #たぶんメモリの無駄遣い。
  mol_neu.SetDoubleProp('NumHDonors', NumHDonors_value)
  NumHDonors_list.append(NumHDonors_value)  

  #NumHAcceptors
  NumHAcceptors_value = Chem.Descriptors.NumHAcceptors(mol_neu)
  mol_neu.SetDoubleProp('NumHAcceptors', NumHAcceptors_value)
  NumHAcceptors_list.append(NumHAcceptors_value)  

  #NumRotatableBonds
  NumRotatableBonds_value = Chem.Descriptors.NumRotatableBonds(mol_neu)
  mol_neu.SetDoubleProp('NumRotatableBonds', NumRotatableBonds_value)
  NumRotatableBonds_list.append(NumRotatableBonds_value)    

  #TPSA
  TPSA_value = Chem.Descriptors.TPSA(mol_neu)
  mol_neu.SetDoubleProp('TPSA', TPSA_value)
  TPSA_list.append(TPSA_value)   
 
  #Molオブジェクトのリストも忘れずに
  EAc_mols.append(mol_neu)

以後の処理をできるだけ簡単にするため、指標の数値のみを含むcsvファイルを作成しました。

import pandas as pd
property_names = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA'] 
property_values = [MW_list, MolLogP_list, NumHDonors_list, NumHAcceptors_list, NumRotatableBonds_list, TPSA_list]

df = pd.DataFrame(index=[], columns=property_names)

for i, j in zip(property_names, property_values):
  df[i] = j
  
df.to_csv('Enamine_Advanced_collection_desc.csv', sep=',')

また別途、構造情報と各指標の値を紐づけた上でsdfを作成しました。

#構造と各種プロパティを残したsdfファイルを作成
writer = Chem.SDWriter('Enamine_Advanced_collection_desc.sdf')

writer.SetProps(property_names)   #01/29追記 このままだとidが抜けるので適当に追加してください。またやってしまった・・・
for mol in EAc_mols:
  writer.write(mol)
writer.close()

以上が、各ライブラリファイルに対して実行した前処理の全体の流れとなります。

処理に失敗した分子の確認

先の処理でMolLogPの計算ができなかったものについてはNoneを格納するように例外処理を指定しました。このリストをpandasで読み込むとNoneは自動的にNaNとしてDataFrameに取り込まれるそうです。

どのような化合物が処理に失敗したかを確認するため、

  • 上記で出力したcsvファイルをPandasのDataFrameとして読み込む
  • MolLogPの列で値がNaNとなっている行のindexを取得する
  • 取得したindexに相当する分子の構造を確認する という手順を実施しました。

(上述のValue Errorとなった構造の描画のために行った処理です)

# csvをDataFrameとする
df_EAc = pd.read_csv('./Enamine_Advanced_collection_desc.csv')

# MolLogPの列がNaNとなっている(query)行番号(index)を取り出し、リスト化する(list)
df_EAc_MolLogP_None = list(df_EAc.query('MolLogP == "NaN"').index)

print(df_EAc_MolLogP_None)
# [8135] と出力された

index 8135の分子一つのみが、処理に失敗したもののようです。コードは省略しますが、この分子をsdfから取り出し描画することで、先の構造(ピロールのトリフルオロボレート塩)を確認しました。

SDFの持つ情報をとりあえず確認する方法

上記、前処理ですが Enamine~~ と名前のついた3つのsdfではうまくいったものの、UOS_HTSではうまくいきませんでした。原因はidnumber というプロパティが存在しなかったことです。どうやらこちらのライブラリのみ、化合物のIDが別の名称の属性として格納されているようです。

TextエディタやMarvinViewなどで開いてSDFの持つ構造情報以外の属性を確認する、という手段もありますが、なにぶんファイルサイズが大きく開くだけでも大変です。ここは RDKitをうまく使いたい・・・ということで、とりあえずsdfから最初の1分子のみを読み込んでみることとしました。

以前こちらの記事で検証したように、ForwardSDMolSupplierで読み込んだSDFからはnext()を使うことで順番に一つずつ取り出すことができます。

UH_suppl = Chem.ForwardSDMolSupplier('./UOS_HTS.sdf') 

#組み込み関数next()を使って最初の一つの分子を取り出す
test_mol = next(UH_suppl)

#どんなPropertyが含まれているか取り出し、リスト化、出力
prop_names = list(test_mol.GetPropNames())
print(prop_names)
#['ID']と出力された

UOS_HTSについては、化合物のIDはIDという属性に格納されているようです。 これでidnumberの代わりにIDとして、他のライブラリに行ったのと同じ処理を行えば良いことがわかりました。

なんどもクドイようですが、上記処理に続けて前処理のコードを実行すると失敗します。

理由は属性の確認のため、ForwardSDMolSupplierのオブジェクトから最初の一つをとりだしてしまったため、再度sdfを読み込むところから始めないと、最初の一つを捨ててしまったことになるからです。(私は以前記事を書いておきながらもこの失敗をまた繰り返しました・・・学習能力低すぎ・・・)

統計量の計算

pandasのdescribeを用いると項目数が増えるので、総数(count)、最小値(min)、平均値(mean)、最大値(max)を別個に計算しました。

df_count = df_EAc[descriptors].count() 
df_min = df_EAc[descriptors].min()
df_mean = df_EAc[descriptors].mean()
df_max = df_EAc[descriptors].max()

df_desc_view_EAc =  pd.DataFrame([df_count, df_min, df_mean, df_max], index=['count', 'min', 'mean', 'max'])

df_desc_view_EAc.round(2)

これで冒頭の統計量を出しました。

箱ひげ図を用いたばらつきの可視化

要約統計量のみでは値の広がりがわからないので箱ひげ図を用いてばらつきを可視化しました。

import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline

一つのプロットに全ての指標を乗せてしまうと、値の範囲の違い(分子量と水素結合受容体の数では値の幅が全然違う)のため、潰れてしまったのでsubplotを指定しました。

descriptors = ['MW', 'MolLogP', 'NumHDonors', 'NumHAcceptors', 'NumRotatableBonds', 'TPSA']

df_EAc[descriptors].plot(kind = 'box', subplots=True, figsize=(20, 6))

これで冒頭の図を得ました。

以上です。