use crate::schedule::Project;
use distimate::Pert;
use rand::prelude::*;
use rayon::prelude::*;
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
#[derive(Debug)]
pub struct SimulationResult {
pub total_project_duration: Duration,
pub total_effort_time: Duration,
}
pub fn run_multiple_simulations(
schedule: &Project,
num_simulations: usize,
) -> (Vec<Duration>, Vec<Duration>) {
(0..num_simulations)
.into_par_iter()
.map(|_| {
let result = run_simulation(schedule);
(result.total_project_duration, result.total_effort_time)
})
.unzip()
}
fn run_simulation(schedule: &Project) -> SimulationResult {
let mut rng = thread_rng();
let task_effort_times: HashMap<_, _> = schedule
.tasks
.iter()
.map(|task| {
let task_time = simulate_task_time(&mut rng, task.min_time, task.max_time);
(&task.id, task_time)
})
.collect();
let total_effort_time: Duration = task_effort_times.values().sum();
let mut task_dependencies: HashMap<_, HashSet<_>> = HashMap::new();
let mut reverse_dependencies: HashMap<_, Vec<_>> = HashMap::new();
for task in &schedule.tasks {
task_dependencies.insert(&task.id, task.dependencies.iter().collect());
for dep in &task.dependencies {
reverse_dependencies.entry(dep).or_default().push(&task.id);
}
}
let mut task_queue: Vec<_> = schedule
.tasks
.iter()
.filter(|t| t.dependencies.is_empty())
.map(|t| &t.id)
.collect();
let mut current_time = Duration::default();
let mut completed_tasks = HashSet::new();
let mut worker_finish_times = vec![Duration::default(); schedule.num_workers];
while !task_queue.is_empty() || completed_tasks.len() < schedule.tasks.len() {
let available_workers: Vec<_> = worker_finish_times
.iter()
.enumerate()
.filter(|&(_, &time)| time <= current_time)
.map(|(i, _)| i)
.collect();
if !task_queue.is_empty() && !available_workers.is_empty() {
for &worker in &available_workers {
if task_queue.is_empty() {
break;
}
let task_index = rng.gen_range(0..task_queue.len());
let task_id = task_queue.swap_remove(task_index);
let task_duration = task_effort_times[task_id];
worker_finish_times[worker] = current_time + task_duration;
completed_tasks.insert(task_id);
if let Some(dependent_tasks) = reverse_dependencies.get(task_id) {
for &dep_task in dependent_tasks {
if !completed_tasks.contains(dep_task)
&& !task_queue.contains(&dep_task)
&& task_dependencies[dep_task]
.iter()
.all(|dep| completed_tasks.contains(dep))
{
task_queue.push(dep_task);
}
}
}
}
}
current_time = *worker_finish_times.iter().min().unwrap_or(¤t_time);
}
SimulationResult {
total_project_duration: *worker_finish_times.iter().max().unwrap_or(¤t_time),
total_effort_time,
}
}
fn simulate_task_time(rng: &mut impl Rng, min_time: Duration, max_time: Duration) -> Duration {
let min_secs = min_time.as_secs_f64();
let max_secs = max_time.as_secs_f64();
let mean = (min_secs + max_secs) / 2.0;
let pert = Pert::new(min_secs, mean, max_secs).unwrap();
let sampled_secs = pert.sample(rng);
Duration::from_secs_f64(sampled_secs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::task::Task;
#[test]
fn test_simulate_task_time() {
let mut rng = thread_rng();
let min_time = Duration::from_secs(5);
let max_time = Duration::from_secs(15);
for _ in 0..1000 {
let time = simulate_task_time(&mut rng, min_time, max_time);
assert!(
time >= min_time && time <= max_time,
"Simulated time should be within range"
);
}
}
#[test]
fn test_single_task_simulation() {
let task = Task::new(
"A",
vec![],
Duration::from_secs(5),
Duration::from_secs(10),
Duration::from_secs(15),
);
let schedule = Project::new(vec![task], 1, None).unwrap();
let result = run_simulation(&schedule);
assert_eq!(
result.total_project_duration, result.total_effort_time,
"For a single task, project duration should equal effort time"
);
assert!(
result.total_project_duration >= Duration::from_secs(5)
&& result.total_project_duration <= Duration::from_secs(15),
"Project duration should be within the task's time range"
);
}
#[test]
fn test_multiple_independent_tasks() {
let tasks = vec![
Task::new(
"A",
vec![],
Duration::from_secs(5),
Duration::from_secs(6),
Duration::from_secs(10),
),
Task::new(
"B",
vec![],
Duration::from_secs(7),
Duration::from_secs(10),
Duration::from_secs(12),
),
Task::new(
"C",
vec![],
Duration::from_secs(3),
Duration::from_secs(6),
Duration::from_secs(8),
),
];
let schedule = Project::new(tasks, 2, None).unwrap();
let result = run_simulation(&schedule);
assert!(
result.total_project_duration <= result.total_effort_time,
"Project duration should not exceed total effort time"
);
assert!(
result.total_project_duration >= Duration::from_secs(7),
"Project duration should be at least the longest minimum task duration"
);
}
#[test]
fn test_tasks_with_dependencies() {
let tasks = vec![
Task::new(
"A",
vec![],
Duration::from_secs(5),
Duration::from_secs(8),
Duration::from_secs(10),
),
Task::new(
"B",
vec!["A".to_string()],
Duration::from_secs(7),
Duration::from_secs(9),
Duration::from_secs(12),
),
Task::new(
"C",
vec!["A".to_string()],
Duration::from_secs(3),
Duration::from_secs(6),
Duration::from_secs(8),
),
Task::new(
"D",
vec!["B".to_string(), "C".to_string()],
Duration::from_secs(4),
Duration::from_secs(7),
Duration::from_secs(9),
),
];
let schedule = Project::new(tasks, 2, None).unwrap();
let result = run_simulation(&schedule);
println!("{:?}", result);
assert!(
result.total_project_duration >= Duration::from_secs(13),
"Project duration should be at least the minimum critical path duration"
);
}
#[test]
fn test_multiple_simulations_consistency() {
let tasks = vec![
Task::new(
"A",
vec![],
Duration::from_secs(5),
Duration::from_secs(8),
Duration::from_secs(10),
),
Task::new(
"B",
vec!["A".to_string()],
Duration::from_secs(7),
Duration::from_secs(10),
Duration::from_secs(12),
),
];
let schedule = Project::new(tasks, 1, None).unwrap();
let (durations, efforts) = run_multiple_simulations(&schedule, 1000);
assert_eq!(durations.len(), 1000, "Should run 1000 simulations");
assert_eq!(efforts.len(), 1000, "Should run 1000 simulations");
let avg_duration: Duration = durations.iter().sum::<Duration>() / 1000;
let avg_effort: Duration = efforts.iter().sum::<Duration>() / 1000;
assert!(
avg_duration >= Duration::from_secs(12) && avg_duration <= Duration::from_secs(22),
"Average duration should be within expected range"
);
assert!(
avg_effort >= Duration::from_secs(12) && avg_effort <= Duration::from_secs(22),
"Average effort should be within expected range"
);
}
}