Warm-Starting with Initial Solutions#
When you already have a good feasible solution (from a previous solve, a heuristic, domain knowledge, or a related problem), you can pass it to m.solve() as an initial_solution. This provides two benefits in the Branch and Bound algorithm [Belotti et al., 2013, Land and Doig, 1960]:
The solution is injected as the initial incumbent, giving the solver an upper bound from the start. This allows it to prune suboptimal branches immediately rather than exploring them first.
For continuous (NLP) subproblems, the solution is used as the starting point for the nonlinear solver [Nocedal and Wright, 2006], which can improve convergence.
This is analogous to “MIP-start” functionality in commercial solvers [Koch et al., 2011].
import os
os.environ["JAX_PLATFORMS"] = "cpu"
os.environ["JAX_ENABLE_X64"] = "1"
import discopt.modeling as dm
Basic Usage#
The initial_solution parameter accepts a dictionary mapping Variable objects to their values. Scalars, lists, and numpy arrays are all supported.
# A simple MINLP: minimize (x - 2.5)^2 + (y - 1)^2 with y binary
m = dm.Model("warm_start_demo")
x = m.continuous("x", lb=0, ub=5)
y = m.binary("y")
m.minimize((x - 2.5) ** 2 + (y - 1) ** 2)
m.subject_to(x <= 4 * y + 1, name="linking")
# Solve with a known good initial solution
result = m.solve(initial_solution={x: 2.5, y: 1.0})
print(f"Status: {result.status}")
print(f"Objective: {result.objective:.6f}")
print(f"x = {result.x['x']:.4f}")
print(f"y = {result.x['y']:.4f}")
print(f"Nodes: {result.node_count}")
Status: optimal
Objective: 0.000000
x = 2.5000
y = 1.0000
Nodes: 3
Warm-Starting a Continuous NLP#
For purely continuous problems, the initial solution is used as the starting point for the interior-point solver. When the starting point is close to the optimum, this can reduce the number of iterations needed.
# Continuous NLP: minimize (x-3)^2 + (y-4)^2
m = dm.Model("nlp_warm")
x = m.continuous("x", lb=-10, ub=10)
y = m.continuous("y", lb=-10, ub=10)
m.minimize((x - 3) ** 2 + (y - 4) ** 2)
# Cold start (default midpoint)
result_cold = m.solve()
print(f"Cold start: obj={result_cold.objective:.6f}, time={result_cold.wall_time:.4f}s")
# Warm start near the optimum
result_warm = m.solve(initial_solution={x: 2.9, y: 3.9})
print(f"Warm start: obj={result_warm.objective:.6f}, time={result_warm.wall_time:.4f}s")
Cold start: obj=0.000000, time=0.0538s
Warm start: obj=0.000000, time=0.0244s
Partial Solutions#
You do not need to provide values for every variable. Any variables not included in initial_solution are initialized to the midpoint of their bounds.
# Only provide the binary variable; let the solver find x
m = dm.Model("partial")
x = m.continuous("x", lb=0, ub=5)
y = m.binary("y")
m.minimize((x - 2.5) ** 2 + (y - 1) ** 2)
m.subject_to(x <= 4 * y + 1)
result = m.solve(initial_solution={y: 1.0})
print(f"Status: {result.status}, obj={result.objective:.6f}")
print(f"x={result.x['x']:.4f}, y={result.x['y']:.4f}")
Status: optimal, obj=0.000000
x=2.5000, y=1.0000
Validation and Safety#
The initial_solution is validated before solving. discopt checks variable bounds and integrality, producing warnings and automatically correcting violations rather than failing silently.
import warnings
m = dm.Model("validation_demo")
x = m.continuous("x", lb=0, ub=5)
y = m.binary("y")
m.minimize((x - 2.5) ** 2 + (y - 1) ** 2)
m.subject_to(x <= 4 * y + 1)
# Out-of-bounds value: x=10 exceeds ub=5, gets clamped to 5
# Non-integer binary: y=0.7 gets rounded to 1
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = m.solve(initial_solution={x: 10.0, y: 0.7})
for warning in w:
print(f"Warning: {warning.message}")
print(f"\nStatus: {result.status}, obj={result.objective:.6f}")
Warning: Variable 'x': 1 value(s) outside bounds. Clamping to [lb, ub].
Warning: Variable 'y': 1 value(s) are not integer-valued. Rounding to nearest integer.
Status: optimal, obj=0.000000
Programmatic Feasibility Checking#
You can also check feasibility of a candidate solution without solving, using discopt.warm_start.check_feasibility.
from discopt.warm_start import check_feasibility, validate_initial_solution
m = dm.Model("feasibility_check")
x = m.continuous("x", lb=0, ub=5)
y = m.binary("y")
m.minimize((x - 2.5) ** 2 + (y - 1) ** 2)
m.subject_to(x <= 4 * y + 1, name="linking")
# Check a feasible solution
x_flat = validate_initial_solution(m, {x: 2.5, y: 1.0})
is_feasible, violations = check_feasibility(m, x_flat)
print(f"Solution {{x=2.5, y=1}}: feasible={is_feasible}")
# Check an infeasible solution (x=5 but y=0 means x <= 1)
x_flat = validate_initial_solution(m, {x: 5.0, y: 0.0})
is_feasible, violations = check_feasibility(m, x_flat)
print(f"Solution {{x=5, y=0}}: feasible={is_feasible}")
for v in violations:
print(f" - {v}")
Solution {x=2.5, y=1}: feasible=True
Solution {x=5, y=0}: feasible=False
- Constraint 'linking': value 4 > 0 (sense <=)