Skip to content

Commit 90ac564

Browse files
feat: using more general explanations
1 parent 6f60940 commit 90ac564

File tree

2 files changed

+231
-58
lines changed

2 files changed

+231
-58
lines changed

pumpkin-solver/src/propagators/disjunctive/disjunctive_propagator.rs

Lines changed: 164 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use crate::engine::propagation::Propagator;
1515
use crate::engine::propagation::PropagatorInitialisationContext;
1616
use crate::engine::DomainEvents;
1717
use crate::predicate;
18-
use crate::predicates::Predicate;
1918
use crate::predicates::PropositionalConjunction;
19+
use crate::pumpkin_assert_simple;
2020
use crate::variables::IntegerVariable;
2121
use crate::variables::TransformableVariable;
2222

@@ -30,12 +30,24 @@ use crate::variables::TransformableVariable;
3030
/// # Bibliography
3131
/// \[1\] P. Vilím, ‘Filtering algorithms for the unary resource constraint’, Archives of Control
3232
/// Sciences, vol. 18, no. 2, pp. 159–202, 2008.
33-
pub(crate) struct Disjunctive<Var: IntegerVariable + 'static> {
33+
pub(crate) struct Disjunctive<Var: IntegerVariable> {
3434
/// The tasks which serve as the input to the disjunctive constraint
3535
tasks: Box<[DisjunctiveTask<Var>]>,
3636
/// An additional list of tasks which allows us to sort them (we require [`Disjunctive::tasks`]
3737
/// to keep track of the right indices).
3838
sorted_tasks: Vec<DisjunctiveTask<Var>>,
39+
/// The tasks which are used to propagate the upper-bounds; these are the same as
40+
/// [`Disjunctive::tasks`] but reversed (i.e. instead of being from [EST, LCT] these tasks go
41+
/// from [-LCT, -EST])
42+
reverse_tasks: Box<
43+
[DisjunctiveTask<<<Var as IntegerVariable>::AffineView as IntegerVariable>::AffineView>],
44+
>,
45+
/// An additional list of tasks which allows us to sort them (we require
46+
/// [`Disjunctive::reverse_tasks`]
47+
/// to keep track of the right indices).
48+
sorted_reverse_tasks:
49+
Vec<DisjunctiveTask<<<Var as IntegerVariable>::AffineView as IntegerVariable>::AffineView>>,
50+
3951
/// The elements which are currently present in the set Theta used for edge-finding.
4052
elements_in_theta: FixedBitSet,
4153
}
@@ -51,32 +63,151 @@ impl<Var: IntegerVariable + 'static> Disjunctive<Var> {
5163
id: LocalId::from(index as u32),
5264
})
5365
.collect::<Vec<_>>();
66+
let reverse_tasks = tasks
67+
.iter()
68+
.map(|task| DisjunctiveTask {
69+
start_variable: task.start_variable.offset(task.processing_time).scaled(-1),
70+
processing_time: task.processing_time,
71+
id: task.id,
72+
})
73+
.collect::<Vec<_>>();
5474

5575
let num_tasks = tasks.len();
76+
5677
Self {
5778
tasks: tasks.clone().into_boxed_slice(),
5879
sorted_tasks: tasks,
80+
reverse_tasks: reverse_tasks.clone().into_boxed_slice(),
81+
sorted_reverse_tasks: reverse_tasks,
5982
elements_in_theta: FixedBitSet::with_capacity(num_tasks),
6083
}
6184
}
6285
}
6386

64-
/// Creates an explanation consisting of the current bounds for all elements in Theta
87+
/// Creates an explanation consisting of the tasks in the theta-lambda tree which were responsible
88+
/// for the conflict based on \[1\].
89+
///
90+
/// # Bibliography
91+
/// \[1\] P. Vilím, ‘Computing explanations for the unary resource constraint’, in International
92+
/// Conference on Integration of Artificial Intelligence (AI) and Operations Research (OR)
93+
/// Techniques in Constraint Programming, 2005, pp. 396–409.
94+
fn create_conflict_explanation<'a, Var: IntegerVariable>(
95+
tasks: &'a [DisjunctiveTask<Var>],
96+
theta_lambda_tree: &mut ThetaLambdaTree,
97+
context: &'a PropagationContextMut,
98+
) -> PropositionalConjunction {
99+
// First we calculate some data about the set of tasks that caused the overflow
100+
let mut est_theta = i32::MAX;
101+
let mut lct_theta = i32::MIN;
102+
let mut p_theta = 0;
103+
104+
let reponsible_tasks = theta_lambda_tree
105+
.responsible_ect()
106+
.inspect(|element| {
107+
let task = &tasks[element.index()];
108+
est_theta = est_theta.min(context.lower_bound(&task.start_variable));
109+
lct_theta =
110+
lct_theta.max(context.upper_bound(&task.start_variable) + task.processing_time);
111+
p_theta += task.processing_time;
112+
})
113+
.collect::<Vec<_>>();
114+
115+
// We check whether we indeed overflow the interval
116+
pumpkin_assert_simple!(p_theta > lct_theta - est_theta);
117+
118+
// Then we calculate the amount by which we can lift the interval; i.e. how much does the
119+
// processing time of theta overflow the interval
120+
let delta = p_theta - (lct_theta - est_theta) - 1;
121+
122+
let mut explanation = Vec::new();
123+
124+
// Then for each element in the responsible tasks, we add that they need to be in this interval
125+
// together
126+
for element in reponsible_tasks {
127+
let task = &tasks[element.index()];
128+
explanation.push(predicate!(task.start_variable >= est_theta - delta / 2));
129+
explanation.push(predicate!(
130+
task.start_variable
131+
<= lct_theta - (delta as f64 / 2.0).ceil() as i32 - task.processing_time
132+
))
133+
}
134+
135+
explanation.into()
136+
}
137+
138+
/// Creates an explanation consisting of the tasks in the theta-lambda tree which were responsible
139+
/// for the propagation of `propagated_task` based on \[1\].
65140
///
66-
/// TODO: this explanation could be lifted and should take into account which subset of tasks was
67-
/// responsible for the propagation itself.
68-
fn create_theta_explanation<'a, Var: IntegerVariable>(
141+
/// # Bibliography
142+
/// \[1\] P. Vilím, ‘Computing explanations for the unary resource constraint’, in International
143+
/// Conference on Integration of Artificial Intelligence (AI) and Operations Research (OR)
144+
/// Techniques in Constraint Programming, 2005, pp. 396–409.
145+
fn create_propagation_explanation<'a, Var: IntegerVariable>(
69146
tasks: &'a [DisjunctiveTask<Var>],
147+
propagated_task_id: LocalId,
148+
new_bound: i32,
149+
theta_lambda_tree: &mut ThetaLambdaTree,
70150
elements_in_theta: &'a FixedBitSet,
71151
context: &'a PropagationContextMut,
72-
) -> impl Iterator<Item = Predicate> + 'a {
73-
elements_in_theta.ones().flat_map(move |index| {
74-
let task = &tasks[index];
75-
[
76-
predicate!(task.start_variable >= context.lower_bound(&task.start_variable)),
77-
predicate!(task.start_variable <= context.upper_bound(&task.start_variable)),
78-
]
79-
})
152+
) -> PropositionalConjunction {
153+
let propagated_task = &tasks[propagated_task_id.index()];
154+
155+
// First we calculate the required information for the explanations of all tasks in theta
156+
let mut earliest_release_time = context.lower_bound(&propagated_task.start_variable);
157+
let mut p_theta = 0;
158+
let theta = elements_in_theta
159+
.ones()
160+
.inspect(|theta_element| {
161+
let task = &tasks[*theta_element];
162+
p_theta += task.processing_time;
163+
earliest_release_time =
164+
earliest_release_time.min(context.lower_bound(&task.start_variable));
165+
})
166+
.collect::<Vec<_>>();
167+
168+
// Then we look at the subset which was responsible for the actual bound on EST
169+
let mut p_theta_prime = 0;
170+
let theta_prime = theta_lambda_tree
171+
.responsible_ect()
172+
.inspect(|theta_prime_element| {
173+
let task = &tasks[theta_prime_element.index()];
174+
p_theta_prime += task.processing_time;
175+
})
176+
.collect::<Vec<_>>();
177+
178+
let mut explanation = Vec::new();
179+
180+
// Now we go over each element in theta prime and explain it using lifted intervals
181+
for j in theta_prime.iter() {
182+
let task = &tasks[j.index()];
183+
explanation.push(predicate!(task.start_variable >= new_bound - p_theta_prime));
184+
explanation.push(predicate!(
185+
task.start_variable <= earliest_release_time + p_theta - 1
186+
));
187+
}
188+
189+
// then we go over each element which is in theta but not in theta prime and explin it using
190+
// lifted intervals
191+
for j in theta {
192+
if theta_prime.contains(&LocalId::from(j as u32)) {
193+
continue;
194+
}
195+
196+
let task = &tasks[j];
197+
198+
explanation.push(predicate!(task.start_variable >= earliest_release_time));
199+
explanation.push(predicate!(
200+
task.start_variable <= earliest_release_time + p_theta - 1
201+
));
202+
}
203+
204+
// Finally, we add the bound on the propagated task since this is required to entail the
205+
// propagation
206+
explanation.push(predicate!(
207+
propagated_task.start_variable >= earliest_release_time
208+
));
209+
210+
explanation.into()
80211
}
81212

