1use crate::adjacency::Adjacency;
28use crate::constraint::{ConstraintEnum, VarId};
29use crate::domain::Domain;
30use crate::ordering::{self, Ordering};
31use crate::solver::optimize::DomainCostEval;
32use crate::solver::{Solution, Trail, ac3, propagate};
33use crate::variable::Variable;
34use crate::{Pruning, SolveStats};
35
36pub const PERMANENT_DEPTH: usize = 0;
42const SEARCH_ROOT_DEPTH: usize = 1;
44
45#[derive(Debug, Clone)]
51pub struct SearchParams {
52 pub pruning: Pruning,
53 pub ordering: Ordering,
54 pub max_solutions: usize,
55 pub node_budget: Option<u64>,
58 pub cancel: Option<crate::CancelToken>,
61}
62
63enum Step {
67 Continue,
68 Done,
69}
70
71pub(crate) struct Kernel<'a, D: Domain> {
76 variables: &'a mut [Variable<D>],
77 constraints: &'a [ConstraintEnum<D>],
78 adjacency: &'a Adjacency,
79 weights: &'a mut [f64],
80 var_cids: &'a [Vec<usize>],
81 stats: &'a mut SolveStats,
82 trail: Trail,
83 worklist: ac3::BitsetWorklist,
89 params: &'a SearchParams,
90}
91
92impl<D: Domain> Kernel<'_, D>
93where
94 D::Value: PartialEq + 'static,
95{
96 #[inline]
98 fn is_valid(&self, var: VarId, assignment: &[Option<D::Value>]) -> bool {
99 for &ci in self.adjacency.constraints_for(var) {
100 let ci = ci as usize;
101 let scope = self.constraints[ci].scope();
102 if scope.iter().all(|&v| assignment[v as usize].is_some())
103 && !self.constraints[ci].check(assignment)
104 {
105 return false;
106 }
107 }
108 true
109 }
110
111 #[inline]
121 fn propagate_from(
122 &mut self,
123 var: VarId,
124 assignment: &mut Vec<Option<D::Value>>,
125 depth: usize,
126 ) -> bool {
127 match self.params.pruning {
128 Pruning::None => false,
129 Pruning::ForwardChecking => propagate::forward_check(
130 var,
131 self.variables,
132 self.constraints,
133 self.adjacency,
134 assignment.as_mut_slice(),
135 self.stats,
136 &mut self.trail,
137 depth,
138 )
139 .is_some(),
140 Pruning::Ac3 => ac3::ac3_from_variable(
141 var,
142 self.variables,
143 self.constraints,
144 self.adjacency,
145 assignment,
146 self.stats,
147 &mut self.trail,
148 &mut self.worklist,
149 depth,
150 )
151 .is_some(),
152 Pruning::AcFc => propagate::ac_fc(
153 var,
154 self.variables,
155 self.constraints,
156 self.adjacency,
157 assignment.as_mut_slice(),
158 self.stats,
159 &mut self.trail,
160 depth,
161 )
162 .is_some(),
163 }
164 }
165}
166
167trait SearchPolicy<D: Domain> {
170 fn on_leaf(&mut self, k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step;
172
173 #[inline]
175 fn node_prune(&mut self, _k: &Kernel<'_, D>, _assignment: &[Option<D::Value>]) -> bool {
176 false
177 }
178
179 #[inline]
181 fn order_values(&self, _k: &Kernel<'_, D>, _var: VarId, _values: &mut [D::Value]) {}
182}
183
184fn search<D, P>(
186 k: &mut Kernel<'_, D>,
187 p: &mut P,
188 assignment: &mut Vec<Option<D::Value>>,
189 stack: &mut Vec<VarId>,
190 depth: usize,
191) -> Step
192where
193 D: Domain,
194 D::Value: PartialEq + 'static,
195 P: SearchPolicy<D>,
196{
197 if stack.is_empty() {
198 return p.on_leaf(k, assignment);
199 }
200
201 if let Some(budget) = k.params.node_budget
204 && k.stats.nodes_explored >= budget
205 {
206 k.stats.budget_exceeded = true;
207 return Step::Done;
208 }
209
210 if let Some(tok) = &k.params.cancel
216 && tok.is_cancelled()
217 {
218 k.stats.cancelled = true;
219 return Step::Done;
220 }
221
222 k.stats.nodes_explored += 1;
223
224 if p.node_prune(k, assignment) {
225 return Step::Continue;
226 }
227
228 let idx =
229 ordering::select_variable(stack, k.variables, k.params.ordering, k.weights, k.var_cids)
230 .unwrap();
231 let var = stack.swap_remove(idx);
232
233 let mut values: Vec<_> = k.variables[var as usize].domain.iter().collect();
234 p.order_values(k, var, &mut values);
235
236 for val in values {
237 let mark = k.trail.checkpoint();
238 assignment[var as usize] = Some(val.clone());
239
240 k.variables[var as usize].restrict_to(&val, depth);
242 k.trail.push(var);
243
244 if k.is_valid(var, assignment)
245 && !k.propagate_from(var, assignment, depth)
246 && let Step::Done = search(k, p, assignment, stack, depth + 1)
247 {
248 return Step::Done;
249 }
250
251 k.stats.backtracks += 1;
252 assignment[var as usize] = None;
253 k.trail.undo_to(mark, depth, k.variables);
254 }
255
256 stack.push(var);
257 Step::Continue
258}
259
260struct Feasibility<D: Domain> {
265 solutions: Vec<Solution<D>>,
266 max_solutions: usize,
267}
268
269impl<D: Domain> SearchPolicy<D> for Feasibility<D>
270where
271 D::Value: PartialEq + 'static,
272{
273 #[inline]
274 fn on_leaf(&mut self, _k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step {
275 self.solutions.push(
276 assignment
277 .iter()
278 .map(|v| v.as_ref().unwrap().clone())
279 .collect(),
280 );
281 if self.solutions.len() >= self.max_solutions {
282 Step::Done
283 } else {
284 Step::Continue
285 }
286 }
287}
288
289#[allow(clippy::too_many_arguments)]
292pub fn feasibility_search<D: Domain>(
293 variables: &mut [Variable<D>],
294 constraints: &[ConstraintEnum<D>],
295 adjacency: &Adjacency,
296 weights: &mut [f64],
297 var_cids: &[Vec<usize>],
298 params: &SearchParams,
299 stats: &mut SolveStats,
300 given: Option<&[(VarId, D::Value)]>,
301) -> Vec<Solution<D>>
302where
303 D::Value: PartialEq + 'static,
304{
305 let num_vars = variables.len();
306 let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
307
308 let mut stack: Vec<VarId> = if let Some(given) = given {
309 for (var, val) in given {
310 assignment[*var as usize] = Some(val.clone());
311 }
312 (0..num_vars as u32)
313 .filter(|v| assignment[*v as usize].is_none())
314 .collect()
315 } else {
316 (0..num_vars as u32).collect()
317 };
318
319 let mut policy = Feasibility {
320 solutions: Vec::new(),
321 max_solutions: params.max_solutions,
322 };
323 let mut kernel = Kernel {
324 variables,
325 constraints,
326 adjacency,
327 weights,
328 var_cids,
329 stats,
330 trail: Trail::default(),
331 worklist: ac3::BitsetWorklist::new(constraints.len()),
332 params,
333 };
334
335 search(
336 &mut kernel,
337 &mut policy,
338 &mut assignment,
339 &mut stack,
340 SEARCH_ROOT_DEPTH,
341 );
342
343 policy.solutions
344}
345
346struct ScoredSolution<D: Domain> {
351 solution: Solution<D>,
352 cost: f64,
353}
354
355struct BranchBound<'e, D: Domain> {
356 scored: Vec<ScoredSolution<D>>,
357 best_cost: f64,
358 maximize: bool,
359 eval: &'e dyn DomainCostEval<D>,
360}
361
362impl<D: Domain> BranchBound<'_, D>
363where
364 D::Value: PartialEq + 'static,
365{
366 fn assignment_cost(&self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> f64 {
368 let mut cost = 0.0;
369 for (i, val) in assignment.iter().enumerate() {
370 if let Some(v) = val {
371 cost += self.eval.cost(&k.variables[i].domain, v);
372 }
373 }
374 for c in k.constraints {
375 cost += c.soft_penalty(assignment);
376 }
377 cost
378 }
379
380 fn optimistic_bound(&self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> f64 {
384 let mut bound = 0.0;
385 for (i, val) in assignment.iter().enumerate() {
386 match val {
387 Some(v) => bound += self.eval.cost(&k.variables[i].domain, v),
388 None if self.maximize => bound += self.eval.max_cost(&k.variables[i].domain),
389 None => bound += self.eval.min_cost(&k.variables[i].domain),
390 }
391 }
392 for c in k.constraints {
393 let scope = c.scope();
394 if scope.iter().all(|&v| assignment[v as usize].is_some()) {
395 bound += c.soft_penalty(assignment);
396 }
397 }
398 bound
399 }
400}
401
402impl<D: Domain> SearchPolicy<D> for BranchBound<'_, D>
403where
404 D::Value: PartialEq + 'static,
405{
406 #[inline]
407 fn on_leaf(&mut self, k: &mut Kernel<'_, D>, assignment: &[Option<D::Value>]) -> Step {
408 let cost = self.assignment_cost(k, assignment);
409 let effective = if self.maximize { -cost } else { cost };
410 if effective < self.best_cost {
411 self.best_cost = effective;
412 }
413 self.scored.push(ScoredSolution {
414 solution: assignment
415 .iter()
416 .map(|v| v.as_ref().unwrap().clone())
417 .collect(),
418 cost,
419 });
420 Step::Continue
422 }
423
424 #[inline]
425 fn node_prune(&mut self, k: &Kernel<'_, D>, assignment: &[Option<D::Value>]) -> bool {
426 let bound = self.optimistic_bound(k, assignment);
427 let effective = if self.maximize { -bound } else { bound };
428 effective >= self.best_cost
429 }
430
431 #[inline]
432 fn order_values(&self, k: &Kernel<'_, D>, var: VarId, values: &mut [D::Value]) {
433 let domain = &k.variables[var as usize].domain;
434 if self.maximize {
437 values.sort_by(|a, b| {
438 self.eval
439 .cost(domain, b)
440 .partial_cmp(&self.eval.cost(domain, a))
441 .unwrap_or(std::cmp::Ordering::Equal)
442 });
443 } else {
444 values.sort_by(|a, b| {
445 self.eval
446 .cost(domain, a)
447 .partial_cmp(&self.eval.cost(domain, b))
448 .unwrap_or(std::cmp::Ordering::Equal)
449 });
450 }
451 }
452}
453
454#[allow(clippy::too_many_arguments)]
457pub fn branch_and_bound<D: Domain>(
458 variables: &mut [Variable<D>],
459 constraints: &[ConstraintEnum<D>],
460 adjacency: &Adjacency,
461 weights: &mut [f64],
462 var_cids: &[Vec<usize>],
463 params: &SearchParams,
464 stats: &mut SolveStats,
465 maximize: bool,
466 cost_eval: &dyn DomainCostEval<D>,
467) -> Vec<Solution<D>>
468where
469 D::Value: PartialEq + 'static,
470{
471 let num_vars = variables.len();
472 let mut assignment: Vec<Option<D::Value>> = vec![None; num_vars];
473 let mut stack: Vec<VarId> = (0..num_vars as u32).collect();
474
475 let mut policy = BranchBound {
476 scored: Vec::new(),
477 best_cost: f64::INFINITY,
478 maximize,
479 eval: cost_eval,
480 };
481 let mut kernel = Kernel {
482 variables,
483 constraints,
484 adjacency,
485 weights,
486 var_cids,
487 stats,
488 trail: Trail::default(),
489 worklist: ac3::BitsetWorklist::new(constraints.len()),
490 params,
491 };
492
493 search(
494 &mut kernel,
495 &mut policy,
496 &mut assignment,
497 &mut stack,
498 SEARCH_ROOT_DEPTH,
499 );
500
501 if maximize {
503 policy.scored.sort_by(|a, b| {
504 b.cost
505 .partial_cmp(&a.cost)
506 .unwrap_or(std::cmp::Ordering::Equal)
507 });
508 } else {
509 policy.scored.sort_by(|a, b| {
510 a.cost
511 .partial_cmp(&b.cost)
512 .unwrap_or(std::cmp::Ordering::Equal)
513 });
514 }
515 policy.scored.truncate(params.max_solutions);
516 policy.scored.into_iter().map(|s| s.solution).collect()
517}