mod extract;
mod parse;
mod serialize;
use crate::{
constraint_type::{EvaluatedCollection, SampledCollection, SampledConstraintBehavior},
indicator_constraint::IndicatorConstraint,
Constraint, ConstraintID, EvaluatedConstraint, EvaluatedDecisionVariable,
EvaluatedNamedFunction, NamedFunctionID, SampleID, SampleIDSet, Sampled, SampledConstraint,
SampledDecisionVariable, SampledNamedFunction, Sense, Solution, VariableID,
};
use getset::Getters;
use std::collections::BTreeMap;
#[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 sample IDs: expected {expected:?}, found {found:?}")]
InconsistentSampleIDs {
expected: SampleIDSet,
found: SampleIDSet,
},
#[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("Decision variable key {key:?} does not match value's id {value_id:?}")]
InconsistentDecisionVariableID {
key: VariableID,
value_id: VariableID,
},
#[error("Named function key {key:?} does not match value's id {value_id:?}")]
InconsistentNamedFunctionID {
key: NamedFunctionID,
value_id: NamedFunctionID,
},
}
#[derive(Debug, Clone, Getters)]
pub struct SampleSet {
#[getset(get = "pub")]
decision_variables: BTreeMap<VariableID, SampledDecisionVariable>,
#[getset(get = "pub")]
variable_metadata: crate::decision_variable::VariableMetadataStore,
#[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>,
#[getset(get = "pub")]
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
#[getset(get = "pub")]
named_function_metadata: crate::named_function::NamedFunctionMetadataStore,
#[getset(get = "pub")]
sense: Sense,
#[getset(get = "pub")]
feasible: BTreeMap<SampleID, bool>,
#[getset(get = "pub")]
feasible_relaxed: BTreeMap<SampleID, bool>,
}
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 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(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 {
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_metadata(
evaluated_constraints,
BTreeMap::new(),
self.constraints.metadata().clone(),
))
.evaluated_indicator_constraints_collection(EvaluatedCollection::with_metadata(
evaluated_indicator_constraints,
BTreeMap::new(),
self.indicator_constraints.metadata().clone(),
))
.evaluated_one_hot_constraints_collection(EvaluatedCollection::with_metadata(
evaluated_one_hot_constraints,
BTreeMap::new(),
self.one_hot_constraints.metadata().clone(),
))
.evaluated_sos1_constraints_collection(EvaluatedCollection::with_metadata(
evaluated_sos1_constraints,
BTreeMap::new(),
self.sos1_constraints.metadata().clone(),
))
.objective(objective)
.evaluated_named_functions(evaluated_named_functions)
.decision_variables(decision_variables)
.variable_metadata(self.variable_metadata.clone())
.named_function_metadata(self.named_function_metadata.clone())
.sense(sense)
.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_metadata: crate::decision_variable::VariableMetadataStore,
objectives: Option<Sampled<f64>>,
constraints: Option<SampledCollection<Constraint>>,
indicator_constraints: SampledCollection<IndicatorConstraint>,
one_hot_constraints: SampledCollection<crate::OneHotConstraint>,
sos1_constraints: SampledCollection<crate::Sos1Constraint>,
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
named_function_metadata: crate::named_function::NamedFunctionMetadataStore,
sense: Option<Sense>,
}
impl SampleSetBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn variable_metadata(
mut self,
variable_metadata: crate::decision_variable::VariableMetadataStore,
) -> Self {
self.variable_metadata = variable_metadata;
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()));
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());
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());
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());
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_functions = named_functions;
self
}
pub fn named_function_metadata(
mut self,
named_function_metadata: crate::named_function::NamedFunctionMetadataStore,
) -> Self {
self.named_function_metadata = named_function_metadata;
self
}
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = Some(sense);
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" })?;
for (key, value) in &decision_variables {
if key != value.id() {
return Err(SampleSetError::InconsistentDecisionVariableID {
key: *key,
value_id: *value.id(),
});
}
}
for (key, value) in &self.named_functions {
if key != value.id() {
return Err(SampleSetError::InconsistentNamedFunctionID {
key: *key,
value_id: *value.id(),
});
}
}
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(),
});
}
}
for sampled_constraint in constraints.values() {
if !sampled_constraint
.stage
.evaluated_values
.has_same_ids(&objective_sample_ids)
{
return Err(SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_constraint.stage.evaluated_values.ids(),
});
}
}
for sampled_ic in self.indicator_constraints.values() {
if !sampled_ic
.stage
.evaluated_values
.has_same_ids(&objective_sample_ids)
{
return Err(SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_ic.stage.evaluated_values.ids(),
});
}
}
for sampled_named_function in self.named_functions.values() {
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(),
});
}
}
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,
variable_metadata: self.variable_metadata.clone(),
objectives,
constraints,
indicator_constraints: self.indicator_constraints,
one_hot_constraints: self.one_hot_constraints,
sos1_constraints: self.sos1_constraints,
named_functions: self.named_functions,
named_function_metadata: self.named_function_metadata.clone(),
sense,
feasible,
feasible_relaxed,
})
}
pub unsafe fn build_unchecked(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 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,
);
Ok(SampleSet {
decision_variables,
variable_metadata: self.variable_metadata.clone(),
objectives,
constraints,
indicator_constraints: self.indicator_constraints,
one_hot_constraints: self.one_hot_constraints,
sos1_constraints: self.sos1_constraints,
named_functions: self.named_functions,
named_function_metadata: self.named_function_metadata.clone(),
sense,
feasible,
feasible_relaxed,
})
}
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::{
ATol, ConstraintID, DecisionVariable, Equality, EvaluatedConstraint, SampleID,
SampledDecisionVariable, Sense, VariableID,
};
use std::collections::BTreeMap;
#[test]
fn test_sample_set_get_preserves_metadata() {
let var_id = VariableID::from(1);
let cid = ConstraintID::from(10);
let sample_id = SampleID::from(0);
let dv = DecisionVariable::binary(var_id);
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(dv, x_samples, ATol::default()).unwrap(),
);
let mut variable_metadata = crate::VariableMetadataStore::default();
variable_metadata.set_name(var_id, "x");
variable_metadata.set_subscripts(var_id, vec![0]);
let evaluated_per_sample = EvaluatedConstraint {
equality: Equality::EqualToZero,
stage: EvaluatedData {
evaluated_value: 0.0,
dual_variable: None,
feasible: true,
used_decision_variable_ids: [var_id].into_iter().collect(),
},
};
let mut evaluated_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, true);
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_metadata = crate::ConstraintMetadataStore::<ConstraintID>::default();
constraint_metadata.set_name(cid, "balance");
constraint_metadata.set_description(cid, "demand-balance row");
let constraints = crate::constraint_type::SampledCollection::with_metadata(
constraints_map,
BTreeMap::new(),
constraint_metadata,
);
let mut objectives = crate::Sampled::default();
objectives.append([sample_id], 1.0).unwrap();
let sample_set = SampleSet::builder()
.decision_variables(decision_variables)
.variable_metadata(variable_metadata)
.objectives(objectives)
.constraints_collection(constraints)
.sense(Sense::Minimize)
.build()
.unwrap();
let solution = sample_set.get(sample_id).unwrap();
assert_eq!(solution.variable_metadata().name(var_id), Some("x"));
assert_eq!(solution.variable_metadata().subscripts(var_id), &[0]);
let constraint_meta = solution.evaluated_constraints().metadata();
assert_eq!(constraint_meta.name(cid), Some("balance"));
assert_eq!(constraint_meta.description(cid), Some("demand-balance row"));
}
}