これまで馴染み深かったrdkitにおけるfingerprintの計算方法が新しくなったらしいのでメモ。


計算方法

モジュールのインポート

import numpy as np
import rdkit
from rdkit import Chem, rdBase
from rdkit.Chem import rdFingerprintGenerator, Draw, rdMolDescriptors
from rdkit.DataStructs import ConvertToNumpyArray
rdkit.__version__
'2024.03.1'

化合物の準備

mols = tuple(
    map(
        Chem.MolFromSmiles,
        (
            "Cn1cnc2n(C)c(=O)n(C)c(=O)c12",
            "CC(C)Cc1ccc(cc1)C(C)C(=O)O",
            "CC(=O)Nc1ccc(O)cc1",
        ),
    )
)
mols
(<rdkit.Chem.rdchem.Mol at 0x122b28430>,
 <rdkit.Chem.rdchem.Mol at 0x122b28580>,
 <rdkit.Chem.rdchem.Mol at 0x122b285f0>)

MorganFingerPrint

# 必要なパラメータ
RADIUS = 2
N_BITS = 1024

昔の方法

bit
_list_arr = []
for _mol in mols:
    _arr = np.zeros(N_BITS, dtype=np.uint8)
    _fp = rdMolDescriptors.GetMorganFingerprintAsBitVect(_mol, RADIUS, N_BITS)
    ConvertToNumpyArray(_fp, _arr)
    _list_arr.append(_fp)
fps_morgan_old = np.vstack(_list_arr)
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
print(f"{fps_morgan_old.shape=}")
print(fps_morgan_old)
fps_morgan_old.shape=(3, 1024)
[[1 0 0 ... 0 0 0]
 [0 1 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]
countable
_list_arr = []
for _mol in mols:
    _arr = np.zeros(N_BITS, dtype=np.uint32)
    _fp = rdMolDescriptors.GetHashedMorganFingerprint(
        _mol, radius=RADIUS, nBits=N_BITS
    )
    ConvertToNumpyArray(_fp, _arr)
    _list_arr.append(_arr)
fps_countable_morgan_old = np.vstack(_list_arr)
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
[23:23:35] DEPRECATION WARNING: please use MorganGenerator
print(f"{fps_countable_morgan_old.shape=}")
print(fps_countable_morgan_old)
fps_countable_morgan_old.shape=(3, 1024)
[[1 0 0 ... 0 0 0]
 [0 2 0 ... 0 0 0]
 [0 0 0 ... 0 0 0]]

ちなみに、warningを表示させないこともできる。

rdBase.DisableLog("rdApp.warning")
rdMolDescriptors.GetMorganFingerprintAsBitVect(
    _mol, radius=RADIUS, nBits=N_BITS
)
rdBase.EnableLog("rdApp.warning")

rdkit用のlogging.loggerもあるが、そちらでは無効化できなかった。(C++の関数のため、Pythonのloggerでは非表示にできない?)

rdkit.logger
<Logger rdkit (WARNING)>

モダンな方法

mfgen = rdFingerprintGenerator.GetMorganGenerator(radius=RADIUS, fpSize=N_BITS)
mfgen
<rdkit.Chem.rdFingerprintGenerator.FingeprintGenerator64 at 0x122b29000>
bit
# 1個ずつ計算するとき
mfgen.GetFingerprintAsNumPy(mols[0])
array([1, 0, 0, ..., 0, 0, 0], dtype=uint8)

直接np.ndarrayで得られる。

# 複数個を一度に計算するとき
fps_morgan_new = np.array(mfgen.GetFingerprints(mols, numThreads=1))
print(f"{fps_morgan_new.shape=}")
fps_morgan_new
fps_morgan_new.shape=(3, 1024)





array([[1, 0, 0, ..., 0, 0, 0],
       [0, 1, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]])

ちゃんと新旧で値は一致する。

np.allclose(fps_morgan_old, fps_morgan_new)
True
countable
# 1個ずつ計算するとき
mfgen.GetCountFingerprintAsNumPy(mols[0])
array([1, 0, 0, ..., 0, 0, 0], dtype=uint32)
# 複数個を一度に計算するとき
_list_arr = list()
for _fp in mfgen.GetCountFingerprints(
    mols,
    numThreads=1,
):
    _arr = np.zeros(N_BITS, dtype=np.uint32)
    ConvertToNumpyArray(_fp, _arr)
    _list_arr.append(_arr)
fps_countable_morgan_new = np.vstack(_list_arr)
print(f"{fps_countable_morgan_new.shape=}")
fps_countable_morgan_new.shape=(3, 1024)

ちゃんと新旧で値は一致する。

np.allclose(fps_countable_morgan_old, fps_countable_morgan_new)
True
‘bitInfo’を計算する

countableであろうとなかろうとやり方は同じ。

additional_output = rdFingerprintGenerator.AdditionalOutput()
additional_output.CollectBitInfoMap()
# もしくは`additional_output.AllocateBitInfoMap()`
mfgen.GetCountFingerprintAsNumPy(mols[0], additionalOutput=additional_output)
array([1, 0, 0, ..., 0, 0, 0], dtype=uint32)
additional_output.GetBitInfoMap()
{0: ((5, 2),),
 33: ((0, 0), (6, 0), (10, 0), (4, 2)),
 121: ((0, 1), (6, 1), (10, 1)),
 179: ((3, 2),),
 234: ((9, 2),),
 283: ((11, 2),),
 314: ((8, 1), (12, 1)),
 330: ((13, 2),),
 356: ((4, 0), (7, 0), (11, 0), (13, 0)),
 378: ((3, 0),),
 385: ((1, 1),),
 400: ((7, 2),),
 416: ((13, 1),),
 428: ((3, 1),),
 463: ((1, 2),),
 493: ((2, 2),),
 504: ((11, 1),),
 564: ((5, 1), (9, 1)),
 650: ((8, 0), (12, 0)),
 672: ((4, 1),),
 771: ((7, 1),),
 849: ((2, 0),),
 932: ((2, 1),),
 935: ((1, 0), (5, 0), (9, 0))}

複数個計算するときはたとえば以下の通り。

list_bitinfo: list[dict[int, tuple[tuple[int, int], ...]]] = list()
list_fps: list[np.ndarray] = list()
for _mol in mols:
    _fp = mfgen.GetCountFingerprintAsNumPy(
        _mol, additionalOutput=additional_output
    )
    list_fps.append(_fp)
    list_bitinfo.append(additional_output.GetBitInfoMap())

fps = np.vstack(list_fps)
# ちゃんと違うものになっている
list_bitinfo[0] == list_bitinfo[1]
False
bitの可視化
Draw.DrawMorganBit(
    mols[2],
    bitId=tuple(additional_output.GetBitInfoMap().keys())[0],
    bitInfo=additional_output.GetBitInfoMap(),
    legend=f"bit: {tuple(additional_output.GetBitInfoMap().keys())[0]}",
)

png

Draw.DrawMorganBits(
    tuple(
        (mols[2], bit_id, additional_output.GetBitInfoMap())
        for bit_id in additional_output.GetBitInfoMap()
    ),
    legends=tuple(
        f"bit: {bit_id}" for bit_id in additional_output.GetBitInfoMap()
    ),
)

png

コメント

今回はMorganFingerprintしか試してないが、他のFingerprintも大体似た感じ。

参考

昔のfingerprintの計算方法

モダンなfingerprintの計算方法