magattacaのブログ

日付以外誤報

Jupyter Notebookでも構造式を編集したい! 〜JSMEとipywidgetsでMolを扱う話〜

RDKitとJupyter notebookの組み合わせとても便利ですよね!「プログラミングなんてよくわからないぜ!」っていうへっぽこにとって結果が逐一見られるのは最高です!

ですが、遊んでいて一つ困ることが・・・「RDKitの構造式を書き変えるの面倒!!ちょっと変えたいだけなのに。。。」

というわけで今回はJupyter Notebook上で構造式を編集する方法を試してみたいと思います。具体的にはRDKitMolオブジェクトからMolファイルで座標を取り出して、構造式を編集後、新しい座標から再度Molオブジェクトを作り直します。

f:id:magattaca:20220101142250p:plain

1. JSMEとipywidgetを使おう

1-1. 先例 〜SMILESを取り出す方法〜

調べてみると同様のニーズはあるみたいで、優秀な方々が検討結果を共有してくださっていました。今回は、lithium0003さんのGitHubレポジトリ(JSME_ipywidget)を参考にさせていただくことにしました。*1

こちらの方法によると構造式エディタとしてJSMEを用い、Jupyter notebook上で実効するためipywidgetsを使うとのことでした。

1-2. JSMEって?

JSMEはフリーで利用可能な構造式エディタで、2013年に下記の論文で報告されています。*2

jcheminf.biomedcentral.com

元々Javaで書かれていたJMEというエディタを、JavaScriptにアップデートしたもののようでBSDライセンスで配布されています。

Webページはこちら(→ JSME Molecule Editor)です。「Download the latest JSME distribution as zip file」をクリックすると最新版をダウロードできます。今回は少し古いバージョンを使います。

f:id:magattaca:20220101135442p:plain

今回使うバージョンは2017-02-26です。最新版ではうまく動かなかったので参考例に近いバージョンにしました。

f:id:magattaca:20220101135505p:plain

ダウンロードすると「JSME_2017-02-16ディレクトリ」が手に入りますが、このうち「jsmeディレクトリ」が今回必要となる部分です。これだけ作業ディレクトリに移しておきます。

f:id:magattaca:20220101135528p:plain

なお、JSMEの見た目はこんな感じです。以前こちらのブログでも紹介したZINC20データベースでの利用例をもってきました。

f:id:magattaca:20220101135548p:plain

ChemDrawMarvinといった商用ソフトほどの機能はなさそうですが、十分な機能があり、何よりフリーで公開してくださっていることに感謝です。

1-3. 先例でコードのお勉強

lithium003さんの公開してくださっている方法はGitHubレポジトリのノートブック(smiles_draw.ipynb)の通りです。このノートブックでは「JSMEエディタをjupyter notebook上で使用し、描いた構造式のSMILESを取り出す」ということが行われています。

こんな感じ(READMEより)

f:id:magattaca:20220101135654p:plain

でもさっぱりコードの中身がわからない!!Pythonも分からないのにJavaScriptも必要なの??

ということでGoogleで検索しながら順番に見ていきました。

全部で4つのセルからなります。

1-3-1. セル1

