Other formats: This can also be accessed as a Jupyter notebook or a plain script.

Lorenz63 example

Note: The packages for this example are documented in the Project.toml, except for some unregistered packages.

First, we'll set up the environment, which entails installing and importing packages.

# Install unregistered packages.
using Pkg: Pkg

using Ensembles
try
    using Lorenz63: Lorenz63
catch
    Ensembles.install(:Lorenz63)
end

try
    using EnsembleKalmanFilters: EnsembleKalmanFilters
catch
    Ensembles.install(:EnsembleKalmanFilters)
end

try
    using NormalizingFlowFilters: NormalizingFlowFilters
catch
    Ensembles.install(:NormalizingFlowFilters)
end

# Define a macro for doing imports to avoid duplicating it for remote processes later on.
macro initial_imports()
    return esc(
        quote
            using Ensembles
            using LinearAlgebra: norm
            using Distributed: addprocs, rmprocs, @everywhere, remotecall, fetch, WorkerPool
            using Test: @test
            using Random: Random
            using CairoMakie

            using Lorenz63: Lorenz63
            ext = Base.get_extension(Ensembles, :Lorenz63Ext)
            using .ext

            using EnsembleKalmanFilters: EnsembleKalmanFilters
            ext = Base.get_extension(Ensembles, :EnsembleKalmanFiltersExt)
            using .ext

            using Statistics: Statistics, mean, var
            using LinearAlgebra: Diagonal
            using EnsembleKalmanFilters: EnKF
        end
    )
end

@initial_imports
worker_initial_imports = @macroexpand1 @initial_imports

include("../_utils/utils.jl");
     Cloning git-repo `https://github.com/milankl/Lorenz63.jl#15220a7`
    Updating git-repo `https://github.com/milankl/Lorenz63.jl#15220a7`
   Resolving package versions...
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
  [72db8b11] + Lorenz63 v0.1.0 `https://github.com/milankl/Lorenz63.jl#15220a7#master`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [72db8b11] + Lorenz63 v0.1.0 `https://github.com/milankl/Lorenz63.jl#15220a7#master`
     Cloning git-repo `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl`
    Updating git-repo `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl`
   Resolving package versions...
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
  [489b957c] + EnsembleKalmanFilters v0.0.1 `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl#main`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [489b957c] + EnsembleKalmanFilters v0.0.1 `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl#main`
Precompiling project...
  15384.2 ms  ✓ EnsembleKalmanFilters
   5884.9 ms  ✓ Ensembles → EnsembleKalmanFiltersExt
  2 dependencies successfully precompiled in 22 seconds. 338 already precompiled.
  1 dependency had output during precompilation:
