Skip to content
Prev Previous commit
Next Next commit
margan counter reimplemented
  • Loading branch information
stsouko committed Mar 7, 2026
commit 9ca5f57c2ec094dec1860df5a95c99d23a575ddf
73 changes: 10 additions & 63 deletions chython/algorithms/fingerprints/linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from collections import defaultdict, deque
from math import log2
from numpy import uint8, zeros
from typing import Deque, Dict, List, Set, Tuple, TYPE_CHECKING
from typing import Deque, Dict, Set, Tuple, TYPE_CHECKING


if TYPE_CHECKING:
Expand Down Expand Up @@ -59,9 +59,7 @@ def linear_fingerprint(self, min_radius: int = 1, max_radius: int = 4,

:return: array(n_features)
"""
if number_bit_pairs != 4:
max_count = number_bit_pairs
bits = self.linear_bit_set(min_radius, max_radius, length, number_active_bits, max_count)
bits = self.linear_bit_set(min_radius, max_radius, length, number_active_bits, number_bit_pairs, max_count)
fingerprints = zeros(length, dtype=uint8)
fingerprints[list(bits)] = 1
return fingerprints
Expand All @@ -80,12 +78,10 @@ def linear_bit_set(self, min_radius: int = 1, max_radius: int = 4, length: int =
number of fragment in molecule greater or equal this number, we will activate only this number of
fragments). To take into account all repeating fragments put 0 as a value.
"""
if number_bit_pairs != 4:
max_count = number_bit_pairs
mask = length - 1
log = int(log2(length))

hashes = self.linear_hash_set(min_radius, max_radius, max_count)
hashes = self.linear_hash_set(min_radius, max_radius, number_bit_pairs, max_count)
active_bits = set()
for tpl in hashes:
active_bits.add(tpl & mask)
Expand Down Expand Up @@ -116,60 +112,11 @@ def linear_hash_set(self, min_radius: int = 1, max_radius: int = 4,

return {hash((*tpl, cnt)) for tpl, count in
self._fragments(min_radius, max_radius).items()
for cnt in range(min(len(count), max_count))}

def linear_hash_smiles(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4,
number_bit_pairs: int = 4, max_count: int = 4) -> Dict[int, List[str]]:
"""
Transform structure into dict of integer hashes of fragments with count information and
corresponding fragment SMILES.

:param min_radius: minimal length of fragments
:param max_radius: maximum length of fragments
:param number_bit_pairs: deprecated name, use max_count
:param max_count: describe how much repeating fragments we can count in hashable fingerprint (if
number of fragment in molecule greater or equal this number, we will activate only this number of
fragments). To take into account all repeating fragments put 0 as a value.
"""
if number_bit_pairs != 4:
max_count = number_bit_pairs
if not max_count:
max_count = 999_999_999 # unreachable count

out = defaultdict(set)
for frg, chains in self._fragments(min_radius, max_radius).items():
chain = chains[0]
smiles = [self._format_atom(chain[0], None, stereo=False)]
for x, y in zip(chain, chain[1:]):
smiles.append(self._format_bond(x, y, None, stereo=False, aromatic=False))
smiles.append(self._format_atom(y, None, stereo=False))
smiles = ''.join(smiles)

for cnt in range(min(len(chains), max_count)):
out[hash((*frg, cnt))].add(smiles) # collisions possible
return {k: list(v) for k, v in out.items()}

def linear_smiles_hash(self, min_radius: int = 1, max_radius: int = 4,
number_bit_pairs: int = 4, max_count: int = 4) -> Dict[str, List[int]]:
"""
Transform structure into dict of fragment SMILES and list of corresponding integer hashes of fragments.

:param min_radius: minimal length of fragments
:param max_radius: maximum length of fragments
:param number_bit_pairs: deprecated name, use max_count
:param max_count: describe how much repeating fragments we can count in hashable fingerprint (if
number of fragment in molecule greater or equal this number, we will activate only this number of
fragments). To take into account all repeating fragments put 0 as a value.
"""
if number_bit_pairs != 4:
max_count = number_bit_pairs
out = defaultdict(list)
for k, sl in self.linear_hash_smiles(min_radius, max_radius, max_count).items():
for s in sl:
out[s].append(k)
return dict(out)
for cnt in range(min(count, max_count))}

def _chains(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Set[Tuple[int, ...]]:
assert min_radius >= 1, 'min_radius should be positive'
assert max_radius >= min_radius, 'max_radius should be greater or equal to min_radius'
queue: Deque[Tuple[int, ...]] # typing
atoms = self._atoms
bonds = self._bonds
Expand Down Expand Up @@ -197,10 +144,10 @@ def _chains(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4)
return arr

def _fragments(self: 'MoleculeContainer', min_radius: int = 1,
max_radius: int = 4) -> Dict[Tuple[int, ...], List[Tuple[int, ...]]]:
max_radius: int = 4) -> Dict[Tuple[int, ...], int]:
atoms = self._atom_identifiers
bonds = self._bonds
out = defaultdict(list)
out = defaultdict(int)

for frag in self._chains(min_radius, max_radius):
var = [atoms[frag[0]]]
Expand All @@ -210,9 +157,9 @@ def _fragments(self: 'MoleculeContainer', min_radius: int = 1,
var = tuple(var)
rev_var = var[::-1]
if var > rev_var:
out[var].append(frag)
out[var] += 1
else:
out[rev_var].append(frag[::-1])
out[rev_var] += 1
return dict(out)

@property
Expand Down
113 changes: 27 additions & 86 deletions chython/algorithms/fingerprints/morgan.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright 2021-2023 Ramil Nugmanov <nougmanoff@protonmail.com>
# Copyright 2021-2026 Ramil Nugmanov <nougmanoff@protonmail.com>
# Copyright 2023 Timur Gimadiev <timur.gimadiev@gmail.com>
# Copyright 2021 Aleksandr Sizov <murkyrussian@gmail.com>
# This file is part of chython.
Expand All @@ -22,8 +22,6 @@
from math import log2
from numpy import uint8, zeros
from typing import Dict, List, Set, TYPE_CHECKING
from hashlib import md5



if TYPE_CHECKING:
Expand All @@ -34,97 +32,72 @@ class MorganFingerprint:
__slots__ = ()

def morgan_fingerprint(self, min_radius: int = 1, max_radius: int = 4,
length: int = 1024, number_active_bits: int = 2, max_count_num: int = 0):
length: int = 1024, number_active_bits: int = 2, max_count: int = -1):
"""
Transform structures into array of binary features.
Morgan fingerprints. Similar to RDkit implementation.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
:param length: bit string's length. Should be power of 2
:param number_active_bits: number of active bits for each hashed tuple (int)
:max_count_num: maximum number of counts to consider for each fragment (int). If 0, all counts are considered.

:param number_active_bits: number of active bits for each hashed tuple
:param max_count: maximum number of counts to consider for each fragment.
If 0, all counts are considered. If -1, counting is disabled.
:return: array(n_features)
"""
bits = self.morgan_bit_set(min_radius, max_radius, length, number_active_bits, max_count_num)
bits = self.morgan_bit_set(min_radius, max_radius, length, number_active_bits, max_count)
fingerprints = zeros(length, dtype=uint8)
fingerprints[list(bits)] = 1
return fingerprints

def morgan_bit_set(self, min_radius: int = 1, max_radius: int = 4,
length: int = 1024, number_active_bits: int = 2, max_count_num: int = 0) -> Set[int]:
length: int = 1024, number_active_bits: int = 2, max_count: int = -1) -> Set[int]:
"""
Transform structures into set of indexes of True-valued features.
Transform structures into a set of indexes of True-valued features.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
:param length: bit string's length. Should be power of 2
:param number_active_bits: number of active bits for each hashed tuple (int)
:max_count_num: maximum number of counts to consider for each fragment (int). If 0, all counts are considered.
:param number_active_bits: number of active bits for each hashed tuple
:param max_count: maximum number of counts to consider for each fragment.
If 0, all counts are considered. If -1, counting is disabled.
"""
mask = length - 1
log = int(log2(length))

