Skip to content

Commit c19ab3a

Browse files
Sébastien LoiselSébastien Loisel
authored andcommitted
Cover the JuMP and Gmsh extensions; fix two JuMP-ext bugs the tests found
Extend test_jump.jl (87 tests) and test_gmsh.jl (60 tests) to cover the previously untested surface: the variable/expression interface (identity, zero/one/convert, broadcasting, show methods, MA promotion shims), the JuMP model printer, objective plumbing and its error fallbacks, Uniform() variables, region-restricted cones (a :power piece inside convex_piecewise), MOI.SecondOrderCone entry, every affine-sugar operator (solved bit-identically against the classical API), the convergence-failure status surface, lowering rejections and the Dirichlet-never-differentiated warning; on the Gmsh side, gmsh_import from .geo and .msh files (both init-lifecycle branches), subdomain physical groups on the tensor path, the negative-orientation flip (clockwise surface), and prism/line-only/empty/missing-file rejections. Measured coverage: Gmsh ext 249/249 lines, JuMP ext 492/494 (the two rest are internal invariant guards). Fixes found by the new tests: vector comparisons with On crashed (the GreaterThanZero-style marker methods assumed scalar functions; they now branch vector/scalar like the Zeros/Nonnegatives/Nonpositives methods), and interval syntax died with an internal JuMP error because variable_ref_type was not defined for MGBExpr/Coef. Also delete the dead _model helpers.
1 parent f7492c8 commit c19ab3a

3 files changed

Lines changed: 340 additions & 10 deletions

File tree

ext/MultiGridBarrierJuMPExt/MultiGridBarrierJuMPExt.jl

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,6 @@ end
197197

198198
# --- conversions and arithmetic ---------------------------------------------
199199

200-
_model(x::MGBVarRef) = x.model
201-
_model(x::Coef) = x.model
202-
_model(x::MGBExpr) = x.model
203-
204200
_to_expr(x::MGBExpr) = x
205201
function _to_expr(v::MGBVarRef{T}) where {T}
206202
e = MGBExpr{T}()
@@ -313,6 +309,12 @@ Base.convert(::Type{MGBExpr{T}}, x::MGBVarRef{T}) where {T} = _to_expr(x)
313309
Base.convert(::Type{MGBExpr{T}}, x::Coef{T}) where {T} = _to_expr(x)
314310
Base.convert(::Type{MGBExpr{T}}, r::Real) where {T} = _to_expr(T, r)
315311

312+
# JuMP's macro plumbing asks expression types for their variable type (e.g. when
313+
# parsing interval constraints lb <= expr <= ub); without these the user gets an
314+
# internal JuMP error instead of our "unsupported set" rejection.
315+
JuMP.variable_ref_type(::Type{MGBExpr{T}}) where {T} = MGBVarRef{T}
316+
JuMP.variable_ref_type(::Type{Coef{T}}) where {T} = MGBVarRef{T}
317+
316318
# ---------------------------------------------------------------------------
317319
# JuMP model interface: variables
318320
# ---------------------------------------------------------------------------
@@ -423,15 +425,21 @@ function JuMP.build_constraint(err::Function, f, set::JuMP.Nonpositives, on::On)
423425
JuMP.build_constraint(err, f, MOI.LessThan(0.0))
424426
RegionConstraint(inner, on.pairs)
425427
end
426-
# Newer JuMP versions route scalar comparisons with non-Number sides through
427-
# internal *Zero marker sets, again variadically; cover them when they exist.
428-
for (marker, moiset) in ((:GreaterThanZero, :(MOI.GreaterThan(0.0))),
429-
(:LessThanZero, :(MOI.LessThan(0.0))),
430-
(:EqualToZero, :(MOI.EqualTo(0.0))))
428+
# Newer JuMP versions route comparisons with non-Number sides through internal
429+
# *Zero marker sets, again variadically; cover them when they exist. Vector
430+
# comparisons arrive here too (f is then a Vector), so branch like the
431+
# Zeros/Nonnegatives/Nonpositives methods above.
432+
for (marker, moiset, vecset) in
433+
((:GreaterThanZero, :(MOI.GreaterThan(0.0)), :(MOI.Nonnegatives)),
434+
(:LessThanZero, :(MOI.LessThan(0.0)), :(MOI.Nonpositives)),
435+
(:EqualToZero, :(MOI.EqualTo(0.0)), :(MOI.Zeros)))
431436
if isdefined(JuMP, marker)
432437
@eval function JuMP.build_constraint(err::Function, f,
433438
set::JuMP.$marker, on::On)
434-
RegionConstraint(JuMP.build_constraint(err, f, $moiset), on.pairs)
439+
inner = f isa AbstractVector ?
440+
JuMP.build_constraint(err, f, $vecset(length(f))) :
441+
JuMP.build_constraint(err, f, $moiset)
442+
RegionConstraint(inner, on.pairs)
435443
end
436444
end
437445
end