まず最初にipywidgetsの準備です。ipywidgetsはJupyter notebook上でインタラクティブなUIを手軽に作れるライブラリだそうです*3。ドキュメントはこちら(→ ipywidgetsドキュメンテーション

とりあえずセル1のコードを引用します。ライブラリをインポートした後、SmilesEditorというクラスを作成しています。

from traitlets import Unicode, Bool, validate, TraitError
from ipywidgets import DOMWidget, register

@register
class SmilesEditor(DOMWidget):
    _view_name = Unicode('SmilesView').tag(sync=True)
    _view_module = Unicode('smiles_widget').tag(sync=True)
    _view_module_version = Unicode('0.1.0').tag(sync=True)

    # Attributes
    value = Unicode('', help="SMILES value").tag(sync=True)

ここで行われているのはipywidgetsドキュメンテーションの「Low Level Widget TutorialWidget skeleton項」 と「Building a Custom Widget - Email widgetMaking the widget stateful項」を参考にすると様子がわかりそうです。

1行目のtraitletsはJupyter notebookで機能しているライブラリだそうで、Pythonの(動的に決まる)「クラスの属性の型をきちんと定めて、さらに細かいチェック機能を簡単に呼び出せる」ようにできるそうです*4

2行目でipywidgetsライブラリからインポートしています。DOMWidgetDOMDocument Object Modelの略だと思います。DOMはJavaScriptの「表示されたWebサイトを動的に書き換えることができる」という特徴を支えている仕組みで、「HTMLをJavaScriptで操作することが出来る」そうです*5

またregisterはビューが動的にアップデートするのに関わる関数のようです。*6

このセルの目的はJSMEを使えるようにするための箱(SmilesEditor)を準備することのようです。JavaScriptベースのJSMEを使うためのDOMWidgetのクラスSmilesEditorをつくっているんだと思います。たぶん。。。

1-3-2. セル2

セル2ではJSMEを使うための設定が書き込まれています。マジックコマンド%%javascriptでJupyter notebook上でJavaScriptが実行できるようにした上で処理が書かれています。

セル2を引用します。

%%javascript
require.undef('smiles_widget');
require(['jsme/jsme.nocache.js'])
define('smiles_widget', ["@jupyter-widgets/base"], function(widgets) {
    var SmilesView = widgets.DOMWidgetView.extend({
        // Render the view.
        render: function() {
            this.smiles_input = document.createElement('div');
            this.smiles_input.id = "jsme_container";
            this.smiles_display = document.createElement('div');
            this.smiles_display.textContent="SMILES : ";
            this.el.appendChild(this.smiles_display);
            this.el.appendChild(this.smiles_input);

            function myFunc(callback){
                let jsmeApplet = new JSApplet.JSME("jsme_container", "480px", "480px");
                jsmeApplet.setCallBack("AfterStructureModified", callback);
            };
            setTimeout(myFunc, 500, this.smilesChanged.bind(this));
        },

        smilesChanged: function(jsmeEvent) {
            console.log(this, jsmeEvent)
            let jsme = jsmeEvent.src;
            let smiles = jsme.smiles();
            this.smiles_display.textContent="SMILES : "+smiles;
            this.model.set('value', smiles);
            this.model.save_changes();
        },
    });
    return {
        SmilesView: SmilesView
    };
});

冒頭2行のrequireは「一般的にモジュール化されたJavaScriptファイルを読み込む」ために用いられるものだそうです*7

1行目のrequire.undef()はモジュールの定義を外すためのもので、内部の状態をリセットするための関数のようです*8。一度リセットしてから、2行目でJSME本体を呼びにいっている感じでしょうか??

2行目でJSMEを読み込むパスの通り、JSMEはノートブックと同じディレクトリに置かれた「jsmeディレクトリ」内の「jsme.nocache.jsファイル」を利用しています。requireはモジュール化されたJavaScriptファイルをよぶという説明通りですね。

つづいて3行目以降では具体的なSmilesViewの中身が書かれています。

thisというのは「JavaScriptに最初から用意されている特別な変数のこと」で、「呼び出した場所や方法によってその中身が変化する」という特徴があるそうです*9。よくわかりませんが、変数thisに色々格納して、JSME構造式描画ビューワーsmiles_input)やSMILES表示smiles_display)の機能を設定している感じです。変数を設定した後appendChildメソッドを使って要素を実際に追加していく感じ*10

準備ができたら、真ん中のfunctionから始まるブロックでJSEMのアプレットを読み込んでいます。AfterStructureModifiedというコールバックを使っているので、構造が編集されると変更後の構造を読み込んで反映する仕組みがつくられている感じです。・・・たぶん。

