use crate::constraint::{
Constraint, PropagationResult, domain_bounds, duration_as_i64, energetic_overload, prune,
};
use crate::model::domain::TrailedDomains;
use crate::model::interval::Interval;
use crate::model::variable::VariableId;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TaskInterval {
pub start: VariableId,
pub duration: u64,
}
#[derive(Debug, Clone)]
pub struct NoOverlap {
tasks: Vec<TaskInterval>,
scope: Vec<VariableId>,
}
impl NoOverlap {
pub fn new(tasks: Vec<TaskInterval>) -> Self {
let scope = tasks.iter().map(|t| t.start).collect();
Self { tasks, scope }
}
pub fn from_intervals(intervals: &[Interval], durations: &[u64]) -> Self {
assert_eq!(intervals.len(), durations.len());
let tasks = intervals
.iter()
.zip(durations.iter())
.map(|(inv, &duration)| TaskInterval {
start: inv.start(),
duration,
})
.collect();
Self::new(tasks)
}
}
fn edge_finding_bound_updates(tasks: &[Option<(i64, i64, i64)>]) -> Vec<(usize, i64)> {
let n = tasks.len();
let mut updates = Vec::new();
for i in 0..n {
let Some((est_i, _lct_i, dur_i)) = tasks[i] else {
continue;
};
let mut best_new_est = est_i;
for (b_idx, task_b) in tasks.iter().enumerate() {
if b_idx == i {
continue;
}
let Some((_, b, _)) = *task_b else { continue };
let mut p_omega = 0i64;
let mut est_omega = i64::MAX;
let mut any = false;
for (j, task_j) in tasks.iter().enumerate() {
if j == i {
continue;
}
let Some((est_j, lct_j, dur_j)) = *task_j else {
continue;
};
if lct_j > b {
continue;
}
any = true;
p_omega = p_omega.saturating_add(dur_j);
est_omega = est_omega.min(est_j);
}
if !any || p_omega <= 0 {
continue;
}
if p_omega > b.saturating_sub(est_omega) {
continue;
}
let est_with_i = est_omega.min(est_i);
if est_with_i.saturating_add(p_omega).saturating_add(dur_i) > b {
let candidate = est_omega.saturating_add(p_omega);
if candidate > best_new_est {
best_new_est = candidate;
}
}
}
if best_new_est > est_i {
updates.push((i, best_new_est));
}
}
updates
}
impl Constraint for NoOverlap {
fn name(&self) -> &str {
"NoOverlap"
}
fn scope(&self) -> &[VariableId] {
&self.scope
}
fn is_satisfied(&self, assignment: &HashMap<VariableId, i64>) -> bool {
let n = self.tasks.len();
for i in 0..n {
for j in (i + 1)..n {
let t1 = &self.tasks[i];
let t2 = &self.tasks[j];
if let (Some(&s1), Some(&s2)) =
(assignment.get(&t1.start), assignment.get(&t2.start))
{
let end1 = s1.saturating_add(duration_as_i64(t1.duration));
let end2 = s2.saturating_add(duration_as_i64(t2.duration));
if end1 > s2 && end2 > s1 {
return false;
}
}
}
}
true
}
fn propagate(&self, domains: &mut TrailedDomains) -> PropagationResult {
let mut changed = false;
let n = self.tasks.len();
let energy_windows: Vec<(i64, i64, i64)> = self
.tasks
.iter()
.filter_map(|task| {
let (min, max) = domain_bounds(domains, task.start)?;
let lct = max.saturating_add(duration_as_i64(task.duration));
Some((min, lct, duration_as_i64(task.duration)))
})
.collect();
if energetic_overload(&energy_windows, 1) {
return PropagationResult::Conflict;
}
let task_bounds: Vec<Option<(i64, i64, i64)>> = self
.tasks
.iter()
.map(|task| {
let (min, max) = domain_bounds(domains, task.start)?;
let lct = max.saturating_add(duration_as_i64(task.duration));
Some((min, lct, duration_as_i64(task.duration)))
})
.collect();
for (idx, new_est) in edge_finding_bound_updates(&task_bounds) {
let var = self.tasks[idx].start;
if let Some(result) = prune(domains, &mut changed, var, |d| d.remove_below(new_est)) {
return result;
}
}
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let t1 = &self.tasks[i];
let t2 = &self.tasks[j];
let (min1, max1) = match domain_bounds(domains, t1.start) {
Some(bounds) => bounds,
None => continue,
};
let (min2, max2) = match domain_bounds(domains, t2.start) {
Some(bounds) => bounds,
None => continue,
};
let end1_min = min1.saturating_add(duration_as_i64(t1.duration));
let end2_min = min2.saturating_add(duration_as_i64(t2.duration));
if end1_min > max2
&& let Some(result) = prune(domains, &mut changed, t1.start, |d| {
d.remove_below(end2_min)
})
{
return result;
}
if end2_min > max1
&& let Some(result) = prune(domains, &mut changed, t2.start, |d| {
d.remove_below(end1_min)
})
{
return result;
}
}
}
PropagationResult::Success { changed }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::domain::Domain;
#[test]
fn test_propagate_detects_triple_overload_beyond_pairwise_reasoning() {
let mut domains = HashMap::new();
let a = VariableId(0);
let b = VariableId(1);
let c = VariableId(2);
for &v in &[a, b, c] {
domains.insert(v, Domain::range(0, 3));
}
let mut trailed = TrailedDomains::new(domains);
let constraint = NoOverlap::new(vec![
TaskInterval {
start: a,
duration: 2,
},
TaskInterval {
start: b,
duration: 2,
},
TaskInterval {
start: c,
duration: 2,
},
]);
assert_eq!(
constraint.propagate(&mut trailed),
PropagationResult::Conflict
);
}
#[test]
fn test_propagate_no_false_conflict_when_energy_exactly_fits() {
let mut domains = HashMap::new();
let a = VariableId(0);
let b = VariableId(1);
let c = VariableId(2);
for &v in &[a, b, c] {
domains.insert(v, Domain::range(0, 4)); }
let mut trailed = TrailedDomains::new(domains);
let constraint = NoOverlap::new(vec![
TaskInterval {
start: a,
duration: 2,
},
TaskInterval {
start: b,
duration: 2,
},
TaskInterval {
start: c,
duration: 2,
},
]);
assert_eq!(
constraint.propagate(&mut trailed),
PropagationResult::Success { changed: false }
);
}
#[test]
fn test_propagate_edge_finding_tightens_est_beyond_pairwise_precedence() {
let mut domains = HashMap::new();
let a = VariableId(0);
let b = VariableId(1);
let c = VariableId(2);
domains.insert(a, Domain::range(0, 3));
domains.insert(b, Domain::range(0, 3));
domains.insert(c, Domain::range(0, 10));
let mut trailed = TrailedDomains::new(domains);
let constraint = NoOverlap::new(vec![
TaskInterval {
start: a,
duration: 2,
},
TaskInterval {
start: b,
duration: 2,
},
TaskInterval {
start: c,
duration: 2,
},
]);
let result = constraint.propagate(&mut trailed);
assert_eq!(result, PropagationResult::Success { changed: true });
assert_eq!(
trailed.get(&a).unwrap().values(),
(0..=3).collect::<Vec<_>>()
);
assert_eq!(
trailed.get(&b).unwrap().values(),
(0..=3).collect::<Vec<_>>()
);
assert_eq!(
trailed.get(&c).unwrap().min(),
Some(4),
"c must start at or after a and b (combined) finish, even though neither alone forces it"
);
}
#[test]
fn test_propagate_edge_finding_no_update_when_not_forced() {
let mut domains = HashMap::new();
let a = VariableId(0);
let b = VariableId(1);
let c = VariableId(2);
domains.insert(a, Domain::range(5, 8));
domains.insert(b, Domain::range(5, 8));
domains.insert(c, Domain::range(0, 3));
let mut trailed = TrailedDomains::new(domains);
let constraint = NoOverlap::new(vec![
TaskInterval {
start: a,
duration: 2,
},
TaskInterval {
start: b,
duration: 2,
},
TaskInterval {
start: c,
duration: 2,
},
]);
constraint.propagate(&mut trailed);
assert_eq!(
trailed.get(&c).unwrap().min(),
Some(0),
"c already fits entirely before a/b's earliest possible start; no push needed"
);
}
}