use super::*;
use crate::constraint_type::EvaluatedConstraintBehavior;
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 validate_evaluated_constraint_used_ids<T: crate::ConstraintType>(
constraints: &crate::constraint_type::EvaluatedCollection<T>,
decision_variables: &crate::EvaluatedDecisionVariableTable,
message: &'static str,
field: &'static str,
) -> Result<(), ParseError> {
for (constraint_id, constraint) in constraints.inner() {
for var_id in constraint.used_decision_variable_ids() {
if !decision_variables.contains_key(var_id) {
return Err(RawParseError::InvalidInstance(format!(
"Variable {var_id:?} used in constraint {constraint_id:?} is not defined in decision_variables",
))
.context(message, field));
}
}
}
Ok(())
}
fn evaluated_collection_has_payload<T: crate::ConstraintType>(
collection: &crate::constraint_type::EvaluatedCollection<T>,
) -> bool {
!collection.is_empty()
}
fn validate_solution_indicator_structural_ids(
constraints: &crate::constraint_type::EvaluatedCollection<crate::IndicatorConstraint>,
decision_variables: &crate::EvaluatedDecisionVariableTable,
message: &'static str,
) -> Result<(), ParseError> {
for (constraint_id, constraint) in constraints.inner() {
let id = constraint.indicator_variable;
let Some(variable) = decision_variables.get(&id) else {
return Err(RawParseError::InvalidInstance(format!(
"Indicator variable {id:?} in constraint {constraint_id:?} is not defined in decision_variables",
))
.context(message, "evaluated_indicator_constraints"));
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(RawParseError::InvalidInstance(format!(
"Indicator variable {id:?} in constraint {constraint_id:?} must be binary",
))
.context(message, "evaluated_indicator_constraints"));
}
}
Ok(())
}
fn validate_solution_one_hot_structural_ids(
constraints: &crate::constraint_type::EvaluatedCollection<crate::OneHotConstraint>,
decision_variables: &crate::EvaluatedDecisionVariableTable,
message: &'static str,
) -> Result<(), ParseError> {
for (constraint_id, constraint) in constraints.inner() {
for id in &constraint.variables {
let Some(variable) = decision_variables.get(id) else {
return Err(RawParseError::InvalidInstance(format!(
"One-hot variable {id:?} in constraint {constraint_id:?} is not defined in decision_variables",
))
.context(message, "evaluated_one_hot_constraints"));
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(RawParseError::InvalidInstance(format!(
"One-hot variable {id:?} in constraint {constraint_id:?} must be binary",
))
.context(message, "evaluated_one_hot_constraints"));
}
}
}
Ok(())
}
fn validate_solution_sos1_structural_ids(
constraints: &crate::constraint_type::EvaluatedCollection<crate::Sos1Constraint>,
decision_variables: &crate::EvaluatedDecisionVariableTable,
message: &'static str,
) -> Result<(), ParseError> {
for (constraint_id, constraint) in constraints.inner() {
for id in &constraint.variables {
if !decision_variables.contains_key(id) {
return Err(RawParseError::InvalidInstance(format!(
"SOS1 variable {id:?} in constraint {constraint_id:?} is not defined in decision_variables",
))
.context(message, "evaluated_sos1_constraints"));
}
}
}
Ok(())
}
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 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(crate::RawParseError::SolutionError(
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 = match (state.entries.get(&dv_id), parsed_fixed_value.as_ref()) {
(Some(value), None) | (None, Some(value)) => *value,
(Some(value), Some(substituted_value)) => {
if (*value - *substituted_value).abs() > *atol {
return Err(crate::RawParseError::InvalidDecisionVariable(
crate::DecisionVariableError::SubstitutedValueOverwrite {
id: crate::VariableID::from(dv_id),
previous_value: *substituted_value,
new_value: *value,
atol,
},
)
.context(message, "decision_variables"));
}
*value
}
(None, None) => {
return Err(crate::RawParseError::SolutionError(
SolutionError::MissingVariableValue { id: dv_id },
)
.context(message, "decision_variables"));
}
};
let evaluated_dv = crate::EvaluatedDecisionVariable::new(parsed_id, parsed_dv, value)
.map_err(crate::RawParseError::InvalidDecisionVariable)
.map_err(|e| ParseError::from(e).context(message, "decision_variables"))?;
variable_labels.insert(parsed_id, label);
if decision_variables.insert(parsed_id, evaluated_dv).is_some() {
return Err(crate::RawParseError::SolutionError(
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| {
crate::RawParseError::SolutionError(crate::SolutionError::InvalidSidecar {
message: e.to_string(),
})
.context(message, "evaluated_named_functions")
})?;
let decision_variables =
crate::EvaluatedDecisionVariableTable::new(decision_variables, variable_labels)
.map_err(|e| {
crate::RawParseError::SolutionError(crate::SolutionError::InvalidSidecar {
message: e.to_string(),
})
.context(message, "decision_variables")
})?;
validate_evaluated_named_function_used_ids(&decision_variables, &evaluated_named_functions)
.map_err(|e| {
crate::RawParseError::SolutionError(e).context(message, "evaluated_named_functions")
})?;
let solution = Solution {
objective,
evaluated_constraints: crate::constraint_type::EvaluatedCollection::with_context(
evaluated_constraints,
removed_reasons,
constraint_context,
)
.map_err(|e| {
crate::RawParseError::InvalidInstance(e.to_string())
.context(message, "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(crate::RawParseError::SolutionError(
SolutionError::InconsistentFeasibility {
provided_feasible,
computed_feasible,
},
)
.context(message, "feasible"));
}
if computed_feasible_relaxed != provided_feasible_relaxed {
return Err(crate::RawParseError::SolutionError(
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";
let required_features =
crate::v2_io::parse_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();
crate::v2_io::validate_feature_payload(
&required_features,
v2::Feature::ConstraintIndicator,
evaluated_collection_has_payload(&evaluated_indicator_constraints),
message,
"evaluated_indicator_constraints",
)?;
crate::v2_io::validate_feature_payload(
&required_features,
v2::Feature::ConstraintOneHot,
evaluated_collection_has_payload(&evaluated_one_hot_constraints),
message,
"evaluated_one_hot_constraints",
)?;
crate::v2_io::validate_feature_payload(
&required_features,
v2::Feature::ConstraintSos1,
evaluated_collection_has_payload(&evaluated_sos1_constraints),
message,
"evaluated_sos1_constraints",
)?;
let evaluated_named_functions = self
.evaluated_named_functions
.map(|value| value.parse_as(&(), message, "evaluated_named_functions"))
.transpose()?
.unwrap_or_default();
validate_evaluated_constraint_used_ids(
&evaluated_constraints,
&decision_variables,
message,
"evaluated_regular_constraints",
)?;
validate_evaluated_constraint_used_ids(
&evaluated_indicator_constraints,
&decision_variables,
message,
"evaluated_indicator_constraints",
)?;
validate_evaluated_constraint_used_ids(
&evaluated_one_hot_constraints,
&decision_variables,
message,
"evaluated_one_hot_constraints",
)?;
validate_evaluated_constraint_used_ids(
&evaluated_sos1_constraints,
&decision_variables,
message,
"evaluated_sos1_constraints",
)?;
validate_solution_indicator_structural_ids(
&evaluated_indicator_constraints,
&decision_variables,
message,
)?;
validate_solution_one_hot_structural_ids(
&evaluated_one_hot_constraints,
&decision_variables,
message,
)?;
validate_solution_sos1_structural_ids(
&evaluated_sos1_constraints,
&decision_variables,
message,
)?;
validate_solution_indicator_stage_values(
&decision_variables,
&evaluated_indicator_constraints,
feasibility_atol,
)
.map_err(|e| {
RawParseError::SolutionError(e).context(message, "evaluated_indicator_constraints")
})?;
validate_solution_one_hot_stage_values(
&decision_variables,
&evaluated_one_hot_constraints,
feasibility_atol,
)
.map_err(|e| {
RawParseError::SolutionError(e).context(message, "evaluated_one_hot_constraints")
})?;
validate_solution_sos1_stage_values(
&decision_variables,
&evaluated_sos1_constraints,
feasibility_atol,
)
.map_err(|e| {
RawParseError::SolutionError(e).context(message, "evaluated_sos1_constraints")
})?;
validate_evaluated_named_function_used_ids(&decision_variables, &evaluated_named_functions)
.map_err(|e| {
RawParseError::SolutionError(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(
RawParseError::SolutionError(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(RawParseError::SolutionError(
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, Parse};
#[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();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Solution[decision_variables]
Substituted value for ID=1 cannot be overwritten: previous=3, new=2, atol=ATol(1e-6)
"###);
}
#[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_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();
assert!(matches!(
error.error,
crate::RawParseError::SolutionError(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();
assert!(matches!(
error.error,
crate::RawParseError::SolutionError(
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();
assert!(matches!(
error.error,
crate::RawParseError::SolutionError(
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_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 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}"
);
}
}