┌ EnsembleKalmanFilters
│  CondaPkg Found dependencies: /home/runner/.julia/packages/JOLI/2dSnd/CondaPkg.toml
│      CondaPkg Found dependencies: /home/runner/.julia/packages/PythonCall/WMWY0/CondaPkg.toml
│      CondaPkg Resolving changes
│               + libstdcxx-ng
│               + openssl
│               + python
│               + pywavelets
│      CondaPkg Initialising pixi
│               │ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
│               │ init
│               │ --format pixi
│               └ /home/runner/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/.CondaPkg
│  ✔ Created /home/runner/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/.CondaPkg/pixi.toml
│      CondaPkg Wrote /home/runner/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/.CondaPkg/pixi.toml
│               │ [dependencies]
│               │ openssl = ">=3, <3.1"
│               │ libstdcxx-ng = ">=3.4,<13.0"
│               │ pywavelets = "*"
│               │ 
│               │     [dependencies.python]
│               │     channel = "conda-forge"
│               │     build = "*cpython*"
│               │     version = ">=3.8,<4"
│               │ 
│               │ [project]
│               │ name = ".CondaPkg"
│               │ platforms = ["linux-64"]
│               │ channels = ["conda-forge"]
│               │ channel-priority = "strict"
│               └ description = "automatically generated by CondaPkg.jl"
│      CondaPkg Installing packages
│               │ /home/runner/.julia/artifacts/cefba4912c2b400756d043a2563ef77a0088866b/bin/pixi
│               │ install
│               └ --manifest-path /home/runner/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/.CondaPkg/pixi.toml
│  ✔ The default environment has been installed.
└  
     Cloning git-repo `https://github.com/DataAssimilation/NormalizingFlowFilters.jl`
    Updating git-repo `https://github.com/DataAssimilation/NormalizingFlowFilters.jl`
   Resolving package versions...
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
⌅ [bb331ad6] ↓ JOLI v0.9.0 ⇒ v0.8.5
  [bdff3154] + NormalizingFlowFilters v0.0.1 `https://github.com/DataAssimilation/NormalizingFlowFilters.jl#main`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [4fba245c] + ArrayInterface v7.18.0
  [a9b6321e] + Atomix v1.1.1
  [ab4f0b2a] + BFloat16s v0.5.1
  [62783981] + BitTwiddlingConvenienceFunctions v0.1.6
  [2a0fbf3d] + CPUSummary v0.2.6
  [052768ef] + CUDA v5.7.2
  [1af6417a] + CUDA_Runtime_Discovery v0.3.5
  [aafaddc9] + CatIndices v0.2.2
  [082447d4] + ChainRules v1.72.3
  [fb6a15b2] + CloseOpenIntervals v0.1.13
  [bbf7d656] + CommonSubexpressions v0.3.1
  [f70d9fcc] + CommonWorldInvalidations v1.0.0
  [ed09eef8] + ComputationalResources v0.3.2
  [8f4d0f93] + Conda v1.10.2
  [992eb4ea] - CondaPkg v0.2.28
  [5218b696] + Configurations v0.17.6
  [150eb455] + CoordinateTransformations v0.6.4
  [adafc99b] + CpuId v0.3.1
  [a8cc5b0e] + Crayons v4.1.1
  [dc8bdbbb] + CustomUnitRanges v1.0.2
  [a93c6f00] + DataFrames v1.7.0
  [8bb1440f] + DelimitedFiles v1.9.1
  [163ba53b] + DiffResults v1.1.0
  [b552c78f] + DiffRules v1.15.1
  [b4f34e82] + Distances v0.10.12
  [e2ba6199] + ExprTools v0.1.10
  [55351af7] + ExproniconLite v0.10.14
  [4f61f5a4] + FFTViews v0.3.2
⌅ [587475ba] + Flux v0.14.25
  [f6369f11] + ForwardDiff v1.0.1
⌅ [d9f16b24] + Functors v0.4.12
  [0c68f7d7] + GPUArrays v11.2.2
  [46192b85] + GPUArraysCore v0.2.0
  [61eb1bfa] + GPUCompiler v1.3.2
  [096a3bc2] + GPUToolbox v0.2.0
  [076d061b] + HashArrayMappedTries v0.2.0
  [3e5b6fbb] + HostCPUFeatures v0.1.17
  [7869d1d1] + IRTools v0.4.14
  [615f187c] + IfElse v0.1.1
  [f332f351] + ImageContrastAdjustment v0.3.12
  [51556ac3] + ImageDistances v0.2.17
  [6a3955dd] + ImageFiltering v0.7.9
  [787d08f9] + ImageMorphology v0.4.6
  [2996bd0c] + ImageQualityIndexes v0.3.7
  [02fcd773] + ImageTransformations v0.10.1
  [842dd82b] + InlineStrings v1.4.3
  [41ab1584] + InvertedIndices v1.3.1
  [b7115f24] + InvertibleNetworks v2.3.0
⌅ [bb331ad6] ↓ JOLI v0.9.0 ⇒ v0.8.5
  [0f8b85d8] - JSON3 v1.14.2
  [63c18a36] + KernelAbstractions v0.9.34
  [929cbde3] + LLVM v9.2.0
  [8b046642] + LLVMLoopInfo v1.0.0
  [10f19ff3] + LayoutPointers v0.1.17
  [bdcacae8] + LoopVectorization v0.12.172
  [c2834f40] + MLCore v1.0.0
