へっぽこ博士のなんちゃって研究室

PythonやR、分子軌道計算等に関する記事を書きます

RDKit:分子が描画されないエラー

●事始め

久しぶりにRDKit(1)を使ったプログラムをJupyter Notebookで書いたら、
分子が描画されないエラー(2)が発生し、
色々と奔走してしまったので、その対処方法を備忘録として残しておきます。
結論から先に書いておくと、画像処理ライブラリPillowのバージョンアップ
原因でした。
(1) version 2019.09.1 : Ubuntu 20.04
(2) 以下ようなエラーです。

f:id:Doktor-Heppoko:20210505153225p:plain

●対処方法1[Pillowのバージョン下げ]

この問題は、Pillow 8.0.0以上で発生するので、
素直にそれ未満のバージョンに下げます。
8.0.0未満の最新バージョンは7.2.0です。

Pillow バージョンの確認

pip show pillow

結果例

Name: Pillow
Version: 8.2.0
Summary: Python Imaging Library (Fork)
Home-page: https://python-pillow.org
Author: Alex Clark (PIL Fork Author)
Author-email: aclark@python-pillow.org
License: HPND
Location: /home/ubuntu/.local/lib/python3.8/site-packages
Requires:
Required-by: pytesseract, pyocr, matplotlib, ipymol

Pillow ダウングレード

pip install pillow==7.2.0

結果例

Defaulting to user installation because normal site-packages is not writeable
Collecting pillow==7.2.0
Downloading Pillow-7.2.0-cp38-cp38-manylinux1_x86_64.whl (2.2 MB)
|████████████████████████████████| 2.2 MB 13.7 MB/s
Installing collected packages: pillow
Attempting uninstall: pillow
Found existing installation: Pillow 8.2.0
Uninstalling Pillow-8.2.0:
Successfully uninstalled Pillow-8.2.0
Successfully installed pillow-7.2.0

Pillow 8.0.0からim.tostring()メソッドが削除されたため(3)、
これを使用しているcairoCanvas.pyから呼び出せないことによるエラーです。
一応、version 2.0.0から既にim.tobytes()メソッドへの置き換えが
推奨されていたようです。
(3) https://pillow.readthedocs.io/en/stable/deprecations.html#removed-features
 
 
  ●対処方法2[cairoCanvas.pyの書き換え]

PythonLinuxの知識をお持ちの方限定。
初心者の方は環境を破壊してしまう恐れがありますので、お勧めできません。

エラーの原因となっているファイルはcairoCanvas.pyのみで、該当箇所は4つ。
これを書き換えることで対処します。

該当のディレクトリに移動(環境に合わせ、読み替えてください)

cd /usr/lib/python3/dist-pakages/rdkit/Chem/Draw

念のためバックアップを作成

sudo cp cairoCanvas.py cairoCanvas.py.old

テキストエディタで開く(管理者権限でオープンすることを忘れずに!)

sudo gedit carioCanvas.py

該当箇所(Line 147, 151, 194, 197)の修正
(以下は該当部分を含むソースコードの一部)

 class Canvas(CanvasBase):  
   
     def __init__(self,  
                  image=None,  # PIL image  
                  size=None,  
                  ctx=None,  
                  imageType=None,  # determines file type  
                  fileName=None,  # if set determines output file name  
                  ):  
         """  
         Canvas can be used in four modes:  
         1) using the supplied PIL image  
         2) using the supplied cairo context ctx  
         3) writing to a file fileName with image type imageType  
         4) creating a cairo surface and context within the constructor  
         """  
         self.image = None  
         self.imageType = imageType  
         if image is not None:  
             try:  
                 imgd = getattr(image, 'tobytes', image.tostring)("raw", "BGRA")  
             except SystemError:  
                 r, g, b, a = image.split()  
                 mrg = Image.merge("RGBA", (b, g, r, a))  
                 imgd = getattr(mrg, 'tobytes', mrg.tostring)("raw", "RGBA")  
   
             a = array.array('B', imgd)  
             stride = image.size[0] * 4  
             surface = cairo.ImageSurface.create_for_data(a, cairo.FORMAT_ARGB32, image.size[0],  
                                                          image.size[1], stride)  
             ctx = cairo.Context(surface)  
             size = image.size[0], image.size[1]  
             self.image = image  
         elif ctx is None and size is not None:  
             if hasattr(cairo, "PDFSurface") and imageType == "pdf":  
                 surface = cairo.PDFSurface(fileName, size[0], size[1])  
             elif hasattr(cairo, "SVGSurface") and imageType == "svg":  
                 surface = cairo.SVGSurface(fileName, size[0], size[1])  
             elif hasattr(cairo, "PSSurface") and imageType == "ps":  
                 surface = cairo.PSSurface(fileName, size[0], size[1])  
             elif imageType == "png":  
                 surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size[0], size[1])  
             else:  
                 raise ValueError("Unrecognized file type. Valid choices are pdf, svg, ps, and png")  
             ctx = cairo.Context(surface)  
             ctx.set_source_rgb(1, 1, 1)  
             ctx.paint()  
         else:  
             surface = ctx.get_target()  
             if size is None:  
                 try:  
                     size = surface.get_width(), surface.get_height()  
                 except AttributeError:  
                     size = None  
         self.ctx = ctx  
         self.size = size  
         self.surface = surface  
         self.fileName = fileName  
   
     def flush(self):  
         """temporary interface, must be splitted to different methods,  
         """  
         if self.fileName and self.imageType == 'png':  
             self.surface.write_to_png(self.fileName)  
         elif self.image is not None:  
             # on linux at least it seems like the PIL images are BGRA, not RGBA:  
             if hasattr(self.surface, 'get_data'):  
                 getattr(self.image, 'frombytes', self.image.fromstring)(bytes(self.surface.get_data()),  
                                                                         "raw", "BGRA", 0, 1)  
             else:  
                 getattr(self.image, 'frombytes', self.image.fromstring)(  
                   bytes(self.surface.get_data_as_rgba()), "raw", "RGBA", 0, 1)  
             self.surface.finish()  
         elif self.imageType == "png":  
             if hasattr(self.surface, 'get_data'):  
                 buffer = self.surface.get_data()  
             else:  
                 buffer = self.surface.get_data_as_rgba()  
             return buffer  

Pillow 8.0.0未満の環境で実行する予定がないのであれば、
下のような感じに修正するだけでOK 。

147 imgd = image.tobytes("raw","BGRA")  
151 imgd = mrg.tobytes("raw","RGBA")  
   
194 self.image.frombytes(bytes(self.surface.get_data()), "raw", "BGRA", 0, 1)  
197 self.image.frombytes(bytes(self.surface.get_data_as_rgba()), "raw", "RGBA", 0, 1)  

getattrメソッドでtostring、fromstringをそれぞれtobytes、frombytesに
置き換えるようコードされているため、今回のようにアトリビュートから
削除されてしまうとエラーになってしまいます。
汎用性を求めるなら、try~except~で対処すべきと思うのですが・・・
最新版のソースコード(4)では、上記コードに置き換えられていますので、
RDKitの方を2021.03.1へバージョンアップするのが確実です。
2021/5/5時点だと、Ubuntu 20.04上のaptコマンドでインストールできる
バージョンは 2019.09.1のため、condaでインストールまたは
ソースコードからの ビルドになります。

(4)https://github.com/rdkit/rdkit/blob/master/rdkit/Chem/Draw/cairoCanvas.py