use super::*;
use std::collections::BTreeMap;
impl Instance {
pub fn reduce_binary_power(&mut self) -> Result<bool, crate::CoefficientError> {
let binary_ids = self.binary_ids();
if binary_ids.is_empty() {
return Ok(false);
}
let objective_replacement = self.objective.plan_binary_power_reduction(&binary_ids)?;
let mut replacements = BTreeMap::new();
for (&id, constraint) in self.constraint_collection.active() {
if let Some(replacement) = constraint.plan_binary_power_reduction(&binary_ids)? {
replacements.insert(id, replacement);
}
}
let changed = objective_replacement.is_some() || !replacements.is_empty();
if let Some(replacement) = objective_replacement {
self.replace_active_objective_preserving_output(replacement);
}
if !replacements.is_empty() {
self.constraint_collection
.replace_active_rows(replacements)
.expect("replacement IDs were read from active constraints");
}
Ok(changed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
coeff, constraint::CreatedData, linear, quadratic, Bound, Coefficient, Constraint,
DecisionVariable, Equality, Kind, Sense,
};
use ::approx::assert_abs_diff_eq;
use proptest::prelude::*;
#[test]
fn test_instance_reduce_binary_power() {
let mut decision_variables = BTreeMap::new();
decision_variables.insert(VariableID::from(1), DecisionVariable::binary());
decision_variables.insert(
VariableID::from(2),
DecisionVariable::new(
Kind::Continuous,
Bound::new(0.0, 10.0).unwrap(),
crate::ATol::default(),
)
.unwrap(),
);
let mut objective_poly =
(quadratic!(1, 1) + (coeff!(2.0) * quadratic!(1, 2)).unwrap()).unwrap();
objective_poly = (objective_poly + (coeff!(3.0) * quadratic!(2, 2)).unwrap()).unwrap();
let objective = Function::Quadratic(objective_poly);
let mut constraints = BTreeMap::new();
let constraint_func = Function::Quadratic(
((quadratic!(1, 1) + quadratic!(2)).unwrap() + coeff!(-5.0)).unwrap(),
);
constraints.insert(
ConstraintID::from(1),
Constraint {
equality: Equality::LessThanOrEqualToZero,
stage: CreatedData {
function: constraint_func,
},
},
);
let mut instance = Instance::new(
Sense::Minimize,
objective.clone(),
decision_variables,
constraints,
)
.unwrap();
let changed = instance.reduce_binary_power().unwrap();
assert!(changed);
assert_eq!(instance.output_objective().unwrap().function(), &objective);
let mut expected_objective_poly =
(quadratic!(1) + (coeff!(2.0) * quadratic!(1, 2)).unwrap()).unwrap();
expected_objective_poly =
(expected_objective_poly + (coeff!(3.0) * quadratic!(2, 2)).unwrap()).unwrap();
let expected_objective = Function::Quadratic(expected_objective_poly);
assert_abs_diff_eq!(instance.objective(), &expected_objective);
let expected_constraint_func =
Function::Quadratic(((quadratic!(1) + quadratic!(2)).unwrap() + coeff!(-5.0)).unwrap());
assert_eq!(
instance
.constraints()
.get(&ConstraintID::from(1))
.unwrap()
.function(),
&expected_constraint_func
);
let changed2 = instance.reduce_binary_power().unwrap();
assert!(!changed2);
}
#[test]
fn test_instance_reduce_binary_power_no_binary() {
let mut decision_variables = BTreeMap::new();
decision_variables.insert(VariableID::from(1), DecisionVariable::continuous());
decision_variables.insert(VariableID::from(2), DecisionVariable::integer());
let objective = Function::Quadratic(
(quadratic!(1, 1) + (coeff!(2.0) * quadratic!(2, 2)).unwrap()).unwrap(),
);
let mut instance = Instance::new(
Sense::Minimize,
objective.clone(),
decision_variables,
BTreeMap::new(),
)
.unwrap();
let changed = instance.reduce_binary_power().unwrap();
assert!(!changed);
assert_eq!(instance.objective(), &objective);
assert!(instance.output_objective().is_none());
}
#[test]
fn reduce_binary_power_does_not_capture_constraint_only_rewrite() {
let variable = VariableID::from(1);
let objective = Function::from(linear!(1));
let constraint = Constraint::equal_to_zero(Function::from(quadratic!(1, 1)));
let mut instance = Instance::new(
Sense::Minimize,
objective.clone(),
BTreeMap::from([(variable, DecisionVariable::binary())]),
BTreeMap::from([(ConstraintID::from(1), constraint)]),
)
.unwrap();
assert!(instance.reduce_binary_power().unwrap());
assert_eq!(instance.objective(), &objective);
assert!(instance.output_objective().is_none());
}
#[test]
fn reduce_binary_power_moves_original_expression_to_output_objective() {
let variable = VariableID::from(1);
let objective = Function::from(linear!(1)).powi(2);
let mut instance = Instance::new(
Sense::Minimize,
objective,
BTreeMap::from([(variable, DecisionVariable::binary())]),
BTreeMap::new(),
)
.unwrap();
let Function::Expression(expression) = instance.objective() else {
panic!("powi(2) must create an expression")
};
let original_instructions = crate::function::operation::instructions(expression).as_ptr();
assert!(instance.reduce_binary_power().unwrap());
let Function::Expression(expression) = instance.output_objective().unwrap().function()
else {
panic!("output objective must preserve the original expression")
};
assert_eq!(
crate::function::operation::instructions(expression).as_ptr(),
original_instructions
);
assert_eq!(instance.objective(), &Function::from(linear!(1)));
}
#[test]
fn reduce_binary_power_preserves_instance_on_coefficient_error() {
let mut decision_variables = BTreeMap::new();
decision_variables.insert(VariableID::from(1), DecisionVariable::binary());
let objective = Function::Quadratic(quadratic!(1, 1).into());
let huge = Coefficient::try_from(f64::MAX).unwrap();
let overflowing_constraint = Function::Quadratic(
((huge * quadratic!(1, 1)).unwrap() + (huge * quadratic!(1)).unwrap()).unwrap(),
);
let mut constraints = BTreeMap::new();
constraints.insert(
ConstraintID::from(1),
Constraint {
equality: Equality::LessThanOrEqualToZero,
stage: CreatedData {
function: overflowing_constraint,
},
},
);
let mut instance =
Instance::new(Sense::Minimize, objective, decision_variables, constraints).unwrap();
let before_objective = instance.objective().clone();
let before_constraints = instance.constraints().clone();
let err = instance.reduce_binary_power().unwrap_err();
assert_eq!(err, crate::CoefficientError::Infinite);
assert_eq!(instance.objective(), &before_objective);
assert_eq!(instance.constraints(), &before_constraints);
assert!(instance.output_objective().is_none());
}
proptest! {
#[test]
fn test_instance_reduce_binary_power_idempotent(
mut instance in Instance::arbitrary()
) {
let _first = instance.reduce_binary_power().unwrap();
let second = instance.reduce_binary_power().unwrap();
prop_assert!(!second, "reduce_binary_power should be idempotent");
}
}
}