From 44b843b9913dda391a2f278b5851f854508ada7e Mon Sep 17 00:00:00 2001 From: OleinikovasV Date: Wed, 14 Jan 2026 14:03:38 +0100 Subject: [PATCH 1/5] feat: expose resonant charges + test --- src/peppr/contacts.py | 7 ++++--- tests/test_charge.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/peppr/contacts.py b/src/peppr/contacts.py index 308a987..83c3206 100644 --- a/src/peppr/contacts.py +++ b/src/peppr/contacts.py @@ -1,6 +1,7 @@ __all__ = [ "ContactMeasurement", "find_atoms_by_pattern", + "find_charged_atoms_in_resonance_structures", ] from enum import IntEnum @@ -284,12 +285,12 @@ def find_salt_bridges( """ if use_resonance: pos_mask, neg_mask, binding_site_conjugated_groups = ( - _find_charged_atoms_in_resonance_structures(self._binding_site_mol) + find_charged_atoms_in_resonance_structures(self._binding_site_mol) ) binding_site_pos_indices = np.where(pos_mask)[0] binding_site_neg_indices = np.where(neg_mask)[0] pos_mask, neg_mask, ligand_conjugated_groups = ( - _find_charged_atoms_in_resonance_structures(self._ligand_mol) + find_charged_atoms_in_resonance_structures(self._ligand_mol) ) ligand_pos_indices = np.where(pos_mask)[0] ligand_neg_indices = np.where(neg_mask)[0] @@ -583,7 +584,7 @@ def _acceptable_angle( return abs(angle - ref_angle) <= tolerance -def _find_charged_atoms_in_resonance_structures( +def find_charged_atoms_in_resonance_structures( mol: Chem.Mol, ) -> tuple[NDArray[np.bool_], NDArray[np.bool_], NDArray[np.int_]]: """ diff --git a/tests/test_charge.py b/tests/test_charge.py index b67c6f5..af628b8 100644 --- a/tests/test_charge.py +++ b/tests/test_charge.py @@ -169,3 +169,43 @@ def test_estimate_formal_charges_peptide(): else: expected_charge = 0 assert np.sum(charges[start:stop]) == expected_charge + + +def test_find_charged_atoms_in_resonance_structures(): + """ + Test finding charged atoms in resonance structures using a real ligand from a PDB file. + """ + pdbx_file = pdbx.CIFFile.read(Path(__file__).parent / "data" / "pdb" / "3eca.cif") + structure = pdbx.get_structure( + pdbx_file, + model=1, + include_bonds=True, + ) + structure = peppr.standardize(structure) + structure = structure[structure.chain_id == "A"] + ligand = structure[structure.hetero] + + # set annotations for the benefit of finding charged atoms + ligand.set_annotation("charge", peppr.estimate_formal_charges(ligand, 7.4)) + # create rdkit ligand object + ligand_mol = rdkit_interface.to_mol(ligand) + try: + peppr.sanitize(ligand_mol) + except Exception: + return np.nan + ligand_charged_atoms = np.where(ligand.charge != 0)[0] + assert np.equal(ligand_charged_atoms, [0, 7, 8]).all() + + # get charged atoms and their resonance groups + pos_mask, neg_mask, ligand_conjugated_groups = ( + peppr.find_charged_atoms_in_resonance_structures(ligand_mol) + ) + assert len(set(ligand_conjugated_groups)) < len( + ligand_conjugated_groups + ), "Some atoms are in the same conjugated group" + charged_atom_mask = pos_mask | neg_mask + ligand_charged_in_resonance_atoms = np.where(charged_atom_mask)[0] + assert set(ligand_charged_atoms) != set( + ligand_charged_in_resonance_atoms + ), "Charged atoms do not match those found in resonance structures" + assert np.equal(ligand_charged_in_resonance_atoms, [0, 3, 6, 7, 8]).all() From bf90e7f0951ab6b8dac8758909095daf6d9da90a Mon Sep 17 00:00:00 2001 From: OleinikovasV Date: Wed, 14 Jan 2026 14:10:47 +0100 Subject: [PATCH 2/5] chore: code reformatting --- tests/test_charge.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_charge.py b/tests/test_charge.py index af628b8..3d19aa9 100644 --- a/tests/test_charge.py +++ b/tests/test_charge.py @@ -200,12 +200,12 @@ def test_find_charged_atoms_in_resonance_structures(): pos_mask, neg_mask, ligand_conjugated_groups = ( peppr.find_charged_atoms_in_resonance_structures(ligand_mol) ) - assert len(set(ligand_conjugated_groups)) < len( - ligand_conjugated_groups - ), "Some atoms are in the same conjugated group" + assert len(set(ligand_conjugated_groups)) < len(ligand_conjugated_groups), ( + "Some atoms are in the same conjugated group" + ) charged_atom_mask = pos_mask | neg_mask ligand_charged_in_resonance_atoms = np.where(charged_atom_mask)[0] - assert set(ligand_charged_atoms) != set( - ligand_charged_in_resonance_atoms - ), "Charged atoms do not match those found in resonance structures" + assert set(ligand_charged_atoms) != set(ligand_charged_in_resonance_atoms), ( + "Charged atoms do not match those found in resonance structures" + ) assert np.equal(ligand_charged_in_resonance_atoms, [0, 3, 6, 7, 8]).all() From 41d7b24e25bb96eead4115c1e09ef7dfd2364670 Mon Sep 17 00:00:00 2001 From: OleinikovasV Date: Wed, 14 Jan 2026 14:12:51 +0100 Subject: [PATCH 3/5] chore: add find_charged_atoms_in_resonance_structures to api docs --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 3973d25..1f44084 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -113,6 +113,7 @@ Miscellaneous get_contact_residues find_atoms_by_pattern estimate_formal_charges + find_charged_atoms_in_resonance_structures MatchWarning EvaluationWarning NoContactError From 214665bbc6828d52518b9f3403dabb25dc8d8f86 Mon Sep 17 00:00:00 2001 From: OleinikovasV Date: Wed, 14 Jan 2026 14:16:09 +0100 Subject: [PATCH 4/5] chore: move test --- tests/test_charge.py | 40 ---------------------------------------- tests/test_contacts.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/tests/test_charge.py b/tests/test_charge.py index 3d19aa9..b67c6f5 100644 --- a/tests/test_charge.py +++ b/tests/test_charge.py @@ -169,43 +169,3 @@ def test_estimate_formal_charges_peptide(): else: expected_charge = 0 assert np.sum(charges[start:stop]) == expected_charge - - -def test_find_charged_atoms_in_resonance_structures(): - """ - Test finding charged atoms in resonance structures using a real ligand from a PDB file. - """ - pdbx_file = pdbx.CIFFile.read(Path(__file__).parent / "data" / "pdb" / "3eca.cif") - structure = pdbx.get_structure( - pdbx_file, - model=1, - include_bonds=True, - ) - structure = peppr.standardize(structure) - structure = structure[structure.chain_id == "A"] - ligand = structure[structure.hetero] - - # set annotations for the benefit of finding charged atoms - ligand.set_annotation("charge", peppr.estimate_formal_charges(ligand, 7.4)) - # create rdkit ligand object - ligand_mol = rdkit_interface.to_mol(ligand) - try: - peppr.sanitize(ligand_mol) - except Exception: - return np.nan - ligand_charged_atoms = np.where(ligand.charge != 0)[0] - assert np.equal(ligand_charged_atoms, [0, 7, 8]).all() - - # get charged atoms and their resonance groups - pos_mask, neg_mask, ligand_conjugated_groups = ( - peppr.find_charged_atoms_in_resonance_structures(ligand_mol) - ) - assert len(set(ligand_conjugated_groups)) < len(ligand_conjugated_groups), ( - "Some atoms are in the same conjugated group" - ) - charged_atom_mask = pos_mask | neg_mask - ligand_charged_in_resonance_atoms = np.where(charged_atom_mask)[0] - assert set(ligand_charged_atoms) != set(ligand_charged_in_resonance_atoms), ( - "Charged atoms do not match those found in resonance structures" - ) - assert np.equal(ligand_charged_in_resonance_atoms, [0, 3, 6, 7, 8]).all() diff --git a/tests/test_contacts.py b/tests/test_contacts.py index 6a67644..9a06db7 100644 --- a/tests/test_contacts.py +++ b/tests/test_contacts.py @@ -5,6 +5,7 @@ import biotite.structure.io.pdbx as pdbx import numpy as np import pytest +from biotite.interface import rdkit as rdkit_interface import peppr from peppr.common import ACCEPTOR_PATTERN, DONOR_PATTERN, HBOND_DISTANCE_SCALING @@ -301,3 +302,43 @@ def test_no_bonds(contact_method): contact_measurement = peppr.ContactMeasurement(receptor, ligand, 10.0) contact_method(contact_measurement) + + +def test_find_charged_atoms_in_resonance_structures(): + """ + Test finding charged atoms in resonance structures using a real ligand from a PDB file. + """ + pdbx_file = pdbx.CIFFile.read(Path(__file__).parent / "data" / "pdb" / "3eca.cif") + structure = pdbx.get_structure( + pdbx_file, + model=1, + include_bonds=True, + ) + structure = peppr.standardize(structure) + structure = structure[structure.chain_id == "A"] + ligand = structure[structure.hetero] + + # set annotations for the benefit of finding charged atoms + ligand.set_annotation("charge", peppr.estimate_formal_charges(ligand, 7.4)) + # create rdkit ligand object + ligand_mol = rdkit_interface.to_mol(ligand) + try: + peppr.sanitize(ligand_mol) + except Exception: + return np.nan + ligand_charged_atoms = np.where(ligand.charge != 0)[0] + assert np.equal(ligand_charged_atoms, [0, 7, 8]).all() + + # get charged atoms and their resonance groups + pos_mask, neg_mask, ligand_conjugated_groups = ( + peppr.find_charged_atoms_in_resonance_structures(ligand_mol) + ) + assert len(set(ligand_conjugated_groups)) < len(ligand_conjugated_groups), ( + "Some atoms are in the same conjugated group" + ) + charged_atom_mask = pos_mask | neg_mask + ligand_charged_in_resonance_atoms = np.where(charged_atom_mask)[0] + assert set(ligand_charged_atoms) != set(ligand_charged_in_resonance_atoms), ( + "Charged atoms do not match those found in resonance structures" + ) + assert np.equal(ligand_charged_in_resonance_atoms, [0, 3, 6, 7, 8]).all() From 739d26e63ea0d061e11d4b4446b037b6e3bccb8f Mon Sep 17 00:00:00 2001 From: OleinikovasV Date: Thu, 15 Jan 2026 13:50:47 +0100 Subject: [PATCH 5/5] sync internal updates --- docs/api.rst | 2 +- src/peppr/contacts.py | 12 ++++++------ tests/test_contacts.py | 20 +++++++------------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 1f44084..cc03375 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -113,7 +113,7 @@ Miscellaneous get_contact_residues find_atoms_by_pattern estimate_formal_charges - find_charged_atoms_in_resonance_structures + find_resonance_charges MatchWarning EvaluationWarning NoContactError diff --git a/src/peppr/contacts.py b/src/peppr/contacts.py index 83c3206..ce103cb 100644 --- a/src/peppr/contacts.py +++ b/src/peppr/contacts.py @@ -1,7 +1,7 @@ __all__ = [ "ContactMeasurement", "find_atoms_by_pattern", - "find_charged_atoms_in_resonance_structures", + "find_resonance_charges", ] from enum import IntEnum @@ -284,13 +284,13 @@ def find_salt_bridges( When ``use_resonance=True``, Both oxygen atoms would be checked. """ if use_resonance: - pos_mask, neg_mask, binding_site_conjugated_groups = ( - find_charged_atoms_in_resonance_structures(self._binding_site_mol) + pos_mask, neg_mask, binding_site_conjugated_groups = find_resonance_charges( + self._binding_site_mol ) binding_site_pos_indices = np.where(pos_mask)[0] binding_site_neg_indices = np.where(neg_mask)[0] - pos_mask, neg_mask, ligand_conjugated_groups = ( - find_charged_atoms_in_resonance_structures(self._ligand_mol) + pos_mask, neg_mask, ligand_conjugated_groups = find_resonance_charges( + self._ligand_mol ) ligand_pos_indices = np.where(pos_mask)[0] ligand_neg_indices = np.where(neg_mask)[0] @@ -584,7 +584,7 @@ def _acceptable_angle( return abs(angle - ref_angle) <= tolerance -def find_charged_atoms_in_resonance_structures( +def find_resonance_charges( mol: Chem.Mol, ) -> tuple[NDArray[np.bool_], NDArray[np.bool_], NDArray[np.int_]]: """ diff --git a/tests/test_contacts.py b/tests/test_contacts.py index 9a06db7..3e2459e 100644 --- a/tests/test_contacts.py +++ b/tests/test_contacts.py @@ -304,19 +304,13 @@ def test_no_bonds(contact_method): contact_method(contact_measurement) -def test_find_charged_atoms_in_resonance_structures(): +def test_find_resonance_charges(): """ Test finding charged atoms in resonance structures using a real ligand from a PDB file. """ - pdbx_file = pdbx.CIFFile.read(Path(__file__).parent / "data" / "pdb" / "3eca.cif") - structure = pdbx.get_structure( - pdbx_file, - model=1, - include_bonds=True, - ) - structure = peppr.standardize(structure) - structure = structure[structure.chain_id == "A"] - ligand = structure[structure.hetero] + ligand = info.residue("ASP") + # standardize removes hydrogens - needed to estimate formal charges + ligand = peppr.standardize(ligand) # set annotations for the benefit of finding charged atoms ligand.set_annotation("charge", peppr.estimate_formal_charges(ligand, 7.4)) @@ -330,11 +324,11 @@ def test_find_charged_atoms_in_resonance_structures(): assert np.equal(ligand_charged_atoms, [0, 7, 8]).all() # get charged atoms and their resonance groups - pos_mask, neg_mask, ligand_conjugated_groups = ( - peppr.find_charged_atoms_in_resonance_structures(ligand_mol) + pos_mask, neg_mask, ligand_conjugated_groups = peppr.find_resonance_charges( + ligand_mol ) assert len(set(ligand_conjugated_groups)) < len(ligand_conjugated_groups), ( - "Some atoms are in the same conjugated group" + "Number of groups expected less than number of atoms as some are conjugated" ) charged_atom_mask = pos_mask | neg_mask ligand_charged_in_resonance_atoms = np.where(charged_atom_mask)[0]