From 2b0f6d96a359ba4443e1c74f1eaa4f230ac9c331 Mon Sep 17 00:00:00 2001 From: Patrick Kunzmann Date: Fri, 17 Jan 2025 12:23:27 +0100 Subject: [PATCH] Add generic aromatic bond --- src/biotite/structure/bonds.pyx | 7 ++- src/biotite/structure/io/mol/ctab.py | 14 ++---- src/biotite/structure/io/pdbx/convert.py | 2 + tests/structure/io/test_mol.py | 56 +++++++++++++++++++++++- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/biotite/structure/bonds.pyx b/src/biotite/structure/bonds.pyx index a869fcfd5..66ef22abc 100644 --- a/src/biotite/structure/bonds.pyx +++ b/src/biotite/structure/bonds.pyx @@ -60,6 +60,7 @@ class BondType(IntEnum): - `AROMATIC_SINGLE` - Aromatic bond with a single formal bond - `AROMATIC_DOUBLE` - Aromatic bond with a double formal bond - `AROMATIC_TRIPLE` - Aromatic bond with a triple formal bond + - `AROMATIC` - Aromatic bond without specification of the formal bond - `COORDINATION` - Coordination complex involving a metal atom """ ANY = 0 @@ -71,6 +72,7 @@ class BondType(IntEnum): AROMATIC_DOUBLE = 6 AROMATIC_TRIPLE = 7 COORDINATION = 8 + AROMATIC = 9 def without_aromaticity(self): @@ -97,6 +99,8 @@ class BondType(IntEnum): return BondType.DOUBLE elif self == BondType.AROMATIC_TRIPLE: return BondType.TRIPLE + elif self == BondType.AROMATIC: + return BondType.ANY else: return self @@ -517,7 +521,8 @@ class BondList(Copyable): for aromatic_type, non_aromatic_type in [ (BondType.AROMATIC_SINGLE, BondType.SINGLE), (BondType.AROMATIC_DOUBLE, BondType.DOUBLE), - (BondType.AROMATIC_TRIPLE, BondType.TRIPLE) + (BondType.AROMATIC_TRIPLE, BondType.TRIPLE), + (BondType.AROMATIC, BondType.ANY), ]: bond_types[bond_types == aromatic_type] = non_aromatic_type diff --git a/src/biotite/structure/io/mol/ctab.py b/src/biotite/structure/io/mol/ctab.py index d4577e382..9d0bdec89 100644 --- a/src/biotite/structure/io/mol/ctab.py +++ b/src/biotite/structure/io/mol/ctab.py @@ -24,19 +24,13 @@ 1: BondType.SINGLE, 2: BondType.DOUBLE, 3: BondType.TRIPLE, + 4: BondType.AROMATIC, 5: BondType.ANY, - 6: BondType.SINGLE, - 7: BondType.DOUBLE, + 6: BondType.AROMATIC_SINGLE, + 7: BondType.AROMATIC_DOUBLE, 8: BondType.ANY, } -BOND_TYPE_MAPPING_REV = { - BondType.SINGLE: 1, - BondType.DOUBLE: 2, - BondType.TRIPLE: 3, - BondType.AROMATIC_SINGLE: 1, - BondType.AROMATIC_DOUBLE: 2, - BondType.ANY: 8, -} +BOND_TYPE_MAPPING_REV = {v: k for k, v in BOND_TYPE_MAPPING.items()} CHARGE_MAPPING = {0: 0, 1: 3, 2: 2, 3: 1, 5: -1, 6: -2, 7: -3} CHARGE_MAPPING_REV = {val: key for key, val in CHARGE_MAPPING.items()} diff --git a/src/biotite/structure/io/pdbx/convert.py b/src/biotite/structure/io/pdbx/convert.py index ed76276d1..9ae72e692 100644 --- a/src/biotite/structure/io/pdbx/convert.py +++ b/src/biotite/structure/io/pdbx/convert.py @@ -81,6 +81,7 @@ BondType.AROMATIC_TRIPLE: "trip", # These are masked later, it is merely added here to avoid a KeyError BondType.ANY: "", + BondType.AROMATIC: "", BondType.COORDINATION: "", } # Map 'chem_comp_bond' bond orders and aromaticity to 'BondType'... @@ -92,6 +93,7 @@ ("SING", "Y"): BondType.AROMATIC_SINGLE, ("DOUB", "Y"): BondType.AROMATIC_DOUBLE, ("TRIP", "Y"): BondType.AROMATIC_TRIPLE, + ("AROM", "Y"): BondType.AROMATIC, } # ...and vice versa COMP_BOND_TYPE_TO_ORDER = { diff --git a/tests/structure/io/test_mol.py b/tests/structure/io/test_mol.py index ce4378e86..4d9eaf62e 100644 --- a/tests/structure/io/test_mol.py +++ b/tests/structure/io/test_mol.py @@ -10,6 +10,7 @@ import numpy as np import pytest import biotite.structure as struc +import biotite.structure.info as info import biotite.structure.io.mol as mol import biotite.structure.io.pdbx as pdbx from biotite.structure.bonds import BondType @@ -83,7 +84,7 @@ def test_header_conversion(): [False, True], ), ) -def test_structure_conversion( +def test_structure_conversion_from_file( FileClass, # noqa: N803 path, version, @@ -116,7 +117,6 @@ def test_structure_conversion( temp.seek(0) mol_file = FileClass.read(temp) - print(mol_file) test_atoms = mol.get_structure(mol_file) if omit_charge: assert np.all(test_atoms.charge == 0) @@ -126,6 +126,58 @@ def test_structure_conversion( assert test_atoms == ref_atoms +@pytest.mark.parametrize( + "FileClass, component_name, version, omit_charge, use_charge_property", + itertools.product( + [mol.MOLFile, mol.SDFile], + [ + "ALA", # Alanine + "BNZ", # Benzene (has aromatic bonds) + "3P8", # Methylammonium ion (has charge) + "MCH", # Trichloromethane (has element with multiple letters) + ], + ["V2000", "V3000"], + [False, True], + [False, True], + ), +) +def test_structure_conversion_to_file( + FileClass, # noqa: N803 + component_name, + version, + omit_charge, + use_charge_property, +): + """ + Writing a component to a file and reading it again should give the same + structure. + """ + ref_atoms = info.residue(component_name) + + mol_file = FileClass() + mol.set_structure(mol_file, ref_atoms, version=version) + temp = TemporaryFile("w+") + mol_file.write(temp) + + if version == "V2000": + if use_charge_property: + # Enforce usage of 'M CHG' entries + _delete_charge_columns(temp) + else: + # Enforce usage of charge column in atom block + _delete_charge_property(temp) + + temp.seek(0) + mol_file = FileClass.read(temp) + test_atoms = mol.get_structure(mol_file) + temp.close() + + assert np.all(test_atoms.element == ref_atoms.element) + assert np.all(test_atoms.charge == ref_atoms.charge) + assert np.allclose(test_atoms.coord, ref_atoms.coord) + assert test_atoms.bonds == ref_atoms.bonds + + @pytest.mark.parametrize( "path", [