mod extract;
mod parse;
mod serialize;
use crate::{
ConstraintID, EvaluatedConstraint, EvaluatedDecisionVariable, EvaluatedNamedFunction,
NamedFunctionID, SampleID, SampleIDSet, Sampled, SampledConstraint, SampledDecisionVariable,
SampledNamedFunction, Sense, Solution, UnknownSampleIDError, 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(transparent)]
UnknownSampleIDError(#[from] UnknownSampleIDError),
#[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("Constraint key {key:?} does not match value's id {value_id:?}")]
InconsistentConstraintID {
key: ConstraintID,
value_id: ConstraintID,
},
#[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")]
objectives: Sampled<f64>,
#[getset(get = "pub")]
constraints: BTreeMap<ConstraintID, SampledConstraint>,
#[getset(get = "pub")]
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
#[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) -> Result<bool, UnknownSampleIDError> {
self.feasible
.get(&sample_id)
.copied()
.ok_or(UnknownSampleIDError { id: sample_id })
}
pub fn is_sample_feasible_relaxed(
&self,
sample_id: SampleID,
) -> Result<bool, UnknownSampleIDError> {
self.feasible_relaxed
.get(&sample_id)
.copied()
.ok_or(UnknownSampleIDError { id: sample_id })
}
pub fn get(&self, sample_id: crate::SampleID) -> Result<Solution, crate::UnknownSampleIDError> {
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 {
let evaluated_constraint = constraint.get(sample_id)?;
evaluated_constraints.insert(*constraint_id, evaluated_constraint);
}
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();
Ok(unsafe {
Solution::builder()
.objective(objective)
.evaluated_constraints(evaluated_constraints)
.evaluated_named_functions(evaluated_named_functions)
.decision_variables(decision_variables)
.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).map_err(SampleSetError::from)
}
pub fn best_feasible_relaxed(&self) -> Result<Solution, SampleSetError> {
let id = self.best_feasible_relaxed_id()?;
self.get(id).map_err(SampleSetError::from)
}
pub fn builder() -> SampleSetBuilder {
SampleSetBuilder::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct SampleSetBuilder {
decision_variables: Option<BTreeMap<VariableID, SampledDecisionVariable>>,
objectives: Option<Sampled<f64>>,
constraints: Option<BTreeMap<ConstraintID, SampledConstraint>>,
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
sense: Option<Sense>,
}
impl SampleSetBuilder {
pub fn new() -> Self {
Self::default()
}
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(constraints);
self
}
pub fn named_functions(
mut self,
named_functions: BTreeMap<NamedFunctionID, SampledNamedFunction>,
) -> Self {
self.named_functions = named_functions;
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 &constraints {
if key != value.id() {
return Err(SampleSetError::InconsistentConstraintID {
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
.evaluated_values()
.has_same_ids(&objective_sample_ids)
{
return Err(SampleSetError::InconsistentSampleIDs {
expected: objective_sample_ids.clone(),
found: sampled_constraint.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, &objective_sample_ids);
Ok(SampleSet {
decision_variables,
objectives,
constraints,
named_functions: self.named_functions,
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, &objective_sample_ids);
Ok(SampleSet {
decision_variables,
objectives,
constraints,
named_functions: self.named_functions,
sense,
feasible,
feasible_relaxed,
})
}
fn compute_feasibility(
constraints: &BTreeMap<ConstraintID, SampledConstraint>,
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 is_feasible = constraints.values().all(|constraint| {
constraint
.feasible()
.get(sample_id)
.copied()
.unwrap_or(false)
});
let is_feasible_relaxed = constraints
.values()
.filter(|constraint| constraint.removed_reason().is_none())
.all(|constraint| {
constraint
.feasible()
.get(sample_id)
.copied()
.unwrap_or(false)
});
feasible.insert(*sample_id, is_feasible);
feasible_relaxed.insert(*sample_id, is_feasible_relaxed);
}
(feasible, feasible_relaxed)
}
}