@@ -1252,6 +1252,79 @@ function test_preserve_diff_model_structural_change_falls_back()
12521252 return
12531253end
12541254
1255+ # A model feasible for small p and INFEASIBLE for large p via a Parameter-only change
1256+ # (x ∈ [0,1], x ≥ p is infeasible once p > 1): lets a preserved re-solve reach a
1257+ # non-differentiable status without any structural edit.
1258+ function _build_boundable_preserve_model ()
1259+ model = DiffOpt. nonlinear_diff_model (Ipopt. Optimizer)
1260+ set_silent (model)
1261+ @variable (model, p in MOI. Parameter (0.5 ))
1262+ @variable (model, 0.0 <= x <= 1.0 )
1263+ @constraint (model, x >= p)
1264+ @objective (model, Min, (x - 0.3 )^ 2 )
1265+ return model, p, x
1266+ end
1267+
1268+ function _reverse_dp_scalar (model, p, x, seed)
1269+ DiffOpt. empty_input_sensitivities! (model)
1270+ DiffOpt. set_reverse_variable (model, x, seed)
1271+ DiffOpt. reverse_differentiate! (model)
1272+ return MOI. get (model, DiffOpt. ReverseConstraintSet (), ParameterRef (p)). value
1273+ end
1274+
1275+ function test_preserve_diff_model_nondifferentiable_solve_discards ()
1276+ # Covers the MOI.optimize! fallback (moi_wrapper.jl): a preserved Parameter-only
1277+ # re-solve that ends non-differentiable must DISCARD the diff model, and the next
1278+ # feasible solve must rebuild gradients identical to a fresh model.
1279+ model, p, x = _build_boundable_preserve_model ()
1280+ MOI. set (model, DiffOpt. PreserveDiffModel (), true )
1281+ optimize! (model)
1282+ @assert is_solved_and_feasible (model)
1283+ _reverse_dp_scalar (model, p, x, 1.0 ) # instantiate the preserved diff model
1284+ diffopt = JuMP. unsafe_backend (model)
1285+ @test diffopt. diff != = nothing
1286+ set_parameter_value (p, 2.0 ) # Parameter-only push into the infeasible region
1287+ optimize! (model)
1288+ @test ! is_solved_and_feasible (model)
1289+ @test diffopt. diff === nothing # the non-differentiable fallback fired
1290+ set_parameter_value (p, 0.5 )
1291+ optimize! (model)
1292+ @assert is_solved_and_feasible (model)
1293+ dp = _reverse_dp_scalar (model, p, x, 1.0 )
1294+ fresh, pf, xf = _build_boundable_preserve_model ()
1295+ optimize! (fresh)
1296+ dp_fresh = _reverse_dp_scalar (fresh, pf, xf, 1.0 )
1297+ @test isapprox (dp, dp_fresh; rtol = 1e-10 )
1298+ return
1299+ end
1300+
1301+ function test_preserve_diff_model_parameter_set_getter_roundtrips ()
1302+ # Covers MOI.get(::NonLinearProgram.Model, ::ConstraintSet, ::Parameter): after a
1303+ # preserved re-solve refreshes the value into the diff model, reading it back through
1304+ # the diff model's own getter must return that value.
1305+ model, p, x = _build_boundable_preserve_model ()
1306+ MOI. set (model, DiffOpt. PreserveDiffModel (), true )
1307+ optimize! (model)
1308+ _reverse_dp_scalar (model, p, x, 1.0 ) # instantiate the preserved diff model
1309+ newval = 0.8
1310+ set_parameter_value (p, newval)
1311+ optimize! (model)
1312+ _reverse_dp_scalar (model, p, x, 1.0 ) # _refresh_parameters writes newval into diff
1313+ diffopt = JuMP. unsafe_backend (model)
1314+ ci_src = only (
1315+ MOI. get (
1316+ diffopt. optimizer,
1317+ MOI. ListOfConstraintIndices{
1318+ MOI. VariableIndex,
1319+ MOI. Parameter{Float64},
1320+ }(),
1321+ ),
1322+ )
1323+ got = MOI. get (diffopt. diff, MOI. ConstraintSet (), diffopt. index_map[ci_src])
1324+ @test got. value == newval
1325+ return
1326+ end
1327+
12551328end # module
12561329
12571330TestNLPProgram. runtests ()
0 commit comments