82213
/// Performs the edge-finding algorithm (see [`Disjunctive`] for an intuition and the work on which
@@ -111,9 +242,11 @@ fn edge_finding<Var: IntegerVariable, SortedTaskVar: IntegerVariable>(
111242
// (which takes into account `j`) is larger than the LCT of `j` then we can report an
112243
// overflow
113244
if theta_lambda_tree.ect() > lct_j {
114-
return Err(Inconsistency::Conflict(
115-
create_theta_explanation(tasks, elements_in_theta, context).collect(),
116-
));
245+
return Err(Inconsistency::Conflict(create_conflict_explanation(
246+
tasks,
247+
&mut theta_lambda_tree,
248+
context,
249+
)));
117250
}
118251

119252
// If there was no overflow then we continue by checking whether we can find a propagation
@@ -146,21 +279,14 @@ fn edge_finding<Var: IntegerVariable, SortedTaskVar: IntegerVariable>(
146279
context.set_lower_bound(
147280
&tasks[i.index()].start_variable,
148281
theta_lambda_tree.ect(),
149-
create_theta_explanation(tasks, elements_in_theta, context)
150-
.chain({
151-
let task_i = &tasks[i.index()];
152-
[
153-
predicate!(
154-
task_i.start_variable
155-
>= context.lower_bound(&task_i.start_variable)
156-
),
157-
predicate!(
158-
task_i.start_variable
159-
<= context.upper_bound(&task_i.start_variable)
160-
),
161-
]
162-
})
163-
.collect::<PropositionalConjunction>(),
282+
create_propagation_explanation(
283+
tasks,
284+
i,
285+
theta_lambda_tree.ect(),
286+
&mut theta_lambda_tree,
287+
elements_in_theta,
288+
context,
289+
),
164290
)?;
165291
}
166292
// Then we remove the element from consideration entirely by removing it from Lambda
@@ -189,19 +315,10 @@ impl<Var: IntegerVariable + 'static> Propagator for Disjunctive<Var> {
189315
// Now we want to also update the upper-bounds
190316
//
191317
// We do this by "reversing" the tasks to make the LCT be represented as the EST of a task
192-
let reverse_tasks = self
193-
.tasks
194-
.iter()
195-
.map(|task| DisjunctiveTask {
196-
start_variable: task.start_variable.offset(task.processing_time).scaled(-1),
197-
processing_time: task.processing_time,
198-
id: task.id,
199-
})
200-
.collect::<Vec<_>>();
201318
edge_finding(
202319
&mut context,
203-
&reverse_tasks,
204-
&mut reverse_tasks.clone(),
320+
&self.reverse_tasks,
321+
&mut self.sorted_reverse_tasks,
205322
&mut self.elements_in_theta,
206323
)
207324
}
@@ -218,19 +335,12 @@ impl<Var: IntegerVariable + 'static> Propagator for Disjunctive<Var> {
218335
&mut sorted_tasks,
219336
&mut elements_in_theta,
220337
)?;
221-
let reverse_tasks = self
222-
.tasks
223-
.iter()
224-
.map(|task| DisjunctiveTask {
225-
start_variable: task.start_variable.clone(),
226-
processing_time: task.processing_time,
227-
id: task.id,
228-
})
229-
.collect::<Vec<_>>();
338+
339+
let mut sorted_reverse_tasks = self.sorted_reverse_tasks.clone();
230340
edge_finding(
231341
&mut context,
232-
&reverse_tasks,
233-
&mut reverse_tasks.clone(),
342+
&self.reverse_tasks,
343+
&mut sorted_reverse_tasks,
234344
&mut elements_in_theta,
235345
)
236346
}

0 commit comments

Comments
 (0)