use super::simulation_config;
use crate::EngineError;
use chrono::{DateTime, Utc};
use dpm_model::{Applicability, DecisionId, DecisionStatus, Key, Plan, Timeline};
use dpm_schedule::{RemainingOptions, deterministic_remaining_with, simulate_remaining_with};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const MAX_SCENARIOS: usize = 16;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InapplicableWork {
pub key: Key,
pub applicability: Applicability,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OpenChoices {
pub decisions: Vec<Key>,
pub scenario_count: usize,
pub scenarios: Vec<ScenarioForecast>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScenarioForecast {
pub choices: BTreeMap<Key, String>,
pub expected_finish_hours: f64,
pub p50_finish_hours: Option<f64>,
pub p80_finish_hours: Option<f64>,
pub p95_finish_hours: Option<f64>,
pub stranded: Vec<Key>,
#[serde(default)]
pub unestimated: Vec<Key>,
}
pub(crate) fn not_applicable(plan: &Plan, timeline: &Timeline) -> Vec<InapplicableWork> {
let mut work: Vec<_> = plan
.work_items
.values()
.filter(|w| !timeline.applicability(w.id).is_applicable())
.map(|w| InapplicableWork {
key: w.key.clone(),
applicability: timeline.applicability(w.id).clone(),
})
.collect();
work.sort_by(|a, b| a.key.natural_cmp(&b.key));
work
}
fn open_decisions(plan: &Plan) -> Vec<(DecisionId, Key, Vec<String>)> {
let ids: BTreeSet<_> = plan
.work_items
.values()
.filter_map(|w| w.contract.condition.as_ref())
.filter_map(|c| plan.effective_decision(c.decision))
.filter(|d| d.status == DecisionStatus::Open)
.map(|d| d.id)
.collect();
let mut open: Vec<_> = ids
.into_iter()
.filter_map(|id| plan.decisions.get(&id))
.map(|d| {
let options = d.options.iter().map(|o| o.key.clone()).collect();
(d.id, d.key.clone(), options)
})
.collect();
open.sort_by(|a, b| a.1.cmp(&b.1));
open
}
pub(crate) fn has_open_choices(plan: &Plan) -> bool {
!open_decisions(plan).is_empty()
}
pub(crate) fn open_choices(
plan: &Plan,
probabilistic: bool,
now: DateTime<Utc>,
options: RemainingOptions,
) -> Result<Option<OpenChoices>, EngineError> {
let open = open_decisions(plan);
if open.is_empty() {
return Ok(None);
}
let scenario_count = open.iter().fold(1_usize, |n, (_, _, options)| {
n.saturating_mul(options.len())
});
let mut scenarios = Vec::new();
if scenario_count <= MAX_SCENARIOS {
for index in 0..scenario_count {
let mut rest = index;
let choices: Vec<_> = open
.iter()
.map(|(id, key, options)| {
let count = options.len().max(1);
let option = options.get(rest % count).cloned().unwrap_or_default();
rest /= count;
(*id, key.clone(), option)
})
.collect();
scenarios.push(forecast(plan, &choices, (probabilistic, options), now)?);
}
}
Ok(Some(OpenChoices {
decisions: open.into_iter().map(|(_, key, _)| key).collect(),
scenario_count,
scenarios,
}))
}
fn forecast(
plan: &Plan,
choices: &[(DecisionId, Key, String)],
(probabilistic, options): (bool, RemainingOptions),
now: DateTime<Utc>,
) -> Result<ScenarioForecast, EngineError> {
let mut hypothetical = plan.clone();
for (id, _, option) in choices {
if let Some(decision) = hypothetical.decisions.get_mut(id) {
decision.status = DecisionStatus::Decided;
decision.outcome = Some(option.clone());
decision.resolved_at = Some(now);
}
}
let timeline = Timeline::at(&hypothetical, now);
let schedule = deterministic_remaining_with(&hypothetical, &timeline, options)?;
let simulation = if probabilistic
&& hypothetical
.work_items
.values()
.any(|w| w.schedule.estimate.is_some())
{
Some(simulate_remaining_with(
&hypothetical,
simulation_config(),
&timeline,
options,
)?)
} else {
None
};
let mut stranded: Vec<_> = hypothetical
.applicability()
.into_iter()
.filter(|(_, a)| {
matches!(
a,
Applicability::Stranded { .. }
| Applicability::ChildrenStranded { .. }
| Applicability::EmptyJoin
)
})
.filter_map(|(id, _)| hypothetical.work_items.get(&id).map(|w| w.key.clone()))
.collect();
stranded.sort_by(dpm_model::Key::natural_cmp);
Ok(ScenarioForecast {
choices: choices
.iter()
.map(|(_, key, option)| (key.clone(), option.clone()))
.collect(),
expected_finish_hours: schedule.project_finish_hours,
p50_finish_hours: simulation.as_ref().map(|s| s.p50_finish_hours),
p80_finish_hours: simulation.as_ref().map(|s| s.p80_finish_hours),
p95_finish_hours: simulation.as_ref().map(|s| s.p95_finish_hours),
stranded,
unestimated: super::unestimated(&hypothetical, &timeline),
})
}