Skip to content

Commit 2b02297

Browse files
Sébastien LoiselSébastien Loisel
authored andcommitted
Add JuMP modeling front end (draft) with Zoo cross-validation and docs tab
- jump/MultiGridBarrierJuMP.jl: JuMP.AbstractModel extension. Standard @variable/@constraint/@objective syntax lowers directly to amg -> assemble -> mgb_solve (no MOI model); the AMG hierarchy is built automatically from the geometry and the Dirichlet constraints. Vocabulary: Coef (eager spatial data), deriv(u, :dx), EpiPower(p) with slack last, On(pairs) regions (equality -> dirichlet_nodes mask+lift, inequality -> convex_piecewise selector), Broken()/Uniform() variable tags. - jump/test_zoo.jl: all six Zoo problems restated in JuMP syntax match the classical constructors component-wise (five bit-identically, norton_hoff to one ulp). - jump/demo.jl: p-Laplacian cross-check (bit-identical) and a region-restricted obstacle exercising the feasibility phase. - jump/README.md: modeling reference, lowering semantics, version notes. - docs: new JuMP tab with live examples in the style of the Zoo page (cross-checks render 0.0 at build time); JuMP added to the docs environment; pointer from the front-end summary on the home page.
1 parent abe136a commit 2b02297

8 files changed

Lines changed: 1636 additions & 1 deletion

File tree

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[deps]
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
3+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
34
MultiGridBarrier = "9e2c1f1d-9131-4ad4-b32f-bd2a0b0ecd1e"
45
PyPlot = "d330b81b-6aea-500a-939a-2ce795aea3ee"

docs/make.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ Pkg.develop(PackageSpec(; path=(@__DIR__) * "/../"))
66
using MultiGridBarrier
77
using Documenter
88
using PyPlot
9+
using JuMP
10+
11+
# The JuMP front end is not (yet) a package; load it here so its docstrings
12+
# are available to the @autodocs block in jump.md.
13+
include(joinpath(@__DIR__, "..", "jump", "MultiGridBarrierJuMP.jl"))
14+
using .MultiGridBarrierJuMP
915

1016
DocMeta.setdocmeta!(MultiGridBarrier, :DocTestSetup, :(using MultiGridBarrier); recursive=true)
1117

