Skip to content

Commit 6f01337

Browse files
Sébastien LoiselSébastien Loisel
authored andcommitted
JuMP front end: make On(geom, mask) eager, dropping the region union
On stored its region as Union{Vector{Tuple{Int,Int}},Vector{Bool}} and resolved a Bool mask to (vertex, element) pairs lazily at add_constraint time, because On(mask) alone lacked the geometry's V. That deferral was the front end's one non-eager corner. Take the geometry explicitly instead -- On(geom, mask) converts to pairs at construction -- so On.pairs is a single concrete field again and the extension's deferred-resolution path (_region_pairs, the unresolved RegionConstraint) is gone. Mask length errors now surface at the On(...) call rather than inside @constraint.
1 parent 7c8fad1 commit 6f01337

4 files changed

Lines changed: 43 additions & 52 deletions

File tree

docs/src/jump.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ accepts it directly:
8585
- `Coef(m, vals::AbstractVector)`
8686
- `set_start(u, vals::AbstractVector)`
8787
- `EpiPower(pvals::AbstractVector)`
88-
- `On(mask::AbstractVector{Bool})` — region membership, one Bool per node
88+
- `On(geom, mask::AbstractVector{Bool})` — region membership, one Bool per node
8989

9090
Node `i` is vertex `v` of element `e` with `i = v + (e-1)V` where
9191
`V = size(geom.x, 1)`: the rows of `reshape(geom.x, :, d)` are the node
@@ -109,13 +109,12 @@ say) drops in directly, without wrapping it in an interpolating closure.
109109

110110
## Regions: constraints on part of the domain
111111

112-
A constraint holds everywhere by default; adding `On(region)` restricts it to
113-
a node set. The region is either a Bool mask over the nodal vectors (one
114-
entry per broken node, in the ordering of
115-
[the data model](@ref jump-data-model)) or a vector of `(vertex, element)`
116-
pairs — the format of [`find_boundary`](@ref) and the low-level
117-
`dirichlet_nodes` API; a mask is sugar that resolves to the pair set when the
118-
constraint is added. Equality + `On` is a Dirichlet condition;
112+
A constraint holds everywhere by default; adding `On(...)` restricts it to a
113+
node set of `(vertex, element)` pairs — the format of [`find_boundary`](@ref)
114+
and the low-level `dirichlet_nodes` API. `On(geom, mask)` is grid-level sugar
115+
for the same thing: a Bool mask over the nodal vectors (one entry per broken
116+
node, in the ordering of [the data model](@ref jump-data-model)), converted
117+
eagerly to the pair set. Equality + `On` is a Dirichlet condition;
119118
inequality/cone + `On` becomes a piecewise barrier, active only on the
120119
region. Region *selection* is ordinary data preparation — a comparison on the
121120
node coordinates gives the mask.
@@ -133,7 +132,7 @@ set_attribute(m2, "verbose", false)
133132
set_start(s2, 100.0)
134133
@constraint(m2, u2 == Coef(m2, 0.0), On(find_boundary(geom2)))
135134
@constraint(m2, [deriv(u2, :dx); deriv(u2, :dy); s2] in EpiPower(2.0))
136-
@constraint(m2, u2 >= Coef(m2, x -> 0.25 - x[1]^2 - x[2]^2), On(left))
135+
@constraint(m2, u2 >= Coef(m2, x -> 0.25 - x[1]^2 - x[2]^2), On(geom2, left))
137136
@objective(m2, Min, integral(Coef(m2, -1.0) * u2 + s2))
138137
optimize!(m2)
139138
plot(mgb_solution(m2)); savefig("jump_obstacle.svg"); nothing # hide

ext/MultiGridBarrierJuMPExt/MultiGridBarrierJuMPExt.jl

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -389,32 +389,17 @@ end
389389
MOI.dimension(s::MOIEpiPower) = s.dim
390390
JuMP.moi_set(s::EpiPower, dim::Int) = MOIEpiPower(s.p, dim)
391391

392-
# region-restricted constraint: wrap the underlying constraint. Carries the On
393-
# unresolved because a Bool mask needs the geometry (V) to become pairs, and
394-
# build_constraint has no model access; add_constraint resolves it.
392+
# region-restricted constraint: wrap the underlying constraint. On is already
393+
# resolved to (vertex, element) pairs (masks convert eagerly in On(geom, mask)).
395394
struct RegionConstraint{C} <: JuMP.AbstractConstraint
396395
con::C
397-
region::On
396+
pairs::Vector{Tuple{Int,Int}}
398397
end
399398
JuMP.jump_function(rc::RegionConstraint) = JuMP.jump_function(rc.con)
400399
JuMP.moi_set(rc::RegionConstraint) = JuMP.moi_set(rc.con)
401400

