"""
    FineAcquisitionResults

A struct that stores the fine acquisition fine acquisition
results for both the carrier and FFT based methods.

Fields:

- `prn::Int64`: PRN number processed
- `type::String`: fine acquisition method used (either `"fft"` or `"carrier"`)
- `fd_course::Float64`: course acquired Doppler frequency in Hz
- `fd_rate::Float64`: signal Doppler rate in Hz/s
- `n0_idx_course::Int64`: course acquired code start index
- `t_length::Float64`: coherent integration time in seconds
- `fd_fine::Float64`: fine Doppler frequency in Hz
- `fd_est::Float64`: (fine + course) Doppler frequency in Hz
- `phi_init::Float64`: initial phase estimate in rads
- `M::Int64`: number of integrations performed (carrier method only)
- `P::Array{Float64,2}`: 3x3 diagonal matrix containin initial uncertainties
                         of the phase, Doppler, and Doppler rate estimates
- `R::Array{Float64,1}`: measurement uncertainty in rads
"""
struct FineAcquisitionResults
    prn::Int64
    type::String
    fd_course::Float64
    fd_rate::Float64
    n0_idx_course::Int64
    t_length::Float64
    fd_fine::Float64
    fd_est::Float64
    phi_init::Float64
    M::Int64
    P::Array{Float64,2}
    R::Array{Float64,1}
end

"""
    fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                    n₀_idx_course, type::Val{:fft}; fd_rate=0.,
                    t_length=replica.t_length,
                    σω=10., err_bin_num_ϕ=1, err_bin_num_f=2,
                    M=1)

Performs an FFT based fine acquisition on `data`. Note that `t_length` must
equal `replica.t_length`. `data` can be either a `GNSSData` or `L5QSignal`
struct, however, `data` and `replica` must be two seperate structs.

Required Arguments:

- `data::GNSSSignal`: either `GNSSData` or `ReplicaSignal` struct
- `replica::ReplicaSignal`: struct to use for replica signal generation
    * `replica.t_length` determines the integration time, `T`, used in FFT fine
      acquisition
- `prn::Int`: PRN number to track
- `fd_course`: course acquired Doppler frequency in Hz
- `n₀_idx_course`: initial code start index location
- `type::Val{:fft}`: `Val(:fft)` uses the FFT based fine acquisition method

Optional Arguments:

- `fd_rate`: the Doppler frequency rate in Hz/s `(default = 0Hz)`
- `σω`: expected uncertainty of Doppler rate in Hz/s `(default = 1000Hz/s)`
- `err_bin_num_ϕ`: number of bins to to look around peak to estimate the phase
                   uncertainty `(default = 1)`
- `err_bin_num_f`: number of frequency bins equal to the frequency uncertainty
                   `(default = 2)`
    * filter σ is equal to `err_bin_num_f×(1/t_length)`
- `M`: the number of consecutive integrations and use the one with the highest SNR
    * `(default = 1)`

Returns:

- `FineAcquisitionResults` struct
"""
function fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                         n₀_idx_course, type::Val{:fft}; fd_rate=0.,
                         σω=1000., err_bin_num_ϕ=1, err_bin_num_f=0.25,
                         M=1)
    t_length=replica.t_length
    Δf = 1/replica.t_length
    N = replica.sample_num
    N_over_2 = Int64(floor(N/2))
    pk_vals = Array{Complex{Float64}}(undef, M)
    pk_valabs2s = Array{Float64}(undef, M)
    pk_idxs = Array{Int}(undef, M)
    PNs = Array{Float64}(undef, M)
    SNR_ests = Array{Float64}(undef, M)
    fd_fines = Array{Float64}(undef, M)
    fd_ests = Array{Float64}(undef, M)
    ϕ_inits = Array{Float64}(undef, M)
    ϕ_init_errs = Array{Float64}(undef, M)
    for m in 1:M
        # Generate replica
        # Set signal parameters
        definereplica!(replica;
                    prn=prn, f_d=fd_course,
                    fd_rate=fd_rate, phi=0., f_if=0.,
                    code_start_idx=n₀_idx_course,
                    include_carrier=false,
                    start_t=(m-1)*t_length)
        # Generate signal
        generatereplica!(replica)
        # Wipeoff IF and course Doppler from data and multiply by replica
        @threads for i in 1:replica.sample_num
            j = (m-1)*N + i
            t = calc_t_at_i(j, (m-1)*t_length, data.f_s)
            @inbounds replica.data[i] = data.data[j]*cis(-2π*(data.f_if+fd_course+0.5*fd_rate*t)*t)*replica.data[i]
        end
        # Perform in place FFT of `replica.data`
        fft!(replica.data)
        # Find peak within ±[x]kHz, where `x` is defined by freq_lim
        # From index 1 to N/2: positive frequencies
        # From index N/2 to N: negative frequencies
        # N_lim = Int64(floor(freq_lim/Δf))
        # pos_freq_interval = (1, N_lim)
        # neg_freq_interval = (N-N_lim, N)
        pk_val = 0. + 0im
        pk_valabs2 = 0.
        pk_idx = 1
        PN = 0.
        #### TODO: Make the peak searching loops below functions and multi-thread them. ####
        # Check positive frequencies for peak
        @inbounds for i in 1:N
            data_abs2 = abs2(replica.data[i])
            PN += data_abs2
            if data_abs2 > pk_valabs2
                pk_valabs2 = data_abs2
                pk_val = replica.data[i]
                pk_idx = i
            end
        end
        PN = (PN - pk_valabs2) / (N - 1)
        SNR_est = calc_snr(pk_valabs2, PN)
        # # Check negative frequencies for peak
        # @inbounds for i in neg_freq_interval[1]:neg_freq_interval[2]
        #     data_abs2 = abs2(replica.data[i])
        #     if data_abs2 > pk_valabs2
        #         pk_valabs2 = data_abs2
        #         pk_val = replica.data[i]
        #         pk_idx = i
        #     end
        # end
        # Calculate peak frequency (the Doppler frequency error)
        if pk_idx > N_over_2
            fd_fine = (pk_idx-N-1)*Δf
        else
            fd_fine = (pk_idx-1)*Δf
        end
        fd_est = fd_course + fd_fine
        # Calculate initial phase
        ϕ_init = atan(imag(pk_val), real(pk_val))
        # Calculate the covariance matrix
        # We estimate the error to be ±2 Doppler bin for the frequency error
        # and ±1 for the phase error.
        pk_low_idx = ((pk_idx-err_bin_num_ϕ)+replica.sample_num)%replica.sample_num
        pk_high_idx = ((pk_idx+err_bin_num_ϕ)+replica.sample_num)%replica.sample_num
        if pk_low_idx == 0
            pk_low_idx = replica.sample_num
        end
        if pk_high_idx == 0
            pk_high_idx = replica.sample_num
        end
        pk_low = replica.data[pk_low_idx]
        pk_high = replica.data[pk_high_idx]
        ϕ_low = atan(imag(pk_low)/real(pk_low))
        ϕ_high = atan(imag(pk_high)/real(pk_high))
        ϕ_init_err = mean([abs(ϕ_init-ϕ_low), abs(ϕ_init-ϕ_high)])

        pk_vals[m] = pk_val
        pk_valabs2s[m] = pk_valabs2
        pk_idxs[m] = pk_idx
        PNs[m] = PN
        SNR_ests[m] = SNR_est
        fd_fines[m] = fd_fine
        fd_ests[m] = fd_est
        ϕ_inits[m] = ϕ_init
        ϕ_init_errs[m] = ϕ_init_err
    end
    idx = argmax(SNR_ests)
    pk_val = pk_vals[idx]
    pk_valabs2 = pk_valabs2s[idx]
    pk_idx = pk_idxs[idx]
    PN = PNs[idx]
    SNR_est = SNR_ests[idx]
    fd_fine = fd_fines[idx]
    fd_est = fd_ests[idx]
    ϕ_init = ϕ_inits[idx]
    ϕ_init_err = ϕ_init_errs[idx]
    # replica.isreplica = false
    P = diagm([ϕ_init_err^2, (2π*err_bin_num_f*Δf)^2, (2π*σω)^2])
    R = [ϕ_init_err^2]
    # Return `FineAcquisitionResults` struct
    return FineAcquisitionResults(prn, String(:fft), fd_course, fd_rate, n₀_idx_course,
                                  t_length, fd_fine, fd_est, ϕ_init, M, P, R)
end

"""
    fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                    n₀_idx_course, type::Val{:carrier}; fd_rate=0.,
                    t_length=replica.t_length, freq_lim=50000., M=10,
                    σω=1000.)

Performs an carrier based fine acquisition on `data`.
`replica` decides what signal type to use and the length of each `M` segment.

`M` is the multiple of the length of replica to be used for fine acquisition.

Required Arguments:

- `data::GNSSSignal`: either `GNSSData` or `ReplicaSignal` struct
- `replica::ReplicaSignal`: struct to use for replica signal generation
    * `replica.t_length` determines the integration time, `T`
- `prn::Int`: PRN number to track
- `fd_course`: course acquired Doppler frequency in Hz
- `n₀_idx_course`: initial code start index location
- `type::Val{:fft}`: `Val(:fft)` uses the FFT based fine acquisition method

Optional Arguments:

- `fd_rate`: the Doppler frequency rate in Hz/s `(default = 0Hz)`
- `t_length`: `[NOT USED]` `(default = replica.t_length)`
- `M`: Number of integrations to perform `(default = 10)`
- `σω`: expected uncertainty of Doppler rate in Hz/s `(default = 1000Hz/s)`

Returns:

- `FineAcquisitionResults` struct
"""
function fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                         n₀_idx_course, type::Val{:carrier}; fd_rate=0.,
                         t_length=replica.t_length, M=10, σω=1000.)
    # Check that there are at least M replica signals that can fit in time
    # into data.
    Mblocks = floor(Int, data.sample_num/replica.sample_num)
    if Mblocks < M
        error("Data must be at least $(M*replica.t_length) seconds long.")
    end
    # Samples per `M` data segment
    N = replica.sample_num
    # Set ϕ_init and ϕ
    ϕ_init = 0.
    ϕ = Array{Float64}(undef, M)
    ϕi = 0.
    for i in 1:M
        # Generate replica
        # Set signal parameters
        definereplica!(replica;
                       prn=prn, f_d=fd_course,
                       fd_rate=fd_rate, phi=0, f_if=0.,
                       code_start_idx=n₀_idx_course,
                       include_carrier=false)
        # Generate signal
        generatereplica!(replica)
        # Get a `view` of the current data segment and its corresponding time array
        datasegment = view(data.data, (i-1)*N+1:i*N)
        # Wipeoff IF and course Doppler from data
        @threads for j in 1:replica.sample_num
            t = calc_t_at_i((i-1)*N + j, 0, data.f_s)
            @inbounds replica.data[j] = datasegment[j]*cis(-2π*(data.f_if+fd_course+0.5*fd_rate*t)*t)*replica.data[j]
        end
        # Perform in place fft operation
        fft!(replica.data)
        # Get FFT peak value
        pk_idx = argmax(abs2.(replica.data))
        # Take square root of `pk`
        pk = replica.data[pk_idx]
        # Calculate ϕ for current processing block
        ϕi = atan(imag(pk), real(pk))
        # ϕi = atan(imag(pk)/real(pk))
        @inbounds ϕ[i] = ϕi
        if i == 1
            @inbounds ϕ_init = ϕi
        end
    end
    # Take the difference between ϕs & check and correct phase values if > than π
    dϕ = diff(ϕ)
    for i in 1:M-1
        if dϕ[i] > π
            dϕ[i] -= 2π
        elseif dϕ[i] < -π
            dϕ[i] += 2π
        else
            # Do nothing. Within bounds.
        end
    end
    # Compute variance from average phase change
    # dϕvar = var(dϕ)
    dϕvar = abs2.(dϕ)
    # Find the maxumum variance and omit from fine Doppler fequency calculation
    maxvaridx = argmax(dϕvar)
    # Take the mean while ommitting the largest phase change in `dϕ`
    dϕavg = 0.
    for i in 1:M-1
        if i != maxvaridx
            dϕavg += dϕ[i]
        end
    end
    dϕavg /= (M-2)
    # Calculate the fine Doppler frequency
    f_s = replica.f_s
    # fd_fine = dϕavg/(2π*N/f_s)
    fd_fine = dϕavg/(2π*replica.t_length)
    fd_est = fd_course + fd_fine
    dϕ_init_err = std(([dϕ[1:maxvaridx-1]; dϕ[maxvaridx+1:end]] .- dϕavg))
    fd_err = dϕ_init_err/(2π*replica.t_length)
    # P = diagm([dϕ_init_err^2, 74.7*fd_err^2, σω^2])
    # P = diagm([dϕ_init_err^2, fd_err^2, σω^2])
    P = diagm([dϕ_init_err^2, (2π*replica.t_length)^2, (2π*σω)^2])
    R = [dϕ_init_err^2]
    # Return `FineAcquisitionResults` struct
    return FineAcquisitionResults(prn, String(:carrier), fd_course, fd_rate, n₀_idx_course,
                                  t_length, fd_fine, fd_est, ϕ_init, M, P, R)
