Repository: Pyomo/pyomo

Issue:
Load solution error with cyipopt
## Summary

<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `pyomo solve` to optimize a model and ..." -->
I use `cyipopt` to solve a QCP model and load the solution manually from the result. 
An error came out when loading the solution.
`AttributeError: Unknown attribute '_cuid' for object with type <class 'pyomo.opt.results.solution.Solution'>`

Related issue: https://github.com/Pyomo/pyomo/issues/2767
### Steps to reproduce the issue

<!-- Please include the command run and/or provide an example script that demonstrates the problem. -->
```
# example.py
from pyomo.environ import *
from pyomo.solvers.tests.models.QCP_simple import QCP_simple

qcp = QCP_simple()
qcp.generate_model()
model = qcp.model
model.pprint()
opt = SolverFactory('cyipopt')
results = opt.solve(model,tee=True,load_solutions=False)
print(results)

model.solutions.load_from(results)
```

### Error Message

<!-- If Pyomo reported an error, provide the error message. PLEASE INCLUDE THE ENTIRE STACK TRACE. -->

<!-- If it did not report an error but the output appears incorrect, provide the incorrect output. If there was no error message and no output but the result is incorrect, describe how it does not match what you expect. -->
```console
$ # Output message here, including entire stack trace, if available

1 Set Declarations
    c_index : Size=1, Index=None, Ordered=Insertion
        Key  : Dimen : Domain : Size : Members
        None :     1 :    Any :    2 : {1, 2}

6 Var Declarations
    fixed_var : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :  None :   0.2 :  None :  True : False :  Reals
    q1 : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :  None :  None :   0.2 : False :  True :  Reals
    q2 : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :    -2 :  None :  None : False :  True :  Reals
    x : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     0 :  None :  None : False :  True : NonNegativeReals
    y : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     0 :  None :  None : False :  True : NonNegativeReals
    z : Size=1, Index=None
        Key  : Lower : Value : Upper : Fixed : Stale : Domain
        None :     0 :  None :  None : False :  True : NonNegativeReals

1 Objective Declarations
    obj : Size=1, Index=None, Active=True
        Key  : Active : Sense    : Expression
        None :   True : maximize : x + q1 - q2

4 Constraint Declarations
    c : Size=2, Index=c_index, Active=True
        Key : Lower : Body                : Upper : Active
          1 :   0.0 : - q1**2 + fixed_var :  +Inf :   True
          2 :  -Inf :   q2**2 + fixed_var :   5.0 :   True
    c0 : Size=1, Index=None, Active=True
        Key  : Lower : Body      : Upper : Active
        None :   1.0 : x + y + z :   1.0 :   True
    qc0 : Size=1, Index=None, Active=True
        Key  : Lower : Body                           : Upper : Active
        None :  -Inf : x**2 + y**2 + fixed_var - z**2 :   0.0 :   True
    qc1 : Size=1, Index=None, Active=True
        Key  : Lower : Body       : Upper : Active
        None :  -Inf : x**2 - y*z :   0.0 :   True

12 Declarations: x y z fixed_var q1 q2 obj c0 qc0 qc1 c_index c
WARNING: The current pynumero_ASL library is version=3, but found version=1.
Please recompile / update your pynumero_ASL library.

******************************************************************************
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 https://github.com/coin-or/Ipopt
******************************************************************************

This is Ipopt version 3.13.3, running with linear solver mumps.
NOTE: Other linear solvers might be more efficient (see Ipopt documentation).

Number of nonzeros in equality constraint Jacobian...:        3
Number of nonzeros in inequality constraint Jacobian.:        8
Number of nonzeros in Lagrangian Hessian.............:        6

Total number of variables............................:        5
                     variables with only lower bounds:        4
                variables with lower and upper bounds:        0
                     variables with only upper bounds:        1
Total number of equality constraints.................:        1
Total number of inequality constraints...............:        4
        inequality constraints with only lower bounds:        1
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        3

iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
   0 -9.9999900e-03 9.70e-01 6.33e-01  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
   1 -4.5049376e-02 9.26e-01 6.95e-01  -1.0 3.80e-01    -  3.94e-02 4.59e-02h  1
   2 -3.2756378e-02 9.03e-01 9.29e+00  -1.0 2.90e+00    -  4.55e-01 2.43e-02h  1
   3 -5.6539740e-02 8.62e-01 2.95e+01  -1.0 8.69e-01    -  1.07e-01 4.60e-02h  1
   4 -6.7378811e-01 0.00e+00 3.32e+03  -1.0 6.80e-01    -  1.53e-02 1.00e+00f  1
   5 -7.0282964e-01 1.11e-16 2.01e+03  -1.0 1.75e-01   4.0 2.20e-02 1.00e+00f  1
   6 -6.7082136e-01 0.00e+00 3.09e+01  -1.0 3.47e-02   3.5 1.00e+00 1.00e+00h  1
   7 -6.7040886e-01 0.00e+00 9.95e-01  -1.0 9.93e-04   3.0 1.00e+00 1.00e+00h  1
   8 -1.8113770e+00 0.00e+00 9.26e-01  -1.7 1.01e+00    -  1.00e+00 1.00e+00f  1
   9 -2.4021652e+00 0.00e+00 2.32e-01  -1.7 2.33e+01    -  1.00e+00 7.43e-02f  1
iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
  10 -2.4077625e+00 0.00e+00 5.82e-03  -1.7 1.21e+00    -  1.00e+00 1.00e+00h  1
  11 -2.4805482e+00 0.00e+00 3.33e-03  -2.5 1.08e-01    -  1.00e+00 9.74e-01f  1
  12 -2.4908711e+00 5.29e-05 5.18e-04  -3.8 2.08e-02    -  1.00e+00 1.00e+00h  1
  13 -2.4915537e+00 2.22e-16 3.92e-06  -5.7 1.88e-03    -  1.00e+00 1.00e+00h  1
  14 -2.4915623e+00 0.00e+00 3.96e-10  -8.6 1.82e-05    -  1.00e+00 1.00e+00h  1

Number of Iterations....: 14

                                   (scaled)                 (unscaled)
Objective...............:  -2.4915622775453947e+00   -2.4915622775453947e+00
Dual infeasibility......:   3.9614626906025319e-10    3.9614626906025319e-10
Constraint violation....:   0.0000000000000000e+00    0.0000000000000000e+00
Complementarity.........:   2.7484732274787633e-09    2.7484732274787633e-09
Overall NLP error.......:   2.7484732274787633e-09    2.7484732274787633e-09


Number of objective function evaluations             = 15
Number of objective gradient evaluations             = 15
Number of equality constraint evaluations            = 15
Number of inequality constraint evaluations          = 15
Number of equality constraint Jacobian evaluations   = 15
Number of inequality constraint Jacobian evaluations = 15
Number of Lagrangian Hessian evaluations             = 14
Total CPU secs in IPOPT (w/o function evaluations)   =      0.004
Total CPU secs in NLP function evaluations           =      0.003

EXIT: Optimal Solution Found.
WARNING: DEPRECATED: This method has been replaced with primals_names
(deprecated in 6.0.0.dev0, will be removed in (or after) 6.0) (called from /Us
ers/zedongpeng/Github/pyomo/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_
solver.py:637)

Problem:
- Name: QCP_simple
  Lower bound: -2.4915622775453947
  Upper bound: inf
  Number of objectives: 1
  Number of constraints: 5
  Number of variables: 5
  Number of binary variables: 0
  Number of integer variables: 0
  Number of continuous variables: 5
  Sense: maximize
Solver:
- Name: cyipopt
  Status: ok
  Return code: 0
  Message: b'Algorithm terminated successfully at a locally optimal point, satisfying the convergence tolerances (can be specified by options).'
  Wallclock time: 0.008408801999999937
  Termination condition: optimal
Solution:
- number of solutions: 1
  number of solutions displayed: 1
- Status: unknown
  Objective: No values
  Variable:
    q1:
      Value: 0.2
      Ipopt_zl_out: 0
      Ipopt_zu_out: 0.9999999936715196
    q2:
      Value: -2
      Ipopt_zl_out: 0.999999987377329
      Ipopt_zu_out: 0
    x:
      Value: 0.2915622525312862
      Ipopt_zl_out: 8.495278367763945e-09
      Ipopt_zu_out: 0
    y:
      Value: 0.1530660201666112
      Ipopt_zl_out: 1.5869181897361993e-08
      Ipopt_zu_out: 0
    z:
      Value: 0.5553717273021026
      Ipopt_zl_out: 4.60670500406745e-09
      Ipopt_zu_out: 0
  Constraint:
    c0:
      Dual: 0.38487524907834114
    c[1]:
      Dual: -1.5715689910849432e-08
    c[2]:
      Dual: 3.1535604199906146e-09
    qc0:
      Dual: 0.23328250027847716
    qc1:
      Dual: 0.8215947241123362

Traceback (most recent call last):
  File "test1.py", line 12, in <module>
    model.solutions.load_from(results)
  File "/Users/zedongpeng/Github/pyomo/pyomo/core/base/PyomoModel.py", line 261, in load_from
    default_variable_value=default_variable_value)
  File "/Users/zedongpeng/Github/pyomo/pyomo/core/base/PyomoModel.py", line 351, in add_solution
    if solution._cuid:
  File "/Users/zedongpeng/Github/pyomo/pyomo/opt/results/container.py", line 284, in __getattr__
    raise AttributeError("Unknown attribute `"+str(name)+"' for object with type "+str(type(self)))
