Jupyter Notebookでも構造式を編集したい! 〜JSMEとipywidgetsでMolを扱う話〜
RDKitとJupyter notebookの組み合わせとても便利ですよね!「プログラミングなんてよくわからないぜ!」っていうへっぽこにとって結果が逐一見られるのは最高です!
ですが、遊んでいて一つ困ることが・・・「RDKitの構造式を書き変えるの面倒!!ちょっと変えたいだけなのに。。。」
というわけで今回はJupyter Notebook上で構造式を編集する方法を試してみたいと思います。具体的にはRDKitのMolオブジェクトからMolファイルで座標を取り出して、構造式を編集後、新しい座標から再度Molオブジェクトを作り直します。
- 1. JSMEとipywidgetを使おう
- 2. この記事で試す方針
- 3. RDKitで構造式を編集する正攻法
- 4. JSMEとくみあわせてRDKit Molを編集しよう
- 5. 試したけど上手くいかなかったこと
- 6. おわりに
1. JSMEとipywidgetを使おう
1-1. 先例 〜SMILESを取り出す方法〜
調べてみると同様のニーズはあるみたいで、優秀な方々が検討結果を共有してくださっていました。今回は、lithium0003さんのGitHubレポジトリ(JSME_ipywidget)を参考にさせていただくことにしました。*1
こちらの方法によると構造式エディタとしてJSMEを用い、Jupyter notebook上で実効するためipywidgetsを使うとのことでした。
1-2. JSMEって?
JSMEはフリーで利用可能な構造式エディタで、2013年に下記の論文で報告されています。*2
元々Javaで書かれていたJMEというエディタを、JavaScriptにアップデートしたもののようでBSDライセンスで配布されています。
Webページはこちら(→ JSME Molecule Editor)です。「Download the latest JSME distribution as zip file」をクリックすると最新版をダウロードできます。今回は少し古いバージョンを使います。
今回使うバージョンは2017-02-26です。最新版ではうまく動かなかったので参考例に近いバージョンにしました。
ダウンロードすると「JSME_2017-02-16ディレクトリ」が手に入りますが、このうち「jsmeディレクトリ」が今回必要となる部分です。これだけ作業ディレクトリに移しておきます。
なお、JSMEの見た目はこんな感じです。以前こちらのブログでも紹介したZINC20データベースでの利用例をもってきました。
ChemDrawやMarvinといった商用ソフトほどの機能はなさそうですが、十分な機能があり、何よりフリーで公開してくださっていることに感謝です。
1-3. 先例でコードのお勉強
lithium003さんの公開してくださっている方法はGitHubレポジトリのノートブック(smiles_draw.ipynb)の通りです。このノートブックでは「JSMEエディタをjupyter notebook上で使用し、描いた構造式のSMILESを取り出す」ということが行われています。
こんな感じ(READMEより)
でもさっぱりコードの中身がわからない!!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 TutorialのWidget skeleton項」 と「Building a Custom Widget - Email widgetのMaking the widget stateful項」を参考にすると様子がわかりそうです。
1行目のtraitlets
はJupyter notebookで機能しているライブラリだそうで、Pythonの(動的に決まる)「クラスの属性の型をきちんと定めて、さらに細かいチェック機能を簡単に呼び出せる」ようにできるそうです*4。
2行目でipywidgets
ライブラリからインポートしています。DOMWidget
のDOMはDocument 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の情報を取り出して、変数に格納している感じです*11。jsmeEvent.src
や.smiles()
はJSME特有の箇所でJSME API ドキュメントなどに記載があります。
とりだしたSMILES情報をもとに、「this.smiles_display.textContent="SMILES : "+smiles;
」ではインタラクティブな編集に応答してSMILESを表示する仕組みが、「this.model.set('value', smiles);
」ではmodelのvalueというプロパティにSMILESを格納しています。
以上、セル2でJavaScriptのアプリ、JSMEを利用するための設定と書かれた構造からSMILESを取り出す仕組みの設定までが終わったようです。
1-3-3. セル3
セル3は、セル1とセル2で設定が完了したSmilesEditorを起動しています。
コードはシンプルです。
smiles = SmilesEditor() smiles
参考例の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ファイル形式の情報として編集結果を取得」すれば良さそうです。
具体的には以下を行います。
- RDKitのMolオブジェクトを
MolFile
で出力する - JSMEをnotebook上で起動する
- MolFileを読み込んで構造式を編集
- 編集後の座標(Molファイル形式)を取得
- RDKit Molオブジェクトを作り直す(
MolFromMolBlock
)
一時的にMolFileで出力するのが格好悪いですが、書き直すよりは楽かな???ということで。
3. RDKitで構造式を編集する正攻法
お試しの前に、RDKitで構造式を編集する真っ当な方法をご紹介しておきます。以下の日本語解説記事がとても参考になります。
①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)
# 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)
できました!けど面倒。。。分子ごとに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
立ち上がりました!
右上の上下三角アイコンをクリックすると、構造式を種々の形式で読み書きできるプルダウンが開きます。
ここから「Paste Mol or SDF or SMILES
」を選択すると新しい画面が開きます。「ファイルを選択
」という箇所を選択するとファイルの読み込みができるので、先に出力しておいたMolファイル(temp.mol
)を選択して取り込みます。「Accept
」で描画スペースに構造式が反映されるので、あとは好きに編集しましょう!
適当に書き加えてみました。編集後の構造式の座標は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)
再構築できました!
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と組み合わせられることがわかってよかったです。開発・公開してくださっている方々に感謝です!
結局、コードの中身をあまり理解しないままコピー&ペーストしてしまいました。今回も間違いがたくさんありそうです。ご指摘、また改善方法等ご教示いただけると嬉しいです。
ではでは!!今年もよろしくお願いいたします!!
*1:他にもrdEditor(GitHubレポジトリ)というのがあって過去のRDKit UGMデモ発表があった様です。作成者の方のブログはこちら→rdEditor: An open-source molecular editor based using Python, PySide2 and RDKit
*2:B. Bienfait and P. Ertl, JSME: a free molecule editor in JavaScript, J. Cheminformatics 5:24 (2013)
*3:参考: Python: ipywidgets で Jupyter に簡単な UI を作る
*4:参考:jupyterを支える技術:traitlets(の解読を試みようとした話)
*5:参考:JavaScriptでDOMを操作する方法【初心者向け】
*6:参考:ipywidgetsドキュメンテーション Dynamic updateの説明より
*7:参考:【JavaScript入門】初心者がrequireの使い方で迷った時に読むまとめ!
*9:参考:thisって何?使い方を覚えて、JavaScriptをもっと楽しく使おう!
*10:参考:JavaScriptのappendChildメソッドの使い方を現役エンジニアが解説【初心者向け】
*12:参考:【Python】pyperclipでコピー&ペースト
*13:conda-forge pyperclip, BSD 3-clauseライセンス