Evaluation
Evaluation scores collected Candidates against a Task. It records route validity, task-constraint satisfaction, acceptable-route matches, and optional per-target runtime without mutating the route tree.
Score Collected Candidates
stocks = {"buyables": list(stock_inchikeys)}
evaluation = retrocast.score(
predictions,
task,
stocks,
match_level="full",
acceptable_route_match="prefix",
workers=12,
)
use retrocast_core::score::score_owned;
let evaluation = score_owned(
predictions,
task,
&stocks,
"full",
"prefix",
None,
12,
)?;
from retrocast.metrics import StockTerminationChecker
from retrocast.workflow import score
evaluation = score(
predictions,
task,
constraint_checkers=[
StockTerminationChecker(stocks={"buyables": stock_inchikeys}),
],
)
Python 0.8.x receives NativeEvaluation. Passing NativePredictions to score consumes the handle and moves the candidate graph into the evaluation. Rust expresses the same ownership directly through score_owned. Python 0.7.1 returns an independent Pydantic Evaluation and retains its input models.
Write or inspect predictions before scoring if you need them afterward:
predictions.write("candidates.json.gz")
evaluation = retrocast.score(predictions, task, stocks)
Solv-N Separation
Solv-N is defined as:
Solv-i[task] = Tier-i route validity + task constraint satisfaction
That separation appears directly in every scored candidate:
candidate.validity # Tier results
candidate.constraints # Effective task-constraint result
snapshot = evaluation.to_dict()
scored = snapshot["targets"]["target-001"]["candidates"][0]
tier_0_passes = scored["validity"]["tiers"]["0"]["status"] == "pass"
task_passes = scored["constraints"]["status"] == "pass"
solv_0 = tier_0_passes and task_passes
let scored = &evaluation.targets["target-001"].candidates[0];
let tier_0_passes = scored.satisfies_validity(0);
let task_passes = scored.satisfies_task();
let solv_0 = scored.satisfies_solv(0);
scored = evaluation.targets["target-001"].candidates[0]
tier_0_passes = scored.satisfies_validity(0)
task_passes = scored.satisfies_task()
solv_0 = scored.satisfies_solv(0)
Tier-0 validity comes from adaptation. A candidate with a route passes Tier 0; a candidate containing a FailureRecord fails it.
Inspect Failed Adaptation Slots
Failed adaptation slots stay in the ranked list.
for scored in snapshot["targets"]["target-001"]["candidates"]:
if failure := scored.get("failure"):
print(scored["rank"], failure["code"])
continue
print(scored["rank"], scored["route"]["target"]["smiles"])
for scored in &evaluation.targets["target-001"].candidates {
if let Some(failure) = &scored.failure {
println!("{} {}", scored.rank, failure.code);
continue;
}
if let Some(route) = &scored.route {
println!("{} {}", scored.rank, route.target.smiles);
}
}
for scored in evaluation.targets["target-001"].candidates:
if scored.failed_adaptation():
print(scored.rank, scored.failure.code)
continue
print(scored.rank, scored.route.target.smiles)
The Candidate invariant guarantees exactly one of route or failure.
Track Runtime
Execution statistics are target-id maps. Record model inference outside RetroCast, then attach the measurements during scoring.
execution_stats = {
"wall_time": {"target-001": 1.42, "target-002": 0.87},
"cpu_time": {"target-001": 1.31, "target-002": 0.79},
}
evaluation = retrocast.score(
predictions,
task,
stocks,
execution_stats=execution_stats,
)
report = retrocast.analyze(evaluation)
print(report["runtime"]["total_wall_time"])
print(report["runtime"]["mean_cpu_time"])
use retrocast_core::model::ExecutionStats;
let execution_stats = ExecutionStats {
wall_time,
cpu_time,
};
let evaluation = score_owned(
predictions,
task,
&stocks,
"full",
"prefix",
Some(&execution_stats),
12,
)?;
from retrocast.utils import ExecutionTimer
timer = ExecutionTimer()
with timer.measure("target-001"):
raw_by_target["target-001"] = model.predict(target.smiles)
evaluation = score(
predictions,
task,
constraint_checkers=checkers,
execution_stats=timer.to_model(),
)
report = analyze(evaluation)
print(report.runtime.total_wall_time)
Runtime is optional. Analysis summarizes only targets that contain measurements.
Task Constraints
The task carries default constraints and optional per-target overrides. The native scorer currently implements these schema-2 constraint kinds:
retrocast.stock_terminationretrocast.required_leavesretrocast.route_depth
Stock sets are supplied separately because they can be large. The stock field on a task constraint selects a named set from the stocks map passed to score.
Acceptable-Route Matching
match_level controls molecular identity during stock and acceptable-route comparison:
fullno_stereoconnectivity
acceptable_route_match="prefix" accepts a target-rooted prefix of a benchmark route. "exact" requires the full route identity.
Reaction ids inside validity details use route-local paths such as rc:r:/ and rc:r:/1/0. See Route Node IDs.
Extension Boundary
Task constraints are data; their execution belongs to retrocast-core. The Python binding does not call a Python callback once per candidate because that would create different scoring semantics and parallel behavior for the two interfaces.
Add a new production constraint kind in the Rust scorer, expose it through the shared schema, and cover the same serialized result through Python and Rust contract tests.