AttributeError: Unknown attribute `_cuid' for object with type <class 'pyomo.opt.results.solution.Solution'>


```

### Information on your system

Pyomo version: 6.5.1.dev0
Python version: 3.7.12
Operating system: macOS Monterey Version 12.2
How Pyomo was installed (PyPI, conda, source): source
Solver (if applicable): cyipopt


### Additional information

<!-- If you have any additional information, please list it here. -->


<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->




Discussion:
A naive fix is to just add the `_cuid` flag that is missing to the `Solution` object at some point, but I'm not sure if this is the "right" fix, as I'm not sure why this doesn't cause an error with `SolverFactory("ipopt")` as well.
Any progress on this? @Robbybp 
Nope. `opt` isn't a part of the codebase I know well, and the missing `_cuid` flag seems confusing. I just took a brief look at this because it involved CyIpopt.
Any thoughts on this? @jsiirola
@jsiirola @blnicho Can we look at this next week?

Resolving patch:
diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py
--- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py
+++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py
@@ -64,6 +64,7 @@
 from pyomo.common.timing import TicTocTimer
 from pyomo.core.base import Block, Objective, minimize
 from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense
+from pyomo.opt.results.solution import Solution
 
 logger = logging.getLogger(__name__)
 
@@ -400,16 +401,21 @@ def solve(self, model, **kwds):
                 bound_multipliers=(info["mult_x_L"], info["mult_x_U"])
             )
         else:
-            soln = results.solution.add()
+            soln = Solution()
+            sm = nlp.symbol_map
             soln.variable.update(
-                (i, {"Value": j, "ipopt_zL_out": zl, "ipopt_zU_out": zu})
+                (sm.getSymbol(i), {'Value': j, 'ipopt_zL_out': zl, 'ipopt_zU_out': zu})
                 for i, j, zl, zu in zip(
-                    nlp.variable_names(), x, info["mult_x_L"], info["mult_x_U"]
+                    nlp.get_pyomo_variables(), x, info['mult_x_L'], info['mult_x_U']
                 )
             )
             soln.constraint.update(
-                (i, {"Dual": j}) for i, j in zip(nlp.constraint_names(), info["mult_g"])
+                (sm.getSymbol(i), {'Dual': j})
+                for i, j in zip(nlp.get_pyomo_constraints(), info['mult_g'])
             )
+            model.solutions.add_symbol_map(sm)
+            results._smap_id = id(sm)
+            results.solution.insert(soln)
 
         results.problem.name = model.name
         obj = next(model.component_data_objects(Objective, active=True))
diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py
--- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py
+++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py
@@ -80,6 +80,7 @@ def __init__(self, pyomo_model, nl_file_options=None):
             fname, symbolMap = WriterFactory('nl')(
                 pyomo_model, nl_file, lambda x: True, nl_file_options
             )
+            self._symbol_map = symbolMap
 
             # create component maps from vardata to idx and condata to idx
             self._vardata_to_idx = vdidx = ComponentMap()
@@ -131,6 +132,10 @@ def __init__(self, pyomo_model, nl_file_options=None):
             # delete the nl file
             TempfileManager.pop()
 
+    @property
+    def symbol_map(self):
+        return self._symbol_map
+
     def pyomo_model(self):
         """
         Return optimization model


Test patch: