Skip to main content

dpm_engine/
schedule_view.rs

1//! The shared read-only schedule projection: the deterministic remaining-work schedule, package
2//! spans and the seeded simulation's criticality and percentiles, as every client draws them.
3
4use crate::EngineError;
5use chrono::{DateTime, Utc};
6use dpm_model::{
7    Applicability, DependencyId, DependencyKind, Key, Plan, Priority, Timeline, WorkItem,
8    WorkItemId, WorkKind, WorkStatus,
9};
10use dpm_schedule::{
11    ActivityCalendar, ActivitySchedule, Schedule, SimulationSummary, deterministic_remaining_at,
12    simulate_remaining_at,
13};
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17mod span;
18pub use span::{ScheduleSpan, schedule_span};
19
20#[cfg(test)]
21mod tests;
22
23/// Critical-path times of one work item the active graph schedules, in elapsed hours after the
24/// evaluation time.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct ScheduledTimes {
27    /// Minimum feasible start.
28    pub earliest_start_hours: f64,
29    /// Earliest start plus duration.
30    pub earliest_finish_hours: f64,
31    /// Latest start that does not delay project completion.
32    pub latest_start_hours: f64,
33    /// Latest finish that does not delay project completion.
34    pub latest_finish_hours: f64,
35    /// Delay available without postponing project completion.
36    pub total_float_hours: f64,
37    /// Delay available without moving a direct successor's earliest start.
38    pub free_float_hours: f64,
39    /// Whether total float is within numerical tolerance of zero.
40    pub critical: bool,
41}
42
43/// One row of the schedule projection.
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct ScheduledWork {
46    /// Stable identity.
47    pub id: WorkItemId,
48    /// Human-readable key.
49    pub key: Key,
50    /// Task, work package or milestone.
51    pub kind: WorkKind,
52    /// Containing work package.
53    pub parent: Option<WorkItemId>,
54    /// Title.
55    pub title: String,
56    /// Lifecycle status, with aggregate completion reflected for containers and milestones.
57    pub status: WorkStatus,
58    /// Whether the work is in the active graph, and why not.
59    pub applicability: Applicability,
60    /// Human priority; separate from criticality.
61    pub priority: Priority,
62    /// What the Gantt draws: the activity's own range, or a package's applicable descendants;
63    /// absent for work outside the active graph.
64    pub span: Option<ScheduleSpan>,
65    /// Critical-path times; a package with applicable descendants reports the enclosing bounds and
66    /// least float of those still outstanding. Absent for work outside the active graph and for a
67    /// package whose work is all complete.
68    pub times: Option<ScheduledTimes>,
69    /// Fraction of simulated schedules in which the work is critical, or for a package the
70    /// largest fraction among its descendants; absent (never zero) when
71    /// the projection is deterministic only, while open choices make it meaningless, or for work
72    /// outside the active graph.
73    pub criticality: Option<f64>,
74    /// Calendar placement, present only when the plan has calendars.
75    pub calendar: Option<ActivityCalendar>,
76}
77
78/// Seeded Monte Carlo completion percentiles, in elapsed hours.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct ScheduleUncertainty {
81    /// Number of sampled schedules.
82    pub iterations: usize,
83    /// Pseudo-random seed.
84    pub seed: u64,
85    /// Median completion.
86    pub p50_finish_hours: f64,
87    /// 80th percentile completion.
88    pub p80_finish_hours: f64,
89    /// 95th percentile completion.
90    pub p95_finish_hours: f64,
91}
92
93/// The authoritative remaining-work schedule projection of one snapshot at one clock reading.
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct ScheduleProjection {
96    /// Deterministic remaining duration of the active graph, in elapsed hours.
97    pub project_finish_hours: f64,
98    /// Percentiles; absent for a deterministic-only request, while open choices make them
99    /// meaningless, or when no work has an estimate.
100    pub uncertainty: Option<ScheduleUncertainty>,
101    /// Every work item in key order.
102    pub work: Vec<ScheduledWork>,
103    /// Constraints that still bound outstanding work, in plan order; satisfied, waived and
104    /// inapplicable ones are absent.
105    pub relations: Vec<ScheduledRelation>,
106}
107
108/// One outstanding constraint and how tightly it binds its successor.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub struct ScheduledRelation {
111    /// Stable dependency identity.
112    pub id: DependencyId,
113    /// Work providing the constrained event.
114    pub predecessor: WorkItemId,
115    /// Work whose event is bounded.
116    pub successor: WorkItemId,
117    /// FS, SS, FF or SF.
118    pub kind: DependencyKind,
119    /// How far the successor's start lies past what this constraint alone requires; zero when it
120    /// drives.
121    pub slack_hours: f64,
122    /// This constraint sets its successor's earliest start, or on calendars holds its finish.
123    pub driving: bool,
124    /// Driving between two critical activities, so it lies on a critical path.
125    pub critical: bool,
126}
127
128/// Project the remaining schedule; `probabilistic` adds the simulation `status` and `next` use.
129pub fn schedule_projection(
130    plan: &Plan,
131    probabilistic: bool,
132    now: DateTime<Utc>,
133) -> Result<ScheduleProjection, EngineError> {
134    plan.validate()?;
135    let timeline = Timeline::at(plan, now);
136    let schedule = deterministic_remaining_at(plan, &timeline)?;
137    let config = crate::query::simulation_config();
138    let simulation = if probabilistic
139        && !crate::query::choices::has_open_choices(plan)
140        && plan
141            .work_items
142            .values()
143            .any(|w| w.schedule.estimate.is_some())
144    {
145        Some(simulate_remaining_at(plan, config, &timeline)?)
146    } else {
147        None
148    };
149    let spans = span::spans(plan, &timeline, &schedule);
150    let leaves = span::package_leaves(plan, &timeline, &schedule);
151    let mut work: Vec<ScheduledWork> = plan
152        .work_items
153        .values()
154        .map(|item| {
155            let mut scheduled = row(&timeline, &schedule, simulation.as_ref(), &spans, item);
156            if let Some(leaves) = leaves.get(&item.id) {
157                roll_up(&mut scheduled, &schedule, simulation.as_ref(), leaves);
158            }
159            scheduled
160        })
161        .collect();
162    work.sort_by(|a, b| a.key.natural_cmp(&b.key));
163    Ok(ScheduleProjection {
164        project_finish_hours: schedule.project_finish_hours,
165        uncertainty: simulation.map(|s| ScheduleUncertainty {
166            iterations: s.iterations,
167            seed: config.seed,
168            p50_finish_hours: s.p50_finish_hours,
169            p80_finish_hours: s.p80_finish_hours,
170            p95_finish_hours: s.p95_finish_hours,
171        }),
172        work,
173        relations: relations(plan, &schedule),
174    })
175}
176
177fn relations(plan: &Plan, schedule: &Schedule) -> Vec<ScheduledRelation> {
178    plan.dependencies
179        .iter()
180        .filter_map(|d| {
181            let r = schedule.relations.get(&d.id)?;
182            Some(ScheduledRelation {
183                id: d.id,
184                predecessor: d.predecessor,
185                successor: d.successor,
186                kind: d.kind,
187                slack_hours: r.slack_hours,
188                driving: r.driving,
189                critical: r.critical,
190            })
191        })
192        .collect()
193}
194
195/// A package reports its descendants' bounds, least float, and the criticality of its most
196/// critical descendant, never the meaningless float of its own isolated node.
197fn roll_up(
198    scheduled: &mut ScheduledWork,
199    schedule: &Schedule,
200    simulation: Option<&SimulationSummary>,
201    leaves: &[WorkItemId],
202) {
203    scheduled.times = span::rolled_up(schedule, leaves).map(|a| times(&a));
204    scheduled.criticality = simulation.and_then(|s| {
205        leaves
206            .iter()
207            .filter_map(|id| s.criticality.get(id).copied())
208            .reduce(f64::max)
209    });
210}
211
212fn times(a: &ActivitySchedule) -> ScheduledTimes {
213    ScheduledTimes {
214        earliest_start_hours: a.earliest_start_hours,
215        earliest_finish_hours: a.earliest_finish_hours,
216        latest_start_hours: a.latest_start_hours,
217        latest_finish_hours: a.latest_finish_hours,
218        total_float_hours: a.total_float_hours,
219        free_float_hours: a.free_float_hours,
220        critical: a.critical,
221    }
222}
223
224fn row(
225    timeline: &Timeline,
226    schedule: &Schedule,
227    simulation: Option<&SimulationSummary>,
228    spans: &BTreeMap<WorkItemId, ScheduleSpan>,
229    item: &WorkItem,
230) -> ScheduledWork {
231    let applicability = timeline.applicability(item.id);
232    let activity = schedule
233        .activities
234        .get(&item.id)
235        .filter(|_| applicability.is_applicable());
236    let mut status = item.execution.status;
237    if !item.is_executable() && timeline.completed_at(item.id).is_some() {
238        status = WorkStatus::Verified;
239    }
240    ScheduledWork {
241        id: item.id,
242        key: item.key.clone(),
243        kind: item.kind,
244        parent: item.parent,
245        title: item.title.clone(),
246        status,
247        applicability: applicability.clone(),
248        priority: item.schedule.priority,
249        span: spans.get(&item.id).copied(),
250        times: activity.map(times),
251        criticality: simulation.and_then(|s| s.criticality.get(&item.id).copied()),
252        calendar: activity.and_then(|a| a.calendar.clone()),
253    }
254}