1218
makedocs(;
13-
modules=[MultiGridBarrier],
19+
modules=[MultiGridBarrier, MultiGridBarrierJuMP],
1420
authors="Sébastien Loisel",
1521
sitename="MultiGridBarrier.jl $(pkgversion(MultiGridBarrier))",
1622
warnonly = [:missing_docs, :cross_references, :docs_block],
@@ -23,6 +29,7 @@ makedocs(;
2329
pages=[
2430
"Home" => "index.md",
2531
"Zoo" => "zoo.md",
32+
"JuMP" => "jump.md",
2633
],
2734
)
2835

docs/src/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,10 @@ multigrid hierarchy on the fine mesh. To refine the mesh first, compose with
252252
The legacy `geometric_mg(geom, L)` builds a geometric-subdivision hierarchy instead of
253253
AMG; it remains available for callers that specifically want geometric transfers.
254254

255+
Prefer stating problems in an algebraic modeling language? The
256+
[JuMP front end](jump.md) accepts standard `@variable`/`@constraint`/`@objective`
257+
syntax and lowers it to this same pipeline, building the hierarchy automatically.
258+
255259
### Meshes, coordinates, and connectivity
256260

257261
Fundamentally a mesh — and the `Geometry` that holds it — is a pair `(t, x)`:

docs/src/jump.md

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
```@meta
2+
CurrentModule = MultiGridBarrier
3+
```
4+
5+
# Modeling with JuMP
6+
7+
`MultiGridBarrierJuMP` lets you state convex variational problems in
8+
[JuMP](https://jump.dev) syntax and solve them with the MultiGridBarrier
9+
multigrid interior-point method. It is a `JuMP.AbstractModel` extension: the
10+
standard macros (`@variable`, `@constraint`, `@objective`) and accessors
11+
(`value`, `objective_value`, `termination_status`) work unchanged, but no MOI
12+
model is ever built — `optimize!` lowers the model directly to the classical
13+
pipeline `amg``assemble``mgb_solve`. The AMG hierarchy is constructed
14+
automatically from the geometry and the Dirichlet constraints; it is never
15+
user-visible.
16+
17+
!!! note "Status"
18+
Working draft, not yet a registered package. It ships in the `jump/`
19+
directory of the repository and is loaded with `include`. All six
20+
[Zoo](zoo.md) problems restated in JuMP syntax reproduce the classical
21+
constructors' solutions bit-for-bit (or to one ulp); the regression suite
22+
is `jump/test_zoo.jl`, and the full modeling reference is
23+
[`jump/README.md`](https://github.com/sloisel/MultiGridBarrier.jl/tree/main/jump).
24+
25+
## Setup
26+
27+
JuMP is not a dependency of MultiGridBarrier — add it to your environment
28+
(`pkg> add JuMP`), then:
29+
30+
```@example jump
31+
using MultiGridBarrier, JuMP, PyPlot
32+
include(joinpath(dirname(dirname(pathof(MultiGridBarrier))),
33+
"jump", "MultiGridBarrierJuMP.jl"))
34+
using .MultiGridBarrierJuMP
35+
nothing # hide
36+
```
37+
38+
## Quick tour: the p-Laplacian
39+
40+
```math
41+
\min \int \tfrac{1}{2} u + s \, dx
42+
\quad \text{s.t.} \quad s \geq \|\nabla u\|^{1.5},
43+
\qquad u = x_1^2 + x_2^2 \text{ on } \partial\Omega .
44+
```
45+
46+
A model is built over a fixed discretization, so every piece of spatial data
47+
(`Coef`) is evaluated at the quadrature nodes at modeling time. Derivatives
48+
are written `deriv(u, :dx)` where the symbol is a key of `geom.operators`;
49+
the epigraph cone `[q...; slack] in EpiPower(p)` means
50+
`slack ≥ ‖q‖₂ᵖ` pointwise (slack **last**).
51+
52+
```@example jump
53+
geom = subdivide(fem2d_P2(), 2)
54+
m = MGBModel(geom)
55+
set_attribute(m, "verbose", false)
56+
@variable(m, u) # conforming (inferred)
57+
@variable(m, s, Broken()) # broken slack: one dof per node
58+
set_start(u, x -> x[1]^2 + x[2]^2) # initial iterate & Dirichlet lift
59+
set_start(s, 100.0)
60+
@constraint(m, u == Coef(m, x -> x[1]^2 + x[2]^2), On(find_boundary(geom)))
61+
@constraint(m, [deriv(u, :dx); deriv(u, :dy); s] in EpiPower(1.5))
62+
@objective(m, Min, integral(Coef(m, 0.5) * u + s))
63+
optimize!(m)
64+
termination_status(m)
65+
```
66+
67+
```@example jump
68+
plot(mgb_solution(m)); savefig("jump_plaplace.svg"); nothing # hide
69+
close() # hide
70+
```
71+
![](jump_plaplace.svg)
72+
73+
This is exactly the package's default problem, so we can compare against the
74+
classical API on the same geometry. The lowering produces the identical
75+
discrete problem, so the solutions agree bit-for-bit:
76+
77+
```@example jump
78+
sol_ref = mgb_solve(assemble(amg(geom); p = 1.5); verbose = false)
79+
maximum(abs.(value(u) .- sol_ref.z[:, 1]))
80+
```
81+
82+
## Regions: constraints on part of the domain
83+
84+
A constraint holds everywhere by default; adding `On(pairs)` restricts it to
85+
a node set given as `(vertex, element)` pairs — the same format as
86+
[`find_boundary`](@ref) and the low-level `dirichlet_nodes` API. Equality +
87+
`On` is a Dirichlet condition; inequality/cone + `On` becomes a piecewise
88+
barrier, active only on the region. Region *selection* is ordinary data
89+
preparation — build the pairs with a comprehension.
90+
91+
Here is a membrane pushed upward by a uniform load, with an obstacle imposed
92+
only on the left half of the domain:
93+
94+
```@example jump
95+
geom2 = subdivide(fem2d_P2(), 3)
96+
Vn, Nn = size(geom2.x, 1), size(geom2.x, 2)
97+
left = [(v, e) for e in 1:Nn for v in 1:Vn if geom2.x[v, e, 1] < 0]
98+
99+
m2 = MGBModel(geom2)
100+
set_attribute(m2, "verbose", false)
101+
@variable(m2, u2); @variable(m2, s2, Broken())
102+
set_start(s2, 100.0)
103+
@constraint(m2, u2 == Coef(m2, 0.0), On(find_boundary(geom2)))
104+
@constraint(m2, [deriv(u2, :dx); deriv(u2, :dy); s2] in EpiPower(2.0))
105+
@constraint(m2, u2 >= Coef(m2, x -> 0.25 - x[1]^2 - x[2]^2), On(left))
106+
@objective(m2, Min, integral(Coef(m2, -1.0) * u2 + s2))
107+
optimize!(m2)
108+
plot(mgb_solution(m2)); savefig("jump_obstacle.svg"); nothing # hide
109+
close() # hide
110+
```
111+
![](jump_obstacle.svg)
112+
113+
The obstacle binds on its region (the infeasible start is handled by the
114+
feasibility phase automatically) and is genuinely absent elsewhere:
115+
116+
```@example jump
117+
phi = value(Coef(m2, x -> 0.25 - x[1]^2 - x[2]^2))
118+
lin = [v + (e - 1) * Vn for (v, e) in left]
119+
rgt = setdiff(1:length(phi), lin)
120+
println("min(u - φ) on the obstacle region: ", minimum(value(u2)[lin] .- phi[lin]))
121+
println("min(u - φ) off the region: ", minimum(value(u2)[rgt] .- phi[rgt]))
122+
```
123+
124+
## The Zoo, restated in JuMP
125+
126+
Every [Zoo](zoo.md) problem is a few lines in this syntax; `jump/test_zoo.jl`
127+
checks all six against the classical constructors. Two examples. The minimal
128+
surface uses a *constant row* inside the cone — `s ≥ ‖(∇u, 1)‖` is the
129+
shifted Lorentz cone:
130+
131+
```@example jump
132+
gu = x -> 0.5 * (x[1]^2 - x[2]^2)
133+
ms = MGBModel(geom)
134+
set_attribute(ms, "verbose", false)
135+
@variable(ms, v); @variable(ms, sv, Broken())
136+
set_start(v, gu); set_start(sv, 10.0)
137+
@constraint(ms, v == Coef(ms, gu), On(find_boundary(geom)))
138+
@constraint(ms, [deriv(v, :dx); deriv(v, :dy); Coef(ms, 1.0); sv] in EpiPower(1.0))
139+
@objective(ms, Min, integral(1.0 * sv))
140+
optimize!(ms)
141+
ref = mgb_solve(Zoo.minimal_surface(amg(geom)); verbose = false)
142+
maximum(abs.(value(v) .- ref.z[:, 1]))
143+
```
144+
145+
Rudin–Osher–Fatemi denoising uses spatial *data inside a cone* — the
146+
fidelity slack is `r ≥ (u - f_data)²`:
147+
148+
```@example jump
149+
fdata = x -> 0.5 * tanh(5 * x[1])
150+
mr = MGBModel(geom)
151+
set_attribute(mr, "verbose", false)
152+
@variable(mr, w); @variable(mr, sw, Broken()); @variable(mr, r, Broken())
153+
set_start(w, fdata); set_start(sw, 10.0); set_start(r, 10.0)
154+
fd = Coef(mr, fdata)
155+
@constraint(mr, w == fd, On(find_boundary(geom)))
156+
@constraint(mr, [deriv(w, :dx); deriv(w, :dy); sw] in EpiPower(1.0)) # s ≥ |∇u|
157+
@constraint(mr, [w - fd; r] in EpiPower(2.0)) # r ≥ (u-f)²
158+
@objective(mr, Min, integral(sw + Coef(mr, 0.5) * r))
159+
optimize!(mr)
160+
ref = mgb_solve(Zoo.rof(amg(geom)); verbose = false)
161+
maximum(abs.(value(w) .- ref.z[:, 1]))
162+
```
163+
164+
## What lowers to what
165+
166+
| model content | classical object |
167+
|---|---|
168+
| geometry passed to `MGBModel` | `Geometry` |
169+
| variables, kinds, Dirichlet constraints | `state_variables` + `dirichlet_nodes``amg(geom; dirichlet_nodes)` |
170+
| distinct atoms `(component, operator)` used anywhere | the `D` table |
171+
| each cone constraint | one `Convex` piece (`convex_linear` / `convex_Euclidian_power`) |
172+
| `On` regions on cones | `convex_piecewise` selector columns |
173+
| `integral(...)` objective | the cost grid `f_grid` |
174+
| starts + Dirichlet data | the initial/lift grid `g_grid` |
175+
176+
Untagged variables are conforming if differentiated or Dirichlet-constrained
177+
and broken otherwise; `Broken()` / `Uniform()` override. The model must be in
178+
conic form (epigraph slacks are yours to declare, as with any conic solver);
179+
pointwise equality requires `On`; variable bounds and products of variable
180+
expressions are rejected with explanatory errors. `dual` and spectral
181+
geometries are not wired up yet.
182+
183+
## Module reference
184+
185+
```@autodocs
186+
Modules = [Main.MultiGridBarrierJuMP]
187+
Order = [:type, :function]
188+
```

0 commit comments

Comments
 (0)