Skip to main content

Solving Basics

This page covers the fundamentals of solving constraint programming models: calling the solver, interpreting results, and understanding what the solution represents.

Basic Solve

The simplest way to solve a model is to call solve() without parameters:

import optalcp as cp

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

The solve() function returns a SolveResult object containing the solution (if found), proof status, statistics, and search history.

Signature

def solve(
params: Parameters | None = None,
warm_start: Solution | None = None
) -> SolveResult
  • params: Optional solver parameters (time limit, worker count, etc.).
  • warmStart: Optional initial solution. The solver verifies it and uses it as a starting point for the search.

Solve with Parameters

Control solver behavior with parameters:

params: cp.Parameters = {
'timeLimit': 60, # 60 seconds
'nbWorkers': 4, # Use 4 parallel workers
'logLevel': 2, # Verbose logging
}

result = model.solve(params)

Warm Start

Provide an initial solution to guide the search:

# Build an 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
# ... set more values ...

result = model.solve(params, warm_start)

See External Solutions for more on building and injecting solutions.

SolveResult Structure

The SolveResult object contains:

result.solution              # Solution | None - best solution found
result.proof # bool - optimality/infeasibility proved?
result.objective # int | None - objective value
result.objective_bound # int | None - proved bound
result.objective_sense # str | None - 'minimize', 'maximize', None

# Statistics
result.nb_solutions # int - number of solutions found
result.duration # float - solve time in seconds
result.nb_branches # int - search tree branches
result.nb_fails # int - failed search nodes
result.nb_lns_steps # int - LNS iterations
result.nb_restarts # int - search restarts
result.memory_used # int - bytes
result.actual_workers # int - workers used

# Model info
result.nb_int_vars # int - number of IntVars
result.nb_interval_vars # int - number of IntervalVars
result.nb_constraints # int - number of constraints

# History (see Solutions page)
result.objective_history # Sequence[ObjectiveEntry]
result.objective_bound_history # Sequence[ObjectiveBoundEntry]
result.solution_time # float | None - when best solution found
result.bound_time # float | None - when best bound proved
result.solution_valid # bool | None - verification result

# Solver info
result.solver # str - solver version
result.cpu # str - CPU model

Interpreting Results

Interpret results using the decision tree below:

result = model.solve()

if result.solution is not None:
# A solution was found
if result.proof:
print("Optimal solution found!")
# Or: optimal within gap tolerance
else:
print("Feasible solution found (not proven optimal)")
# Stopped due to time/solution limit, Solver.stop(), or Ctrl-C
else:
# No solution found
if result.proof:
print("Problem is infeasible - no solution exists")
else:
print("No solution found yet")
# Stopped due to time/solution limit, Solver.stop(), or Ctrl-C

Result Interpretation Summary

result.solutionresult.proofMeaning
Not NoneTrueOptimal - best possible solution (or within gap tolerance)
Not NoneFalseFeasible - valid solution, may not be optimal
NoneTrueInfeasible - no solution exists
NoneFalseUnknown - no solution found yet

Optimality Gaps

For optimization problems, you can specify when to stop based on gap tolerances. Default values are absoluteGapTolerance=0 and relativeGapTolerance=0.0001 (0.01%).

params: cp.Parameters = {
# Stop when within 5% of optimal
'relativeGapTolerance': 0.05,

# Or: stop when within 100 units
'absoluteGapTolerance': 100,
}

result = model.solve(params)

if result.proof:
# Optimal within specified tolerance
gap = abs(result.objective - result.objective_bound)
print(f"Solution within gap: {gap}")

Satisfaction Problems

For satisfaction problems (no objective), the solver automatically stops after finding the first feasible solution:

# No objective - just want a feasible schedule
model = cp.Model()
# ... add constraints ...

# Solver automatically stops after first solution
result = model.solve()

if result.solution is not None:
print("Found a valid schedule")

To find multiple solutions, set solutionLimit explicitly:

# Find up to 5 different solutions
result = model.solve({'solutionLimit': 5})
print(f"Found {result.nb_solutions} solutions")

Controlling Log Output

Two parameters control solver output: logLevel and printLog.

Controlling Verbosity

The logLevel parameter (0-3) controls how much log output the solver generates. Set logLevel=0 to suppress log messages entirely (warnings and errors are still generated).

Similarly, warningLevel (0-3) controls warning verbosity. Set warningLevel=0 to suppress warnings.

# Suppress log output entirely (recommended for production)
result = model.solve({'logLevel': 0})

# Suppress both logs and warnings
result = model.solve({'logLevel': 0, 'warningLevel': 0})

Redirecting Log Output

The printLog parameter controls where log messages, warnings, and errors are written:

ValueNode.jsBrowserPython
DefaultConsole (stdout)SilentConsole (stdout)
false/FalseSilentSilentSilent
true/TrueConsoleConsoleConsole
Stream/FileCustom streamN/ACustom stream
# Redirect to file
with open('solver.log', 'w') as f:
result = model.solve({'printLog': f})

logLevel vs printLog

Choose based on your needs:

  • logLevel=0: The solver doesn't generate log messages at all. Warnings and errors are still generated. Use this for production or benchmarking.

  • printLog=false: The solver still generates log, warning, and error messages, but they are not written anywhere by default. Use this when you want to capture messages via callbacks (see Async Solving) without console output.

Prefer logLevel for Silencing

To suppress output, prefer logLevel=0 over printLog=false. It's more efficient since the solver skips generating log messages entirely.

Complete Example

import optalcp as cp

model = cp.Model()

# Create three tasks
task_a = model.interval_var(length=20, name="A")
task_b = model.interval_var(length=15, name="B")
task_c = model.interval_var(length=25, name="C")

# Add constraints
task_a.end_before_start(task_b)
task_b.end_before_start(task_c)

# Minimize makespan
model.minimize(task_c.end())

# Solve with time limit
params: cp.Parameters = {'timeLimit': 30, 'logLevel': 2}
result = model.solve(params)

# Interpret results
if result.solution is not None:
print(f"Makespan: {result.objective}")
print(f"Solutions found: {result.nb_solutions}")
print(f"Solve time: {result.duration:.2f}s")

if result.proof:
print("Proven optimal!")
else:
print(f"Gap: {result.objective - result.objective_bound}")
print("May improve with more time")

# Access solution values
print(f"Task A: {result.solution.get_start(task_a)}-{result.solution.get_end(task_a)}")
print(f"Task B: {result.solution.get_start(task_b)}-{result.solution.get_end(task_b)}")
print(f"Task C: {result.solution.get_start(task_c)}-{result.solution.get_end(task_c)}")
else:
if result.proof:
print("Problem is infeasible")
else:
print("No solution found in time limit")
Edition Note

In Preview edition, solution values (start/end times, variable values) are masked and reported as absent. The objective value is correct. Contact us for Academic or Full edition for complete solution data.

See Also

  • Solutions - Accessing solution values and history