Skip to content

Handle bounds in solve_strongly_connected_components #3573

Open
@Robbybp

Description

@Robbybp

Summary

The algorithm in solve_strongly_connected_components is not robust to the following situation:

  1. A subsystem has multiple solutions. We converge to one of them.
  2. For this particular local solution (but not some other local solution), a downstream subsystem is infeasible.

We would like the first subsystem to converge to the "right" local solution.

Rationale

Inspired by some problems @dallan-keylogic is having. See #3571.

Description

I propose we do this with FBBT. Here's a sketch of what this looks like in solve_strongly_connected_components:

    scc_input_list = list(
        generate_strongly_connected_components(constraints, variables, igraph=igraph)
    )

    if propagate_bounds:
        orig_bounds = [(var.lb, var.ub) for var in variables]
        for scc, inputs in scc_input_list:
            # Propagate bounds forward. This computes bounds on decision variables
            # based on fixed "degree of freedom" values. This may help us propagate
            # bound backwards in the reverse pass.
            fbbt(scc)
        for scc, inputs in reversed(scc_input_list):
            # Propagate bounds in the "reverse direction". This attempts to strengthen
            # "upstream" bounds based on existing bounds on decision variables.
            fbbt(scc)

    for scc, inputs in scc_input_list:
        # Solve scc...

    if propagate_bounds:
        # Reset bounds to their original values
        for var, (lb, ub) in zip(variables, orig_bounds):
            var.setlb(lb)
            var.setub(ub)

I've implemented this in my scc-fbbt branch. @dallan-keylogic, can you check if this branch helps with your infeasibility issue? Here's an example:

import pyomo.environ as pyo
from pyomo.contrib.incidence_analysis import solve_strongly_connected_components

m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2, 3, 4], initialize=1.0)
m.eq = pyo.Constraint(pyo.PositiveIntegers)
m.eq[1] = m.x[1]**2 == 1.0
m.x[1] = -2.0
m.eq[2] = m.x[2]**2 == 4.0
m.x[2].setlb(1.0)
m.eq[3] = m.x[2] == m.x[3]
m.eq[4] = m.x[1] * m.x[3] * m.x[4] == 1.0
m.x[4].setlb(0.0)
solver = pyo.SolverFactory("ipopt")
solve_strongly_connected_components(m, solver=solver, use_calc_var=False, propagate_bounds=True)
m.x.pprint()

With propagate_bounds=False, this prints:

WARNING: Loading a SolverResults object with a warning status into
model.name="ScalarBlock";
    - termination condition: infeasible
    - message from solver: Ipopt 3.14.11: Converged to a locally infeasible
      point. Problem may be infeasible.
x : Size=4, Index={1, 2, 3, 4}
    Key : Lower : Value                  : Upper : Fixed : Stale : Domain
      1 :  None :     -1.000000000000001 :  None : False :  True :  Reals
      2 :   1.0 :     2.0000000000000013 :  None : False :  True :  Reals
      3 :  None :     2.0000000000000013 :  None : False :  True :  Reals
      4 :   0.0 : -9.958130312559555e-09 :  None : False : False :  Reals

With propagate_bounds=True:

x : Size=4, Index={1, 2, 3, 4}
    Key : Lower : Value : Upper : Fixed : Stale : Domain
      1 :  None :   1.0 :  None : False :  True :  Reals
      2 :   1.0 :   2.0 :  None : False :  True :  Reals
      3 :  None :   2.0 :  None : False :  True :  Reals
      4 :   0.0 :   0.5 :  None : False : False :  Reals

I'm not sure how robust this is in general — maybe fbbt(scc) is not guaranteed to converge? You could probably find a case where this fails to restrict an infeasible solution, but it seems like it should work for simple systems and small SCCs.

Note that this only makes sense if the "subsolver" can solve square problems with bounds, which rules out calculate_variable_from_constraint. Luckily we can turn this off with use_calc_var=False. There's also the question of what to do about inequalities. I think we can just include them when they contain any variable in a particular subsystem. We could also punt on these until they come up.

Another approach would be to use a global solver, enumerate all solutions of each feasibility problem, and build a tree of possible solutions. This is probably out-of-scope.

Additional info

A larger issue here is that solve_strongly_connected_components isn't really a solver, it's just a utility function. It does very little to try to be consistent when different subsolvers are used and doesn't return a results object. If we ever make it a real solver, we should be explicit and consistent about whether bounds and inequalities are considered.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions