From 8d874d32426db783a0072c038107aae8f9dbe09e Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Mon, 5 Oct 2026 09:14:46 +0200 Subject: [PATCH 1/2] refactor: unify the time-table test suite into a single one --- Cargo.lock | 30 + .../propagators/cumulative/time_table/mod.rs | 3 + .../time_table_over_interval_incremental.rs | 612 -------- .../time_table_per_point_incremental.rs | 1235 ----------------- .../cumulative/time_table/test_suite.rs | 598 ++++++++ .../time_table/time_table_over_interval.rs | 571 -------- .../time_table/time_table_per_point.rs | 598 -------- 7 files changed, 631 insertions(+), 3016 deletions(-) create mode 100644 pumpkin-crates/propagators/src/propagators/cumulative/time_table/test_suite.rs diff --git a/Cargo.lock b/Cargo.lock index 4bace134f..2faf97b89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1464,6 +1464,36 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.15+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs index b542aa54e..efb01f295 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/mod.rs @@ -65,6 +65,9 @@ mod time_table_over_interval; mod time_table_per_point; mod time_table_util; +#[cfg(test)] +mod test_suite; + pub use checker::*; pub use explanations::CumulativeExplanationType; pub use over_interval_incremental_propagator::*; diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs index c9ab2adb9..2d7693a80 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs @@ -638,615 +638,3 @@ fn find_overlapping_profile( std::cmp::Ordering::Greater }) } - -#[cfg(test)] -mod tests { - #[allow( - deprecated, - reason = "TestSolver is deprecated but still used in these tests" - )] - use pumpkin_core::TestSolver; - use pumpkin_core::conjunction; - use pumpkin_core::predicate; - use pumpkin_core::predicates::Predicate; - use pumpkin_core::predicates::PropositionalConjunction; - use pumpkin_core::propagation::CurrentNogood; - use pumpkin_core::propagation::EnqueueDecision; - use pumpkin_core::state::Conflict; - use pumpkin_core::state::State; - use pumpkin_core::variables::DomainId; - - use crate::StateExt; - use crate::cumulative::ArgTask; - use crate::cumulative::options::CumulativePropagatorOptions; - use crate::cumulative::time_table::CumulativeExplanationType; - use crate::cumulative::time_table::TimeTableOverIntervalIncrementalPropagator; - - #[test] - fn propagator_propagates_from_profile() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - - state.propagate_to_fixed_point().expect("No conflict"); - - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - } - - #[test] - fn propagator_detects_conflict() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - - let Conflict::Propagator(conflict) = state.propagate_to_fixed_point().unwrap_err() else { - panic!("an explicit conflict should be detected"); - }; - - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 >= 1), - predicate!(s2 <= 1), - ]; - - assert!(expected.iter().all(|y| { - conflict - .conjunction - .iter() - .collect::>() - .contains(&y) - })); - - assert!(conflict.conjunction.iter().all(|y| expected.contains(y))); - } - - #[test] - fn propagator_propagates_nothing() { - let mut state = State::default(); - let s1 = state.new_interval_variable(0, 6, None); - let s2 = state.new_interval_variable(0, 6, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 0, 6); - state.assert_bounds(s2, 0, 6); - } - - #[test] - fn propagator_propagates_example_4_3_schutt() { - let mut state = State::default(); - let f = state.new_interval_variable(0, 14, None); - let e = state.new_interval_variable(2, 4, None); - let d = state.new_interval_variable(0, 2, None); - let c = state.new_interval_variable(8, 9, None); - let b = state.new_interval_variable(2, 3, None); - let a = state.new_interval_variable(0, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - assert_eq!(state.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_after_assignment() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(6, 10); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 6); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 7); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 5); - assert_eq!(solver.upper_bound(s1), 6); - } - - #[test] - fn propagator_propagates_end_time() { - let mut state = State::default(); - let s1 = state.new_interval_variable(6, 6, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 6, 6); - state.assert_bounds(s2, 1, 3); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 <= 3), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 8] & [s1 >= 6] & [s1 <= 6]), reason); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_after_update() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(b), 2); - assert_eq!(solver.upper_bound(b), 3); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_multiple_profiles() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b2 = solver.new_variable(5, 5); - let b1 = solver.new_variable(3, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b1, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: b2, - processing_time: 3, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - fn propagator_propagates_from_profile_reason() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 >= 5), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), reason); - } - - #[test] - fn propagator_propagates_generic_bounds() { - let mut state = State::default(); - let s1 = state.new_interval_variable(3, 3, None); - let s2 = state.new_interval_variable(5, 5, None); - let s3 = state.new_interval_variable(1, 15, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 3, 3); - state.assert_bounds(s2, 5, 5); - state.assert_bounds(s3, 7, 15); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s3 >= 7), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 5]), reason); - } - - #[test] - fn propagator_propagates_with_holes() { - let mut state = State::default(); - let s1 = state.new_interval_variable(4, 4, None); - let s2 = state.new_interval_variable(0, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTableOverIntervalIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - - state.assert_bounds(s1, 4, 4); - state.assert_bounds(s2, 0, 8); - - for removed in 2..8 { - assert!(!state.contains(s2, removed)); - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 != removed), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); - } - } -} diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs index 99e2752e3..57c66c6f8 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs @@ -633,1238 +633,3 @@ mod debug { }) } } - -#[cfg(test)] -mod tests { - #[allow( - deprecated, - reason = "TestSolver is deprecated but still used in these tests" - )] - use pumpkin_core::TestSolver; - use pumpkin_core::conjunction; - use pumpkin_core::predicate; - use pumpkin_core::predicates::Predicate; - use pumpkin_core::predicates::PropositionalConjunction; - use pumpkin_core::propagation::CurrentNogood; - use pumpkin_core::propagation::EnqueueDecision; - use pumpkin_core::state::Conflict; - use pumpkin_core::state::State; - use pumpkin_core::variables::DomainId; - - use crate::StateExt; - use crate::cumulative::ArgTask; - use crate::cumulative::options::CumulativePropagatorOptions; - use crate::cumulative::time_table::CumulativeExplanationType; - use crate::cumulative::time_table::TimeTablePerPointIncrementalPropagator; - use crate::cumulative::time_table::TimeTablePerPointPropagator; - - #[test] - fn propagator_propagates_from_profile() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - } - - #[test] - fn propagator_detects_conflict() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { - panic!("an explicit conflict should have been detected"); - }; - - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 <= 1), - predicate!(s2 >= 1), - ]; - - assert!(expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - })); - - assert!(x.conjunction.iter().all(|y| expected.contains(y))); - } - - #[test] - fn propagator_propagates_nothing() { - let mut state = State::default(); - let s1 = state.new_interval_variable(0, 6, None); - let s2 = state.new_interval_variable(0, 6, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 0, 6); - state.assert_bounds(s2, 0, 6); - } - - #[test] - fn propagator_propagates_example_4_3_schutt() { - let mut state = State::default(); - let f = state.new_interval_variable(0, 14, None); - let e = state.new_interval_variable(2, 4, None); - let d = state.new_interval_variable(0, 2, None); - let c = state.new_interval_variable(8, 9, None); - let b = state.new_interval_variable(2, 3, None); - let a = state.new_interval_variable(0, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - assert_eq!(state.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_after_assignment() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(6, 10); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 6); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 7); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 5); - assert_eq!(solver.upper_bound(s1), 6); - } - - #[test] - fn propagator_propagates_end_time() { - let mut state = State::default(); - let s1 = state.new_interval_variable(6, 6, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 6, 6); - state.assert_bounds(s2, 1, 3); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 <= 3), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 5] & [s1 >= 6] & [s1 <= 6]), reason); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_after_update() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(b), 2); - assert_eq!(solver.upper_bound(b), 3); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_multiple_profiles() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b2 = solver.new_variable(5, 5); - let b1 = solver.new_variable(3, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b1, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: b2, - processing_time: 3, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - ), - ) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - fn propagator_propagates_from_profile_reason() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 >= 5), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!( - conjunction!([s2 >= 4] & [s1 >= 1] & [s1 <= 1]), /* Note that this not - * the most general - * explanation, if s2 - * could have started at - * 0 then it would still - * have - * overlapped with the - * current interval */ - reason - ); - } - - #[test] - fn propagator_propagates_generic_bounds() { - let mut state = State::default(); - let s1 = state.new_interval_variable(3, 3, None); - let s2 = state.new_interval_variable(5, 5, None); - let s3 = state.new_interval_variable(1, 15, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 3, 3); - state.assert_bounds(s2, 5, 5); - state.assert_bounds(s3, 7, 15); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s3 >= 7), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!( - conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 6]), /* Note that s3 would - * have been able to - * propagate - * this bound even if it - * started at time 0 */ - reason - ); - } - - #[test] - fn propagator_propagates_with_holes() { - let mut state = State::default(); - let s1 = state.new_interval_variable(4, 4, None); - let s2 = state.new_interval_variable(0, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() - }, - constraint_tag, - ), - ); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 4, 4); - state.assert_bounds(s2, 0, 8); - - for removed in 2..8 { - assert!(!state.contains(s2, removed)); - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 != removed), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); - } - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn synchronisation_leads_to_same_conflict_explanation() { - let mut solver_scratch = TestSolver::default(); - let s1_scratch = solver_scratch.new_variable(5, 5); - let s2_scratch = solver_scratch.new_variable(1, 10); - let s3_scratch = solver_scratch.new_variable(1, 10); - let constraint_tag = solver_scratch.new_constraint_tag(); - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1_scratch, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3_scratch, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s2_scratch, 7); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 2, s3_scratch, 7); - let result_scratch = solver_scratch.propagate(propagator_scratch); - - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(5, 5); - let s2 = solver.new_variable(1, 10); - let s3 = solver.new_variable(1, 10); - let constraint_tag = solver.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 2, s3, 7); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s2, 7); - let result = solver.propagate(propagator); - assert!( - { - if let Err(Conflict::Propagator(conflict)) = &result { - if let Err(Conflict::Propagator(explanation_scratch)) = &result_scratch { - conflict.conjunction.iter().collect::>() - == explanation_scratch.conjunction.iter().collect::>() - } else { - false - } - } else { - false - } - }, - "The results are different than expected - Expected: {result_scratch:?} but was: {result:?}" - ); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn synchronisation_leads_to_same_conflict_after_propagating() { - let mut solver_scratch = TestSolver::default(); - let s1_scratch = solver_scratch.new_variable(5, 5); - let s2_scratch = solver_scratch.new_variable(1, 10); - let s3_scratch = solver_scratch.new_variable(1, 10); - let constraint_tag = solver_scratch.new_constraint_tag(); - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1_scratch, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3_scratch, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 2, s3_scratch, 7); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s2_scratch, 7); - - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(5, 5); - let s2 = solver.new_variable(1, 10); - let s3 = solver.new_variable(1, 10); - let constraint_tag = solver.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 2, s3, 7); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s2, 7); - let result = solver.propagate(propagator); - assert!(result.is_err()); - let result_scratch = solver_scratch.propagate(propagator_scratch); - assert!(result_scratch.is_err()); - assert!({ - if let Err(Conflict::Propagator(explanation)) = &result { - if let Err(Conflict::Propagator(explanation_scratch)) = &result_scratch { - explanation.conjunction.iter().collect::>() - == explanation_scratch.conjunction.iter().collect::>() - } else { - false - } - } else { - false - } - }); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn no_synchronisation_leads_to_different_conflict_explanation() { - let mut solver_scratch = TestSolver::default(); - let s1_scratch = solver_scratch.new_variable(5, 5); - let s2_scratch = solver_scratch.new_variable(1, 10); - let s3_scratch = solver_scratch.new_variable(1, 10); - let constraint_tag = solver_scratch.new_constraint_tag(); - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1_scratch, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3_scratch, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 2, s3_scratch, 7); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s2_scratch, 7); - - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(5, 5); - let s2 = solver.new_variable(1, 10); - let s3 = solver.new_variable(1, 10); - let constraint_tag = solver.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 2, s3, 7); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s2, 7); - let result = solver.propagate(propagator); - let result_scratch = solver_scratch.propagate(propagator_scratch); - assert!({ - if let Err(Conflict::Propagator(explanation)) = &result { - if let Err(Conflict::Propagator(explanation_scratch)) = &result_scratch { - explanation.conjunction.iter().collect::>() - != explanation_scratch.conjunction.iter().collect::>() - } else { - false - } - } else { - false - } - }); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn synchronisation_leads_to_same_explanation() { - let mut solver_scratch = TestSolver::default(); - let s1_scratch = solver_scratch.new_variable(1, 6); - let s2_scratch = solver_scratch.new_variable(1, 6); - let s3_scratch = solver_scratch.new_variable(5, 11); - let constraint_tag = solver_scratch.new_constraint_tag(); - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1_scratch, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3_scratch, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 2, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s2_scratch, 5); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 0, s1_scratch, 5); - - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 6); - let s2 = solver.new_variable(1, 6); - let s3 = solver.new_variable(5, 11); - let constraint_tag = solver.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 2, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s2, 5); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - let _ = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - let result_scratch = solver_scratch.propagate(propagator_scratch); - assert!(result_scratch.is_ok()); - assert_eq!(solver_scratch.lower_bound(s3_scratch), 7); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s3), 7); - let reason_scratch = solver_scratch.get_reason_int(predicate!(s3_scratch >= 7)); - let reason = solver.get_reason_int(predicate!(s3 >= 7)); - assert_eq!( - reason_scratch.iter().collect::>(), - reason.iter().collect::>() - ); - } - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn no_synchronisation_leads_to_different_explanation() { - let mut solver_scratch = TestSolver::default(); - let s1_scratch = solver_scratch.new_variable(1, 6); - let s2_scratch = solver_scratch.new_variable(1, 6); - let s3_scratch = solver_scratch.new_variable(5, 11); - let constraint_tag = solver_scratch.new_constraint_tag(); - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1_scratch, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3_scratch, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 2, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s2_scratch, 5); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 0, s1_scratch, 5); - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(1, 6); - let s2 = solver.new_variable(1, 6); - let s3 = solver.new_variable(5, 11); - let constraint_tag = solver.new_constraint_tag(); - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 2, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s2, 5); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - let _ = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - - let result_scratch = solver_scratch.propagate(propagator_scratch); - assert!(result_scratch.is_ok()); - assert_eq!(solver_scratch.lower_bound(s3_scratch), 7); - - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s3), 7); - - let reason_scratch = solver_scratch.get_reason_int(predicate!(s3_scratch >= 7)); - let reason = solver.get_reason_int(predicate!(s3 >= 7)); - assert_ne!( - reason_scratch.iter().collect::>(), - reason.iter().collect::>() - ); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn synchronisation_leads_to_same_conflict() { - let mut solver_scratch = TestSolver::default(); - let s0_scratch = solver_scratch.new_variable(1, 11); - let s1_scratch = solver_scratch.new_variable(1, 5); - let s2_scratch = solver_scratch.new_variable(1, 5); - let constraint_tag = solver_scratch.new_constraint_tag(); - - let propagator_scratch = solver_scratch - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s0_scratch, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s1_scratch, - processing_time: 1, - resource_usage: 1, - }, - ArgTask { - start_time: s2_scratch, - processing_time: 1, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )) - .expect("No conflict"); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 2, s2_scratch, 5); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 1, s1_scratch, 5); - let _ = - solver_scratch.increase_lower_bound_and_notify(propagator_scratch, 0, s0_scratch, 5); - let _ = - solver_scratch.decrease_upper_bound_and_notify(propagator_scratch, 0, s0_scratch, 5); - - let mut solver = TestSolver::default(); - let s0 = solver.new_variable(1, 11); - let s1 = solver.new_variable(1, 5); - let s2 = solver.new_variable(1, 5); - - let propagator = solver - .new_propagator( - TimeTablePerPointIncrementalPropagator::::new( - &[ - ArgTask { - start_time: s0, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s1, - processing_time: 1, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 1, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - ), - ) - .expect("No conflict"); - let _ = solver.increase_lower_bound_and_notify(propagator, 2, s2, 5); - let _ = solver.increase_lower_bound_and_notify(propagator, 1, s1, 5); - let _ = solver.increase_lower_bound_and_notify(propagator, 0, s0, 5); - let _ = solver.decrease_upper_bound_and_notify(propagator, 0, s0, 5); - - let result_scratch = solver_scratch.propagate(propagator_scratch); - assert!(result_scratch.is_err()); - let result = solver.propagate(propagator); - assert!(result.is_err()); - if let ( - Err(Conflict::Propagator(explanation)), - Err(Conflict::Propagator(explanation_scratch)), - ) = (result, result_scratch) - { - assert_eq!( - explanation.conjunction.iter().collect::>(), - explanation_scratch.conjunction.iter().collect::>() - ); - } else { - panic!("Incorrect result") - } - } -} diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/test_suite.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/test_suite.rs new file mode 100644 index 000000000..369347518 --- /dev/null +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/test_suite.rs @@ -0,0 +1,598 @@ +//! Tests which are shared between all time-table propagators. +//! +//! Every test is written once as a function which is generic over the propagator under test; the +//! [`time_table_tests`] and [`synchronisation_tests`] macros instantiate them for each propagator. +use pumpkin_core::conjunction; +use pumpkin_core::predicate; +use pumpkin_core::predicates::Predicate; +use pumpkin_core::predicates::PropositionalConjunction; +use pumpkin_core::proof::ConstraintTag; +use pumpkin_core::propagation::CurrentNogood; +use pumpkin_core::propagation::PropagatorConstructor; +use pumpkin_core::state::Conflict; +use pumpkin_core::state::State; +use pumpkin_core::variables::DomainId; + +use crate::StateExt; +use crate::cumulative::ArgTask; +use crate::cumulative::options::CumulativePropagatorOptions; +use crate::cumulative::time_table::CumulativeExplanationType; +use crate::cumulative::time_table::TimeTableOverIntervalIncrementalPropagator; +use crate::cumulative::time_table::TimeTableOverIntervalPropagator; +use crate::cumulative::time_table::TimeTablePerPointIncrementalPropagator; +use crate::cumulative::time_table::TimeTablePerPointPropagator; + +/// The way in which the time-table is constructed; the explanations of the +/// propagators reasoning over intervals can differ from those which reason per point. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TimeTableStrategy { + PerPoint, + OverInterval, +} + +/// A time-table propagator which can be tested by the shared test suite. +trait TimeTableTestCase: PropagatorConstructor + Sized { + const STRATEGY: TimeTableStrategy; + + fn construct( + tasks: &[ArgTask], + capacity: i32, + options: CumulativePropagatorOptions, + constraint_tag: ConstraintTag, + ) -> Self; +} + +impl TimeTableTestCase for TimeTablePerPointPropagator { + const STRATEGY: TimeTableStrategy = TimeTableStrategy::PerPoint; + + fn construct( + tasks: &[ArgTask], + capacity: i32, + options: CumulativePropagatorOptions, + constraint_tag: ConstraintTag, + ) -> Self { + Self::new(tasks, capacity, options, constraint_tag) + } +} + +impl TimeTableTestCase for TimeTableOverIntervalPropagator { + const STRATEGY: TimeTableStrategy = TimeTableStrategy::OverInterval; + + fn construct( + tasks: &[ArgTask], + capacity: i32, + options: CumulativePropagatorOptions, + constraint_tag: ConstraintTag, + ) -> Self { + Self::new(tasks, capacity, options, constraint_tag) + } +} + +impl TimeTableTestCase + for TimeTablePerPointIncrementalPropagator +{ + const STRATEGY: TimeTableStrategy = TimeTableStrategy::PerPoint; + + fn construct( + tasks: &[ArgTask], + capacity: i32, + options: CumulativePropagatorOptions, + constraint_tag: ConstraintTag, + ) -> Self { + Self::new(tasks, capacity, options, constraint_tag) + } +} + +impl TimeTableTestCase + for TimeTableOverIntervalIncrementalPropagator +{ + const STRATEGY: TimeTableStrategy = TimeTableStrategy::OverInterval; + + fn construct( + tasks: &[ArgTask], + capacity: i32, + options: CumulativePropagatorOptions, + constraint_tag: ConstraintTag, + ) -> Self { + Self::new(tasks, capacity, options, constraint_tag) + } +} + +fn naive_options() -> CumulativePropagatorOptions { + CumulativePropagatorOptions { + explanation_type: CumulativeExplanationType::Naive, + ..Default::default() + } +} + +/// Adds propagator `P` over the tasks, given as `(start_time, processing_time, resource_usage)`, +/// to `state` and propagates to a fixed point. +fn add_propagator( + state: &mut State, + tasks: &[(DomainId, i32, i32)], + capacity: i32, + options: CumulativePropagatorOptions, +) -> Result<(), Conflict> { + let tasks = tasks + .iter() + .map(|&(start_time, processing_time, resource_usage)| ArgTask { + start_time, + processing_time, + resource_usage, + }) + .collect::>(); + let constraint_tag = state.new_constraint_tag(); + let _ = state.add_propagator(P::construct(&tasks, capacity, options, constraint_tag)); + state.propagate_to_fixed_point() +} + +fn reason_for(state: &mut State, predicate: Predicate) -> PropositionalConjunction { + let mut reason_buffer: Vec = vec![]; + let _ = state.get_propagation_reason(predicate, &mut reason_buffer, CurrentNogood::empty()); + reason_buffer.into() +} + +fn conflict_explanation(result: Result<(), Conflict>) -> PropositionalConjunction { + let Err(Conflict::Propagator(conflict)) = result else { + panic!("an explicit conflict should have been detected, but was {result:?}"); + }; + conflict.conjunction +} + +fn propagator_propagates_from_profile() { + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + + add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 3, 1)], 1, Default::default()) + .expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); +} + +fn propagator_detects_conflict() { + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 1, None); + + let result = add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 4, 1)], 1, naive_options()); + assert_eq!( + conjunction!([s1 <= 1] & [s1 >= 1] & [s2 <= 1] & [s2 >= 1]), + conflict_explanation(result) + ); +} + +fn propagator_propagates_nothing() { + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(0, 6, None); + + add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 3, 1)], 1, Default::default()) + .expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 0, 6); +} + +fn propagator_propagates_example_4_3_schutt() { + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(2, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + + add_propagator::

