# Find project root and include ensure_packages.jl
project_root = let
current = pwd()
while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml"))
parent = dirname(current)
parent == current && break
current = parent
end
current
end
include(joinpath(project_root, "scripts", "ensure_packages.jl"))
@auto_using Random Distributions DifferentialEquations CairoMakie
Random.seed!(42)
# Example: Latent state X_t evolving over time
function latent_dynamics!(du, u, p, t)
"""Latent state dynamics: exponential decay with rate 0.1."""
X = u[1]
du[1] = -0.1 * X # Decay process
end
u0 = [1.0]
tspan = (0.0, 20.0)
prob = ODEProblem(latent_dynamics!, u0, tspan)
sol = solve(prob, Tsit5())
X_t = [u[1] for u in sol.u] # Latent state
# Type 1: Direct measurement Y_t = X_t + noise
σ_direct = 0.1
Y_direct = X_t .+ rand(Normal(0, σ_direct), length(X_t))
# Type 2: Indirect measurement Y_t = h(X_t) + noise
# Example: h(X) = X² (nonlinear observation)
Y_indirect = X_t.^2 .+ rand(Normal(0, 0.05), length(X_t))
# Type 3: Aggregate measurement (if we had multiple states)
# Y_t = Σ X_i + noise (simplified here)
Y_aggregate = X_t .+ rand(Normal(0, 0.1), length(X_t)) # Simplified
# Visualise
let
fig = Figure(size = (1200, 400))
ax1 = Axis(fig[1, 1], title = "Direct Measurement: Y = X + noise",
xlabel = "Time", ylabel = "Value")
ax2 = Axis(fig[1, 2], title = "Indirect Measurement: Y = X² + noise",
xlabel = "Time", ylabel = "Value")
ax3 = Axis(fig[1, 3], title = "Latent vs Observed",
xlabel = "Time", ylabel = "Value")
lines!(ax1, sol.t, X_t, label = "Latent X_t", linewidth = 2, color = :blue)
scatter!(ax1, sol.t, Y_direct, label = "Observed Y_t", color = :red, alpha = 0.5, markersize = 3)
axislegend(ax1)
lines!(ax2, sol.t, X_t.^2, label = "h(X_t) = X²", linewidth = 2, color = :blue)
scatter!(ax2, sol.t, Y_indirect, label = "Observed Y_t", color = :red, alpha = 0.5, markersize = 3)
axislegend(ax2)
lines!(ax3, sol.t, X_t, label = "Latent X_t", linewidth = 2, color = :blue)
scatter!(ax3, sol.t, Y_direct, label = "Direct obs", color = :red, alpha = 0.5, markersize = 3)
scatter!(ax3, sol.t, sqrt.(Y_indirect), label = "Indirect obs (√Y)", color = :green, alpha = 0.5, markersize = 3)
axislegend(ax3)
fig # Only this gets displayed
end
println("Observation models:")
println(" Direct: Y_t = X_t + noise (σ = ", σ_direct, ")")
println(" Indirect: Y_t = X_t² + noise (nonlinear)")
println(" Inference: Must recover X_t from Y_t (addressed in state-space models)")