Project::evaluate#

Scores alternative change sets against the solved baseline of a Project. The project is never modified. Use it to compare options, such as which resource should take a task, before committing one with apply.

Signature#

pub fn evaluate(
    &self,
    candidates: &[Candidate],
    options: &EvaluateOptions,
) -> Result<EvaluateResult, Error>

Takes &self. Synchronous and CPU-only.

Parameters#

Name Type Description
candidates &[Candidate] The change sets to score
options &EvaluateOptions Baseline check, leveling stage, focus tasks and clear_first

Behavior#

Each candidate runs on a disposable copy of the reference state (the baseline, or its cleared copy with options.clear_first):

  1. apply the candidate's changes.
  2. solve(None) and measure the scheduled stage.
  3. With options.level set, level with those options and measure the leveled stage.

Every candidate starts from the reference state, never from the previous candidate. A failure inside one candidate goes into that candidate's error and doesn't fail the call. The result has scores, not dates.

Returns#

An EvaluateResult with the baseline metrics and one result per candidate, in input order.

Errors#

Error When
Error::Project(ProjectError::NoBaseline) The project is unsolved, including after apply
Error::Project(ProjectError::StaleBaseline) options.baseline_id or any candidate's changes.baseline_id doesn't name the current baseline
Error::Input An unknown task in focus (path focus[i]); options.level is invalid for the baseline

evaluate_with#

#[cfg(not(target_family = "wasm"))]
pub fn evaluate_with(
    &self,
    candidates: &[Candidate],
    options: &EvaluateOptions,
    execution: Execution,
) -> Result<EvaluateResult, Error>

Same as evaluate, with a choice of Execution. Execution::Parallel runs candidates on scoped worker threads. The results are identical to evaluate and keep input order. A call-level error is the first one in input order. Errors are the same as for evaluate.

Note

evaluate_with isn't compiled for wasm targets. Use evaluate there.

Example#

Compare leaving the plan as it is with moving task B to an idle resource.

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let monday = DateTime::from_ymd_hm(2026, 8, 10, 8, 0).ok_or("invalid date")?;
    let task = |id: &str| TaskData {
        start_date: Some(monday),
        duration: Some(2.0),
        ..TaskData::new(id)
    };
    let resource = |id: &str| ResourceData { id: id.into(), capacity: None, calendar_id: None };
    let assign = |id: &str, task: &str| AssignmentData {
        id: Some(id.into()),
        task: task.into(),
        resource: "dev1".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };
    let data = ProjectData {
        tasks: vec![task("A"), task("B")],
        resources: vec![resource("dev1"), resource("dev2")],
        assignments: vec![assign("aA", "A"), assign("aB", "B")],
        ..Default::default()
    };
    let mut project = Project::default();
    project.solve(Some(data), &SolveOptions::default())?;

    let mut moved = AssignmentUpdate::new("aB");
    moved.resource = Patch::Value("dev2".into());
    let mut to_dev2 = SchedulingChanges::default();
    to_dev2.assignments.update.push(moved);

    let candidates = [
        Candidate { id: "stay".into(), changes: SchedulingChanges::default() },
        Candidate { id: "dev2".into(), changes: to_dev2 },
    ];
    let result = project.evaluate(&candidates, &EvaluateOptions::default())?;

    for candidate in &result.candidates {
        println!("{}: cost {:?}", candidate.id, candidate.cost); // stay: 2, dev2: 0
    }
    Ok(())
}