end

"""
    fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                    n₀_idx_course, type::Val{:ffl}; fd_rate=0.,
                    t_length=replica.t_length, freq_lim=50000., M=10,
                    σω=1000.)

Performs an carrier based fine acquisition on `data`.
`replica` decides what signal type to use and the length of each `M` segment.

`M` is the multiple of the length of replica to be used for fine acquisition.

Required Arguments:

- `data::GNSSSignal`: either `GNSSData` or `ReplicaSignal` struct
- `replica::ReplicaSignal`: struct to use for replica signal generation
    * `replica.t_length` determines the integration time, `T`
- `prn::Int`: PRN number to track
- `fd_course`: course acquired Doppler frequency in Hz
- `n₀_idx_course`: initial code start index location
- `type::Val{:fll}`: `Val(:fll)` uses the FLL based fine acquisition method

Optional Arguments:

- `fd_rate`: the Doppler frequency rate in Hz/s `(default = 0Hz)`
- `t_length`: `[NOT USED]` `(default = replica.t_length)`
- `M`: Number of integrations to perform `(default = 10)`
- `σω`: expected uncertainty of Doppler rate in Hz/s `(default = 1000Hz/s)`
- `B`: FLL bandwith in Hz `(default = 20)`
- `dampening`: dampening ratio for FLL `(default = 0.707)`

Returns:

- `FineAcquisitionResults` struct
"""
function fineacquisition(data::GNSSSignal, replica::ReplicaSignal, prn, fd_course,
                         n₀_idx_course, type::Val{:fll}; fd_rate=0.,
                         t_length=replica.t_length, M=10, σω=1000.,
                         B=20, dampening=0.707, q_a=1, h₋₂=2e-20)
    ωₙ = B/0.53
    T = t_length
    sig_freq = replica.signal_type.sig_freq
	qω = 2*π^2*h₋₂  # oscillator frequency PSD
    # Check that there are at least M replica signals that can fit in time
    # into data.
    Mblocks = floor(Int, data.sample_num/replica.sample_num)
    if Mblocks < M
        error("Data must be at least $(M*replica.t_length) seconds long.")
    end
    # Samples per `M` data segment
    N = replica.sample_num
    # Initialize FLL
    Δω = Array{Float64}(undef, M-1)
    SNR = Array{Float64}(undef, M)
    x = Array{Float64}(undef, 2, M)
    x[:,1] .= [fd_course, fd_rate] .* 2π
    P = Array{Float64}(undef, 2, M)
    P[:,1] .= ([0.5/t_length, σω] .* 2π).^2
    x⁺ = [x[1,1]; x[2,1]]
    P⁺ = [P[1,1] 0; 0 P[2,1]]
    Δθₖ₋₁ = 0
    K = [2*dampening*ωₙ*T-3*ωₙ^2*T^2/2; ωₙ^2*T]
    K[2] = 0
    H = [1 T/2]
    A = [1 T; 0 1]
    Q = (2π*sig_freq)^2 .* [T*qω+T^3*q_a/(3*c^2) T^2*q_a/(2*c^2);
                            T^2*q_a/(2*c^2)      T*q_a/c^2]
    for i in 1:M
        # Generate replica
        # Set signal parameters
        f_d, fd_rate = x⁺ ./ 2π
        fd_rate = 0
        definereplica!(replica;
                       prn=prn, f_d=fd_course,
                       fd_rate=fd_rate, phi=0, f_if=0.,
                       code_start_idx=n₀_idx_course,
                       include_carrier=false,
                       start_t=(i-1)*T)
        # Generate signal
        generatereplica!(replica)
        # Get a `view` of the current data segment and its corresponding time array
        datasegment = view(data.data, (i-1)*N+1:i*N)
        # Wipeoff IF and course Doppler from data
        @threads for j in 1:replica.sample_num
            t = calc_t_at_i((i-1)*N + j, 0, data.f_s)
            @inbounds replica.data[j] = datasegment[j]*cis(-2π*(data.f_if+fd_course+0.5*fd_rate*t)*t)*replica.data[j]
        end
        # Perform in place fft operation
        fft!(replica.data)
        # Get FFT peak value
        pk_idx = argmax(abs2.(replica.data))
        # Take square root of `pk`
        pk = replica.data[pk_idx]
        # Calculate SNR
        PS = abs2(pk)
        PN = (sum(abs2.(replica.data)) - PS) / (N - 1)
        snr = calc_snr(PS, PN)
        SNR[i] = snr
        # Calculate ϕ for current processing block
        # ϕi = atan(imag(pk), real(pk))
        Δϕₖ = atan(imag(pk)/real(pk))
        if i == 1
            Δωₖ = 0
        else
            dϕ = (Δϕₖ - Δθₖ₋₁)
            if dϕ > π
                dϕ -= 2π
            elseif dϕ < -π
                dϕ += 2π
            else
                # Do nothing. Within bounds.
            end
            Δωₖ = dϕ/T
            Δω[i-1] = Δωₖ
        end
        Δθₖ₋₁ = Δϕₖ
        # x⁺ = x⁺ + K*Δωₖ
        x⁺ = x⁺ .+ Δωₖ
        # println(Δωₖ/2π)
        P⁺ = (I - K*H)*P⁺
        x[:,i] .= x⁺
        P[:,i] .= diag(P⁺)
        # Propagate states
        x⁺ = A*x⁺
        P⁺ = A*P⁺*A' + Q
    end
    return (x, P, A, Q, H, K, SNR, Δω)
    # Return `FineAcquisitionResults` struct
    # return FineAcquisitionResults(prn, String(:carrier), fd_course, fd_rate, n₀_idx_course,
    #                               t_length, fd_fine, fd_est, ϕ_init, "N/A", P, R)
end


using POMCPOW
using Test

using POMDPs
using POMDPModels
using ParticleFilters
using POMDPTesting
using D3Trees
using BeliefUpdaters
using POMDPModelTools

@testset "all" begin

    @testset "POMDPTesting" begin
        solver = POMCPOWSolver()
        pomdp = BabyPOMDP()
        test_solver(solver, pomdp, updater=DiscreteUpdater(pomdp))
        test_solver(solver, pomdp)

        solver = POMCPOWSolver(max_time=0.1, tree_queries=typemax(Int))
        test_solver(solver, pomdp, updater=DiscreteUpdater(pomdp))
    end

    @testset "type stability" begin
        # make sure internal function is type stable
        pomdp = BabyPOMDP()
        solver = POMCPOWSolver()
        planner = solve(solver, pomdp)
        b = initialstate_distribution(pomdp)
        B = POMCPOW.belief_type(POMCPOW.POWNodeFilter, typeof(pomdp))
        tree = POMCPOWTree{B,Bool,Bool,typeof(b)}(b, 2*planner.solver.tree_queries)
        @inferred POMCPOW.simulate(planner, POMCPOW.POWTreeObsNode(tree, 1), true, 10)

        pomdp = LightDark1D()
        solver = POMCPOWSolver(default_action=485)
        planner = solve(solver, pomdp)

        b = ParticleCollection([LightDark1DState(-1, 0)])
        @test @test_logs (:warn,) @inferred(action(planner, b)) == 485

        b = initialstate_distribution(pomdp)
        @inferred action(planner, b)
    end;

    @testset "currentobs and history" begin
        pomdp = BabyPOMDP()
        solver = POMCPOWSolver()
        planner = solve(solver, pomdp)
        b = initialstate_distribution(pomdp)
        B = POMCPOW.belief_type(POMCPOW.POWNodeFilter, typeof(pomdp))
        tree = POMCPOWTree{B,Bool,Bool,typeof(b)}(b, 2*planner.solver.tree_queries)

        n = POMCPOW.POWTreeObsNode(tree, 1)
        nb = belief(n)
        # we can't call current obs on the root node
        @test_throws MethodError currentobs(nb)
        # simulate the tree to expand one step
        POMCPOW.simulate(planner, n, true, 1)
        n = POMCPOW.POWTreeObsNode(tree, 2)
        nb = belief(n)
        # but at a non-root node, this should work
        @test currentobs(nb) isa Bool
        @test currentobs(nb) == history(nb)[end].o
        @test history(nb)[end].a isa Bool
    end;

    @testset "D3tree" begin
        # make sure internal function is type stable
        pomdp = BabyPOMDP()
        solver = POMCPOWSolver()
        planner = solve(solver, pomdp)
        b = initialstate_distribution(pomdp)
        a, info = action_info(planner, b)
        # d3t = D3Tree(planner)
        @test_throws KeyError d3t = D3Tree(info[:tree])

        a, info = action_info(planner, b, tree_in_info=true)
        # d3t = D3Tree(planner)
        d3t = D3Tree(info[:tree])
        # inchrome(d3t)
    end;

    @testset "init_node_sr_belief_error" begin
        include("init_node_sr_belief_error.jl")
    end;
end;


push!(LOAD_PATH, "../src")

using CASIM

render_option = Not_Render()
sim_horizon = 5000
num_run = 3
init_num_ac = 100
const_num_ac = [150, 200, 250, 300, 350, 400, 450]

"""
	VICAS + Nearest
	num_ac
"""
exp = "ConstNumAC_VICAS_10km_init_ac_" * string(init_num_ac) * "/" * VICAS_policyname # to make sub-folder

if !ispath("../simulation_results/ConstNumAC_VICAS_10km_init_ac_" * string(init_num_ac))
	mkdir("../simulation_results/ConstNumAC_VICAS_10km_init_ac_" * string(init_num_ac))
end
cas = VICAS()
cas_ctrl = Uniform_CAS(cas)
sensor = Nearest_Intruder_Tracker()
sensor_ctrl = Uniform_Sensor(sensor)

for run in 1:num_run
	for num_ac in const_num_ac
		scheme = "Nearest_num_ac_" *
			string(Int(num_ac)) * "_run_" * string(run)
		airspace_ctrl = Constant_Number_Controller(num_ac)
		simulate(exp, scheme, sim_horizon, airspace_ctrl, cas_ctrl, sensor_ctrl, render_option)
	end
end


using Test
using StaticArrays
using ClimateMachine.VariableTemplates
using CUDA
using KernelAbstractions
using KernelAbstractions.Extras: @unroll

include("complex_models.jl")

run_gpu = CUDA.has_cuda_gpu()
if run_gpu
    CUDA.allowscalar(false)
else
    CUDA.allowscalar(true)
end

get_device() = run_gpu ? CPU() : CUDADevice()
device_array(a, ::CUDADevice) = CuArray(a)
device_array(a, ::CPU) = Array(a)
device_rand(::CUDADevice, args...) = CUDA.rand(args...)
device_rand(::CPU, args...) = rand(args...)
number_states(m) = varsize(state(m, Int))

@kernel function mem_copy_kernel!(
    m::AbstractModel,
    dst::AbstractArray{FT, N},
    src::AbstractArray{FT, N},
) where {FT, N}
    @uniform begin
        ns = number_states(m)
        vs = state(m, FT)
        local_src = MArray{Tuple{ns}, FT}(undef)
        local_dst = MArray{Tuple{ns}, FT}(undef)
    end
    i = @index(Group, Linear)
    @inbounds begin
        @unroll for s in 1:ns
            local_src[s] = src[s, i]
        end
        mem_copy!(m, Vars{vs}(local_dst), Vars{vs}(local_src))
        @unroll for s in 1:ns
            dst[s, i] = local_dst[s]
        end
    end
end

function mem_copy!(m::ScalarModel, dst::Vars, src::Vars)
    dst.x = src.x
end

function mem_copy!(m::NTupleContainingModel{N}, dst::Vars, src::Vars) where {N}
    dst.vector_model.x = src.vector_model.x
    dst.scalar_model.x = src.scalar_model.x

    up = vuntuple(i -> src.ntuple_model[i].scalar_model.x, N)
    up_v = vuntuple(i -> src.ntuple_model[i].vector_model.x, N)
    up_sv = SVector(up...)

    @unroll_map(N) do i
        dst.ntuple_model[i].scalar_model.x = up[i]    # index into tuple
        dst.ntuple_model[i].scalar_model.x = up_sv[i] # index into SArray
        dst.ntuple_model[i].vector_model.x = up_v[i]
    end
end

@testset "ScalarModel" begin
    FT = Float32
    device = get_device()
    n_elem = 10
    m = ScalarModel()
    ns = number_states(m)
    a_src = Array{FT}(undef, ns, n_elem)
    a_dst = Array{FT}(undef, ns, n_elem)
    d_src = device_array(a_src, device)
    d_dst = device_array(a_dst, device)
    d_src .= device_rand(device, FT, ns, n_elem)
    fill!(d_dst, 0)

    work_groups = (1,)
    ndrange = (n_elem,)
    kernel! = mem_copy_kernel!(device, work_groups)
    event = kernel!(m, d_dst, d_src, ndrange = ndrange)
    wait(device, event)

    a_src = Array(d_src)
    a_dst = Array(d_dst)
    @test a_src == a_dst
end

@testset "NTupleContainingModel" begin
    FT = Float32
    device = get_device()
    n_elem = 10
    Nv = 4
    N = 3
    m = NTupleContainingModel(N, Nv)
    ns = number_states(m)
    a_src = Array{FT}(undef, ns, n_elem)
    a_dst = Array{FT}(undef, ns, n_elem)
    d_src = device_array(a_src, device)
    d_dst = device_array(a_dst, device)
    d_src .= device_rand(device, FT, ns, n_elem)
    fill!(d_dst, 0)

    work_groups = (1,)
    ndrange = (n_elem,)
    kernel! = mem_copy_kernel!(device, work_groups)
    event = kernel!(m, d_dst, d_src, ndrange = ndrange)
    wait(device, event)

    a_src = Array(d_src)
    a_dst = Array(d_dst)
    @test a_src == a_dst
end


"""
Structures to keep information sorted; for 2D arrays
"""
mutable struct pri
    g::Array{Float64,2}     # approximation
    gInit::Array{Float64,2} # initial state
    # Energies
    Ep::Array{Float64,1}
    Em::Array{Float64,1}
    E::Array{Float64,1}
    # Additional/optional quantities for debugging etc
    p::Array{Float64,2}
    q::Array{Float64,2}
    d::Array{Float64,1}
    d2::Array{Float64,1}
    name::String
end

pri(gInit, nSteps, name="NoName") =
    pri(gInit, gInit,
        zeros(nSteps), zeros(nSteps), zeros(nSteps),
        gInit, gInit,
        zeros(nSteps), zeros(nSteps),
        name)

mutable struct lpri
    g::Array{Float64,2}     # approximation
    gInit::Array{Float64,2} # initial state
    # Energies
    Ep::Array{Float64,1}
    Em::Array{Float64,1}
    E::Array{Float64,1}
    # Additional/optional quantities for debugging etc
    p::Array{Float64,2}
    q::Array{Float64,2}
    go::Array{Float64,2}
    qo::Array{Float64,2}
    po::Array{Float64,2}
    d::Array{Float64,1}
    d2::Array{Float64,1}
    d3::Array{Float64,1}
    d4::Array{Float64,1}
    d5::Array{Float64,1}
    name::String
end

lpri(gInit, nSteps, name="NoName") =
    pri(gInit, gInit,
        zeros(nSteps), zeros(nSteps), zeros(nSteps),
        gInit, gInit, gInit, gInit, gInit,
        zeros(nSteps), zeros(nSteps), zeros(nSteps), zeros(nSteps), zeros(nSteps),
        name)

"""
    Structures to keep information sorted; for 1D arrays
"""
mutable struct pri1D
    g      # approximation
    gInit  # initial state
    # Energies
    Ep
    Em
    E
    # Additional/optional quantities for debugging etc
    p
    q
    d
    d2
    name::String
end

pri1D(gInit, nSteps, name="NoName") =
    pri(gInit, gInit,
        zeros(nSteps), zeros(nSteps), zeros(nSteps),
        gInit, gInit,
        zeros(nSteps), zeros(nSteps),
        name)


# this file is to run JWAS
using DataFrames,CSV,Random,Distributions,JWAS,DelimitedFiles, LinearAlgebra
Random.seed!(123)

method            = ARGS[1]
rep               = parse(Int, ARGS[2])  #rep1,...,rep20
pct               = parse(Float64, ARGS[3])

@show method,rep,pct

mainpath="/group/qtlchenggrp/tianjing/omics_fix_all/missing_pattern_legarra/"
cd(mainpath*"$pct/$method/rep$rep/")

############ READ DATA ##########
#read missing index
#some individuals have no omics data
missingIndex=Int.(vec(readdlm(mainpath*"missing_index/missingIndex.pct$pct.rep$rep.txt")))
@show missingIndex[1:3]
@show length(missingIndex)

#read phenotype
phenofile="/group/qtlchenggrp/tianjing/omics_data/andrew1055/data1/y_no_ar.csv"
phenotypes = CSV.read(phenofile,DataFrame,delim = ',',header=true,missingstrings=["NA"])

#read omics
omicsfile  = "/group/qtlchenggrp/tianjing/omics_data/andrew1055/data1/omics.csv"
omics  = CSV.read(omicsfile,DataFrame,delim = ',',header=true,missingstrings=["NA"]);
insertcols!(omics,2,:y => phenotypes[:,:y]); #insert phenotype into 2nd column of omics
for i in 2:size(omics,2) #skip ID
    omics[!,i]= convert(Vector{Union{Missing,Float64}}, omics[!,i]); #convert the data type to allow missing data
end
omics[956:1055,:y] .= missing  #missing phenotype for individuals in testing datasett
omics[missingIndex,3:end].=missing; #some individuals have no omics data
@show [count(ismissing,col) for col in eachcol(omics)]

omics_name=["gene$i" for i in 1:1200]

@show size(omics)
@show omics[1:5,1:5];
@show omics[end-4:end,1:5];

#fixed variances components required in Christensenet al.(2021)
if occursin("gblup",method) #NN-GBLUP
	G=Matrix(Diagonal(repeat([1.22],1200)))  #genetic variance for each omics is 1.22 in Christensenet al.(2021)
	R=Matrix(Diagonal(repeat([0.78],1200)))  #residual variance for omics is 0.78 in Christensenet al.(2021)
	genofile   = "/group/qtlchenggrp/tianjing/omics_data/andrew1055/data1/GRM_geno.csv"
	genotypes  = get_genotypes(genofile,G;separator=',',method="GBLUP",header=false,G_is_marker_variance = false,estimateVariance=false);
end

# build model
model_equation = "y = intercept + genotypes";
#fixed variances components required in Christensenet al.(2021)
user_σ2_yobs=19.37777777777778 #residual variance for observed phenotype
user_σ2_weightsNN=0.01         #variance for neural network weight between omics and observed phenotype

if occursin("gblup",method)   #NN-GBLUP
	model = build_model(model_equation,R;
                        num_hidden_nodes=1200,
                        latent_traits=omics_name,
                        nonlinear_function="linear");
	out   = runMCMC(model,omics,chain_length=5000,printout_model_info=false,user_σ2_yobs=user_σ2_yobs,user_σ2_weightsNN=user_σ2_weightsNN,estimate_variance=false);
end

# Step 5: Check Accuruacy
#read true bv
bv=vec(readdlm("/group/qtlchenggrp/tianjing/omics_data/andrew1055/data1/tbv_m.txt"));
accuruacy_all  = cor(out["EBV_NonLinear"][!,:EBV],bv)
accuruacy_test  = cor(out["EBV_NonLinear"][!,:EBV][956:1055],bv[956:1055])

@show pct,method,rep
@show accuruacy_all
@show accuruacy_test

res=[rep,accuruacy_test, accuruacy_all]
writedlm("res$rep.txt",res)


# This file was generated, do not modify it. # hide
for _ in 1:10^4
    push!(G, rand(Float32, dim))  # push! inserts one item at a time
end

append!(G, MatrixDatabase(rand(Float32, dim, 10^4))) # append! inserts many items at once


# A tuple of (Pix,Spin,Basis) defines the generic behavior of our fields
abstract type Pix end
abstract type Spin end
abstract type Basis end

# All fields are a subtype of this.
abstract type Field{B<:Basis, S<:Spin, P<:Pix, T} <: AbstractVector{T} end

# Spin types, "S0" is spin-0, i.e. a scalar map. "S2" is spin-2 like QU, and S02
# is a tuple of S0 and S2 like TQU.
abstract type S0 <: Spin end
abstract type S2 <: Spin end
abstract type S02 <: Spin end

# the basis type for a FieldTuple can be a BasisTuple which just holds the bases
# of the sub-fields
abstract type BasisTuple{T} <: Basis end
promote_type(::Type{BasisTuple{BT1}}, ::Type{BasisTuple{BT2}}) where {BT1,BT2} = BasisTuple{Tuple{map_tupleargs(promote_type,BT1,BT2)...}}

# Basis types
abstract type Map       <: Basis end
abstract type Fourier   <: Basis end
abstract type QUMap     <: Basis end
abstract type EBMap     <: Basis end
abstract type QUFourier <: Basis end
abstract type EBFourier <: Basis end
const IQUFourier = BasisTuple{Tuple{Fourier,QUFourier}}
const IEBFourier = BasisTuple{Tuple{Fourier,EBFourier}}
const IQUMap     = BasisTuple{Tuple{Map,QUMap}}
const IEBMap     = BasisTuple{Tuple{Map,EBMap}}

basis_promotion_rules = Dict(
    # S0
    (Map,       Fourier)    => Map,
    # S2
    (QUMap,     QUFourier)  => QUMap,
    (EBMap,     EBFourier)  => EBFourier,
    (QUMap,     EBMap)      => QUMap,
    (QUFourier, EBFourier)  => QUFourier,
    (QUMap,     EBFourier)  => QUMap,
    (QUFourier, EBMap)      => QUFourier
)
for ((B1,B2),B′) in basis_promotion_rules
    @eval promote_rule(::Type{$B1}, ::Type{$B2}) = $B′
end

# A "basis-like" object, e.g. the lensing basis Ł or derivative basis Ð. For any
# particular types of fields, these might be different actual bases, e.g. the
# lensing basis is Map for S0 but QUMap for S2.
abstract type Basislike <: Basis end
(::Type{B})(f::F) where {F<:Field,B<:Basislike} = B(F)(f)
(::Type{B})(f′::Field, f::F) where {F<:Field,B<:Basislike} = B(F)(f′,f)
(::Type{B})(a::AbstractArray{<:Field}...) where {B<:Basis} = B.(a...)

# Basis in which derivatives are sparse for a given field
abstract type DerivBasis <: Basislike end
const Ð = DerivBasis
Ð!(args...) = Ð(args...)

# Basis in which lensing is a pixel remapping for a given field
abstract type LenseBasis <: Basislike end
const Ł = LenseBasis
Ł!(args...) = Ł(args...)

# B(f) where B is a basis converts f to that basis. This is the fallback if the
# field is already in the right basis.
(::Type{B})(f::Field{B}) where {B} = f

# The abstract `Basis` type means "any basis", hence this conversion rule:
Basis(f::Field) = f

# B(f′, f) converts f to basis B and stores the result inplace in f′. If f is
# already in basis B, we just return f (but note, we never actually set f′ in
# this case, which is more efficient, but necessitates some care when using this
# construct)
(::Type{B})(f′::Field{B}, f::Field{B}) where {B} = f

### ImplicitOp
# ImplicitOps and ImplicitFields are operators and fields which may not be
# explicitly stored, but can be multiplied, etc... and generally implicitly take
# on whatever size they need. The specification of Float32 as the the Array type
# is only for inference, such that its widened to whatever its needed to be the
# end.
abstract type ImplicitOp{B<:Basis, S<:Spin, P<:Pix} <: AbstractMatrix{Float32} end
abstract type ImplicitField{B<:Basis, S<:Spin, P<:Pix} <: Field{B,S,P,Float32} end

# no size and 0-length represents the fact that ImplicitOps aquire the size of
# the fields they're applied to. it also helps makes the generic printing
# methods for AbstractMatrix work well.
size(::Union{ImplicitOp,ImplicitField}) = ()
length(::Union{ImplicitOp,ImplicitField}) = 0

adapt_structure(to, x::Union{ImplicitOp,ImplicitField}) = x

# printing
show(io::IO, ::MIME"text/plain", L::ImplicitOp) = show(io,L)
show(io::IO, ::MIME"text/plain", L::Adjoint{<:Any,<:ImplicitOp}) = show(io,L)
show(io::IO, L::Adjoint{<:Any,<:ImplicitOp}) = (print(io,"Adjoint{"); show(io,parent(L)); print(io,"}"))
# this is the main function ImplicitOps should specialize if this default behavior isn't enough:
show(io::IO, L::ImplicitOp) = showarg(io, L, true)

