using PyPlot, PyCall
using Distributions
using StatsFuns
using Requests
PyDict(matplotlib["rcParams"])["mathtext.fontset"] = "cm"
PyDict(matplotlib["rcParams"])["mathtext.rm"] = "serif"
PyDict(matplotlib["rcParams"])["lines.linewidth"] = 1.5
PyDict(matplotlib["rcParams"])["font.family"] = "TakaoPGothic"

test = false

function VI(C, a1, b1, a2, b2, phi, max_iter)
    N = size(C, 1)
    phi_h = deepcopy(phi)

    a1_h = NaN
    b1_h = NaN
    a2_h = NaN
    b2_h = NaN
    for i in 1 : max_iter
        # update parameters
        phi_sum = sum(phi_h)

        #delta1 = reverse(cumsum(reverse(phi_h)))
        delta1 = reverse(cumsum(reverse(cat(1, phi_h[2:end], 0))))
        #tmp = cumsum(phi_h)
        #delta2 = cat(1, 0, tmp[1:end-1])
        delta2 = cumsum(phi_h)
        a1_h = a1 + sum(C.*delta1)
        b1_h = b1 + sum(delta1)
        a2_h = a2 + sum(C.*delta2)
        b2_h = b2 + sum(delta2)

        # update tau
        ln_p1 = Array{Float64, 1}(N)
        ln_p2 = Array{Float64, 1}(N)
        for n in 1 : N
            ln_p1[n] = C[n] * (digamma(a1_h) - log(b1_h)) - a1_h/b1_h
            ln_p2[n] = C[n] * (digamma(a2_h) - log(b2_h)) - a2_h/b2_h
        end

        ln_tmp = Array{Float64, 1}(N)
        for n in 1 : N
            #ln_tmp[n] = sum(ln_p1[1:n]) + sum(ln_p2[n+1:end]) + log(phi[n])
            ln_tmp[n] = sum(ln_p1[1:n-1]) + sum(ln_p2[n:end]) + log(phi[n])
        end
        phi_h = exp(ln_tmp - logsumexp(ln_tmp))
    end
    return phi_h, a1_h / b1_h, a2_h / b2_h
end

# model params
a1 = 2.0
b1 = 0.1
a2 = 2.0
b2 = 0.1

# generate or download data
if test
    N = 74
    phi = ones(N) / N

    # generate sample
    lambda1 = rand(Gamma(a1, 1.0/b1))
    lambda2 = rand(Gamma(a2, 1.0/b2))
    tau = rand(Categorical(phi))
    C = Array{Float64, 1}(N)
    for n in 1 : tau
        C[n] = rand(Poisson(lambda1))
    end
    for n in tau+1 : N
        C[n] = rand(Poisson(lambda2))
    end
else
    data_url = "https://git.io/vXTVC"
    dir_name = "data"
    file_name = "txtdata.csv"
    if !isdir(dir_name)
        mkdir(dir_name)
    end
    if isfile(dir_name * "/" * file_name)
        println("read csv file $(dir_name * "/" * file_name)")
        data = readcsv(dir_name * "/" * file_name)
    else
        println("download csv file from $data_url")
        res = get(data_url)
        data = readcsv(IOBuffer(res.data))
        writecsv(dir_name * "/" * file_name, data)
    end
    C = data[:,1]

    N = size(C, 1)
    phi = ones(N) / N
end

# visualize
if test
    figure("data samples")
    clf()
    bar(1:N, C, color="#348ABD")
    plot([1, tau], [lambda1, lambda1], "g--")
    plot([tau+1, N], [lambda2, lambda2], "g--")
    plot([tau+0.5, tau+0.5],[0, maximum(C)],"r--")
    xlim([0, N])
else
    figure("data samples")
    clf()
    bar(1:N, C, color="#348ABD")
    xlim([0, N])
end

# inference
max_iter = 100
phi_est, lambda1_est, lambda2_est = VI(C, a1, b1, a2, b2, phi, max_iter)

# calc expectation
tau_expt = sum(phi_est .* range(1,N))
lambda_est = Array{Float64, 1}(N)
for n in 1 : N
    tmp = sum(phi_est[1:n])
    lambda_est[n] =(1.0 - tmp) * lambda1_est + tmp * lambda2_est
end

figure("results")
clf()
subplot(2,1,1)
bar(1:N, C, color="#348ABD", label="受信数")
plot(1:N, lambda_est, color="#E24A33", label="期待値")
xlim([0, N])
ylabel("受信メッセージ数")
xlabel("経過日数")
legend()
subplot(2,1,2)
bar(1:N, phi_est, color="#348ABD", label="事後確率")
plot([tau_expt, tau_expt], [0, maximum(phi_est)], "r--", label="期待値")
xlim([0, N])
ylabel("事後確率")
xlabel("経過日数")
legend()


"""
    mmg_3dof_model!(dX, X, p, t)

MMG 3DOF model on DifferentialEquations.ODEProblem. Update `dX`.

# Arguments
- `dX`: [du, dv, dr, dδ, dn_p]
- `X`: the initial state values. [`u`, `v`, `r`, `δ`, `n_p`].
- `p`: ρ and the basic & maneuvering parameters and δ & n_p spline info.
    - ρ
    - L_pp
    - B
    - d
    - x_G
    - D_p
    - m
    - I_zG
    - A_R
    - η
    - m_x
    - m_y
    - J_z
    - f_α
    - ϵ
    - t_R
    - x_R
    - a_H
    - x_H
    - γ_R_minus
    - γ_R_plus
    - l_R
    - κ
    - t_P
    - w_P0
    - x_P
    - k_0
    - k_1
    - k_2
    - R_0_dash
    - X_vv_dash
    - X_vr_dash
    - X_rr_dash
    - X_vvvv_dash
    - Y_v_dash
    - Y_r_dash
    - Y_vvv_dash
    - Y_vvr_dash
    - Y_vrr_dash
    - Y_rrr_dash
    - N_v_dash
    - N_r_dash
    - N_vvv_dash
    - N_vvr_dash
    - N_vrr_dash
    - N_rrr_dash
    - spl_δ
    - spl_n_p
- `t`: the time.
"""
function mmg_3dof_model!(dX, X, p, t)
    u, v, r, δ, n_p = X
    ρ,
    L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P,
    k_0,
    k_1,
    k_2,
    R_0_dash,
    X_vv_dash,
    X_vr_dash,
    X_rr_dash,
    X_vvvv_dash,
    Y_v_dash,
    Y_r_dash,
    Y_vvv_dash,
    Y_vvr_dash,
    Y_vrr_dash,
    Y_rrr_dash,
    N_v_dash,
    N_r_dash,
    N_vvv_dash,
    N_vvr_dash,
    N_vrr_dash,
    N_rrr_dash,
    spl_δ,
    spl_n_p = p

    U = sqrt(u^2 + (v - r * x_G)^2)

    β = 0.0
    if U == 0.0
        β = 0.0
    else
        β = asin(-(v - r * x_G) / U)
    end

    v_dash = 0.0
    if U == 0.0
        v_dash = 0.0
    else
        v_dash = v / U
    end

    r_dash = 0.0
    if U == 0.0
        r_dash = 0.0
    else
        r_dash = r * L_pp / U
    end

    w_P = w_P0 * exp(-4.0 * (β - x_P * r_dash)^2)

    J = 0.0
    if n_p == 0.0
        J = 0.0
    else
        J = (1 - w_P) * u / (n_p * D_p)
    end
    K_T = k_0 + k_1 * J + k_2 * J^2
    β_R = β - l_R * r_dash
    γ_R = γ_R_minus

    if β_R < 0.0
        γ_R = γ_R_minus
    else
        γ_R = γ_R_plus
    end

    v_R = U * γ_R * β_R

    u_R = 0.0
    if J == 0.0
        u_R = sqrt(η * (κ * ϵ * 8.0 * k_0 * n_p^2 * D_p^4 / pi)^2)
    else
        u_R =
            u *
            (1.0 - w_P) *
            ϵ *
            sqrt(η * (1.0 + κ * (sqrt(1.0 + 8.0 * K_T / (pi * J^2)) - 1))^2 + (1 - η))
    end

    U_R = sqrt(u_R^2 + v_R^2)
    α_R = δ - atan(v_R, u_R)
    F_N = 0.5 * A_R * ρ * f_α * (U_R^2) * sin(α_R)

    X_H = (
        0.5 *
        ρ *
        L_pp *
        d *
        (U^2) *
        (
            -R_0_dash +
            X_vv_dash * (v_dash^2) +
            X_vr_dash * v_dash * r_dash +
            X_rr_dash * (r_dash^2) +
            X_vvvv_dash * (v_dash^4)
        )
    )
    X_R = -(1.0 - t_R) * F_N * sin(δ)
    X_P = (1.0 - t_P) * ρ * K_T * n_p^2 * D_p^4
    Y_H = (
        0.5 *
        ρ *
        L_pp *
        d *
        (U^2) *
        (
            Y_v_dash * v_dash +
            Y_r_dash * r_dash +
            Y_vvv_dash * (v_dash^3) +
            Y_vvr_dash * (v_dash^2) * r_dash +
            Y_vrr_dash * v_dash * (r_dash^2) +
            Y_rrr_dash * (r_dash^3)
        )
    )
    Y_R = -(1 + a_H) * F_N * cos(δ)
    N_H = (
        0.5 *
        ρ *
        (L_pp^2) *
        d *
        (U^2) *
        (
            N_v_dash * v_dash +
            N_r_dash * r_dash +
            N_vvv_dash * (v_dash^3) +
            N_vvr_dash * (v_dash^2) * r_dash +
            N_vrr_dash * v_dash * (r_dash^2) +
            N_rrr_dash * (r_dash^3)
        )
    )
    N_R = -(x_R + a_H * x_H) * F_N * cos(δ)
    dX[1] = du = ((X_H + X_R + X_P) + (m + m_y) * v * r + x_G * m * (r^2)) / (m + m_x)
    dX[2] =
        dv =
            (
                (x_G^2) * (m^2) * u * r - (N_H + N_R) * x_G * m +
                ((Y_H + Y_R) - (m + m_x) * u * r) * (I_zG + J_z + (x_G^2) * m)
            ) / ((I_zG + J_z + (x_G^2) * m) * (m + m_y) - (x_G^2) * (m^2))
    dX[3] = dr = (N_H + N_R - x_G * m * (dv + u * r)) / (I_zG + J_z + (x_G^2) * m)
    dX[4] = dδ = derivative(spl_δ, t)
    dX[5] = dn_p = derivative(spl_n_p, t)
end

"""
Basic parameters of target ship for MMG 3DOF simulation.

# Arguments
- `L_pp`: L_pp
- `B`:
- `d`:
- `x_G`:
- `D_p`:
- `m`:
- `I_zG`:
- `A_R`:
- `η`:
- `m_x`:
- `m_y`:
- `J_z`:
- `f_α`:
- `ϵ`:
- `t_R`:
- `x_R`:
- `a_H`:
- `x_H`:
- `γ_R_minus`:
- `γ_R_plus`:
- `l_R`:
- `κ`:
- `t_P`:
- `w_P0`:
- `x_P`:
"""
@with_kw mutable struct Mmg3DofBasicParams{T}
    L_pp::T
    B::T
    d::T
    x_G::T
    D_p::T
    m::T
    I_zG::T
    A_R::T
    η::T
    m_x::T
    m_y::T
    J_z::T
    f_α::T
    ϵ::T
    t_R::T
    x_R::T
    a_H::T
    x_H::T
    γ_R_minus::T
    γ_R_plus::T
    l_R::T
    κ::T
    t_P::T
    w_P0::T
    x_P::T
end

"""
Maneuvering parameters of target ship for MMG 3DOF simulation.

# Arguments
- `k_0::T`
- `k_1::T`
- `k_2::T`
- `R_0_dash::T`
- `X_vv_dash::T`
- `X_vr_dash::T`
- `X_rr_dash::T`
- `X_vvvv_dash::T`
- `Y_v_dash::T`
- `Y_r_dash::T`
- `Y_vvv_dash::T`
- `Y_vvr_dash::T`
- `Y_vrr_dash::T`
- `Y_rrr_dash::T`
- `N_v_dash::T`
- `N_r_dash::T`
- `N_vvv_dash::T`
- `N_vvr_dash::T`
- `N_vrr_dash::T`
- `N_rrr_dash::T`
"""
@with_kw mutable struct Mmg3DofManeuveringParams{T}
    k_0::T
    k_1::T
    k_2::T
    R_0_dash::T
    X_vv_dash::T
    X_vr_dash::T
    X_rr_dash::T
    X_vvvv_dash::T
    Y_v_dash::T
    Y_r_dash::T
    Y_vvv_dash::T
    Y_vvr_dash::T
    Y_vrr_dash::T
    Y_rrr_dash::T
    N_v_dash::T
    N_r_dash::T
    N_vvv_dash::T
    N_vvr_dash::T
    N_vrr_dash::T
    N_rrr_dash::T
end

"""
    mmg_3dof_simulate(time_list, n_p_list, δ_list, basic_params, maneuvering_params, [, u0, v0, r0, ρ, algorithm, reltol, abstol]) -> u, v, r, δ, n_p

Returns the MMG 3DOF simulation results including the lists of time, u, v, r, δ, n_p.
This function has the same logic of `ShipMMG.simulate()`.

# Arguments
- `basic_params::Mmg3DofBasicParams`: the basic parameters of target ship.
- `maneuvering_params::Mmg3DofManeuveringParams`: the maneuvering parameters of target ship.
- `time_list`: the list of simulatino time.
- `δ_list`: the list of rudder angle [rad].
- `n_p_list`: the list of propeller rps.
- `u0=0.0`: the initial x (surge) velocity.
- `v0=0.0`: the initial y (sway) velocity.
- `r0=0.0`: the initial rate of turn [rad/s].
- `ρ=1025.0`: the seawater density [kg/m^3].
- `algorithm=Tsit5()`: the parameter of DifferentialEquations.ODEProblem.solve()
- `reltol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()
- `abstol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()

# Examples
KVLCC2_L7 turning test.

```julia-rep1
julia> basic_params, maneuvering_params = get_KVLCC2_L7_params();
julia> duration = 200; # [s]
julia> max_δ_rad = 35 * pi / 180.0;  # [rad]
julia> n_const = 17.95;  # [rps]
julia> sampling = duration * 10;
julia> time_list = range(0.00, stop = duration, length = sampling);
julia> δ_rad_list = max_δ_rad .* ones(Float64, sampling);
julia> n_p_list = n_const .* ones(Float64, sampling);
julia> mmg_results = mmg_3dof_simulate(
    basic_params,
    maneuvering_params,
    time_list,
    δ_rad_list,
    n_p_list,
    u0 = 2.29 * 0.512,
    v0 = 0.0,
    r0 = 0.0,
);
```
"""
function mmg_3dof_simulate(
    basic_params::Mmg3DofBasicParams,
    maneuvering_params::Mmg3DofManeuveringParams,
    time_list,
    δ_list,
    n_p_list;
    u0 = 0.0,
    v0 = 0.0,
    r0 = 0.0,
    ρ = 1025.0,
    algorithm = Tsit5(),
    reltol = 1e-8,
    abstol = 1e-8,
)
    @unpack L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P = basic_params

    @unpack k_0,
    k_1,
    k_2,
    R_0_dash,
    X_vv_dash,
    X_vr_dash,
    X_rr_dash,
    X_vvvv_dash,
    Y_v_dash,
    Y_r_dash,
    Y_vvv_dash,
    Y_vvr_dash,
    Y_vrr_dash,
    Y_rrr_dash,
    N_v_dash,
    N_r_dash,
    N_vvv_dash,
    N_vvr_dash,
    N_vrr_dash,
    N_rrr_dash = maneuvering_params
    simulate(
        L_pp,
        B,
        d,
        x_G,
        D_p,
        m,
        I_zG,
        A_R,
        η,
        m_x,
        m_y,
        J_z,
        f_α,
        ϵ,
        t_R,
        x_R,
        a_H,
        x_H,
        γ_R_minus,
        γ_R_plus,
        l_R,
        κ,
        t_P,
        w_P0,
        x_P,
        k_0,
        k_1,
        k_2,
        R_0_dash,
        X_vv_dash,
        X_vr_dash,
        X_rr_dash,
        X_vvvv_dash,
        Y_v_dash,
        Y_r_dash,
        Y_vvv_dash,
        Y_vvr_dash,
        Y_vrr_dash,
        Y_rrr_dash,
        N_v_dash,
        N_r_dash,
        N_vvv_dash,
        N_vvr_dash,
        N_vrr_dash,
        N_rrr_dash,
        time_list,
        δ_list,
        n_p_list,
        u0 = u0,
        v0 = v0,
        r0 = r0,
        ρ = ρ,
        algorithm = algorithm,
        reltol = reltol,
        abstol = abstol,
    )
