LeveledMetrics#

Metrics of a leveled stage, plus a summary of the leveling run that produced it. It's returned in CandidateResult::leveled by Project::evaluate and Project::evaluate_with, and in SuggestProposal::leveled by Project::suggest_assignments. It isn't built by hand.

For when those fields are Some, see CandidateResult and SuggestProposal.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct LeveledMetrics {
    pub metrics: Metrics,
    pub converged: bool,
    pub rounds: u32,
    pub windows: u32,
}

LeveledMetrics doesn't implement Default. With the serde feature it implements Serialize and Deserialize with camelCase field names, and metrics is flattened: finish, overload, conflicts, cycles and cost sit next to converged, rounds and windows.

Fields#

Name Type Description
metrics Metrics Finish, overload, conflicts, cycles and cost of the leveled stage.
converged bool Whether the planner reached a fixed point in the final run after leveling, the same value as ScheduleResult::converged of that run.
rounds u32 Leveling rounds that proposed a correction, as in LevelingResult::rounds.
windows u32 Overallocation windows left after leveling, summed over all resources and counted whatever their reason. 0 when no overallocation is left. See OverallocationWindow.

Example#

ann has two 3-day tasks at the same time. The plan is scored as is with leveling on, and the leveled stage is printed.

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    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 assignment = |id: &str, task: &str| AssignmentData {
        id: Some(id.into()),
        task: task.into(),
        resource: "ann".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };

    let data = ProjectData {
        tasks: vec![task("design"), task("build")],
        resources: vec![ResourceData {
            id: "ann".into(),
            capacity: None,
            calendar_id: None,
        }],
        assignments: vec![assignment("a1", "design"), assignment("a2", "build")],
        ..ProjectData::default()
    };

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

    let candidates = vec![Candidate {
        id: "as-is".into(),
        changes: SchedulingChanges::default(),
    }];
    let options = EvaluateOptions {
        level: Some(LevelingOptions::default()),
        ..EvaluateOptions::default()
    };
    let result = project.evaluate(&candidates, &options)?;

    for scored in &result.candidates {
        if let Some(leveled) = &scored.leveled {
            let finish_delta = leveled
                .metrics
                .finish
                .project
                .as_ref()
                .map_or(0.0, |shift| shift.delta);
            // as-is: finish +3, cost 3
            println!("{}: finish {:+}, cost {}", scored.id, finish_delta, leveled.metrics.cost);
            // converged true, rounds 3, windows left 0
            println!(
                "converged {}, rounds {}, windows left {}",
                leveled.converged, leveled.rounds, leveled.windows
            );
        }
    }
    Ok(())
}