Skip to main content

External Solutions

OptalCP allows you to inject external solutions into the solver, either as a warm start before solving or dynamically during search. This enables integration with custom heuristics, previous solutions, or external optimization tools.

Building Solutions

Before providing a solution to the solver, you need to build it. The constructor creates a solution with all variables absent—you then set values for the variables you want to be present:

solution = cp.Solution()

# Set interval start and end (both required)
solution.set_value(task1, 0, 10) # start=0, end=10
solution.set_value(task2, 30, 45) # start=30, end=45

# Set integer variable
solution.set_value(int_var, 42)

# Set boolean variable
solution.set_value(bool_var, True)

# Mark variable as absent
solution.set_absent(optional_task)

# Set objective value
solution.set_objective(150)
All Variables Required

All variables must be set, including the objective value. Partial solutions are not supported.

The solver automatically validates all external solutions. It checks constraint satisfaction and rejects invalid solutions with a warning.

Warm Start

Provide an initial solution when calling solve():

import optalcp as cp

model = cp.Model()
# ... build model ...

# Build initial solution
warm_start = cp.Solution()
warm_start.set_value(task1, 0, 20) # start=0, end=20
warm_start.set_value(task2, 30, 50) # start=30, end=50
warm_start.set_value(task3, 60, 85) # start=60, end=85

# Solve with warm start
result = model.solve(params, warm_start)

The solver uses the warm start to:

  1. Verify feasibility
  2. Establish an initial bound
  3. Guide search toward similar solutions

Dynamic Solution Injection

Inject solutions during search using the Solver class:

import asyncio
import optalcp as cp

solver = cp.Solver()

# Start solving in background
solve_task = asyncio.create_task(solver.solve(model, params))

# Wait a bit for search to start
await asyncio.sleep(1)

# Build and inject a solution
solution = cp.Solution()
solution.set_value(task1, 5, 25) # start=5, end=25
solution.set_value(task2, 35, 55) # start=35, end=55
solution.set_value(task3, 70, 95) # start=70, end=95

await solver.send_solution(solution)

# Wait for final result
result = await solve_task

The solver validates and integrates injected solutions during search.

For a complete working example, see demo-external-solutions.

Use Cases

1. Reusing Previous Solutions

Save solutions from previous runs and reuse them:

# First solve
result1 = model.solve()

if result1.solution is not None:
# Save for later
previous_solution = result1.solution

# Later: similar model
model2 = cp.Model()
# ... build similar model ...

# Reuse previous solution as warm start
result2 = model2.solve(params, previous_solution)

2. Custom Heuristics

Integrate domain-specific heuristics:

import optalcp as cp

def greedy_heuristic(model, tasks):
"""Simple greedy schedule: tasks in order, earliest start."""
solution = cp.Solution()
current_time = 0

for task in tasks:
length = task.length_max
solution.set_value(task, current_time, current_time + length)
current_time += length

return solution

# Build model
model = cp.Model()
tasks = [model.interval_var(length=l, name=f"t{i}") for i, l in enumerate([10, 20, 15])]
# ... add constraints ...

# Generate heuristic solution
heuristic_solution = greedy_heuristic(model, tasks)

# Use as warm start
result = model.solve(params, heuristic_solution)
Event Loop

When running heuristics concurrently with the solver, do not block the event loop—this would block communication with the solver process. For CPU-intensive heuristics, use subprocesses or threads.

Run multiple heuristics in parallel and inject solutions:

import asyncio
import optalcp as cp

async def run_heuristic(solver, model, heuristic_fn):
"""Run heuristic and inject solution."""
await asyncio.sleep(0.5) # Simulate heuristic computation
solution = heuristic_fn(model)
await solver.send_solution(solution)

async def solve_with_heuristics(model, params, heuristics):
solver = cp.Solver()

# Start solver
solve_task = asyncio.create_task(solver.solve(model, params))

# Run heuristics in parallel
heuristic_tasks = [
asyncio.create_task(run_heuristic(solver, model, h))
for h in heuristics
]

# Wait for all heuristics
await asyncio.gather(*heuristic_tasks)

# Wait for solve to complete
return await solve_task

# Define heuristics
heuristics = [greedy_heuristic, random_heuristic, priority_heuristic]

# Solve with parallel heuristics
result = await solve_with_heuristics(model, params, heuristics)

4. Iterative Improvement

Solve in stages with progressively tighter constraints:

import optalcp as cp

# Stage 1: Quick solve for feasibility
model1 = cp.Model()
# ... build with relaxed constraints ...

params: cp.Parameters = {'timeLimit': 10, 'solutionLimit': 1}
result1 = model1.solve(params)

if result1.solution is not None:
# Stage 2: Add more constraints, use previous solution as warm start
model2 = cp.Model()
# ... build with tighter constraints ...

params = {'timeLimit': 60}
result2 = model2.solve(params, result1.solution)

print(f"Stage 1 objective: {result1.objective}")
print(f"Stage 2 objective: {result2.objective}")

Complete Example

import asyncio
import optalcp as cp

# Build model
model = cp.Model()
tasks = [model.interval_var(length=l, name=f"task{i}") for i, l in enumerate([20, 30, 25])]

# Add precedence constraints
tasks[0].end_before_start(tasks[1])
tasks[1].end_before_start(tasks[2])

# Minimize makespan
model.minimize(tasks[2].end())

# Create heuristic solution (tasks have lengths 20, 30, 25)
heuristic = cp.Solution()
heuristic.set_value(tasks[0], 0, 20) # length=20
heuristic.set_value(tasks[1], 20, 50) # length=30
heuristic.set_value(tasks[2], 50, 75) # length=25

# Solve with warm start
print("Solving with warm start...")
params: cp.Parameters = {'timeLimit': 30}
result = model.solve(params, heuristic)

print(f"With warm start: {result.objective}, time: {result.duration:.2f}s")

# Compare: solve without warm start
print("\nSolving without warm start...")
result_no_warm = model.solve(params)

print(f"Without warm start: {result_no_warm.objective}, time: {result_no_warm.duration:.2f}s")
print(f"Speedup: {result_no_warm.duration / result.duration:.2f}x")

See Also