CandidateResult#

The outcome of one Candidate: its metrics at the scheduled stage and, with leveling, at the leveled stage, or the error that rejected it. Project::evaluate and Project::evaluate_with return one per candidate in EvaluateResult::candidates, in input order.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct CandidateResult {
    pub id: Id,
    pub error: Option<RejectedOperation>,
    pub scheduled: Option<Metrics>,
    pub leveled: Option<LeveledMetrics>,
    pub cost: Option<f64>,
}

Fields#

Field Type Description
id Id The candidate's id, echoed. Uniqueness isn't checked.
error Option<RejectedOperation> Why the candidate failed: the first rejected operation of its change set, or an input error from its solve or level run. None when the candidate was scored.
scheduled Option<Metrics> Metrics after the candidate's changes are applied and solved. Some whenever error is None.
leveled Option<LeveledMetrics> Metrics after leveling, plus the leveling summary. Some only when EvaluateOptions::level is set and the scheduled stage has no cycles.
cost Option<f64> The cost of the last stage present: leveled, else scheduled. Lower is better. None when error is set.

error excludes every other optional field: a rejected candidate carries no metrics and no cost. The cost formula is on Metrics.

Field combinations#

Case error scheduled leveled cost
Rejected Some None None None
Scored, no level None Some None from scheduled
Scored with level None Some Some from leveled
Cyclic, with level None Some (cycles.value > 0) None from scheduled

A cyclic candidate isn't rejected: scheduled is measured on the dates the cyclic solve returns.

Serialization#

With the serde feature, CandidateResult implements Serialize and Deserialize. Field names are camelCase, and None fields are left out of the output.

Example#

Score two candidates with leveling. The second one names a resource that doesn't exist, so its result carries only error.

use plen_core::{
    AssignmentData, AssignmentUpdate, Candidate, DateTime, EvaluateOptions, LevelingOptions,
    Patch, Project, ProjectData, ResourceData, SchedulingChanges, SolveOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Two tasks book "ann" at the same time; "bob" is free.
    let monday = DateTime::from_ymd_hm(2026, 3, 2, 8, 0).ok_or("invalid date")?;
    let task = |id: &str| TaskData {
        start_date: Some(monday),
        duration: Some(3.0),
        ..TaskData::new(id)
    };
    let resource = |id: &str| ResourceData { id: id.into(), capacity: None, calendar_id: None };
    let booking = |id: &str, task: &str| AssignmentData {
        id: Some(id.into()),
        task: task.into(),
        resource: "ann".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };

    let mut project = Project::default();
    project.solve(
        Some(ProjectData {
            tasks: vec![task("design"), task("build")],
            resources: vec![resource("ann"), resource("bob")],
            assignments: vec![booking("a1", "design"), booking("a2", "build")],
            ..ProjectData::default()
        }),
        &SolveOptions::default(),
    )?;

    // Candidate "to-bob" is valid; "to-carol" names a resource that doesn't exist.
    let move_build_to = |resource: &str| {
        let mut changes = SchedulingChanges::default();
        changes.assignments.update.push(AssignmentUpdate {
            resource: Patch::Value(resource.into()),
            ..AssignmentUpdate::new("a2")
        });
        Candidate { id: format!("to-{resource}").into(), changes }
    };
    let candidates = [move_build_to("bob"), move_build_to("carol")];

    let options = EvaluateOptions {
        level: Some(LevelingOptions::default()),
        ..EvaluateOptions::default()
    };
    let result = project.evaluate(&candidates, &options)?;

    for scored in &result.candidates {
        if let Some(error) = &scored.error {
            println!("{}: rejected at {}: {}", scored.id, error.path, error.message);
            continue;
        }
        if let Some(scheduled) = &scored.scheduled {
            println!("{}: scheduled cost {}, overload delta {}", scored.id, scheduled.cost, scheduled.overload.total.delta);
        }
        if let Some(leveled) = &scored.leveled {
            println!("{}: leveled in {} rounds", scored.id, leveled.rounds);
        }
        if let Some(cost) = scored.cost {
            println!("{}: cost {cost}", scored.id);
        }
    }
    Ok(())
}

Output:

to-bob: scheduled cost 0, overload delta -3
to-bob: leveled in 0 rounds
to-bob: cost 0
to-carol: rejected at assignments.update[0]: unknown resource "carol"

For how candidates are run and ranked, see Evaluating alternatives.