Skip to content

Commit

Permalink
Merge pull request #216 from alyst/optional_http
Browse files Browse the repository at this point in the history
Refactor real-time plots, make HTTP.jl optional
  • Loading branch information
robertfeldt authored Jun 18, 2023
2 parents 35f1e73 + e5c8f82 commit b5d3d72
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 173 deletions.
15 changes: 12 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
name = "BlackBoxOptim"
uuid = "a134a8b2-14d6-55f6-9291-3336d3ab0209"
version = "0.6.2"
version = "0.6.3"

[deps]
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"
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]
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]
Expand Down
25 changes: 0 additions & 25 deletions examples/vega_lite_fitness_graph_frontend.jl

This file was deleted.

19 changes: 19 additions & 0 deletions examples/vega_lite_fitness_plot_frontend.jl
Original file line number Diff line number Diff line change
@@ -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))
90 changes: 90 additions & 0 deletions ext/BlackBoxOptimRealtimePlotServerExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module BlackBoxOptimRealtimePlotServerExt

using HTTP, Sockets, JSON
using BlackBoxOptim: RealtimePlot, replace_template_param, hasnewdata, printmsg

const VegaLiteWebsocketFrontEndTemplate = """
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/vega@3"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@2"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>
</head>
<body>
<div id="vis"></div>
<script>
const vegalitespec = %%VEGALITESPEC%%;
vegaEmbed('#vis', vegalitespec, {defaultStyle: true})
.then(function(result) {
const view = result.view;
const port = %%SOCKETPORT%%;
const conn = new WebSocket("ws://127.0.0.1:" + port);
conn.onopen = function(event) {
// insert data as it arrives from the socket
conn.onmessage = function(event) {
console.log(event.data);
// Use the Vega view api to insert data
var newentries = JSON.parse(event.data);
view.insert("table", newentries).run();
}
}
})
.catch(console.warn);
</script>
</body>
"""

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
16 changes: 14 additions & 2 deletions src/BlackBoxOptim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
91 changes: 91 additions & 0 deletions src/gui/realtime_plot.jl
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit b5d3d72

Please sign in to comment.