Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SolverCore Revamp #6

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
7 changes: 7 additions & 0 deletions .JuliaFormatter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
margin = 100
indent = 2
whitespace_typedefs = true
whitespace_ops_in_indices = true
remove_extra_newlines = true
annotate_untyped_fields_with_any = false
normalize_line_endings = "unix"
10 changes: 5 additions & 5 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
name = "SolverCore"
uuid = "ff4d7338-4cf1-434d-91df-b86cb86fb843"
version = "0.1.0"
version = "0.2.0-dev"

[deps]
NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[compat]
NLPModels = "0.14"
julia = "^1.3.0"

[extras]
ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["ADNLPModels", "LinearAlgebra", "Logging", "Test"]
test = ["LinearAlgebra", "Logging", "Random", "Test"]
12 changes: 6 additions & 6 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ makedocs(
doctest = true,
linkcheck = true,
strict = true,
format = Documenter.HTML(assets = ["assets/style.css"], prettyurls = get(ENV, "CI", nothing) == "true"),
format = Documenter.HTML(
assets = ["assets/style.css"],
prettyurls = get(ENV, "CI", nothing) == "true",
),
sitename = "SolverCore.jl",
pages = ["Home" => "index.md",
"API" => "api.md",
"Reference" => "reference.md",
]
pages = ["Home" => "index.md", "Reference" => "reference.md"],
)

deploydocs(
repo = "github.com/JuliaSmoothOptimizers/SolverCore.jl.git",
push_preview = true,
devbranch = "master"
devbranch = "master",
)
18 changes: 0 additions & 18 deletions docs/src/api.md

This file was deleted.

8 changes: 7 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# [SolverCore.jl documentation](@id Home)

Core package to build novel optimization algorithms in Julia.
Core package to build JSO-compliant solvers in Julia.

---

This package is extended for specific problem types:
- **OptSolver.jl**: For optimization problems
- **LinearSolvers.jl**: For linear solvers.
13 changes: 13 additions & 0 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# Reference

## Contents

```@contents
Pages = ["reference.md"]
```

## Index

```@index
Pages = ["reference.md"]
```

```@autodocs
Modules = [SolverCore]
```
13 changes: 9 additions & 4 deletions src/SolverCore.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
module SolverCore

# stdlib
using Printf
using Logging, Printf
using OrderedCollections

# our packages
using NLPModels
include("problem.jl")
include("solver.jl")
include("output.jl")

include("logger.jl")
include("stats.jl")
include("parameters.jl")
include("traits.jl")

include("grid-search-tuning.jl")

end
101 changes: 101 additions & 0 deletions src/grid-search-tuning.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
export grid_search_tune, reset_problem!

# TODO: Decide success and costs of grid_search_tune below

"""
solver, results = grid_search_tune(SolverType, problems; kwargs...)

Simple tuning of solver `SolverType` by grid search, on `problems`, which should be iterable.
The following keyword arguments are available:
- `success`: A function to be applied on a solver output that returns whether the problem has terminated succesfully. Defaults to `o -> o.status == :first_order`.
- `costs`: A vector of cost functions and penalties. Each element is a tuple of two elements. The first is a function to be applied to the output of the solver, and the second is the cost when the solver fails (see `success` above) or throws an error. Defaults to
```
[
(o -> o.elapsed_time, 100.0),
(o -> o.counters.neval_obj + o.counters.neval_cons, 1000),
(o -> !success(o), 1),
]
```
which represent the total elapsed_time (with a penalty of 100.0 for failures); the number of objective and constraints functions evaluations (with a penalty of 1000 for failures); and the number of failures.
- `grid_length`: The number of points in the ranges of the grid for continuous points.
- `solver_kwargs`: Arguments to be passed to the solver. Note: use this to set the stopping parameters, but not the other parameters being optimize.
- Any parameters accepted by the `Solver`: a range to be used instead of the default range.

The default ranges are based on the parameters types, and are as follows:
- `:real`: linear range from `:min` to `:max` with `grid_length` points.
- `:log`: logarithmic range from `:min` to `:max` with `grid_length` points. Computed by exp of linear range of `log(:min)` to `log(:max)`.
- `:bool`: either `false` or `true`.
- `:int`: integer range from `:min` to `:max`.
"""
function grid_search_tune(
::Type{Solver},
problems;
success = o -> o.status == :first_order,
costs = [(o -> o.elapsed_time, 100.0), (o -> !success(o), 1)],
grid_length = 10,
solver_kwargs = Dict(),
kwargs...,
) where {Solver <: AbstractSolver}
solver_params = parameters(Solver)
params = OrderedDict()
for (k, v) in pairs(solver_params)
if v[:type] <: AbstractFloat && (!haskey(v, :scale) || v[:scale] == :linear)
params[k] = LinRange(v[:min], v[:max], grid_length)
elseif v[:type] <: AbstractFloat && v[:scale] == :log
params[k] = exp.(LinRange(log(v[:min]), log(v[:max]), grid_length))
elseif v[:type] == Bool
params[k] = (false, true)
elseif v[:type] <: Integer
params[k] = v[:min]:v[:max]
elseif v[:type] == Symbol || v[:type] <: AbstractString
params[k] = v[:options]
else
error(ArgumentError("Unexpected parameter type for $k: type = $(v[:type])"))
end
end
for (k, v) in kwargs
params[k] = v
end