# all CMBLensing operators are then either Diagonals or ImplicitOps
# ImplicitOrAdjOp are things for which algebra is done lazily, including Diagonal{<:ImplicitField}
const DiagOp{F<:Field,T} = Diagonal{T,F}
const LinOp{B,S,P} = Union{ImplicitOp{B,S,P},DiagOp{<:Field{B,S,P}}}
const ImplicitOrAdjOp{B,S,P} = Union{ImplicitOp{B,S,P}, Adjoint{<:Any,<:ImplicitOp{B,S,P}}, DiagOp{<:ImplicitField{B,S,P}}}
const LinOrAdjOp{B,S,P} = Union{ImplicitOrAdjOp{B,S,P},DiagOp{<:Field{B,S,P}}}

### Scalars
# scalars which are allowed in our expressions must be real because we
# implicitly assume our maps are real, and addition/multiplication by a complex
# number, even of the fourier transform, would break this.
const Scalar = Real
const FieldOrOp = Union{Field,LinOp}
const FieldOpScal = Union{Field,LinOp,Scalar}

"""
    logdet(L::LinOp, θ)

If L depends on θ, evaluates `logdet(L(θ))` offset by its fiducial value at
`L()`. Otherwise, returns 0.
"""
logdet(L::LinOp, θ) = depends_on(L,θ) ? logdet(L()\L(θ)) : 0

#
# ### Matrix conversion
#
# # We can build explicit matrix representations of linear operators by applying
# # them to a set of basis vectors which form a complete basis (in practice this
# # is prohibitive except for fairly small maps, e.g. 16x16 pixels, but is quite
# # useful)
#
# """
#     full(::Type{Fin}, ::Type{Fout}, L::LinOp; progress=true)
#     full(::Type{F}, L::LinOp; progress=true)
#
# Construct an explicit matrix representation of the linear operator `L` by
# applying it to a set of vectors which form a complete basis. The `Fin` and
# `Fout` types should be fields which specify the input and output bases for the
# representation (or just `F` if `L` is square and we want the same input/output
# bases)
#
# The name `full` is to be consistent with Julia's SparseArrays where `full`
# builds a full matrix from a sparse one.
# """
# full(::Type{Fin}, ::Type{Fout}, L::LinOp; progress=true) where {Fin<:Field, Fout<:Field} =
#     hcat(@showprogress(progress ? 1 : Inf, [(Fout(L*(x=zeros(length(Fin)); x[i]=1; x)[Tuple{Fin}]))[:] for i=1:length(Fin)])...)
# full(::Type{F}, L::LinOp; kwargs...) where {F<:Field} = full(F,F,L; kwargs...)
#
#

### Other generic stuff

# convenience "getter" functions for the Basis/Spin/Pix
# basis of UnionAlls like basis(Field) will return Basis (which means any Basis)
basis(::Type{<:Field}) = Basis
basis(::Type{<:Field{B,S,P}}) where {B<:Basis,S<:Spin,P<:Pix} = B
basis(::F) where {F<:Field} = basis(F)
spin(::Type{<:Field}) = Spin
spin(::Type{<:Field{B,S,P}}) where {B<:Basis,S<:Spin,P<:Pix} = S
spin(::F) where {F<:Field} = spin(F)
pix(::Type{<:Field}) = Pix
pix(::Type{<:Field{B,S,P}}) where {B<:Basis,S<:Spin,P<:Pix} = P
pix(::F) where {F<:Field} = pix(F)

# we use Field cat'ing mainly for plotting, e.g. plot([f f; f f]) plots a 2×2
# matrix of maps. the following definitions make it so that Fields aren't
# splatted into a giant matrix when doing [f f; f f] (which they would othewise
# be since they're Arrays)
hvcat(rows::Tuple{Vararg{Int}}, values::Field...) = hvcat(rows, ([x] for x in values)...)
hcat(values::Field...) = hcat(([x] for x in values)...)

### printing
print_array(io::IO, f::Field) = print_array(io, f[:])
show_vector(io::IO, f::Field) = show_vector(io, f[:])
show_vector(io::IO, f::ImplicitField) = print(io, "[…]")

# addition/subtraction works between any fields and scalars, promotion is done
# automatically if fields are in different bases
for op in (:+,:-), (T1,T2) in ((:Field,:Scalar),(:Scalar,:Field),(:Field,:Field))
    @eval ($op)(a::$T1, b::$T2) = broadcast($op,($T1==$T2 ? promote : tuple)(a,b)...)
end

# multiplication/division is not strictly defined for abstract vectors, but
# make it work anyway if the two fields are exactly the same type, in which case
# its clear we wanted broadcasted multiplication/division.
for op in (:*, :/)
    @eval ($op)(a::Field{B,S,P}, b::Field{B,S,P}) where {B,S,P} = broadcast($op, a, b)
end

(::Type{T})(f::Field{<:Any,<:Any,<:Any,<:Real}) where {T<:Real} = T.(f)
(::Type{T})(f::Field{<:Any,<:Any,<:Any,<:Complex}) where {T<:Real} = Complex{T}.(f)

one(f::Field) = fill!(similar(f), one(eltype(f)))

invalid_broadcast_error(B1,B2) =
    error("""Can't broadcast fields in different bases. ($B1, $B2)
    Try the same operation without broadcasting (which will do an automatic basis conversion).""")

@init @require PyCall="438e738f-606a-5dbb-bf0a-cddfbfd45ab0" begin
    # never try to auto-convert Fields or LinOps to Python arrays
    PyCall.PyObject(x::Union{LinOp,Field}) = PyCall.pyjlwrap_new(x)
end


# TODO:
#   - give variables sensible names
#   - give functions sensible names
#       - follow Julia convention
#   - remove superfluous type annotaitons
#   - Multithreading...
#       - Each root node is independent

# Notes
# canonical labelling -> graph: (where nodes is number of nodes in subgraph)
# a = BitArray(64,[nodes])
# a.chunks = nauty.canonical_labelling(g)[1]
# Array{Int64,2}(a[end-nodes+1:end,:])
include("nauty.jl")

module kavosh
    import LightGraphs
    import nauty
    const lg = LightGraphs
    # IterTools is 10x-2x faster than Combinatorics
    # Make sure it uses revolving door algorithm
    import IterTools
    const it = IterTools

    # Find all subgraphs of size k in G
    # mild breaking change below: verbose now a keyword argument
    function getsubgraphs(G,k;norm=true, verbose=false)::Dict{Array{UInt64,1},Float64}
        # No speedup compared to []
        answers = Dict{Array{UInt64,1},Int64}()
        Visited = zeros(Bool,lg.nv(G))
        # For each node u
        for u in lg.vertices(G)
            if verbose #&& (mod(u,10) == 0)
                print("\r", round(u/lg.nv(G)*100), "% done")
            end
            # 3x speedup compared to Dict(). UInt8 uses ~5% less memory but is ~7% slower.
            S = Dict{Int64,Array{Int64,1}}()
            # "Global" variable Visited
            Visited .= false
            Visited[u] = true
            # S are parents?
            S[1] = [u]
            Enumerate_Vertex(G,u,S,k-1,1,Visited,answers)
        end
        if !norm return answers end
        normalisation = sum(values(answers))
        d = Dict()
        for (key,v) in answers
            d[key] = v/normalisation
        end
        d
    end

    # Take graph, root vertex, "Selection": the vertices that are part of the current motif, the number of nodes left to choose
    function Enumerate_Vertex(G::GraphType,u::Int64,S,Remainder::Int64,i::Int64,Visited::Array{Bool,1},answers)::Void where GraphType <: lg.SimpleGraphs.AbstractSimpleGraph
        # If there are no more nodes to choose, terminate
        s = copy(S) # Stops shorter trees from accidentally sharing data. Must be a neater way of doing this.
        if Remainder == 0
            # Next step: olieshomegrowncannonlabeller(lg.adjacency_matrix(lg.induced_subgraph(G,temp)))
            # This vcat line makes programme ~20% slower
            #temp = vcat(values(s)...)
            #push!(answers,temp)
            temp = vcat(values(s)...)
            # Most of memory usage is in the G[temp] call
            k = nauty.canonical_form(G[temp])[1]
            # Human readable alternative
            #k = nauty.label_to_adj(nauty.canonical_form(G[temp])[1],3)
            answers[k] = get(answers,k,0) + 1
            return
        else
            # Find vertices that could be part of unique motifs
            ValList = Validate(G,s[i],u,Visited)
            # Make sure we don't try to pick more than we can
            n = min(length(ValList),Remainder)
            # Pick k nodes from our current depth
            for k = 1:n
                for combination in it.subsets(ValList,k)
                    # set the current selection at the current depth to them nodes
                    # Might get a performance boost if s was just a list of numbers, and Parents was stored separately.
                    s[i+1] = combination
                    # and Remainder-k from other depths
                    Enumerate_Vertex(G,u,s,Remainder-k,i+1,Visited,answers)
                    # Repeat for all combinations of k nodes at current depth
                end
            end
            # Tidy up
            for v in ValList
                Visited[v] = false
            end
        end
    end

    # Take graph, selected vertices of previous layer, and the root vertex, return vertices that could form unique motifs
    # This is the bit where the labels are considered. Only labels bigger than root are considered. This is to stop double counting.
    function Validate(G::GraphType,Parents::Array{Int64,1},u::Int64,Visited::Array{Bool,1})::Array{Int64,1} where GraphType <: lg.SimpleGraphs.AbstractSimpleGraph
        ValList = Array{Int64,1}()
        # For all of the immediate neighbours of the parents
        for v in Parents
            # Original paper has this as the neighbours of U. 100% sure that it's a typo.
            for w in lg.all_neighbors(G,v)
                # If the label of the neighbour is greater than the parent, and the neighbour has not yet been visited,
                # I think perhaps it should be the root node, actually
                if (u < w) && !Visited[w]
                    # Mark it as visited and add it to our list of candidates
                    Visited[w] = true
                    push!(ValList, w)
                end
            end
        end
        return ValList
    end

    # Questions: what's the difference between ValList and Visited?
end


__precompile__()

module GeoInterface

    using RecipesBase

    export  AbstractPosition, Position,
            AbstractGeometry, AbstractGeometryCollection, GeometryCollection,
            AbstractPoint, Point,
            AbstractMultiPoint, MultiPoint,
            AbstractLineString, LineString,
            AbstractMultiLineString, MultiLineString,
            AbstractPolygon, Polygon,
            AbstractMultiPolygon, MultiPolygon,
            AbstractFeature, Feature,
            AbstractFeatureCollection, FeatureCollection,

            geotype, # methods
            xcoord, ycoord, zcoord, hasz,
            coordinates,
            geometries,
            geometry, bbox, crs, properties,
            features

    abstract type AbstractPosition{T <: Real} <: AbstractVector{T} end
    geotype(::AbstractPosition) = :Position
    xcoord(::AbstractPosition) = error("xcoord(::AbstractPosition) not defined.")
    ycoord(::AbstractPosition) = error("ycoord(::AbstractPosition) not defined.")
    # optional
    zcoord(::AbstractPosition) = error("zcoord(::AbstractPosition) not defined.")
    hasz(::AbstractPosition) = false
    coordinates(p::AbstractPosition) = hasz(p) ? Float64[xcoord(p),ycoord(p),zcoord(p)] : Float64[xcoord(p),ycoord(p)]
    # (Array-like indexing # http://julia.readthedocs.org/en/latest/manual/arrays/#arrays)
    Base.eltype(p::AbstractPosition{T}) where {T <: Real} = T
    Base.ndims(AbstractPosition) = 1
    Base.length(p::AbstractPosition) = hasz(p) ? 3 : 2
    Base.size(p::AbstractPosition) = (length(p),)
    Base.size(p::AbstractPosition, n::Int) = (n == 1) ? length(p) : 1
    Base.getindex(p::AbstractPosition, i::Int) = (i==1) ? xcoord(p) : (i==2) ? ycoord(p) : (i==3) ? zcoord(p) : nothing
    Base.convert(::Type{Vector{Float64}}, p::AbstractPosition) = coordinates(p)
    # Base.linearindexing{T <: AbstractPosition}(::Type{T}) = LinearFast()

    abstract type AbstractGeometry end
    coordinates(obj::AbstractGeometry) = error("coordinates(::AbstractGeometry) not defined.")

        abstract type AbstractPoint <: AbstractGeometry end
        geotype(::AbstractPoint) = :Point

        abstract type AbstractMultiPoint <: AbstractGeometry end
        geotype(::AbstractMultiPoint) = :MultiPoint

        abstract type AbstractLineString <: AbstractGeometry end
        geotype(::AbstractLineString) = :LineString

        abstract type AbstractMultiLineString <: AbstractGeometry end
        geotype(::AbstractMultiLineString) = :MultiLineString

        abstract type AbstractPolygon <: AbstractGeometry end
        geotype(::AbstractPolygon) = :Polygon

        abstract type AbstractMultiPolygon <: AbstractGeometry end
        geotype(::AbstractMultiPolygon) = :MultiPolygon

        abstract type AbstractGeometryCollection <: AbstractGeometry end
        geotype(::AbstractGeometryCollection) = :GeometryCollection
        geometries(obj::AbstractGeometryCollection) = error("geometries(::AbstractGeometryCollection) not defined.")

    abstract type AbstractFeature end
    geotype(::AbstractFeature) = :Feature
    geometry(obj::AbstractFeature) = error("geometry(::AbstractFeature) not defined.")
    # optional
    properties(obj::AbstractFeature) = Dict{String,Any}()
    bbox(obj::AbstractFeature) = nothing
    crs(obj::AbstractFeature) = nothing

    abstract type AbstractFeatureCollection end
    geotype(::AbstractFeatureCollection) = :FeatureCollection
    features(obj::AbstractFeatureCollection) = error("features(::AbstractFeatureCollection) not defined.")
    # optional
    bbox(obj::AbstractFeatureCollection) = nothing
    crs(obj::AbstractFeatureCollection) = nothing

    include("operations.jl")
    include("geotypes.jl")
    include("plotrecipes.jl")
end


"""
    Ridge{TF<:AbstractFloat} <: RegressionModel

Data from a ridge regression.
"""
struct Ridge{TF<:AbstractFloat} <: RegressionModel
    y::Vector{TF}
    C::Matrix{TF}
    P::Matrix{TF}
    Cy::Vector{TF}
    invCCP::Matrix{TF}
    crossy::TF
    crossC::Matrix{TF}
    Xs::Union{Vector{Matrix{TF}},Nothing}
    λ::TF
    θ::Vector{TF}
    resid::Vector{TF}
    score::Matrix{TF}
    dof_res::TF
    dof_adj::Int
end

modelmatrix(m::Ridge) = m.C
coef(m::Ridge) = m.θ
residuals(m::Ridge) = m.resid

show(io::IO, ::Ridge) = print(io, "Ridge regression")

"""
    SearchCriterion

Supertype for all criteria used for model selection.
"""
abstract type SearchCriterion end

show(io::IO, c::SearchCriterion) = print(io, typeof(c).name.name)

"""
    LOOCV <: SearchCriterion

Leave-one-out cross validation.
"""
struct LOOCV <: SearchCriterion end

show(io::IO, ::MIME"text/plain", ::LOOCV) = print(io, "Leave-one-out cross validation")

"""
    GCV <: SearchCriterion

Generalized cross validation.
"""
struct GCV <: SearchCriterion end

show(io::IO, ::MIME"text/plain", ::GCV) = print(io, "Generalized cross validation")

"""
    AIC <: SearchCriterion

Akaike information criterion.
"""
struct AIC <: SearchCriterion end

show(io::IO, ::MIME"text/plain", ::AIC) = print(io, "Akaike information criterion")

"""
    SmoothAlgorithm

Supertype for all algorithms used to compute estimates
needed for selecting the smoothing parameter.
"""
abstract type SmoothAlgorithm end

show(io::IO, a::SmoothAlgorithm) = print(io, typeof(a).name.name)

"""
    DemmlerReinsch <: SmoothAlgorithm

Demmler-Reinsch orthogonalization for fast ridge regressions
with alternative smoothing parameters.
The implementation follows Ruppert et al. (2003).

Once the orthogonalization is solved,
estimates with different smoothing parameters can be generated at very low cost.
The resulting estimates are accurate enough for model selection
and are typically very close to the estimates
generated by the slower [`DirectSolve`](@ref) approach
that directly solves the penalized least squares problem for each parameter.

# Reference
Ruppert, David, M. P. Wand, M., and R. J. Carroll. 2003.
Semiparametric Regression (Cambridge Series in Statistical and Probabilistic Mathematics).
Cambridge: Cambridge University Press.
"""
struct DemmlerReinsch <: SmoothAlgorithm end

show(io::IO, ::MIME"text/plain", ::DemmlerReinsch) =
    print(io, "Demmler-Reinsch orthogonalization")

"""
    DirectSolve <: SmoothAlgorithm

Solve the ridge regressions with alternative smoothing parameters
by directly repeating each penalized least squares problem.

This algorithm is intended to generate the most accurate estimates
for each smoothing parameter
when the computational cost is not a concern.
For a faster alternative, see [`DemmlerReinsch`](@ref).
"""
struct DirectSolve <: SmoothAlgorithm end

"""
    ModelSelection

Supertype for all methods used for generating alternative models.
"""
abstract type ModelSelection end

@fieldequal ModelSelection

show(io::IO, s::ModelSelection) = print(io, typeof(s).name.name)

"""
    GridSearch{A<:SmoothAlgorithm, TF<:AbstractFloat} <: ModelSelection

Exhaustively assess all parameters on a specified grid. See also [`grid`](@ref).
"""
struct GridSearch{A<:SmoothAlgorithm, TF<:AbstractFloat} <: ModelSelection
    v::Vector{TF}
    algo::SmoothAlgorithm
    function GridSearch(v::Vector; algo::SmoothAlgorithm=DemmlerReinsch())
        eltype(v) <: AbstractFloat || (v = convert(Vector{Float64}, v))
        TF = eltype(v)
        thres = eps(TF)
        all(v.>thres) || throw(ArgumentError("only positive values are accepted"))
        return new{typeof(algo),TF}(v, algo)
    end
end

"""
    grid([v]; algo=DemmlerReinsch())

Specify the grid vector `v` used for [`GridSearch`](@ref).
If `v` is not provided, a default grid is used.
The default algorithm used for assessing the alternative parameters
on the grid is [`DemmlerReinsch`](@ref), which is very fast.
"""
grid(v::Vector; kwargs...) = GridSearch(v; kwargs...)
grid(v; kwargs...) = GridSearch([v...]; kwargs...)
grid(; kwargs...) = GridSearch(exp.(LinRange(-6, 12, 50)); kwargs...)

function show(io::IO, ::MIME"text/plain", s::GridSearch{A, TF}) where {A, TF}
    N = length(s.v)
    print(io, typeof(s).name.name, '{', A, ", ", TF, '}')
    println(io, " across ", N, " candidate value", N>1 ? "s:" : ":")
    print(IOContext(io, :compact=>true), "  ", s.v)
end

"""
    ModelSelectionResult

Supertype for all results from model selection.
"""
abstract type ModelSelectionResult end

show(io::IO, sr::ModelSelectionResult) = print(io, typeof(sr).name.name)

"""
    GridSearchResult{TF<:AbstractFloat} <: ModelSelectionResult

Additional results from [`GridSearch`](@ref),
including estimates calculated for each parameter on the grid.

# Fields
- `iopt::Dict{SearchCriterion,Int}`: index of the optimal parameter based on each criterion.
- `θs::Matrix{TF}`: point estimates obtained.
- `loocv::Vector{TF}`: leave-one-out cross validation errors.
- `rss::Vector{TF}`: residual sums of squares.
- `gcv::Vector{TF}`: generalized cross validation errors.
- `aic::Vector{TF}`: Akaike information criterion values.
- `dof_fit::Vector{TF}`: degrees of freedom of the fit.
- `dof_res::Vector{TF}`: residual degrees of freedom.
"""
struct GridSearchResult{TF<:AbstractFloat} <: ModelSelectionResult
    iopt::Dict{SearchCriterion,Int}
    θs::Matrix{TF}
    loocv::Vector{TF}
    rss::Vector{TF}
    gcv::Vector{TF}
    aic::Vector{TF}
    dof_fit::Vector{TF}
    dof_res::Vector{TF}
end

function show(io::IO, ::MIME"text/plain", sr::GridSearchResult)
    N = length(sr.rss)
    print(io, typeof(sr).name.name, " across ", N, " candidate value", N>1 ? "s:" : ":")
    for c in (LOOCV(), GCV(), AIC())
        print(io, "\n  ", rpad(c, 6), "=> ", sr.iopt[c])
    end
end

"""
    SmoothLP{TS<:ModelSelection, TC<:SearchCriterion} <: AbstractEstimator

Smooth local projection method introduced by Barnichon and Brownlees (2019).

The implementation is more computationally efficient than the original Matlab example,
as it does not involve explicit construction of the smoother matrix,
which can be very large.
Regressors that are not B-splines are partialled out before any ridge regression
to save computational cost.
For selecting the smoothing parameter,
the supported criteria are [`LOOCV`](@ref), [`GCV`](@ref) and [`AIC`](@ref).

# Reference
Barnichon, Regis and Christian Brownlees. 2019.
"Impulse Response Estimation by Smooth Local Projections."
The Review of Economics and Statistics 101 (3): 522-530.
"""
struct SmoothLP{TS<:ModelSelection, TC<:SearchCriterion} <: AbstractEstimator
    names::Vector{VarIndex}
    order::Int
    ndiff::Int
    search::TS
    criterion::TC
    fullX::Bool
    function SmoothLP(names::Vector{VarIndex}, order::Int, ndiff::Int, search::TS,
            criterion::TC, fullX::Bool) where {TS<:ModelSelection, TC<:SearchCriterion}
        order > 1 || throw(ArgumentError(
            "the order of the polynomial must be greater than one"))
        ndiff > 0 || throw(ArgumentError(
            "the order of the difference to be penalized must be positive"))
        return new{TS,TC}(names, order, ndiff, search, criterion, fullX)
    end
end

"""
    SmoothLP(names, order::Int=3, ndiff::Int=3; kwargs)

Constructor for [`SmoothLP`](@ref).

# Arguments
- `names`: column indices of variables from the data table to be fit with penalized splines.
- `order`: the highest order of the polynomial term in a spline basis.
- `ndiff`: the order of the finite difference operator used to penalize B-spline coefficients.

# Keywords
- `search::ModelSelection=grid()`: method for generating alternative smoothing parameters.
- `criterion::SearchCriterion=LOOCV()`: criterion for selecting the optimal parameter.
- `fullX::Bool=false`: whether regressor matrices not involved in smoothing are kept.
"""
function SmoothLP(names, order::Int=3, ndiff::Int=3;
        search::ModelSelection=grid(), criterion::SearchCriterion=LOOCV(), fullX::Bool=false)
    names isa VarIndex && (names = (names,))
    names = VarIndex[n for n in names]
    return SmoothLP(names, order, ndiff, search, criterion, fullX)
end

function show(io::IO, ::MIME"text/plain", e::SmoothLP)
    println(io, "Smooth Local Projection:")
    print(io, "  smoothed regressor", length(e.names) > 1 ? "s:" : ":")
    println(io, (" $n" for n in e.names)...)
    println(io, "  polynomial order: ", e.order)
    print(io, "  finite difference order: ", e.ndiff)
end

"""
    SmoothLPResult{TF<:AbstractFloat, TS<:ModelSelectionResult} <: AbstractEstimatorResult

Additional results from estimating a smooth local projection.
See also [`SmoothLP`](@ref) and [`LocalProjectionResult`](@ref).

# Fields
- `θ::Vector{TF}`: coefficient estimates for the B-splines.
- `Σ::Vector{TF}`: variance-covariance estimates for the B-splines.
- `bm::Matrix{TF}`: basis matrix of the B-splines.
- `λ::TF`: the optimal smoothing parameter from model selection.
- `loocv::TF`: leave-one-out cross validation error.
- `rss::TF`: residual sum of squares.
- `gcv::TF`: generalized cross validation error.
- `aic::TF`: Akaike information criterion value.
- `dof_fit::TF`: degree of freedom of the fit.
- `dof_res::TF`: residual degree of freedom.
- `search::TS`: additional results from model selection.
- `m::Ridge{TF}`: data from the ridge regression.
"""
struct SmoothLPResult{TF<:AbstractFloat, TS<:ModelSelectionResult} <: AbstractEstimatorResult
    θ::Vector{TF}
    Σ::Matrix{TF}
    bm::Matrix{TF}
    λ::TF
    loocv::TF
    rss::TF
    gcv::TF
    aic::TF
    dof_fit::TF
    dof_res::TF
    search::TS
    m::Ridge{TF}
