use super::*;
use crate::{v2, Parse, ParseError, RawParseError};
use std::collections::{BTreeMap, BTreeSet};
fn invalid_sample_set_sidecar(
error: crate::Error,
message: &'static str,
field: &'static str,
) -> ParseError {
let signal = crate::SampleSetError::InvalidSidecar {
message: error.to_string(),
};
ParseError::new(signal).context(message, field)
}
fn validate_sampled_indicator_structural_ids(
constraints: &crate::constraint_type::SampledCollection<crate::IndicatorConstraint>,
decision_variables: &crate::SampledDecisionVariableTable,
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(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{constraint_id:?}"),
message: format!("indicator variable {id:?} is not in decision_variables"),
})
.context(message, "sampled_indicator_constraints"),
);
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{constraint_id:?}"),
message: format!("indicator variable {id:?} must be binary"),
})
.context(message, "sampled_indicator_constraints"),
);
}
}
Ok(())
}
fn validate_sampled_one_hot_structural_ids(
constraints: &crate::constraint_type::SampledCollection<crate::OneHotConstraint>,
decision_variables: &crate::SampledDecisionVariableTable,
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(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{constraint_id:?}"),
message: format!("variable {id:?} is not in decision_variables"),
})
.context(message, "sampled_one_hot_constraints"),
);
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{constraint_id:?}"),
message: format!("variable {id:?} must be binary"),
})
.context(message, "sampled_one_hot_constraints"),
);
}
}
}
Ok(())
}
fn validate_sampled_sos1_structural_ids(
constraints: &crate::constraint_type::SampledCollection<crate::Sos1Constraint>,
decision_variables: &crate::SampledDecisionVariableTable,
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(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{constraint_id:?}"),
message: format!("variable {id:?} is not in decision_variables"),
})
.context(message, "sampled_sos1_constraints"),
);
}
}
}
Ok(())
}
fn first_feasibility_mismatch(
provided: &BTreeMap<SampleID, bool>,
computed: &BTreeMap<SampleID, bool>,
) -> Option<(SampleID, bool, bool)> {
computed.iter().find_map(|(id, computed)| {
let provided = provided
.get(id)
.copied()
.expect("feasibility maps must be validated to have identical sample IDs");
let computed = *computed;
(provided != computed).then_some((*id, provided, computed))
})
}
fn validate_sample_bool_map_ids(
map: &BTreeMap<SampleID, bool>,
expected: &SampleIDSet,
message: &'static str,
field: &'static str,
) -> Result<(), ParseError> {
let found = map.keys().copied().collect::<SampleIDSet>();
if &found != expected {
return Err(
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: expected.clone(),
found,
})
.context(message, field),
);
}
Ok(())
}
impl Parse for crate::v1::SampleSet {
type Output = SampleSet;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v1.SampleSet";
crate::parse::check_format_version(self.format_version, message)?;
crate::parse::validate_extension_annotations(&self.annotations, message)?;
let mut decision_variables = BTreeMap::new();
let mut variable_labels = crate::VariableLabelStore::default();
for v1_sampled_dv in self.decision_variables {
let parsed: crate::decision_variable::parse::ParsedSampledDecisionVariable =
v1_sampled_dv.parse_as(&(), message, "decision_variables")?;
let dv_id = parsed.id;
variable_labels.insert(dv_id, parsed.label);
if decision_variables.insert(dv_id, parsed.variable).is_some() {
return Err(
ParseError::new(crate::SampleSetError::DuplicatedVariableID { id: dv_id })
.context(message, "decision_variables"),
);
}
}
let objectives = self
.objectives
.ok_or(
crate::RawParseError::MissingField {
message,
field: "objectives",
}
.context(message, "objectives"),
)?
.parse_as(&(), message, "objectives")?;
let mut constraints = std::collections::BTreeMap::new();
let mut constraint_removed_reasons = std::collections::BTreeMap::new();
let mut constraint_context =
crate::ConstraintContextStore::<crate::ConstraintID>::default();
for v1_constraint in self.constraints {
let (id, parsed_constraint, context, removed_reason): (
crate::ConstraintID,
crate::SampledConstraint,
crate::ConstraintContext,
Option<crate::RemovedReason>,
) = v1_constraint.parse_as(&(), message, "constraints")?;
if constraints.contains_key(&id) {
return Err(
ParseError::new(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "regular",
constraint_id: format!("{id:?}"),
message: "duplicated constraint ID in constraints".to_string(),
})
.context(message, "constraints"),
);
}
if let Some(reason) = removed_reason {
constraint_removed_reasons.insert(id, reason);
}
constraint_context.insert(id, context);
constraints.insert(id, parsed_constraint);
}
let mut named_functions = std::collections::BTreeMap::new();
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
for v1_named_function in self.named_functions {
let parsed: crate::named_function::parse::ParsedSampledNamedFunction =
v1_named_function.parse_as(&(), message, "named_functions")?;
let id = parsed.id;
if named_functions
.insert(id, parsed.sampled_named_function)
.is_some()
{
return Err(
ParseError::new(crate::SampleSetError::DuplicatedNamedFunctionID { id })
.context(message, "named_functions"),
);
}
named_function_labels.insert(id, parsed.label);
}
let sense = self.sense.try_into().map_err(|_| {
crate::RawParseError::UnknownEnumValue {
enum_name: "ommx.v1.Sense",
value: self.sense,
}
.context(message, "sense")
})?;
let sample_set = SampleSet::builder()
.decision_variables(decision_variables)
.variable_labels(variable_labels)
.objectives(objectives)
.constraints_collection(
crate::constraint_type::SampledCollection::with_context(
constraints,
constraint_removed_reasons,
constraint_context,
)
.map_err(|error| invalid_sample_set_sidecar(error, message, "constraints"))?,
)
.named_functions(named_functions)
.named_function_labels(named_function_labels)
.sense(sense)
.build()
.map_err(ParseError::new)?;
let mut sample_set = sample_set;
sample_set.metadata = self.metadata;
sample_set.annotations = self.annotations;
for (sample_id_u64, provided_feasible) in self.feasible {
let sample_id = crate::SampleID::from(sample_id_u64);
if let Some(computed_feasible) = sample_set.is_sample_feasible(sample_id) {
if provided_feasible != computed_feasible {
return Err(
ParseError::new(crate::SampleSetError::InconsistentFeasibility {
sample_id: sample_id_u64,
provided_feasible,
computed_feasible,
})
.context(message, "feasible"),
);
}
}
}
for (sample_id_u64, provided_feasible_relaxed) in self.feasible_relaxed {
let sample_id = crate::SampleID::from(sample_id_u64);
if let Some(computed_feasible_relaxed) =
sample_set.is_sample_feasible_relaxed(sample_id)
{
if provided_feasible_relaxed != computed_feasible_relaxed {
return Err(ParseError::new(
crate::SampleSetError::InconsistentFeasibilityRelaxed {
sample_id: sample_id_u64,
provided_feasible_relaxed,
computed_feasible_relaxed,
},
)
.context(message, "feasible_relaxed"));
}
}
}
Ok(sample_set)
}
}
impl Parse for v2::SampleSet {
type Output = SampleSet;
type Context = ();
fn parse(self, _: &Self::Context) -> Result<Self::Output, ParseError> {
let message = "ommx.v2.SampleSet";
crate::v2_io::validate_required_features(self.required_features, message)?;
let feasibility_atol =
crate::v2_io::parse_feasibility_atol(self.feasibility_atol, message)?;
let annotations =
crate::v2_io::extension_annotations_from_v2_map(self.annotations, message)?;
let decision_variables = self
.decision_variables
.ok_or(RawParseError::MissingField {
message,
field: "decision_variables",
})?
.parse_as(&(), message, "decision_variables")?;
let objectives = self
.objectives
.ok_or(RawParseError::MissingField {
message,
field: "objectives",
})?
.parse_as(&(), message, "objectives")?;
crate::v2_io::validate_sampled_f64_values(&objectives, message, "objectives")?;
let constraints = self
.sampled_regular_constraints
.map(|value| value.parse_as(&feasibility_atol, message, "sampled_regular_constraints"))
.transpose()?
.unwrap_or_default();
let indicator_constraints = self
.sampled_indicator_constraints
.map(|value| {
value.parse_as(&feasibility_atol, message, "sampled_indicator_constraints")
})
.transpose()?
.unwrap_or_default();
let one_hot_constraints = self
.sampled_one_hot_constraints
.map(|value| value.parse_as(&feasibility_atol, message, "sampled_one_hot_constraints"))
.transpose()?
.unwrap_or_default();
let sos1_constraints = self
.sampled_sos1_constraints
.map(|value| value.parse_as(&feasibility_atol, message, "sampled_sos1_constraints"))
.transpose()?
.unwrap_or_default();
let named_functions = self
.sampled_named_functions
.map(|value| value.parse_as(&(), message, "sampled_named_functions"))
.transpose()?
.unwrap_or_default();
let sense = crate::v2_io::parse_v2_required_sense(self.sense, message)?;
let objective_sample_ids = objectives.ids();
for sampled_dv in decision_variables.values() {
if !sampled_dv.samples().has_same_ids(&objective_sample_ids) {
return Err(
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_dv.samples().ids(),
})
.context(message, "decision_variables"),
);
}
}
constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| {
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})
.context(message, "sampled_regular_constraints")
})?;
indicator_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| {
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})
.context(message, "sampled_indicator_constraints")
})?;
one_hot_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| {
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})
.context(message, "sampled_one_hot_constraints")
})?;
sos1_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| {
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})
.context(message, "sampled_sos1_constraints")
})?;
let decision_variable_ids = decision_variables.keys().copied().collect::<BTreeSet<_>>();
validate_sampled_constraint_used_ids("regular", &constraints, &decision_variable_ids)
.map_err(|e| ParseError::new(e).context(message, "sampled_regular_constraints"))?;
validate_sampled_constraint_used_ids(
"indicator",
&indicator_constraints,
&decision_variable_ids,
)
.map_err(|e| ParseError::new(e).context(message, "sampled_indicator_constraints"))?;
validate_sampled_constraint_used_ids(
"one-hot",
&one_hot_constraints,
&decision_variable_ids,
)
.map_err(|e| ParseError::new(e).context(message, "sampled_one_hot_constraints"))?;
validate_sampled_constraint_used_ids("SOS1", &sos1_constraints, &decision_variable_ids)
.map_err(|e| ParseError::new(e).context(message, "sampled_sos1_constraints"))?;
validate_sampled_indicator_structural_ids(
&indicator_constraints,
&decision_variables,
message,
)?;
validate_sampled_one_hot_structural_ids(
&one_hot_constraints,
&decision_variables,
message,
)?;
validate_sampled_sos1_structural_ids(&sos1_constraints, &decision_variables, message)?;
validate_sampled_indicator_stage_values(
&decision_variables,
&indicator_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "sampled_indicator_constraints"))?;
validate_sampled_one_hot_stage_values(
&decision_variables,
&one_hot_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "sampled_one_hot_constraints"))?;
validate_sampled_sos1_stage_values(
&decision_variables,
&sos1_constraints,
feasibility_atol,
)
.map_err(|e| ParseError::new(e).context(message, "sampled_sos1_constraints"))?;
for (named_function_id, sampled_named_function) in named_functions.iter() {
if !sampled_named_function
.evaluated_values()
.has_same_ids(&objective_sample_ids)
{
return Err(
ParseError::new(crate::SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_named_function.evaluated_values().ids(),
})
.context(message, "sampled_named_functions"),
);
}
for var_id in sampled_named_function.used_decision_variable_ids() {
if !decision_variables.contains_key(var_id) {
return Err(ParseError::new(
crate::SampleSetError::UndefinedVariableInNamedFunction {
id: *var_id,
named_function_id: *named_function_id,
},
)
.context(message, "sampled_named_functions"));
}
}
}
let (computed_feasible, computed_feasible_relaxed) = SampleSetBuilder::compute_feasibility(
&constraints,
&indicator_constraints,
&one_hot_constraints,
&sos1_constraints,
&objective_sample_ids,
);
let feasible = crate::v2_io::sample_bool_map_from_v2(self.feasible);
validate_sample_bool_map_ids(&feasible, &objective_sample_ids, message, "feasible")?;
if let Some((sample_id, provided_feasible, computed_feasible)) =
first_feasibility_mismatch(&feasible, &computed_feasible)
{
return Err(
ParseError::new(crate::SampleSetError::InconsistentFeasibility {
sample_id: sample_id.into_inner(),
provided_feasible,
computed_feasible,
})
.context(message, "feasible"),
);
}
let feasible_relaxed = crate::v2_io::sample_bool_map_from_v2(self.feasible_relaxed);
validate_sample_bool_map_ids(
&feasible_relaxed,
&objective_sample_ids,
message,
"feasible_relaxed",
)?;
if let Some((sample_id, provided_feasible_relaxed, computed_feasible_relaxed)) =
first_feasibility_mismatch(&feasible_relaxed, &computed_feasible_relaxed)
{
return Err(
ParseError::new(crate::SampleSetError::InconsistentFeasibilityRelaxed {
sample_id: sample_id.into_inner(),
provided_feasible_relaxed,
computed_feasible_relaxed,
})
.context(message, "feasible_relaxed"),
);
}
Ok(SampleSet {
decision_variables,
objectives,
constraints,
indicator_constraints,
one_hot_constraints,
sos1_constraints,
named_functions,
sense,
feasible,
feasible_relaxed,
feasibility_atol,
metadata: self.metadata,
annotations,
})
}
}
impl TryFrom<v2::SampleSet> for SampleSet {
type Error = ParseError;
fn try_from(value: v2::SampleSet) -> Result<Self, Self::Error> {
value.parse(&())
}
}
impl From<SampleSet> for crate::v1::SampleSet {
fn from(sample_set: SampleSet) -> Self {
let SampleSet {
decision_variables,
objectives,
constraints,
indicator_constraints: _,
one_hot_constraints: _,
sos1_constraints: _,
named_functions,
sense,
feasible,
feasible_relaxed,
feasibility_atol: _,
metadata,
annotations,
} = sample_set;
let decision_variables: Vec<crate::v1::SampledDecisionVariable> =
(&decision_variables).into();
let objectives = Some(objectives.into());
let constraints: Vec<crate::v1::SampledConstraint> = constraints.into();
let named_functions: Vec<crate::v1::SampledNamedFunction> = named_functions.into();
let sense = sense.into();
let feasible = feasible
.into_iter()
.map(|(sample_id, value)| (sample_id.into_inner(), value))
.collect();
let feasible_relaxed = feasible_relaxed
.into_iter()
.map(|(sample_id, value)| (sample_id.into_inner(), value))
.collect();
crate::v1::SampleSet {
decision_variables,
objectives,
constraints,
named_functions,
feasible_relaxed,
feasible,
sense,
format_version: crate::CURRENT_FORMAT_VERSION,
metadata,
annotations: crate::protobuf_extension_annotations(annotations),
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{v1, Parse};
fn parse_error_source(error: &ParseError) -> &(dyn std::error::Error + 'static) {
std::error::Error::source(error).expect("ParseError should expose its cause")
}
fn sampled_decision_variables(kind: crate::Kind) -> crate::SampledDecisionVariableTable {
let id = crate::VariableID::from(1);
let decision_variable = crate::DecisionVariable::new(
kind,
if kind == crate::Kind::Binary {
crate::Bound::of_binary()
} else {
crate::Bound::default()
},
crate::ATol::default(),
)
.unwrap();
let row = crate::SampledDecisionVariable::new(
id,
decision_variable,
crate::Sampled::from((crate::SampleID::from(0), 0.0)),
)
.unwrap();
crate::SampledDecisionVariableTable::new(
BTreeMap::from([(id, row)]),
crate::VariableLabelStore::default(),
)
.unwrap()
}
fn empty_v2_sample_set() -> v2::SampleSet {
let sample_id = crate::SampleID::from(0);
SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.sense(crate::Sense::Minimize)
.build()
.unwrap()
.into()
}
fn v2_sample_set_with_indicator_constraint() -> v2::SampleSet {
use crate::indicator_constraint::{IndicatorSampledData, SampledIndicatorConstraint};
let variable_id = crate::VariableID::from(1);
let sample_id = crate::SampleID::from(0);
let decision_variable = crate::SampledDecisionVariable::new(
variable_id,
crate::DecisionVariable::binary(),
crate::Sampled::from((sample_id, 1.0)),
)
.unwrap();
let constraint = SampledIndicatorConstraint {
indicator_variable: variable_id,
equality: crate::Equality::EqualToZero,
stage: IndicatorSampledData {
evaluated_values: crate::Sampled::from((sample_id, 0.0)),
feasible: BTreeMap::from([(sample_id, true)]),
indicator_active: BTreeMap::from([(sample_id, true)]),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
SampleSet::builder()
.decision_variables(BTreeMap::from([(variable_id, decision_variable)]))
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.indicator_constraints(BTreeMap::from([(
crate::IndicatorConstraintID::from(1),
constraint,
)]))
.sense(crate::Sense::Minimize)
.build()
.unwrap()
.into()
}
fn v2_sample_set_with_one_hot_constraint() -> v2::SampleSet {
use crate::one_hot_constraint::{OneHotSampledData, SampledOneHotConstraint};
let variable_id = crate::VariableID::from(1);
let sample_id = crate::SampleID::from(0);
let decision_variable = crate::SampledDecisionVariable::new(
variable_id,
crate::DecisionVariable::binary(),
crate::Sampled::from((sample_id, 1.0)),
)
.unwrap();
let constraint = SampledOneHotConstraint {
variables: BTreeSet::from([variable_id]),
stage: OneHotSampledData {
feasible: BTreeMap::from([(sample_id, true)]),
active_variable: BTreeMap::from([(sample_id, Some(variable_id))]),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
SampleSet::builder()
.decision_variables(BTreeMap::from([(variable_id, decision_variable)]))
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.one_hot_constraints(BTreeMap::from([(
crate::OneHotConstraintID::from(1),
constraint,
)]))
.sense(crate::Sense::Minimize)
.build()
.unwrap()
.into()
}
fn v2_sample_set_with_sos1_constraint() -> v2::SampleSet {
use crate::sos1_constraint::{SampledSos1Constraint, Sos1SampledData};
let variable_id = crate::VariableID::from(1);
let sample_id = crate::SampleID::from(0);
let decision_variable = crate::SampledDecisionVariable::new(
variable_id,
crate::DecisionVariable::binary(),
crate::Sampled::from((sample_id, 0.0)),
)
.unwrap();
let constraint = SampledSos1Constraint {
variables: BTreeSet::from([variable_id]),
stage: Sos1SampledData {
feasible: BTreeMap::from([(sample_id, true)]),
active_variable: BTreeMap::from([(sample_id, None)]),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
SampleSet::builder()
.decision_variables(BTreeMap::from([(variable_id, decision_variable)]))
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.sos1_constraints(BTreeMap::from([(
crate::Sos1ConstraintID::from(1),
constraint,
)]))
.sense(crate::Sense::Minimize)
.build()
.unwrap()
.into()
}
fn assert_undefined_variable_in_constraint(
error: &ParseError,
expected_variable_id: crate::VariableID,
expected_family: &str,
expected_constraint_id: &str,
) {
assert!(matches!(
parse_error_source(error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::UndefinedVariableInConstraint {
id,
constraint_family,
constraint_id,
}) if *id == expected_variable_id
&& *constraint_family == expected_family
&& constraint_id == expected_constraint_id
));
}
fn assert_invalid_constraint_structure(
error: &ParseError,
family: &str,
constraint_id: &str,
message: &str,
) {
assert!(matches!(
parse_error_source(error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::InvalidConstraintStructure {
constraint_family,
constraint_id: actual_constraint_id,
message: actual_message,
}) if *constraint_family == family
&& actual_constraint_id == constraint_id
&& actual_message == message
));
}
#[test]
fn invalid_sample_set_sidecar_preserves_sample_set_error() {
let constraint_id = crate::ConstraintID::from(7);
let mut context = crate::ConstraintContextStore::default();
context.set_name(constraint_id, "orphan");
let source = crate::constraint_type::SampledCollection::<crate::Constraint>::with_context(
BTreeMap::new(),
BTreeMap::new(),
context,
)
.unwrap_err();
let error = invalid_sample_set_sidecar(source, "ommx.v1.SampleSet", "constraints");
assert!(matches!(
parse_error_source(&error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::InvalidSidecar { message })
if message == "Constraint label/provenance references unknown constraint ID ConstraintID(7)"
));
assert_eq!(error.context.len(), 1);
assert_eq!(error.context[0].message, "ommx.v1.SampleSet");
assert_eq!(error.context[0].field, "constraints");
}
#[test]
fn sampled_indicator_structural_checks_preserve_sample_set_error() {
use crate::indicator_constraint::{IndicatorSampledData, SampledIndicatorConstraint};
let variable_id = crate::VariableID::from(1);
let constraint_id = crate::IndicatorConstraintID::from(3);
let constraint = SampledIndicatorConstraint {
indicator_variable: crate::VariableID::from(2),
equality: crate::Equality::EqualToZero,
stage: IndicatorSampledData {
evaluated_values: crate::Sampled::from((crate::SampleID::from(0), 0.0)),
feasible: BTreeMap::from([(crate::SampleID::from(0), true)]),
indicator_active: BTreeMap::from([(crate::SampleID::from(0), false)]),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
};
let constraints = crate::SampledCollection::new(
BTreeMap::from([(constraint_id, constraint)]),
BTreeMap::new(),
)
.unwrap();
let binary = sampled_decision_variables(crate::Kind::Binary);
let error =
validate_sampled_indicator_structural_ids(&constraints, &binary, "ommx.v2.SampleSet")
.unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(3)",
"indicator variable VariableID(2) is not in decision_variables",
);
let mut constraint = constraints.inner().get(&constraint_id).unwrap().clone();
constraint.indicator_variable = variable_id;
let constraints = crate::SampledCollection::new(
BTreeMap::from([(constraint_id, constraint)]),
BTreeMap::new(),
)
.unwrap();
let continuous = sampled_decision_variables(crate::Kind::Continuous);
let error = validate_sampled_indicator_structural_ids(
&constraints,
&continuous,
"ommx.v2.SampleSet",
)
.unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(3)",
"indicator variable VariableID(1) must be binary",
);
}
#[test]
fn sampled_one_hot_structural_checks_preserve_sample_set_error() {
use crate::one_hot_constraint::{OneHotSampledData, SampledOneHotConstraint};
let variable_id = crate::VariableID::from(1);
let constraint_id = crate::OneHotConstraintID::from(4);
let make_constraints = |structural_id| {
crate::SampledCollection::new(
BTreeMap::from([(
constraint_id,
SampledOneHotConstraint {
variables: [structural_id].into_iter().collect(),
stage: OneHotSampledData {
feasible: BTreeMap::from([(crate::SampleID::from(0), true)]),
active_variable: BTreeMap::from([(
crate::SampleID::from(0),
Some(structural_id),
)]),
used_decision_variable_ids: [variable_id].into_iter().collect(),
},
},
)]),
BTreeMap::new(),
)
.unwrap()
};
let error = validate_sampled_one_hot_structural_ids(
&make_constraints(crate::VariableID::from(2)),
&sampled_decision_variables(crate::Kind::Binary),
"ommx.v2.SampleSet",
)
.unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(4)",
"variable VariableID(2) is not in decision_variables",
);
let error = validate_sampled_one_hot_structural_ids(
&make_constraints(variable_id),
&sampled_decision_variables(crate::Kind::Continuous),
"ommx.v2.SampleSet",
)
.unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(4)",
"variable VariableID(1) must be binary",
);
}
#[test]
fn sampled_sos1_structural_check_preserves_sample_set_error() {
use crate::sos1_constraint::{SampledSos1Constraint, Sos1SampledData};
let constraint_id = crate::Sos1ConstraintID::from(5);
let undefined_id = crate::VariableID::from(2);
let constraint = SampledSos1Constraint {
variables: [undefined_id].into_iter().collect(),
stage: Sos1SampledData {
feasible: BTreeMap::from([(crate::SampleID::from(0), true)]),
active_variable: BTreeMap::from([(crate::SampleID::from(0), None)]),
used_decision_variable_ids: [crate::VariableID::from(1)].into_iter().collect(),
},
};
let constraints = crate::SampledCollection::new(
BTreeMap::from([(constraint_id, constraint)]),
BTreeMap::new(),
)
.unwrap();
let error = validate_sampled_sos1_structural_ids(
&constraints,
&sampled_decision_variables(crate::Kind::Continuous),
"ommx.v2.SampleSet",
)
.unwrap_err();
assert_invalid_constraint_structure(
&error,
"SOS1",
"Sos1ConstraintID(5)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_used_id_in_regular_constraint() {
let mut proto = empty_v2_sample_set();
proto
.sampled_regular_constraints
.as_mut()
.unwrap()
.entries
.insert(
7,
v2::SampledRegularConstraint {
equality: v1::Equality::EqualToZero as i32,
evaluated_values: Some(v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 0.0,
}],
}),
feasible: BTreeMap::from([(0, true)]),
used_decision_variable_ids: vec![42],
dual_variables: None,
},
);
let error = SampleSet::try_from(proto).unwrap_err();
assert_undefined_variable_in_constraint(
&error,
crate::VariableID::from(42),
"regular",
"ConstraintID(7)",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_used_id_in_indicator_constraint() {
let mut proto = v2_sample_set_with_indicator_constraint();
proto
.sampled_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = SampleSet::try_from(proto).unwrap_err();
assert_undefined_variable_in_constraint(
&error,
crate::VariableID::from(2),
"indicator",
"IndicatorConstraintID(1)",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_used_id_in_one_hot_constraint() {
let mut proto = v2_sample_set_with_one_hot_constraint();
proto
.sampled_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = SampleSet::try_from(proto).unwrap_err();
assert_undefined_variable_in_constraint(
&error,
crate::VariableID::from(2),
"one-hot",
"OneHotConstraintID(1)",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_used_id_in_sos1_constraint() {
let mut proto = v2_sample_set_with_sos1_constraint();
proto
.sampled_sos1_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.used_decision_variable_ids
.push(2);
let error = SampleSet::try_from(proto).unwrap_err();
assert_undefined_variable_in_constraint(
&error,
crate::VariableID::from(2),
"SOS1",
"Sos1ConstraintID(1)",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_indicator_variable() {
let mut proto = v2_sample_set_with_indicator_constraint();
proto
.sampled_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.indicator_variable = 2;
let error = SampleSet::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(1)",
"indicator variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_non_binary_indicator_variable() {
let mut proto = v2_sample_set_with_indicator_constraint();
proto
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.kind = v1::decision_variable::Kind::Continuous as i32;
let error = SampleSet::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"indicator",
"IndicatorConstraintID(1)",
"indicator variable VariableID(1) must be binary",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_one_hot_variable() {
let mut proto = v2_sample_set_with_one_hot_constraint();
let row = proto
.sampled_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.variables = vec![2];
row.active_variable.insert(
0,
v2::SampledActiveVariable {
variable_id: Some(2),
},
);
let error = SampleSet::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_non_binary_one_hot_variable() {
let mut proto = v2_sample_set_with_one_hot_constraint();
proto
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.kind = v1::decision_variable::Kind::Continuous as i32;
let error = SampleSet::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"one-hot",
"OneHotConstraintID(1)",
"variable VariableID(1) must be binary",
);
}
#[test]
fn test_v2_sample_set_parse_classifies_undefined_sos1_variable() {
let mut proto = v2_sample_set_with_sos1_constraint();
proto
.sampled_sos1_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.variables = vec![2];
let error = SampleSet::try_from(proto).unwrap_err();
assert_invalid_constraint_structure(
&error,
"SOS1",
"Sos1ConstraintID(1)",
"variable VariableID(2) is not in decision_variables",
);
}
#[test]
fn test_sample_set_parse_rejects_reserved_annotation_key() {
let v1_sample_set = v1::SampleSet {
annotations: std::collections::HashMap::from([(
format!("{}.solver", crate::annotation_keys::SAMPLE_SET_NAMESPACE),
"bad".to_string(),
)]),
..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[annotations]
Annotation key `org.ommx.v1.sample-set.solver` is reserved for OMMX metadata and cannot be stored in extension annotations.
"###);
}
#[test]
fn test_sample_set_to_v1_bytes_filters_reserved_annotation_key() {
let mut sample_set: SampleSet = v1::SampleSet {
objectives: Some(v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 1.0,
}],
}),
sense: v1::instance::Sense::Minimize as i32,
..Default::default()
}
.parse(&())
.unwrap();
let reserved_key = format!("{}.solver", crate::annotation_keys::SAMPLE_SET_NAMESPACE);
sample_set.annotations = std::collections::HashMap::from([
(reserved_key.clone(), "invalid extension solver".to_string()),
("org.example.owner".to_string(), "domain".to_string()),
]);
let restored = SampleSet::from_v1_bytes(&sample_set.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_sample_set_parse() {
let v1_sample_set = v1::SampleSet {
decision_variables: vec![v1::SampledDecisionVariable {
decision_variable: Some(v1::DecisionVariable {
id: 1,
name: Some("x1".to_string()),
kind: v1::decision_variable::Kind::Continuous as i32,
bound: Some(v1::Bound {
lower: 0.0,
upper: 10.0,
}),
..Default::default()
}),
samples: Some(v1::SampledValues {
entries: vec![
v1::sampled_values::SampledValuesEntry {
ids: vec![0, 1],
value: 1.0,
},
v1::sampled_values::SampledValuesEntry {
ids: vec![2],
value: 2.0,
},
],
}),
}],
objectives: Some(v1::SampledValues {
entries: vec![
v1::sampled_values::SampledValuesEntry {
ids: vec![0, 1],
value: 10.0,
},
v1::sampled_values::SampledValuesEntry {
ids: vec![2],
value: 20.0,
},
],
}),
constraints: vec![],
feasible_relaxed: [(0, true), (1, true), (2, true)].iter().cloned().collect(),
feasible: [(0, true), (1, true), (2, true)].iter().cloned().collect(),
sense: v1::instance::Sense::Minimize as i32,
..Default::default()
};
let parsed: SampleSet = v1_sample_set.parse(&()).unwrap();
assert_eq!(parsed.sense(), &crate::Sense::Minimize);
assert_eq!(parsed.decision_variables().len(), 1);
assert_eq!(parsed.constraints().len(), 0);
let sample_id_0 = crate::SampleID::from(0);
let sample_id_1 = crate::SampleID::from(1);
let sample_id_2 = crate::SampleID::from(2);
assert!(parsed.is_sample_feasible(sample_id_0).unwrap());
assert!(parsed.is_sample_feasible(sample_id_1).unwrap());
assert!(parsed.is_sample_feasible(sample_id_2).unwrap());
assert!(parsed.is_sample_feasible_relaxed(sample_id_0).unwrap());
assert!(parsed.is_sample_feasible_relaxed(sample_id_1).unwrap());
assert!(parsed.is_sample_feasible_relaxed(sample_id_2).unwrap());
let unknown_sample_id = crate::SampleID::from(999);
assert!(parsed.is_sample_feasible(unknown_sample_id).is_none());
assert!(parsed
.is_sample_feasible_relaxed(unknown_sample_id)
.is_none());
let v1_converted: v1::SampleSet = parsed.into();
assert_eq!(v1_converted.sense, v1::instance::Sense::Minimize as i32);
assert_eq!(v1_converted.decision_variables.len(), 1);
}
#[test]
fn test_unknown_sense_enum_value() {
let v1_sample_set = v1::SampleSet {
objectives: Some(v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 10.0,
}],
}),
sense: 999, ..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[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_inconsistent_feasibility_validation() {
use crate::v1;
let v1_sample_set = v1::SampleSet {
decision_variables: vec![],
objectives: Some(v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 10.0,
}],
}),
constraints: vec![v1::SampledConstraint {
equality: v1::Equality::EqualToZero as i32,
evaluated_values: Some(v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 1.0, }],
}),
feasible: [(0, false)].iter().cloned().collect(), ..Default::default()
}],
feasible: [(0, true)].iter().cloned().collect(), feasible_relaxed: [(0, true)].iter().cloned().collect(),
sense: v1::instance::Sense::Minimize as i32,
..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
let error = result.unwrap_err();
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[feasible]
Inconsistent feasibility for sample 0: provided=true, computed=false
"###);
}
#[test]
fn from_v1_bytes_rejects_duplicated_sampled_constraint_ids() {
let sampled_values = v1::SampledValues {
entries: vec![v1::sampled_values::SampledValuesEntry {
ids: vec![0],
value: 0.0,
}],
};
let first = v1::SampledConstraint {
id: 7,
equality: v1::Equality::EqualToZero as i32,
evaluated_values: Some(sampled_values.clone()),
feasible: std::collections::HashMap::from([(0, true)]),
removed_reason: Some("removed first row".to_string()),
..Default::default()
};
let second = v1::SampledConstraint {
id: 7,
equality: v1::Equality::EqualToZero as i32,
evaluated_values: Some(sampled_values.clone()),
feasible: std::collections::HashMap::from([(0, true)]),
name: Some("active second row".to_string()),
..Default::default()
};
let proto = v1::SampleSet {
objectives: Some(sampled_values),
constraints: vec![first, second],
sense: v1::instance::Sense::Minimize as i32,
..Default::default()
};
let error = SampleSet::from_v1_bytes(&crate::Message::encode_to_vec(&proto)).unwrap_err();
let parse_error = error
.downcast_ref::<ParseError>()
.expect("semantic byte decoding must retain ParseError as the outer owner");
assert!(matches!(
parse_error_source(parse_error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::InvalidConstraintStructure {
constraint_family: "regular",
constraint_id,
message,
}) if constraint_id == "ConstraintID(7)"
&& message == "duplicated constraint ID in constraints"
));
assert_eq!(parse_error.context.len(), 1);
assert_eq!(parse_error.context[0].message, "ommx.v1.SampleSet");
assert_eq!(parse_error.context[0].field, "constraints");
}
#[test]
fn test_sample_set_parse_rejects_future_format_version() {
let v1_sample_set = v1::SampleSet {
format_version: 1,
..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
insta::assert_snapshot!(result.unwrap_err().to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[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_sample_set_parse_fails_with_duplicated_variable_id() {
let sample_id = crate::SampleID::from(0);
let v1_sampled_dv = crate::v1::SampledDecisionVariable {
decision_variable: Some(crate::v1::DecisionVariable {
id: 1,
kind: crate::v1::decision_variable::Kind::Continuous as i32,
bound: Some(crate::v1::Bound {
lower: 0.0,
upper: 10.0,
}),
..Default::default()
}),
samples: Some(crate::v1::SampledValues {
entries: vec![crate::v1::sampled_values::SampledValuesEntry {
ids: vec![sample_id.into_inner()],
value: 2.0,
}],
}),
};
let v1_sample_set = crate::v1::SampleSet {
decision_variables: vec![v1_sampled_dv.clone(), v1_sampled_dv],
objectives: Some(crate::v1::SampledValues {
entries: vec![crate::v1::sampled_values::SampledValuesEntry {
ids: vec![sample_id.into_inner()],
value: 0.0,
}],
}),
sense: crate::v1::instance::Sense::Minimize as i32,
..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
let error = result.unwrap_err();
assert!(matches!(
parse_error_source(&error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::DuplicatedVariableID { id })
if *id == crate::VariableID::from(1)
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[decision_variables]
Duplicated variable ID is found in definition: VariableID(1)
"###);
}
#[test]
fn test_sample_set_parse_fails_with_duplicated_named_function_id() {
let sample_id = crate::SampleID::from(0);
let sampled_values = crate::v1::SampledValues {
entries: vec![crate::v1::sampled_values::SampledValuesEntry {
ids: vec![sample_id.into_inner()],
value: 1.0,
}],
};
let v1_sample_set = crate::v1::SampleSet {
objectives: Some(crate::v1::SampledValues {
entries: vec![crate::v1::sampled_values::SampledValuesEntry {
ids: vec![sample_id.into_inner()],
value: 0.0,
}],
}),
named_functions: vec![
crate::v1::SampledNamedFunction {
id: 7,
evaluated_values: Some(sampled_values.clone()),
..Default::default()
},
crate::v1::SampledNamedFunction {
id: 7,
evaluated_values: Some(sampled_values),
..Default::default()
},
],
sense: crate::v1::instance::Sense::Minimize as i32,
..Default::default()
};
let result: Result<SampleSet, ParseError> = v1_sample_set.parse(&());
let error = result.unwrap_err();
assert!(matches!(
parse_error_source(&error).downcast_ref::<crate::SampleSetError>(),
Some(crate::SampleSetError::DuplicatedNamedFunctionID { id })
if *id == crate::NamedFunctionID::from(7)
));
insta::assert_snapshot!(error.to_string(), @r###"
Traceback for OMMX Message parse error:
└─ommx.v1.SampleSet[named_functions]
Duplicated named function ID is found in definition: NamedFunctionID(7)
"###);
}
#[test]
fn test_sample_set_roundtrip_preserves_labels_and_context() {
use crate::constraint::SampledData;
use crate::{
ConstraintID, DecisionVariable, Equality, NamedFunctionID, SampleID,
SampledDecisionVariable, Sense, VariableID,
};
use std::collections::BTreeMap;
let var_id = VariableID::from(1);
let cid = ConstraintID::from(10);
let nf_id = NamedFunctionID::from(0);
let sample_id = SampleID::from(0);
let dv = DecisionVariable::binary();
let mut x_samples = crate::Sampled::default();
x_samples.append([sample_id], 1.0).unwrap();
let mut decision_variables = BTreeMap::new();
decision_variables.insert(
var_id,
SampledDecisionVariable::new(var_id, dv, x_samples).unwrap(),
);
let mut variable_labels = crate::VariableLabelStore::default();
variable_labels.set_name(var_id, "x");
variable_labels.set_subscripts(var_id, vec![0]);
let mut evaluated_values = crate::Sampled::default();
evaluated_values.append([sample_id], 0.0).unwrap();
let mut feasible = BTreeMap::new();
feasible.insert(sample_id, true);
let sampled_constraint = crate::Constraint {
equality: Equality::EqualToZero,
stage: SampledData {
evaluated_values,
dual_variables: None,
feasible,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let mut constraints_map = BTreeMap::new();
constraints_map.insert(cid, sampled_constraint);
let mut constraint_context = crate::ConstraintContextStore::<ConstraintID>::default();
constraint_context.set_name(cid, "balance");
constraint_context.set_description(cid, "demand-balance row");
let constraints = crate::constraint_type::SampledCollection::with_context(
constraints_map,
BTreeMap::new(),
constraint_context,
)
.unwrap();
let sampled_nf = {
use crate::parse::Parse as _;
let v1_snf = crate::v1::SampledNamedFunction {
id: nf_id.into_inner(),
evaluated_values: Some(crate::v1::SampledValues {
entries: vec![crate::v1::sampled_values::SampledValuesEntry {
ids: vec![sample_id.into_inner()],
value: 1.0,
}],
}),
used_decision_variable_ids: vec![var_id.into_inner()],
..Default::default()
};
let parsed: crate::named_function::parse::ParsedSampledNamedFunction =
v1_snf.parse(&()).unwrap();
parsed.sampled_named_function
};
let mut named_functions = BTreeMap::new();
named_functions.insert(nf_id, sampled_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 mut objectives = crate::Sampled::default();
objectives.append([sample_id], 1.0).unwrap();
let sample_set = SampleSet::builder()
.decision_variables(decision_variables)
.variable_labels(variable_labels)
.objectives(objectives)
.constraints_collection(constraints)
.named_functions(named_functions)
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build()
.unwrap();
let bytes = sample_set.to_v1_bytes();
let recovered = SampleSet::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.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_sample_set_parse_rejects_inconsistent_regular_feasibility() {
use crate::{
constraint::SampledData, Constraint, ConstraintID, Equality, SampleID, Sampled, Sense,
};
use std::collections::BTreeMap;
let sample_id = SampleID::from(0);
let mut objectives = Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let mut evaluated_values = Sampled::default();
evaluated_values.append([sample_id], 0.0).unwrap();
let constraint = Constraint {
equality: Equality::EqualToZero,
stage: SampledData {
evaluated_values,
feasible: BTreeMap::from([(sample_id, true)]),
used_decision_variable_ids: Default::default(),
dual_variables: None,
},
};
let sample_set = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(objectives)
.constraints(BTreeMap::from([(ConstraintID::from(1), constraint)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::SampleSet::from(sample_set);
let row = proto
.sampled_regular_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.evaluated_values
.as_mut()
.unwrap()
.entries
.first_mut()
.unwrap()
.value = 1.0;
row.feasible.insert(sample_id.into_inner(), true);
let err = SampleSet::try_from(proto).unwrap_err();
assert!(
err.to_string()
.contains("Inconsistent constraint feasibility"),
"unexpected error: {err}"
);
}
#[test]
fn v2_special_constraint_stage_validation_includes_the_atol_boundary() {
let atol = ATol::new(0.125).unwrap();
let mut indicator = v2_sample_set_with_indicator_constraint();
indicator.feasibility_atol = Some(*atol);
indicator
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.samples
.as_mut()
.unwrap()
.entries[0]
.value = 1.0 + *atol;
let indicator_row = indicator
.sampled_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
indicator_row.evaluated_values.as_mut().unwrap().entries[0].value = *atol;
indicator_row.feasible.insert(0, true);
indicator_row.indicator_active.insert(0, true);
SampleSet::try_from(indicator).unwrap();
let mut one_hot = v2_sample_set_with_one_hot_constraint();
one_hot.feasibility_atol = Some(*atol);
one_hot
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap()
.samples
.as_mut()
.unwrap()
.entries[0]
.value = 1.0 + *atol;
SampleSet::try_from(one_hot).unwrap();
let mut sos1 = v2_sample_set_with_sos1_constraint();
sos1.feasibility_atol = Some(*atol);
let sos1_variable = sos1
.decision_variables
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
sos1_variable.kind = v1::decision_variable::Kind::Continuous as i32;
sos1_variable.samples.as_mut().unwrap().entries[0].value = *atol;
SampleSet::try_from(sos1).unwrap();
}
#[test]
fn test_v2_sample_set_parse_rejects_non_finite_objective() {
use crate::{SampleID, Sampled, Sense};
use std::collections::BTreeMap;
let sample_id = SampleID::from(0);
let mut objectives = Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let sample_set = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(objectives)
.constraints(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::SampleSet::from(sample_set);
proto
.objectives
.as_mut()
.unwrap()
.entries
.first_mut()
.unwrap()
.value = f64::INFINITY;
let err = SampleSet::try_from(proto).unwrap_err();
assert!(
err.to_string().contains("objectives must be finite")
&& err.to_string().contains("SampleID(0)"),
"unexpected error: {err}"
);
}
#[test]
fn test_v2_sample_set_parse_rejects_indicator_active_mismatching_variable_value() {
use crate::{
indicator_constraint::{IndicatorSampledData, SampledIndicatorConstraint},
DecisionVariable, IndicatorConstraintID, SampleID, Sampled, SampledDecisionVariable,
Sense, VariableID,
};
use std::collections::BTreeMap;
let var_id = VariableID::from(1);
let sample_id = SampleID::from(0);
let decision_variable = DecisionVariable::binary();
let sampled_variable = SampledDecisionVariable::new(
var_id,
decision_variable,
Sampled::from((sample_id, 1.0)),
)
.unwrap();
let indicator = SampledIndicatorConstraint {
indicator_variable: var_id,
equality: crate::Equality::EqualToZero,
stage: IndicatorSampledData {
evaluated_values: Sampled::from((sample_id, 0.0)),
feasible: BTreeMap::from([(sample_id, true)]),
indicator_active: BTreeMap::from([(sample_id, true)]),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let sample_set = SampleSet::builder()
.decision_variables(BTreeMap::from([(var_id, sampled_variable)]))
.objectives(Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.indicator_constraints(BTreeMap::from([(
IndicatorConstraintID::from(1),
indicator,
)]))
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::SampleSet::from(sample_set);
let row = proto
.sampled_indicator_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.indicator_active.insert(sample_id.into_inner(), false);
row.feasible.insert(sample_id.into_inner(), true);
let err = SampleSet::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_sample_set_parse_rejects_one_hot_active_variable_mismatching_values() {
use crate::{
one_hot_constraint::{OneHotSampledData, SampledOneHotConstraint},
DecisionVariable, OneHotConstraintID, SampleID, Sampled, SampledDecisionVariable,
Sense, VariableID,
};
use std::collections::{BTreeMap, BTreeSet};
let var_id = VariableID::from(1);
let sample_id = SampleID::from(0);
let decision_variable = DecisionVariable::binary();
let sampled_variable = SampledDecisionVariable::new(
var_id,
decision_variable,
Sampled::from((sample_id, 1.0)),
)
.unwrap();
let one_hot = SampledOneHotConstraint {
variables: BTreeSet::from([var_id]),
stage: OneHotSampledData {
feasible: BTreeMap::from([(sample_id, true)]),
active_variable: BTreeMap::from([(sample_id, Some(var_id))]),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let sample_set = SampleSet::builder()
.decision_variables(BTreeMap::from([(var_id, sampled_variable)]))
.objectives(Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.one_hot_constraints_collection(
crate::SampledCollection::new(
BTreeMap::from([(OneHotConstraintID::from(1), one_hot)]),
BTreeMap::new(),
)
.unwrap(),
)
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::SampleSet::from(sample_set);
let row = proto
.sampled_one_hot_constraints
.as_mut()
.unwrap()
.entries
.get_mut(&1)
.unwrap();
row.feasible.insert(sample_id.into_inner(), false);
row.active_variable.insert(
sample_id.into_inner(),
crate::v2::SampledActiveVariable { variable_id: None },
);
let err = SampleSet::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_sample_set_parse_rejects_unspecified_sense() {
use crate::{SampleID, Sampled, Sense};
use std::collections::BTreeMap;
let sample_id = SampleID::from(0);
let mut objectives = Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let sample_set = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(objectives)
.constraints(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap();
let mut proto = crate::v2::SampleSet::from(sample_set);
proto.sense = crate::v1::instance::Sense::Unspecified as i32;
let err = SampleSet::try_from(proto).unwrap_err();
assert!(
err.to_string()
.contains("Unknown or unsupported enum value")
&& err.to_string().contains("ommx.v2.SampleSet")
&& err.to_string().contains("sense"),
"unexpected error: {err}"
);
}
}