I have a relatively complicated function to produce values and a relatively complicated function to plot the values. The combination already works to produce a still image.
Now, I want to produce an animation changing a parameter, say time t. Both functions depend on t. Both functions include some arithmetics on t.
When I turn this program into one using Observables as below, the complier complains that some functions I use aren’t defined on Observable{Float64}, which the new t is.
The following is a much simplified toy program that (I hope correctly) duplicates my problem. Below, the function single() succeeds in producing the still image but anime() fails for the reason stated above.
How should I proceed? I’m stuck because I don’t understand Observables after all.
"Returns a vector"
function func(t, xs)
sin.(t/100.0 .+ xs)
end
function plot_func(xs, fs; t)
lab = "$(t/(24*60*60)) days"
fig = Figure()
ax = Axis(fig[1,1]; title = lab)
lines!(ax, xs, fs)
return fig
end
function single() # -> Works
xs = -5.0:(5/32):5.0
t = 0.0
fs = func.(t, xs)
fig = plot_func(xs, fs; t = t)
display(fig)
end
# Fails because Observable{Float64} cannot be used in calculations
function anime()
xs = -5.0:(5/32):5.0
t = Observable(0.0) # -> t/100.0 fails?
fs = Observable(func.(t, xs))
fig = plot_func(xs, fs; t = t)
nrange = 0:20
record(fig, "tmp.mp4", nrange; framerate=2) do n
t.val = 40.0*n
fs[] = func.(t, xs)
end
end
single()
anime() # -> fails
To operate on the value of an Observable, you need to use map or @lift to unwrap it, use the value, and create a new observable. There is no need to explicitly evaluate fs inside the animation, setting t pushes the update through.
using GLMakie
"Returns a vector"
function func(t, xs)
sin.(t/100.0 .+ xs)
end
function plot_func(xs, fs; t)
lab = map(t) do t
"$(t/(24*60*60)) days"
end
fig = Figure()
ax = Axis(fig[1,1]; title = lab)
lines!(ax, xs, fs)
return fig
end
function single() # -> Works
xs = -5.0:(5/32):5.0
t = 0.0
fs = func.(t, xs)
fig = plot_func(xs, fs; t )
display(fig)
end
function anime()
xs = -5.0:(5/32):5.0
t = Observable(0.0)
fs = @lift( func.($t, xs) )
fig = plot_func(xs, fs; t)
nrange = 0:20
record(fig, "tmp.mp4", nrange; framerate=2) do n
t[] = 40.0*n
end
end
single()
anime()
Aha! Thank you for your kind explanation! That’s exactly what I wanted to know. (I thought: This Observable thing looks like a monad or functor or something along the lines . . . there must be some functionality to unwrap and wrap . . . )
Does that mean that within the Makie code, map is always used before any calculations because any of the arguments can by an Observable ?
Or is there a way to write your code that works whether the arguments are Observables or not without using map all the time? I can imagine that you could build a mechanism to automatically generate the unwrap-calculate-wrap version of a function:
f(x::Float64) = 2*x
#--> generate the following
f(x::Observable{Float64}) = @lift(f($x))
Another question: Is the macro form @lift( . . . $t . . . ) provided only as a convenience? Or is it more general than map? Perhaps you can use multiple Observables within @lift ?
For reference, the following is my final solution. (I’ve also fixed my little awkward func function.)
using GLMakie
function func(t, x)
sin(t/100.0 + x)
end
"`t` can be an Observable."
function plot_func(xs, fs; t)
lab = map(t) do tval # unrap t, do the calculation, and wrap the result.
"$(tval/(24*60*60)) days"
end
fig = Figure()
ax = Axis(fig[1,1]; title = lab)
lines!(ax, xs, fs)
return fig
end
function single()
xs = -5.0:(5/32):5.0
t = 0.0
fs = func.(t, xs)
fig = plot_func(xs, fs; t = t)
display(fig)
end
function anime()
xs = -5.0:(5/32):5.0
t = Observable(0.0)
# fs = @lift( func.($t, xs) ) # same as below?
fs = map(tval -> func.(tval,xs), t)
fig = plot_func(xs, fs; t = t)
nrange = 0:20
record(fig, "tmp.mp4", nrange; framerate=2) do n
t[] = 40.0*n
end
end
single()
anime()