use super::*;
use crate::{v1::State, ATol, AcyclicAssignments, Bound, Bounds, Evaluate, Kind, VariableIDSet};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, getset::Getters, serde::Serialize, serde::Deserialize)]
pub struct DecisionVariableAnalysis {
#[getset(get = "pub")]
all: VariableIDSet,
#[getset(get = "pub")]
binary: Bounds,
#[getset(get = "pub")]
integer: Bounds,
#[getset(get = "pub")]
continuous: Bounds,
#[getset(get = "pub")]
semi_integer: Bounds,
#[getset(get = "pub")]
semi_continuous: Bounds,
#[getset(get = "pub")]
used_in_objective: VariableIDSet,
#[getset(get = "pub")]
used_in_constraints: BTreeMap<ConstraintID, VariableIDSet>,
#[getset(get = "pub")]
used: VariableIDSet,
#[getset(get = "pub")]
fixed: BTreeMap<VariableID, f64>,
#[getset(get = "pub")]
dependent: BTreeMap<VariableID, (Kind, Bound, Function)>,
#[getset(get = "pub")]
irrelevant: BTreeMap<VariableID, (Kind, Bound)>,
}
impl DecisionVariableAnalysis {
pub fn used_binary(&self) -> Bounds {
let used_ids = self.used();
self.binary()
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, bound)| (*id, *bound))
.collect()
}
pub fn used_integer(&self) -> Bounds {
let used_ids = self.used();
self.integer()
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, bound)| (*id, *bound))
.collect()
}
pub fn used_continuous(&self) -> Bounds {
let used_ids = self.used();
self.continuous()
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, bound)| (*id, *bound))
.collect()
}
pub fn used_semi_integer(&self) -> Bounds {
let used_ids = self.used();
self.semi_integer()
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, bound)| (*id, *bound))
.collect()
}
pub fn used_semi_continuous(&self) -> Bounds {
let used_ids = self.used();
self.semi_continuous()
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, bound)| (*id, *bound))
.collect()
}
pub fn populate(&self, mut state: State, atol: ATol) -> Result<State, StateValidationError> {
let state_ids: VariableIDSet = state.entries.keys().map(|id| (*id).into()).collect();
let unknown_ids: VariableIDSet = state_ids.difference(&self.all).cloned().collect();
if !unknown_ids.is_empty() {
return Err(StateValidationError::UnknownIDs { unknown_ids });
}
let missing_ids: VariableIDSet = self.used().difference(&state_ids).cloned().collect();
if !missing_ids.is_empty() {
return Err(StateValidationError::MissingRequiredIDs { missing_ids });
}
for (id, value) in self.fixed() {
use std::collections::hash_map::Entry;
match state.entries.entry(id.into_inner()) {
Entry::Occupied(entry) => {
if (entry.get() - value).abs() > atol {
return Err(StateValidationError::StateValueInconsistent {
id: *id,
state_value: *entry.get(),
instance_value: *value,
});
}
}
Entry::Vacant(entry) => {
entry.insert(*value);
}
}
}
for (id, (kind, bound)) in self.irrelevant() {
use std::collections::hash_map::Entry;
match state.entries.entry(id.into_inner()) {
Entry::Occupied(_entry) => {
}
Entry::Vacant(entry) => {
let value = match kind {
Kind::Binary | Kind::Integer | Kind::Continuous => bound.nearest_to_zero(),
Kind::SemiInteger | Kind::SemiContinuous => 0.0,
};
entry.insert(value);
}
}
}
let acyclic = AcyclicAssignments::new(
self.dependent()
.iter()
.map(|(id, (_kind, _bound, f))| (*id, f.clone())),
)
.map_err(|error| StateValidationError::CyclicDependency { error })?;
for (id, f) in acyclic.evaluation_order_iter() {
let value = f.evaluate(&state, atol).map_err(|error| {
StateValidationError::FailedToEvaluateDependentVariable { id, error }
})?;
if let Some(v) = state.entries.insert(id.into_inner(), value) {
if (v - value).abs() > atol {
return Err(StateValidationError::StateValueInconsistent {
id,
state_value: v,
instance_value: value,
});
}
}
}
Ok(state)
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum StateValidationError {
#[error("The state contains some unknown IDs: {unknown_ids:?}")]
UnknownIDs { unknown_ids: VariableIDSet },
#[error("The state does not contain some required IDs: {missing_ids:?}")]
MissingRequiredIDs { missing_ids: VariableIDSet },
#[deprecated(
since = "2.2.0",
note = "Bound and kind constraints are no longer checked during state population. Use Solution::feasible_decision_variables() instead."
)]
#[error(
"Value for {kind:?} variable {id:?} is out of bounds. Value: {value}, Bound: {bound:?}"
)]
ValueOutOfBounds {
id: VariableID,
value: f64,
bound: Bound,
kind: Kind,
},
#[deprecated(
since = "2.2.0",
note = "Bound and kind constraints are no longer checked during state population. Use Solution::feasible_decision_variables() instead."
)]
#[error("Value for integer variable {id:?} is not an integer. Value: {value}")]
NotAnInteger { id: VariableID, value: f64 },
#[error("State's value for variable {id:?} is inconsistent to instance. State value: {state_value}, Instance value: {instance_value}")]
StateValueInconsistent {
id: VariableID,
state_value: f64,
instance_value: f64,
},
#[error("Evaluation of dependent variable {id:?} failed. Error: {error:?}")]
FailedToEvaluateDependentVariable {
id: VariableID,
error: anyhow::Error,
},
#[error("Cyclic dependency detected in dependent variables: {error}")]
CyclicDependency { error: crate::SubstitutionError },
}
impl Instance {
pub fn binary_ids(&self) -> VariableIDSet {
self.decision_variables
.iter()
.filter_map(|(id, dv)| {
if dv.kind() == Kind::Binary {
Some(*id)
} else {
None
}
})
.collect()
}
pub fn used_decision_variable_ids(&self) -> VariableIDSet {
let mut used = self.objective.required_ids();
for constraint in self.constraints.values() {
used.extend(constraint.function.required_ids());
}
used
}
pub fn used_decision_variables(&self) -> BTreeMap<VariableID, &DecisionVariable> {
let used_ids = self.used_decision_variable_ids();
self.decision_variables
.iter()
.filter(|(id, _)| used_ids.contains(id))
.map(|(id, dv)| (*id, dv))
.collect()
}
pub fn analyze_decision_variables(&self) -> DecisionVariableAnalysis {
let mut all = VariableIDSet::default();
let mut fixed = BTreeMap::default();
let mut binary = Bounds::default();
let mut integer = Bounds::default();
let mut continuous = Bounds::default();
let mut semi_integer = Bounds::default();
let mut semi_continuous = Bounds::default();
for (id, dv) in &self.decision_variables {
match dv.kind() {
Kind::Binary => binary.insert(*id, dv.bound()),
Kind::Integer => integer.insert(*id, dv.bound()),
Kind::Continuous => continuous.insert(*id, dv.bound()),
Kind::SemiInteger => semi_integer.insert(*id, dv.bound()),
Kind::SemiContinuous => semi_continuous.insert(*id, dv.bound()),
};
all.insert(*id);
if let Some(value) = dv.substituted_value() {
fixed.insert(*id, value);
}
}
let used_in_objective: VariableIDSet = self.objective.required_ids().into_iter().collect();
debug_assert!(
used_in_objective.is_subset(&all),
"Objective function uses variables not in the instance"
);
let mut used_in_constraints: BTreeMap<ConstraintID, VariableIDSet> = BTreeMap::default();
for constraint in self.constraints.values() {
let required_ids: VariableIDSet =
constraint.function.required_ids().into_iter().collect();
debug_assert!(
required_ids.is_subset(&all),
"Constraints use variables not in the instance"
);
used_in_constraints.insert(constraint.id, required_ids);
}
let mut used = used_in_objective.clone();
for ids in used_in_constraints.values() {
used.extend(ids);
}
let dependent: BTreeMap<VariableID, _> = self
.decision_variable_dependency
.iter()
.map(|(id, f)| {
let dv = self
.decision_variables
.get(id)
.expect("Invariant of Instance.decision_variable_dependency is violated");
(*id, (dv.kind(), dv.bound(), f.clone()))
})
.collect();
let relevant: VariableIDSet = used
.iter()
.chain(dependent.keys())
.chain(fixed.keys())
.cloned()
.collect();
let irrelevant = all
.difference(&relevant)
.map(|id| {
let dv = self.decision_variables.get(id).unwrap(); debug_assert!(dv.substituted_value().is_none()); (*id, (dv.kind(), dv.bound()))
})
.collect();
DecisionVariableAnalysis {
all,
fixed,
binary,
integer,
continuous,
semi_integer,
semi_continuous,
used_in_objective,
used_in_constraints,
used,
dependent,
irrelevant,
}
}
}
impl std::fmt::Display for DecisionVariableAnalysis {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "DecisionVariableAnalysis {{")?;
writeln!(f, " Total Variables: {}", self.all.len())?;
writeln!(f)?;
writeln!(f, " Kind-based Partitioning:")?;
writeln!(
f,
" Binary: {}, Integer: {}, Continuous: {}, Semi-Integer: {}, Semi-Continuous: {}",
self.binary.len(),
self.integer.len(),
self.continuous.len(),
self.semi_integer.len(),
self.semi_continuous.len()
)?;
writeln!(f)?;
writeln!(f, " Usage-based Partitioning:")?;
let used_in_constraints_count: usize = self
.used_in_constraints
.values()
.flat_map(|ids| ids.iter())
.collect::<std::collections::BTreeSet<_>>()
.len();
writeln!(
f,
" Used: {} (in objective: {}, in constraints: {}), Fixed: {}, Dependent: {}, Irrelevant: {}",
self.used.len(),
self.used_in_objective.len(),
used_in_constraints_count,
self.fixed.len(),
self.dependent.len(),
self.irrelevant.len()
)?;
if !self.binary.is_empty() {
writeln!(f, "\n Binary Variables ({}):", self.binary.len())?;
for (id, bound) in &self.binary {
writeln!(f, " x{}: {}", id.into_inner(), bound)?;
}
}
if !self.integer.is_empty() {
writeln!(f, "\n Integer Variables ({}):", self.integer.len())?;
for (id, bound) in &self.integer {
writeln!(f, " x{}: {}", id.into_inner(), bound)?;
}
}
if !self.continuous.is_empty() {
writeln!(f, "\n Continuous Variables ({}):", self.continuous.len())?;
for (id, bound) in &self.continuous {
writeln!(f, " x{}: {}", id.into_inner(), bound)?;
}
}
if !self.semi_integer.is_empty() {
writeln!(
f,
"\n Semi-Integer Variables ({}):",
self.semi_integer.len()
)?;
for (id, bound) in &self.semi_integer {
writeln!(f, " x{}: {}", id.into_inner(), bound)?;
}
}
if !self.semi_continuous.is_empty() {
writeln!(
f,
"\n Semi-Continuous Variables ({}):",
self.semi_continuous.len()
)?;
for (id, bound) in &self.semi_continuous {
writeln!(f, " x{}: {}", id.into_inner(), bound)?;
}
}
if !self.used_in_objective.is_empty() {
writeln!(
f,
"\n Used in Objective ({}):",
self.used_in_objective.len()
)?;
let vars: Vec<String> = self
.used_in_objective
.iter()
.map(|id| format!("x{}", id.into_inner()))
.collect();
writeln!(f, " {}", vars.join(", "))?;
}
if !self.used_in_constraints.is_empty() {
writeln!(
f,
"\n Used in Constraints ({} constraints):",
self.used_in_constraints.len()
)?;
for (constraint_id, var_ids) in &self.used_in_constraints {
write!(f, " {}: ", constraint_id)?;
let vars: Vec<String> = var_ids
.iter()
.map(|id| format!("x{}", id.into_inner()))
.collect();
writeln!(f, "{}", vars.join(", "))?;
}
}
if !self.fixed.is_empty() {
writeln!(f, "\n Fixed Variables ({}):", self.fixed.len())?;
for (id, value) in &self.fixed {
writeln!(f, " x{} = {}", id.into_inner(), value)?;
}
}
if !self.dependent.is_empty() {
writeln!(f, "\n Dependent Variables ({}):", self.dependent.len())?;
for (id, (kind, bound, function)) in &self.dependent {
writeln!(
f,
" x{} ({:?}, {}): {}",
id.into_inner(),
kind,
bound,
function
)?;
}
}
if !self.irrelevant.is_empty() {
writeln!(f, "\n Irrelevant Variables ({}):", self.irrelevant.len())?;
for (id, (kind, bound)) in &self.irrelevant {
let default_value = bound.nearest_to_zero();
writeln!(
f,
" x{} ({:?}, {}): will be set to {}",
id.into_inner(),
kind,
bound,
default_value
)?;
}
}
write!(f, "}}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
assign, coeff, linear, v1::State, Constraint, ConstraintID, Evaluate, Sense, Substitute,
VariableID,
};
use maplit::hashmap;
use proptest::prelude::*;
use std::collections::BTreeMap;
#[test]
fn test_decision_variable_analysis_display() {
let mut decision_variables = BTreeMap::new();
for i in 0..5 {
decision_variables.insert(
VariableID::from(i),
crate::DecisionVariable::binary(VariableID::from(i)),
);
}
let objective = crate::Function::from(linear!(0) + linear!(1) + linear!(2));
let mut constraints = BTreeMap::new();
constraints.insert(
ConstraintID::from(0),
Constraint::equal_to_zero(
ConstraintID::from(0),
(linear!(1) + linear!(2) + coeff!(-1.0)).into(),
),
);
constraints.insert(
ConstraintID::from(1),
Constraint::equal_to_zero(
ConstraintID::from(1),
(linear!(3) + coeff!(-1.0) * linear!(0) + coeff!(-1.0) * linear!(1)).into(),
),
);
let mut instance =
Instance::new(Sense::Maximize, objective, decision_variables, constraints).unwrap();
let state = State {
entries: hashmap! { 0 => 1.0 },
};
instance
.partial_evaluate(&state, crate::ATol::default())
.unwrap();
let substitutions = assign! {
3 <- linear!(0) + linear!(1)
};
let instance = instance.substitute_acyclic(&substitutions).unwrap();
let analysis = instance.analyze_decision_variables();
insta::assert_snapshot!(analysis);
}
#[test]
fn test_populate_dependent_variables_topological_order() {
use crate::{assign, coeff, linear, ATol, DecisionVariable, Sense};
use maplit::btreemap;
use std::collections::HashMap;
let decision_variables = btreemap! {
VariableID::from(1) => DecisionVariable::continuous(VariableID::from(1)),
VariableID::from(2) => DecisionVariable::continuous(VariableID::from(2)),
VariableID::from(5) => DecisionVariable::continuous(VariableID::from(5)),
VariableID::from(10) => DecisionVariable::continuous(VariableID::from(10)),
};
let dependency = assign! {
10 <- linear!(1) + linear!(2),
5 <- linear!(10) + coeff!(1.0)
};
let instance = Instance::builder()
.sense(Sense::Minimize)
.objective(Function::from(linear!(1) + linear!(2))) .decision_variables(decision_variables)
.constraints(btreemap! {})
.decision_variable_dependency(dependency)
.build()
.unwrap();
let analysis = instance.analyze_decision_variables();
assert!(analysis.dependent().contains_key(&VariableID::from(5)));
assert!(analysis.dependent().contains_key(&VariableID::from(10)));
let state = crate::v1::State::from(HashMap::from([(1, 2.0), (2, 3.0)]));
let populated = analysis.populate(state, ATol::default()).unwrap();
assert_eq!(populated.entries.get(&1), Some(&2.0));
assert_eq!(populated.entries.get(&2), Some(&3.0));
assert_eq!(populated.entries.get(&10), Some(&5.0)); assert_eq!(populated.entries.get(&5), Some(&6.0)); }
proptest! {
#[test]
fn test_kind_partition(instance in Instance::arbitrary()) {
let analysis = instance.analyze_decision_variables();
prop_assert_eq!(
analysis.all.len(),
analysis.binary.len() + analysis.integer.len() + analysis.continuous.len()
+ analysis.semi_integer.len() + analysis.semi_continuous.len()
);
let mut all: VariableIDSet = analysis.binary.keys().cloned().collect();
prop_assert_eq!(&all, &instance.binary_ids());
all.extend(analysis.integer.keys());
all.extend(analysis.continuous.keys());
all.extend(analysis.semi_integer.keys());
all.extend(analysis.semi_continuous.keys());
prop_assert_eq!(&all, &analysis.all);
}
#[test]
fn test_used_partition(instance in Instance::arbitrary()) {
let analysis = instance.analyze_decision_variables();
let used = analysis.used();
let all_len = analysis.all.len();
let used_len = used.len();
let fixed_len = analysis.fixed.len();
let dependent_len = analysis.dependent.len();
let irrelevant_len = analysis.irrelevant().len();
prop_assert_eq!(used, &instance.used_decision_variable_ids());
prop_assert_eq!(
all_len,
used_len + fixed_len + dependent_len + irrelevant_len,
"all: {}, used: {}, fixed: {}, dependent: {}, irrelevant: {}",
all_len, used_len, fixed_len, dependent_len, irrelevant_len
);
let mut all = used.clone();
all.extend(analysis.fixed.keys());
all.extend(analysis.dependent.keys());
all.extend(analysis.irrelevant.keys());
prop_assert_eq!(&all, &analysis.all);
}
#[test]
fn test_populate(
(instance, state) in Instance::arbitrary()
.prop_flat_map(move |instance| instance.arbitrary_state().prop_map(move |state| (instance.clone(), state)))
) {
let analysis = instance.analyze_decision_variables();
let populated = analysis.populate(state.clone(), ATol::default()).unwrap();
let populated_ids: VariableIDSet = populated.entries.keys().map(|id| (*id).into()).collect();
prop_assert_eq!(populated_ids, analysis.all);
}
}
}