end

"""
    mmg_3dof_simulate(time_list, n_p_list, δ_list, L_pp, B, d, x_G, D_p, m, I_zG, A_R, η, m_x, m_y, J_z, f_α, ϵ, t_R, x_R, a_H, x_H, γ_R_minus, γ_R_plus, l_R, κ, t_P, w_P0, x_P, k_0, k_1, k_2, R_0_dash, X_vv_dash, X_vr_dash, X_rr_dash, X_vvvv_dash, Y_v_dash, Y_r_dash, Y_vvv_dash, Y_vvr_dash, Y_vrr_dash, Y_rrr_dash, N_v_dash, N_r_dash, N_vvv_dash, N_vvr_dash, N_vrr_dash, N_rrr_dash, [, u0, v0, r0, ρ, algorithm, reltol, abstol]) -> u, v, r, δ, n_p

Returns the MMG 3DOF simulation results including the lists of time, u, v, r, δ, n_p.
This function has the same logic of `ShipMMG.mmg_3dof_simulate()`.

# Arguments
- `L_pp`: L_pp
- `B`:
- `d`:
- `x_G`:
- `D_p`:
- `m`:
- `I_zG`:
- `A_R`:
- `η`:
- `m_x`:
- `m_y`:
- `J_z`:
- `f_α`:
- `ϵ`:
- `t_R`:
- `x_R`:
- `a_H`:
- `x_H`:
- `γ_R_minus`:
- `γ_R_plus`:
- `l_R`:
- `κ`:
- `t_P`:
- `w_P0`:
- `x_P`:
- `k_0`
- `k_1`
- `k_2`
- `R_0_dash`
- `X_vv_dash`
- `X_vr_dash`
- `X_rr_dash`
- `X_vvvv_dash`
- `Y_v_dash`
- `Y_r_dash`
- `Y_vvv_dash`
- `Y_vvr_dash`
- `Y_vrr_dash`
- `Y_rrr_dash`
- `N_v_dash`
- `N_r_dash`
- `N_vvv_dash`
- `N_vvr_dash`
- `N_vrr_dash`
- `N_rrr_dash`
- `time_list`: the list of simulatino time.
- `δ_list`: the list of rudder angle [rad].
- `n_p_list`: the list of propeller rps.
- `u0=0.0`: the initial x (surge) velocity.
- `v0=0.0`: the initial y (sway) velocity.
- `r0=0.0`: the initial rate of turn [rad/s].
- `ρ=1025.0`: the seawater density [kg/m^3].
- `algorithm=Tsit5()`: the parameter of DifferentialEquations.ODEProblem.solve()
- `reltol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()
- `abstol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()
"""
function simulate(
    L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P,
    k_0,
    k_1,
    k_2,
    R_0_dash,
    X_vv_dash,
    X_vr_dash,
    X_rr_dash,
    X_vvvv_dash,
    Y_v_dash,
    Y_r_dash,
    Y_vvv_dash,
    Y_vvr_dash,
    Y_vrr_dash,
    Y_rrr_dash,
    N_v_dash,
    N_r_dash,
    N_vvv_dash,
    N_vvr_dash,
    N_vrr_dash,
    N_rrr_dash,
    time_list,
    δ_list,
    n_p_list;
    u0 = 0.0,
    v0 = 0.0,
    r0 = 0.0,
    ρ = 1025.0,
    algorithm = Tsit5(),
    reltol = 1e-8,
    abstol = 1e-8,
)
    spl_δ = Spline1D(time_list, δ_list)
    spl_n_p = Spline1D(time_list, n_p_list)

    X0 = [u0; v0; r0; δ_list[1]; n_p_list[1]]
    p = [
        ρ,
        L_pp,
        B,
        d,
        x_G,
        D_p,
        m,
        I_zG,
        A_R,
        η,
        m_x,
        m_y,
        J_z,
        f_α,
        ϵ,
        t_R,
        x_R,
        a_H,
        x_H,
        γ_R_minus,
        γ_R_plus,
        l_R,
        κ,
        t_P,
        w_P0,
        x_P,
        k_0,
        k_1,
        k_2,
        R_0_dash,
        X_vv_dash,
        X_vr_dash,
        X_rr_dash,
        X_vvvv_dash,
        Y_v_dash,
        Y_r_dash,
        Y_vvv_dash,
        Y_vvr_dash,
        Y_vrr_dash,
        Y_rrr_dash,
        N_v_dash,
        N_r_dash,
        N_vvv_dash,
        N_vvr_dash,
        N_vrr_dash,
        N_rrr_dash,
        spl_δ,
        spl_n_p,
    ]
    prob = ODEProblem(mmg_3dof_model!, X0, (time_list[1], time_list[end]), p)
    sol = solve(prob, algorithm, reltol = reltol, abstol = abstol)
    sol_timelist = sol(time_list)
    results = hcat(sol_timelist.u...)
    u = results[1, :]
    v = results[2, :]
    r = results[3, :]
    δ = results[4, :]
    n_p = results[5, :]
    u, v, r, δ, n_p
end

"""
    mmg_3dof_zigzag_test(basic_params, maneuvering_params, time_list, n_p_list, target_δ_rad, target_ψ_rad_deviation, [, u0, v0, r0, ψ0, δ0, δ_rad_rate, algorithm, reltol, abstol]) -> final_u_list, final_v_list, final_r_list, final_ψ_list, final_δ_list

Returns the MMG 3DOF zigzag simulation results.

# Arguments
- `basic_params::Mmg3DofBasicParams`: the basic parameters of target ship.
- `maneuvering_params::Mmg3DofManeuveringParams`: the maneuvering parameters of target ship.
- `time_list`: the list of simulatino time.
- `n_p_list`: the list of propeller rps.
- `target_δ_rad`: target rudder angle of zigzag test.
- `target_ψ_rad_deviation`: target azimuth deviation of zigzag test.
- `u0=0.0`: the initial x (surge) velocity.
- `v0=0.0`: the initial y (sway) velocity.
- `r0=0.0`: the initial rate of turn [rad/s].
- `δ0=0.0`: the initial rudder angle.
- `δ_rad_rate=10.0*π/180`: the change rate of rudder angle [rad/s].
- `ρ=1025.0`: the seawater density [kg/m^3].
- `algorithm=Tsit5()`: the parameter of DifferentialEquations.ODEProblem.solve()
- `reltol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()
- `abstol=1e-8`: the parameter of DifferentialEquations.ODEProblem.solve()

# Examples
KVLCC2_L7 zigzag test.

```julia-rep1
julia> ρ = 1025.0;
julia> basic_params, maneuvering_params = get_KVLCC2_L7_params();
julia> target_δ_rad = 20.0 * π / 180.0
julia> target_ψ_rad_deviation = 20.0 * π / 180.0
julia> start_time_second = 0.00
julia> time_second_interval = 0.01
julia> end_time_second = 80.00
julia> time_list = start_time_second:time_second_interval:end_time_second
julia> n_const = 17.95  # [rps]
julia> n_p_list = n_const * ones(Float64, length(time_list))
julia> δ_list, u_list, v_list, r_list, ψ_list = mmg_3dof_zigzag_test(
    basic_params,
    maneuvering_params,
    time_list
    n_p_list,
    target_δ_rad,
    target_ψ_rad_deviation,
);
```
"""
function mmg_3dof_zigzag_test(
    basic_params::Mmg3DofBasicParams,
    maneuvering_params::Mmg3DofManeuveringParams,
    time_list,
    n_p_list,
    target_δ_rad,
    target_ψ_rad_deviation;
    u0 = 0.0,
    v0 = 0.0,
    r0 = 0.0,
    ψ0 = 0.0,
    δ0 = 0.0,
    δ_rad_rate = 10.0 * π / 180,
    ρ = 1025.0,
    algorithm = Tsit5(),
    reltol = 1e-8,
    abstol = 1e-8,
)
    target_ψ_rad_deviation = abs(target_ψ_rad_deviation)

    final_δ_list = zeros(length(time_list))
    final_u_list = zeros(length(time_list))
    final_v_list = zeros(length(time_list))
    final_r_list = zeros(length(time_list))
    final_ψ_list = zeros(length(time_list))

    next_stage_index = 1
    target_δ_rad = -target_δ_rad  # for changing in while loop
    ψ = ψ0
    while next_stage_index < length(time_list)
        target_δ_rad = -target_δ_rad
        start_index = next_stage_index

        # Make delta list
        δ_list = zeros(length(time_list) - start_index + 1)
        if start_index == 1
            δ_list[1] = δ0
            u0 = u0
            v0 = v0
            r0 = r0
        else
            δ_list[1] = final_δ_list[start_index-1]
            u0 = final_u_list[start_index-1]
            v0 = final_v_list[start_index-1]
            r0 = final_r_list[start_index-1]
        end

        for i = (start_index+1):length(time_list)
            Δt = time_list[i] - time_list[i-1]
            if target_δ_rad > 0
                δ = δ_list[i-start_index] + δ_rad_rate * Δt
                if δ >= target_δ_rad
                    δ = target_δ_rad
                end
                δ_list[i-start_index+1] = δ
            elseif target_δ_rad <= 0
                δ = δ_list[i-start_index] - δ_rad_rate * Δt
                if δ <= target_δ_rad
                    δ = target_δ_rad
                end
                δ_list[i-start_index+1] = δ
            end
        end

        u, v, r, δ, n_p = mmg_3dof_simulate(
            basic_params,
            maneuvering_params,
            time_list[start_index:end],
            δ_list,
            n_p_list[start_index:end],
            u0 = u0,
            v0 = v0,
            r0 = r0,
            ρ = ρ,
            algorithm = algorithm,
            reltol = reltol,
            abstol = abstol,
        )
        x_dummy, y_dummy, ψ_list =
            calc_position(time_list[start_index:end], u, v, r, x0 = 0.0, y0 = 0.0, ψ0 = ψ)
        # get finish index
        target_ψ_rad = ψ0 + target_ψ_rad_deviation
        if target_δ_rad < 0
            target_ψ_rad = ψ0 - target_ψ_rad_deviation
        end
        over_index = findfirst(e -> e > target_ψ_rad, ψ_list)
        if target_δ_rad < 0
            over_index = findfirst(e -> e < target_ψ_rad, ψ_list)
        end
        next_stage_index = length(time_list)
        if isnothing(over_index)
            final_δ_list[start_index:next_stage_index] = δ_list
            final_u_list[start_index:next_stage_index] = u
            final_v_list[start_index:next_stage_index] = v
            final_r_list[start_index:next_stage_index] = r
            final_ψ_list[start_index:next_stage_index] = ψ_list
        else
            ψ = ψ_list[over_index]
            next_stage_index = over_index + start_index - 1
            final_δ_list[start_index:next_stage_index-1] = δ_list[begin:over_index-1]
            final_u_list[start_index:next_stage_index-1] = u[begin:over_index-1]
            final_v_list[start_index:next_stage_index-1] = v[begin:over_index-1]
            final_r_list[start_index:next_stage_index-1] = r[begin:over_index-1]
            final_ψ_list[start_index:next_stage_index-1] = ψ_list[begin:over_index-1]
        end
    end
    final_u_list, final_v_list, final_r_list, final_ψ_list, final_δ_list
end

function estimate_mmg_approx_lsm(
    data::ShipData,
    basic_params::Mmg3DofBasicParams,
    k_0,
    k_1,
    k_2;
    ρ = 1025.0,
)
    @unpack time, u, v, r, x, y, ψ, δ, n_p = data
    @unpack L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P = basic_params

    # -------------------------------------
    # calculating basic info
    U = sqrt.(u .^ 2 + (v - r .* x_G) .^ 2)

    β = U .^ 0
    for i = 1:length(time)
        if isapprox(U[i], 0.0) == false
            β[i] = asin(-(v[i] - r[i] * x_G) / U[i])
        end
    end

    v_dash = U .^ 0
    for i = 1:length(time)
        if isapprox(U[i], 0.0) == false
            v_dash[i] = v[i] / U[i]
        end
    end

    r_dash = U .^ 0
    for i = 1:length(time)
        if isapprox(U[i], 0.0) == false
            r_dash[i] = r[i] * L_pp / U[i]
        end
    end

    w_P = w_P0 * exp.(.-4.0 * (β - r_dash .* x_P) .^ 2)

    J = U .^ 0
    for i = 1:length(time)
        if isapprox(n_p[i], 0.0) == false
            J[i] = (1.0 - w_P[i]) * u[i] / (n_p[i] * D_p)
        end
    end

    K_T = J .^ 2 .* k_2 + J .* k_1 .+ k_0
    β_R = β - r_dash .* l_R

    γ_R = U .^ 0
    for i = 1:length(time)
        if β_R[i] < 0.0
            γ_R[i] = γ_R_minus
        else
            γ_R[i] = γ_R_plus
        end
    end

    v_R = U .* γ_R .* β_R

    u_R = U .^ 0
    for i = 1:length(time)
        if J[i] == 0.0
            u_R[i] = sqrt(η * (κ * ϵ * 8.0 * k_0 * n_p[i]^2 * D_p^4 / pi)^2)
        else
            u_R[i] =
                u[i] *
                (1.0 - w_P[i]) *
                ϵ *
                sqrt(
                    η * (1.0 + κ * (sqrt(1.0 + 8.0 * K_T[i] / (pi * J[i]^2)) - 1.0))^2 +
                    (1.0 - η),
                )
        end
    end

    U_R = sqrt.(u_R .^ 2 + v_R .^ 2)
    α_R = δ - atan.(v_R, u_R)
    F_N = 0.5 * A_R * ρ * f_α * (U_R .^ 2) .* sin.(α_R)
    X_R = -(1.0 - t_R) .* F_N .* sin.(δ)
    X_P = (1.0 - t_P) * ρ .* K_T .* n_p .^ 2 .* D_p^4
    Y_R = -(1.0 + a_H) .* F_N .* cos.(δ)
    N_R = -(x_R + a_H * x_H) .* F_N .* cos.(δ)
    # -------------------------------------

    # -------------------------------------
    spl_u = Spline1D(time, u)
    spl_v = Spline1D(time, v)
    spl_r = Spline1D(time, r)
    f = (spl, x) -> derivative(spl, x)
    du = f(spl_u, time)
    dv = f(spl_v, time)
    dr = f(spl_r, time)

    # calculating parameters
    Ax = hcat(
        -v_dash .^ 0,
        v_dash .^ 2,
        v_dash .* r_dash,
        r_dash .^ 2,
        (v_dash .^ 2) .* (r_dash .^ 2),
    )
    bx =
        ((m + m_x) .* du - (m + m_y) .* v .* r - x_G * m .* r .^ 2 - X_R - X_P) ./
        (0.5 * ρ * L_pp * d .* (U .^ 2))
    R_0_dash, X_vv_dash, X_vr_dash, X_rr_dash, X_vvvv_dash = Ax \ bx

    Ay = hcat(
        v_dash,
        r_dash,
        v_dash .^ 3,
        (v_dash .^ 2) .* (r_dash),
        (v_dash) .* (r_dash .^ 2),
        (r_dash .^ 3),
    )
    by =
        ((m + m_y) .* dv - (m + m_x) .* u .* r + x_G * m .* dr - Y_R) ./
        (0.5 * ρ * L_pp * d .* (U .^ 2))
    Y_v_dash, Y_r_dash, Y_vvv_dash, Y_vvr_dash, Y_vrr_dash, Y_rrr_dash = Ay \ by

    An = hcat(
        v_dash,
        r_dash,
        v_dash .^ 3,
        (v_dash .^ 2) .* (r_dash),
        (v_dash) .* (r_dash .^ 2),
        (r_dash .^ 3),
    )
    bn =
        ((I_zG + x_G^2 * m + J_z) .* dr + x_G * m .* (dv .+ u .* r) - N_R) ./
        (0.5 * ρ * L_pp .^ 2 * d .* (U .^ 2))
    N_v_dash, N_r_dash, N_vvv_dash, N_vvr_dash, N_vrr_dash, N_rrr_dash = An \ bn

    R_0_dash,
    X_vv_dash,
    X_vr_dash,
    X_rr_dash,
    X_vvvv_dash,
    Y_v_dash,
    Y_r_dash,
    Y_vvv_dash,
    Y_vvr_dash,
    Y_vrr_dash,
    Y_rrr_dash,
    N_v_dash,
    N_r_dash,
    N_vvv_dash,
    N_vvr_dash,
    N_vrr_dash,
    N_rrr_dash
end

function estimate_mmg_approx_lsm_time_window_sampling(
    data::ShipData,
    window_size::Int,
    basic_params::Mmg3DofBasicParams,
    k_0,
    k_1,
    k_2;
    ρ = 1025.0,
)
    n_samples = length(data.time) - window_size
    R_0_dash_samples = zeros(n_samples)
    X_vv_dash_samples = zeros(n_samples)
    X_vr_dash_samples = zeros(n_samples)
    X_rr_dash_samples = zeros(n_samples)
    X_vvvv_dash_samples = zeros(n_samples)
    Y_v_dash_samples = zeros(n_samples)
    Y_r_dash_samples = zeros(n_samples)
    Y_vvv_dash_samples = zeros(n_samples)
    Y_vvr_dash_samples = zeros(n_samples)
    Y_vrr_dash_samples = zeros(n_samples)
    Y_rrr_dash_samples = zeros(n_samples)
    N_v_dash_samples = zeros(n_samples)
    N_r_dash_samples = zeros(n_samples)
    N_vvv_dash_samples = zeros(n_samples)
    N_vvr_dash_samples = zeros(n_samples)
    N_vrr_dash_samples = zeros(n_samples)
    N_rrr_dash_samples = zeros(n_samples)
    for i = 1:n_samples
        sample_data = ShipData(
            data.time[i:i+window_size],
            data.u[i:i+window_size],
            data.v[i:i+window_size],
            data.r[i:i+window_size],
            data.x[i:i+window_size],
            data.y[i:i+window_size],
            data.ψ[i:i+window_size],
            data.δ[i:i+window_size],
            data.n_p[i:i+window_size],
        )
        R_0_dash_samples[i],
        X_vv_dash_samples[i],
        X_vr_dash_samples[i],
        X_rr_dash_samples[i],
        X_vvvv_dash_samples[i],
        Y_v_dash_samples[i],
        Y_r_dash_samples[i],
        Y_vvv_dash_samples[i],
        Y_vvr_dash_samples[i],
        Y_vrr_dash_samples[i],
        Y_rrr_dash_samples[i],
        N_v_dash_samples[i],
        N_r_dash_samples[i],
        N_vvv_dash_samples[i],
        N_vvr_dash_samples[i],
        N_vrr_dash_samples[i],
        N_rrr_dash_samples[i] =
            estimate_mmg_approx_lsm(sample_data, basic_params, k_0, k_1, k_2, ρ = ρ)
    end
    R_0_dash_samples,
    X_vv_dash_samples,
    X_vr_dash_samples,
    X_rr_dash_samples,
    X_vvvv_dash_samples,
    Y_v_dash_samples,
    Y_r_dash_samples,
    Y_vvv_dash_samples,
    Y_vvr_dash_samples,
    Y_vrr_dash_samples,
    Y_rrr_dash_samples,
    N_v_dash_samples,
    N_r_dash_samples,
    N_vvv_dash_samples,
    N_vvr_dash_samples,
    N_vrr_dash_samples,
    N_rrr_dash_samples
end

function create_model_for_mcmc_sample_mmg(
    data::ShipData,
    basic_params::Mmg3DofBasicParams,
    k_0,
    k_1,
    k_2;
    ρ = 1025.0,
    σ_u_prior_dist = Uniform(0.00, 0.20),
    σ_v_prior_dist = Uniform(0.00, 0.20),
    σ_r_prior_dist = Uniform(0.00, 0.20),
    R_0_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vv_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vr_dash_prior_dist = Uniform(-2.0, 2.0),
    X_rr_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vvvv_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_v_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_r_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vvv_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vvr_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vrr_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_rrr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_v_dash_prior_dist = Uniform(-2.0, 2.0),
    N_r_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vvv_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vvr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vrr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_rrr_dash_prior_dist = Uniform(-2.0, 2.0),
    solver = Tsit5(),
    abstol = 1e-6,
    reltol = 1e-3,
)
    time_obs = data.time
    u_obs = data.u
    v_obs = data.v
    r_obs = data.r
    δ_obs = data.δ
    n_p_obs = data.n_p

    @unpack L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P = basic_params

    # create sytem model
    spl_δ = Spline1D(time_obs, δ_obs)
    spl_n_p = Spline1D(time_obs, n_p_obs)
    function MMG!(dX, X, p, t)
        u, v, r, δ, n_p = X
        R_0_dash,
        X_vv_dash,
        X_vr_dash,
        X_rr_dash,
        X_vvvv_dash,
        Y_v_dash,
        Y_r_dash,
        Y_vvv_dash,
        Y_vvr_dash,
        Y_vrr_dash,
        Y_rrr_dash,
        N_v_dash,
        N_r_dash,
        N_vvv_dash,
        N_vvr_dash,
        N_vrr_dash,
        N_rrr_dash = p

        U = sqrt(u^2 + (v - r * x_G)^2)

        β = 0.0
        if U == 0.0
            β = 0.0
        else
            β = asin(-(v - r * x_G) / U)
        end

        v_dash = 0.0
        if U == 0.0
            v_dash = 0.0
        else
            v_dash = v / U
        end

        r_dash = 0.0
        if U == 0.0
            r_dash = 0.0
        else
            r_dash = r * L_pp / U
        end

        w_P = w_P0 * exp(-4.0 * (β - x_P * r_dash)^2)

        J = 0.0
        if n_p == 0.0
            J = 0.0
        else
            J = (1 - w_P) * u / (n_p * D_p)
        end
        K_T = k_0 + k_1 * J + k_2 * J^2
        β_R = β - l_R * r_dash
        γ_R = γ_R_minus

        if β_R < 0.0
            γ_R = γ_R_minus
        else
            γ_R = γ_R_plus
        end

        v_R = U * γ_R * β_R

        u_R = 0.0
        if J == 0.0
            u_R = sqrt(η * (κ * ϵ * 8.0 * k_0 * n_p^2 * D_p^4 / pi)^2)
        else
            u_R =
                u *
                (1.0 - w_P) *
                ϵ *
                sqrt(η * (1.0 + κ * (sqrt(1.0 + 8.0 * K_T / (pi * J^2)) - 1))^2 + (1 - η))
        end

        U_R = sqrt(u_R^2 + v_R^2)
        α_R = δ - atan(v_R, u_R)
        F_N = 0.5 * A_R * ρ * f_α * (U_R^2) * sin(α_R)

        X_H = (
            0.5 *
            ρ *
            L_pp *
            d *
            (U^2) *
            (
                -R_0_dash +
                X_vv_dash * (v_dash^2) +
                X_vr_dash * v_dash * r_dash +
                X_rr_dash * (r_dash^2) +
                X_vvvv_dash * (v_dash^4)
            )
        )
        X_R = -(1.0 - t_R) * F_N * sin(δ)
        X_P = (1.0 - t_P) * ρ * K_T * n_p^2 * D_p^4
        Y_H = (
            0.5 *
            ρ *
            L_pp *
            d *
            (U^2) *
            (
                Y_v_dash * v_dash +
                Y_r_dash * r_dash +
                Y_vvv_dash * (v_dash^3) +
                Y_vvr_dash * (v_dash^2) * r_dash +
                Y_vrr_dash * v_dash * (r_dash^2) +
                Y_rrr_dash * (r_dash^3)
            )
        )
        Y_R = -(1 + a_H) * F_N * cos(δ)
        N_H = (
            0.5 *
            ρ *
            (L_pp^2) *
            d *
            (U^2) *
            (
                N_v_dash * v_dash +
                N_r_dash * r_dash +
                N_vvv_dash * (v_dash^3) +
                N_vvr_dash * (v_dash^2) * r_dash +
                N_vrr_dash * v_dash * (r_dash^2) +
                N_rrr_dash * (r_dash^3)
            )
        )
        N_R = -(x_R + a_H * x_H) * F_N * cos(δ)
        dX[1] = du = ((X_H + X_R + X_P) + (m + m_y) * v * r + x_G * m * (r^2)) / (m + m_x)
        dX[2] =
            dv =
                (
                    (x_G^2) * (m^2) * u * r - (N_H + N_R) * x_G * m +
                    ((Y_H + Y_R) - (m + m_x) * u * r) * (I_zG + J_z + (x_G^2) * m)
                ) / ((I_zG + J_z + (x_G^2) * m) * (m + m_y) - (x_G^2) * (m^2))
        dX[3] = dr = (N_H + N_R - x_G * m * (dv + u * r)) / (I_zG + J_z + (x_G^2) * m)
        dX[4] = dδ = derivative(spl_δ, t)
        dX[5] = dn_p = derivative(spl_n_p, t)
    end

    R_0_dash_start = 0.022
    X_vv_dash_start = -0.040
    X_vr_dash_start = 0.002
    X_rr_dash_start = 0.011
    X_vvvv_dash_start = 0.771
    Y_v_dash_start = -0.315
    Y_r_dash_start = 0.083
    Y_vvv_dash_start = -1.607
    Y_vvr_dash_start = 0.379
    Y_vrr_dash_start = -0.391
    Y_rrr_dash_start = 0.008
    N_v_dash_start = -0.137
    N_r_dash_start = -0.049
    N_vvv_dash_start = -0.030
    N_vvr_dash_start = -0.294
    N_vrr_dash_start = 0.055
    N_rrr_dash_start = -0.013

    p = [
        R_0_dash_start,
        X_vv_dash_start,
        X_vr_dash_start,
        X_rr_dash_start,
        X_vvvv_dash_start,
        Y_v_dash_start,
        Y_r_dash_start,
        Y_vvv_dash_start,
        Y_vvr_dash_start,
        Y_vrr_dash_start,
        Y_rrr_dash_start,
        N_v_dash_start,
        N_r_dash_start,
        N_vvv_dash_start,
        N_vvr_dash_start,
        N_vrr_dash_start,
        N_rrr_dash_start,
    ]

    u0 = 2.29 * 0.512
    v0 = 0.0
    r0 = 0.0
    X0 = [u_obs[1]; v_obs[1]; r_obs[1]; δ_obs[1]; n_p_obs[1]]
    prob1 = ODEProblem(MMG!, X0, (time_obs[1], time_obs[end]), p)

    # create probabilistic model
    @model function fitMMG(time_obs, obs, prob1)
        σ_u ~ σ_u_prior_dist
        σ_v ~ σ_v_prior_dist
        σ_r ~ σ_r_prior_dist
        R_0_dash ~ R_0_dash_prior_dist
        X_vv_dash ~ X_vv_dash_prior_dist
        X_vr_dash ~ X_vr_dash_prior_dist
        X_rr_dash ~ X_rr_dash_prior_dist
        X_vvvv_dash ~ X_vvvv_dash_prior_dist
        Y_v_dash ~ Y_v_dash_prior_dist
        Y_r_dash ~ Y_r_dash_prior_dist
        Y_vvv_dash ~ Y_vvv_dash_prior_dist
        Y_vvr_dash ~ Y_vvr_dash_prior_dist
        Y_vrr_dash ~ Y_vrr_dash_prior_dist
        Y_rrr_dash ~ Y_rrr_dash_prior_dist
        N_v_dash ~ N_v_dash_prior_dist
        N_r_dash ~ N_r_dash_prior_dist
        N_vvv_dash ~ N_vvv_dash_prior_dist
        N_vvr_dash ~ N_vvr_dash_prior_dist
        N_vrr_dash ~ N_vrr_dash_prior_dist
        N_rrr_dash ~ N_rrr_dash_prior_dist

        p = [
            R_0_dash,
            X_vv_dash,
            X_vr_dash,
            X_rr_dash,
            X_vvvv_dash,
            Y_v_dash,
            Y_r_dash,
            Y_vvv_dash,
            Y_vvr_dash,
            Y_vrr_dash,
            Y_rrr_dash,
            N_v_dash,
            N_r_dash,
            N_vvv_dash,
            N_vvr_dash,
            N_vrr_dash,
            N_rrr_dash,
        ]
        prob = remake(prob1, p = p)
        sol = solve(prob, solver, abstol = abstol, reltol = reltol)
        predicted = sol(time_obs)
        for i = 1:length(predicted)
            obs[1][i] ~ Normal(predicted[i][1], σ_u) # u
            obs[2][i] ~ Normal(predicted[i][2], σ_v) # v
            obs[3][i] ~ Normal(predicted[i][3], σ_r) # r
        end
    end

    return fitMMG(time_obs, [u_obs, v_obs, r_obs], prob1)
end

function create_model_for_mcmc_sample_mmg(
    data::ShipData,
    basic_params::Mmg3DofBasicParams,
    k_0,
    k_1,
    k_2,
    σ_u,
    σ_v,
    σ_r;
    ρ = 1025.0,
    R_0_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vv_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vr_dash_prior_dist = Uniform(-2.0, 2.0),
    X_rr_dash_prior_dist = Uniform(-2.0, 2.0),
    X_vvvv_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_v_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_r_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vvv_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vvr_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_vrr_dash_prior_dist = Uniform(-2.0, 2.0),
    Y_rrr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_v_dash_prior_dist = Uniform(-2.0, 2.0),
    N_r_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vvv_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vvr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_vrr_dash_prior_dist = Uniform(-2.0, 2.0),
    N_rrr_dash_prior_dist = Uniform(-2.0, 2.0),
    solver = Tsit5(),
    abstol = 1e-6,
    reltol = 1e-3,
)
    time_obs = data.time
    u_obs = data.u
    v_obs = data.v
    r_obs = data.r
    δ_obs = data.δ
    n_p_obs = data.n_p

    @unpack L_pp,
    B,
    d,
    x_G,
    D_p,
    m,
    I_zG,
    A_R,
    η,
    m_x,
    m_y,
    J_z,
    f_α,
    ϵ,
    t_R,
    x_R,
    a_H,
    x_H,
    γ_R_minus,
    γ_R_plus,
    l_R,
    κ,
    t_P,
    w_P0,
    x_P = basic_params

    # create sytem model
    spl_δ = Spline1D(time_obs, δ_obs)
    spl_n_p = Spline1D(time_obs, n_p_obs)
    function MMG!(dX, X, p, t)
        u, v, r, δ, n_p = X
        R_0_dash,
        X_vv_dash,
        X_vr_dash,
        X_rr_dash,
        X_vvvv_dash,
        Y_v_dash,
        Y_r_dash,
        Y_vvv_dash,
        Y_vvr_dash,
        Y_vrr_dash,
        Y_rrr_dash,
        N_v_dash,
        N_r_dash,
        N_vvv_dash,
        N_vvr_dash,
        N_vrr_dash,
        N_rrr_dash = p

        U = sqrt(u^2 + (v - r * x_G)^2)

        β = 0.0
        if U == 0.0
            β = 0.0
        else
            β = asin(-(v - r * x_G) / U)
        end

        v_dash = 0.0
        if U == 0.0
            v_dash = 0.0
        else
            v_dash = v / U
        end

        r_dash = 0.0
        if U == 0.0
            r_dash = 0.0
        else
            r_dash = r * L_pp / U
        end

        w_P = w_P0 * exp(-4.0 * (β - x_P * r_dash)^2)

        J = 0.0
        if n_p == 0.0
            J = 0.0
        else
            J = (1 - w_P) * u / (n_p * D_p)
        end
        K_T = k_0 + k_1 * J + k_2 * J^2
        β_R = β - l_R * r_dash
        γ_R = γ_R_minus

        if β_R < 0.0
            γ_R = γ_R_minus
        else
            γ_R = γ_R_plus
        end

        v_R = U * γ_R * β_R

        u_R = 0.0
        if J == 0.0
            u_R = sqrt(η * (κ * ϵ * 8.0 * k_0 * n_p^2 * D_p^4 / pi)^2)
        else
            u_R =
                u *
                (1.0 - w_P) *
                ϵ *
                sqrt(η * (1.0 + κ * (sqrt(1.0 + 8.0 * K_T / (pi * J^2)) - 1))^2 + (1 - η))
        end

        U_R = sqrt(u_R^2 + v_R^2)
        α_R = δ - atan(v_R, u_R)
        F_N = 0.5 * A_R * ρ * f_α * (U_R^2) * sin(α_R)

        X_H = (
            0.5 *
            ρ *
            L_pp *
            d *
            (U^2) *
            (
                -R_0_dash +
                X_vv_dash * (v_dash^2) +
                X_vr_dash * v_dash * r_dash +
                X_rr_dash * (r_dash^2) +
                X_vvvv_dash * (v_dash^4)
            )
        )
        X_R = -(1.0 - t_R) * F_N * sin(δ)
        X_P = (1.0 - t_P) * ρ * K_T * n_p^2 * D_p^4
        Y_H = (
            0.5 *
            ρ *
            L_pp *
            d *
            (U^2) *
            (
                Y_v_dash * v_dash +
                Y_r_dash * r_dash +
                Y_vvv_dash * (v_dash^3) +
                Y_vvr_dash * (v_dash^2) * r_dash +
                Y_vrr_dash * v_dash * (r_dash^2) +
                Y_rrr_dash * (r_dash^3)
            )
        )
        Y_R = -(1 + a_H) * F_N * cos(δ)
        N_H = (
            0.5 *
            ρ *
            (L_pp^2) *
            d *
            (U^2) *
            (
                N_v_dash * v_dash +
                N_r_dash * r_dash +
                N_vvv_dash * (v_dash^3) +
                N_vvr_dash * (v_dash^2) * r_dash +
                N_vrr_dash * v_dash * (r_dash^2) +
                N_rrr_dash * (r_dash^3)
            )
        )
        N_R = -(x_R + a_H * x_H) * F_N * cos(δ)
        dX[1] = du = ((X_H + X_R + X_P) + (m + m_y) * v * r + x_G * m * (r^2)) / (m + m_x)
        dX[2] =
            dv =
                (
                    (x_G^2) * (m^2) * u * r - (N_H + N_R) * x_G * m +
                    ((Y_H + Y_R) - (m + m_x) * u * r) * (I_zG + J_z + (x_G^2) * m)
                ) / ((I_zG + J_z + (x_G^2) * m) * (m + m_y) - (x_G^2) * (m^2))
        dX[3] = dr = (N_H + N_R - x_G * m * (dv + u * r)) / (I_zG + J_z + (x_G^2) * m)
        dX[4] = dδ = derivative(spl_δ, t)
        dX[5] = dn_p = derivative(spl_n_p, t)
    end

    R_0_dash_start = 0.022
    X_vv_dash_start = -0.040
    X_vr_dash_start = 0.002
    X_rr_dash_start = 0.011
    X_vvvv_dash_start = 0.771
    Y_v_dash_start = -0.315
    Y_r_dash_start = 0.083
    Y_vvv_dash_start = -1.607
    Y_vvr_dash_start = 0.379
    Y_vrr_dash_start = -0.391
    Y_rrr_dash_start = 0.008
    N_v_dash_start = -0.137
    N_r_dash_start = -0.049
    N_vvv_dash_start = -0.030
    N_vvr_dash_start = -0.294
    N_vrr_dash_start = 0.055
    N_rrr_dash_start = -0.013

    p = [
        R_0_dash_start,
        X_vv_dash_start,
        X_vr_dash_start,
        X_rr_dash_start,
        X_vvvv_dash_start,
        Y_v_dash_start,
        Y_r_dash_start,
        Y_vvv_dash_start,
        Y_vvr_dash_start,
        Y_vrr_dash_start,
        Y_rrr_dash_start,
        N_v_dash_start,
        N_r_dash_start,
        N_vvv_dash_start,
        N_vvr_dash_start,
        N_vrr_dash_start,
        N_rrr_dash_start,
    ]

    u0 = 2.29 * 0.512
    v0 = 0.0
    r0 = 0.0
    X0 = [u_obs[1]; v_obs[1]; r_obs[1]; δ_obs[1]; n_p_obs[1]]
    prob1 = ODEProblem(MMG!, X0, (time_obs[1], time_obs[end]), p)

    # create probabilistic model
    @model function fitMMG(time_obs, obs, prob1)
        R_0_dash ~ R_0_dash_prior_dist
        X_vv_dash ~ X_vv_dash_prior_dist
        X_vr_dash ~ X_vr_dash_prior_dist
        X_rr_dash ~ X_rr_dash_prior_dist
        X_vvvv_dash ~ X_vvvv_dash_prior_dist
        Y_v_dash ~ Y_v_dash_prior_dist
        Y_r_dash ~ Y_r_dash_prior_dist
        Y_vvv_dash ~ Y_vvv_dash_prior_dist
        Y_vvr_dash ~ Y_vvr_dash_prior_dist
        Y_vrr_dash ~ Y_vrr_dash_prior_dist
        Y_rrr_dash ~ Y_rrr_dash_prior_dist
        N_v_dash ~ N_v_dash_prior_dist
        N_r_dash ~ N_r_dash_prior_dist
        N_vvv_dash ~ N_vvv_dash_prior_dist
        N_vvr_dash ~ N_vvr_dash_prior_dist
        N_vrr_dash ~ N_vrr_dash_prior_dist
        N_rrr_dash ~ N_rrr_dash_prior_dist

        p = [
            R_0_dash,
            X_vv_dash,
            X_vr_dash,
            X_rr_dash,
            X_vvvv_dash,
            Y_v_dash,
            Y_r_dash,
            Y_vvv_dash,
            Y_vvr_dash,
            Y_vrr_dash,
            Y_rrr_dash,
            N_v_dash,
            N_r_dash,
            N_vvv_dash,
            N_vvr_dash,
            N_vrr_dash,
            N_rrr_dash,
        ]
        prob = remake(prob1, p = p)
        sol = solve(prob, solver, abstol = abstol, reltol = reltol)
        predicted = sol(time_obs)
        for i = 1:length(predicted)
            obs[1][i] ~ Normal(predicted[i][1], σ_u) # u
            obs[2][i] ~ Normal(predicted[i][2], σ_v) # v
            obs[3][i] ~ Normal(predicted[i][3], σ_r) # r
        end
    end

    return fitMMG(time_obs, [u_obs, v_obs, r_obs], prob1)
end


@doc raw"""
    Hyperbolic{N} <: AbstractEmbeddedManifold{ℝ,DefaultIsometricEmbeddingType}

The hyperbolic space $\mathcal H^n$ represented by $n+1$-Tuples, i.e. embedded in the
[`Lorentz`](@ref)ian manifold equipped with the [`MinkowskiMetric`](@ref)
$⟨\cdot,\cdot⟩_{\mathrm{M}}$. The space is defined as

```math
\mathcal H^n = \Bigl\{p ∈ ℝ^{n+1}\ \Big|\ ⟨p,p⟩_{\mathrm{M}}= -p_{n+1}^2
  + \displaystyle\sum_{k=1}^n p_k^2 = -1, p_{n+1} > 0\Bigr\},.
```

The tangent space $T_p \mathcal H^n$ is given by

````math
T_p \mathcal H^n := \bigl\{
X ∈ ℝ^{n+1} : ⟨p,X⟩_{\mathrm{M}} = 0
\bigr\}.
````
Note that while the [`MinkowskiMetric`](@ref) renders the [`Lorentz`](@ref) manifold (only)
pseudo-Riemannian, on the tangent bundle of the Hyperbolic space it induces a Riemannian
metric. The corresponding sectional curvature is $-1$.

If `p` and `X` are `Vector`s of length `n+1` they are assumed to be
a [`HyperboloidPoint`](@ref) and a [`HyperboloidTVector`](@ref), respectively

Other models are the Poincaré ball model, see [`PoincareBallPoint`](@ref) and [`PoincareBallTVector`](@ref), respectiely
and the Poincaré half space model, see [`PoincareHalfSpacePoint`](@ref) and [`PoincareHalfSpaceTVector`](@ref), respectively.

# Constructor

    Hyperbolic(n)

Generate the Hyperbolic manifold of dimension `n`.
"""
struct Hyperbolic{N} <: AbstractEmbeddedManifold{ℝ,DefaultIsometricEmbeddingType} end

Hyperbolic(n::Int) = Hyperbolic{n}()

@doc raw"""
    HyperboloidPoint <: AbstractManifoldPoint

In the Hyperboloid model of the [`Hyperbolic`](@ref) $\mathcal H^n$ points are represented
as vectors in $ℝ^{n+1}$ with [`MinkowskiMetric`](@ref) equal to $-1$.

This representation is the default, i.e. `AbstractVector`s are assumed to have this repesentation.
"""
struct HyperboloidPoint{TValue<:AbstractVector} <: AbstractManifoldPoint
    value::TValue
end

@doc raw"""
    HyperboloidTVector <: TVector

In the Hyperboloid model of the [`Hyperbolic`](@ref) $\mathcal H^n$ tangent vctors are represented
as vectors in $ℝ^{n+1}$ with [`MinkowskiMetric`](@ref) $⟨p,X⟩_{\mathrm{M}}=0$ to their base
point $p$.

This representation is the default, i.e. vectors are assumed to have this repesentation.
"""
struct HyperboloidTVector{TValue<:AbstractVector} <: TVector
    value::TValue
end

@doc raw"""
    PoincareBallPoint <: AbstractManifoldPoint

A point on the [`Hyperbolic`](@ref) manifold $\mathcal H^n$ can be represented as a vector of norm
less than one in $\mathbb R^n$.
"""
struct PoincareBallPoint{TValue<:AbstractVector} <: AbstractManifoldPoint
    value::TValue
end

@doc raw"""
    PoincareBallTVector <: TVector

In the Poincaré ball model of the [`Hyperbolic`](@ref) $\mathcal H^n$ tangent vectors are represented
as vectors in $ℝ^{n}$.
"""
struct PoincareBallTVector{TValue<:AbstractVector} <: AbstractManifoldPoint
    value::TValue
end

@doc raw"""
    PoincareHalfSpacePoint <: AbstractManifoldPoint

A point on the [`Hyperbolic`](@ref) manifold $\mathcal H^n$ can be represented as a vector in the
half plane, i.e. $x ∈ ℝ^n$ with $x_d > 0$.
"""
struct PoincareHalfSpacePoint{TValue<:AbstractVector} <: AbstractManifoldPoint
    value::TValue
end

@doc raw"""
    PoincareHalfPlaneTVector <: TVector

In the Poincaré half plane model of the [`Hyperbolic`](@ref) $\mathcal H^n$ tangent vectors are
represented as vectors in $ℝ^{n}$.
"""
struct PoincareHalfSpaceTVector{TValue<:AbstractVector} <: TVector
    value::TValue
end

include("HyperbolicHyperboloid.jl")
include("HyperbolicPoincareBall.jl")
include("HyperbolicPoincareHalfspace.jl")

_HyperbolicPointTypes = [HyperboloidPoint, PoincareBallPoint, PoincareHalfSpacePoint]
_HyperbolicTangentTypes =
    [HyperboloidTVector, PoincareBallTVector, PoincareHalfSpaceTVector]
_HyperbolicTypes = [_HyperbolicPointTypes..., _HyperbolicTangentTypes...]

for T in _HyperbolicTangentTypes
    @eval begin
        Base.:*(v::$T, s::Number) = $T(v.value * s)
        Base.:*(s::Number, v::$T) = $T(s * v.value)
        Base.:/(v::$T, s::Number) = $T(v.value / s)
        Base.:\(s::Number, v::$T) = $T(s \ v.value)
        Base.:+(v::$T, w::$T) = $T(v.value + w.value)
        Base.:-(v::$T, w::$T) = $T(v.value - w.value)
        Base.:-(v::$T) = $T(-v.value)
        Base.:+(v::$T) = $T(v.value)
    end
end

for T in _HyperbolicTypes
    @eval begin
        Base.:(==)(v::$T, w::$T) = (v.value == w.value)

        allocate(p::$T) = $T(allocate(p.value))
        allocate(p::$T, ::Type{P}) where {P} = $T(allocate(p.value, P))
        allocate(p::$T, ::Type{P}, dims::Tuple) where {P} = $T(allocate(p.value, P, dims))

        @inline Base.copy(p::$T) = $T(copy(p.value))
        Base.copyto!(q::$T, p::$T) = copyto!(q.value, p.value)

        Base.similar(p::$T) = $T(similar(p.value))

        function Broadcast.BroadcastStyle(::Type{<:$T})
            return Broadcast.Style{$T}()
        end
        function Broadcast.BroadcastStyle(
            ::Broadcast.AbstractArrayStyle{0},
            b::Broadcast.Style{$T},
        )
            return b
        end

        Broadcast.instantiate(bc::Broadcast.Broadcasted{Broadcast.Style{$T},Nothing}) = bc
        function Broadcast.instantiate(bc::Broadcast.Broadcasted{Broadcast.Style{$T}})
            Broadcast.check_broadcast_axes(bc.axes, bc.args...)
            return bc
        end

        Broadcast.broadcastable(v::$T) = v

        @inline function Base.copy(bc::Broadcast.Broadcasted{Broadcast.Style{$T}})
            return $T(Broadcast._broadcast_getindex(bc, 1))
        end

        Base.@propagate_inbounds Broadcast._broadcast_getindex(v::$T, I) = v.value

        Base.axes(v::$T) = axes(v.value)

        @inline function Base.copyto!(
            dest::$T,
            bc::Broadcast.Broadcasted{Broadcast.Style{$T}},
        )
            axes(dest) == axes(bc) || Broadcast.throwdm(axes(dest), axes(bc))
            # Performance optimization: broadcast!(identity, dest, A) is equivalent to copyto!(dest, A) if indices match
            if bc.f === identity && bc.args isa Tuple{$T} # only a single input argument to broadcast!
                A = bc.args[1]
                if axes(dest) == axes(A)
                    return copyto!(dest, A)
                end
            end
            bc′ = Broadcast.preprocess(dest, bc)
            # Performance may vary depending on whether `@inbounds` is placed outside the
            # for loop or not. (cf. https://github.com/JuliaLang/julia/issues/38086)
            copyto!(dest.value, bc′[1])
            return dest
        end
    end
end

for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval allocate(p::$P, ::Type{$T}) = $T(allocate(p.value))
    @eval allocate_result_type(::Hyperbolic, ::typeof(log), ::Tuple{$P,$P}) = $T
    @eval allocate_result_type(::Hyperbolic, ::typeof(inverse_retract), ::Tuple{$P,$P}) = $T
end

@doc raw"""
    check_point(M::Hyperbolic, p; kwargs...)

Check whether `p` is a valid point on the [`Hyperbolic`](@ref) `M`.

For the [`HyperboloidPoint`](@ref) or plain vectors this means that, `p` is a vector of
length $n+1$ with inner product in the embedding of -1, see [`MinkowskiMetric`](@ref).
The tolerance for the last test can be set using the `kwargs...`.

For the [`PoincareBallPoint`](@ref) a valid point is a vector $p ∈ ℝ^n$ with a norm stricly
less than 1.

For the [`PoincareHalfSpacePoint`](@ref) a valid point is a vector from $p ∈ ℝ^n$ with a positive
last entry, i.e. $p_n>0$
"""
check_point(::Hyperbolic, ::Any)

@doc raw"""
    check_vector(M::Hyperbolic{n}, p, X; kwargs... )

Check whether `X` is a tangent vector to `p` on the [`Hyperbolic`](@ref) `M`, i.e.
after [`check_point`](@ref)`(M,p)`, `X` has to be of the same dimension as `p`.
The tolerance for the last test can be set using the `kwargs...`.

For a the hyperboloid model or vectors, `X` has to be  orthogonal to `p` with respect
to the inner product from the embedding, see [`MinkowskiMetric`](@ref).

For a the Poincaré ball as well as the Poincaré half plane model, `X` has to be a vector from $ℝ^{n}$.
"""
check_vector(::Hyperbolic, ::Any, ::Any)

function check_vector(
    M::Hyperbolic{N},
    p,
    X::Union{PoincareBallTVector,PoincareHalfSpaceTVector};
    kwargs...,
) where {N}
    return check_point(Euclidean(N), X.value; kwargs...)
end

# Define self conversions
#
for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval convert(::Type{$T}, p::$P, X::$T) = X
    @eval function convert(
        ::Type{Tuple{AbstractVector,AbstractVector}},
        (p, X)::Tuple{$P,$T},
    )
        return (convert(AbstractVector, p), convert(AbstractVector, p, X))
    end
end

decorated_manifold(::Hyperbolic{N}) where {N} = Lorentz(N + 1, MinkowskiMetric())

default_metric_dispatch(::Hyperbolic, ::MinkowskiMetric) = Val(true)

@doc raw"""
    exp(M::Hyperbolic, p, X)

Compute the exponential map on the [`Hyperbolic`](@ref) space $\mathcal H^n$ emanating
from `p` towards `X`. The formula reads

````math
\exp_p X = \cosh(\sqrt{⟨X,X⟩_{\mathrm{M}}})p
+ \sinh(\sqrt{⟨X,X⟩_{\mathrm{M}}})\frac{X}{\sqrt{⟨X,X⟩_{\mathrm{M}}}},
````

where $⟨\cdot,\cdot⟩_{\mathrm{M}}$ denotes the [`MinkowskiMetric`](@ref) on the embedding,
the [`Lorentz`](@ref)ian manifold.
"""
exp(::Hyperbolic, ::Any...)

for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval function exp!(M::Hyperbolic, q::$P, p::$P, X::$T)
        q.value .=
            convert(
                $P,
                exp(M, convert(AbstractVector, p), convert(AbstractVector, p, X)),
            ).value
        return q
    end
end

@doc raw"""
    injectivity_radius(M::Hyperbolic)
    injectivity_radius(M::Hyperbolic, p)

Return the injectivity radius on the [`Hyperbolic`](@ref), which is $∞$.
"""
injectivity_radius(::Hyperbolic) = Inf
injectivity_radius(::Hyperbolic, ::ExponentialRetraction) = Inf
injectivity_radius(::Hyperbolic, ::Any) = Inf
injectivity_radius(::Hyperbolic, ::Any, ::ExponentialRetraction) = Inf
eval(
    quote
        @invoke_maker 1 AbstractManifold injectivity_radius(
            M::Hyperbolic,
            rm::AbstractRetractionMethod,
        )
    end,
)

for T in _HyperbolicPointTypes
    @eval function isapprox(::Hyperbolic, p::$T, q::$T; kwargs...)
        return isapprox(p.value, q.value; kwargs...)
    end
end
for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval function isapprox(::Hyperbolic, ::$P, X::$T, Y::$T; kwargs...)
        return isapprox(X.value, Y.value; kwargs...)
    end
end

@doc raw"""
    log(M::Hyperbolic, p, q)

Compute the logarithmic map on the [`Hyperbolic`](@ref) space $\mathcal H^n$, the tangent
vector representing the [`geodesic`](@ref) starting from `p`
reaches `q` after time 1. The formula reads for $p ≠ q$

```math
\log_p q = d_{\mathcal H^n}(p,q)
\frac{q-⟨p,q⟩_{\mathrm{M}} p}{\lVert q-⟨p,q⟩_{\mathrm{M}} p \rVert_2},
```

where $⟨\cdot,\cdot⟩_{\mathrm{M}}$ denotes the [`MinkowskiMetric`](@ref) on the embedding,
the [`Lorentz`](@ref)ian manifold. For $p=q$ the logarihmic map is equal to the zero vector.
"""
log(::Hyperbolic, ::Any...)

for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval function log!(M::Hyperbolic, X::$T, p::$P, q::$P)
        X.value .=
            convert(
                $T,
                convert(AbstractVector, p),
                log(M, convert(AbstractVector, p), convert(AbstractVector, q)),
            ).value
        return X
    end
end

@doc raw"""
    manifold_dimension(M::Hyperbolic)

Return the dimension of the hyperbolic space manifold $\mathcal H^n$, i.e. $\dim(\mathcal H^n) = n$.
"""
manifold_dimension(::Hyperbolic{N}) where {N} = N

"""
    mean(
        M::Hyperbolic,
        x::AbstractVector,
        [w::AbstractWeights,]
        method = CyclicProximalPointEstimation();
        kwargs...,
    )

Compute the Riemannian [`mean`](@ref mean(M::AbstractManifold, args...)) of `x` on the
[`Hyperbolic`](@ref) space using [`CyclicProximalPointEstimation`](@ref).
"""
mean(::Hyperbolic, ::Any...)

function Statistics.mean!(M::Hyperbolic, p, x::AbstractVector, w::AbstractVector; kwargs...)
    return mean!(M, p, x, w, CyclicProximalPointEstimation(); kwargs...)
end

for T in _HyperbolicTypes
    @eval number_eltype(p::$T) = typeof(one(eltype(p.value)))
end

@doc raw"""
    project(M::Hyperbolic, p, X)

Perform an orthogonal projection with respect to the Minkowski inner product of `X` onto
the tangent space at `p` of the [`Hyperbolic`](@ref) space `M`.

The formula reads
````math
Y = X + ⟨p,X⟩_{\mathrm{M}} p,
````
where $⟨\cdot, \cdot⟩_{\mathrm{M}}$ denotes the [`MinkowskiMetric`](@ref) on the embedding,
the [`Lorentz`](@ref)ian manifold.

!!! note

    Projection is only available for the (default) [`HyperboloidTVector`](@ref) representation,
    the others don't have such an embedding
"""
project(::Hyperbolic, ::Any, ::Any)

Base.show(io::IO, ::Hyperbolic{N}) where {N} = print(io, "Hyperbolic($(N))")
for T in _HyperbolicTypes
    @eval Base.show(io::IO, p::$T) = print(io, "$($T)($(p.value))")
end

@doc raw"""
    vector_transport_to(M::Hyperbolic, p, X, q, ::ParallelTransport)

Compute the paralllel transport of the `X` from the tangent space at `p` on the
[`Hyperbolic`](@ref) space $\mathcal H^n$ to the tangent at `q` along the [`geodesic`](@ref)
connecting `p` and `q`. The formula reads

````math
\mathcal P_{q←p}X = X - \frac{⟨\log_p q,X⟩_p}{d^2_{\mathcal H^n}(p,q)}
\bigl(\log_p q + \log_qp \bigr),
````
where $⟨\cdot,\cdot⟩_p$ denotes the inner product in the tangent space at `p`.
"""
vector_transport_to(::Hyperbolic, ::Any, ::Any, ::Any, ::ParallelTransport)

for (P, T) in zip(_HyperbolicPointTypes, _HyperbolicTangentTypes)
    @eval function vector_transport_to!(
        M::Hyperbolic,
        Y::$T,
        p::$P,
        X::$T,
        q::$P,
        m::ParallelTransport,
    )
        Y.value .=
            convert(
                $T,
                convert(AbstractVector, q),
                vector_transport_to(
                    M,
                    convert(AbstractVector, p),
                    convert(AbstractVector, p, X),
                    convert(AbstractVector, q),
                    m,
                ),
            ).value
        return Y
    end
    @eval zero_vector(::Hyperbolic, p::$P) = $T(zero(p.value))
    @eval zero_vector!(::Hyperbolic, X::$T, ::$P) = fill!(X.value, 0)
end


const βc = log(1+sqrt(2))/2

βofh(h) = βc/asinh(1) * asinh(sqrt(1/h))
hofβ(β) = sinh(β * asinh(1)/βc)^(-2)

function magofβ(β)
    if β > βc
        (1-sinh(2*β)^-4)^(1/8)
    else
        0
    end
end

#Crone Thesis tensors:
function isingtensor(β)
    a = DTensor{Float64}((2,2,2,2))
    initwithzero!(a)
    a[1,1,1,1] = a[2,2,2,2] = 1
    cβ, sβ = sqrt(cosh(β)), sqrt(sinh(β))
    Q = DTensor(1/sqrt(2) * [cβ+sβ cβ-sβ; cβ-sβ cβ+sβ])
    @tensor a[1,2,3,4] := a[-1,-2,-3,-4] * Q[-1,1] * Q[-2,2] * Q[-3,3] * Q[-4,4]
    return a
end

function isingmagtensor(β)
    a = DTensor{Float64}((2,2,2,2))
    initwithzero!(a)
    a[1,1,1,1] = 1
    a[2,2,2,2] = -1
    cβ, sβ = sqrt(cosh(β)), sqrt(sinh(β))
    Q = DTensor(1/sqrt(2) * [cβ+sβ cβ-sβ; cβ-sβ cβ+sβ])
    @tensor a[1,2,3,4] := a[-1,-2,-3,-4] * Q[-1,1] * Q[-2,2] * Q[-3,3] * Q[-4,4]
    return a
end

function isingenvironment(β, χ; fixed::Bool = true)
    C, T = zeros(Float64,χ,χ), zeros(Float64, χ, 2, χ)
    C[1,1] = T[1,1,1] =  1
    !fixed && (C[2,2] = T[2,2,2] = 1)

    cβ, sβ = sqrt(cosh(β)), sqrt(sinh(β))
    q = 1/sqrt(2) * [cβ+sβ cβ-sβ; cβ-sβ cβ+sβ]
    Q = zeros(Float64, χ, χ)
    Q[1:2,1:2] = q
    @tensor T[1,2,3] := T[-1,-2,-3] * Q[1,-1] * q[2,-2] * Q[3,-3]
    @tensor C[1,2] := C[-1,-2] * Q[1,-1] * Q[2,-2]
    return (DTensor(C), DTensor(T))
end

