use std::{
any::TypeId,
cmp::{self, Ordering},
iter::{repeat_with, zip},
};
use itertools::izip;
use crate::{
IntVal,
actions::{
ConstructionActions, InitActions, IntDecisionActions, IntInspectionActions, IntPropCond,
PostingActions, ReasoningContext, ReasoningEngine, Trailed, TrailingActions,
},
constraints::{
BoxedPropagator, Constraint, IntModelActions, IntSolverActions, Propagator,
SimplificationStatus,
},
helpers::matrix::Matrix,
lower::{LoweringContext, LoweringError},
solver::{IntLitMeaning, engine::Engine, queue::PriorityLevel},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct IntNoOverlapSweep<const STRICT: bool, I1, I2> {
origin: Matrix<2, I1>,
size: Matrix<2, I2>,
target: Box<[Trailed<bool>]>,
source: Box<[Trailed<bool>]>,
origin_ub: Matrix<2, IntVal>,
origin_lb: Matrix<2, IntVal>,
size_lb: Matrix<2, IntVal>,
bounding_box: Region,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct Region(Vec<(IntVal, IntVal)>);
impl<const STRICT: bool, I1, I2> IntNoOverlapSweep<STRICT, I1, I2> {
fn adjust_sweep_max(
sweep: &mut [IntVal],
jump: &mut [IntVal],
curr_obj_lb: &[IntVal],
curr_obj_ub: &[IntVal],
curr_dimension: usize,
) -> bool {
let dimensions = sweep.len();
debug_assert_eq!(jump.len(), dimensions);
debug_assert_eq!(curr_obj_lb.len(), dimensions);
debug_assert_eq!(curr_obj_ub.len(), dimensions);
for i in (0..dimensions).rev() {
let rotation = (i + curr_dimension) % dimensions;
sweep[rotation] = jump[rotation];
jump[rotation] = curr_obj_lb[rotation] - 1;
if sweep[rotation] >= curr_obj_lb[rotation] {
return true;
} else {
sweep[rotation] = curr_obj_ub[rotation];
}
}
sweep[curr_dimension] = curr_obj_lb[curr_dimension] - 1;
false
}
fn adjust_sweep_min(
sweep: &mut [IntVal],
jump: &mut [IntVal],
obj_lb: &[IntVal],
obj_ub: &[IntVal],
dim: usize,
) -> bool {
let dimensions = sweep.len();
debug_assert_eq!(jump.len(), dimensions);
debug_assert_eq!(obj_lb.len(), dimensions);
debug_assert_eq!(obj_ub.len(), dimensions);
for i in (0..dimensions).rev() {
let rotation = (i + dim) % dimensions;
sweep[rotation] = jump[rotation];
jump[rotation] = obj_ub[rotation] + 1;
if sweep[rotation] <= obj_ub[rotation] {
return true;
} else {
sweep[rotation] = obj_lb[rotation];
}
}
sweep[dim] = obj_ub[dim] + 1;
false
}
fn disjoint(&self, region: &Region, obj: usize) -> bool {
let origin_lb = self.origin_lb.row(obj);
let origin_ub = self.origin_ub.row(obj);
let size_lb = self.size_lb.row(obj);
izip!(®ion.0, origin_lb, origin_ub, size_lb)
.any(|(&(lb, ub), &o_lb, &o_ub, &size)| o_ub + size - 1 < lb || o_lb > ub)
}
fn explain_forbidden_regions<Ctx>(
&mut self,
ctx: &mut Ctx,
forbidden_regions: &[(Region, usize)],
obj: usize,
) -> Vec<Ctx::Atom>
where
Ctx: ReasoningContext + ?Sized,
I1: IntDecisionActions<Ctx>,
I2: IntInspectionActions<Ctx>,
{
let mut reason = Vec::new();
for (region, support) in forbidden_regions.iter() {
for d in 0..self.num_dimensions() {
if TypeId::of::<I2>() != TypeId::of::<IntVal>() {
reason.push(self.size[[*support, d]].min_lit(ctx));
}
let mut possible_ub = self.origin_ub[[*support, d]];
let origin_ub = self.origin_ub[[obj, d]];
let mut possible_lb = self.origin_lb[[*support, d]];
let origin_lb = self.origin_lb[[obj, d]];
if region.max(d) > origin_ub {
possible_lb = origin_ub - self.size_lb[[*support, d]] + 1;
}
if region.min(d) < origin_lb {
possible_ub = origin_lb + self.size_lb[[obj, d]] - 1;
}
reason.push(
self.origin[[*support, d]].lit(ctx, IntLitMeaning::Less(possible_ub + 1)),
);
reason.push(
self.origin[[*support, d]].lit(ctx, IntLitMeaning::GreaterEq(possible_lb)),
);
}
}
reason
}
fn explain_propagation<Ctx>(
&mut self,
ctx: &mut Ctx,
forbidden_regions: &[(Region, usize)],
obj: usize,
dim: usize,
prune_upper: bool,
) -> Vec<Ctx::Atom>
where
Ctx: ReasoningContext + ?Sized,
I1: IntDecisionActions<Ctx>,
I2: IntInspectionActions<Ctx>,
{
let mut reason: Vec<_> = Vec::new();
for d in 0..self.num_dimensions() {
if TypeId::of::<I2>() != TypeId::of::<IntVal>() {
reason.push(self.size[[obj, d]].min_lit(ctx));
}
if d == dim {
if prune_upper {
reason.push(
self.origin[[obj, d]]
.lit(ctx, IntLitMeaning::Less(self.origin_ub[[obj, d]] + 1)),
);
} else {
reason.push(
self.origin[[obj, d]]
.lit(ctx, IntLitMeaning::GreaterEq(self.origin_lb[[obj, d]])),
);
}
} else {
reason.push(
self.origin[[obj, d]]
.lit(ctx, IntLitMeaning::Less(self.origin_ub[[obj, d]] + 1)),
);
reason.push(
self.origin[[obj, d]]
.lit(ctx, IntLitMeaning::GreaterEq(self.origin_lb[[obj, d]])),
);
}
}
reason.extend(self.explain_forbidden_regions(ctx, forbidden_regions, obj));
reason
}
fn fixed_object(&self, obj: usize) -> bool {
self.origin_lb.row(obj) == self.origin_ub.row(obj)
}
fn forbidden_regions<Ctx>(&mut self, ctx: &mut Ctx, obj: usize) -> Vec<(Region, usize)>
where
Ctx: ReasoningContext + TrailingActions,
{
let mut forbidden_regions: Vec<Option<(Region, usize)>> = Vec::new();
'obj_iter: for i in 0..self.num_objects() {
if ctx.trailed(self.source[i]) {
continue;
}
if i == obj {
continue;
};
let mut forbidden = Region::with_dimensions(self.num_dimensions());
for d in 0..self.num_dimensions() {
let fr_lb = self.origin_ub[[i, d]] - self.size_lb[[obj, d]] + 1;
let fr_ub = self.origin_lb[[i, d]] + self.size_lb[[i, d]] - 1;
if fr_lb <= fr_ub {
forbidden.0[d] = (fr_lb, fr_ub);
} else {
continue 'obj_iter;
}
}
let lb = self.origin_lb.row(obj);
let ub = self.origin_ub.row(obj);
if forbidden.overlaps(lb, ub) {
for tup in &mut forbidden_regions {
if let Some((f, _)) = tup {
match f.coalesce(&forbidden) {
Some(Ordering::Equal) | Some(Ordering::Greater) => {
continue 'obj_iter;
}
Some(Ordering::Less) => {
*tup = None;
}
None => continue,
}
}
}
forbidden_regions.push(Some((forbidden, i)));
}
}
forbidden_regions.into_iter().flatten().collect()
}
pub(crate) fn new<E>(engine: &mut E, origin: Vec<Vec<I1>>, size: Vec<Vec<I2>>) -> Self
where
E: ConstructionActions + ReasoningContext + ?Sized,
{
assert_eq!(origin.len(), size.len());
let num_objects = origin.len();
let num_dimensions = if num_objects > 0 { origin[0].len() } else { 0 };
assert!(origin.is_empty() || origin.iter().all(|v| v.len() == num_dimensions));
assert!(size.is_empty() || size.iter().all(|v| v.len() == num_dimensions));
let origin = Matrix::new(
[num_objects, num_dimensions],
origin.into_iter().flatten().collect(),
);
let size = Matrix::new(
[num_objects, num_dimensions],
size.into_iter().flatten().collect(),
);
let target = repeat_with(|| engine.new_trailed(false))
.take(num_objects)
.collect();
let source = repeat_with(|| engine.new_trailed(false))
.take(num_objects)
.collect();
let origin_ub = Matrix::with_dimensions([num_objects, num_dimensions]);
let origin_lb = Matrix::with_dimensions([num_objects, num_dimensions]);
let size_lb = Matrix::with_dimensions([num_objects, num_dimensions]);
let bounding_box = Region::with_dimensions(num_dimensions);
Self {
origin,
size,
target,
source,
origin_ub,
origin_lb,
size_lb,
bounding_box,
}
}
fn num_dimensions(&self) -> usize {
self.origin.len(1)
}
fn num_objects(&self) -> usize {
self.origin.len(0)
}
pub fn post<E>(solver: &mut E, origin: Vec<Vec<I1>>, size: Vec<Vec<I2>>)
where
E: PostingActions + ?Sized,
I1: IntSolverActions<Engine>,
I2: IntSolverActions<Engine> + IntInspectionActions<E>,
{
if size.iter().flatten().all(|v| v.val(solver).is_some()) {
let size: Vec<Vec<_>> = size
.iter()
.map(|v| v.iter().map(|v| v.val(solver).unwrap()).collect())
.collect();
let con: BoxedPropagator =
Box::new(IntNoOverlapSweep::<STRICT, _, _>::new(solver, origin, size));
solver.add_propagator(con);
return;
}
let con: BoxedPropagator = Box::new(Self::new(solver, origin, size));
solver.add_propagator(con);
}
fn prune_max<E>(
&mut self,
ctx: &mut E::PropagationContext<'_>,
forbidden_regions: &[(Region, usize)],
obj: usize,
dim: usize,
) -> Result<(), E::Conflict>
where
E: ReasoningEngine,
I1: IntSolverActions<E>,
I2: IntSolverActions<E>,
{
let mut sweep = self.origin_ub.row(obj).to_vec();
let mut jump: Vec<_> = self.origin_lb.row(obj).iter().map(|v| v - 1).collect();
let mut b = true;
let mut fr = Region::find_collision(forbidden_regions.iter().map(|(r, _)| r), &sweep);
while b && fr.is_some() {
for (i, j) in jump.iter_mut().enumerate() {
*j = cmp::max(*j, fr.unwrap().min(i) - 1);
}
let lb = self.origin_lb.row(obj);
let ub = self.origin_ub.row(obj);
b = Self::adjust_sweep_max(&mut sweep, &mut jump, lb, ub, dim);
fr = Region::find_collision(forbidden_regions.iter().map(|(r, _)| r), &sweep);
}
if sweep[dim] != self.origin_ub[[obj, dim]] {
let reason = self.explain_propagation(ctx, forbidden_regions, obj, dim, true);
self.origin[[obj, dim]].tighten_max(ctx, sweep[dim], reason)?;
self.origin_ub[[obj, dim]] = sweep[dim];
}
Ok(())
}
fn prune_min<E>(
&mut self,
ctx: &mut E::PropagationContext<'_>,
forbidden_regions: &[(Region, usize)],
obj: usize,
dim: usize,
) -> Result<(), E::Conflict>
where
E: ReasoningEngine,
I1: IntSolverActions<E>,
I2: IntSolverActions<E>,
{
let mut sweep = self.origin_lb.row(obj).to_vec();
let mut jump: Vec<_> = self.origin_ub.row(obj).iter().map(|v| v + 1).collect();
let mut b = true;
let mut fr = Region::find_collision(forbidden_regions.iter().map(|(r, _)| r), &sweep);
while b && fr.is_some() {
for (i, j) in jump.iter_mut().enumerate() {
*j = cmp::min(*j, fr.unwrap().max(i) + 1);
}
let lb = self.origin_lb.row(obj);
let ub = self.origin_ub.row(obj);
b = Self::adjust_sweep_min(&mut sweep, &mut jump, lb, ub, dim);
fr = Region::find_collision(forbidden_regions.iter().map(|(r, _)| r), &sweep);
}
if sweep[dim] != self.origin_lb[[obj, dim]] {
let reason = self.explain_propagation(ctx, forbidden_regions, obj, dim, false);
self.origin[[obj, dim]].tighten_min(ctx, sweep[dim], reason)?;
self.origin_lb[[obj, dim]] = sweep[dim];
}
Ok(())
}
}
impl<const STRICT: bool, E, I1, I2> Constraint<E> for IntNoOverlapSweep<STRICT, I1, I2>
where
E: ReasoningEngine,
I1: IntModelActions<E>,
I2: IntModelActions<E>,
{
fn simplify(
&mut self,
ctx: &mut E::PropagationContext<'_>,
) -> Result<SimplificationStatus, E::Conflict> {
self.propagate(ctx)?;
if self.origin.iter_elem().all(|v| v.val(ctx).is_some())
&& self.size.iter_elem().all(|v| v.val(ctx).is_some())
{
return Ok(SimplificationStatus::Subsumed);
}
Ok(SimplificationStatus::NoFixpoint)
}
fn to_solver(&self, slv: &mut LoweringContext<'_>) -> Result<(), LoweringError> {
let box_pos = self
.origin
.row_iter()
.map(|row| {
row.iter()
.map(|v| slv.solver_view(v.clone().into()))
.collect()
})
.collect();
let box_size = self
.size
.row_iter()
.map(|row| {
row.iter()
.map(|v| slv.solver_view(v.clone().into()))
.collect()
})
.collect();
IntNoOverlapSweep::<STRICT, _, _>::post(slv, box_pos, box_size);
Ok(())
}
}
impl<const STRICT: bool, E, I1, I2> Propagator<E> for IntNoOverlapSweep<STRICT, I1, I2>
where
E: ReasoningEngine,
I1: IntSolverActions<E>,
I2: IntSolverActions<E>,
{
fn initialize(&mut self, ctx: &mut E::InitializationContext<'_>) {
ctx.set_priority(PriorityLevel::Lowest);
for v in self.origin.iter_elem() {
v.enqueue_when(ctx, IntPropCond::Bounds);
}
for v in self.size.iter_elem() {
v.enqueue_when(ctx, IntPropCond::Bounds);
}
}
#[tracing::instrument(
name = "int_no_overlap_sweep",
target = "solver",
level = "trace",
skip(self, ctx)
)]
fn propagate(&mut self, ctx: &mut E::PropagationContext<'_>) -> Result<(), E::Conflict> {
for o in 0..self.num_objects() {
for d in 0..self.num_dimensions() {
self.origin_ub[[o, d]] = self.origin[[o, d]].max(ctx);
self.origin_lb[[o, d]] = self.origin[[o, d]].min(ctx);
self.size_lb[[o, d]] = self.size[[o, d]].min(ctx);
}
}
for obj in 0..self.num_objects() {
if ctx.trailed(self.target[obj]) {
continue;
}
if !STRICT && self.size.row(obj).iter().any(|v| v.val(ctx) == Some(0)) {
continue;
}
let forbidden_regions = self.forbidden_regions(ctx, obj);
if !forbidden_regions.is_empty() {
if self.fixed_object(obj) {
let reason = self.explain_propagation(ctx, &forbidden_regions, obj, 0, false);
self.origin[[obj, 0]].tighten_min(ctx, self.origin_lb[[obj, 0]] + 1, reason)?;
}
let mut all_fixed = true;
for d in 0..self.num_dimensions() {
self.prune_min(ctx, &forbidden_regions, obj, d)?;
self.prune_max(ctx, &forbidden_regions, obj, d)?;
if self.origin_lb[[obj, d]] != self.origin_ub[[obj, d]] {
all_fixed = false;
}
}
if all_fixed {
let _ = ctx.set_trailed(self.target[obj], true);
}
}
}
for obj in 0..self.num_objects() {
if ctx.trailed(self.target[obj]) {
continue;
}
for i in 0..self.num_dimensions() {
*self.bounding_box.min_mut(i) =
cmp::min(self.bounding_box.min(i), self.origin_lb[[obj, i]]);
*self.bounding_box.max_mut(i) = cmp::max(
self.bounding_box.max(i),
self.origin_ub[[obj, i]] + self.size_lb[[obj, i]] - 1,
);
}
}
for obj in 0..self.num_objects() {
if ctx.trailed(self.target[obj]) && self.disjoint(&self.bounding_box, obj) {
let _ = ctx.set_trailed(self.source[obj], true);
}
}
Ok(())
}
}
impl Region {
fn coalesce(&self, other: &Self) -> Option<Ordering> {
debug_assert_eq!(self.0.len(), other.0.len());
let mut trend = Ordering::Equal;
for (&(self_lb, self_ub), &(other_lb, other_ub)) in zip(&self.0, &other.0) {
if self_ub + 1 < other_lb || self_lb > other_ub + 1 {
return None;
} else if (self_lb, self_ub) == (other_lb, other_ub) {
continue;
} else if self_lb <= other_lb && self_ub >= other_ub {
match trend {
Ordering::Equal | Ordering::Greater => trend = Ordering::Greater,
_ => return None,
}
} else if self_lb >= other_lb && self_ub <= other_ub {
match trend {
Ordering::Equal | Ordering::Less => trend = Ordering::Less,
_ => return None,
}
} else {
return None;
}
}
Some(trend)
}
fn find_collision<'a>(
regions: impl IntoIterator<Item = &'a Region>,
point: &[IntVal],
) -> Option<&'a Region> {
regions.into_iter().find(|r| {
debug_assert_eq!(r.0.len(), point.len());
r.0.iter()
.zip(point)
.all(|((lb, ub), p)| p >= lb && p <= ub)
})
}
fn max(&self, dim: usize) -> IntVal {
self.0[dim].1
}
fn max_mut(&mut self, dim: usize) -> &mut IntVal {
&mut self.0[dim].1
}
fn min(&self, dim: usize) -> IntVal {
self.0[dim].0
}
fn min_mut(&mut self, dim: usize) -> &mut IntVal {
&mut self.0[dim].0
}
fn overlaps(&self, other_lb: &[IntVal], other_ub: &[IntVal]) -> bool {
debug_assert_eq!(self.0.len(), other_lb.len());
debug_assert_eq!(self.0.len(), other_ub.len());
self.0
.iter()
.zip(other_lb.iter().zip(other_ub))
.all(|(&(lb, ub), (&o_lb, &o_ub))| o_lb <= ub && o_ub >= lb)
}
fn with_dimensions(dimensions: usize) -> Self {
Self(vec![(IntVal::default(), IntVal::default()); dimensions])
}
}
#[cfg(test)]
mod tests {
use expect_test::expect;
use itertools::Itertools;
use tracing_test::traced_test;
use crate::model::Model;
#[test]
#[traced_test]
fn no_overlap_sat_2d() {
let mut prb = Model::default();
let x1 = prb.new_int_decision(1..=3);
let y1 = prb.new_int_decision(1..=3);
let x2 = prb.new_int_decision(1..=1);
let y2 = prb.new_int_decision(1..=1);
let size = prb.new_int_decision(2..=2);
prb.no_overlap()
.origins(vec![vec![x1, y1], vec![x2, y2]])
.sizes(vec![vec![size, size], vec![size, size]])
.strict(true)
.post()
.unwrap();
let (mut slv, map) = prb.lower().to_solver().unwrap();
let vars = vec![x1, y1, x2, y2]
.into_iter()
.map(|x| map.get(&mut slv, x))
.collect_vec();
slv.expect_solutions(
&vars,
expect![[r#"
1, 3, 1, 1
2, 3, 1, 1
3, 1, 1, 1
3, 2, 1, 1
3, 3, 1, 1"#]],
);
}
#[test]
#[traced_test]
fn no_overlap_sat_2d_nonstrict() {
let mut prb = Model::default();
let x1 = prb.new_int_decision(1..=3);
let y1 = prb.new_int_decision(1..=1);
let x2 = prb.new_int_decision(2..=3);
let y2 = prb.new_int_decision(1..=1);
let size1 = prb.new_int_decision(2..=2);
let size2 = prb.new_int_decision(0..=0);
prb.no_overlap()
.origins(vec![vec![x1, y1], vec![x2, y2]])
.sizes(vec![vec![size1, size1], vec![size2, size2]])
.strict(false)
.post()
.unwrap();
let (mut slv, map) = prb.lower().to_solver().unwrap();
let vars = vec![x1, y1, x2, y2]
.into_iter()
.map(|x| map.get(&mut slv, x))
.collect_vec();
slv.expect_solutions(
&vars,
expect![[r#"
1, 1, 2, 1
1, 1, 3, 1
2, 1, 2, 1
2, 1, 3, 1
3, 1, 2, 1
3, 1, 3, 1"#]],
);
}
#[test]
#[traced_test]
fn no_overlap_sat_3d() {
let mut prb = Model::default();
let x1 = prb.new_int_decision(2..=3);
let y1 = prb.new_int_decision(5..=5);
let z1 = prb.new_int_decision(2..=3);
let x2 = prb.new_int_decision(1..=3);
let y2 = prb.new_int_decision(4..=4);
let z2 = prb.new_int_decision(2..=7);
let size = prb.new_int_decision(5..=5);
prb.no_overlap()
.origins(vec![vec![x1, y1, z1], vec![x2, y2, z2]])
.sizes(vec![vec![size, size, size], vec![size, size, size]])
.strict(true)
.post()
.unwrap();
let (mut slv, map) = prb.lower().to_solver().unwrap();
let vars = vec![x1, y1, z1, x2, y2, z2]
.into_iter()
.map(|v| map.get(&mut slv, v))
.collect_vec();
slv.expect_solutions(
&vars,
expect![[r#"
2, 5, 2, 1, 4, 7
2, 5, 2, 2, 4, 7
2, 5, 2, 3, 4, 7
3, 5, 2, 1, 4, 7
3, 5, 2, 2, 4, 7
3, 5, 2, 3, 4, 7"#]],
);
}
#[test]
#[traced_test]
fn no_overlap_unsat() {
let mut prb = Model::default();
let x1 = prb.new_int_decision(1..=2);
let y1 = prb.new_int_decision(1..=2);
let x2 = prb.new_int_decision(1..=2);
let y2 = prb.new_int_decision(1..=2);
let size = prb.new_int_decision(4..=4);
assert!(
prb.no_overlap()
.origins(vec![vec![x1, y1], vec![x2, y2]])
.sizes(vec![vec![size, size], vec![size, size]])
.strict(true)
.post()
.is_err()
);
}
}