⌃ [7e8f7934] + MLDataDevices v1.5.3
  [f1d291b0] + MLUtils v0.4.8
  [d125e4d3] + ManualMemory v0.1.8
  [0b3b1443] - MicroMamba v0.1.14
  [872c559c] + NNlib v0.9.30
  [5da4648a] + NVTX v1.0.0
  [bdff3154] + NormalizingFlowFilters v0.0.1 `https://github.com/DataAssimilation/NormalizingFlowFilters.jl#main`
  [0b1bfda6] + OneHotArrays v0.2.7
⌅ [3bd65402] + Optimisers v0.3.4
  [d96e819e] + Parameters v0.12.3
  [fa939f87] - Pidfile v1.3.0
  [1d0040c9] + PolyesterWeave v0.2.2
  [2dfb63ee] + PooledArrays v1.4.3
  [08abe8d2] + PrettyTables v2.4.0
  [33c8b6b6] + ProgressLogging v0.1.4
  [438e738f] + PyCall v1.96.4
  [6099a3de] - PythonCall v0.9.24
  [94ee1d12] + Quaternions v0.7.6
  [74087812] + Random123 v1.7.0
  [e6cf234a] + RandomNumbers v1.6.0
  [c1ae055f] + RealDot v0.1.0
  [6038ab10] + Rotations v1.7.1
  [94e857df] + SIMDTypes v0.1.0
  [476501e8] + SLEEFPirates v0.6.43
  [7e506255] + ScopedValues v1.3.0
  [91c51154] + SentinelArrays v1.4.8
  [605ecd9f] + ShowCases v0.1.0
  [dc90abb0] + SparseInverseSubset v0.1.2
  [aedffcd0] + Static v1.2.0
  [0d7ed370] + StaticArrayInterface v1.8.0
  [892a3eda] + StringManipulation v0.4.1
  [856f2bd8] - StructTypes v1.11.0
  [8290d209] + ThreadingUtilities v0.5.3
  [06e1c1a7] + TiledIteration v0.5.0
  [a759f4b9] + TimerOutputs v0.5.28
  [3a884ed6] + UnPack v1.0.2
  [013be700] + UnsafeAtomics v0.3.0
  [e17b2a0c] - UnsafePointers v1.0.0
  [3d5dd08c] + VectorizationBase v0.21.71
  [81def892] + VersionParsing v1.3.0
⌅ [e88e6eb3] + Zygote v0.6.76
  [700de1a5] + ZygoteRules v0.2.7
  [02a925ec] + cuDNN v1.4.2
  [4ee394cb] + CUDA_Driver_jll v0.12.1+1
  [76a88914] + CUDA_Runtime_jll v0.16.1+0
⌅ [62b44479] + CUDNN_jll v9.4.0+0
  [9c1d0b0a] + JuliaNVTXCallbacks_jll v0.2.1+0
  [dad2f222] + LLVMExtra_jll v0.0.35+0
  [e98f9f5b] + NVTX_jll v3.1.1+0
  [1e29f10c] + demumble_jll v1.3.0+0
  [f8abcde7] - micromamba_jll v1.5.8+0
  [4d7b5844] - pixi_jll v0.41.3+0
        Info Packages marked with ⌃ and ⌅ have new versions available. Those with ⌃ may be upgradable, but those with ⌅ are restricted by compatibility constraints from upgrading. To see why use `status --outdated -m`
Precompiling project...
   2316.2 ms  ✓ EnsembleKalmanFilters
   3357.6 ms  ✓ Ensembles → EnsembleKalmanFiltersExt
  2 dependencies successfully precompiled in 6 seconds. 475 already precompiled.

Define parameters.

params = Dict(
    "format" => "v0.1",
    "transition" => Dict(
        "sigma" => 10,
        "rho" => 28,
        "beta" => 8 / 3,
        "scaling" => 1,
        "ministep_nt" => missing,
        "ministep_dt" => 0.05,
    ),
    "observation" =>
        Dict("noise_scale" => 2, "timestep_size" => 0.1, "num_timesteps" => 300),
    "ensemble" => Dict(
        "size" => 10,
        "seed" => 9347215,
        "prior" => "gaussian",
        "prior_params" => [0.0, 1.0],
    ),
    "spinup" => Dict(
        "num_timesteps" => 300,
        "transition_noise_scale" => 1.0,

        # EnKF params
        "algorithm" => "enkf",
        "include_noise_in_y_covariance" => true,
        "multiplicative_prior_inflation" => 0.0,
        "observation_noise_stddev" => 2.0,
        "observation_noise_type" => "diagonal",
    ),
);

