Skip to content

Commit 8342177

Browse files
authored
Merge pull request #10 from Robbybp/subtype-conref
2 parents c3f1f0d + c6c5e92 commit 8342177

File tree

6 files changed

+161
-19
lines changed

6 files changed

+161
-19
lines changed

src/identify_variables.jl

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ julia> display(vars)
6161
6262
"""
6363
function identify_unique_variables(
64-
constraints::Vector,
65-
# FIXME: Couldn't get this working with Vector{ConstraintRef}...
64+
constraints::Vector{<:JuMP.ConstraintRef},
6665
)::Vector{JuMP.VariableRef}
6766
variables = Vector{JuMP.VariableRef}()
6867
for con in constraints

src/incidence_graph.jl

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ import JuMP
2727

2828
import JuMPIn: get_equality_constraints, identify_unique_variables
2929

30+
const GraphDataTuple = Tuple{
31+
# (A, B, E) describing the bipartite graph
32+
Tuple{Vector{Int}, Vector{Int}, Vector{Tuple{Int, Int}}},
33+
# Map from constraints to nodes (in A)
34+
Dict{JuMP.ConstraintRef, Int},
35+
# Map from variables to nodes (in B)
36+
Dict{JuMP.VariableRef, Int},
37+
}
38+
3039
"""
3140
get_bipartite_incidence_graph(model, include_inequality = false)
3241
@@ -85,7 +94,7 @@ regardless of which variables participate in the constraints.
8594
function get_bipartite_incidence_graph(
8695
model::JuMP.Model;
8796
include_inequality::Bool = false,
88-
)
97+
)::GraphDataTuple
8998
if include_inequality
9099
# Note that this may generate some constraints that are incompatible
91100
# with downstream function calls (e.g. constraints involving vector
@@ -109,17 +118,19 @@ function get_bipartite_incidence_graph(
109118
return get_bipartite_incidence_graph(constraints)
110119
end
111120

112-
function get_bipartite_incidence_graph(constraints::Vector{JuMP.ConstraintRef})
121+
function get_bipartite_incidence_graph(
122+
constraints::Vector{<:JuMP.ConstraintRef}
123+
)::GraphDataTuple
113124
variables = identify_unique_variables(constraints)
114125
# We could build up a variable-index map dynamically to get the incidence
115126
# in a single loop over the constraints, but this is easier to implement.
116127
return get_bipartite_incidence_graph(constraints, variables)
117128
end
118129

119130
function get_bipartite_incidence_graph(
120-
constraints::Vector{JuMP.ConstraintRef},
131+
constraints::Vector{<:JuMP.ConstraintRef},
121132
variables::Vector{JuMP.VariableRef},
122-
)
133+
)::GraphDataTuple
123134
ncon = length(constraints)
124135
nvar = length(variables)
125136
# Note the convention we apply: Constraints take the first 1:ncon

src/interface.jl

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ A JuMP interface to the algorithms implemented by JuMPIn
2424

2525
import JuMP
2626

27-
import JuMPIn: get_bipartite_incidence_graph, maximum_matching
27+
import JuMPIn: get_bipartite_incidence_graph, maximum_matching, GraphDataTuple
2828

2929
import Graphs as gjl
3030
import BipartiteMatching as bpm
@@ -101,7 +101,7 @@ end
101101

102102