function isingenvironmentz2(β::Float64, χ::Int)
    C = DASTensor{Float64,2}(Z2(),ntuple(i -> Z2Charges(), 2),
            ([χ,χ],[χ,χ]), InOut(-1,-1))
    T = DASTensor{Float64,3}(Z2(),ntuple(i -> Z2Charges(),3),
            ([χ,χ],[1,1],[χ,χ]),InOut(1,-1,1))
    initwithzero!(C)
    initwithzero!(T)
    foreach(k -> C[k][1] = 1, keys(C))
    foreach(k -> T[k][1] = 1, keys(T))

    q =  DASTensor{Float64,2}(Z2(),(Z2Charges(), Z2Charges()),
            ([1,1],[1,1]), InOut(1,-1))
    qχ = DASTensor{Float64,2}(Z2(),(Z2Charges(), Z2Charges()),
            ([χ,χ],[χ,χ]), InOut(1,-1))
    cβ, sβ = sqrt(2) * [sqrt(cosh(β))], sqrt(2) * [sqrt(sinh(β))]
    k00 = DASSector(Z2Charge(0),Z2Charge(0))
    k11 = DASSector(Z2Charge(1),Z2Charge(1))
    q[k00]  = reshape(cβ, 1,1)
    q[k11]  = reshape(sβ, 1,1)
    qχ[k00] = zeros(Float64,χ,χ)
    qχ[k11] = zeros(Float64,χ,χ)
    qχ[k00][1] = cβ[1]
    qχ[k11][1] = sβ[1]
    @tensor T[1,2,3] := T[-1,-2,-3] * qχ[1,-1] * q[2,-2] * qχ[3,-3]
    @tensor C[1,2] := C[-1,-2] * qχ[1,-1] * qχ[2,-2]
    return (C, T)
end

function isingtensorz2(β)
    a = DASTensor{Float64,4}(Z2(),
        ntuple(i -> Z2Charges(), 4),
        ntuple(i -> [1,1], 4),
        InOut(1,1,1,1))
    for k in invariantsectors(charges(a), in_out(a))
        a[k] = reshape([1],1,1,1,1)
    end
    b = DASTensor{Float64,2}(Z2(),
        ntuple(i -> Z2Charges(), 2),
        ntuple(i -> [1,1], 2),
        InOut(1,-1))
    cβ, sβ = sqrt(2) * [sqrt(cosh(β))], sqrt(2) * [sqrt(sinh(β))]
    b[DASSector(Z2Charge(0),Z2Charge(0))] = reshape(cβ,1,1)
    b[DASSector(Z2Charge(1),Z2Charge(1))] = reshape(sβ,1,1)
    @tensor a[o1,o2,o3,o4] := a[c1,c2,c3,c4] * b[o1,c1] * b[o2,c2] * b[o3,c3] * b[o4,c4]
    return a
end

isingmagtensorz2(β) = isingtensorz2(β)

isingtensors(β) = (isingtensor(β), isingmagtensor(β))
isingtensorsz2(β) = (isingtensorz2(β), isingmagtensorz2(β))

magnetisation(state, a, asz) = magnetisation(environment(state), a, asz)
magnetisation((C,T,a,asz)) = magnetisation(C,T,a,asz)

function magnetisation(env::AbstractTensor{<:Any,4}, a, asz)
    @tensor mag  = env[-1,-2,-3,-4] * asz[-1,-2,-3,-4]
    @tensor norm = env[-1,-2,-3,-4] * a[-1,-2,-3,-4]
    return mag/norm
end

function magnetisation(C::AbstractTensor{S,2}, T::AbstractTensor{S,3},
        a::AbstractTensor{S,4}, asz::AbstractTensor{S,4}) where S
    env = environment(ntuple(i -> C,4), ntuple(i -> T,4))
    return magnetisation(env, a, asz)
end

function isingctm(β, χ, fixed::Bool = true; log::Bool = false, kwargs...)
    cinit, tinit = isingenvironment(β, χ, fixed = fixed)
    a, asz = isingtensors(β)
    if log
        st = ctm(a, asz, χ, Cinit = cinit, Tinit = tinit; kwargs...)
        info = (β = β, χ = χ, n_it = st.n_it[], diffs = st.diffs)
        return (C = st.C, T = st.T, A = a, M = asz, ttype = :DTensor, info...)
    end
    st = ctm(a, asz, χ, Cinit = cinit, Tinit = tinit ; kwargs...)
    c, t = st.C, st.T
    return (C = st.C, T = st.T, A = a, M = asz)
end

function isingctmz2(β, χ; log::Bool = false, kwargs...)
    cinit, tinit = isingenvironmentz2(β, χ)
    a, asz = isingtensorsz2(β)
    if log
        st = ctm(a, asz, χ, Cinit = cinit, Tinit = tinit; kwargs...)
        info = (β = β, χ = χ, n_it = st.n_it[], diffs = st.diffs)
        return (C = st.C, T = st.T, A = a, M = asz, ttype = :ZNTensor, info...)
    end
    st = ctm(a, asz, χ, Cinit = cinit, Tinit = tinit; kwargs...)
    c, t = st.C, st.T
    return (C = c, T = t, A = a, M = asz)
end

function σxtensor()
    σx = DASTensor{Float64,2}(
            ZN{2}(),
            (Z2Charges(), Z2Charges()),
            ([1,1], [1,1]),
            InOut(1,-1))
    σx[DASSector(Z2Charge(1), Z2Charge(0))] = reshape([1], 1, 1)
    σx[DASSector(Z2Charge(0), Z2Charge(1))] = reshape([1], 1, 1)
    return σx
end

function σztensor()
    σz = DASTensor{Float64,2}(
            ZN{2}(),
            (Z2Charges(), Z2Charges()),
            ([1,1], [1,1]),
            InOut(1,-1))
    σz[DASSector(Z2Charge(0), Z2Charge(0))] = reshape([1], 1, 1)
    σz[DASSector(Z2Charge(1), Z2Charge(1))] = reshape([-1], 1, 1)
    return σz
end

environment(state::CTMState) = environment(state.Cs, state.Ts)
environment(state::rotsymCTMState) =
    environment(ntuple(i -> state.C,4), ntuple(i->state.T,4))
function environment(state::transconjCTMState)
    C = state.C
    T1, T2 = state.Ts
    environment((C,C',C,C'), (T1, T2', T1', T2))
end
function environment((C1,C2,C3,C4), (T1,T2,T3,T4))
    @tensor env[1,2,3,4] := C1[-8, -1] * T1[-1,3,-2] *  C2[-2,-3] *
                            T4[-7,4,-8] * #=A[1,2,3,4]=#T2[-3,2,-4] *
                            C4[-6,-7] *  T3[-5,1,-6] *  C3[-4,-5]
    return env
end


using Stheno: Xt_invA_Y, Xt_invA_X, diag_At_A, diag_At_B, diag_Xt_invA_X, diag_Xt_invA_Y

