Files
enviPy-bayer/tests/test_compound_model.py
2026-07-15 23:56:24 +12:00

347 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from django.conf import settings as s
from django.test import TestCase, override_settings
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class CompoundTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
def setUp(self):
pass
@classmethod
def setUpClass(cls):
super(CompoundTest, cls).setUpClass()
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
cls.INVALID_MOLFILE = "this is not a valid molfile"
def test_smoke(self):
c = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
self.assertEqual(
c.default_structure.smiles,
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
self.assertEqual(c.name, "Afoxolaner")
self.assertEqual(c.description, "No Desc")
def test_missing_smiles(self):
with self.assertRaises(InvalidSMILESException):
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException):
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException):
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
def test_smiles_are_trimmed(self):
c = Compound.create(
self.package,
smiles=" C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ",
name="Afoxolaner",
description="No Desc",
)
self.assertEqual(
c.default_structure.smiles,
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
def test_name_and_description_optional(self):
c = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
self.assertEqual(c.name, "Compound 1")
self.assertEqual(c.description, "no description")
def test_empty_name_and_description_are_ignored(self):
c = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="",
description="",
)
self.assertEqual(c.name, "Compound 1")
self.assertEqual(c.description, "no description")
def test_deduplication(self):
c1 = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
c2 = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
# Check if create detects that this Compound already exist
# In this case the existing object should be returned
self.assertEqual(c1.pk, c2.pk)
self.assertEqual(len(self.package.compounds), 1)
def test_wrong_smiles(self):
with self.assertRaises(InvalidSMILESException):
_ = Compound.create(
self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
def test_create_with_standardized_smiles(self):
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
self.assertEqual(len(c.structures.all()), 1)
cs = c.structures.all()[0]
self.assertEqual(cs.normalized_structure, True)
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
def test_create_with_non_standardized_smiles(self):
c = Compound.create(
self.package,
smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1",
name="Non Standardized SMILES",
description="No Desc",
)
self.assertEqual(len(c.structures.all()), 2)
for cs in c.structures.all():
if cs.normalized_structure:
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
break
else:
# Loop finished without break, lets fail...
self.assertEqual(1, 2)
def test_add_structure_smoke(self):
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
c.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
self.assertEqual(len(c.structures.all()), 2)
def test_add_structure_with_different_normalized_smiles(self):
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
with self.assertRaises(ValueError):
c.add_structure(
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
"Different Standardized SMILES",
)
def test_delete(self):
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardization Test",
description="No Desc",
)
c.delete()
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0)
self.assertEqual(
CompoundStructure.objects.filter(compound__package=self.package).count(), 0
)
def test_set_as_default_structure(self):
c1 = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
default_structure = c1.default_structure
c2 = c1.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
c1.set_default_structure(c2)
self.assertNotEqual(default_structure, c2)
def test_create_structure_from_molfile(self):
c = Compound.create(
self.package,
smiles="PLACEHOLDER",
molfile=self.VALID_MOLFILE,
name="Molfile Compound",
description="Created from molfile",
)
cs = CompoundStructure.create(
compound=c,
smiles="PLACEHOLDER",
molfile=self.VALID_MOLFILE,
name="Molfile Structure",
description="Structure from molfile",
)
# The SMILES must have been derived from the molfile, not the placeholder
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
self.assertIsNotNone(cs.smiles)
self.assertTrue(len(cs.smiles) > 0)
def test_create_structure_from_molfile_stores_molfile(self):
c = Compound.create(
self.package,
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
name="Molfile Store Test",
description="No Desc",
)
cs = c.default_structure
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
# on the existing structure
_ = CompoundStructure.create(
compound=c,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
molfile=self.VALID_MOLFILE,
name="Structure with Molfile",
description="No Desc",
)
# Fetch fresh from DB to confirm persistence
cs_db = CompoundStructure.objects.get(pk=cs.pk)
self.assertIsNotNone(cs_db.molfile)
self.assertNotEqual(cs_db.molfile.strip(), "")
def test_create_structure_from_invalid_molfile_raises(self):
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Invalid Molfile Test",
description="No Desc",
)
with self.assertRaises(InvalidMolfileException):
CompoundStructure.create(
compound=c,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
molfile=self.INVALID_MOLFILE,
name="Bad Structure",
description="No Desc",
)
def test_molfile_takes_precedence_over_smiles(self):
"""When both molfile and smiles are supplied, molfile must win."""
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Precedence Test",
description="No Desc",
)
cs = CompoundStructure.create(
compound=c,
smiles="C", # intentionally wrong / different SMILES
molfile=self.VALID_MOLFILE,
name="Molfile Wins",
description="No Desc",
)
# SMILES should be derived from molfile, not the supplied "C"
self.assertNotEqual(cs.smiles, "C")
def test_empty_molfile_falls_back_to_smiles(self):
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Empty Molfile Test",
description="No Desc",
)
cs = CompoundStructure.create(
compound=c,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
molfile=" ", # whitespace only should be ignored
name="Fallback to SMILES",
description="No Desc",
)
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
def test_none_molfile_falls_back_to_smiles(self):
"""None as molfile should be ignored and SMILES used instead."""
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="None Molfile Test",
description="No Desc",
)
cs = CompoundStructure.create(
compound=c,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
molfile=None,
name="Fallback None Molfile",
description="No Desc",
)
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
def test_molfile_deduplication(self):
"""Creating a structure twice from the same molfile should return the existing object."""
c = Compound.create(
self.package,
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Molfile Dedup Test",
description="No Desc",
)
cs1 = CompoundStructure.create(
compound=c,
smiles="PLACEHOLDER",
molfile=self.VALID_MOLFILE,
name="Molfile Structure",
description="No Desc",
)
cs2 = CompoundStructure.create(
compound=c,
smiles="PLACEHOLDER",
molfile=self.VALID_MOLFILE,
name="Molfile Structure",
description="No Desc",
)
self.assertEqual(cs1.pk, cs2.pk)