77# software. This software is distributed under the 3-clause BSD License.
88# ____________________________________________________________________________________
99
10+ import io
11+ import logging
1012import pyomo .common .unittest as unittest
13+ from pyomo .common .log import LoggingIntercept
1114import pyomo .environ as pyo
1215from pyomo .common .tempfiles import TempfileManager
1316
3942)
4043
4144from pyomo .contrib .pynumero .algorithms .solvers .cyipopt_solver import CyIpoptSolver
45+ from pyomo .contrib .pynumero .interfaces .external_grey_box import ExternalGreyBoxBlock
46+ import pyomo .contrib .pynumero .interfaces .tests .external_grey_box_models as ex_models
47+ from pyomo .contrib .pynumero .examples .external_grey_box .react_example .reactor_model_outputs import (
48+ ReactorConcentrationsOutputModel ,
49+ )
4250
4351if cyipopt_available :
4452 # We don't raise unittest.SkipTest if not cyipopt_available as there is a
@@ -207,7 +215,7 @@ def test_model1(self):
207215 nlp .set_primals (x )
208216 nlp .set_duals (y_sol )
209217 self .assertAlmostEqual (nlp .evaluate_objective (), - 428.6362455416348 , places = 5 )
210- self .assertTrue (np .allclose (info [' mult_g' ], y_sol , rtol = 1e-4 ))
218+ self .assertTrue (np .allclose (info [" mult_g" ], y_sol , rtol = 1e-4 ))
211219
212220 def test_model1_with_scaling (self ):
213221 m = create_model1 ()
@@ -219,35 +227,35 @@ def test_model1_with_scaling(self):
219227
220228 with TempfileManager .new_context () as temp :
221229 cynlp = CyIpoptNLP (PyomoNLP (m ))
222- logfile = temp .create_tempfile (' _cyipopt-scaling.log' )
230+ logfile = temp .create_tempfile (" _cyipopt-scaling.log" )
223231 options = {
224- ' nlp_scaling_method' : ' user-scaling' ,
225- ' output_file' : logfile ,
226- ' file_print_level' : 10 ,
227- ' max_iter' : 0 ,
232+ " nlp_scaling_method" : " user-scaling" ,
233+ " output_file" : logfile ,
234+ " file_print_level" : 10 ,
235+ " max_iter" : 0 ,
228236 }
229237 solver = CyIpoptSolver (cynlp , options = options )
230238 x , info = solver .solve ()
231239 cynlp .close ()
232240
233- with open (logfile , 'r' ) as fd :
241+ with open (logfile , "r" ) as fd :
234242 solver_trace = fd .read ()
235243
236244 # check for the following strings in the log
237- self .assertIn (' nlp_scaling_method = user-scaling' , solver_trace )
245+ self .assertIn (" nlp_scaling_method = user-scaling" , solver_trace )
238246 self .assertIn (f"output_file = { logfile } " , solver_trace )
239- self .assertIn (' objective scaling factor = 1e-06' , solver_trace )
240- self .assertIn (' x scaling provided' , solver_trace )
241- self .assertIn (' c scaling provided' , solver_trace )
242- self .assertIn (' d scaling provided' , solver_trace )
247+ self .assertIn (" objective scaling factor = 1e-06" , solver_trace )
248+ self .assertIn (" x scaling provided" , solver_trace )
249+ self .assertIn (" c scaling provided" , solver_trace )
250+ self .assertIn (" d scaling provided" , solver_trace )
243251 self .assertIn ('DenseVector "x scaling vector" with 3 elements:' , solver_trace )
244- self .assertIn (' x scaling vector[ 1]= 1.0000000000000000e+00' , solver_trace )
245- self .assertIn (' x scaling vector[ 2]= 1.0000000000000000e+00' , solver_trace )
246- self .assertIn (' x scaling vector[ 3]= 4.0000000000000000e+00' , solver_trace )
252+ self .assertIn (" x scaling vector[ 1]= 1.0000000000000000e+00" , solver_trace )
253+ self .assertIn (" x scaling vector[ 2]= 1.0000000000000000e+00" , solver_trace )
254+ self .assertIn (" x scaling vector[ 3]= 4.0000000000000000e+00" , solver_trace )
247255 self .assertIn ('DenseVector "c scaling vector" with 1 elements:' , solver_trace )
248- self .assertIn (' c scaling vector[ 1]= 2.0000000000000000e+00' , solver_trace )
256+ self .assertIn (" c scaling vector[ 1]= 2.0000000000000000e+00" , solver_trace )
249257 self .assertIn ('DenseVector "d scaling vector" with 1 elements:' , solver_trace )
250- self .assertIn (' d scaling vector[ 1]= 3.0000000000000000e+00' , solver_trace )
258+ self .assertIn (" d scaling vector[ 1]= 3.0000000000000000e+00" , solver_trace )
251259
252260 def test_model2 (self ):
253261 model = create_model2 ()
@@ -260,7 +268,7 @@ def test_model2(self):
260268 nlp .set_primals (x )
261269 nlp .set_duals (y_sol )
262270 self .assertAlmostEqual (nlp .evaluate_objective (), - 31.000000057167462 , places = 5 )
263- self .assertTrue (np .allclose (info [' mult_g' ], y_sol , rtol = 1e-4 ))
271+ self .assertTrue (np .allclose (info [" mult_g" ], y_sol , rtol = 1e-4 ))
264272
265273 def test_model3 (self ):
266274 G = np .array ([[6 , 2 , 1 ], [2 , 5 , 2 ], [1 , 2 , 4 ]])
@@ -278,12 +286,12 @@ def test_model3(self):
278286 nlp .set_primals (x )
279287 nlp .set_duals (y_sol )
280288 self .assertAlmostEqual (nlp .evaluate_objective (), - 3.5 , places = 5 )
281- self .assertTrue (np .allclose (info [' mult_g' ], y_sol , rtol = 1e-4 ))
289+ self .assertTrue (np .allclose (info [" mult_g" ], y_sol , rtol = 1e-4 ))
282290
283291 def test_options (self ):
284292 model = create_model1 ()
285293 nlp = PyomoNLP (model )
286- solver = CyIpoptSolver (CyIpoptNLP (nlp ), options = {' max_iter' : 1 })
294+ solver = CyIpoptSolver (CyIpoptNLP (nlp ), options = {" max_iter" : 1 })
287295 x , info = solver .solve (tee = False )
288296 nlp .set_primals (x )
289297 self .assertAlmostEqual (nlp .evaluate_objective (), - 5.0879028e02 , places = 5 )
@@ -414,3 +422,71 @@ def intermediate(
414422 x , y = iterate_data [- 1 ]
415423 self .assertTrue (np .allclose (x_sol , x ))
416424 self .assertTrue (np .allclose (y_sol , y ))
425+
426+
427+ @unittest .skipUnless (cyipopt_available , "cyipopt is not available" )
428+ class TestCyIpoptGreyBox (unittest .TestCase ):
429+ """Most of the grey-box functionality is tested elsewhere. Here we just
430+ need to test that we correctly override the hessian_approximation option
431+ when Hessians aren't supported.
432+ """
433+
434+ def test_solve_hessian_not_supported (self ):
435+ m = pyo .ConcreteModel ()
436+ m .reactor = ExternalGreyBoxBlock (
437+ external_model = ReactorConcentrationsOutputModel ()
438+ )
439+ m .k1con = pyo .Constraint (expr = m .reactor .inputs ["k1" ] == 5 / 6 )
440+ m .k2con = pyo .Constraint (expr = m .reactor .inputs ["k2" ] == 5 / 3 )
441+ m .k3con = pyo .Constraint (expr = m .reactor .inputs ["k3" ] == 1 / 6000 )
442+ m .cafcon = pyo .Constraint (expr = m .reactor .inputs ["caf" ] == 10000 )
443+ m .obj = pyo .Objective (expr = m .reactor .outputs ["cb" ], sense = pyo .maximize )
444+ solver = pyo .SolverFactory ("cyipopt" )
445+ results = solver .solve (m , tee = True )
446+ pyo .assert_optimal_termination (results )
447+ self .assertNotIn ("hessian_approximation" , solver .config .options )
448+ self .assertAlmostEqual (pyo .value (m .reactor .inputs ["sv" ]), 1.34381 , places = 3 )
449+ self .assertAlmostEqual (pyo .value (m .reactor .outputs ["cb" ]), 1072.4372 , places = 2 )
450+
451+ def test_solve_hessian_not_supported_override_user_option (self ):
452+ """Make sure we do this even when the user says to use an exact Hessian"""
453+ m = pyo .ConcreteModel ()
454+ m .reactor = ExternalGreyBoxBlock (
455+ external_model = ReactorConcentrationsOutputModel ()
456+ )
457+ m .k1con = pyo .Constraint (expr = m .reactor .inputs ["k1" ] == 5 / 6 )
458+ m .k2con = pyo .Constraint (expr = m .reactor .inputs ["k2" ] == 5 / 3 )
459+ m .k3con = pyo .Constraint (expr = m .reactor .inputs ["k3" ] == 1 / 6000 )
460+ m .cafcon = pyo .Constraint (expr = m .reactor .inputs ["caf" ] == 10000 )
461+ m .obj = pyo .Objective (expr = m .reactor .outputs ["cb" ], sense = pyo .maximize )
462+ solver = pyo .SolverFactory ("cyipopt" )
463+ solver .config .options ["hessian_approximation" ] = "exact"
464+ buf = io .StringIO ()
465+ with LoggingIntercept (buf , "pyomo.contrib.pynumero" , logging .WARNING ):
466+ results = solver .solve (m , tee = True )
467+ msg = "at least one grey box model does not support Hessians"
468+ self .assertIn (msg , buf .getvalue ())
469+ pyo .assert_optimal_termination (results )
470+ self .assertEqual (solver .config .options ["hessian_approximation" ], "exact" )
471+ self .assertAlmostEqual (pyo .value (m .reactor .inputs ["sv" ]), 1.34381 , places = 3 )
472+ self .assertAlmostEqual (pyo .value (m .reactor .outputs ["cb" ]), 1072.4372 , places = 2 )
473+
474+ def test_hessian_supported_no_override (self ):
475+ """Make sure we didn't accidentally override the hessian_approximation
476+ option when Hessian *is* supported.
477+ """
478+ m = pyo .ConcreteModel ()
479+ external_model = ex_models .PressureDropSingleOutputWithHessian ()
480+ m .x = pyo .Var (range (external_model .n_inputs ()))
481+ m .eq = pyo .Constraint (expr = sum (m .x .values ()) == 1 )
482+ solver = pyo .SolverFactory ("cyipopt" )
483+ with TempfileManager .new_context () as temp :
484+ logfile = temp .create_tempfile ("hessian-no-override.log" )
485+ options = dict (output_file = logfile , max_iter = 0 , print_user_options = "yes" )
486+ solver .config .options .update (options )
487+ _ , cynlp = solver .solve (m , tee = True , return_nlp = True )
488+ # IIRC this may be necessary to avoid file IO race condition
489+ # cynlp.close()
490+ with open (logfile , "r" ) as fd :
491+ solver_trace = fd .read ()
492+ self .assertNotIn ("hessian_approximation" , solver_trace )
0 commit comments