Skip to main content

problemreductions/rules/
openshopscheduling_ilp.rs

1//! Reduction from OpenShopScheduling to ILP<i32>.
2//!
3//! Disjunctive formulation with binary ordering variables and integer start times:
4//!
5//! **Variables:**
6//! - `x_{j,k,i}` for j < k, all machines i: binary, 1 if job j precedes job k on machine i.
7//!   Index: pair index * m + i, where pair index = `j*(2n-j-1)/2 + (k-j-1)`.
8//!   Count: n*(n-1)/2 * m variables.
9//! - `s_{j,i}` for all (j, i): integer start time of job j on machine i.
10//!   Index: num_order_vars + j * m + i.
11//!   Count: n * m variables.
12//! - `C` (makespan): integer, index num_order_vars + n * m.
13//!
14//! **Constraints:**
15//! 1. Binary bounds: 0 ≤ x_{j,k,i} ≤ 1 for all j < k, i.
16//! 2. Machine non-overlap for each pair (j, k) and machine i:
17//!    - s_{k,i} ≥ s_{j,i} + p_{j,i} - M*(1 - x_{j,k,i})  →  s_{k,i} - s_{j,i} + M*x_{j,k,i} ≥ p_{j,i}
18//!    - s_{j,i} ≥ s_{k,i} + p_{k,i} - M*x_{j,k,i}         →  s_{j,i} - s_{k,i} - M*x_{j,k,i} ≥ p_{k,i} - M
19//! 3. Job non-overlap for each job j and each pair of machines (i, i'):
20//!    Uses separate binary variable y_{j,i,i'} for i < i' to decide which task runs first.
21//!    Variables y_{j,i,i'}: appended after s variables.
22//!    - s_{j,i'} ≥ s_{j,i} + p_{j,i} - M*(1 - y_{j,i,i'})
23//!    - s_{j,i} ≥ s_{j,i'} + p_{j,i'} - M*y_{j,i,i'}
24//! 4. Makespan: C ≥ s_{j,i} + p_{j,i} for all (j, i).
25//! 5. Non-negativity of start times: s_{j,i} ≥ 0 (implied by ILP non-negativity).
26//!
27//! **Objective:** Minimize C.
28
29use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
30use crate::models::misc::OpenShopScheduling;
31use crate::reduction;
32use crate::rules::traits::{ReduceTo, ReductionResult};
33
34/// Result of reducing OpenShopScheduling to ILP<i32>.
35///
36/// Variable layout:
37/// - `x_{j,k,i}` at index `pair_idx(j,k) * m + i`    (num_pairs * m vars)
38/// - `s_{j,i}`   at index `num_order_vars + j * m + i`  (n * m vars)
39/// - `y_{j,i,i'}` for i < i': at `num_order_vars + n*m + j * num_machine_pairs + machine_pair_idx(i,i')`
40///   (n * m*(m-1)/2 vars)
41/// - `C`: at index `num_order_vars + n * m + n * m*(m-1)/2` (1 var)
42#[derive(Debug, Clone)]
43pub struct ReductionOSSToILP {
44    target: ILP<i32>,
45    num_jobs: usize,
46    num_machines: usize,
47    /// n*(n-1)/2 * m — start index of s_{j,i} variables
48    num_order_vars: usize,
49}
50
51impl ReductionOSSToILP {
52    fn pair_idx(&self, j: usize, k: usize) -> usize {
53        debug_assert!(j < k);
54        let n = self.num_jobs;
55        j * (2 * n - j - 1) / 2 + (k - j - 1)
56    }
57
58    fn x_var(&self, j: usize, k: usize, i: usize) -> usize {
59        self.pair_idx(j, k) * self.num_machines + i
60    }
61
62    fn s_var(&self, j: usize, i: usize) -> usize {
63        self.num_order_vars + j * self.num_machines + i
64    }
65
66    fn machine_pair_idx(&self, i: usize, ip: usize) -> usize {
67        debug_assert!(i < ip);
68        let m = self.num_machines;
69        i * (2 * m - i - 1) / 2 + (ip - i - 1)
70    }
71
72    fn y_var(&self, j: usize, i: usize, ip: usize) -> usize {
73        let num_machine_pairs = self.num_machines * self.num_machines.saturating_sub(1) / 2;
74        self.num_order_vars
75            + self.num_jobs * self.num_machines
76            + j * num_machine_pairs
77            + self.machine_pair_idx(i, ip)
78    }
79}
80
81impl ReductionResult for ReductionOSSToILP {
82    type Source = OpenShopScheduling;
83    type Target = ILP<i32>;
84
85    fn target_problem(&self) -> &ILP<i32> {
86        &self.target
87    }
88
89    /// Extract per-machine job orderings from the ILP start times, then
90    /// convert to the config format (direct permutation indices per machine).
91    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
92        let n = self.num_jobs;
93        let m = self.num_machines;
94
95        // Read start times s_{j,i} for each (j, i)
96        let start = |j: usize, i: usize| -> usize {
97            let idx = self.num_order_vars + j * m + i;
98            target_solution.get(idx).copied().unwrap_or(0)
99        };
100
101        // For each machine, sort jobs by their start time on that machine
102        let mut config = Vec::with_capacity(n * m);
103        for i in 0..m {
104            let mut jobs: Vec<usize> = (0..n).collect();
105            jobs.sort_by_key(|&j| (start(j, i), j));
106            config.extend(jobs);
107        }
108        config
109    }
110}
111
112#[reduction(overhead = {
113    num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1",
114    num_constraints = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines",
115})]
116impl ReduceTo<ILP<i32>> for OpenShopScheduling {
117    type Result = ReductionOSSToILP;
118
119    fn reduce_to(&self) -> Self::Result {
120        let n = self.num_jobs();
121        let m = self.num_machines();
122        let p = self.processing_times();
123
124        let num_pairs = n * n.saturating_sub(1) / 2;
125        let num_machine_pairs = m * m.saturating_sub(1) / 2;
126
127        // Variable counts
128        let num_order_vars = num_pairs * m; // x_{j,k,i}: binary
129        let num_start_vars = n * m; // s_{j,i}: integer
130        let num_job_pair_vars = n * num_machine_pairs; // y_{j,i,i'}: binary
131        let num_vars = num_order_vars + num_start_vars + num_job_pair_vars + 1; // +1 for C
132
133        let result = ReductionOSSToILP {
134            target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize),
135            num_jobs: n,
136            num_machines: m,
137            num_order_vars,
138        };
139
140        // Big-M: sum of all processing times (loose upper bound on makespan)
141        let total_p: usize = p.iter().flat_map(|row| row.iter()).sum();
142        let big_m = total_p as f64;
143
144        let c_var = num_order_vars + num_start_vars + num_job_pair_vars;
145
146        let mut constraints = Vec::new();
147
148        // 1. Binary bounds on x_{j,k,i}: 0 ≤ x ≤ 1
149        for j in 0..n {
150            for k in (j + 1)..n {
151                for i in 0..m {
152                    let x = result.x_var(j, k, i);
153                    constraints.push(LinearConstraint::le(vec![(x, 1.0)], 1.0));
154                }
155            }
156        }
157
158        // Upper bounds on start time variables: s_{j,i} ≤ total_p
159        // (no task can start after all tasks have finished)
160        for j in 0..n {
161            for i in 0..m {
162                let sji = result.s_var(j, i);
163                constraints.push(LinearConstraint::le(vec![(sji, 1.0)], big_m));
164            }
165        }
166
167        // Upper bound on makespan C ≤ total_p
168        constraints.push(LinearConstraint::le(vec![(c_var, 1.0)], big_m));
169
170        // 2. Machine non-overlap: for each pair (j,k) with j<k, each machine i
171        //    x_{j,k,i}=1 means j precedes k on machine i:
172        //      s_{k,i} ≥ s_{j,i} + p_{j,i}  →  s_{k,i} - s_{j,i} + M*x_{j,k,i} ≥ p_{j,i} (active when x=0)
173        //    Actually: s_{k,i} ≥ s_{j,i} + p_{j,i} - M*(1 - x_{j,k,i})
174        //              ⟺ s_{k,i} - s_{j,i} - M*x_{j,k,i} ≥ p_{j,i} - M
175        //    And:    s_{j,i} ≥ s_{k,i} + p_{k,i} - M*x_{j,k,i}
176        //              ⟺ s_{j,i} - s_{k,i} + M*x_{j,k,i} ≥ p_{k,i}  (active when x=1, i.e. k before j)
177        //    Wait, let's be careful. x=1 means j before k.
178        //      (a) if j before k: s_k ≥ s_j + p_{j,i}  →  when x=1 this is active, when x=0 inactive
179        //      (b) if k before j (x=0): s_j ≥ s_k + p_{k,i}
180        //
181        //    Linearization:
182        //      (a) s_{k,i} - s_{j,i} + M*(1-x) ≥ p_{j,i}
183        //          s_{k,i} - s_{j,i} - M*x ≥ p_{j,i} - M
184        //      (b) s_{j,i} - s_{k,i} + M*x ≥ p_{k,i}
185        for j in 0..n {
186            for k in (j + 1)..n {
187                for (i, (&pji_val, &pki_val)) in p[j].iter().zip(p[k].iter()).enumerate() {
188                    let x = result.x_var(j, k, i);
189                    let sj = result.s_var(j, i);
190                    let sk = result.s_var(k, i);
191                    let pji = pji_val as f64;
192                    let pki = pki_val as f64;
193
194                    // (a) s_{k,i} - s_{j,i} - M*x_{j,k,i} >= p_{j,i} - M
195                    constraints.push(LinearConstraint::ge(
196                        vec![(sk, 1.0), (sj, -1.0), (x, -big_m)],
197                        pji - big_m,
198                    ));
199
200                    // (b) s_{j,i} - s_{k,i} + M*x_{j,k,i} >= p_{k,i}
201                    constraints.push(LinearConstraint::ge(
202                        vec![(sj, 1.0), (sk, -1.0), (x, big_m)],
203                        pki,
204                    ));
205                }
206            }
207        }
208
209        // 3. Binary bounds on y_{j,i,i'}: 0 ≤ y ≤ 1
210        for j in 0..n {
211            for i in 0..m {
212                for ip in (i + 1)..m {
213                    let y = result.y_var(j, i, ip);
214                    constraints.push(LinearConstraint::le(vec![(y, 1.0)], 1.0));
215                }
216            }
217        }
218
219        // 4. Job non-overlap: for each job j and each pair (i, i') with i < i'
220        //    y_{j,i,i'}=1 means machine i is scheduled before machine i' for job j:
221        //      (a) s_{j,i'} ≥ s_{j,i} + p_{j,i} - M*(1-y)
222        //          s_{j,i'} - s_{j,i} - M*y ≥ p_{j,i} - M
223        //      (b) s_{j,i} ≥ s_{j,i'} + p_{j,i'} - M*y
224        //          s_{j,i} - s_{j,i'} + M*y ≥ p_{j,i'}
225        for (j, pj) in p.iter().enumerate() {
226            for i in 0..m {
227                for ip in (i + 1)..m {
228                    let y = result.y_var(j, i, ip);
229                    let sji = result.s_var(j, i);
230                    let sjip = result.s_var(j, ip);
231                    let pji = pj[i] as f64;
232                    let pjip = pj[ip] as f64;
233
234                    // (a) s_{j,i'} - s_{j,i} - M*y >= p_{j,i} - M
235                    constraints.push(LinearConstraint::ge(
236                        vec![(sjip, 1.0), (sji, -1.0), (y, -big_m)],
237                        pji - big_m,
238                    ));
239
240                    // (b) s_{j,i} - s_{j,i'} + M*y >= p_{j,i'}
241                    constraints.push(LinearConstraint::ge(
242                        vec![(sji, 1.0), (sjip, -1.0), (y, big_m)],
243                        pjip,
244                    ));
245                }
246            }
247        }
248
249        // 5. Makespan: C ≥ s_{j,i} + p_{j,i}  ⟺  C - s_{j,i} ≥ p_{j,i}
250        for (j, pj) in p.iter().enumerate() {
251            for (i, &pji) in pj.iter().enumerate() {
252                let sji = result.s_var(j, i);
253                constraints.push(LinearConstraint::ge(
254                    vec![(c_var, 1.0), (sji, -1.0)],
255                    pji as f64,
256                ));
257            }
258        }
259
260        // Objective: minimize C
261        let objective = vec![(c_var, 1.0)];
262
263        ReductionOSSToILP {
264            target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize),
265            num_jobs: n,
266            num_machines: m,
267            num_order_vars,
268        }
269    }
270}
271
272#[cfg(feature = "example-db")]
273pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
274    vec![crate::example_db::specs::RuleExampleSpec {
275        id: "openshopscheduling_to_ilp",
276        build: || {
277            // Small 2x2 instance for canonical example
278            let source = OpenShopScheduling::new(2, vec![vec![1, 2], vec![2, 1]]);
279            crate::example_db::specs::rule_example_via_ilp::<_, i32>(source)
280        },
281    }]
282}
283
284#[cfg(test)]
285#[path = "../unit_tests/rules/openshopscheduling_ilp.rs"]
286mod tests;