Plugins

Five plugin kinds, each an abstract base class in rbfenetmap.core.meta:

Kind

Responsibility

Built-ins

mapper

Propose an atom correspondence

mcss, mcss-e, mcss-e2, cartograph, kartograf, identity

scorer

Reduce descriptors to a cost

linear, lomaplike, softcore-size, variance

planner

Select the final edge set

mst, star, explicit, complete, optimal

exporter

Serialize for a downstream program

json, edgelist, graphml, amber, html

intermediate

Invent a ligand bridging a gap

pairmap, fragment-swap

The intermediate generator is the only kind whose output changes the ligand set rather than the network over it. Its contract is correspondingly narrow: it proposes molecules and nothing else. Posing them is rbfenetmap.core.posing.pose_intermediate()’s job, deciding whether the resulting edges are feasible is the repair’s, and pricing them is the scorer’s – each of those already has an owner, and a generator that took one over would make an intermediate’s quality depend on which generator happened to invent it. See Intermediate ligands for what the two built-in generators actually do.

Lazy registration

A PluginSpec describes a plugin without importing it. Registration is pure metadata; the implementation module is imported only when create() is called.

That is what lets rbfenet plugins --all list every backend – including ones whose dependencies are absent – without importing RDKit or kartograf, and what lets the whole test suite run with no optional dependency installed.

The requires tuple is probed with importlib.util.find_spec(), so a missing backend is reported as needs kartograf, gufe rather than as a raw ModuleNotFoundError.

Writing a mapper

from typing import ClassVar

from rbfenetmap.core.meta.mappers import AbstractMapper
from rbfenetmap.core.models import AtomMapping

class MyMapper(AbstractMapper):
    name: ClassVar[str] = "mine"

    def map_pair(self, source, target, options) -> AtomMapping:
        core = {...}  # {index_in_source: index_in_target}
        return AtomMapping.from_core_pairs(
            core, n_atoms_1=source.n_atoms, n_atoms_2=target.n_atoms, method=self.name
        )

A mapper does not need to produce a connected soft-core. Repairing fragmentation is rbfenetmap.core.softcore.repair_softcore_connectivity()’s job, and it runs on every mapper’s output. Duplicating that logic inside a mapper only makes mappers harder to write and compare.

Registering it

from rbfenetmap.core.pluginregistry import PluginRegistry, PluginSpec

registry = PluginRegistry()
registry.register(PluginSpec(
    name="mine",
    kind="mapper",
    target="my_package.mappers:MyMapper",
    description="What it does.",
    requires=("some_backend",),
))

An instance can also be passed straight to build_network(); registering is only needed to make the plugin selectable by name from the CLI.

Exporters: the hook into other programs

An exporter adapts a planned network to a downstream consumer without that consumer’s concerns reaching back into the core. It may also implement validate(), which checks format-specific constraints the core does not enforce, early and without writing anything.

The amber exporter is the motivating case: Amber soft-core masks select atoms by name, so a soft-core atom sharing a name with a common-core atom would silently widen the mask and produce wrong free energies with no error at run time. validate catches that before any expensive work happens.