diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 36f262b35..174a389a6 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -33,6 +33,7 @@ jobs: - OptimizationOptimJL - OptimizationOptimisers - OptimizationPRIMA + - OptimizationPyCMA - OptimizationQuadDIRECT - OptimizationSpeedMapping - OptimizationPolyalgorithms diff --git a/lib/OptimizationPyCMA/CondaPkg.toml b/lib/OptimizationPyCMA/CondaPkg.toml new file mode 100644 index 000000000..95582d2f6 --- /dev/null +++ b/lib/OptimizationPyCMA/CondaPkg.toml @@ -0,0 +1,3 @@ +[deps] +matplotlib = "" +cma = "" diff --git a/lib/OptimizationPyCMA/Project.toml b/lib/OptimizationPyCMA/Project.toml new file mode 100644 index 000000000..4ae3bdf77 --- /dev/null +++ b/lib/OptimizationPyCMA/Project.toml @@ -0,0 +1,18 @@ +name = "OptimizationPyCMA" +uuid = "fb0822aa-1fe5-41d8-99a6-e7bf6c238d3b" +authors = ["Maximilian Pochapski <67759684+mxpoch@users.noreply.github.com>"] +version = "0.1.0" + +[deps] +CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" +Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" +Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +CondaPkg = "0.2.29" +Optimization = "4.4.0" +PythonCall = "0.9.25" +Reexport = "1.2.2" +Test = "1.11.0" diff --git a/lib/OptimizationPyCMA/src/OptimizationPyCMA.jl b/lib/OptimizationPyCMA/src/OptimizationPyCMA.jl new file mode 100644 index 000000000..a8048937e --- /dev/null +++ b/lib/OptimizationPyCMA/src/OptimizationPyCMA.jl @@ -0,0 +1,152 @@ +module OptimizationPyCMA + +using Reexport +@reexport using Optimization +using PythonCall, Optimization.SciMLBase + +export PyCMAOpt + +struct PyCMAOpt end + +# importing PyCMA +const cma = Ref{Py}() +function get_cma() + if !isassigned(cma) || cma[] === nothing + cma[] = pyimport("cma") + end + return cma[] +end + +# Defining the SciMLBase interface for PyCMAOpt + +SciMLBase.allowsbounds(::PyCMAOpt) = true +SciMLBase.supports_opt_cache_interface(opt::PyCMAOpt) = true +SciMLBase.requiresgradient(::PyCMAOpt) = false +SciMLBase.requireshessian(::PyCMAOpt) = false +SciMLBase.requiresconsjac(::PyCMAOpt) = false +SciMLBase.requiresconshess(::PyCMAOpt) = false + +# wrapping Optimization.jl args into a python dict as arguments to PyCMA opts +function __map_optimizer_args(prob::OptimizationCache, opt::PyCMAOpt; + maxiters::Union{Number, Nothing} = nothing, + maxtime::Union{Number, Nothing} = nothing, + abstol::Union{Number, Nothing} = nothing, + reltol::Union{Number, Nothing} = nothing) + if !isnothing(reltol) + @warn "common reltol is currently not used by $(opt)" + end + + mapped_args = Dict( + "verbose" => -5, + "bounds" => (prob.lb, prob.ub), + ) + + if !isnothing(abstol) + mapped_args["tolfun"] = abstol + end + + if !isnothing(reltol) + mapped_args["tolfunrel"] = reltol + end + + if !isnothing(maxtime) + mapped_args["timeout"] = maxtime + end + + if !isnothing(maxiters) + mapped_args["maxiter"] = maxiters + end + + return mapped_args +end + +function __map_pycma_retcode(stop_dict::Dict{String, Any}) + # mapping termination conditions to SciMLBase return codes + if any(k ∈ keys(stop_dict) for k in ["ftarget", "tolfun", "tolx"]) + return ReturnCode.Success + elseif any(k ∈ keys(stop_dict) for k in ["maxiter", "maxfevals"]) + return ReturnCode.MaxIters + elseif "timeout" ∈ keys(stop_dict) + return ReturnCode.MaxTime + elseif "callback" ∈ keys(stop_dict) + return ReturnCode.Terminated + elseif any(k ∈ keys(stop_dict) for k in ["tolupsigma", "tolconditioncov", "noeffectcoord", "noeffectaxis", "tolxstagnation", "tolflatfitness", "tolfacupx", "tolstagnation"]) + return ReturnCode.Failure + else + return ReturnCode.Default + end +end + +function SciMLBase.__solve(cache::OptimizationCache{ + F, + RC, + LB, + UB, + LC, + UC, + S, + O, + D, + P, + C +}) where { + F, + RC, + LB, + UB, + LC, + UC, + S, + O <: + PyCMAOpt, + D, + P, + C +} + local x + + # doing conversions + maxiters = Optimization._check_and_convert_maxiters(cache.solver_args.maxiters) + maxtime = Optimization._check_and_convert_maxtime(cache.solver_args.maxtime) + + # wrapping the objective function + _loss = function (θ) + x = cache.f(θ, cache.p) + return first(x) + end + + # converting the Optimization.jl Args to PyCMA format + opt_args = __map_optimizer_args(cache, cache.opt; cache.solver_args..., + maxiters = maxiters, + maxtime = maxtime) + + # init the CMAopt class + es = get_cma().CMAEvolutionStrategy(cache.u0, 1, pydict(opt_args)) + logger = es.logger + + # running the optimization + t0 = time() + opt_res = es.optimize(_loss) + t1 = time() + + # loading logged files from disk + logger.load() + + # reading the results + opt_ret_dict = opt_res.stop() + retcode = __map_pycma_retcode(pyconvert(Dict{String, Any}, opt_ret_dict)) + + # logging and returning results of the optimization + stats = Optimization.OptimizationStats(; + iterations = length(logger.xmean), + time = t1 - t0, + fevals = length(logger.xmean)) + + SciMLBase.build_solution(cache, cache.opt, + pyconvert(Float64, logger.xrecent[-1][-1]), + pyconvert(Float64, logger.f[-1][-1]); original = opt_res, + retcode = retcode, + stats = stats) +end + +end # module OptimizationPyCMA diff --git a/lib/OptimizationPyCMA/test/runtests.jl b/lib/OptimizationPyCMA/test/runtests.jl new file mode 100644 index 000000000..e01f74432 --- /dev/null +++ b/lib/OptimizationPyCMA/test/runtests.jl @@ -0,0 +1,14 @@ +using OptimizationPyCMA +using Test + +@testset "OptimizationPyCMA.jl" begin + rosenbrock(x, p) = (p[1] - x[1])^2 + p[2] * (x[2] - x[1]^2)^2 + x0 = zeros(2) + _p = [1.0, 100.0] + l1 = rosenbrock(x0, _p) + f = OptimizationFunction(rosenbrock) + prob = OptimizationProblem(f, x0, _p, lb = [-1.0, -1.0], ub = [0.8, 0.8]) + sol = solve(prob, PyCMAOpt()) + @test 10 * sol.objective < l1 + sol = solve(prob, PyCMAOpt(), maxiters = 100) +end