[ANN] SimulationLogger.jl: a convenient logging tool for DifferentialEquations.jl

NOTE

  1. This package is now being registered in General.
  2. This package is tested only with ODEs.

Hi, you all!

I’m glad to share a package SimulationLogger.jl.
This package provides convenient logging macros compatible with DifferentialEquations.jl. Thanks to @jonniedie as the basic form of this package is based on SimulationLogs.jl.

Although this package is not yet mature, from several discussions such as the original question and the idea of this package, I found that this functionality would also be convenient for other users.

The following examples and instruction are borrowed from the original README.md.

Have a nice day!

TL; DR: example code

Example 1: typical usage (see FlightSims.jl for details)

using FlightSims
const FS = FlightSims
using DifferentialEquations
using LinearAlgebra
using Plots


function test()
    # linear system
    A = [0 1;
         0 0]
    B = [0;
         1]
    n, m = 2, 1
    env = LinearSystemEnv(A, B)  # exported from FlightSims
    x0 = State(env)([1.0, 2.0])
    p0 = zero.(x0)  # auxiliary parameter
    # optimal control
    Q = Matrix(I, n, n)
    R = Matrix(I, m, m)
    lqr = LQR(A, B, Q, R)  # exported from FlightSims
    u_lqr = FS.OptimalController(lqr)  # (x, p, t) -> -K*x; minimise J = ∫ (x' Q x + u' R u) from 0 to ∞

    # simulation
    tf = 10.0
    Δt = 0.01
    affect!(integrator) = integrator.p = copy(integrator.u)  # auxiliary callback
    cb = PeriodicCallback(affect!, Δt; initial_affect=true)
    @Loggable function dynamics!(dx, x, p, t; u)
        @onlylog p  # activate this line only when logging data
        @log state = x
        @log input = u
        # nested logging
        @nested_log :linear state_square = x .^ 2  # to put additional data into the same symbol (:linear)
        @nested_log :linear Dynamics!(env)(dx, x, p, t; u=u)
    end
    prob, df = sim(
                   x0,  # initial condition
                   # apply_inputs(Dynamics!(env); u=u_lqr),  # dynamics with input of LQR
                   apply_inputs(dynamics!; u=u_lqr),  # dynamics with input of LQR
                   p0;
                   tf=tf,  # final time
                   callback=cb,
                   savestep=Δt,
                  )
    p_x = plot(df.time, hcat(df.state...)';
               title="state variable", label=["x1" "x2"], color=[:black :black], lw=1.5,
              )  # Plots
    plot!(p_x, df.time, hcat(df.p...)';
          ls=:dash, label="param", color=[:red :orange], lw=1.5
         )
    savefig("figures/x_lqr.png")
    plot(df.time, hcat(df.input...)'; title="control input", label="u")  # Plots
    savefig("figures/u_lqr.png")
    df
end

x_lqr
u_lqr

Example 2: low-level usage

using SimulationLogger
using DifferentialEquations
using Transducers
using Plots


function test()
    @Loggable function dynamics!(dx, x, p, t)
        @log x
        @log u = -x
        @onlylog state = x
        @onlylog input = u
        dx .= u
    end
    t0, tf = 0.0, 10.0
    Δt = 0.01
    log_func(x, t, integrator::DiffEqBase.DEIntegrator; kwargs...) = dynamics!(zero.(x), copy(x), integrator.p, t, __LOG_INDICATOR__(); kwargs...)
    saved_values = SavedValues(Float64, Dict)
    cb = SavingCallback(log_func, saved_values;
                        saveat=t0:Δt:tf)
    # # sim
    x0 = [1, 2, 3]
    tspan = (t0, tf)
    prob = ODEProblem(
                      dynamics!, x0, tspan;
                      callback=cb,
                     )
    _ = solve(prob)
    ts = saved_values.t
    xs = saved_values.saveval |> Map(datum -> datum[:state]) |> collect
    us = saved_values.saveval |> Map(datum -> datum[:input]) |> collect
    p_x = plot(ts, hcat(xs...)')
    p_u = plot(ts, hcat(us...)')
    dir_log = "figures"
    mkpath(dir_log)
    savefig(p_x, joinpath(dir_log, "state.png"))
end

state

Main macros

@Loggable

@Loggable is a macro that makes an ODE function loggable.

Example

@Loggable function dynamics!(dx, x, p, t)
    dx .= -x
end

Mechanism

@Loggable generates additional method for the generic function of the annotated function definition.
The additional method receives __log__indicator__::__LOG_INDICATOR__ as the last argument (other arguments are the same as the original function definition).

Notice

  • This macro should be used in front of “function definition”. For example,
@Loggable function dynamics!(dx, x, p, t)
    dx .= -x
end

is good.

@Loggable dynamics! = (dx, x, p, t) -> dx .= -x

may not work properly.

  • Functions annotated by @Loggable MUST NOT have return keyword. For example,
@Loggable function dynamics!(dx, x, p, t)
    dx .= -x
    nothing
end

works fine, but the logging functionality with return, for example,

@Loggable function dynamics!(dx, x, p, t)
    dx .= -x
    return nothing
end

may behave poorly.

@log

This macro logs the annotated variable, and also executes the followed expression when both solving DEProblem and logging data.

Example

@Loggable function dynamics!(dx, x, p, t)
    @log state = x
    @log p  # the same as `@log p = p`
    dx .= -x
end
6 Likes

(Update)

  • annoying return convention is now resolved.
  • v0.1.0 is released.
    • v0.1.1 is being registered (including bug fixes).