forked from enviPath/enviPy
[Feature] Implemented SMARTS filtering for Rules (#246)
Reactant Filter SMARTS as well as Product Filter SMARTS are now reflected when applying rules. Fixes #245 Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#246
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,3 +16,4 @@ node_modules/
|
|||||||
static/css/output.css
|
static/css/output.css
|
||||||
|
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
/pnpm-workspace.yaml
|
||||||
|
|||||||
@ -1241,7 +1241,12 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
return "simple-rule"
|
return "simple-rule"
|
||||||
|
|
||||||
def apply(self, smiles):
|
def apply(self, smiles):
|
||||||
return FormatConverter.apply(smiles, self.smirks)
|
return FormatConverter.apply(
|
||||||
|
smiles,
|
||||||
|
self.smirks,
|
||||||
|
reactant_filter_smarts=self.reactant_filter_smarts,
|
||||||
|
product_filter_smarts=self.product_filter_smarts,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reactants_smarts(self):
|
def reactants_smarts(self):
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from django.test import TestCase
|
|||||||
|
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import SimpleAmbitRule, User
|
from epdb.models import SimpleAmbitRule, User
|
||||||
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
|
|
||||||
class SimpleAmbitRuleTest(TestCase):
|
class SimpleAmbitRuleTest(TestCase):
|
||||||
@ -189,7 +190,9 @@ class SimpleAmbitRuleTest(TestCase):
|
|||||||
test_smiles = "CCO"
|
test_smiles = "CCO"
|
||||||
result = rule.apply(test_smiles)
|
result = rule.apply(test_smiles)
|
||||||
|
|
||||||
mock_apply.assert_called_once_with(test_smiles, rule.smirks)
|
mock_apply.assert_called_once_with(
|
||||||
|
test_smiles, rule.smirks, reactant_filter_smarts=None, product_filter_smarts=None
|
||||||
|
)
|
||||||
self.assertEqual(result, ["product1", "product2"])
|
self.assertEqual(result, ["product1", "product2"])
|
||||||
|
|
||||||
def test_reactants_smarts_property(self):
|
def test_reactants_smarts_property(self):
|
||||||
@ -338,3 +341,41 @@ class SimpleAmbitRuleTest(TestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
rule._meta.get_field("product_filter_smarts").verbose_name, "Product Filter SMARTS"
|
rule._meta.get_field("product_filter_smarts").verbose_name, "Product Filter SMARTS"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_reactant_filter_smarts(self):
|
||||||
|
rule = SimpleAmbitRule.create(
|
||||||
|
package=self.package,
|
||||||
|
smirks="[#6,#8,#16:6]-[#6:4](=[O:5])[#6:2]=[#6;!$(CO)!$(C=CN[H])!$(C(=C)N[H]):1]>>[#8]([H])-[#6:1]-[#6:2]-[#6:4](-[#6,#8,#16:6])=[O:5]",
|
||||||
|
reactant_filter_smarts="[$(OC(=O)C=CC=CC(O)=O),$(OC1C=CC=CC1O)]",
|
||||||
|
product_filter_smarts=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
SMILES = "OC(=O)C=CC=CC(O)=O"
|
||||||
|
res = FormatConverter.apply(SMILES, rule.smirks)
|
||||||
|
|
||||||
|
# Assert Rule triggers, if we use FormatConverter without reactant_filter_smarts passed along
|
||||||
|
self.assertTrue(len(res) > 0)
|
||||||
|
|
||||||
|
res = rule.apply(SMILES)
|
||||||
|
|
||||||
|
# Check if reactant_filter_smarts prevents rule from application
|
||||||
|
self.assertTrue(len(res) == 0)
|
||||||
|
|
||||||
|
def test_product_filter_smarts(self):
|
||||||
|
rule = SimpleAmbitRule.create(
|
||||||
|
package=self.package,
|
||||||
|
smirks="[#6,#8,#16:6]-[#6:4](=[O:5])[#6:2]=[#6;!$(CO)!$(C=CN[H])!$(C(=C)N[H]):1]>>[#8]([H])-[#6:1]-[#6:2]-[#6:4](-[#6,#8,#16:6])=[O:5]",
|
||||||
|
reactant_filter_smarts=None,
|
||||||
|
product_filter_smarts="[$(O=C(O)C=CC=CC(O)CC(=O)O)]",
|
||||||
|
)
|
||||||
|
|
||||||
|
SMILES = "OC(=O)C=CC=CC=CC(O)=O"
|
||||||
|
res = FormatConverter.apply(SMILES, rule.smirks)
|
||||||
|
|
||||||
|
# Assert Rule triggers, if we use FormatConverter without product_filter_smarts passed along
|
||||||
|
self.assertTrue(len(res) > 0)
|
||||||
|
|
||||||
|
res = rule.apply(SMILES)
|
||||||
|
|
||||||
|
# Check if reactant_filter_smarts prevents rule from application
|
||||||
|
self.assertTrue(len(res) == 0)
|
||||||
|
|||||||
@ -279,6 +279,24 @@ class FormatConverter(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def smarts_matches(mol: str | Chem.Mol, smarts: str) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if the SMARTS pattern matches the given SMILES / Molecule.
|
||||||
|
"""
|
||||||
|
_mol = mol
|
||||||
|
if isinstance(mol, str):
|
||||||
|
_mol = Chem.MolFromSmiles(mol)
|
||||||
|
|
||||||
|
if _mol is None:
|
||||||
|
raise ValueError(f"Invalid Molecule: {mol}")
|
||||||
|
|
||||||
|
pattern = Chem.MolFromSmarts(smarts)
|
||||||
|
if pattern is None:
|
||||||
|
raise ValueError(f"Invalid SMARTS: {smarts}")
|
||||||
|
|
||||||
|
return _mol.HasSubstructMatch(pattern)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def apply(
|
def apply(
|
||||||
smiles: str,
|
smiles: str,
|
||||||
@ -288,6 +306,8 @@ class FormatConverter(object):
|
|||||||
standardize: bool = True,
|
standardize: bool = True,
|
||||||
kekulize: bool = True,
|
kekulize: bool = True,
|
||||||
remove_stereo: bool = True,
|
remove_stereo: bool = True,
|
||||||
|
reactant_filter_smarts: str | None = None,
|
||||||
|
product_filter_smarts: str | None = None,
|
||||||
) -> List["ProductSet"]:
|
) -> List["ProductSet"]:
|
||||||
logger.debug(f"Applying {smirks} on {smiles}")
|
logger.debug(f"Applying {smirks} on {smiles}")
|
||||||
|
|
||||||
@ -306,6 +326,15 @@ class FormatConverter(object):
|
|||||||
Chem.SanitizeMol(mol)
|
Chem.SanitizeMol(mol)
|
||||||
mol = Chem.AddHs(mol)
|
mol = Chem.AddHs(mol)
|
||||||
|
|
||||||
|
# Check if reactant_filter_smarts matches and we shouldn't apply the rule
|
||||||
|
if reactant_filter_smarts and FormatConverter.smarts_matches(
|
||||||
|
mol, reactant_filter_smarts
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
f"Reactant {FormatConverter.to_smiles(mol)} matches {reactant_filter_smarts}, skipping"
|
||||||
|
)
|
||||||
|
return list(pss)
|
||||||
|
|
||||||
# apply!
|
# apply!
|
||||||
sites = rxn.RunReactants((mol,))
|
sites = rxn.RunReactants((mol,))
|
||||||
logger.debug(f"{len(sites)} products sets generated")
|
logger.debug(f"{len(sites)} products sets generated")
|
||||||
@ -321,6 +350,17 @@ class FormatConverter(object):
|
|||||||
p = FormatConverter.standardize(
|
p = FormatConverter.standardize(
|
||||||
Chem.MolToSmiles(p), remove_stereo=remove_stereo
|
Chem.MolToSmiles(p), remove_stereo=remove_stereo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if product_filter_smarts and FormatConverter.smarts_matches(
|
||||||
|
p, product_filter_smarts
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
f"Product {FormatConverter.to_smiles(mol)} matches {product_filter_smarts}, skipping"
|
||||||
|
)
|
||||||
|
# clear products we might have already collected
|
||||||
|
prods.clear()
|
||||||
|
break
|
||||||
|
|
||||||
prods.append(p)
|
prods.append(p)
|
||||||
|
|
||||||
# if kekulize:
|
# if kekulize:
|
||||||
@ -357,7 +397,7 @@ class FormatConverter(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Applying {smirks} on {smiles} failed:\n{e}")
|
logger.error(f"Applying {smirks} on {smiles} failed:\n{e}")
|
||||||
|
|
||||||
return pss
|
return list(pss)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def MACCS(smiles):
|
def MACCS(smiles):
|
||||||
|
|||||||
Reference in New Issue
Block a user