Hide code cell content
###############################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
###############################################################################

Degeneracy Hunter Examples#

Author: Prof. Alex Dowling (adowling@nd.edu), University of Notre Dame
Maintainer: Andrew Lee
Updated: 2023-06-01

Alexander W. Dowling and Lorenz T. Biegler (2015). Degeneracy Hunter: An Algorithm for Determining Irreducible Sets of Degenerate Constraints in Mathematical Programs. 12th International Symposium on Process Systems Engineering and 25th European Symposium on Computer Aided Process Engineering. Ed. by Krist V. Gernaey, Jakob K. Huusom, and Raqul Gani. Computer-Aided Chemical Engineering, Vol. 37, p. 809 – 814. link to PDF

This notebook shows how to use the Degeneracy Hunter tool which is part of the IDAES Diagnostics Toolbox features using two motivating examples:

  • Inspect constraint violations and bounds of a Pyomo model

  • Compute the Irreducible Degenerate Set (IDS) for a Pyomo model

  • Demonstrates the performance benefits in Ipopt from removing a single redundant constraint

Note: Degeneracy Hunter requires a suitable MILP solver. This example uses the open-source solver SCIP (https://scipopt.org/), however users may specify another solver of their choice if they have one available.

What does Degeneracy Mean?#

In simple terms, a degenerate model contains one or more constraints that do not add any additional information that is not already included in other constraints in the model. The simplest example of this is the case where a constraint in the model is duplicated. For example, consider the model below:

\[\begin{split}\begin{align*} & x_1 + x_2 = 1 \\ & x_1 + x_2 = 1 \\ \end{align*} \end{split}\]

Notice the two equality constraints are identical and thus redundant. This means the constraint qualifications that solvers rely on (e.g., Linear Independence Constraint Qualification or LICQ) do not hold which has three important implications:

  1. The optimal solution may not be mathematically well-defined (e.g., the dual variables are not unique)

  2. The calculations performed by the optimization solver may become numerically poorly scaled

  3. Theoretical convergence properties of optimization algorithms may not hold

Put another way, for a deterministic (square) problem we required that the number of equality constraints be equal to the number of free variables. In this case, there appear to be two equality constraints but in reality there is effectively only one as the two constraints are the same. Thus, the problem is not well defined and there exist a family of solutions that can satisfy the constraints given; any combination of values for x1 and x2 that sum to one will satisfy the constraints as written.

In practice, degeneracies are rarely as straight-forward as a duplicated constraint. A more common occurrence is a set of linearly dependent constraints; that is a system of constraints where one constraint in the set can be formed by a linear combination of other constraints in the set. For example:

\[\begin{split}\begin{align*} & x_1 + x_2 + x_3 = 1 \\ & 2x_1 + 2x_2 + 2x_3 = 2 \\ \end{align*} \end{split}\]

Here, the second constraint can be formed by multiplying the first constraint by two. Another common example of linearly dependent constraints is the combination of a set of material balances for all components in a system and an overall material balance. In this case, the overall material balances can be formed by adding all the individual component balances together. Depending on the number of components being modeled, this system of constraints can be quite large, but the end result is the same; the model effectively has one less constraint than it would appear to have.

The absolute best defense against this is to detect degenerate equations and reformulate the model to remove them; this is the purpose of Degeneracy Hunter. Let’s see it in action.

Setup#

We start by importing Pyomo and the IDAES Diagnostics Toolbox.

import pyomo.environ as pyo

from idaes.core.util.model_diagnostics import DiagnosticsToolbox

Example: Linear Program with Redundant Equality Constraints#

Let’s apply these tools to a poorly formulated optimization problem:

\[\begin{split}\begin{align*} \min_{\mathbf{x}} \quad & \sum_{i=\{1,...,3\}} x_i \\ \mathrm{s.t.}~~& x_1 + x_2 \geq 1 \\ & x_1 + x_2 + x_3 = 1 \\ & x_2 - 2 x_3 \leq 1 \\ & x_1 + x_3 \geq 1 \\ & x_1 + x_2 + x_3 = 1 \\ \end{align*} \end{split}\]

As you can see, this is similar to our example above with duplicated equality constraints.

The cell below shows how to construct this model in Pyomo.

def build_example(include_redundant_constraint=True):
    m = pyo.ConcreteModel()

    m.I = pyo.Set(initialize=[i for i in range(1, 4)])

    m.x = pyo.Var(m.I, bounds=(0, 5), initialize=1.0)

    m.con1 = pyo.Constraint(expr=m.x[1] + m.x[2] >= 1)
    m.con2 = pyo.Constraint(expr=m.x[1] + m.x[2] + m.x[3] == 1)
    m.con3 = pyo.Constraint(expr=m.x[2] - 2 * m.x[3] <= 1)
    m.con4 = pyo.Constraint(expr=m.x[1] + m.x[3] >= 1)
    
    if include_redundant_constraint:
        m.con5 = pyo.Constraint(expr=m.x[1] + m.x[2] + m.x[3] == 1)

    m.obj = pyo.Objective(expr=sum(m.x[i] for i in m.I))
    
    return m

m = build_example()

Check for Structural Issues#

Before we try to solve the model, the first thing we should do is use the Diagnostics Toolbox to check for any structural issues in our model. The cell below creates an instance of the Diagnostics Toolbox and calls the report_structural_issues method.

dt = DiagnosticsToolbox(m)
dt.report_structural_issues()
====================================================================================
Model Statistics

        Activated Blocks: 1 (Deactivated: 0)
        Free Variables in Activated Constraints: 3 (External: 0)
            Free Variables with only lower bounds: 0
            Free Variables with only upper bounds: 0
            Free Variables with upper and lower bounds: 3
        Fixed Variables in Activated Constraints: 0 (External: 0)
        Activated Equality Constraints: 2 (Deactivated: 0)
        Activated Inequality Constraints: 3 (Deactivated: 0)
        Activated Objectives: 1 (Deactivated: 0)

------------------------------------------------------------------------------------
2 WARNINGS

    WARNING: 1 Degree of Freedom
    WARNING: Structural singularity found
        Under-Constrained Set: 3 variables, 2 constraints
        Over-Constrained Set: 0 variables, 0 constraints

------------------------------------------------------------------------------------
0 Cautions

    No cautions found!

------------------------------------------------------------------------------------
Suggested next steps:

    display_underconstrained_set()

====================================================================================

As you can see, the toolbox is reporting one degree of freedom and a structural singularity. In this case, these are both expected as we are trying to solve an optimization problem with one degree of freedom so we can move on to the next step.

Evaluate the initial point#

Before we try to solve our degenerate model, it can often be useful to check the state of the model at the initial step seen by the solver. The cell below creates an instance of Ipopt and sets the iteration limit to 0 so that it terminates at the initial condition.

solver = pyo.SolverFactory("ipopt")

# Specifying an iteration limit of 0 allows us to inspect the initial point
solver.options["max_iter"] = 0

# "Solving" the model with an iteration limit of 0 load the initial point and applies
# any preprocessors (e.g., enforces bounds)
solver.solve(m, tee=True)
WARNING: Could not locate the 'ipopt' executable, which is required for solver
ipopt
---------------------------------------------------------------------------
ApplicationError                          Traceback (most recent call last)
Cell In[5], line 8
      4 solver.options["max_iter"] = 0
      6 # "Solving" the model with an iteration limit of 0 load the initial point and applies
      7 # any preprocessors (e.g., enforces bounds)
----> 8 solver.solve(m, tee=True)

File ~/checkouts/readthedocs.org/user_builds/idaes-examples/envs/latest/lib/python3.8/site-packages/pyomo/opt/base/solvers.py:534, in OptSolver.solve(self, *args, **kwds)
    531 def solve(self, *args, **kwds):
    532     """Solve the problem"""
--> 534     self.available(exception_flag=True)
    535     #
    536     # If the inputs are models, then validate that they have been
    537     # constructed! Collect suffix names to try and import from solution.
    538     #
    539     from pyomo.core.base.block import BlockData

File ~/checkouts/readthedocs.org/user_builds/idaes-examples/envs/latest/lib/python3.8/site-packages/pyomo/opt/solver/shellcmd.py:140, in SystemCallSolver.available(self, exception_flag)
    138     if exception_flag:
    139         msg = "No executable found for solver '%s'"
--> 140         raise ApplicationError(msg % self.name)
    141     return False
    142 return True

ApplicationError: No executable found for solver 'ipopt'

Identify constraints with large residuals at the initial point#

With the solution at the initial point, we can then use the Diagnostics Toolbox to check the residuals of all constraints at the initial point.

# Check for large residuals
dt.display_constraints_with_large_residuals()
====================================================================================
The following constraint(s) have large residuals (>1.0E-05):

    con2: 2.00000E+00
    con5: 2.00000E+00

====================================================================================

We can also check for variables which are close to (or outside of) their bounds.

dt.display_variables_near_bounds()
====================================================================================
The following variable(s) have values close to their bounds (abs=1.0E-04, rel=1.0E-04):


====================================================================================

Solve the optimization problem and extract the solution#

There appear to be no significant issues at the initial point, so let us move on to solving the optimization problem.

solver.options["max_iter"] = 50
solver.solve(m, tee=True)
Ipopt 3.13.2: max_iter=50


******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt

This version of Ipopt was compiled from source code available at
    https://github.com/IDAES/Ipopt as part of the Institute for the Design of
    Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
    Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.

This version of Ipopt was compiled using HSL, a collection of Fortran codes
    for large-scale scientific computation.  All technical papers, sales and
    publicity material resulting from use of the HSL codes within IPOPT must
    contain the following acknowledgement:
        HSL, a collection of Fortran codes for large-scale scientific
        computation. See http://www.hsl.rl.ac.uk.
******************************************************************************

This is Ipopt version 3.13.2, running with linear solver ma27.

Number of nonzeros in equality constraint Jacobian...:        6
Number of nonzeros in inequality constraint Jacobian.:        6
Number of nonzeros in Lagrangian Hessian.............:        0

Total number of variables............................:        3
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        3
                     variables with only upper bounds:        0
Total number of equality constraints.................:        2
Total number of inequality constraints...............:        3
        inequality constraints with only lower bounds:        2
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        1

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  3.0000000e+00 2.00e+00 1.00e+00  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  1.3934810e+00 3.93e-01 2.17e-01  -1.0 1.23e+00    -  7.90e-01 8.03e-01h  1
   2  1.0184419e+00 1.84e-02 1.26e-02  -1.7 7.02e-01    -  9.41e-01 9.53e-01h  1
   3  1.0011246e+00 1.12e-03 3.82e-02  -2.5 1.50e-02    -  1.00e+00 9.39e-01h  1
   4  1.0006914e+00 6.91e-04 3.12e+00  -2.5 3.17e-03    -  1.00e+00 3.85e-01h  1
   5  1.0002664e+00 2.66e-04 4.35e+00  -2.5 1.12e-03    -  1.00e+00 6.15e-01h  1
   6  1.0001115e+00 1.12e-04 1.07e+01  -2.5 4.99e-04    -  1.00e+00 5.82e-01h  1
   7  1.0000788e+00 7.88e-05 4.33e+01  -2.5 1.88e-04    -  1.00e+00 2.94e-01f  2
   8  1.0000153e+00 1.53e-05 2.19e+01  -2.5 6.85e-05    -  1.00e+00 8.08e-01h  1
   9  1.0000118e+00 1.18e-05 2.78e+02  -2.5 3.47e-05    -  1.00e+00 2.45e-01f  2
iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
  10  1.0000014e+00 1.44e-06 2.83e-08  -2.5 7.79e-06    -  1.00e+00 1.00e+00h  1
  11  1.0000000e+00 1.39e-09 1.84e-11  -5.7 2.56e-06    -  1.00e+00 1.00e+00h  1
  12  1.0000000e+00 1.39e-09 1.24e+02  -8.6 1.16e-08    -  1.00e+00 9.54e-07h 21
  13  9.9999999e-01 9.82e-09 1.14e-13  -8.6 1.33e-08    -  1.00e+00 1.00e+00s 22
  14  9.9999999e-01 9.96e-09 6.46e-14  -8.6 2.29e-10    -  1.00e+00 1.00e+00s 22
  15  9.9999999e-01 9.96e-09 1.82e+02  -9.0 4.00e-11    -  1.00e+00 9.54e-07h 21
  16  9.9999999e-01 1.00e-08 1.52e-13  -9.0 5.89e-11    -  1.00e+00 1.00e+00s 22

Number of Iterations....: 16

                                   (scaled)                 (unscaled)
Objective...............:   9.9999999000238915e-01    9.9999999000238915e-01
Dual infeasibility......:   1.5188693260016487e-13    1.5188693260016487e-13
Constraint violation....:   9.9976108502985994e-09    9.9976108502985994e-09
Complementarity.........:   9.2217032157601989e-10    9.2217032157601989e-10
Overall NLP error.......:   9.9976108502985994e-09    9.9976108502985994e-09


Number of objective function evaluations             = 111
Number of objective gradient evaluations             = 17
Number of equality constraint evaluations            = 111
Number of inequality constraint evaluations          = 111
Number of equality constraint Jacobian evaluations   = 17
Number of inequality constraint Jacobian evaluations = 17
Number of Lagrangian Hessian evaluations             = 16
Total CPU secs in IPOPT (w/o function evaluations)   =      0.000
Total CPU secs in NLP function evaluations           =      0.000

EXIT: Optimal Solution Found.
{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 5, 'Number of variables': 3, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.04887509346008301}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}