end

function _basismatrix(order::Int, minh::Int, maxh::Int)
    b = BSplineBasis(order+1, minh-order+1:maxh+order-1)
    return basismatrix(b, minh:maxh)[:, order:end-order+1]
end

# Find the regressors to be fit with splines
function _getcols!(est::SmoothLP, data, xnames)
    ix_sm = Vector{Int}(undef, length(est.names))
    for (i, n) in enumerate(est.names)
        fr = findfirst(x->x==_toint(data, n), xnames)
        fr === nothing && throw(ArgumentError(
            "Variable $(n) is not found among x variables"))
        ix_sm[i] = fr
    end
    return ix_sm
end

function _makeYSr(ys, ss, xs, ws, sts, nlag, horz, subset; TF=Float64)
    Y, X, T, esample = _makeYX(ys, xs, ws, sts, nlag, horz, subset; TF=TF)
    Tfull = size(ys[1],1)
    ns = length(ss)
    # Filter valid rows within those filtered by _makeYX
    esampleT = trues(T)
    aux = BitVector(undef, T)
    S = Matrix{TF}(undef, T, ns)
    for j in 1:ns
        v = view(vec(ss[j], subset, :x, horz, TF), view(nlag+1:Tfull-horz, esample))
        subset === nothing || j > 1 ||
            (esampleT .&= view(view(subset, nlag+1:Tfull-horz), esample))
        _esample!(esampleT, aux, v)
        copyto!(view(S,esampleT,j), view(v,esampleT))
    end
    T1 = sum(esampleT)
    if T1 < T
        T1 > size(X,2) || throw(ArgumentError(
            "not enough observations for nlag=$nlag and horz=$horz"))
        T = T1
        Y = Y[esampleT, :]
        X = X[esampleT, :]
        S = S[esampleT, :]
    end
    # Partial out X
    YS = [Y S]
    B = X'YS
    ldiv!(cholesky!(X'X), B)
    res = YS - X * B
    return res, X, T, esample, esampleT
end

# Generate the penalty matrix
function _makeP(K::Int, ndiff::Int; TF=Float64)
    D = Matrix{TF}(I, K, K)
    for i in 1:ndiff
        D = diff(D, dims=1)
    end
    return D'D
end

# See appendix B.1 in Ruppert et al. (2003)
function _DemmlerReinsch(C::AbstractMatrix, P::AbstractMatrix, δ::AbstractFloat)
    # Need to approximate C'C with a nonsingular matrix
    CC = cholesky!(C'C+δ*P)
    invR = inv!(CC.U)
    F = svd!(invR'P*invR)
    U = F.U
    s = F.S
    invRU = invR * U
    A = C * invRU
    return A, s, invRU
end

# Compute the diagonal elements in the smoother matrix without forming it
function _Sdiag!(Sdiag::Vector, A::Matrix, d::Vector)
    (T, K) = size(A)
    fill!(Sdiag, 0.0)
    @inbounds for j in 1:K
        dj = d[j]
        for i in 1:T
            Sdiag[i] += (A[i,j]^2)/dj
        end
    end
    return Sdiag
end

function _select(est::SmoothLP{<:GridSearch{DemmlerReinsch}}, y, C, crossy, crossC, Cy, P)
    T, K = size(C)
    A, s, invRU = _DemmlerReinsch(C, P, 1e-10)
    b = A'y
    λs = est.search.v
    N = length(λs)
    TF = eltype(y)
    Sdiag = Vector{TF}(undef, T)
    d = Vector{TF}(undef, length(s))
    bd = similar(d)
    θs = Matrix{TF}(undef, K, N)
    rss = Vector{TF}(undef, N)
    dof_fit = similar(rss)
    cve = similar(y)
    loocv = similar(rss)
    dof_res = similar(rss)
    for i in 1:N
        λ = λs[i]
        d .= 1.0 .+ λ.*s
        bd .= b./d
        rss[i] = crossy - 2.0 * b'bd + bd'bd
        dof_fit[i] = sum(1.0./d)
        _Sdiag!(Sdiag, A, d)
        θ = view(θs,:,i)
        mul!(θ, invRU, bd)
        mul!(cve, C, θ)
        cve .= ((y .- cve)./(1.0 .- Sdiag)).^2
        loocv[i] = sum(cve)
        d .= 1.0./d
        dof_res[i] = T - 2.0*sum(d) + d'd
    end
    gcv = rss./(1.0.-dof_fit./T).^2
    aic = log.(rss) .+ 2.0.*dof_fit./T
    iopt = Dict{SearchCriterion,Int}()
    for (c, v) in zip((LOOCV(), GCV(), AIC()), (loocv, gcv, aic))
        _, il = findmin(v)
        iopt[c] = il
    end
    r = GridSearchResult(iopt, θs, loocv, rss, gcv, aic, dof_fit, dof_res)
    return λs[iopt[est.criterion]], r, Sdiag
end

function _Sdiag!(Sdiag::Vector, C::Matrix, invCCP::Matrix)
    T, K = size(C)
    v = Vector{eltype(C)}(undef, K)
    @inbounds for i in 1:T
        v .= view(C,i,:)
        Sdiag[i] = v' * invCCP * v
    end
    return Sdiag
end

function _select(est::SmoothLP{<:GridSearch{DirectSolve}}, y, C, crossy, crossC, Cy, P)
    T, K = size(C)
    CCP = similar(crossC)
    λs = est.search.v
    N = length(λs)
    TF = eltype(y)
    Sdiag = Vector{TF}(undef, T)
    θs = Matrix{TF}(undef, K, N)
    rss = Vector{TF}(undef, N)
    dof_fit = similar(rss)
    cve = similar(y)
    loocv = similar(rss)
    dof_res = similar(rss)
    for i in 1:N
        λ = λs[i]
        CCP .= crossC .+ λ.*P
        ccp = cholesky!(CCP)
        θ = view(θs,:,i)
        ldiv!(θ, ccp, Cy)
        invCCP = inv!(ccp)
        _Sdiag!(Sdiag, C, invCCP)
        rss[i] = crossy - 2.0 * Cy'θ + θ'*crossC*θ
        dof_fit[i] = sum(Sdiag)
        mul!(cve, C, θ)
        cve .= ((y .- cve)./(1.0 .- Sdiag)).^2
        loocv[i] = sum(cve)
        dof_res[i] = T - 2.0*dof_fit[i] + Sdiag'Sdiag
    end
    gcv = rss./(1.0.-dof_fit./T).^2
    aic = log.(rss) .+ 2.0.*dof_fit./T
    iopt = Dict{SearchCriterion,Int}()
    for (c, v) in zip((LOOCV(), GCV(), AIC()), (loocv, gcv, aic))
        _, il = findmin(v)
        iopt[c] = il
    end
    r = GridSearchResult(iopt, θs, loocv, rss, gcv, aic, dof_fit, dof_res)
    return λs[iopt[est.criterion]], r, Sdiag
end

function _est(est::SmoothLP, data, xnames, ys, xs, ws, sts, nlag, minhorz, nhorz, vce, subset,
        iv, ix_iv, yfs, xfs, firststagebyhorz; TF=Float64)
    length(ys) > 1 && throw(ArgumentError("accept only one outcome variable"))
    ix_sm = _getcols!(est, data, xnames)
    ix_nsm = setdiff(1:length(xs), ix_sm)
    nx = sum(ix_nsm) + length(ws)*nlag
    YSr = Vector{Matrix{TF}}(undef, nhorz)
    Xs = Vector{Matrix{TF}}(undef, nhorz)
    T = Vector{Int}(undef, nhorz)
    for h in minhorz:minhorz+nhorz-1
        if iv !== nothing && firststagebyhorz
            xs[ix_iv] .= _firststage(yfs, xfs, ws, sts, nlag, h, subset; TF=TF)
        end
        # xs could be changed by first-stage regression
        ss = view(xs, ix_sm)
        xs_s = view(xs, ix_nsm)
        i = h - minhorz + 1
        YSr[i], Xs[i], T[i], _, _ = _makeYSr(ys, ss, xs_s, ws, sts, nlag, h, subset; TF=TF)
    end
    Tall = sum(T)
    nys = size(YSr[1], 2)
    ns = nys - 1
    bm = _basismatrix(est.order, minhorz, minhorz+nhorz-1)
    nb = size(bm, 2)
    y = Vector{TF}(undef, Tall)
    C = Matrix{TF}(undef, Tall, ns*nb)
    ir = 0
    @inbounds for h in 1:nhorz
        copyto!(view(y,ir+1:ir+T[h]), view(YSr[h],:,1))
        for j in 1:ns
            C[ir+1:ir+T[h],1+nb*(j-1):nb*j] .= view(bm,h,:)'.*view(YSr[h],:,1+j)
        end
        ir += T[h]
    end
    crossy = y'y
    crossC = C'C
    Cy = C'y
    P = _makeP(ns*nb, est.ndiff; TF=TF)
    λ, sr, Sdiag = _select(est, y, C, crossy, crossC, Cy, P)
    CCP = cholesky!(crossC + λ*P)
    if est.search isa GridSearch{DirectSolve}
        l = sr.iopt[est.criterion]
        θ, loocv, rss, gcv, aic, dof_fit, dof_res = sr.θs[:,l], sr.loocv[l],
            sr.rss[l], sr.gcv[l], sr.aic[l], sr.dof_fit[l], sr.dof_res[l]
    else
        # Recalculate to ensure the accuracy of final estimates
        θ = CCP \ (Cy)
        rss = crossy - 2.0 * Cy'θ + θ'*crossC*θ
    end
    resid = C * θ
    resid .= y .- resid
    invCCP = inv!(CCP)
    if !(est.search.algo isa DirectSolve)
        _Sdiag!(Sdiag, C, invCCP)
        dof_fit = sum(Sdiag)
        dof_res = Tall - 2.0*dof_fit + Sdiag'Sdiag
        loocv = sum((resid./(1.0.-Sdiag)).^2)
        gcv = rss/(1.0-dof_fit/Tall)^2
        aic = log(rss) + 2.0*dof_fit/Tall
    end
    B = permutedims(reshape(bm*reshape(θ,nb,ns), nhorz, ns, 1), (2,3,1))
    m = Ridge(y, C, P, Cy, invCCP, crossy, crossC, est.fullX ? Xs : nothing, λ, θ, resid,
        getscore(C, resid), dof_res, nx)
    bms = repeat(bm, 1, ns)
    Σ = vcov(m, vce)
    Vfull = bms * Σ * bms'
    V = Array{TF,3}(undef, ns, ns, nhorz)
    inds = [nhorz*(n-1) for n in 1:ns]
    for h in 1:nhorz
        inds .+= 1
        V[:,:,h] = reshape(view(Vfull, inds, inds), ns, ns, 1)
    end
    slpr = SmoothLPResult(θ, Σ, bm, λ, loocv, rss, gcv, aic, dof_fit, dof_res, sr, m)
    return B, V, T, slpr
end

function lp(r::LocalProjectionResult{<:SmoothLP}, vce::CovarianceEstimator)
    ns = size(r.V, 1)
    s = r.estres
    Σ = vcov(s.m, vce)
    bms = repeat(s.bm, 1, ns)
    Vfull = bms * Σ * bms'
    V = similar(r.V)
    nhorz = length(r.T)
    inds = [nhorz*(n-1) for n in 1:ns]
    for h in 1:nhorz
        inds .+= 1
        V[:,:,h] = reshape(view(Vfull, inds, inds), ns, ns, 1)
    end
    slpr = SmoothLPResult(s.θ, Σ, s.bm, s.λ, s.loocv, s.rss, s.gcv, s.aic,
        s.dof_fit, s.dof_res, s.search, s.m)
    return LocalProjectionResult(r.B, V, r.T, r.est, slpr, vce,
        r.ynames, r.xnames, r.wnames, r.lookupy, r.lookupx, r.lookupw,
        r.nlag, r.minhorz, r.subset, r.normnames, r.normtars, r.normmults,
        r.endonames, r.ivnames, r.firststagebyhorz, r.nocons)
end

"""
    lp(r::LocalProjectionResult{<:SmoothLP}, λ::Real; vce=nothing)

Reestimate the smooth local projection with the smoothing parameter `λ`.
Optionally, an alternative variance-covariance estimator may be specified
with the keyword `vce`.
"""
function lp(r::LocalProjectionResult{<:SmoothLP}, λ::Real;
        vce::Union{CovarianceEstimator, Nothing}=nothing)
    m = r.estres.m
    λ = convert(typeof(m.λ), λ)
    Tall = length(m.y)
    CCP = cholesky!(m.crossC + λ*m.P)
    θ = CCP \ (m.Cy)
    rss = m.crossy - 2.0 * m.Cy'θ + θ'*m.crossC*θ
    resid = m.C * θ
    resid .= m.y .- resid
    invCCP = inv!(CCP)
    Sdiag = similar(m.y)
    _Sdiag!(Sdiag, m.C, invCCP)
    dof_fit = sum(Sdiag)
    dof_res = Tall - 2.0*dof_fit + Sdiag'Sdiag
    loocv = sum((resid./(1.0.-Sdiag)).^2)
    gcv = rss/(1.0-dof_fit/Tall)^2
    aic = log(rss) + 2.0*dof_fit/Tall

    bm = r.estres.bm
    nb = size(bm, 2)
    ns = length(r.est.names)
    nhorz = length(r.T)
    B = permutedims(reshape(bm*reshape(θ,nb,ns), nhorz, ns, 1), (2,3,1))
    m1 = Ridge(m.y, m.C, m.P, m.Cy, invCCP, m.crossy, m.crossC, m.Xs, λ, θ, resid,
        getscore(m.C, resid), dof_res, m.dof_adj)
    bms = repeat(bm, 1, ns)
    vce1 = vce===nothing ? r.vce : vce
    Σ = vcov(m1, vce1)
    Vfull = bms * Σ * bms'
    V = similar(r.V)
    inds = [nhorz*(n-1) for n in 1:ns]
    for h in 1:nhorz
        inds .+= 1
        V[:,:,h] = reshape(view(Vfull, inds, inds), ns, ns, 1)
    end
    slpr = SmoothLPResult(θ, Σ, bm, λ, loocv, rss, gcv, aic, dof_fit, dof_res,
        r.estres.search, m1)
    return LocalProjectionResult(B, V, r.T, r.est, slpr, vce1,
        r.ynames, r.xnames, r.wnames, r.lookupy, r.lookupx, r.lookupw,
        r.nlag, r.minhorz, r.subset, r.normnames, r.normtars, r.normmults,
        r.endonames, r.ivnames, r.firststagebyhorz, r.nocons)
end

_estimatortitle(::LocalProjectionResult{<:SmoothLP}) = "Smooth Local Projection"

function _estimatorinfo(r::LocalProjectionResult{<:SmoothLP}, halfwidth::Int)
    info = ["Smoothing parameter" => TestStat(r.estres.λ),
        "Smoothed regressor" => join(r.est.names, " "),
        "Polynomial order" => r.est.order,
        "Finite difference order" => r.est.ndiff,
        "Selection criterion" => r.est.criterion,
        "Selection algorithm" => r.est.search.algo,
        "Leave-one-out CV" => TestStat(r.estres.loocv),
        "Generalized CV" => TestStat(r.estres.gcv),
        "Akaike information" => TestStat(r.estres.aic),
        "Residual sum of squares" => TestStat(r.estres.rss)]
    return info
end


# initialize screen

WIDTH = 600
HEIGHT = 720
BACKGROUND = colorant"antiquewhite"

header = 120

# define snake actor

snake_pos_x = WIDTH / 2
snake_pos_y = (HEIGHT - header) / 2 + header

snake_size = 20    # default = 20 | try 10, 30, 50

snake_color = colorant"green"

snake_head = Rect(
    snake_pos_x, snake_pos_y, snake_size, snake_size
)

# grow snake

snake_body = []

function grow()
    push!(snake_body,
        Rect(
            snake_head.x, snake_head.y, snake_size, snake_size
        )
    )
end

grow()

# define apple actor

function spawn()
    # all possible locations
    xrange = collect(0:snake_size:(WIDTH - snake_size))
    yrange = collect(header:snake_size:(HEIGHT - snake_size))
    x = rand(xrange)
    y = rand(yrange)
    # array of snake_body locations
    occupied = []
    for i in 1:length(snake_body)
        push!(occupied, (snake_body[i].x, snake_body[i].y))
    end
    # select spawning location
    if (x, y) in occupied
        spawn()
    else
        return x, y
    end
end

apple_pos_x, apple_pos_y = spawn()

apple_size = snake_size

apple_color = colorant"red"

apple = Rect(
    apple_pos_x, apple_pos_y, apple_size, apple_size
)

# define header box actor

headerbox = Rect(0, 0, WIDTH, header)

# initialize other game variables

score = 0

gameover = false

# draw actors

function draw(g::Game)
    # snake
    draw(snake_head, snake_color, fill = true)
    for i in 1:length(snake_body)
        draw(snake_body[i], snake_color, fill = true)
    end
    # apple
    draw(apple, apple_color, fill = true)
    # headerbox
    draw(headerbox, colorant"navyblue", fill = true)
    # display score
    if gameover == false
        display = "Score = $score"
    else
        display = "GAME OVER! Final Score = $score"
        # play again instructions
        replay = TextActor("Click to Play Again", "moonhouse";
            font_size = 36, color = Int[0, 0, 0, 255]
        )
        replay.pos = (135, 390)
        draw(replay)
    end
    txt = TextActor(display, "moonhouse";
        font_size = 36, color = Int[255, 255, 0, 255]
    )
    txt.pos = (30, 30)
    draw(txt)
end

# move snake

speed = snake_size

vx = speed
vy = 0

function move()
    snake_head.x += vx
    snake_head.y += vy
end

# set variables to regulate snake speed

delay = 0.2    # delay in seconds

delay_limit = 0.05

# define border function

function border()
    global gameover
    if snake_head.x == WIDTH ||
        snake_head.x < 0 ||
        snake_head.y == HEIGHT ||
        snake_head.y < header
            gameover = true
    end
end

# define collision functions

function collide_head_body()
    global gameover
    for i in 1:length(snake_body)
        if collide(snake_head, snake_body[i])
            gameover = true
        end
    end
end

function collide_head_apple()
    global delay
    if collide(snake_head, apple)
        # spawn new apple
        apple.x, apple.y = spawn()
        # grow snake body
        grow()
        # reduce delay
        if delay > delay_limit
            delay -= 0.01
        end
    end
end

# define update function

function update(g::Game)
    if gameover == false
        global snake_body, score
        move()
        border()
        collide_head_body()
        collide_head_apple()
        grow()
        popat!(snake_body, 1)
        score = length(snake_body) - 1
        sleep(delay)
    end
end

# define keyboard interaction

function direction(x, y)
    global vx, vy
    vx = x
    vy = y
end

right() = direction(speed, 0)
left() = direction(-speed, 0)
down() = direction(0, speed)
up() = direction(0, -speed)

function on_key_down(g::Game, k)
    if g.keyboard.RIGHT
        if vx !== -speed
            right()
        end
    elseif g.keyboard.LEFT
        if vx !== speed
            left()
        end
    elseif g.keyboard.DOWN
        if vy !== -speed
            down()
        end
    elseif g.keyboard.UP
        if vy !== speed
            up()
        end
    end
end

# define reset function

function reset()
    # reset all global variables

    global snake_head, snake_body
    snake_head = Rect(
        snake_pos_x, snake_pos_y, snake_size, snake_size
    )
    snake_body = []
    grow()

    global apple_pos_x, apple_pos_y, apple
    apple_pos_x, apple_pos_y = spawn()
    apple = Rect(
        apple_pos_x, apple_pos_y, apple_size, apple_size
    )

    global score, vx, vy, delay
    score = 0
    vx = speed
    vy = 0
    delay = 0.2

    global gameover
    gameover = false

end

# define mouse interaction

function on_mouse_down(g::Game)
    if gameover == true
        reset()
    end
end


#=
    Figure S2
=#

using Model # To get colours

using CSV
using DataFrames
using DataFramesMeta
using Plots
using StatsPlots

##############################################################
## Data
##############################################################

data = CSV.read("Data/CSV/DualMarker.csv",DataFrame)

# Get autofluorescence information
E = [@subset(data,:Time .== 0.0, :Quenched .== false, :Temp .== 37.0).Signal_Cy5,
     @subset(data,:Time .== 0.0, :Quenched .== false, :Temp .== 37.0).Signal_BDP]

##############################################################
## Figure S2
##############################################################

figS2 = density2d(E[1],E[2],
    colorbar=false,
    xlim=(0.0,20.0),
    ylim=(0.0,20.0),
    box=:on,axis=:all,aspect_ratio=:equal,lw=0.0,c=:RdPu_9,levels=8,fill=true,
    xlabel="Cy5 autofluoresence",
    ylabel="BDP autofluoresence"
)

##############################################################
## Save
##############################################################

savefig("$(@__DIR__)/figS2.svg")


# import GPUArrays
import Knet
import ProgressMeter
import ValueHistories

function KnetModel(
        ;
        name::AbstractString = "",
        predict_function_source::AbstractString = "",
        loss_function_source::AbstractString = "",
        predict_function::Function = identity,
        loss_function::Function = identity,
        losshyperparameters::AbstractDict = Dict(),
        optimizationalgorithm::Symbol = :nothing,
        optimizerhyperparameters::AbstractDict = Dict(),
        minibatchsize::Integer = 0,
        modelweights::AbstractArray = [],
        isclassificationmodel::Bool = false,
        isregressionmodel::Bool = false,
        maxepochs::Integer = 0,
        printlosseverynepochs::Integer = 0,
        )
    optimizersymbol2type=Dict()
    optimizersymbol2type[:Sgd] = Knet.Sgd
    optimizersymbol2type[:Momentum] = Knet.Momentum
    optimizersymbol2type[:Nesterov] = Knet.Nesterov
    optimizersymbol2type[:Rmsprop] = Knet.Rmsprop
    optimizersymbol2type[:Adagrad] = Knet.Adagrad
    optimizersymbol2type[:Adadelta] = Knet.Adadelta
    optimizersymbol2type[:Adam] = Knet.Adam
    optimizersymbol2type=fix_type(optimizersymbol2type)
    modelweightoptimizers = Knet.optimizers(
        modelweights,
        optimizersymbol2type[optimizationalgorithm];
        optimizerhyperparameters...
        )
    # lastepoch = 0
    # lastiteration = 0
    history = ValueHistories.MVHistory()
    ValueHistories.push!(
        history,
        :epoch_at_iteration,
        0,
        0,
        )
    losshyperparameters = fix_type(losshyperparameters)
    optimizerhyperparameters = fix_type(optimizerhyperparameters)
    result = KnetModel(
        name,
        isclassificationmodel,
        isregressionmodel,
        predict_function_source,
        loss_function_source,
        predict_function,
        loss_function,
        losshyperparameters,
        optimizationalgorithm,
        optimizerhyperparameters,
        minibatchsize,
        maxepochs,
        printlosseverynepochs,
        modelweights,
        modelweightoptimizers,
        history,
        )
    return result
end

"""
"""
function set_max_epochs!(
        x::KnetModel,
        new_max_epochs::Integer,
        )
    x.maxepochs = new_max_epochs
    return nothing
end

"""
"""
function get_underlying(
        x::KnetModel;
        saving::Bool = false,
        loading::Bool = false,
        )
    result = (x.modelweights, x.modelweightoptimizers,)
    return result
end

"""
"""
function get_history(
        x::KnetModel;
        saving::Bool = false,
        loading::Bool = false,
        )
    result = x.history
    return result
end

function parse_functions!(estimator::KnetModel)
    estimator.predict_function = eval(
        Meta.parse(
            strip(
                estimator.predict_function_source
                )
            )
        )
    estimator.loss_function = eval(
        Meta.parse(
            strip(
                estimator.loss_function_source
                )
            )
        )
    return nothing
end

"""
"""
function fit!(
        estimator::KnetModel,
        training_features_array::AbstractArray,
        training_labels_array::AbstractArray,
        tuning_features_array::Union{Nothing, AbstractArray} = nothing,
        tuning_labels_array::Union{Nothing, AbstractArray} = nothing,
        )
    has_tuning_data::Bool =
        !is_nothing(tuning_labels_array) && !is_nothing(tuning_features_array)
    training_features_array = Float64.(training_features_array)
    if estimator.isclassificationmodel && !estimator.isregressionmodel
        training_labels_array = Int.(training_labels_array)
    elseif !estimator.isclassificationmodel && estimator.isregressionmodel
        training_labels_array = Float64.(training_labels_array)
    else
        error(
            "Could not figure out if model is classification or regression"
            )
    end
    training_data = Knet.minibatch(
        training_features_array,
        training_labels_array,
        estimator.minibatchsize,
        )
    loss_function_gradient = Knet.grad(
        estimator.loss_function,
        2,
        )
    all_iterations_so_far, all_epochs_so_far = ValueHistories.get(
        estimator.history,
        :epoch_at_iteration,
        )
    last_iteration = all_iterations_so_far[end]
    last_epoch = all_epochs_so_far[end]
    @info(
        string(
            "Starting to train Knet model. Max epochs: ",
            estimator.maxepochs,
            ".",
            )
        )
    training_lossbeforetrainingstarts = estimator.loss_function(
        estimator.predict_function,
        estimator.modelweights,
        training_features_array,
        training_labels_array;
        estimator.losshyperparameters...
        )
    if has_tuning_data
        tuning_lossbeforetrainingstarts = estimator.loss_function(
           estimator.predict_function,
           estimator.modelweights,
           tuning_features_array,
           tuning_labels_array;
           estimator.losshyperparameters...
           )
    end
    if (estimator.printlosseverynepochs) > 0
        if has_tuning_data
            @info(
                string(
                    "Epoch: ",
                    last_epoch,
                    ". Loss (training set): ",
                    training_lossbeforetrainingstarts,
                    ". Loss (tuning set): ",
                    tuning_lossbeforetrainingstarts,
                    ".",
                    )
                )
        else
            @info(
                string(
                    "Epoch: ",
                    lastepoch,
                    ". Loss: ",
                    lossbeforetrainingstarts,
                    "."
                    )
                )
        end
    end
    while last_epoch < estimator.maxepochs
        for (x_training, y_training) in training_data
            grads = loss_function_gradient(
                estimator.predict_function,
                estimator.modelweights,
                x_training,
                y_training;
                estimator.losshyperparameters...
                )
            Knet.update!(
                estimator.modelweights,
                grads,
                estimator.modelweightoptimizers,
                )
            last_iteration += 1
            training_currentiterationloss = estimator.loss_function(
                estimator.predict_function,
                estimator.modelweights,
                x_training,
                y_training;
                estimator.losshyperparameters...
                )
            ValueHistories.push!(
                estimator.history,
                :training_loss_at_iteration,
                last_iteration,
                training_currentiterationloss,
                )
        end # end for
        last_epoch += 1
        ValueHistories.push!(
            estimator.history,
            :epoch_at_iteration,
            last_iteration,
            last_epoch,
            )
        training_currentepochloss = estimator.loss_function(
            estimator.predict_function,
            estimator.modelweights,
            training_features_array,
            training_labels_array;
            estimator.losshyperparameters...
            )
        ValueHistories.push!(
            estimator.history,
            :training_loss_at_epoch,
            last_epoch,
            training_currentepochloss,
            )
        if has_tuning_data
            tuning_currentepochloss = estimator.loss_function(
                estimator.predict_function,
                estimator.modelweights,
                tuning_features_array,
                tuning_labels_array;
                estimator.losshyperparameters...
                )
            ValueHistories.push!(
                estimator.history,
                :tuning_loss_at_epoch,
                last_epoch,
                tuning_currentepochloss,
                )
        end
        printlossthisepoch = (estimator.printlosseverynepochs > 0) &&
            ( (last_epoch == estimator.maxepochs) ||
                ( (last_epoch %
                    estimator.printlosseverynepochs) == 0 ) )
        if printlossthisepoch
            if has_tuning_data
                @info(
                   string(
                       "Epoch: ",
                       last_epoch,
                       ". Loss (training set): ",
                       training_currentepochloss,
                       ". Loss (tuning set): ",
                       tuning_currentepochloss,
                       ".",
                       ),
                   )
            else
                @info(
                   string(
                       "Epoch: ",
                       last_epoch,
                       ". Loss: ",
                       training_currentepochloss,
                       ".",
                       ),
                   )
            end
        end
    end # end while
    @info(string("Finished training Knet model."))
    return estimator
end

"""
"""
function predict(
        estimator::KnetModel,
        featuresarray::AbstractArray,
        )
    if estimator.isclassificationmodel
        probabilitiesassoc = predict_proba(
            estimator,
            featuresarray,
            )
        predictionsvector = single_labelprobabilitiestopredictions(
            probabilitiesassoc
            )
        return predictionsvector
    elseif estimator.isregressionmodel
        output = estimator.predict_function(
            estimator.modelweights,
            featuresarray;
            )
        outputtransposed = transpose(output)
        result = convert(Array, outputtransposed)
        return result
    else
        error("unable to predict")
    end
end

"""
"""
function predict(
        estimator::KnetModel,
        featuresarray::AbstractArray,
        positive_class::Integer,
        threshold::AbstractFloat,
        )
    if estimator.isclassificationmodel
        probabilitiesassoc = predict_proba(
            estimator,
            featuresarray,
            )
        predictionsvector = single_labelprobabilitiestopredictions(
            probabilitiesassoc,
            positive_class,
            threshold,
            )
        return predictionsvector
    else
        error("can only use the `threshold` argument with classification models")
    end
end

"""
"""
function predict_proba(
        estimator::KnetModel,
        featuresarray::AbstractArray,
        )
    if estimator.isclassificationmodel
        output = estimator.predict_function(
            estimator.modelweights,
            featuresarray;
            probabilities = true,
            )
        outputtransposed = transpose(output)
        numclasses = size(outputtransposed, 2)
        result = Dict()
        for i = 1:numclasses
            result[i] = outputtransposed[:, i]
        end
        return result
    elseif estimator.isregressionmodel
        error("predict_proba is not defined for regression models")
    else
        error("unable to predict")
    end
end

"""
"""
function single_labelmulticlassdataframeknetclassifier_Knet(
        feature_names::AbstractVector,
        single_label_name::Symbol,
        single_label_levels::AbstractVector;
        name::AbstractString = "",
        predict_function_source::AbstractString = "",
        loss_function_source::AbstractString = "",
        losshyperparameters::AbstractDict = Dict(),
        optimizationalgorithm::Symbol = :nothing,
        optimizerhyperparameters::AbstractDict = Dict(),
        minibatchsize::Integer = 0,
        modelweights::AbstractArray = [],
        maxepochs::Integer = 0,
        printlosseverynepochs::Integer = 0,
        feature_contrasts::Union{Nothing, AbstractFeatureContrasts} =
            nothing,
        )
    label_names = [single_label_name]
    label_levels = Dict()
    label_levels[single_label_name] = single_label_levels
    label_levels = fix_type(label_levels)
    dftransformer_index = 1
    dftransformer_transposefeatures = true
    dftransformer_transposelabels = true
    dftransformer = MutableDataFrame2ClassificationKnetTransformer(
        feature_names,
        label_names,
        label_levels,
        dftransformer_index;
        transposefeatures = dftransformer_transposefeatures,
        transposelabels = dftransformer_transposelabels,
        )
    knetestimator = KnetModel(
        ;
        name = name,
        predict_function_source = predict_function_source,
        loss_function_source = loss_function_source,
        losshyperparameters = losshyperparameters,
        optimizationalgorithm = optimizationalgorithm,
        optimizerhyperparameters = optimizerhyperparameters,
        minibatchsize = minibatchsize,
        modelweights = modelweights,
        isclassificationmodel = true,
        isregressionmodel = false,
        maxepochs = maxepochs,
        printlosseverynepochs = printlosseverynepochs,
        )
    predprobalabelfixer =
            ImmutablePredictProbaSingleLabelInt2StringTransformer(
                        1,
                        single_label_levels
                        )
    predictlabelfixer =
            ImmutablePredictionsSingleLabelInt2StringTransformer(
                        1,
                        single_label_levels
                        )
    probapackager = ImmutablePackageSingleLabelPredictProbaTransformer(
        single_label_name,
        )
    predpackager = ImmutablePackageSingleLabelPredictionTransformer(
        single_label_name,
        )
    finalpipeline = dftransformer |>
                    knetestimator |>
                    predprobalabelfixer |>
                    predictlabelfixer |>
                    probapackager |>
                    predpackager
    finalpipeline.name = name
    if !is_nothing(feature_contrasts)
        set_feature_contrasts!(finalpipeline, feature_contrasts)
    end
    return finalpipeline
end

"""
"""
function single_labelmulticlassdataframeknetclassifier(
        feature_names::AbstractVector,
        single_label_name::Symbol,
        single_label_levels::AbstractVector;
        package::Symbol = :none,
        name::AbstractString = "",
        predict_function_source::AbstractString = "",
        loss_function_source::AbstractString = "",
        losshyperparameters::AbstractDict = Dict(),
        optimizationalgorithm::Symbol = :nothing,
        optimizerhyperparameters::AbstractDict = Dict(),
        minibatchsize::Integer = 0,
        modelweights::AbstractArray = [],
        maxepochs::Integer = 0,
        printlosseverynepochs::Integer = 0,
        feature_contrasts::Union{Nothing, AbstractFeatureContrasts} =
            nothing,
        )
    if package == :Knet
        result = single_labelmulticlassdataframeknetclassifier_Knet(
            feature_names,
            single_label_name,
            single_label_levels;
            name = name,
            predict_function_source = predict_function_source,
            loss_function_source = loss_function_source,
            losshyperparameters = losshyperparameters,
            optimizationalgorithm = optimizationalgorithm,
            optimizerhyperparameters = optimizerhyperparameters,
            minibatchsize = minibatchsize,
            modelweights = modelweights,
            maxepochs = maxepochs,
            printlosseverynepochs = printlosseverynepochs,
            feature_contrasts = feature_contrasts
            )
        return result
    else
        error("$(package) is not a valid value for package")
    end
end

"""
"""
function single_labeldataframeknetregression_Knet(
        feature_names::AbstractVector,
        single_label_name::Symbol;
        name::AbstractString = "",
        predict_function_source::AbstractString = "",
        loss_function_source::AbstractString = "",
        losshyperparameters::AbstractDict = Dict(),
        optimizationalgorithm::Symbol = :nothing,
        optimizerhyperparameters::AbstractDict = Dict(),
        minibatchsize::Integer = 0,
        modelweights::AbstractArray = [],
        maxepochs::Integer = 0,
        printlosseverynepochs::Integer = 0,
        feature_contrasts::Union{Nothing, AbstractFeatureContrasts} =
            nothing,
        )
    label_names = [single_label_name]
    dftransformer_index = 1
    dftransformer_transposefeatures = true
    dftransformer_transposelabels = true
    dftransformer = MutableDataFrame2RegressionKnetTransformer(
        feature_names,
        label_names;
        transposefeatures = true,
        transposelabels = true,
        )
    knetestimator = KnetModel(
        ;
        name = name,
        predict_function_source = predict_function_source,
        loss_function_source = loss_function_source,
        losshyperparameters = losshyperparameters,
        optimizationalgorithm = optimizationalgorithm,
        optimizerhyperparameters = optimizerhyperparameters,
        minibatchsize = minibatchsize,
        modelweights = modelweights,
        isclassificationmodel = false,
        isregressionmodel = true,
        maxepochs = maxepochs,
        printlosseverynepochs = printlosseverynepochs,
        )
    predpackager = ImmutablePackageMultiLabelPredictionTransformer(
        [single_label_name,],
        )
    finalpipeline = dftransformer |>
                    knetestimator |>
                    predpackager
    finalpipeline.name = name
    if !is_nothing(feature_contrasts)
        set_feature_contrasts!(finalpipeline, feature_contrasts)
    end
    return finalpipeline
end

"""
"""
function single_labeldataframeknetregression(
        feature_names::AbstractVector,
        single_label_name::Symbol;
        package::Symbol = :none,
        name::AbstractString = "",
        predict_function_source::AbstractString = "",
        loss_function_source::AbstractString = "",
        losshyperparameters::AbstractDict = Dict(),
        optimizationalgorithm::Symbol = :nothing,
        optimizerhyperparameters::AbstractDict = Dict(),
        minibatchsize::Integer = 0,
        modelweights::AbstractArray = [],
        maxepochs::Integer = 0,
        printlosseverynepochs::Integer = 0,
        feature_contrasts::Union{Nothing, AbstractFeatureContrasts} =
            nothing,
        )
    if package == :Knet
        result = single_labeldataframeknetregression_Knet(
            feature_names,
            single_label_name;
            name = name,
            predict_function_source = predict_function_source,
            loss_function_source = loss_function_source,
            losshyperparameters = losshyperparameters,
            optimizationalgorithm = optimizationalgorithm,
            optimizerhyperparameters = optimizerhyperparameters,
            minibatchsize = minibatchsize,
            modelweights = modelweights,
            maxepochs = maxepochs,
            printlosseverynepochs = printlosseverynepochs,
            feature_contrasts = feature_contrasts
            )
        return result
    else
        error("$(package) is not a valid value for package")
    end
end