1use super::simulation_config;
5use crate::EngineError;
6use chrono::{DateTime, Utc};
7use dpm_model::{Applicability, DecisionId, DecisionStatus, Key, Plan, Timeline};
8use dpm_schedule::{RemainingOptions, deterministic_remaining_with, simulate_remaining_with};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12pub const MAX_SCENARIOS: usize = 16;
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct InapplicableWork {
18 pub key: Key,
20 pub applicability: Applicability,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct OpenChoices {
27 pub decisions: Vec<Key>,
29 pub scenario_count: usize,
31 pub scenarios: Vec<ScenarioForecast>,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct ScenarioForecast {
40 pub choices: BTreeMap<Key, String>,
42 pub expected_finish_hours: f64,
44 pub p50_finish_hours: Option<f64>,
46 pub p80_finish_hours: Option<f64>,
48 pub p95_finish_hours: Option<f64>,
50 pub stranded: Vec<Key>,
52 #[serde(default)]
54 pub unestimated: Vec<Key>,
55}
56
57pub(crate) fn not_applicable(plan: &Plan, timeline: &Timeline) -> Vec<InapplicableWork> {
58 let mut work: Vec<_> = plan
59 .work_items
60 .values()
61 .filter(|w| !timeline.applicability(w.id).is_applicable())
62 .map(|w| InapplicableWork {
63 key: w.key.clone(),
64 applicability: timeline.applicability(w.id).clone(),
65 })
66 .collect();
67 work.sort_by(|a, b| a.key.natural_cmp(&b.key));
68 work
69}
70
71fn open_decisions(plan: &Plan) -> Vec<(DecisionId, Key, Vec<String>)> {
73 let ids: BTreeSet<_> = plan
74 .work_items
75 .values()
76 .filter_map(|w| w.contract.condition.as_ref())
77 .filter_map(|c| plan.effective_decision(c.decision))
78 .filter(|d| d.status == DecisionStatus::Open)
79 .map(|d| d.id)
80 .collect();
81 let mut open: Vec<_> = ids
82 .into_iter()
83 .filter_map(|id| plan.decisions.get(&id))
84 .map(|d| {
85 let options = d.options.iter().map(|o| o.key.clone()).collect();
86 (d.id, d.key.clone(), options)
87 })
88 .collect();
89 open.sort_by(|a, b| a.1.cmp(&b.1));
90 open
91}
92
93pub(crate) fn has_open_choices(plan: &Plan) -> bool {
95 !open_decisions(plan).is_empty()
96}
97
98pub(crate) fn open_choices(
100 plan: &Plan,
101 probabilistic: bool,
102 now: DateTime<Utc>,
103 options: RemainingOptions,
104) -> Result<Option<OpenChoices>, EngineError> {
105 let open = open_decisions(plan);
106 if open.is_empty() {
107 return Ok(None);
108 }
109 let scenario_count = open.iter().fold(1_usize, |n, (_, _, options)| {
110 n.saturating_mul(options.len())
111 });
112 let mut scenarios = Vec::new();
113 if scenario_count <= MAX_SCENARIOS {
114 for index in 0..scenario_count {
115 let mut rest = index;
116 let choices: Vec<_> = open
117 .iter()
118 .map(|(id, key, options)| {
119 let count = options.len().max(1);
120 let option = options.get(rest % count).cloned().unwrap_or_default();
121 rest /= count;
122 (*id, key.clone(), option)
123 })
124 .collect();
125 scenarios.push(forecast(plan, &choices, (probabilistic, options), now)?);
126 }
127 }
128 Ok(Some(OpenChoices {
129 decisions: open.into_iter().map(|(_, key, _)| key).collect(),
130 scenario_count,
131 scenarios,
132 }))
133}
134
135fn forecast(
136 plan: &Plan,
137 choices: &[(DecisionId, Key, String)],
138 (probabilistic, options): (bool, RemainingOptions),
139 now: DateTime<Utc>,
140) -> Result<ScenarioForecast, EngineError> {
141 let mut hypothetical = plan.clone();
142 for (id, _, option) in choices {
143 if let Some(decision) = hypothetical.decisions.get_mut(id) {
144 decision.status = DecisionStatus::Decided;
145 decision.outcome = Some(option.clone());
146 decision.resolved_at = Some(now);
147 }
148 }
149 let timeline = Timeline::at(&hypothetical, now);
150 let schedule = deterministic_remaining_with(&hypothetical, &timeline, options)?;
151 let simulation = if probabilistic
152 && hypothetical
153 .work_items
154 .values()
155 .any(|w| w.schedule.estimate.is_some())
156 {
157 Some(simulate_remaining_with(
158 &hypothetical,
159 simulation_config(),
160 &timeline,
161 options,
162 )?)
163 } else {
164 None
165 };
166 let mut stranded: Vec<_> = hypothetical
167 .applicability()
168 .into_iter()
169 .filter(|(_, a)| {
170 matches!(
171 a,
172 Applicability::Stranded { .. }
173 | Applicability::ChildrenStranded { .. }
174 | Applicability::EmptyJoin
175 )
176 })
177 .filter_map(|(id, _)| hypothetical.work_items.get(&id).map(|w| w.key.clone()))
178 .collect();
179 stranded.sort_by(dpm_model::Key::natural_cmp);
180 Ok(ScenarioForecast {
181 choices: choices
182 .iter()
183 .map(|(_, key, option)| (key.clone(), option.clone()))
184 .collect(),
185 expected_finish_hours: schedule.project_finish_hours,
186 p50_finish_hours: simulation.as_ref().map(|s| s.p50_finish_hours),
187 p80_finish_hours: simulation.as_ref().map(|s| s.p80_finish_hours),
188 p95_finish_hours: simulation.as_ref().map(|s| s.p95_finish_hours),
189 stranded,
190 unestimated: super::unestimated(&hypothetical, &timeline),
191 })
192}