Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@ If your work uses Gencan, the suggested references are:
with negative curvature directions and spectral projected gradients",
Computing [Suppl] 15, pp. 49-60, 2001.

## Affiliation

NLPModelsAlgencan.jl is developed and maintained by Paulo J. S. Silva (@pjssilva). It is not a product of the Tango Project, which develops
Algencan itself. For the Algencan developers, see [How to cite](#how-to-cite)
above.

## Getting help

Open an issue on the [issue
tracker](https://github.com/pjssilva/NLPModelsAlgencan.jl/issues) for bugs,
feature requests and other questions.

## Status

At this point this is beta software. It requires Julia 1.10 or later.
Expand Down Expand Up @@ -115,6 +127,49 @@ Prefer `set_algencan_library!`: environment variables are invisible to
precompilation, so changing one does not invalidate the cached module and can be
silently ignored.

## Use with JuMP

Algencan is an NLPModels solver, so JuMP reaches it through
[NLPModelsJuMP.jl](https://github.com/JuliaSmoothOptimizers/NLPModelsJuMP.jl),
the generic MathOptInterface wrapper for NLPModels solvers. Install it alongside
this package and pass `AlgencanSolver` as the `solver` attribute:

```julia
using JuMP, NLPModelsJuMP, NLPModelsAlgencan

model = Model(NLPModelsJuMP.Optimizer)
set_attribute(model, "solver", NLPModelsAlgencan.AlgencanSolver)

@variable(model, 0 <= x[1:2] <= 5)
set_start_value.(x, 1.0)
@objective(model, Min, x[1] * x[2] + 5)
@constraint(model, x[1] + x[2] <= 5)
@constraint(model, x[1]^2 + x[2]^2 == 10)

optimize!(model)
@show termination_status(model), objective_value(model), value.(x)
```

Solver options are set the same way, using the names from
[`docs/src/parameters.md`](docs/src/parameters.md):

```julia
set_attribute(model, "epsfeas", 1.0e-10)
set_attribute(model, "epsopt", 1.0e-10)
```

Two things to know about this path:

* `set_silent(model)` suppresses the iteration table but not Algencan's banner
and parameter listing, which Algencan 3.1.1 always writes to standard output.
* Constraint duals are not yet mapped onto MathOptInterface. Algencan does
compute the multipliers; they are available from the NLPModels interface via
`stats.multipliers`.

To use Algencan directly on an `AbstractNLPModel`, without JuMP, see [First
steps](https://pjssilva.github.io/NLPModelsAlgencan.jl/dev/first_steps/) in the
documentation.

## Contributing

See the [developer
Expand Down
4 changes: 2 additions & 2 deletions src/NLPModelsAlgencan.jl
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ mutable struct AlgencanSolver <: AbstractOptimizationSolver
:eostain => (atol)^1.5, :efacc => sqrt(atol),
:eoacc => sqrt(atol), :outputfnm => "", :specfnm => specfnm)
if verbose != 10
solver.options["iterations_output_detail"] = verbose
solver.options[:iterations_output_detail] = verbose
end
if max_iter != Int(typemax(Int32))
solver.options["outer_iterations_limit"] = max_iter
solver.options[:outer_iterations_limit] = max_iter
end
for k in keys(kwargs)
solver.options[k] = kwargs[k]
Expand Down
116 changes: 116 additions & 0 deletions test/MOI_wrapper.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# MathOptInterface conformance suite, run against Algencan.
#
# Algencan has no MOI wrapper of its own. It is an NLPModels solver, and JuMP
# reaches it through `NLPModelsJuMP.Optimizer`, the generic MOI wrapper for any
# `SolverCore.AbstractOptimizationSolver`:
#
# model = Model(NLPModelsJuMP.Optimizer)
# set_attribute(model, "solver", NLPModelsAlgencan.AlgencanSolver)
#
# That is the path exercised here. 541 of the 561 `MOI.Test` tests pass; the 20
# exclusions below are grouped by cause and each group is explained.
module TestMOI

using Test
import MathOptInterface as MOI
import NLPModelsJuMP
import NLPModelsAlgencan

# Attributes that `NLPModelsJuMP.Optimizer` does not implement. Duals are the
# notable one: Algencan returns multipliers, but the wrapper does not yet map
# them onto MOI's constraint duals.
const EXCLUDED_ATTRIBUTES = Any[
MOI.ConstraintBasisStatus,
MOI.VariableBasisStatus,
MOI.ConstraintName,
MOI.VariableName,
MOI.ObjectiveBound,
MOI.SolverVersion,
MOI.DualObjectiveValue,
MOI.ConstraintDual,
]

const EXCLUDED_TESTS = [
# ---------------------------------------------------------------------
# Second-derivative oracles are not negotiated with the user's evaluator.
#
# Algencan is a second-order method: it evaluates the Hessian of the
# Lagrangian. When the model arrives as a raw `MOI.NLPBlock`,
# `NLPModelsJuMP` requests `:Hess`/`:HessVec`/`:JacVec` from the
# user-supplied evaluator unconditionally, and the hand-written evaluators
# in `MOI.Test` deliberately advertise only a subset, so `MOI.initialize`
# raises `Unsupported feature ...`.
#
# This is a limitation of the shared wrapper, not of Algencan: models built
# through JuMP get a full `MOI.Nonlinear` evaluator and work fine (see the
# "JuMP interface test" testset in runtests.jl). Fixing it means having
# `NLPModelsJuMP` request only the features the evaluator reports and fall
# back to finite differences or a quasi-Newton Hessian otherwise.
r"^test_nonlinear_expression_multivariate_function$", # Unsupported feature Hess
r"^test_nonlinear_hs071$", # Unsupported feature HessVec
r"^test_nonlinear_hs071_NLPBlockDual$", # Unsupported feature HessVec
r"^test_nonlinear_hs071_hessian_vector_product$", # Unsupported feature Hess
r"^test_nonlinear_hs071_no_hessian$", # Unsupported feature Hess
r"^test_nonlinear_invalid$", # feat in features_available(d)
r"^test_nonlinear_objective$", # Unsupported feature JacVec
r"^test_nonlinear_objective_and_moi_objective_test$", # Unsupported feature JacVec
r"^test_nonlinear_without_objective$", # Unsupported feature JacVec

# ---------------------------------------------------------------------
# No global infeasibility or unboundedness certificate.
#
# Algencan is a local augmented-Lagrangian method. When it stops at a
# stationary point of the infeasibility measure it reports
# `LOCALLY_INFEASIBLE`, which is the honest answer; these tests require the
# global `INFEASIBLE` / `DUAL_INFEASIBLE` / `INFEASIBLE_OR_UNBOUNDED`.
# Other local NLP solvers exclude the same tests.
r"^test_conic_NormInfinityCone_INFEASIBLE$",
r"^test_conic_NormOneCone_INFEASIBLE$",
r"^test_conic_linear_INFEASIBLE$",
r"^test_conic_linear_INFEASIBLE_2$",
r"^test_linear_DUAL_INFEASIBLE$",
r"^test_linear_INFEASIBLE$",
r"^test_linear_INFEASIBLE_2$",

# ---------------------------------------------------------------------
# Converges to an infeasible stationary point from the starting point the
# test supplies: `LOCALLY_INFEASIBLE` where `LOCALLY_SOLVED` is expected.
# Again a property of local optimization, not a wrong answer.
r"^test_nonlinear_expression_hs109$",
r"^test_quadratic_SecondOrderCone_basic$",
r"^test_quadratic_nonconvex_constraint_basic$",

# ---------------------------------------------------------------------
# `NLPModelsJuMP.Optimizer` reads the model attributes it knows and ignores
# the rest, so it does not raise `MOI.UnsupportedAttribute` for an unknown
# one. Wrapper-side; there is a FIXME to this effect in NLPModelsJuMP.
r"^test_model_copy_to_UnsupportedAttribute$",
]

function test_runtests()
model = MOI.instantiate(NLPModelsJuMP.Optimizer, with_bridge_type=Float64)
MOI.set(model, MOI.RawOptimizerAttribute("solver"), NLPModelsAlgencan.AlgencanSolver)
MOI.set(model, MOI.Silent(), true) # comment out to see Algencan's own output
config = MOI.Test.Config(
atol=1.0e-2,
optimal_status=MOI.LOCALLY_SOLVED,
exclude=EXCLUDED_ATTRIBUTES,
)
MOI.Test.runtests(model, config; exclude=EXCLUDED_TESTS)
return
end

function runtests()
for name in names(@__MODULE__; all=true)
if startswith("$(name)", "test_")
@testset "$(name)" begin
getfield(@__MODULE__, name)()
end
end
end
return
end

end # module

TestMOI.runtests()
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a"
CUTEst = "1b53aba6-35b6-5f92-a507-53c67d53f819"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
NLPModelsJuMP = "792afdf1-32c1-5681-94e0-d7bf7a5df49e"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
22 changes: 22 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ end
@test stats.multipliers ≈ [3.277936962780388, 2.905444126051696, -7.747851002665478] rtol = 1.0e-4
end

# Regression test for the `verbose` and `max_iter` keyword arguments.
#
# `solver.options` is a `Dict{Symbol,Any}`, but both were stored under `String`
# keys, so any non-default value threw
# MethodError: Cannot `convert` an object of type String to an object of type Symbol
# Nothing exercised them, which is how it went unnoticed.
@testset "verbose and max_iter keyword arguments" begin
stats = algencan(hs12(); verbose=0)
@test stats.status == :first_order
@test stats.objective ≈ -30.0 rtol = 1.0e-6

solver = NLPModelsAlgencan.AlgencanSolver(hs12(); verbose=0)
@test solver.options[:iterations_output_detail] == 0

solver = (@test_logs (:warn,) (:warn,) NLPModelsAlgencan.AlgencanSolver(hs12(); max_iter=5))
@test solver.options[:outer_iterations_limit] == 5
end

# Regression test for Apple Silicon (aarch64).
#
# The Algencan callbacks used to be built with the closure form of `@cfunction`,
Expand Down Expand Up @@ -220,3 +238,7 @@ end
optimize!(model)
@test value.(x) ≈ center
end

@testset "MathOptInterface conformance tests" begin
include("MOI_wrapper.jl")
end
Loading