use super::{ExactIntegerSlackUnavailable, Instance, SpecialConstraintKinds};
use crate::{ATol, ConstraintID, Equality, InstanceClass, InstanceClassMembershipReport, Sense};
use std::collections::BTreeMap;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpecialConstraintPreparation {
LowerSpecialConstraints {
kinds: SpecialConstraintKinds,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectivePreparation {
pub target: Sense,
}
#[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,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct BinaryPowerPreparation;
#[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 objective: Option<ObjectivePreparation>,
pub integer_slack: Option<IntegerSlackPreparation>,
pub integer_encoding: Option<IntegerEncodingPreparation>,
pub fixed_penalty: Option<FixedPenaltyPreparation>,
pub binary_power_reduction: Option<BinaryPowerPreparation>,
}
impl PreparationPolicy {
pub fn for_qubo() -> Self {
Self::for_binary_polynomial_format(Some(BinaryPowerPreparation))
}
pub fn for_hubo() -> Self {
Self::for_binary_polynomial_format(None)
}
fn for_binary_polynomial_format(
binary_power_reduction: Option<BinaryPowerPreparation>,
) -> Self {
Self {
special_constraints: Some(SpecialConstraintPreparation::LowerSpecialConstraints {
kinds: [
super::SpecialConstraintKind::Indicator,
super::SpecialConstraintKind::OneHot,
super::SpecialConstraintKind::Sos1,
]
.into_iter()
.collect(),
}),
objective: Some(ObjectivePreparation {
target: Sense::Minimize,
}),
integer_slack: Some(IntegerSlackPreparation {
max_integer_range: 31,
atol: ATol::default(),
slack_upper_bound: Some(31),
}),
integer_encoding: Some(IntegerEncodingPreparation::LogEncodeAllUsedIntegers {
atol: ATol::default(),
}),
fixed_penalty: Some(
FixedPenaltyPreparation::UniformPenaltyMethodWithFixedWeight {
weight: 1.0,
atol: ATol::default(),
},
),
binary_power_reduction,
}
}
}
#[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 ObjectivePreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
instance.convert_active_objective(self.target);
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(())
}
}
impl PreparationStep for BinaryPowerPreparation {
fn apply(&self, instance: &mut Instance) -> crate::Result<()> {
instance.reduce_binary_power()?;
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.objective.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.fixed_penalty.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.binary_power_reduction.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, DecisionVariableRole,
DegreeBound, Evaluate, ExactIntegerSlackUnavailable, Function, InfeasibleDetected,
InstanceClassClause, Kind, MonomialDyn, OneHotConstraint, OneHotConstraintID, Polynomial,
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()
}
fn cubic_binary_instance(ids: Vec<VariableID>) -> Instance {
let variables = ids
.iter()
.copied()
.map(|id| (id, DecisionVariable::binary()))
.collect();
let objective = Function::from(Polynomial::single_term(MonomialDyn::new(ids), coeff!(1.0)));
Instance::new(Sense::Minimize, objective, variables, BTreeMap::new()).unwrap()
}
#[test]
fn qubo_preparation_reduces_repeated_binary_power() {
let variable = VariableID::from(1);
let mut instance = cubic_binary_instance(vec![variable, variable, variable]);
let source_objective = instance.objective().clone();
instance
.prepare(&InstanceClass::qubo(), &PreparationPolicy::for_qubo())
.unwrap();
assert!(InstanceClass::qubo().contains(&instance));
assert_eq!(instance.objective().degree(), crate::Degree::from(1));
assert_eq!(
instance.objective().required_ids(),
crate::VariableIDSet::from([variable])
);
let output = instance.output_objective().unwrap();
assert_eq!(output.sense(), Sense::Minimize);
assert_eq!(output.function(), &source_objective);
assert!(output.preserves_optimality());
instance.as_qubo_format().unwrap();
}
#[test]
fn qubo_rejects_but_hubo_accepts_three_distinct_binary_variables() {
let ids = vec![
VariableID::from(1),
VariableID::from(2),
VariableID::from(3),
];
let source = cubic_binary_instance(ids);
let mut qubo = source.clone();
let error = qubo
.prepare(&InstanceClass::qubo(), &PreparationPolicy::for_qubo())
.unwrap_err();
assert!(error.is::<PreparationTargetNotReached>());
assert!(!InstanceClass::qubo().contains(&qubo));
assert!(qubo.output_objective().is_none());
let mut hubo = source;
hubo.prepare(&InstanceClass::hubo(), &PreparationPolicy::for_hubo())
.unwrap();
assert!(InstanceClass::hubo().contains(&hubo));
assert!(hubo.output_objective().is_none());
hubo.as_hubo_format().unwrap();
}
#[test]
fn qubo_preparation_composes_encoding_and_penalty_output_semantics() {
let variable = VariableID::from(1);
let constraint_id = ConstraintID::from(7);
let objective = Function::from(linear!(variable));
let constraint_function = (Function::from(linear!(variable)) + coeff!(-1.0)).unwrap();
let mut instance = Instance::new(
Sense::Minimize,
objective.clone(),
BTreeMap::from([(
variable,
DecisionVariable::new(
Kind::Integer,
Bound::new(0.0, 3.0).unwrap(),
ATol::default(),
)
.unwrap(),
)]),
BTreeMap::from([(
constraint_id,
Constraint::equal_to_zero(constraint_function.clone()),
)]),
)
.unwrap();
instance
.prepare(&InstanceClass::qubo(), &PreparationPolicy::for_qubo())
.unwrap();
assert!(InstanceClass::qubo().contains(&instance));
let output = instance.output_objective().unwrap();
assert_eq!(output.sense(), Sense::Minimize);
assert_eq!(output.function(), &objective);
assert!(!output.preserves_optimality());
assert_eq!(
instance.removed_constraints()[&constraint_id].0.function(),
&constraint_function
);
assert_eq!(
instance.decision_variable_role(variable),
Some(DecisionVariableRole::Dependent)
);
}
#[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]),
}),
objective: Some(ObjectivePreparation {
target: Sense::Minimize,
}),
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 {
objective: Some(ObjectivePreparation {
target: Sense::Minimize,
}),
..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
);
}
}