# Precompiling
problem = first(problems)
try
solver = Solver(problem)
output = with_logger(NullLogger()) do
solve!(solver, problem)
end
finally
finalize(problem)
end

cost(θ) = begin
total_cost = [zero(x[2]) for x in costs]
for problem in problems
reset_problem!(problem)
try
solver = Solver(problem)
P = (k => θi for (k, θi) in zip(keys(solver_params), θ))
output = with_logger(NullLogger()) do
solve!(solver, problem; P...)
end
for (i, c) in enumerate(costs)
if success(output)
total_cost[i] += (c[1])(output)
else
total_cost[i] += c[2]
end
end
catch ex
for (i, c) in enumerate(costs)
total_cost[i] += c[2]
end
@error ex
finally
finalize(problem)
end
end
total_cost
end

[θ => cost(θ) for θ in Iterators.product(values(params)...)]
end
51 changes: 28 additions & 23 deletions src/logger.jl
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
export log_header, log_row

const formats = Dict{DataType, String}(Signed => "%6d",
AbstractFloat => "%8.1e",
AbstractString => "%15s",
Symbol => "%15s",
Missing => "%15s"
)

const default_headers = Dict{Symbol, String}(:name => "Name",
:elapsed_time => "Time",
:objective => "f(x)",
:dual_feas => "Dual",
:primal_feas => "Primal")
const formats = Dict{DataType, String}(
Signed => "%6d",
AbstractFloat => "%8.1e",
AbstractString => "%15s",
Symbol => "%15s",
Missing => "%15s",
)

const default_headers = Dict{Symbol, String}(
:name => "Name",
:elapsed_time => "Time",
:objective => "f(x)",
:dual_feas => "Dual",
:primal_feas => "Primal",
)

for (typ, fmt) in formats
hdr_fmt_foo = Symbol("header_formatter_$typ")
len = match(r"\%([0-9]*)", fmt)[1]
fmt2 = "%$(len)s"

@eval begin
row_formatter(x :: $typ) = @sprintf($fmt, x)
row_formatter(:: Type{<:$typ}) = @sprintf($fmt2, "-")
row_formatter(x::$typ) = @sprintf($fmt, x)
row_formatter(::Type{<:$typ}) = @sprintf($fmt2, "-")

$hdr_fmt_foo(x) = @sprintf($fmt2, x)
header_formatter(x :: Union{Symbol,String}, :: Type{<:$typ}) = $hdr_fmt_foo(x)
header_formatter(x::Union{Symbol, String}, ::Type{<:$typ}) = $hdr_fmt_foo(x)
end
end

Expand All @@ -44,11 +47,13 @@ Keyword arguments:

See also [`log_row`](@ref).
"""
function log_header(colnames :: AbstractVector{Symbol}, coltypes :: AbstractVector{DataType};
hdr_override :: Dict{Symbol,String} = Dict{Symbol,String}(),
colsep :: Int = 2,
)
out = ""
function log_header(
colnames::AbstractVector{Symbol},
coltypes::AbstractVector{DataType};
hdr_override::Dict{Symbol, String} = Dict{Symbol, String}(),
colsep::Int = 2,
)
out_vec = String[]
for (name, typ) in zip(colnames, coltypes)
x = if haskey(hdr_override, name)
hdr_override[name]
Expand All @@ -57,9 +62,9 @@ function log_header(colnames :: AbstractVector{Symbol}, coltypes :: AbstractVect
else
string(name)
end
out *= header_formatter(x, typ) * " "^colsep
push!(out_vec, header_formatter(x, typ))
end
return out
return join(out_vec, " "^colsep)
end

"""
Expand All @@ -82,7 +87,7 @@ Prints
Keyword arguments:
- `colsep::Int`: Number of spaces between columns (Default: 2)
"""
function log_row(vals; colsep :: Int = 2)
function log_row(vals; colsep::Int = 2)
string_cols = (row_formatter(val) for val in vals)
return join(string_cols, " "^colsep)
end
41 changes: 41 additions & 0 deletions src/output.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export AbstractSolverOutput, solver_output_type

solver_output_type(::Type{S}) where S <: AbstractSolver = error("Output type not defined for $S")
solver_output_type(::S) where S <: AbstractSolver = solver_output_type(S)

# TODO: Define the required fields and API for all Outputs
"""
AbstractSolverOutput{T,S}

Base type for output of JSO-compliant solvers.
An output must have at least the following:
- `status :: Symbol`
- `solution`

The type `T` is used for element types of the arrays, and `S` is used for the storage container type.
"""
abstract type AbstractSolverOutput{T, S} end

# TODO: Decision: Should STATUSES be fixed? Should it be all here?
const STATUSES = Dict(
:exception => "unhandled exception",
:first_order => "first-order stationary",
:acceptable => "solved to within acceptable tolerances",
:infeasible => "problem may be infeasible",
:max_eval => "maximum number of function evaluations",
:max_iter => "maximum iteration",
:max_time => "maximum elapsed time",
:neg_pred => "negative predicted reduction",
:not_desc => "not a descent direction",
:small_residual => "small residual",
:small_step => "step too small",
:stalled => "stalled",
:unbounded => "objective function may be unbounded from below",
:unknown => "unknown",
:user => "user-requested stop",
)

function Base.show(io::IO, output::AbstractSolverOutput)
println(io, "Solver output of type $(typeof(output))")
println(io, "Status: $(STATUSES[output.status])")
end
Loading