active_bits = set()

if max_count_num==0:
max_count_num = 999_999_999 # effectively unlimited

for tpl, cnt in self.morgan_hash_counts(min_radius, max_radius):
hsh=tpl #save tpl for counts
# Activate bits based on the number of active bits specified
for tpl in self.morgan_hash_set(min_radius, max_radius, max_count):
active_bits.add(tpl & mask)
if number_active_bits == 2:
active_bits.add(tpl >> log & mask)
elif number_active_bits > 2:
for _ in range(1, number_active_bits):
tpl >>= log
active_bits.add(tpl & mask)

#Activate bits for counts
for i in range(min(cnt, max_count_num)-1):
# Generate a new hash for each occurrence up to max_count_num
new_hash = hash((hsh, i))
active_bits.add(new_hash & mask)
active_bits.add(new_hash >> log & mask)

return active_bits

def morgan_hash_set(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Set[int]:
def morgan_hash_set(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4,
max_count: int = -1) -> Set[int]:
"""
Transform structures into integer hashes of atoms with EC.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
:param max_count: maximum number of counts to consider for each fragment.
If 0, all counts are considered. If -1, counting is disabled.
"""
return {x for x in self._morgan_hash_dict(min_radius, max_radius) for x in x.values()}

def morgan_hash_smiles(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Dict[int, List[str]]:
"""
Transform structures into dictionary of hashes of atoms with EC and corresponding SMILES.
if max_count == -1:
# default standard MFP
return {x for x in self._morgan_hash_dict(min_radius, max_radius) for x in x.values()}

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
"""
smiles_dict = defaultdict(set)
for radius, hash_dict in enumerate(self._morgan_hash_dict(min_radius, max_radius), min_radius - 1):
for atom, morgan_hash in hash_dict.items():
smiles_dict[morgan_hash].add(format(self.augmented_substructure((atom,), deep=radius), 'A'))
return {k: list(v) for k, v in smiles_dict.items()}

def morgan_smiles_hash(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> Dict[str, List[int]]:
"""
Transform structures into dictionary of smiles and corresponding hashes of atoms with EC.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
"""
out = defaultdict(list)
for k, sl in self.morgan_hash_smiles(min_radius, max_radius).items():
for s in sl:
out[s].append(k)
return dict(out)
# count fragment hashes
counts = defaultdict(int)
for ids in self._morgan_hash_dict(min_radius, max_radius):
for x in ids.values():
counts[x] += 1
if max_count == 0:
max_count = 999_999_999 # effectively unlimited
return {hash((tpl, cnt)) for tpl, count in counts.items() for cnt in range(min(count, max_count))}

def _morgan_hash_dict(self: 'MoleculeContainer', min_radius: int = 1, max_radius: int = 4) -> List[Dict[int, int]]:
"""
Expand All @@ -147,38 +120,6 @@ def _morgan_hash_dict(self: 'MoleculeContainer', min_radius: int = 1, max_radius
out.append(identifiers)
return out[-(max_radius - min_radius + 1):] # slice [min, max] radii range

def morgan_hash_counts(self, min_radius: int = 1, max_radius: int = 4) -> List[tuple]:
"""
Count occurrences of each hash from _morgan_hash_dict.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
:return: list of (hash, count) tuples
"""
counts = defaultdict(int)
for hash_dict in self._morgan_hash_dict(min_radius, max_radius):
for h in hash_dict.values():
counts[h] += 1
return list(counts.items())

def morgan_count_fingerprint(self, min_radius: int = 1, max_radius: int = 4, length: int = 256):
"""
Transform structures into array of integer features, where each feature is the count of its corresponding hash.
Each fragment (hash) contributes only one positional bit in the fingerprint.

:param min_radius: minimal radius of EC
:param max_radius: maximum radius of EC
:param length: fingerprint length. Should be power of 2
:return: array(n_features) of counts
"""
mask = length - 1
fingerprint = zeros(length, dtype=int)
for hsh, cnt in self.morgan_hash_counts(min_radius, max_radius):
fingerprint[hsh & mask] += cnt
return fingerprint



@property
def _atom_identifiers(self) -> Dict[int, int]:
raise NotImplementedError
Expand Down