diff --git a/README.md b/README.md index 39482bbdf..cf1d6b03b 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Or build from source: ```bash git clone https://github.com/CodingThrust/problem-reductions cd problem-reductions -make cli # builds target/release/pred +make cli # installs `pred` from the local workspace ``` See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 01372f7cb..699d78ae6 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -109,6 +109,7 @@ "SubgraphIsomorphism": [Subgraph Isomorphism], "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], + "SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "SequencingWithinIntervals": [Sequencing Within Intervals], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], @@ -2109,6 +2110,82 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#{ + let x = load-model-example("SchedulingWithIndividualDeadlines") + let ntasks = x.instance.num_tasks + let nproc = x.instance.num_processors + let deadlines = x.instance.deadlines + let precs = x.instance.precedences + let sample = x.samples.at(0) + let start = sample.config + let horizon = deadlines.fold(0, (acc, d) => if d > acc { d } else { acc }) + let slot-groups = range(horizon).map(slot => range(ntasks).filter(t => start.at(t) == slot)) + let tight-tasks = range(ntasks).filter(t => start.at(t) + 1 == deadlines.at(t)) + let start-label = start.map(v => str(v)).join(", ") + let deadline-pairs = deadlines.enumerate().map(((t, d)) => [$d(t_#(t + 1)) = #d$]) + let slot-summaries = slot-groups.enumerate().map(((slot, tasks)) => [slot #slot: #tasks.map(task => $t_#(task + 1)$).join(", ")]) + let tight-task-labels = tight-tasks.map(task => $t_#(task + 1)$) + [ + #problem-def("SchedulingWithIndividualDeadlines")[ + Given a set $T$ of $n$ unit-length tasks, a number $m in ZZ^+$ of identical processors, a deadline function $d: T -> ZZ^+$, and a partial order $prec.eq$ on $T$, determine whether there exists a schedule $sigma: T -> {0, 1, dots, D - 1}$, where $D = max_(t in T) d(t)$, such that every task meets its own deadline ($sigma(t) + 1 <= d(t)$), every precedence constraint is respected (if $t_i prec.eq t_j$ then $sigma(t_i) + 1 <= sigma(t_j)$), and at most $m$ tasks are scheduled in each time slot. + ][ + Scheduling With Individual Deadlines is the parallel-machine feasibility problem catalogued as A5 SS11 in Garey & Johnson @garey1979. Garey & Johnson record NP-completeness via reduction from Vertex Cover, and Brucker, Garey, and Johnson sharpen the complexity picture: the problem remains NP-complete for out-tree precedence constraints, but becomes polynomial-time solvable for in-trees @bruckerGareyJohnson1977. The two-processor case is also polynomial-time solvable @garey1979. + + The direct encoding in this library uses one start-time variable per task, with each variable ranging over its allowable deadline window. If $D = max_t d(t)$, exhaustive search over that encoding yields an $O^*(D^n)$ brute-force bound.#footnote[This is the worst-case search bound induced by the implementation's configuration space; deadlines can be smaller on individual tasks, so practical instances may enumerate fewer than $D^n$ assignments.] + + *Example.* Consider $n = #ntasks$ tasks on $m = #nproc$ processors with deadlines #{deadline-pairs.join(", ")} and precedence constraints #{precs.map(p => [$t_#(p.at(0) + 1) prec.eq t_#(p.at(1) + 1)$]).join(", ")}. The sample schedule $sigma = [#start-label]$ assigns #{slot-summaries.join("; ")}. Every slot uses at most #nproc processors, and the tight tasks #{tight-task-labels.join(", ")} finish exactly at their deadlines. + + #figure( + canvas(length: 1cm, { + import draw: * + let colors = ( + rgb("#4e79a7"), + rgb("#e15759"), + rgb("#76b7b2"), + rgb("#f28e2b"), + rgb("#59a14f"), + rgb("#edc948"), + rgb("#b07aa1"), + ) + let scale = 1.25 + let row-h = 0.58 + let gap = 0.18 + + for lane in range(nproc) { + let y = -lane * (row-h + gap) + content((-0.8, y), text(7pt, "P" + str(lane + 1))) + } + + for (slot, tasks) in slot-groups.enumerate() { + for (lane, task) in tasks.enumerate() { + let x0 = slot * scale + let x1 = (slot + 1) * scale + let y = -lane * (row-h + gap) + let color = colors.at(calc.rem(task, colors.len())) + rect( + (x0, y - row-h / 2), + (x1, y + row-h / 2), + fill: color.transparentize(30%), + stroke: 0.4pt + color, + ) + content(((x0 + x1) / 2, y), text(7pt)[$t_#(task + 1)$]) + } + } + + let y-axis = -(nproc - 1) * (row-h + gap) - row-h / 2 - 0.2 + line((0, y-axis), (horizon * scale, y-axis), stroke: 0.4pt) + for t in range(horizon + 1) { + let x = t * scale + line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt) + content((x, y-axis - 0.24), text(6pt, str(t))) + } + content((horizon * scale / 2, y-axis - 0.46), text(7pt)[time slot]) + }), + caption: [A feasible 3-processor schedule for Scheduling With Individual Deadlines. Tasks sharing a column run in the same unit-length time slot; the sample assignment uses slots $0, 1, 2$ and meets every deadline.], + ) + ] + ] +} #{ let x = load-model-example("SequencingWithinIntervals") let ntasks = x.instance.lengths.len() diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..2e0cdcbe4 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -64,6 +64,16 @@ @book{garey1979 year = {1979} } +@article{bruckerGareyJohnson1977, + author = {Peter Brucker and Michael R. Garey and David S. Johnson}, + title = {Scheduling equal-length tasks under tree-like precedence constraints to minimize maximum lateness}, + journal = {Mathematics of Operations Research}, + volume = {2}, + number = {3}, + pages = {275--284}, + year = {1977} +} + @article{gareyJohnsonStockmeyer1976, author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer}, title = {Some Simplified {NP}-Complete Graph Problems}, diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..01e00880b 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -298,11 +298,16 @@ pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json +pred create SchedulingWithIndividualDeadlines --n 7 --deadlines 2,1,2,2,3,3,2 --num-processors 3 --precedence-pairs "0>3,1>3,1>4,2>4,2>5" -o swid.json +pred solve swid.json --solver brute-force ``` For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field `max_length`. +For problem-specific create help, run `pred create ` with no additional flags. +The generic `pred create --help` output lists all flags across all problem types. + Canonical examples are useful when you want a known-good instance from the paper/example database. For model examples, `pred create --example ` emits the canonical instance for that graph node. @@ -372,12 +377,31 @@ pred create MIS --graph 0-1,1-2,2-3 | pred evaluate - --config 1,0,1,0 ### `pred inspect` — Inspect a problem file -Show a summary of what's inside a problem JSON or reduction bundle: +Show JSON metadata about what's inside a problem JSON or reduction bundle: ```bash $ pred inspect problem.json -Type: MaximumIndependentSet {graph=SimpleGraph, weight=i32} -Size: 5 vertices, 5 edges +{ + "kind": "problem", + "num_variables": 4, + "reduces_to": [ + "MaximumSetPacking", + "MinimumVertexCover" + ], + "size_fields": [ + "num_vertices", + "num_edges" + ], + "solvers": [ + "ilp", + "brute-force" + ], + "type": "MaximumIndependentSet", + "variant": { + "graph": "SimpleGraph", + "weight": "i32" + } +} ``` Works with reduction bundles and stdin: diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e71a9e426..9fb910874 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -249,6 +249,7 @@ Flags by problem type: FVS --arcs [--weights] [--num-vertices] FlowShopScheduling --task-lengths, --deadline [--num-processors] MinimumTardinessSequencing --n, --deadlines [--precedence-pairs] + SchedulingWithIndividualDeadlines --n, --num-processors/--m, --deadlines [--precedence-pairs] SCS --strings, --bound [--alphabet-size] D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 ILP, CircuitSAT (via reduction only) @@ -342,7 +343,7 @@ pub struct CreateArgs { /// Target value (for Factoring and SubsetSum) #[arg(long)] pub target: Option, - /// Bits for first factor (for Factoring) + /// Bits for first factor (for Factoring); also accepted as a processor-count alias for scheduling create commands #[arg(long)] pub m: Option, /// Bits for second factor (for Factoring) @@ -438,10 +439,10 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, - /// Deadlines for MinimumTardinessSequencing (comma-separated, e.g., "5,5,5,3,3") + /// Deadlines for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines (comma-separated, e.g., "5,5,5,3,3") #[arg(long)] pub deadlines: Option, - /// Precedence pairs for MinimumTardinessSequencing (e.g., "0>3,1>3,1>4,2>4") + /// Precedence pairs for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines (e.g., "0>3,1>3,1>4,2>4") #[arg(long)] pub precedence_pairs: Option, /// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3") @@ -450,7 +451,7 @@ pub struct CreateArgs { /// Deadline for FlowShopScheduling #[arg(long)] pub deadline: Option, - /// Number of processors/machines for FlowShopScheduling + /// Number of processors/machines for FlowShopScheduling or SchedulingWithIndividualDeadlines #[arg(long)] pub num_processors: Option, /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) @@ -573,3 +574,38 @@ pub fn print_subcommand_help_hint(error_msg: &str) { } } } + +#[cfg(test)] +mod tests { + use super::Cli; + use clap::CommandFactory; + + #[test] + fn test_create_help_mentions_scheduling_with_individual_deadlines_shared_flags() { + let mut cmd = Cli::command(); + let create = cmd + .find_subcommand_mut("create") + .expect("create subcommand"); + let mut help = Vec::new(); + create + .write_long_help(&mut help) + .expect("render create help"); + let help = String::from_utf8(help).expect("utf8 help"); + + assert!(help.contains( + "Deadlines for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines" + )); + assert!(help.contains( + "Precedence pairs for MinimumTardinessSequencing or SchedulingWithIndividualDeadlines" + )); + assert!( + help.contains( + "Number of processors/machines for FlowShopScheduling or SchedulingWithIndividualDeadlines" + ), + "create help should describe --num-processors for both scheduling models" + ); + assert!(help.contains( + "SchedulingWithIndividualDeadlines --n, --num-processors/--m, --deadlines [--precedence-pairs]" + )); + } +} diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 03220f4f6..1a4ac50d8 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,7 +14,8 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, + PaintShop, SchedulingWithIndividualDeadlines, SequencingWithinIntervals, + ShortestCommonSupersequence, SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -233,7 +234,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec>" => "semicolon-separated groups: \"0,1;2,3\"", "usize" => "integer", "u64" => "integer", - "Vec" => "comma-separated integers: 0,0,5", "i64" => "integer", "BigUint" => "nonnegative decimal integer", "Vec" => "comma-separated nonnegative decimal integers: 3,7,1,8", @@ -305,6 +305,10 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { match (canonical, field_name) { ("BoundedComponentSpanningForest", "max_components") => return "k".to_string(), ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), + ("FlowShopScheduling", "num_processors") + | ("SchedulingWithIndividualDeadlines", "num_processors") => { + return "num-processors/--m".to_string(); + } _ => {} } // General field-name overrides (previously in cli_flag_name) @@ -331,6 +335,7 @@ fn help_flag_hint( ) -> &'static str { match (canonical, field_name) { ("BoundedComponentSpanningForest", "max_weight") => "integer", + ("SequencingWithinIntervals", "release_times") => "comma-separated integers: 0,0,5", _ => type_format_hint(type_name, graph_type), } } @@ -340,6 +345,26 @@ fn parse_nonnegative_usize_bound(bound: i64, problem_name: &str, usage: &str) -> .map_err(|_| anyhow::anyhow!("{problem_name} requires nonnegative --bound\n\n{usage}")) } +fn resolve_processor_count_flags( + problem_name: &str, + usage: &str, + num_processors: Option, + m_alias: Option, +) -> Result> { + match (num_processors, m_alias) { + (Some(num_processors), Some(m_alias)) => { + anyhow::ensure!( + num_processors == m_alias, + "{problem_name} received conflicting processor counts: --num-processors={num_processors} but --m={m_alias}\n\n{usage}" + ); + Ok(Some(num_processors)) + } + (Some(num_processors), None) => Ok(Some(num_processors)), + (None, Some(m_alias)) => Ok(Some(m_alias)), + (None, None) => Ok(None), + } +} + fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { let is_geometry = matches!( graph_type, @@ -1218,6 +1243,75 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // SchedulingWithIndividualDeadlines + "SchedulingWithIndividualDeadlines" => { + let usage = "Usage: pred create SchedulingWithIndividualDeadlines --n 7 --deadlines 2,1,2,2,3,3,2 [--num-processors 3 | --m 3] [--precedence-pairs \"0>3,1>3,1>4,2>4,2>5\"]"; + let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --deadlines, --n, and a processor count (--num-processors or --m)\n\n{usage}" + ) + })?; + let num_tasks = args.n.ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --n (number of tasks)\n\n{usage}" + ) + })?; + let num_processors = resolve_processor_count_flags( + "SchedulingWithIndividualDeadlines", + usage, + args.num_processors, + args.m, + )? + .ok_or_else(|| { + anyhow::anyhow!( + "SchedulingWithIndividualDeadlines requires --num-processors or --m\n\n{usage}" + ) + })?; + let deadlines: Vec = util::parse_comma_list(deadlines_str)?; + let precedences: Vec<(usize, usize)> = match args.precedence_pairs.as_deref() { + Some(s) if !s.is_empty() => s + .split(',') + .map(|pair| { + let parts: Vec<&str> = pair.trim().split('>').collect(); + anyhow::ensure!( + parts.len() == 2, + "Invalid precedence format '{}', expected 'u>v'", + pair.trim() + ); + Ok(( + parts[0].trim().parse::()?, + parts[1].trim().parse::()?, + )) + }) + .collect::>>()?, + _ => vec![], + }; + anyhow::ensure!( + deadlines.len() == num_tasks, + "deadlines length ({}) must equal num_tasks ({})", + deadlines.len(), + num_tasks + ); + for &(pred, succ) in &precedences { + anyhow::ensure!( + pred < num_tasks && succ < num_tasks, + "precedence index out of range: ({}, {}) but num_tasks = {}", + pred, + succ, + num_tasks + ); + } + ( + ser(SchedulingWithIndividualDeadlines::new( + num_tasks, + num_processors, + deadlines, + precedences, + ))?, + resolved_variant.clone(), + ) + } + // SequencingWithinIntervals "SequencingWithinIntervals" => { let usage = "Usage: pred create SequencingWithinIntervals --release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1"; @@ -1278,15 +1372,18 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { .split(';') .map(|row| util::parse_comma_list(row.trim())) .collect::>>()?; - let num_processors = if let Some(np) = args.num_processors { - np - } else if let Some(m) = args.m { - m - } else if let Some(first) = task_lengths.first() { - first.len() - } else { - bail!("Cannot infer num_processors from empty task list; use --num-processors"); - }; + let num_processors = resolve_processor_count_flags( + "FlowShopScheduling", + "Usage: pred create FlowShopScheduling --task-lengths \"3,4,2;2,3,5;4,1,3\" --deadline 25 --num-processors 3", + args.num_processors, + args.m, + )? + .or_else(|| task_lengths.first().map(Vec::len)) + .ok_or_else(|| { + anyhow::anyhow!( + "Cannot infer num_processors from empty task list; use --num-processors" + ) + })?; for (j, row) in task_lengths.iter().enumerate() { if row.len() != num_processors { bail!( @@ -2412,7 +2509,10 @@ fn create_random( #[cfg(test)] mod tests { - use super::problem_help_flag_name; + use super::{create, help_flag_name, problem_help_flag_name}; + use crate::cli::{Cli, Commands}; + use crate::output::OutputConfig; + use clap::Parser; #[test] fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() { @@ -2434,4 +2534,55 @@ mod tests { "num-paths-required" ); } + + #[test] + fn test_help_flag_name_mentions_m_alias_for_scheduling_processors() { + assert_eq!( + help_flag_name("SchedulingWithIndividualDeadlines", "num_processors"), + "num-processors/--m" + ); + assert_eq!( + help_flag_name("FlowShopScheduling", "num_processors"), + "num-processors/--m" + ); + } + + #[test] + fn test_create_scheduling_with_individual_deadlines_accepts_m_alias() { + let cli = Cli::try_parse_from([ + "pred", + "create", + "SchedulingWithIndividualDeadlines", + "--n", + "3", + "--deadlines", + "1,1,2", + "--m", + "2", + ]) + .expect("parse create command"); + + let Commands::Create(args) = cli.command else { + panic!("expected create subcommand"); + }; + + let out = OutputConfig { + output: Some( + std::env::temp_dir() + .join("pred_test_create_scheduling_with_individual_deadlines_m_alias.json"), + ), + quiet: true, + json: false, + auto_json: false, + }; + create(&args, &out).expect("`--m` should satisfy --num-processors alias"); + + let created: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(out.output.as_ref().unwrap()).unwrap()) + .unwrap(); + std::fs::remove_file(out.output.as_ref().unwrap()).ok(); + + assert_eq!(created["type"], "SchedulingWithIndividualDeadlines"); + assert_eq!(created["data"]["num_processors"], 2); + } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..bcd4aa060 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4816,6 +4816,82 @@ fn test_create_sequencing_within_intervals() { std::fs::remove_file(&output_file).ok(); } +#[test] +fn test_create_scheduling_with_individual_deadlines_with_m_alias() { + let output_file = + std::env::temp_dir().join("pred_test_create_scheduling_with_individual_deadlines.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "SchedulingWithIndividualDeadlines", + "--n", + "7", + "--deadlines", + "2,1,2,2,3,3,2", + "--m", + "3", + "--precedence-pairs", + "0>3,1>3,1>4,2>4,2>5", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "SchedulingWithIndividualDeadlines"); + assert_eq!(json["data"]["num_processors"], 3); + assert_eq!(json["data"]["num_tasks"], 7); + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_scheduling_with_individual_deadlines_help_mentions_m_alias() { + let output = pred() + .args(["create", "SchedulingWithIndividualDeadlines"]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "problem-specific help should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--num-processors/--m"), + "expected alias in problem-specific help, got: {stderr}" + ); +} + +#[test] +fn test_create_scheduling_with_individual_deadlines_rejects_conflicting_processor_flags() { + let output = pred() + .args([ + "create", + "SchedulingWithIndividualDeadlines", + "--n", + "3", + "--deadlines", + "1,1,2", + "--num-processors", + "3", + "--m", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("conflicting processor counts"), + "expected conflict error, got: {stderr}" + ); +} + #[test] fn test_create_model_example_sequencing_within_intervals() { let output = pred() diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 6e52883b2..31d94819f 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -32,6 +32,7 @@ {"problem":"PartitionIntoTriangles","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[3,4,null],[3,5,null],[4,5,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,1,1],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1],"metric":true},{"config":[1,1,1,0,0,0],"metric":true}]}, {"problem":"QUBO","variant":{"weight":"f64"},"instance":{"matrix":[[-1.0,2.0,0.0],[0.0,-1.0,2.0],[0.0,0.0,-1.0]],"num_vars":3},"samples":[{"config":[1,0,1],"metric":{"Valid":-2.0}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":-2.0}}]}, {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, + {"problem":"SchedulingWithIndividualDeadlines","variant":{},"instance":{"deadlines":[2,1,2,2,3,3,2],"num_processors":3,"num_tasks":7,"precedences":[[0,3],[1,3],[1,4],[2,4],[2,5]]},"samples":[{"config":[0,0,0,1,2,1,1],"metric":true}],"optimal":[{"config":[0,0,0,1,1,2,1],"metric":true},{"config":[0,0,0,1,2,1,1],"metric":true},{"config":[0,0,0,1,2,2,1],"metric":true},{"config":[0,0,1,1,2,2,0],"metric":true},{"config":[0,0,1,1,2,2,1],"metric":true}]}, {"problem":"SequencingWithinIntervals","variant":{},"instance":{"deadlines":[11,11,11,11,6],"lengths":[3,1,2,4,1],"release_times":[0,0,0,0,5]},"samples":[{"config":[0,6,3,7,0],"metric":true}],"optimal":[{"config":[0,6,3,7,0],"metric":true},{"config":[0,10,3,6,0],"metric":true},{"config":[2,6,0,7,0],"metric":true},{"config":[2,10,0,6,0],"metric":true},{"config":[6,0,9,1,0],"metric":true},{"config":[6,4,9,0,0],"metric":true},{"config":[8,0,6,1,0],"metric":true},{"config":[8,4,6,0,0],"metric":true}]}, {"problem":"SetBasis","variant":{},"instance":{"collection":[[0,1],[1,2],[0,2],[0,1,2]],"k":3,"universe_size":4},"samples":[{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,0,1,0,0,1,0,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0],"metric":true},{"config":[0,1,0,0,1,0,0,0,0,0,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,1,1,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,0,1,0,0,0,0,1,0],"metric":true},{"config":[1,0,1,0,0,1,1,0,1,1,0,0],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[1,1,0,0,0,1,1,0,1,0,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,1,1,0],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..bbe87683d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,8 +58,8 @@ pub mod prelude { }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + MinimumTardinessSequencing, PaintShop, SchedulingWithIndividualDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index c4b125274..761541dbf 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -8,6 +8,7 @@ //! - [`LongestCommonSubsequence`]: Longest Common Subsequence //! - [`MinimumTardinessSequencing`]: Minimize tardy tasks in single-machine scheduling //! - [`PaintShop`]: Minimize color switches in paint shop scheduling +//! - [`SchedulingWithIndividualDeadlines`]: Meet per-task deadlines on parallel processors //! - [`SequencingWithinIntervals`]: Schedule tasks within time windows //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value @@ -19,6 +20,7 @@ mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; pub(crate) mod paintshop; +mod scheduling_with_individual_deadlines; mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; mod subset_sum; @@ -30,6 +32,7 @@ pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; pub use paintshop::PaintShop; +pub use scheduling_with_individual_deadlines::SchedulingWithIndividualDeadlines; pub use sequencing_within_intervals::SequencingWithinIntervals; pub use shortest_common_supersequence::ShortestCommonSupersequence; pub use subset_sum::SubsetSum; @@ -39,6 +42,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Deadline d(t) for each task" }, + FieldInfo { name: "precedences", type_name: "Vec<(usize, usize)>", description: "Precedence pairs (predecessor, successor)" }, + ], + } +} + +/// Scheduling With Individual Deadlines. +/// +/// A configuration assigns each task `t` a start slot `sigma(t)` with domain +/// `0..d(t)`. The schedule is feasible if every precedence pair `(u, v)` +/// satisfies `sigma(u) + 1 <= sigma(v)` and no time slot hosts more than +/// `num_processors` tasks. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SchedulingWithIndividualDeadlines { + num_tasks: usize, + num_processors: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} + +impl SchedulingWithIndividualDeadlines { + pub fn new( + num_tasks: usize, + num_processors: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, + ) -> Self { + assert_eq!( + deadlines.len(), + num_tasks, + "deadlines length must equal num_tasks" + ); + for &(pred, succ) in &precedences { + assert!( + pred < num_tasks, + "predecessor index {} out of range (num_tasks = {})", + pred, + num_tasks + ); + assert!( + succ < num_tasks, + "successor index {} out of range (num_tasks = {})", + succ, + num_tasks + ); + } + + Self { + num_tasks, + num_processors, + deadlines, + precedences, + } + } + + pub fn num_tasks(&self) -> usize { + self.num_tasks + } + + pub fn num_processors(&self) -> usize { + self.num_processors + } + + pub fn deadlines(&self) -> &[usize] { + &self.deadlines + } + + pub fn precedences(&self) -> &[(usize, usize)] { + &self.precedences + } + + pub fn num_precedences(&self) -> usize { + self.precedences.len() + } + + pub fn max_deadline(&self) -> usize { + self.deadlines.iter().copied().max().unwrap_or(0) + } +} + +impl Problem for SchedulingWithIndividualDeadlines { + const NAME: &'static str = "SchedulingWithIndividualDeadlines"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + self.deadlines.clone() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.num_tasks { + return false; + } + + for (&start, &deadline) in config.iter().zip(&self.deadlines) { + if start >= deadline { + return false; + } + } + + for &(pred, succ) in &self.precedences { + if config[pred] + 1 > config[succ] { + return false; + } + } + + let mut slot_loads = BTreeMap::new(); + for &start in config { + let load = slot_loads.entry(start).or_insert(0usize); + *load += 1; + if *load > self.num_processors { + return false; + } + } + + true + } +} + +impl SatisfactionProblem for SchedulingWithIndividualDeadlines {} + +crate::declare_variants! { + default sat SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "scheduling_with_individual_deadlines", + build: || { + let problem = SchedulingWithIndividualDeadlines::new( + 7, + 3, + vec![2, 1, 2, 2, 3, 3, 2], + vec![(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)], + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 0, 1, 2, 1, 1]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/scheduling_with_individual_deadlines.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 072034f02..949f7df3a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -22,7 +22,7 @@ pub use graph::{ }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, - SubsetSum, + MinimumTardinessSequencing, PaintShop, SchedulingWithIndividualDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; diff --git a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs new file mode 100644 index 000000000..6c46ac905 --- /dev/null +++ b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs @@ -0,0 +1,137 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +fn issue_example_problem() -> SchedulingWithIndividualDeadlines { + SchedulingWithIndividualDeadlines::new( + 7, + 3, + vec![2, 1, 2, 2, 3, 3, 2], + vec![(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)], + ) +} + +#[test] +fn test_scheduling_with_individual_deadlines_basic() { + let problem = issue_example_problem(); + + assert_eq!(problem.num_tasks(), 7); + assert_eq!(problem.num_processors(), 3); + assert_eq!(problem.deadlines(), &[2, 1, 2, 2, 3, 3, 2]); + assert_eq!( + problem.precedences(), + &[(0, 3), (1, 3), (1, 4), (2, 4), (2, 5)] + ); + assert_eq!(problem.num_precedences(), 5); + assert_eq!(problem.max_deadline(), 3); + assert_eq!(problem.dims(), vec![2, 1, 2, 2, 3, 3, 2]); + assert_eq!( + ::NAME, + "SchedulingWithIndividualDeadlines" + ); + assert_eq!( + ::variant(), + vec![] + ); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_issue_example() { + let problem = issue_example_problem(); + + assert!(problem.evaluate(&[0, 0, 0, 1, 2, 1, 1])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_rejects_wrong_length() { + let problem = issue_example_problem(); + + assert!(!problem.evaluate(&[0, 0, 0])); + assert!(!problem.evaluate(&[0, 0, 0, 1, 2, 1, 1, 0])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_rejects_deadline_violation() { + let problem = issue_example_problem(); + + assert!(!problem.evaluate(&[0, 1, 0, 1, 2, 1, 1])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_rejects_precedence_violation() { + let problem = issue_example_problem(); + + assert!(!problem.evaluate(&[0, 0, 0, 0, 2, 1, 1])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_rejects_capacity_violation() { + let problem = issue_example_problem(); + + assert!(!problem.evaluate(&[0, 0, 0, 1, 2, 1, 0])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_evaluate_handles_huge_sparse_deadline() { + let problem = SchedulingWithIndividualDeadlines::new(1, 1, vec![usize::MAX], vec![]); + + let result = std::panic::catch_unwind(|| problem.evaluate(&[0])); + + assert!(matches!(result, Ok(true))); +} + +#[test] +fn test_scheduling_with_individual_deadlines_brute_force_satisfiable() { + let problem = SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 2], vec![(0, 2)]); + let solver = BruteForce::new(); + + assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0, 0, 1]]); + assert_eq!(solver.find_satisfying(&problem), Some(vec![0, 0, 1])); +} + +#[test] +fn test_scheduling_with_individual_deadlines_brute_force_unsatisfiable() { + let problem = SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); +} + +#[test] +fn test_scheduling_with_individual_deadlines_serialization() { + let problem = issue_example_problem(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: SchedulingWithIndividualDeadlines = 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.deadlines(), problem.deadlines()); + assert_eq!(restored.precedences(), problem.precedences()); +} + +#[test] +fn test_scheduling_with_individual_deadlines_paper_example() { + let problem = issue_example_problem(); + let solver = BruteForce::new(); + + let satisfying = solver.find_all_satisfying(&problem); + + assert!(problem.evaluate(&[0, 0, 0, 1, 2, 1, 1])); + assert!(satisfying.contains(&vec![0, 0, 0, 1, 2, 1, 1])); + assert_eq!( + solver.find_satisfying(&problem), + satisfying.into_iter().next() + ); +} + +#[test] +#[should_panic(expected = "deadlines length must equal num_tasks")] +fn test_scheduling_with_individual_deadlines_mismatched_deadlines() { + SchedulingWithIndividualDeadlines::new(2, 1, vec![1], vec![]); +} + +#[test] +#[should_panic(expected = "predecessor index 4 out of range")] +fn test_scheduling_with_individual_deadlines_invalid_precedence() { + SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 1], vec![(4, 1)]); +}