use super::*;
use crate::{
constraint::{ConstraintContext, RemovedReason},
constraint_type::ConstraintCollection,
parse::{as_variable_id, Parse, ParseError, RawParseError},
v1::{self},
v2, Constraint, ConstraintID, VariableID,
};
fn ignore_legacy_constraint_hints(hints: Option<v1::ConstraintHints>, message: &'static str) {
let Some(hints) = hints else {
return;
};
if hints.one_hot_constraints.is_empty() && hints.sos1_constraints.is_empty() {
return;
}
tracing::debug!(
message_type = message,
one_hot_constraints = hints.one_hot_constraints.len(),
sos1_constraints = hints.sos1_constraints.len(),
"Ignoring legacy constraint hints during deserialization"
);
}
fn validate_fixed_decision_variable_partition(
message: &'static str,
objective: &Function,
constraints: &BTreeMap<ConstraintID, Constraint>,
decision_variable_dependency: &AcyclicAssignments,
fixed_decision_variable_values: &BTreeMap<VariableID, f64>,
) -> Result<(), ParseError> {
let mut used: VariableIDSet = objective.required_ids();
for constraint in constraints.values() {
used.extend(constraint.required_ids());
}
let fixed: VariableIDSet = fixed_decision_variable_values.keys().copied().collect();
let dependent: VariableIDSet = decision_variable_dependency.keys().collect();
if let Some(id) = used.intersection(&dependent).next() {
return Err(RawParseError::InvalidInstance(format!(
"Dependent variable cannot be used in objectives or constraints: {id:?}"
))
.context(message, "decision_variables"));
}
if let Some(id) = used.intersection(&fixed).next() {
return Err(RawParseError::InvalidInstance(format!(
"Fixed variable {id:?} cannot be used in objectives or constraints"
))
.context(message, "decision_variables"));
}
if let Some(id) = fixed.intersection(&dependent).next() {
return Err(RawParseError::InvalidInstance(format!(
"Variable {id:?} cannot be both fixed and dependent"
))
.context(message, "decision_variables"));
}
Ok(())
}
fn parse_v2_decision_variable_dependency(
dependency: BTreeMap<u64, v1::Function>,
message: &'static str,
) -> Result<AcyclicAssignments, ParseError> {
let dependency = dependency
.into_iter()
.map(|(id, function)| {
Ok((
VariableID::from(id),
function.parse_as(&(), message, "decision_variable_dependency")?,
))
})
.collect::<Result<BTreeMap<_, _>, ParseError>>()?;
AcyclicAssignments::new(dependency)
.map_err(|e| RawParseError::from(e).context(message, "decision_variable_dependency"))
}
fn created_collection_has_payload<T: crate::ConstraintType>(
collection: &ConstraintCollection<T>,
) -> bool {
!collection.active().is_empty() || !collection.removed().is_empty()
}
fn validate_instance_special_features(
required_features: &std::collections::BTreeSet<v2::Feature>,
indicator_constraints: &ConstraintCollection<crate::IndicatorConstraint>,
one_hot_constraints: &ConstraintCollection<crate::OneHotConstraint>,
sos1_constraints: &ConstraintCollection<crate::Sos1Constraint>,
message: &'static str,
) -> Result<(), ParseError> {
crate::v2_io::validate_feature_payload(
required_features,
v2::Feature::ConstraintIndicator,
created_collection_has_payload(indicator_constraints),
message,
"indicator_constraints",
)?;
crate::v2_io::validate_feature_payload(
required_features,
v2::Feature::ConstraintOneHot,
created_collection_has_payload(one_hot_constraints),
message,
"one_hot_constraints",
)?;
crate::v2_io::validate_feature_payload(
required_features,
v2::Feature::ConstraintSos1,
created_collection_has_payload(sos1_constraints),
message,
"sos1_constraints",
)?;
Ok(())
}
fn validate_created_constraint_references<T: crate::ConstraintType>(
collection: &ConstraintCollection<T>,
valid_ids: &VariableIDSet,
message: &'static str,
field: &'static str,
) -> Result<(), ParseError> {
for constraint in collection.active().values().chain(
collection
.removed()
.values()
.map(|(constraint, _)| constraint),
) {
for id in constraint.required_ids() {
if !valid_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, field));
}
}
}
Ok(())
}
fn active_used_ids_from_created_collection<T: crate::ConstraintType>(
collection: &ConstraintCollection<T>,
) -> VariableIDSet {
collection
.active()
.values()
.flat_map(|constraint| constraint.required_ids())
.collect()
}
fn validate_fixed_dependent_partition_from_sets(
used: &VariableIDSet,
fixed_values: &BTreeMap<VariableID, f64>,
decision_variable_dependency: &AcyclicAssignments,
message: &'static str,
) -> Result<(), ParseError> {
let fixed: VariableIDSet = fixed_values.keys().copied().collect();
let dependent: VariableIDSet = decision_variable_dependency.keys().collect();
if let Some(id) = used.intersection(&dependent).next() {
return Err(RawParseError::InvalidInstance(format!(
"Dependent variable cannot be used in objectives or constraints: {id:?}"
))
.context(message, "decision_variables"));
}
if let Some(id) = used.intersection(&fixed).next() {
return Err(RawParseError::InvalidInstance(format!(
"Fixed variable {id:?} cannot be used in objectives or constraints"
))
.context(message, "decision_variables"));
}
if let Some(id) = fixed.intersection(&dependent).next() {
return Err(RawParseError::InvalidInstance(format!(
"Variable {id:?} cannot be both fixed and dependent"
))
.context(message, "decision_variables"));
}
Ok(())
}
fn validate_structural_special_constraints(
decision_variables: &DecisionVariableTable,
parameter_ids: Option<&VariableIDSet>,
indicator_constraints: &ConstraintCollection<crate::IndicatorConstraint>,
one_hot_constraints: &ConstraintCollection<crate::OneHotConstraint>,
sos1_constraints: &ConstraintCollection<crate::Sos1Constraint>,
message: &'static str,
) -> Result<(), ParseError> {
for indicator in indicator_constraints
.active()
.values()
.chain(indicator_constraints.removed().values().map(|(row, _)| row))
{
let id = indicator.indicator_variable;
let Some(variable) = decision_variables.get(&id) else {
let detail = if parameter_ids.is_some_and(|ids| ids.contains(&id)) {
format!(
"Parameter id {id:?} cannot occupy the structural indicator-variable position; it must be a binary decision variable"
)
} else {
format!("Indicator variable {id:?} is not defined in decision_variables")
};
return Err(
RawParseError::InvalidInstance(detail).context(message, "indicator_constraints")
);
};
if variable.kind() != crate::decision_variable::Kind::Binary {
return Err(RawParseError::InvalidInstance(format!(
"Indicator variable {id:?} must be binary"
))
.context(message, "indicator_constraints"));
}
}
for one_hot in one_hot_constraints
.active()
.values()
.chain(one_hot_constraints.removed().values().map(|(row, _)| row))
{
for id in &one_hot.variables {
let Some(variable) = decision_variables.get(id) else {
let detail = if parameter_ids.is_some_and(|ids| ids.contains(id)) {
format!(
"Parameter id {id:?} cannot occupy a structural one-hot variable position; it must be a binary decision variable"
)
} else {
format!("One-hot variable {id:?} is not defined in decision_variables")
};
return Err(
RawParseError::InvalidInstance(detail).context(message, "one_hot_constraints")
);
};
if variable.kind() != crate::decision_variable::Kind::Binary {
return Err(RawParseError::InvalidInstance(format!(
"One-hot variable {id:?} must be binary"
))
.context(message, "one_hot_constraints"));
}
}
}
for sos1 in sos1_constraints
.active()
.values()
.chain(sos1_constraints.removed().values().map(|(row, _)| row))
{
for id in &sos1.variables {
if !decision_variables.contains_key(id) {
let detail = if parameter_ids.is_some_and(|ids| ids.contains(id)) {
format!(
"Parameter id {id:?} cannot occupy a structural SOS1 variable position; it must be a decision variable"
)
} else {
format!("SOS1 variable {id:?} is not defined in decision_variables")
};
return Err(
RawParseError::InvalidInstance(detail).context(message, "sos1_constraints")
);
}
}
}
Ok(())
}
impl Parse for v1::instance::Sense {
type Output = Sense;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
match self {
v1::instance::Sense::Minimize => Ok(Sense::Minimize),
v1::instance::Sense::Maximize => Ok(Sense::Maximize),
v1::instance::Sense::Unspecified => {
tracing::warn!("Unspecified ommx.v1.instance.Sense found, defaulting to Minimize");
Ok(Sense::Minimize)
}
}
}
}
impl TryFrom<v1::instance::Sense> for Sense {
type Error = ParseError;
fn try_from(value: v1::instance::Sense) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl TryFrom<i32> for Sense {
type Error = anyhow::Error;
fn try_from(value: i32) -> Result<Self, Self::Error> {
let v1_sense = v1::instance::Sense::try_from(value).map_err(|_| {
anyhow::anyhow!("Invalid integer for ommx.v1.instance.Sense: {}", value)
})?;
Ok(v1_sense.try_into()?)
}
}
impl From<Sense> for v1::instance::Sense {
fn from(value: Sense) -> Self {
match value {
Sense::Minimize => v1::instance::Sense::Minimize,
Sense::Maximize => v1::instance::Sense::Maximize,
}
}
}
impl From<Sense> for i32 {
fn from(value: Sense) -> Self {
v1::instance::Sense::from(value).into()
}
}
impl Parse for v1::Instance {
type Output = Instance;
type Context = ();
fn parse(self, _context: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v1.Instance";
crate::parse::check_format_version(self.format_version, message)?;
crate::parse::validate_extension_annotations(&self.annotations, message)?;
let sense = self.sense().parse_as(&(), message, "sense")?;
let (decision_variables, variable_labels, fixed_decision_variable_values): (
BTreeMap<VariableID, DecisionVariable>,
crate::VariableLabelStore,
BTreeMap<VariableID, f64>,
) = self
.decision_variables
.parse_as(&(), message, "decision_variables")?;
let objective = self
.objective
.ok_or(RawParseError::MissingField {
message,
field: "objective",
})?
.parse_as(&(), message, "objective")?;
let decision_variable_ids: VariableIDSet = decision_variables.keys().cloned().collect();
for id in objective.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "objective"));
}
}
let (constraints, mut constraint_context): (
BTreeMap<ConstraintID, Constraint>,
crate::ConstraintContextStore<ConstraintID>,
) = self.constraints.parse_as(&(), message, "constraints")?;
for constraint in constraints.values() {
for id in constraint.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "constraints"));
}
}
}
let removed_constraints_with_context: BTreeMap<
ConstraintID,
(Constraint, ConstraintContext, RemovedReason),
> = self
.removed_constraints
.parse_as(&constraints, message, "removed_constraints")?;
let mut removed_constraints: BTreeMap<ConstraintID, (Constraint, RemovedReason)> =
BTreeMap::new();
for (id, (c, context, reason)) in removed_constraints_with_context {
constraint_context.insert(id, context);
removed_constraints.insert(id, (c, reason));
}
let (named_functions, named_function_labels): (
BTreeMap<NamedFunctionID, NamedFunction>,
crate::named_function::NamedFunctionLabelStore,
) = self
.named_functions
.parse_as(&(), message, "named_functions")?;
for named_function in named_functions.values() {
for id in named_function.function.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "named_functions"));
}
}
}
let mut decision_variable_dependency = BTreeMap::default();
for (id, f) in self.decision_variable_dependency {
decision_variable_dependency.insert(
as_variable_id(&decision_variables, id)
.map_err(|e| e.context(message, "decision_variable_dependency"))?,
f.parse_as(&(), message, "decision_variable_dependency")?,
);
}
let decision_variable_dependency = AcyclicAssignments::new(decision_variable_dependency)
.map_err(|e| RawParseError::from(e).context(message, "decision_variable_dependency"))?;
ignore_legacy_constraint_hints(self.constraint_hints, message);
validate_fixed_decision_variable_partition(
message,
&objective,
&constraints,
&decision_variable_dependency,
&fixed_decision_variable_values,
)?;
let decision_variables = DecisionVariableTable::with_fixed_values(
decision_variables,
variable_labels,
fixed_decision_variable_values,
crate::ATol::default(),
)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "decision_variables")
})?;
Ok(Instance {
sense,
objective,
decision_variables,
constraint_collection: ConstraintCollection::with_context(
constraints,
removed_constraints,
constraint_context,
)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "constraints")
})?,
indicator_constraint_collection: Default::default(),
one_hot_constraint_collection: Default::default(),
sos1_constraint_collection: Default::default(),
decision_variable_dependency,
parameters: self.parameters,
description: self.description,
annotations: self.annotations,
named_functions: crate::NamedFunctionTable::new(named_functions, named_function_labels)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string())
.context(message, "named_functions")
})?,
})
}
}
impl TryFrom<v1::Instance> for Instance {
type Error = ParseError;
fn try_from(value: v1::Instance) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl Parse for v2::Instance {
type Output = Instance;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v2.Instance";
let required_features =
crate::v2_io::parse_required_features(self.required_features, message)?;
let annotations =
crate::v2_io::extension_annotations_from_v2_map(self.annotations, message)?;
let sense = crate::v2_io::parse_v2_required_sense(self.sense, message)?;
let decision_variables = self
.decision_variables
.ok_or(RawParseError::MissingField {
message,
field: "decision_variables",
})?
.parse_as(&(), message, "decision_variables")?;
let decision_variable_ids: VariableIDSet = decision_variables.keys().copied().collect();
let objective = self
.objective
.ok_or(RawParseError::MissingField {
message,
field: "objective",
})?
.parse_as(&(), message, "objective")?;
for id in objective.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "objective"));
}
}
let constraint_collection = self
.regular_constraints
.map(|value| value.parse_as(&(), message, "regular_constraints"))
.transpose()?
.unwrap_or_default();
let indicator_constraint_collection = self
.indicator_constraints
.map(|value| value.parse_as(&(), message, "indicator_constraints"))
.transpose()?
.unwrap_or_default();
let one_hot_constraint_collection = self
.one_hot_constraints
.map(|value| value.parse_as(&(), message, "one_hot_constraints"))
.transpose()?
.unwrap_or_default();
let sos1_constraint_collection = self
.sos1_constraints
.map(|value| value.parse_as(&(), message, "sos1_constraints"))
.transpose()?
.unwrap_or_default();
validate_instance_special_features(
&required_features,
&indicator_constraint_collection,
&one_hot_constraint_collection,
&sos1_constraint_collection,
message,
)?;
validate_created_constraint_references(
&constraint_collection,
&decision_variable_ids,
message,
"regular_constraints",
)?;
validate_created_constraint_references(
&indicator_constraint_collection,
&decision_variable_ids,
message,
"indicator_constraints",
)?;
validate_created_constraint_references(
&one_hot_constraint_collection,
&decision_variable_ids,
message,
"one_hot_constraints",
)?;
validate_created_constraint_references(
&sos1_constraint_collection,
&decision_variable_ids,
message,
"sos1_constraints",
)?;
validate_structural_special_constraints(
&decision_variables,
None,
&indicator_constraint_collection,
&one_hot_constraint_collection,
&sos1_constraint_collection,
message,
)?;
let named_functions = self
.named_functions
.map(|value| value.parse_as(&(), message, "named_functions"))
.transpose()?
.unwrap_or_default();
for named_function in named_functions.values() {
for id in named_function.function.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "named_functions"));
}
}
}
let decision_variable_dependency =
parse_v2_decision_variable_dependency(self.decision_variable_dependency, message)?;
for id in decision_variable_dependency.keys() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Variable ID {id:?} in decision_variable_dependency is not in decision_variables"
))
.context(message, "decision_variable_dependency"));
}
}
for id in decision_variable_dependency.required_ids() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used in decision_variable_dependency: {id:?}"
))
.context(message, "decision_variable_dependency"));
}
}
let mut used = objective.required_ids();
used.extend(active_used_ids_from_created_collection(
&constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&indicator_constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&one_hot_constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&sos1_constraint_collection,
));
validate_fixed_dependent_partition_from_sets(
&used,
decision_variables.fixed_values(),
&decision_variable_dependency,
message,
)?;
Ok(Instance {
sense,
objective,
decision_variables,
constraint_collection,
indicator_constraint_collection,
one_hot_constraint_collection,
sos1_constraint_collection,
decision_variable_dependency,
parameters: self.parameters,
description: self.description,
annotations,
named_functions,
})
}
}
impl TryFrom<v2::Instance> for Instance {
type Error = ParseError;
fn try_from(value: v2::Instance) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl TryFrom<Instance> for v1::Instance {
type Error = crate::Error;
fn try_from(value: Instance) -> crate::Result<Self> {
let decision_variables: Vec<v1::DecisionVariable> = (&value.decision_variables).into();
let (constraints, removed_constraints): (Vec<v1::Constraint>, Vec<v1::RemovedConstraint>) =
value.constraint_collection.into();
let named_functions: Vec<v1::NamedFunction> = value.named_functions.into();
let decision_variable_dependency = value
.decision_variable_dependency
.into_iter()
.map(|(id, dep)| (id.into(), dep.into()))
.collect();
if !value.indicator_constraint_collection.active().is_empty()
|| !value.indicator_constraint_collection.removed().is_empty()
{
crate::bail!(
"Indicator constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
if !value.one_hot_constraint_collection.active().is_empty()
|| !value.one_hot_constraint_collection.removed().is_empty()
{
crate::bail!(
"One-hot constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
if !value.sos1_constraint_collection.active().is_empty()
|| !value.sos1_constraint_collection.removed().is_empty()
{
crate::bail!(
"SOS1 constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
Ok(Self {
sense: v1::instance::Sense::from(value.sense).into(),
decision_variables,
objective: Some(value.objective.into()),
constraints,
named_functions,
removed_constraints,
decision_variable_dependency,
parameters: value.parameters,
description: value.description,
constraint_hints: None,
format_version: crate::CURRENT_FORMAT_VERSION,
annotations: crate::protobuf_extension_annotations(value.annotations),
})
}
}
impl Parse for v1::ParametricInstance {
type Output = ParametricInstance;
type Context = ();
fn parse(self, _context: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v1.ParametricInstance";
crate::parse::check_format_version(self.format_version, message)?;
crate::parse::validate_extension_annotations(&self.annotations, message)?;
let sense = self.sense().parse_as(&(), message, "sense")?;
let (decision_variables, variable_labels, fixed_decision_variable_values): (
BTreeMap<VariableID, DecisionVariable>,
crate::VariableLabelStore,
BTreeMap<VariableID, f64>,
) = self
.decision_variables
.parse_as(&(), message, "decision_variables")?;
let parameters = ParameterTable::from_v1_parameters(self.parameters).map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "parameters")
})?;
let decision_variable_ids: VariableIDSet = decision_variables.keys().cloned().collect();
let parameter_ids: VariableIDSet = parameters.keys().cloned().collect();
let intersection: VariableIDSet = decision_variable_ids
.intersection(¶meter_ids)
.cloned()
.collect();
if !intersection.is_empty() {
let id = *intersection.iter().next().unwrap();
return Err(RawParseError::InvalidInstance(format!(
"Duplicated variable ID is found in definition: {id:?}"
))
.context(message, "parameters"));
}
let objective = self
.objective
.ok_or(RawParseError::MissingField {
message,
field: "objective",
})?
.parse_as(&(), message, "objective")?;
let all_variable_ids: VariableIDSet = decision_variable_ids
.union(¶meter_ids)
.cloned()
.collect();
for id in objective.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "objective"));
}
}
let (constraints, mut constraint_context): (
BTreeMap<ConstraintID, Constraint>,
crate::ConstraintContextStore<ConstraintID>,
) = self.constraints.parse_as(&(), message, "constraints")?;
for constraint in constraints.values() {
for id in constraint.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "constraints"));
}
}
}
let removed_constraints_with_context: BTreeMap<
ConstraintID,
(Constraint, ConstraintContext, RemovedReason),
> = self
.removed_constraints
.parse_as(&constraints, message, "removed_constraints")?;
let mut removed_constraints: BTreeMap<ConstraintID, (Constraint, RemovedReason)> =
BTreeMap::new();
for (id, (c, context, reason)) in removed_constraints_with_context {
constraint_context.insert(id, context);
removed_constraints.insert(id, (c, reason));
}
let (named_functions, named_function_labels): (
BTreeMap<NamedFunctionID, NamedFunction>,
crate::named_function::NamedFunctionLabelStore,
) = self
.named_functions
.parse_as(&(), message, "named_functions")?;
for named_function in named_functions.values() {
for id in named_function.function.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "named_functions"));
}
}
}
let mut decision_variable_dependency = BTreeMap::default();
for (id, f) in self.decision_variable_dependency {
decision_variable_dependency.insert(
as_variable_id(&decision_variables, id)
.map_err(|e| e.context(message, "decision_variable_dependency"))?,
f.parse_as(&(), message, "decision_variable_dependency")?,
);
}
let decision_variable_dependency = AcyclicAssignments::new(decision_variable_dependency)
.map_err(|e| RawParseError::from(e).context(message, "decision_variable_dependency"))?;
ignore_legacy_constraint_hints(self.constraint_hints, message);
validate_fixed_decision_variable_partition(
message,
&objective,
&constraints,
&decision_variable_dependency,
&fixed_decision_variable_values,
)?;
let decision_variables = DecisionVariableTable::with_fixed_values(
decision_variables,
variable_labels,
fixed_decision_variable_values,
crate::ATol::default(),
)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "decision_variables")
})?;
Ok(ParametricInstance {
sense,
objective,
decision_variables,
parameters,
constraint_collection: ConstraintCollection::with_context(
constraints,
removed_constraints,
constraint_context,
)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "constraints")
})?,
indicator_constraint_collection: Default::default(),
one_hot_constraint_collection: Default::default(),
sos1_constraint_collection: Default::default(),
named_functions: crate::NamedFunctionTable::new(named_functions, named_function_labels)
.map_err(|e| {
RawParseError::InvalidInstance(e.to_string())
.context(message, "named_functions")
})?,
decision_variable_dependency,
description: self.description,
annotations: self.annotations,
})
}
}
impl Parse for v2::ParametricInstance {
type Output = ParametricInstance;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v2.ParametricInstance";
let required_features =
crate::v2_io::parse_required_features(self.required_features, message)?;
let annotations =
crate::v2_io::extension_annotations_from_v2_map(self.annotations, message)?;
let sense = crate::v2_io::parse_v2_required_sense(self.sense, message)?;
let decision_variables = self
.decision_variables
.ok_or(RawParseError::MissingField {
message,
field: "decision_variables",
})?
.parse_as(&(), message, "decision_variables")?;
let parameters = self
.parameters
.map(|value| value.parse_as(&(), message, "parameters"))
.transpose()?
.unwrap_or_default();
let decision_variable_ids: VariableIDSet = decision_variables.keys().copied().collect();
let parameter_ids: VariableIDSet = parameters.keys().copied().collect();
if let Some(id) = decision_variable_ids.intersection(¶meter_ids).next() {
return Err(RawParseError::InvalidInstance(format!(
"Duplicated variable ID is found in definition: {id:?}"
))
.context(message, "parameters"));
}
let all_variable_ids: VariableIDSet = decision_variable_ids
.union(¶meter_ids)
.copied()
.collect();
let objective = self
.objective
.ok_or(RawParseError::MissingField {
message,
field: "objective",
})?
.parse_as(&(), message, "objective")?;
for id in objective.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "objective"));
}
}
let constraint_collection = self
.regular_constraints
.map(|value| value.parse_as(&(), message, "regular_constraints"))
.transpose()?
.unwrap_or_default();
let indicator_constraint_collection = self
.indicator_constraints
.map(|value| value.parse_as(&(), message, "indicator_constraints"))
.transpose()?
.unwrap_or_default();
let one_hot_constraint_collection = self
.one_hot_constraints
.map(|value| value.parse_as(&(), message, "one_hot_constraints"))
.transpose()?
.unwrap_or_default();
let sos1_constraint_collection = self
.sos1_constraints
.map(|value| value.parse_as(&(), message, "sos1_constraints"))
.transpose()?
.unwrap_or_default();
validate_instance_special_features(
&required_features,
&indicator_constraint_collection,
&one_hot_constraint_collection,
&sos1_constraint_collection,
message,
)?;
validate_created_constraint_references(
&constraint_collection,
&all_variable_ids,
message,
"regular_constraints",
)?;
validate_created_constraint_references(
&indicator_constraint_collection,
&all_variable_ids,
message,
"indicator_constraints",
)?;
validate_created_constraint_references(
&one_hot_constraint_collection,
&all_variable_ids,
message,
"one_hot_constraints",
)?;
validate_created_constraint_references(
&sos1_constraint_collection,
&all_variable_ids,
message,
"sos1_constraints",
)?;
validate_structural_special_constraints(
&decision_variables,
Some(¶meter_ids),
&indicator_constraint_collection,
&one_hot_constraint_collection,
&sos1_constraint_collection,
message,
)?;
let named_functions = self
.named_functions
.map(|value| value.parse_as(&(), message, "named_functions"))
.transpose()?
.unwrap_or_default();
for named_function in named_functions.values() {
for id in named_function.function.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used: {id:?}"
))
.context(message, "named_functions"));
}
}
}
let decision_variable_dependency =
parse_v2_decision_variable_dependency(self.decision_variable_dependency, message)?;
for id in decision_variable_dependency.keys() {
if !decision_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Variable ID {id:?} in decision_variable_dependency is not in decision_variables"
))
.context(message, "decision_variable_dependency"));
}
}
for id in decision_variable_dependency.required_ids() {
if !all_variable_ids.contains(&id) {
return Err(RawParseError::InvalidInstance(format!(
"Undefined variable ID is used in decision_variable_dependency: {id:?}"
))
.context(message, "decision_variable_dependency"));
}
}
let mut used = objective.required_ids();
used.extend(active_used_ids_from_created_collection(
&constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&indicator_constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&one_hot_constraint_collection,
));
used.extend(active_used_ids_from_created_collection(
&sos1_constraint_collection,
));
validate_fixed_dependent_partition_from_sets(
&used,
decision_variables.fixed_values(),
&decision_variable_dependency,
message,
)?;
Ok(ParametricInstance {
sense,
objective,
decision_variables,
parameters,
constraint_collection,
indicator_constraint_collection,
one_hot_constraint_collection,
sos1_constraint_collection,
named_functions,
decision_variable_dependency,
description: self.description,
annotations,
})
}
}
impl TryFrom<v2::ParametricInstance> for ParametricInstance {
type Error = ParseError;
fn try_from(value: v2::ParametricInstance) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl TryFrom<ParametricInstance> for v1::ParametricInstance {
type Error = crate::Error;
fn try_from(
ParametricInstance {
sense,
objective,
decision_variables,
parameters,
constraint_collection,
indicator_constraint_collection,
one_hot_constraint_collection,
sos1_constraint_collection,
decision_variable_dependency,
description,
named_functions,
annotations,
}: ParametricInstance,
) -> crate::Result<Self> {
if !indicator_constraint_collection.active().is_empty()
|| !indicator_constraint_collection.removed().is_empty()
{
crate::bail!(
"Indicator constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
if !one_hot_constraint_collection.active().is_empty()
|| !one_hot_constraint_collection.removed().is_empty()
{
crate::bail!(
"One-hot constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
if !sos1_constraint_collection.active().is_empty()
|| !sos1_constraint_collection.removed().is_empty()
{
crate::bail!(
"SOS1 constraints cannot be serialized to ommx.v1 protobuf; use to_v2_bytes()"
);
}
let v1_decision_variables: Vec<v1::DecisionVariable> = (&decision_variables).into();
let (v1_constraints, v1_removed_constraints): (
Vec<v1::Constraint>,
Vec<v1::RemovedConstraint>,
) = constraint_collection.into();
let v1_named_functions: Vec<v1::NamedFunction> = named_functions.into();
Ok(Self {
description,
sense: v1::instance::Sense::from(sense) as i32,
objective: Some(objective.into()),
decision_variables: v1_decision_variables,
parameters: parameters.into_v1_parameters(),
constraints: v1_constraints,
named_functions: v1_named_functions,
removed_constraints: v1_removed_constraints,
decision_variable_dependency: decision_variable_dependency
.into_iter()
.map(|(id, dep)| (id.into(), dep.into()))
.collect(),
constraint_hints: None,
format_version: crate::CURRENT_FORMAT_VERSION,
annotations: crate::protobuf_extension_annotations(annotations),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coeff;
use crate::instance::Instance;
use proptest::prelude::*;
use prost::Message;
use std::collections::HashMap;
fn binary_decision_variables() -> Vec<v1::DecisionVariable> {
[0, 1]
.into_iter()
.map(|id| {
decision_variable_to_v1(
crate::VariableID::from(id),
crate::DecisionVariable::binary(),
Default::default(),
)
})
.collect()
}
fn decision_variable_to_v1(
id: VariableID,
decision_variable: DecisionVariable,
label: crate::ModelingLabel,
) -> v1::DecisionVariable {
v1::DecisionVariable {
id: id.into_inner(),
kind: decision_variable.kind().into(),
bound: Some(decision_variable.bound().into()),
substituted_value: None,
name: label.name,
subscripts: label.subscripts,
parameters: label.parameters.into_iter().collect(),
description: label.description,
}
}
fn constraint_to_v1(
id: ConstraintID,
value: Constraint,
context: ConstraintContext,
) -> v1::Constraint {
let label = context.label;
v1::Constraint {
id: id.into_inner(),
equality: value.equality.into(),
function: Some(value.stage.function.into()),
name: label.name,
subscripts: label.subscripts,
parameters: label.parameters.into_iter().collect(),
description: label.description,
}
}
fn removed_constraint_to_v1(
id: ConstraintID,
constraint: Constraint,
context: ConstraintContext,
removed_reason: RemovedReason,
) -> v1::RemovedConstraint {
v1::RemovedConstraint {
constraint: Some(constraint_to_v1(id, constraint, context)),
removed_reason: removed_reason.reason,
removed_reason_parameters: removed_reason.parameters.into_iter().collect(),
}
}
fn labeled_constraint(id: u64, name: &str) -> v1::Constraint {
constraint_to_v1(
ConstraintID::from(id),
Constraint::equal_to_zero(crate::Function::Zero),
ConstraintContext {
label: crate::ModelingLabel {
name: Some(name.to_string()),
subscripts: vec![id as i64],
..Default::default()
},
..Default::default()
},
)
}
fn adversarial_constraint_hints() -> v1::ConstraintHints {
v1::ConstraintHints {
one_hot_constraints: vec![
v1::OneHot {
constraint_id: 1,
decision_variables: vec![0, 1],
},
v1::OneHot {
constraint_id: 999,
decision_variables: vec![2, 2, 999],
},
v1::OneHot {
constraint_id: 20,
decision_variables: vec![0, 1],
},
],
sos1_constraints: vec![
v1::Sos1 {
binary_constraint_id: 10,
big_m_constraint_ids: vec![11, 11],
decision_variables: vec![0, 1],
},
v1::Sos1 {
binary_constraint_id: 1,
big_m_constraint_ids: vec![999],
decision_variables: vec![0, 0],
},
],
}
}
fn decision_variables_for_hint_tests() -> Vec<v1::DecisionVariable> {
let mut decision_variables = binary_decision_variables();
decision_variables.push(decision_variable_to_v1(
VariableID::from(2),
DecisionVariable::integer(),
Default::default(),
));
decision_variables
}
fn constraints_for_hint_tests() -> Vec<v1::Constraint> {
vec![
labeled_constraint(1, "one_hot_source"),
labeled_constraint(10, "sos1_cardinality_source"),
labeled_constraint(11, "sos1_big_m_source"),
]
}
fn removed_constraints_for_hint_tests() -> Vec<v1::RemovedConstraint> {
vec![removed_constraint_to_v1(
ConstraintID::from(20),
Constraint::equal_to_zero(Function::Zero),
ConstraintContext {
label: crate::ModelingLabel {
name: Some("removed_source".to_string()),
..Default::default()
},
..Default::default()
},
RemovedReason {
reason: "test".to_string(),
parameters: Default::default(),
},
)]
}
proptest! {
#[test]
fn instance_roundtrip(
original_instance in Instance::arbitrary_with(crate::InstanceParameters::v1_compatible())
) {
let v1_instance = v1::Instance::try_from(original_instance.clone()).unwrap();
let roundtripped_instance = Instance::try_from(v1_instance).unwrap();
assert_eq!(original_instance, roundtripped_instance);
}
}
#[test]
fn test_instance_parse_rejects_reserved_annotation_key() {
let v1_instance = v1::Instance {
annotations: HashMap::from([(
crate::annotation_keys::INSTANCE_TITLE.to_string(),
"bad".to_string(),
)]),
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[annotations]
Annotation key `org.ommx.v1.instance.title` is reserved for OMMX metadata and cannot be stored in extension annotations.
"###);
}
#[test]
fn test_instance_parse_ignores_constraint_hints() {
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(crate::Function::Zero.into()),
decision_variables: decision_variables_for_hint_tests(),
constraints: constraints_for_hint_tests(),
removed_constraints: removed_constraints_for_hint_tests(),
constraint_hints: Some(adversarial_constraint_hints()),
..Default::default()
};
let mut without_hints = v1_instance.clone();
without_hints.constraint_hints = None;
let expected = Instance::from_v1_bytes(&without_hints.encode_to_vec()).unwrap();
let parsed = Instance::from_v1_bytes(&v1_instance.encode_to_vec()).unwrap();
assert_eq!(parsed, expected);
assert_eq!(parsed.constraints().len(), 3);
assert!(parsed.one_hot_constraints().is_empty());
assert!(parsed.sos1_constraints().is_empty());
assert!(parsed.required_capabilities().is_empty());
for (id, expected_name) in [
(1, "one_hot_source"),
(10, "sos1_cardinality_source"),
(11, "sos1_big_m_source"),
] {
let id = ConstraintID::from(id);
assert!(parsed.constraints().contains_key(&id));
assert_eq!(parsed.constraint_context().name(id), Some(expected_name));
}
let removed_id = ConstraintID::from(20);
assert!(parsed.removed_constraints().contains_key(&removed_id));
assert_eq!(
parsed.constraint_context().name(removed_id),
Some("removed_source")
);
let roundtripped = Instance::from_v1_bytes(&parsed.to_v1_bytes().unwrap()).unwrap();
assert_eq!(roundtripped, parsed);
}
#[test]
fn test_instance_parse_does_not_promote_exact_one_hot_hint() {
let source_id = ConstraintID::from(1);
let source = Constraint::equal_to_zero(Function::from(
((crate::linear!(0) + crate::linear!(1)).unwrap() + crate::coeff!(-1.0)).unwrap(),
));
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(crate::Function::Zero.into()),
decision_variables: binary_decision_variables(),
constraints: vec![constraint_to_v1(
source_id,
source.clone(),
ConstraintContext::default(),
)],
constraint_hints: Some(v1::ConstraintHints {
one_hot_constraints: vec![v1::OneHot {
constraint_id: source_id.into_inner(),
decision_variables: vec![0, 1],
}],
sos1_constraints: vec![],
}),
..Default::default()
};
let parsed = Instance::from_v1_bytes(&v1_instance.encode_to_vec()).unwrap();
assert_eq!(parsed.constraints().get(&source_id), Some(&source));
assert!(parsed.one_hot_constraints().is_empty());
assert!(parsed.required_capabilities().is_empty());
}
#[test]
fn test_instance_parse_does_not_promote_exact_binary_sos1_hint() {
let source_id = ConstraintID::from(1);
let source = Constraint::less_than_or_equal_to_zero(Function::from(
((crate::linear!(0) + crate::linear!(1)).unwrap() + crate::coeff!(-1.0)).unwrap(),
));
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(crate::Function::Zero.into()),
decision_variables: binary_decision_variables(),
constraints: vec![constraint_to_v1(
source_id,
source.clone(),
ConstraintContext::default(),
)],
constraint_hints: Some(v1::ConstraintHints {
one_hot_constraints: vec![],
sos1_constraints: vec![v1::Sos1 {
binary_constraint_id: source_id.into_inner(),
big_m_constraint_ids: vec![],
decision_variables: vec![0, 1],
}],
}),
..Default::default()
};
let parsed = Instance::from_v1_bytes(&v1_instance.encode_to_vec()).unwrap();
assert_eq!(parsed.constraints().get(&source_id), Some(&source));
assert!(parsed.sos1_constraints().is_empty());
assert!(parsed.required_capabilities().is_empty());
}
#[test]
fn test_parametric_instance_parse_ignores_constraint_hints() {
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(crate::Function::Zero.into()),
decision_variables: decision_variables_for_hint_tests(),
constraints: constraints_for_hint_tests(),
removed_constraints: removed_constraints_for_hint_tests(),
constraint_hints: Some(adversarial_constraint_hints()),
..Default::default()
};
let mut without_hints = v1_parametric_instance.clone();
without_hints.constraint_hints = None;
let expected =
crate::ParametricInstance::from_v1_bytes(&without_hints.encode_to_vec()).unwrap();
let parsed =
crate::ParametricInstance::from_v1_bytes(&v1_parametric_instance.encode_to_vec())
.unwrap();
assert_eq!(parsed, expected);
assert_eq!(parsed.constraints().len(), 3);
assert!(parsed.one_hot_constraints().is_empty());
assert!(parsed.sos1_constraints().is_empty());
for (id, expected_name) in [
(1, "one_hot_source"),
(10, "sos1_cardinality_source"),
(11, "sos1_big_m_source"),
] {
let id = ConstraintID::from(id);
assert!(parsed.constraints().contains_key(&id));
assert_eq!(parsed.constraint_context().name(id), Some(expected_name));
}
let removed_id = ConstraintID::from(20);
assert!(parsed.removed_constraints().contains_key(&removed_id));
assert_eq!(
parsed.constraint_context().name(removed_id),
Some("removed_source")
);
let roundtripped =
crate::ParametricInstance::from_v1_bytes(&parsed.to_v1_bytes().unwrap()).unwrap();
assert_eq!(roundtripped, parsed);
}
#[test]
fn test_parametric_instance_parse_rejects_reserved_annotation_key() {
let v1_parametric_instance = v1::ParametricInstance {
annotations: HashMap::from([(
format!(
"{}.title",
crate::annotation_keys::PARAMETRIC_INSTANCE_NAMESPACE
),
"bad".to_string(),
)]),
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[annotations]
Annotation key `org.ommx.v1.parametric-instance.title` is reserved for OMMX metadata and cannot be stored in extension annotations.
"###);
}
#[test]
fn test_instance_to_v1_bytes_filters_reserved_annotation_key() {
let instance = Instance {
annotations: HashMap::from([
(
crate::annotation_keys::INSTANCE_TITLE.to_string(),
"invalid extension title".to_string(),
),
("org.example.owner".to_string(), "domain".to_string()),
]),
..Default::default()
};
let restored = Instance::from_v1_bytes(&instance.to_v1_bytes().unwrap()).unwrap();
assert!(!restored
.annotations
.contains_key(crate::annotation_keys::INSTANCE_TITLE));
assert_eq!(
restored.annotations.get("org.example.owner"),
Some(&"domain".to_string())
);
}
#[test]
fn test_parametric_instance_to_v1_bytes_filters_reserved_annotation_key() {
let mut instance: crate::ParametricInstance = Instance::default().into();
let reserved_key = format!(
"{}.title",
crate::annotation_keys::PARAMETRIC_INSTANCE_NAMESPACE
);
instance.annotations = HashMap::from([
(reserved_key.clone(), "invalid extension title".to_string()),
("org.example.owner".to_string(), "domain".to_string()),
]);
let restored =
crate::ParametricInstance::from_v1_bytes(&instance.to_v1_bytes().unwrap()).unwrap();
assert!(!restored.annotations.contains_key(&reserved_key));
assert_eq!(
restored.annotations.get("org.example.owner"),
Some(&"domain".to_string())
);
}
#[test]
fn test_parametric_instance_parse_fails_with_undefined_variable_in_objective() {
use crate::{coeff, linear, DecisionVariable, Function, VariableID};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(999) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[objective]
Undefined variable ID is used: VariableID(999)
"###);
}
#[test]
fn test_parametric_instance_parse_fails_with_undefined_variable_in_constraint() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![constraint_to_v1(
ConstraintID::from(1),
Constraint::equal_to_zero(Function::from(linear!(999) + coeff!(1.0))),
Default::default(),
)],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[constraints]
Undefined variable ID is used: VariableID(999)
"###);
}
#[test]
fn test_instance_parse_fails_with_undefined_variable_in_objective() {
use crate::{coeff, linear, DecisionVariable, Function, VariableID};
use std::collections::HashMap;
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(999) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[objective]
Undefined variable ID is used: VariableID(999)
"###);
}
#[test]
fn test_instance_parse_rejects_fixed_variable_used_in_objective() {
use crate::{linear, Function};
use std::collections::HashMap;
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1)).into()),
decision_variables: vec![v1::DecisionVariable {
id: 1,
kind: v1::decision_variable::Kind::Binary as i32,
bound: Some(crate::Bound::of_binary().into()),
substituted_value: Some(1.0),
..Default::default()
}],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[decision_variables]
Fixed variable VariableID(1) cannot be used in objectives or constraints
"###);
}
#[test]
fn test_instance_parse_fails_with_undefined_variable_in_constraint() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![constraint_to_v1(
ConstraintID::from(1),
Constraint::equal_to_zero(Function::from(linear!(999) + coeff!(1.0))),
Default::default(),
)],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[constraints]
Undefined variable ID is used: VariableID(999)
"###);
}
#[test]
fn test_parametric_instance_parse_fails_with_duplicate_constraint_ids() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let cid = ConstraintID::from(1);
let constraint = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(1.0)));
let removed_reason = crate::constraint::RemovedReason {
reason: "test".to_string(),
parameters: Default::default(),
};
let removed_constraint =
removed_constraint_to_v1(cid, constraint.clone(), Default::default(), removed_reason);
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![constraint_to_v1(cid, constraint, Default::default())],
named_functions: vec![],
removed_constraints: vec![removed_constraint],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[removed_constraints]
Duplicated constraint ID is found in definition: ConstraintID(1)
"###);
}
#[test]
fn test_instance_parse_fails_with_duplicate_constraint_ids() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let cid = ConstraintID::from(1);
let constraint = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(1.0)));
let removed_reason = crate::constraint::RemovedReason {
reason: "test".to_string(),
parameters: Default::default(),
};
let removed_constraint =
removed_constraint_to_v1(cid, constraint.clone(), Default::default(), removed_reason);
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![constraint_to_v1(cid, constraint, Default::default())],
named_functions: vec![],
removed_constraints: vec![removed_constraint],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[removed_constraints]
Duplicated constraint ID is found in definition: ConstraintID(1)
"###);
}
#[test]
fn test_parametric_instance_parse_with_invalid_sense_uses_default() {
use crate::{linear, DecisionVariable, Function, Sense, VariableID};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: 999, objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.sense, Sense::Minimize);
}
#[test]
fn test_instance_parse_with_invalid_sense_uses_default() {
use crate::{linear, DecisionVariable, Function, Sense, VariableID};
use std::collections::HashMap;
let v1_instance = v1::Instance {
sense: 999, objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.sense, Sense::Minimize);
}
#[test]
fn test_parametric_instance_parse_fails_with_missing_objective() {
use crate::{DecisionVariable, VariableID};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: None, decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
Field objective in ommx.v1.ParametricInstance is missing.
"###);
}
#[test]
fn test_instance_parse_fails_with_missing_objective() {
use crate::{DecisionVariable, VariableID};
use std::collections::HashMap;
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: None, decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
Field objective in ommx.v1.Instance is missing.
"###);
}
#[test]
fn test_parametric_instance_parse_fails_with_duplicated_variable_id() {
use crate::{coeff, linear, DecisionVariable, Function, VariableID};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 1,
name: Some("p1".to_string()),
..Default::default()
}], constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[parameters]
Duplicated variable ID is found in definition: VariableID(1)
"###);
}
#[test]
fn test_parametric_instance_parse_fails_with_duplicated_parameter_id() {
use crate::{linear, DecisionVariable, Function, VariableID};
use std::collections::HashMap;
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(100)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![
v1::Parameter {
id: 100,
name: Some("p".to_string()),
..Default::default()
},
v1::Parameter {
id: 100,
name: Some("q".to_string()),
..Default::default()
},
],
constraints: vec![],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[parameters]
Duplicated parameter ID is found in definition: VariableID(100)
"###);
}
#[test]
fn test_parametric_instance_parse_fails_with_duplicated_constraint_id_in_constraints() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let cid = ConstraintID::from(1);
let constraint1 = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(1.0)));
let constraint2 = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(2.0)));
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
parameters: vec![v1::Parameter {
id: 100,
name: Some("p1".to_string()),
..Default::default()
}],
constraints: vec![
constraint_to_v1(cid, constraint1, Default::default()),
constraint_to_v1(cid, constraint2, Default::default()),
],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
constraint_hints: None,
description: None,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[constraints]
Duplicated constraint ID is found in definition: ConstraintID(1)
"###);
}
#[test]
fn test_instance_parse_fails_with_duplicated_constraint_id_in_constraints() {
use crate::{
coeff, linear, Constraint, ConstraintID, DecisionVariable, Function, VariableID,
};
use std::collections::HashMap;
let cid = ConstraintID::from(1);
let constraint1 = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(1.0)));
let constraint2 = Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(2.0)));
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Function::from(linear!(1) + coeff!(1.0)).into()),
decision_variables: vec![decision_variable_to_v1(
VariableID::from(1),
DecisionVariable::binary(),
Default::default(),
)],
constraints: vec![
constraint_to_v1(cid, constraint1, Default::default()),
constraint_to_v1(cid, constraint2, Default::default()),
],
named_functions: vec![],
removed_constraints: vec![],
decision_variable_dependency: HashMap::new(),
parameters: None,
description: None,
constraint_hints: None,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[constraints]
Duplicated constraint ID is found in definition: ConstraintID(1)
"###);
}
#[test]
fn test_instance_parse_rejects_future_format_version() {
let v1_instance = v1::Instance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Default::default()),
format_version: 1,
..Default::default()
};
let result = v1_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.Instance[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_v2_instance_parse_rejects_undefined_dependency_rhs() {
use crate::{linear, DecisionVariable, Function, Sense, VariableID};
let instance = Instance::builder()
.sense(Sense::Minimize)
.objective(Function::from(linear!(1)))
.decision_variables(maplit::btreemap! {
VariableID::from(1) => DecisionVariable::binary(),
})
.constraints(Default::default())
.build()
.unwrap();
let mut proto = crate::v2::Instance::from(instance);
proto
.decision_variable_dependency
.insert(1, Function::from(linear!(999)).into());
let err = Instance::try_from(proto).unwrap_err();
assert!(
err.to_string().contains(
"Undefined variable ID is used in decision_variable_dependency: VariableID(999)"
),
"unexpected error: {err}"
);
}
#[test]
fn test_parametric_instance_parse_rejects_future_format_version() {
let v1_parametric_instance = v1::ParametricInstance {
sense: v1::instance::Sense::Minimize as i32,
objective: Some(Default::default()),
format_version: 1,
..Default::default()
};
let result = v1_parametric_instance.parse(&());
insta::assert_snapshot!(result.unwrap_err(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.ParametricInstance[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_parametric_instance_roundtrip_preserves_labels_and_context() {
use crate::{
coeff, linear, Constraint, ConstraintContext, ConstraintID, DecisionVariable, Function,
ModelingLabel, Sense, VariableID,
};
let var_id = VariableID::from(1);
let cid = ConstraintID::from(10);
let mut instance = ParametricInstance::builder()
.sense(Sense::Minimize)
.objective(Function::from(linear!(1)))
.decision_variables(maplit::btreemap! {
var_id => DecisionVariable::binary(),
})
.parameters(ParameterTable::default())
.constraints(maplit::btreemap! {
cid => Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(-1.0))),
})
.build()
.unwrap();
instance
.set_variable_label(
var_id,
ModelingLabel {
name: Some("x".to_string()),
subscripts: vec![0],
..Default::default()
},
)
.unwrap();
instance
.set_constraint_context(
cid,
ConstraintContext {
label: ModelingLabel {
name: Some("balance".to_string()),
description: Some("demand-balance row".to_string()),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let bytes = instance.to_v1_bytes().unwrap();
let recovered = ParametricInstance::from_v1_bytes(&bytes).unwrap();
assert_eq!(recovered.variable_labels().name(var_id), Some("x"));
assert_eq!(recovered.variable_labels().subscripts(var_id), &[0]);
assert_eq!(
recovered.constraint_collection().context().name(cid),
Some("balance"),
);
assert_eq!(
recovered.constraint_collection().context().description(cid),
Some("demand-balance row"),
);
}
#[test]
fn test_instance_roundtrip_preserves_named_function_labels() {
use crate::{
coeff, linear, DecisionVariable, Function, NamedFunctionID, Sense, VariableID,
};
let var_id = VariableID::from(1);
let nf_id = NamedFunctionID::from(0);
let mut instance = Instance::builder()
.sense(Sense::Minimize)
.objective(Function::from(linear!(1)))
.decision_variables(maplit::btreemap! {
var_id => DecisionVariable::binary(),
})
.constraints(BTreeMap::new())
.build()
.unwrap();
instance
.new_named_function(
Function::from(linear!(1) + coeff!(1.0)),
Some("offset_x".to_string()),
vec![0],
fnv::FnvHashMap::default(),
Some("x plus a constant".to_string()),
)
.unwrap();
let bytes = instance.to_v1_bytes().unwrap();
let recovered = Instance::from_v1_bytes(&bytes).unwrap();
assert_eq!(
recovered.named_function_labels().name(nf_id),
Some("offset_x"),
);
assert_eq!(recovered.named_function_labels().subscripts(nf_id), &[0]);
assert_eq!(
recovered.named_function_labels().description(nf_id),
Some("x plus a constant"),
);
}
}