We got lucky here. Ipopt implements several algorithmic and numerical safeguards to handle (mildy) degenerate equations. Nevertheless, notice the last column of the Ipopt output labeled ls. This is the number of line search evaluations. For iterations 0 to 11, ls is 1, which means Ipopt is taking full steps. For iterations 12 to 16, however, ls is greater than 20. This means Ipopt is struggling (a little) to converge to the solution.

The cell below shows the values of the variables at the solution.

m.x.display()
x : Size=3, Index=I
    Key : Lower : Value              : Upper : Fixed : Stale : Domain
      1 :     0 : 1.0000000099975996 :     5 : False : False :  Reals
      2 :     0 :                0.0 :     5 : False : False :  Reals
      3 :     0 :                0.0 :     5 : False : False :  Reals

Check the rank of the Jacobian of the equality constraints#

One way to check for the presence of degenerate equations is to calculate the rank of the Jacobian matrix of the equality constraints. A singular value near 0 indicates the Jacobian is rank deficient, and for each near-zero singular value there is likely one degenerate constraint.

The Diagnostics Toolbox contains a Singular Value Decomposition (SVD) toolbox which can be used to determine the number of small singular values in the Jacobian for a model as shown below.

svd = dt.prepare_svd_toolbox()
svd.display_rank_of_equality_constraints()
====================================================================================