Seed for reproducibility.

Random.seed!(1983745);

Make operators.

transitioner = Lorenz63Model(; params)
observer = NoisyObserver(get_state_keys(transitioner); params);

Set seed for ground-truth simulation.

Random.seed!(0xfee55e45)
xor_seed!(observer, UInt64(0x243ecae5));

Define observation times

observation_times = let
    step = params["observation"]["timestep_size"]
    length = params["observation"]["num_timesteps"]
    range(; start=0, length, step)
end
0.0:0.1:29.9

Generate synthetic ground-truth observations.

if !(@isdefined ground_truth) || isnothing(ground_truth)
    ground_truth = @time let
        state0 = Dict{Symbol,Any}(:state => randn(3))

        # Set seed for ground-truth simulation.
        Random.seed!(0xfee55e45)
        xor_seed!(observer, UInt64(0x243ecae5))

        # Generate states and observations.
        t0 = 0.0
        states = Vector{Dict{Symbol,Any}}(undef, length(observation_times))
        observations = Vector{Dict{Symbol,Any}}(undef, length(observation_times))
        let state = state0
            for (i, t) in enumerate(observation_times)
                state = transitioner(state, t0, t)
                obs = observer(state)
                states[i] = state
                observations[i] = split_clean_noisy(observer, obs)[2]
                t0 = t
            end
        end
        (; states, observations)
    end
    println("  ^ timing for making ground truth observations")
    ground_truth_states_vec = get_ensemble_matrix([:state], ground_truth.states)
    ground_truth_obs_vec = get_ensemble_matrix([:state], ground_truth.observations)
end;
  1.155779 seconds (1.64 M allocations: 82.655 MiB, 3.48% gc time, 99.66% compilation time)
  ^ timing for making ground truth observations

Plot the ground-truth.

if !(@isdefined plot_ground_truth)
    plot_ground_truth = true
end
if plot_ground_truth
    figs = let
        ts = observation_times
        data = reduce(hcat, state[:state] for state in ground_truth.states)

        plot_kwargs = (; color="#7fc97f", marker='.', markersize=15, markercolor=:black)
        plot_state_over_time(observation_times, data; plot_kwargs...)
    end
    figs[1]
end

Ensembles

Make initial ensemble.

function generate_ensemble(params::Dict)
    seed = params["ensemble"]["seed"]
    ensemble_size = params["ensemble"]["size"]
    prior_type = params["ensemble"]["prior"]

    members = Vector{Dict{Symbol,Any}}(undef, ensemble_size)
    if prior_type == "gaussian"
        rng = Random.MersenneTwister(seed)
        prior_mean, prior_std = params["ensemble"]["prior_params"]
        for i in 1:ensemble_size
            data = prior_mean .+ prior_std .* randn(rng, 3)
            state = Dict{Symbol,Any}(:state => data)
            members[i] = state
        end
    else
        throw(ArgumentError("Invalid prior type: $prior_type"))
    end

    ensemble = Ensemble(members)
    return ensemble
end

if !(@isdefined ensemble_initial0) || isnothing(ensemble_initial0)
    ensemble_initial0 = generate_ensemble(params)
end

ensemble_initial0 = generate_ensemble(params);
ensemble_initial = Ensemble(ensemble_initial0, ensemble_initial0.members, [:state]);

t_index_end = params["spinup"]["num_timesteps"]
observation_times = observation_times[1:t_index_end]
ground_truth_observations = ground_truth.observations[1:t_index_end]
transition_noise = params["spinup"]["transition_noise_scale"];

Data assimilation

Choose filtering algorithm.

filter = get_enkf_filter(params["spinup"])
EnsembleKalmanFilters.EnKF([4.0 0.0 0.0; 0.0 4.0 0.0; 0.0 0.0 4.0], true, 0.0)

Run sequential algorithm.

