@@ -216,4 +216,194 @@ _maxdiff(ref, cols...) =
216216 @test termination_status (ma) == JuMP. MOI. LOCALLY_SOLVED
217217 @test maximum (abs .(value (ua) .- value (ub))) < 1e-6
218218 end
219+
220+ @testset " variable/expression interface + model printing" begin
221+ # The small JuMP/Base interface surface: identity, hashing, zero/one,
222+ # convert, broadcasting, string forms, objective plumbing, and the
223+ # native-JuMP-style model printer. No solve — this is all modeling-time.
224+ mp = MGBModel (geom); _quiet! (mp)
225+ @variable (mp, u); @variable (mp, s, Broken ())
226+ E = typeof (zero (u)) # MGBExpr{Float64}
227+
228+ # identity / accessors
229+ @test JuMP. num_variables (mp) == 2
230+ @test JuMP. owner_model (u) === mp
231+ @test JuMP. name (u) == " u"
232+ @test JuMP. name (deriv (u, :dx )) == " deriv(u, :dx)"
233+ @test deriv (u, :id ) == u && u != s
234+ @test copy (u) == u
235+ @test JuMP. isequal_canonical (u, u)
236+ @test Dict (u => 1 )[deriv (u, :id )] == 1 # hash + == in real use
237+ @test sprint (show, deriv (u, :dy )) == " deriv(u, :dy)"
238+ @test occursin (" MGBModel over" , sprint (show, mp))
239+
240+ # zero / one / convert / broadcastable
241+ @test sprint (show, zero (u)) == " 0.0"
242+ @test sprint (show, zero (typeof (u))) == " 0.0"
243+ @test sprint (show, zero (E)) == " 0.0" && sprint (show, zero (zero (u))) == " 0.0"
244+ @test sprint (show, one (typeof (u))) == " 1.0"
245+ @test sprint (show, convert (E, Coef (mp, 2.5 ))) == " 2.5"
246+ @test sprint (show, convert (E, 1.5 )) == " 1.5"
247+ @test (2.0 .* u) isa E && ((u + s) .+ 1.0 ) isa E && (Coef (mp, 1.0 ) .* 2.0 ) isa E
248+ es = sprint (show, 2.0 * u + Coef (mp, x -> x[1 ]) * s + Coef (mp, x -> x[1 ]))
249+ @test occursin (" 2.0*" , es) && occursin (" ⟨coef⟩" , es)
250+
251+ # MutableArithmetics promotion (the Real variants; the macros need these
252+ # on some JuMP versions, so exercise them through the ext's own alias)
253+ MA = JuMP. _MA
254+ @test MA. promote_operation (+ , typeof (u), Float64) == E
255+ @test MA. promote_operation (* , Float64, typeof (u)) == E
256+ @test MA. promote_operation (- , typeof (Coef (mp, 1.0 )), Float64) == E
257+ @test MA. promote_operation (+ , E, Float64) == E
258+
259+ # objective plumbing: sense accessors, split sense/function route,
260+ # and the not-an-integral error fallbacks
261+ JuMP. set_objective_sense (mp, JuMP. MOI. MAX_SENSE)
262+ @test JuMP. objective_sense (mp) == JuMP. MOI. MAX_SENSE
263+ JuMP. set_objective_sense (mp, JuMP. MOI. MIN_SENSE)
264+ JuMP. set_objective_function (mp, integral (1.0 * s + 1.0 ))
265+ @test_throws ArgumentError JuMP. set_objective_function (mp, s)
266+ @test_throws ArgumentError @objective (mp, Min, s)
267+ @objective (mp, Min, integral (1.0 * s + 1.0 ))
268+
269+ # printing with no constraints yet, then with every constraint flavor
270+ @test occursin (" ∫(" , sprint (print, mp))
271+ cr = @constraint (mp, u == Coef (mp, 0.0 ), On (bd))
272+ @test sprint (show, cr) isa String
273+ @test_throws ArgumentError JuMP. dual (cr)
274+ @constraint (mp, u >= - 10.0 )
275+ @constraint (mp, [deriv (u, :dx ); deriv (u, :dy ); s] in EpiPower (x -> 1.5 + 0.1 * x[1 ]^ 2 ))
276+ @constraint (mp, [s; deriv (u, :dx ); deriv (u, :dy )] in JuMP. MOI. SecondOrderCone (3 ))
277+ str = sprint (print, mp)
278+ @test occursin (" == ⟨data⟩" , str) && occursin (" on " , str) # Dirichlet + region
279+ @test occursin (" ≥ 0" , str) # nonneg row
280+ @test occursin (" ^⟨coef⟩" , str) && occursin (" ^1.0" , str) # spatial + uniform exponent
281+ @test occursin (" ∫(" , str) && occursin (" + 1.0" , str) # objective with constant
282+
283+ # a fresh model prints "0" for the missing objective
284+ m0 = MGBModel (geom)
285+ @variable (m0, w0)
286+ @test occursin (" 0" , sprint (print, m0))
287+
288+ # RegionConstraint is a JuMP.AbstractConstraint: function/set accessors
289+ rc = JuMP. build_constraint (error, 1.0 * u, JuMP. MOI. GreaterThan (0.0 ), On (bd))
290+ @test JuMP. jump_function (rc) isa E
291+ @test JuMP. moi_set (rc) isa JuMP. MOI. GreaterThan{Float64}
292+
293+ # vector comparisons (with and without On) must land in the same
294+ # region/cone machinery as their scalar spellings
295+ @constraint (mp, [u, u] >= [Coef (mp, - 5.0 ), Coef (mp, - 6.0 )], On (bd))
296+ @constraint (mp, [u, u] <= [Coef (mp, 5.0 ), Coef (mp, 6.0 )], On (bd))
297+ @constraint (mp, [u, u] >= [Coef (mp, - 7.0 ), Coef (mp, - 8.0 )])
298+ @test_throws ArgumentError @constraint (mp, [u, u] == [Coef (mp, 0.0 ), Coef (mp, 0.0 )], On (bd))
299+ @test JuMP. num_constraints (mp) == 7
300+
301+ # rejections with explanatory errors
302+ @test_throws ArgumentError u * s # nonlinear
303+ @test_throws ArgumentError @constraint (mp, - 1.0 <= u <= 1.0 ) # Interval set
304+ @test_throws ArgumentError @constraint (mp, [u, s, u] in JuMP. MOI. ExponentialCone ())
305+ end
306+
307+ @testset " lowering guards" begin
308+ # a Broken variable cannot carry a Dirichlet condition
309+ mb = MGBModel (geom); _quiet! (mb)
310+ @variable (mb, ub, Broken ()); @variable (mb, sb, Broken ())
311+ @constraint (mb, ub == Coef (mb, 0.0 ), On (bd))
312+ @constraint (mb, [ub; sb] in EpiPower (2.0 ))
313+ @objective (mb, Min, integral (1.0 * sb))
314+ @test_throws ArgumentError optimize! (mb)
315+
316+ # Dirichlet-but-never-differentiated is legal but warns (likely accident)
317+ mw = MGBModel (geom); _quiet! (mw)
318+ @variable (mw, uw); @variable (mw, sw, Broken ())
319+ set_start (sw, 10.0 )
320+ @constraint (mw, uw == Coef (mw, 0.0 ), On (bd))
321+ @constraint (mw, [uw; sw] in EpiPower (2.0 ))
322+ @objective (mw, Min, integral (1.0 * sw))
323+ @test_logs (:warn , r" never differentiated" ) match_mode = :any optimize! (mw)
324+ @test termination_status (mw) == JuMP. MOI. LOCALLY_SOLVED
325+ end
326+
327+ @testset " scalar-arithmetic sugar lowers to the classical problem" begin
328+ # Objective spelled with every affine-sugar form (unary +/-, Real on
329+ # either side, /); algebraically it is 0.5u + s, i.e. the package's
330+ # default p-Laplacian, so the solve must match the classical reference.
331+ g1 = x -> x[1 ]^ 2 + x[2 ]^ 2
332+ m = MGBModel (geom); _quiet! (m)
333+ @variable (m, u); @variable (m, s, Broken ())
334+ set_start (u, g1)
335+ JuMP. set_start_value (s, 100.0 ) # JuMP-native start API delegates
336+ @constraint (m, u == Coef (m, g1), On (bd))
337+ @constraint (m, [deriv (u, :dx ); deriv (u, :dy ); s] in EpiPower (1.5 ))
338+ @objective (m, Min,
339+ integral (+ (u / 2 ) - (- s) + (u + 1.0 ) + (1.0 + u) + (1.0 - u) - u - 3.0 ))
340+ optimize! (m)
341+ sol_ref = mgb_solve (assemble (amg (geom); p = 1.5 ); verbose = false )
342+ @test maximum (abs .(value (u) .- sol_ref. z[:, 1 ])) < _JUMP_MATCH_TOL
343+ # success-side status accessors
344+ @test occursin (" converged" , JuMP. raw_status (m))
345+ @test JuMP. solve_time (m) > 0
346+ @test JuMP. primal_status (m) == JuMP. MOI. FEASIBLE_POINT
347+ @test JuMP. dual_status (m) == JuMP. MOI. NO_SOLUTION
348+ end
349+
350+ @testset " region-restricted cone + Uniform variable" begin
351+ # ROF-style fidelity cone imposed only on the left half (a :power piece
352+ # inside convex_piecewise), with a global r >= 0 keeping the slack
353+ # bounded off the region.
354+ fdata = x -> 0.5 * tanh (5 * x[1 ])
355+ mask = reshape (geom. x, :, 2 )[:, 1 ] .< 0
356+ mr = MGBModel (geom); _quiet! (mr)
357+ @variable (mr, w); @variable (mr, sw, Broken ()); @variable (mr, r, Broken ())
358+ set_start (w, fdata); set_start (sw, 10.0 ); set_start (r, 10.0 )
359+ fd = Coef (mr, fdata)
360+ @constraint (mr, w == fd, On (bd))
361+ @constraint (mr, [deriv (w, :dx ); deriv (w, :dy ); sw] in EpiPower (1.0 ))
362+ @constraint (mr, [w - fd; r] in EpiPower (2.0 ), On (geom, mask))
363+ @constraint (mr, r >= 0.0 )
364+ @objective (mr, Min, integral (sw + Coef (mr, 0.5 ) * r))
365+ optimize! (mr)
366+ @test termination_status (mr) == JuMP. MOI. LOCALLY_SOLVED
367+ rv = value (r); gap2 = (value (w) .- value (fd)) .^ 2
368+ @test maximum (rv[.! mask]) < 1e-3 # cone absent off-region
369+ @test all (rv[mask] .>= gap2[mask] .- 1e-8 ) # epigraph holds on-region
370+
371+ # Uniform(): one global constant c with c >= u pointwise; minimizing a
372+ # small weight on c drives it to max(u), and its column is constant.
373+ g0 = x -> x[1 ]^ 2 + x[2 ]^ 2
374+ mu = MGBModel (geom); _quiet! (mu)
375+ @variable (mu, uu); @variable (mu, su, Broken ()); @variable (mu, c, Uniform ())
376+ set_start (uu, g0); set_start (su, 100.0 ); set_start (c, 10.0 )
377+ @constraint (mu, uu == Coef (mu, g0), On (bd))
378+ @constraint (mu, [deriv (uu, :dx ); deriv (uu, :dy ); su] in EpiPower (2.0 ))
379+ @constraint (mu, c >= uu)
380+ @objective (mu, Min, integral (Coef (mu, 0.5 ) * uu + su + Coef (mu, 0.1 ) * c))
381+ optimize! (mu)
382+ @test termination_status (mu) == JuMP. MOI. LOCALLY_SOLVED
383+ cv = value (c)
384+ @test maximum (abs .(cv .- cv[1 ])) < 1e-8 # genuinely one global dof
385+ @test cv[1 ] > maximum (value (uu)) - 1e-6 # dominates u ...
386+ @test cv[1 ] < maximum (value (uu)) + 1e-2 # ... and binds
387+ end
388+
389+ @testset " convergence failure surfaces as OTHER_ERROR" begin
390+ # maxit too small for the barrier to reach its target t: optimize! must
391+ # catch MGBConvergenceFailure, report it, and refuse to hand out values.
392+ g1 = x -> x[1 ]^ 2 + x[2 ]^ 2
393+ mf = MGBModel (geom); _quiet! (mf)
394+ set_attribute (mf, " maxit" , 2 )
395+ @variable (mf, u); @variable (mf, s, Broken ())
396+ set_start (u, g1); set_start (s, 100.0 )
397+ @constraint (mf, u == Coef (mf, g1), On (bd))
398+ @constraint (mf, [deriv (u, :dx ); deriv (u, :dy ); s] in EpiPower (1.5 ))
399+ @objective (mf, Min, integral (Coef (mf, 0.5 ) * u + s))
400+ optimize! (mf) # must not throw
401+ @test termination_status (mf) == JuMP. MOI. OTHER_ERROR
402+ @test occursin (" onvergence failure" , JuMP. raw_status (mf))
403+ @test JuMP. primal_status (mf) == JuMP. MOI. NO_SOLUTION
404+ @test_throws ArgumentError value (u)
405+ @test_throws ArgumentError objective_value (mf)
406+ @test_throws ArgumentError mgb_solution (mf)
407+ @test_throws ArgumentError solver_log (mf)
408+ end
219409end
0 commit comments