一番最後のブロックでは、構造式エディタに書かれた構造式からSMILESをとり出す仕組みが書かれているようです。letでは始まる2行はJSMEからSMILESの情報を取り出して、変数に格納している感じです*11jsmeEvent.src.smiles()はJSME特有の箇所でJSME API ドキュメントなどに記載があります。

とりだしたSMILES情報をもとに、「this.smiles_display.textContent="SMILES : "+smiles;」ではインタラクティブな編集に応答してSMILESを表示する仕組みが、「this.model.set('value', smiles);」ではmodelvalueというプロパティにSMILESを格納しています。

以上、セル2でJavaScriptのアプリ、JSMEを利用するための設定と書かれた構造からSMILESを取り出す仕組みの設定までが終わったようです。

1-3-3. セル3

セル3は、セル1とセル2で設定が完了したSmilesEditorを起動しています。

コードはシンプルです。

smiles = SmilesEditor()
smiles

f:id:magattaca:20220101135853p:plain

参考例のREADMEと同じものが立ち上がりました!

構造式を書き込むとEditor上部の「SMILES:」欄に対応するSMILES表記が反映されます。

1-3-4. セル4

セル4は構造式に対応するSMILESをJupyter notebookのセルに取り出すだけです。

コードはこちら

smiles.value
#  'OCc1ccccc1'

セル2でみたように、valueプロパティにSMILESを格納していたので.vlaueとすることでSMILESが取り出せました!

以上がlithium003さんが公開してくださっている方法です。

2. この記事で試す方針

コードを辿るだけで長くなってしまいましたが、今回私が行いたいのは「編集した構造式の新しい座標をベースにMolオブジェクトを作り直す」ことです。このためには「JSMEからSMILESではなくMolファイル形式の情報として編集結果を取得」すれば良さそうです。

具体的には以下を行います。

  1. RDKitのMolオブジェクトをMolFileで出力する
  2. JSMEをnotebook上で起動する
  3. MolFileを読み込んで構造式を編集
  4. 編集後の座標(Molファイル形式)を取得
  5. RDKit Molオブジェクトを作り直す(MolFromMolBlock

一時的にMolFileで出力するのが格好悪いですが、書き直すよりは楽かな???ということで。

3. RDKitで構造式を編集する正攻法

お試しの前に、RDKitで構造式を編集する真っ当な方法をご紹介しておきます。以下の日本語解説記事がとても参考になります。

sishida21.github.io

RWMolオブジェクトに変更して、②編集した後、③再度Molオブジェクトに戻すそうです。

例えばトルエンベンジルアルコールにしようとするとこういう感じです。

from rdkit import Chem
from rdkit.Chem import Draw

# トルエンのMolオブジェクト
toluene = Chem.MolFromSmiles("Cc1ccccc1")

# 編集前後でindexを保つためにAtom Mapping
for i, atom in enumerate(toluene.GetAtoms(), start=1):
        atom.SetAtomMapNum(i)

Draw.MolToImage(toluene)

f:id:magattaca:20220101140045p:plain

# RWMolオブジェクトに変換
rw_toluene = Chem.RWMol(toluene)

# 酸素原子(Chem.Atom(8))を炭素原子(C:1)に追加して結合をつくる(AddBond)
from_idx = rw_toluene.AddAtom(Chem.Atom(8))
to_idx = [atom.GetIdx() for atom in rw_toluene.GetAtoms() if atom.GetAtomMapNum() == 1][0]
rw_toluene.GetAtomWithIdx(from_idx).SetAtomMapNum(8)
rw_toluene.AddBond(from_idx, to_idx, Chem.BondType.SINGLE)

# 編集後にMolオブジェクトに戻す
benzylalcohol = rw_toluene.GetMol()
Chem.SanitizeMol(benzylalcohol)

Draw.MolToImage(benzylalcohol)

f:id:magattaca:20220101140101p:plain

できました!けど面倒。。。分子ごとにatom indexを確認しないといけないのも一手間です。

4. JSMEとくみあわせてRDKit Molを編集しよう

では、JSMEと組み合わせて楽になるか???試してみましょう。

4-1. RDKitのMolオブジェクトから座標抽出

まずは、RDKitのMolオブジェクトから構造式の座標Molファイル形式temp.mol)で出力しておきます。

