Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
"MinMaxMulticenter": [Min-Max Multicenter],
"FlowShopScheduling": [Flow Shop Scheduling],
"JobShopScheduling": [Job-Shop Scheduling],
"GroupingBySwapping": [Grouping by Swapping],
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
Expand Down Expand Up @@ -5126,6 +5127,97 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("JobShopScheduling")
let D = x.instance.deadline
let blocks = (
(0, 0, 0, 0, 3),
(0, 1, 1, 3, 6),
(0, 2, 0, 6, 10),
(0, 3, 1, 10, 12),
(0, 4, 0, 12, 14),
(0, 4, 2, 17, 18),
(1, 1, 0, 0, 2),
(1, 3, 0, 2, 7),
(1, 0, 1, 7, 11),
(1, 2, 1, 11, 14),
(1, 4, 1, 14, 17),
(1, 1, 2, 17, 19),
)
let makespan = 19
[
#problem-def("JobShopScheduling")[
Given a positive integer $m$, a set $J$ of jobs, where each job $j in J$ consists of an ordered list of tasks $t_1[j], dots, t_(n_j)[j]$ with processor assignments $p(t_k[j]) in {1, dots, m}$, processing lengths $ell(t_k[j]) in ZZ^+_0$, consecutive-processor constraint $p(t_k[j]) != p(t_(k+1)[j])$, and a deadline $D in ZZ^+$, find start times $sigma(t_k[j]) in ZZ^+_0$ such that tasks sharing a processor do not overlap, each job respects $sigma(t_(k+1)[j]) >= sigma(t_k[j]) + ell(t_k[j])$, and every job finishes by time $D$.
][
Job-Shop Scheduling is the classical disjunctive scheduling problem SS18 in Garey & Johnson; Garey, Johnson, and Sethi proved it strongly NP-complete already for two machines @garey1976. Unlike Flow Shop Scheduling, each job carries its own machine route, so the difficulty lies in choosing a compatible relative order on every machine and then checking whether those local orders admit global start times. This implementation follows the original Garey-Johnson formulation, including the requirement that consecutive tasks of the same job use different processors, and evaluates a witness by orienting the machine-order edges and propagating longest paths through the resulting precedence DAG. The registered baseline therefore exposes a factorial upper bound over task orders#footnote[The auto-generated complexity table records the concrete upper bound used by the Rust implementation; no sharper exact bound is cited here.].

*Example.* The canonical fixture has two machines, deadline $D = #D$, and five jobs
$
J_1 = ((M_1, 3), (M_2, 4)),
J_2 = ((M_2, 2), (M_1, 3), (M_2, 2)),
J_3 = ((M_1, 4), (M_2, 3)),
J_4 = ((M_2, 5), (M_1, 2)),
J_5 = ((M_1, 2), (M_2, 3), (M_1, 1)).
$
The witness stored in the example DB orders the six tasks on $M_1$ as $(J_1^1, J_2^2, J_3^1, J_4^2, J_5^1, J_5^3)$ and the six tasks on $M_2$ as $(J_2^1, J_4^1, J_1^2, J_3^2, J_5^2, J_2^3)$. Taking the earliest schedule consistent with those machine orders yields the Gantt chart in @fig:jobshop, whose last completion time is $#makespan <= #D$, so the verifier returns YES.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o job-shop-scheduling.json",
"pred solve job-shop-scheduling.json --solver brute-force",
"pred evaluate job-shop-scheduling.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 1cm, {
import draw: *
let colors = (rgb("#4e79a7"), rgb("#e15759"), rgb("#76b7b2"), rgb("#f28e2b"), rgb("#59a14f"))
let scale = 0.38
let row-h = 0.6
let gap = 0.15

for mi in range(2) {
let y = -mi * (row-h + gap)
content((-0.8, y), text(8pt, "M" + str(mi + 1)))
}

for block in blocks {
let (mi, ji, ti, s, e) = block
let x0 = s * scale
let x1 = e * scale
let y = -mi * (row-h + gap)
rect(
(x0, y - row-h / 2),
(x1, y + row-h / 2),
fill: colors.at(ji).transparentize(30%),
stroke: 0.4pt + colors.at(ji),
)
content(((x0 + x1) / 2, y), text(6pt, "j" + str(ji + 1) + "." + str(ti + 1)))
}

let y-axis = -(2 - 1) * (row-h + gap) - row-h / 2 - 0.2
line((0, y-axis), (makespan * scale, y-axis), stroke: 0.4pt)
for t in range(calc.ceil(makespan / 5) + 1).map(i => calc.min(i * 5, makespan)) {
let x = t * scale
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
content((x, y-axis - 0.25), text(6pt, str(t)))
}
if calc.rem(makespan, 5) != 0 {
let x = makespan * scale
line((x, y-axis), (x, y-axis - 0.1), stroke: 0.4pt)
content((x, y-axis - 0.25), text(6pt, str(makespan)))
}
content((makespan * scale / 2, y-axis - 0.5), text(7pt)[$t$])

let dl-x = D * scale
line((dl-x, row-h / 2 + 0.1), (dl-x, y-axis), stroke: (paint: red, thickness: 0.8pt, dash: "dashed"))
content((dl-x, row-h / 2 + 0.25), text(6pt, fill: red)[$D = #D$])
}),
caption: [Job-shop schedule induced by the canonical machine-order witness. The final completion time is #makespan, which stays to the left of the deadline marker $D = #D$.],
) <fig:jobshop>
]
]
}

