Hello! Welcome back to our final lesson in the module on Advanced Hybrid Optimization Methods.
In our last session, we focused on the design of a powerful hybrid optimization pattern: using Constraint Programming (CP) as an intelligent subroutine within a Large Neighborhood Search (LNS) framework. We discussed how to create partial solutions by "ruining" parts of a complete schedule and letting the CP solver "recreate" them optimally.
Today, we transition from design to practice. Your learning outcome is to implement a Large Neighborhood Search (LNS) framework that uses CP to repair or improve solutions. We'll break down the core algorithm, detail the data structures and logic needed for the key components, and provide a clear blueprint that you can use to build this solver for your thesis work on the Flexible Job Shop Problem (FJSP).
The LNS "Destroy and Repair" Loop
Let's begin by recapping the overall structure of an LNS algorithm. It's an iterative process that systematically destroys a part of a solution and then intelligently repairs it, seeking improvements at each step.

This "destroy and repair" (or "ruin and recreate") cycle is the engine of the LNS metaheuristic. Let's formalize this into an algorithm that you can implement.
The Core LNS-CP Algorithm
The general algorithm for an LNS framework that uses CP as its repair mechanism is straightforward. We can model it based on the state-of-the-art approaches used in academic research.
To start, let's watch a brief segment of the video from our last lesson to refresh our memory of the LNS process.
Discrete Optimization || 01 Large Neighborhood Search asymmetric TSP with time windows 8 42
This clip from 'Discrete Optimization || 01 Large Neighborhood Search' quickly recaps the essential steps of LNS, setting the stage for our implementation discussion.
Please watch from 00:31 to 01:33. Focus on the sequence of steps: (1) find an initial feasible solution, (2) select a neighborhood (destroy), (3) use CP to optimize that neighborhood (repair), and (4) repeat.
Now, let's translate this into a concrete algorithmic structure. A great reference is Algorithm 1 from the paper "A unified solution framework for flexible job shop scheduling..." by Kasapidis et al., which we studied in the last lesson. We will adapt and simplify it into a foundational LNS framework.
Here is the pseudocode for your core LNS loop:
Function LNS_CP_Solver(problem_instance, time_limit):
1. // Initialization
2. start_time = CurrentTime()
3. s_best = FindInitialSolution(problem_instance, time_limit_initial)
4. s_current = s_best
5.
6. // Main Loop
7. WHILE (CurrentTime() - start_time < time_limit):
8. // 1. Ruin Phase: Select operator and destroy part of the solution
9. destroy_operator = SelectDestroyOperator()
10. partial_solution = destroy_operator.apply(s_current)
11. fixing_constraints = GenerateFixingConstraints(partial_solution)
12.
13. // 2. Repair Phase: Use CP to find a new solution
14. s_new = CPSolve(problem_instance, fixing_constraints, time_limit_per_iteration)
15.
16. // 3. Acceptance Phase
17. IF Objective(s_new) < Objective(s_current):
18. s_current = s_new
19. IF Objective(s_new) < Objective(s_best):
20. s_best = s_new
21. print("New best solution found:", Objective(s_best))
22.
23. RETURN s_best
Let's break down each part:
- Initialization (Lines 2-4): You start by finding an initial feasible solution. This is typically done by running your base CP model with a short time limit. This solution,
s_best, is your initial benchmark. - Main Loop (Line 7): The search continues until a global stopping criterion is met, like a total time budget.
- Ruin Phase (Lines 9-11): At each iteration, you select a "destroy" operator. This operator takes the current solution
s_currentand decides which parts to "free up". The output is essentially a set offixing_constraintsthat lock down the parts of the solution you didn't destroy. - Repair Phase (Line 14): This is the call to your CP solver. You solve the original problem model, but you add the
fixing_constraints. Crucially, this call has its own, much shorter, time limit (time_limit_per_iteration). Its job is to find a good completion quickly, not to prove optimality. - Acceptance Phase (Lines 17-21): This is the simplest acceptance criterion: only accept improvements (a "greedy" approach). If the new solution
s_newis better thans_current, you accept it. If it's also the best seen so far, you updates_best.
Implementing the Destruction Operators
The real art of LNS lies in the implementation of the destruction operators. Their goal is to define a "neighborhood" of the current solution to be explored by the CP solver. A good operator generates subproblems that are both small enough to be solved quickly and promising enough to contain better solutions.
The paper "Large Neighborhood Search and Adaptive Randomized Decompositions for Flexible Jobshop Scheduling" provides very clear definitions that are perfect for a first implementation.
Large Neighborhood Search and Adaptive Randomized ...
This paper by Pacino and Van Hentenryck offers a foundational approach to implementing LNS for FJSP. It clearly defines several key neighborhood types.
Please read Section 5, 'Large Neighborhood Search'. Focus on the three main neighborhoods defined: (1) random, (2) time-window, and (3) machine. Also, pay close attention to the definition of a 'Fully POS-feasible Solution'. This concept is critical for implementation: it tells us that we should fix the machine assignments and the relative ordering of operations for the 'undestroyed' part of the schedule.
Based on what you've read, let's outline how to implement these three fundamental operators. In each case, the operator must perform two tasks:
- Select a set of operations to "free," let's call this set .
- Generate fixing constraints for all other operations, , where is the set of all operations.
1. Random Operator
This operator is the simplest and is great for diversifying the search.
- Logic:
- Choose a destruction rate,
p(e.g., 0.2 for 20%). - Randomly select
p * |Ω|operations and add them to the set .
- Choose a destruction rate,
- Fixing Constraints: For all operations in .
2. Time-Window Operator
This operator focuses the search on a specific temporal part of the schedule.
- Logic:
- Get the makespan
C_maxof the current solution. - Choose a window size,
w(e.g.,0.3 * C_max). - Choose a random start time for the window,
t_start, between0andC_max - w. The window is[t_start, t_start + w]. - Iterate through all operations. An operation
iis added to if its schedule ins_currentoverlaps with the window (i.e.,start_time(i) < t_start + wandend_time(i) > t_start).
- Get the makespan
- Fixing Constraints: For all operations in .
3. Machine Operator
This operator focuses on re-optimizing the workload of one or more machines, which is often effective for resolving bottlenecks.
- Logic:
- Choose a number of machines to free,
k(e.g., 1 or 2). - Randomly select
kmachines. - Add all operations currently assigned to these
kmachines ins_currentto the set .
- Choose a number of machines to free,
- Fixing Constraints: For all operations in .
Generating the Fixing Constraints
This is the most technical part of the implementation. Based on the concept of a "Fully POS-feasible Solution", for every operation , you must constrain its machine assignment and its relative order with other fixed operations on the same machine.
Here is the pseudocode for the GenerateFixingConstraints function:
Function GenerateFixingConstraints(s_current, O_fixed):
constraints = []
// 1. Fix machine assignments
FOR each operation i IN O_fixed:
assigned_machine = s_current.get_machine(i)
// This assumes your CP model has a way to enforce an assignment,
// e.g., by fixing a variable or using PresenceOf().
constraints.add(ForceAssignment(i, assigned_machine))
// 2. Fix relative sequences on each machine
machines = GetAllMachines()
FOR each machine m IN machines:
// Get the sequence of fixed operations on this machine from s_current
sequence_on_m = []
FOR each operation j scheduled on m IN s_current:
IF j IN O_fixed:
sequence_on_m.add(j)
// Sort them by their start times in s_current
sequence_on_m.sort(by=s_current.get_start_time)
// Add precedence constraints for adjacent operations in the sequence
FOR k from 0 to len(sequence_on_m) - 2:
op1 = sequence_on_m[k]
op2 = sequence_on_m[k+1]
// Add a constraint like EndBeforeStart(op1, op2)
constraints.add(EnforcePrecedence(op1, op2))
RETURN constraints
Your CPSolve function will then take these constraints and add them to the base model before solving.
Test your understanding!
You are implementing a machine-based destruction operator for an FJSP. Your current solution has 15 operations in total. Operations {O1, O5, O9} are scheduled on machine M1. You decide to "ruin" the schedule on M1, so = {O1, O5, O9}.
Your CP model uses interval variables op_i_k representing operation i on machine k, and NoOverlap constraints on sequences of these intervals for each machine. What specific fixing constraints do you need to generate for the remaining 12 operations in ?
Show answer
For the 12 operations in , you need to generate two types of constraints:
-
Machine Assignment Constraints: For each operation , find the machine it was assigned to in the current solution. Add a constraint that fixes its assignment to that machine. In a CP Optimizer context, this would be
PresenceOf(op_j_mj) = 1. -
Sequence-Preserving Constraints: For each machine other than M1, identify the sequence of operations from that are scheduled on it. For example, if M2 had the sequence (O2, O7, O4), you would add the constraints
EndBeforeStart(op_2_M2, op_7_M2)andEndBeforeStart(op_7_M2, op_4_M2). This preserves the relative ordering of the fixed operations, effectively creating a "frozen" schedule into which the CP solver must fit the freed operations {O1, O5, O9}.
Advanced Considerations
While the framework above is a complete and powerful starting point, the research papers we've seen point to more advanced techniques you can integrate as you develop your solver:
- Adaptive Operator Selection: Instead of choosing a destruction operator randomly, you can implement a roulette-wheel selection mechanism that gives more weight to operators that have recently produced better solutions (as described in Kasapidis et al., Section 4.3).
- Memory-based Operators: As seen in the "Machine Assignment Operators" from Kasapidis et al., you can store a pool of "elite" solutions and use the frequency of certain assignments or sequences to guide the destruction process, either by forbidding rare patterns or enforcing common ones.
- Sophisticated Acceptance Criteria: Instead of only accepting better solutions, you can use a criterion from Simulated Annealing that sometimes accepts worse solutions with a certain probability, helping the search escape local optima.
Conclusion
In this lesson, we have laid out a comprehensive, practical blueprint for implementing an LNS-CP hybrid solver for the FJSP.
Key Takeaways:
- The LNS algorithm is an iterative loop of ruining a part of a solution and repairing it with a time-limited CP solver call.
- The "ruin" phase is implemented via destruction operators (e.g., random, time-window, machine-based) that select a subset of operations to free.
- The implementation of these operators involves generating fixing constraints for the unfreed parts of the solution, which typically involves fixing both machine assignments and the relative sequence of operations on each machine.
- This LNS-CP framework provides a robust and high-performance approach that leverages the respective strengths of metaheuristics (broad exploration) and CP (deep, constrained search), making it an excellent choice for your thesis research.
Preview of the Next Module:
You have now completed the modules on modeling and solving complex scheduling problems. You have learned to formulate problems using MILP and CP, model advanced constraints, and design and implement sophisticated hybrid metaheuristics.
The next step is to apply these skills within a rigorous academic framework. Our next module, Research Methodology for Scheduling, will prepare you for this. We will cover how to conduct a thorough literature review, identify research gaps, select appropriate benchmark instances and performance metrics, design and execute computational experiments, and ultimately articulate a clear and defensible contribution for your thesis.