1use crate::task::{Bottleneck, CriticalPathResult, Task, TaskBatch};
13use std::collections::{HashMap, HashSet, VecDeque};
14
15pub const CPM_VERSION: i64 = 3;
24
25const TIGHT_EPS: f32 = 1e-3;
26
27pub struct CpmAlgorithm;
29
30impl CpmAlgorithm {
31 pub fn calculate(tasks: &mut [Task]) -> CriticalPathResult {
39 if tasks.is_empty() {
40 return CriticalPathResult::default();
41 }
42
43 let mut ids: HashSet<&str> = HashSet::with_capacity(tasks.len());
47 if !tasks.iter().all(|t| ids.insert(t.id.as_str())) {
48 let mut unscheduled: Vec<String> = tasks.iter().map(|t| t.id.clone()).collect();
49 unscheduled.sort_unstable();
50 unscheduled.dedup();
51 return CriticalPathResult {
52 total_tasks: tasks.len(),
53 tasks: tasks.to_vec(),
54 unscheduled,
55 ..CriticalPathResult::default()
56 };
57 }
58
59 let (successors, _predecessors) = Self::build_dependency_graphs(tasks);
61
62 let (mut unscheduled, order) = Self::forward_pass(tasks);
66
67 let backward_unscheduled = Self::backward_pass(tasks, &successors, &order);
80 if unscheduled.is_empty() {
81 unscheduled = backward_unscheduled;
82 }
83 unscheduled.sort_unstable();
84 unscheduled.dedup();
85
86 Self::calculate_float(tasks);
88
89 let batches = Self::identify_parallel_batches(tasks);
91
92 let bottlenecks = Self::identify_bottlenecks(tasks);
94
95 Self::build_result(tasks, batches, bottlenecks, unscheduled)
97 }
98
99 fn build_dependency_graphs(
101 tasks: &[Task],
102 ) -> (HashMap<String, Vec<String>>, HashMap<String, Vec<String>>) {
103 let mut successors: HashMap<String, Vec<String>> = HashMap::new();
104 let mut predecessors: HashMap<String, Vec<String>> = HashMap::new();
105
106 for task in tasks {
108 successors.entry(task.id.clone()).or_default();
109 predecessors.entry(task.id.clone()).or_default();
110 }
111
112 for task in tasks {
114 for dep in &task.dependencies {
115 successors
119 .entry(dep.clone())
120 .or_default()
121 .push(task.id.clone());
122 predecessors
123 .entry(task.id.clone())
124 .or_default()
125 .push(dep.clone());
126 }
127 }
128
129 (successors, predecessors)
130 }
131
132 fn forward_pass(tasks: &mut [Task]) -> (Vec<String>, Vec<usize>) {
143 for task in tasks.iter_mut() {
145 task.earliest_start = 0.0;
146 task.earliest_finish = 0.0;
147 }
148 let task_count = tasks.len();
149 let ids: Vec<String> = tasks.iter().map(|t| t.id.clone()).collect();
150 let task_map: HashMap<&str, usize> = ids
151 .iter()
152 .enumerate()
153 .map(|(i, id)| (id.as_str(), i))
154 .collect();
155
156 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); task_count];
160 let mut in_degree: Vec<usize> = Vec::with_capacity(task_count);
161 for (i, task) in tasks.iter().enumerate() {
162 let distinct: HashSet<&str> = task.dependencies.iter().map(String::as_str).collect();
163 in_degree.push(distinct.len());
164 for dep in distinct {
165 if let Some(&d) = task_map.get(dep) {
166 dependents[d].push(i);
167 }
168 }
169 }
170 for list in &mut dependents {
171 list.sort_unstable();
172 }
173
174 let mut queue: VecDeque<usize> = (0..task_count)
176 .filter(|&i| tasks[i].dependencies.is_empty())
177 .collect();
178 for &i in &queue {
179 tasks[i].earliest_finish = tasks[i].effort_hours;
180 }
181
182 let mut order: Vec<usize> = Vec::with_capacity(task_count);
183 while let Some(current) = queue.pop_front() {
184 order.push(current);
185 let current_ef = tasks[current].earliest_finish;
186 for &i in &dependents[current] {
187 let task = &mut tasks[i];
188 let lag = task
190 .lag_by_dependency
191 .get(&ids[current])
192 .copied()
193 .unwrap_or(0.0);
194 if current_ef + lag > task.earliest_start {
195 task.earliest_start = current_ef + lag;
196 task.earliest_finish = task.earliest_start + task.effort_hours;
197 }
198 in_degree[i] -= 1;
199 if in_degree[i] == 0 {
200 task.earliest_finish = task.earliest_start + task.effort_hours;
202 queue.push_back(i);
203 }
204 }
205 }
206
207 if order.len() < task_count {
211 let mut scheduled = vec![false; task_count];
212 for &i in &order {
213 scheduled[i] = true;
214 }
215 let mut unscheduled: Vec<String> = (0..task_count)
216 .filter(|&i| !scheduled[i])
217 .map(|i| tasks[i].id.clone())
218 .collect();
219 unscheduled.sort_unstable();
220 tracing::warn!(
221 unscheduled = unscheduled.len(),
222 "CPM forward pass could not schedule all tasks (possible dependency cycle)"
223 );
224 return (unscheduled, order);
225 }
226
227 (Vec::new(), order)
228 }
229
230 fn backward_pass(
245 tasks: &mut [Task],
246 successors: &HashMap<String, Vec<String>>,
247 order: &[usize],
248 ) -> Vec<String> {
249 let project_duration = tasks
251 .iter()
252 .map(|t| t.earliest_finish)
253 .fold(0.0_f32, f32::max);
254
255 let task_map: HashMap<String, usize> = tasks
257 .iter()
258 .enumerate()
259 .map(|(i, t)| (t.id.clone(), i))
260 .collect();
261
262 for task in tasks.iter_mut() {
264 let has_successors = successors.get(&task.id).is_some_and(|s| !s.is_empty());
265 if has_successors {
266 task.latest_finish = f32::MAX;
268 task.latest_start = f32::MAX;
269 } else {
270 task.latest_finish = project_duration;
271 task.latest_start = project_duration - task.effort_hours;
272 }
273 }
274
275 let mut visit: Vec<usize> = order.iter().rev().copied().collect();
278 if visit.len() < tasks.len() {
279 let mut in_order = vec![false; tasks.len()];
280 for &i in order {
281 in_order[i] = true;
282 }
283 visit.extend((0..tasks.len()).filter(|&i| !in_order[i]));
284 }
285 let mut changed = true;
286 let mut iterations = 0;
287 let max_iterations = tasks.len() * 2;
288
289 while changed && iterations < max_iterations {
290 changed = false;
291 iterations += 1;
292
293 for &i in &visit {
294 let task_id = tasks[i].id.clone();
295 let task_effort = tasks[i].effort_hours;
296
297 if let Some(succ_ids) = successors.get(&task_id)
299 && !succ_ids.is_empty()
300 {
301 let min_succ_ls = succ_ids
302 .iter()
303 .filter_map(|sid| {
304 task_map.get(sid).map(|&idx| {
305 let lag = tasks[idx]
306 .lag_by_dependency
307 .get(&task_id)
308 .copied()
309 .unwrap_or(0.0);
310 if tasks[idx].latest_start < f32::MAX {
311 tasks[idx].latest_start - lag
312 } else {
313 f32::MAX
314 }
315 })
316 })
317 .filter(|&ls| ls < f32::MAX)
318 .fold(f32::MAX, f32::min);
319
320 if min_succ_ls < f32::MAX && min_succ_ls < tasks[i].latest_finish {
321 tasks[i].latest_finish = min_succ_ls;
322 tasks[i].latest_start = min_succ_ls - task_effort;
323 changed = true;
324 }
325 }
326 }
327 }
328
329 let mut unscheduled: Vec<String> = Vec::new();
337 for task in tasks.iter_mut() {
338 if task.latest_finish >= f32::MAX - 1.0 {
339 task.latest_finish = project_duration;
340 task.latest_start = project_duration - task.effort_hours;
341 unscheduled.push(task.id.clone());
342 }
343 }
344 unscheduled.sort_unstable();
345 unscheduled
346 }
347
348 fn calculate_float(tasks: &mut [Task]) {
350 for task in tasks.iter_mut() {
351 task.float = task.latest_start - task.earliest_start;
352 task.is_critical = task.float.abs() < 0.001;
354 }
355 }
356
357 fn identify_parallel_batches(tasks: &[Task]) -> Vec<TaskBatch> {
359 let mut batches_map: HashMap<i32, Vec<&Task>> = HashMap::new();
361
362 for task in tasks {
363 let es_key = (task.earliest_start * 10.0).round() as i32;
365 batches_map.entry(es_key).or_default().push(task);
366 }
367
368 let mut sorted_keys: Vec<i32> = batches_map.keys().copied().collect();
370 sorted_keys.sort_unstable();
371
372 let mut batches = Vec::new();
373 let mut batch_num = 0;
374
375 for es_key in sorted_keys {
376 if let Some(batch_tasks) = batches_map.get(&es_key) {
377 let total_effort: f32 = batch_tasks.iter().map(|t| t.effort_hours).sum();
378 let duration = batch_tasks
379 .iter()
380 .map(|t| t.effort_hours)
381 .fold(0.0_f32, f32::max);
382 let start_time = es_key as f32 / 10.0;
383
384 batches.push(TaskBatch {
385 id: format!("Batch-{batch_num}"),
386 tasks: batch_tasks.iter().map(|t| t.id.clone()).collect(),
387 total_effort_hours: total_effort,
388 duration_hours: duration,
389 start_time,
390 });
391 batch_num += 1;
392 }
393 }
394
395 batches
396 }
397
398 fn identify_bottlenecks(tasks: &[Task]) -> Vec<Bottleneck> {
402 let task_map: HashMap<&str, usize> = tasks
403 .iter()
404 .enumerate()
405 .map(|(i, t)| (t.id.as_str(), i))
406 .collect();
407 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); tasks.len()];
408 for (i, task) in tasks.iter().enumerate() {
409 for dep in &task.dependencies {
410 if let Some(&d) = task_map.get(dep.as_str()) {
411 dependents[d].push(i);
412 }
413 }
414 }
415
416 let mut bottlenecks = Vec::new();
417 let mut stamp: Vec<usize> = vec![usize::MAX; tasks.len()];
418 let mut stack: Vec<usize> = Vec::new();
419
420 for (root, task) in tasks.iter().enumerate() {
421 let mut blocks_count = 0;
423 let mut blocked_hours = 0.0_f32;
424 stack.clear();
425 stack.push(root);
426 while let Some(current) = stack.pop() {
427 for &succ in &dependents[current] {
428 if stamp[succ] != root {
429 stamp[succ] = root;
430 blocks_count += 1;
431 blocked_hours += tasks[succ].effort_hours;
432 stack.push(succ);
433 }
434 }
435 }
436
437 if blocks_count == 0 {
438 continue;
439 }
440
441 let roi = if task.effort_hours > 0.0 {
443 blocked_hours / task.effort_hours
444 } else {
445 0.0
446 };
447
448 bottlenecks.push(Bottleneck {
449 task_id: task.id.clone(),
450 task_name: task.name.clone(),
451 blocks_count,
452 blocked_hours,
453 roi,
454 effort_hours: task.effort_hours,
455 });
456 }
457
458 bottlenecks.sort_by(|a, b| {
460 b.roi
461 .partial_cmp(&a.roi)
462 .unwrap_or(std::cmp::Ordering::Equal)
463 });
464
465 bottlenecks
466 }
467
468 fn trace_critical_path(tasks: &[Task]) -> Vec<String> {
474 let Some(sink) = tasks.iter().max_by(|a, b| {
475 a.earliest_finish
476 .partial_cmp(&b.earliest_finish)
477 .unwrap_or(std::cmp::Ordering::Equal)
478 .then_with(|| {
479 a.earliest_start
480 .partial_cmp(&b.earliest_start)
481 .unwrap_or(std::cmp::Ordering::Equal)
482 })
483 .then_with(|| b.id.cmp(&a.id))
484 }) else {
485 return Vec::new();
486 };
487 Self::trace_back(tasks, sink)
488 }
489
490 pub(crate) fn trace_path_to(tasks: &[Task], end_id: &str) -> Vec<String> {
494 tasks
495 .iter()
496 .find(|t| t.id == end_id)
497 .map_or_else(Vec::new, |end| Self::trace_back(tasks, end))
498 }
499
500 fn trace_back(tasks: &[Task], sink: &Task) -> Vec<String> {
503 let by_id: HashMap<&str, &Task> = tasks.iter().map(|t| (t.id.as_str(), t)).collect();
504 let mut path = vec![sink.id.clone()];
505 let mut current = sink;
506 let mut seen: HashSet<&str> = HashSet::from([sink.id.as_str()]);
507 while path.len() <= tasks.len() {
508 let next = current
509 .dependencies
510 .iter()
511 .filter_map(|d| by_id.get(d.as_str()).copied())
512 .filter(|p| {
513 let lag = current.lag_by_dependency.get(&p.id).copied().unwrap_or(0.0);
514 (p.earliest_finish + lag - current.earliest_start).abs() < TIGHT_EPS
515 })
516 .filter(|p| !seen.contains(p.id.as_str()))
517 .min_by(|a, b| a.id.cmp(&b.id));
518 let Some(pred) = next else { break };
519 seen.insert(pred.id.as_str());
520 path.push(pred.id.clone());
521 current = pred;
522 }
523 path.reverse();
524 path
525 }
526
527 fn build_result(
529 tasks: &[Task],
530 batches: Vec<TaskBatch>,
531 bottlenecks: Vec<Bottleneck>,
532 unscheduled: Vec<String>,
533 ) -> CriticalPathResult {
534 let critical_path = Self::trace_critical_path(tasks);
535 let project_length = tasks
536 .iter()
537 .map(|t| t.earliest_finish)
538 .fold(0.0_f32, f32::max);
539
540 let mut critical_tasks: Vec<&Task> = tasks.iter().filter(|t| t.is_critical).collect();
541 critical_tasks.sort_by(|a, b| {
542 a.earliest_start
543 .partial_cmp(&b.earliest_start)
544 .unwrap_or(std::cmp::Ordering::Equal)
545 .then_with(|| a.id.cmp(&b.id))
546 });
547 let critical_ids: Vec<String> = critical_tasks.iter().map(|t| t.id.clone()).collect();
548
549 let total_duration_sequential: f32 = tasks.iter().map(|t| t.effort_hours).sum();
550 let optimal_duration_parallel = project_length;
551
552 let speedup_factor = if optimal_duration_parallel > 0.0 {
553 total_duration_sequential / optimal_duration_parallel
554 } else {
555 1.0
556 };
557
558 CriticalPathResult {
559 total_tasks: tasks.len(),
560 critical_path,
561 critical_ids,
562 critical_path_duration: project_length,
563 total_duration_sequential,
564 optimal_duration_parallel,
565 speedup_factor,
566 parallelizable_batches: batches,
567 bottlenecks,
568 tasks: tasks.to_vec(),
569 unscheduled,
570 }
571 }
572}
573
574#[cfg(test)]
575#[allow(clippy::float_cmp)]
576mod tests {
577 use super::*;
578
579 fn make_task(id: &str, effort: f32, deps: Vec<&str>) -> Task {
580 Task {
581 id: id.to_string(),
582 name: format!("Task {id}"),
583 effort_hours: effort,
584 dependencies: deps.into_iter().map(String::from).collect(),
585 ..Default::default()
586 }
587 }
588
589 #[test]
590 fn calculate_terminates_on_duplicate_ids() {
591 let mut tasks = vec![
593 make_task("a", 1.0, vec![]),
594 make_task("a", 1.0, vec!["b"]),
595 make_task("b", 1.0, vec!["a"]),
596 ];
597 let result = CpmAlgorithm::calculate(&mut tasks);
598 assert_eq!(result.unscheduled, vec!["a".to_string(), "b".to_string()]);
599 }
600
601 #[test]
602 fn test_simple_chain() {
603 let mut tasks = vec![
605 make_task("A", 2.0, vec![]),
606 make_task("B", 3.0, vec!["A"]),
607 make_task("C", 1.0, vec!["B"]),
608 ];
609
610 let result = CpmAlgorithm::calculate(&mut tasks);
611
612 assert_eq!(result.critical_path.len(), 3);
614 assert_eq!(result.critical_path_duration, 6.0);
615 assert!((result.speedup_factor - 1.0).abs() < 0.01);
617 }
618
619 #[test]
620 fn test_parallel_tasks() {
621 let mut tasks = vec![
624 make_task("A", 1.0, vec![]),
625 make_task("B", 2.0, vec!["A"]),
626 make_task("C", 3.0, vec!["A"]),
627 ];
628
629 let result = CpmAlgorithm::calculate(&mut tasks);
630
631 assert_eq!(result.critical_path_duration, 4.0);
633 assert_eq!(result.total_duration_sequential, 6.0);
635 assert!(result.speedup_factor > 1.0);
637 }
638
639 #[test]
640 fn test_diamond_dependency() {
641 let mut tasks = vec![
647 make_task("A", 1.0, vec![]),
648 make_task("B", 2.0, vec!["A"]),
649 make_task("C", 4.0, vec!["A"]),
650 make_task("D", 1.0, vec!["B", "C"]),
651 ];
652
653 let result = CpmAlgorithm::calculate(&mut tasks);
654
655 assert_eq!(result.critical_path_duration, 6.0);
657 assert!(!result.critical_path.contains(&"B".to_string()));
659 }
660
661 #[test]
662 fn test_bottleneck_identification() {
663 let mut tasks = vec![
665 make_task("A", 1.0, vec![]),
666 make_task("B", 5.0, vec!["A"]),
667 make_task("C", 5.0, vec!["A"]),
668 make_task("D", 5.0, vec!["A"]),
669 ];
670
671 let result = CpmAlgorithm::calculate(&mut tasks);
672
673 assert!(!result.bottlenecks.is_empty());
675 assert_eq!(result.bottlenecks[0].task_id, "A");
676 assert!(result.bottlenecks[0].roi >= 14.0);
678 }
679
680 #[test]
681 fn test_empty_tasks() {
682 let mut tasks: Vec<Task> = vec![];
683 let result = CpmAlgorithm::calculate(&mut tasks);
684 assert_eq!(result.total_tasks, 0);
685 assert!(result.critical_path.is_empty());
686 }
687
688 #[test]
689 fn test_single_task() {
690 let mut tasks = vec![make_task("A", 5.0, vec![])];
691 let result = CpmAlgorithm::calculate(&mut tasks);
692
693 assert_eq!(result.total_tasks, 1);
694 assert_eq!(result.critical_path_duration, 5.0);
695 assert_eq!(result.critical_path, vec!["A".to_string()]);
696 }
697
698 #[test]
699 fn test_cyclic_graph_is_reported_unscheduled() {
700 let mut tasks = vec![
702 make_task("A", 1.0, vec!["C"]),
703 make_task("B", 1.0, vec!["A"]),
704 make_task("C", 1.0, vec!["B"]),
705 ];
706
707 let result = CpmAlgorithm::calculate(&mut tasks);
708
709 assert_eq!(
713 result.unscheduled,
714 vec!["A".to_string(), "B".to_string(), "C".to_string()]
715 );
716 }
717
718 #[test]
719 fn test_partial_cycle_reports_only_cycle_members() {
720 let mut tasks = vec![
722 make_task("ROOT", 1.0, vec![]),
723 make_task("X", 1.0, vec!["ROOT", "Y"]),
724 make_task("Y", 1.0, vec!["X"]),
725 ];
726
727 let result = CpmAlgorithm::calculate(&mut tasks);
728
729 assert_eq!(result.unscheduled, vec!["X".to_string(), "Y".to_string()]);
730 }
731
732 #[test]
733 fn test_acyclic_graph_has_empty_unscheduled() {
734 let mut tasks = vec![make_task("A", 2.0, vec![]), make_task("B", 3.0, vec!["A"])];
735 let result = CpmAlgorithm::calculate(&mut tasks);
736 assert!(result.unscheduled.is_empty());
737 }
738
739 #[test]
740 fn test_batch_grouping() {
741 let mut tasks = vec![make_task("A", 2.0, vec![]), make_task("B", 3.0, vec![])];
743
744 let result = CpmAlgorithm::calculate(&mut tasks);
745
746 assert_eq!(result.parallelizable_batches.len(), 1);
748 assert_eq!(result.parallelizable_batches[0].tasks.len(), 2);
749 assert_eq!(result.parallelizable_batches[0].duration_hours, 3.0);
751 assert_eq!(result.parallelizable_batches[0].total_effort_hours, 5.0);
752 }
753}