Skip to content

Commit 01a3a13

Browse files
committed
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
1 parent 7d55ca2 commit 01a3a13

7 files changed

+224
-171
lines changed

Project.toml

+10-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,21 @@ CPUTime = "a9c8d775-2e2e-55fc-8582-045d282d599e"
77
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
88
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
99
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
10-
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
1110
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1211
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1312
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1413
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
14+
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
1515
SpatialIndexing = "d4ead438-fe20-5cc5-a293-4fd39a41b74c"
1616
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
1717

18+
[weakdeps]
19+
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
20+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
21+
22+
[extensions]
23+
BlackBoxOptimRealtimePlotServerExt = ["HTTP", "Sockets"]
24+
1825
[compat]
1926
CPUTime = "1.0"
2027
Compat = "3.27, 4"
@@ -28,12 +35,14 @@ julia = "1.3.0"
2835
[extras]
2936
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
3037
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
38+
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
3139
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
3240
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
3341
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
3442
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
3543
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
3644
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
45+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
3746
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3847

3948
[targets]

examples/vega_lite_fitness_graph_frontend.jl

-25
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using BlackBoxOptim, HTTP, Sockets
2+
3+
# Create the fitness plot object and serve it
4+
const vlplot = BlackBoxOptim.VegaLiteMetricOverTimePlot(verbose=false)
5+
HTTP.serve!(vlplot)
6+
7+
# Func to optimize.
8+
function rosenbrock(x)
9+
sum(i -> 100*abs2(x[i+1] - x[i]^2) + abs2(x[i] - 1), Base.OneTo(length(x)-1))
10+
end
11+
12+
# Now optimize for 2 minutes.
13+
# Go to http://127.0.0.1:8081 to view fitness progress!
14+
res = bboptimize(rosenbrock;
15+
SearchRange=(-10.0,10.0), NumDimensions = 500,
16+
PopulationSize=100, MaxTime=2*60.0,
17+
CallbackFunction = Base.Fix1(BlackBoxOptim.fitness_plot_callback, vlplot),
18+
CallbackInterval = 2.0);
19+
println("Best fitness = ", best_fitness(res))
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
module BlackBoxOptimRealtimePlotServerExt
2+
3+
using HTTP, Sockets, JSON
4+
using BlackBoxOptim: RealtimePlot, replace_template_param, hasnewdata, printmsg
5+
6+
const VegaLiteWebsocketFrontEndTemplate = """
7+
<!DOCTYPE html>
8+
<head>
9+
<meta charset="utf-8">
10+
<script src="https://cdn.jsdelivr.net/npm/vega@3"></script>
11+
<script src="https://cdn.jsdelivr.net/npm/vega-lite@2"></script>
12+
<script src="https://cdn.jsdelivr.net/npm/vega-embed@3"></script>
13+
</head>
14+
15+
<body>
16+
<div id="vis"></div>
17+
18+
<script>
19+
const vegalitespec = %%VEGALITESPEC%%;
20+
vegaEmbed('#vis', vegalitespec, {defaultStyle: true})
21+
.then(function(result) {
22+
const view = result.view;
23+
const port = %%SOCKETPORT%%;
24+
const conn = new WebSocket("ws://127.0.0.1:" + port);
25+
26+
conn.onopen = function(event) {
27+
// insert data as it arrives from the socket
28+
conn.onmessage = function(event) {
29+
console.log(event.data);
30+
// Use the Vega view api to insert data
31+
var newentries = JSON.parse(event.data);
32+
view.insert("table", newentries).run();
33+
}
34+
}
35+
})
36+
.catch(console.warn);
37+
</script>
38+
</body>
39+
"""
40+
41+
rand_websocket_port() = 9000 + rand(0:42)
42+
43+
frontend_html(vegalitespec::String, socketport::Integer) =
44+
reduce(replace_template_param, [
45+
:SOCKETPORT => string(socketport),
46+
:VEGALITESPEC => vegalitespec,
47+
], init = VegaLiteWebsocketFrontEndTemplate)
48+
49+
function static_content_handler(content::AbstractString, request::HTTP.Request)
50+
try
51+
return HTTP.Response(content)
52+
catch e
53+
return HTTP.Response(404, "Error: $e")
54+
end
55+
end
56+
57+
function HTTP.serve(plot::RealtimePlot{:VegaLite},
58+
host=Sockets.localhost, port::Integer = 8081;
59+
websocketport::Integer = rand_websocket_port(),
60+
mindelay::Number = 1.0,
61+
kwargs...
62+
)
63+
@assert mindelay > 0.0
64+
@async websocket_serve(plot, websocketport, mindelay)
65+
printmsg(plot, "Serving VegaLite frontend on http://$(host):$(port)")
66+
return HTTP.serve(Base.Fix1(static_content_handler, frontend_html(plot.spec, websocketport)),
67+
host, port; kwargs...)
68+
end
69+
70+
HTTP.serve!(plot::RealtimePlot, args...; kwargs...) =
71+
@async(HTTP.serve(plot, args...; kwargs...))
72+
73+
function websocket_serve(plot::RealtimePlot, port::Integer, mindelay::Number)
74+
HTTP.WebSockets.listen(Sockets.localhost, UInt16(port)) do ws
75+
while true
76+
if hasnewdata(plot)
77+
len = length(plot.data)
78+
newdata = plot.data[(plot.last_sent_index+1):len]
79+
printmsg(plot, "Sending data $newdata")
80+
HTTP.WebSockets.send(ws, JSON.json(newdata))
81+
plot.last_sent_index = len
82+
end
83+
isnothing(plot.stoptime) || break
84+
sleep(mindelay + rand())
85+
end
86+
end
87+
printmsg(plot, "RealtimePlot websocket stopped")
88+
end
89+
90+
end

