mod extract;
mod parse;
mod serialize;
use crate::{
constraint_type::{
ConstraintType, EvaluatedCollection, SampledCollection, SampledConstraintBehavior,
},
indicator_constraint::IndicatorConstraint,
ATol, Constraint, ConstraintID, EvaluatedConstraint, EvaluatedDecisionVariable,
EvaluatedNamedFunction, NamedFunctionID, NamedFunctionTable, SampleID, SampleIDSet, Sampled,
SampledConstraint, SampledDecisionVariable, SampledNamedFunction, Sense, Solution, VariableID,
};
use getset::Getters;
use std::collections::{BTreeMap, BTreeSet, HashMap};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum SampleSetError {
#[error("Inconsistent feasibility for sample {sample_id}: provided={provided_feasible}, computed={computed_feasible}")]
InconsistentFeasibility {
sample_id: u64,
provided_feasible: bool,
computed_feasible: bool,
},
#[error("Inconsistent feasibility (relaxed) for sample {sample_id}: provided={provided_feasible_relaxed}, computed={computed_feasible_relaxed}")]
InconsistentFeasibilityRelaxed {
sample_id: u64,
provided_feasible_relaxed: bool,
computed_feasible_relaxed: bool,
},
#[error("Inconsistent feasibility for {constraint_family} constraint {constraint_id} at sample {sample_id:?}: provided={provided_feasible}, computed={computed_feasible}")]
InconsistentConstraintFeasibility {
constraint_family: &'static str,
constraint_id: String,
sample_id: SampleID,
provided_feasible: bool,
computed_feasible: bool,
},
#[error("Invalid structure for {constraint_family} constraint {constraint_id}: {message}")]
InvalidConstraintStructure {
constraint_family: &'static str,
constraint_id: String,
message: String,
},
#[error("Inconsistent sample IDs: expected {expected:?}, found {found:?}")]
InconsistentSampleIDs {
expected: SampleIDSet,
found: SampleIDSet,
},
#[error("Duplicated variable ID is found in definition: {id:?}")]
DuplicatedVariableID { id: VariableID },
#[error("Duplicated named function ID is found in definition: {id:?}")]
DuplicatedNamedFunctionID { id: NamedFunctionID },
#[error("Duplicate subscripts for {name}: {subscripts:?}")]
DuplicateSubscripts { name: String, subscripts: Vec<i64> },
#[error("No decision variables with name '{name}' found")]
UnknownVariableName { name: String },
#[error("No constraint with name '{name}' found")]
UnknownConstraintName { name: String },
#[deprecated(
note = "Parameters are now ignored in extract_decision_variables and extract_all_decision_variables"
)]
#[error("Decision variable with parameters is not supported")]
ParameterizedVariable,
#[error("Constraint with parameters is not supported")]
ParameterizedConstraint,
#[error("Unknown sample ID: {id:?}")]
UnknownSampleID { id: SampleID },
#[error("No feasible solution found")]
NoFeasibleSolution,
#[error("No feasible solution found in relaxed problem")]
NoFeasibleSolutionRelaxed,
#[error("No named function with name '{name}' found")]
UnknownNamedFunctionName { name: String },
#[deprecated(
note = "Parameters are now allowed in extract methods; only subscripts are used as keys"
)]
#[error("Named function with parameters is not supported")]
ParameterizedNamedFunction,
#[error("Required field is missing: {field}")]
MissingRequiredField { field: &'static str },
#[error(
"Variable ID {id:?} used in {constraint_family} constraint {constraint_id} is not in decision_variables"
)]
UndefinedVariableInConstraint {
id: VariableID,
constraint_family: &'static str,
constraint_id: String,
},
#[error(
"Variable ID {id:?} used in named function {named_function_id:?} is not in decision_variables"
)]
UndefinedVariableInNamedFunction {
id: VariableID,
named_function_id: NamedFunctionID,
},
#[error("{message}")]
InvalidSidecar { message: String },
}
#[derive(Debug, Clone, Getters)]
pub struct SampleSet {
decision_variables: crate::SampledDecisionVariableTable,
#[getset(get = "pub")]
objectives: Sampled<f64>,
#[getset(get = "pub")]
constraints: SampledCollection<Constraint>,
#[getset(get = "pub")]
indicator_constraints: SampledCollection<IndicatorConstraint>,
#[getset(get = "pub")]
one_hot_constraints: SampledCollection<crate::OneHotConstraint>,
#[getset(get = "pub")]
sos1_constraints: SampledCollection<crate::Sos1Constraint>,
named_functions: NamedFunctionTable<SampledNamedFunction>,
#[getset(get = "pub")]
sense: Sense,
#[getset(get = "pub")]
feasible: BTreeMap<SampleID, bool>,
#[getset(get = "pub")]
feasible_relaxed: BTreeMap<SampleID, bool>,
#[getset(get_copy = "pub")]
feasibility_atol: ATol,
pub metadata: Option<crate::v1::ProcessMetadata>,
pub annotations: HashMap<String, String>,
}
fn validate_sampled_constraint_used_ids<T: ConstraintType>(
constraint_family: &'static str,
constraints: &SampledCollection<T>,
decision_variable_ids: &BTreeSet<VariableID>,
) -> Result<(), SampleSetError> {
constraints
.validate_used_decision_variable_ids(decision_variable_ids)
.map_err(
|(constraint_id, var_id)| SampleSetError::UndefinedVariableInConstraint {
id: var_id,
constraint_family,
constraint_id: format!("{constraint_id:?}"),
},
)
}
impl SampleSet {
#[deprecated(
since = "2.5.0",
note = "Use SampleSet::builder().build() for construction with named_functions support"
)]
pub fn new(
decision_variables: BTreeMap<VariableID, SampledDecisionVariable>,
objectives: Sampled<f64>,
constraints: BTreeMap<ConstraintID, SampledConstraint>,
sense: Sense,
) -> Result<Self, SampleSetError> {
Self::builder()
.decision_variables(decision_variables)
.objectives(objectives)
.constraints(constraints)
.sense(sense)
.build()
}
pub fn named_function_table(&self) -> &NamedFunctionTable<SampledNamedFunction> {
&self.named_functions
}
pub fn named_functions(&self) -> &BTreeMap<NamedFunctionID, SampledNamedFunction> {
self.named_functions.entries()
}
pub fn named_function_labels(&self) -> &crate::named_function::NamedFunctionLabelStore {
self.named_functions.labels()
}
pub fn decision_variable_table(&self) -> &crate::SampledDecisionVariableTable {
&self.decision_variables
}
pub fn decision_variables(&self) -> &BTreeMap<VariableID, SampledDecisionVariable> {
self.decision_variables.entries()
}
pub fn variable_labels(&self) -> &crate::decision_variable::VariableLabelStore {
self.decision_variables.labels()
}
pub fn sample_ids(&self) -> SampleIDSet {
self.objectives.ids()
}
pub fn feasible_ids(&self) -> SampleIDSet {
self.feasible
.iter()
.filter_map(|(id, &is_feasible)| if is_feasible { Some(*id) } else { None })
.collect()
}
pub fn feasible_relaxed_ids(&self) -> SampleIDSet {
self.feasible_relaxed
.iter()
.filter_map(|(id, &is_feasible)| if is_feasible { Some(*id) } else { None })
.collect()
}
pub fn feasible_unrelaxed_ids(&self) -> SampleIDSet {
self.feasible_ids()
}
pub fn is_sample_feasible(&self, sample_id: SampleID) -> Option<bool> {
self.feasible.get(&sample_id).copied()
}
pub fn is_sample_feasible_relaxed(&self, sample_id: SampleID) -> Option<bool> {
self.feasible_relaxed.get(&sample_id).copied()
}
pub fn get(&self, sample_id: crate::SampleID) -> Option<Solution> {
let objective = *self.objectives.get(sample_id)?;
let mut decision_variables: BTreeMap<VariableID, EvaluatedDecisionVariable> =
BTreeMap::default();
for (variable_id, sampled_dv) in &self.decision_variables {
let evaluated_dv = sampled_dv.get(*variable_id, sample_id)?;
decision_variables.insert(*variable_id, evaluated_dv);
}
let mut evaluated_constraints: BTreeMap<ConstraintID, EvaluatedConstraint> =
BTreeMap::default();
for (constraint_id, constraint) in self.constraints.iter() {
let evaluated_constraint = constraint.get(sample_id)?;
evaluated_constraints.insert(*constraint_id, evaluated_constraint);
}
let mut evaluated_indicator_constraints = BTreeMap::default();
for (constraint_id, constraint) in self.indicator_constraints.iter() {
use crate::constraint_type::SampledConstraintBehavior;
let evaluated = constraint.get(sample_id)?;
evaluated_indicator_constraints.insert(*constraint_id, evaluated);
}
let mut evaluated_one_hot_constraints = BTreeMap::default();
for (constraint_id, constraint) in self.one_hot_constraints.iter() {
use crate::constraint_type::SampledConstraintBehavior;
let evaluated = constraint.get(sample_id)?;
evaluated_one_hot_constraints.insert(*constraint_id, evaluated);
}
let mut evaluated_sos1_constraints = BTreeMap::default();
for (constraint_id, constraint) in self.sos1_constraints.iter() {
use crate::constraint_type::SampledConstraintBehavior;
let evaluated = constraint.get(sample_id)?;
evaluated_sos1_constraints.insert(*constraint_id, evaluated);
}
let mut evaluated_named_functions: BTreeMap<NamedFunctionID, EvaluatedNamedFunction> =
BTreeMap::default();
for (named_function_id, named_function) in self.named_functions.iter() {
let evaluated_named_function = named_function.get(sample_id)?;
evaluated_named_functions.insert(*named_function_id, evaluated_named_function);
}
let sense = *self.sense();
Some(unsafe {
Solution::builder()
.evaluated_constraints_collection(
EvaluatedCollection::with_context(
evaluated_constraints,
self.constraints.removed_reasons().clone(),
self.constraints.context().clone(),
)
.expect("SampleSet sidecars must reference constraints present in this sample"),
)
.evaluated_indicator_constraints_collection(
EvaluatedCollection::with_context(
evaluated_indicator_constraints,
self.indicator_constraints.removed_reasons().clone(),
self.indicator_constraints.context().clone(),
)
.expect("SampleSet sidecars must reference constraints present in this sample"),
)
.evaluated_one_hot_constraints_collection(
EvaluatedCollection::with_context(
evaluated_one_hot_constraints,
self.one_hot_constraints.removed_reasons().clone(),
self.one_hot_constraints.context().clone(),
)
.expect("SampleSet sidecars must reference constraints present in this sample"),
)
.evaluated_sos1_constraints_collection(
EvaluatedCollection::with_context(
evaluated_sos1_constraints,
self.sos1_constraints.removed_reasons().clone(),
self.sos1_constraints.context().clone(),
)
.expect("SampleSet sidecars must reference constraints present in this sample"),
)
.objective(objective)
.evaluated_named_functions(evaluated_named_functions)
.decision_variables(decision_variables)
.variable_labels(self.variable_labels().clone())
.named_function_labels(self.named_functions.labels().clone())
.sense(sense)
.feasibility_atol(self.feasibility_atol)
.build_unchecked()
.expect("SampleSet invariants guarantee Solution invariants")
})
}
pub fn best_feasible_id(&self) -> Result<SampleID, SampleSetError> {
let mut feasible_objectives: Vec<(SampleID, f64)> = self
.feasible
.iter()
.filter_map(|(k, v)| if *v { Some(k) } else { None })
.map(|id| (*id, *self.objectives.get(*id).unwrap())) .collect();
if feasible_objectives.is_empty() {
return Err(SampleSetError::NoFeasibleSolution);
}
feasible_objectives.sort_by(|a, b| a.1.total_cmp(&b.1));
match self.sense {
Sense::Minimize => Ok(feasible_objectives.first().unwrap().0),
Sense::Maximize => Ok(feasible_objectives.last().unwrap().0),
}
}
pub fn best_feasible_relaxed_id(&self) -> Result<SampleID, SampleSetError> {
let mut feasible_objectives: Vec<(SampleID, f64)> = self
.feasible_relaxed
.iter()
.filter_map(|(k, v)| if *v { Some(k) } else { None })
.map(|id| (*id, *self.objectives.get(*id).unwrap())) .collect();
if feasible_objectives.is_empty() {
return Err(SampleSetError::NoFeasibleSolutionRelaxed);
}
feasible_objectives.sort_by(|a, b| a.1.total_cmp(&b.1));
match self.sense {
Sense::Minimize => Ok(feasible_objectives.first().unwrap().0),
Sense::Maximize => Ok(feasible_objectives.last().unwrap().0),
}
}
pub fn best_feasible(&self) -> Result<Solution, SampleSetError> {
let id = self.best_feasible_id()?;
self.get(id).ok_or(SampleSetError::UnknownSampleID { id })
}
pub fn best_feasible_relaxed(&self) -> Result<Solution, SampleSetError> {
let id = self.best_feasible_relaxed_id()?;
self.get(id).ok_or(SampleSetError::UnknownSampleID { id })
}
pub fn builder() -> SampleSetBuilder {
SampleSetBuilder::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct SampleSetBuilder {
decision_variables: Option<BTreeMap<VariableID, SampledDecisionVariable>>,
variable_labels: crate::decision_variable::VariableLabelStore,
objectives: Option<Sampled<f64>>,
constraints: Option<SampledCollection<Constraint>>,
indicator_constraints: SampledCollection<IndicatorConstraint>,
one_hot_constraints: SampledCollection<crate::OneHotConstraint>,
sos1_constraints: SampledCollection<crate::Sos1Constraint>,
named_function_table: Option<NamedFunctionTable<SampledNamedFunction>>,
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
named_function_labels: crate::named_function::NamedFunctionLabelStore,
sense: Option<Sense>,
feasibility_atol: ATol,
}
fn expected_sampled_regular_constraint_feasible(
equality: crate::Equality,
evaluated_value: f64,
atol: ATol,
) -> bool {
match equality {
crate::Equality::EqualToZero => evaluated_value.abs() < *atol,
crate::Equality::LessThanOrEqualToZero => evaluated_value < *atol,
}
}
fn expected_sampled_indicator_constraint_feasible(
equality: crate::Equality,
evaluated_value: f64,
indicator_active: bool,
atol: ATol,
) -> bool {
if !indicator_active {
return true;
}
expected_sampled_regular_constraint_feasible(equality, evaluated_value, atol)
}
fn validate_sampled_constraint_feasibility(
regular_constraints: &SampledCollection<Constraint>,
indicator_constraints: &SampledCollection<IndicatorConstraint>,
one_hot_constraints: &SampledCollection<crate::OneHotConstraint>,
sos1_constraints: &SampledCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SampleSetError> {
for (id, constraint) in regular_constraints.inner() {
for (sample_id, provided_feasible) in &constraint.stage.feasible {
let Some(evaluated_value) = constraint.stage.evaluated_values.get(*sample_id) else {
continue;
};
let computed_feasible = expected_sampled_regular_constraint_feasible(
constraint.equality,
*evaluated_value,
atol,
);
if *provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "regular",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: *provided_feasible,
computed_feasible,
});
}
}
}
for (id, constraint) in indicator_constraints.inner() {
for (sample_id, provided_feasible) in &constraint.stage.feasible {
let (Some(evaluated_value), Some(indicator_active)) = (
constraint.stage.evaluated_values.get(*sample_id),
constraint.stage.indicator_active.get(sample_id),
) else {
continue;
};
let computed_feasible = expected_sampled_indicator_constraint_feasible(
constraint.equality,
*evaluated_value,
*indicator_active,
atol,
);
if *provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: *provided_feasible,
computed_feasible,
});
}
}
}
for (id, constraint) in one_hot_constraints.inner() {
for (sample_id, provided_feasible) in &constraint.stage.feasible {
let computed_feasible = constraint
.stage
.active_variable
.get(sample_id)
.is_some_and(Option::is_some);
if *provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: *provided_feasible,
computed_feasible,
});
}
}
}
for (id, constraint) in sos1_constraints.inner() {
for (sample_id, active_variable) in &constraint.stage.active_variable {
if active_variable.is_some()
&& constraint
.stage
.feasible
.get(sample_id)
.is_some_and(|feasible| !feasible)
{
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: false,
computed_feasible: true,
});
}
}
}
Ok(())
}
fn validate_sampled_special_constraint_structure(
decision_variables: &crate::SampledDecisionVariableTable,
indicator_constraints: &SampledCollection<IndicatorConstraint>,
one_hot_constraints: &SampledCollection<crate::OneHotConstraint>,
sos1_constraints: &SampledCollection<crate::Sos1Constraint>,
) -> Result<(), SampleSetError> {
for (id, constraint) in indicator_constraints.inner() {
let variable_id = constraint.indicator_variable;
let Some(variable) = decision_variables.get(&variable_id) else {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!("indicator variable {variable_id:?} is not in decision_variables"),
});
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!("indicator variable {variable_id:?} must be binary"),
});
}
}
for (id, constraint) in one_hot_constraints.inner() {
if constraint.variables.is_empty() {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: "one-hot constraints must contain at least one variable".to_string(),
});
}
for variable_id in &constraint.variables {
let Some(variable) = decision_variables.get(variable_id) else {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} is not in decision_variables"),
});
};
if *variable.kind() != crate::decision_variable::Kind::Binary {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} must be binary"),
});
}
}
for active_variable in constraint.stage.active_variable.values().flatten() {
if !constraint.variables.contains(active_variable) {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: "active_variable must be a member of variables".to_string(),
});
}
}
}
for (id, constraint) in sos1_constraints.inner() {
if constraint.variables.is_empty() {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: "SOS1 constraints must contain at least one variable".to_string(),
});
}
for variable_id in &constraint.variables {
if !decision_variables.contains_key(variable_id) {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: format!("variable {variable_id:?} is not in decision_variables"),
});
}
}
for active_variable in constraint.stage.active_variable.values().flatten() {
if !constraint.variables.contains(active_variable) {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: "active_variable must be a member of variables".to_string(),
});
}
}
}
Ok(())
}
fn expected_binary_activity(
variable_id: VariableID,
value: f64,
atol: ATol,
role: &'static str,
) -> Result<bool, String> {
if (value - 1.0).abs() < *atol {
Ok(true)
} else if value.abs() < *atol {
Ok(false)
} else {
Err(format!(
"{role} variable {variable_id:?} has value {value}, but must be 0 or 1",
))
}
}
fn expected_sampled_one_hot_active_variable(
variables: &BTreeSet<VariableID>,
decision_variables: &crate::SampledDecisionVariableTable,
sample_id: SampleID,
atol: ATol,
) -> (bool, Option<VariableID>) {
let mut active = None;
for variable_id in variables {
let value = *decision_variables
.get(variable_id)
.expect("one-hot structural variables must be validated first")
.samples()
.get(sample_id)
.expect("sample IDs must be validated first");
if (value - 1.0).abs() < *atol {
if active.is_some() {
return (false, None);
}
active = Some(*variable_id);
} else if value.abs() >= *atol {
return (false, None);
}
}
match active {
Some(variable_id) => (true, Some(variable_id)),
None => (false, None),
}
}
fn expected_sampled_sos1_active_variable(
variables: &BTreeSet<VariableID>,
decision_variables: &crate::SampledDecisionVariableTable,
sample_id: SampleID,
atol: ATol,
) -> (bool, Option<VariableID>) {
let mut active = None;
for variable_id in variables {
let value = *decision_variables
.get(variable_id)
.expect("SOS1 structural variables must be validated first")
.samples()
.get(sample_id)
.expect("sample IDs must be validated first");
if value.abs() >= *atol {
if active.is_some() {
return (false, None);
}
active = Some(*variable_id);
}
}
(true, active)
}
fn validate_sampled_indicator_stage_values(
decision_variables: &crate::SampledDecisionVariableTable,
indicator_constraints: &SampledCollection<IndicatorConstraint>,
atol: ATol,
) -> Result<(), SampleSetError> {
for (id, constraint) in indicator_constraints.inner() {
let variable = decision_variables
.get(&constraint.indicator_variable)
.expect("indicator structural variables must be validated first");
for (sample_id, provided_indicator_active) in &constraint.stage.indicator_active {
let value = *variable
.samples()
.get(*sample_id)
.expect("sample IDs must be validated first");
let computed_indicator_active =
expected_binary_activity(constraint.indicator_variable, value, atol, "indicator")
.map_err(|message| SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!("{message} at sample {sample_id:?}"),
})?;
if *provided_indicator_active != computed_indicator_active {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
message: format!(
"indicator_active={} at sample {sample_id:?} does not match indicator variable {:?} value {value}",
provided_indicator_active,
constraint.indicator_variable,
),
});
}
let evaluated_value = *constraint
.stage
.evaluated_values
.get(*sample_id)
.expect("sample IDs must be validated first");
let computed_feasible = expected_sampled_indicator_constraint_feasible(
constraint.equality,
evaluated_value,
computed_indicator_active,
atol,
);
let provided_feasible = *constraint
.stage
.feasible
.get(sample_id)
.expect("sample IDs must be validated first");
if provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "indicator",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible,
computed_feasible,
});
}
}
}
Ok(())
}
fn validate_sampled_one_hot_stage_values(
decision_variables: &crate::SampledDecisionVariableTable,
one_hot_constraints: &SampledCollection<crate::OneHotConstraint>,
atol: ATol,
) -> Result<(), SampleSetError> {
for (id, constraint) in one_hot_constraints.inner() {
for (sample_id, provided_feasible) in &constraint.stage.feasible {
let (computed_feasible, computed_active_variable) =
expected_sampled_one_hot_active_variable(
&constraint.variables,
decision_variables,
*sample_id,
atol,
);
let provided_active_variable = constraint
.stage
.active_variable
.get(sample_id)
.copied()
.flatten();
if provided_active_variable != computed_active_variable {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
message: format!(
"active_variable={provided_active_variable:?} at sample {sample_id:?} does not match decision-variable values; computed={computed_active_variable:?}",
),
});
}
if *provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "one-hot",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: *provided_feasible,
computed_feasible,
});
}
}
}
Ok(())
}
fn validate_sampled_sos1_stage_values(
decision_variables: &crate::SampledDecisionVariableTable,
sos1_constraints: &SampledCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SampleSetError> {
for (id, constraint) in sos1_constraints.inner() {
for (sample_id, provided_feasible) in &constraint.stage.feasible {
let (computed_feasible, computed_active_variable) =
expected_sampled_sos1_active_variable(
&constraint.variables,
decision_variables,
*sample_id,
atol,
);
let provided_active_variable = constraint
.stage
.active_variable
.get(sample_id)
.copied()
.flatten();
if provided_active_variable != computed_active_variable {
return Err(SampleSetError::InvalidConstraintStructure {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
message: format!(
"active_variable={provided_active_variable:?} at sample {sample_id:?} does not match decision-variable values; computed={computed_active_variable:?}",
),
});
}
if *provided_feasible != computed_feasible {
return Err(SampleSetError::InconsistentConstraintFeasibility {
constraint_family: "SOS1",
constraint_id: format!("{id:?}"),
sample_id: *sample_id,
provided_feasible: *provided_feasible,
computed_feasible,
});
}
}
}
Ok(())
}
fn validate_sampled_special_constraint_stage_values(
decision_variables: &crate::SampledDecisionVariableTable,
indicator_constraints: &SampledCollection<IndicatorConstraint>,
one_hot_constraints: &SampledCollection<crate::OneHotConstraint>,
sos1_constraints: &SampledCollection<crate::Sos1Constraint>,
atol: ATol,
) -> Result<(), SampleSetError> {
validate_sampled_indicator_stage_values(decision_variables, indicator_constraints, atol)?;
validate_sampled_one_hot_stage_values(decision_variables, one_hot_constraints, atol)?;
validate_sampled_sos1_stage_values(decision_variables, sos1_constraints, atol)
}
impl SampleSetBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn variable_labels(
mut self,
variable_labels: crate::decision_variable::VariableLabelStore,
) -> Self {
self.variable_labels = variable_labels;
self
}
pub fn decision_variables(
mut self,
decision_variables: BTreeMap<VariableID, SampledDecisionVariable>,
) -> Self {
self.decision_variables = Some(decision_variables);
self
}
pub fn objectives(mut self, objectives: Sampled<f64>) -> Self {
self.objectives = Some(objectives);
self
}
pub fn constraints(mut self, constraints: BTreeMap<ConstraintID, SampledConstraint>) -> Self {
self.constraints = Some(
SampledCollection::new(constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints"),
);
self
}
pub fn constraints_collection(mut self, constraints: SampledCollection<Constraint>) -> Self {
self.constraints = Some(constraints);
self
}
pub fn indicator_constraints(
mut self,
indicator_constraints: BTreeMap<
crate::IndicatorConstraintID,
crate::indicator_constraint::SampledIndicatorConstraint,
>,
) -> Self {
self.indicator_constraints = SampledCollection::new(indicator_constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints");
self
}
pub fn indicator_constraints_collection(
mut self,
indicator_constraints: SampledCollection<IndicatorConstraint>,
) -> Self {
self.indicator_constraints = indicator_constraints;
self
}
pub fn one_hot_constraints(
mut self,
one_hot_constraints: BTreeMap<
crate::OneHotConstraintID,
crate::one_hot_constraint::SampledOneHotConstraint,
>,
) -> Self {
self.one_hot_constraints = SampledCollection::new(one_hot_constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints");
self
}
pub fn one_hot_constraints_collection(
mut self,
one_hot_constraints: SampledCollection<crate::OneHotConstraint>,
) -> Self {
self.one_hot_constraints = one_hot_constraints;
self
}
pub fn sos1_constraints(
mut self,
sos1_constraints: BTreeMap<
crate::Sos1ConstraintID,
crate::sos1_constraint::SampledSos1Constraint,
>,
) -> Self {
self.sos1_constraints = SampledCollection::new(sos1_constraints, BTreeMap::new())
.expect("empty removed reasons cannot reference unknown constraints");
self
}
pub fn sos1_constraints_collection(
mut self,
sos1_constraints: SampledCollection<crate::Sos1Constraint>,
) -> Self {
self.sos1_constraints = sos1_constraints;
self
}
pub fn named_functions(
mut self,
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
) -> Self {
self.named_function_table = None;
self.named_functions = named_functions;
self
}
pub(crate) fn named_function_table(
mut self,
named_functions: NamedFunctionTable<SampledNamedFunction>,
) -> Self {
self.named_function_table = Some(named_functions);
self
}
pub fn named_function_labels(
mut self,
named_function_labels: crate::named_function::NamedFunctionLabelStore,
) -> Self {
self.named_function_table = None;
self.named_function_labels = named_function_labels;
self
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
self
}
pub fn feasibility_atol(mut self, feasibility_atol: ATol) -> Self {
self.feasibility_atol = feasibility_atol;
self
}
pub fn build(self) -> Result<SampleSet, SampleSetError> {
let decision_variables =
self.decision_variables
.ok_or(SampleSetError::MissingRequiredField {
field: "decision_variables",
})?;
let objectives = self
.objectives
.ok_or(SampleSetError::MissingRequiredField {
field: "objectives",
})?;
let constraints = self
.constraints
.ok_or(SampleSetError::MissingRequiredField {
field: "constraints",
})?;
let sense = self
.sense
.ok_or(SampleSetError::MissingRequiredField { field: "sense" })?;
let decision_variables =
crate::SampledDecisionVariableTable::new(decision_variables, self.variable_labels)
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?;
let decision_variable_ids = decision_variables.keys().copied().collect::<BTreeSet<_>>();
let named_functions = match self.named_function_table {
Some(named_functions) => named_functions,
None => NamedFunctionTable::new(self.named_functions, self.named_function_labels)
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?,
};
constraints
.validate_context_ids()
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?;
self.indicator_constraints
.validate_context_ids()
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?;
self.one_hot_constraints
.validate_context_ids()
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?;
self.sos1_constraints.validate_context_ids().map_err(|e| {
SampleSetError::InvalidSidecar {
message: e.to_string(),
}
})?;
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(SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_dv.samples().ids(),
});
}
}
constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})?;
self.indicator_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})?;
self.one_hot_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})?;
self.sos1_constraints
.validate_sample_ids(&objective_sample_ids)
.map_err(|found| SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found,
})?;
validate_sampled_constraint_feasibility(
&constraints,
&self.indicator_constraints,
&self.one_hot_constraints,
&self.sos1_constraints,
self.feasibility_atol,
)?;
validate_sampled_special_constraint_structure(
&decision_variables,
&self.indicator_constraints,
&self.one_hot_constraints,
&self.sos1_constraints,
)?;
validate_sampled_special_constraint_stage_values(
&decision_variables,
&self.indicator_constraints,
&self.one_hot_constraints,
&self.sos1_constraints,
self.feasibility_atol,
)?;
validate_sampled_constraint_used_ids("regular", &constraints, &decision_variable_ids)?;
validate_sampled_constraint_used_ids(
"indicator",
&self.indicator_constraints,
&decision_variable_ids,
)?;
validate_sampled_constraint_used_ids(
"one-hot",
&self.one_hot_constraints,
&decision_variable_ids,
)?;
validate_sampled_constraint_used_ids(
"SOS1",
&self.sos1_constraints,
&decision_variable_ids,
)?;
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(SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_named_function.evaluated_values().ids(),
});
}
for var_id in sampled_named_function.used_decision_variable_ids() {
if !decision_variables.contains_key(var_id) {
return Err(SampleSetError::UndefinedVariableInNamedFunction {
id: *var_id,
named_function_id: *named_function_id,
});
}
}
}
let (feasible, feasible_relaxed) = Self::compute_feasibility(
&constraints,
&self.indicator_constraints,
&self.one_hot_constraints,
&self.sos1_constraints,
&objective_sample_ids,
);
Ok(SampleSet {
decision_variables,
objectives,
constraints,
indicator_constraints: self.indicator_constraints,
one_hot_constraints: self.one_hot_constraints,
sos1_constraints: self.sos1_constraints,
named_functions,
sense,
feasible,
feasible_relaxed,
feasibility_atol: self.feasibility_atol,
metadata: Default::default(),
annotations: Default::default(),
})
}
pub unsafe fn build_unchecked(self) -> Result<SampleSet, SampleSetError> {
let decision_variables =
self.decision_variables
.ok_or(SampleSetError::MissingRequiredField {
field: "decision_variables",
})?;
let decision_variables =
crate::SampledDecisionVariableTable::new(decision_variables, self.variable_labels)
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?;
let objectives = self
.objectives
.ok_or(SampleSetError::MissingRequiredField {
field: "objectives",
})?;
let constraints = self
.constraints
.ok_or(SampleSetError::MissingRequiredField {
field: "constraints",
})?;
let sense = self
.sense
.ok_or(SampleSetError::MissingRequiredField { field: "sense" })?;
let objective_sample_ids = objectives.ids();
let (feasible, feasible_relaxed) = Self::compute_feasibility(
&constraints,
&self.indicator_constraints,
&self.one_hot_constraints,
&self.sos1_constraints,
&objective_sample_ids,
);
let named_functions = match self.named_function_table {
Some(named_functions) => named_functions,
None => NamedFunctionTable::new(self.named_functions, self.named_function_labels)
.map_err(|e| SampleSetError::InvalidSidecar {
message: e.to_string(),
})?,
};
Ok(SampleSet {
decision_variables,
objectives,
constraints,
indicator_constraints: self.indicator_constraints,
one_hot_constraints: self.one_hot_constraints,
sos1_constraints: self.sos1_constraints,
named_functions,
sense,
feasible,
feasible_relaxed,
feasibility_atol: self.feasibility_atol,
metadata: Default::default(),
annotations: Default::default(),
})
}
fn compute_feasibility(
constraints: &SampledCollection<Constraint>,
indicator_constraints: &SampledCollection<IndicatorConstraint>,
one_hot_constraints: &SampledCollection<crate::OneHotConstraint>,
sos1_constraints: &SampledCollection<crate::Sos1Constraint>,
sample_ids: &SampleIDSet,
) -> (BTreeMap<SampleID, bool>, BTreeMap<SampleID, bool>) {
let mut feasible = BTreeMap::new();
let mut feasible_relaxed = BTreeMap::new();
for sample_id in sample_ids {
let f = constraints.is_feasible_for(*sample_id)
&& indicator_constraints.is_feasible_for(*sample_id)
&& one_hot_constraints.is_feasible_for(*sample_id)
&& sos1_constraints.is_feasible_for(*sample_id);
let fr = constraints.is_feasible_relaxed_for(*sample_id)
&& indicator_constraints.is_feasible_relaxed_for(*sample_id)
&& one_hot_constraints.is_feasible_relaxed_for(*sample_id)
&& sos1_constraints.is_feasible_relaxed_for(*sample_id);
feasible.insert(*sample_id, f);
feasible_relaxed.insert(*sample_id, fr);
}
(feasible, feasible_relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraint::EvaluatedData;
use crate::{
ConstraintID, DecisionVariable, Equality, EvaluatedConstraint, SampleID,
SampledDecisionVariable, Sense, VariableID,
};
use std::collections::BTreeMap;
#[test]
fn builder_rejects_sampled_one_hot_side_map_id_mismatch() {
let var_id = VariableID::from(1);
let sample_id = SampleID::from(0);
let unexpected_sample_id = SampleID::from(1);
let decision_variable = DecisionVariable::binary();
let mut variable_samples = crate::Sampled::default();
variable_samples.append([sample_id], 1.0).unwrap();
let sampled_variable =
SampledDecisionVariable::new(var_id, decision_variable.clone(), variable_samples)
.unwrap();
let mut objectives = crate::Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let sampled_one_hot: crate::SampledOneHotConstraint = crate::OneHotConstraint {
variables: [var_id].into_iter().collect(),
stage: crate::OneHotSampledData {
feasible: BTreeMap::from([(sample_id, true)]),
active_variable: BTreeMap::from([(unexpected_sample_id, Some(var_id))]),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let one_hot_constraints = crate::SampledCollection::new(
BTreeMap::from([(crate::OneHotConstraintID::from(1), sampled_one_hot)]),
BTreeMap::new(),
)
.unwrap();
let err = SampleSet::builder()
.decision_variables(BTreeMap::from([(var_id, sampled_variable)]))
.objectives(objectives)
.constraints(BTreeMap::new())
.one_hot_constraints_collection(one_hot_constraints)
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string().contains("Inconsistent sample IDs"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_orphan_variable_label_id() {
let mut variable_labels = crate::VariableLabelStore::default();
variable_labels.set_name(VariableID::from(99), "orphan");
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.variable_labels(variable_labels)
.objectives(crate::Sampled::default())
.constraints(BTreeMap::new())
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string().contains("unknown decision variable ID")
&& err.to_string().contains("VariableID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_inconsistent_regular_constraint_feasibility() {
let sample_id = SampleID::from(0);
let constraint = SampledConstraint {
equality: crate::Equality::EqualToZero,
stage: crate::constraint::SampledData {
evaluated_values: crate::Sampled::from((sample_id, 1.0)),
feasible: BTreeMap::from([(sample_id, true)]),
used_decision_variable_ids: BTreeSet::new(),
dual_variables: None,
},
};
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::from([(ConstraintID::from(1), constraint)]))
.sense(Sense::Minimize)
.feasibility_atol(ATol::new(0.1).unwrap())
.build()
.unwrap_err();
assert!(
err.to_string()
.contains("Inconsistent feasibility for regular constraint"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_unknown_one_hot_structural_variable() {
let sample_id = SampleID::from(0);
let variable_id = VariableID::from(1);
let one_hot = crate::one_hot_constraint::SampledOneHotConstraint {
variables: BTreeSet::from([variable_id]),
stage: crate::one_hot_constraint::OneHotSampledData {
feasible: BTreeMap::from([(sample_id, false)]),
active_variable: BTreeMap::from([(sample_id, None)]),
used_decision_variable_ids: BTreeSet::new(),
},
};
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(crate::Sampled::from((sample_id, 0.0)))
.constraints(BTreeMap::new())
.one_hot_constraints_collection(
SampledCollection::new(
BTreeMap::from([(crate::OneHotConstraintID::from(1), one_hot)]),
BTreeMap::new(),
)
.unwrap(),
)
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string()
.contains("Invalid structure for one-hot constraint"),
"unexpected error: {err}"
);
}
#[test]
fn unchecked_builder_rejects_orphan_variable_label_id() {
let mut variable_labels = crate::VariableLabelStore::default();
variable_labels.set_name(VariableID::from(99), "orphan");
let err = unsafe {
SampleSet::builder()
.decision_variables(BTreeMap::new())
.variable_labels(variable_labels)
.objectives(crate::Sampled::default())
.constraints(BTreeMap::new())
.sense(Sense::Minimize)
.build_unchecked()
.unwrap_err()
};
assert!(
err.to_string().contains("unknown decision variable ID")
&& err.to_string().contains("VariableID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_orphan_named_function_label_id() {
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
named_function_labels.set_name(NamedFunctionID::from(99), "orphan");
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(crate::Sampled::default())
.constraints(BTreeMap::new())
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(
err.to_string().contains("unknown named function ID")
&& err.to_string().contains("NamedFunctionID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn unchecked_builder_rejects_orphan_named_function_label_id() {
let mut named_function_labels = crate::named_function::NamedFunctionLabelStore::default();
named_function_labels.set_name(NamedFunctionID::from(99), "orphan");
let err = unsafe {
SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(crate::Sampled::default())
.constraints(BTreeMap::new())
.named_function_labels(named_function_labels)
.sense(Sense::Minimize)
.build_unchecked()
.unwrap_err()
};
assert!(
err.to_string().contains("unknown named function ID")
&& err.to_string().contains("NamedFunctionID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn builder_rejects_undefined_variable_in_sampled_named_function() {
use crate::parse::Parse as _;
let var_id = VariableID::from(1);
let nf_id = NamedFunctionID::from(7);
let sample_id = SampleID::from(0);
let parsed: crate::named_function::parse::ParsedSampledNamedFunction =
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()
}
.parse(&())
.unwrap();
let mut objectives = crate::Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(objectives)
.constraints(BTreeMap::new())
.named_functions(BTreeMap::from([(nf_id, parsed.sampled_named_function)]))
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(matches!(
err,
SampleSetError::UndefinedVariableInNamedFunction { id, named_function_id }
if id == var_id && named_function_id == nf_id
));
}
#[test]
fn builder_rejects_undefined_variable_in_sampled_constraint() {
let var_id = VariableID::from(1);
let constraint_id = ConstraintID::from(2);
let sample_id = SampleID::from(0);
let mut evaluated_values = crate::Sampled::default();
evaluated_values.append([sample_id], 0.0).unwrap();
let sampled_constraint = crate::Constraint {
equality: Equality::EqualToZero,
stage: crate::constraint::SampledData {
evaluated_values,
dual_variables: None,
feasible: BTreeMap::from([(sample_id, true)]),
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let mut objectives = crate::Sampled::default();
objectives.append([sample_id], 0.0).unwrap();
let err = SampleSet::builder()
.decision_variables(BTreeMap::new())
.objectives(objectives)
.constraints(BTreeMap::from([(constraint_id, sampled_constraint)]))
.sense(Sense::Minimize)
.build()
.unwrap_err();
assert!(matches!(
err,
SampleSetError::UndefinedVariableInConstraint {
id,
constraint_family: "regular",
constraint_id: ref found_constraint_id,
} if id == var_id && found_constraint_id == &format!("{constraint_id:?}")
));
}
#[test]
fn test_sample_set_get_preserves_sidecars() {
let var_id = VariableID::from(1);
let cid = ConstraintID::from(10);
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 evaluated_per_sample = EvaluatedConstraint {
equality: Equality::EqualToZero,
stage: EvaluatedData {
evaluated_value: 1.0,
dual_variable: None,
feasible: false,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let mut evaluated_values = crate::Sampled::default();
evaluated_values
.append([sample_id], evaluated_per_sample.stage.evaluated_value)
.unwrap();
let mut feasible = BTreeMap::new();
feasible.insert(sample_id, false);
let sampled_constraint = crate::Constraint {
equality: Equality::EqualToZero,
stage: crate::constraint::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 removed_reason = crate::RemovedReason {
reason: "relaxed for test".to_string(),
parameters: Default::default(),
};
let constraints = crate::constraint_type::SampledCollection::with_context(
constraints_map,
BTreeMap::from([(cid, removed_reason)]),
constraint_context,
)
.unwrap();
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)
.sense(Sense::Minimize)
.build()
.unwrap();
assert_eq!(sample_set.is_sample_feasible(sample_id), Some(false));
assert_eq!(sample_set.is_sample_feasible_relaxed(sample_id), Some(true));
let solution = sample_set.get(sample_id).unwrap();
assert!(!solution.feasible());
assert!(solution.feasible_relaxed());
assert!(solution.evaluated_constraints().is_removed(&cid));
assert_eq!(solution.variable_labels().name(var_id), Some("x"));
assert_eq!(solution.variable_labels().subscripts(var_id), &[0]);
let constraint_meta = solution.evaluated_constraints().context();
assert_eq!(constraint_meta.name(cid), Some("balance"));
assert_eq!(constraint_meta.description(cid), Some("demand-balance row"));
}
}