use crate::constraint::{Constraint, PropagationResult, compare_assigned, prune};
use crate::model::domain::{Domain, TrailedDomains};
use crate::model::variable::VariableId;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct MinimumDistance {
first: VariableId,
second: VariableId,
min_distance: i64,
scope: [VariableId; 2],
}
impl MinimumDistance {
pub fn new(first: VariableId, second: VariableId, min_distance: i64) -> Self {
Self {
first,
second,
min_distance,
scope: [first, second],
}
}
pub const fn min_distance(&self) -> i64 {
self.min_distance
}
fn forbidden_band(&self, w: i64) -> (i64, i64) {
(
w.saturating_sub(self.min_distance).saturating_add(1),
w.saturating_add(self.min_distance).saturating_sub(1),
)
}
}
impl Constraint for MinimumDistance {
fn name(&self) -> &str {
"MinimumDistance"
}
fn scope(&self) -> &[VariableId] {
&self.scope
}
fn is_satisfied(&self, assignment: &HashMap<VariableId, i64>) -> bool {
compare_assigned(assignment, self.first, self.second, |first, second| {
if self.min_distance <= 0 {
return true;
}
first.abs_diff(second) >= self.min_distance as u64
})
}
fn propagate(&self, domains: &mut TrailedDomains) -> PropagationResult {
let mut changed = false;
if let Some(fixed) = single_value(domains, self.first) {
let (low, high) = self.forbidden_band(fixed);
if let Some(result) = prune(domains, &mut changed, self.second, |domain| {
remove_band(domain, low, high)
}) {
return result;
}
}
if let Some(fixed) = single_value(domains, self.second) {
let (low, high) = self.forbidden_band(fixed);
if let Some(result) = prune(domains, &mut changed, self.first, |domain| {
remove_band(domain, low, high)
}) {
return result;
}
}
PropagationResult::Success { changed }
}
}
fn single_value(domains: &TrailedDomains, var: VariableId) -> Option<i64> {
domains
.get(&var)
.filter(|d| d.len() == 1)
.and_then(Domain::min)
}
fn remove_band(domain: &mut Domain, low: i64, high: i64) -> bool {
if low > high {
return false;
}
enum Plan {
None,
Empty,
Below(i64),
Above(i64),
Values(Vec<i64>),
}
let plan = match &*domain {
Domain::Range { min, max } => {
let (min, max) = (*min, *max);
if min > max {
Plan::None
} else if low <= min && max <= high {
Plan::Empty
} else if low <= min && min <= high {
Plan::Below(high)
} else if low <= max && max <= high {
Plan::Above(low)
} else {
Plan::None
}
}
Domain::Explicit(set) => Plan::Values(
set.iter()
.copied()
.filter(|value| low <= *value && *value <= high)
.collect(),
),
};
match plan {
Plan::None => false,
Plan::Empty => {
*domain = Domain::from_values(std::iter::empty());
true
}
Plan::Below(high) => domain.remove_below(high.saturating_add(1)),
Plan::Above(low) => domain.remove_above(low.saturating_sub(1)),
Plan::Values(values) => {
let mut changed = false;
for value in values {
changed |= domain.remove(value);
}
changed
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::domain::Domain;
fn assignment(entries: &[(VariableId, i64)]) -> HashMap<VariableId, i64> {
entries.iter().copied().collect()
}
#[test]
fn distance_exactly_at_the_minimum_is_allowed() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 3);
assert!(constraint.is_satisfied(&assignment(&[(a, 0), (b, 3)])));
assert!(constraint.is_satisfied(&assignment(&[(a, 3), (b, 0)])));
}
#[test]
fn too_short_distance_is_rejected() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 3);
assert!(!constraint.is_satisfied(&assignment(&[(a, 1), (b, 2)])));
assert!(!constraint.is_satisfied(&assignment(&[(a, 2), (b, 1)])));
assert!(
!constraint.is_satisfied(&assignment(&[(a, 4), (b, 4)])),
"identical values have distance 0"
);
}
#[test]
fn partial_assignment_is_not_a_violation() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 3);
assert!(constraint.is_satisfied(&assignment(&[(a, 0)])));
}
#[test]
fn propagation_prunes_values_near_a_fixed_counterpart() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 3);
let mut domains = TrailedDomains::new(HashMap::from([
(a, Domain::from_values([5])),
(b, Domain::range(5, 10)),
]));
let result = constraint.propagate(&mut domains);
assert!(matches!(
result,
PropagationResult::Success { changed: true }
));
assert_eq!(domains.get(&b).unwrap().values(), vec![8, 9, 10]);
}
#[test]
fn propagation_reports_conflict_when_everything_is_too_close() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 3);
let mut domains = TrailedDomains::new(HashMap::from([
(a, Domain::from_values([5])),
(b, Domain::range(4, 6)),
]));
assert_eq!(
constraint.propagate(&mut domains),
PropagationResult::Conflict
);
}
#[test]
fn non_positive_minimum_is_trivial() {
let (a, b) = (VariableId(0), VariableId(1));
let constraint = MinimumDistance::new(a, b, 0);
assert!(constraint.is_satisfied(&assignment(&[(a, 7), (b, 7)])));
let mut domains = TrailedDomains::new(HashMap::from([
(a, Domain::from_values([7])),
(b, Domain::from_values([7])),
]));
assert_eq!(
constraint.propagate(&mut domains),
PropagationResult::Success { changed: false }
);
}
#[test]
fn builder_and_solver_respect_the_minimum_distance() {
use crate::dsl::ModelBuilder;
use crate::solver::{BacktrackingSolver, SolverOptions};
let mut builder = ModelBuilder::new();
let pinned = builder.new_var("pinned", 0..=0);
let free = builder.new_var("free", 0..=2);
builder.add_minimum_distance(pinned, free, 2);
let graph = builder.build().expect("model should validate");
let solution = BacktrackingSolver::new()
.solve(&graph, &SolverOptions::default())
.solution
.expect("feasible: free can move to distance 2");
assert_eq!(solution.assignment[&free], 2);
}
#[test]
fn builder_and_solver_report_infeasibility() {
use crate::dsl::ModelBuilder;
use crate::solver::{BacktrackingSolver, SolverOptions};
let mut builder = ModelBuilder::new();
let a = builder.new_var("a", 0..=1);
let b = builder.new_var("b", 0..=1);
builder.add_minimum_distance(a, b, 2);
let graph = builder.build().expect("model should validate");
let outcome = BacktrackingSolver::new().solve(&graph, &SolverOptions::default());
assert!(outcome.solution.is_none());
}
}