Quick startΒΆ

The target bilevel problem is

\[\begin{split} \begin{array}{ll} \text{minimize} & (x-1)^2+(y+1)^2 \\ \text{subject to} & x\geq -1, \\ & y\in\mathop{\mathrm{argmin}}_z (z-x)^2 \end{array} \end{split}\]

with variables \(x, y \in \mathbf{R}\). For every fixed \(x\), the lower problem has the unique response \(y=x\). Substitution gives the reduced upper objective \(2x^2+2\), so the exact bilevel solution is \((x,y)=(0,0)\) with objective value \(2\). The CVXPY variable y below is shared by the lower model and the upper objective.

import cvxpy as cp
import blvpy as bp

# x is selected by the upper problem; y is selected by the lower problem.
x = cp.Variable(name="x")
y = cp.Variable(name="y")

# Listing x in parameters makes it fixed data whenever the lower problem is
# checked or solved. The original y object remains shared with the upper model.
lower = bp.LowerProblem(
    cp.Minimize(cp.square(y - x)),
    parameters=[x],
)

problem = bp.BilevelProblem(
    cp.Minimize(cp.square(x - 1.0) + cp.square(y + 1.0)),
    lower,
    upper_constraints=[x >= -1.0],
)

# validate() raises a detailed exception if BLVPY cannot reformulate the model.
problem.validate()
assert problem.is_dblp()

# The default solve follows one deterministic epsilon-continuation path.
result = problem.solve()
if not result.succeeded:
    raise RuntimeError(result.message)

print(result.status)
print("x =", result.variable_values[x])
print("y =", result.variable_values[y])
print("maximum violation =", result.residuals.max_violation)

# This optional call performs one additional fixed-upper lower solve.
diagnostics = problem.gap_diagnostics(result)
print("source gap =", diagnostics.source_gap)

To obtain a fresh lower response at the returned upper point, call polished = problem.polish(result). The compact result reports feasibility, the polished upper objective, and its relative improvement over the original point without changing the model. See Polishing for interpretation and explicit adoption.