Overload#

The resource overload of one evaluation stage: the total, the share of each resource, and the overload around each focus task. Each value comes with its difference from the baseline. You read it from Metrics::overload, which appears in EvaluateResult::baseline and in the scheduled and leveled stages of every CandidateResult.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct Overload {
    pub total: Amount,
    pub resources: Vec<ResourceOverload>,
    pub focus: Vec<FocusOverload>,
}

With the serde feature, Overload implements Serialize and Deserialize with camelCase field names.

Fields#

Field Type Description
total Amount Overload summed over all resources. delta is the difference from the baseline total.
resources Vec<ResourceOverload> One entry per resource with non-zero overload in this stage or in the baseline. Resources of this stage come first, then resources overloaded only in the baseline.
focus Vec<FocusOverload> One entry per id in EvaluateOptions::focus. Empty when focus is empty.

How overload is measured#

For each resource, overload is the sum over time buckets of max(0, load − capacity). The unit is capacity × buckets: one resource at capacity 1, double-booked for two day buckets, has an overload of 2.

The baseline and every stage of every candidate use the same bucket basis: LevelingOptions::basis when EvaluateOptions::level sets it, otherwise the basis derived from config.duration_unit. Values from one EvaluateResult are therefore comparable with each other.

Example#

Two tasks run on the same days and both use dev1. Score a candidate that moves task 2 to dev2, and print the overload of the baseline and of the candidate.

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

fn print(label: &str, overload: &Overload) {
    println!("{label}: total {} ({:+})", overload.total.value, overload.total.delta);
    for r in &overload.resources {
        println!("  {}: {} ({:+})", r.resource, r.overload.value, r.overload.delta);
    }
    for f in &overload.focus {
        match &f.resources {
            Some(resources) => println!("  around task {}: {} resource(s)", f.task, resources.len()),
            None => println!("  task {} removed", f.task),
        }
    }
}

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 resource = |id: &str| ResourceData {
        id: id.into(),
        capacity: None,
        calendar_id: None,
    };
    let assignment = |id: &str, task: i32| AssignmentData {
        id: Some(id.into()),
        task: task.into(),
        resource: "dev1".into(),
        value: 1.0,
        offset: None,
        duration: None,
    };
    let data = ProjectData {
        tasks: vec![task(1), task(2)],
        resources: vec![resource("dev1"), resource("dev2")],
        assignments: vec![assignment("a1", 1), assignment("a2", 2)],
        ..Default::default()
    };

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

    let mut changes = SchedulingChanges::default();
    let mut to_dev2 = AssignmentUpdate::new("a2");
    to_dev2.resource = Patch::Value("dev2".into());
    changes.assignments.update.push(to_dev2);
    let candidates = [Candidate { id: "dev2".into(), changes }];

    let options = EvaluateOptions {
        focus: vec![2.into()],
        ..Default::default()
    };
    let result = project.evaluate(&candidates, &options)?;

    print("baseline", &result.baseline.overload);
    for candidate in &result.candidates {
        if let Some(scheduled) = &candidate.scheduled {
            print(&candidate.id.to_string(), &scheduled.overload);
        }
    }
    Ok(())
}