mirror of
https://github.com/rdk/p2rank.git
synced 2026-06-04 12:44:24 +08:00
Tests for pocket-grid-point descriptors + extract DescriptorListValidator
Adds focused regression tests for the new framework: 11 tests in three
new files plus 4 added to PocketGridRowsTest.
PocketGridRowsTest +4
- descriptor schema uses "{name}.{col}" prefix for multi-col
- getRow appends descriptor values after the base 4 columns
- unknown descriptor name throws at construction
- scalar descriptor emits bare name() with no prefix (uses an
inline ScalarTestDescriptor registered via the now-public
registry hook — none of the shipped descriptors are scalar so
the branch was untested)
VolsiteGridPointDescriptorTest (new, 4 tests)
- covers indicator aggregation + radius cutoff
VolsiteSmoothGridPointDescriptorTest (new, 4 tests)
- covers Gaussian kernel arithmetic + 4σ cutoff
PocketGridPointDescriptorRegistryTest (new, 2 tests)
- shipped names resolve, unknown name throws helpful error
DescriptorListValidatorTest (new, 8 tests)
- null/empty/valid/unknown/duplicate/null-entry/blank/dash-prefix
Refactors Main.validateDescriptorList out to a self-contained Java
utility (DescriptorListValidator) under predict/output/. The two call
sites in Main.validatePocketGridParams now invoke the static helper;
the private helper in Main is removed (-37 lines).
PocketGridPointDescriptorRegistry.register is promoted from private to
public so tests (and future external descriptor plugins) can add
descriptors without touching the registry's static initializer. The
shipped registrations still happen at class-load.
This commit is contained in:
@@ -235,13 +235,8 @@ class Main implements Parametrized, Writable {
|
|||||||
"Known: ${knownAssigners}")
|
"Known: ${knownAssigners}")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every name in -pocket_descriptors must be registered. null/blank entries are
|
cz.siret.prank.program.routines.predict.output.DescriptorListValidator.validate(
|
||||||
// rejected (rather than skipped) so a malformed config file fails fast instead
|
params.pocket_descriptors,
|
||||||
// of slipping through to the consumers (which would otherwise hit
|
|
||||||
// PocketDescriptorRegistry.get('') with a less useful error). Duplicates are
|
|
||||||
// rejected too — accepting them would produce a CSV with duplicate header cells
|
|
||||||
// and break Parquet's schema builder.
|
|
||||||
validateDescriptorList(params.pocket_descriptors,
|
|
||||||
cz.siret.prank.program.routines.predict.output.descriptors.PocketDescriptorRegistry.knownNames(),
|
cz.siret.prank.program.routines.predict.output.descriptors.PocketDescriptorRegistry.knownNames(),
|
||||||
"pocket_descriptors")
|
"pocket_descriptors")
|
||||||
|
|
||||||
@@ -285,7 +280,8 @@ class Main implements Parametrized, Writable {
|
|||||||
"-vis_pocket_grid_gaussian_iso must be > 0 (got ${params.vis_pocket_grid_gaussian_iso}).")
|
"-vis_pocket_grid_gaussian_iso must be > 0 (got ${params.vis_pocket_grid_gaussian_iso}).")
|
||||||
}
|
}
|
||||||
|
|
||||||
validateDescriptorList(params.pocket_grid_point_descriptors,
|
cz.siret.prank.program.routines.predict.output.DescriptorListValidator.validate(
|
||||||
|
params.pocket_grid_point_descriptors,
|
||||||
cz.siret.prank.program.routines.predict.output.grid.descriptors.PocketGridPointDescriptorRegistry.knownNames(),
|
cz.siret.prank.program.routines.predict.output.grid.descriptors.PocketGridPointDescriptorRegistry.knownNames(),
|
||||||
"pocket_grid_point_descriptors")
|
"pocket_grid_point_descriptors")
|
||||||
if (params.pocket_grid_volsite_radius <= 0d) {
|
if (params.pocket_grid_volsite_radius <= 0d) {
|
||||||
@@ -305,31 +301,6 @@ class Main implements Parametrized, Writable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared shape for validating a name-list param against a registry: rejects
|
|
||||||
* null/blank entries, unknown names, and duplicates. {@code paramName} is the
|
|
||||||
* Params property name (without the {@code -} prefix) used in error messages.
|
|
||||||
*/
|
|
||||||
private static void validateDescriptorList(List<String> names, Set<String> known, String paramName) {
|
|
||||||
if (names == null) return
|
|
||||||
Set<String> seen = new HashSet<>()
|
|
||||||
for (String name : names) {
|
|
||||||
if (name == null || name.trim().isEmpty()) {
|
|
||||||
throw new PrankException(
|
|
||||||
"-${paramName} contains an empty/null entry. Known: ${known}")
|
|
||||||
}
|
|
||||||
if (!known.contains(name)) {
|
|
||||||
throw new PrankException(
|
|
||||||
"Unknown name in -${paramName}: '${name}'. Known: ${known}")
|
|
||||||
}
|
|
||||||
if (!seen.add(name)) {
|
|
||||||
throw new PrankException(
|
|
||||||
"-${paramName} contains duplicate name '${name}'. " +
|
|
||||||
"Each descriptor may be listed at most once.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String evalDirParam(String dirParam, String relativePrefixDir) {
|
String evalDirParam(String dirParam, String relativePrefixDir) {
|
||||||
if (dirParam == null) {
|
if (dirParam == null) {
|
||||||
dirParam = "."
|
dirParam = "."
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package cz.siret.prank.program.routines.predict.output;
|
||||||
|
|
||||||
|
import cz.siret.prank.program.PrankException;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a name-list param (e.g. {@code -pocket_descriptors},
|
||||||
|
* {@code -pocket_grid_point_descriptors}) against the names registered in a
|
||||||
|
* descriptor registry. Rejects:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>null or blank entries — a malformed config file should fail fast rather than
|
||||||
|
* slip through and hit {@code Registry.get('')} with a less useful error.</li>
|
||||||
|
* <li>unknown names — names not registered in the registry.</li>
|
||||||
|
* <li>duplicates — would produce duplicate output columns and break Parquet's
|
||||||
|
* schema builder.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>A {@code null} list is treated as "no entries selected" and accepted without
|
||||||
|
* change — matches the existing Params convention where a missing list means
|
||||||
|
* "no extra columns".
|
||||||
|
*/
|
||||||
|
public final class DescriptorListValidator {
|
||||||
|
|
||||||
|
private DescriptorListValidator() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param names values passed on the CLI / from a config file. A {@code null}
|
||||||
|
* list is treated as empty and accepted.
|
||||||
|
* @param known the registry's known-names set (from {@code Registry.knownNames()}).
|
||||||
|
* @param paramName the Params property name, used to format error messages
|
||||||
|
* (the leading {@code -} is added by the validator).
|
||||||
|
*/
|
||||||
|
public static void validate(List<String> names, Set<String> known, String paramName)
|
||||||
|
throws PrankException {
|
||||||
|
if (names == null) return;
|
||||||
|
Set<String> seen = new HashSet<>();
|
||||||
|
for (String name : names) {
|
||||||
|
if (name == null || name.trim().isEmpty()) {
|
||||||
|
throw new PrankException(
|
||||||
|
"-" + paramName + " contains an empty/null entry. Known: " + known);
|
||||||
|
}
|
||||||
|
if (!known.contains(name)) {
|
||||||
|
throw new PrankException(
|
||||||
|
"Unknown name in -" + paramName + ": '" + name + "'. Known: " + known);
|
||||||
|
}
|
||||||
|
if (!seen.add(name)) {
|
||||||
|
throw new PrankException(
|
||||||
|
"-" + paramName + " contains duplicate name '" + name + "'. " +
|
||||||
|
"Each descriptor may be listed at most once.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -29,7 +29,14 @@ public final class PocketGridPointDescriptorRegistry {
|
|||||||
|
|
||||||
private PocketGridPointDescriptorRegistry() {}
|
private PocketGridPointDescriptorRegistry() {}
|
||||||
|
|
||||||
private static void register(PocketGridPointDescriptor d) {
|
/**
|
||||||
|
* Add a descriptor to the registry. Called from the static initializer for
|
||||||
|
* the shipped descriptors; also exposed for tests that need to register a
|
||||||
|
* fixture descriptor and for future external descriptor plugins. The
|
||||||
|
* registry has no remove/clear — a registered descriptor lives for the JVM's
|
||||||
|
* lifetime, which is intentional (CLI selection by name must be deterministic).
|
||||||
|
*/
|
||||||
|
public static void register(PocketGridPointDescriptor d) {
|
||||||
List<String> cols = d.columnNames();
|
List<String> cols = d.columnNames();
|
||||||
if (cols.size() > 1 && new HashSet<>(cols).size() != cols.size()) {
|
if (cols.size() > 1 && new HashSet<>(cols).size() != cols.size()) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package cz.siret.prank.program.routines.predict.output
|
||||||
|
|
||||||
|
import cz.siret.prank.program.PrankException
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
class DescriptorListValidatorTest {
|
||||||
|
|
||||||
|
private static final Set<String> KNOWN = ['volume', 'sphericity', 'num_residues'] as Set
|
||||||
|
private static final String PARAM = 'pocket_descriptors'
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullListIsAcceptedAsEmpty() {
|
||||||
|
// A missing list should be a no-op so a default-empty Params field doesn't
|
||||||
|
// require the caller to special-case it. Failure mode = any thrown exception.
|
||||||
|
DescriptorListValidator.validate(null, KNOWN, PARAM)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyListIsAccepted() {
|
||||||
|
DescriptorListValidator.validate([], KNOWN, PARAM)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validNamesAreAccepted() {
|
||||||
|
DescriptorListValidator.validate(['volume', 'sphericity'], KNOWN, PARAM)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownNameThrowsAndNamesTheOffender() {
|
||||||
|
// The error message MUST include the typo so the user can locate it in their
|
||||||
|
// config, and MUST include the known list so they can correct it.
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
DescriptorListValidator.validate(['volume', 'sphericty'], KNOWN, PARAM)
|
||||||
|
} as PrankException
|
||||||
|
assertTrue(e.message.contains("'sphericty'"), "missing typo: ${e.message}")
|
||||||
|
assertTrue(e.message.contains('-pocket_descriptors'), "missing param: ${e.message}")
|
||||||
|
assertTrue(e.message.contains('volume'), "missing known list: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void duplicateNameThrows() {
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
DescriptorListValidator.validate(['volume', 'volume'], KNOWN, PARAM)
|
||||||
|
} as PrankException
|
||||||
|
assertTrue(e.message.contains("'volume'"), "missing dup name: ${e.message}")
|
||||||
|
assertTrue(e.message.toLowerCase().contains('duplicate'), "missing 'duplicate': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void nullEntryThrows() {
|
||||||
|
// Distinguishes "list of one valid name" from "list with a null inside" —
|
||||||
|
// catches malformed Groovy config files (e.g. trailing comma in a list literal).
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
DescriptorListValidator.validate([null] as List<String>, KNOWN, PARAM)
|
||||||
|
} as PrankException
|
||||||
|
assertTrue(e.message.toLowerCase().contains('empty/null'), e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void blankEntryThrows() {
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
DescriptorListValidator.validate(['volume', ' '], KNOWN, PARAM)
|
||||||
|
} as PrankException
|
||||||
|
assertTrue(e.message.toLowerCase().contains('empty/null'), e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void paramNameIsRenderedWithDashPrefix() {
|
||||||
|
// We don't want the validator to be inconsistent about CLI-flag formatting.
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
DescriptorListValidator.validate(['xx'], KNOWN, 'some_param')
|
||||||
|
} as PrankException
|
||||||
|
assertTrue(e.message.contains('-some_param'),
|
||||||
|
"expected leading dash on param name, got: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
package cz.siret.prank.program.routines.predict.output
|
package cz.siret.prank.program.routines.predict.output
|
||||||
|
|
||||||
import com.carrotsearch.hppc.LongIntHashMap
|
import com.carrotsearch.hppc.LongIntHashMap
|
||||||
|
import cz.siret.prank.domain.Pocket
|
||||||
|
import cz.siret.prank.domain.Protein
|
||||||
import cz.siret.prank.geom.Atoms
|
import cz.siret.prank.geom.Atoms
|
||||||
import cz.siret.prank.geom.Point
|
import cz.siret.prank.geom.Point
|
||||||
|
import cz.siret.prank.program.PrankException
|
||||||
import cz.siret.prank.program.routines.predict.output.grid.PocketGrid
|
import cz.siret.prank.program.routines.predict.output.grid.PocketGrid
|
||||||
|
import cz.siret.prank.program.routines.predict.output.grid.descriptors.PocketGridPointContext
|
||||||
|
import cz.siret.prank.program.routines.predict.output.grid.descriptors.PocketGridPointDescriptor
|
||||||
|
import cz.siret.prank.program.routines.predict.output.grid.descriptors.PocketGridPointDescriptorRegistry
|
||||||
import groovy.transform.CompileStatic
|
import groovy.transform.CompileStatic
|
||||||
import org.biojava.nbio.structure.Atom
|
import org.biojava.nbio.structure.Atom
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*
|
import static org.junit.jupiter.api.Assertions.*
|
||||||
@@ -80,4 +87,83 @@ class PocketGridRowsTest {
|
|||||||
assertEquals(TableData.ColumnType.INT, data.getColumnType(3))
|
assertEquals(TableData.ColumnType.INT, data.getColumnType(3))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Protein emptyProtein() {
|
||||||
|
Protein p = new Protein()
|
||||||
|
p.proteinAtoms = new Atoms()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void descriptorColumnsPrefixedWithDescriptorName() {
|
||||||
|
// Multi-column descriptor (volsite, 6 cols) must produce 6 prefixed
|
||||||
|
// headers; the prefix rule is documented contract for the export.
|
||||||
|
PocketGridRows data = new PocketGridRows(buildTwoPocketGrid(), false,
|
||||||
|
emptyProtein(), [] as List<Pocket>, ['volsite'])
|
||||||
|
assertEquals(['x', 'y', 'z', 'pocket',
|
||||||
|
'volsite.vsAromatic', 'volsite.vsCation', 'volsite.vsAnion',
|
||||||
|
'volsite.vsHydrophobic', 'volsite.vsAcceptor', 'volsite.vsDonor'],
|
||||||
|
data.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getRowAppendsDescriptorValuesAfterBaseColumns() {
|
||||||
|
// Empty protein → cutoutSphere is empty → all 6 indicator columns are 0.
|
||||||
|
// The point of the test is the row LAYOUT (base 4 then 6 descriptor cols),
|
||||||
|
// not the descriptor's numeric semantics — that's covered in
|
||||||
|
// VolsiteGridPointDescriptorTest.
|
||||||
|
PocketGridRows data = new PocketGridRows(buildTwoPocketGrid(), false,
|
||||||
|
emptyProtein(), [] as List<Pocket>, ['volsite'])
|
||||||
|
double[] row = data.getRow(0)
|
||||||
|
assertEquals(10, row.length)
|
||||||
|
// base columns intact
|
||||||
|
assertEquals(1.0d, row[0], 0d); assertEquals(0d, row[1], 0d); assertEquals(0d, row[2], 0d)
|
||||||
|
assertEquals(1, (int) row[3])
|
||||||
|
// descriptor columns all zero (no atoms to classify)
|
||||||
|
for (int i = 4; i < row.length; i++) assertEquals(0d, row[i], 0d)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownDescriptorNameThrowsAtConstruction() {
|
||||||
|
PocketGrid grid = buildTwoPocketGrid()
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
new PocketGridRows(grid, false, emptyProtein(), [] as List<Pocket>, ['no_such_descriptor'])
|
||||||
|
} as PrankException
|
||||||
|
// The message must name the typo so the user can fix it.
|
||||||
|
assertTrue(e.message.contains('no_such_descriptor'),
|
||||||
|
"expected message to mention typo, got: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fixture: a 1-column descriptor that exercises the scalar branch of the header rule. */
|
||||||
|
@CompileStatic
|
||||||
|
private static final class ScalarTestDescriptor implements PocketGridPointDescriptor {
|
||||||
|
@Override String name() { return TEST_SCALAR_NAME }
|
||||||
|
@Override List<String> columnNames() { return ['ignored'] }
|
||||||
|
@Override List<TableData.ColumnType> columnTypes() { return [TableData.ColumnType.DOUBLE] }
|
||||||
|
@Override double[] compute(PocketGridPointContext ctx) { return [42.0d] as double[] }
|
||||||
|
}
|
||||||
|
private static final String TEST_SCALAR_NAME = '__test_scalar_descriptor__'
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void registerScalarFixture() {
|
||||||
|
// Idempotent: register() overwrites by name, so re-running tests in the same JVM
|
||||||
|
// is safe. Name is namespaced with underscores so it can't collide with any
|
||||||
|
// user-facing CLI name.
|
||||||
|
PocketGridPointDescriptorRegistry.register(new ScalarTestDescriptor())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scalarDescriptorEmitsBareNameWithNoPrefix() {
|
||||||
|
// The "{name}.{col}" prefix rule applies ONLY when a descriptor has more than
|
||||||
|
// one column. A single-column descriptor's header is exactly name() — sub-name
|
||||||
|
// is ignored. None of the shipped descriptors are scalar, so this branch
|
||||||
|
// exists for future descriptors and the registered fixture exercises it.
|
||||||
|
PocketGridRows data = new PocketGridRows(buildTwoPocketGrid(), false,
|
||||||
|
emptyProtein(), [] as List<Pocket>, [TEST_SCALAR_NAME])
|
||||||
|
assertEquals(['x', 'y', 'z', 'pocket', TEST_SCALAR_NAME], data.header)
|
||||||
|
// The value 42 from compute() must land in the trailing descriptor column.
|
||||||
|
double[] row = data.getRow(0)
|
||||||
|
assertEquals(5, row.length)
|
||||||
|
assertEquals(42.0d, row[4], 0d)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package cz.siret.prank.program.routines.predict.output.grid.descriptors
|
||||||
|
|
||||||
|
import cz.siret.prank.program.PrankException
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
@CompileStatic
|
||||||
|
class PocketGridPointDescriptorRegistryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shippedDescriptorsAreRegistered() {
|
||||||
|
// If someone removes one of the shipped descriptors, the CLI -pocket_grid_point_descriptors
|
||||||
|
// default no longer resolves and existing user config files start to fail. This test
|
||||||
|
// pins both shipped names.
|
||||||
|
assertNotNull(PocketGridPointDescriptorRegistry.get('volsite'))
|
||||||
|
assertNotNull(PocketGridPointDescriptorRegistry.get('volsite_smooth'))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownNameThrowsWithKnownList() {
|
||||||
|
PrankException e = assertThrows(PrankException.class) {
|
||||||
|
PocketGridPointDescriptorRegistry.get('does_not_exist')
|
||||||
|
} as PrankException
|
||||||
|
// The error message must name the typo AND list the known names so the
|
||||||
|
// user can see the correct spelling.
|
||||||
|
assertTrue(e.message.contains('does_not_exist'), "missing typo in: ${e.message}")
|
||||||
|
assertTrue(e.message.contains('volsite'), "missing 'volsite' in known list: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package cz.siret.prank.program.routines.predict.output.grid.descriptors
|
||||||
|
|
||||||
|
import cz.siret.prank.domain.Protein
|
||||||
|
import cz.siret.prank.geom.Atoms
|
||||||
|
import cz.siret.prank.geom.Point
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import org.biojava.nbio.structure.Atom
|
||||||
|
import org.biojava.nbio.structure.AtomImpl
|
||||||
|
import org.biojava.nbio.structure.Element
|
||||||
|
import org.biojava.nbio.structure.Group
|
||||||
|
import org.biojava.nbio.structure.AminoAcidImpl
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Behavioural tests for the indicator volsite descriptor. We don't test every
|
||||||
|
* pharmacophore branch (that's {@code VolSitePharmacophore}'s domain) — we
|
||||||
|
* verify that the descriptor correctly aggregates per-atom flags into the
|
||||||
|
* 6-column row and honors the cutoff radius.
|
||||||
|
*
|
||||||
|
* <p>Column order (matches {@code VolSitePharmacophore.COLUMN_NAMES}):
|
||||||
|
* aromatic, cation, anion, hydrophobic, acceptor, donor.
|
||||||
|
*/
|
||||||
|
@CompileStatic
|
||||||
|
class VolsiteGridPointDescriptorTest {
|
||||||
|
|
||||||
|
private static final int AROMATIC = 0, CATION = 1, ANION = 2,
|
||||||
|
HYDROPHOBIC = 3, ACCEPTOR = 4, DONOR = 5
|
||||||
|
|
||||||
|
private static Atom atomAt(String element, String resName, String atomName,
|
||||||
|
double x, double y, double z) {
|
||||||
|
AtomImpl a = new AtomImpl()
|
||||||
|
a.element = Element.valueOfIgnoreCase(element)
|
||||||
|
a.name = atomName
|
||||||
|
a.x = x; a.y = y; a.z = z
|
||||||
|
Group g = new AminoAcidImpl()
|
||||||
|
g.setPDBName(resName)
|
||||||
|
a.setGroup(g)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PocketGridPointContext ctxAt(double x, double y, double z, Atoms proteinAtoms) {
|
||||||
|
Protein p = new Protein()
|
||||||
|
p.proteinAtoms = proteinAtoms
|
||||||
|
return new PocketGridPointContext(0, new Point(x, y, z), 0, null, p, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void singleHydrophobicAtomNearbySetsOnlyHydrophobic() {
|
||||||
|
// Atom name "C" + any residue → hydrophobic (first branch in VolSitePharmacophore).
|
||||||
|
Atom c = atomAt("C", "ALA", "C", 1d, 0d, 0d) // 1 Å from grid point — well inside default 4 Å
|
||||||
|
double[] out = new VolsiteGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms([c])))
|
||||||
|
|
||||||
|
assertEquals(1d, out[HYDROPHOBIC])
|
||||||
|
for (int i = 0; i < 6; i++) if (i != HYDROPHOBIC) assertEquals(0d, out[i], 0d, "col $i")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void atomOutsideRadiusContributesNothing() {
|
||||||
|
// Default -pocket_grid_volsite_radius is 4.0; place a hydrophobic atom at 5 Å.
|
||||||
|
Atom c = atomAt("C", "ALA", "C", 5d, 0d, 0d)
|
||||||
|
double[] out = new VolsiteGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms([c])))
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++) assertEquals(0d, out[i], 0d, "col $i")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mixedNearbyAtomsSetMultipleIndependentFlags() {
|
||||||
|
// One donor (N backbone) + one anion (OD1 in ASP) + one cation (ZN — name-only rule).
|
||||||
|
// All within 4 Å of origin. Three indicators should fire; the other three should not.
|
||||||
|
Atoms protein = new Atoms([
|
||||||
|
atomAt("N", "ALA", "N", 1d, 0d, 0d),
|
||||||
|
atomAt("O", "ASP", "OD1", 0d, 1d, 0d),
|
||||||
|
atomAt("ZN", "ZN", "ZN", 0d, 0d, 1d),
|
||||||
|
])
|
||||||
|
double[] out = new VolsiteGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, protein))
|
||||||
|
|
||||||
|
assertEquals(1d, out[DONOR])
|
||||||
|
assertEquals(1d, out[ANION])
|
||||||
|
assertEquals(1d, out[CATION])
|
||||||
|
assertEquals(0d, out[AROMATIC])
|
||||||
|
assertEquals(0d, out[HYDROPHOBIC])
|
||||||
|
assertEquals(0d, out[ACCEPTOR])
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyNeighborhoodAllZeros() {
|
||||||
|
double[] out = new VolsiteGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms()))
|
||||||
|
for (int i = 0; i < 6; i++) assertEquals(0d, out[i], 0d)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package cz.siret.prank.program.routines.predict.output.grid.descriptors
|
||||||
|
|
||||||
|
import cz.siret.prank.domain.Protein
|
||||||
|
import cz.siret.prank.geom.Atoms
|
||||||
|
import cz.siret.prank.geom.Point
|
||||||
|
import cz.siret.prank.program.params.Params
|
||||||
|
import groovy.transform.CompileStatic
|
||||||
|
import org.biojava.nbio.structure.Atom
|
||||||
|
import org.biojava.nbio.structure.AtomImpl
|
||||||
|
import org.biojava.nbio.structure.Element
|
||||||
|
import org.biojava.nbio.structure.Group
|
||||||
|
import org.biojava.nbio.structure.AminoAcidImpl
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the Gaussian weighting math: kernel value at known distances, summing
|
||||||
|
* across atoms, and the 4σ cutoff. These are the numeric facts that, if broken,
|
||||||
|
* silently produce wrong scores — exactly what unit tests should catch.
|
||||||
|
*/
|
||||||
|
@CompileStatic
|
||||||
|
class VolsiteSmoothGridPointDescriptorTest {
|
||||||
|
|
||||||
|
private static final double DELTA = 1e-9
|
||||||
|
|
||||||
|
private static final int AROMATIC = 0, CATION = 1, ANION = 2,
|
||||||
|
HYDROPHOBIC = 3, ACCEPTOR = 4, DONOR = 5
|
||||||
|
|
||||||
|
private static Atom atomAt(String atomName, String resName, double x, double y, double z) {
|
||||||
|
AtomImpl a = new AtomImpl()
|
||||||
|
a.element = Element.C
|
||||||
|
a.name = atomName
|
||||||
|
a.x = x; a.y = y; a.z = z
|
||||||
|
Group g = new AminoAcidImpl()
|
||||||
|
g.setPDBName(resName)
|
||||||
|
a.setGroup(g)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PocketGridPointContext ctxAt(double x, double y, double z, Atoms proteinAtoms) {
|
||||||
|
Protein p = new Protein()
|
||||||
|
p.proteinAtoms = proteinAtoms
|
||||||
|
return new PocketGridPointContext(0, new Point(x, y, z), 0, null, p, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void weightAtZeroDistanceIsOne() {
|
||||||
|
// exp(0) = 1.0 exactly. Atom name "C" is hydrophobic.
|
||||||
|
Atom c = atomAt("C", "ALA", 0d, 0d, 0d)
|
||||||
|
double[] out = new VolsiteSmoothGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms([c])))
|
||||||
|
assertEquals(1.0d, out[HYDROPHOBIC], DELTA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void weightAtSigmaMatchesGaussianFormula() {
|
||||||
|
// At distance r = σ, weight = exp(-r²/(2σ²)) = exp(-1/2) ≈ 0.6065.
|
||||||
|
double sigma = Params.inst.pocket_grid_volsite_sigma
|
||||||
|
Atom c = atomAt("C", "ALA", sigma, 0d, 0d)
|
||||||
|
double[] out = new VolsiteSmoothGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms([c])))
|
||||||
|
assertEquals(Math.exp(-0.5d), out[HYDROPHOBIC], DELTA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void weightsFromMultipleAtomsOfSameTypeSum() {
|
||||||
|
// Two hydrophobic atoms at distance σ each → sum = 2 × exp(-0.5).
|
||||||
|
double sigma = Params.inst.pocket_grid_volsite_sigma
|
||||||
|
Atoms protein = new Atoms([
|
||||||
|
atomAt("C", "ALA", sigma, 0d, 0d),
|
||||||
|
atomAt("C", "ALA", 0d, sigma, 0d),
|
||||||
|
])
|
||||||
|
double[] out = new VolsiteSmoothGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, protein))
|
||||||
|
assertEquals(2d * Math.exp(-0.5d), out[HYDROPHOBIC], DELTA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void atomBeyondCutoffContributesZero() {
|
||||||
|
// 4σ is the hard cutoff (cutoutSphere is the gate). At 5σ the atom isn't even
|
||||||
|
// in the kdtree result. Zero contribution.
|
||||||
|
double sigma = Params.inst.pocket_grid_volsite_sigma
|
||||||
|
Atom c = atomAt("C", "ALA", 5d * sigma, 0d, 0d)
|
||||||
|
double[] out = new VolsiteSmoothGridPointDescriptor().compute(
|
||||||
|
ctxAt(0d, 0d, 0d, new Atoms([c])))
|
||||||
|
assertEquals(0d, out[HYDROPHOBIC], DELTA)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user