402-
# resolve an On region to (vertex, element) pairs — the package's canonical
403-
# node-set format (find_boundary, dirichlet_nodes). A Bool mask is grid-level
404-
# sugar for it: mask entry i = v + (e-1)V selects vertex v of element e.
405-
function _region_pairs(m::MGBModel, on::On)
406-
r = on.region
407-
r isa Vector{Bool} || return r
408-
length(r) == m.nnodes || _argerror(
409-
"region mask has length $(length(r)) but the geometry has $(m.nnodes) " *
410-
"broken nodes; entry i is vertex v of element e with i = v + (e-1)V, " *
411-
"V = $(size(m.geometry.x, 1))")
412-
V = size(m.geometry.x, 1)
413-
Tuple{Int,Int}[(mod1(i, V), cld(i, V)) for i in findall(r)]
414-
end
415-
416401
JuMP.build_constraint(err::Function, f, set, on::On) =
417-
RegionConstraint(JuMP.build_constraint(err, f, set), on)
402+
RegionConstraint(JuMP.build_constraint(err, f, set), on.pairs)
418403

419404
# JuMP's macro maps generic scalar comparisons (non-Number rhs) to its
420405
# Zeros/Nonnegatives/Nonpositives shortcut sets through VARIADIC
@@ -424,19 +409,19 @@ function JuMP.build_constraint(err::Function, f, set::JuMP.Zeros, on::On)
424409
inner = f isa AbstractVector ?
425410
JuMP.build_constraint(err, f, MOI.Zeros(length(f))) :
426411
JuMP.build_constraint(err, f, MOI.EqualTo(0.0))
427-
RegionConstraint(inner, on)
412+
RegionConstraint(inner, on.pairs)
428413
end
429414
function JuMP.build_constraint(err::Function, f, set::JuMP.Nonnegatives, on::On)
430415
inner = f isa AbstractVector ?
431416
JuMP.build_constraint(err, f, MOI.Nonnegatives(length(f))) :
432417
JuMP.build_constraint(err, f, MOI.GreaterThan(0.0))
433-
RegionConstraint(inner, on)
418+
RegionConstraint(inner, on.pairs)
434419
end
435420
function JuMP.build_constraint(err::Function, f, set::JuMP.Nonpositives, on::On)
436421
inner = f isa AbstractVector ?
437422
JuMP.build_constraint(err, f, MOI.Nonpositives(length(f))) :
438423
JuMP.build_constraint(err, f, MOI.LessThan(0.0))
439-
RegionConstraint(inner, on)
424+
RegionConstraint(inner, on.pairs)
440425
end
441426
# Newer JuMP versions route scalar comparisons with non-Number sides through
442427
# internal *Zero marker sets, again variadically; cover them when they exist.
@@ -446,7 +431,7 @@ for (marker, moiset) in ((:GreaterThanZero, :(MOI.GreaterThan(0.0))),
446431
if isdefined(JuMP, marker)
447432
@eval function JuMP.build_constraint(err::Function, f,
448433
set::JuMP.$marker, on::On)
449-
RegionConstraint(JuMP.build_constraint(err, f, $moiset), on)
434+
RegionConstraint(JuMP.build_constraint(err, f, $moiset), on.pairs)
450435
end
451436
end
452437
end
@@ -553,11 +538,10 @@ function JuMP.add_constraint(m::MGBModel, c::JuMP.VectorConstraint,
553538
end
554539
function JuMP.add_constraint(m::MGBModel, rc::RegionConstraint, name::String = "")
555540
c = rc.con
556-
pairs = _region_pairs(m, rc.region)
557541
if c isa JuMP.ScalarConstraint
558-
_add_scalar(m, JuMP.jump_function(c), JuMP.moi_set(c), pairs, name)
542+
_add_scalar(m, JuMP.jump_function(c), JuMP.moi_set(c), rc.pairs, name)
559543
elseif c isa JuMP.VectorConstraint
560-
_add_vector(m, JuMP.jump_function(c), JuMP.moi_set(c), pairs, name)
544+
_add_vector(m, JuMP.jump_function(c), JuMP.moi_set(c), rc.pairs, name)
561545
else
562546
_argerror("unsupported constraint type $(typeof(c)) with On(...)")
563547
end

src/jump_frontend.jl

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,30 @@ function solver_log end
106106

107107
"""
108108
On(pairs::Vector{Tuple{Int,Int}})
109-
On(mask::AbstractVector{Bool})
109+
On(geom::Geometry, mask::AbstractVector{Bool})
110110
111-
Constraint region for a [`MGBModel`](@ref): a set of broken nodes, as
111+
Constraint region for a [`MGBModel`](@ref): a set of broken nodes as
112112
`(vertex, element)` pairs — the same format as [`find_boundary`](@ref) and the
113-
low-level `dirichlet_nodes` API — or as a Bool mask over the nodal vectors of
114-
[`Coef`](@ref), where `mask[v + (e-1)V]` selects vertex `v` of element `e`
115-
(the mask is resolved to the pair set when the constraint is added). The mask
116-
form composes directly with grid-level data: `On(reshape(geom.x, :, d)[:, 1] .< 0)`
117-
is the left half of the domain. `@constraint(m, u == g, On(pairs))` is a
118-
Dirichlet condition; an inequality/cone with `On` holds only on those nodes.
113+
low-level `dirichlet_nodes` API. The second form is grid-level sugar: a Bool
114+
mask over the nodal vectors of [`Coef`](@ref), converted eagerly to the pair
115+
set (`mask[v + (e-1)V]` selects vertex `v` of element `e`; the geometry
116+
supplies `V`). A mask composes directly with grid-level data:
117+
`On(geom, reshape(geom.x, :, d)[:, 1] .< 0)` is the left half of the domain.
118+
`@constraint(m, u == g, On(pairs))` is a Dirichlet condition; an
119+
inequality/cone with `On` holds only on those nodes.
119120
"""
120121
struct On
121-
region::Union{Vector{Tuple{Int,Int}},Vector{Bool}}
122+
pairs::Vector{Tuple{Int,Int}}
122123
On(pairs::AbstractVector{<:Tuple{Integer,Integer}}) =
123124
new(Tuple{Int,Int}[(Int(v), Int(e)) for (v, e) in pairs])
124-
On(mask::AbstractVector{Bool}) = new(collect(Bool, mask))
125+
end
126+
function On(geom::Geometry, mask::AbstractVector{Bool})
127+
V, N = size(geom.x, 1), size(geom.x, 2)
128+
length(mask) == V * N || throw(ArgumentError(
129+
"region mask has length $(length(mask)) but the geometry has $(V * N) " *
130+
"broken nodes; entry i is vertex v of element e with i = v + (e-1)V, " *
131+
"V = $V"))
132+
On(Tuple{Int,Int}[(mod1(i, V), cld(i, V)) for i in findall(mask)])
125133
end
126134

127135
"""

test/test_jump.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,18 @@ _maxdiff(ref, cols...) =
146146
left = [(v, e) for e in 1:Nn for v in 1:Vn if geom3.x[v, e, 1] < 0]
147147
mask = reshape(geom3.x, :, 2)[:, 1] .< 0
148148

149-
function obstacle(region)
149+
function obstacle(region::On)
150150
m = MGBModel(geom3); _quiet!(m)
151151
@variable(m, u); @variable(m, s, Broken())
152152
set_start(s, 100.0)
153153
@constraint(m, u == Coef(m, 0.0), On(find_boundary(geom3)))
154154
@constraint(m, [deriv(u, :dx); deriv(u, :dy); s] in EpiPower(2.0))
155-
@constraint(m, u >= Coef(m, x -> 0.25 - x[1]^2 - x[2]^2), On(region))
155+
@constraint(m, u >= Coef(m, x -> 0.25 - x[1]^2 - x[2]^2), region)
156156
@objective(m, Min, integral(Coef(m, -1.0) * u + s))
157157
optimize!(m)
158158
m, u
159159
end
160-
m, u = obstacle(left)
160+
m, u = obstacle(On(left))
161161
@test termination_status(m) == JuMP.MOI.LOCALLY_SOLVED
162162
@test mgb_solution(m).SOL_feasibility !== nothing # phase 1 ran
163163
phi = value(Coef(m, x -> 0.25 - x[1]^2 - x[2]^2))
@@ -167,11 +167,11 @@ _maxdiff(ref, cols...) =
167167
@test count(<(1e-4), gapL) > 0 # ... with actual contact
168168
@test minimum(zu[.!mask] .- phi[.!mask]) < 0 # ... and is absent off it
169169

170-
# the mask is sugar for the same node set
171-
m2, u2 = obstacle(mask)
170+
# On(geom, mask) is eager sugar for the same node set
171+
@test On(geom3, mask).pairs == On(left).pairs
172+
m2, u2 = obstacle(On(geom3, mask))
172173
@test value(u2) == zu
173-
@test_throws ArgumentError @constraint(m2, u2 >= Coef(m2, 0.0),
174-
On(trues(5)))
174+
@test_throws ArgumentError On(geom3, trues(5))
175175
end
176176

177177
@testset "nodal vectors are the fundamental data form" begin

0 commit comments

Comments
 (0)