if !(@isdefined ensembles) || isnothing(ensembles)
    ensembles =
        let t_index_end = params["spinup"]["num_timesteps"],
            observation_times = observation_times[1:t_index_end],
            ground_truth_observations = ground_truth.observations[1:t_index_end],
            ensemble = ensemble_initial0,
            t0 = 0.0,
            transition_noise = params["spinup"]["transition_noise_scale"]

            Random.seed!(0x3289745)
            xor_seed!(observer, UInt64(0x375ef928))

            logs = []
            ensembles = []
            @time begin
                push!(ensembles, (; ensemble, t=t0))
                for (t, y_obs) in zip(observation_times, ground_truth_observations)
                    # Advance ensemble to time t.
                    ensemble = transitioner(ensemble, t0, t; inplace=false)

                    # Keep ensemble separated.
                    if transition_noise != 0
                        for em in ensemble.members
                            em[:state] .+= transition_noise .* Random.randn(3)
                        end
                    end

                    # Take observation at time t.
                    ensemble_obs = observer(ensemble)
                    ensemble_obs_clean, ensemble_obs_noisy = split_clean_noisy(
                        observer, ensemble_obs
                    )

                    # Record.
                    push!(
                        ensembles, (; ensemble, ensemble_obs_clean, ensemble_obs_noisy, t)
                    )

                    # Assimilate observation
                    log_data = Dict{Symbol,Any}()
                    (posterior, timing...) = @timed assimilate_data(
                        filter,
                        ensemble,
                        ensemble_obs_clean,
                        ensemble_obs_noisy,
                        y_obs,
                        log_data,
                    )
                    log_data[:timing] = timing
                    ensemble = posterior

                    # Record.
                    push!(ensembles, (; ensemble, t))
                    push!(logs, log_data)

                    # Let time pass.
                    t0 = t
                end
            end
            println("  ^ timing for making initial ensemble")
            ensembles
        end
end;
  9.128559 seconds (13.50 M allocations: 1.562 GiB, 2.30% gc time, 50.16% compilation time)
  ^ timing for making initial ensemble

Define handy functions.

function get_ground_truth_iterator(ensembles_ts, observation_times)
    gt_index = 1
    gt_indices = []
    post_assim_indices = []
    for (i, t) in enumerate(ensembles_ts)
        while gt_index <= length(observation_times) && observation_times[gt_index] < t
            gt_index += 1
        end
        if gt_index > length(observation_times)
            error(
                "Comparing at time $(t) is impossible because final ground-truth observation is at time $(observation_times[end])",
            )
        end
        if observation_times[gt_index] != t
            error(
                "No observation at time $(t). Closest are $(observation_times[gt_index-1]) and $(observation_times[gt_index])",
            )
        end
        push!(gt_indices, gt_index)
        if i == length(ensembles_ts) || t < ensembles_ts[i + 1]
            push!(post_assim_indices, i)
        end
    end
    return gt_indices, post_assim_indices
end

function compute_errors(gt_indices, ensembles_means_vec, ground_truth_states_vec)
    rmses = Vector{Float64}(undef, length(ensembles_means_vec))
    for (i, gt_index) in enumerate(gt_indices)
        rmses[i] = rmse(ensembles_means_vec[:, i], ground_truth_states_vec[:, gt_index])
    end
    return rmses
end

function compute_metrics(ensembles)
    means_vec = get_ensemble_matrix(
        ensembles[1].ensemble.state_keys, mean(e.ensemble) for e in ensembles
    )
    vars_vec = get_ensemble_matrix(
        ensembles[1].ensemble.state_keys, var(e.ensemble) for e in ensembles
    )
    ts = [e.t for e in ensembles]

    gt_indices, post_assim_indices = get_ground_truth_iterator(ts, observation_times)
    rmses = compute_errors(gt_indices, means_vec, ground_truth_states_vec)

    spread = sqrt.(mean(vars_vec; dims=1)[1, :])
    return (;
        ts,
        vars_vec,
        means_vec,
        gt_indices,
        post_assim_indices,
        rmses,
        spread,
        post_assim_rmses=(rmses[i] for i in post_assim_indices),
        post_assim_spread=(spread[i] for i in post_assim_indices),
    )
