Skip to main content

zincite_check/
set_globals.rs

1//! Compact aggregate, ordered and weighted set relations and scalar outputs.
2use super::*;
3use std::collections::BTreeMap;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum SetAggregate {
8    Union,
9    Intersection,
10    Partition,
11    SequenceUnion,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub(crate) enum SetGlobalName {
16    Aggregate(SetAggregate),
17    Convex,
18    Hull,
19    Sequence,
20    AtMostOne,
21    Weights,
22    Extremum { maximum: bool, mode: RelationMode },
23}
24impl SetGlobalName {
25    pub fn from_name(name: &str) -> Option<Self> {
26        // Registry::add prefixes each literal root exactly once.
27        for base in [
28            name,
29            name.strip_prefix("gecode_").unwrap_or(""),
30            name.strip_prefix("fzn_").unwrap_or(""),
31        ] {
32            let found = match base {
33                "array_set_union" => Self::Aggregate(SetAggregate::Union),
34                "array_set_partition" => Self::Aggregate(SetAggregate::Partition),
35                "array_set_seq_union" => Self::Aggregate(SetAggregate::SequenceUnion),
36                "gecode_array_set_intersect" => Self::Aggregate(SetAggregate::Intersection),
37                "set_convex" => Self::Convex,
38                "array_set_seq" => Self::Sequence,
39                "gecode_set_convex_hull" => Self::Hull,
40                "gecode_set_atmost_one" => Self::AtMostOne,
41                "gecode_set_weights" => Self::Weights,
42                "gecode_set_min" => Self::Extremum {
43                    maximum: false,
44                    mode: RelationMode::Plain,
45                },
46                "gecode_set_max" => Self::Extremum {
47                    maximum: true,
48                    mode: RelationMode::Plain,
49                },
50                "gecode_set_min_reif" => Self::Extremum {
51                    maximum: false,
52                    mode: RelationMode::Reified,
53                },
54                "gecode_set_max_reif" => Self::Extremum {
55                    maximum: true,
56                    mode: RelationMode::Reified,
57                },
58                "gecode_set_min_imp" => Self::Extremum {
59                    maximum: false,
60                    mode: RelationMode::Implied,
61                },
62                "gecode_set_max_imp" => Self::Extremum {
63                    maximum: true,
64                    mode: RelationMode::Implied,
65                },
66                _ => continue,
67            };
68            return Some(found);
69        }
70        None
71    }
72    #[cfg(feature = "prepare")]
73    pub fn signature(self) -> Vec<prepare::ArgumentType> {
74        let scalar = |kind, is_var| prepare::ArgumentType {
75            kind,
76            is_var,
77            is_array: false,
78        };
79        let array = |kind, is_var| prepare::ArgumentType {
80            kind,
81            is_var,
82            is_array: true,
83        };
84        let set = scalar(ValueType::IntegerSet, true);
85        let sets = array(ValueType::IntegerSet, true);
86        match self {
87            Self::Aggregate(_) => vec![sets, set],
88            Self::Convex => vec![set],
89            Self::Hull => vec![set.clone(), set],
90            Self::Sequence => vec![sets],
91            Self::AtMostOne => vec![sets, scalar(ValueType::Integer, false)],
92            Self::Weights => vec![
93                array(ValueType::Integer, false),
94                array(ValueType::Integer, false),
95                set,
96                scalar(ValueType::Integer, true),
97            ],
98            Self::Extremum { mode, .. } => {
99                let mut args = vec![set, scalar(ValueType::Integer, true)];
100                if mode != RelationMode::Plain {
101                    args.push(scalar(ValueType::Boolean, true));
102                }
103                args
104            }
105        }
106    }
107}
108
109#[derive(Debug, Serialize, Deserialize)]
110pub(crate) enum SetGlobal {
111    Aggregate {
112        operation: SetAggregate,
113        entries: Vec<Operand>,
114        result: Operand,
115    },
116    Convex {
117        set: Operand,
118    },
119    Hull {
120        set: Operand,
121        result: Operand,
122    },
123    Sequence {
124        entries: Vec<Operand>,
125    },
126    AtMostOne {
127        entries: Vec<Operand>,
128        #[serde(
129            serialize_with = "serialize_integer",
130            deserialize_with = "deserialize_integer"
131        )]
132        cardinality: i64,
133    },
134    Weights {
135        #[serde(
136            serialize_with = "serialize_integers",
137            deserialize_with = "deserialize_integers"
138        )]
139        elements: Vec<i64>,
140        #[serde(
141            serialize_with = "serialize_integers",
142            deserialize_with = "deserialize_integers"
143        )]
144        weights: Vec<i64>,
145        set: Operand,
146        result: Operand,
147    },
148    Extremum {
149        set: Operand,
150        integer: Operand,
151        maximum: bool,
152        condition: RelationCondition,
153    },
154}
155
156/// Array positions are one-based; unavailable values carry their variable reference.
157#[derive(Clone, Debug, Serialize)]
158pub struct SetArgument {
159    pub position: Option<usize>,
160    pub value: Option<ArgumentValue>,
161    pub unresolved: Option<Box<VariableRef>>,
162}
163#[derive(Clone, Debug, Serialize)]
164pub struct SetPair {
165    pub first: usize,
166    pub second: usize,
167}
168#[derive(Clone, Debug, Serialize)]
169#[serde(tag = "kind", rename_all = "snake_case")]
170pub enum SetCardinalityFault {
171    Cardinality {
172        position: usize,
173        #[serde(serialize_with = "serialize_integer")]
174        actual: i64,
175    },
176    Intersection {
177        first: usize,
178        second: usize,
179        #[serde(serialize_with = "serialize_integer")]
180        actual: i64,
181    },
182}
183#[derive(Clone, Debug, Serialize)]
184pub struct SetWeight {
185    pub position: usize,
186    #[serde(serialize_with = "serialize_integer")]
187    pub element: i64,
188    #[serde(serialize_with = "serialize_integer")]
189    pub weight: i64,
190}
191#[derive(Clone, Debug, Serialize)]
192#[serde(tag = "operation", rename_all = "snake_case")]
193pub enum SetGlobalFinding {
194    Aggregate {
195        aggregate: SetAggregate,
196        entries: Vec<SetArgument>,
197        result: SetArgument,
198        expected: Option<IntegerSet>,
199        conflicts: Vec<SetPair>,
200    },
201    Convex {
202        set: SetArgument,
203        hull: IntegerSet,
204    },
205    Hull {
206        set: SetArgument,
207        result: SetArgument,
208        expected: IntegerSet,
209    },
210    Sequence {
211        entries: Vec<SetArgument>,
212        conflicts: Vec<SetPair>,
213    },
214    AtMostOne {
215        entries: Vec<SetArgument>,
216        #[serde(serialize_with = "serialize_integer")]
217        cardinality: i64,
218        faults: Vec<SetCardinalityFault>,
219    },
220    Weights {
221        rows: Vec<SetWeight>,
222        set: SetArgument,
223        result: SetArgument,
224        uncovered: IntegerSet,
225        #[serde(serialize_with = "serialize_optional_integer")]
226        expected: Option<i64>,
227    },
228    Extremum {
229        set: SetArgument,
230        integer: SetArgument,
231        maximum: bool,
232        #[serde(serialize_with = "serialize_optional_integer")]
233        expected: Option<i64>,
234        truth: bool,
235        condition: RelationConditionValue,
236    },
237}
238
239impl SetGlobal {
240    pub fn name(&self) -> SetGlobalName {
241        match self {
242            Self::Aggregate { operation, .. } => SetGlobalName::Aggregate(*operation),
243            Self::Convex { .. } => SetGlobalName::Convex,
244            Self::Hull { .. } => SetGlobalName::Hull,
245            Self::Sequence { .. } => SetGlobalName::Sequence,
246            Self::AtMostOne { .. } => SetGlobalName::AtMostOne,
247            Self::Weights { .. } => SetGlobalName::Weights,
248            Self::Extremum {
249                maximum, condition, ..
250            } => SetGlobalName::Extremum {
251                maximum: *maximum,
252                mode: condition.mode(),
253            },
254        }
255    }
256    pub fn validate_shape(&self) -> Result<(), &'static str> {
257        match self {
258            Self::AtMostOne { cardinality, .. } if *cardinality < 0 => {
259                Err("set cardinality must be nonnegative")
260            }
261            Self::Weights {
262                elements, weights, ..
263            } if elements.len() != weights.len() => {
264                Err("set element and weight arrays must have equal lengths")
265            }
266            _ => Ok(()),
267        }
268    }
269    pub fn operands(&self) -> Vec<&Operand> {
270        match self {
271            Self::Aggregate {
272                entries, result, ..
273            } => entries.iter().chain(std::iter::once(result)).collect(),
274            Self::Convex { set } => vec![set],
275            Self::Hull { set, result } | Self::Weights { set, result, .. } => vec![set, result],
276            Self::Sequence { entries } | Self::AtMostOne { entries, .. } => {
277                entries.iter().collect()
278            }
279            Self::Extremum {
280                set,
281                integer,
282                condition,
283                ..
284            } => [set, integer]
285                .into_iter()
286                .chain(condition.operand())
287                .collect(),
288        }
289    }
290    pub fn inactive(&self, values: &[Option<Value>]) -> bool {
291        matches!(self, Self::Extremum { condition, .. } if condition.inactive(values))
292    }
293    pub fn required_operands(&self, values: &[Option<Value>]) -> Vec<&Operand> {
294        match self {
295            Self::Aggregate {
296                operation, entries, ..
297            } if !aggregate_conflicts(*operation, entries, values).is_empty() => Vec::new(),
298            Self::Sequence { entries }
299                if entries.len() < 2 || !sequence_conflicts(entries, values).is_empty() =>
300            {
301                Vec::new()
302            }
303            Self::AtMostOne {
304                entries,
305                cardinality,
306            } if entries.iter().any(|entry| {
307                entry
308                    .value(values)
309                    .is_some_and(|value| value.set().cardinality() != Some(*cardinality))
310            }) || has_intersection_fault(entries, values) =>
311            {
312                Vec::new()
313            }
314            Self::Weights { elements, set, .. }
315                if set
316                    .value(values)
317                    .is_some_and(|value| !uncovered(elements, value.set()).is_empty()) =>
318            {
319                vec![set]
320            }
321            Self::Extremum { set, condition, .. }
322                if set
323                    .value(values)
324                    .is_some_and(|value| value.set().is_empty()) =>
325            {
326                std::iter::once(set).chain(condition.operand()).collect()
327            }
328            _ => self.operands(),
329        }
330    }
331    fn output(&self) -> Option<&Operand> {
332        match self {
333            Self::Aggregate { result, .. }
334            | Self::Hull { result, .. }
335            | Self::Weights { result, .. } => Some(result),
336            Self::Extremum {
337                integer,
338                condition: RelationCondition::Plain,
339                ..
340            } => Some(integer),
341            Self::Extremum {
342                condition: RelationCondition::Reified { result },
343                ..
344            } => Some(result),
345            _ => None,
346        }
347    }
348    pub fn can_define(&self, output: usize) -> bool {
349        matches!(self.output(), Some(Operand::Variable(slot)) if *slot == output)
350            && self
351                .operands()
352                .iter()
353                .filter(|operand| matches!(operand, Operand::Variable(slot) if *slot == output))
354                .count()
355                == 1
356    }
357    pub fn derive(
358        &self,
359        values: &[Option<Value>],
360        constraint: &Constraint,
361    ) -> Result<Option<Value>, CheckError> {
362        Ok(match self {
363            Self::Aggregate {
364                operation, entries, ..
365            } => aggregate(*operation, entries, values).map(Value::IntegerSet),
366            Self::Hull { set, .. } => set
367                .value(values)
368                .map(|value| Value::IntegerSet(value.set().hull())),
369            Self::Weights {
370                elements,
371                weights,
372                set,
373                ..
374            } => {
375                let Some(set) = set.value(values) else {
376                    return Ok(None);
377                };
378                weighted_sum(elements, weights, set.set(), constraint)?.map(Value::Integer)
379            }
380            Self::Extremum {
381                set,
382                integer,
383                maximum,
384                condition,
385            } => {
386                let Some(set) = set.value(values) else {
387                    return Ok(None);
388                };
389                let best = extremum(set.set(), *maximum);
390                match condition {
391                    RelationCondition::Plain => best.map(Value::Integer),
392                    RelationCondition::Reified { .. } => {
393                        let truth = match best {
394                            None => Some(false),
395                            Some(best) => {
396                                integer.value(values).map(|value| value.integer() == best)
397                            }
398                        };
399                        truth.map(Value::Boolean)
400                    }
401                    RelationCondition::Implied { .. } => None,
402                }
403            }
404            _ => None,
405        })
406    }
407}
408fn aggregate(
409    operation: SetAggregate,
410    entries: &[Operand],
411    values: &[Option<Value>],
412) -> Option<IntegerSet> {
413    let mut concrete = entries
414        .iter()
415        .map(|entry| entry.value(values).map(Value::set));
416    let mut result = if operation == SetAggregate::Intersection {
417        match concrete.next() {
418            Some(first) => first?.clone(),
419            None => IntegerSet::interval(GECODE_SET_MIN, GECODE_SET_MAX),
420        }
421    } else {
422        IntegerSet::empty()
423    };
424    for set in concrete {
425        result = if operation == SetAggregate::Intersection {
426            result.intersection(set?)
427        } else {
428            result.union(set?)
429        };
430    }
431    Some(result)
432}
433fn aggregate_conflicts(
434    operation: SetAggregate,
435    entries: &[Operand],
436    values: &[Option<Value>],
437) -> Vec<SetPair> {
438    match operation {
439        SetAggregate::Partition => pairs(entries, values, |a, b| !a.is_disjoint(b)),
440        SetAggregate::SequenceUnion => sequence_conflicts(entries, values),
441        _ => Vec::new(),
442    }
443}
444fn pairs(
445    entries: &[Operand],
446    values: &[Option<Value>],
447    conflict: impl Fn(&IntegerSet, &IntegerSet) -> bool,
448) -> Vec<SetPair> {
449    let mut conflicts = Vec::new();
450    for (i, entry) in entries.iter().enumerate() {
451        let Some(a) = entry.value(values) else {
452            continue;
453        };
454        for (j, other) in entries.iter().enumerate().skip(i + 1) {
455            if let Some(b) = other.value(values)
456                && conflict(a.set(), b.set())
457            {
458                conflicts.push(SetPair {
459                    first: i + 1,
460                    second: j + 1,
461                });
462            }
463        }
464    }
465    conflicts
466}
467fn sequence_conflicts(entries: &[Operand], values: &[Option<Value>]) -> Vec<SetPair> {
468    pairs(entries, values, |a, b| match (a.maximum(), b.minimum()) {
469        (Some(a), Some(b)) => a >= b,
470        _ => false,
471    })
472}
473fn has_intersection_fault(entries: &[Operand], values: &[Option<Value>]) -> bool {
474    !pairs(entries, values, |a, b| {
475        a.intersection(b).cardinality().is_none_or(|size| size > 1)
476    })
477    .is_empty()
478}
479fn cardinality_faults(
480    entries: &[Operand],
481    cardinality: i64,
482    values: &[Option<Value>],
483    constraint: &Constraint,
484) -> Result<Vec<SetCardinalityFault>, CheckError> {
485    let size = |set: &IntegerSet| {
486        set.cardinality()
487            .ok_or_else(|| constraint.execution_error("set cardinality exceeds i64::MAX"))
488    };
489    let mut faults = Vec::new();
490    for (i, entry) in entries.iter().enumerate() {
491        let Some(a) = entry.value(values) else {
492            continue;
493        };
494        let actual = size(a.set())?;
495        if actual != cardinality {
496            faults.push(SetCardinalityFault::Cardinality {
497                position: i + 1,
498                actual,
499            });
500        }
501        for (j, other) in entries.iter().enumerate().skip(i + 1) {
502            if let Some(b) = other.value(values) {
503                let actual = size(&a.set().intersection(b.set()))?;
504                if actual > 1 {
505                    faults.push(SetCardinalityFault::Intersection {
506                        first: i + 1,
507                        second: j + 1,
508                        actual,
509                    });
510                }
511            }
512        }
513    }
514    Ok(faults)
515}
516fn extremum(set: &IntegerSet, maximum: bool) -> Option<i64> {
517    if maximum {
518        set.maximum()
519    } else {
520        set.minimum()
521    }
522}
523fn uncovered(elements: &[i64], selected: &IntegerSet) -> IntegerSet {
524    selected.difference(&IntegerSet::from_values(elements.iter().copied()))
525}
526fn weighted_sum(
527    elements: &[i64],
528    weights: &[i64],
529    selected: &IntegerSet,
530    constraint: &Constraint,
531) -> Result<Option<i64>, CheckError> {
532    if !uncovered(elements, selected).is_empty() {
533        return Ok(None);
534    }
535    // First original row wins; key order fixes checked intermediate arithmetic.
536    let mut mapping = BTreeMap::new();
537    for (&element, &weight) in elements.iter().zip(weights) {
538        mapping.entry(element).or_insert(weight);
539    }
540    let mut sum = 0i64;
541    for (element, weight) in mapping {
542        if selected.contains(element) {
543            sum = sum.checked_add(weight).ok_or_else(|| {
544                constraint.execution_error("checked i64 overflow in set weighted sum")
545            })?;
546        }
547    }
548    Ok(Some(sum))
549}
550impl PreparedModel {
551    fn set_argument(
552        &self,
553        operand: &Operand,
554        position: Option<usize>,
555        values: &[Option<Value>],
556    ) -> SetArgument {
557        SetArgument {
558            position,
559            value: self.argument(operand, values),
560            unresolved: match operand {
561                Operand::Variable(slot) if values[*slot].is_none() => {
562                    Some(Box::new(self.slots[*slot].reference.clone()))
563                }
564                _ => None,
565            },
566        }
567    }
568    pub(crate) fn evaluate_set_global(
569        &self,
570        constraint: &Constraint,
571        global: &SetGlobal,
572        values: &[Option<Value>],
573    ) -> Result<Option<ViolationKind>, CheckError> {
574        let argument = |operand: &Operand| self.set_argument(operand, None, values);
575        let arguments = |entries: &[Operand]| {
576            entries
577                .iter()
578                .enumerate()
579                .map(|(i, entry)| self.set_argument(entry, Some(i + 1), values))
580                .collect()
581        };
582        let finding = match global {
583            SetGlobal::Aggregate {
584                operation,
585                entries,
586                result,
587            } => {
588                let expected = aggregate(*operation, entries, values);
589                let conflicts = aggregate_conflicts(*operation, entries, values);
590                let mismatch = expected
591                    .as_ref()
592                    .zip(result.value(values))
593                    .is_some_and(|(expected, actual)| expected != actual.set());
594                if !mismatch && conflicts.is_empty() {
595                    return Ok(None);
596                }
597                SetGlobalFinding::Aggregate {
598                    aggregate: *operation,
599                    entries: arguments(entries),
600                    result: argument(result),
601                    expected,
602                    conflicts,
603                }
604            }
605            SetGlobal::Convex { set } => {
606                let value = set.value(values).expect("required convex set").set();
607                if value.is_convex() {
608                    return Ok(None);
609                }
610                SetGlobalFinding::Convex {
611                    set: argument(set),
612                    hull: value.hull(),
613                }
614            }
615            SetGlobal::Hull { set, result } => {
616                let expected = set.value(values).expect("required hull set").set().hull();
617                if result.value(values).expect("required hull result").set() == &expected {
618                    return Ok(None);
619                }
620                SetGlobalFinding::Hull {
621                    set: argument(set),
622                    result: argument(result),
623                    expected,
624                }
625            }
626            SetGlobal::Sequence { entries } => {
627                let conflicts = sequence_conflicts(entries, values);
628                if conflicts.is_empty() {
629                    return Ok(None);
630                }
631                SetGlobalFinding::Sequence {
632                    entries: arguments(entries),
633                    conflicts,
634                }
635            }
636            SetGlobal::AtMostOne {
637                entries,
638                cardinality,
639            } => {
640                let faults = cardinality_faults(entries, *cardinality, values, constraint)?;
641                if faults.is_empty() {
642                    return Ok(None);
643                }
644                SetGlobalFinding::AtMostOne {
645                    entries: arguments(entries),
646                    cardinality: *cardinality,
647                    faults,
648                }
649            }
650            SetGlobal::Weights {
651                elements,
652                weights,
653                set,
654                result,
655            } => {
656                let selected = set.value(values).expect("required weighted set").set();
657                let missing = uncovered(elements, selected);
658                let expected = weighted_sum(elements, weights, selected, constraint)?;
659                let mismatch = expected
660                    .zip(result.value(values))
661                    .is_some_and(|(expected, actual)| expected != actual.integer());
662                if missing.is_empty() && !mismatch {
663                    return Ok(None);
664                }
665                let rows = elements
666                    .iter()
667                    .zip(weights)
668                    .enumerate()
669                    .map(|(i, (&element, &weight))| SetWeight {
670                        position: i + 1,
671                        element,
672                        weight,
673                    })
674                    .collect();
675                SetGlobalFinding::Weights {
676                    rows,
677                    set: argument(set),
678                    result: argument(result),
679                    uncovered: missing,
680                    expected,
681                }
682            }
683            SetGlobal::Extremum {
684                set,
685                integer,
686                maximum,
687                condition,
688            } => {
689                let expected = extremum(
690                    set.value(values).expect("required extremum set").set(),
691                    *maximum,
692                );
693                let truth = expected.is_some_and(|best| {
694                    integer
695                        .value(values)
696                        .expect("required nonempty extremum integer")
697                        .integer()
698                        == best
699                });
700                if condition.satisfied(truth, values) {
701                    return Ok(None);
702                }
703                SetGlobalFinding::Extremum {
704                    set: argument(set),
705                    integer: argument(integer),
706                    maximum: *maximum,
707                    expected,
708                    truth,
709                    condition: self.condition_value(condition, values),
710                }
711            }
712        };
713        Ok(Some(ViolationKind::SetGlobal { finding }))
714    }
715}