SuggestProposal#

The score of the final working state of a Project::suggest_assignments call: its cost, the number of resources in use, and its metrics against the reference state. You read it from SuggestResult::proposed. It isn't built by hand.

Compare it with SuggestResult::baseline, which holds the same cost and resource count for the reference state.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct SuggestProposal {
    pub cost: f64,
    pub resources: u32,
    pub scheduled: Metrics,
    pub leveled: Option<LeveledMetrics>,
}

With the serde feature, SuggestProposal implements Serialize only, with camelCase field names. leveled is left out when it's None.

Fields#

Field Type Description
cost f64 Cost of the final working state on the scoring stage, new-resource penalty included. Lower is better.
resources u32 Resources of the counted pool that have at least one active assignment in the final working state. Counted the same way as AssignTotal::resources.
scheduled Metrics Metrics of the scheduled stage, with deltas against the reference state.
leveled Option<LeveledMetrics> Metrics of the leveled stage plus the leveling summary. Some only when SuggestOptions::level is set, and None when the scheduled stage is cyclic.

scheduled and leveled come from the last accepted step; no extra run is made for them. When no step was taken, they describe the reference state itself: scheduled deltas are zero, while leveled deltas can be non-zero.

For how the state cost and the new-resource penalty are computed, and when a commit can differ from proposed, see Suggesting assignments.

Example#

dev1 already works on task A. Slot B#1 is filled with leveling on, and the proposal is printed next to the reference state.

use plen_core::{
    AssignmentData, DateTime, LevelingOptions, Project, ProjectData, ResourceData, Slot,
    SolveOptions, SuggestOptions, 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 mut tasks = Vec::new();
    for id in ["A", "B"] {
        let mut task = TaskData::new(id);
        task.start_date = Some(monday);
        task.duration = Some(2.0);
        tasks.push(task);
    }
    let resource = |id: &str| ResourceData { id: id.into(), capacity: None, calendar_id: None };

    let mut project = Project::default();
    project.solve(
        Some(ProjectData {
            tasks,
            resources: vec![resource("dev1"), resource("dev2")],
            assignments: vec![AssignmentData {
                id: None,
                task: "A".into(),
                resource: "dev1".into(),
                value: 1.0,
                offset: None,
                duration: None,
            }],
            ..Default::default()
        }),
        &SolveOptions::default(),
    )?;

    let slots = [Slot::new("B#1", "B", vec!["dev1".into(), "dev2".into()])];
    let options = SuggestOptions {
        new_resource_penalty: 0.5,
        level: Some(LevelingOptions::default()),
        ..SuggestOptions::default()
    };
    let result = project.suggest_assignments(&slots, &options)?;

    let proposed = &result.proposed;
    println!("cost {} -> {}", result.baseline.cost, proposed.cost);
    println!("people {} -> {}", result.baseline.resources, proposed.resources);
    println!("scheduled overload {}", proposed.scheduled.overload.total.value);
    if let Some(leveled) = &proposed.leveled {
        let finish_delta = leveled.metrics.finish.project.as_ref().map(|s| s.delta);
        println!("leveled finish delta {:?}, windows {}", finish_delta, leveled.windows);
    }
    Ok(())
}