#problem-def("StaffScheduling")[
Given a collection $C$ of binary schedule patterns of length $m$, where each pattern has exactly $k$ ones, a requirement vector $overline(R) in ZZ_(>= 0)^m$, and a worker budget $n in ZZ_(>= 0)$, determine whether there exists a function $f: C -> ZZ_(>= 0)$ such that $sum_(c in C) f(c) <= n$ and $sum_(c in C) f(c) dot c >= overline(R)$ component-wise.
][
Expand Down
8 changes: 6 additions & 2 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ Flags by problem type:
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
QAP --matrix (cost), --distance-matrix
StrongConnectivityAugmentation --arcs, --candidate-arcs, --bound [--num-vertices]
JobShopScheduling --job-tasks, --deadline [--num-processors]
FlowShopScheduling --task-lengths, --deadline [--num-processors]
StaffScheduling --schedules, --requirements, --num-workers, --k
TimetableDesign --num-periods, --num-craftsmen, --num-tasks, --craftsman-avail, --task-avail, --requirements
Expand Down Expand Up @@ -646,10 +647,13 @@ pub struct CreateArgs {
/// Task lengths for FlowShopScheduling (semicolon-separated rows: "3,4,2;2,3,5;4,1,3")
#[arg(long)]
pub task_lengths: Option<String>,
/// Deadline for FlowShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling
/// Job tasks for JobShopScheduling (semicolon-separated jobs, comma-separated processor:length tasks, e.g., "0:3,1:4;1:2,0:3,1:2")
#[arg(long)]
pub job_tasks: Option<String>,
/// Deadline for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, or ResourceConstrainedScheduling
#[arg(long)]
pub deadline: Option<u64>,
/// Number of processors/machines for FlowShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
/// Number of processors/machines for FlowShopScheduling, JobShopScheduling, MultiprocessorScheduling, ResourceConstrainedScheduling, or SchedulingWithIndividualDeadlines
#[arg(long)]
pub num_processors: Option<usize>,
/// Binary schedule patterns for StaffScheduling (semicolon-separated rows, e.g., "1,1,0;0,1,1")
Expand Down
189 changes: 181 additions & 8 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ use problemreductions::models::graph::{
use problemreductions::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation,
ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation,
ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, KnownValue,
LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression,
ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines,
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime,
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition, TimetableDesign,
ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping,
JobShopScheduling, KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing,
MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg,
RectilinearPictureCompression, ResourceConstrainedScheduling,
SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost,
SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence,
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, TimetableDesign,
};
use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
Expand Down Expand Up @@ -150,6 +150,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.resource_bounds.is_none()
&& args.resource_requirements.is_none()
&& args.task_lengths.is_none()
&& args.job_tasks.is_none()
&& args.deadline.is_none()
&& args.num_processors.is_none()
&& args.schedules.is_none()
Expand Down Expand Up @@ -449,6 +450,51 @@ fn parse_precedence_pairs(raw: Option<&str>) -> Result<Vec<(usize, usize)>> {
.unwrap_or_else(|| Ok(vec![]))
}

fn parse_job_shop_jobs(raw: &str) -> Result<Vec<Vec<(usize, u64)>>> {
let raw = raw.trim();
if raw.is_empty() {
return Ok(vec![]);
}

raw.split(';')
.enumerate()
.map(|(job_index, job_str)| {
let job_str = job_str.trim();
anyhow::ensure!(
!job_str.is_empty(),
"Invalid --job-tasks value: empty job at position {}",
job_index
);

job_str
.split(',')
.map(|task_str| {
let task_str = task_str.trim();
let (processor, length) = task_str.split_once(':').ok_or_else(|| {
anyhow::anyhow!(
"Invalid --job-tasks operation '{}': expected 'processor:length'",
task_str
)
})?;
let processor = processor.trim().parse::<usize>().map_err(|_| {
anyhow::anyhow!(
"Invalid --job-tasks operation '{}': processor must be a nonnegative integer",
task_str
)
})?;
let length = length.trim().parse::<u64>().map_err(|_| {
anyhow::anyhow!(
"Invalid --job-tasks operation '{}': length must be a nonnegative integer",
task_str
)
})?;
Ok((processor, length))
})
.collect()
})
.collect()
}

fn validate_precedence_pairs(precedences: &[(usize, usize)], num_tasks: usize) -> Result<()> {
for &(pred, succ) in precedences {
anyhow::ensure!(
Expand Down Expand Up @@ -620,6 +666,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--capacities 1,2,3 --cost-matrix \"1,3,6;2,4,7;1,2,5\" --delay-matrix \"8,4,1;7,3,1;6,3,1\" --cost-budget 10 --delay-budget 12"
}
"MultiprocessorScheduling" => "--lengths 4,5,3,2,6 --num-processors 2 --deadline 10",
"JobShopScheduling" => {
"--job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1\" --deadline 20 --num-processors 2"
}
"MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1",
"ExpectedRetrievalCost" => EXPECTED_RETRIEVAL_COST_EXAMPLE_ARGS,
"SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1",
Expand Down Expand Up @@ -735,9 +784,11 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
("BoundedComponentSpanningForest", "max_components") => return "k".to_string(),
("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(),
("FlowShopScheduling", "num_processors")
| ("JobShopScheduling", "num_processors")
| ("SchedulingWithIndividualDeadlines", "num_processors") => {
return "num-processors/--m".to_string();
}
("JobShopScheduling", "jobs") => return "job-tasks".to_string(),
("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(),
("RectilinearPictureCompression", "bound") => return "bound".to_string(),
("PrimeAttributeName", "num_attributes") => return "universe".to_string(),
Expand Down Expand Up @@ -3560,6 +3611,51 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// JobShopScheduling
"JobShopScheduling" => {
let usage = "Usage: pred create JobShopScheduling --job-tasks \"0:3,1:4;1:2,0:3,1:2;0:4,1:3\" --deadline 20 --num-processors 2";
let job_tasks = args.job_tasks.as_deref().ok_or_else(|| {
anyhow::anyhow!("JobShopScheduling requires --job-tasks and --deadline\n\n{usage}")
})?;
let deadline = args.deadline.ok_or_else(|| {
anyhow::anyhow!("JobShopScheduling requires --deadline\n\n{usage}")
})?;
let jobs = parse_job_shop_jobs(job_tasks)?;
let inferred_processors = jobs
.iter()
.flat_map(|job| job.iter().map(|(processor, _)| *processor))
.max()
.map(|processor| processor + 1);
let num_processors = resolve_processor_count_flags(
"JobShopScheduling",
usage,
args.num_processors,
args.m,
)?
.or(inferred_processors)
.ok_or_else(|| {
anyhow::anyhow!(
"Cannot infer num_processors from empty job list; use --num-processors"
)
})?;
anyhow::ensure!(
num_processors > 0,
"JobShopScheduling requires --num-processors > 0\n\n{usage}"
);
for (job_index, job) in jobs.iter().enumerate() {
for (task_index, &(processor, _)) in job.iter().enumerate() {
anyhow::ensure!(
processor < num_processors,
"job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}"
);
}
}
(
ser(JobShopScheduling::new(num_processors, jobs, deadline))?,
resolved_variant.clone(),
)
}

// StaffScheduling
"StaffScheduling" => {
let usage = "Usage: pred create StaffScheduling --schedules \"1,1,1,1,1,0,0;0,1,1,1,1,1,0;0,0,1,1,1,1,1;1,0,0,1,1,1,1;1,1,0,0,1,1,1\" --requirements 2,2,2,3,3,2,1 --num-workers 4 --k 5";
Expand Down Expand Up @@ -7311,6 +7407,7 @@ mod tests {
deadlines: None,
precedence_pairs: None,
task_lengths: None,
job_tasks: None,
resource_bounds: None,
resource_requirements: None,
deadline: None,
Expand Down Expand Up @@ -7379,6 +7476,13 @@ mod tests {
assert!(!all_data_flags_empty(&args));
}

#[test]
fn test_all_data_flags_empty_treats_job_tasks_as_input() {
let mut args = empty_args();
args.job_tasks = Some("0:1,1:1;1:1,0:1".to_string());
assert!(!all_data_flags_empty(&args));
}

#[test]
fn test_parse_potential_edges() {
let mut args = empty_args();
Expand Down Expand Up @@ -7713,6 +7817,75 @@ mod tests {
assert!(err.contains("ExpectedRetrievalCost requires --latency-bound"));
}

#[test]
fn test_create_job_shop_scheduling_json() {
use crate::dispatch::ProblemJsonOutput;
use problemreductions::models::misc::JobShopScheduling;

let mut args = empty_args();
args.problem = Some("JobShopScheduling".to_string());
args.job_tasks = Some("0:3,1:4;1:2,0:3,1:2;0:4,1:3;1:5,0:2;0:2,1:3,0:1".to_string());
args.deadline = Some(20);

let output_path =
std::env::temp_dir().join(format!("job-shop-scheduling-{}.json", std::process::id()));
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = std::fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "JobShopScheduling");
assert!(created.variant.is_empty());

let problem: JobShopScheduling = serde_json::from_value(created.data).unwrap();
assert_eq!(problem.num_processors(), 2);
assert_eq!(problem.num_jobs(), 5);
assert!(problem.evaluate(&[0, 0, 0, 0, 0, 0, 1, 3, 0, 1, 1, 0]));

let _ = std::fs::remove_file(output_path);
}

#[test]
fn test_create_job_shop_scheduling_requires_job_tasks() {
let mut args = empty_args();
args.problem = Some("JobShopScheduling".to_string());
args.deadline = Some(20);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("JobShopScheduling requires --job-tasks"));
}

#[test]
fn test_create_job_shop_scheduling_rejects_malformed_operation() {
let mut args = empty_args();
args.problem = Some("JobShopScheduling".to_string());
args.job_tasks = Some("0-3,1:4".to_string());
args.deadline = Some(20);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("expected 'processor:length'"));
}

#[test]
fn test_create_rooted_tree_storage_assignment_json() {
let mut args = empty_args();
Expand Down
3 changes: 3 additions & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub fn resolve_alias(input: &str) -> String {
if input.eq_ignore_ascii_case("GroupingBySwapping") {
return "GroupingBySwapping".to_string();
}
if input.eq_ignore_ascii_case("JobShopScheduling") {
return "JobShopScheduling".to_string();
}
if let Some(pt) = problemreductions::registry::find_problem_type_by_alias(input) {
return pt.canonical_name.to_string();
}
Expand Down
Loading
Loading