end;

Compute metrics.

metrics_initial = compute_metrics(ensembles)

println("SPINUP")
println("  Average metrics")
println("      RMSE: $(mean(metrics_initial.post_assim_rmses))")
println("    Spread: $(mean(metrics_initial.post_assim_spread))")
println()

println("Observations")
println("  Average metrics")
println("      RMSE: $(mean(rmse.(ground_truth_obs_vec, ground_truth_states_vec)))")
println()
SPINUP
  Average metrics
      RMSE: 0.94082500434391
    Spread: 1.049306132209731

Observations
  Average metrics
      RMSE: 1.6076413581833835

Plot metrics over time

if !(@isdefined plot_initial_metrics)
    plot_initial_metrics = true
end
if plot_initial_metrics
    figs_state, figs_spread, figs_rmse = let metrics = metrics_initial
        cut = Colon()

        # Plot variance.
        handler = function (fig)
            for ax in fig.content
                if isa(ax, Axis)
                    if ax.ylabel[] == L"\text{x}"
                        ax.ylabel = L"\sigma^2_x"
                        ax.yscale = log10
                    elseif ax.ylabel[] == L"\text{y}"
                        ax.ylabel = L"\sigma^2_y"
                        ax.yscale = log10
                    elseif ax.ylabel[] == L"\text{z}"
                        ax.ylabel = L"\sigma^2_z"
                        ax.yscale = log10
                    end
                end
            end
        end
        plot_kwargs = (;
            handler,
            max_dt=50,
            make_positive=true,
            color="#7fc97f",
            marker='.',
            markersize=0,
            markercolor=:black,
            connect=(; linestyle=:dash, color=[1, 2], colormap=:BuGn, markersize=0),
        )
        figs_state = plot_state_over_time(
            metrics.ts[cut], metrics.vars_vec[:, cut]; plot_kwargs...
        )

        # Plot ensemble spread.
        handler = function (fig)
            for ax in fig.content
                if isa(ax, Axis)
                    if ax.ylabel[] == L"\text{metric}"
                        ax.ylabel = L"\text{spread}"
                        ax.yscale = log10
                    end
                end
            end
        end
        plot_kwargs = (;
            handler,
            max_dt=50,
            color="#041a1c",
            marker='.',
            markersize=15,
            markercolor=:black,
            connect=(; linestyle=:dash, color=[1, 2], colormap=:BuGn, markersize=0),
        )
        figs_spread = plot_error_metric_over_time(
            metrics.ts[cut], metrics.spread[cut]; plot_kwargs...
        )

        # Plot RMSE.
        handler = function (fig)
            for ax in fig.content
                if isa(ax, Axis)
                    if ax.ylabel[] == L"\text{metric}"
                        ax.ylabel = L"\text{RMSE}"
                        ax.yscale = log10
                    end
                end
            end
        end
        plot_kwargs = (;
            handler,
            max_dt=50,
            color="#e41a1c",
            marker='.',
            markersize=15,
            markercolor=:black,
            connect=(; linestyle=:dash, color=[1, 2], colormap=:BuGn, markersize=0),
        )
        figs_rmse = plot_error_metric_over_time(
            metrics.ts[cut], metrics.rmses[cut]; plot_kwargs...
        )
        figs_state, figs_spread, figs_rmse
    end
    figs_state[1]
end
if plot_initial_metrics
    figs_spread[1]
end
if plot_initial_metrics
    figs_rmse[1]
end

Plot the ensemble mean.

if !(@isdefined plot_initial_ensemble_mean)
    plot_initial_ensemble_mean = true
