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

SolverTools revampt TODO list #3

Open
6 tasks
abelsiqueira opened this issue Mar 24, 2021 · 7 comments
Open
6 tasks

SolverTools revampt TODO list #3

abelsiqueira opened this issue Mar 24, 2021 · 7 comments
Labels

Comments

@abelsiqueira
Copy link
Member

Discussion is happening here: JuliaSmoothOptimizers/Organization#19

  • SolverCore
    • AbstractSolver
    • traits system
    • Better logger
  • SolverTest
    • Generalize to more problem types
@sshin23
Copy link

sshin23 commented Jun 21, 2022

Is there progress in implementing AbstractSolver? As mentioned in the discussion above, the current solver interface doesn't allow efficient re-solves, and it has been one of the blocking factors of our project.

I can work on a PR if no one is working on this. Below is a draft of AbstractNLPSolver

"""
    AbstractNLPSolver

Base type for an optimization solver.
"""
abstract type AbstractNLPSolver{T,S} end

"""
    optimize!(solver)

Call optimization solver to solve the problem.
"""
function optimize! end

"""
    get_execution_stats(solver)

Return the ExecutionStats of the solver.
"""
function get_execution_stats end

"""
    get_nlp(solver)

Return the NLPModel of the solver.
"""
function get_nlp end

"""
    get_elapsed_time(solver)

Return the elapsed time.
"""
function get_elapsed_time end

"""
    get_status(solver)

Return the solver status.
"""
function get_status end

"""
    get_objective(solver)

Return the objective value
"""
function get_objective end

"""
    get_dual_feas(solver)

Return the dual feasibility.
"""
function get_dual_feas end

"""
    get_primal_feas(solver)

Return the primal feasibility.
"""
function get_primal_feas end

"""
    get_iter(solver)

Return the iteration count.
"""
function get_iter end


"""
    get_solution(solver)

Return the solution.
"""
function get_solution end

"""
    get_multiplier(solver)

Return the multiplier.
"""
function get_multipliers end

"""
    get_multiplier_L(solver)

Return the lower bound multiplier.
"""
function get_multipliers_L end

"""
    get_multiplier_U(solver)

Return the upper bound multiplier.
"""
function get_multipliers_U end

"""
    set_solution!(solver, x)

Set the initial guess of solution.
"""
function set_solution!(solver::AbstractNLPSolver, x::AbstractVector) 
    copyto!(get_solution(solver), x)
end

"""
    set_solution!(solver, x)

Set the initial guess of multiplier.
"""
function set_multipliers!(solver::AbstractNLPSolver, mult::AbstractVector)
    copyto!(get_multipliers(solver), x)
end

"""
    set_multiplier!(solver, x)

Set the initial guess of lower bound multiplier.
"""
function set_multipliers_L!(solver::AbstractNLPSolver, mult_L::AbstractVector)
    copyto!(get_multipliers_L(solver), x)
end

"""
    set_solution!(solver, x)

Set the initial guess of upper bound multiplier.
"""
function set_multipliers_U!(solver::AbstractNLPSolver, mult_U::AbstractVector)
    copyto!(get_multipliers_U(solver), x)
end

@frapac @grishabh147

@dpo
Copy link
Member

dpo commented Jun 21, 2022

Thanks @sshin23 ! There's no AbstractNLPSolver yet, but several solvers now have a "solver object" where memory is preallocated and should allow to re-solve a problem of the same dimensions efficiently.

Also, we separate the model from the solver and from the solver results. A solver object is created from an AbstractNLPModel. Calling the solver returns a GenericExecutionStats object. The initial guess and multipliers are built into the model (though we can also pass some in when we call the solver, or the solver could compute its own initial guess).

I would say get_status, get_multiplier & Co. should apply to a GenericExecutionStats object.

cc-ing @tmigot @abelsiqueira @geoffroyleconte

@abelsiqueira
Copy link
Member Author