test/test_gmsh.jl

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,90 @@ end
158158
@test all(gm.geometry.x[v, e, 1] < 1e-9 for (v, e) in gm.regions["left_half"])
159159
end
160160

161+
@testset "subdomain physical group on quads (tensor path)" begin
162+
# same split domain, all-quad: subdomain membership on the tensor
163+
# families goes through the element-tag branch of _regions_tensor
164+
# (every DOF of every element of the group), not node tags.
165+
gmsh.clear()
166+
r1 = gmsh.model.occ.addRectangle(-1.0, -1.0, 0.0, 1.0, 2.0)
167+
r2 = gmsh.model.occ.addRectangle(0.0, -1.0, 0.0, 1.0, 2.0)
168+
gmsh.model.occ.fragment([(2, r1)], [(2, r2)])
169+
gmsh.model.occ.synchronize()
170+
leftsurf = Int[]
171+
for (d, t) in gmsh.model.getEntities(2)
172+
xmin, _, _, xmax, _, _ = gmsh.model.getBoundingBox(2, t)
173+
xmax < 1e-6 && push!(leftsurf, t)
174+
end
175+
gmsh.model.addPhysicalGroup(2, leftsurf, -1, "left_half")
176+
gmsh.option.setNumber("Mesh.MeshSizeMax", 0.5)
177+
gmsh.option.setNumber("Mesh.RecombineAll", 1)
178+
gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", 1) # all-quad
179+
gmsh.model.mesh.generate(2)
180+
gm = gmsh_import()
181+
gmsh.option.setNumber("Mesh.RecombineAll", 0)
182+
gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", 0)
183+
@test typeof(gm.geometry.discretization) <: TensorFEM
184+
left = gm.regions["left_half"]
185+
@test !isempty(left)
186+
@test all(gm.geometry.x[v, e, 1] < 1e-9 for (v, e) in left)
187+
# whole-element semantics: every DOF of every member element
188+
es = unique(e for (v, e) in left)
189+
@test length(left) == size(gm.geometry.x, 1) * length(es)
190+
end
191+
192+
@testset "import from a .msh file (gmsh already initialized)" begin
193+
mktempdir() do dir
194+
gmsh.clear()
195+
gmsh.model.occ.addRectangle(-1.0, -1.0, 0.0, 2.0, 2.0)
196+
gmsh.model.occ.synchronize()
197+
gmsh.model.addPhysicalGroup(1,
198+
[t for (d, t) in gmsh.model.getEntities(1)], -1, "boundary")
199+
# gmsh.write only saves elements in physical groups, so the
200+
# surface needs one too (or the file would hold bare lines)
201+
gmsh.model.addPhysicalGroup(2,
202+
[t for (d, t) in gmsh.model.getEntities(2)], -1, "domain")
203+
gmsh.option.setNumber("Mesh.MeshSizeMax", 0.5)
204+
gmsh.model.mesh.generate(2)
205+
mshpath = joinpath(dir, "square.msh")
206+
gmsh.write(mshpath)
207+
gmsh.clear()
208+
gm = gmsh_import(mshpath) # meshed file: opened, not re-generated
209+
@test typeof(gm.geometry.discretization) <: FEM2D_P1
210+
@test length(gm.regions["boundary"]) > 8
211+
V, N = size(gm.geometry.x, 1), size(gm.geometry.x, 2)
212+
@test length(gm.regions["domain"]) == V * N
213+
end
214+
end
215+
216+
@testset "clockwise surface: negative orientation is flipped" begin
217+
# geo-kernel curve loop wound clockwise -> surface normal -z ->
218+
# negatively-oriented quads; the import must flip them and still
219+
# reproduce linears exactly.
220+
gmsh.clear()
221+
p1 = gmsh.model.geo.addPoint(-1.0, -1.0, 0.0)
222+
p2 = gmsh.model.geo.addPoint(1.0, -1.0, 0.0)
223+
p3 = gmsh.model.geo.addPoint(1.0, 1.0, 0.0)
224+
p4 = gmsh.model.geo.addPoint(-1.0, 1.0, 0.0)
225+
l1 = gmsh.model.geo.addLine(p1, p4)
226+
l2 = gmsh.model.geo.addLine(p4, p3)
227+
l3 = gmsh.model.geo.addLine(p3, p2)
228+
l4 = gmsh.model.geo.addLine(p2, p1)
229+
loop = gmsh.model.geo.addCurveLoop([l1, l2, l3, l4])
230+
surf = gmsh.model.geo.addPlaneSurface([loop])
231+
gmsh.model.geo.synchronize()
232+
for l in (l1, l2, l3, l4)
233+
gmsh.model.mesh.setTransfiniteCurve(l, 5)
234+
end
235+
gmsh.model.mesh.setTransfiniteSurface(surf)
236+
gmsh.model.mesh.setRecombine(2, surf)
237+
gmsh.model.addPhysicalGroup(1, [l1, l2, l3, l4], -1, "boundary")
238+
gmsh.model.mesh.generate(2)
239+
gm = gmsh_import()
240+
@test typeof(gm.geometry.discretization) <: TensorFEM
241+
@test abs(sum(gm.geometry.w) - 4.0) < 1e-9 # positive measure
242+
@test _lin_gap(gm.geometry, gm.regions["boundary"]) < 1e-6
243+
end
244+
161245
@testset "unsupported elements are rejected with hints" begin
162246
gmsh.clear()
163247
gmsh.model.occ.addBox(0.0, 0.0, 0.0, 1.0, 1.0, 1.0)
@@ -166,8 +250,56 @@ end
166250
@test_throws ArgumentError gmsh_import()
167251
err = try gmsh_import(); "" catch e; sprint(showerror, e) end
168252
@test occursin("SubdivisionAlgorithm", err)
253+
254+
# prisms (recombined extrusion of a triangle mesh): generic rejection
255+
gmsh.clear()
256+
r = gmsh.model.occ.addRectangle(0.0, 0.0, 0.0, 1.0, 1.0)
257+
gmsh.model.occ.extrude([(2, r)], 0.0, 0.0, 1.0, [1], [], true)
258+
gmsh.model.occ.synchronize()
259+
gmsh.option.setNumber("Mesh.MeshSizeMax", 0.6)
260+
gmsh.model.mesh.generate(3)
261+
@test_throws ArgumentError gmsh_import()
262+
263+
# a mesh with only line elements, and no mesh at all
264+
gmsh.clear()
265+
gmsh.model.occ.addRectangle(-1.0, -1.0, 0.0, 2.0, 2.0)
266+
gmsh.model.occ.synchronize()
267+
gmsh.model.mesh.generate(1)
268+
@test_throws ArgumentError gmsh_import()
269+
gmsh.clear()
270+
@test_throws ArgumentError gmsh_import()
169271
end
170272
finally
171273
gmsh.finalize()
172274
end
173275
end
276+
277+
@testset "Gmsh import from a .geo file (gmsh not initialized)" begin
278+
# gmsh was finalized above, so gmsh_import(path) owns the
279+
# initialize/finalize lifecycle here, and a .geo file forces the
280+
# mesh-generation branch.
281+
@test_throws ArgumentError gmsh_import("no_such_file_xyz.msh")
282+
mktempdir() do dir
283+
geopath = joinpath(dir, "square.geo")
284+
write(geopath, """
285+
Point(1) = {-1, -1, 0, 0.6};
286+
Point(2) = {1, -1, 0, 0.6};
287+
Point(3) = {1, 1, 0, 0.6};
288+
Point(4) = {-1, 1, 0, 0.6};
289+
Line(1) = {1, 2};
290+
Line(2) = {2, 3};
291+
Line(3) = {3, 4};
292+
Line(4) = {4, 1};
293+
Curve Loop(5) = {1, 2, 3, 4};
294+
Plane Surface(6) = {5};
295+
Physical Curve("boundary") = {1, 2, 3, 4};
296+
Physical Surface("domain") = {6};
297+
""")
298+
gm = gmsh_import(geopath)
299+
@test typeof(gm.geometry.discretization) <: FEM2D_P1
300+
V, N = size(gm.geometry.x, 1), size(gm.geometry.x, 2)
301+
@test length(gm.regions["boundary"]) > 4
302+
# the whole-domain group contains every (vertex, element) pair
303+
@test sort(gm.regions["domain"]) == sort([(v, e) for e in 1:N for v in 1:V])
304+
end
305+
end

test/test_jump.jl

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
219409
end

0 commit comments

Comments
 (0)