use super::*;
use crate::constraint_type::ConstraintCollection;
#[derive(Debug, Clone, Default)]
pub struct ParametricInstanceBuilder {
sense: Option<Sense>,
objective: Option<Function>,
decision_variables: Option<BTreeMap<VariableID, DecisionVariable>>,
parameters: Option<BTreeMap<VariableID, v1::Parameter>>,
constraints: Option<BTreeMap<ConstraintID, Constraint>>,
named_functions: BTreeMap<NamedFunctionID, NamedFunction>,
removed_constraints: BTreeMap<ConstraintID, (Constraint, crate::constraint::RemovedReason)>,
indicator_constraints: BTreeMap<crate::IndicatorConstraintID, crate::IndicatorConstraint>,
removed_indicator_constraints: BTreeMap<
crate::IndicatorConstraintID,
(crate::IndicatorConstraint, crate::constraint::RemovedReason),
>,
one_hot_constraints: BTreeMap<crate::OneHotConstraintID, crate::OneHotConstraint>,
sos1_constraints: BTreeMap<crate::Sos1ConstraintID, crate::Sos1Constraint>,
decision_variable_dependency: AcyclicAssignments,
description: Option<v1::instance::Description>,
}
impl ParametricInstanceBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self
}
pub fn objective(mut self, objective: Function) -> Self {
self.objective = Some(objective);
self
}
pub fn decision_variables(
mut self,
decision_variables: BTreeMap<VariableID, DecisionVariable>,
) -> Self {
self.decision_variables = Some(decision_variables);
self
}
pub fn parameters(mut self, parameters: BTreeMap<VariableID, v1::Parameter>) -> Self {
self.parameters = Some(parameters);
self
}
pub fn constraints(mut self, constraints: BTreeMap<ConstraintID, Constraint>) -> Self {
self.constraints = Some(constraints);
self
}
pub fn named_functions(
mut self,
named_functions: BTreeMap<NamedFunctionID, NamedFunction>,
) -> Self {
self.named_functions = named_functions;
self
}
pub fn removed_constraints(
mut self,
removed_constraints: BTreeMap<ConstraintID, (Constraint, crate::constraint::RemovedReason)>,
) -> Self {
self.removed_constraints = removed_constraints;
self
}
pub fn indicator_constraints(
mut self,
indicator_constraints: BTreeMap<crate::IndicatorConstraintID, crate::IndicatorConstraint>,
) -> Self {
self.indicator_constraints = indicator_constraints;
self
}
pub fn removed_indicator_constraints(
mut self,
removed_indicator_constraints: BTreeMap<
crate::IndicatorConstraintID,
(crate::IndicatorConstraint, crate::constraint::RemovedReason),
>,
) -> Self {
self.removed_indicator_constraints = removed_indicator_constraints;
self
}
pub fn one_hot_constraints(
mut self,
one_hot_constraints: BTreeMap<crate::OneHotConstraintID, crate::OneHotConstraint>,
) -> Self {
self.one_hot_constraints = one_hot_constraints;
self
}
pub fn sos1_constraints(
mut self,
sos1_constraints: BTreeMap<crate::Sos1ConstraintID, crate::Sos1Constraint>,
) -> Self {
self.sos1_constraints = sos1_constraints;
self
}
pub fn decision_variable_dependency(
mut self,
decision_variable_dependency: AcyclicAssignments,
) -> Self {
self.decision_variable_dependency = decision_variable_dependency;
self
}
pub fn description(mut self, description: v1::instance::Description) -> Self {
self.description = Some(description);
self
}
pub fn build(self) -> crate::Result<ParametricInstance> {
let sense = self
.sense
.ok_or_else(|| crate::error!("Required field is missing: sense"))?;
let objective = self
.objective
.ok_or_else(|| crate::error!("Required field is missing: objective"))?;
let decision_variables = self
.decision_variables
.ok_or_else(|| crate::error!("Required field is missing: decision_variables"))?;
let parameters = self
.parameters
.ok_or_else(|| crate::error!("Required field is missing: parameters"))?;
let constraints = self
.constraints
.ok_or_else(|| crate::error!("Required field is missing: constraints"))?;
for (key, value) in &decision_variables {
if *key != value.id() {
let value_id = value.id();
crate::bail!(
{ ?key, ?value_id },
"Decision variable map key {key:?} does not match value's id {value_id:?}",
);
}
}
for (key, value) in ¶meters {
if key.into_inner() != value.id {
let value_id = value.id;
crate::bail!(
{ ?key, value_id },
"Parameter map key {key:?} does not match value's id {value_id}",
);
}
}
let decision_variable_ids: VariableIDSet = decision_variables.keys().cloned().collect();
let parameter_ids: VariableIDSet = parameters.keys().cloned().collect();
let intersection: VariableIDSet = decision_variable_ids
.intersection(¶meter_ids)
.cloned()
.collect();
if !intersection.is_empty() {
let id = *intersection.iter().next().unwrap();
crate::bail!(
{ ?id },
"Duplicated variable ID is found in definition: {id:?}",
);
}
let all_variable_ids: VariableIDSet = decision_variable_ids
.union(¶meter_ids)
.cloned()
.collect();
for id in objective.required_ids() {
if !all_variable_ids.contains(&id) {
crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
}
}
for constraint in constraints.values() {
for id in constraint.required_ids() {
if !all_variable_ids.contains(&id) {
crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
}
}
}
for (removed, _reason) in self.removed_constraints.values() {
for id in removed.required_ids() {
if !all_variable_ids.contains(&id) {
crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
}
}
}
for (key, nf) in &self.named_functions {
if *key != nf.id {
let id = nf.id;
crate::bail!(
{ ?key, ?id },
"Named function map key {key:?} does not match value's id {id:?}",
);
}
for id in nf.function.required_ids() {
if !all_variable_ids.contains(&id) {
crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
}
}
}
let validate_indicator = |ic: &crate::IndicatorConstraint| -> crate::Result<()> {
let indicator_id = ic.indicator_variable;
let Some(dv) = decision_variables.get(&indicator_id) else {
if parameter_ids.contains(&indicator_id) {
crate::bail!(
{ ?indicator_id },
"Parameter id {indicator_id:?} cannot occupy the structural indicator-variable position; it must be a binary decision variable",
);
}
crate::bail!(
{ ?indicator_id },
"Indicator variable {indicator_id:?} is not defined in decision_variables",
);
};
if dv.kind() != crate::decision_variable::Kind::Binary {
crate::bail!(
{ ?indicator_id },
"Indicator variable {indicator_id:?} must be binary",
);
}
for id in ic.required_ids() {
if !all_variable_ids.contains(&id) {
crate::bail!({ ?id }, "Undefined variable ID is used: {id:?}");
}
}
Ok(())
};
for value in self.indicator_constraints.values() {
validate_indicator(value)?;
}
for (ic, _reason) in self.removed_indicator_constraints.values() {
validate_indicator(ic)?;
}
for id in self.removed_indicator_constraints.keys() {
if self.indicator_constraints.contains_key(id) {
crate::bail!(
{ ?id },
"Indicator constraint ID {id:?} is in both indicator_constraints and removed_indicator_constraints, but they must be disjoint",
);
}
}
for value in self.one_hot_constraints.values() {
for var_id in &value.variables {
let Some(dv) = decision_variables.get(var_id) else {
if parameter_ids.contains(var_id) {
crate::bail!(
{ ?var_id },
"Parameter id {var_id:?} cannot occupy a structural one-hot variable position; it must be a binary decision variable",
);
}
crate::bail!(
{ ?var_id },
"One-hot variable {var_id:?} is not defined in decision_variables",
);
};
if dv.kind() != crate::decision_variable::Kind::Binary {
crate::bail!({ ?var_id }, "One-hot variable {var_id:?} must be binary");
}
}
}
for (id, value) in &self.sos1_constraints {
if value.variables.is_empty() {
crate::bail!(
{ ?id },
"SOS1 constraint {id:?} has no variables; SOS1 constraints must contain at least one variable",
);
}
for var_id in &value.variables {
if !decision_variable_ids.contains(var_id) {
if parameter_ids.contains(var_id) {
crate::bail!(
{ ?var_id },
"Parameter id {var_id:?} cannot occupy a structural SOS1 variable position; it must be a decision variable",
);
}
crate::bail!(
{ ?var_id },
"SOS1 variable {var_id:?} is not defined in decision_variables",
);
}
}
}
for id in self.removed_constraints.keys() {
if constraints.contains_key(id) {
crate::bail!(
{ ?id },
"Constraint ID {id:?} is in both constraints and removed_constraints, but they must be disjoint",
);
}
}
for id in self.decision_variable_dependency.keys() {
if !decision_variable_ids.contains(&id) {
crate::bail!(
{ ?id },
"Variable ID {id:?} in decision_variable_dependency is not in decision_variables",
);
}
}
let mut used: VariableIDSet = objective.required_ids().into_iter().collect();
for constraint in constraints.values() {
used.extend(constraint.required_ids());
}
for ic in self.indicator_constraints.values() {
used.extend(ic.required_ids());
}
for oh in self.one_hot_constraints.values() {
used.extend(oh.required_ids());
}
for sos1 in self.sos1_constraints.values() {
used.extend(sos1.required_ids());
}
let fixed: VariableIDSet = decision_variables
.values()
.filter(|dv| dv.substituted_value().is_some())
.map(|dv| dv.id())
.collect();
let dependent: VariableIDSet = self.decision_variable_dependency.keys().collect();
if let Some(id) = used.intersection(&dependent).next() {
crate::bail!(
{ ?id },
"Dependent variable cannot be used in objectives or constraints: {id:?}",
);
}
if let Some(id) = used.intersection(&fixed).next() {
crate::bail!(
{ ?id },
"Fixed variable {id:?} (substituted_value set) cannot be used in objectives or constraints",
);
}
if let Some(id) = fixed.intersection(&dependent).next() {
crate::bail!(
{ ?id },
"Variable {id:?} cannot be both fixed (substituted_value set) and dependent",
);
}
Ok(ParametricInstance {
sense,
objective,
decision_variables,
parameters,
variable_metadata: Default::default(),
constraint_collection: ConstraintCollection::new(constraints, self.removed_constraints),
indicator_constraint_collection: ConstraintCollection::new(
self.indicator_constraints,
self.removed_indicator_constraints,
),
one_hot_constraint_collection: ConstraintCollection::new(
self.one_hot_constraints,
BTreeMap::new(),
),
sos1_constraint_collection: ConstraintCollection::new(
self.sos1_constraints,
BTreeMap::new(),
),
named_functions: self.named_functions,
named_function_metadata: Default::default(),
decision_variable_dependency: self.decision_variable_dependency,
description: self.description,
})
}
}
impl ParametricInstance {
pub fn builder() -> ParametricInstanceBuilder {
ParametricInstanceBuilder::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parametric_builder_basic() {
let instance = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(BTreeMap::new())
.parameters(BTreeMap::new())
.constraints(BTreeMap::new())
.build()
.unwrap();
assert_eq!(*instance.sense(), Sense::Minimize);
assert!(instance.decision_variables().is_empty());
assert!(instance.parameters().is_empty());
assert!(instance.constraints().is_empty());
}
#[test]
fn test_parametric_builder_missing_required_field() {
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(BTreeMap::new())
.constraints(BTreeMap::new())
.build()
.unwrap_err();
assert!(
err.to_string().contains("missing: parameters"),
"unexpected error: {err}"
);
}
#[test]
fn test_parametric_builder_overlapping_ids() {
use maplit::btreemap;
let var_id = VariableID::from(1);
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
var_id => DecisionVariable::binary(var_id),
})
.parameters(btreemap! {
var_id => v1::Parameter { id: 1, ..Default::default() },
})
.constraints(BTreeMap::new())
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Duplicated variable ID") && msg.contains(&format!("{:?}", var_id)),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_inconsistent_named_function_id() {
use crate::{NamedFunction, NamedFunctionID};
use maplit::btreemap;
let named_function = NamedFunction {
id: NamedFunctionID::from(1),
function: Function::Zero,
};
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(BTreeMap::new())
.parameters(BTreeMap::new())
.constraints(BTreeMap::new())
.named_functions(btreemap! {
NamedFunctionID::from(2) => named_function, })
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Named function map key")
&& msg.contains("NamedFunctionID(2)")
&& msg.contains("NamedFunctionID(1)"),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_undefined_variable_in_named_function() {
use crate::{coeff, linear, NamedFunction, NamedFunctionID};
use maplit::btreemap;
let named_function = NamedFunction {
id: NamedFunctionID::from(1),
function: Function::from(linear!(999) + coeff!(1.0)),
};
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(BTreeMap::new())
.parameters(BTreeMap::new())
.constraints(BTreeMap::new())
.named_functions(btreemap! {
NamedFunctionID::from(1) => named_function,
})
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Undefined variable ID") && msg.contains("999"),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_with_indicator_constraint() {
use crate::{coeff, linear, Equality};
use maplit::btreemap;
let indicator = crate::IndicatorConstraint::new(
VariableID::from(1),
Equality::EqualToZero,
Function::from(linear!(2) + linear!(100) + coeff!(1.0)),
);
let instance = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
})
.parameters(btreemap! {
VariableID::from(100) => v1::Parameter { id: 100, ..Default::default() },
})
.constraints(BTreeMap::new())
.indicator_constraints(btreemap! {
crate::IndicatorConstraintID::from(7) => indicator,
})
.build()
.unwrap();
assert_eq!(instance.indicator_constraints().len(), 1);
assert!(instance
.indicator_constraints()
.contains_key(&crate::IndicatorConstraintID::from(7)));
}
#[test]
fn test_parametric_builder_rejects_parameter_as_indicator_variable() {
use crate::{coeff, linear, Equality};
use maplit::btreemap;
let indicator = crate::IndicatorConstraint::new(
VariableID::from(100), Equality::EqualToZero,
Function::from(linear!(1) + coeff!(1.0)),
);
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
})
.parameters(btreemap! {
VariableID::from(100) => v1::Parameter { id: 100, ..Default::default() },
})
.constraints(BTreeMap::new())
.indicator_constraints(btreemap! {
crate::IndicatorConstraintID::from(0) => indicator,
})
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Parameter id") && msg.contains("structural"),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_rejects_non_binary_indicator_variable() {
use crate::{coeff, linear, Equality};
use maplit::btreemap;
let indicator = crate::IndicatorConstraint::new(
VariableID::from(1),
Equality::EqualToZero,
Function::from(linear!(2) + coeff!(1.0)),
);
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
VariableID::from(1) => DecisionVariable::integer(VariableID::from(1)),
VariableID::from(2) => DecisionVariable::binary(VariableID::from(2)),
})
.parameters(BTreeMap::new())
.constraints(BTreeMap::new())
.indicator_constraints(btreemap! {
crate::IndicatorConstraintID::from(0) => indicator,
})
.build()
.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("must be binary"), "unexpected error: {msg}");
}
#[test]
fn test_parametric_builder_rejects_parameter_in_one_hot_variables() {
use maplit::btreemap;
let one_hot = crate::OneHotConstraint::new(
[VariableID::from(1), VariableID::from(100)] .into_iter()
.collect(),
);
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
})
.parameters(btreemap! {
VariableID::from(100) => v1::Parameter { id: 100, ..Default::default() },
})
.constraints(BTreeMap::new())
.one_hot_constraints(btreemap! {
crate::OneHotConstraintID::from(0) => one_hot,
})
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Parameter id") && msg.contains("structural"),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_rejects_empty_sos1() {
use maplit::btreemap;
let sos1 = crate::Sos1Constraint::new(std::collections::BTreeSet::new());
let err = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
VariableID::from(1) => DecisionVariable::binary(VariableID::from(1)),
})
.parameters(BTreeMap::new())
.constraints(BTreeMap::new())
.sos1_constraints(btreemap! {
crate::Sos1ConstraintID::from(0) => sos1,
})
.build()
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("at least one variable"),
"unexpected error: {msg}"
);
}
#[test]
fn test_parametric_builder_named_function_with_parameter() {
use crate::{linear, NamedFunction, NamedFunctionID};
use maplit::btreemap;
let var_id = VariableID::from(1);
let param_id = VariableID::from(2);
let named_function = NamedFunction {
id: NamedFunctionID::from(1),
function: Function::from(linear!(1) + linear!(2)), };
let instance = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::Zero)
.decision_variables(btreemap! {
var_id => DecisionVariable::binary(var_id),
})
.parameters(btreemap! {
param_id => v1::Parameter { id: 2, ..Default::default() },
})
.constraints(BTreeMap::new())
.named_functions(btreemap! {
NamedFunctionID::from(1) => named_function,
})
.build()
.unwrap();
assert_eq!(instance.named_functions().len(), 1);
}
}