from rdkit import Chem

# トルエンを作ってMolFileで出力する
toluene = Chem.MolFromSmiles("Cc1ccccc1")
Chem.MolToMolFile(toluene, 'temp.mol')

4-2. JSMEの設定(MolFile抽出)

JSMEを利用する箇所はほとんどlithium003さんのコードを利用させていただきます。ただし、ここで欲しいのは編集後のMol形式の座標なので、SMILESではなくMolFileを取り出す様に変更します。

目的にあわせて名前もSmilesEditorからMolEditorにしてみました。

準備のセル1はクラス名称等をかえていますが基本そのままです。

from traitlets import Unicode, Bool, validate, TraitError
from ipywidgets import DOMWidget, register

@register
class MolEditor(DOMWidget):
    _view_name = Unicode('MolView').tag(sync=True)
    _view_module = Unicode('mol_widget').tag(sync=True)
    _view_module_version = Unicode('0.1.0').tag(sync=True)

    # Attributes
    value = Unicode('', help="Mol value").tag(sync=True)

JavaScriptのセル2では、よりシンプルにするためSMILES確認用ビューワーを削除しています。最後のブロックでMolFileを取り出すように変更しています。

%%javascript
require.undef('mol_widget');
require(['jsme/jsme.nocache.js'])
define('mol_widget', ["@jupyter-widgets/base"], function(widgets) {
    var MolView = widgets.DOMWidgetView.extend({
        // Render the view.
        render: function() {
            this.mol_input = document.createElement('div');
            this.mol_input.id = "jsme_container";
            this.el.appendChild(this.mol_input);

            function myFunc(callback){
                let jsmeApplet = new JSApplet.JSME("jsme_container", "480px", "480px");
                jsmeApplet.setCallBack("AfterStructureModified", callback);
            };
            setTimeout(myFunc, 500, this.molChanged.bind(this));
        },

        molChanged: function(jsmeEvent) {
            console.log(this, jsmeEvent)
            let jsme = jsmeEvent.src;
            let mol_data = jsme.molFile();
            this.model.set('value', mol_data);
            this.model.save_changes();
        },
    });
    return {
        MolView: MolView
    };
});

準備完了!

4-3. JSMEの実行と構造編集

JSMEは立ち上がるでしょうか?

MolEditorクラスをインスタンス化して実行します。

mol_editor = MolEditor()
mol_editor

f:id:magattaca:20220101140253p:plain

立ち上がりました!

右上の上下三角アイコンをクリックすると、構造式を種々の形式で読み書きできるプルダウンが開きます。

ここから「Paste Mol or SDF or SMILES」を選択すると新しい画面が開きます。「ファイルを選択」という箇所を選択するとファイルの読み込みができるので、先に出力しておいたMolファイル(temp.mol)を選択して取り込みます。「Accept」で描画スペースに構造式が反映されるので、あとは好きに編集しましょう!

f:id:magattaca:20220101140356p:plain

適当に書き加えてみました。編集後の構造式の座標はvalueプロパティから無事取り出せるでしょうか???

print(mol_editor.value)

"""
    Cc1ccc(C(C)O)nc1
    JME 2017-02-26 Sat Jan 01 12:59:19 GMT+900 2022
    
     10 10  0  0  0  0  0  0  0  0999 V2000
        5.6001    1.2124    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        4.2000    1.2124    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        3.5000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        2.1000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        1.4000    1.2124    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        2.1000    2.4249    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        3.5000    2.4249    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
        6.3001    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
        6.3001    2.4249    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
        0.0000    1.2124    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
      1  2  1  0  0  0  0
      2  3  2  0  0  0  0
      3  4  1  0  0  0  0
      4  5  2  0  0  0  0
      5  6  1  0  0  0  0
      6  7  2  0  0  0  0
      7  2  1  0  0  0  0
      1  8  1  0  0  0  0
      1  9  1  0  0  0  0
      5 10  1  0  0  0  0
    M  END
"""    