end
if plot_initial_ensemble_mean
    figs = let metrics = metrics_initial
        cut = Colon()

        ts_gt = observation_times
        xs_gt = view(ground_truth_states_vec, 1, :)
        ys_gt = view(ground_truth_states_vec, 2, :)
        zs_gt = view(ground_truth_states_vec, 3, :)

        gt_kwargs = (;
            color=("#d95f02", 0.5),
            marker='.',
            markersize=15,
            markercolor=(:yellow, 0.5),
        )

        handler = function (fig)
            for ax in fig.content
                if isa(ax, Axis)
                    t0 = minimum(ax.scene.plots[1].args[1][])
                    tf = maximum(ax.scene.plots[1].args[1][])
                    start = searchsortedfirst(ts_gt, t0)
                    finish = searchsortedfirst(ts_gt, tf)
                    if ax.ylabel[] == L"\text{x}"
                        scatterlines!(
                            ax,
                            ts_gt[start:finish],
                            xs_gt[start:finish];
                            gt_kwargs...,
                        )
                    elseif ax.ylabel[] == L"\text{y}"
                        scatterlines!(
                            ax,
                            ts_gt[start:finish],
                            ys_gt[start:finish];
                            gt_kwargs...,
                        )
                    elseif ax.ylabel[] == L"\text{z}"
                        scatterlines!(
                            ax,
                            ts_gt[start:finish],
                            zs_gt[start:finish];
                            gt_kwargs...,
                        )
                    end
                end
            end
        end
        plot_kwargs = (;
            handler,
            max_dt=50,
            color="#7fc97f",
            marker='.',
            markersize=0,
            markercolor=:black,
            connect=(; linestyle=:dash, color=[1, 2], colormap=:BuGn, markersize=0),
        )
        figs = plot_state_over_time(
            metrics.ts[cut], metrics.means_vec[:, cut]; plot_kwargs...
        )
    end
    figs[1]
end

And so everything is great!

Note that this cleanup only needs to be run in automated contexts. The unregistered packages can cause issues.

try
    Pkg.rm("Lorenz63")
    Pkg.rm("EnsembleKalmanFilters")
    Pkg.rm("NormalizingFlowFilters")
catch e
    @warn e
