Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fedad79
Remove need for the MOI fork
blegat Jan 6, 2026
d6b6156
Update ci
blegat Jan 6, 2026
06b3e00
Rename
blegat Jan 6, 2026
c871453
Fixes
blegat Jan 6, 2026
8e67e16
Correct typo and change model for Model
SophieL1 Jan 6, 2026
c7345b0
Fix tests
blegat Jan 6, 2026
e165a0e
Fix format
blegat Jan 6, 2026
b8f31cf
Fixes
blegat Jan 6, 2026
69dbb4f
Fix problems
SophieL1 Jan 8, 2026
8cc4f33
Remove need for the MOI fork
blegat Jan 6, 2026
daecc2b
Update ci
blegat Jan 6, 2026
678da5e
Rename
blegat Jan 6, 2026
e40b878
Fixes
blegat Jan 6, 2026
ae806d2
Correct typo and change model for Model
SophieL1 Jan 6, 2026
bb2d59d
Fix tests
blegat Jan 6, 2026
5ebb925
Fix format
blegat Jan 6, 2026
29fc603
Fixes
blegat Jan 6, 2026
98ee103
Fix problems
SophieL1 Jan 8, 2026
d56ae72
Merge branch 'main' into bl/fix_parse
SophieL1 Jan 8, 2026
48836eb
Merge remote-tracking branch 'origin/bl/fix_parse' into bl/fix_parse
SophieL1 Jan 8, 2026
41ca187
Change tests accordingly when merging with main
SophieL1 Jan 8, 2026
1239956
Correct format
SophieL1 Jan 8, 2026
3948bfe
Copy-paste OperatorRegistry, Model and Evaluator from MOI.Nonlinear
SophieL1 Jan 15, 2026
75432f3
Copy-paste functions we need from MOI.Nonlinear and change to ours
SophieL1 Jan 15, 2026
ba548b0
We seem to need this?
SophieL1 Jan 15, 2026
4ecd3ff
Change to our functions in ArrayDiff tests
SophieL1 Jan 15, 2026
e77013c
Change to our functions in ReverseAD tests
SophieL1 Jan 15, 2026
6d6cb7e
Delete double definition and change places
SophieL1 Jan 16, 2026
3ad14fc
Format and remove unused
SophieL1 Jan 16, 2026
08bcf64
Remove unused
SophieL1 Feb 3, 2026
95407fc
Reorganise MOI_Nonlinear_fork into operators, model, parse and evaluator
SophieL1 Feb 3, 2026
c034297
Format
SophieL1 Feb 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ jobs:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v1
- name: MOI
shell: julia --project=@. {0}
run: |
using Pkg
Pkg.add([
PackageSpec(name="MathOptInterface", rev="bl/arraydiff"),
])
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
with:
Expand Down
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NaNMath = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"

[compat]
DataStructures = "0.18, 0.19"
Expand Down
35 changes: 19 additions & 16 deletions src/ArrayDiff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,10 @@ import ForwardDiff
import MathOptInterface as MOI
const Nonlinear = MOI.Nonlinear
import SparseArrays
import OrderedCollections: OrderedDict

"""
Mode() <: AbstractAutomaticDifferentiation

Fork of `MOI.Nonlinear.SparseReverseMode` to add array support.
"""
struct Mode <: MOI.Nonlinear.AbstractAutomaticDifferentiation end

function MOI.Nonlinear.Evaluator(
model::MOI.Nonlinear.Model,
::Mode,
ordered_variables::Vector{MOI.VariableIndex},
)
return MOI.Nonlinear.Evaluator(
model,
NLPEvaluator(model, ordered_variables),
)
end

# Override basic math functions to return NaN instead of throwing errors.
# This is what NLP solvers expect, and sometimes the results aren't needed
# anyway, because the code may compute derivatives wrt constants.
Expand Down Expand Up @@ -57,5 +42,23 @@ include("utils.jl")
include("reverse_mode.jl")
include("forward_over_reverse.jl")
include("mathoptinterface_api.jl")
include("operators.jl")
include("model.jl")
include("parse.jl")
include("evaluator.jl")

"""
Mode() <: AbstractAutomaticDifferentiation

Fork of `MOI.Nonlinear.SparseReverseMode` to add array support.
"""

function Evaluator(
model::ArrayDiff.Model,
::Mode,
ordered_variables::Vector{MOI.VariableIndex},
)
return Evaluator(model, NLPEvaluator(model, ordered_variables))
end

