use crate::Result;
use crate::{
constraint::{
ConstraintContext, ConstraintContextStore, ConstraintID, EvaluatedConstraint,
RemovedReason, SampledConstraint,
},
v1, ATol, Constraint, Evaluate, Parse, ParseError, RawParseError, SampleID, SampleIDSet,
VariableIDSet,
};
use std::sync::LazyLock;
static EMPTY_VARIABLE_ID_SET: LazyLock<VariableIDSet> = LazyLock::new(VariableIDSet::default);
use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
fn validate_no_key_overlap<ID, L, R>(
left: &BTreeMap<ID, L>,
right: &BTreeMap<ID, R>,
left_name: &str,
right_name: &str,
) -> crate::Result<()>
where
ID: IDType,
{
if let Some(id) = left.keys().find(|id| right.contains_key(id)) {
crate::bail!(
{ ?id },
"Constraint ID {id:?} appears in both {left_name} and {right_name}",
);
}
Ok(())
}
fn validate_removed_reasons_reference_entries<ID, V>(
constraints: &BTreeMap<ID, V>,
removed_reasons: &BTreeMap<ID, RemovedReason>,
) -> crate::Result<()>
where
ID: IDType,
{
if let Some(id) = removed_reasons
.keys()
.find(|id| !constraints.contains_key(id))
{
crate::bail!({ ?id }, "Removed reason references unknown constraint ID {id:?}");
}
Ok(())
}
fn validate_context_reference_ids<ID>(
context: &ConstraintContextStore<ID>,
owned_ids: &BTreeSet<ID>,
) -> crate::Result<()>
where
ID: IDType,
{
if let Some(id) = context.ids().into_iter().find(|id| !owned_ids.contains(id)) {
crate::bail!(
{ ?id },
"Constraint label/provenance references unknown constraint ID {id:?}",
);
}
Ok(())
}
pub(crate) fn sample_ids_from_map<V>(map: &BTreeMap<SampleID, V>) -> SampleIDSet {
map.keys().copied().collect()
}
pub trait IDType:
Clone
+ Copy
+ Ord
+ std::hash::Hash
+ std::fmt::Debug
+ From<u64>
+ Into<u64>
+ crate::logical_memory::LogicalMemoryProfile
{
}
impl<T> IDType for T where
T: Clone
+ Copy
+ Ord
+ std::hash::Hash
+ std::fmt::Debug
+ From<u64>
+ Into<u64>
+ crate::logical_memory::LogicalMemoryProfile
{
}
pub trait ConstraintType {
type ID: IDType;
type Created: Evaluate<Output = Self::Evaluated, SampledOutput = Self::Sampled>
+ Clone
+ std::fmt::Debug
+ PartialEq;
type Evaluated: EvaluatedConstraintBehavior<ID = Self::ID>;
type Sampled: SampledConstraintBehavior<ID = Self::ID, Evaluated = Self::Evaluated>;
}
pub trait EvaluatedConstraintBehavior {
type ID;
fn is_feasible(&self) -> bool;
fn used_decision_variable_ids(&self) -> &VariableIDSet {
&EMPTY_VARIABLE_ID_SET
}
}
pub trait SampledConstraintBehavior {
type ID;
type Evaluated;
fn is_feasible_for(&self, sample_id: SampleID) -> Option<bool>;
fn validate_sample_ids(&self, expected: &SampleIDSet) -> std::result::Result<(), SampleIDSet>;
fn used_decision_variable_ids(&self) -> &VariableIDSet {
&EMPTY_VARIABLE_ID_SET
}
fn get(&self, sample_id: SampleID) -> Option<Self::Evaluated>;
}
impl EvaluatedConstraintBehavior for EvaluatedConstraint {
type ID = ConstraintID;
fn is_feasible(&self) -> bool {
self.stage.feasible
}
fn used_decision_variable_ids(&self) -> &VariableIDSet {
&self.stage.used_decision_variable_ids
}
}
impl SampledConstraintBehavior for SampledConstraint {
type ID = ConstraintID;
type Evaluated = EvaluatedConstraint;
fn is_feasible_for(&self, sample_id: SampleID) -> Option<bool> {
self.stage.feasible.get(&sample_id).copied()
}
fn validate_sample_ids(&self, expected: &SampleIDSet) -> std::result::Result<(), SampleIDSet> {
if !self.stage.evaluated_values.has_same_ids(expected) {
return Err(self.stage.evaluated_values.ids());
}
let feasible_ids = sample_ids_from_map(&self.stage.feasible);
if &feasible_ids != expected {
return Err(feasible_ids);
}
if let Some(dual_variables) = &self.stage.dual_variables {
if !dual_variables.has_same_ids(expected) {
return Err(dual_variables.ids());
}
}
Ok(())
}
fn used_decision_variable_ids(&self) -> &VariableIDSet {
&self.stage.used_decision_variable_ids
}
fn get(&self, sample_id: SampleID) -> Option<Self::Evaluated> {
use crate::constraint::EvaluatedData;
let evaluated_value = *self.stage.evaluated_values.get(sample_id)?;
let dual_variable = self
.stage
.dual_variables
.as_ref()
.and_then(|duals| duals.get(sample_id))
.copied();
let feasible = *self.stage.feasible.get(&sample_id)?;
Some(crate::Constraint {
equality: self.equality,
stage: EvaluatedData {
evaluated_value,
dual_variable,
feasible,
used_decision_variable_ids: self.stage.used_decision_variable_ids.clone(),
},
})
}
}
impl ConstraintType for Constraint {
type ID = ConstraintID;
type Created = Constraint;
type Evaluated = EvaluatedConstraint;
type Sampled = SampledConstraint;
}
#[derive(Debug, Clone, PartialEq)]
pub struct ConstraintCollection<T: ConstraintType> {
active: BTreeMap<T::ID, T::Created>,
removed: BTreeMap<T::ID, (T::Created, RemovedReason)>,
context: ConstraintContextStore<T::ID>,
}
impl<T: ConstraintType> Default for ConstraintCollection<T> {
fn default() -> Self {
Self {
active: BTreeMap::new(),
removed: BTreeMap::new(),
context: ConstraintContextStore::default(),
}
}
}
impl<T: ConstraintType> ConstraintCollection<T> {
pub fn new(
active: BTreeMap<T::ID, T::Created>,
removed: BTreeMap<T::ID, (T::Created, RemovedReason)>,
) -> crate::Result<Self> {
validate_no_key_overlap(
&active,
&removed,
"active constraints",
"removed constraints",
)?;
Ok(Self {
active,
removed,
context: ConstraintContextStore::default(),
})
}
pub fn with_context(
active: BTreeMap<T::ID, T::Created>,
removed: BTreeMap<T::ID, (T::Created, RemovedReason)>,
context: ConstraintContextStore<T::ID>,
) -> crate::Result<Self> {
validate_no_key_overlap(
&active,
&removed,
"active constraints",
"removed constraints",
)?;
let owned_ids = active
.keys()
.chain(removed.keys())
.copied()
.collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids)?;
Ok(Self {
active,
removed,
context,
})
}
pub fn context(&self) -> &ConstraintContextStore<T::ID> {
&self.context
}
pub fn validate_context_ids(&self) -> crate::Result<()> {
let owned_ids = self
.active
.keys()
.chain(self.removed.keys())
.copied()
.collect::<BTreeSet<_>>();
validate_context_reference_ids(&self.context, &owned_ids)
}
pub fn active(&self) -> &BTreeMap<T::ID, T::Created> {
&self.active
}
pub fn removed(&self) -> &BTreeMap<T::ID, (T::Created, RemovedReason)> {
&self.removed
}
fn contains_id(&self, id: T::ID) -> bool {
self.active.contains_key(&id) || self.removed.contains_key(&id)
}
pub(crate) fn set_context_for_owner(
&mut self,
id: T::ID,
context: ConstraintContext,
owner_name: &str,
) -> crate::Result<()> {
if !self.contains_id(id) {
crate::bail!(
{ ?id },
"Constraint label/provenance references unknown {owner_name} ID {id:?}",
);
}
self.context.insert(id, context);
Ok(())
}
fn replace_active(&mut self, id: T::ID, constraint: T::Created) -> Option<T::Created> {
match self.active.entry(id) {
Entry::Occupied(mut entry) => Some(entry.insert(constraint)),
Entry::Vacant(_) => None,
}
}
fn replace_removed(&mut self, id: T::ID, constraint: T::Created) -> Option<T::Created> {
self.removed
.get_mut(&id)
.map(|(removed_constraint, _reason)| std::mem::replace(removed_constraint, constraint))
}
pub(crate) fn replace_row_preserving_lifecycle(
&mut self,
id: T::ID,
constraint: T::Created,
) -> Option<T::Created> {
if self.active.contains_key(&id) {
self.replace_active(id, constraint)
} else {
self.replace_removed(id, constraint)
}
}
pub(crate) fn replace_rows_preserving_lifecycle(
&mut self,
active_replacements: BTreeMap<T::ID, T::Created>,
removed_replacements: BTreeMap<T::ID, T::Created>,
) -> crate::Result<()> {
validate_no_key_overlap(
&active_replacements,
&removed_replacements,
"active row replacements",
"removed row replacements",
)?;
for id in active_replacements.keys() {
if !self.active.contains_key(id) {
crate::bail!({ ?id }, "Active constraint with ID {id:?} not found");
}
}
for id in removed_replacements.keys() {
if !self.removed.contains_key(id) {
crate::bail!({ ?id }, "Removed constraint with ID {id:?} not found");
}
}
for (id, constraint) in active_replacements {
self.active.insert(id, constraint);
}
for (id, constraint) in removed_replacements {
let (removed_constraint, _reason) = self
.removed
.get_mut(&id)
.expect("removed row was validated before replacement");
*removed_constraint = constraint;
}
debug_assert!(self.validate_context_ids().is_ok());
Ok(())
}
pub(crate) fn replace_and_remove_active_rows(
&mut self,
replacements: BTreeMap<T::ID, T::Created>,
removals: BTreeMap<T::ID, (T::Created, RemovedReason)>,
) -> crate::Result<()> {
if let Some(id) = replacements.keys().find(|id| removals.contains_key(id)) {
crate::bail!(
{ ?id },
"Constraint ID {id:?} appears in both active row replacements and removals",
);
}
for id in replacements.keys().chain(removals.keys()) {
if !self.active.contains_key(id) {
crate::bail!({ ?id }, "Active constraint with ID {id:?} not found");
}
}
for (id, constraint) in replacements {
self.active.insert(id, constraint);
}
for (id, (constraint, reason)) in removals {
self.active
.remove(&id)
.expect("active row was validated before removal");
self.removed.insert(id, (constraint, reason));
}
debug_assert!(self.validate_context_ids().is_ok());
Ok(())
}
pub(crate) fn replace_active_rows(
&mut self,
replacements: BTreeMap<T::ID, T::Created>,
) -> crate::Result<()> {
self.replace_and_remove_active_rows(replacements, BTreeMap::new())
}
pub(crate) fn replace_active_row(
&mut self,
id: T::ID,
constraint: T::Created,
) -> crate::Result<()> {
self.replace_active_rows(BTreeMap::from([(id, constraint)]))
}
pub(crate) fn move_active_rows_to_removed(
&mut self,
removals: BTreeMap<T::ID, (T::Created, RemovedReason)>,
) -> crate::Result<()> {
self.replace_and_remove_active_rows(BTreeMap::new(), removals)
}
pub(crate) fn insert_active_with_context(
&mut self,
id: T::ID,
constraint: T::Created,
context: ConstraintContext,
) -> crate::Result<()> {
if self.active.contains_key(&id) {
crate::bail!(
{ ?id },
"Constraint ID {id:?} already exists in the active constraint collection",
);
}
if self.removed.contains_key(&id) {
crate::bail!(
{ ?id },
"Constraint ID {id:?} already exists in the removed constraint collection",
);
}
self.active.insert(id, constraint);
self.context.insert(id, context);
Ok(())
}
pub fn unused_id(&self) -> T::ID {
let max_active = self.active.keys().last().copied().map(Into::into);
let max_removed = self.removed.keys().last().copied().map(Into::into);
let next = match (max_active, max_removed) {
(None, None) => 0u64,
(Some(a), None) => a.checked_add(1).expect("constraint ID space exhausted"),
(None, Some(r)) => r.checked_add(1).expect("constraint ID space exhausted"),
(Some(a), Some(r)) => a
.max(r)
.checked_add(1)
.expect("constraint ID space exhausted"),
};
T::ID::from(next)
}
pub fn relax(&mut self, id: T::ID, removed_reason: RemovedReason) -> crate::Result<()> {
let c = self
.active
.remove(&id)
.ok_or_else(|| crate::error!("Constraint with ID {:?} not found", id))?;
self.removed.insert(id, (c, removed_reason));
Ok(())
}
pub(crate) fn restore_removed_row(
&mut self,
id: T::ID,
constraint: T::Created,
) -> crate::Result<RemovedReason> {
let Some((_removed, reason)) = self.removed.remove(&id) else {
return Err(crate::error!(
"Removed constraint with ID {:?} not found",
id
));
};
debug_assert!(!self.active.contains_key(&id));
self.active.insert(id, constraint);
debug_assert!(self.validate_context_ids().is_ok());
Ok(reason)
}
pub fn required_ids(&self) -> VariableIDSet {
let mut ids = VariableIDSet::default();
for constraint in self.active.values() {
ids.extend(constraint.required_ids());
}
ids
}
}
impl From<ConstraintCollection<Constraint>> for (Vec<v1::Constraint>, Vec<v1::RemovedConstraint>) {
fn from(value: ConstraintCollection<Constraint>) -> Self {
let ConstraintCollection {
active,
removed,
mut context,
} = value;
let active = active
.into_iter()
.map(|(id, constraint)| constraint_to_v1(id, constraint, context.remove(id)))
.collect();
let removed = removed
.into_iter()
.map(|(id, (constraint, reason))| {
removed_constraint_to_v1(id, constraint, context.remove(id), reason)
})
.collect();
(active, removed)
}
}
fn constraint_to_v1(
id: ConstraintID,
value: Constraint,
context: ConstraintContext,
) -> v1::Constraint {
let label = context.label;
v1::Constraint {
id: id.into_inner(),
equality: value.equality.into(),
function: Some(value.stage.function.into()),
name: label.name,
subscripts: label.subscripts,
parameters: label.parameters.into_iter().collect(),
description: label.description,
}
}
fn removed_constraint_to_v1(
id: ConstraintID,
constraint: Constraint,
context: ConstraintContext,
removed_reason: RemovedReason,
) -> v1::RemovedConstraint {
v1::RemovedConstraint {
constraint: Some(constraint_to_v1(id, constraint, context)),
removed_reason: removed_reason.reason,
removed_reason_parameters: removed_reason.parameters.into_iter().collect(),
}
}
macro_rules! impl_v2_created_collection {
($source:ty => $target:ty) => {
impl From<ConstraintCollection<$source>> for $target {
fn from(value: ConstraintCollection<$source>) -> Self {
let ConstraintCollection {
active,
removed,
context,
} = value;
let (removed, removed_reasons) = removed_entries_to_v2_maps(removed);
Self {
active: entries_to_v2_map(active),
removed,
removed_reasons,
contexts: constraint_context_store_to_v2_map(&context),
}
}
}
};
}
impl_v2_created_collection!(Constraint => crate::v2::RegularConstraintCollection);
impl_v2_created_collection!(crate::IndicatorConstraint => crate::v2::IndicatorConstraintCollection);
impl_v2_created_collection!(crate::OneHotConstraint => crate::v2::OneHotConstraintCollection);
impl_v2_created_collection!(crate::Sos1Constraint => crate::v2::Sos1ConstraintCollection);
macro_rules! impl_v2_evaluated_collection {
($source:ty => $target:ty) => {
impl From<EvaluatedCollection<$source>> for $target {
fn from(value: EvaluatedCollection<$source>) -> Self {
let EvaluatedCollection {
constraints,
removed_reasons,
context,
} = value;
Self {
entries: entries_to_v2_map(constraints),
removed_reasons: removed_reasons_to_v2_map(removed_reasons),
contexts: constraint_context_store_to_v2_map(&context),
}
}
}
};
}
impl_v2_evaluated_collection!(Constraint => crate::v2::EvaluatedRegularConstraintCollection);
impl_v2_evaluated_collection!(crate::IndicatorConstraint => crate::v2::EvaluatedIndicatorConstraintCollection);
impl_v2_evaluated_collection!(crate::OneHotConstraint => crate::v2::EvaluatedOneHotConstraintCollection);
impl_v2_evaluated_collection!(crate::Sos1Constraint => crate::v2::EvaluatedSos1ConstraintCollection);
macro_rules! impl_v2_sampled_collection {
($source:ty => $target:ty) => {
impl From<SampledCollection<$source>> for $target {
fn from(value: SampledCollection<$source>) -> Self {
let SampledCollection {
constraints,
removed_reasons,
context,
} = value;
Self {
entries: entries_to_v2_map(constraints),
removed_reasons: removed_reasons_to_v2_map(removed_reasons),
contexts: constraint_context_store_to_v2_map(&context),
}
}
}
};
}
impl_v2_sampled_collection!(Constraint => crate::v2::SampledRegularConstraintCollection);
impl_v2_sampled_collection!(crate::IndicatorConstraint => crate::v2::SampledIndicatorConstraintCollection);
impl_v2_sampled_collection!(crate::OneHotConstraint => crate::v2::SampledOneHotConstraintCollection);
impl_v2_sampled_collection!(crate::Sos1Constraint => crate::v2::SampledSos1ConstraintCollection);
fn constraint_context_store_to_v2_map<ID: IDType>(
store: &ConstraintContextStore<ID>,
) -> BTreeMap<u64, crate::v2::ConstraintContext> {
store
.ids()
.into_iter()
.map(|id| (id.into(), store.collect_for(id).into()))
.collect()
}
fn constraint_context_store_from_v2_map<ID: IDType>(
contexts: BTreeMap<u64, crate::v2::ConstraintContext>,
message: &'static str,
field: &'static str,
) -> std::result::Result<ConstraintContextStore<ID>, ParseError> {
let mut store = ConstraintContextStore::default();
for (id, context) in contexts {
store.insert(ID::from(id), context.parse_as(&(), message, field)?);
}
Ok(store)
}
macro_rules! impl_parse_v2_created_collection {
($source:ty => $target:ty) => {
impl Parse for $target {
type Output = ConstraintCollection<$source>;
type Context = ();
fn parse(self, _: &Self::Context) -> std::result::Result<Self::Output, ParseError> {
let message = stringify!($target);
let active = parse_v2_constraint_entries(self.active, message, "active")?;
let removed = parse_v2_removed_constraint_entries(
self.removed,
self.removed_reasons,
message,
)?;
let context =
constraint_context_store_from_v2_map(self.contexts, message, "contexts")?;
let owned_ids = active
.keys()
.chain(removed.keys())
.copied()
.collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids).map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "contexts")
})?;
ConstraintCollection::with_context(active, removed, context).map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "active")
})
}
}
};
}
impl_parse_v2_created_collection!(Constraint => crate::v2::RegularConstraintCollection);
impl_parse_v2_created_collection!(crate::IndicatorConstraint => crate::v2::IndicatorConstraintCollection);
impl_parse_v2_created_collection!(crate::OneHotConstraint => crate::v2::OneHotConstraintCollection);
impl_parse_v2_created_collection!(crate::Sos1Constraint => crate::v2::Sos1ConstraintCollection);
macro_rules! impl_parse_v2_evaluated_collection {
($source:ty => $target:ty) => {
impl Parse for $target {
type Output = EvaluatedCollection<$source>;
type Context = crate::ATol;
fn parse(self, atol: &Self::Context) -> std::result::Result<Self::Output, ParseError> {
let message = stringify!($target);
let entries = parse_v2_constraint_entries_with_context(
self.entries,
message,
"entries",
atol,
)?;
let removed_reasons =
parse_v2_removed_reasons(self.removed_reasons, message, "removed_reasons")?;
let context =
constraint_context_store_from_v2_map(self.contexts, message, "contexts")?;
let owned_ids = entries.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids).map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "contexts")
})?;
EvaluatedCollection::with_context(entries, removed_reasons, context).map_err(|e| {
RawParseError::InvalidInstance(e.to_string())
.context(message, "removed_reasons")
})
}
}
};
}
impl_parse_v2_evaluated_collection!(Constraint => crate::v2::EvaluatedRegularConstraintCollection);
impl_parse_v2_evaluated_collection!(crate::IndicatorConstraint => crate::v2::EvaluatedIndicatorConstraintCollection);
impl_parse_v2_evaluated_collection!(crate::OneHotConstraint => crate::v2::EvaluatedOneHotConstraintCollection);
impl_parse_v2_evaluated_collection!(crate::Sos1Constraint => crate::v2::EvaluatedSos1ConstraintCollection);
macro_rules! impl_parse_v2_sampled_collection {
($source:ty => $target:ty) => {
impl Parse for $target {
type Output = SampledCollection<$source>;
type Context = crate::ATol;
fn parse(self, atol: &Self::Context) -> std::result::Result<Self::Output, ParseError> {
let message = stringify!($target);
let entries = parse_v2_constraint_entries_with_context(
self.entries,
message,
"entries",
atol,
)?;
let removed_reasons =
parse_v2_removed_reasons(self.removed_reasons, message, "removed_reasons")?;
let context =
constraint_context_store_from_v2_map(self.contexts, message, "contexts")?;
let owned_ids = entries.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids).map_err(|e| {
RawParseError::InvalidInstance(e.to_string()).context(message, "contexts")
})?;
SampledCollection::with_context(entries, removed_reasons, context).map_err(|e| {
RawParseError::InvalidInstance(e.to_string())
.context(message, "removed_reasons")
})
}
}
};
}
impl_parse_v2_sampled_collection!(Constraint => crate::v2::SampledRegularConstraintCollection);
impl_parse_v2_sampled_collection!(crate::IndicatorConstraint => crate::v2::SampledIndicatorConstraintCollection);
impl_parse_v2_sampled_collection!(crate::OneHotConstraint => crate::v2::SampledOneHotConstraintCollection);
impl_parse_v2_sampled_collection!(crate::Sos1Constraint => crate::v2::SampledSos1ConstraintCollection);
fn entries_to_v2_map<ID, Row, V2Row>(entries: BTreeMap<ID, Row>) -> BTreeMap<u64, V2Row>
where
ID: IDType,
Row: Into<V2Row>,
{
entries
.into_iter()
.map(|(id, row)| (id.into(), row.into()))
.collect()
}
fn parse_v2_constraint_entries<ID, V2Row, Row>(
entries: BTreeMap<u64, V2Row>,
message: &'static str,
field: &'static str,
) -> std::result::Result<BTreeMap<ID, Row>, ParseError>
where
ID: IDType,
V2Row: Parse<Output = Row, Context = ()>,
{
entries
.into_iter()
.map(|(id, row)| Ok((ID::from(id), row.parse_as(&(), message, field)?)))
.collect()
}
fn parse_v2_constraint_entries_with_context<ID, V2Row, Row, C>(
entries: BTreeMap<u64, V2Row>,
message: &'static str,
field: &'static str,
context: &C,
) -> std::result::Result<BTreeMap<ID, Row>, ParseError>
where
ID: IDType,
V2Row: Parse<Output = Row, Context = C>,
{
entries
.into_iter()
.map(|(id, row)| Ok((ID::from(id), row.parse_as(context, message, field)?)))
.collect()
}
fn removed_entries_to_v2_maps<ID, Row, V2Row>(
removed: BTreeMap<ID, (Row, RemovedReason)>,
) -> (
BTreeMap<u64, V2Row>,
BTreeMap<u64, crate::v2::RemovedReason>,
)
where
ID: IDType,
Row: Into<V2Row>,
{
let mut rows = BTreeMap::new();
let mut reasons = BTreeMap::new();
for (id, (row, reason)) in removed {
let id = id.into();
rows.insert(id, row.into());
reasons.insert(id, reason.into());
}
(rows, reasons)
}
fn parse_v2_removed_constraint_entries<ID, V2Row, Row>(
removed: BTreeMap<u64, V2Row>,
mut removed_reasons: BTreeMap<u64, crate::v2::RemovedReason>,
message: &'static str,
) -> std::result::Result<BTreeMap<ID, (Row, RemovedReason)>, ParseError>
where
ID: IDType,
V2Row: Parse<Output = Row, Context = ()>,
{
let mut out = BTreeMap::new();
for (id, row) in removed {
let reason = removed_reasons.remove(&id).ok_or_else(|| {
RawParseError::InvalidInstance(format!(
"Removed constraint ID {:?} has no removed reason",
ID::from(id)
))
.context(message, "removed_reasons")
})?;
out.insert(
ID::from(id),
(row.parse_as(&(), message, "removed")?, reason.into()),
);
}
if let Some(id) = removed_reasons.keys().next().copied() {
return Err(RawParseError::InvalidInstance(format!(
"Removed reason references unknown constraint ID {:?}",
ID::from(id)
))
.context(message, "removed_reasons"));
}
Ok(out)
}
fn removed_reasons_to_v2_map<ID: IDType>(
removed_reasons: BTreeMap<ID, RemovedReason>,
) -> BTreeMap<u64, crate::v2::RemovedReason> {
removed_reasons
.into_iter()
.map(|(id, reason)| (id.into(), reason.into()))
.collect()
}
fn parse_v2_removed_reasons<ID: IDType>(
removed_reasons: BTreeMap<u64, crate::v2::RemovedReason>,
_message: &'static str,
_field: &'static str,
) -> std::result::Result<BTreeMap<ID, RemovedReason>, ParseError> {
Ok(removed_reasons
.into_iter()
.map(|(id, reason)| (ID::from(id), reason.into()))
.collect())
}
impl<T: ConstraintType> Evaluate for ConstraintCollection<T> {
type Output = EvaluatedCollection<T>;
type SampledOutput = SampledCollection<T>;
fn evaluate(&self, state: &v1::State, atol: ATol) -> Result<Self::Output> {
let mut results = BTreeMap::new();
let mut removed_reasons = BTreeMap::new();
for (id, constraint) in &self.active {
let evaluated = constraint.evaluate(state, atol).inspect_err(|e| {
tracing::error!(?id, error = %e, "failed to evaluate active constraint");
})?;
results.insert(*id, evaluated);
}
for (id, (constraint, reason)) in &self.removed {
let evaluated = constraint.evaluate(state, atol).inspect_err(|e| {
tracing::error!(?id, error = %e, "failed to evaluate removed constraint");
})?;
results.insert(*id, evaluated);
removed_reasons.insert(*id, reason.clone());
}
EvaluatedCollection::with_context(results, removed_reasons, self.context.clone())
}
fn evaluate_samples(
&self,
samples: &crate::Sampled<v1::State>,
atol: ATol,
) -> Result<Self::SampledOutput> {
let mut results = BTreeMap::new();
let mut removed_reasons = BTreeMap::new();
for (id, constraint) in &self.active {
let evaluated = constraint.evaluate_samples(samples, atol).inspect_err(|e| {
tracing::error!(?id, error = %e, "failed to evaluate_samples active constraint");
})?;
results.insert(*id, evaluated);
}
for (id, (constraint, reason)) in &self.removed {
let evaluated = constraint.evaluate_samples(samples, atol).inspect_err(|e| {
tracing::error!(?id, error = %e, "failed to evaluate_samples removed constraint");
})?;
results.insert(*id, evaluated);
removed_reasons.insert(*id, reason.clone());
}
SampledCollection::with_context(results, removed_reasons, self.context.clone())
}
fn partial_evaluate(&mut self, state: &v1::State, atol: ATol) -> Result<()> {
for (id, constraint) in self.active.iter_mut() {
constraint.partial_evaluate(state, atol).inspect_err(|e| {
tracing::error!(?id, error = %e, "failed to partial_evaluate constraint");
})?;
}
Ok(())
}
fn required_ids(&self) -> VariableIDSet {
ConstraintCollection::required_ids(self)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct EvaluatedCollection<T: ConstraintType> {
constraints: BTreeMap<T::ID, T::Evaluated>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
context: ConstraintContextStore<T::ID>,
}
impl<T: ConstraintType> std::ops::Deref for EvaluatedCollection<T> {
type Target = BTreeMap<T::ID, T::Evaluated>;
fn deref(&self) -> &Self::Target {
&self.constraints
}
}
impl<T: ConstraintType> Default for EvaluatedCollection<T> {
fn default() -> Self {
Self {
constraints: BTreeMap::new(),
removed_reasons: BTreeMap::new(),
context: ConstraintContextStore::default(),
}
}
}
impl<T: ConstraintType> EvaluatedCollection<T> {
pub fn new(
constraints: BTreeMap<T::ID, T::Evaluated>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
) -> crate::Result<Self> {
validate_removed_reasons_reference_entries(&constraints, &removed_reasons)?;
Ok(Self {
constraints,
removed_reasons,
context: ConstraintContextStore::default(),
})
}
pub fn with_context(
constraints: BTreeMap<T::ID, T::Evaluated>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
context: ConstraintContextStore<T::ID>,
) -> crate::Result<Self> {
validate_removed_reasons_reference_entries(&constraints, &removed_reasons)?;
let owned_ids = constraints.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids)?;
Ok(Self {
constraints,
removed_reasons,
context,
})
}
pub fn inner(&self) -> &BTreeMap<T::ID, T::Evaluated> {
&self.constraints
}
pub(crate) fn replace_evaluated_row(
&mut self,
id: T::ID,
constraint: T::Evaluated,
) -> Option<T::Evaluated> {
match self.constraints.entry(id) {
Entry::Occupied(mut entry) => Some(entry.insert(constraint)),
Entry::Vacant(_) => None,
}
}
pub fn removed_reasons(&self) -> &BTreeMap<T::ID, RemovedReason> {
&self.removed_reasons
}
pub fn context(&self) -> &ConstraintContextStore<T::ID> {
&self.context
}
pub fn validate_context_ids(&self) -> crate::Result<()> {
let owned_ids = self.constraints.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&self.context, &owned_ids)
}
pub fn is_removed(&self, id: &T::ID) -> bool {
self.removed_reasons.contains_key(id)
}
pub fn is_empty(&self) -> bool {
self.constraints.is_empty()
}
pub fn is_feasible(&self) -> bool {
self.constraints.values().all(|c| c.is_feasible())
}
pub fn is_feasible_relaxed(&self) -> bool {
self.constraints
.iter()
.filter(|(id, _)| !self.removed_reasons.contains_key(id))
.all(|(_, c)| c.is_feasible())
}
}
impl From<EvaluatedCollection<Constraint>> for Vec<v1::EvaluatedConstraint> {
fn from(value: EvaluatedCollection<Constraint>) -> Self {
let EvaluatedCollection {
constraints,
mut removed_reasons,
mut context,
} = value;
constraints
.into_iter()
.map(|(id, constraint)| {
let context = context.remove(id);
match removed_reasons.remove(&id) {
Some(reason) => evaluated_constraint_to_v1(id, constraint, context, reason),
None => evaluated_constraint_to_v1_unremoved(id, constraint, context),
}
})
.collect()
}
}
fn evaluated_constraint_to_v1_unremoved(
id: ConstraintID,
constraint: EvaluatedConstraint,
context: ConstraintContext,
) -> v1::EvaluatedConstraint {
let label = context.label;
v1::EvaluatedConstraint {
id: id.into_inner(),
equality: constraint.equality.into(),
evaluated_value: constraint.stage.evaluated_value,
used_decision_variable_ids: constraint
.stage
.used_decision_variable_ids
.into_iter()
.map(|id| id.into_inner())
.collect(),
subscripts: label.subscripts,
parameters: label.parameters.into_iter().collect(),
name: label.name,
description: label.description,
dual_variable: constraint.stage.dual_variable,
removed_reason: None,
removed_reason_parameters: Default::default(),
}
}
fn evaluated_constraint_to_v1(
id: ConstraintID,
constraint: EvaluatedConstraint,
context: ConstraintContext,
removed_reason: RemovedReason,
) -> v1::EvaluatedConstraint {
let mut value = evaluated_constraint_to_v1_unremoved(id, constraint, context);
value.removed_reason = Some(removed_reason.reason);
value.removed_reason_parameters = removed_reason.parameters.into_iter().collect();
value
}
#[derive(Debug, Clone)]
pub struct SampledCollection<T: ConstraintType> {
constraints: BTreeMap<T::ID, T::Sampled>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
context: ConstraintContextStore<T::ID>,
}
impl<T: ConstraintType> std::ops::Deref for SampledCollection<T> {
type Target = BTreeMap<T::ID, T::Sampled>;
fn deref(&self) -> &Self::Target {
&self.constraints
}
}
impl<T: ConstraintType> Default for SampledCollection<T> {
fn default() -> Self {
Self {
constraints: BTreeMap::new(),
removed_reasons: BTreeMap::new(),
context: ConstraintContextStore::default(),
}
}
}
impl<T: ConstraintType> SampledCollection<T> {
pub fn new(
constraints: BTreeMap<T::ID, T::Sampled>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
) -> crate::Result<Self> {
validate_removed_reasons_reference_entries(&constraints, &removed_reasons)?;
Ok(Self {
constraints,
removed_reasons,
context: ConstraintContextStore::default(),
})
}
pub fn with_context(
constraints: BTreeMap<T::ID, T::Sampled>,
removed_reasons: BTreeMap<T::ID, RemovedReason>,
context: ConstraintContextStore<T::ID>,
) -> crate::Result<Self> {
validate_removed_reasons_reference_entries(&constraints, &removed_reasons)?;
let owned_ids = constraints.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&context, &owned_ids)?;
Ok(Self {
constraints,
removed_reasons,
context,
})
}
pub fn inner(&self) -> &BTreeMap<T::ID, T::Sampled> {
&self.constraints
}
pub fn validate_sample_ids(
&self,
expected: &SampleIDSet,
) -> std::result::Result<(), SampleIDSet> {
for constraint in self.constraints.values() {
constraint.validate_sample_ids(expected)?;
}
Ok(())
}
pub fn validate_used_decision_variable_ids(
&self,
decision_variable_ids: &BTreeSet<crate::VariableID>,
) -> std::result::Result<(), (T::ID, crate::VariableID)> {
for (constraint_id, constraint) in &self.constraints {
for var_id in constraint.used_decision_variable_ids() {
if !decision_variable_ids.contains(var_id) {
return Err((*constraint_id, *var_id));
}
}
}
Ok(())
}
pub fn removed_reasons(&self) -> &BTreeMap<T::ID, RemovedReason> {
&self.removed_reasons
}
pub fn context(&self) -> &ConstraintContextStore<T::ID> {
&self.context
}
pub fn validate_context_ids(&self) -> crate::Result<()> {
let owned_ids = self.constraints.keys().copied().collect::<BTreeSet<_>>();
validate_context_reference_ids(&self.context, &owned_ids)
}
pub fn is_removed(&self, id: &T::ID) -> bool {
self.removed_reasons.contains_key(id)
}
pub fn is_empty(&self) -> bool {
self.constraints.is_empty()
}
pub fn is_feasible_for(&self, sample_id: SampleID) -> bool {
self.constraints
.values()
.all(|c| c.is_feasible_for(sample_id).unwrap_or(false))
}
pub fn is_feasible_relaxed_for(&self, sample_id: SampleID) -> bool {
self.constraints
.iter()
.filter(|(id, _)| !self.removed_reasons.contains_key(id))
.all(|(_, c)| c.is_feasible_for(sample_id).unwrap_or(false))
}
}
impl From<SampledCollection<Constraint>> for Vec<v1::SampledConstraint> {
fn from(value: SampledCollection<Constraint>) -> Self {
let SampledCollection {
constraints,
mut removed_reasons,
mut context,
} = value;
constraints
.into_iter()
.map(|(id, constraint)| {
let context = context.remove(id);
match removed_reasons.remove(&id) {
Some(reason) => sampled_constraint_to_v1(id, constraint, context, reason),
None => sampled_constraint_to_v1_unremoved(id, constraint, context),
}
})
.collect()
}
}
fn sampled_constraint_to_v1_unremoved(
id: ConstraintID,
constraint: SampledConstraint,
context: ConstraintContext,
) -> v1::SampledConstraint {
let label = context.label;
let evaluated_values: v1::SampledValues = constraint.stage.evaluated_values.into();
let feasible = constraint
.stage
.feasible
.into_iter()
.map(|(id, value)| (id.into_inner(), value))
.collect();
v1::SampledConstraint {
id: id.into_inner(),
equality: constraint.equality.into(),
name: label.name,
subscripts: label.subscripts,
parameters: label.parameters.into_iter().collect(),
description: label.description,
removed_reason: None,
removed_reason_parameters: Default::default(),
evaluated_values: Some(evaluated_values),
used_decision_variable_ids: constraint
.stage
.used_decision_variable_ids
.into_iter()
.map(|id| id.into_inner())
.collect(),
feasible,
}
}
fn sampled_constraint_to_v1(
id: ConstraintID,
constraint: SampledConstraint,
context: ConstraintContext,
removed_reason: RemovedReason,
) -> v1::SampledConstraint {
let mut value = sampled_constraint_to_v1_unremoved(id, constraint, context);
value.removed_reason = Some(removed_reason.reason);
value.removed_reason_parameters = removed_reason.parameters.into_iter().collect();
value
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{coeff, constraint::ConstraintID, linear, Equality, Function, ModelingLabel};
fn removed_reason() -> RemovedReason {
RemovedReason {
reason: "test".to_string(),
parameters: Default::default(),
}
}
#[test]
fn constraint_type_aliases() {
let c = Constraint::equal_to_zero(Function::Zero);
let _: <Constraint as ConstraintType>::Created = c;
}
#[test]
fn empty_collection() {
let collection = ConstraintCollection::<Constraint>::default();
assert!(collection.active().is_empty());
assert!(collection.removed().is_empty());
}
#[test]
fn evaluate_collection() {
let mut active = BTreeMap::new();
active.insert(
ConstraintID::from(1),
Constraint::less_than_or_equal_to_zero(Function::from(linear!(1) + coeff!(-1.0))),
);
active.insert(
ConstraintID::from(2),
Constraint::equal_to_zero(Function::from(linear!(1) + coeff!(-2.0))),
);
let collection = ConstraintCollection::<Constraint>::new(active, BTreeMap::new()).unwrap();
let state = v1::State {
entries: [(1, 1.5)].into_iter().collect(),
};
let results = collection.evaluate(&state, ATol::default()).unwrap();
assert_eq!(results.len(), 2);
assert!(!results[&ConstraintID::from(1)].stage.feasible);
assert!(!results[&ConstraintID::from(2)].stage.feasible);
assert!(results.removed_reasons().is_empty());
}
#[test]
fn collection_accessors() {
let mut active = BTreeMap::new();
active.insert(
ConstraintID::from(1),
Constraint::equal_to_zero(Function::Zero),
);
let collection = ConstraintCollection::<Constraint>::new(active, BTreeMap::new()).unwrap();
assert_eq!(collection.active().len(), 1);
assert_eq!(collection.removed().len(), 0);
}
#[test]
fn collection_rejects_active_removed_overlap() {
let id = ConstraintID::from(1);
let active = BTreeMap::from([(id, Constraint::equal_to_zero(Function::Zero))]);
let removed = BTreeMap::from([(
id,
(Constraint::equal_to_zero(Function::Zero), removed_reason()),
)]);
let err = ConstraintCollection::<Constraint>::new(active, removed).unwrap_err();
assert!(err
.to_string()
.contains("appears in both active constraints and removed constraints"));
}
#[test]
fn collection_rejects_orphan_context_id() {
let id = ConstraintID::from(1);
let orphan_id = ConstraintID::from(99);
let active = BTreeMap::from([(id, Constraint::equal_to_zero(Function::Zero))]);
let mut context = ConstraintContextStore::default();
context.set_name(orphan_id, "orphan");
let err =
ConstraintCollection::<Constraint>::with_context(active, BTreeMap::new(), context)
.unwrap_err();
assert!(
err.to_string().contains("unknown constraint ID")
&& err.to_string().contains("ConstraintID(99)"),
"unexpected error: {err}"
);
}
#[test]
fn set_context_for_owner_rejects_unknown_id_without_orphan() {
let id = ConstraintID::from(1);
let orphan_id = ConstraintID::from(99);
let active = BTreeMap::from([(id, Constraint::equal_to_zero(Function::Zero))]);
let mut collection =
ConstraintCollection::<Constraint>::new(active, BTreeMap::new()).unwrap();
let context = ConstraintContext {
label: ModelingLabel {
name: Some("orphan".to_string()),
..Default::default()
},
provenance: vec![],
};
let err = collection
.set_context_for_owner(orphan_id, context, "constraint")
.unwrap_err();
assert!(
err.to_string().contains("unknown constraint ID")
&& err.to_string().contains("ConstraintID(99)"),
"unexpected error: {err}"
);
assert!(!collection.context().contains(orphan_id));
collection.validate_context_ids().unwrap();
}
#[test]
fn replace_and_remove_active_rows_preserves_context() {
let removed_id = ConstraintID::from(1);
let active_id = ConstraintID::from(2);
let active = BTreeMap::from([
(removed_id, Constraint::equal_to_zero(Function::Zero)),
(
active_id,
Constraint::equal_to_zero(Function::from(linear!(1))),
),
]);
let mut context = ConstraintContextStore::default();
context.set_name(removed_id, "original");
let mut collection =
ConstraintCollection::<Constraint>::with_context(active, BTreeMap::new(), context)
.unwrap();
let removed = collection.active().get(&removed_id).unwrap().clone();
collection
.move_active_rows_to_removed(BTreeMap::from([(
removed_id,
(removed, removed_reason()),
)]))
.unwrap();
assert!(!collection.active().contains_key(&removed_id));
assert!(collection.active().contains_key(&active_id));
assert!(collection.removed().contains_key(&removed_id));
assert_eq!(collection.context().name(removed_id), Some("original"));
collection.validate_context_ids().unwrap();
}
#[test]
fn replace_active_rows_rejects_unknown_id_before_mutation() {
let id = ConstraintID::from(1);
let orphan_id = ConstraintID::from(99);
let original = Constraint::equal_to_zero(Function::Zero);
let mut collection = ConstraintCollection::<Constraint>::new(
BTreeMap::from([(id, original.clone())]),
BTreeMap::new(),
)
.unwrap();
let err = collection
.replace_active_rows(BTreeMap::from([
(id, Constraint::less_than_or_equal_to_zero(Function::Zero)),
(orphan_id, Constraint::equal_to_zero(Function::Zero)),
]))
.unwrap_err();
assert!(
err.to_string().contains("Active constraint with ID")
&& err.to_string().contains("ConstraintID(99)"),
"unexpected error: {err}"
);
assert_eq!(collection.active().get(&id), Some(&original));
assert!(collection.removed().is_empty());
collection.validate_context_ids().unwrap();
}
#[test]
fn replace_rows_preserving_lifecycle_keeps_reasons_and_context() {
let active_id = ConstraintID::from(1);
let removed_id = ConstraintID::from(2);
let reason = removed_reason();
let mut context = ConstraintContextStore::default();
context.set_name(active_id, "active");
context.set_name(removed_id, "removed");
let mut collection = ConstraintCollection::<Constraint>::with_context(
BTreeMap::from([(active_id, Constraint::equal_to_zero(Function::Zero))]),
BTreeMap::from([(
removed_id,
(
Constraint::less_than_or_equal_to_zero(Function::Zero),
reason.clone(),
),
)]),
context,
)
.unwrap();
collection
.replace_rows_preserving_lifecycle(
BTreeMap::from([(
active_id,
Constraint::less_than_or_equal_to_zero(Function::Zero),
)]),
BTreeMap::from([(removed_id, Constraint::equal_to_zero(Function::Zero))]),
)
.unwrap();
assert_eq!(
collection.active().get(&active_id).map(|c| c.equality),
Some(Equality::LessThanOrEqualToZero)
);
assert_eq!(
collection
.removed()
.get(&removed_id)
.map(|(c, _)| c.equality),
Some(Equality::EqualToZero)
);
assert_eq!(
collection.removed().get(&removed_id).map(|(_, r)| r),
Some(&reason)
);
assert_eq!(collection.context().name(active_id), Some("active"));
assert_eq!(collection.context().name(removed_id), Some("removed"));
collection.validate_context_ids().unwrap();
}
#[test]
fn replace_rows_preserving_lifecycle_rejects_unknown_removed_id_before_mutation() {
let active_id = ConstraintID::from(1);
let removed_id = ConstraintID::from(2);
let orphan_id = ConstraintID::from(99);
let original_active = Constraint::equal_to_zero(Function::Zero);
let original_removed = Constraint::less_than_or_equal_to_zero(Function::Zero);
let reason = removed_reason();
let mut collection = ConstraintCollection::<Constraint>::new(
BTreeMap::from([(active_id, original_active.clone())]),
BTreeMap::from([(removed_id, (original_removed.clone(), reason.clone()))]),
)
.unwrap();
let err = collection
.replace_rows_preserving_lifecycle(
BTreeMap::from([(
active_id,
Constraint::less_than_or_equal_to_zero(Function::Zero),
)]),
BTreeMap::from([(orphan_id, Constraint::equal_to_zero(Function::Zero))]),
)
.unwrap_err();
assert!(
err.to_string().contains("Removed constraint with ID")
&& err.to_string().contains("ConstraintID(99)"),
"unexpected error: {err}"
);
assert_eq!(collection.active().get(&active_id), Some(&original_active));
assert_eq!(
collection.removed().get(&removed_id),
Some(&(original_removed, reason))
);
collection.validate_context_ids().unwrap();
}
#[test]
fn restore_removed_row_preserves_context() {
let id = ConstraintID::from(1);
let removed = Constraint::less_than_or_equal_to_zero(Function::Zero);
let mut context = ConstraintContextStore::default();
context.set_name(id, "restored");
let mut collection = ConstraintCollection::<Constraint>::with_context(
BTreeMap::new(),
BTreeMap::from([(id, (removed.clone(), removed_reason()))]),
context,
)
.unwrap();
let mut restored = removed.clone();
restored.equality = Equality::EqualToZero;
let reason = collection.restore_removed_row(id, restored).unwrap();
assert_eq!(reason.reason, "test");
assert!(collection
.active()
.get(&id)
.is_some_and(|c| c.equality == Equality::EqualToZero));
assert!(!collection.removed().contains_key(&id));
assert_eq!(collection.context().name(id), Some("restored"));
collection.validate_context_ids().unwrap();
}
#[test]
fn insert_active_with_context_rejects_duplicate_ids() {
let id = ConstraintID::from(1);
let mut collection = ConstraintCollection::<Constraint>::new(
BTreeMap::from([(id, Constraint::equal_to_zero(Function::Zero))]),
BTreeMap::new(),
)
.unwrap();
let err = collection
.insert_active_with_context(
id,
Constraint::equal_to_zero(Function::Zero),
ConstraintContext::default(),
)
.unwrap_err();
assert!(err.to_string().contains("already exists in the active"));
let removed_id = ConstraintID::from(2);
let mut collection = ConstraintCollection::<Constraint>::new(
BTreeMap::new(),
BTreeMap::from([(
removed_id,
(Constraint::equal_to_zero(Function::Zero), removed_reason()),
)]),
)
.unwrap();
let err = collection
.insert_active_with_context(
removed_id,
Constraint::equal_to_zero(Function::Zero),
ConstraintContext::default(),
)
.unwrap_err();
assert!(err.to_string().contains("already exists in the removed"));
}
#[test]
fn evaluated_collection_rejects_removed_reason_without_constraint() {
let constraints: BTreeMap<ConstraintID, EvaluatedConstraint> = BTreeMap::new();
let removed_reasons = BTreeMap::from([(ConstraintID::from(1), removed_reason())]);
let err = EvaluatedCollection::<Constraint>::new(constraints, removed_reasons).unwrap_err();
assert!(err
.to_string()
.contains("Removed reason references unknown constraint ID"));
}
#[test]
fn sampled_collection_rejects_removed_reason_without_constraint() {
let constraints: BTreeMap<ConstraintID, SampledConstraint> = BTreeMap::new();
let removed_reasons = BTreeMap::from([(ConstraintID::from(1), removed_reason())]);
let err = SampledCollection::<Constraint>::new(constraints, removed_reasons).unwrap_err();
assert!(err
.to_string()
.contains("Removed reason references unknown constraint ID"));
}
#[test]
fn parse_v2_created_collection_orphan_context_reports_contexts_field() {
let err = crate::v2::RegularConstraintCollection {
contexts: [(
1,
crate::v2::ConstraintContext {
label: Some(crate::v2::ModelingLabel {
name: Some("orphan".to_string()),
..Default::default()
}),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
}
.parse(&())
.unwrap_err();
assert!(
err.to_string().contains("[contexts]"),
"unexpected error: {err}"
);
}
#[test]
fn parse_v2_evaluated_collection_orphan_context_reports_contexts_field() {
let atol = ATol::default();
let err = crate::v2::EvaluatedRegularConstraintCollection {
contexts: [(
1,
crate::v2::ConstraintContext {
label: Some(crate::v2::ModelingLabel {
name: Some("orphan".to_string()),
..Default::default()
}),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
}
.parse(&atol)
.unwrap_err();
assert!(
err.to_string().contains("[contexts]"),
"unexpected error: {err}"
);
}
#[test]
fn parse_v2_sampled_collection_orphan_context_reports_contexts_field() {
let atol = ATol::default();
let err = crate::v2::SampledRegularConstraintCollection {
contexts: [(
1,
crate::v2::ConstraintContext {
label: Some(crate::v2::ModelingLabel {
name: Some("orphan".to_string()),
..Default::default()
}),
..Default::default()
},
)]
.into_iter()
.collect(),
..Default::default()
}
.parse(&atol)
.unwrap_err();
assert!(
err.to_string().contains("[contexts]"),
"unexpected error: {err}"
);
}
}