From 4b9f44dc04e7c18de04695313b38ba2820fb5d5f Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 25 Dec 2024 21:29:38 +0900 Subject: [PATCH 1/2] Instance::log_encoding --- rust/ommx/src/convert/instance.rs | 66 ++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/rust/ommx/src/convert/instance.rs b/rust/ommx/src/convert/instance.rs index 2114f6f7..44ea6242 100644 --- a/rust/ommx/src/convert/instance.rs +++ b/rust/ommx/src/convert/instance.rs @@ -1,9 +1,10 @@ use crate::v1::{ decision_variable::Kind, instance::{Description, Sense}, - Equality, Function, Instance, Parameter, ParametricInstance, RemovedConstraint, + DecisionVariable, Equality, Function, Instance, Linear, Parameter, ParametricInstance, + RemovedConstraint, }; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, ensure, Context, Result}; use approx::AbsDiffEq; use maplit::hashmap; use num::Zero; @@ -341,6 +342,67 @@ impl Instance { } Ok((quad, constant)) } + + /// Encode an integer decision variable into binary decision variables. + pub fn log_encoding(&mut self, decision_variable_id: u64) -> Result<()> { + let v = self + .decision_variables + .iter_mut() + .find(|dv| dv.id == decision_variable_id) + .with_context(|| format!("Decision variable ID {} not found", decision_variable_id))?; + if v.kind() != Kind::Integer { + bail!( + "The decision variable is not an integer type: ID={}", + decision_variable_id + ); + } + let bound = v + .bound + .as_ref() + .with_context(|| format!("Bound is not defined: ID={}", decision_variable_id))?; + let u_l = bound.upper - bound.lower; + ensure!( + u_l > 0.0, + "Upper bound must be greater than lower bound: ID={}, lower={}, upper={}", + decision_variable_id, + bound.lower, + bound.upper + ); + + let n = (u_l + 1.0).log2().ceil() as usize; + let id_base = self + .defined_ids() + .last() + .map(|id| id + 1) + .expect("At least one decision variable here"); + + let mut f = Function::zero(); + for i in 0..n { + let id = id_base + i as u64; + f = f + Linear::single_term( + id, + if i == n - 1 { + 2.0f64.powi(i as i32) - u_l + } else { + 2.0f64.powi(i as i32) + }, + ); + self.decision_variables.push(DecisionVariable { + id, + name: Some("ommx.log_encoding".to_string()), + subscripts: vec![decision_variable_id as i64, i as i64], + kind: Kind::Binary as i32, + bound: Some(crate::v1::Bound { + lower: 0.0, + upper: 1.0, + }), + ..Default::default() + }); + } + self.decision_variable_dependency + .insert(decision_variable_id, f); + Ok(()) + } } fn arbitrary_instance( From beedac5ce9953bbb69a15311662faf9fa1cdebab Mon Sep 17 00:00:00 2001 From: Toshiki Teramura Date: Wed, 25 Dec 2024 22:23:58 +0900 Subject: [PATCH 2/2] Test for integer reconstruction --- rust/ommx/src/convert/instance.rs | 65 ++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/rust/ommx/src/convert/instance.rs b/rust/ommx/src/convert/instance.rs index 44ea6242..cd475621 100644 --- a/rust/ommx/src/convert/instance.rs +++ b/rust/ommx/src/convert/instance.rs @@ -347,7 +347,7 @@ impl Instance { pub fn log_encoding(&mut self, decision_variable_id: u64) -> Result<()> { let v = self .decision_variables - .iter_mut() + .iter() .find(|dv| dv.id == decision_variable_id) .with_context(|| format!("Decision variable ID {} not found", decision_variable_id))?; if v.kind() != Kind::Integer { @@ -360,10 +360,13 @@ impl Instance { .bound .as_ref() .with_context(|| format!("Bound is not defined: ID={}", decision_variable_id))?; - let u_l = bound.upper - bound.lower; + // Bound of integer may be non-integer value + let upper = bound.upper.floor(); + let lower = bound.lower.ceil(); + let u_l = upper - lower; ensure!( u_l > 0.0, - "Upper bound must be greater than lower bound: ID={}, lower={}, upper={}", + "No feasible integer found in the bound: ID={}, lower={}, upper={}", decision_variable_id, bound.lower, bound.upper @@ -376,13 +379,13 @@ impl Instance { .map(|id| id + 1) .expect("At least one decision variable here"); - let mut f = Function::zero(); + let mut f = Function::from(lower); for i in 0..n { let id = id_base + i as u64; f = f + Linear::single_term( id, if i == n - 1 { - 2.0f64.powi(i as i32) - u_l + u_l - 2.0f64.powi(i as i32) + 1.0 } else { 2.0f64.powi(i as i32) }, @@ -401,6 +404,9 @@ impl Instance { } self.decision_variable_dependency .insert(decision_variable_id, f); + + // TODO: substitute objective and constraints + Ok(()) } } @@ -588,7 +594,10 @@ impl AbsDiffEq for Instance { #[cfg(test)] mod tests { use super::*; - use crate::v1::Parameters; + use crate::{ + v1::{Parameters, State}, + Evaluate, + }; proptest! { #[test] @@ -683,5 +692,49 @@ mod tests { prop_assert!(c.abs() > f64::EPSILON); } } + + #[test] + fn log_encoding(lower in -10.0_f64..10.0, upper in -10.0_f64..10.0) { + if lower.ceil() >= upper.floor() { + return Ok(()); + } + let mut instance = Instance::default(); + instance.decision_variables.push(DecisionVariable { + id: 0, + name: Some("x".to_string()), + kind: Kind::Integer as i32, + bound: Some(crate::v1::Bound { lower, upper }), + ..Default::default() + }); + instance.log_encoding(0).unwrap(); + instance.validate().unwrap(); + + // Coefficient of the log encoding decision variables should be positive except for the constant term + for (ids, coefficient) in instance.decision_variable_dependency.get(&0).unwrap().into_iter() { + if !ids.is_empty() { + prop_assert!(coefficient > 0.0); + } + } + + let aux_bits = instance + .decision_variables + .iter() + .filter_map(|dv| { + if dv.name == Some("ommx.log_encoding".to_string()) { + Some(dv.id) + } else { + None + } + }) + .collect::>(); + + let state = State { entries: aux_bits.iter().map(|&id| (id, 0.0)).collect::>() }; + let (solution, _) = instance.evaluate(&state).unwrap(); + prop_assert_eq!(*solution.state.unwrap().entries.get(&0).unwrap(), lower.ceil()); + + let state = State { entries: aux_bits.iter().map(|&id| (id, 1.0)).collect::>() }; + let (solution, _) = instance.evaluate(&state).unwrap(); + prop_assert_eq!(*solution.state.unwrap().entries.get(&0).unwrap(), upper.floor()); + } } }