Source code for rbfenetmap.plugins.scorers.lomaplike_scorer

"""Multiplicative similarity scorer in the spirit of LOMAP.

Scores an edge as a product of independent penalty factors in ``(0, 1]``, then converts
that similarity to a cost with ``-log``. Multiplicative composition behaves differently
from the weighted sum in :mod:`rbfenetmap.plugins.scorers.linear_scorer`: any single
factor near zero drags the whole similarity to zero regardless of how good the rest is.
That is the right shape when the penalties are *independent reasons the edge will not
converge*, rather than competing preferences to be balanced.

Implemented from the published form; LOMAP itself is not a dependency.
"""

from __future__ import annotations

import math
from types import MappingProxyType
from typing import ClassVar, Mapping, Sequence

from rbfenetmap.core.meta.scorers import AbstractScorer
from rbfenetmap.core.models import EdgeScore, RejectionReason

__all__ = ("DEFAULT_LOMAP_PARAMETERS", "LomapLikeScorer")

#: Tunable factors. ``beta`` sets how fast similarity decays with soft-core size; the
#: ``*_penalty`` values are the multiplier applied per unit of the corresponding change.
DEFAULT_LOMAP_PARAMETERS: Mapping[str, float] = MappingProxyType(
    {"beta": 0.10, "charge_penalty": 0.10, "ring_penalty": 0.40, "ring_atom_penalty": 0.90, "rmsd_penalty": 0.70}
)

#: Similarity floor, so a hopeless-but-feasible edge yields a large finite cost rather
#: than an infinity that would be indistinguishable from a structural rejection.
_MIN_SIMILARITY = 1e-9


[docs] class LomapLikeScorer(AbstractScorer): """Score an edge by a product of penalty factors. Parameters ---------- parameters : Mapping[str, float], optional Overrides merged onto :data:`DEFAULT_LOMAP_PARAMETERS`. Raises ------ ValueError If *parameters* names an unknown key. """ name: ClassVar[str] = "lomaplike" def __init__(self, parameters: Mapping[str, float] | None = None) -> None: """Merge *parameters* onto the defaults, rejecting unknown keys.""" merged = dict(DEFAULT_LOMAP_PARAMETERS) if parameters: unknown = sorted(set(parameters) - set(DEFAULT_LOMAP_PARAMETERS)) if unknown: raise ValueError(f"Unknown parameter(s) {unknown}. Available: {sorted(DEFAULT_LOMAP_PARAMETERS)}.") merged.update({k: float(v) for k, v in parameters.items()}) self._parameters = MappingProxyType(merged)
[docs] def describe_weights(self) -> Mapping[str, float]: """Return the effective parameters.""" return self._parameters
[docs] def score_edge(self, descriptors: Mapping[str, float], *, rejections: Sequence[RejectionReason]) -> EdgeScore: """Return ``-log(similarity)`` as the cost.""" if rejections: return EdgeScore.rejected(*rejections, scorer=self.name, descriptors=MappingProxyType(dict(descriptors))) parameters = self._parameters factors = { "softcore": math.exp(-parameters["beta"] * float(descriptors.get("n_softcore_max_heavy", 0.0))), "charge": parameters["charge_penalty"] ** float(descriptors.get("charge_delta", 0.0)), "ring": parameters["ring_penalty"] ** float(descriptors.get("ring_delta", 0.0)), "ring_atoms": parameters["ring_atom_penalty"] ** float(descriptors.get("n_ring_atoms_in_softcore", 0.0)), "geometry": parameters["rmsd_penalty"] ** float(descriptors.get("core_rmsd", 0.0)), } similarity = max(math.prod(factors.values()), _MIN_SIMILARITY) # Report each factor's own -log contribution, so the terms sum to the total and # `rbfenet score --explain` reads the same way for both scorers. contributions = {name: -math.log(max(value, _MIN_SIMILARITY)) for name, value in factors.items()} total = -math.log(similarity) # Clamping the similarity can make the parts sum to more than the whole; scale # them back so the reported breakdown stays honest. part_sum = sum(contributions.values()) if part_sum > 0: contributions = {k: v * total / part_sum for k, v in contributions.items()} return EdgeScore( total=float(total), feasible=True, descriptors=MappingProxyType(dict(descriptors)), contributions=MappingProxyType(contributions), rejections=(), scorer=self.name, )