problemreductions/rules/
sequencingwithinintervals_ilp.rs1use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
19use crate::models::misc::SequencingWithinIntervals;
20use crate::reduction;
21use crate::rules::traits::{ReduceTo, ReductionResult};
22
23#[derive(Debug, Clone)]
28pub struct ReductionSWIToILP {
29 target: ILP<bool>,
30 task_layout: Vec<(usize, usize)>,
32}
33
34impl ReductionResult for ReductionSWIToILP {
35 type Source = SequencingWithinIntervals;
36 type Target = ILP<bool>;
37
38 fn target_problem(&self) -> &ILP<bool> {
39 &self.target
40 }
41
42 fn extract_solution(
47 &self,
48 target_solution: &<Self::Target as crate::traits::Problem>::Solution,
49 ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
50 crate::rules::traits::validate_target_witness(
51 self.target_problem(),
52 target_solution,
53 |value| value.value.is_some(),
54 "target ILP assignment is infeasible",
55 )?;
56
57 self.task_layout
58 .iter()
59 .enumerate()
60 .map(|(task, &(base, count))| {
61 let mut selected = (0..count).filter(|&offset| target_solution[base + offset] == 1);
62 match (selected.next(), selected.next()) {
63 (Some(offset), None) => Ok(offset),
64 (None, _) => Err(crate::rules::ExtractionError::invalid(format!(
65 "task {task} has no selected start time"
66 ))),
67 (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!(
68 "task {task} has multiple selected start times"
69 ))),
70 }
71 })
72 .collect()
73 }
74}
75
76#[crate::aggregate_reduction(ilp_feasibility)]
77impl crate::rules::AggregateReductionResult for ReductionSWIToILP {}
78
79#[reduction(transform = upper_bound {
82 max_constraint_magnitude_bits = "1",
83 num_vars = "num_start_slots",
84 num_constraints = "num_tasks + num_start_slots * (num_start_slots - 1) / 2",
85 num_nonzeros = "num_start_slots^2",
86})]
87impl ReduceTo<ILP<bool>> for SequencingWithinIntervals {
88 type Result = ReductionSWIToILP;
89
90 fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
91 let n = self.num_tasks();
92 let release = self.release_times();
93 let lengths = self.lengths();
94
95 let overflow = |operation| {
97 crate::rules::ReductionError::integer_overflow::<Self, ILP<bool>>(operation)
98 };
99 let slot_counts: Vec<usize> = self.start_slot_counts().collect();
100
101 let mut bases = vec![0usize; n];
102 for j in 1..n {
103 bases[j] = bases[j - 1]
104 .checked_add(slot_counts[j - 1])
105 .ok_or_else(|| overflow("computing task variable offsets"))?;
106 }
107 let num_vars = bases
108 .last()
109 .copied()
110 .unwrap_or(0)
111 .checked_add(slot_counts.last().copied().unwrap_or(0))
112 .ok_or_else(|| overflow("computing the ILP variable count"))?;
113
114 let task_layout: Vec<(usize, usize)> = (0..n).map(|j| (bases[j], slot_counts[j])).collect();
115
116 let mut constraints = Vec::new();
117
118 for j in 0..n {
120 let terms: Vec<(usize, i64)> = (0..slot_counts[j]).map(|k| (bases[j] + k, 1)).collect();
121 constraints.push(LinearConstraint::eq(terms, 1));
122 }
123
124 for i in 0..n {
128 for j in (i + 1)..n {
129 for k1 in 0..slot_counts[i] {
130 let offset_i = Self::exact_i64(k1, "converting a task start offset to i64")?;
131 let start_i = release[i]
132 .checked_add(offset_i)
133 .ok_or_else(|| overflow("computing a task start time"))?;
134 let end_i = start_i
135 .checked_add(lengths[i])
136 .ok_or_else(|| overflow("computing a task end time"))?;
137 for k2 in 0..slot_counts[j] {
138 let offset_j =
139 Self::exact_i64(k2, "converting a task start offset to i64")?;
140 let start_j = release[j]
141 .checked_add(offset_j)
142 .ok_or_else(|| overflow("computing a task start time"))?;
143 let end_j = start_j
144 .checked_add(lengths[j])
145 .ok_or_else(|| overflow("computing a task end time"))?;
146 if !(end_i <= start_j || end_j <= start_i) {
148 constraints.push(LinearConstraint::le(
149 vec![(bases[i] + k1, 1), (bases[j] + k2, 1)],
150 1,
151 ));
152 }
153 }
154 }
155 }
156 }
157
158 Ok(ReductionSWIToILP {
159 target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize)
160 .map_err(Self::target_construction)?,
161 task_layout,
162 })
163 }
164}
165
166#[cfg(feature = "example-db")]
167pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
168 use crate::export::SolutionPair;
169
170 vec![crate::example_db::specs::RuleExampleSpec {
171 id: "sequencingwithinintervals_to_ilp",
172 build: || {
173 let source =
177 SequencingWithinIntervals::new(vec![0, 2], vec![3, 5], vec![2, 2]).unwrap();
178 let reduction: ReductionSWIToILP =
179 ReduceTo::<ILP<bool>>::reduce_to(&source).expect("reduction should succeed");
180 let solver = crate::solvers::ILPSolver::new();
181 let target_config = solver
182 .solve(reduction.target_problem())
183 .expect("canonical example should be feasible");
184 let source_config = reduction.extract_solution(&target_config).unwrap();
185 crate::example_db::specs::rule_example_with_witness::<_, ILP<bool>>(
186 source,
187 SolutionPair {
188 source_config: serde_json::to_value(source_config)
189 .expect("solution serialization must succeed"),
190 target_config: serde_json::to_value(target_config)
191 .expect("solution serialization must succeed"),
192 },
193 )
194 },
195 }]
196}
197
198#[cfg(test)]
199#[path = "../unit_tests/rules/sequencingwithinintervals_ilp.rs"]
200mod tests;