end
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
  [72db8b11] - Lorenz63 v0.1.0 `https://github.com/milankl/Lorenz63.jl#15220a7#master`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [72db8b11] - Lorenz63 v0.1.0 `https://github.com/milankl/Lorenz63.jl#15220a7#master`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
  [489b957c] - EnsembleKalmanFilters v0.0.1 `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl#main`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [489b957c] - EnsembleKalmanFilters v0.0.1 `https://github.com/DataAssimilation/EnsembleKalmanFilters.jl#main`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Project.toml`
  [bdff3154] - NormalizingFlowFilters v0.0.1 `https://github.com/DataAssimilation/NormalizingFlowFilters.jl#main`
    Updating `~/work/Ensembles.jl/Ensembles.jl/examples/lorenz63/Manifest.toml`
  [4fba245c] - ArrayInterface v7.18.0
  [a9b6321e] - Atomix v1.1.1
  [ab4f0b2a] - BFloat16s v0.5.1
  [62783981] - BitTwiddlingConvenienceFunctions v0.1.6
  [2a0fbf3d] - CPUSummary v0.2.6
  [052768ef] - CUDA v5.7.2
  [1af6417a] - CUDA_Runtime_Discovery v0.3.5
  [aafaddc9] - CatIndices v0.2.2
  [082447d4] - ChainRules v1.72.3
  [fb6a15b2] - CloseOpenIntervals v0.1.13
  [bbf7d656] - CommonSubexpressions v0.3.1
  [f70d9fcc] - CommonWorldInvalidations v1.0.0
  [ed09eef8] - ComputationalResources v0.3.2
  [5218b696] - Configurations v0.17.6
  [150eb455] - CoordinateTransformations v0.6.4
  [adafc99b] - CpuId v0.3.1
  [a8cc5b0e] - Crayons v4.1.1
  [dc8bdbbb] - CustomUnitRanges v1.0.2
  [a93c6f00] - DataFrames v1.7.0
  [8bb1440f] - DelimitedFiles v1.9.1
  [163ba53b] - DiffResults v1.1.0
  [b552c78f] - DiffRules v1.15.1
  [b4f34e82] - Distances v0.10.12
  [e2ba6199] - ExprTools v0.1.10
  [55351af7] - ExproniconLite v0.10.14
  [4f61f5a4] - FFTViews v0.3.2
  [587475ba] - Flux v0.14.25
  [f6369f11] - ForwardDiff v1.0.1
  [d9f16b24] - Functors v0.4.12
  [0c68f7d7] - GPUArrays v11.2.2
  [46192b85] - GPUArraysCore v0.2.0
  [61eb1bfa] - GPUCompiler v1.3.2
  [096a3bc2] - GPUToolbox v0.2.0
  [076d061b] - HashArrayMappedTries v0.2.0
  [3e5b6fbb] - HostCPUFeatures v0.1.17
  [7869d1d1] - IRTools v0.4.14
  [615f187c] - IfElse v0.1.1
  [f332f351] - ImageContrastAdjustment v0.3.12
  [51556ac3] - ImageDistances v0.2.17
  [6a3955dd] - ImageFiltering v0.7.9
  [787d08f9] - ImageMorphology v0.4.6
  [2996bd0c] - ImageQualityIndexes v0.3.7
  [02fcd773] - ImageTransformations v0.10.1
  [842dd82b] - InlineStrings v1.4.3
  [41ab1584] - InvertedIndices v1.3.1
  [b7115f24] - InvertibleNetworks v2.3.0
  [63c18a36] - KernelAbstractions v0.9.34
  [929cbde3] - LLVM v9.2.0
  [8b046642] - LLVMLoopInfo v1.0.0
  [10f19ff3] - LayoutPointers v0.1.17
  [bdcacae8] - LoopVectorization v0.12.172
  [c2834f40] - MLCore v1.0.0
  [7e8f7934] - MLDataDevices v1.5.3
  [f1d291b0] - MLUtils v0.4.8
  [d125e4d3] - ManualMemory v0.1.8
  [872c559c] - NNlib v0.9.30
  [5da4648a] - NVTX v1.0.0
  [bdff3154] - NormalizingFlowFilters v0.0.1 `https://github.com/DataAssimilation/NormalizingFlowFilters.jl#main`
  [0b1bfda6] - OneHotArrays v0.2.7
  [3bd65402] - Optimisers v0.3.4
  [d96e819e] - Parameters v0.12.3
  [1d0040c9] - PolyesterWeave v0.2.2
  [2dfb63ee] - PooledArrays v1.4.3
  [08abe8d2] - PrettyTables v2.4.0
  [33c8b6b6] - ProgressLogging v0.1.4
  [94ee1d12] - Quaternions v0.7.6
  [74087812] - Random123 v1.7.0
  [e6cf234a] - RandomNumbers v1.6.0
  [c1ae055f] - RealDot v0.1.0
  [6038ab10] - Rotations v1.7.1
  [94e857df] - SIMDTypes v0.1.0
  [476501e8] - SLEEFPirates v0.6.43
  [7e506255] - ScopedValues v1.3.0
  [91c51154] - SentinelArrays v1.4.8
  [605ecd9f] - ShowCases v0.1.0
  [dc90abb0] - SparseInverseSubset v0.1.2
  [aedffcd0] - Static v1.2.0
  [0d7ed370] - StaticArrayInterface v1.8.0
  [892a3eda] - StringManipulation v0.4.1
  [8290d209] - ThreadingUtilities v0.5.3
  [06e1c1a7] - TiledIteration v0.5.0
  [a759f4b9] - TimerOutputs v0.5.28
  [3a884ed6] - UnPack v1.0.2
  [013be700] - UnsafeAtomics v0.3.0
  [3d5dd08c] - VectorizationBase v0.21.71
  [e88e6eb3] - Zygote v0.6.76
  [700de1a5] - ZygoteRules v0.2.7
  [02a925ec] - cuDNN v1.4.2
  [4ee394cb] - CUDA_Driver_jll v0.12.1+1
  [76a88914] - CUDA_Runtime_jll v0.16.1+0
  [62b44479] - CUDNN_jll v9.4.0+0
  [9c1d0b0a] - JuliaNVTXCallbacks_jll v0.2.1+0
  [dad2f222] - LLVMExtra_jll v0.0.35+0
  [e98f9f5b] - NVTX_jll v3.1.1+0
  [1e29f10c] - demumble_jll v1.3.0+0

This page was generated using Literate.jl.