Skip to main content

cpm_planner/
simulate.rs

1//! Read-only plan simulation: lint, CPM, optional resource leveling and Monte
2//! Carlo, and the scorecard, for a graph that is never persisted.
3
4use serde::{Deserialize, Serialize};
5
6use crate::lint::{LintReport, Severity, lint};
7use crate::metrics::{Scorecard, scorecard};
8use crate::monte_carlo::{MonteCarloRequest, monte_carlo_with_cpm};
9use crate::plan::{MilestoneRow, PlanGraph, PlannerError, ScheduleRow};
10use crate::resource_schedule::{ResourceSchedule, ScheduleRequest, resource_schedule_with_cpm};
11use crate::schedule::{compute_cpm, milestone_rows, schedule_rows};
12
13/// What to compute beyond the always-on lint, CPM and scorecard.
14#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct SimulateRequest {
17    /// Resource capacities; when present the plan is leveled.
18    #[serde(default)]
19    pub schedule: Option<ScheduleRequest>,
20    /// Monte Carlo parameters; when present the scorecard carries a summary.
21    #[serde(default)]
22    pub monte_carlo: Option<MonteCarloRequest>,
23}
24
25/// Everything `plan.simulate` reports.
26#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
27pub struct SimulationResult {
28    pub lint: LintReport,
29    pub critical_path: Vec<String>,
30    pub critical_path_hours: f32,
31    /// Same rows as `plan.status`, including the synthetic endpoints.
32    pub schedule: Vec<ScheduleRow>,
33    pub milestones: Vec<MilestoneRow>,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub resource_schedule: Option<ResourceSchedule>,
36    /// `monte_carlo` is filled when requested.
37    pub scorecard: Scorecard,
38}
39
40/// Simulate `graph` without persisting anything.
41///
42/// A lint `Error` finding (including a cycle) is [`PlannerError::InvalidGraph`]
43/// listing each code, its ids and its message; anything else submit would reject is
44/// [`PlannerError::InvalidGraph`] with submit's message. CPM runs once and
45/// is shared by leveling and Monte Carlo.
46pub fn simulate(
47    graph: &PlanGraph,
48    req: &SimulateRequest,
49) -> Result<SimulationResult, PlannerError> {
50    let lint_report = lint(graph);
51    let errors: Vec<String> = lint_report
52        .findings
53        .iter()
54        .filter(|f| f.severity == Severity::Error)
55        .map(|f| format!("{} [{}]: {}", f.code, f.ids.join(", "), f.message))
56        .collect();
57    if !errors.is_empty() {
58        return Err(PlannerError::InvalidGraph {
59            reason: format!("lint errors: {}", errors.join("; ")),
60        });
61    }
62    // Lint's message wins when it already found errors; validation catches
63    // anything lint does not check.
64    crate::planner::validate_graph(graph)?;
65    let cpm = compute_cpm(graph)?;
66    let leveled = req
67        .schedule
68        .as_ref()
69        .map(|s| resource_schedule_with_cpm(graph, s, &cpm))
70        .transpose()?;
71    let summary = req
72        .monte_carlo
73        .as_ref()
74        .map(|m| monte_carlo_with_cpm(graph, m, &cpm))
75        .transpose()?;
76    let mut card = scorecard(graph, &cpm, leveled.as_ref(), &lint_report);
77    card.monte_carlo = summary;
78    Ok(SimulationResult {
79        lint: lint_report,
80        critical_path: cpm.critical_path.clone(),
81        critical_path_hours: cpm.critical_path_duration,
82        schedule: schedule_rows(graph, &cpm),
83        milestones: milestone_rows(graph, &cpm, |_| false),
84        resource_schedule: leveled,
85        scorecard: card,
86    })
87}