Using LHS Value to Debug An Infeasible Problem#
This notebook aims to show how to use the left-hand side (LHS) value (Constraint.e) to debug an infeasible problem.
The LHS value is the value of the left-hand side of the equation. It is useful to check which constraints are violated.
[1]:
import ams
[2]:
ams.config_logger(stream_level=20)
First, let’s solve a DCOPF.
[3]:
sp = ams.load(ams.get_case('matpower/case14.m'),
no_output=True)
Working directory: "/Users/jinningwang/work/ams/examples/demonstration"
> Loaded config from file "/Users/jinningwang/.ams/ams.rc"
Parsing input file "/Users/jinningwang/work/ams/ams/cases/matpower/case14.m"...
CASE14 Power flow data for IEEE 14 bus test case.
Input file parsed in 0.0033 seconds.
Zero Line parameters detected, adjusted to default values: rate_a, rate_b, rate_c.
System set up in 0.0017 seconds.
[4]:
sp.DCOPF.run(solver='CLARABEL')
Entering data check for <DCOPF>
-> Data check passed
Building system matrices
<DCOPF> formulation: codegen=15/15
Parsing OModel for <DCOPF>
Evaluating OModel for <DCOPF>
Finalizing OModel for <DCOPF>
<DCOPF> initialized in 0.0403 seconds.
<DCOPF> solved as optimal in 0.0070 seconds, converged in 11 iterations with CLARABEL.
[4]:
True
[5]:
sp.DCOPF.pg.v.round(2)
[5]:
array([2.21, 0.38, 0. , 0. , 0. ])
Then, we can lower down the generator maximum output to create an infeasible problem.
[6]:
sp.StaticGen.set(src='pmax', attr='v', idx=[1, 2, 3, 4, 5], value=0.5)
[6]:
True
Remember to update the corresponding parameters in the routine.
[7]:
sp.DCOPF.update('pmax')
[7]:
True
Now, we can see that the problem is infeasible.
[8]:
sp.DCOPF.run(solver='CLARABEL')
DCOPF failed as infeasible in 9 iterations with CLARABEL!
[8]:
False
Then, we can turn off some constraints and sovle it again.
[9]:
sp.DCOPF.disable(['pglb', 'pgub'])
Turn off constraints: pglb, pgub
[9]:
True
Note that since there are some constraints touched, the OModel will be updated automatically.
[10]:
sp.DCOPF.run(solver='CLARABEL')
Entering data check for <DCOPF>
-> Data check passed
Disabled constraints: pglb, pgub
<DCOPF> formulation: codegen=15/15
Finalizing OModel for <DCOPF>
<DCOPF> initialized in 0.0022 seconds.
<DCOPF> solved as optimal in 0.0048 seconds, converged in 9 iterations with CLARABEL.
[10]:
True
[11]:
sp.DCOPF.pg.v
[11]:
array([ 2.31448356, 0.39836206, -0.04094854, -0.04094854, -0.04094854])
Why Constraint.e is always slack-from-zero#
Every routine constraint is authored in the LHS-zero form — all terms on the left, <= 0 / == 0 / >= 0 on the right (post-v1.2.3; see the is_eq retirement migration). CVXPY then canonicalizes every inequality internally to lhs - rhs <= 0 regardless of operator direction:
import cvxpy as cp
a = cp.Variable(3, name='a'); b = cp.Variable(3, name='b')
(a <= b)._expr # → a - b
(b >= a)._expr # → a - b (same! __ge__ swaps operands)
Constraint.e (and Constraint.v) returns this canonical lhs - rhs value — i.e. the slack from zero. So:
negative = constraint is respected, with that much margin
positive = constraint is violated by that amount
zero = constraint is exactly tight
The diagnostic is uniform across every constraint in every routine. For an infeasible problem, scanning each constraint’s .e for positive entries pinpoints exactly which constraints fail and by how much. CVXPY rejects strict < / > (raises NotImplementedError), so only <=, >=, and == reach the solver — the canonicalization is total.
[12]:
sp.DCOPF.pgub.e
[12]:
array([ 1.81448356, -0.10163794, -0.54094854, -0.54094854, -0.54094854])
[13]:
sp.DCOPF.pglb.e
[13]:
array([-2.31448356, -0.39836206, 0.04094854, 0.04094854, 0.04094854])