@timedtestset "cholesky" begin
    # Set up some matrices and factorisations.
    rng, N, N′, P, Q = MersenneTwister(123456), 5, 3, 6, 2
    B = randn(rng, N, N)
    A_ = B' * B + UniformScaling(1e-6)
    A = cholesky(A_)
    x, y, z = randn(rng, N), randn(rng, N′), randn(rng, N)
    X, Y = randn(rng, N, P), randn(rng, N, Q)
    Z = randn(rng, N, P)
    A_1_ = exp(randn(rng))
    A_1 = cholesky(A_1_)

    # Specialised matrix operations.
    @test Xt_invA_X(A, x) isa Real
    @test Xt_invA_X(A, x) ≈ x' * (A \ x)

    @test Xt_invA_X(A, X) isa Symmetric
    @test Xt_invA_X(A, X) ≈ X' * (A \ X)

    @test Xt_invA_Y(X, A, Y) isa Matrix
    @test Xt_invA_Y(X, A, Y) ≈ X' * (A \ Y)

    @test diag_At_A(x) ≈ [x'x]
    @test diag_At_A(X) ≈ diag(X'X)

    @test diag_At_B(x, z) ≈ [x'z]
    @test diag_At_B(X, Z) ≈ diag(X'Z)

    @test diag_Xt_invA_X(A, X) ≈ diag(Xt_invA_X(A, X))

    @test diag_Xt_invA_Y(X, A, Z) ≈ diag(Xt_invA_Y(X, A, Z))
end


"""
Returns (N1, N2) with `N1 x N2 == n`. N2 might become 1
"""
function close2square(n::Real)
    # a cannot be greater than the square root of n
    # b cannot be smaller than the square root of n
    # we get the maximum allowed value of a
    amax = floor(Int, sqrt(n));
    if 0 == rem(n, amax)
        # special case where n is a square number
        return (amax, div(n, amax))
    end
    # Get its prime factors of n
    primeFactors  = factor(n);
    # Start with a factor 1 in the list of candidates for a
    candidates = [1]
    for (f, _) in primeFactors
        # Add new candidates which are obtained by multiplying
        # existing candidates with the new prime factor f
        # Set union ensures that duplicate candidates are removed
        candidates = union(candidates, f .* candidates)
        # throw out candidates which are larger than amax
        filter!(x-> x <= amax, candidates)
    end
    # Take the largest factor in the list d
    (candidates[end], div(n, candidates[end]))
end

same_length_array(array, value::NativeFont) = Iterators.repeated(value, length(array))
function extrema_nan(x::ClosedInterval)
    (minimum(x), maximum(x))
end

# TODO upgrade GeometryTypes to do these kind of things.
# Problem: I don't really want to introduce a depedency on intervals
function Base.in(point::StaticVector{N}, rectangle::HyperRectangle{N}) where N
    mini, maxi = minimum(rectangle), maximum(rectangle)
    for i = 1:N
        point[i] in (mini[i] .. maxi[i]) || return false
    end
    return true
end

to_range(x::ClosedInterval) = (minimum(x), maximum(x))
to_range(x::VecTypes{2}) = x
to_range(x::AbstractRange) = (minimum(x), maximum(x))
to_range(x::AbstractVector) = (minimum(x), maximum(x))
function to_range(x::AbstractArray)
    if length(x) in size(x) # assert that just one dim != 1
        to_range(vec(x))
    else
        error("Can't convert to a range. Please supply a range/vector/interval or a tuple (min, max)")
    end
end


# Input data
data = BitArray(
    hcat(
        map(
            line -> map(x -> parse(Int64, x), split(line, "")),
            readlines("Day03.input"),
        )...,
    ),
);

# Part 1
γ = (A) -> (sum(A, dims = 2) .>= size(A, 2) / 2)';
ε = (A) -> γ(A) .⊻ 1;
todecimal = (V) -> sum(map(((i, v),) -> v << (size(V, 2) - i), enumerate(V)));

ans = todecimal(γ(data)) * todecimal(ε(data));
println("Part 1: $ans")

# Part 2
rating =
    (F, A) ->
        reduce(
            (acc, v) -> size(acc, 2) > 1 ? acc[:, findall(==(F(acc)[v]), acc[v, :])] : acc,
            1:size(A, 1),
            init = A,
        )';

ans = todecimal(rating(γ, data)) * todecimal(rating(ε, data));
println("Part 2: $ans")


function testIntervals(A,V,W)
    #const n = size(A,1);
    n = size(A,1);

    roots = map( x ->
                 endPoints(A,V,x)[1],
                 [15; 25; 35; 45])          |>
                     Base.Iterators.flatten |>
                     collect                |>
                     sort

    intvls = Base.Iterators.product( roots, roots) |> collect |> vec

    # post-process intervals
    intvls = filter(intvls) do  x
        count = sum(x[1] .<= W .<= x[2])
        return n*0.05 <= count <= n*0.2
    end

    intvls = map(intvls) do x
        count = sum(x[1] .<= W .<= x[2])
        return (x...,count)
    end
end

function testIntervals(B,V)

    W = eigen(Matrix(B)).values
    c = (W[1] + W[end]) / 2 ;
    d = (W[end] - W[1]) / 2 ;

    A = (B - I.*c) / d;
    W = (W .- c) / d;

    intvls = testIntervals(A,V,W)

return intvls,W

end


using MathProgBase, KNITRO, JuMP, Compat.Test

function testmodel(m)
    @variable(m, x[1:3]>=0)
    @NLobjective(m, Min, 9.0 - 8.0*x[1] - 6.0*x[2] - 4.0*x[3]
                            + 2.0*x[1]^2 + 2.0*x[2]^2 + x[3]^2
                            + 2.0*x[1]*x[2] + 2.0*x[1]*x[3])
    @constraint(m, x[1] + x[2] + 2.0*x[3] <= 3)
    solve(m)
    @test isapprox(getvalue(x[1]), 1.3333333, atol=1.0e-5)
    @test isapprox(getvalue(x[2]), 0.7777777, atol=1.0e-5)
    @test isapprox(getvalue(x[3]), 0.4444444, atol=1.0e-5)
    @test isapprox(getobjectivevalue(m), 0.1111111, atol=1.0e-5)
end

m = Model(solver=KnitroSolver())
testmodel(m)
ktrmod = internalmodel(m)
MathProgBase.freemodel!(ktrmod)

m = Model(solver=KnitroSolver(KTR_PARAM_ALG=5))
testmodel(m)
ktrmod = internalmodel(m)
MathProgBase.freemodel!(ktrmod)

m = Model(solver=KnitroSolver(hessopt=1))
testmodel(m)
ktrmod = internalmodel(m)
MathProgBase.freemodel!(ktrmod)

m = Model(solver=KnitroSolver(options_file=joinpath(dirname(@__FILE__), "tuner-fixed.opt")))

testmodel(m)
ktrmod = internalmodel(m)
MathProgBase.freemodel!(ktrmod)

m = Model(solver=KnitroSolver(tuner_file=joinpath(dirname(@__FILE__), "tuner-explore.opt")))

testmodel(m)
ktrmod = internalmodel(m)
MathProgBase.freemodel!(ktrmod)


using Mosek
using MosekTools
using LinearAlgebra
using MinimaxEstimation
using Plots
using Random

Random.seed!(10)
n_steps = 20
F = [1.1 -.5 0.1
    1 0 0
    0 1 0]
B = [-1;2;3]
H = [1 0 0]
x0 = [0; 0; 0]
Q = I(3)
R = 1
P = I(3)
gamma = 2

kf1 = KalmanFilter(x0, F, H,B, P, Q, R)
kf2 = KalmanFilter(x0, -F, H,B, P, Q, R)
mini = MinimaxMMAE(copy([kf1, kf2]), gamma, Mosek.Optimizer)
bayesian = BayesianMMAE(copy([kf1, kf2]))
v = randn((1,n_steps))
w = randn((3, n_steps))
u(k) = sin(k/5)
y = zeros(n_steps+1)
z = zeros(n_steps+1)
y[1] = 0
z[1] = 0

zhat_mini = zeros(n_steps+1)
vals_mini = zeros(n_steps+1)
zhat_bayesian = zeros(n_steps+1)

zhat_mini[1]= predict(mini, true)[1][1]
zhat_bayesian[1] = predict(bayesian)[1]
x = x0
for k=1:n_steps
    update!(mini,y[k], u(k))
    update!(bayesian, y[k], u(k))

    zhat_mini[k+1] = predict(mini, true)[1][1]
    zhat_bayesian[k+1] = predict(bayesian)[1]
    global x = F*x + B*u(k) + w[:,k]
    y[k+1] = (H*x)[1] + v[k]
    z[k+1] = (H*x)[1]
    k +=1
end
print("||z - zhat_mini|| = ", norm(z - zhat_mini), "\n")
print("||z - zhat_bayesian|| = ", norm(z - zhat_bayesian), "\n")
print("||z_{n+1} - y_n|| = ", norm(z[2:end]-y[1:end-1]), "\n")
plot(
    [z zhat_mini zhat_bayesian],
    legend=:bottomright,
    labels = ["z" "zhat_minimax" "zhat_bayesian"],
    linewidth=3,
    markershape = :o,
    xlabel = "time-step",
    ylabel = "z")

savefig("experiment.png")


using ControlSystems
using PredictiveControl
using Test

include( "../testUtils.jl" )

clqrUnstable  = createSampleLTISystem()
clqrStablized = createSampleLTISystem( usek = true )
clqrWithS     = createSampleLTISystem( uses = true, usek = true )

# These are all conditions that are not allowed
@test_throws DomainError conditionBound( clqrUnstable )
@test_throws DomainError conditionBound( clqrWithS )

# A test for a single-input pre-stabilized system
@test conditionBound( clqrStablized ) ≈ 9.986301 atol=1e-4

# Setup a test for a multi-input system (it means actual matrices are used in the computation)
A = [0.9 1.0;
     0.0 0.9]
B = [0.0 1.0;
     1.0 0.0]
C = [1.0 0.0;
     0.0 1.0]
D = [0.0 0.0;
     0.0 0.0]
Ts = 0.1
sys = StateSpace( A, B, C, D, Ts )

Q = [1.0 1.0;
     1.0 1.0]
R = [1.0 0.0
     0.0 1.0]

clqrTwoInputs = ConstrainedTimeInvariantLQR( sys, 5, Q, R, :Q )

@test conditionBound( clqrTwoInputs ) ≈ 12201.000000000648


using LinearAlgebra
using Random
using Distributions

# plot features require Gadfly, etc
using ApproxManifoldProducts

# more packages required
using TransformUtils

const LinAlg = LinearAlgebra
const KDE = KernelDensityEstimate
const TU = TransformUtils
const AMP = ApproxManifoldProducts

# temporarily required for experimental versions of on-manifold products
KDE.setForceEvalDirect!(true)

## more standard interface

@testset "test standard API for AMP.manikde!" begin

# two densities on a cylinder
p = manikde!(randn(2,100), (:Euclid, :Circular) )

pts2a = 3.0*randn(1,100).+5.0
pts2b = TransformUtils.wrapRad.(0.5*randn(1,100).+pi)
q = manikde!([pts2a;pts2b], (:Euclid, :Circular) )

# approximate the product between hybrid manifold densities
pq = manifoldProduct([p;q], (:Euclid, :Circular), Niter=2)

## check marginals

@test getBW( marginal(pq, [1;]) )[1,1] == getBW(pq)[1,1]
@test getBW( marginal(pq, [2;]) )[1,1] == getBW(pq)[2,1]

@test 0 < getBW(pq)[1,1] < 10.0
@test 0 < getBW(pq)[2,1] < 1.5

end

## lower level API

@testset "test lower level API..." begin

pts1a = 180.0.+10.0*randn(100)
pts2a = 200.0.+10.0*randn(100)

pts1b = TU.wrapRad.(0.1*randn(100).-pi.+0.5)
pts2b = TU.wrapRad.(0.1*randn(100).+pi.-0.5)

pts1 = [pts1a';pts1b']
pts2 = [pts2a';pts2b']

# pc1 = kde!_CircularNaiveCV(pts1)
# pc2 = kde!_CircularNaiveCV(pts2)
pc1 = kde!(pts1, [3.0; 0.1], (+,AMP.addtheta), (-,AMP.difftheta))
pc2 = kde!(pts2, [3.0; 0.1], (+,AMP.addtheta), (-,AMP.difftheta))

dummy = kde!(rand(2,100),[1.0;], (+,AMP.addtheta), (-,AMP.difftheta));

pGM, = prodAppxMSGibbsS(dummy, [pc1; pc2], nothing, nothing, Niter=1,
                          addop=(+,AMP.addtheta), diffop=(-,AMP.difftheta), getMu=(KDE.getEuclidMu,AMP.getCircMu));
#

lin1 = getBW(kde!(pGM[1,:]))[:,1][1]
cir1 = getBW(kde!_CircularNaiveCV(pGM[2,:]))[:,1][1]

pc12 = kde!(pGM, [lin1;cir1], (+,AMP.addtheta), (-,AMP.difftheta))

end


using Parameters, LinearAlgebra

"""

Parameter container for an Ohmic Spin Boson Model

"""
# NOTE: Change the default type tp be Real to allow for simple precision eventually
@with_kw struct OhmicSBParams @deftype Float64
    Δ = 5.0
    ϵ = 0.0
    α = 0.1
    ωc = 7.5 * Δ
    ωm = 5 * ωc
    β = 1.0
    No :: Int64 = 50
end

"""

structure container for an Ohmic Spin Boson Model

"""
struct SBModel
    No :: Int64
    Δ :: Float64
    ϵ :: Float64
    β :: Float64
    ωs :: Vector{Float64}
    cs :: Vector{Float64}
    function SBModel(sbp::OhmicSBParams)
        @unpack No, Δ, ϵ, ωm, ωc = sbp
        Δ = sbp.Δ
        ϵ = sbp.ϵ
        β = sbp.β
        ωs = zeros(Float64,No)
        cs = zeros(Float64,No)
        for j=1:No
            ωs[j] = ωm - ωc * log((j - exp(ωm/ωc)*j + exp(ωm/ωc)*No)/No)
            cs[j] = sqrt((2/π)*ωs[j]*ohmicJ0(ωs[j],sbp)/ρosc(ωs[j],sbp))
        end
        new(No,Δ,ϵ,β,ωs,cs)
    end
end

"""

Ohmic spectral density

"""
function ohmicJ0(ω, sbp)
    @unpack α, ωc = sbp
    (π/2)*α*ω.*exp.(-ω/ωc)
end

"""

Oscilator theoretical density of states

"""
function ρosc(ω, sbp)
    @unpack No, ωc, ωm = sbp
    (No/ωc)*exp.(-ω/ωc)/(1-exp.(-ωm/ωc))
end

"""

Calculate thermal ocupation numbers for the oscillators

"""
function thermalns(sbm)
    N = Vector{Float64}(undef,sbm.No)
    for i in 1:sbm.No
        N[i] = 1.0 / (exp(HBAR * sbm.ωs[i] * sbm.β) - 1.0)
    end
    return N
end

"""

Gaussian distribution

"""
function gaussian(σ,x)
    exp(-((x/σ)^2)/2)/(σ*sqrt(2*π))
end

"""

Calculate the oscillator density of states for a model

"""
function densityofstates(sbm,range;σ=1.5)
    dos = zeros(size(range))
    for x in sbm.ωs
        dos += gaussian.(σ,range .- x)
    end
    return dos
end

"""

Calculate spectral density for a model

"""
function spectraldensity(sbm,range;σ=1.5)
    sd = zeros(size(range))
    for (i,ω) in enumerate(sbm.ωs)
        sd += sbm.cs[i]^2*gaussian.(σ,range .- ω)/sbm.ωs[i]
    end
    return pi*sd/2
end

"""

Hamiltonian for an Ohmic Spin Boson Model

"""
@inline function H(sbm, q , p)
    H = sbm.ϵ * σz + sbm.Δ * σx
    for nu = 1:sbm.No
        H += eye * 0.5 * sbm.ωs[nu]^2 * q[nu]^2 + σz * sbm.cs[nu] * q[nu]
    end
    return H
end

"""

Force operator for an Ohmic Spin Boson Model

"""
@inline function F(sbm, q, nu)
    F = - eye * sbm.ωs[nu]^2 * q[nu] - σz * sbm.cs[nu]
    return F
end

"""

Second order derivative operator for an Ohmic Spin Boson Model (scalar)

"""
@inline function K(sbm, q, nu, nup)
    if nu == nup
        K = sbm.ωs[nu]^2
    else
        K = 0.0
    end
end


### A Pluto.jl notebook ###
# v0.14.7

using Markdown
using InteractiveUtils

# ╔═╡ 6a2c9516-dbda-4005-8d85-c19b5c0f54ee
begin
  using Pkg
  Pkg.activate(Base.Filesystem.homedir() * "/.config/julia/projects/oft")
  using Plots
  using LinearAlgebra
end

# ╔═╡ 4cdf2cb0-568c-47f6-bb13-a4a693e042f6
md"""
## Affine Transformations
An _affine transformation_ is a _linear transformation_ plus _a displacment_.
```math
\begin{array}
  &\mathbb{R}^2 &\to &\mathbb{R}^2& \\
  \begin{bmatrix}
    t_{1}' \\
    t_{2}'
  \end{bmatrix}
  &\mapsto
  &A
  \begin{bmatrix}
    t_{1} \\
    t_{2}
\end{bmatrix}
  + b&
  =
  \begin{bmatrix}
    a_{1,1} & a_{1,2} \\
    a_{2,1} & a_{2,2} \\
  \end{bmatrix}
  \begin{bmatrix}
    t_{1} \\
    t_{2}
  \end{bmatrix}
  +
  \begin{bmatrix}
    b_{1} \\
    b_{2}
  \end{bmatrix}
\end{array}
```
"""

# ╔═╡ f4611f9f-7f42-4164-8135-0fdcbf8f7ca0
begin
  chapeau_xy = [-1 -1 7 4 7 -1
                -5  5 5 3 1  1]
  function draw_flag(A, b, linecolor; new=false)
    if new
      plot()
    end
    xmax = 15
    ymax = xmax
    # chapeau_xy = [-1 -1 7 4 7 -1
    #               -5  5 5 3 1  1]
    transformed = A*chapeau_xy .+ b
    plot!(transformed[1, 1:end],
          transformed[2, 1:end],
          lw=3,
          framestyle=:zerolines,
          aspect_ratio=:equal,
          linecolor=linecolor,
          legend=false,
          xlims=[-xmax, xmax],
          ylims=[-ymax, ymax],
    )
  end
end

# ╔═╡ 5c95082e-75e5-466d-89e5-11b501add71d
md"""
### Translation
"""

# ╔═╡ 6aa15192-3ec7-417f-9847-97248d7b3ca5
begin
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(I(2), [4 3]', :red)
end

# ╔═╡ 434c9a7f-14f1-488e-9580-78b4f250e126
begin
  draw_flag(I(2), [1 2]', :grey; new=true)
  draw_flag(I(2), [-3 4]', :red)
end

# ╔═╡ b7e57e32-d5f1-40ba-b833-01e131ca0e64
md"""
### Scaling
"""

# ╔═╡ cfc871af-0ab3-4f7c-8f3d-a1421a32f3f5
begin
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(2*I(2), [0 0]', :red)
end

# ╔═╡ bd68e9bc-bff8-4820-9e8a-cbcfeee76371
begin
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag((1/3)*I(2), [0 0]', :red)
end

# ╔═╡ 8ca625d1-a595-4b00-8b0c-810c04d5927e
let  # We use let here because we shall repeatedly use the matrix A
  A = [2 0
       0 1/2]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ b34d9ee0-13ea-4b5b-846e-69785abadec2
md"""
### Rotation
"""

# ╔═╡ 6154092f-a161-45a4-8b86-5c3e436c1442
let
  θ = 2π / 3
  A = [cos(θ) -sin(θ)
       sin(θ)  cos(θ)]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ c48341a5-4434-4816-a026-638e59d1344c
let
  θ = -π / 6
  A = [cos(θ) -sin(θ)
       sin(θ)  cos(θ)]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ 2cc2fab1-ca14-4940-9050-e8f7d145a646
md"""
### Flips
"""

# ╔═╡ d9fe42fd-587f-4a1e-bd93-6553208109fe
let
  # horizontal flip
  A = [-1 0
        0 1]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ 99f2817e-f951-4123-9347-79c071329748
let
  # vertical flip
  A = [1  0
       0 -1]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ 516ebe04-e164-424b-832d-c9a91583788c
md"""
### Shearing
"""

# ╔═╡ ae6c8a6b-0ab5-4a0b-b058-2f5e3fecb0af
let
  s = 1.5
  A = [1 s
       0 1]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ fb1068ee-597a-44d5-be66-db42be7d7fb5
let
  s = 1.5
  A = [1 0
       s 1]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ a38ed456-87e3-4572-be89-e4f34d4196f9
let
  s = 1.5
  A = [1 -s
       0 1]
  draw_flag(I(2), [0 0]', :grey; new=true)
  draw_flag(A, [0 0]', :red)
end

# ╔═╡ Cell order:
# ╠═6a2c9516-dbda-4005-8d85-c19b5c0f54ee
# ╟─4cdf2cb0-568c-47f6-bb13-a4a693e042f6
# ╠═f4611f9f-7f42-4164-8135-0fdcbf8f7ca0
# ╟─5c95082e-75e5-466d-89e5-11b501add71d
# ╠═6aa15192-3ec7-417f-9847-97248d7b3ca5
# ╠═434c9a7f-14f1-488e-9580-78b4f250e126
# ╟─b7e57e32-d5f1-40ba-b833-01e131ca0e64
# ╠═cfc871af-0ab3-4f7c-8f3d-a1421a32f3f5
# ╠═bd68e9bc-bff8-4820-9e8a-cbcfeee76371
# ╠═8ca625d1-a595-4b00-8b0c-810c04d5927e
# ╟─b34d9ee0-13ea-4b5b-846e-69785abadec2
# ╠═6154092f-a161-45a4-8b86-5c3e436c1442
# ╠═c48341a5-4434-4816-a026-638e59d1344c
# ╟─2cc2fab1-ca14-4940-9050-e8f7d145a646
# ╠═d9fe42fd-587f-4a1e-bd93-6553208109fe
# ╠═99f2817e-f951-4123-9347-79c071329748
# ╟─516ebe04-e164-424b-832d-c9a91583788c
# ╠═ae6c8a6b-0ab5-4a0b-b058-2f5e3fecb0af
# ╠═fb1068ee-597a-44d5-be66-db42be7d7fb5
# ╠═a38ed456-87e3-4572-be89-e4f34d4196f9


module QuadEig

using LinearAlgebra, SparseArrays, SuiteSparse

export linearize, deflate

### PencilScaling ##########################################################################
# Compute the optimal scaling
############################################################################################
struct PencilScaling{T}
    γ::T
    δ::T
end

function pencilscaling(A0::M, A1::M, A2::M; scaling = nothing, kw...) where {T,M<:AbstractMatrix{T}}
    scaling === nothing && return PencilScaling(T(1), T(1))
    n0, n1, n2 = opnorm.(Matrix.((A0, A1, A2)))
    τ = n1 / sqrt(n0 * n2)
    if τ <= 1
        γ = sqrt(n0 / n2)
        δ = 2 / (n0 + γ * n1)
    elseif scaling === -
        # distinct tropical roots, choose γ+ to favor large eigenvalues
        γ = n1 / n2
        δ = 1 / max(n2 * γ^2, n1 * γ, n0)
    else
        # distinct tropical roots, choose γ- to favor small eigenvalues
        γ = n0 / n1
        δ = 1 / max(n2 * γ^2, n1 * γ, n0)
    end
    return PencilScaling(T(γ), T(δ))
end

function scale!(A0, A1, A2, s::PencilScaling)
    γ, δ = scalingfactors(s)
    if γ != 1 || δ != 1
        A0 .*= γ^2 * δ
        A1 .*= γ * δ
        A2 .*= δ
    end
    return A0, A1, A2
end

scalingfactors(s::PencilScaling) = s.γ, s.δ

### QuadPencil #############################################################################
# Encodes the quadratic pencil problem
############################################################################################
struct QuadPencil{T<:Complex,M<:AbstractMatrix{T}}
    A0::M
    A1::M
    A2::M
    scaling::PencilScaling{T}
end

function quadpencil(A0, A1, A2; kw...)
    size(A0) == size(A1) == size(A2) && size(A0, 1) == size(A0, 2) ||
        throw(DimensionMismatch("Expected square matrices of the same size"))
    ptype = complex(promote_type(eltype(A0), eltype(A1), eltype(A2)))
    A0´ = copy_to_eltype(A0, ptype)
    A1´ = copy_to_eltype(A1, ptype)
    A2´ = copy_to_eltype(A2, ptype)
    s = pencilscaling(A0´, A1´, A2´; kw...)
    scale!(A0´, A1´, A2´, s)
    return QuadPencil(A0´, A1´, A2´, s)
end

# This also ensures any Adjoint/Transpose wrapper is removed
copy_to_eltype(A, ::Type{T}) where {T} = copy!(similar(A, T, size(A)), A)

Base.size(q::QuadPencil, n...) = size(q.A0, n...)

Base.iterate(p::QuadPencil) = p.A0, Val(:A1)
Base.iterate(p::QuadPencil, ::Val{:A1}) = p.A1, Val(:A2)
Base.iterate(p::QuadPencil, ::Val{:A2}) = p.A2, Val(:done)
Base.iterate(p::QuadPencil, ::Val{:done}) = nothing

### QuadPencilQR ###########################################################################
# Pivoted QR-factorization
############################################################################################
struct QuadPencilPQR{T,M}
    pencil::QuadPencil{T,M}
    Q0::M
    RP0::M
    Q2´::M
    RP2::M
end

function pqr(pencil::QuadPencil)
    qr0 = pqr(pencil.A0)
    qr2 = pqr(pencil.A2)
    Q0, RP0 = getQ(qr0), getRP´(qr0)
    Q2´, RP2 = getQ´(qr2), getRP´(qr2)
    return QuadPencilPQR(pencil, Q0, RP0, Q2´, RP2)
end

# Sparse QR from SparseSuite is also pivoted
pqr(a::SparseMatrixCSC) = qr(a)
pqr(a::Adjoint{T,<:SparseMatrixCSC}) where {T} = qr(copy(a))
pqr(a::Transpose{T,<:SparseMatrixCSC}) where {T} = qr(copy(a))
pqr(a) = qr!(copy(a), Val(true))

### Linearized Pencils #####################################################################
# Build second companion linearization, or its Q*C2*V rotation
############################################################################################
struct Linearization{T,M<:AbstractMatrix{T},R}
    pencilpqr::QuadPencilPQR{T,M}
    A::M
    B::M
    Q::M
    V::M
    deflate_tol::R
end

Linearization(q::QuadPencilPQR{T,M}, mats::M...) where {T,M} =
    Linearization(q, mats..., zero(real(T)))

linearize(A0, A1, A2; kw...) = linearize(pqr(quadpencil(A0, A1, A2; kw...)))

function linearize(q::QuadPencilPQR)
    A0, A1, A2 = q.pencil.A0, q.pencil.A1, q.pencil.A2
    o, z = one(A1), zero(A1)
    Q = [q.Q2´ z; z q.Q0']
    V = [o z; z q.Q0]
    A = [q.Q2´*A1 -q.Q2´*q.Q0; q.RP0 z]
    B = [q.RP2 z; z o]
    B .= .- B
    return Linearization(q, A, B, Q, V)
end

function Base.show(io::IO, l::Linearization{T,M}) where {T,M}
    print(io, summary(l), "\n",
"  Matrix size    : $(size(l.Q, 2)) × $(size(l.V, 1))
  Matrix type    : $M
  Scalings γ, δ  : $(real.(scalingfactors(l)))
  Deflated       : $(deflationstring(l))")
end

Base.summary(l::Linearization{T}) where {T} =
    "Linearization{$T}: second companion linearization of quadratic pencil"

deflationstring(l::Linearization) =
    isdeflated(l) ? "true ($(size(l.V, 1)) -> $(size(l.A, 1)))" : "false"

isdeflated(l) = !iszero(l.deflate_tol)

scalingfactors(l::Linearization) = scalingfactors(l.pencilpqr.pencil.scaling)

quadpencil(l::Linearization) = l.pencilpqr.pencil

Base.size(l::Linearization, n...) = size(l.A, n...)

Base.iterate(l::Linearization) = l.A, Val(:B)
Base.iterate(l::Linearization, ::Val{:B}) = l.B, Val(:Q)
Base.iterate(l::Linearization, ::Val{:Q}) = l.Q, Val(:V)
Base.iterate(l::Linearization, ::Val{:V}) = l.V, Val(:done)
Base.iterate(l::Linearization, ::Val{:done}) = nothing

### deflate ################################################################################
# Compute deflated pencil
############################################################################################
deflate(A0, A1, A2; kw...) = deflate(linearize(A0, A1, A2; kw...); kw...)

function deflate(l::Linearization{T}; atol = sqrt(eps(real(T))), force_deflated_size = nothing, kw...) where {T}
    isdeflated(l) && return l
    n = size(l, 1) ÷ 2
    if force_deflated_size === nothing
        r0, r2 = deflated_size(l, atol)
    else
        r0 = r2 = force_deflated_size
    end
    r0 == n && r2 == n && return l
    s = n - r2
    X = view(l.A, 1+r2:n, 1:r2+s+r0) # [X21 X22 X23]
    ZX´ = fod_z´(X, 1+s:s+r0+r2)
    A, B = deflatedAB(l.A, l.B, ZX´, r0, r2, s)
    Q = l.Q[[1:r2; n+1:n+r0], :]
    V = view(l.V, :, 1:n+r0) * ZX´
    q = l.pencilpqr
    return Linearization(q, A, B, Q, V, atol)
end

# get some columns of Z' in a full orthogonal decomposition of X
fod_z´(X::SparseMatrixCSC, cols = :) = _fod_z´(sparse(X'), cols)
fod_z´(X::SubArray{<:Any,2,<:SparseMatrixCSC}, cols = :) = _fod_z´(sparse(X'), cols)
fod_z´(X, cols = :) = _fod_z´(X', cols)
_fod_z´(X´, cols) = getQ(qr(X´), cols)

# carry out quadeig deflation
function deflatedAB(lA::SparseMatrixCSC, lB, ZX´, r0, r2, s)
    selected_rows = Isparse(size(lA, 1), [1:r2; 1+s+r2:s+r0+r2], :)
    A = view(selected_rows * lA, :, 1:s+r0+r2) * ZX´
    B = view(selected_rows * lB, :, 1:s+r0+r2) * ZX´
    return A, B
end

function deflatedAB(lA::AbstractMatrix, lB, ZX´, r0, r2, s)
    A = similar(lA, r0+r2, r0+r2)
    mul!(view(A, 1:r2, :), view(lA, 1:r2, 1:r2+s+r0), ZX´)
    mul!(view(A, 1+r2:r0+r2, :), view(lA, 1+s+r2:r2+s+r0, 1:r2+s+r0), ZX´)
    B = similar(A)
    mul!(view(B, 1:r2, :), view(lB, 1:r2, 1:r2+s+r0), ZX´)
    mul!(view(B, 1+r2:r0+r2, :), view(lB, 1+s+r2:r2+s+r0, 1:r2+s+r0), ZX´)
    return A, B
end

### Tools ##################################################################################

getQ(qr::Factorization, cols = :) = qr.Q * Idense(size(qr, 1), cols)
getQ(qr::SuiteSparse.SPQR.QRSparse, cols = :) =  Isparse(size(qr, 1), :, qr.prow) * sparse(qr.Q * Idense(size(qr, 1), cols))

getQ´(qr::Factorization, cols = :) = qr.Q' * Idense(size(qr, 1), cols)
getQ´(qr::SuiteSparse.SPQR.QRSparse, cols = :) = sparse((qr.Q * Idense(size(qr, 1), cols))') * Isparse(size(qr,1), qr.prow, :)

getRP´(qr::Factorization) = qr.R * qr.P'
getRP´(qr::SuiteSparse.SPQR.QRSparse) = qr.R * Isparse(size(qr, 2), qr.pcol, :)

getPR´(qr::Factorization) = qr.P * qr.R'
getPR´(qr::SuiteSparse.SPQR.QRSparse) = Isparse(size(qr, 2), :, qr.pcol) * qr.R'

Idense(n, ::Colon) = Matrix(I, n, n)

function Idense(n, cols)
    m = zeros(Bool, n, length(cols))
    for (j, col) in enumerate(cols)
        m[col, j] = true
    end
    return m
end

# Equivalent to I(n)[rows, cols], but faster
Isparse(n, rows, cols) = Isparse(inds(rows, n), inds(cols, n))

function Isparse(rows, cols)
    rowval = Int[]
    nzval = Bool[]
    colptr = Vector{Int}(undef, length(cols) + 1)
    colptr[1] = 1
    for (j, col) in enumerate(cols)
        push_rows!(rowval, nzval, rows, col)
        colptr[j+1] = length(rowval) + 1
    end
    return SparseMatrixCSC(length(rows), length(cols), colptr, rowval, nzval)
end

inds(::Colon, n) = 1:n
inds(is, n) = is

function push_rows!(rowval, nzval, rows::AbstractUnitRange, col)
    if col in rows
        push!(rowval, 1 + rows[1 + col - first(rows)] - first(rows))
        push!(nzval, true)
    end
    return nothing
end

function push_rows!(rowval, nzval, rows, col)
    for (j, row) in enumerate(rows)
        if row == col
            push!(rowval, j)
            push!(nzval, true)
        end
    end
    return nothing
end

deflated_size(l::Linearization, args...) = deflated_size(l.pencilpqr, args...)

deflated_size(p::QuadPencilPQR{T}, atol = default_tol(T)) where {T} =
    nonzero_rows(p.RP0, atol), nonzero_rows(p.RP2, atol)

function nonzero_rows(m::AbstractMatrix{T}, atol = default_tol(T)) where {T}
    nrows = size(m, 1)
    zerorows = 0
    for j in 0:nrows-1
        maximum(abs, view(m, nrows - j, :)) > atol && break
        zerorows += 1
    end
    return nrows - zerorows
end

default_tol(::Type{T}) where {T} = sqrt(eps(real(T)))

end # Module


n = 2 .^ [6 6 7 8 9 10 11 12]

MaxGB = 2
MaxGF = 4 * 1.6

m = n .^ 2
dense_gbytes = 2 * 8 * m ./ 1e9
dense_flops = 2 * n .^ 3
dense_gflops = zeros(1,length(n))
dense_time = zeros(1,length(n))

for i = 1:length(n)

    global dense_gflops, dense_time

    A = rand(n[i],n[i])
    B = rand(n[i],n[i])

    dense_time[i] = @elapsed C = A*B

    dense_gflops[i] = dense_flops[i] ./ dense_time[i] ./ 1e9

    print("Time: ", string(dense_time[i],))
    print(", GFlops: ", string(dense_gflops[i]))
    println(", GBytes: ", string(dense_gbytes[i]))

end


using Distributions
using Klara

ρ = Hyperparameter(:ρ, 1)

p1 = BasicContUnvParameter(
  :p1,
  setpdf=(p::Float64, v::Vector) -> Normal(v[1]*v[3], sqrt(1-abs2(v[1]))),
  nkeys=3
)

p2 = BasicContUnvParameter(
  :p2,
  setpdf=(p::Float64, v::Vector) -> Normal(v[1]*v[2], sqrt(1-abs2(v[1]))),
  nkeys=3
)

model = GenericModel([ρ, p1, p2], isindexed=false)

mcrange = BasicMCRange(nsteps=10000, burnin=1000)

v0 = Dict(:ρ=>0.8, :p1=>5.1, :p2=>2.3)

job = BasicGibbsJob(model, Dict(), mcrange, v0)

@time run(job)

output(job)

chains = Dict(job)

[mean(chains[k]) for k in keys(chains)]

cor(chains[:p1].value, chains[:p2].value)


module hhNeuron

mutable struct Neuron
    Vref::Float64
    gNa::Float64
    gK::Float64
    gL::Float64
    ENa::Float64
    EK::Float64
    EL::Float64
    Cm::Float64
    v::Float64
    m::Float64
    h::Float64
    n::Float64
end

function advance!(neuron, dt, I)
    # calculate the ion channel currents
    INa = neuron.gNa * neuron.m*neuron.m*neuron.m * neuron.h * (neuron.ENa - neuron.v)
    IK = neuron.gK * neuron.n*neuron.n*neuron.n*neuron.n * (neuron.EK - neuron.v)
    IL = neuron.gL * (neuron.EL - neuron.v)

    # calculate channel kinetics
    aM = 0.1 * (neuron.v - neuron.Vref -25) / ( 1 - exp(-(neuron.v-neuron.Vref-25)/10))
    bM = 4 * exp(-(neuron.v-neuron.Vref)/18)

    aH = 0.07 * exp(-(neuron.v-neuron.Vref)/20)
    bH = 1 / ( 1 + exp( -(neuron.v-neuron.Vref-30)/10 ) )

    aN = 0.01 * (neuron.v-neuron.Vref-10) / ( 1 - exp(-(neuron.v-neuron.Vref-10)/10) )
    bN = 0.125 * exp(-(neuron.v-neuron.Vref)/80)

    # calculate derivatives
    dv_dt = ( INa + IK + IL + I*1e-6 ) / neuron.Cm
    dm_dt = (1.0-neuron.m) * aM - neuron.m * bM
    dh_dt = (1.0-neuron.h) * aH - neuron.h * bH
    dn_dt = (1.0-neuron.n) * aN - neuron.n * bN

    # calculate next step
    neuron.v += dv_dt * dt
    neuron.m += dm_dt * dt
    neuron.h += dh_dt * dt
    neuron.n += dn_dt * dt

    return(neuron)
end

function simulation(dt::Float64=0.01, T::Int=1000)

    const numSteps = T/dt

    # initial membrane potential
    const Vinit = -70.0
    const Vref = -70.0

    # neuron morphology and membrane conductance
    const Smemb = 4000.0                                # surface area
    const Cmemb = 1.0                                   # membrane conductance
    const Cm = Cmemb * Smemb * 1e-8

    # channel parameter
    const gNa = 120.0 * Cmemb * Smemb * 1e-8            # sodium conductance
    const gK = 36.0 * Cmemb * Smemb * 1e-8              # potassium conductance
    const gL = 0.3 * Cmemb * Smemb * 1e-8               # leack conductance
    const ENa = 125.0                                   # sodium equilibrium potential
    const EK = -70.0                                    # potassium equilibrium potential
    const EL = -25.0                                    # leak equilibrium potential

    # initial values for m, h, n
    m = 0.0529
    h = 0.6
    n = 0.32

    v = []
    neuron = Neuron( Vref, gNa, gK, gL, ENa, EK, EL, Cm, Vinit, m, h, n )

    for i = 1:numSteps
        advance!(neuron, dt, 1000)
        push!(v, neuron.v)
    end

    return(v)
end

end