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)]
58pub struct MinimumTardinessSequencing<W> {
59    lengths: Vec<W>,
60    deadlines: Vec<i64>,
61    precedences: Vec<(usize, usize)>,
62}
63
64macro_rules! minimum_tardiness_create_spec {
65    ($name:ident, $weight:ty, $construct:expr) => {
66        #[derive(Debug, Deserialize, crate::CreateSpec)]
67        struct $name {
68            lengths: Vec<$weight>,
69            deadlines: Vec<i64>,
70            precedences: Option<Vec<(usize, usize)>>,
71        }
72
73        impl TryFrom<$name> for MinimumTardinessSequencing<$weight> {
74            type Error = crate::registry::ConstructionError;
75
76            fn try_from(spec: $name) -> Result<Self, Self::Error> {
77                if spec.lengths.len() != spec.deadlines.len() {
78                    return Err("lengths and deadlines must have the same length"
79                        .to_string()
80                        .into());
81                }
82                let precedences = spec.precedences.unwrap_or_default();
83                let num_tasks = spec.lengths.len();
84                if let Some(&(pred, succ)) = precedences
85                    .iter()
86                    .find(|&&(pred, succ)| pred >= num_tasks || succ >= num_tasks)
87                {
88                    return Err(format!(
89                        "precedence ({pred}, {succ}) is out of range for {num_tasks} tasks"
90                    )
91                    .into());
92                }
93                $construct(spec.lengths, spec.deadlines, precedences)
94            }
95        }
96    };
97}
98
99#[derive(Debug, Deserialize, crate::CreateSpec)]
100struct MinimumTardinessSequencingOneCreateSpec {
101    deadlines: Vec<i64>,
102    precedences: Option<Vec<(usize, usize)>>,
103}
104impl TryFrom<MinimumTardinessSequencingOneCreateSpec> for MinimumTardinessSequencing<One> {
105    type Error = crate::registry::ConstructionError;
106    fn try_from(spec: MinimumTardinessSequencingOneCreateSpec) -> Result<Self, Self::Error> {
107        let num_tasks = spec.deadlines.len();
108        let precedences = spec.precedences.unwrap_or_default();
109        if precedences
110            .iter()
111            .any(|&(a, b)| a >= num_tasks || b >= num_tasks)
112        {
113            return Err("precedence indices must be within the task count".into());
114        }
115        Ok(Self::new(num_tasks, spec.deadlines, precedences))
116    }
117}
118
119minimum_tardiness_create_spec!(
120    MinimumTardinessSequencingI64CreateSpec,
121    i64,
122    |lengths: Vec<i64>, deadlines, precedences| {
123        if lengths.iter().any(|&length| length <= 0) {
124            return Err("all task lengths must be positive".to_string().into());
125        }
126        Ok(MinimumTardinessSequencing::with_lengths(
127            lengths,
128            deadlines,
129            precedences,
130        ))
131    }
132);
133
134impl MinimumTardinessSequencing<One> {
135    /// Create a new unit-length MinimumTardinessSequencing instance.
136    ///
137    /// # Panics
138    ///
139    /// Panics if `deadlines.len() != num_tasks` or if any task index in `precedences`
140    /// is out of range.
141    pub fn new(num_tasks: usize, deadlines: Vec<i64>, precedences: Vec<(usize, usize)>) -> Self {
142        assert_eq!(
143            deadlines.len(),
144            num_tasks,
145            "deadlines length must equal num_tasks"
146        );
147        validate_precedences(num_tasks, &precedences);
148        Self {
149            lengths: vec![One; num_tasks],
150            deadlines,
151            precedences,
152        }
153    }
154}
155
156impl MinimumTardinessSequencing<i64> {
157    /// Create a new arbitrary-length MinimumTardinessSequencing instance.
158    ///
159    /// # Panics
160    ///
161    /// Panics if `lengths.len() != deadlines.len()`, if any length is 0,
162    /// or if any task index in `precedences` is out of range.
163    pub fn with_lengths(
164        lengths: Vec<i64>,
165        deadlines: Vec<i64>,
166        precedences: Vec<(usize, usize)>,
167    ) -> Self {
168        assert_eq!(
169            lengths.len(),
170            deadlines.len(),
171            "lengths and deadlines must have the same length"
172        );
173        assert!(
174            lengths.iter().all(|&l| l > 0),
175            "all task lengths must be positive"
176        );
177        let num_tasks = lengths.len();
178        validate_precedences(num_tasks, &precedences);
179        Self {
180            lengths,
181            deadlines,
182            precedences,
183        }
184    }
185}
186
187fn validate_precedences(num_tasks: usize, precedences: &[(usize, usize)]) {
188    for &(pred, succ) in precedences {
189        assert!(
190            pred < num_tasks,
191            "predecessor index {} out of range (num_tasks = {})",
192            pred,
193            num_tasks
194        );
195        assert!(
196            succ < num_tasks,
197            "successor index {} out of range (num_tasks = {})",
198            succ,
199            num_tasks
200        );
201    }
202}
203
204impl<W: WeightElement> MinimumTardinessSequencing<W> {
205    /// Returns the number of tasks.
206    pub fn num_tasks(&self) -> usize {
207        self.deadlines.len()
208    }
209
210    /// Returns the task lengths.
211    pub fn lengths(&self) -> &[W] {
212        &self.lengths
213    }
214
215    /// Returns the deadlines.
216    pub fn deadlines(&self) -> &[i64] {
217        &self.deadlines
218    }
219
220    /// Returns the precedence constraints.
221    pub fn precedences(&self) -> &[(usize, usize)] {
222        &self.precedences
223    }
224
225    /// Returns the number of precedence constraints.
226    pub fn num_precedences(&self) -> usize {
227        self.precedences.len()
228    }
229
230    /// Validate a schedule and return the inverse permutation (sigma).
231    /// Returns None if the config is invalid or violates precedences.
232    fn decode_and_validate(&self, config: &[usize]) -> Option<Vec<usize>> {
233        let n = self.num_tasks();
234        let schedule = super::decode_permutation(config, n)?;
235
236        let mut sigma = vec![0usize; n];
237        for (pos, &task) in schedule.iter().enumerate() {
238            sigma[task] = pos;
239        }
240
241        for &(pred, succ) in &self.precedences {
242            if sigma[pred] >= sigma[succ] {
243                return None;
244            }
245        }
246
247        Some(sigma)
248    }
249}
250
251impl Problem for MinimumTardinessSequencing<One> {
252    const NAME: &'static str = "MinimumTardinessSequencing";
253    type Solution = Vec<usize>;
254    type Value = Min<i64>;
255
256    crate::problem_parameters![
257        ("num_precedences", num_precedences),
258        ("num_tasks", num_tasks),
259    ];
260
261    fn variant() -> Vec<(&'static str, &'static str)> {
262        crate::variant_params![One]
263    }
264
265    fn evaluate(
266        &self,
267        config: &Self::Solution,
268    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
269        let n = self.num_tasks();
270        if config.len() != n {
271            return Err(crate::traits::EvaluationError::InvalidConfiguration(
272                "schedule length does not match the tasks".into(),
273            ));
274        }
275        if config.iter().any(|&task| task >= n) {
276            return Err(crate::traits::EvaluationError::InvalidConfiguration(
277                "schedule contains an out-of-range task".into(),
278            ));
279        }
280        Ok({
281            let Some(sigma) = self.decode_and_validate(config) else {
282                return Ok(Min(None));
283            };
284
285            // Unit length: completion time at position p is p + 1
286            let mut tardy_count = 0_i64;
287            for (task, &position) in sigma.iter().enumerate() {
288                let completion = i64::try_from(position)
289                    .ok()
290                    .and_then(|position| position.checked_add(1))
291                    .ok_or_else(|| {
292                        crate::traits::EvaluationError::IntegerOverflow(
293                            "computing a unit-length task completion time".to_string(),
294                        )
295                    })?;
296                if completion > self.deadlines[task] {
297                    tardy_count = tardy_count.checked_add(1).ok_or_else(|| {
298                        crate::traits::EvaluationError::IntegerOverflow(
299                            "counting tardy tasks".to_string(),
300                        )
301                    })?;
302                }
303            }
304
305            Min(Some(tardy_count))
306        })
307    }
308}
309
310impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing<One> {
311    fn dimensions(&self) -> Vec<usize> {
312        super::lehmer_dims(self.num_tasks())
313    }
314}
315
316impl Problem for MinimumTardinessSequencing<i64> {
317    const NAME: &'static str = "MinimumTardinessSequencing";
318    type Solution = Vec<usize>;
319    type Value = Min<i64>;
320
321    crate::problem_parameters![
322        ("num_precedences", num_precedences),
323        ("num_tasks", num_tasks),
324    ];
325
326    fn variant() -> Vec<(&'static str, &'static str)> {
327        crate::variant_params![i64]
328    }
329
330    fn evaluate(
331        &self,
332        config: &Self::Solution,
333    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
334        let n = self.num_tasks();
335        if config.len() != n {
336            return Err(crate::traits::EvaluationError::InvalidConfiguration(
337                "schedule length does not match the tasks".into(),
338            ));
339        }
340        if config.iter().any(|&task| task >= n) {
341            return Err(crate::traits::EvaluationError::InvalidConfiguration(
342                "schedule contains an out-of-range task".into(),
343            ));
344        }
345        Ok({
346            let Some(sigma) = self.decode_and_validate(config) else {
347                return Ok(Min(None));
348            };
349
350            // Build schedule order from sigma (inverse permutation)
351            let mut schedule = vec![0usize; n];
352            for (task, &pos) in sigma.iter().enumerate() {
353                schedule[pos] = task;
354            }
355
356            // Compute completion times using actual lengths
357            let mut completion = vec![0_i64; n];
358            let mut cumulative = 0_i64;
359            for &task in &schedule {
360                cumulative = cumulative.checked_add(self.lengths[task]).ok_or_else(|| {
361                    crate::traits::EvaluationError::IntegerOverflow(
362                        "summing task lengths while computing completion times".to_string(),
363                    )
364                })?;
365                completion[task] = cumulative;
366            }
367
368            let mut tardy_count = 0_i64;
369            for (task, &completion_time) in completion.iter().enumerate() {
370                if completion_time > self.deadlines[task] {
371                    tardy_count = tardy_count.checked_add(1).ok_or_else(|| {
372                        crate::traits::EvaluationError::IntegerOverflow(
373                            "counting tardy tasks".to_string(),
374                        )
375                    })?;
376                }
377            }
378
379            Min(Some(tardy_count))
380        })
381    }
382}
383
384impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing<i64> {
385    fn dimensions(&self) -> Vec<usize> {
386        super::lehmer_dims(self.num_tasks())
387    }
388}
389
390crate::declare_variants! {
391    default MinimumTardinessSequencing<One> => "2^num_tasks" create MinimumTardinessSequencingOneCreateSpec,
392    MinimumTardinessSequencing<i64> => "2^num_tasks" create MinimumTardinessSequencingI64CreateSpec,
393}
394
395crate::register_brute_force! {
396    MinimumTardinessSequencing<One> decode |problem: &MinimumTardinessSequencing<One>, indices: Vec<usize>| super::decode_lehmer(&indices, problem.num_tasks()).expect("enumerated Lehmer digits are valid"),
397    MinimumTardinessSequencing<i64> decode |problem: &MinimumTardinessSequencing<i64>, indices: Vec<usize>| super::decode_lehmer(&indices, problem.num_tasks()).expect("enumerated Lehmer digits are valid"),
398}
399
400#[cfg(feature = "example-db")]
401pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
402    vec![
403        // Unit-length variant
404        crate::example_db::specs::ModelExampleSpec {
405            id: "minimum_tardiness_sequencing",
406            instance: Box::new(MinimumTardinessSequencing::<One>::new(
407                4,
408                vec![2, 3, 1, 4],
409                vec![(0, 2)],
410            )),
411            optimal_config: serde_json::json!(vec![0, 1, 2, 3]),
412            optimal_value: serde_json::json!(1),
413        },
414        // Arbitrary-length variant
415        crate::example_db::specs::ModelExampleSpec {
416            id: "minimum_tardiness_sequencing_weighted",
417            // 5 tasks, lengths [3,2,2,1,2], deadlines [4,3,8,3,6], prec (0→2, 1→3)
418            // Optimal schedule: t0,t4,t2,t1,t3 → 2 tardy
419            // Lehmer [0,3,1,0,0]: avail=[0,1,2,3,4] pick 0→0; [1,2,3,4] pick 3→4;
420            //   [1,2,3] pick 1→2; [1,3] pick 0→1; [3] pick 0→3
421            instance: Box::new(MinimumTardinessSequencing::<i64>::with_lengths(
422                vec![3, 2, 2, 1, 2],
423                vec![4, 3, 8, 3, 6],
424                vec![(0, 2), (1, 3)],
425            )),
426            optimal_config: serde_json::json!(vec![0, 4, 2, 1, 3]),
427            optimal_value: serde_json::json!(2),
428        },
429    ]
430}
431
432#[cfg(test)]
433#[path = "../../unit_tests/models/misc/minimum_tardiness_sequencing.rs"]
434mod tests;