use super::*;
use crate::{v2, ATol, Parse, ParseError, RawParseError, SolutionError};
fn parse_v2_solution_sense(value: i32, message: &'static str) -> Result<Option<Sense>, ParseError> {
let sense = crate::v1::instance::Sense::try_from(value)
.map_err(|_| RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Sense",
value,
})
.map_err(|e| ParseError::from(e).context(message, "sense"))?;
Ok(match sense {
crate::v1::instance::Sense::Unspecified => None,
crate::v1::instance::Sense::Minimize => Some(crate::Sense::Minimize),
crate::v1::instance::Sense::Maximize => Some(crate::Sense::Maximize),
})
}
fn invalid_solution_sidecar(
error: crate::Error,
message: &'static str,
field: &'static str,
) -> ParseError {
let signal = SolutionError::InvalidSidecar {
message: error.to_string(),
};
ParseError::new(signal).context(message, field)
}
impl Parse for crate::v1::Solution {
type Output = Solution;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v1.Solution";
crate::parse::check_format_version(self.format_version, message)?;
crate::parse::validate_extension_annotations(&self.annotations, message)?;
let provided_feasible = match self.feasible_relaxed {
Some(_) => self.feasible,
None =>
{
#[allow(deprecated)]
self.feasible_unrelaxed
}
};
let provided_feasible_relaxed = self.feasible_relaxed.unwrap_or(self.feasible);
let state = self.state.unwrap_or_default();
let objective = self.objective;
let v1_sense = crate::v1::instance::Sense::try_from(self.sense)
.map_err(|_| crate::RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Sense",
value: self.sense,
})
.map_err(|e| ParseError::from(e).context(message, "sense"))?;
let sense = match v1_sense {
crate::v1::instance::Sense::Unspecified => None,
crate::v1::instance::Sense::Minimize => Some(crate::Sense::Minimize),
crate::v1::instance::Sense::Maximize => Some(crate::Sense::Maximize),
};
let mut evaluated_constraints = std::collections::BTreeMap::default();
let mut removed_reasons = std::collections::BTreeMap::default();
let mut constraint_context =
crate::ConstraintContextStore::<crate::ConstraintID>::default();
for ec in self.evaluated_constraints {
let (id, parsed_constraint, context, removed_reason): (
crate::ConstraintID,
crate::EvaluatedConstraint,
crate::ConstraintContext,
Option<crate::RemovedReason>,
) = ec.parse_as(&(), message, "evaluated_constraints")?;
if evaluated_constraints.contains_key(&id) {
return Err(ParseError::new(SolutionError::InvalidConstraintStructure {
constraint_family: "regular",
constraint_id: format!("{id:?}"),
message: "duplicated constraint ID in evaluated_constraints".to_string(),
})
.context(message, "evaluated_constraints"));
}
if let Some(reason) = removed_reason {
removed_reasons.insert(id, reason);
}
constraint_context.insert(id, context);
evaluated_constraints.insert(id, parsed_constraint);
}
let mut evaluated_named_functions = std::collections::BTreeMap::default();
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
for enf in self.evaluated_named_functions {
let parsed: crate::named_function::parse::ParsedEvaluatedNamedFunction =
enf.parse_as(&(), message, "evaluated_named_functions")?;
let id = parsed.id;
if evaluated_named_functions
.insert(id, parsed.evaluated_named_function)
.is_some()
{
return Err(
ParseError::new(SolutionError::DuplicatedNamedFunctionID { id })
.context(message, "evaluated_named_functions"),
);
}
named_function_labels.insert(id, parsed.label);
}
let mut decision_variables = std::collections::BTreeMap::default();
let mut variable_labels = crate::VariableLabelStore::default();
for dv in self.decision_variables {
let parsed: crate::decision_variable::parse::ParsedDecisionVariable =
dv.parse_as(&(), message, "decision_variables")?;
let parsed_id = parsed.id;
let dv_id = parsed_id.into_inner();
let parsed_dv = parsed.variable;
let label = parsed.label;
let parsed_fixed_value = parsed.fixed_value;
let atol = ATol::default();
let (value, substituted_value_assertion) =
match (state.entries.get(&dv_id), parsed_fixed_value.as_ref()) {
(Some(value), None) | (None, Some(value)) => (*value, None),
(Some(value), Some(substituted_value)) => (*value, Some(*substituted_value)),
(None, None) => {
return Err(ParseError::new(SolutionError::MissingVariableValue {
id: dv_id,
})
.context(message, "decision_variables"));
}
};
let evaluated_dv =
crate::EvaluatedDecisionVariable::new(parsed_id, parsed_dv, value)
.map_err(|e| ParseError::new(e).context(message, "decision_variables"))?;
if let Some(substituted_value) = substituted_value_assertion {
if !atol.approx_eq(value, substituted_value) {
return Err(ParseError::new(SolutionError::InconsistentVariableValue {
id: dv_id,
state_value: value,
substituted_value,
})
.context(message, "decision_variables"));
}
}
variable_labels.insert(parsed_id, label);
if decision_variables.insert(parsed_id, evaluated_dv).is_some() {
return Err(
ParseError::new(SolutionError::DuplicatedVariableID { id: parsed_id })
.context(message, "decision_variables"),
);
}
}
let optimality = self
.optimality
.try_into()
.map_err(|_| crate::RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Optimality",
value: self.optimality,
})
.map_err(|e| ParseError::from(e).context(message, "optimality"))?;
let relaxation = self
.relaxation
.try_into()
.map_err(|_| crate::RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Relaxation",
value: self.relaxation,
})
.map_err(|e| ParseError::from(e).context(message, "relaxation"))?;
let evaluated_named_functions =
crate::NamedFunctionTable::new(evaluated_named_functions, named_function_labels)
.map_err(|e| invalid_solution_sidecar(e, message, "evaluated_named_functions"))?;
let decision_variables =
crate::EvaluatedDecisionVariableTable::new(decision_variables, variable_labels)
.map_err(|e| invalid_solution_sidecar(e, message, "decision_variables"))?;
validate_evaluated_named_function_used_ids(&decision_variables, &evaluated_named_functions)
.map_err(|e| ParseError::new(e).context(message, "evaluated_named_functions"))?;
let evaluated_constraints = crate::constraint_type::EvaluatedCollection::with_context(
evaluated_constraints,
removed_reasons,
constraint_context,
)
.map_err(|e| invalid_solution_sidecar(e, message, "evaluated_constraints"))?;
validate_solution_regular_constraint_used_ids(&decision_variables, &evaluated_constraints)
.map_err(|e| ParseError::new(e).context(message, "evaluated_constraints"))?;
let solution = Solution {
objective,
evaluated_constraints,
evaluated_indicator_constraints: Default::default(),
evaluated_one_hot_constraints: Default::default(),
evaluated_sos1_constraints: Default::default(),
evaluated_named_functions,
decision_variables,
optimality,
relaxation,
sense,
feasibility_atol: ATol::default(),
metadata: self.metadata,
annotations: self.annotations,
};
let computed_feasible = solution.feasible();
let computed_feasible_relaxed = solution.feasible_relaxed();
if computed_feasible != provided_feasible {
return Err(ParseError::new(SolutionError::InconsistentFeasibility {
provided_feasible,
computed_feasible,
})
.context(message, "feasible"));
}
if computed_feasible_relaxed != provided_feasible_relaxed {
return Err(
ParseError::new(SolutionError::InconsistentFeasibilityRelaxed {
provided_feasible_relaxed,
computed_feasible_relaxed,
})
.context(message, "feasible_relaxed"),
);
}
Ok(solution)
}
}
impl Parse for v2::Solution {
type Output = Solution;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v2.Solution";
crate::v2_io::validate_required_features(self.required_features, message)?;
let feasibility_atol =
crate::v2_io::parse_feasibility_atol(self.feasibility_atol, message)?;
let annotations =
crate::v2_io::extension_annotations_from_v2_map(self.annotations, message)?;
crate::v2_io::validate_finite_f64(self.objective, message, "objective")?;
let decision_variables = self
.decision_variables
.ok_or(RawParseError::MissingField {
message,
field: "decision_variables",
})?
.parse_as(&(), message, "decision_variables")?;
let evaluated_constraints = self
.evaluated_regular_constraints
.map(|value| {
value.parse_as(&feasibility_atol, message, "evaluated_regular_constraints")
})
.transpose()?
.unwrap_or_default();
let evaluated_indicator_constraints = self
.evaluated_indicator_constraints
.map(|value| {
value.parse_as(
&feasibility_atol,
message,
"evaluated_indicator_constraints",
)
})
.transpose()?
.unwrap_or_default();
let evaluated_one_hot_constraints = self
.evaluated_one_hot_constraints
.map(|value| {
value.parse_as(&feasibility_atol, message, "evaluated_one_hot_constraints")
})
.transpose()?
.unwrap_or_default();
let evaluated_sos1_constraints = self
.evaluated_sos1_constraints
.map(|value| value.parse_as(&feasibility_atol, message, "evaluated_sos1_constraints"))
.transpose()?
.unwrap_or_default();
let evaluated_named_functions = self
.evaluated_named_functions
.map(|value| value.parse_as(&(), message, "evaluated_named_functions"))
.transpose()?
.unwrap_or_default();
validate_solution_indicator_constraint_structure(
&decision_variables,
&evaluated_indicator_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_indicator_constraints"))?;
validate_solution_one_hot_constraint_structure(
&decision_variables,
&evaluated_one_hot_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_one_hot_constraints"))?;
validate_solution_sos1_constraint_structure(
&decision_variables,
&evaluated_sos1_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_sos1_constraints"))?;
validate_solution_regular_constraint_used_ids(&decision_variables, &evaluated_constraints)
.map_err(|e| ParseError::new(e).context(message, "evaluated_regular_constraints"))?;
validate_solution_indicator_constraint_used_ids(
&decision_variables,
&evaluated_indicator_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_indicator_constraints"))?;
validate_solution_one_hot_constraint_used_ids(
&decision_variables,
&evaluated_one_hot_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_one_hot_constraints"))?;
validate_solution_sos1_constraint_used_ids(
&decision_variables,
&evaluated_sos1_constraints,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_sos1_constraints"))?;
validate_solution_indicator_stage_values(
&decision_variables,
&evaluated_indicator_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_indicator_constraints"))?;
validate_solution_one_hot_stage_values(
&decision_variables,
&evaluated_one_hot_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_one_hot_constraints"))?;
validate_solution_sos1_stage_values(
&decision_variables,
&evaluated_sos1_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "evaluated_sos1_constraints"))?;
validate_evaluated_named_function_used_ids(&decision_variables, &evaluated_named_functions)
.map_err(|e| ParseError::new(e).context(message, "evaluated_named_functions"))?;
let optimality = crate::v1::Optimality::try_from(self.optimality)
.map_err(|_| RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Optimality",
value: self.optimality,
})
.map_err(|e| ParseError::from(e).context(message, "optimality"))?;
let relaxation = crate::v1::Relaxation::try_from(self.relaxation)
.map_err(|_| RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Relaxation",
value: self.relaxation,
})
.map_err(|e| ParseError::from(e).context(message, "relaxation"))?;
let sense = parse_v2_solution_sense(self.sense, message)?;
let solution = Solution {
objective: self.objective,
evaluated_constraints,
evaluated_indicator_constraints,
evaluated_one_hot_constraints,
evaluated_sos1_constraints,
evaluated_named_functions,
decision_variables,
optimality,
relaxation,
sense,
feasibility_atol,
metadata: self.metadata,
annotations,
};
let computed_feasible = solution.feasible();
if computed_feasible != self.feasible {
return Err(ParseError::new(SolutionError::InconsistentFeasibility {
provided_feasible: self.feasible,
computed_feasible,
})
.context(message, "feasible"));
}
let provided_feasible_relaxed = self.feasible_relaxed.unwrap_or(self.feasible);
let computed_feasible_relaxed = solution.feasible_relaxed();
if computed_feasible_relaxed != provided_feasible_relaxed {
return Err(
ParseError::new(SolutionError::InconsistentFeasibilityRelaxed {
provided_feasible_relaxed,
computed_feasible_relaxed,
})
.context(message, "feasible_relaxed"),
);
}
Ok(solution)
}
}
impl TryFrom<v2::Solution> for Solution {
type Error = ParseError;
fn try_from(value: v2::Solution) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl From<Solution> for crate::v1::Solution {
fn from(solution: Solution) -> Self {
let feasible = solution.feasible();
let feasible_relaxed = Some(solution.feasible_relaxed());
let Solution {
objective,
evaluated_constraints,
evaluated_indicator_constraints: _,
evaluated_one_hot_constraints: _,
evaluated_sos1_constraints: _,
evaluated_named_functions,
decision_variables,
optimality,
relaxation,
sense,
feasibility_atol: _,
metadata,
annotations,
} = solution;
let state = {
let entries = decision_variables
.iter()
.map(|(id, dv)| (id.into_inner(), *dv.value()))
.collect();
crate::v1::State { entries }
};
let evaluated_constraints: Vec<crate::v1::EvaluatedConstraint> =
evaluated_constraints.into();
let evaluated_named_functions: Vec<crate::v1::EvaluatedNamedFunction> =
evaluated_named_functions.into();
let decision_variables: Vec<crate::v1::DecisionVariable> = (&decision_variables).into();
let optimality = optimality.into();
let relaxation = relaxation.into();
let feasible_unrelaxed = feasible;
let sense = match sense {
None => crate::v1::instance::Sense::Unspecified as i32,
Some(crate::Sense::Minimize) => crate::v1::instance::Sense::Minimize as i32,
Some(crate::Sense::Maximize) => crate::v1::instance::Sense::Maximize as i32,
};
#[allow(deprecated)]
crate::v1::Solution {
state: Some(state),
objective,
evaluated_constraints,
evaluated_named_functions,
decision_variables,
feasible,
feasible_relaxed,
optimality,
relaxation,
feasible_unrelaxed,
sense,
format_version: crate::CURRENT_FORMAT_VERSION,
metadata,
annotations: crate::protobuf_extension_annotations(annotations),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{v1, DecisionVariable, Parse};
fn parse_error_source(error: &ParseError) -> &(dyn std::error::Error + 'static) {
std::error::Error::source(error).expect("ParseError should expose its cause")
}
fn empty_v2_solution() -> v2::Solution {
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(std::collections::BTreeMap::new())
.decision_variables(std::collections::BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
solution.into()
}
fn v2_solution_with_indicator_constraint() -> v2::Solution {
let variable_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(variable_id, DecisionVariable::binary(), 1.0).unwrap();
let constraint = crate::indicator_constraint::EvaluatedIndicatorConstraint {
indicator_variable: variable_id,
equality: crate::Equality::EqualToZero,
stage: crate::indicator_constraint::IndicatorEvaluatedData {
evaluated_value: 0.0,
feasible: true,
indicator_active: true,
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(std::collections::BTreeMap::new())
.evaluated_indicator_constraints(std::collections::BTreeMap::from([(
crate::IndicatorConstraintID::from(1),
constraint,
)]))
.decision_variables(std::collections::BTreeMap::from([(
variable_id,
decision_variable,
)]))
.sense(Sense::Minimize)
.build()
.unwrap();
solution.into()
}
fn v2_solution_with_one_hot_constraint() -> v2::Solution {
let variable_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(variable_id, DecisionVariable::binary(), 1.0).unwrap();
let constraint = crate::one_hot_constraint::EvaluatedOneHotConstraint {
variables: std::collections::BTreeSet::from([variable_id]),
stage: crate::one_hot_constraint::OneHotEvaluatedData {
feasible: true,
active_variable: Some(variable_id),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
let collection = crate::constraint_type::EvaluatedCollection::new(
std::collections::BTreeMap::from([(crate::OneHotConstraintID::from(1), constraint)]),
std::collections::BTreeMap::new(),
)
.unwrap();
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(std::collections::BTreeMap::new())
.evaluated_one_hot_constraints_collection(collection)
.decision_variables(std::collections::BTreeMap::from([(
variable_id,
decision_variable,
)]))
.sense(Sense::Minimize)
.build()
.unwrap();
solution.into()
}
fn v2_solution_with_sos1_constraint() -> v2::Solution {
let variable_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(variable_id, DecisionVariable::binary(), 0.0).unwrap();
let constraint = crate::sos1_constraint::EvaluatedSos1Constraint {
variables: std::collections::BTreeSet::from([variable_id]),
stage: crate::sos1_constraint::Sos1EvaluatedData {
feasible: true,
active_variable: None,
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
let collection = crate::constraint_type::EvaluatedCollection::new(
std::collections::BTreeMap::from([(crate::Sos1ConstraintID::from(1), constraint)]),
std::collections::BTreeMap::new(),
)
.unwrap();
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(std::collections::BTreeMap::new())
.evaluated_sos1_constraints_collection(collection)
.decision_variables(std::collections::BTreeMap::from([(
variable_id,
decision_variable,
)]))
.sense(Sense::Minimize)
.build()
.unwrap();
solution.into()
}
fn assert_invalid_constraint_structure(
error: &ParseError,
expected_family: &str,
expected_constraint_id: &str,
expected_message: &str,
) {
let error = parse_error_source(error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(
matches!(
error,
SolutionError::InvalidConstraintStructure {
constraint_family,
constraint_id,
message,
} if *constraint_family == expected_family
&& constraint_id.as_str() == expected_constraint_id
&& message.as_str() == expected_message
),
"unexpected error: {error}",
);
}
#[test]
fn invalid_solution_sidecar_preserves_solution_error() {
let constraint_id = ConstraintID::from(7);
let mut context = crate::ConstraintContextStore::default();
context.set_name(constraint_id, "orphan");
let source = crate::constraint_type::EvaluatedCollection::<Constraint>::with_context(
std::collections::BTreeMap::new(),
std::collections::BTreeMap::new(),
context,
)
.unwrap_err();
let error = invalid_solution_sidecar(source, "ommx.v1.Solution", "evaluated_constraints");
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::InvalidSidecar { message }
if message.as_str() == "Constraint label/provenance references unknown constraint ID ConstraintID(7)"
));
assert_eq!(error.context.len(), 1);
assert_eq!(error.context[0].message, "ommx.v1.Solution");
assert_eq!(error.context[0].field, "evaluated_constraints");
}
#[test]
fn test_v2_solution_parse_classifies_undefined_variable_in_regular_constraint() {
let mut proto = empty_v2_solution();
proto
.evaluated_regular_constraints
.as_mut()
.unwrap()
.entries
.insert(
7,
v2::EvaluatedRegularConstraint {
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 0.0,
feasible: true,
used_decision_variable_ids: vec![42],
dual_variable: None,
},
);
let error = Solution::try_from(proto).unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::UndefinedVariableInConstraint {
id,
constraint_id,
} if *id == VariableID::from(42) && *constraint_id == ConstraintID::from(7)
));
}
#[test]
fn test_v2_solution_parse_classifies_undefined_used_id_in_indicator_constraint() {
let mut proto = v2_solution_with_indicator_constraint();
proto
.evaluated_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_solution_parse_classifies_undefined_used_id_in_one_hot_constraint() {
let mut proto = v2_solution_with_one_hot_constraint();
proto
.evaluated_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_solution_parse_classifies_undefined_used_id_in_sos1_constraint() {
let mut proto = v2_solution_with_sos1_constraint();
proto
.evaluated_sos1_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"SOS1",
"Sos1ConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_solution_parse_classifies_undefined_indicator_variable() {
let mut proto = v2_solution_with_indicator_constraint();
proto
.evaluated_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.indicator_variable = 2;
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(1)",
"indicator variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_solution_parse_classifies_non_binary_indicator_variable() {
let mut proto = v2_solution_with_indicator_constraint();
proto
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.kind = v1::decision_variable::Kind::Continuous as i32;
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(1)",
"indicator variable VariableID(1) must be binary",
);
}
#[test]
fn test_v2_solution_parse_classifies_undefined_one_hot_variable() {
let mut proto = v2_solution_with_one_hot_constraint();
let row = proto
.evaluated_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.variables = vec![2];
row.active_variable = Some(2);
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_solution_parse_classifies_non_binary_one_hot_variable() {
let mut proto = v2_solution_with_one_hot_constraint();
proto
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.kind = v1::decision_variable::Kind::Continuous as i32;
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(1)",
"variable VariableID(1) must be binary",
);
}
#[test]
fn test_v2_solution_parse_classifies_undefined_sos1_variable() {
let mut proto = v2_solution_with_sos1_constraint();
proto
.evaluated_sos1_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.variables = vec![2];
let error = Solution::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"SOS1",
"Sos1ConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_solution_parse_rejects_reserved_annotation_key() {
let v1_solution = v1::Solution {
annotations: std::collections::HashMap::from([(
format!("{}.solver", crate::annotation_keys::SOLUTION_NAMESPACE),
"bad".to_string(),
)]),
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[annotations]
Annotation key `org.ommx.v1.solution.solver` is reserved for OMMX metadata and cannot be stored in extension annotations.
"###);
}
#[test]
#[allow(deprecated)]
fn test_solution_to_v1_bytes_filters_reserved_annotation_key() {
let mut solution: Solution = v1::Solution {
state: Some(v1::State::default()),
feasible: true,
feasible_relaxed: Some(true),
feasible_unrelaxed: true,
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
..Default::default()
}
.parse(&())
.unwrap();
let reserved_key = format!("{}.solver", crate::annotation_keys::SOLUTION_NAMESPACE);
solution.annotations = std::collections::HashMap::from([
(reserved_key.clone(), "invalid extension solver".to_string()),
("org.example.owner".to_string(), "domain".to_string()),
]);
let restored = Solution::from_v1_bytes(&solution.to_v1_bytes()).unwrap();
assert!(!restored.annotations.contains_key(&reserved_key));
assert_eq!(
restored.annotations.get("org.example.owner"),
Some(&"domain".to_string())
);
}
#[test]
fn test_solution_parse() {
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, 2.0), (2, 3.0)].iter().cloned().collect(),
}),
objective: 42.5,
evaluated_constraints: vec![v1::EvaluatedConstraint {
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 0.0,
dual_variable: Some(1.5),
name: Some("test_constraint".to_string()),
..Default::default()
}],
decision_variables: vec![v1::DecisionVariable {
id: 1,
name: Some("x1".to_string()),
kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: -100.0,
upper: 100.0,
}),
..Default::default()
}],
feasible: true,
feasible_relaxed: Some(true),
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
sense: v1::instance::Sense::Maximize as i32,
..Default::default()
};
let parsed: Solution = v1_solution.parse(&()).unwrap();
assert_eq!(parsed.objective(), &42.5);
assert!(parsed.feasible());
assert!(parsed.feasible_relaxed());
assert_eq!(parsed.optimality, v1::Optimality::Optimal);
assert_eq!(parsed.relaxation, v1::Relaxation::Unspecified);
assert_eq!(parsed.evaluated_constraints().len(), 1);
assert_eq!(parsed.decision_variables().len(), 1);
assert_eq!(parsed.sense().unwrap(), crate::Sense::Maximize);
let v1_converted: v1::Solution = parsed.into();
assert_eq!(v1_converted.objective, 42.5);
assert!(v1_converted.feasible);
assert_eq!(v1_converted.feasible_relaxed, Some(true));
assert_eq!(v1_converted.sense, v1::instance::Sense::Maximize as i32);
}
#[test]
fn test_solution_parser_unspecified_sense() {
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, 2.0), (2, 3.0)].iter().cloned().collect(),
}),
objective: 42.5,
evaluated_constraints: vec![],
decision_variables: vec![],
feasible: true,
feasible_relaxed: Some(true),
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
sense: v1::instance::Sense::Unspecified as i32,
..Default::default()
};
let parsed: Solution = v1_solution.parse(&()).unwrap();
assert!(parsed.sense().is_none());
}
#[test]
fn test_unknown_sense_enum_value() {
let v1_solution = v1::Solution {
state: None,
objective: 42.0,
evaluated_constraints: vec![],
decision_variables: vec![],
feasible: true,
feasible_relaxed: Some(true),
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
sense: 999, ..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[sense]
Unknown or unsupported enum value 999 for ommx.v1.Sense. This may be due to an unspecified value or a newer version of the protocol.
"###);
}
#[test]
fn test_unknown_enum_value_error() {
let v1_solution = v1::Solution {
state: None,
optimality: 99, relaxation: v1::Relaxation::Unspecified as i32,
feasible: true,
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[optimality]
Unknown or unsupported enum value 99 for ommx.v1.Optimality. This may be due to an unspecified value or a newer version of the protocol.
"###);
let v1_solution2 = v1::Solution {
state: None,
optimality: v1::Optimality::Optimal as i32,
relaxation: 123, feasible: true,
..Default::default()
};
let result2: Result<Solution, ParseError> = v1_solution2.parse(&());
let error2 = result2.unwrap_err();
insta::assert_snapshot!(error2.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[relaxation]
Unknown or unsupported enum value 123 for ommx.v1.Relaxation. This may be due to an unspecified value or a newer version of the protocol.
"###);
}
#[test]
fn test_inconsistent_feasibility_validation() {
use crate::v1;
let v1_solution = v1::Solution {
state: None, objective: 42.5,
evaluated_constraints: vec![v1::EvaluatedConstraint {
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 1.0, dual_variable: Some(1.5),
name: Some("test_constraint".to_string()),
..Default::default()
}],
decision_variables: vec![],
feasible: true, feasible_relaxed: Some(true),
optimality: v1::Optimality::Optimal as i32,
relaxation: v1::Relaxation::Unspecified as i32,
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[feasible]
Inconsistent feasibility for solution: provided=true, computed=false
"###);
}
#[test]
fn test_inconsistent_variable_value() {
use crate::v1;
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, 2.0)].iter().cloned().collect(),
}),
objective: 42.5,
decision_variables: vec![v1::DecisionVariable {
id: 1,
substituted_value: Some(3.0), kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: 0.0,
upper: 10.0,
}),
..Default::default()
}],
feasible: true,
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::InconsistentVariableValue {
id,
state_value,
substituted_value,
} if *id == 1 && *state_value == 2.0 && *substituted_value == 3.0
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[decision_variables]
Inconsistent value for variable 1: state=2, substituted_value=3
"###);
}
#[test]
fn test_variable_value_accepts_substituted_value_at_atol_boundary() {
use crate::v1;
let atol = *ATol::default();
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, atol)].iter().cloned().collect(),
}),
objective: 42.5,
decision_variables: vec![v1::DecisionVariable {
id: 1,
substituted_value: Some(0.0),
kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: 0.0,
upper: 10.0,
}),
..Default::default()
}],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let parsed: Solution = v1_solution.parse(&()).unwrap();
assert_eq!(
*parsed
.decision_variables()
.get(&crate::VariableID::from(1))
.unwrap()
.value(),
atol
);
}
#[test]
fn test_non_finite_state_value_precedes_substituted_value_consistency() {
use crate::v1;
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, value)].into_iter().collect(),
}),
objective: 42.5,
decision_variables: vec![v1::DecisionVariable {
id: 1,
substituted_value: Some(0.0),
kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: -10.0,
upper: 10.0,
}),
..Default::default()
}],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let error: ParseError = v1_solution.parse(&()).unwrap_err();
assert!(matches!(
parse_error_source(&error).downcast_ref::<crate::DecisionVariableError>(),
Some(crate::DecisionVariableError::NonFiniteValue {
id,
value: error_value,
}) if *id == VariableID::from(1)
&& error_value.to_bits() == value.to_bits()
));
}
}
#[test]
fn test_missing_variable_value() {
use crate::v1;
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: Default::default(), }),
objective: 42.5,
decision_variables: vec![v1::DecisionVariable {
id: 1,
substituted_value: None, kind: v1::decision_variable::Kind::Continuous as i32,
..Default::default()
}],
feasible: true,
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[decision_variables]
Missing value for variable 1: not found in state and no substituted_value
"###);
}
#[test]
fn test_solution_parse_fails_with_duplicated_variable_id() {
use crate::v1;
let decision_variable = v1::DecisionVariable {
id: 1,
kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: 0.0,
upper: 10.0,
}),
..Default::default()
};
let v1_solution = v1::Solution {
state: Some(v1::State {
entries: [(1, 2.0)].iter().cloned().collect(),
}),
objective: 42.5,
decision_variables: vec![decision_variable.clone(), decision_variable],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::DuplicatedVariableID { id }
if *id == crate::VariableID::from(1)
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[decision_variables]
Duplicated variable ID is found in definition: VariableID(1)
"###);
}
#[test]
fn test_solution_parse_fails_with_duplicated_named_function_id() {
use crate::v1;
let v1_solution = v1::Solution {
objective: 0.0,
evaluated_named_functions: vec![
v1::EvaluatedNamedFunction {
id: 7,
evaluated_value: 1.0,
..Default::default()
},
v1::EvaluatedNamedFunction {
id: 7,
evaluated_value: 2.0,
..Default::default()
},
],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::DuplicatedNamedFunctionID { id }
if *id == crate::NamedFunctionID::from(7)
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[evaluated_named_functions]
Duplicated named function ID is found in definition: NamedFunctionID(7)
"###);
}
#[test]
fn test_solution_parse_rejects_undefined_variable_in_named_function() {
use crate::v1;
let v1_solution = v1::Solution {
objective: 42.5,
evaluated_named_functions: vec![v1::EvaluatedNamedFunction {
id: 7,
evaluated_value: 1.0,
used_decision_variable_ids: vec![1],
..Default::default()
}],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
let error = result.unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::UndefinedVariableInNamedFunction {
id,
named_function_id,
} if *id == crate::VariableID::from(1)
&& *named_function_id == crate::NamedFunctionID::from(7)
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[evaluated_named_functions]
Variable ID VariableID(1) used in named function NamedFunctionID(7) is not in decision_variables
"###);
}
#[test]
fn test_v1_solution_parse_classifies_undefined_variable_in_regular_constraint() {
let v1_solution = v1::Solution {
evaluated_constraints: vec![v1::EvaluatedConstraint {
id: 7,
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 0.0,
used_decision_variable_ids: vec![42],
..Default::default()
}],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let error = v1_solution.parse(&()).unwrap_err();
let solution_error = parse_error_source(&error)
.downcast_ref::<SolutionError>()
.expect("expected SolutionError");
assert!(matches!(
solution_error,
SolutionError::UndefinedVariableInConstraint {
id,
constraint_id,
} if *id == VariableID::from(42) && *constraint_id == ConstraintID::from(7)
));
assert_eq!(error.context[0].field, "evaluated_constraints");
}
#[test]
fn from_v1_bytes_rejects_duplicated_evaluated_constraint_ids() {
let first = v1::EvaluatedConstraint {
id: 7,
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 0.0,
removed_reason: Some("removed first row".to_string()),
..Default::default()
};
let second = v1::EvaluatedConstraint {
id: 7,
equality: v1::Equality::EqualToZero as i32,
evaluated_value: 0.0,
name: Some("active second row".to_string()),
..Default::default()
};
let proto = v1::Solution {
evaluated_constraints: vec![first, second],
feasible: true,
feasible_relaxed: Some(true),
..Default::default()
};
let error = Solution::from_v1_bytes(&crate::Message::encode_to_vec(&proto)).unwrap_err();
let parse_error = error
.downcast_ref::<ParseError>()
.expect("semantic byte decoding must retain ParseError as the outer owner");
assert!(matches!(
parse_error_source(parse_error).downcast_ref::<SolutionError>(),
Some(SolutionError::InvalidConstraintStructure {
constraint_family: "regular",
constraint_id,
message,
}) if constraint_id == "ConstraintID(7)"
&& message == "duplicated constraint ID in evaluated_constraints"
));
assert_eq!(parse_error.context.len(), 1);
assert_eq!(parse_error.context[0].message, "ommx.v1.Solution");
assert_eq!(parse_error.context[0].field, "evaluated_constraints");
}
#[test]
fn test_solution_parse_rejects_future_format_version() {
let v1_solution = v1::Solution {
format_version: 1,
..Default::default()
};
let result: Result<Solution, ParseError> = v1_solution.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[format_version]
Unsupported ommx format version: data has format_version=1, but this SDK supports up to 0. Please upgrade the OMMX SDK.
"###);
}
#[test]
fn test_solution_roundtrip_preserves_labels_and_context() {
use crate::{
constraint::EvaluatedData, constraint_type::EvaluatedCollection, ConstraintID,
DecisionVariable, Equality, EvaluatedConstraint, EvaluatedDecisionVariable,
NamedFunctionID, Sense, VariableID,
};
use std::collections::BTreeMap;
let var_id = VariableID::from(1);
let cid = ConstraintID::from(10);
let nf_id = NamedFunctionID::from(0);
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 mut variable_labels = crate::VariableLabelStore::default();
variable_labels.set_name(var_id, "x");
variable_labels.set_subscripts(var_id, vec![0]);
let evaluated = EvaluatedConstraint {
equality: Equality::EqualToZero,
stage: EvaluatedData {
evaluated_value: 0.0,
dual_variable: None,
feasible: true,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let mut evaluated_map = BTreeMap::new();
evaluated_map.insert(cid, evaluated);
let mut constraint_context = crate::ConstraintContextStore::<ConstraintID>::default();
constraint_context.set_name(cid, "balance");
constraint_context.set_description(cid, "demand-balance row");
let evaluated_constraints =
EvaluatedCollection::with_context(evaluated_map, BTreeMap::new(), constraint_context)
.unwrap();
let evaluated_nf = {
use crate::parse::Parse as _;
let v1_enf = crate::v1::EvaluatedNamedFunction {
id: nf_id.into_inner(),
evaluated_value: 1.0,
used_decision_variable_ids: vec![var_id.into_inner()],
..Default::default()
};
let parsed: crate::named_function::parse::ParsedEvaluatedNamedFunction =
v1_enf.parse(&()).unwrap();
parsed.evaluated_named_function
};
let mut evaluated_named_functions = BTreeMap::new();
evaluated_named_functions.insert(nf_id, evaluated_nf);
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
named_function_labels.set_name(nf_id, "offset_x");
named_function_labels.set_subscripts(nf_id, vec![0]);
named_function_labels.set_description(nf_id, "x plus a constant");
let solution = unsafe {
Solution::builder()
.objective(1.0)
.evaluated_constraints_collection(evaluated_constraints)
.evaluated_named_functions(evaluated_named_functions)
.decision_variables(decision_variables)
.variable_labels(variable_labels)
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
let bytes = solution.to_v1_bytes();
let recovered = Solution::from_v1_bytes(&bytes).unwrap();
assert_eq!(recovered.variable_labels().name(var_id), Some("x"));
assert_eq!(recovered.variable_labels().subscripts(var_id), &[0]);
let constraint_meta = recovered.evaluated_constraints().context();
assert_eq!(constraint_meta.name(cid), Some("balance"));
assert_eq!(constraint_meta.description(cid), Some("demand-balance row"));
let nf_meta = recovered.named_function_labels();
assert_eq!(nf_meta.name(nf_id), Some("offset_x"));
assert_eq!(nf_meta.subscripts(nf_id), &[0]);
assert_eq!(nf_meta.description(nf_id), Some("x plus a constant"));
}
#[test]
fn test_v2_solution_parse_rejects_inconsistent_regular_feasibility() {
use crate::{
constraint::EvaluatedData, ConstraintID, Equality, EvaluatedConstraint, Sense,
};
use std::collections::BTreeMap;
let constraint = EvaluatedConstraint {
equality: Equality::EqualToZero,
stage: EvaluatedData {
evaluated_value: 0.0,
feasible: true,
used_decision_variable_ids: Default::default(),
dual_variable: None,
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::from([(ConstraintID::from(1), constraint)]))
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
let row = proto
.evaluated_regular_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.evaluated_value = 1.0;
row.feasible = true;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string()
.contains("Inconsistent constraint feasibility"),
"unexpected error: {err}"
);
}
#[test]
fn v2_special_constraint_stage_validation_includes_the_atol_boundary() {
let atol = ATol::new(0.125).unwrap();
let mut indicator = v2_solution_with_indicator_constraint();
indicator.feasibility_atol = Some(*atol);
indicator
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.value = 1.0 + *atol;
let indicator_row = indicator
.evaluated_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
indicator_row.evaluated_value = *atol;
indicator_row.feasible = true;
indicator_row.indicator_active = true;
Solution::try_from(indicator).unwrap();
let mut one_hot = v2_solution_with_one_hot_constraint();
one_hot.feasibility_atol = Some(*atol);
one_hot
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.value = 1.0 + *atol;
Solution::try_from(one_hot).unwrap();
let mut sos1 = v2_solution_with_sos1_constraint();
sos1.feasibility_atol = Some(*atol);
let sos1_variable = sos1
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
sos1_variable.kind = v1::decision_variable::Kind::Continuous as i32;
sos1_variable.value = *atol;
Solution::try_from(sos1).unwrap();
}
#[test]
fn test_v2_solution_parse_rejects_non_finite_objective() {
use crate::Sense;
use std::collections::BTreeMap;
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
proto.objective = f64::INFINITY;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string().contains("objective must be finite"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_solution_parse_rejects_non_finite_feasibility_atol() {
use crate::Sense;
use std::collections::BTreeMap;
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.decision_variables(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
proto.feasibility_atol = Some(f64::INFINITY);
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string().contains("feasibility_atol must be finite"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_solution_parse_rejects_indicator_active_mismatching_variable_value() {
use crate::{
indicator_constraint::{EvaluatedIndicatorConstraint, IndicatorEvaluatedData},
DecisionVariable, EvaluatedDecisionVariable, IndicatorConstraintID, Sense, VariableID,
};
use std::collections::BTreeMap;
let var_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(var_id, DecisionVariable::binary(), 1.0).unwrap();
let indicator = EvaluatedIndicatorConstraint {
indicator_variable: var_id,
equality: crate::Equality::EqualToZero,
stage: IndicatorEvaluatedData {
evaluated_value: 0.0,
feasible: true,
indicator_active: true,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_indicator_constraints(BTreeMap::from([(
IndicatorConstraintID::from(1),
indicator,
)]))
.decision_variables(BTreeMap::from([(var_id, decision_variable)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
let row = proto
.evaluated_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.indicator_active = false;
row.feasible = true;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string().contains("indicator_active=false")
&& err
.to_string()
.contains("does not match indicator variable"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_solution_parse_rejects_inactive_infeasible_indicator() {
use crate::{
indicator_constraint::{EvaluatedIndicatorConstraint, IndicatorEvaluatedData},
DecisionVariable, EvaluatedDecisionVariable, IndicatorConstraintID, Sense, VariableID,
};
use std::collections::BTreeMap;
let var_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(var_id, DecisionVariable::binary(), 0.0).unwrap();
let indicator = EvaluatedIndicatorConstraint {
indicator_variable: var_id,
equality: crate::Equality::EqualToZero,
stage: IndicatorEvaluatedData {
evaluated_value: 1.0,
feasible: true,
indicator_active: false,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_indicator_constraints(BTreeMap::from([(
IndicatorConstraintID::from(1),
indicator,
)]))
.decision_variables(BTreeMap::from([(var_id, decision_variable)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
let row = proto
.evaluated_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.indicator_active = false;
row.feasible = false;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string()
.contains("Inconsistent indicator constraint feasibility"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_solution_parse_rejects_one_hot_active_variable_mismatching_values() {
use crate::{
one_hot_constraint::{EvaluatedOneHotConstraint, OneHotEvaluatedData},
DecisionVariable, EvaluatedDecisionVariable, OneHotConstraintID, Sense, VariableID,
};
use std::collections::{BTreeMap, BTreeSet};
let var_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(var_id, DecisionVariable::binary(), 1.0).unwrap();
let one_hot = EvaluatedOneHotConstraint {
variables: BTreeSet::from([var_id]),
stage: OneHotEvaluatedData {
feasible: true,
active_variable: Some(var_id),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_one_hot_constraints_collection(
crate::constraint_type::EvaluatedCollection::new(
BTreeMap::from([(OneHotConstraintID::from(1), one_hot)]),
BTreeMap::new(),
)
.unwrap(),
)
.decision_variables(BTreeMap::from([(var_id, decision_variable)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
let row = proto
.evaluated_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.feasible = false;
row.active_variable = None;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string().contains("active_variable=None")
&& err
.to_string()
.contains("does not match decision-variable values"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_solution_parse_rejects_feasible_one_hot_without_active_variable() {
use crate::{
one_hot_constraint::{EvaluatedOneHotConstraint, OneHotEvaluatedData},
DecisionVariable, EvaluatedDecisionVariable, OneHotConstraintID, Sense, VariableID,
};
use std::collections::{BTreeMap, BTreeSet};
let var_id = VariableID::from(1);
let decision_variable =
EvaluatedDecisionVariable::new(var_id, DecisionVariable::binary(), 1.0).unwrap();
let one_hot = EvaluatedOneHotConstraint {
variables: BTreeSet::from([var_id]),
stage: OneHotEvaluatedData {
feasible: true,
active_variable: Some(var_id),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let solution = Solution::builder()
.objective(0.0)
.evaluated_constraints(BTreeMap::new())
.evaluated_one_hot_constraints_collection(
crate::constraint_type::EvaluatedCollection::new(
BTreeMap::from([(OneHotConstraintID::from(1), one_hot)]),
BTreeMap::new(),
)
.unwrap(),
)
.decision_variables(BTreeMap::from([(var_id, decision_variable)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::Solution::from(solution);
let row = proto
.evaluated_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.feasible = true;
row.active_variable = None;
let err = Solution::try_from(proto).unwrap_err();
assert!(
err.to_string()
.contains("feasible must be true exactly when active_variable is set"),
"unexpected error: {err}"
);
}
}