From 5f55b389ba527f8b45a753a8852f68824c887308 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 07:57:11 +0000 Subject: [PATCH 1/3] Add plan for #501: PrecedenceConstrainedScheduling Co-Authored-By: Claude Opus 4.6 --- ...03-13-precedence-constrained-scheduling.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/plans/2026-03-13-precedence-constrained-scheduling.md diff --git a/docs/plans/2026-03-13-precedence-constrained-scheduling.md b/docs/plans/2026-03-13-precedence-constrained-scheduling.md new file mode 100644 index 00000000..dfedf2b1 --- /dev/null +++ b/docs/plans/2026-03-13-precedence-constrained-scheduling.md @@ -0,0 +1,82 @@ +# Plan: Add PrecedenceConstrainedScheduling Model (#501) + +## Overview + +Add the Precedence Constrained Scheduling problem — a satisfaction problem from Garey & Johnson (A5 SS9). Given unit-length tasks with precedence constraints, m processors, and a deadline D, determine whether all tasks can be scheduled to meet D while respecting precedences. + +This is a satisfaction problem (`Metric = bool`). No type parameters (all unit-length tasks, integer parameters). + +## Steps + +### Step 1: Create Model File + +**File:** `src/models/misc/precedence_constrained_scheduling.rs` + +**Struct:** +```rust +pub struct PrecedenceConstrainedScheduling { + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, +} +``` + +**Constructor:** `new(num_tasks, num_processors, deadline, precedences)` — validate that precedence indices are within bounds. + +**Getters:** `num_tasks()`, `num_processors()`, `deadline()`, `precedences()`. + +**Problem impl:** +- `NAME = "PrecedenceConstrainedScheduling"` +- `type Metric = bool` +- `dims()`: Each task is assigned a time slot in `{0, ..., deadline-1}`, so `vec![deadline; num_tasks]` +- `evaluate(config)`: Check (1) config length == num_tasks, (2) all values < deadline, (3) at most `num_processors` tasks per time slot, (4) for each precedence (i, j): config[j] >= config[i] + 1. Return true iff all constraints satisfied. +- `variant()`: `crate::variant_params![]` + +**SatisfactionProblem:** empty impl. + +**Complexity:** `declare_variants!{ PrecedenceConstrainedScheduling => "deadline ^ num_tasks" }` — brute force bound D^n. + +**Schema registration:** `inventory::submit!` with fields: num_tasks, num_processors, deadline, precedences. + +### Step 2: Register Model + +**`src/models/misc/mod.rs`:** Add `mod precedence_constrained_scheduling;` and `pub use`. + +**`src/models/mod.rs`:** Add to `misc::` re-export line. + +### Step 3: CLI Registration + +**`problemreductions-cli/src/dispatch.rs`:** +- `load_problem`: Add `"PrecedenceConstrainedScheduling" => deser_sat::(data),` +- `serialize_any_problem`: Add `"PrecedenceConstrainedScheduling" => try_ser::(any),` +- Add import for `PrecedenceConstrainedScheduling` from `problemreductions::models::misc` + +**`problemreductions-cli/src/problem_name.rs`:** +- `resolve_alias`: Add `"precedenceconstrainedscheduling" => "PrecedenceConstrainedScheduling".to_string(),` + +### Step 4: Unit Tests + +**File:** `src/unit_tests/models/misc/precedence_constrained_scheduling.rs` + +Tests (following SubsetSum pattern): +1. `test_precedence_constrained_scheduling_basic` — verify construction, getters, dims, NAME, variant +2. `test_precedence_constrained_scheduling_evaluate_valid` — valid schedule from issue example (8 tasks, 3 processors, deadline 4) +3. `test_precedence_constrained_scheduling_evaluate_invalid_precedence` — schedule violating a precedence constraint +4. `test_precedence_constrained_scheduling_evaluate_invalid_capacity` — schedule with too many tasks in one slot +5. `test_precedence_constrained_scheduling_evaluate_wrong_config_length` — wrong-length config +6. `test_precedence_constrained_scheduling_evaluate_invalid_variable_value` — value >= deadline +7. `test_precedence_constrained_scheduling_brute_force` — find_satisfying with small instance +8. `test_precedence_constrained_scheduling_brute_force_all` — find_all_satisfying +9. `test_precedence_constrained_scheduling_unsatisfiable` — instance with no valid schedule +10. `test_precedence_constrained_scheduling_serialization` — round-trip JSON +11. `test_precedence_constrained_scheduling_empty` — 0 tasks +12. `test_precedence_constrained_scheduling_no_precedences` — tasks with no ordering constraints + +Link test module in model file with `#[cfg(test)] #[path = "..."] mod tests;`. + +Also add entry in `src/unit_tests/models/misc/mod.rs` if it exists. + +### Step 5: Verify + +Run `make check` (fmt + clippy + test). Ensure all tests pass. From 0be9c116b82ac792edb5895d94cfb702c2a7274d Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:05:12 +0000 Subject: [PATCH 2/3] Implement PrecedenceConstrainedScheduling model (#501) Add satisfaction problem for scheduling unit-length tasks on m processors by deadline D with precedence constraints (Garey & Johnson A5 SS9, Ullman 1975). Co-Authored-By: Claude Opus 4.6 --- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/src/problem_name.rs | 1 + src/models/misc/mod.rs | 3 + .../misc/precedence_constrained_scheduling.rs | 159 ++++++++++++++++++ src/models/mod.rs | 2 +- .../misc/precedence_constrained_scheduling.rs | 135 +++++++++++++++ 6 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 src/models/misc/precedence_constrained_scheduling.rs create mode 100644 src/unit_tests/models/misc/precedence_constrained_scheduling.rs diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 49dd523c..1a370a75 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, ILP}; -use problemreductions::models::misc::{BinPacking, Knapsack, SubsetSum}; +use problemreductions::models::misc::{BinPacking, Knapsack, PrecedenceConstrainedScheduling, SubsetSum}; use problemreductions::prelude::*; use problemreductions::rules::{MinimizeSteps, ReductionGraph}; use problemreductions::solvers::{BruteForce, ILPSolver, Solver}; @@ -247,6 +247,7 @@ pub fn load_problem( }, "Knapsack" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "PrecedenceConstrainedScheduling" => deser_sat::(data), "SubsetSum" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } @@ -310,6 +311,7 @@ pub fn serialize_any_problem( }, "Knapsack" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "PrecedenceConstrainedScheduling" => try_ser::(any), "SubsetSum" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2b6c8c73..8c896d85 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -55,6 +55,7 @@ pub fn resolve_alias(input: &str) -> String { "cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(), "knapsack" => "Knapsack".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "precedenceconstrainedscheduling" => "PrecedenceConstrainedScheduling".to_string(), "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 36ebe905..89761833 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -5,16 +5,19 @@ //! - [`Factoring`]: Integer factorization //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; pub(crate) mod factoring; mod knapsack; pub(crate) mod paintshop; +mod precedence_constrained_scheduling; mod subset_sum; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use knapsack::Knapsack; pub use paintshop::PaintShop; +pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use subset_sum::SubsetSum; diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs new file mode 100644 index 00000000..b35bfe08 --- /dev/null +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -0,0 +1,159 @@ +//! Precedence Constrained Scheduling problem implementation. +//! +//! Given unit-length tasks with precedence constraints, m processors, and a +//! deadline D, determine whether all tasks can be scheduled to meet D while +//! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "PrecedenceConstrainedScheduling", + module_path: module_path!(), + description: "Schedule unit-length tasks on m processors by deadline D respecting precedence constraints", + fields: &[ + FieldInfo { name: "num_tasks", type_name: "usize", description: "Number of tasks n = |T|" }, + FieldInfo { name: "num_processors", type_name: "usize", description: "Number of processors m" }, + FieldInfo { name: "deadline", type_name: "usize", description: "Global deadline D" }, + FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (i, j) meaning task i must finish before task j starts" }, + ], + } +} + +/// The Precedence Constrained Scheduling problem. +/// +/// Given `n` unit-length tasks with precedence constraints (a partial order), +/// `m` processors, and a deadline `D`, determine whether there exists a schedule +/// assigning each task to a time slot in `{0, ..., D-1}` such that: +/// - At most `m` tasks are assigned to any single time slot +/// - For each precedence `(i, j)`: task `j` starts after task `i` completes, +/// i.e., `slot(j) >= slot(i) + 1` +/// +/// # Representation +/// +/// Each task has a variable in `{0, ..., D-1}` representing its assigned time slot. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::PrecedenceConstrainedScheduling; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3 +/// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrecedenceConstrainedScheduling { + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, +} + +impl PrecedenceConstrainedScheduling { + /// Create a new Precedence Constrained Scheduling instance. + /// + /// # Panics + /// + /// Panics if `num_processors` or `deadline` is zero (when `num_tasks > 0`), + /// or if any precedence index is out of bounds (>= num_tasks). + pub fn new( + num_tasks: usize, + num_processors: usize, + deadline: usize, + precedences: Vec<(usize, usize)>, + ) -> Self { + if num_tasks > 0 { + assert!(num_processors > 0, "num_processors must be > 0 when there are tasks"); + assert!(deadline > 0, "deadline must be > 0 when there are tasks"); + } + for &(i, j) in &precedences { + assert!( + i < num_tasks && j < num_tasks, + "Precedence ({}, {}) out of bounds for {} tasks", + i, + j, + num_tasks + ); + } + Self { + num_tasks, + num_processors, + deadline, + precedences, + } + } + + /// Get the number of tasks. + pub fn num_tasks(&self) -> usize { + self.num_tasks + } + + /// Get the number of processors. + pub fn num_processors(&self) -> usize { + self.num_processors + } + + /// Get the deadline. + pub fn deadline(&self) -> usize { + self.deadline + } + + /// Get the precedence constraints. + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } +} + +impl Problem for PrecedenceConstrainedScheduling { + const NAME: &'static str = "PrecedenceConstrainedScheduling"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.deadline; self.num_tasks] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_tasks { + return false; + } + // Check all values are valid time slots + if config.iter().any(|&v| v >= self.deadline) { + return false; + } + // Check processor capacity: at most num_processors tasks per time slot + let mut slot_count = vec![0usize; self.deadline]; + for &slot in config { + slot_count[slot] += 1; + if slot_count[slot] > self.num_processors { + return false; + } + } + // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 + for &(i, j) in &self.precedences { + if config[j] < config[i] + 1 { + return false; + } + } + true + } +} + +impl SatisfactionProblem for PrecedenceConstrainedScheduling {} + +crate::declare_variants! { + PrecedenceConstrainedScheduling => "deadline ^ num_tasks", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/precedence_constrained_scheduling.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index ceb584ce..91266b5b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,5 +16,5 @@ pub use graph::{ MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; -pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum}; +pub use misc::{BinPacking, Factoring, Knapsack, PaintShop, PrecedenceConstrainedScheduling, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs new file mode 100644 index 00000000..8a3ace31 --- /dev/null +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -0,0 +1,135 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +#[test] +fn test_precedence_constrained_scheduling_basic() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + assert_eq!(problem.num_tasks(), 4); + assert_eq!(problem.num_processors(), 2); + assert_eq!(problem.deadline(), 3); + assert_eq!(problem.precedences(), &[(0, 2), (1, 3)]); + assert_eq!(problem.dims(), vec![3; 4]); + assert_eq!( + ::NAME, + "PrecedenceConstrainedScheduling" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_valid() { + // Issue example: 8 tasks, 3 processors, deadline 4 + // Precedences: 0<2, 0<3, 1<3, 1<4, 2<5, 3<6, 4<6, 5<7, 6<7 + let problem = PrecedenceConstrainedScheduling::new( + 8, + 3, + 4, + vec![ + (0, 2), + (0, 3), + (1, 3), + (1, 4), + (2, 5), + (3, 6), + (4, 6), + (5, 7), + (6, 7), + ], + ); + // Valid schedule: slot 0: {t0, t1}, slot 1: {t2, t3, t4}, slot 2: {t5, t6}, slot 3: {t7} + let config = vec![0, 0, 1, 1, 1, 2, 2, 3]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_precedence() { + // t0 < t1, but we assign both to slot 0 + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![(0, 1)]); + assert!(!problem.evaluate(&[0, 0])); // slot[1] = 0 < slot[0] + 1 = 1 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_capacity() { + // 3 tasks, 2 processors, all in slot 0 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![]); + assert!(!problem.evaluate(&[0, 0, 0])); // 3 tasks in slot 0, capacity 2 +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_wrong_config_length() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[0, 1, 2, 0])); +} + +#[test] +fn test_precedence_constrained_scheduling_evaluate_invalid_variable_value() { + let problem = PrecedenceConstrainedScheduling::new(2, 2, 3, vec![]); + assert!(!problem.evaluate(&[0, 3])); // 3 >= deadline=3 +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force() { + // Small instance: 3 tasks, 2 processors, deadline 2, t0 < t2 + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_precedence_constrained_scheduling_brute_force_all() { + let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_precedence_constrained_scheduling_unsatisfiable() { + // 3 tasks in a chain t0 < t1 < t2, but only deadline 2 (need 3 slots) + let problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]); + let solver = BruteForce::new(); + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_precedence_constrained_scheduling_serialization() { + let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); + let json = serde_json::to_value(&problem).unwrap(); + let restored: PrecedenceConstrainedScheduling = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_tasks(), problem.num_tasks()); + assert_eq!(restored.num_processors(), problem.num_processors()); + assert_eq!(restored.deadline(), problem.deadline()); + assert_eq!(restored.precedences(), problem.precedences()); +} + +#[test] +fn test_precedence_constrained_scheduling_empty() { + let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]); + assert_eq!(problem.num_tasks(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_precedence_constrained_scheduling_no_precedences() { + // 4 tasks, 2 processors, deadline 2, no precedences + let problem = PrecedenceConstrainedScheduling::new(4, 2, 2, vec![]); + // 2 tasks per slot, 2 slots = 4 tasks + assert!(problem.evaluate(&[0, 0, 1, 1])); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} From f4dd7319f2e7c6b74541b43e1fe4420dab46c76f Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 08:05:18 +0000 Subject: [PATCH 3/3] chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 --- ...03-13-precedence-constrained-scheduling.md | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 docs/plans/2026-03-13-precedence-constrained-scheduling.md diff --git a/docs/plans/2026-03-13-precedence-constrained-scheduling.md b/docs/plans/2026-03-13-precedence-constrained-scheduling.md deleted file mode 100644 index dfedf2b1..00000000 --- a/docs/plans/2026-03-13-precedence-constrained-scheduling.md +++ /dev/null @@ -1,82 +0,0 @@ -# Plan: Add PrecedenceConstrainedScheduling Model (#501) - -## Overview - -Add the Precedence Constrained Scheduling problem — a satisfaction problem from Garey & Johnson (A5 SS9). Given unit-length tasks with precedence constraints, m processors, and a deadline D, determine whether all tasks can be scheduled to meet D while respecting precedences. - -This is a satisfaction problem (`Metric = bool`). No type parameters (all unit-length tasks, integer parameters). - -## Steps - -### Step 1: Create Model File - -**File:** `src/models/misc/precedence_constrained_scheduling.rs` - -**Struct:** -```rust -pub struct PrecedenceConstrainedScheduling { - num_tasks: usize, - num_processors: usize, - deadline: usize, - precedences: Vec<(usize, usize)>, -} -``` - -**Constructor:** `new(num_tasks, num_processors, deadline, precedences)` — validate that precedence indices are within bounds. - -**Getters:** `num_tasks()`, `num_processors()`, `deadline()`, `precedences()`. - -**Problem impl:** -- `NAME = "PrecedenceConstrainedScheduling"` -- `type Metric = bool` -- `dims()`: Each task is assigned a time slot in `{0, ..., deadline-1}`, so `vec![deadline; num_tasks]` -- `evaluate(config)`: Check (1) config length == num_tasks, (2) all values < deadline, (3) at most `num_processors` tasks per time slot, (4) for each precedence (i, j): config[j] >= config[i] + 1. Return true iff all constraints satisfied. -- `variant()`: `crate::variant_params![]` - -**SatisfactionProblem:** empty impl. - -**Complexity:** `declare_variants!{ PrecedenceConstrainedScheduling => "deadline ^ num_tasks" }` — brute force bound D^n. - -**Schema registration:** `inventory::submit!` with fields: num_tasks, num_processors, deadline, precedences. - -### Step 2: Register Model - -**`src/models/misc/mod.rs`:** Add `mod precedence_constrained_scheduling;` and `pub use`. - -**`src/models/mod.rs`:** Add to `misc::` re-export line. - -### Step 3: CLI Registration - -**`problemreductions-cli/src/dispatch.rs`:** -- `load_problem`: Add `"PrecedenceConstrainedScheduling" => deser_sat::(data),` -- `serialize_any_problem`: Add `"PrecedenceConstrainedScheduling" => try_ser::(any),` -- Add import for `PrecedenceConstrainedScheduling` from `problemreductions::models::misc` - -**`problemreductions-cli/src/problem_name.rs`:** -- `resolve_alias`: Add `"precedenceconstrainedscheduling" => "PrecedenceConstrainedScheduling".to_string(),` - -### Step 4: Unit Tests - -**File:** `src/unit_tests/models/misc/precedence_constrained_scheduling.rs` - -Tests (following SubsetSum pattern): -1. `test_precedence_constrained_scheduling_basic` — verify construction, getters, dims, NAME, variant -2. `test_precedence_constrained_scheduling_evaluate_valid` — valid schedule from issue example (8 tasks, 3 processors, deadline 4) -3. `test_precedence_constrained_scheduling_evaluate_invalid_precedence` — schedule violating a precedence constraint -4. `test_precedence_constrained_scheduling_evaluate_invalid_capacity` — schedule with too many tasks in one slot -5. `test_precedence_constrained_scheduling_evaluate_wrong_config_length` — wrong-length config -6. `test_precedence_constrained_scheduling_evaluate_invalid_variable_value` — value >= deadline -7. `test_precedence_constrained_scheduling_brute_force` — find_satisfying with small instance -8. `test_precedence_constrained_scheduling_brute_force_all` — find_all_satisfying -9. `test_precedence_constrained_scheduling_unsatisfiable` — instance with no valid schedule -10. `test_precedence_constrained_scheduling_serialization` — round-trip JSON -11. `test_precedence_constrained_scheduling_empty` — 0 tasks -12. `test_precedence_constrained_scheduling_no_precedences` — tasks with no ordering constraints - -Link test module in model file with `#[cfg(test)] #[path = "..."] mod tests;`. - -Also add entry in `src/unit_tests/models/misc/mod.rs` if it exists. - -### Step 5: Verify - -Run `make check` (fmt + clippy + test). Ensure all tests pass.