Number of Singular Values less than 1.0E-06 is 1

====================================================================================

As can be seen above, there is one singular value less than 1e-6, suggesting we have one degenerate constraint.

Finding Irreducible Degenerate Sets (IDS)#

Next, we can use Degeneracy Hunter to identify irreducible degenerate sets; that is, the smallest set of constraints that contain a redundant constraint.

Degeneracy Hunter first identifies candidate degenerate equations by solving a mixed integer linear program (MILP). It then iterates through the candidate equations and solves a second MILP for each to compute the irreducible degenerate set that must contain the candidate equation.

Note: Degeneracy Hunter requires a suitable MILP solver, users can specify their solver of choice via a configuration argument. The default option is SCIP (https://scipopt.org/) which is an open-source solver.

The cell below shows how to create an instance of Degeneracy Hunter and generate a report of the irreducible degenerate sets.

dh = dt.prepare_degeneracy_hunter()
dh.report_irreducible_degenerate_sets()
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Searching for Candidate Equations
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Building MILP model.
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Solving Candidates MILP model.
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Searching for Irreducible Degenerate Sets
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Building MILP model to compute irreducible degenerate set.
Solving MILP 1 of 2.
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Solving IDS MILP for constraint con2.
Solving MILP 2 of 2.
2023-11-17 14:33:20 [INFO] idaes.core.util.model_diagnostics: Solving IDS MILP for constraint con5.
====================================================================================
Irreducible Degenerate Sets

    Irreducible Degenerate Set 0
        nu    Constraint Name
        1.0   con2
        -1.0  con5

    Irreducible Degenerate Set 1
        nu    Constraint Name
        -1.0  con2
        1.0   con5

====================================================================================

As can be seen above, Degeneracy Hunter identified two constraints as potentially redundant (the duplicate constraints con2 and con5). Degeneracy Hunter then reports an Irreducible Degenerate Set for each of these constraints, which in the case as identical. More complex models may have partially overlapping IDS’s.

These results show us that con2 and con5 are mutually redundant; i.e., we can eliminate one of these with no effect on the model solution.

Reformulate Model to Remove Redundant Constraint#

Now let’s reformulate the model by removing the redundant equality constraint con5 and solve the reformulated model.

m2 = build_example(include_redundant_constraint=False)

solver.solve(m2, tee=True)
Ipopt 3.13.2: max_iter=50


******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit http://projects.coin-or.org/Ipopt

This version of Ipopt was compiled from source code available at
    https://github.com/IDAES/Ipopt as part of the Institute for the Design of
    Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE
    Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.

This version of Ipopt was compiled using HSL, a collection of Fortran codes
    for large-scale scientific computation.  All technical papers, sales and
    publicity material resulting from use of the HSL codes within IPOPT must
    contain the following acknowledgement:
        HSL, a collection of Fortran codes for large-scale scientific
        computation. See http://www.hsl.rl.ac.uk.
******************************************************************************

This is Ipopt version 3.13.2, running with linear solver ma27.

Number of nonzeros in equality constraint Jacobian...:        3
Number of nonzeros in inequality constraint Jacobian.:        6
Number of nonzeros in Lagrangian Hessian.............:        0

Total number of variables............................:        3
                     variables with only lower bounds:        0
                variables with lower and upper bounds:        3
                     variables with only upper bounds:        0
Total number of equality constraints.................:        1
Total number of inequality constraints...............:        3
        inequality constraints with only lower bounds:        2
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        1

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0  3.0000000e+00 2.00e+00 6.30e-01  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1  1.3934810e+00 3.93e-01 1.34e-01  -1.0 1.23e+00    -  7.90e-01 8.03e-01f  1
   2  1.0184419e+00 1.84e-02 9.15e-03  -1.7 7.02e-01    -  9.41e-01 9.53e-01h  1
   3  1.0011246e+00 1.12e-03 3.88e-02  -2.5 1.50e-02    -  1.00e+00 9.39e-01h  1
   4  1.0006914e+00 6.91e-04 3.12e+00  -2.5 3.17e-03    -  1.00e+00 3.85e-01h  1
   5  1.0002664e+00 2.66e-04 4.35e+00  -2.5 1.12e-03    -  1.00e+00 6.15e-01h  1
   6  1.0001115e+00 1.12e-04 1.07e+01  -2.5 5.00e-04    -  1.00e+00 5.81e-01h  1
   7  1.0000788e+00 7.88e-05 4.34e+01  -2.5 1.88e-04    -  1.00e+00 2.93e-01f  2
   8  1.0000154e+00 1.54e-05 2.26e+01  -2.5 6.89e-05    -  1.00e+00 8.04e-01h  1
   9  1.0000118e+00 1.18e-05 2.98e+02  -2.5 3.64e-05    -  1.00e+00 2.33e-01f  2
iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
  10  1.0000020e+00 2.04e-06 1.33e+02  -2.5 9.72e-06    -  1.00e+00 8.28e-01h  1
  11  1.0000016e+00 1.61e-06 2.26e+03  -2.5 4.79e-06    -  1.00e+00 2.12e-01f  2
  12  1.0000002e+00 2.46e-07 8.72e+02  -2.5 1.20e-06    -  1.00e+00 8.47e-01h  1
  13  1.0000002e+00 1.95e-07 1.71e+04  -2.5 7.02e-07    -  1.00e+00 2.09e-01f  2
  14  1.0000000e+00 1.54e-08 3.23e+03  -2.5 1.50e-07    -  1.00e+00 9.21e-01h  1
  15  1.0000000e+00 1.15e-08 9.99e+04  -2.5 6.89e-08    -  1.00e+00 2.54e-01f  2
  16  1.0000000e+00 2.22e-16 2.83e-08  -2.5 8.21e-09    -  1.00e+00 1.00e+00h  1
  17  1.0000000e+00 2.22e-16 4.14e-11  -8.6 8.25e-16    -  1.00e+00 1.00e+00   0

Number of Iterations....: 17

                                   (scaled)                 (unscaled)
Objective...............:   9.9999999999999978e-01    9.9999999999999978e-01
Dual infeasibility......:   4.1425156707686206e-11    4.1425156707686206e-11
Constraint violation....:   2.2204460492503131e-16    2.2204460492503131e-16
Complementarity.........:   2.6658012082875325e-09    2.6658012082875325e-09
Overall NLP error.......:   2.6658012082875325e-09    2.6658012082875325e-09


Number of objective function evaluations             = 23
Number of objective gradient evaluations             = 18
Number of equality constraint evaluations            = 23
Number of inequality constraint evaluations          = 23
Number of equality constraint Jacobian evaluations   = 18
Number of inequality constraint Jacobian evaluations = 18
Number of Lagrangian Hessian evaluations             = 17
Total CPU secs in IPOPT (w/o function evaluations)   =      0.002
Total CPU secs in NLP function evaluations           =      0.000

EXIT: Optimal Solution Found.
{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 4, 'Number of variables': 3, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.006506204605102539}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}