end # module
156 changes: 156 additions & 0 deletions src/evaluator.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Largely inspired by MathOptInterface/src/Nonlinear/parse.jl
# Most functions have been copy-pasted and slightly modified to adapt to small changes in OperatorRegistry and Model.

function MOI.initialize(evaluator::Evaluator, features::Vector{Symbol})
start_time = time()
empty!(evaluator.ordered_constraints)
evaluator.eval_objective_timer = 0.0
evaluator.eval_objective_gradient_timer = 0.0
evaluator.eval_constraint_timer = 0.0
evaluator.eval_constraint_gradient_timer = 0.0
evaluator.eval_constraint_jacobian_timer = 0.0
evaluator.eval_hessian_objective_timer = 0.0
evaluator.eval_hessian_constraint_timer = 0.0
evaluator.eval_hessian_lagrangian_timer = 0.0
append!(evaluator.ordered_constraints, keys(evaluator.model.constraints))
# Every backend supports :ExprGraph, so don't forward it.
filter!(f -> f != :ExprGraph, features)
if evaluator.backend !== nothing
MOI.initialize(evaluator.backend, features)
elseif !isempty(features)
@assert evaluator.backend === nothing # ==> ExprGraphOnly used
error(
"Unable to initialize `Nonlinear.Evaluator` because the " *
"following features are not supported: $features",
)
end
evaluator.initialize_timer = time() - start_time
return
end

function MOI.eval_objective(evaluator::Evaluator, x)
start = time()
obj = MOI.eval_objective(evaluator.backend, x)
evaluator.eval_objective_timer += time() - start
return obj
end

function MOI.eval_objective_gradient(evaluator::Evaluator, g, x)
start = time()
MOI.eval_objective_gradient(evaluator.backend, g, x)
evaluator.eval_objective_gradient_timer += time() - start
return
end

function MOI.eval_constraint_gradient(evaluator::Evaluator, ∇g, x, i)
start = time()
MOI.eval_constraint_gradient(evaluator.backend, ∇g, x, i)
evaluator.eval_constraint_gradient_timer += time() - start
return
end

function MOI.constraint_gradient_structure(evaluator::Evaluator, i)
return MOI.constraint_gradient_structure(evaluator.backend, i)
end

function MOI.eval_constraint(evaluator::Evaluator, g, x)
start = time()
MOI.eval_constraint(evaluator.backend, g, x)
evaluator.eval_constraint_timer += time() - start
return
end

function MOI.jacobian_structure(evaluator::Evaluator)
return MOI.jacobian_structure(evaluator.backend)
end

function MOI.eval_constraint_jacobian(evaluator::Evaluator, J, x)
start = time()
MOI.eval_constraint_jacobian(evaluator.backend, J, x)
evaluator.eval_constraint_jacobian_timer += time() - start
return
end

function MOI.hessian_objective_structure(evaluator::Evaluator)
return MOI.hessian_objective_structure(evaluator.backend)
end

function MOI.hessian_constraint_structure(evaluator::Evaluator, i)
return MOI.hessian_constraint_structure(evaluator.backend, i)
end

function MOI.hessian_lagrangian_structure(evaluator::Evaluator)
return MOI.hessian_lagrangian_structure(evaluator.backend)
end

function MOI.eval_hessian_objective(evaluator::Evaluator, H, x)
start = time()
MOI.eval_hessian_objective(evaluator.backend, H, x)
evaluator.eval_hessian_objective_timer += time() - start
return
end

function MOI.eval_hessian_constraint(evaluator::Evaluator, H, x, i)
start = time()
MOI.eval_hessian_constraint(evaluator.backend, H, x, i)
evaluator.eval_hessian_constraint_timer += time() - start
return
end

function MOI.eval_hessian_lagrangian(evaluator::Evaluator, H, x, σ, μ)
start = time()
MOI.eval_hessian_lagrangian(evaluator.backend, H, x, σ, μ)
evaluator.eval_hessian_lagrangian_timer += time() - start
return
end

function MOI.eval_constraint_jacobian_product(evaluator::Evaluator, y, x, w)
start = time()
MOI.eval_constraint_jacobian_product(evaluator.backend, y, x, w)
evaluator.eval_constraint_jacobian_timer += time() - start
return
end

