From 7d55ca2cec452b66b66feb8c97fe956fad5c733a Mon Sep 17 00:00:00 2001 From: Alexey Stukalov Date: Fri, 16 Jun 2023 18:05:46 -0700 Subject: [PATCH 1/3] Project.toml: allow StatsBase 0.34 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index fb3320ac..30135fe2 100644 --- a/Project.toml +++ b/Project.toml @@ -22,7 +22,7 @@ Distributions = "0.24, 0.25" HTTP = "0.9, 1" JSON = "0.21" SpatialIndexing = "0.1.0" -StatsBase = "0.33" +StatsBase = "0.33, 0.34" julia = "1.3.0" [extras] From 01a3a13cf618b3901a144a038f86d5bd13145e7b Mon Sep 17 00:00:00 2001 From: Alexey Stukalov Date: Fri, 16 Jun 2023 18:36:49 -0700 Subject: [PATCH 2/3] refactor realtime plot, make HTTP optional - convert VegaLiteGraphFitnessGraph into RealtimePlot - make HTTP dependency optional - move HTTP server specific code to RealtimePlotServerExt extension (proper extension on 1.9, uses Requires on earliear Julia versions) - update real-time plot example --- Project.toml | 11 +- examples/vega_lite_fitness_graph_frontend.jl | 25 ---- examples/vega_lite_fitness_plot_frontend.jl | 19 +++ ext/BlackBoxOptimRealtimePlotServerExt.jl | 90 ++++++++++++ src/BlackBoxOptim.jl | 16 ++- src/gui/realtime_plot.jl | 91 ++++++++++++ src/gui/vega_lite_fitness_graph.jl | 143 ------------------- 7 files changed, 224 insertions(+), 171 deletions(-) delete mode 100644 examples/vega_lite_fitness_graph_frontend.jl create mode 100644 examples/vega_lite_fitness_plot_frontend.jl create mode 100644 ext/BlackBoxOptimRealtimePlotServerExt.jl create mode 100644 src/gui/realtime_plot.jl delete mode 100644 src/gui/vega_lite_fitness_graph.jl diff --git a/Project.toml b/Project.toml index 30135fe2..e2174862 100644 --- a/Project.toml +++ b/Project.toml @@ -7,14 +7,21 @@ CPUTime = "a9c8d775-2e2e-55fc-8582-045d282d599e" Compat = "34da2185-b29b-5c13-b0c7-acf172513d20" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" -HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +[weakdeps] +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" + +[extensions] +BlackBoxOptimRealtimePlotServerExt = ["HTTP", "Sockets"] + [compat] CPUTime = "1.0" Compat = "3.27, 4" @@ -28,12 +35,14 @@ julia = "1.3.0" [extras] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] diff --git a/examples/vega_lite_fitness_graph_frontend.jl b/examples/vega_lite_fitness_graph_frontend.jl deleted file mode 100644 index a6c8612a..00000000 --- a/examples/vega_lite_fitness_graph_frontend.jl +++ /dev/null @@ -1,25 +0,0 @@ -using BlackBoxOptim - -# Create the fitness graph object and serve it -const Vlg = BlackBoxOptim.VegaLiteGraphFitnessGraph(false) -BlackBoxOptim.serve(Vlg) - -# We will use a callback function to add fitness data to the graph. -# Since no "Time" is given the graph object will set time 0.0 for the first -# time we add data and then will count relative to that. -function callback(oc) - BlackBoxOptim.add_data!(Vlg, Dict("Fitness" => best_fitness(oc))) -end - -# Func to optimize. -function rosenbrock(x) - sum(i -> 100*abs2(x[i+1] - x[i]^2) + abs2(x[i] - 1), Base.OneTo(length(x)-1)) -end - -# Now optimize for 2 minutes. -# Go to http://127.0.0.1:8081 to view fitness progress! -res = bboptimize(rosenbrock; - SearchRange=(-10.0,10.0), NumDimensions = 500, - PopulationSize=100, MaxTime=2*60.0, - CallbackFunction = callback, CallbackInterval = 2.0); -println("Best fitness = ", best_fitness(res)) \ No newline at end of file diff --git a/examples/vega_lite_fitness_plot_frontend.jl b/examples/vega_lite_fitness_plot_frontend.jl new file mode 100644 index 00000000..6d6de0c5 --- /dev/null +++ b/examples/vega_lite_fitness_plot_frontend.jl @@ -0,0 +1,19 @@ +using BlackBoxOptim, HTTP, Sockets + +# Create the fitness plot object and serve it +const vlplot = BlackBoxOptim.VegaLiteMetricOverTimePlot(verbose=false) +HTTP.serve!(vlplot) + +# Func to optimize. +function rosenbrock(x) + sum(i -> 100*abs2(x[i+1] - x[i]^2) + abs2(x[i] - 1), Base.OneTo(length(x)-1)) +end + +# Now optimize for 2 minutes. +# Go to http://127.0.0.1:8081 to view fitness progress! +res = bboptimize(rosenbrock; + SearchRange=(-10.0,10.0), NumDimensions = 500, + PopulationSize=100, MaxTime=2*60.0, + CallbackFunction = Base.Fix1(BlackBoxOptim.fitness_plot_callback, vlplot), + CallbackInterval = 2.0); +println("Best fitness = ", best_fitness(res)) diff --git a/ext/BlackBoxOptimRealtimePlotServerExt.jl b/ext/BlackBoxOptimRealtimePlotServerExt.jl new file mode 100644 index 00000000..8e66cad1 --- /dev/null +++ b/ext/BlackBoxOptimRealtimePlotServerExt.jl @@ -0,0 +1,90 @@ +module BlackBoxOptimRealtimePlotServerExt + +using HTTP, Sockets, JSON +using BlackBoxOptim: RealtimePlot, replace_template_param, hasnewdata, printmsg + +const VegaLiteWebsocketFrontEndTemplate = """ + + + + + + + + + +
+ + + +""" + +rand_websocket_port() = 9000 + rand(0:42) + +frontend_html(vegalitespec::String, socketport::Integer) = + reduce(replace_template_param, [ + :SOCKETPORT => string(socketport), + :VEGALITESPEC => vegalitespec, + ], init = VegaLiteWebsocketFrontEndTemplate) + +function static_content_handler(content::AbstractString, request::HTTP.Request) + try + return HTTP.Response(content) + catch e + return HTTP.Response(404, "Error: $e") + end +end + +function HTTP.serve(plot::RealtimePlot{:VegaLite}, + host=Sockets.localhost, port::Integer = 8081; + websocketport::Integer = rand_websocket_port(), + mindelay::Number = 1.0, + kwargs... +) + @assert mindelay > 0.0 + @async websocket_serve(plot, websocketport, mindelay) + printmsg(plot, "Serving VegaLite frontend on http://$(host):$(port)") + return HTTP.serve(Base.Fix1(static_content_handler, frontend_html(plot.spec, websocketport)), + host, port; kwargs...) +end + +HTTP.serve!(plot::RealtimePlot, args...; kwargs...) = + @async(HTTP.serve(plot, args...; kwargs...)) + +function websocket_serve(plot::RealtimePlot, port::Integer, mindelay::Number) + HTTP.WebSockets.listen(Sockets.localhost, UInt16(port)) do ws + while true + if hasnewdata(plot) + len = length(plot.data) + newdata = plot.data[(plot.last_sent_index+1):len] + printmsg(plot, "Sending data $newdata") + HTTP.WebSockets.send(ws, JSON.json(newdata)) + plot.last_sent_index = len + end + isnothing(plot.stoptime) || break + sleep(mindelay + rand()) + end + end + printmsg(plot, "RealtimePlot websocket stopped") +end + +end \ No newline at end of file diff --git a/src/BlackBoxOptim.jl b/src/BlackBoxOptim.jl index 17d057fd..4d7f9b2e 100644 --- a/src/BlackBoxOptim.jl +++ b/src/BlackBoxOptim.jl @@ -86,6 +86,10 @@ export Optimizer, AskTellOptimizer, SteppingOptimizer, PopulationOptimizer, FrequencyAdapter, update!, frequencies, name +if !isdefined(Base, :get_extension) + using Requires +end + module Utils using Random @@ -150,7 +154,15 @@ include("compare_optimizers.jl") include(joinpath("problems", "single_objective.jl")) include(joinpath("problems", "multi_objective.jl")) -# GUIs and front-ends -include(joinpath("gui", "vega_lite_fitness_graph.jl")) +# GUIs and front-ends (to really use it, one needs HTTP to enable BlackBoxOptimRealtimePlotServerExt) +include(joinpath("gui", "realtime_plot.jl")) + +@static if !isdefined(Base, :get_extension) + function __init__() + @require Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" begin + @require HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" include("../ext/BlackBoxOptimRealtimePlotServerExt.jl") + end + end +end end # module BlackBoxOptim diff --git a/src/gui/realtime_plot.jl b/src/gui/realtime_plot.jl new file mode 100644 index 00000000..09a19839 --- /dev/null +++ b/src/gui/realtime_plot.jl @@ -0,0 +1,91 @@ +replace_template_param(template::AbstractString, param_to_value::Pair{Symbol, <:Any}) = + replace(template, string("%%", first(param_to_value), "%%") => string(last(param_to_value))) + +""" +Specification of a plot for the real-time tracking of the fitness progress. + +To use the [*VegaLite*](https://vega.github.io/vega-lite/) front-end via +*BlackBoxOptimRealtimePlotServerExt* extension, *HTTP.jl* and *Sockets.jl* are required. +""" +mutable struct RealtimePlot{E} + spec::String + verbose::Bool + data::Vector{Any} + last_sent_index::Int + starttime::Float64 + stoptime::Union{Float64, Nothing} + + function RealtimePlot{E}(template::AbstractString; + verbose::Bool = false, + spec_kwargs... + ) where E + @assert E isa Symbol + spec = reduce(replace_template_param, spec_kwargs, + init = template) + new{E}(spec, verbose, Any[], 0, 0.0, nothing) + end +end + +timestamp(t = time()) = Libc.strftime("%Y-%m-%d %H:%M.%S", t) +printmsg(plot::RealtimePlot, msg) = plot.verbose ? println(timestamp(), ": ", msg) : nothing + +function shutdown!(plot::RealtimePlot) + plot.stoptime = time() +end + +function Base.push!(plot::RealtimePlot, newentry::AbstractDict) + if length(plot.data) < 1 + plot.starttime = time() + end + if !haskey(newentry, "Time") + newentry["Time"] = time() - plot.starttime + end + printmsg(plot, "Adding data $newentry") + push!(plot.data, newentry) +end + +hasnewdata(plot::RealtimePlot) = length(plot.data) > plot.last_sent_index + +const VegaLiteMetricOverTimePlotTemplate = """ +{ + "\$schema": "https://vega.github.io/schema/vega-lite/v4.json", + "description": "%%metric%% value over time", + "width": %%width%%, + "height": %%height%%, + "padding": {"left": 20, "top": 10, "right": 10, "bottom": 20}, + "data": { + "name": "table" + }, + "mark": "line", + "encoding": { + "x": { + "field": "Time", + "type": "quantitative" + }, + "y": { + "field": "%%metric%%", + "type": "quantitative", + "scale": {"type": "log"} + } + } +} +""" + +VegaLiteMetricOverTimePlot(; metric::String = "Fitness", + width::Integer = 800, height::Integer = 600, + kwargs...) = + RealtimePlot{:VegaLite}(VegaLiteMetricOverTimePlotTemplate; metric, width, height, kwargs...) + +""" + fitness_plot_callback(plot::RealtimePlot, oc::OptRunController) + +[OptController](@ref) callback function that updates the real-time fitness plot. +""" +function fitness_plot_callback(plot::RealtimePlot, oc::OptRunController) + push!(plot, Dict("num_steps" => num_steps(oc), + "Fitness" => best_fitness(oc))) + if oc.stop_reason != "" + @info "Shutting down realtime plot" + shutdown!(plot) + end +end diff --git a/src/gui/vega_lite_fitness_graph.jl b/src/gui/vega_lite_fitness_graph.jl deleted file mode 100644 index 2594fb87..00000000 --- a/src/gui/vega_lite_fitness_graph.jl +++ /dev/null @@ -1,143 +0,0 @@ -using HTTP, JSON - -const VegaLiteWebsocketFrontEndTemplate = """ - - - - - - - - - -
- - - -""" - -const VegaLiteFitnessOverTimeSpecTemplate = """ -{ - "\$schema": "https://vega.github.io/schema/vega-lite/v4.json", - "description": "Fitness value over time", - "width": %%WIDTH%%, - "height": %%HEIGHT%%, - "padding": {"left": 20, "top": 10, "right": 10, "bottom": 20}, - "data": { - "name": "table" - }, - "mark": "line", - "encoding": { - "x": { - "field": "Time", - "type": "quantitative" - }, - "y": { - "field": "Fitness", - "type": "quantitative", - "scale": {"type": "log"} - } - } -} -""" - -rand_websocket_port() = 9000 + rand(0:42) - -function vegalite_websocket_frontend(vegalitespec::String = VegaLiteFitnessOverTimeSpecTemplate; - width::Int = 800, height::Int = 600, port::Int = rand_websocket_port()) - res = replace(VegaLiteWebsocketFrontEndTemplate, "%%VEGALITESPEC%%" => vegalitespec) - res = replace(res, "%%WIDTH%%" => string(width)) - res = replace(res, "%%HEIGHT%%" => string(height)) - replace(res, "%%SOCKETPORT%%" => string(port)) -end - -function serve_html_file(file::String) - HTTP.serve() do request::HTTP.Request - try - return HTTP.Response(file) - catch e - return HTTP.Response(404, "Error: $e") - end - end -end - -# Serve a VegaLite front-end on html and then push updates to data -# over a websocket so that the VegaLite graph is updated. -mutable struct VegaLiteGraphFitnessGraph - verbose::Bool - httpport::Int - websocketport::Int - mindelay::Float64 - data::Vector{Any} - last_sent_index::Int - starttime::Float64 - function VegaLiteGraphFitnessGraph(verbose::Bool = true, - httpport::Int = 8081, websocketport::Int = 9000, mindelay::Float64 = 1.0) - @assert mindelay > 0.0 - new(verbose, httpport, websocketport, mindelay, Any[], 0, 0.0) - end -end - -timestamp(t = time()) = Libc.strftime("%Y-%m-%d %H:%M.%S", t) -printmsg(vlg::VegaLiteGraphFitnessGraph, msg) = vlg.verbose ? println(timestamp(), ": ", msg) : nothing - -function add_data!(vlg::VegaLiteGraphFitnessGraph, newentry::Dict) - if length(vlg.data) < 1 - vlg.starttime = time() - end - if !haskey(newentry, "Time") - newentry["Time"] = time() - vlg.starttime - end - printmsg(vlg, "Adding data $newentry") - push!(vlg.data, newentry) -end - -function serve(vlg::VegaLiteGraphFitnessGraph) - @async websocket_push_data_loop(vlg.websocketport, vlg.mindelay) do ws - send_new_data_on_socket(vlg, ws) - end - frontend_html = vegalite_websocket_frontend(; port = vlg.websocketport) - @async serve_html_file(frontend_html) - println("Serving frontend on http://127.0.0.1:$(vlg.httpport)") - sleep(3.0) # To give people time to copy the url... -end - -hasnewdata(vlg::VegaLiteGraphFitnessGraph) = length(vlg.data) > vlg.last_sent_index - -function send_new_data_on_socket(vlg::VegaLiteGraphFitnessGraph, ws) - if hasnewdata(vlg) - len = length(vlg.data) - newdata = vlg.data[(vlg.last_sent_index+1):len] - printmsg(vlg, "Sending data $newdata") - write(ws, JSON.json(newdata)) - vlg.last_sent_index = len - end -end - -function websocket_push_data_loop(fn::Function, wsport::Int, mindelay = 1.0) - HTTP.WebSockets.listen("127.0.0.1", UInt16(wsport)) do ws - while true - fn(ws) # Call with websocket - sleep(mindelay + rand()) - end - end -end From e5c8f82b6afa5b4c8a4b6a6ebf75e64368ec753e Mon Sep 17 00:00:00 2001 From: Alexey Stukalov Date: Sat, 17 Jun 2023 15:55:31 -0700 Subject: [PATCH 3/3] bump package version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e2174862..9522bc0e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "BlackBoxOptim" uuid = "a134a8b2-14d6-55f6-9291-3336d3ab0209" -version = "0.6.2" +version = "0.6.3" [deps] CPUTime = "a9c8d775-2e2e-55fc-8582-045d282d599e"