@@ -369,6 +369,101 @@ function test_loa_restores_prior_time_limit()
369369 @test objective_value (model) ≈ 7.0 atol = 1e-4
370370end
371371
372+ function test_loa_restores_displaced_optimizer ()
373+ # LOA solves the NLP on the model itself, so it displaces the
374+ # model's own optimizer. A later reformulation solve must get that
375+ # optimizer back, not LOA's NLP solver.
376+ model = GDPModel (HiGHS. Optimizer)
377+ set_silent (model)
378+ set_optimizer_attribute (model, " mip_rel_gap" , 0.25 )
379+ @variable (model, 0 <= x <= 10 )
380+ @variable (model, Y[1 : 2 ], Logical)
381+ @constraint (model, x <= 3 , Disjunct (Y[1 ]))
382+ @constraint (model, x <= 7 , Disjunct (Y[2 ]))
383+ @disjunction (model, Y)
384+ @objective (model, Max, x)
385+ displaced = JuMP. unsafe_backend (model)
386+
387+ optimize! (model, gdp_method = LOA (HiGHS. Optimizer))
388+ @test DP. gdp_data (model). displaced_optimizer === displaced
389+
390+ # The reformulation solve restores it, attributes intact.
391+ optimize! (model, gdp_method = BigM ())
392+ @test JuMP. unsafe_backend (model) === displaced
393+ @test get_optimizer_attribute (model, " mip_rel_gap" ) == 0.25
394+ @test DP. gdp_data (model). displaced_optimizer === nothing
395+ @test termination_status (model) == MOI. OPTIMAL
396+ @test objective_value (model) ≈ 7.0 atol = 1e-4
397+ end
398+
399+ function test_loa_repeated_solve_keeps_displaced_optimizer ()
400+ # A second LOA solve displaces the first one's result cache, not the
401+ # model's own optimizer. The stash must still hold the latter.
402+ model = GDPModel (HiGHS. Optimizer)
403+ set_silent (model)
404+ @variable (model, 0 <= x <= 10 )
405+ @variable (model, Y[1 : 2 ], Logical)
406+ @constraint (model, x <= 3 , Disjunct (Y[1 ]))
407+ @constraint (model, x <= 7 , Disjunct (Y[2 ]))
408+ @disjunction (model, Y)
409+ @objective (model, Max, x)
410+ displaced = JuMP. unsafe_backend (model)
411+
412+ optimize! (model, gdp_method = LOA (HiGHS. Optimizer))
413+ optimize! (model, gdp_method = LOA (HiGHS. Optimizer))
414+ @test DP. gdp_data (model). displaced_optimizer === displaced
415+
416+ optimize! (model, gdp_method = BigM ())
417+ @test JuMP. unsafe_backend (model) === displaced
418+ @test termination_status (model) == MOI. OPTIMAL
419+ @test objective_value (model) ≈ 7.0 atol = 1e-4
420+ end
421+
422+ function test_loa_no_optimizer_to_restore ()
423+ # With no optimizer to put back, the result cache must be dropped
424+ # rather than left to serve its stored results as a fresh solve.
425+ model = GDPModel ()
426+ @variable (model, 0 <= x <= 10 )
427+ @variable (model, Y[1 : 2 ], Logical)
428+ @constraint (model, x <= 3 , Disjunct (Y[1 ]))
429+ @constraint (model, x <= 7 , Disjunct (Y[2 ]))
430+ @disjunction (model, Y)
431+ @objective (model, Max, x)
432+
433+ optimize! (model, gdp_method = LOA (HiGHS. Optimizer))
434+ @test DP. gdp_data (model). displaced_optimizer === nothing
435+ @test objective_value (model) ≈ 7.0 atol = 1e-4
436+
437+ @test_throws JuMP. NoOptimizer optimize! (model, gdp_method = BigM ())
438+ set_optimizer (model, HiGHS. Optimizer)
439+ set_silent (model)
440+ optimize! (model, gdp_method = BigM ())
441+ @test objective_value (model) ≈ 7.0 atol = 1e-4
442+ end
443+
444+ function test_loa_cache_refuses_direct_solve ()
445+ # `ignore_optimize_hook = true` would "solve" the result cache and
446+ # clear JuMP's dirty flag, reporting a stale point as freshly
447+ # solved. It must error instead.
448+ model = GDPModel (HiGHS. Optimizer)
449+ set_silent (model)
450+ @variable (model, 0 <= x <= 10 )
451+ @variable (model, Y[1 : 2 ], Logical)
452+ @constraint (model, x <= 3 , Disjunct (Y[1 ]))
453+ @constraint (model, x <= 7 , Disjunct (Y[2 ]))
454+ @disjunction (model, Y)
455+ @objective (model, Max, x)
456+ optimize! (model, gdp_method = LOA (HiGHS. Optimizer))
457+ @test_throws ErrorException optimize! (model;
458+ ignore_optimize_hook = true )
459+
460+ # Still true once the model has been edited underneath the cache.
461+ @constraint (model, x <= 1 )
462+ @test ! has_values (model)
463+ @test_throws ErrorException optimize! (model;
464+ ignore_optimize_hook = true )
465+ end
466+
372467function test_loa_limit_hit_report ()
373468 # Stop the main loop on `max_iter = 1` after the master has produced
374469 # a bound: the report must label the run "limit hit" (not converged),
901996 test_loa_nonlinear_equality_disjunct ()
902997 test_loa_nonlinear_interval_disjunct ()
903998 test_loa_restores_prior_time_limit ()
999+ test_loa_restores_displaced_optimizer ()
1000+ test_loa_repeated_solve_keeps_displaced_optimizer ()
1001+ test_loa_no_optimizer_to_restore ()
1002+ test_loa_cache_refuses_direct_solve ()
9041003 test_loa_limit_hit_report ()
9051004 test_loa_complement_indicator_nonlinear_disjunct ()
9061005 test_loa_nlpf_infeasible_disjunct ()
0 commit comments