use super::{ExactIntegerSlackUnavailable, Instance, SpecialConstraintKinds};
use crate::{ATol, ConstraintID, Equality, InstanceClass, InstanceClassMembershipReport};
use std::collections::BTreeMap;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpecialConstraintPreparation {
LowerSpecialConstraints {
kinds: SpecialConstraintKinds,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SensePreparation {
AsMinimizationProblem,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IntegerSlackPreparation {
pub max_integer_range: u64,
pub atol: ATol,
pub slack_upper_bound: Option<u64>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IntegerEncodingPreparation {
LogEncodeAllUsedIntegers {
atol: ATol,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub enum FixedPenaltyPreparation {
PenaltyMethodWithFixedWeights {
weights: BTreeMap<ConstraintID, f64>,
atol: ATol,
},
UniformPenaltyMethodWithFixedWeight {
weight: f64,
atol: ATol,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PreparationPolicy {
pub special_constraints: Option<SpecialConstraintPreparation>,
pub sense: Option<SensePreparation>,
pub integer_slack: Option<IntegerSlackPreparation>,
pub integer_encoding: Option<IntegerEncodingPreparation>,
pub fixed_penalty: Option<FixedPenaltyPreparation>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("Preparation did not reach the target InstanceClass:\n{report}")]
pub struct PreparationTargetNotReached {
report: InstanceClassMembershipReport,
}
impl PreparationTargetNotReached {
pub fn report(&self) -> &InstanceClassMembershipReport {
&self.report
}
}
trait PreparationStep {
fn apply(&self, instance: &mut Instance) -> crate::Result<()>;
}
impl PreparationStep for SpecialConstraintPreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
match self {
Self::LowerSpecialConstraints { kinds } => {
instance.lower_special_constraints(kinds)?;
}
}
Ok(())
}
}
impl PreparationStep for SensePreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
match self {
Self::AsMinimizationProblem => {
instance.as_minimization_problem();
}
}
Ok(())
}
}
impl PreparationStep for IntegerSlackPreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
let inequality_ids = instance
.constraints()
.iter()
.filter_map(|(&id, constraint)| {
(constraint.equality == Equality::LessThanOrEqualToZero).then_some(id)
})
.collect::<Vec<_>>();
for id in inequality_ids {
match instance.convert_inequality_to_equality_with_integer_slack(
id.into_inner(),
self.max_integer_range,
self.atol,
) {
Ok(()) => {}
Err(error) if error.is::<ExactIntegerSlackUnavailable>() => {
let Some(slack_upper_bound) = self.slack_upper_bound else {
return Err(error);
};
instance.add_integer_slack_to_inequality(id.into_inner(), slack_upper_bound)?;
}
Err(error) => return Err(error),
}
}
Ok(())
}
}
impl PreparationStep for IntegerEncodingPreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
match self {
Self::LogEncodeAllUsedIntegers { atol } => {
instance.log_encode_all_used_integers(*atol)?;
}
}
Ok(())
}
}
impl PreparationStep for FixedPenaltyPreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
match self {
Self::PenaltyMethodWithFixedWeights { weights, atol } => {
instance.penalty_method_with_fixed_weights(weights, *atol)?;
}
Self::UniformPenaltyMethodWithFixedWeight { weight, atol } => {
instance.uniform_penalty_method_with_fixed_weight(*weight, *atol)?;
}
}
Ok(())
}
}
fn apply_preparation_step<S: PreparationStep>(
instance: &mut Instance,
input_class: &InstanceClass,
step: Option<&S>,
) -> crate::Result<bool> {
match step {
Some(step) => {
step.apply(instance)?;
Ok(input_class.contains(instance))
}
None => Ok(false),
}
}
impl Instance {
pub fn prepare(
&mut self,
input_class: &InstanceClass,
policy: &PreparationPolicy,
) -> crate::Result<()> {
if input_class.contains(self) {
return Ok(());
}
if apply_preparation_step(self, input_class, policy.special_constraints.as_ref())? {
return Ok(());
}
if apply_preparation_step(self, input_class, policy.sense.as_ref())? {
return Ok(());
}
if apply_preparation_step(self, input_class, policy.integer_slack.as_ref())? {
return Ok(());
}
if apply_preparation_step(self, input_class, policy.integer_encoding.as_ref())? {
return Ok(());
}
if apply_preparation_step(self, input_class, policy.fixed_penalty.as_ref())? {
return Ok(());
}
let report = input_class.check_membership(self);
crate::bail!(PreparationTargetNotReached { report })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
coeff, linear, quadratic, Bound, Constraint, DecisionVariable, DegreeBound,
ExactIntegerSlackUnavailable, Function, InfeasibleDetected, InstanceClassClause, Kind,
OneHotConstraint, OneHotConstraintID, Sense, SpecialConstraintKind, VariableID,
};
use std::collections::BTreeSet;
fn unconstrained_class(
label: &str,
allowed_variable_kinds: impl IntoIterator<Item = Kind>,
objective_degree_bound: DegreeBound,
) -> InstanceClass {
InstanceClassClause::new(
label,
allowed_variable_kinds.into_iter().collect(),
objective_degree_bound,
BTreeSet::from([Sense::Minimize]),
)
.into()
}
fn integer_inequality_instance() -> Instance {
let variable = VariableID::from(1);
let decision_variable = DecisionVariable::new(
Kind::Integer,
Bound::new(0.0, 3.0).unwrap(),
ATol::default(),
)
.unwrap();
let function = (Function::from(linear!(variable)) + coeff!(-2.0)).unwrap();
Instance::new(
Sense::Minimize,
Function::from(linear!(variable)),
BTreeMap::from([(variable, decision_variable)]),
BTreeMap::from([(
ConstraintID::from(1),
Constraint::less_than_or_equal_to_zero(function),
)]),
)
.unwrap()
}
#[test]
fn already_member_is_exact_identity() {
let mut instance = Instance::new(
Sense::Minimize,
Function::Zero,
BTreeMap::new(),
BTreeMap::new(),
)
.unwrap();
let target = unconstrained_class("constant", [], DegreeBound::at_most(0));
let policy = PreparationPolicy {
fixed_penalty: Some(FixedPenaltyPreparation::PenaltyMethodWithFixedWeights {
weights: BTreeMap::from([(ConstraintID::from(999), 1.0)]),
atol: ATol::default(),
}),
..Default::default()
};
let before = instance.clone();
instance.prepare(&target, &policy).unwrap();
assert_eq!(instance, before);
}
#[test]
fn equality_requirement_propagates_exact_slack_unavailable_without_an_alternative() {
let mut instance = integer_inequality_instance();
let before = instance.clone();
let target = InstanceClassClause::new(
"integer equality",
BTreeSet::from([Kind::Integer]),
DegreeBound::at_most(1),
BTreeSet::from([Sense::Minimize]),
)
.with_regular_constraint(Equality::EqualToZero, DegreeBound::at_most(1))
.into();
let policy = PreparationPolicy {
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 1,
atol: ATol::default(),
slack_upper_bound: None,
}),
..Default::default()
};
let error = instance.prepare(&target, &policy).unwrap_err();
assert!(error.is::<ExactIntegerSlackUnavailable>());
assert_eq!(instance, before);
}
#[test]
fn equality_requirement_reaches_an_equality_target() {
let mut instance = integer_inequality_instance();
let target = InstanceClassClause::new(
"integer equality",
BTreeSet::from([Kind::Integer]),
DegreeBound::at_most(1),
BTreeSet::from([Sense::Minimize]),
)
.with_regular_constraint(Equality::EqualToZero, DegreeBound::at_most(1))
.into();
let policy = PreparationPolicy {
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 32,
atol: ATol::default(),
slack_upper_bound: None,
}),
..Default::default()
};
instance.prepare(&target, &policy).unwrap();
assert!(target.contains(&instance));
}
#[test]
fn allowing_inequality_still_prefers_an_equality_when_available() {
let mut instance = integer_inequality_instance();
let target = InstanceClassClause::new(
"integer equality",
BTreeSet::from([Kind::Integer]),
DegreeBound::at_most(1),
BTreeSet::from([Sense::Minimize]),
)
.with_regular_constraint(Equality::EqualToZero, DegreeBound::at_most(1))
.into();
let policy = PreparationPolicy {
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 32,
atol: ATol::default(),
slack_upper_bound: Some(2),
}),
..Default::default()
};
instance.prepare(&target, &policy).unwrap();
assert!(target.contains(&instance));
}
#[test]
fn integer_slack_phase_completes_before_membership_stops_later_phases() {
let variable = VariableID::from(1);
let first_id = ConstraintID::from(1);
let second_id = ConstraintID::from(2);
let quadratic_inequality =
Function::Quadratic((quadratic!(variable, variable) + coeff!(-10.0)).unwrap());
let linear_inequality = (Function::from(linear!(variable)) + coeff!(-2.0)).unwrap();
let mut instance = Instance::new(
Sense::Minimize,
Function::Zero,
BTreeMap::from([(
variable,
DecisionVariable::new(
Kind::Integer,
Bound::new(0.0, 3.0).unwrap(),
ATol::default(),
)
.unwrap(),
)]),
BTreeMap::from([
(
first_id,
Constraint::less_than_or_equal_to_zero(quadratic_inequality),
),
(
second_id,
Constraint::less_than_or_equal_to_zero(linear_inequality),
),
]),
)
.unwrap();
let target: InstanceClass = InstanceClassClause::new(
"linear integer inequalities",
BTreeSet::from([Kind::Integer]),
DegreeBound::at_most(0),
BTreeSet::from([Sense::Minimize]),
)
.with_regular_constraint(Equality::LessThanOrEqualToZero, DegreeBound::at_most(1))
.into();
let policy = PreparationPolicy {
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 1,
atol: ATol::default(),
slack_upper_bound: Some(2),
}),
fixed_penalty: Some(FixedPenaltyPreparation::PenaltyMethodWithFixedWeights {
weights: BTreeMap::from([(ConstraintID::from(999), 1.0)]),
atol: ATol::default(),
}),
..Default::default()
};
let variable_count = instance.decision_variables().len();
assert!(!target.contains(&instance));
instance.prepare(&target, &policy).unwrap();
assert!(target.contains(&instance));
assert_eq!(instance.decision_variables().len(), variable_count + 1);
}
#[test]
fn allowing_inequality_propagates_unrelated_error_after_earlier_commits() {
let converted_id = ConstraintID::from(1);
let infeasible_id = ConstraintID::from(2);
let variables = BTreeMap::from([
(VariableID::from(1), DecisionVariable::binary()),
(VariableID::from(2), DecisionVariable::binary()),
]);
let converted_function = (Function::from(linear!(1)) + coeff!(-0.5)).unwrap();
let infeasible_function =
Function::from(((coeff!(0.5) * linear!(1)).unwrap() + coeff!(1.0)).unwrap());
let one_hot =
OneHotConstraint::new(BTreeSet::from([VariableID::from(1), VariableID::from(2)]))
.unwrap();
let mut instance = Instance::builder()
.sense(Sense::Maximize)
.objective(Function::from(linear!(1)))
.decision_variables(variables)
.constraints(BTreeMap::from([
(
converted_id,
Constraint::less_than_or_equal_to_zero(converted_function),
),
(
infeasible_id,
Constraint::less_than_or_equal_to_zero(infeasible_function),
),
]))
.one_hot_constraints(BTreeMap::from([(OneHotConstraintID::from(1), one_hot)]))
.build()
.unwrap();
let target =
unconstrained_class("binary quadratic", [Kind::Binary], DegreeBound::at_most(2));
let policy = PreparationPolicy {
special_constraints: Some(SpecialConstraintPreparation::LowerSpecialConstraints {
kinds: BTreeSet::from([SpecialConstraintKind::OneHot]),
}),
sense: Some(SensePreparation::AsMinimizationProblem),
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 32,
atol: ATol::default(),
slack_upper_bound: Some(2),
}),
..Default::default()
};
let error = instance.prepare(&target, &policy).unwrap_err();
assert!(matches!(
error.downcast_ref::<InfeasibleDetected>(),
Some(InfeasibleDetected::InequalityConstraintBound { id, bound })
if *id == infeasible_id && *bound == Bound::new(2.0, 3.0).unwrap()
));
assert_eq!(instance.sense(), Sense::Minimize);
assert_eq!(instance.removed_one_hot_constraints().len(), 1);
assert_eq!(
instance.constraints()[&converted_id].equality,
Equality::EqualToZero
);
}
#[test]
fn exhausted_policy_returns_the_final_membership_report() {
let variable = VariableID::from(1);
let mut instance = Instance::new(
Sense::Maximize,
Function::from(linear!(variable)),
BTreeMap::from([(variable, DecisionVariable::binary())]),
BTreeMap::from([(
ConstraintID::from(1),
Constraint::equal_to_zero(Function::from(linear!(variable))),
)]),
)
.unwrap();
let target = unconstrained_class(
"unconstrained binary",
[Kind::Binary],
DegreeBound::at_most(1),
);
let policy = PreparationPolicy {
sense: Some(SensePreparation::AsMinimizationProblem),
..Default::default()
};
let error = instance.prepare(&target, &policy).unwrap_err();
let membership_report = target.check_membership(&instance);
assert!(!target.contains(&instance));
assert_eq!(instance.sense(), Sense::Minimize);
assert_eq!(
error
.downcast_ref::<PreparationTargetNotReached>()
.unwrap()
.report(),
&membership_report
);
}
}