Metrics#

The measurements of one evaluated state: finish dates, resource overload, conflict and cycle counts, and a cost that combines them. Project::evaluate and Project::evaluate_with return it inside an EvaluateResult: as EvaluateResult::baseline, as CandidateResult::scheduled, and nested in LeveledMetrics::metrics for the leveled stage. It isn't built by hand.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct Metrics {
    pub finish: Finish,
    pub overload: Overload,
    pub conflicts: Amount,
    pub cycles: Amount,
    pub cost: f64,
}

With the serde feature (cargo add plen-core --features serde), Metrics also implements Serialize and Deserialize, with field names in camelCase.

Fields#

Every Amount holds a value for this stage and a delta against the baseline.

Field Type Description
finish Finish Project finish and the finish of each focus task.
overload Overload Resource overload: total, per resource, and per focus task.
conflicts Amount Number of entries in the stage's ScheduleResult::conflicts (see Conflict).
cycles Amount Number of entries in the stage's ScheduleResult::cycles (see Cycle).
cost f64 Score of the stage. Lower is better. Formula below.

cost#

cost = max(0, finish.project.delta)
     + overload.total.value
     + 1000 × (conflicts.value + cycles.value)
  • A finish earlier than the baseline adds nothing. When finish.project is None, the finish term is 0.
  • The 1000 × penalty puts any state with conflicts or cycles behind every state without them.
  • Finish delay and overload are on the same scale when the overload basis follows config.duration_unit: one day late weighs about as much as one resource-day over capacity.

The formula is fixed. To rank by other criteria, compare the raw fields instead of cost.

Example#

Score one candidate that assigns a second task to the same resource, then print the metrics of the baseline and of the candidate.

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

fn print(label: &str, m: &Metrics) {
    let finish_delta = m.finish.project.as_ref().map(|s| s.delta);
    println!(
        "{label}: finish delta {:?}, overload {}, conflicts {}, cycles {}, cost {}",
        finish_delta, m.overload.total.value, m.conflicts.value, m.cycles.value, m.cost
    );
}

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: i32| TaskData {
        start_date: Some(monday),
        duration: Some(2.0),
        ..TaskData::new(id)
    };
    let assign = |task: i32| AssignmentData {
        id: None,
        task: task.into(),
        resource: "dev1".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };

    let data = ProjectData {
        tasks: vec![task(1), task(2)],
        resources: vec![ResourceData { id: "dev1".into(), capacity: None, calendar_id: None }],
        assignments: vec![assign(1)],
        ..Default::default()
    };

    let mut project = Project::new(ProjectOptions::default());
    project.solve(Some(data), &SolveOptions::default())?;

    let mut changes = SchedulingChanges::default();
    changes.assignments.add.push(assign(2));
    let candidates = [Candidate { id: "dev1-on-2".into(), changes }];

    let result = project.evaluate(&candidates, &EvaluateOptions::default())?;
    print("baseline", &result.baseline);
    for c in &result.candidates {
        if let Some(scheduled) = &c.scheduled {
            print("candidate", scheduled);
        }
    }
    Ok(())
}