use super::*;
use crate::{ATol, Parse, ParseError, SolutionError};
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";
let provided_feasible = self.get_feasible();
let provided_feasible_relaxed = self.get_feasible_relaxed();
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();
for ec in self.evaluated_constraints {
let parsed_constraint = ec.parse_as(&(), message, "evaluated_constraints")?;
evaluated_constraints.insert(*parsed_constraint.id(), parsed_constraint);
}
let mut evaluated_named_functions = std::collections::BTreeMap::default();
for enf in self.evaluated_named_functions {
let parsed_named_function = enf.parse_as(&(), message, "evaluated_named_functions")?;
evaluated_named_functions.insert(parsed_named_function.id(), parsed_named_function);
}
let mut decision_variables = std::collections::BTreeMap::default();
for dv in self.decision_variables {
let parsed_dv = dv.clone().parse_as(&(), message, "decision_variables")?;
let value = match (state.entries.get(&dv.id), dv.substituted_value.as_ref()) {
(Some(value), None) | (None, Some(value)) => *value,
(Some(value), Some(_substituted_value)) => *value, (None, None) => {
return Err(crate::RawParseError::SolutionError(
SolutionError::MissingVariableValue { id: dv.id },
)
.context(message, "decision_variables"));
}
};
let evaluated_dv =
crate::EvaluatedDecisionVariable::new(parsed_dv, value, ATol::default())
.map_err(crate::RawParseError::InvalidDecisionVariable)
.map_err(|e| ParseError::from(e).context(message, "decision_variables"))?;
decision_variables.insert(*evaluated_dv.id(), evaluated_dv);
}
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 solution = Solution {
objective,
evaluated_constraints,
evaluated_named_functions,
decision_variables,
optimality,
relaxation,
sense,
};
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 From<Solution> for crate::v1::Solution {
fn from(solution: Solution) -> Self {
let state = solution.state();
let objective = *solution.objective();
let evaluated_constraints = solution
.evaluated_constraints()
.values()
.map(|ec| ec.clone().into())
.collect();
let evaluated_named_functions = solution
.evaluated_named_functions()
.values()
.map(|enf| enf.clone().into())
.collect();
let decision_variables: Vec<crate::v1::DecisionVariable> = solution
.decision_variables()
.values()
.map(|dv| dv.clone().into())
.collect();
let feasible = solution.feasible();
let feasible_relaxed = Some(solution.feasible_relaxed());
let optimality = solution.optimality.into();
let relaxation = solution.relaxation.into();
let feasible_unrelaxed = feasible;
let sense = match solution.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,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{v1, Parse};
#[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 {
id: 1,
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 {
id: 1,
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_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
"###);
}
}