mod parse;
mod serialize;
use crate::{
constraint_type::EvaluatedCollection,
decision_variable::{EvaluatedDecisionVariableTable, VariableLabelStore},
indicator_constraint::IndicatorConstraint,
ATol, Constraint, ConstraintID, EvaluatedConstraint, EvaluatedDecisionVariable,
EvaluatedNamedFunction, NamedFunctionID, NamedFunctionTable, Sense, VariableID,
};
use getset::Getters;
use std::collections::{BTreeMap, BTreeSet, HashMap};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum SolutionError {
#[error("Inconsistent feasibility for solution: provided={provided_feasible}, computed={computed_feasible}")]
InconsistentFeasibility {
provided_feasible: bool,
computed_feasible: bool,
},
#[error("Inconsistent feasibility (relaxed) for solution: provided={provided_feasible_relaxed}, computed={computed_feasible_relaxed}")]
InconsistentFeasibilityRelaxed {
provided_feasible_relaxed: bool,
computed_feasible_relaxed: bool,
},
#[error("Inconsistent feasibility for {constraint_family} constraint {constraint_id}: provided={provided_feasible}, computed={computed_feasible}")]
InconsistentConstraintFeasibility {
constraint_family: &'static str,
constraint_id: String,
provided_feasible: bool,
computed_feasible: bool,
},
#[error("Invalid structure for {constraint_family} constraint {constraint_id}: {message}")]
InvalidConstraintStructure {
constraint_family: &'static str,
constraint_id: String,
message: String,
},
#[error("Inconsistent value for variable {id}: state={state_value}, substituted_value={substituted_value}")]
InconsistentVariableValue {
id: u64,
state_value: f64,
substituted_value: f64,
},
#[error("Missing value for variable {id}: not found in state and no substituted_value")]
MissingVariableValue { id: u64 },
#[error("Duplicated variable ID is found in definition: {id:?}")]
DuplicatedVariableID { id: VariableID },
#[error("Duplicated named function ID is found in definition: {id:?}")]
DuplicatedNamedFunctionID { id: NamedFunctionID },
#[deprecated(
note = "Parameters are now ignored in extract_decision_variables and extract_all_decision_variables"
)]
#[error("Decision variable with parameters is not supported")]
ParameterizedVariable,
#[error("Constraint with parameters is not supported")]
ParameterizedConstraint,
#[error("Duplicate subscript: {subscripts:?}")]
DuplicateSubscript { subscripts: Vec<i64> },
#[error("Unknown constraint ID: {id:?}")]
UnknownConstraintID { id: ConstraintID },
#[error("No decision variables with name '{name}' found")]
UnknownVariableName { name: String },
#[error("No constraint with name '{name}' found")]
UnknownConstraintName { name: String },
#[error("Unknown named function ID: {id:?}")]
UnknownNamedFunctionID { id: NamedFunctionID },
#[error("No named function with name '{name}' found")]
UnknownNamedFunctionName { name: String },
#[deprecated(
note = "Parameters are now allowed in extract methods; only subscripts are used as keys"
)]
#[error("Named function with parameters is not supported")]
ParameterizedNamedFunction,
#[error("Required field is missing: {field}")]
MissingRequiredField { field: &'static str },
#[error(
"Variable ID {id:?} used in constraint {constraint_id:?} is not in decision_variables"
)]
UndefinedVariableInConstraint {
id: VariableID,
constraint_id: ConstraintID,
},
#[error(
"Variable ID {id:?} used in named function {named_function_id:?} is not in decision_variables"
)]
UndefinedVariableInNamedFunction {
id: VariableID,
named_function_id: NamedFunctionID,
},
#[error("{message}")]
InvalidSidecar { message: String },
}
#[derive(Debug, Clone, PartialEq, Getters)]
pub struct Solution {
#[getset(get = "pub")]
objective: f64,
#[getset(get = "pub")]
evaluated_constraints: EvaluatedCollection<Constraint>,
#[getset(get = "pub")]
evaluated_indicator_constraints: EvaluatedCollection<IndicatorConstraint>,
#[getset(get = "pub")]
evaluated_one_hot_constraints: EvaluatedCollection<crate::OneHotConstraint>,
#[getset(get = "pub")]
evaluated_sos1_constraints: EvaluatedCollection<crate::Sos1Constraint>,
evaluated_named_functions: NamedFunctionTable<EvaluatedNamedFunction>,
decision_variables: EvaluatedDecisionVariableTable,
pub optimality: crate::v1::Optimality,
pub relaxation: crate::v1::Relaxation,
#[getset(get = "pub")]
sense: Option<Sense>,
#[getset(get_copy = "pub")]
feasibility_atol: ATol,
pub metadata: Option<crate::v1::ProcessMetadata>,
pub annotations: HashMap<String, String>,
}
fn validate_evaluated_named_function_used_ids(
decision_variables: &EvaluatedDecisionVariableTable,
evaluated_named_functions: &NamedFunctionTable<EvaluatedNamedFunction>,
) -> Result<(), SolutionError> {
for (named_function_id, named_function) in evaluated_named_functions.iter() {
for var_id in named_function.used_decision_variable_ids() {
if !decision_variables.contains_key(var_id) {
return Err(SolutionError::UndefinedVariableInNamedFunction {
id: *var_id,
named_function_id: *named_function_id,
});
}
}
}
Ok(())
}
impl Solution {
pub fn evaluated_named_function_table(&self) -> &NamedFunctionTable<EvaluatedNamedFunction> {
&self.evaluated_named_functions
}
pub fn evaluated_named_functions(&self) -> &BTreeMap<NamedFunctionID, EvaluatedNamedFunction> {
self.evaluated_named_functions.entries()
}
pub fn named_function_labels(&self) -> &crate::named_function::NamedFunctionLabelStore {
self.evaluated_named_functions.labels()
}
pub fn decision_variable_table(&self) -> &EvaluatedDecisionVariableTable {
&self.decision_variables
}
pub fn decision_variables(&self) -> &BTreeMap<VariableID, EvaluatedDecisionVariable> {
self.decision_variables.entries()
}
pub fn variable_labels(&self) -> &VariableLabelStore {
self.decision_variables.labels()
}
pub fn decision_variable_ids(&self) -> BTreeSet<VariableID> {
self.decision_variables.keys().cloned().collect()
}
pub fn constraint_ids(&self) -> BTreeSet<ConstraintID> {
self.evaluated_constraints.keys().cloned().collect()
}
pub fn named_function_ids(&self) -> BTreeSet<NamedFunctionID> {
self.evaluated_named_functions.keys().cloned().collect()
}
pub fn feasible_decision_variables(&self) -> bool {
self.decision_variables
.values()
.all(|dv| dv.is_valid(self.feasibility_atol))
}
pub fn feasible_constraints(&self) -> bool {
self.evaluated_constraints.is_feasible()
&& self.evaluated_indicator_constraints.is_feasible()
&& self.evaluated_one_hot_constraints.is_feasible()
&& self.evaluated_sos1_constraints.is_feasible()
}
pub fn feasible(&self) -> bool {
self.feasible_constraints() && self.feasible_decision_variables()
}
pub fn feasible_constraints_relaxed(&self) -> bool {
self.evaluated_constraints.is_feasible_relaxed()
&& self.evaluated_indicator_constraints.is_feasible_relaxed()
&& self.evaluated_one_hot_constraints.is_feasible_relaxed()
&& self.evaluated_sos1_constraints.is_feasible_relaxed()
}
pub fn feasible_relaxed(&self) -> bool {
self.feasible_constraints_relaxed() && self.feasible_decision_variables()
}
pub fn total_violation_l1(&self) -> f64 {
self.evaluated_constraints
.values()
.map(|c| c.violation())
.sum()
}
pub fn total_violation_l2(&self) -> f64 {
self.evaluated_constraints
.values()
.map(|c| {
let v = c.violation();
v * v
})
.sum()
}
pub fn state(&self) -> crate::v1::State {
let entries = self
.decision_variables
.iter()
.map(|(id, dv)| (id.into_inner(), *dv.value()))
.collect();
crate::v1::State { entries }
}
pub fn decision_variable_names(&self) -> BTreeSet<String> {
self.decision_variables
.keys()
.filter_map(|id| self.variable_labels().name(*id).map(|s| s.to_owned()))
.collect()
}
pub fn extract_decision_variables(
&self,
name: &str,
) -> Result<BTreeMap<Vec<i64>, f64>, SolutionError> {
let variables_with_name: Vec<(VariableID, &EvaluatedDecisionVariable)> = self
.decision_variables
.iter()
.filter(|(id, _)| self.variable_labels().name(**id) == Some(name))
.map(|(id, v)| (*id, v))
.collect();
if variables_with_name.is_empty() {
return Err(SolutionError::UnknownVariableName {
name: name.to_string(),
});
}
let mut result = BTreeMap::new();
for (id, dv) in &variables_with_name {
let key = self.variable_labels().subscripts(*id).to_vec();
if result.contains_key(&key) {
return Err(SolutionError::DuplicateSubscript { subscripts: key });
}
result.insert(key, *dv.value());
}
Ok(result)
}
pub fn extract_all_decision_variables(
&self,
) -> Result<BTreeMap<String, BTreeMap<Vec<i64>, f64>>, SolutionError> {
let mut result: BTreeMap<String, BTreeMap<Vec<i64>, f64>> = BTreeMap::new();
for (id, dv) in self.decision_variables.iter() {
let name = match self.variable_labels().name(*id) {
Some(n) => n.to_owned(),
None => continue, };
let subscripts = self.variable_labels().subscripts(*id).to_vec();
let value = *dv.value();
let vars_map = result.entry(name).or_default();
if vars_map.contains_key(&subscripts) {
return Err(SolutionError::DuplicateSubscript { subscripts });
}
vars_map.insert(subscripts, value);
}
Ok(result)
}
pub fn extract_constraints(
&self,
name: &str,
) -> Result<BTreeMap<Vec<i64>, f64>, SolutionError> {
let context = self.evaluated_constraints.context();
let matches: Vec<(ConstraintID, &EvaluatedConstraint)> = self
.evaluated_constraints
.iter()
.filter(|(id, _)| context.name(**id) == Some(name))
.map(|(id, c)| (*id, c))
.collect();
if matches.is_empty() {
return Err(SolutionError::UnknownConstraintName {
name: name.to_string(),
});
}
let mut result = BTreeMap::new();
for (id, ec) in &matches {
if !context.parameters(*id).is_empty() {
return Err(SolutionError::ParameterizedConstraint);
}
let key = context.subscripts(*id).to_vec();
if result.contains_key(&key) {
return Err(SolutionError::DuplicateSubscript { subscripts: key });
}
result.insert(key, ec.stage.evaluated_value);
}
Ok(result)
}
pub fn get_constraint_value(&self, constraint_id: ConstraintID) -> Result<f64, SolutionError> {
self.evaluated_constraints
.get(&constraint_id)
.map(|c| c.stage.evaluated_value)
.ok_or(SolutionError::UnknownConstraintID { id: constraint_id })
}
pub fn get_dual_variable(
&self,
constraint_id: ConstraintID,
) -> Result<Option<f64>, SolutionError> {
self.evaluated_constraints
.get(&constraint_id)
.map(|c| c.stage.dual_variable)
.ok_or(SolutionError::UnknownConstraintID { id: constraint_id })
}
pub fn set_dual_variable(
&mut self,
constraint_id: ConstraintID,
value: Option<f64>,
) -> Result<(), SolutionError> {
if let Some(mut constraint) = self.evaluated_constraints.get(&constraint_id).cloned() {
constraint.stage.dual_variable = value;
assert!(
self.evaluated_constraints
.replace_evaluated_row(constraint_id, constraint)
.is_some(),
"constraint presence was verified above"
);
Ok(())
} else {
Err(SolutionError::UnknownConstraintID { id: constraint_id })
}
}
pub fn named_function_names(&self) -> BTreeSet<String> {
self.evaluated_named_functions
.keys()
.filter_map(|id| self.named_function_labels().name(*id).map(str::to_owned))
.collect()
}
pub fn extract_named_functions(
&self,
name: &str,
) -> Result<BTreeMap<Vec<i64>, f64>, SolutionError> {
let functions_with_name: Vec<(NamedFunctionID, &EvaluatedNamedFunction)> = self
.evaluated_named_functions
.iter()
.filter(|(id, _)| self.named_function_labels().name(**id) == Some(name))
.map(|(id, nf)| (*id, nf))
.collect();
if functions_with_name.is_empty() {
return Err(SolutionError::UnknownNamedFunctionName {
name: name.to_string(),
});
}
let mut result = BTreeMap::new();
for (id, nf) in &functions_with_name {
let key = self.named_function_labels().subscripts(*id).to_vec();
if result.contains_key(&key) {
return Err(SolutionError::DuplicateSubscript { subscripts: key });
}
result.insert(key, nf.evaluated_value());
}
Ok(result)
}
pub fn extract_all_named_functions(
&self,
) -> Result<BTreeMap<String, BTreeMap<Vec<i64>, f64>>, SolutionError> {
let mut result: BTreeMap<String, BTreeMap<Vec<i64>, f64>> = BTreeMap::new();
for (id, nf) in &self.evaluated_named_functions {
let name = match self.named_function_labels().name(*id) {
Some(n) => n.to_string(),
None => continue, };
let subscripts = self.named_function_labels().subscripts(*id).to_vec();
let value = nf.evaluated_value();
let funcs_map = result.entry(name).or_default();
if funcs_map.contains_key(&subscripts) {
return Err(SolutionError::DuplicateSubscript { subscripts });
}
funcs_map.insert(subscripts, value);
}
Ok(result)
}
pub fn builder() -> SolutionBuilder {
SolutionBuilder::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct SolutionBuilder {
objective: Option<f64>,
evaluated_constraints: Option<EvaluatedCollection<Constraint>>,
evaluated_indicator_constraints: EvaluatedCollection<IndicatorConstraint>,
evaluated_one_hot_constraints: EvaluatedCollection<crate::OneHotConstraint>,
evaluated_sos1_constraints: EvaluatedCollection<crate::Sos1Constraint>,
evaluated_named_function_table: Option<NamedFunctionTable<EvaluatedNamedFunction>>,
evaluated_named_functions: BTreeMap<NamedFunctionID, EvaluatedNamedFunction>,
decision_variables: Option<BTreeMap<VariableID, EvaluatedDecisionVariable>>,
variable_labels: VariableLabelStore,
named_function_labels: crate::named_function::NamedFunctionLabelStore,
sense: Option<Sense>,
feasibility_atol: ATol,
optimality: crate::v1::Optimality,
relaxation: crate::v1::Relaxation,
}
fn expected_regular_constraint_feasible(
equality: crate::Equality,
evaluated_value: f64,
atol: ATol,
) -> bool {
match equality {
crate::Equality::EqualToZero => evaluated_value.abs() < *atol,
crate::Equality::LessThanOrEqualToZero => evaluated_value < *atol,
}
}
fn expected_indicator_constraint_feasible(
equality: crate::Equality,
evaluated_value: f64,
indicator_active: bool,
atol: ATol,
) -> bool {
if !indicator_active {
return true;
}
expected_regular_constraint_feasible(equality, evaluated_value, atol)
}
fn validate_solution_constraint_feasibility(
regular_constraints: &EvaluatedCollection<Constraint>,
indicator_constraints: &EvaluatedCollection<IndicatorConstraint>,
one_hot_constraints: &EvaluatedCollection<crate::OneHotConstraint>,
sos1_constraints: &EvaluatedCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SolutionError> {
for (id, constraint) in regular_constraints.inner() {
let computed_feasible = expected_regular_constraint_feasible(
constraint.equality,
constraint.stage.evaluated_value,
atol,
);
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "regular",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
for (id, constraint) in indicator_constraints.inner() {
let computed_feasible = expected_indicator_constraint_feasible(
constraint.equality,
constraint.stage.evaluated_value,
constraint.stage.indicator_active,
atol,
);
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
for (id, constraint) in one_hot_constraints.inner() {
let computed_feasible = constraint.stage.active_variable.is_some();
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
for (id, constraint) in sos1_constraints.inner() {
if constraint.stage.active_variable.is_some() && !constraint.stage.feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible: true,
});
}
}
Ok(())
}
fn validate_solution_special_constraint_structure(
decision_variables: &EvaluatedDecisionVariableTable,
indicator_constraints: &EvaluatedCollection<IndicatorConstraint>,
one_hot_constraints: &EvaluatedCollection<crate::OneHotConstraint>,
sos1_constraints: &EvaluatedCollection<crate::Sos1Constraint>,
) -> Result<(), SolutionError> {
for (id, constraint) in indicator_constraints.inner() {
let variable_id = constraint.indicator_variable;
let Some(variable) = decision_variables.get(&variable_id) else {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!("indicator variable {variable_id:?} is not in decision_variables"),
});
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!("indicator variable {variable_id:?} must be binary"),
});
}
}
for (id, constraint) in one_hot_constraints.inner() {
if constraint.variables.is_empty() {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: "one-hot constraints must contain at least one variable".to_string(),
});
}
for variable_id in &constraint.variables {
let Some(variable) = decision_variables.get(variable_id) else {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} is not in decision_variables"),
});
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} must be binary"),
});
}
}
if constraint
.stage
.active_variable
.is_some_and(|variable_id| !constraint.variables.contains(&variable_id))
{
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: "active_variable must be a member of variables".to_string(),
});
}
}
for (id, constraint) in sos1_constraints.inner() {
if constraint.variables.is_empty() {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: "SOS1 constraints must contain at least one variable".to_string(),
});
}
for variable_id in &constraint.variables {
if !decision_variables.contains_key(variable_id) {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} is not in decision_variables"),
});
}
}
if constraint
.stage
.active_variable
.is_some_and(|variable_id| !constraint.variables.contains(&variable_id))
{
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: "active_variable must be a member of variables".to_string(),
});
}
}
Ok(())
}
fn expected_binary_activity(
variable_id: VariableID,
value: f64,
atol: ATol,
role: &'static str,
) -> Result<bool, String> {
if (value - 1.0).abs() < *atol {
Ok(true)
} else if value.abs() < *atol {
Ok(false)
} else {
Err(format!(
"{role} variable {variable_id:?} has value {value}, but must be 0 or 1",
))
}
}
fn expected_one_hot_active_variable(
variables: &BTreeSet<VariableID>,
decision_variables: &EvaluatedDecisionVariableTable,
atol: ATol,
) -> (bool, Option<VariableID>) {
let mut active = None;
for variable_id in variables {
let value = *decision_variables
.get(variable_id)
.expect("one-hot structural variables must be validated first")
.value();
if (value - 1.0).abs() < *atol {
if active.is_some() {
return (false, None);
}
active = Some(*variable_id);
} else if value.abs() >= *atol {
return (false, None);
}
}
match active {
Some(variable_id) => (true, Some(variable_id)),
None => (false, None),
}
}
fn expected_sos1_active_variable(
variables: &BTreeSet<VariableID>,
decision_variables: &EvaluatedDecisionVariableTable,
atol: ATol,
) -> (bool, Option<VariableID>) {
let mut active = None;
for variable_id in variables {
let value = *decision_variables
.get(variable_id)
.expect("SOS1 structural variables must be validated first")
.value();
if value.abs() >= *atol {
if active.is_some() {
return (false, None);
}
active = Some(*variable_id);
}
}
(true, active)
}
fn validate_solution_indicator_stage_values(
decision_variables: &EvaluatedDecisionVariableTable,
indicator_constraints: &EvaluatedCollection<IndicatorConstraint>,
atol: ATol,
) -> Result<(), SolutionError> {
for (id, constraint) in indicator_constraints.inner() {
let variable = decision_variables
.get(&constraint.indicator_variable)
.expect("indicator structural variables must be validated first");
let computed_indicator_active = expected_binary_activity(
constraint.indicator_variable,
*variable.value(),
atol,
"indicator",
)
.map_err(|message| SolutionError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message,
})?;
if constraint.stage.indicator_active != computed_indicator_active {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!(
"indicator_active={} does not match indicator variable {:?} value {}",
constraint.stage.indicator_active,
constraint.indicator_variable,
variable.value(),
),
});
}
let computed_feasible = expected_indicator_constraint_feasible(
constraint.equality,
constraint.stage.evaluated_value,
computed_indicator_active,
atol,
);
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
Ok(())
}
fn validate_solution_one_hot_stage_values(
decision_variables: &EvaluatedDecisionVariableTable,
one_hot_constraints: &EvaluatedCollection<crate::OneHotConstraint>,
atol: ATol,
) -> Result<(), SolutionError> {
for (id, constraint) in one_hot_constraints.inner() {
let (computed_feasible, computed_active_variable) =
expected_one_hot_active_variable(&constraint.variables, decision_variables, atol);
if constraint.stage.active_variable != computed_active_variable {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!(
"active_variable={:?} does not match decision-variable values; computed={computed_active_variable:?}",
constraint.stage.active_variable,
),
});
}
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
Ok(())
}
fn validate_solution_sos1_stage_values(
decision_variables: &EvaluatedDecisionVariableTable,
sos1_constraints: &EvaluatedCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SolutionError> {
for (id, constraint) in sos1_constraints.inner() {
let (computed_feasible, computed_active_variable) =
expected_sos1_active_variable(&constraint.variables, decision_variables, atol);
if constraint.stage.active_variable != computed_active_variable {
return Err(SolutionError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: format!(
"active_variable={:?} does not match decision-variable values; computed={computed_active_variable:?}",
constraint.stage.active_variable,
),
});
}
if constraint.stage.feasible != computed_feasible {
return Err(SolutionError::InconsistentConstraintFeasibility {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
provided_feasible: constraint.stage.feasible,
computed_feasible,
});
}
}
Ok(())
}
fn validate_solution_special_constraint_stage_values(
decision_variables: &EvaluatedDecisionVariableTable,
indicator_constraints: &EvaluatedCollection<IndicatorConstraint>,
one_hot_constraints: &EvaluatedCollection<crate::OneHotConstraint>,
sos1_constraints: &EvaluatedCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SolutionError> {
validate_solution_indicator_stage_values(decision_variables, indicator_constraints, atol)?;
validate_solution_one_hot_stage_values(decision_variables, one_hot_constraints, atol)?;
validate_solution_sos1_stage_values(decision_variables, sos1_constraints, atol)
}
impl SolutionBuilder {
pub fn new() -> Self {
Self {
optimality: crate::v1::Optimality::Unspecified,
relaxation: crate::v1::Relaxation::Unspecified,
..Default::default()
}
}
pub fn objective(mut self, objective: f64) -> Self {
self.objective = Some(objective);
self
}
pub fn evaluated_constraints(
mut self,
evaluated_constraints: BTreeMap<ConstraintID, EvaluatedConstraint>,
) -> Self {
self.evaluated_constraints = Some(
EvaluatedCollection::new(evaluated_constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints"),
);
self
}
pub fn evaluated_constraints_collection(
mut self,
evaluated_constraints: EvaluatedCollection<Constraint>,
) -> Self {
self.evaluated_constraints = Some(evaluated_constraints);
self
}
pub fn evaluated_indicator_constraints_collection(
mut self,
evaluated_indicator_constraints: EvaluatedCollection<IndicatorConstraint>,
) -> Self {
self.evaluated_indicator_constraints = evaluated_indicator_constraints;
self
}
pub fn evaluated_indicator_constraints(
mut self,
evaluated_indicator_constraints: BTreeMap<
crate::IndicatorConstraintID,
crate::indicator_constraint::EvaluatedIndicatorConstraint,
>,
) -> Self {
self.evaluated_indicator_constraints =
EvaluatedCollection::new(evaluated_indicator_constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints");
self
}
pub fn evaluated_one_hot_constraints_collection(
mut self,
evaluated_one_hot_constraints: EvaluatedCollection<crate::OneHotConstraint>,
) -> Self {
self.evaluated_one_hot_constraints = evaluated_one_hot_constraints;
self
}
pub fn evaluated_sos1_constraints_collection(
mut self,
evaluated_sos1_constraints: EvaluatedCollection<crate::Sos1Constraint>,
) -> Self {
self.evaluated_sos1_constraints = evaluated_sos1_constraints;
self
}
pub fn evaluated_named_functions(
mut self,
evaluated_named_functions: BTreeMap<NamedFunctionID, EvaluatedNamedFunction>,
) -> Self {
self.evaluated_named_function_table = None;
self.evaluated_named_functions = evaluated_named_functions;
self
}
pub(crate) fn evaluated_named_function_table(
mut self,
evaluated_named_functions: NamedFunctionTable<EvaluatedNamedFunction>,
) -> Self {
self.evaluated_named_function_table = Some(evaluated_named_functions);
self
}
pub fn decision_variables(
mut self,
decision_variables: BTreeMap<VariableID, EvaluatedDecisionVariable>,
) -> Self {
self.decision_variables = Some(decision_variables);
self
}
pub fn variable_labels(mut self, variable_labels: VariableLabelStore) -> Self {
self.variable_labels = variable_labels;
self
}
pub fn named_function_labels(
mut self,
named_function_labels: crate::named_function::NamedFunctionLabelStore,
) -> Self {
self.evaluated_named_function_table = None;
self.named_function_labels = named_function_labels;
self
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self
}
pub fn feasibility_atol(mut self, feasibility_atol: ATol) -> Self {
self.feasibility_atol = feasibility_atol;
self
}
pub fn optimality(mut self, optimality: crate::v1::Optimality) -> Self {
self.optimality = optimality;
self
}
pub fn relaxation(mut self, relaxation: crate::v1::Relaxation) -> Self {
self.relaxation = relaxation;
self
}
pub fn build(self) -> crate::Result<Solution> {
let objective = self
.objective
.ok_or(SolutionError::MissingRequiredField { field: "objective" })?;
let evaluated_constraints =
self.evaluated_constraints
.ok_or(SolutionError::MissingRequiredField {
field: "evaluated_constraints",
})?;
let decision_variables =
self.decision_variables
.ok_or(SolutionError::MissingRequiredField {
field: "decision_variables",
})?;
let sense = self
.sense
.ok_or(SolutionError::MissingRequiredField { field: "sense" })?;
let decision_variables =
EvaluatedDecisionVariableTable::new(decision_variables, self.variable_labels)?;
let evaluated_named_functions = match self.evaluated_named_function_table {
Some(evaluated_named_functions) => evaluated_named_functions,
None => {
NamedFunctionTable::new(self.evaluated_named_functions, self.named_function_labels)?
}
};
evaluated_constraints.validate_context_ids()?;
self.evaluated_indicator_constraints
.validate_context_ids()?;
self.evaluated_one_hot_constraints.validate_context_ids()?;
self.evaluated_sos1_constraints.validate_context_ids()?;
validate_solution_constraint_feasibility(
&evaluated_constraints,
&self.evaluated_indicator_constraints,
&self.evaluated_one_hot_constraints,
&self.evaluated_sos1_constraints,
self.feasibility_atol,
)?;
validate_solution_special_constraint_structure(
&decision_variables,
&self.evaluated_indicator_constraints,
&self.evaluated_one_hot_constraints,
&self.evaluated_sos1_constraints,
)?;
validate_solution_special_constraint_stage_values(
&decision_variables,
&self.evaluated_indicator_constraints,
&self.evaluated_one_hot_constraints,
&self.evaluated_sos1_constraints,
self.feasibility_atol,
)?;
for (ic_id, ic) in self.evaluated_indicator_constraints.iter() {
for var_id in &ic.stage.used_decision_variable_ids {
if !decision_variables.contains_key(var_id) {
crate::bail!(
{ ?var_id, ?ic_id },
"Variable {var_id:?} used in indicator constraint {ic_id:?} is not defined in decision_variables",
);
}
}
}
for (oh_id, oh) in self.evaluated_one_hot_constraints.iter() {
for var_id in &oh.stage.used_decision_variable_ids {
if !decision_variables.contains_key(var_id) {
crate::bail!(
{ ?var_id, ?oh_id },
"Variable {var_id:?} used in one-hot constraint {oh_id:?} is not defined in decision_variables",
);
}
}
}
for (s1_id, s1) in self.evaluated_sos1_constraints.iter() {
for var_id in &s1.stage.used_decision_variable_ids {
if !decision_variables.contains_key(var_id) {
crate::bail!(
{ ?var_id, ?s1_id },
"Variable {var_id:?} used in SOS1 constraint {s1_id:?} is not defined in decision_variables",
);
}
}
}
for (constraint_id, constraint) in evaluated_constraints.iter() {
for var_id in &constraint.stage.used_decision_variable_ids {
if !decision_variables.contains_key(var_id) {
return Err(SolutionError::UndefinedVariableInConstraint {
id: *var_id,
constraint_id: *constraint_id,
}
.into());
}
}
}
validate_evaluated_named_function_used_ids(
&decision_variables,
&evaluated_named_functions,
)?;
Ok(Solution {
objective,
evaluated_constraints,
evaluated_indicator_constraints: self.evaluated_indicator_constraints,
evaluated_one_hot_constraints: self.evaluated_one_hot_constraints,
evaluated_sos1_constraints: self.evaluated_sos1_constraints,
evaluated_named_functions,
decision_variables,
optimality: self.optimality,
relaxation: self.relaxation,
sense: Some(sense),
feasibility_atol: self.feasibility_atol,
metadata: Default::default(),
annotations: Default::default(),
})
}
pub unsafe fn build_unchecked(self) -> crate::Result<Solution> {
let objective = self
.objective
.ok_or(SolutionError::MissingRequiredField { field: "objective" })?;
let evaluated_constraints =
self.evaluated_constraints
.ok_or(SolutionError::MissingRequiredField {
field: "evaluated_constraints",
})?;
let decision_variables =
self.decision_variables
.ok_or(SolutionError::MissingRequiredField {
field: "decision_variables",
})?;
let decision_variables =
EvaluatedDecisionVariableTable::new(decision_variables, self.variable_labels)?;
let sense = self
.sense
.ok_or(SolutionError::MissingRequiredField { field: "sense" })?;
let evaluated_named_functions = match self.evaluated_named_function_table {
Some(evaluated_named_functions) => evaluated_named_functions,
None => {
NamedFunctionTable::new(self.evaluated_named_functions, self.named_function_labels)?
}
};
Ok(Solution {
objective,
evaluated_constraints,
evaluated_indicator_constraints: self.evaluated_indicator_constraints,
evaluated_one_hot_constraints: self.evaluated_one_hot_constraints,
evaluated_sos1_constraints: self.evaluated_sos1_constraints,
evaluated_named_functions,
decision_variables,
optimality: self.optimality,
relaxation: self.relaxation,
sense: Some(sense),
feasibility_atol: self.feasibility_atol,
metadata: Default::default(),
annotations: Default::default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Coefficient, Constraint, Evaluate, Function};
#[test]
fn test_total_violation_l1_all_satisfied() {
let mut constraints = BTreeMap::new();
let c1 =
Constraint::equal_to_zero(Function::Constant(Coefficient::try_from(0.0001).unwrap()));
let state = crate::v1::State::default();
constraints.insert(
ConstraintID::from(1),
c1.evaluate(&state, crate::ATol::default()).unwrap(),
);
let c2 = Constraint::less_than_or_equal_to_zero(Function::Constant(
Coefficient::try_from(-1.0).unwrap(),
));
constraints.insert(
ConstraintID::from(2),
c2.evaluate(&state, crate::ATol::default()).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(constraints)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
assert_eq!(solution.total_violation_l1(), 0.0001);
}
#[test]
fn test_total_violation_l1_mixed() {
let mut constraints = BTreeMap::new();
let state = crate::v1::State::default();
let c1 = Constraint::equal_to_zero(Function::Constant(Coefficient::try_from(2.5).unwrap()));
constraints.insert(
ConstraintID::from(1),
c1.evaluate(&state, crate::ATol::default()).unwrap(),
);
let c2 = Constraint::less_than_or_equal_to_zero(Function::Constant(
Coefficient::try_from(1.5).unwrap(),
));
constraints.insert(
ConstraintID::from(2),
c2.evaluate(&state, crate::ATol::default()).unwrap(),
);
let c3 = Constraint::less_than_or_equal_to_zero(Function::Constant(
Coefficient::try_from(-0.5).unwrap(),
));
constraints.insert(
ConstraintID::from(3),
c3.evaluate(&state, crate::ATol::default()).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(constraints)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
assert_eq!(solution.total_violation_l1(), 4.0);
}
#[test]
fn test_total_violation_l2_mixed() {
let mut constraints = BTreeMap::new();
let state = crate::v1::State::default();
let c1 = Constraint::equal_to_zero(Function::Constant(Coefficient::try_from(2.5).unwrap()));
constraints.insert(
ConstraintID::from(1),
c1.evaluate(&state, crate::ATol::default()).unwrap(),
);
let c2 = Constraint::less_than_or_equal_to_zero(Function::Constant(
Coefficient::try_from(1.5).unwrap(),
));
constraints.insert(
ConstraintID::from(2),
c2.evaluate(&state, crate::ATol::default()).unwrap(),
);
let c3 = Constraint::less_than_or_equal_to_zero(Function::Constant(
Coefficient::try_from(-0.5).unwrap(),
));
constraints.insert(
ConstraintID::from(3),
c3.evaluate(&state, crate::ATol::default()).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(constraints)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
assert_eq!(solution.total_violation_l2(), 8.5);
}
#[test]
fn test_total_violation_empty() {
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
assert_eq!(solution.total_violation_l1(), 0.0);
assert_eq!(solution.total_violation_l2(), 0.0);
}
#[test]
fn test_total_violation_equality_negative() {
let mut constraints = BTreeMap::new();
let state = crate::v1::State::default();
let c1 =
Constraint::equal_to_zero(Function::Constant(Coefficient::try_from(-3.0).unwrap()));
constraints.insert(
ConstraintID::from(1),
c1.evaluate(&state, crate::ATol::default()).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(constraints)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
assert_eq!(solution.total_violation_l1(), 3.0);
assert_eq!(solution.total_violation_l2(), 9.0);
}
#[test]
fn test_extract_parameterized_variable_success() {
use crate::{
decision_variable::{DecisionVariable, DecisionVariableLabel, Kind},
EvaluatedDecisionVariable, Sense, VariableID,
};
let mut decision_variables = BTreeMap::new();
let dv = DecisionVariable::new(
Kind::Continuous,
crate::Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap(),
crate::ATol::default(),
)
.unwrap();
let mut variable_labels = VariableLabelStore::default();
variable_labels.insert(
VariableID::from(1),
DecisionVariableLabel {
name: Some("x".to_string()),
subscripts: vec![0],
parameters: {
let mut params = fnv::FnvHashMap::default();
params.insert("param1".to_string(), "value1".to_string());
params
},
..Default::default()
},
);
decision_variables.insert(
VariableID::from(1),
EvaluatedDecisionVariable::new(VariableID::from(1), dv, 1.0).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(decision_variables)
.variable_labels(variable_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
let result = solution.extract_decision_variables("x");
assert!(result.is_ok());
let vars = result.unwrap();
assert_eq!(vars.len(), 1);
assert_eq!(vars[&vec![0]], 1.0);
}
#[test]
fn test_extract_duplicate_subscripts_error() {
use crate::{
decision_variable::{DecisionVariable, DecisionVariableLabel, Kind},
EvaluatedDecisionVariable, Sense, VariableID,
};
let mut decision_variables = BTreeMap::new();
let mut variable_labels = VariableLabelStore::default();
let dv1 = DecisionVariable::new(
Kind::Continuous,
crate::Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap(),
crate::ATol::default(),
)
.unwrap();
variable_labels.insert(
VariableID::from(1),
DecisionVariableLabel {
name: Some("x".to_string()),
subscripts: vec![0],
parameters: {
let mut params = fnv::FnvHashMap::default();
params.insert("param".to_string(), "value1".to_string());
params
},
..Default::default()
},
);
decision_variables.insert(
VariableID::from(1),
EvaluatedDecisionVariable::new(VariableID::from(1), dv1, 1.0).unwrap(),
);
let dv2 = DecisionVariable::new(
Kind::Continuous,
crate::Bound::new(f64::NEG_INFINITY, f64::INFINITY).unwrap(),
crate::ATol::default(),
)
.unwrap();
variable_labels.insert(
VariableID::from(2),
DecisionVariableLabel {
name: Some("x".to_string()),
subscripts: vec![0], parameters: {
let mut params = fnv::FnvHashMap::default();
params.insert("param".to_string(), "value2".to_string());
params
},
..Default::default()
},
);
decision_variables.insert(
VariableID::from(2),
EvaluatedDecisionVariable::new(VariableID::from(2), dv2, 2.0).unwrap(),
);
let solution = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(decision_variables)
.variable_labels(variable_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
let result = solution.extract_decision_variables("x");
assert!(matches!(
result,
Err(SolutionError::DuplicateSubscript { .. })
));
}
#[test]
fn test_builder_missing_required_field() {
let err = Solution::builder()
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap_err();
let solution_err = err.downcast_ref::<SolutionError>().unwrap();
assert!(matches!(
solution_err,
SolutionError::MissingRequiredField { field: "objective" }
));
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.build()
.unwrap_err();
let solution_err = err.downcast_ref::<SolutionError>().unwrap();
assert!(matches!(
solution_err,
SolutionError::MissingRequiredField { field: "sense" }
));
}
#[test]
fn builder_rejects_orphan_variable_label_id() {
let mut variable_labels = VariableLabelStore::default();
variable_labels.set_name(VariableID::from(99), "orphan");
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.variable_labels(variable_labels)
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string().contains("unknown decision variable ID")
&& err.to_string().contains("VariableID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_inconsistent_regular_constraint_feasibility() {
let constraint = EvaluatedConstraint {
equality: crate::Equality::EqualToZero,
stage: crate::constraint::EvaluatedData {
evaluated_value: 1.0,
feasible: true,
used_decision_variable_ids: BTreeSet::new(),
dual_variable: None,
},
};
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::from([(ConstraintID::from(1), constraint)]))
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.feasibility_atol(ATol::new(0.1).unwrap())
.build()
.unwrap_err();
assert!(
err.to_string()
.contains("Inconsistent feasibility for regular constraint"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_unknown_one_hot_structural_variable() {
let variable_id = VariableID::from(1);
let one_hot = crate::one_hot_constraint::EvaluatedOneHotConstraint {
variables: BTreeSet::from([variable_id]),
stage: crate::one_hot_constraint::OneHotEvaluatedData {
feasible: false,
active_variable: None,
used_decision_variable_ids: BTreeSet::new(),
},
};
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_one_hot_constraints_collection(
EvaluatedCollection::new(
BTreeMap::from([(crate::OneHotConstraintID::from(1), one_hot)]),
BTreeMap::new(),
)
.unwrap(),
)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string()
.contains("Invalid structure for one-hot constraint"),
"unexpected error: {err}"
);
}
#[test]
fn unchecked_builder_rejects_orphan_variable_label_id() {
let mut variable_labels = VariableLabelStore::default();
variable_labels.set_name(VariableID::from(99), "orphan");
let err = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.variable_labels(variable_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap_err()
};
assert!(
err.to_string().contains("unknown decision variable ID")
&& err.to_string().contains("VariableID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_orphan_named_function_label_id() {
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
named_function_labels.set_name(NamedFunctionID::from(99), "orphan");
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string().contains("unknown named function ID")
&& err.to_string().contains("NamedFunctionID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn unchecked_builder_rejects_orphan_named_function_label_id() {
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
named_function_labels.set_name(NamedFunctionID::from(99), "orphan");
let err = unsafe {
Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap_err()
};
assert!(
err.to_string().contains("unknown named function ID")
&& err.to_string().contains("NamedFunctionID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_undefined_variable_in_evaluated_named_function() {
use crate::parse::Parse as _;
let var_id = VariableID::from(1);
let nf_id = NamedFunctionID::from(7);
let parsed: crate::named_function::parse::ParsedEvaluatedNamedFunction =
crate::v1::EvaluatedNamedFunction {
id: nf_id.into_inner(),
evaluated_value: 1.0,
used_decision_variable_ids: vec![var_id.into_inner()],
..Default::default()
}
.parse(&())
.unwrap();
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_named_functions(BTreeMap::from([(nf_id, parsed.evaluated_named_function)]))
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap_err();
let solution_err = err.downcast_ref::<SolutionError>().unwrap();
assert!(matches!(
solution_err,
SolutionError::UndefinedVariableInNamedFunction { id, named_function_id }
if *id == var_id && *named_function_id == nf_id
));
}
#[test]
fn test_builder_undefined_variable_in_constraint() {
use crate::linear;
let state = crate::v1::State::from(std::collections::HashMap::from([(1, 1.0)]));
let constraint_id = ConstraintID::from(1);
let var_id = VariableID::from(1);
let c = Constraint::equal_to_zero(Function::from(linear!(1)));
let evaluated_c = c.evaluate(&state, crate::ATol::default()).unwrap();
let mut evaluated_constraints = BTreeMap::new();
evaluated_constraints.insert(constraint_id, evaluated_c);
let err = Solution::builder()
.objective(0.0)
.evaluated_constraints(evaluated_constraints)
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap_err();
let solution_err = err.downcast_ref::<SolutionError>().unwrap();
assert!(matches!(
solution_err,
SolutionError::UndefinedVariableInConstraint { id, constraint_id: cid }
if *id == var_id && *cid == constraint_id
));
}
#[test]
fn test_builder_success() {
use crate::DecisionVariable;
let var_id = VariableID::from(1);
let dv = DecisionVariable::binary();
let evaluated_dv = EvaluatedDecisionVariable::new(var_id, dv, 1.0).unwrap();
let mut decision_variables = BTreeMap::new();
decision_variables.insert(var_id, evaluated_dv);
let solution = Solution::builder()
.objective(42.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(decision_variables)
.sense(Sense::Maximize)
.build()
.unwrap();
assert_eq!(*solution.objective(), 42.0);
assert_eq!(*solution.sense(), Some(Sense::Maximize));
}
}