Results and diagnostics¶
Bilevel results¶
blvpy.BilevelProblem.solve() returns a
blvpy.BilevelResult.
Use result.succeeded as the main success check. Important fields include:
status,objective, andmessage;immutable
variable_valuessnapshots keyed by the original CVXPY variables;canonical primal, slack, and dual snapshots;
residuals,complementarity, andfinal_epsilonconveniences;accepted and attempted epsilon histories;
all
blvpy.RunRecordobjects andselected_runfor a best-of search.
Every upper objective stored in an IterationRecord,
RunRecord, or BilevelResult is evaluated in the
modeled sense. Thus a cp.Maximize value is not negated in the records,
all_objectives, or progress output. The original lower objective object is
likewise available unchanged as blvpy.LowerProblem.objective.
The following statuses can be produced by blvpy.BilevelProblem.solve().
The scope column distinguishes the top-level result from a best-of run and
an individual epsilon attempt.
Status |
Scope |
Meaning |
Successful? |
|---|---|---|---|
|
result, run, attempt |
The returned point passed the solver and BLVPY’s independent residual checks. |
Yes |
|
attempt |
CVXPY reported a numerically less accurate solution that still passed BLVPY’s residual checks. BLVPY may accept the attempt, but it recomputes the selected run’s final diagnostics before returning the result. |
Yes |
|
result, run |
The run initialized, but continuation did not reach the requested target epsilon. The top-level result contains the best partial run when no run reached the target. |
No |
|
run |
This run failed before BLVPY accepted its initial-epsilon point. Other best-of runs may still continue. |
No |
|
run, attempt |
A solver returned a point, but BLVPY’s independently recomputed feasibility or relaxed-gap residuals exceeded the tolerance. |
No |
|
run, final attempt |
A target-epsilon point was available, but its upper objective was missing or nonfinite. |
No |
|
run, attempt |
The selected nonlinear backend raised an ordinary solve error or failed to provide a status. |
No |
|
run, attempt |
CVXPY reports that the solver stopped at a user limit, such as an iteration or time limit. |
No |
|
run, attempt |
CVXPY reports the lifted problem as infeasible. |
No |
|
run, attempt |
CVXPY reports likely infeasibility, but with reduced numerical confidence. |
No |
|
run, attempt |
CVXPY reports the lifted problem as unbounded. |
No |
|
run, attempt |
CVXPY reports likely unboundedness, but with reduced numerical confidence. |
No |
|
run, attempt |
The solver could not distinguish infeasibility from unboundedness. |
No |
Use result.succeeded rather than matching status strings in application code.
For continuation_failed, inspect result.message, result.selected_run, and
the per-run histories; its top-level snapshots describe the best available
partial point, not a successful target-epsilon solution.
Not every failure produces a result. Invalid models or settings, an unavailable
backend, and failure of every run before an initial point is accepted raise a
BLVPY exception instead. In particular, initialization_failed records are
available only when another run initialized far enough for BLVPY to return a
blvpy.BilevelResult.
Residuals¶
At a returned upper point \(x\), BLVPY writes the canonical lower problem as
where \(u\) is the canonical lower primal vector, \(s\) is the conic slack
variable, and \(\mathcal{K}\) is the product of cones.
For a lower cp.Minimize(f), the canonical objective represents \(f\). For a
lower cp.Maximize(f), it represents \(-f\).
The numerical arrays
\(A(x)\), \(b(x)\), \(c(x)\), and \(d(x)\) are evaluated at the returned upper point.
Let \(\lambda\) be the equality dual vector
that should belong to the dual cone \(\mathcal{K}^*\).
These objects are not additional inputs that the user must provide. CVXPY’s
canonicalization produces \(A\), \(b\), \(c\), \(d\), and \(\mathcal K\) from the
modeled lower objective and constraints, and BLVPY evaluates their dependence
on \(x\). In the returned result, canonical_primal stores \(u\), slack stores
\(s\), and dual stores \(\lambda\). BLVPY recomputes the residuals from these
snapshots after each nonlinear attempt; no additional lower-problem solve is
required.
blvpy.Residuals reports how closely the returned numerical point
satisfies this canonical system and the original bilevel model:
Field |
Mathematical quantity |
Interpretation |
|---|---|---|
|
\(\|A(x)u+s-b(x)\|_2\) |
Violation of the canonical lower equality. It is zero when the primal vector and slack satisfy the conic equations. |
|
\(\|A(x)^T\lambda+c(x)\|_2\) |
Violation of lower-level stationarity with respect to \(u\). |
|
\(\max_i\|y_i-(R_i u+r_i)\|_2\) |
Checks that every returned source lower variable \(y_i\) agrees with the affine value recovered from the canonical vector. Canonicalization produces the matrices \(R_i\) and offsets \(r_i\). |
|
\(\max_j\operatorname{violation}(g_j(x,y))\) |
Largest CVXPY violation norm among the upper constraints and generated linked-variable domain constraints. |
|
\(\operatorname{dist}(s,\mathcal{K})\) |
Numerical distance of the slack from the primal product cone. |
|
\(\operatorname{dist}(\lambda,\mathcal{K}^*)\) |
Numerical distance of the dual vector from the dual product cone. |
|
\(s^T\lambda\) |
Signed primal-dual cone pairing. Exact lower optimality requires zero complementarity; a numerically infeasible point can make it slightly negative. |
|
\(\max(s^T\lambda-\epsilon,0)\) |
Amount by which complementarity exceeds the continuation relaxation \(s^T\lambda\leq\epsilon\). It is zero whenever that relaxed inequality is satisfied. |
Cone distance diagnostics¶
Algebraic residuals and zero-, nonnegative-, and second-order-cone distances are evaluated directly. For exponential and 3D power cones, the reported distances are numerical estimates. BLVPY reports zero only after an exact membership check and retries uncertain solver results when possible.
If no usable positive estimate is available, BLVPY reports the distance to the cone’s zero element. This conservative upper bound can make a residual check fail, but it cannot cause a known nonmember to be reported as zero.
Cone distances are evaluated wherever residuals are recomputed, including initialization, restoration, continuation attempts, final selection, and polishing.
All six feasibility residuals ideally equal zero. For a residual record \(r\), BLVPY defines the aggregate
and the relaxed-gap violation
The convenience property max_feasibility returns \(F\), while max_violation
returns \(\max(F,G)\). Thus max_feasibility answers “how far is this point from
the lifted feasibility and stationarity conditions?”, whereas max_violation
also asks whether it satisfies the current epsilon-gap relaxation.
The feasibility_tolerance passed to blvpy.BilevelProblem.solve()
controls whether BLVPY accepts an attempt. During solving, the same tolerance
is used for \(F\) and \(G\). A user can later apply different thresholds without
resolving the problem:
residuals = result.residuals
print(residuals.max_feasibility) # F
print(residuals.gap_violation) # G
print(residuals.max_violation) # max(F, G)
acceptable = residuals.is_feasible(
tolerance=1e-7,
gap_tolerance=1e-6,
)
This check concerns the returned lifted lower-optimality conditions.
Polished candidates¶
blvpy.BilevelProblem.polish() re-solves the lower problem while holding
the result’s upper values fixed and returns an immutable
blvpy.PolishResult containing the complete candidate, its upper
objective, and its relative improvement over the original objective. Its
feasible property is derived from the retained residuals and originating
feasibility tolerance, so a failed check can be inspected without another
solve. Those residuals diagnose only the polished candidate; the originating
blvpy.BilevelResult residuals and
blvpy.BilevelProblem.gap_diagnostics() describe the source point, while
Polishing covers the workflow and ratio definition.
Complete gap diagnostics¶
Call the convenience method only when the extra fixed-upper lower solve is useful:
diagnostics = problem.gap_diagnostics(result)
print(diagnostics.source_gap)
print(diagnostics.identity_error)
The method reconstructs canonical data at the result’s upper point and checks the inexact identity
where \(r_p=Au+s-b\) and \(r_d=A^T\lambda+c\). It then performs one additional fixed-upper conic solve and reports the lower-level source-objective suboptimality as the source gap
Both definitions measure suboptimality and are nonnegative in exact
arithmetic. Small negative values can occur from numerical tolerances and are
not clamped. The other objective terms in blvpy.GapDiagnostics use
the normalized canonical minimization convention.
The diagnostic solve defaults to silent Clarabel, but it is configurable:
diagnostics = problem.gap_diagnostics(
result,
solver=cp.SCS,
solver_options={"eps": 1e-7},
solver_verbose=False,
)
Diagnosis accepts successful results and complete continuation_failed
results. It snapshots and restores affected model state.