1use crate::network::{EPSILON, Network};
2use crate::placement::Placement;
3use crate::remaining_duration::RemainingDuration;
4use crate::{
5 ActivityCalendar, ActivitySchedule, RelationSchedule, RemainingOptions, Schedule, ScheduleError,
6};
7use dpm_model::{Dependency, Plan, Release, WorkItemId};
8use std::collections::{BTreeMap, BTreeSet};
9
10type Times = BTreeMap<WorkItemId, f64>;
11
12pub fn deterministic(plan: &Plan) -> Result<Schedule, ScheduleError> {
14 let durations = plan
15 .work_items
16 .iter()
17 .map(|(id, work)| (*id, work.expected_duration_hours()))
18 .collect();
19 deterministic_with_durations(plan, &durations)
20}
21
22pub fn deterministic_remaining(
32 plan: &Plan,
33 now: chrono::DateTime<chrono::Utc>,
34) -> Result<Schedule, ScheduleError> {
35 deterministic_remaining_at(plan, &dpm_model::Timeline::at(plan, now))
36}
37
38pub fn deterministic_remaining_at(
40 plan: &Plan,
41 timeline: &dpm_model::Timeline,
42) -> Result<Schedule, ScheduleError> {
43 deterministic_remaining_with(plan, timeline, RemainingOptions::default())
44}
45
46pub fn deterministic_remaining_with(
49 plan: &Plan,
50 timeline: &dpm_model::Timeline,
51 options: RemainingOptions,
52) -> Result<Schedule, ScheduleError> {
53 options.validate()?;
54 plan.validate()?;
55 let remaining = remaining_plan_at(plan, timeline)?;
56 let durations = remaining
57 .durations()?
58 .into_iter()
59 .map(|(id, duration)| (id, duration.expected()))
60 .collect();
61 let network = Network::compile(&remaining.plan)?;
62 let dense = dense(&network, &durations)?;
63 let mut placement = remaining.placement(&network, timeline, &dense, options)?;
64 let mut schedule = project(&network, &dense, placement.as_mut())?;
65 schedule
66 .activities
67 .retain(|id, _| !remaining.excluded.contains(id));
68 schedule
69 .critical_activities
70 .retain(|id| !remaining.excluded.contains(id));
71 Ok(schedule)
72}
73
74pub(crate) struct Remaining {
76 pub(crate) plan: Plan,
78 pub(crate) excluded: BTreeSet<WorkItemId>,
80 pub(crate) elapsed: BTreeMap<WorkItemId, f64>,
82 pub(crate) awaiting: BTreeSet<WorkItemId>,
84}
85
86impl Remaining {
87 pub(crate) fn durations(&self) -> Result<Vec<(WorkItemId, RemainingDuration)>, ScheduleError> {
89 self.plan
90 .work_items
91 .iter()
92 .map(|(id, work)| {
93 let done = work.execution.status.satisfies_dependency();
94 let elapsed = self.elapsed.get(id).copied().unwrap_or(0.0);
95 Ok((*id, RemainingDuration::of(*id, work, done, elapsed)?))
96 })
97 .collect()
98 }
99
100 pub(crate) fn placement(
104 &self,
105 network: &Network,
106 timeline: &dpm_model::Timeline,
107 durations: &[f64],
108 options: RemainingOptions,
109 ) -> Result<Option<Placement>, ScheduleError> {
110 let work: f64 = durations.iter().sum();
111 let lags: f64 = self
112 .plan
113 .dependencies
114 .iter()
115 .map(|d| d.lag_hours.abs())
116 .sum();
117 let reviews = options.review_delay_hours * self.awaiting.len() as f64;
118 Placement::new(
119 &self.plan,
120 network,
121 timeline,
122 &self.awaiting,
123 5.0 * (work + lags + reviews),
124 options,
125 )
126 }
127}
128
129fn elapsed_since_start(
136 plan: &Plan,
137 timeline: &dpm_model::Timeline,
138 excluded: &BTreeSet<WorkItemId>,
139) -> Result<BTreeMap<WorkItemId, f64>, ScheduleError> {
140 plan.work_items
141 .iter()
142 .filter(|(id, work)| {
143 work.is_executable()
144 && !excluded.contains(id)
145 && !work.execution.status.satisfies_dependency()
146 })
147 .filter_map(|(id, work)| match work.start_event()? {
148 dpm_model::EventTime::Recorded(at) => Some(
149 crate::placement::worked_between(plan, work, at, timeline.now())
150 .map(|hours| (*id, hours)),
151 ),
152 dpm_model::EventTime::Unrecorded => None,
153 })
154 .collect()
155}
156
157pub(crate) fn remaining_plan_at(
169 plan: &Plan,
170 timeline: &dpm_model::Timeline,
171) -> Result<Remaining, ScheduleError> {
172 let mut remaining = plan.clone();
173 let now = timeline.now();
174 let completed = timeline.completed();
175 let excluded: BTreeSet<_> = plan
176 .work_items
177 .keys()
178 .filter(|id| !timeline.applicability(**id).is_applicable())
179 .copied()
180 .collect();
181 for id in &excluded {
182 if let Some(work) = remaining.work_items.get_mut(id) {
183 work.schedule.estimate = None;
184 }
185 }
186 remaining.dependencies = plan
187 .enforced_dependencies()
188 .filter(|d| !excluded.contains(&d.predecessor) && !excluded.contains(&d.successor))
189 .filter(|d| !completed.contains(&d.successor))
190 .filter_map(|d| {
191 if !completed.contains(&d.predecessor) && !started_from(plan, timeline, d) {
192 return Some(d.clone());
193 }
194 let (lag_hours, lag_basis) = match timeline.edge(plan, d) {
196 Release::Released { .. }
197 | Release::SkippedBranch { .. }
198 | Release::AwaitingEvent => return None,
199 Release::Elapsing { opens_at, .. } => (
200 (opens_at - now).num_milliseconds() as f64 / 3_600_000.0,
201 dpm_model::LagBasis::Elapsed,
202 ),
203 Release::UnrecordedEventTime
204 | Release::LagOutOfRange { .. }
205 | Release::NotSelected => (d.lag_hours, d.lag_basis),
206 };
207 Some(Dependency {
208 lag_hours,
209 lag_basis,
210 ..d.clone()
211 })
212 })
213 .collect();
214 let awaiting = plan
215 .work_items
216 .values()
217 .filter(|w| w.is_executable() && !w.execution.status.satisfies_dependency())
218 .filter(|w| !excluded.contains(&w.id))
219 .map(|w| w.id)
220 .collect();
221 Ok(Remaining {
222 awaiting,
223 elapsed: elapsed_since_start(plan, timeline, &excluded)?,
224 plan: remaining,
225 excluded,
226 })
227}
228
229fn started_from(plan: &Plan, timeline: &dpm_model::Timeline, edge: &Dependency) -> bool {
235 edge.kind.predecessor_endpoint() == dpm_model::Endpoint::Start
236 && timeline
237 .event(plan, edge.predecessor, dpm_model::Endpoint::Start)
238 .is_some()
239}
240
241pub fn deterministic_with_durations(
245 plan: &Plan,
246 durations: &Times,
247) -> Result<Schedule, ScheduleError> {
248 let network = Network::compile(plan)?;
249 let dense = dense(&network, durations)?;
250 project(&network, &dense, None)
251}
252
253fn dense(network: &Network, durations: &Times) -> Result<Vec<f64>, ScheduleError> {
255 let mut dense = Vec::with_capacity(network.order().len());
256 for id in network.order() {
257 let duration = durations
258 .get(id)
259 .ok_or(ScheduleError::InvalidDuration(*id))?;
260 if !duration.is_finite() || *duration < 0.0 {
261 return Err(ScheduleError::InvalidDuration(*id));
262 }
263 dense.push(*duration);
264 }
265 Ok(dense)
266}
267
268fn project(
270 network: &Network,
271 dense: &[f64],
272 mut placement: Option<&mut Placement>,
273) -> Result<Schedule, ScheduleError> {
274 let times = match placement.as_deref_mut() {
275 Some(placement) => network.times_placed(dense, placement)?,
276 None => network.times(dense)?,
277 };
278 let placement = placement.as_deref();
279 let mut activities = BTreeMap::new();
280 let mut critical_activities = Vec::new();
281 for (at, id) in network.order().iter().enumerate() {
282 let read = |values: &[f64]| {
283 values
284 .get(at)
285 .copied()
286 .ok_or(ScheduleError::UnknownPosition(at))
287 };
288 let total_float = network.total_float(×, at)?;
289 let critical = total_float <= EPSILON;
290 if critical {
291 critical_activities.push(*id);
292 }
293 let (earliest_finish, latest_finish, review_wait) = times.finishes(network, dense, at)?;
294 let calendar =
295 placement
296 .and_then(|p| p.resolved.get(id))
297 .map(|resolved| ActivityCalendar {
298 resolved: resolved.clone(),
299 review_wait_hours: review_wait,
300 });
301 activities.insert(
302 *id,
303 ActivitySchedule {
304 earliest_start_hours: read(×.earliest)?,
305 earliest_finish_hours: earliest_finish,
306 latest_start_hours: read(×.latest)?,
307 latest_finish_hours: latest_finish,
308 total_float_hours: total_float,
309 free_float_hours: network
310 .free_float(dense, ×, at, placement)?
311 .min(total_float),
312 critical,
313 calendar,
314 },
315 );
316 }
317 let critical_at = |at: usize| {
318 network
319 .order()
320 .get(at)
321 .and_then(|id| activities.get(id))
322 .is_some_and(|a: &ActivitySchedule| a.critical)
323 };
324 let relations = network
325 .relation_slacks(dense, ×, placement)?
326 .into_iter()
327 .map(|(id, from, to, slack_hours)| {
328 let driving = slack_hours <= EPSILON;
329 let critical = driving && critical_at(from) && critical_at(to);
330 let relation = RelationSchedule {
331 slack_hours,
332 driving,
333 critical,
334 };
335 (id, relation)
336 })
337 .collect();
338 Ok(Schedule {
339 project_finish_hours: times.finish,
340 activities,
341 critical_activities,
342 relations,
343 })
344}
345
346#[cfg(test)]
347mod tests;