Source code for discrete_optimization.singlemachine.transformations.to_jsp
# Copyright (c) 2026 AIRBUS and its affiliates.
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
"""Transformation from Single Machine Scheduling to Job Shop Problem.
This is a LOSSY transformation because:
- Due dates are lost (JSP doesn't model deadlines)
- Weights are lost (JSP optimizes makespan, not weighted tardiness)
- Release dates may not be fully enforced (JSP doesn't have built-in release constraints)
"""
from typing import Optional
from discrete_optimization.generic_tools.transformation.problem_transformation import (
ProblemTransformation,
)
from discrete_optimization.generic_tools.transformation.transformation_metadata import (
InformationLoss,
LossImpact,
LossType,
TransformationMetadata,
lossy_transformation,
)
from discrete_optimization.shop.base import Job, Subjob, SubjobRecipe
from discrete_optimization.shop.jsp.problem import JobShopProblem, JobShopSolution
from discrete_optimization.singlemachine.problem import (
WeightedTardinessProblem,
WTSolution,
)
[docs]
class SingleMachineToJspTransformation(
ProblemTransformation[
WeightedTardinessProblem,
WTSolution,
JobShopProblem,
JobShopSolution,
]
):
"""Transform Single Machine Scheduling to Job Shop Problem.
Mapping:
- Single machine → JSP with 1 machine
- Each job → JSP job with 1 subjob
- All subjobs assigned to machine 0
- Processing times map directly
Information Loss:
- Due dates: Lost (JSP doesn't model deadlines)
- Weights: Lost (JSP only optimizes makespan)
- Release dates: Partially lost (not enforced in standard JSP)
This transformation is LOSSY but still useful for:
- Reusing powerful JSP solvers for single machine scheduling
- Focusing on minimizing makespan (ignoring tardiness)
- Benchmarking JSP solvers on single-machine instances
"""
[docs]
def get_forward_metadata(self) -> TransformationMetadata:
"""Metadata for forward transformation (SingleMachine → JSP)."""
return lossy_transformation(
losses=[
InformationLoss(
name="due_dates",
loss_type=LossType.CONSTRAINT,
description="Job due dates / deadlines",
reason="JSP doesn't model deadlines or due dates",
impact=LossImpact.MAJOR,
workaround="Post-process solution to evaluate tardiness",
),
InformationLoss(
name="weights",
loss_type=LossType.OBJECTIVE,
description="Job weights for tardiness penalty",
reason="JSP optimizes makespan, not weighted tardiness",
impact=LossImpact.MAJOR,
workaround="Use makespan as proxy or post-process to compute tardiness",
),
InformationLoss(
name="release_dates",
loss_type=LossType.CONSTRAINT,
description="Job release dates (earliest start times)",
reason="Standard JSP doesn't enforce release dates",
impact=LossImpact.MODERATE,
workaround="Use JSP variant with release constraints if available",
),
],
use_cases=[
"Reuse JSP solvers for single machine scheduling",
"Focus on makespan minimization instead of weighted tardiness",
"Benchmark JSP algorithms on single machine instances",
],
warnings=[
"Due dates and weights are lost",
"Release dates not enforced in standard JSP",
"Optimal JSP solution may not be optimal for weighted tardiness",
],
)
[docs]
def transform_problem(
self, source_problem: WeightedTardinessProblem
) -> JobShopProblem:
"""Transform Single Machine problem to JSP.
Args:
source_problem: WeightedTardinessProblem instance
Returns:
Equivalent JobShopProblem (single machine, single subjob per job)
"""
# Each single-machine job becomes a JSP job with 1 subjob on machine 0
list_jobs = [
Job(
job_index=j,
subjobs=[
Subjob(
job_index=j,
subjob_index=0,
recipes=[
SubjobRecipe(
machine_index=0,
processing_time=source_problem.processing_times[j],
)
],
)
],
)
for j in range(source_problem.num_jobs)
]
return JobShopProblem(
list_jobs=list_jobs,
n_jobs=source_problem.num_jobs,
n_machines=1,
horizon=source_problem.get_makespan_upper_bound(),
)
[docs]
def back_transform_solution(
self, solution: JobShopSolution, source_problem: WeightedTardinessProblem
) -> WTSolution:
"""Transform JSP solution back to Single Machine solution.
Args:
solution: JSP solution (schedule for jobs on 1 machine)
source_problem: Original WeightedTardinessProblem
Returns:
Equivalent WTSolution (permutation + schedule)
"""
# Extract permutation: order jobs by start time
jobs_with_start_times = [
(job_idx, solution.schedule[job_idx][0][0]) # (job, start_time)
for job_idx in range(source_problem.num_jobs)
]
# Sort by start time
sorted_jobs = sorted(jobs_with_start_times, key=lambda x: x[1])
permutation = [job_idx for job_idx, _ in sorted_jobs]
# Build schedule: (start, end) for each job
schedule = [
solution.schedule[job_idx][0] # (start, end) from JSP
for job_idx in range(source_problem.num_jobs)
]
return WTSolution(
problem=source_problem, schedule=schedule, permutation=permutation
)
[docs]
def forward_transform_solution(
self, solution: WTSolution, target_problem: JobShopProblem
) -> Optional[JobShopSolution]:
"""Transform Single Machine solution to JSP solution (for warmstart).
Args:
solution: WTSolution (permutation + schedule)
target_problem: Target JobShopProblem
Returns:
Equivalent JobShopSolution
"""
# Build JSP schedule from single-machine schedule
# Each job has 1 subjob on machine 0
jsp_schedule = [
[solution.schedule[job_idx]] # Wrap in list (single subjob)
for job_idx in range(solution.problem.num_jobs)
]
return JobShopSolution(problem=target_problem, schedule=jsp_schedule)