できました!

最後の仕上げにこの情報からRDKit Molオブジェクトを再度作り直します。

# 編集後の構造式からMolオブジェクトを作る
modified_mol = Chem.MolFromMolBlock(mol_editor.value)

Draw.MolToImage(modified_mol)

f:id:magattaca:20220101140618p:plain

再構築できました!

JSMEを使った構造式編集は、Atom indexとか考えなくても直感的に操作できるのでやりやすいですね!

5. 試したけど上手くいかなかったこと

やりたかったことは以上ですが、他にも試して上手くいかなかったことがいくつかあります。改善方法をご存知の方がいらっしゃったらご教示いただければ幸いです。

5-1. 失敗例1:JSMEバージョン依存

JSMEは継続的に開発されており、最新版は2021-07-13です。こちらを使って上記コードを試してみましたが上手くいきませんでした。

具体的にはJupyter notebok上にJSMEエディタ画面が立ち上がりませんでした。なので、今回は古いバージョンの2017-02-26を利用しています。

5-2. 失敗例2:Molファイルのクリップボードからのペースト

記事ではRDKitのMolオブジェクトを一度Molファイルとして出力し、JSMEにファイル読み込みという操作をしています。

「余計なファイルを増やすのは嫌だ」ということで、ファイル出力せずにMol情報だけコピー&ペーストしようとしました。

具体的にはpyperclipというライブラリを試しました。コード内でクリップボードコピーしたりペーストしたりできるそうです*12

pipでもcondaでもインストールできるそうなのでmambaしました*13

mamba install -c conda-forge pyperclip

RDKitのMolオブジェクトからクリップボードへのコピーまでは以下の通りできました。

import pyperclip

m = Chem.MolFromSmiles('CC')
mb = Chem.MolToMolBlock(m)

pyperclip.copy(mb)

このあと「Cntrol + V」すると、以下の通りペーストできます。

     RDKit          2D

  2  1  0  0  0  0  0  0  0  0999 V2000
    0.0000    0.0000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.2990    0.7500    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0
M  END

問題はJSMEでペーストできなかったことです。Jupyter notebook上で起動したJSMEの「Paste Mol or SDF or SMILES」で枠内で「Cntrol + V」しましたが、全く反映されず書き込めませんでした。JSMEのテストページでは問題なく貼り付けと構造式の描画ができたので、私のコード(or Jupyter notebook上での挙動)に問題があるようです。

5-3. 失敗例3:JSME起動時に構造反映

最後に、全く方法がわからなかったのが「JSMEを起動する際に、編集したい元の構造式が書かれた状態で起動する」という方法です。

「構造式の読み込み作業が面倒だなー」と思ったので、RDKitのMolオブジェクトからMolBlockで取り出した情報を、JSME起動時に書き込みたかったのですが、ipywidgetもJavaScriptもよくわからないので無理でした。

6. おわりに

以上、今回は「Jupyter notebook上でも構造式を編集したい!」ということで、先行例を参考にJSMEとの連携を試してみました。

途中でふれた通りRDKitの構造編集はノンプログラマーの私にはハードルが高いです。「直感的に構造式を編集したい!」という点でエディターを使えるととても便利な気がします。

商用ソフトはお高そうなので個人では手を出しづらかったのですが、フリーでつかえるJSMEと組み合わせられることがわかってよかったです。開発・公開してくださっている方々に感謝です!

結局、コードの中身をあまり理解しないままコピー&ペーストしてしまいました。今回も間違いがたくさんありそうです。ご指摘、また改善方法等ご教示いただけると嬉しいです。

ではでは!!今年もよろしくお願いいたします!!