use crate::network::{EPSILON, Network};
use crate::placement::Placement;
use crate::remaining_duration::RemainingDuration;
use crate::{
ActivityCalendar, ActivitySchedule, RelationSchedule, RemainingOptions, Schedule, ScheduleError,
};
use dpm_model::{Dependency, Plan, Release, WorkItemId};
use std::collections::{BTreeMap, BTreeSet};
type Times = BTreeMap<WorkItemId, f64>;
pub fn deterministic(plan: &Plan) -> Result<Schedule, ScheduleError> {
let durations = plan
.work_items
.iter()
.map(|(id, work)| (*id, work.expected_duration_hours()))
.collect();
deterministic_with_durations(plan, &durations)
}
pub fn deterministic_remaining(
plan: &Plan,
now: chrono::DateTime<chrono::Utc>,
) -> Result<Schedule, ScheduleError> {
deterministic_remaining_at(plan, &dpm_model::Timeline::at(plan, now))
}
pub fn deterministic_remaining_at(
plan: &Plan,
timeline: &dpm_model::Timeline,
) -> Result<Schedule, ScheduleError> {
deterministic_remaining_with(plan, timeline, RemainingOptions::default())
}
pub fn deterministic_remaining_with(
plan: &Plan,
timeline: &dpm_model::Timeline,
options: RemainingOptions,
) -> Result<Schedule, ScheduleError> {
options.validate()?;
plan.validate()?;
let remaining = remaining_plan_at(plan, timeline)?;
let durations = remaining
.durations()?
.into_iter()
.map(|(id, duration)| (id, duration.expected()))
.collect();
let network = Network::compile(&remaining.plan)?;
let dense = dense(&network, &durations)?;
let mut placement = remaining.placement(&network, timeline, &dense, options)?;
let mut schedule = project(&network, &dense, placement.as_mut())?;
schedule
.activities
.retain(|id, _| !remaining.excluded.contains(id));
schedule
.critical_activities
.retain(|id| !remaining.excluded.contains(id));
Ok(schedule)
}
pub(crate) struct Remaining {
pub(crate) plan: Plan,
pub(crate) excluded: BTreeSet<WorkItemId>,
pub(crate) elapsed: BTreeMap<WorkItemId, f64>,
pub(crate) awaiting: BTreeSet<WorkItemId>,
}
impl Remaining {
pub(crate) fn durations(&self) -> Result<Vec<(WorkItemId, RemainingDuration)>, ScheduleError> {
self.plan
.work_items
.iter()
.map(|(id, work)| {
let done = work.execution.status.satisfies_dependency();
let elapsed = self.elapsed.get(id).copied().unwrap_or(0.0);
Ok((*id, RemainingDuration::of(*id, work, done, elapsed)?))
})
.collect()
}
pub(crate) fn placement(
&self,
network: &Network,
timeline: &dpm_model::Timeline,
durations: &[f64],
options: RemainingOptions,
) -> Result<Option<Placement>, ScheduleError> {
let work: f64 = durations.iter().sum();
let lags: f64 = self
.plan
.dependencies
.iter()
.map(|d| d.lag_hours.abs())
.sum();
let reviews = options.review_delay_hours * self.awaiting.len() as f64;
Placement::new(
&self.plan,
network,
timeline,
&self.awaiting,
5.0 * (work + lags + reviews),
options,
)
}
}
fn elapsed_since_start(
plan: &Plan,
timeline: &dpm_model::Timeline,
excluded: &BTreeSet<WorkItemId>,
) -> Result<BTreeMap<WorkItemId, f64>, ScheduleError> {
plan.work_items
.iter()
.filter(|(id, work)| {
work.is_executable()
&& !excluded.contains(id)
&& !work.execution.status.satisfies_dependency()
})
.filter_map(|(id, work)| match work.start_event()? {
dpm_model::EventTime::Recorded(at) => Some(
crate::placement::worked_between(plan, work, at, timeline.now())
.map(|hours| (*id, hours)),
),
dpm_model::EventTime::Unrecorded => None,
})
.collect()
}
pub(crate) fn remaining_plan_at(
plan: &Plan,
timeline: &dpm_model::Timeline,
) -> Result<Remaining, ScheduleError> {
let mut remaining = plan.clone();
let now = timeline.now();
let completed = timeline.completed();
let excluded: BTreeSet<_> = plan
.work_items
.keys()
.filter(|id| !timeline.applicability(**id).is_applicable())
.copied()
.collect();
for id in &excluded {
if let Some(work) = remaining.work_items.get_mut(id) {
work.schedule.estimate = None;
}
}
remaining.dependencies = plan
.enforced_dependencies()
.filter(|d| !excluded.contains(&d.predecessor) && !excluded.contains(&d.successor))
.filter(|d| !completed.contains(&d.successor))
.filter_map(|d| {
if !completed.contains(&d.predecessor) && !started_from(plan, timeline, d) {
return Some(d.clone());
}
let (lag_hours, lag_basis) = match timeline.edge(plan, d) {
Release::Released { .. }
| Release::SkippedBranch { .. }
| Release::AwaitingEvent => return None,
Release::Elapsing { opens_at, .. } => (
(opens_at - now).num_milliseconds() as f64 / 3_600_000.0,
dpm_model::LagBasis::Elapsed,
),
Release::UnrecordedEventTime
| Release::LagOutOfRange { .. }
| Release::NotSelected => (d.lag_hours, d.lag_basis),
};
Some(Dependency {
lag_hours,
lag_basis,
..d.clone()
})
})
.collect();
let awaiting = plan
.work_items
.values()
.filter(|w| w.is_executable() && !w.execution.status.satisfies_dependency())
.filter(|w| !excluded.contains(&w.id))
.map(|w| w.id)
.collect();
Ok(Remaining {
awaiting,
elapsed: elapsed_since_start(plan, timeline, &excluded)?,
plan: remaining,
excluded,
})
}
fn started_from(plan: &Plan, timeline: &dpm_model::Timeline, edge: &Dependency) -> bool {
edge.kind.predecessor_endpoint() == dpm_model::Endpoint::Start
&& timeline
.event(plan, edge.predecessor, dpm_model::Endpoint::Start)
.is_some()
}
pub fn deterministic_with_durations(
plan: &Plan,
durations: &Times,
) -> Result<Schedule, ScheduleError> {
let network = Network::compile(plan)?;
let dense = dense(&network, durations)?;
project(&network, &dense, None)
}
fn dense(network: &Network, durations: &Times) -> Result<Vec<f64>, ScheduleError> {
let mut dense = Vec::with_capacity(network.order().len());
for id in network.order() {
let duration = durations
.get(id)
.ok_or(ScheduleError::InvalidDuration(*id))?;
if !duration.is_finite() || *duration < 0.0 {
return Err(ScheduleError::InvalidDuration(*id));
}
dense.push(*duration);
}
Ok(dense)
}
fn project(
network: &Network,
dense: &[f64],
mut placement: Option<&mut Placement>,
) -> Result<Schedule, ScheduleError> {
let times = match placement.as_deref_mut() {
Some(placement) => network.times_placed(dense, placement)?,
None => network.times(dense)?,
};
let placement = placement.as_deref();
let mut activities = BTreeMap::new();
let mut critical_activities = Vec::new();
for (at, id) in network.order().iter().enumerate() {
let read = |values: &[f64]| {
values
.get(at)
.copied()
.ok_or(ScheduleError::UnknownPosition(at))
};
let total_float = network.total_float(×, at)?;
let critical = total_float <= EPSILON;
if critical {
critical_activities.push(*id);
}
let (earliest_finish, latest_finish, review_wait) = times.finishes(network, dense, at)?;
let calendar =
placement
.and_then(|p| p.resolved.get(id))
.map(|resolved| ActivityCalendar {
resolved: resolved.clone(),
review_wait_hours: review_wait,
});
activities.insert(
*id,
ActivitySchedule {
earliest_start_hours: read(×.earliest)?,
earliest_finish_hours: earliest_finish,
latest_start_hours: read(×.latest)?,
latest_finish_hours: latest_finish,
total_float_hours: total_float,
free_float_hours: network
.free_float(dense, ×, at, placement)?
.min(total_float),
critical,
calendar,
},
);
}
let critical_at = |at: usize| {
network
.order()
.get(at)
.and_then(|id| activities.get(id))
.is_some_and(|a: &ActivitySchedule| a.critical)
};
let relations = network
.relation_slacks(dense, ×, placement)?
.into_iter()
.map(|(id, from, to, slack_hours)| {
let driving = slack_hours <= EPSILON;
let critical = driving && critical_at(from) && critical_at(to);
let relation = RelationSchedule {
slack_hours,
driving,
critical,
};
(id, relation)
})
.collect();
Ok(Schedule {
project_finish_hours: times.finish,
activities,
critical_activities,
relations,
})
}
#[cfg(test)]
mod tests;