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";
crate::parse::check_format_version(self.format_version, 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_metadata =
crate::ConstraintMetadataStore::<crate::ConstraintID>::default();
for ec in self.evaluated_constraints {
let (id, parsed_constraint, metadata, removed_reason): (
crate::ConstraintID,
crate::EvaluatedConstraint,
crate::ConstraintMetadata,
Option<crate::RemovedReason>,
) = ec.parse_as(&(), message, "evaluated_constraints")?;
if let Some(reason) = removed_reason {
removed_reasons.insert(id, reason);
}
constraint_metadata.insert(id, metadata);
evaluated_constraints.insert(id, parsed_constraint);
}
let mut evaluated_named_functions = std::collections::BTreeMap::default();
let mut named_function_metadata =
crate::named_function::NamedFunctionMetadataStore::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.evaluated_named_function.id();
evaluated_named_functions.insert(id, parsed.evaluated_named_function);
named_function_metadata.insert(id, parsed.metadata);
}
let mut decision_variables = std::collections::BTreeMap::default();
let mut variable_metadata = crate::VariableMetadataStore::default();
for dv in self.decision_variables {
let dv_id = dv.id;
let dv_substituted_value = dv.substituted_value;
let parsed: crate::decision_variable::parse::ParsedDecisionVariable =
dv.parse_as(&(), message, "decision_variables")?;
let parsed_dv = parsed.variable;
let metadata = parsed.metadata;
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"))?;
let id = *evaluated_dv.id();
variable_metadata.insert(id, metadata);
decision_variables.insert(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: crate::constraint_type::EvaluatedCollection::with_metadata(
evaluated_constraints,
removed_reasons,
constraint_metadata,
),
evaluated_indicator_constraints: Default::default(),
evaluated_one_hot_constraints: Default::default(),
evaluated_sos1_constraints: Default::default(),
evaluated_named_functions,
decision_variables,
variable_metadata,
named_function_metadata,
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 constraint_metadata_store = solution.evaluated_constraints().metadata().clone();
let removed_reasons = solution.evaluated_constraints().removed_reasons().clone();
let evaluated_constraints: Vec<crate::v1::EvaluatedConstraint> = solution
.evaluated_constraints()
.iter()
.map(|(id, ec)| {
let metadata = constraint_metadata_store.collect_for(*id);
let mut v1_ec =
crate::constraint::evaluated_constraint_to_v1(*id, ec.clone(), metadata);
if let Some(reason) = removed_reasons.get(id) {
v1_ec.removed_reason = Some(reason.reason.clone());
v1_ec.removed_reason_parameters = reason
.parameters
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
}
v1_ec
})
.collect();
let named_function_metadata_store = solution.named_function_metadata().clone();
let evaluated_named_functions: Vec<crate::v1::EvaluatedNamedFunction> = solution
.evaluated_named_functions()
.iter()
.map(|(id, enf)| {
let metadata = named_function_metadata_store.collect_for(*id);
crate::named_function::parse::evaluated_named_function_to_v1(enf.clone(), metadata)
})
.collect();
let variable_metadata_store = solution.variable_metadata().clone();
let decision_variables: Vec<crate::v1::DecisionVariable> = solution
.decision_variables()
.iter()
.map(|(id, dv)| {
let metadata = variable_metadata_store.collect_for(*id);
crate::decision_variable::evaluated_decision_variable_to_v1(dv.clone(), metadata)
})
.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,
format_version: crate::CURRENT_FORMAT_VERSION,
}
}
}
#[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 {
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_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_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_metadata() {
use crate::{
constraint::EvaluatedData, constraint_type::EvaluatedCollection, ATol, 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(var_id);
let evaluated_dv = EvaluatedDecisionVariable::new(dv, 1.0, ATol::default()).unwrap();
let mut decision_variables = BTreeMap::new();
decision_variables.insert(var_id, evaluated_dv);
let mut variable_metadata = crate::VariableMetadataStore::default();
variable_metadata.set_name(var_id, "x");
variable_metadata.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_metadata = crate::ConstraintMetadataStore::<ConstraintID>::default();
constraint_metadata.set_name(cid, "balance");
constraint_metadata.set_description(cid, "demand-balance row");
let evaluated_constraints =
EvaluatedCollection::with_metadata(evaluated_map, BTreeMap::new(), constraint_metadata);
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_metadata =
crate::named_function::NamedFunctionMetadataStore::default();
named_function_metadata.set_name(nf_id, "offset_x");
named_function_metadata.set_subscripts(nf_id, vec![0]);
named_function_metadata.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_metadata(variable_metadata)
.named_function_metadata(named_function_metadata)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap()
};
let bytes = solution.to_bytes();
let recovered = Solution::from_bytes(&bytes).unwrap();
assert_eq!(recovered.variable_metadata().name(var_id), Some("x"));
assert_eq!(recovered.variable_metadata().subscripts(var_id), &[0]);
let constraint_meta = recovered.evaluated_constraints().metadata();
assert_eq!(constraint_meta.name(cid), Some("balance"));
assert_eq!(constraint_meta.description(cid), Some("demand-balance row"));
let nf_meta = recovered.named_function_metadata();
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"));
}
}