103103
IncidenceGraphInterface(
104-
args::Tuple{Tuple, Dict, Dict}
104+
args::GraphDataTuple
105105
) = IncidenceGraphInterface(
106106
_tuple_to_graphs_jl(args[1]),
107107
args[2],
@@ -119,7 +119,7 @@ IncidenceGraphInterface(
119119

120120

121121
IncidenceGraphInterface(
122-
constraints::Vector{JuMP.ConstraintRef},
122+
constraints::Vector{<:JuMP.ConstraintRef},
123123
variables::Vector{JuMP.VariableRef},
124124
) = IncidenceGraphInterface(
125125
get_bipartite_incidence_graph(constraints, variables)
@@ -138,7 +138,7 @@ Return the variables adjacent to a constraint in an incidence graph.
138138
function get_adjacent(
139139
igraph::IncidenceGraphInterface,
140140
constraint::JuMP.ConstraintRef,
141-
)
141+
)::Vector{JuMP.VariableRef}
142142
con_node = igraph._con_node_map[constraint]
143143
var_nodes = gjl.neighbors(igraph._graph, con_node)
144144
variables = [igraph._nodes[n] for n in var_nodes]
@@ -165,7 +165,7 @@ julia> @NLconstraint(m, eq_2, v[1]*v[2]^3 == 2);
165165
julia> igraph = ji.IncidenceGraphInterface(m);
166166
julia> adj_cons = ji.get_adjacent(igraph, v[1]);
167167
julia> display(adj_cons)
168-
2-element Vector{ConstraintRef{Model, C, ScalarShape} where C}:
168+
2-element Vector{ConstraintRef}:
169169
eq_1 : v[1] + v[3] = 1.0
170170
v[1] * v[2] ^ 3.0 - 2.0 = 0
171171
```
@@ -174,7 +174,7 @@ julia> display(adj_cons)
174174
function get_adjacent(
175175
igraph::IncidenceGraphInterface,
176176
variable::JuMP.VariableRef,
177-
)
177+
)::Vector{JuMP.ConstraintRef}
178178
var_node = igraph._var_node_map[variable]
179179
con_nodes = gjl.neighbors(igraph._graph, var_node)
180180
constraints = [igraph._nodes[n] for n in con_nodes]
@@ -200,7 +200,7 @@ julia> @NLconstraint(m, eq_2, v[1]*v[2]^3 == 2);
200200
julia> igraph = ji.IncidenceGraphInterface(m);
201201
julia> matching = ji.maximum_matching(igraph);
202202
julia> display(matching)
203-
Dict{ConstraintRef{Model, C, ScalarShape} where C, VariableRef} with 2 entries:
203+
Dict{ConstraintRef, VariableRef} with 2 entries:
204204
v[1] * v[2] ^ 3.0 - 2.0 = 0 => v[2]
205205
eq_1 : v[1] + v[3] = 1.0 => v[1]
206206
```
@@ -220,13 +220,32 @@ end
220220

221221

222222
function maximum_matching(
223-
constraints::Vector{JuMP.ConstraintRef},
223+
constraints::Vector{<:JuMP.ConstraintRef},
224224
variables::Vector{JuMP.VariableRef},
225225
)::Dict{JuMP.ConstraintRef, JuMP.VariableRef}
226226
igraph = IncidenceGraphInterface(constraints, variables)
227227
return maximum_matching(igraph)
228228
end
229229

230+
const DMConPartition = NamedTuple{
231+
(:underconstrained, :square, :overconstrained, :unmatched),
232+
Tuple{
233+
Vector{JuMP.ConstraintRef},
234+
Vector{JuMP.ConstraintRef},
235+
Vector{JuMP.ConstraintRef},
236+
Vector{JuMP.ConstraintRef},
237+
},
238+
}
239+
240+
const DMVarPartition = NamedTuple{
241+
(:unmatched, :underconstrained, :square, :overconstrained),
242+
Tuple{
243+
Vector{JuMP.VariableRef},
244+
Vector{JuMP.VariableRef},
245+
Vector{JuMP.VariableRef},
246+
Vector{JuMP.VariableRef},
247+
},
248+
}
230249

231250
"""
232251
dulmage_mendelsohn(igraph::IncidenceGraphInterface)
@@ -294,21 +313,23 @@ julia> display(var_dmp.underconstrained)
294313
v[1]
295314
v[2]
296315
julia> display(con_dmp.underconstrained)
297-
2-element Vector{ConstraintRef{Model, C, ScalarShape} where C}:
316+
2-element Vector{ConstraintRef}:
298317
eq_1 : v[1] + v[3] = 1.0
299318
v[1] * v[2] ^ 3.0 - 2.0 = 0
300319
julia> display(var_dmp.square)
301320
1-element Vector{VariableRef}:
302321
v[4]
303322
julia> display(con_dmp.square)
304-
1-element Vector{ConstraintRef{Model, MathOptInterface.ConstraintIndex{MathOptInterface.ScalarQuadraticFunction{Float64}, MathOptInterface.EqualTo{Float64}}, ScalarShape}}:
323+
1-element Vector{ConstraintRef}:
305324
eq_3 : v[4]² = 3.0
306325
julia> # As there are no unmatched constraints, the overconstrained subsystem
307326
julia> # is empty.
308327
```
309328
310329
"""
311-
function dulmage_mendelsohn(igraph::IncidenceGraphInterface)
330+
function dulmage_mendelsohn(
331+
igraph::IncidenceGraphInterface
332+
)::Tuple{DMConPartition, DMVarPartition}
312333
ncon = length(igraph._con_node_map)
313334
con_node_set = Set(1:ncon)
314335
con_dmp, var_dmp = dulmage_mendelsohn(igraph._graph, con_node_set)
@@ -332,9 +353,9 @@ end
332353

333354

334355
function dulmage_mendelsohn(
335-
constraints::Vector{JuMP.ConstraintRef},
356+
constraints::Vector{<:JuMP.ConstraintRef},
336357
variables::Vector{JuMP.VariableRef},
337-
)
358+
)::Tuple{DMConPartition, DMVarPartition}
338359
igraph = IncidenceGraphInterface(constraints, variables)
339360
return dulmage_mendelsohn(igraph)
340361
end

test/identify_variables.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,17 @@ function test_inequality_with_bounds()
199199
@test Set(variables) == pred_var_set
200200
end
201201

202+
function test_two_constraints_same_type()
203+
m = jmp.Model()
204+
@jmp.variable(m, x[1:3])
205+
@jmp.constraint(m, eq1, x[1] + x[2] == 2)
206+
@jmp.constraint(m, eq2, x[2] + 2*x[3] == 3)
207+
cons = [eq1, eq2]
208+
vars = identify_unique_variables(cons)
209+
pred_var_set = Set([x[1], x[2], x[3]])
210+
@test Set(vars) == pred_var_set
211+
end
212+
202213
function runtests()
203214
test_linear()
204215
test_quadratic()
@@ -213,6 +224,7 @@ function runtests()
213224
test_function_with_variable_squared()
214225
test_fixing_constraint()
215226
test_inequality_with_bounds()
227+
test_two_constraints_same_type()
216228
return
217229
end
218230

test/incidence_graph.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,61 @@ function test_include_bound_as_inequality()
106106
@test length(E) == 4
107107
pred_con_set = Set([eq1, jmp.LowerBoundRef(x[1]), jmp.LowerBoundRef(x[2])])
108108
@test pred_con_set == keys(con_node_map)
109+
return
110+
end
111+
112+
function test_construct_from_constraints()
113+
m = jmp.Model()
114+
@jmp.variable(m, x[1:3])
115+
@jmp.constraint(m, eq1, x[1] + 2*x[2] == 1)
116+
@jmp.constraint(m, eq2, x[2]*x[3] == 0.5)
117+
cons = [eq1, eq2]
118+
graph, con_node_map, var_node_map = get_bipartite_incidence_graph(cons)
119+
A, B, E = graph
120+
121+
@test con_node_map[eq1] == 1
122+
@test con_node_map[eq2] == 2
123+
124+
predicted_edges = [(eq1, x[1]), (eq1, x[2]), (eq2, x[2]), (eq2, x[3])]
125+
@test length(E) == length(predicted_edges)
126+
edge_set = Set(E)
127+
for (con, var) in predicted_edges
128+
@test (con_node_map[con], var_node_map[var]) in edge_set
129+
end
130+
return
131+
end
132+
133+
function test_construct_from_constraints_and_variables()
134+
m = jmp.Model()
135+
@jmp.variable(m, x[1:3])
136+
@jmp.constraint(m, eq1, x[1] + 2*x[2] == 1)
137+
@jmp.constraint(m, eq2, x[2]*x[3] == 0.5)
138+
cons = [eq2, eq1]
139+
vars = [x[3], x[2], x[1]]
140+
graph, con_node_map, var_node_map = get_bipartite_incidence_graph(cons, vars)
141+
A, B, E = graph
142+
143+
@test con_node_map[eq1] == 2
144+
@test con_node_map[eq2] == 1
145+
@test var_node_map[x[1]] == 5
146+
@test var_node_map[x[2]] == 4
147+
@test var_node_map[x[3]] == 3
148+
149+
predicted_edges = [(eq1, x[1]), (eq1, x[2]), (eq2, x[2]), (eq2, x[3])]
150+
@test length(E) == length(predicted_edges)
151+
edge_set = Set(E)
152+
for (con, var) in predicted_edges
153+
@test (con_node_map[con], var_node_map[var]) in edge_set
154+
end
155+
return
109156
end
110157

111158
function runtests()
112159
test_get_incidence_graph()
113160
test_get_incidence_graph_badconstraint()
114161
test_include_bound_as_inequality()
162+
test_construct_from_constraints()
163+
test_construct_from_constraints_and_variables()
115164
return
116165
end
117166

test/interface.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ function test_overconstrained_due_to_fixed_variable()
236236
@test length(var_dmp.overconstrained) == 2
237237
@test length(con_dmp.overconstrained) == 2
238238
@test length(con_dmp.unmatched) == 1
239+
return
239240
end
240241

241242
function test_overconstrained_due_to_including_bound()
@@ -249,6 +250,52 @@ function test_overconstrained_due_to_including_bound()
249250
@test length(var_dmp.overconstrained) == 2
250251
@test length(con_dmp.overconstrained) == 2
251252
@test length(con_dmp.unmatched) == 1
253+
return
254+
end
255+
256+
function test_interface_from_constraints_and_variables()
257+
m = JuMP.Model()
258+
@JuMP.variable(m, x[1:3])
259+
@JuMP.constraint(m, eq1, x[1] + x[2] == 2)
260+
@JuMP.constraint(m, eq2, x[3]*x[2] == 1.1)
261+
constraints = [eq1, eq2]
262+
variables = [x[1], x[3]]
263+
igraph = ji.IncidenceGraphInterface(constraints, variables)
264+
_test_igraph_fields(igraph, constraints, variables)
265+
return
266+
end
267+
268+
function test_matching_from_constraints_and_variables()
269+
m = JuMP.Model()
270+
@JuMP.variable(m, x[1:3])
271+
@JuMP.constraint(m, eq1, x[1] + x[2] == 2)
272+
@JuMP.constraint(m, eq2, x[3]*x[2] == 1.1)
273+
constraints = [eq1, eq2]
274+
variables = [x[1], x[3]]
275+
matching = ji.maximum_matching(constraints, variables)
276+
@test length(matching) == 2
277+
@test matching[eq1] == x[1]
278+
@test matching[eq2] == x[3]
279+
return
280+
end
281+
282+
function test_dulmage_mendelsohn_from_constraints_and_variables()
283+
m = JuMP.Model()
284+
@JuMP.variable(m, x[1:3])
285+
@JuMP.constraint(m, eq1, x[1] + x[2] == 2)
286+
@JuMP.constraint(m, eq2, x[3]*x[2] == 1.1)
287+
constraints = [eq1, eq2]
288+
variables = [x[1], x[3]]
289+
con_dmp, var_dmp = ji.dulmage_mendelsohn(constraints, variables)
290+
@test con_dmp.unmatched == []
291+
@test con_dmp.underconstrained == []
292+
@test con_dmp.overconstrained == []
293+
@test Set(con_dmp.square) == Set(constraints)
294+
@test var_dmp.unmatched == []
295+
@test var_dmp.underconstrained == []
296+
@test var_dmp.overconstrained == []
297+
@test Set(var_dmp.square) == Set(variables)
298+
return
252299
end
253300

254301
function runtests()
@@ -262,6 +309,9 @@ function runtests()
262309
test_dulmage_mendelsohn()
263310
test_overconstrained_due_to_fixed_variable()
264311
test_overconstrained_due_to_including_bound()
312+
test_interface_from_constraints_and_variables()
313+
test_matching_from_constraints_and_variables()
314+
test_dulmage_mendelsohn_from_constraints_and_variables()
265315
end
266316

267317
end

0 commit comments

Comments
 (0)