Skip to content

Scoring

Every score in molforge is a bare float whose "good" direction you have to know out of band:

Source Field Better
Vina Pose.score (kcal/mol) lower
Gnina Pose.score (CNN score) higher
ProteinMPNN DesignedSequence.score (NLL) lower
Folding engines metadata["mean_confidence"] (pLDDT) higher

The same Pose.score field is lower-is-better for Vina and higher-is-better for Gnina. molforge.scoring makes the direction explicit so you can rank, compare, and threshold across sources uniformly.

from molforge.scoring import ConfidenceScorer, rank

best_first = rank(structures, ConfidenceScorer())
top = best_first[0][0]

Score and Direction

A Scorer returns a Score carrying its Direction. The key method is ranking_key — a "higher is always better" view that negates lower-is-better values, so scores of either direction sort and compare uniformly:

from molforge.scoring import Score, Direction

affinity = Score(-9.5, Direction.LOWER_IS_BETTER)
plddt    = Score(87.0, Direction.HIGHER_IS_BETTER)

affinity.ranking_key            # 9.5  (negated)
affinity.is_better_than(Score(-7.0, Direction.LOWER_IS_BETTER))   # True

A nan value is never better than a real one, so unscoreable items sink in a ranking rather than surfacing spuriously.

The v1 scorers

All dependency-free — they read already-computed numbers or wrap a callable, so scoring never re-runs a heavy engine.

ConfidenceScorer — a folded structure's mean pLDDT (higher is better):

from molforge.scoring import ConfidenceScorer
score = ConfidenceScorer().score(protein)   # reads metadata["mean_confidence"]

DockingScorer — a Pose or DockingResult's score, with the correct direction. Because docking scores don't share a direction, read it from the engine that produced the result:

from molforge.scoring import DockingScorer

scorer = DockingScorer.from_engine(vina)     # -> lower_is_better
scorer.score(result)                         # scores the best pose

The engines expose this via a score_direction attribute (Vina / DiffDock are "lower_is_better"; Gnina follows its sort_order).

FunctionScorer — wrap any item -> float callable with a direction. The escape hatch for engine re-scoring, an ESM perplexity call, or a bespoke composite:

from molforge.scoring import FunctionScorer, Direction

esm = FunctionScorer(esm_perplexity, direction=Direction.LOWER_IS_BETTER, name="esm_ppl")

Ranking helpers

rank(items, scorer) returns (item, score) pairs best-first; best(items, scorer) returns the single winner. Both are direction-aware and put nan scores last. Scorer.score_many(items) scores a batch (serial by default; see molforge.parallel).

Using a scorer as a design objective

Any Scorer plugs straight into DesignLoop as an objective — it grades each candidate's folded structure by ranking_key, so higher is always better regardless of the scorer's native direction:

from molforge.design import DesignLoop
from molforge.scoring import ConfidenceScorer

loop = DesignLoop(designer=..., folder=..., objective=ConfidenceScorer())

What v1 doesn't do

Learned scorers that compute a value — ESM perplexity, ProteinMPNN log-likelihood, engine re-scoring (re-running Vina/Gnina) — are follow-ups that implement the same Scorer ABC. For now, wrap them with a FunctionScorer.