src/BlackBoxOptim.jl

+14-2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ export Optimizer, AskTellOptimizer, SteppingOptimizer, PopulationOptimizer,
8686
FrequencyAdapter, update!, frequencies,
8787
name
8888

89+
if !isdefined(Base, :get_extension)
90+
using Requires
91+
end
92+
8993
module Utils
9094
using Random
9195

@@ -150,7 +154,15 @@ include("compare_optimizers.jl")
150154
include(joinpath("problems", "single_objective.jl"))
151155
include(joinpath("problems", "multi_objective.jl"))
152156

153-
# GUIs and front-ends
154-
include(joinpath("gui", "vega_lite_fitness_graph.jl"))
157+
# GUIs and front-ends (to really use it, one needs HTTP to enable BlackBoxOptimRealtimePlotServerExt)
158+
include(joinpath("gui", "realtime_plot.jl"))
159+
160+
@static if !isdefined(Base, :get_extension)
161+
function __init__()
162+
@require Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" begin
163+
@require HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" include("../ext/BlackBoxOptimRealtimePlotServerExt.jl")
164+
end
165+
end
166+
end
155167

156168
end # module BlackBoxOptim

src/gui/realtime_plot.jl

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
replace_template_param(template::AbstractString, param_to_value::Pair{Symbol, <:Any}) =
2+
replace(template, string("%%", first(param_to_value), "%%") => string(last(param_to_value)))
3+
4+
"""
5+
Specification of a plot for the real-time tracking of the fitness progress.
6+
7+
To use the [*VegaLite*](https://vega.github.io/vega-lite/) front-end via
8+
*BlackBoxOptimRealtimePlotServerExt* extension, *HTTP.jl* and *Sockets.jl* are required.
9+
"""
10+
mutable struct RealtimePlot{E}
11+
spec::String
12+
verbose::Bool
13+
data::Vector{Any}
14+
last_sent_index::Int
15+
starttime::Float64
16+
stoptime::Union{Float64, Nothing}
17+
18+
function RealtimePlot{E}(template::AbstractString;
19+
verbose::Bool = false,
20+
spec_kwargs...
21+
) where E
22+
@assert E isa Symbol
23+
spec = reduce(replace_template_param, spec_kwargs,
24+
init = template)
25+
new{E}(spec, verbose, Any[], 0, 0.0, nothing)
26+
end
27+
end
28+
29+
timestamp(t = time()) = Libc.strftime("%Y-%m-%d %H:%M.%S", t)
30+
printmsg(plot::RealtimePlot, msg) = plot.verbose ? println(timestamp(), ": ", msg) : nothing
31+
32+
function shutdown!(plot::RealtimePlot)
33+
plot.stoptime = time()
34+
end
35+
36+
function Base.push!(plot::RealtimePlot, newentry::AbstractDict)
37+
if length(plot.data) < 1
38+
plot.starttime = time()
39+
end
40+
if !haskey(newentry, "Time")
41+
newentry["Time"] = time() - plot.starttime
42+
end
43+
printmsg(plot, "Adding data $newentry")
44+
push!(plot.data, newentry)
45+
end
46+
47+
hasnewdata(plot::RealtimePlot) = length(plot.data) > plot.last_sent_index
48+
49+
const VegaLiteMetricOverTimePlotTemplate = """
50+
{
51+
"\$schema": "https://vega.github.io/schema/vega-lite/v4.json",
52+
"description": "%%metric%% value over time",
53+
"width": %%width%%,
54+
"height": %%height%%,
55+
"padding": {"left": 20, "top": 10, "right": 10, "bottom": 20},
56+
"data": {
57+
"name": "table"
58+
},
59+
"mark": "line",
60+
"encoding": {
61+
"x": {
62+
"field": "Time",
63+
"type": "quantitative"
64+
},
65+
"y": {
66+
"field": "%%metric%%",
67+
"type": "quantitative",
68+
"scale": {"type": "log"}
69+
}
70+
}
71+
}
72+
"""
73+
74+
VegaLiteMetricOverTimePlot(; metric::String = "Fitness",
75+
width::Integer = 800, height::Integer = 600,
76+
kwargs...) =
77+
RealtimePlot{:VegaLite}(VegaLiteMetricOverTimePlotTemplate; metric, width, height, kwargs...)
78+
79+
"""
80+
fitness_plot_callback(plot::RealtimePlot, oc::OptRunController)
81+
82+
[OptController](@ref) callback function that updates the real-time fitness plot.
83+
"""
84+
function fitness_plot_callback(plot::RealtimePlot, oc::OptRunController)
85+
push!(plot, Dict("num_steps" => num_steps(oc),
86+
"Fitness" => best_fitness(oc)))
87+
if oc.stop_reason != ""
88+
@info "Shutting down realtime plot"
89+
shutdown!(plot)
90+
end
91+
end

0 commit comments

Comments
 (0)