#![doc = include_str!("../README.md")]
use std::{
cell::RefCell,
collections::VecDeque,
rc::{Rc, Weak},
};
use rand::{
Rng,
distr::{Distribution, Uniform},
};
use rand_distr::Triangular;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[non_exhaustive] #[derive(Debug, Clone, Serialize)]
pub struct PertTask {
pub id: String,
pub deps: Vec<String>,
pub duration: f32,
pub early_start_time: f32,
pub early_finish_time: f32,
pub late_start_time: f32,
pub late_finish_time: f32,
#[serde(skip_serializing)]
parents: Vec<PertTaskWeakRef>,
#[serde(skip_serializing)]
children: Vec<PertTaskWeakRef>,
}
impl PartialEq for PertTask {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl PertTask {
pub fn new(id: String, deps: Vec<String>, duration: f32) -> PertTask {
Self {
id,
deps,
duration,
early_start_time: 0.,
early_finish_time: f32::MIN,
late_start_time: f32::MAX,
late_finish_time: f32::MAX,
parents: Vec::new(),
children: Vec::new(),
}
}
pub fn is_critical(&self) -> bool {
let diff = (self.late_start_time - self.early_start_time).abs();
diff < 0.001
}
pub fn slack(&self) -> f32 {
self.late_start_time - self.early_start_time
}
}
type PertTaskRcRef = Rc<RefCell<PertTask>>;
type PertTaskWeakRef = Weak<RefCell<PertTask>>;
pub trait TryIntoPertTask {
type Error;
fn try_into_pert_task(&self) -> Result<PertTask, Self::Error>;
}
pub trait Pert {
type Error;
fn try_into_pert_tasks(&self) -> Result<Vec<PertTask>, PertError<Self::Error>>;
fn solve_pert(&self, max_iter: usize) -> Result<Vec<PertTask>, PertError<Self::Error>>;
}
#[derive(Debug)]
pub enum PertError<T> {
TaskErrors(Vec<T>),
MissingTasks(Vec<String>),
NoStartingTasks,
MaxIterReached(usize),
}
impl<T> Pert for Vec<T>
where
T: TryIntoPertTask + PartialEq,
{
type Error = T::Error;
fn try_into_pert_tasks(&self) -> Result<Vec<PertTask>, PertError<T::Error>> {
let mut tasks: Vec<PertTask> = Vec::new();
let mut errs: Vec<Self::Error> = Vec::new();
for t in self.iter() {
match t.try_into_pert_task() {
Ok(t) => tasks.push(t),
Err(e) => errs.push(e),
}
}
match errs.is_empty() {
true => Ok(tasks),
false => Err(PertError::TaskErrors(errs)),
}
}
fn solve_pert(&self, max_iter: usize) -> Result<Vec<PertTask>, PertError<T::Error>> {
let mut tasks: Vec<PertTaskRcRef> = self
.try_into_pert_tasks()?
.into_iter()
.map(|t| Rc::new(RefCell::new(t)))
.collect();
build_parent_edges::<T>(&mut tasks)?;
build_child_edges(&mut tasks);
solve_early_times::<T>(&mut tasks, max_iter)?;
solve_late_times::<T>(&mut tasks, max_iter)?;
let owned: Vec<PertTask> = tasks
.into_iter()
.map(|t| {
let t = Rc::try_unwrap(t).expect("Should only have one strong rc");
let mut t = t.into_inner();
t.parents.clear();
t.children.clear();
t
})
.collect();
Ok(owned)
}
}
fn build_parent_edges<T: TryIntoPertTask>(
tasks: &mut [PertTaskRcRef],
) -> Result<(), PertError<T::Error>> {
let mut missing: Vec<String> = Vec::new();
for a in tasks.iter() {
let mut a = a.borrow_mut();
let deps = a.deps.to_owned();
for d in deps {
let mut flag = false;
for b in tasks.iter() {
if b.borrow().id == d {
a.parents.push(Rc::downgrade(b));
flag = true;
break;
}
}
if !flag {
missing.push(d);
}
}
}
if !missing.is_empty() {
return Err(PertError::MissingTasks(missing));
}
Ok(())
}
fn build_child_edges(tasks: &mut [PertTaskRcRef]) {
for a in tasks.iter() {
for b in tasks.iter() {
if a != b {
let weak_a = Rc::downgrade(a);
let is_in = b.borrow().parents.iter().any(|p| Weak::ptr_eq(p, &weak_a));
if is_in {
a.borrow_mut().children.push(Rc::downgrade(b));
}
}
}
}
}
fn solve_early_times<T: TryIntoPertTask>(
tasks: &mut [PertTaskRcRef],
max_iter: usize,
) -> Result<(), PertError<T::Error>> {
let mut processing: VecDeque<PertTaskRcRef> = VecDeque::new();
for t in tasks.iter() {
if t.borrow().parents.is_empty() {
processing.push_back(t.clone());
}
}
if processing.is_empty() {
return Err(PertError::NoStartingTasks);
}
let mut n: usize = 0;
while let Some(task) = processing.pop_front() {
n += 1;
if n > max_iter {
return Err(PertError::MaxIterReached(max_iter));
}
let mut parent = task.borrow_mut();
let early_finish_time = parent.early_start_time + parent.duration;
parent.early_finish_time = early_finish_time;
for child in parent.children.iter() {
let child = child.upgrade().unwrap();
if parent.early_finish_time > child.borrow().early_start_time {
child.borrow_mut().early_start_time = parent.early_finish_time;
processing.push_back(child);
}
}
}
Ok(())
}
fn solve_late_times<T: TryIntoPertTask>(
tasks: &mut [PertTaskRcRef],
max_iter: usize,
) -> Result<(), PertError<T::Error>> {
let mut processing: VecDeque<PertTaskRcRef> = VecDeque::new();
let mut latest: f32 = 0.;
for t in tasks.iter() {
let t = t.borrow();
let time = t.early_finish_time;
if time > latest {
latest = time;
}
}
for task in tasks.iter() {
if task.borrow().children.is_empty() {
task.borrow_mut().late_finish_time = latest;
processing.push_back(task.clone());
}
}
let mut n: usize = 0;
while let Some(child) = processing.pop_front() {
n += 1;
if n > max_iter {
return Err(PertError::MaxIterReached(max_iter));
}
let mut child = child.borrow_mut();
let delta = child.late_finish_time - child.duration;
child.late_start_time = delta;
for parent in child.parents.iter() {
let parent = parent.upgrade().unwrap();
let mut p = parent.borrow_mut();
if p.late_finish_time > child.late_start_time {
p.late_finish_time = child.late_start_time;
processing.push_back(parent.clone())
}
}
}
Ok(())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub deps: Vec<String>,
pub duration: Duration,
}
impl PartialEq for Task {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(tag = "type")]
pub enum Duration {
Constant(f32),
Pert {
optimistic: f32,
most_likely: f32,
pessimistic: f32,
},
Uniform { min: f32, max: f32 },
Triangular { min: f32, max: f32, mode: f32 },
}
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum DurationError {
#[error("There is something wrong with the bounds you have submitted. Received: {0:?}")]
ParameterMisMatch(Vec<f32>),
}
impl Duration {
fn sample(&self) -> Result<f32, DurationError> {
match self {
Self::Constant(c) => Ok(*c),
Self::Pert {
optimistic,
most_likely,
pessimistic,
} => {
if 0. > *optimistic || 0. > *most_likely || 0. > *pessimistic {
return Err(DurationError::ParameterMisMatch(vec![
*optimistic,
*most_likely,
*pessimistic,
]));
}
if optimistic > most_likely || most_likely > pessimistic {
return Err(DurationError::ParameterMisMatch(vec![
*optimistic,
*most_likely,
*pessimistic,
]));
}
Ok((optimistic + 4. * most_likely + pessimistic) / 6.)
}
Self::Uniform { min, max } => {
let mut rng = rand::rng();
match Uniform::new(min, max) {
Ok(u) => Ok(rng.sample(u)),
Err(_) => Err(DurationError::ParameterMisMatch(vec![*min, *max])),
}
}
Self::Triangular { min, max, mode } => {
let mut rng = rand::rng();
match Triangular::new(*min, *max, *mode) {
Ok(u) => Ok(u.sample(&mut rng)),
Err(_) => Err(DurationError::ParameterMisMatch(vec![*min, *max])),
}
}
}
}
}
impl Task {
pub fn new(id: String, deps: Vec<String>, duration: Duration) -> Task {
Self { id, deps, duration }
}
}
impl TryIntoPertTask for Task {
type Error = DurationError;
fn try_into_pert_task(&self) -> Result<PertTask, Self::Error> {
let d = self.duration.sample()?;
let node = PertTask::new(self.id.clone(), self.deps.clone(), d);
Ok(node)
}
}
pub trait PostProcess {
fn project_duration(&self) -> f32;
fn critical_tasks(&self) -> Vec<&PertTask>;
}
impl PostProcess for Vec<PertTask> {
fn project_duration(&self) -> f32 {
let mut max: f32 = 0.;
for t in self.iter() {
let t = t.early_finish_time;
if t > max {
max = t
}
}
max
}
fn critical_tasks(&self) -> Vec<&PertTask> {
self.iter().filter(|t| t.is_critical()).collect()
}
}
pub trait MontePostProcess {
fn mean_project_duration(&self) -> f32;
fn median_project_duration(&self) -> f32;
fn task_criticality(&self) -> Vec<(String, f32)>;
}
impl MontePostProcess for Vec<Vec<PertTask>> {
fn mean_project_duration(&self) -> f32 {
let mut sum = 0.;
for run in self.iter() {
sum += run.project_duration();
}
sum / self.len() as f32
}
fn median_project_duration(&self) -> f32 {
let mut durations: Vec<f32> = self.iter().map(|t| t.project_duration()).collect();
durations.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = durations.len();
if len % 2 == 1 {
durations[len / 2]
} else {
let mid1 = durations[len / 2 - 1];
let mid2 = durations[len / 2];
(mid1 + mid2) / 2.0
}
}
fn task_criticality(&self) -> Vec<(String, f32)> {
let mut tc: Vec<(String, f32)> = Vec::new();
for t in self[0].iter() {
tc.push((t.id.clone(), 0.0));
}
for run in self.iter() {
for (a, b) in tc.iter_mut().zip(run) {
if b.is_critical() {
a.1 += 1.0;
}
}
}
let n = self.len() as f32;
for t in tc.iter_mut() {
t.1 /= n;
}
tc
}
}