Skip to content

Commit 5852cf1

Browse files
committed
Add array output support
1 parent a4c8fab commit 5852cf1

6 files changed

Lines changed: 195 additions & 8 deletions

File tree

src/evaluator.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ function MOI.eval_constraint(evaluator::Evaluator, g, x)
6060
return
6161
end
6262

63+
eval_residual!(evaluator::Evaluator, F, x) = eval_residual!(evaluator.backend, F, x)
64+
65+
function eval_residual_jtprod!(evaluator::Evaluator, Jtv, x, v)
66+
return eval_residual_jtprod!(evaluator.backend, Jtv, x, v)
67+
end
68+
69+
function eval_residual_jprod!(evaluator::Evaluator, Jv, x, v)
70+
return eval_residual_jprod!(evaluator.backend, Jv, x, v)
71+
end
72+
73+
residual_dimension(evaluator::Evaluator) = residual_dimension(evaluator.backend)
74+
6375
function MOI.jacobian_structure(evaluator::Evaluator)
6476
return MOI.jacobian_structure(evaluator.backend)
6577
end

src/mathoptinterface_api.jl

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
3636
largest_user_input_dimension = max(largest_user_input_dimension, op.N)
3737
end
3838
d.objective = nothing
39+
d.residual = nothing
3940
d.user_output_buffer = zeros(largest_user_input_dimension)
4041
d.jac_storage = zeros(max(N, largest_user_input_dimension))
4142
d.constraints = _FunctionStorage[]
@@ -47,6 +48,9 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
4748
max_expr_with_sub_length = 0
4849
#
4950
main_expressions = [c.expression.nodes for (_, c) in d.data.constraints]
51+
if d.data.residual !== nothing
52+
pushfirst!(main_expressions, something(d.data.residual).nodes)
53+
end
5054
if d.data.objective !== nothing
5155
pushfirst!(main_expressions, something(d.data.objective).nodes)
5256
end
@@ -102,7 +106,9 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
102106
end
103107
max_chunk = 1
104108
shared_partials_storage_ϵ = Float64[]
109+
main_offset = 0
105110
if d.data.objective !== nothing
111+
main_offset += 1
106112
expr = something(d.data.objective)
107113
subexpr, linearity = _subexpression_and_linearity(
108114
expr,
@@ -116,7 +122,7 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
116122
coloring_storage,
117123
d.want_hess,
118124
d.subexpressions,
119-
individual_order[1],
125+
individual_order[main_offset],
120126
subexpression_edgelist,
121127
subexpression_variables,
122128
linearity,
@@ -125,8 +131,32 @@ function MOI.initialize(d::NLPEvaluator, requested_features::Vector{Symbol})
125131
max_chunk = max(max_chunk, size(objective.seed_matrix, 2))
126132
d.objective = objective
127133
end
134+
if d.data.residual !== nothing
135+
main_offset += 1
136+
expr = something(d.data.residual)
137+
subexpr, linearity = _subexpression_and_linearity(
138+
expr,
139+
moi_index_to_consecutive_index,
140+
shared_partials_storage_ϵ,
141+
d,
142+
)
143+
residual = _FunctionStorage(
144+
subexpr,
145+
N,
146+
coloring_storage,
147+
d.want_hess,
148+
d.subexpressions,
149+
individual_order[main_offset],
150+
subexpression_edgelist,
151+
subexpression_variables,
152+
linearity,
153+
)
154+
max_expr_length = max(max_expr_length, length(expr.nodes))
155+
max_chunk = max(max_chunk, size(residual.seed_matrix, 2))
156+
d.residual = residual
157+
end
128158
for (k, (_, constraint)) in enumerate(d.data.constraints)
129-
idx = d.data.objective !== nothing ? k + 1 : k
159+
idx = main_offset + k
130160
expr = constraint.expression
131161
subexpr, linearity = _subexpression_and_linearity(
132162
expr,
@@ -211,6 +241,116 @@ function MOI.eval_constraint(d::NLPEvaluator, g, x)
211241
return
212242
end
213243

244+
# Forward-evaluate subexpressions and the residual at `x`.
245+
function _forward_pass_residual!(d::NLPEvaluator, x)
246+
for k in d.subexpression_order
247+
d.subexpression_forward_values[k] =
248+
_forward_eval(d.subexpressions[k], d, x)
249+
end
250+
_forward_eval(something(d.residual).expr, d, x)
251+
return
252+
end
253+
254+
# Read the residual values from forward storage into `F`.
255+
function _read_residual!(F::AbstractVector, d::NLPEvaluator)
256+
res = something(d.residual)
257+
range = _storage_range(res.expr.sizes, 1)
258+
@assert length(F) == length(range)
259+
for (i, j) in enumerate(range)
260+
F[i] = res.expr.forward_storage[j]
261+
end
262+
return
263+
end
264+
265+
"""
266+
eval_residual!(d::NLPEvaluator, F, x)
267+
268+
Forward-evaluate the residual at `x` and write the values into `F`.
269+
"""
270+
function eval_residual!(d::NLPEvaluator, F::AbstractVector, x::AbstractVector)
271+
if d.residual === nothing
272+
error("No nonlinear residual.")
273+
end
274+
_forward_pass_residual!(d, x)
275+
_read_residual!(F, d)
276+
return F
277+
end
278+
279+
"""
280+
eval_residual_jtprod!(d::NLPEvaluator, Jtv, x, v)
281+
282+
Compute `J(x)' * v` for the residual `F`, writing the result into `Jtv`.
283+
This is one reverse-mode pass with `v` as the seed at the residual root.
284+
"""
285+
function eval_residual_jtprod!(
286+
d::NLPEvaluator,
287+
Jtv::AbstractVector,
288+
x::AbstractVector,
289+
v::AbstractVector,
290+
)
291+
if d.residual === nothing
292+
error("No nonlinear residual.")
293+
end
294+
res = something(d.residual)
295+
_forward_pass_residual!(d, x)
296+
for k in d.subexpression_order
297+
_reverse_eval(d.subexpressions[k])
298+
end
299+
_reverse_eval(res.expr, v)
300+
fill!(Jtv, 0.0)
301+
_extract_reverse_pass(Jtv, d, res)
302+
return Jtv
303+
end
304+
305+
"""
306+
residual_dimension(d::NLPEvaluator) -> Int
307+
308+
Number of residual components (i.e. the length of the residual vector).
309+
"""
310+
function residual_dimension(d::NLPEvaluator)
311+
return length(_storage_range(something(d.residual).expr.sizes, 1))
312+
end
313+
314+
"""
315+
eval_residual_jprod!(d::NLPEvaluator, Jv, x, v)
316+
317+
Compute `J(x) * v` for the residual `F`. ArrayDiff doesn't yet have a
318+
forward-mode JVP, so this materializes the Jacobian via `nresid` reverse-mode
319+
passes (each with a unit-basis seed) and then takes `J * v`.
320+
"""
321+
function eval_residual_jprod!(
322+
d::NLPEvaluator,
323+
Jv::AbstractVector,
324+
x::AbstractVector,
325+
v::AbstractVector,
326+
)
327+
if d.residual === nothing
328+
error("No nonlinear residual.")
329+
end
330+
res = something(d.residual)
331+
nresid = residual_dimension(d)
332+
_forward_pass_residual!(d, x)
333+
for k in d.subexpression_order
334+
_reverse_eval(d.subexpressions[k])
335+
end
336+
seed = zeros(Float64, nresid)
337+
row = zeros(Float64, length(v))
338+
fill!(Jv, 0.0)
339+
for i in 1:nresid
340+
fill!(seed, 0.0)
341+
seed[i] = 1.0
342+
_reverse_eval(res.expr, seed)
343+
fill!(row, 0.0)
344+
_extract_reverse_pass(row, d, res)
345+
s = 0.0
346+
for j in eachindex(v)
347+
s += row[j] * v[j]
348+
end
349+
Jv[i] = s
350+
end
351+
return Jv
352+
end
353+
214354
function MOI.jacobian_structure(d::NLPEvaluator)
215355
J = Tuple{Int64,Int64}[]
216356
for (row, constraint) in enumerate(d.constraints)

src/model.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ function set_objective(model::Model, obj)
66
return
77
end
88

9+
function set_residual!(model::Model, residual)
10+
model.residual = parse_expression(model, residual)
11+
return
12+
end
13+
914
function add_constraint(
1015
model::Model{T},
1116
func,

src/reverse_mode.jl

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,9 @@ function _forward_eval(
568568
f.partials_storage[rhs] = zero(T)
569569
end
570570
end
571-
@assert f.sizes.ndims[1] == 0 "Final result must be scalar, got ndims = $(f.sizes.ndims[1])"
571+
# Caller is responsible for reading the right range of `f.forward_storage`
572+
# for vector-valued roots (use `_storage_range(f.sizes, 1)`); the scalar
573+
# return is only meaningful when the root is scalar.
572574
return f.forward_storage[1]
573575
end
574576

@@ -580,15 +582,26 @@ Reverse-mode evaluation of an expression tree given in `f`.
580582
* This function assumes `f.partials_storage` is already updated.
581583
* This function assumes that `f.reverse_storage` has been initialized with 0.0.
582584
"""
583-
function _reverse_eval(f::_SubexpressionStorage)
585+
function _reverse_eval(
586+
f::_SubexpressionStorage,
587+
seed::Union{Nothing,AbstractVector{Float64}} = nothing,
588+
)
584589
@assert length(f.reverse_storage) >= _length(f.sizes)
585590
@assert length(f.partials_storage) >= _length(f.sizes)
586591
# f.nodes is already in order such that parents always appear before
587592
# children so a forward pass through nodes is a backwards pass through the
588593
# tree.
589594
children_arr = SparseArrays.rowvals(f.adj)
590-
for i in _storage_range(f.sizes, 1)
591-
f.reverse_storage[i] = one(Float64)
595+
root_range = _storage_range(f.sizes, 1)
596+
if seed === nothing
597+
for i in root_range
598+
f.reverse_storage[i] = one(Float64)
599+
end
600+
else
601+
@assert length(seed) == length(root_range)
602+
for (j, i) in enumerate(root_range)
603+
f.reverse_storage[i] = seed[j]
604+
end
592605
end
593606
for k in 1:length(f.nodes)
594607
node = f.nodes[k]

src/types.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ It has the following fields:
194194
"""
195195
mutable struct Model{T}
196196
objective::Union{Nothing,Expression{T}}
197+
# Vector residual for nonlinear least-squares objectives. When set, callers
198+
# can query its value, J*v, J'*v, etc. via the evaluator.
199+
residual::Union{Nothing,Expression{T}}
197200
expressions::Vector{Expression{T}}
198201
constraints::OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}}
199202
parameters::Vector{T}
@@ -202,6 +205,7 @@ mutable struct Model{T}
202205
last_constraint_index::Int64
203206
function Model{T}() where {T}
204207
return new{T}(
208+
nothing,
205209
nothing,
206210
Expression{T}[],
207211
OrderedCollections.OrderedDict{ConstraintIndex,Constraint{T}}(),
@@ -294,6 +298,7 @@ mutable struct NLPEvaluator <: MOI.AbstractNLPEvaluator
294298
ordered_variables::Vector{MOI.VariableIndex}
295299

296300
objective::Union{Nothing,_FunctionStorage}
301+
residual::Union{Nothing,_FunctionStorage}
297302
constraints::Vector{_FunctionStorage}
298303
subexpressions::Vector{_SubexpressionStorage}
299304
subexpression_order::Vector{Int}

test/NLPModelsJuMP.jl

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ function runtests()
1919
return
2020
end
2121

22-
function test_neural_nlpmodels_jump()
22+
function _test_neural_nlpmodels_jump(solver)
2323
n = 2
2424
X = [1.0 0.5; 0.3 0.8]
2525
target = [0.5 0.2; 0.1 0.7]
2626
model = Model(NLPModelsJuMP.Optimizer)
27-
set_attribute(model, "solver", JSOSolvers.LBFGSSolver)
27+
set_attribute(model, "solver", solver)
2828
set_attribute(
2929
model,
3030
MOI.AutomaticDifferentiationBackend(),
@@ -48,6 +48,18 @@ function test_neural_nlpmodels_jump()
4848
return
4949
end
5050

51+
function test_neural_lbfgs()
52+
return _test_neural_nlpmodels_jump(JSOSolvers.LBFGSSolver)
53+
end
54+
55+
function test_neural_trunkls()
56+
return _test_neural_nlpmodels_jump(JSOSolvers.TrunkSolverNLS)
57+
end
58+
59+
function test_neural_tronls()
60+
return _test_neural_nlpmodels_jump(JSOSolvers.TronSolverNLS)
61+
end
62+
5163
end
5264

5365
TestWithNLPModelsJuMP.runtests()

0 commit comments

Comments
 (0)