( + &mut state, + &[ + (a, 2, 1), + (b, 6, 2), + (c, 2, 4), + (d, 2, 2), + (e, 5, 2), + (f, 6, 2), + ], + 5, + Default::default(), + ) + .expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); +} + +fn propagator_propagates_after_assignment() { + let mut state = State::default(); + let s1 = state.new_interval_variable(0, 6, None); + let s2 = state.new_interval_variable(6, 10, None); + + add_propagator::

(&mut state, &[(s1, 2, 1), (s2, 3, 1)], 1, Default::default()) + .expect("No conflict"); + state.assert_bounds(s1, 0, 6); + state.assert_bounds(s2, 6, 10); + + let _ = state.post(predicate!(s1 >= 5)).expect("No empty domain"); + state.propagate_to_fixed_point().expect("No conflict"); + state.assert_bounds(s1, 5, 6); + state.assert_bounds(s2, 7, 10); +} + +fn propagator_propagates_end_time() { + let mut state = State::default(); + let s1 = state.new_interval_variable(6, 6, None); + let s2 = state.new_interval_variable(1, 8, None); + + add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 3, 1)], 1, naive_options()) + .expect("No conflict"); + state.assert_bounds(s1, 6, 6); + state.assert_bounds(s2, 1, 3); + + let expected = match P::STRATEGY { + TimeTableStrategy::PerPoint => conjunction!([s2 <= 5] & [s1 >= 6] & [s1 <= 6]), + TimeTableStrategy::OverInterval => conjunction!([s2 <= 8] & [s1 >= 6] & [s1 <= 6]), + }; + assert_eq!(expected, reason_for(&mut state, predicate!(s2 <= 3))); +} + +fn propagator_propagates_example_4_3_schutt_after_update() { + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(0, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b = state.new_interval_variable(2, 3, None); + let a = state.new_interval_variable(0, 1, None); + + add_propagator::

( + &mut state, + &[ + (a, 2, 1), + (b, 6, 2), + (c, 2, 4), + (d, 2, 2), + (e, 4, 2), + (f, 6, 2), + ], + 5, + Default::default(), + ) + .expect("No conflict"); + state.assert_bounds(a, 0, 1); + state.assert_bounds(b, 2, 3); + state.assert_bounds(c, 8, 9); + state.assert_bounds(d, 0, 2); + state.assert_bounds(e, 0, 4); + state.assert_bounds(f, 0, 14); + + let _ = state.post(predicate!(e >= 3)).expect("No empty domain"); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); +} + +fn propagator_propagates_example_4_3_schutt_multiple_profiles() { + let mut state = State::default(); + let f = state.new_interval_variable(0, 14, None); + let e = state.new_interval_variable(0, 4, None); + let d = state.new_interval_variable(0, 2, None); + let c = state.new_interval_variable(8, 9, None); + let b2 = state.new_interval_variable(5, 5, None); + let b1 = state.new_interval_variable(3, 3, None); + let a = state.new_interval_variable(0, 1, None); + + add_propagator::

( + &mut state, + &[ + (a, 2, 1), + (b1, 2, 2), + (b2, 3, 2), + (c, 2, 4), + (d, 2, 2), + (e, 4, 2), + (f, 6, 2), + ], + 5, + Default::default(), + ) + .expect("No conflict"); + state.assert_bounds(a, 0, 1); + state.assert_bounds(c, 8, 9); + state.assert_bounds(d, 0, 2); + state.assert_bounds(e, 0, 4); + state.assert_bounds(f, 0, 14); + + let _ = state.post(predicate!(e >= 3)).expect("No empty domain"); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(f), 10); +} + +fn propagator_propagates_from_profile_reason() { + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 1, None); + let s2 = state.new_interval_variable(1, 8, None); + + add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 3, 1)], 1, naive_options()) + .expect("No conflict"); + state.assert_bounds(s1, 1, 1); + state.assert_bounds(s2, 5, 8); + + let expected = match P::STRATEGY { + // Note that this not the most general explanation, if s2 could have started at 0 then it + // would still have overlapped with the current interval + TimeTableStrategy::PerPoint => conjunction!([s2 >= 4] & [s1 >= 1] & [s1 <= 1]), + TimeTableStrategy::OverInterval => conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), + }; + assert_eq!(expected, reason_for(&mut state, predicate!(s2 >= 5))); +} + +fn propagator_propagates_generic_bounds() { + let mut state = State::default(); + let s1 = state.new_interval_variable(3, 3, None); + let s2 = state.new_interval_variable(5, 5, None); + let s3 = state.new_interval_variable(1, 15, None); + + add_propagator::

( + &mut state, + &[(s1, 2, 1), (s2, 2, 1), (s3, 4, 1)], + 1, + naive_options(), + ) + .expect("No conflict"); + state.assert_bounds(s1, 3, 3); + state.assert_bounds(s2, 5, 5); + state.assert_bounds(s3, 7, 15); + + let expected = match P::STRATEGY { + // Note that s3 would have been able to propagate this bound even if it started at time 0 + TimeTableStrategy::PerPoint => conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 6]), + TimeTableStrategy::OverInterval => conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 5]), + }; + assert_eq!(expected, reason_for(&mut state, predicate!(s3 >= 7))); +} + +fn propagator_propagates_with_holes() { + let mut state = State::default(); + let s1 = state.new_interval_variable(4, 4, None); + let s2 = state.new_interval_variable(0, 8, None); + + let options = CumulativePropagatorOptions { + allow_holes_in_domain: true, + ..naive_options() + }; + add_propagator::

(&mut state, &[(s1, 4, 1), (s2, 3, 1)], 1, options).expect("No conflict"); + state.assert_bounds(s1, 4, 4); + state.assert_bounds(s2, 0, 8); + + for removed in 2..8 { + assert!(!state.contains(s2, removed)); + assert_eq!( + conjunction!([s1 <= 4] & [s1 >= 4]), + reason_for(&mut state, predicate!(s2 != removed)) + ); + } +} + +// The scenarios below return the explanations as a `Vec` rather than a `PropositionalConjunction` +// since the order of the predicates matters; without synchronisation, the explanations of the +// incremental propagators only differ in the order of the predicates. + +/// Returns the explanation of the conflict found by `P` after two tasks are pushed onto a fixed +/// task; when `reverse_updates` is true, the bounds are updated in the opposite order. +fn conflict_after_updates(reverse_updates: bool) -> Vec { + let mut state = State::default(); + let s1 = state.new_interval_variable(5, 5, None); + let s2 = state.new_interval_variable(1, 10, None); + let s3 = state.new_interval_variable(1, 10, None); + add_propagator::

( + &mut state, + &[(s1, 2, 1), (s2, 4, 1), (s3, 4, 1)], + 1, + naive_options(), + ) + .expect("No conflict"); + + let mut updates = [predicate!(s3 >= 7), predicate!(s2 >= 7)]; + if reverse_updates { + updates.reverse(); + } + for update in updates { + let _ = state.post(update).expect("No empty domain"); + } + conflict_explanation(state.propagate_to_fixed_point()) + .iter() + .copied() + .collect() +} + +/// Returns the reason for the lower-bound propagation which `P` performs after two updates; when +/// `propagate_in_between` is true, `P` propagates after each update. +fn explanation_after_updates(propagate_in_between: bool) -> Vec { + let mut state = State::default(); + let s1 = state.new_interval_variable(1, 6, None); + let s2 = state.new_interval_variable(1, 6, None); + let s3 = state.new_interval_variable(5, 11, None); + add_propagator::

( + &mut state, + &[(s1, 2, 1), (s2, 4, 1), (s3, 4, 1)], + 2, + naive_options(), + ) + .expect("No conflict"); + + let _ = state.post(predicate!(s2 >= 5)).expect("No empty domain"); + if propagate_in_between { + state.propagate_to_fixed_point().expect("No conflict"); + } + let _ = state.post(predicate!(s1 >= 5)).expect("No empty domain"); + state.propagate_to_fixed_point().expect("No conflict"); + assert_eq!(state.lower_bound(s3), 7); + + reason_for(&mut state, predicate!(s3 >= 7)) + .iter() + .copied() + .collect() +} + +/// Returns the explanation of the conflict found by `P` after a task is fixed onto two others. +fn conflict_after_fixing() -> Vec { + let mut state = State::default(); + let s0 = state.new_interval_variable(1, 11, None); + let s1 = state.new_interval_variable(1, 5, None); + let s2 = state.new_interval_variable(1, 5, None); + add_propagator::

( + &mut state, + &[(s0, 4, 1), (s1, 1, 1), (s2, 1, 1)], + 1, + naive_options(), + ) + .expect("No conflict"); + + for update in [ + predicate!(s2 >= 5), + predicate!(s1 >= 5), + predicate!(s0 >= 5), + predicate!(s0 <= 5), + ] { + let _ = state.post(update).expect("No empty domain"); + } + conflict_explanation(state.propagate_to_fixed_point()) + .iter() + .copied() + .collect() +} + +/// Instantiates the shared tests for each provided propagator type, in a module with the given +/// name. +macro_rules! time_table_tests { + ($($module:ident: $propagator:ty),* $(,)?) => { + $( + mod $module { + use super::*; + + #[test] + fn propagator_propagates_from_profile() { + super::propagator_propagates_from_profile::<$propagator>() + } + + #[test] + fn propagator_detects_conflict() { + super::propagator_detects_conflict::<$propagator>() + } + + #[test] + fn propagator_propagates_nothing() { + super::propagator_propagates_nothing::<$propagator>() + } + + #[test] + fn propagator_propagates_example_4_3_schutt() { + super::propagator_propagates_example_4_3_schutt::<$propagator>() + } + + #[test] + fn propagator_propagates_after_assignment() { + super::propagator_propagates_after_assignment::<$propagator>() + } + + #[test] + fn propagator_propagates_end_time() { + super::propagator_propagates_end_time::<$propagator>() + } + + #[test] + fn propagator_propagates_example_4_3_schutt_after_update() { + super::propagator_propagates_example_4_3_schutt_after_update::<$propagator>() + } + + #[test] + fn propagator_propagates_example_4_3_schutt_multiple_profiles() { + super::propagator_propagates_example_4_3_schutt_multiple_profiles::<$propagator>() + } + + #[test] + fn propagator_propagates_from_profile_reason() { + super::propagator_propagates_from_profile_reason::<$propagator>() + } + + #[test] + fn propagator_propagates_generic_bounds() { + super::propagator_propagates_generic_bounds::<$propagator>() + } + + #[test] + fn propagator_propagates_with_holes() { + super::propagator_propagates_with_holes::<$propagator>() + } + } + )* + }; +} + +/// Instantiates the tests comparing an incremental propagator (with and without synchronisation) +/// against its non-incremental counterpart, in a module with the given name. +macro_rules! synchronisation_tests { + ($($module:ident: $scratch:ident, $incremental:ident);* $(;)?) => { + $( + mod $module { + use super::*; + + type Scratch = $scratch; + type Synced = $incremental; + type Unsynced = $incremental; + + #[test] + fn synchronisation_leads_to_same_conflict_explanation() { + // The incremental propagator is notified in a different order than the + // scratch propagator + assert_eq!( + conflict_after_updates::(true), + conflict_after_updates::(false) + ); + } + + #[test] + fn no_synchronisation_leads_to_different_conflict_explanation() { + assert_ne!( + conflict_after_updates::(false), + conflict_after_updates::(false) + ); + } + + #[test] + fn synchronisation_leads_to_same_explanation() { + assert_eq!( + explanation_after_updates::(false), + explanation_after_updates::(true) + ); + } + + #[test] + fn no_synchronisation_leads_to_different_explanation() { + assert_ne!( + explanation_after_updates::(false), + explanation_after_updates::(true) + ); + } + + #[test] + fn synchronisation_leads_to_same_conflict() { + assert_eq!( + conflict_after_fixing::(), + conflict_after_fixing::() + ); + } + } + )* + }; +} + +time_table_tests! { + per_point: TimeTablePerPointPropagator, + over_interval: TimeTableOverIntervalPropagator, + per_point_incremental: TimeTablePerPointIncrementalPropagator, + per_point_incremental_synchronised: TimeTablePerPointIncrementalPropagator, + over_interval_incremental: TimeTableOverIntervalIncrementalPropagator, + over_interval_incremental_synchronised: TimeTableOverIntervalIncrementalPropagator, +} + +synchronisation_tests! { + per_point_synchronisation: TimeTablePerPointPropagator, TimeTablePerPointIncrementalPropagator; + over_interval_synchronisation: TimeTableOverIntervalPropagator, TimeTableOverIntervalIncrementalPropagator; +} diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs index fc213b2f0..750fc8103 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs @@ -485,574 +485,3 @@ pub(crate) fn propagate_from_scratch_time_table_interval>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - } - - #[test] - fn propagator_detects_conflict() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - - let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { - panic!("an explicit conflict should have been detected"); - }; - - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 >= 1), - predicate!(s2 <= 1), - ]; - - assert!(expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - })); - - assert!(x.conjunction.iter().all(|y| expected.contains(y))); - } - - #[test] - fn propagator_propagates_nothing() { - let mut state = State::default(); - let s1 = state.new_interval_variable(0, 6, None); - let s2 = state.new_interval_variable(0, 6, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 0, 6); - state.assert_bounds(s2, 0, 6); - } - - #[test] - fn propagator_propagates_example_4_3_schutt() { - let mut state = State::default(); - let f = state.new_interval_variable(0, 14, None); - let e = state.new_interval_variable(2, 4, None); - let d = state.new_interval_variable(0, 2, None); - let c = state.new_interval_variable(8, 9, None); - let b = state.new_interval_variable(2, 3, None); - let a = state.new_interval_variable(0, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - assert_eq!(state.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_after_assignment() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(6, 10); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - solver.assert_bounds(s1, 0, 6); - solver.assert_bounds(s2, 6, 10); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - - let result = solver.propagate(propagator); - assert!(result.is_ok()); - solver.assert_bounds(s1, 5, 6); - solver.assert_bounds(s2, 7, 10); - } - - #[test] - fn propagator_propagates_end_time() { - let mut state = State::default(); - let s1 = state.new_interval_variable(6, 6, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 6, 6); - state.assert_bounds(s2, 1, 3); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 <= 3), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 8] & [s1 >= 6] & [s1 <= 6]), reason); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_multiple_profiles() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b2 = solver.new_variable(5, 5); - let b1 = solver.new_variable(3, 3); - let a = solver.new_variable(0, 1); - - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b1, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: b2, - processing_time: 3, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - solver.assert_bounds(a, 0, 1); - solver.assert_bounds(c, 8, 9); - solver.assert_bounds(d, 0, 2); - solver.assert_bounds(e, 0, 4); - solver.assert_bounds(f, 0, 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - fn propagator_propagates_from_profile_reason() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 >= 5), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 >= 1] & [s1 >= 1] & [s1 <= 1]), reason); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_after_update() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - solver.assert_bounds(a, 0, 1); - solver.assert_bounds(b, 2, 3); - solver.assert_bounds(c, 8, 9); - solver.assert_bounds(d, 0, 2); - solver.assert_bounds(e, 0, 4); - solver.assert_bounds(f, 0, 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - fn propagator_propagates_generic_bounds() { - let mut state = State::default(); - let s1 = state.new_interval_variable(3, 3, None); - let s2 = state.new_interval_variable(5, 5, None); - let s3 = state.new_interval_variable(1, 15, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 3, 3); - state.assert_bounds(s2, 5, 5); - state.assert_bounds(s3, 7, 15); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s3 >= 7), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 5]), reason); - } - - #[test] - fn propagator_propagates_with_holes() { - let mut state = State::default(); - let s1 = state.new_interval_variable(4, 4, None); - let s2 = state.new_interval_variable(0, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTableOverIntervalPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 4, 4); - state.assert_bounds(s2, 0, 8); - - for removed in 2..8 { - assert!(!state.contains(s2, removed)); - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 != removed), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); - } - } -} diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs index 3c5a212c9..227a80cad 100644 --- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs +++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs @@ -291,601 +291,3 @@ pub(crate) fn propagate_from_scratch_time_table_point>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - } - - #[test] - fn propagator_detects_conflict() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - let Conflict::Propagator(x) = state.propagate_to_fixed_point().unwrap_err() else { - panic!("an explicit conflict should have been detected"); - }; - let expected = [ - predicate!(s1 <= 1), - predicate!(s1 >= 1), - predicate!(s2 <= 1), - predicate!(s2 >= 1), - ]; - assert!(expected.iter().all(|y| { - x.conjunction - .iter() - .collect::>() - .contains(&y) - })); - assert!(x.conjunction.iter().all(|y| expected.contains(y))); - } - - #[test] - fn propagator_propagates_nothing() { - let mut state = State::default(); - let s1 = state.new_interval_variable(0, 6, None); - let s2 = state.new_interval_variable(0, 6, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 0, 6); - state.assert_bounds(s2, 0, 6); - } - - #[test] - fn propagator_propagates_example_4_3_schutt() { - let mut state = State::default(); - let f = state.new_interval_variable(0, 14, None); - let e = state.new_interval_variable(2, 4, None); - let d = state.new_interval_variable(0, 2, None); - let c = state.new_interval_variable(8, 9, None); - let b = state.new_interval_variable(2, 3, None); - let a = state.new_interval_variable(0, 1, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 5, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - assert_eq!(state.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_after_assignment() { - let mut solver = TestSolver::default(); - let s1 = solver.new_variable(0, 6); - let s2 = solver.new_variable(6, 10); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(s2), 6); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 0); - assert_eq!(solver.upper_bound(s1), 6); - let notification_status = solver.increase_lower_bound_and_notify(propagator, 0, s1, 5); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(s2), 7); - assert_eq!(solver.upper_bound(s2), 10); - assert_eq!(solver.lower_bound(s1), 5); - assert_eq!(solver.upper_bound(s1), 6); - } - - #[test] - fn propagator_propagates_end_time() { - let mut state = State::default(); - let s1 = state.new_interval_variable(6, 6, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 6, 6); - state.assert_bounds(s2, 1, 3); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 <= 3), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s2 <= 5] & [s1 >= 6] & [s1 <= 6]), reason); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_after_update() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b = solver.new_variable(2, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b, - processing_time: 6, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(b), 2); - assert_eq!(solver.upper_bound(b), 3); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 3, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - #[allow( - deprecated, - reason = "Uses TestSolver for incremental notification assertions" - )] - fn propagator_propagates_example_4_3_schutt_multiple_profiles() { - let mut solver = TestSolver::default(); - let f = solver.new_variable(0, 14); - let e = solver.new_variable(0, 4); - let d = solver.new_variable(0, 2); - let c = solver.new_variable(8, 9); - let b2 = solver.new_variable(5, 5); - let b1 = solver.new_variable(3, 3); - let a = solver.new_variable(0, 1); - let constraint_tag = solver.new_constraint_tag(); - - let propagator = solver - .new_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: a, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: b1, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: b2, - processing_time: 3, - resource_usage: 2, - }, - ArgTask { - start_time: c, - processing_time: 2, - resource_usage: 4, - }, - ArgTask { - start_time: d, - processing_time: 2, - resource_usage: 2, - }, - ArgTask { - start_time: e, - processing_time: 4, - resource_usage: 2, - }, - ArgTask { - start_time: f, - processing_time: 6, - resource_usage: 2, - }, - ] - .into_iter() - .collect::>(), - 5, - CumulativePropagatorOptions::default(), - constraint_tag, - )) - .expect("No conflict"); - assert_eq!(solver.lower_bound(a), 0); - assert_eq!(solver.upper_bound(a), 1); - assert_eq!(solver.lower_bound(c), 8); - assert_eq!(solver.upper_bound(c), 9); - assert_eq!(solver.lower_bound(d), 0); - assert_eq!(solver.upper_bound(d), 2); - assert_eq!(solver.lower_bound(e), 0); - assert_eq!(solver.upper_bound(e), 4); - assert_eq!(solver.lower_bound(f), 0); - assert_eq!(solver.upper_bound(f), 14); - - let notification_status = solver.increase_lower_bound_and_notify(propagator, 4, e, 3); - assert!(match notification_status { - EnqueueDecision::Enqueue => true, - EnqueueDecision::Skip => false, - }); - let result = solver.propagate(propagator); - assert!(result.is_ok()); - assert_eq!(solver.lower_bound(f), 10); - } - - #[test] - fn propagator_propagates_from_profile_reason() { - let mut state = State::default(); - let s1 = state.new_interval_variable(1, 1, None); - let s2 = state.new_interval_variable(1, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 1, 1); - state.assert_bounds(s2, 5, 8); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 >= 5), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!( - conjunction!([s2 >= 4] & [s1 >= 1] & [s1 <= 1]), /* Note that this not - * the most general - * explanation, if s2 - * could have started at - * 0 then it would still - * have - * overlapped with the - * current interval */ - reason - ); - } - - #[test] - fn propagator_propagates_generic_bounds() { - let mut state = State::default(); - let s1 = state.new_interval_variable(3, 3, None); - let s2 = state.new_interval_variable(5, 5, None); - let s3 = state.new_interval_variable(1, 15, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 2, - resource_usage: 1, - }, - ArgTask { - start_time: s3, - processing_time: 4, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 3, 3); - state.assert_bounds(s2, 5, 5); - state.assert_bounds(s3, 7, 15); - - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s3 >= 7), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!( - conjunction!([s2 <= 5] & [s2 >= 5] & [s3 >= 6]), /* Note that s3 would - * have been able to - * propagate - * this bound even if it - * started at time 0 */ - reason - ); - } - - #[test] - fn propagator_propagates_with_holes() { - let mut state = State::default(); - let s1 = state.new_interval_variable(4, 4, None); - let s2 = state.new_interval_variable(0, 8, None); - let constraint_tag = state.new_constraint_tag(); - - let _ = state.add_propagator(TimeTablePerPointPropagator::new( - &[ - ArgTask { - start_time: s1, - processing_time: 4, - resource_usage: 1, - }, - ArgTask { - start_time: s2, - processing_time: 3, - resource_usage: 1, - }, - ] - .into_iter() - .collect::>(), - 1, - CumulativePropagatorOptions { - explanation_type: CumulativeExplanationType::Naive, - allow_holes_in_domain: true, - ..Default::default() - }, - constraint_tag, - )); - state.propagate_to_fixed_point().expect("No conflict"); - state.assert_bounds(s1, 4, 4); - state.assert_bounds(s2, 0, 8); - - for removed in 2..8 { - assert!(!state.contains(s2, removed)); - let mut reason_buffer: Vec = vec![]; - let _ = state.get_propagation_reason( - predicate!(s2 != removed), - &mut reason_buffer, - CurrentNogood::empty(), - ); - let reason: PropositionalConjunction = reason_buffer.into(); - assert_eq!(conjunction!([s1 <= 4] & [s1 >= 4]), reason); - } - } -} From d0c40c816fed824e5e29e9091b8773f854b3b80a Mon Sep 17 00:00:00 2001 From: Imko Marijnissen Date: Mon, 5 Oct 2026 09:31:13 +0200 Subject: [PATCH 2/2] chore: fix new clippy issue --- .../src/propagators/disjunctive/checker.rs | 314 +++++++++--------- 1 file changed, 157 insertions(+), 157 deletions(-) diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs index f9d18f51f..2926b6bf0 100644 --- a/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs +++ b/pumpkin-crates/propagators/src/propagators/disjunctive/checker.rs @@ -122,6 +122,163 @@ where } } +#[derive(Debug, Clone)] +pub(super) struct CheckerThetaLambdaTree { + pub(super) nodes: Vec, + /// Then we keep track of a mapping from the [`LocalId`] to its position in the tree since the + /// methods take as input tasks with [`LocalId`]s. + mapping: KeyedVec, + /// The number of internal nodes in the tree; used to calculate the leaf node index based on + /// the index in the tree + number_of_internal_nodes: usize, + /// The tasks which are stored in the leaves of the tree. + /// + /// These tasks are sorted based on non-decreasing start time. + sorted_tasks: Vec>, + phantom_data: PhantomData, +} + +impl> CheckerThetaLambdaTree { + /// Initialises the theta-lambda tree. + /// + /// Note that [`Self::update`] should be called to actually create the tree itself. + pub(super) fn new(tasks: &[DisjunctiveTask]) -> Self { + // Calculate the number of internal nodes which are required to create the binary tree + let mut number_of_internal_nodes = 1; + while number_of_internal_nodes < tasks.len() { + number_of_internal_nodes <<= 1; + } + + CheckerThetaLambdaTree { + nodes: Default::default(), + mapping: KeyedVec::default(), + number_of_internal_nodes: number_of_internal_nodes - 1, + sorted_tasks: tasks.to_vec(), + phantom_data: PhantomData, + } + } + + /// Update the theta-lambda tree based on the provided `context`. + /// + /// It resets theta and lambda to be the empty set. + pub(super) fn update(&mut self, context: &VariableState) { + // First we sort the tasks by lower-bound/earliest start time. + self.sorted_tasks + .sort_by_key(|task| task.start_time.induced_lower_bound(context)); + + // Then we keep track of a mapping from the [`LocalId`] to its position in the tree and a + // reverse mapping + self.mapping.clear(); + for (index, task) in self.sorted_tasks.iter().enumerate() { + while self.mapping.len() <= task.id.index() { + let _ = self.mapping.push(usize::MAX); + } + self.mapping[task.id] = index; + } + + // Finally, we reset the entire tree to be empty + self.nodes.clear(); + for _ in 0..=2 * self.number_of_internal_nodes { + self.nodes.push(Node::empty()) + } + } + + /// Returns the earliest completion time of Theta + pub(super) fn ect(&self) -> i32 { + assert!(!self.nodes.is_empty()); + self.nodes[0].ect + } + + /// Add the provided task to Theta + pub(super) fn add_to_theta( + &mut self, + task: &DisjunctiveTask, + context: &VariableState, + ) { + // We need to find the leaf node index; note that there are |nodes| / 2 leaves + let position = self.nodes.len() / 2 + self.mapping[task.id]; + let ect = task.start_time.induced_lower_bound(context) + task.processing_time; + + self.nodes[position] = Node::new_white_node( + ect.try_into().expect("Should have bounds"), + task.processing_time, + ); + self.upheap(position) + } + + /// Returns the index of the left child of the provided index + fn get_left_child_index(index: usize) -> usize { + 2 * index + 1 + } + + /// Returns the index of the right child of the provided index + fn get_right_child_index(index: usize) -> usize { + 2 * index + 2 + } + + /// Returns the index of the parent of the provided index + fn get_parent(index: usize) -> usize { + assert!(index > 0); + (index - 1) / 2 + } + + /// Calculate the new values for the ancestors of the provided index + pub(super) fn upheap(&mut self, mut index: usize) { + while index != 0 { + let parent = Self::get_parent(index); + let left_child_of_parent = Self::get_left_child_index(parent); + let right_child_of_parent = Self::get_right_child_index(parent); + assert!(left_child_of_parent == index || right_child_of_parent == index); + + // The sum of processing times is the sum of processing times in the left child + the + // sum of processing times in right child + self.nodes[parent].sum_of_processing_times = self.nodes[left_child_of_parent] + .sum_of_processing_times + + self.nodes[right_child_of_parent].sum_of_processing_times; + + // The ECT is either the ECT of the left child node + the processing times of the right + // child or it is the ECT of the right child (we do not know whether the processing + // times of the left child influence the processing times of the right child) + let ect_left = self.nodes[left_child_of_parent].ect + + self.nodes[right_child_of_parent].sum_of_processing_times; + self.nodes[parent].ect = max(self.nodes[right_child_of_parent].ect, ect_left); + + // The sum of processing times (including one element of lambda) is either: + // 1) The sum of processing times of the right child + the sum of processing times of + // the left child including one element of lambda + // 2) The sum of processing times of the left child + the sum of processing times of the + // right child include one element of lambda + let sum_of_processing_times_left_child_lambda = self.nodes[left_child_of_parent] + .sum_of_processing_times_bar + + self.nodes[right_child_of_parent].sum_of_processing_times; + let sum_of_processing_times_right_child_lambda = self.nodes[left_child_of_parent] + .sum_of_processing_times + + self.nodes[right_child_of_parent].sum_of_processing_times_bar; + self.nodes[parent].sum_of_processing_times_bar = max( + sum_of_processing_times_left_child_lambda, + sum_of_processing_times_right_child_lambda, + ); + + // The earliest completion time (including one element of lambda) is either: + // 1) The earliest completion time including one element of lambda from the right child + // 2) The earliest completion time of the right child + the sum of processing times + // including one element of lambda of the right child + // 2) The earliest completion time of the left child + the sum of processing times + // including one element of lambda of the left child + let ect_right_child_lambda = self.nodes[left_child_of_parent].ect + + self.nodes[right_child_of_parent].sum_of_processing_times_bar; + let ect_left_child_lambda = self.nodes[left_child_of_parent].ect_bar + + self.nodes[right_child_of_parent].sum_of_processing_times; + self.nodes[parent].ect_bar = max( + self.nodes[right_child_of_parent].ect_bar, + max(ect_right_child_lambda, ect_left_child_lambda), + ); + + index = parent; + } + } +} + #[cfg(test)] mod tests { use pumpkin_checking::TestAtomic; @@ -337,160 +494,3 @@ mod tests { assert!(!checker.check(state, &premises, None)); } } - -#[derive(Debug, Clone)] -pub(super) struct CheckerThetaLambdaTree { - pub(super) nodes: Vec, - /// Then we keep track of a mapping from the [`LocalId`] to its position in the tree since the - /// methods take as input tasks with [`LocalId`]s. - mapping: KeyedVec, - /// The number of internal nodes in the tree; used to calculate the leaf node index based on - /// the index in the tree - number_of_internal_nodes: usize, - /// The tasks which are stored in the leaves of the tree. - /// - /// These tasks are sorted based on non-decreasing start time. - sorted_tasks: Vec>, - phantom_data: PhantomData, -} - -impl> CheckerThetaLambdaTree { - /// Initialises the theta-lambda tree. - /// - /// Note that [`Self::update`] should be called to actually create the tree itself. - pub(super) fn new(tasks: &[DisjunctiveTask]) -> Self { - // Calculate the number of internal nodes which are required to create the binary tree - let mut number_of_internal_nodes = 1; - while number_of_internal_nodes < tasks.len() { - number_of_internal_nodes <<= 1; - } - - CheckerThetaLambdaTree { - nodes: Default::default(), - mapping: KeyedVec::default(), - number_of_internal_nodes: number_of_internal_nodes - 1, - sorted_tasks: tasks.to_vec(), - phantom_data: PhantomData, - } - } - - /// Update the theta-lambda tree based on the provided `context`. - /// - /// It resets theta and lambda to be the empty set. - pub(super) fn update(&mut self, context: &VariableState) { - // First we sort the tasks by lower-bound/earliest start time. - self.sorted_tasks - .sort_by_key(|task| task.start_time.induced_lower_bound(context)); - - // Then we keep track of a mapping from the [`LocalId`] to its position in the tree and a - // reverse mapping - self.mapping.clear(); - for (index, task) in self.sorted_tasks.iter().enumerate() { - while self.mapping.len() <= task.id.index() { - let _ = self.mapping.push(usize::MAX); - } - self.mapping[task.id] = index; - } - - // Finally, we reset the entire tree to be empty - self.nodes.clear(); - for _ in 0..=2 * self.number_of_internal_nodes { - self.nodes.push(Node::empty()) - } - } - - /// Returns the earliest completion time of Theta - pub(super) fn ect(&self) -> i32 { - assert!(!self.nodes.is_empty()); - self.nodes[0].ect - } - - /// Add the provided task to Theta - pub(super) fn add_to_theta( - &mut self, - task: &DisjunctiveTask, - context: &VariableState, - ) { - // We need to find the leaf node index; note that there are |nodes| / 2 leaves - let position = self.nodes.len() / 2 + self.mapping[task.id]; - let ect = task.start_time.induced_lower_bound(context) + task.processing_time; - - self.nodes[position] = Node::new_white_node( - ect.try_into().expect("Should have bounds"), - task.processing_time, - ); - self.upheap(position) - } - - /// Returns the index of the left child of the provided index - fn get_left_child_index(index: usize) -> usize { - 2 * index + 1 - } - - /// Returns the index of the right child of the provided index - fn get_right_child_index(index: usize) -> usize { - 2 * index + 2 - } - - /// Returns the index of the parent of the provided index - fn get_parent(index: usize) -> usize { - assert!(index > 0); - (index - 1) / 2 - } - - /// Calculate the new values for the ancestors of the provided index - pub(super) fn upheap(&mut self, mut index: usize) { - while index != 0 { - let parent = Self::get_parent(index); - let left_child_of_parent = Self::get_left_child_index(parent); - let right_child_of_parent = Self::get_right_child_index(parent); - assert!(left_child_of_parent == index || right_child_of_parent == index); - - // The sum of processing times is the sum of processing times in the left child + the - // sum of processing times in right child - self.nodes[parent].sum_of_processing_times = self.nodes[left_child_of_parent] - .sum_of_processing_times - + self.nodes[right_child_of_parent].sum_of_processing_times; - - // The ECT is either the ECT of the left child node + the processing times of the right - // child or it is the ECT of the right child (we do not know whether the processing - // times of the left child influence the processing times of the right child) - let ect_left = self.nodes[left_child_of_parent].ect - + self.nodes[right_child_of_parent].sum_of_processing_times; - self.nodes[parent].ect = max(self.nodes[right_child_of_parent].ect, ect_left); - - // The sum of processing times (including one element of lambda) is either: - // 1) The sum of processing times of the right child + the sum of processing times of - // the left child including one element of lambda - // 2) The sum of processing times of the left child + the sum of processing times of the - // right child include one element of lambda - let sum_of_processing_times_left_child_lambda = self.nodes[left_child_of_parent] - .sum_of_processing_times_bar - + self.nodes[right_child_of_parent].sum_of_processing_times; - let sum_of_processing_times_right_child_lambda = self.nodes[left_child_of_parent] - .sum_of_processing_times - + self.nodes[right_child_of_parent].sum_of_processing_times_bar; - self.nodes[parent].sum_of_processing_times_bar = max( - sum_of_processing_times_left_child_lambda, - sum_of_processing_times_right_child_lambda, - ); - - // The earliest completion time (including one element of lambda) is either: - // 1) The earliest completion time including one element of lambda from the right child - // 2) The earliest completion time of the right child + the sum of processing times - // including one element of lambda of the right child - // 2) The earliest completion time of the left child + the sum of processing times - // including one element of lambda of the left child - let ect_right_child_lambda = self.nodes[left_child_of_parent].ect - + self.nodes[right_child_of_parent].sum_of_processing_times_bar; - let ect_left_child_lambda = self.nodes[left_child_of_parent].ect_bar - + self.nodes[right_child_of_parent].sum_of_processing_times; - self.nodes[parent].ect_bar = max( - self.nodes[right_child_of_parent].ect_bar, - max(ect_right_child_lambda, ect_left_child_lambda), - ); - - index = parent; - } - } -}