Add ca_atoms_centroid site evaluation center method with tests

This commit is contained in:
rdk
2026-03-14 15:57:41 +01:00
parent 1ecb29f876
commit 0e0cb47907
7 changed files with 123 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
package cz.siret.prank.domain
import cz.siret.prank.geom.Atoms
import cz.siret.prank.geom.Struct
import cz.siret.prank.program.params.Parametrized
import cz.siret.prank.utils.PdbUtils
import groovy.transform.CompileStatic
@@ -106,6 +107,12 @@ class Ligand implements BindingSite, Parametrized {
return atoms.centerOfMass
case SiteCentroidMethod.sas_points_centroid:
return getSasPoints().centroid
case SiteCentroidMethod.ca_atoms_centroid:
// Select contact residues and compute geometric centroid of their CA atoms
List<Residue> contactResidues = protein.residues.getDistinctForAtoms(
protein.proteinAtoms.cutoutShell(atoms, params.ligand_protein_contact_distance)
)
return Struct.calcCaCentroid(contactResidues)
default:
throw new IllegalArgumentException("Unsupported site_eval_center_method: '${method}'")
}

View File

@@ -1,6 +1,7 @@
package cz.siret.prank.domain
import cz.siret.prank.geom.Atoms
import cz.siret.prank.geom.Struct
import cz.siret.prank.program.params.Parametrized
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
@@ -68,6 +69,8 @@ class ResidueSite implements BindingSite, Parametrized {
return getSasPoints().centroid
case SiteCentroidMethod.atoms_center_of_mass:
return getAtoms().centerOfMass
case SiteCentroidMethod.ca_atoms_centroid:
return Struct.calcCaCentroid(residues)
default:
throw new IllegalArgumentException("Unsupported site_eval_center_method: '${method}'")
}

View File

@@ -15,7 +15,10 @@ enum SiteCentroidMethod {
atoms_center_of_mass(true, true),
/** Centroid of SAS points around site atoms */
sas_points_centroid(true, true)
sas_points_centroid(true, true),
/** Geometric centroid of CA atoms of contact residues */
ca_atoms_centroid(true, true)
/** Supported for ligand-defined sites */
final boolean supportedForLigandSites

View File

@@ -203,6 +203,29 @@ class Struct {
return getGroups(struc).findAll{ isHetGroup(it) }.asList()
}
/**
* Calculate geometric centroid of CA atoms from the given residues.
* Residues without CA atoms (e.g. non-standard residues) are skipped.
* @return centroid atom or null if no CA atoms found
*/
@Nullable
static Atom calcCaCentroid(List<Residue> residues) {
List<Atom> caAtoms = new ArrayList<>()
for (Residue res : residues) {
AminoAcid aa = res.aminoAcid
if (aa != null) {
Atom ca = aa.getCA()
if (ca != null) {
caAtoms.add(ca)
}
}
}
if (caAtoms.isEmpty()) {
return null
}
return new Atoms(caAtoms).centroid
}
/**
* single linkage clustering
* @param clusters

View File

@@ -829,11 +829,15 @@ class Params {
boolean vis_highlight_ligands = false
/**
* Method for computing binding site center for evaluation (DCC criterion).
* Values: explicit, atoms_center_of_mass, sas_points_centroid
* Method for computing binding site center of observed pocket for evaluation (used by DCC criterion).
* Observed pocket can be defined by ligand or can be explicit (set of residues defined in the dataset).
* Values: explicit, atoms_center_of_mass, sas_points_centroid, ca_atoms_centroid
*
* Note: atoms_center_of_mass uses mass-weighted center of ligand/residue atoms for both site types.
* explicit is only supported for explicitly defined sites (not ligand-defined).
* ca_atoms_centroid uses geometric centroid of CA atoms of contact residues
* (for ligand sites: contact residues within ligand_protein_contact_distance;
* for explicit sites: the defined residues directly).
*
* @see cz.siret.prank.domain.SiteCentroidMethod
*/

View File

@@ -1,8 +1,10 @@
package cz.siret.prank.domain
import cz.siret.prank.geom.Atoms
import cz.siret.prank.geom.Struct
import cz.siret.prank.prediction.pockets.PrankPocket
import cz.siret.prank.prediction.pockets.criteria.DCA
import cz.siret.prank.program.params.Params
import cz.siret.prank.program.routines.results.EvalContext
import cz.siret.prank.program.routines.results.Evaluation
import groovy.transform.CompileStatic
@@ -243,6 +245,51 @@ class SiteMetricsTest {
// TODO: test evaluation getStats() with site-based results
// TODO: test with sites that are far from any pocket (unidentified case)
//===========================================================================================================//
// ca_atoms_centroid tests
//===========================================================================================================//
@Test
void caCentroidMethodWorksForResidueSite() {
List<Residue> residues = protein.residues.toList().subList(0, 10)
ResidueSite site = makeSite("ca_test", residues)
String savedMethod = Params.inst.site_eval_center_method
try {
Params.inst.site_eval_center_method = "ca_atoms_centroid"
def result = site.getCenterForEval()
def expected = Struct.calcCaCentroid(residues)
assertNotNull(result)
assertEquals(expected.x, result.x, 0.001)
assertEquals(expected.y, result.y, 0.001)
assertEquals(expected.z, result.z, 0.001)
} finally {
Params.inst.site_eval_center_method = savedMethod
}
}
@Test
void caCentroidMethodWorksForLigand() {
Ligand ligand = protein.ligands.relevantLigands[0]
assertNotNull(ligand, "Test protein should have at least one relevant ligand")
String savedMethod = Params.inst.site_eval_center_method
try {
Params.inst.site_eval_center_method = "ca_atoms_centroid"
def result = ligand.getCenterForEval()
assertNotNull(result, "ca_atoms_centroid should return non-null for a ligand with nearby protein residues")
// Result should be within reasonable distance of the ligand
double dist = protein.proteinAtoms.dist(result)
assertTrue(dist < 20.0, "CA centroid should be near the protein surface")
} finally {
Params.inst.site_eval_center_method = savedMethod
}
}
//===========================================================================================================//
// Helpers
//===========================================================================================================//

View File

@@ -1,11 +1,14 @@
package cz.siret.prank.geom
import cz.siret.prank.domain.Protein
import cz.siret.prank.domain.Residue
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.biojava.nbio.structure.AminoAcid
import org.biojava.nbio.structure.Atom
import org.junit.jupiter.api.Test
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.*
/**
*
@@ -38,4 +41,33 @@ class StructTest {
// TODo add test for chain with phosphorylated residue
}
@Test
void calcCaCentroidReturnsGeometricCenterOfCaAtoms() {
Protein p = Protein.load("$dataDir/2nbr.pdb.gz")
List<Residue> residues = p.residues.toList().subList(0, 5)
Atom result = Struct.calcCaCentroid(residues)
assertNotNull(result)
// Manually compute expected centroid from CA atoms
List<Atom> caAtoms = []
for (Residue res : residues) {
AminoAcid aa = res.aminoAcid
if (aa != null) {
Atom ca = aa.getCA()
if (ca != null) caAtoms.add(ca)
}
}
Atom expected = new Atoms(caAtoms).centroid
assertEquals(expected.x, result.x, 0.001)
assertEquals(expected.y, result.y, 0.001)
assertEquals(expected.z, result.z, 0.001)
}
@Test
void calcCaCentroidReturnsNullForEmptyList() {
assertNull(Struct.calcCaCentroid([]))
}
}