Skip to main content

zincite_check/
packing.rs

1//! Bin loads over fixed weights and concrete bin labels.
2use super::*;
3
4pub(crate) fn is_bin_packing_name(name: &str) -> bool {
5    [
6        name,
7        name.strip_prefix("gecode_").unwrap_or(""),
8        name.strip_prefix("fzn_").unwrap_or(""),
9    ]
10    .contains(&"gecode_bin_packing_load")
11}
12
13#[cfg(feature = "prepare")]
14pub(crate) fn bin_packing_signature() -> Vec<prepare::ArgumentType> {
15    vec![
16        prepare::ArgumentType {
17            kind: ValueType::Integer,
18            is_array: true,
19            is_var: true,
20        },
21        prepare::ArgumentType {
22            kind: ValueType::Integer,
23            is_array: true,
24            is_var: true,
25        },
26        prepare::ArgumentType {
27            kind: ValueType::Integer,
28            is_array: true,
29            is_var: false,
30        },
31        prepare::ArgumentType {
32            kind: ValueType::Integer,
33            is_array: false,
34            is_var: false,
35        },
36    ]
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub(crate) struct BinPacking {
41    pub loads: Vec<Operand>,
42    pub bins: Vec<Operand>,
43    #[serde(
44        serialize_with = "serialize_integers",
45        deserialize_with = "deserialize_integers"
46    )]
47    pub weights: Vec<i64>,
48    #[serde(
49        serialize_with = "serialize_integer",
50        deserialize_with = "deserialize_integer"
51    )]
52    pub minimum: i64,
53}
54
55#[derive(Clone, Debug, Serialize)]
56pub struct PackingItem {
57    pub position: usize,
58    pub bin: Option<ArgumentValue>,
59    #[serde(serialize_with = "serialize_integer")]
60    pub weight: i64,
61}
62#[derive(Clone, Debug, Serialize)]
63pub struct PackingLoad {
64    pub position: usize,
65    #[serde(serialize_with = "serialize_integer")]
66    pub bin: i64,
67    pub supplied: Option<ArgumentValue>,
68    #[serde(serialize_with = "serialize_optional_integer")]
69    pub calculated: Option<i64>,
70    pub items: Vec<usize>,
71}
72#[derive(Clone, Debug, Serialize)]
73#[serde(tag = "kind", rename_all = "snake_case")]
74pub enum BinPackingFault {
75    Bin {
76        position: usize,
77        #[serde(serialize_with = "serialize_integer")]
78        value: i64,
79    },
80    Load {
81        position: usize,
82    },
83}
84#[derive(Clone, Debug, Serialize)]
85pub struct BinPackingFinding {
86    #[serde(serialize_with = "serialize_integer")]
87    pub minimum: i64,
88    pub items: Vec<PackingItem>,
89    pub loads: Vec<PackingLoad>,
90    pub faults: Vec<BinPackingFault>,
91}
92
93impl BinPacking {
94    pub fn validate(&self) -> Result<(), &'static str> {
95        if self.bins.len() != self.weights.len() {
96            return Err("bin and weight arrays must have equal lengths");
97        }
98        if self.weights.iter().any(|&weight| weight < 0) {
99            return Err("bin weights must be fixed nonnegative integers");
100        }
101        if !self.loads.is_empty()
102            && i64::try_from(self.loads.len() - 1)
103                .ok()
104                .and_then(|last| self.minimum.checked_add(last))
105                .is_none()
106        {
107            return Err("bin label interval exceeds i64");
108        }
109        Ok(())
110    }
111    pub fn operands(&self) -> Vec<&Operand> {
112        self.loads.iter().chain(&self.bins).collect()
113    }
114    fn bin_index(&self, bin: i64) -> Option<usize> {
115        bin.checked_sub(self.minimum)
116            .and_then(|offset| usize::try_from(offset).ok())
117            .filter(|&offset| offset < self.loads.len())
118    }
119    fn bin_faults(&self, values: &[Option<Value>]) -> Vec<BinPackingFault> {
120        self.bins
121            .iter()
122            .enumerate()
123            .filter_map(|(i, bin)| {
124                let value = bin.value(values)?.integer();
125                self.bin_index(value)
126                    .is_none()
127                    .then_some(BinPackingFault::Bin {
128                        position: i + 1,
129                        value,
130                    })
131            })
132            .collect()
133    }
134    pub fn required_operands(&self, values: &[Option<Value>]) -> Vec<&Operand> {
135        if self.bin_faults(values).is_empty() {
136            self.operands()
137        } else {
138            Vec::new()
139        }
140    }
141    fn calculate(
142        &self,
143        values: &[Option<Value>],
144        constraint: &Constraint,
145    ) -> Result<Option<Vec<i64>>, CheckError> {
146        let mut loads = vec![0_i64; self.loads.len()];
147        for (bin, &weight) in self.bins.iter().zip(&self.weights) {
148            let Some(bin) = bin.value(values) else {
149                return Ok(None);
150            };
151            if let Some(position) = self.bin_index(bin.integer()) {
152                loads[position] = loads[position].checked_add(weight).ok_or_else(|| {
153                    constraint.execution_error("checked i64 overflow in bin load sum")
154                })?;
155            }
156        }
157        Ok(Some(loads))
158    }
159    pub fn can_define(&self, output: usize) -> bool {
160        let matches =
161            |operand: &Operand| matches!(operand, Operand::Variable(slot) if *slot == output);
162        self.loads.iter().any(matches) && !self.bins.iter().any(matches)
163    }
164    pub fn derive(
165        &self,
166        output: usize,
167        values: &[Option<Value>],
168        constraint: &Constraint,
169    ) -> Result<Option<Value>, CheckError> {
170        let Some(loads) = self.calculate(values, constraint)? else {
171            return Ok(None);
172        };
173        let mut result = None;
174        for (operand, load) in self.loads.iter().zip(loads) {
175            if matches!(operand, Operand::Variable(slot) if *slot == output) {
176                if result.is_some_and(|prior| prior != load) {
177                    return Ok(None);
178                }
179                result = Some(load);
180            }
181        }
182        Ok(result.map(Value::Integer))
183    }
184}
185impl PreparedModel {
186    pub(crate) fn evaluate_packing(
187        &self,
188        constraint: &Constraint,
189        packing: &BinPacking,
190        values: &[Option<Value>],
191    ) -> Result<Option<ViolationKind>, CheckError> {
192        let mut faults = packing.bin_faults(values);
193        let calculated = packing.calculate(values, constraint)?;
194        for (i, load) in packing.loads.iter().enumerate() {
195            if let (Some(supplied), Some(calculated)) = (load.value(values), &calculated)
196                && supplied.integer() != calculated[i]
197            {
198                faults.push(BinPackingFault::Load { position: i + 1 });
199            }
200        }
201        if faults.is_empty() {
202            return Ok(None);
203        }
204        let loads = packing
205            .loads
206            .iter()
207            .enumerate()
208            .map(|(i, load)| {
209                let supplied = self.argument(load, values);
210                let calculated = calculated.as_ref().map(|loads| loads[i]);
211                let bin = packing.minimum + i64::try_from(i).expect("validated bin interval");
212                PackingLoad {
213                    position: i + 1,
214                    bin,
215                    supplied,
216                    calculated,
217                    items: packing
218                        .bins
219                        .iter()
220                        .enumerate()
221                        .filter_map(|(j, operand)| {
222                            (operand.value(values) == Some(&Value::Integer(bin))).then_some(j + 1)
223                        })
224                        .collect(),
225                }
226            })
227            .collect();
228        Ok(Some(ViolationKind::BinPacking {
229            finding: BinPackingFinding {
230                minimum: packing.minimum,
231                items: packing
232                    .bins
233                    .iter()
234                    .zip(&packing.weights)
235                    .enumerate()
236                    .map(|(i, (bin, &weight))| PackingItem {
237                        position: i + 1,
238                        bin: self.argument(bin, values),
239                        weight,
240                    })
241                    .collect(),
242                loads,
243                faults,
244            },
245        }))
246    }
247}