Our plan was to start from the bottom up, creating the solver structures for JSOSolvers (e.g. trunk) and Percival (JuliaSmoothOptimizers/Percival.jl#80) and then create an AbstractNLPSolver for which TrunkSolver <: AbstractNLPSolver, and eventually join with Krylov

Our main underlying idea is to easily benchmark solvers with SolverBenchmark and for that, we have been assuming JSO-compliance (input: AbstractNLPModels, output: GenericExecutionStats). Do you use something different?

The missing thing for better efficient reuse is storing the GenericExecutionStats object inside the solver. That would allow many of the functions you mentioned, but I am not sure that they are needed.

Let me know your opinions on the plan.

The development has slowed down a lot for me since I changed jobs in late 2021, so help is appreciated.

@sshin23
Copy link

sshin23 commented Jun 27, 2022

Thanks, @dpo and @abelsiqueira for the fast feedback! Apologies for the delay on my side.

A solver object is created from an AbstractNLPModel. Calling the solver returns a GenericExecutionStats object.

MadNLP.jl currently has the exact same structure. And I believe wrapper packages like NLPModelsIpopt.jl can be easily modified to a similar structure.

The reason that I proposed separate get_solution, get_multiplier, set_solution, etc. is to avoid creating GenericExecutionStats whenever we want to access this information. E.g., for control applications, one wants to reset the parameter & initial solution, and then resolve the problem. There's no need to recreate the GenericExecutionStats in each sampling time point.

And we can make optimize! (I think it is implemented as solve! in JSOSolvers.jl) not return GenericExecutionStats so that we don't need to create GenericExecutionStats in every resolve. But we automatically create it when trunk(nlp) or ipopt(nlp) are called.

So, a typical structure of a high-level interface would be like

function ipopt(nlp::AbstractNLPModel; kwargs...)
  solver = IpoptSolver(nlp, kwargs...)
  SolverCore.optimize!(solver)
  return SolverCore.GenericExecutionStats(solver)
end

where we have this in SolverCore.jl

function GenericExecutionStats(solver)
    GenericExecutionStats(
        get_status(solver),
        get_nlp(solver),
        solution = get_solution(solver),
        objective = get_objective(solver),
        dual_feas = get_dual_feas(solver),
        iter = get_iter(solver),
        primal_feas = get_primal_feas(solver),
        elapsed_time = get_elapsed_time(solver),
        multipliers = get_multipliers(solver),
        multipliers_L = get_multipliers_L(solver),
        multipliers_U = get_multipliers_U(solver),
        solver_specific = get_solver_specific(solver)
    )
end

Feedbacks are welcome. I can work on PRs on SolverCore.jl and NLPModelsIpopt.jl to make the discussion more concreate

@dpo
Copy link
Member

dpo commented Jun 27, 2022

Thanks @sshin23. Those are interesting suggestions. I see that there's room for improvement in our current system. In order to avoid storing data both in the solver and the stats object, I would suggest something along the lines of:

mutable struct MySolver <: AbstractSolver
  # ...
end

function solve!(solver::MySolver, model::AbstractNLPModels; kwargs...)
  stats = GenericExecutionStats(:unknown, model)
  solve!(solver, model, stats; kwargs...)
end

function solve!(solver::MySolver, model::AbstractNLPModels, stats::GenericExecutionStats; kwargs...)
  # actual solve happens
  # fill in stats object
end

You could then call solve!(solver, model, stats) repeatedly, and query the stats object. What do you think?

@sshin23
Copy link

sshin23 commented Jun 28, 2022

Thanks, @dpo for the feedback. That approach will resolve the issue of recreating GenericExecutionStats in each resolve.

One remaining question is how do we reset the initial guess of the problem. Currently, the solvers in JSOSolvers.jl resets the starting point with nlp.meta.x0 within solve!. E.g.,
https://github.com/JuliaSmoothOptimizers/JSOSolvers.jl/blob/main/src/trunk.jl#L143

If I want to resolve the problem with an updated initial guess (both primal and dual), should I update nlp.meta.x0 and nlp.meta.y0? Or should we create set_solution! and set_multiplier! functions, as suggested above, and use them? To me, it makes more sense to consider NLPModel as immutable data (though most of them are immutable types), so the latter seems to be a better approach to me. But of course, there might be angles I'm not seeing, so let me know what you think

@dpo
Copy link
Member

dpo commented Jun 28, 2022

You can pass in x0: https://github.com/JuliaSmoothOptimizers/JSOSolvers.jl/blob/main/src/trunk.jl#L120

We should make sure all solvers allow that.

This was referenced Jul 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants