Skip to content

highs iteration count always takes simplex iteration count #3753

@AlexGisi

Description

@AlexGisi

Summary

In contrib.solver.solvers.highs.py, the _postsolve method records the simplex iteration count:

results.iteration_count = info.simplex_iteration_count

However, the highs solver might use the simplex, ipm, pdlp, mip, or qp solvers depending on the problem. Each method has its own iteration count. The unused methods are populated with either -1 or 0 (I can't find rhyme or reason to which and when). Also, if the problem is infeasible, the entire highs info might be invalid.

Steps to reproduce the issue

The latter case can be demonstrated with

from pyomo.environ import (
    ConcreteModel, Var, Reals, Objective, SolverFactory,
)


def main():
    m = ConcreteModel()
    m.x1 = Var(within=Reals, bounds=(0, 2), initialize=1.745)
    m.x4 = Var(within=Reals, bounds=(0, 5), initialize=3.048)
    m.x7 = Var(within=Reals, bounds=(0.9, 0.95), initialize=0.928)
    m.obj = Objective(expr=-6.3 * m.x4 * m.x7 + 5.04 * m.x1)

    opt = SolverFactory("highs")
    opt.solve(m, tee=False)


if __name__ == "__main__":
    main()

In general, we can check the iteration values for all types of problems:

from pyomo.environ import (
    ConcreteModel, Var, Objective, Constraint,
    NonNegativeReals, Binary, Reals, minimize, maximize,
    SolverFactory, value
)


def solve_lp(method: str):
    """Tiny LP, solved with a specific HiGHS LP method (simplex/ipm/pdlp)."""
    m = ConcreteModel()
    m.x = Var(domain=NonNegativeReals)
    m.y = Var(domain=NonNegativeReals)
    # min x + y  s.t.  x + y >= 1
    m.c = Constraint(expr=m.x + m.y >= 1)
    m.obj = Objective(expr=m.x + m.y, sense=minimize)

    opt = SolverFactory("highs")
    opt.options["solver"] = method  # 'simplex' | 'ipm' | 'pdlp'
    res = opt.solve(m, tee=False)
    print(f"[LP/{method}] status={res.solver.status}, term={res.solver.termination_condition}, obj={value(m.obj):.6f}")
    print(f"  x={value(m.x):.6f}, y={value(m.y):.6f}")


def solve_mip():
    """Tiny MIP (binary knapsack)."""
    m = ConcreteModel()
    m.a = Var(domain=Binary)
    m.b = Var(domain=Binary)
    # max 3a + 2b  s.t.  a + b <= 1
    m.cap = Constraint(expr=m.a + m.b <= 1)
    m.obj = Objective(expr=3*m.a + 2*m.b, sense=maximize)

    opt = SolverFactory("highs")  # let HiGHS choose the MIP machinery
    res = opt.solve(m, tee=False)
    print(f"[MIP] status={res.solver.status}, term={res.solver.termination_condition}, obj={value(m.obj):.6f}")
    print(f"  a={int(round(value(m.a)))}, b={int(round(value(m.b)))}")


def solve_qp():
    """Tiny convex QP."""
    m = ConcreteModel()
    m.x = Var(domain=Reals, bounds=(0, None))
    m.y = Var(domain=Reals, bounds=(0, None))
    # min (x-1)^2 + (y-2)^2  s.t.  x + y >= 1
    m.c = Constraint(expr=m.x + m.y >= 1)
    m.obj = Objective(expr=(m.x - 1)**2 + (m.y - 2)**2, sense=minimize)

    opt = SolverFactory("highs")  # HiGHS handles convex QP
    res = opt.solve(m, tee=False)
    print(f"[QP] status={res.solver.status}, term={res.solver.termination_condition}, obj={value(m.obj):.6f}")
    print(f"  x={value(m.x):.6f}, y={value(m.y):.6f}")


def main():
    print("=== LP methods ===")
    for method in ("simplex", "ipm", "pdlp"):
        solve_lp(method)

    print("\n=== MIP ===")
    solve_mip()

    print("\n=== QP (convex) ===")
    solve_qp()


if __name__ == "__main__":
    main()

Error Message

For the former example, we get

ValueError: invalid value for configuration 'iteration_count':
        Failed casting -1
        to NonNegativeInt
        Error: Expected non-negative int, but received -1

because info.simplex_iteration_count is -1.

Information on your system

Pyomo version: 6.9.4
Python version: 3.12.3
Operating system: windows 11
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable): highs

Additional information

I suggest the following fix to replace line 753 of contrib/solver/solvers/highs.py:

if info.valid:
      # The method that ran will have a non-negative iteration count
      # and the others will be 0 or -1.
      max_iters = max(
          info.simplex_iteration_count,
          info.ipm_iteration_count,
          info.mip_node_count,
          info.pdlp_iteration_count,
          info.qp_iteration_count,
      )
      results.iteration_count = max_iters if max_iters != -1 else 0
  else:
      results.iteration_count = 0

If this is appropriate I can make a PR.

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