function MOI.eval_constraint_jacobian_transpose_product(
evaluator::Evaluator,
y,
x,
w,
)
start = time()
MOI.eval_constraint_jacobian_transpose_product(evaluator.backend, y, x, w)
evaluator.eval_constraint_jacobian_timer += time() - start
return
end

function MOI.eval_hessian_lagrangian_product(
evaluator::Evaluator,
H,
x,
v,
σ,
μ,
)
start = time()
MOI.eval_hessian_lagrangian_product(evaluator.backend, H, x, v, σ, μ)
evaluator.eval_hessian_lagrangian_timer += time() - start
return
end

function eval_univariate_hessian(
registry::OperatorRegistry,
id::Integer,
x::T,
) where {T}
if id <= registry.univariate_user_operator_start
ret = Nonlinear._eval_univariate_2nd_deriv(id, x)
if ret === nothing
op = registry.univariate_operators[id]
error("Hessian is not defined for operator $op")
end
return ret::T
end
offset = id - registry.univariate_user_operator_start
operator = registry.registered_univariate_operators[offset]
return eval_univariate_hessian(operator, x)
end
4 changes: 2 additions & 2 deletions src/forward_over_reverse.jl
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ function _forward_eval_ϵ(
d.user_output_buffer,
n_children,
)
has_hessian = Nonlinear.eval_multivariate_hessian(
has_hessian = eval_multivariate_hessian(
d.data.operators,
d.data.operators.multivariate_operators[node.index],
H,
Expand All @@ -351,7 +351,7 @@ function _forward_eval_ϵ(
end
elseif node.type == Nonlinear.NODE_CALL_UNIVARIATE
@inbounds child_idx = children_arr[ex.adj.colptr[k]]
f′′ = Nonlinear.eval_univariate_hessian(
f′′ = eval_univariate_hessian(
d.data.operators,
node.index,
ex.forward_storage[child_idx],
Expand Down
85 changes: 85 additions & 0 deletions src/model.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Largely inspired by MathOptInterface/src/Nonlinear/model.jl
# Most functions have been copy-pasted and slightly modified to adapt to small changes in OperatorRegistry and Model.

function set_objective(model::Model, obj)
model.objective = parse_expression(model, obj)
return
end

function add_constraint(
model::Model,
func,
set::Union{
MOI.GreaterThan{Float64},
MOI.LessThan{Float64},
MOI.Interval{Float64},
MOI.EqualTo{Float64},
},
)
f = parse_expression(model, func)
model.last_constraint_index += 1
index = MOI.Nonlinear.ConstraintIndex(model.last_constraint_index)
model.constraints[index] = MOI.Nonlinear.Constraint(f, set)
return index
end

function add_parameter(model::Model, value::Float64)
push!(model.parameters, value)
return MOI.Nonlinear.ParameterIndex(length(model.parameters))
end

function add_expression(model::Model, expr)
push!(model.expressions, parse_expression(model, expr))
return Nonlinear.ExpressionIndex(length(model.expressions))
end

function Base.getindex(model::Model, index::Nonlinear.ExpressionIndex)
return model.expressions[index.value]
end

function register_operator(model::Model, op::Symbol, nargs::Int, f::Function...)
return register_operator(model.operators, op, nargs, f...)
end

function register_operator(
registry::OperatorRegistry,
op::Symbol,
nargs::Int,
f::Function...,
)
if nargs == 1
if haskey(registry.univariate_operator_to_id, op)
error("Operator $op is already registered.")
elseif haskey(registry.multivariate_operator_to_id, op)
error("Operator $op is already registered.")
end
operator = Nonlinear._UnivariateOperator(op, f...)
push!(registry.univariate_operators, op)
push!(registry.registered_univariate_operators, operator)
registry.univariate_operator_to_id[op] =
length(registry.univariate_operators)
else
if haskey(registry.multivariate_operator_to_id, op)
error("Operator $op is already registered.")
elseif haskey(registry.univariate_operator_to_id, op)
error("Operator $op is already registered.")
end
operator = Nonlinear._MultivariateOperator{nargs}(op, f...)
push!(registry.multivariate_operators, op)
push!(registry.registered_multivariate_operators, operator)
registry.multivariate_operator_to_id[op] =
length(registry.multivariate_operators)
end
return
end

function features_available(evaluator::Evaluator)
features = Symbol[]
if evaluator.backend !== nothing
append!(features, MOI.features_available(evaluator.backend))
end
if !(:ExprGraph in features)
push!(features, :ExprGraph)
end
return features
end
Loading
Loading