First, let us check to see if the optimal solution has changed.

m2.x.display()
x : Size=3, Index=I
    Key : Lower : Value                : Upper : Fixed : Stale : Domain
      1 :     0 :   1.0000000005924994 :     5 : False : False :  Reals
      2 :     0 :                  0.0 :     5 : False : False :  Reals
      3 :     0 : 4.55844318120227e-11 :     5 : False : False :  Reals

We get the same answer as before, but careful inspection of the Ipopt output reveals a subtle improvement. Notice ls is only 1 or 2 for all of the iterations, in contrast to more than 20 for the original model. This means Ipopt is taking (nearly) full steps for all iterations.

Let’s also compare the number of function evaluations.

Original model (using Ipopt 3.13.2 with ma27):

Number of objective function evaluations             = 111
Number of objective gradient evaluations             = 17
Number of equality constraint evaluations            = 111
Number of inequality constraint evaluations          = 111
Number of equality constraint Jacobian evaluations   = 17
Number of inequality constraint Jacobian evaluations = 17
Number of Lagrangian Hessian evaluations             = 16

Reformulated model (using Ipopt 3.13.2 with ma27):

Number of objective function evaluations             = 23
Number of objective gradient evaluations             = 18
Number of equality constraint evaluations            = 23
Number of inequality constraint evaluations          = 23
Number of equality constraint Jacobian evaluations   = 18
Number of inequality constraint Jacobian evaluations = 18
Number of Lagrangian Hessian evaluations             = 17

Removing a single redundant constraint reduced the number of objective and constraint evaluations by a factor of 5!

Often degenerate equations have a much worse impact on large-scale problems; for example, degenerate equations can cause Ipopt to require many more iterations or terminate at an infeasible point.