Skip to main content

problemreductions/models/misc/
minimum_tardiness_sequencing.rs

1//! Minimum Tardiness Sequencing problem implementation.
2//!
3//! A classical NP-complete single-machine scheduling problem (SS2 from
4//! Garey & Johnson, 1979) where tasks with precedence constraints
5//! and deadlines must be scheduled to minimize the number of tardy tasks.
6//!
7//! Variants:
8//! - `MinimumTardinessSequencing<One>` — unit-length tasks (`1|prec, pj=1|∑Uj`)
9//! - `MinimumTardinessSequencing<i64>` — arbitrary-length tasks (`1|prec|∑Uj`)
10
11use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
12use crate::traits::Problem;
13use crate::types::{Min, One, WeightElement};
14use serde::{Deserialize, Serialize};
15
16inventory::submit! {
17    ProblemSchemaEntry {
18        name: "MinimumTardinessSequencing",
19        display_name: "Minimum Tardiness Sequencing",
20        aliases: &[],
21        dimensions: &[VariantDimension::new("weight", "One", &["One", "i64"])],
22        category: crate::registry::ProblemCategory::Misc,
23        module_path: module_path!(),
24        description: "Schedule tasks with precedence constraints and deadlines to minimize the number of tardy tasks",
25        fields: MinimumTardinessSequencingI64CreateSpec::FIELDS,
26    }
27}
28
29/// Minimum Tardiness Sequencing problem.
30///
31/// Given a set T of tasks, each with a processing time l(t) and a deadline d(t),
32/// and a partial order (precedence constraints) on T, find a schedule
33/// that is a valid permutation respecting precedence constraints
34/// and minimizes the number of tardy tasks.
35///
36/// # Type Parameters
37///
38/// * `W` - The weight/length type. `One` for unit-length tasks, `i64` for arbitrary.
39///
40/// # Example
41///
42/// ```
43/// use problemreductions::models::misc::MinimumTardinessSequencing;
44/// use problemreductions::types::One;
45/// use problemreductions::{Problem, BruteForce};
46///
47/// // Unit-length: 3 tasks, task 0 must precede task 2
48/// let problem = MinimumTardinessSequencing::<One>::new(
49///     3,
50///     vec![2, 3, 1],
51///     vec![(0, 2)],
52/// );
53/// let solver = BruteForce::new();
54/// let solution = solver.solve(&problem).unwrap();
55/// assert!(solution.is_some());
56/// ```
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(try_from = "MinimumTardinessSequencingData<W>")]
59#[serde(bound(
60    deserialize = "W: Deserialize<'de>, Self: TryFrom<MinimumTardinessSequencingData<W>>, <Self as TryFrom<MinimumTardinessSequencingData<W>>>::Error: std::fmt::Display"
61))]
62pub struct MinimumTardinessSequencing<W> {
63    lengths: Vec<W>,
64    deadlines: Vec<i64>,
65    precedences: Vec<(usize, usize)>,
66}
67
68#[derive(Deserialize)]
69struct MinimumTardinessSequencingData<W> {
70    lengths: Vec<W>,
71    deadlines: Vec<i64>,
72    precedences: Vec<(usize, usize)>,
73}
74
75impl TryFrom<MinimumTardinessSequencingData<One>> for MinimumTardinessSequencing<One> {
76    type Error = crate::registry::ConstructionError;
77
78    fn try_from(data: MinimumTardinessSequencingData<One>) -> Result<Self, Self::Error> {
79        Self::try_new(data.lengths.len(), data.deadlines, data.precedences)
80    }
81}
82
83impl TryFrom<MinimumTardinessSequencingData<i64>> for MinimumTardinessSequencing<i64> {
84    type Error = crate::registry::ConstructionError;
85
86    fn try_from(data: MinimumTardinessSequencingData<i64>) -> Result<Self, Self::Error> {
87        Self::try_with_lengths(data.lengths, data.deadlines, data.precedences)
88    }
89}
90
91#[derive(Debug, Deserialize, crate::CreateSpec)]
92struct MinimumTardinessSequencingOneCreateSpec {
93    deadlines: Vec<i64>,
94    precedences: Option<Vec<(usize, usize)>>,
95}
96impl TryFrom<MinimumTardinessSequencingOneCreateSpec> for MinimumTardinessSequencing<One> {
97    type Error = crate::registry::ConstructionError;
98    fn try_from(spec: MinimumTardinessSequencingOneCreateSpec) -> Result<Self, Self::Error> {
99        let num_tasks = spec.deadlines.len();
100        let precedences = spec.precedences.unwrap_or_default();
101        Self::try_new(num_tasks, spec.deadlines, precedences)
102    }
103}
104
105#[derive(Debug, Deserialize, crate::CreateSpec)]
106struct MinimumTardinessSequencingI64CreateSpec {
107    lengths: Vec<i64>,
108    deadlines: Vec<i64>,
109    precedences: Option<Vec<(usize, usize)>>,
110}
111impl TryFrom<MinimumTardinessSequencingI64CreateSpec> for MinimumTardinessSequencing<i64> {
112    type Error = crate::registry::ConstructionError;
113    fn try_from(spec: MinimumTardinessSequencingI64CreateSpec) -> Result<Self, Self::Error> {
114        Self::try_with_lengths(
115            spec.lengths,
116            spec.deadlines,
117            spec.precedences.unwrap_or_default(),
118        )
119    }
120}
121
122impl MinimumTardinessSequencing<One> {
123    /// Create a new unit-length MinimumTardinessSequencing instance.
124    ///
125    /// # Panics
126    ///
127    /// Panics if `deadlines.len() != num_tasks` or if any task index in `precedences`
128    /// is out of range.
129    pub fn new(num_tasks: usize, deadlines: Vec<i64>, precedences: Vec<(usize, usize)>) -> Self {
130        Self::try_new(num_tasks, deadlines, precedences).unwrap_or_else(|error| panic!("{error}"))
131    }
132
133    fn try_new(
134        num_tasks: usize,
135        deadlines: Vec<i64>,
136        precedences: Vec<(usize, usize)>,
137    ) -> Result<Self, crate::registry::ConstructionError> {
138        validate_task_data(num_tasks, &deadlines, &precedences)?;
139        Ok(Self {
140            lengths: vec![One; num_tasks],
141            deadlines,
142            precedences,
143        })
144    }
145}
146
147impl MinimumTardinessSequencing<i64> {
148    /// Create a new arbitrary-length MinimumTardinessSequencing instance.
149    ///
150    /// # Panics
151    ///
152    /// Panics if `lengths.len() != deadlines.len()`, if any length is 0,
153    /// or if any task index in `precedences` is out of range.
154    pub fn with_lengths(
155        lengths: Vec<i64>,
156        deadlines: Vec<i64>,
157        precedences: Vec<(usize, usize)>,
158    ) -> Self {
159        Self::try_with_lengths(lengths, deadlines, precedences)
160            .unwrap_or_else(|error| panic!("{error}"))
161    }
162
163    fn try_with_lengths(
164        lengths: Vec<i64>,
165        deadlines: Vec<i64>,
166        precedences: Vec<(usize, usize)>,
167    ) -> Result<Self, crate::registry::ConstructionError> {
168        validate_task_data(lengths.len(), &deadlines, &precedences)?;
169        if lengths.iter().any(|&length| length <= 0) {
170            return Err("all task lengths must be positive".into());
171        }
172        Ok(Self {
173            lengths,
174            deadlines,
175            precedences,
176        })
177    }
178}
179
180fn validate_task_data(
181    num_tasks: usize,
182    deadlines: &[i64],
183    precedences: &[(usize, usize)],
184) -> Result<(), crate::registry::ConstructionError> {
185    if deadlines.len() != num_tasks {
186        return Err("deadlines length must equal num_tasks".into());
187    }
188    for &(pred, succ) in precedences {
189        if pred >= num_tasks {
190            return Err(
191                format!("predecessor index {pred} out of range (num_tasks = {num_tasks})").into(),
192            );
193        }
194        if succ >= num_tasks {
195            return Err(
196                format!("successor index {succ} out of range (num_tasks = {num_tasks})").into(),
197            );
198        }
199    }
200    Ok(())
201}
202
203impl<W: WeightElement> MinimumTardinessSequencing<W> {
204    /// Smallest h >= 1 with every processing time strictly below 2^h.
205    pub fn max_processing_time_bits(&self) -> u64 {
206        crate::types::max_numeric_magnitude_bits(self.lengths.iter().map(|length| length.to_sum()))
207    }
208
209    /// Returns the number of tasks.
210    pub fn num_tasks(&self) -> usize {
211        self.deadlines.len()
212    }
213
214    /// Returns the task lengths.
215    pub fn lengths(&self) -> &[W] {
216        &self.lengths
217    }
218
219    /// Returns the deadlines.
220    pub fn deadlines(&self) -> &[i64] {
221        &self.deadlines
222    }
223
224    /// Returns the precedence constraints.
225    pub fn precedences(&self) -> &[(usize, usize)] {
226        &self.precedences
227    }
228
229    /// Returns the number of precedence constraints.
230    pub fn num_precedences(&self) -> usize {
231        self.precedences.len()
232    }
233
234    /// Validate a schedule and return the inverse permutation (sigma).
235    /// Returns None if the config is invalid or violates precedences.
236    fn decode_and_validate(&self, config: &[usize]) -> Option<Vec<usize>> {
237        let n = self.num_tasks();
238        let schedule = super::decode_permutation(config, n)?;
239
240        let mut sigma = vec![0usize; n];
241        for (pos, &task) in schedule.iter().enumerate() {
242            sigma[task] = pos;
243        }
244
245        for &(pred, succ) in &self.precedences {
246            if sigma[pred] >= sigma[succ] {
247                return None;
248            }
249        }
250
251        Some(sigma)
252    }
253}
254
255impl Problem for MinimumTardinessSequencing<One> {
256    const NAME: &'static str = "MinimumTardinessSequencing";
257    type Solution = Vec<usize>;
258    type Value = Min<i64>;
259
260    crate::problem_parameters![
261        ("max_processing_time_bits", max_processing_time_bits),
262        ("num_precedences", num_precedences),
263        ("num_tasks", num_tasks),
264    ];
265
266    fn variant() -> Vec<(&'static str, &'static str)> {
267        crate::variant_params![One]
268    }
269
270    fn evaluate(
271        &self,
272        config: &Self::Solution,
273    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
274        let n = self.num_tasks();
275        if config.len() != n {
276            return Err(crate::traits::EvaluationError::InvalidConfiguration(
277                "schedule length does not match the tasks".into(),
278            ));
279        }
280        if config.iter().any(|&task| task >= n) {
281            return Err(crate::traits::EvaluationError::InvalidConfiguration(
282                "schedule contains an out-of-range task".into(),
283            ));
284        }
285        Ok({
286            let Some(sigma) = self.decode_and_validate(config) else {
287                return Ok(Min(None));
288            };
289
290            // Unit length: completion time at position p is p + 1
291            let mut tardy_count = 0_i64;
292            for (task, &position) in sigma.iter().enumerate() {
293                let completion = i64::try_from(position)
294                    .ok()
295                    .and_then(|position| position.checked_add(1))
296                    .ok_or_else(|| {
297                        crate::traits::EvaluationError::IntegerOverflow(
298                            "computing a unit-length task completion time".to_string(),
299                        )
300                    })?;
301                if completion > self.deadlines[task] {
302                    tardy_count = tardy_count.checked_add(1).ok_or_else(|| {
303                        crate::traits::EvaluationError::IntegerOverflow(
304                            "counting tardy tasks".to_string(),
305                        )
306                    })?;
307                }
308            }
309
310            Min(Some(tardy_count))
311        })
312    }
313}
314
315impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing<One> {
316    fn dimensions(&self) -> Vec<usize> {
317        super::lehmer_dims(self.num_tasks())
318    }
319}
320
321impl Problem for MinimumTardinessSequencing<i64> {
322    const NAME: &'static str = "MinimumTardinessSequencing";
323    type Solution = Vec<usize>;
324    type Value = Min<i64>;
325
326    crate::problem_parameters![
327        ("max_processing_time_bits", max_processing_time_bits),
328        ("num_precedences", num_precedences),
329        ("num_tasks", num_tasks),
330    ];
331
332    fn variant() -> Vec<(&'static str, &'static str)> {
333        crate::variant_params![i64]
334    }
335
336    fn evaluate(
337        &self,
338        config: &Self::Solution,
339    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
340        let n = self.num_tasks();
341        if config.len() != n {
342            return Err(crate::traits::EvaluationError::InvalidConfiguration(
343                "schedule length does not match the tasks".into(),
344            ));
345        }
346        if config.iter().any(|&task| task >= n) {
347            return Err(crate::traits::EvaluationError::InvalidConfiguration(
348                "schedule contains an out-of-range task".into(),
349            ));
350        }
351        Ok({
352            let Some(sigma) = self.decode_and_validate(config) else {
353                return Ok(Min(None));
354            };
355
356            // Build schedule order from sigma (inverse permutation)
357            let mut schedule = vec![0usize; n];
358            for (task, &pos) in sigma.iter().enumerate() {
359                schedule[pos] = task;
360            }
361
362            // Compute completion times using actual lengths
363            let mut completion = vec![0_i64; n];
364            let mut cumulative = 0_i64;
365            for &task in &schedule {
366                cumulative = cumulative.checked_add(self.lengths[task]).ok_or_else(|| {
367                    crate::traits::EvaluationError::IntegerOverflow(
368                        "summing task lengths while computing completion times".to_string(),
369                    )
370                })?;
371                completion[task] = cumulative;
372            }
373
374            let mut tardy_count = 0_i64;
375            for (task, &completion_time) in completion.iter().enumerate() {
376                if completion_time > self.deadlines[task] {
377                    tardy_count = tardy_count.checked_add(1).ok_or_else(|| {
378                        crate::traits::EvaluationError::IntegerOverflow(
379                            "counting tardy tasks".to_string(),
380                        )
381                    })?;
382                }
383            }
384
385            Min(Some(tardy_count))
386        })
387    }
388}
389
390impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing<i64> {
391    fn dimensions(&self) -> Vec<usize> {
392        super::lehmer_dims(self.num_tasks())
393    }
394}
395
396crate::declare_variants! {
397    default MinimumTardinessSequencing<One> => "2^num_tasks" create MinimumTardinessSequencingOneCreateSpec,
398    MinimumTardinessSequencing<i64> => "2^num_tasks" create MinimumTardinessSequencingI64CreateSpec,
399}
400
401crate::register_brute_force! {
402    MinimumTardinessSequencing<One> decode |problem: &MinimumTardinessSequencing<One>, indices: Vec<usize>| super::decode_lehmer(&indices, problem.num_tasks()).expect("enumerated Lehmer digits are valid"),
403    MinimumTardinessSequencing<i64> decode |problem: &MinimumTardinessSequencing<i64>, indices: Vec<usize>| super::decode_lehmer(&indices, problem.num_tasks()).expect("enumerated Lehmer digits are valid"),
404}
405
406#[cfg(feature = "example-db")]
407pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
408    vec![
409        // Unit-length variant
410        crate::example_db::specs::ModelExampleSpec {
411            id: "minimum_tardiness_sequencing",
412            instance: Box::new(MinimumTardinessSequencing::<One>::new(
413                4,
414                vec![2, 3, 1, 4],
415                vec![(0, 2)],
416            )),
417            optimal_config: serde_json::json!(vec![0, 1, 2, 3]),
418            optimal_value: serde_json::json!(1),
419        },
420        // Arbitrary-length variant
421        crate::example_db::specs::ModelExampleSpec {
422            id: "minimum_tardiness_sequencing_weighted",
423            // 5 tasks, lengths [3,2,2,1,2], deadlines [4,3,8,3,6], prec (0→2, 1→3)
424            // Optimal schedule: t0,t4,t2,t1,t3 → 2 tardy
425            // Lehmer [0,3,1,0,0]: avail=[0,1,2,3,4] pick 0→0; [1,2,3,4] pick 3→4;
426            //   [1,2,3] pick 1→2; [1,3] pick 0→1; [3] pick 0→3
427            instance: Box::new(MinimumTardinessSequencing::<i64>::with_lengths(
428                vec![3, 2, 2, 1, 2],
429                vec![4, 3, 8, 3, 6],
430                vec![(0, 2), (1, 3)],
431            )),
432            optimal_config: serde_json::json!(vec![0, 4, 2, 1, 3]),
433            optimal_value: serde_json::json!(2),
434        },
435    ]
436}
437
438#[cfg(test)]
439#[path = "../../unit_tests/models/misc/minimum_tardiness_sequencing.rs"]
440mod tests;