function auglag_kernel(n::Int, major_iter::Int, max_auglag::Int,
                       line_start::Int, scale::Float64,
                       mu_max::Float64,
                       u_curr::CuDeviceArray{Float64,1}, v_curr::CuDeviceArray{Float64,1},
                       l_curr::CuDeviceArray{Float64,1}, rho::CuDeviceArray{Float64,1},
                       wRIij::CuDeviceArray{Float64,1},
                       param::CuDeviceArray{Float64,2},
                       _YffR::CuDeviceArray{Float64,1}, _YffI::CuDeviceArray{Float64,1},
                       _YftR::CuDeviceArray{Float64,1}, _YftI::CuDeviceArray{Float64,1},
                       _YttR::CuDeviceArray{Float64,1}, _YttI::CuDeviceArray{Float64,1},
                       _YtfR::CuDeviceArray{Float64,1}, _YtfI::CuDeviceArray{Float64,1},
                       frBound::CuDeviceArray{Float64,1}, toBound::CuDeviceArray{Float64,1})
    tx = threadIdx().x
    ty = threadIdx().y
    I = blockIdx().x

    x = @cuDynamicSharedMem(Float64, n)
    xl = @cuDynamicSharedMem(Float64, n, n*sizeof(Float64))
    xu = @cuDynamicSharedMem(Float64, n, (2*n)*sizeof(Float64))

    pij_idx = line_start + 8*(I-1)

    if tx == 1 && ty == 1
        @inbounds for i=1:n
            xl[i] = -Inf
            xu[i] = Inf
        end

        @inbounds begin
            xl[5] = frBound[2*(I-1)+1]
            xu[5] = frBound[2*I]
            xl[6] = toBound[2*(I-1)+1]
            xu[6] = toBound[2*I]
            xl[9] = -2*pi
            xu[9] = 2*pi
            xl[10] = -2*pi
            xu[10] = 2*pi

            x[1] = u_curr[pij_idx]
            x[2] = u_curr[pij_idx+1]
            x[3] = u_curr[pij_idx+2]
            x[4] = u_curr[pij_idx+3]
            x[5] = min(xu[5], max(xl[5], u_curr[pij_idx+4]))
            x[6] = min(xu[6], max(xl[6], u_curr[pij_idx+5]))
            x[7] = wRIij[2*(I-1)+1]
            x[8] = wRIij[2*I]
            x[9] = min(xu[9], max(xl[9], u_curr[pij_idx+6]))
            x[10] = min(xu[10], max(xl[10], u_curr[pij_idx+7]))
        end
    end

    @inbounds begin
        YffR = _YffR[I]; YffI = _YffI[I]
        YftR = _YftR[I]; YftI = _YftI[I]
        YttR = _YttR[I]; YttI = _YttI[I]
        YtfR = _YtfR[I]; YtfI = _YtfI[I]

        param[1,I] = l_curr[pij_idx]
        param[2,I] = l_curr[pij_idx+1]
        param[3,I] = l_curr[pij_idx+2]
        param[4,I] = l_curr[pij_idx+3]
        param[5,I] = l_curr[pij_idx+4]
        param[6,I] = l_curr[pij_idx+5]
        param[7,I] = rho[pij_idx]
        param[8,I] = rho[pij_idx+1]
        param[9,I] = rho[pij_idx+2]
        param[10,I] = rho[pij_idx+3]
        param[11,I] = rho[pij_idx+4]
        param[12,I] = rho[pij_idx+5]
        param[13,I] = v_curr[pij_idx]
        param[14,I] = v_curr[pij_idx+1]
        param[15,I] = v_curr[pij_idx+2]
        param[16,I] = v_curr[pij_idx+3]
        param[17,I] = v_curr[pij_idx+4]
        param[18,I] = v_curr[pij_idx+5]

        param[25,I] = l_curr[pij_idx+6]
        param[26,I] = l_curr[pij_idx+7]
        param[27,I] = rho[pij_idx+6]
        param[28,I] = rho[pij_idx+7]
        param[29,I] = v_curr[pij_idx+6]
        param[30,I] = v_curr[pij_idx+7]
    end

    if major_iter == 1
        @inbounds param[24,I] = 10.0
        mu = 10.0
    else
        @inbounds mu = param[24,I]
    end

    CUDA.sync_threads()

    eta = 1 / CUDA.pow(mu, 0.1)
    omega = 1 / mu
    max_feval = 500
    max_minor = 200
    gtol = 1e-6

    it = 0
    terminate = false

    while !terminate
        it += 1

        # Solve the branch problem.
        status, minor_iter = tron_kernel(n, 0, max_feval, max_minor, gtol, scale, false, x, xl, xu,
                                         param, YffR, YffI, YftR, YftI, YttR, YttI, YtfR, YtfI)

        @inbounds begin
            # Check the termination condition.
            cviol1 = x[1] - (YffR*x[5] + YftR*x[7] + YftI*x[8])
            cviol2 = x[2] - (-YffI*x[5] - YftI*x[7] + YftR*x[8])
            cviol3 = x[3] - (YttR*x[6] + YtfR*x[7] - YtfI*x[8])
            cviol4 = x[4] - (-YttI*x[6] - YtfI*x[7] - YtfR*x[8])
            cviol5 = x[7]^2 + x[8]^2 - x[5]*x[6]
            cviol6 = x[9] - x[10] - atan(x[8], x[7])
        end

        cnorm = max(abs(cviol1), abs(cviol2), abs(cviol3), abs(cviol4), abs(cviol5), abs(cviol6))

        if cnorm <= eta
            if cnorm <= 1e-6
                terminate = true
            else
                if tx == 1 && ty == 1
                    @inbounds begin
                        param[19,I] += mu*cviol1
                        param[20,I] += mu*cviol2
                        param[21,I] += mu*cviol3
                        param[22,I] += mu*cviol4
                        param[23,I] += mu*cviol5
                        param[31,I] += mu*cviol6
                    end
                end

                eta = eta / CUDA.pow(mu, 0.9)
                omega  = omega / mu
            end
        else
            mu = min(mu_max, mu*10)
            eta = 1 / CUDA.pow(mu, 0.1)
            omega = 1 / mu
            @inbounds param[24,I] = mu
        end

        if it >= max_auglag
            terminate = true
        end

        CUDA.sync_threads()
    end

    @inbounds begin
        u_curr[pij_idx] = x[1]
        u_curr[pij_idx+1] = x[2]
        u_curr[pij_idx+2] = x[3]
        u_curr[pij_idx+3] = x[4]
        u_curr[pij_idx+4] = x[5]
        u_curr[pij_idx+5] = x[6]
        wRIij[2*(I-1)+1] = x[7]
        wRIij[2*I] = x[8]
        u_curr[pij_idx+6] = x[9]
        u_curr[pij_idx+7] = x[10]
        param[24,I] = mu
    end

    CUDA.sync_threads()

    return
end

function auglag_kernel_cpu(n::Int, nline::Int, major_iter::Int, max_auglag::Int,
                           line_start::Int, mu_max::Float64,
                           u_curr::Array{Float64}, v_curr::Array{Float64},
                           l_curr::Array{Float64}, rho::Array{Float64},
                           wRIij::Array{Float64},
                           param::Array{Float64},
                           YffR::Array{Float64}, YffI::Array{Float64},
                           YftR::Array{Float64}, YftI::Array{Float64},
                           YttR::Array{Float64}, YttI::Array{Float64},
                           YtfR::Array{Float64}, YtfI::Array{Float64},
                           frBound::Array{Float64}, toBound::Array{Float64})
    avg_auglag_it = 0
    avg_minor_it = 0

    x = zeros(n)
    xl = zeros(n)
    xu = zeros(n)

    @inbounds for I=1:nline
        @inbounds for i=1:n
            xl[i] = -Inf
            xu[i] = Inf
        end

        xl[5] = frBound[2*(I-1)+1]
        xu[5] = frBound[2*I]
        xl[6] = toBound[2*(I-1)+1]
        xu[6] = toBound[2*I]
        xl[9] = -2*pi
        xu[9] = 2*pi
        xl[10] = -2*pi
        xu[10] = 2*pi

        pij_idx = line_start + 8*(I-1)

        x[1] = u_curr[pij_idx]
        x[2] = u_curr[pij_idx+1]
        x[3] = u_curr[pij_idx+2]
        x[4] = u_curr[pij_idx+3]
        x[5] = min(xu[5], max(xl[5], u_curr[pij_idx+4]))
        x[6] = min(xu[6], max(xl[6], u_curr[pij_idx+5]))
        x[7] = wRIij[2*(I-1)+1]
        x[8] = wRIij[2*I]
        x[9] = min(xu[9], max(xl[9], u_curr[pij_idx+6]))
        x[10] = min(xu[10], max(xl[10], u_curr[pij_idx+7]))

        param[1,I] = l_curr[pij_idx]
        param[2,I] = l_curr[pij_idx+1]
        param[3,I] = l_curr[pij_idx+2]
        param[4,I] = l_curr[pij_idx+3]
        param[5,I] = l_curr[pij_idx+4]
        param[6,I] = l_curr[pij_idx+5]
        param[7,I] = rho[pij_idx]
        param[8,I] = rho[pij_idx+1]
        param[9,I] = rho[pij_idx+2]
        param[10,I] = rho[pij_idx+3]
        param[11,I] = rho[pij_idx+4]
        param[12,I] = rho[pij_idx+5]
        param[13,I] = v_curr[pij_idx]
        param[14,I] = v_curr[pij_idx+1]
        param[15,I] = v_curr[pij_idx+2]
        param[16,I] = v_curr[pij_idx+3]
        param[17,I] = v_curr[pij_idx+4]
        param[18,I] = v_curr[pij_idx+5]

        if major_iter == 1
            param[24,I] = 10.0
            mu = 10.0
        else
            mu = param[24,I]
        end

        param[25,I] = l_curr[pij_idx+6]
        param[26,I] = l_curr[pij_idx+7]
        param[27,I] = rho[pij_idx+6]
        param[28,I] = rho[pij_idx+7]
        param[29,I] = v_curr[pij_idx+6]
        param[30,I] = v_curr[pij_idx+7]

        function eval_f_cb(x)
            f= eval_f_kernel_cpu(I, x, param, YffR, YffI, YftR, YftI, YttR, YttI, YtfR, YtfI)
            return f
        end

        function eval_g_cb(x, g)
            eval_grad_f_kernel_cpu(I, x, g, param, YffR, YffI, YftR, YftI, YttR, YttI, YtfR, YtfI)
            return
        end

        function eval_h_cb(x, mode, rows, cols, scale, lambda, values)
            eval_h_kernel_cpu(I, x, mode, scale, rows, cols, lambda, values,
                              param, YffR, YffI, YftR, YftI, YttR, YttI, YtfR, YtfI)
            return
        end

        eta = 1 / mu^0.1
        omega = 1 / mu
        max_feval = 500
        max_minor = 100
        gtol = 1e-6

        nele_hess = 33
        tron = ExaTron.createProblem(n, xl, xu, nele_hess, eval_f_cb, eval_g_cb, eval_h_cb;
                                     :tol => gtol, :matrix_type => :Dense, :max_minor => 200,
                                     :frtol => 1e-12)
        tron.x .= x
        it = 0
        avg_tron_minor = 0
        terminate = false

        while !terminate
            it += 1

            # Solve the branch problem.
            status = ExaTron.solveProblem(tron)
            x .= tron.x
            avg_tron_minor += tron.minor_iter

            # Check the termination condition.
            cviol1 = x[1] - (YffR[I]*x[5] + YftR[I]*x[7] + YftI[I]*x[8])
            cviol2 = x[2] - (-YffI[I]*x[5] - YftI[I]*x[7] + YftR[I]*x[8])
            cviol3 = x[3] - (YttR[I]*x[6] + YtfR[I]*x[7] - YtfI[I]*x[8])
            cviol4 = x[4] - (-YttI[I]*x[6] - YtfI[I]*x[7] - YtfR[I]*x[8])
            cviol5 = x[7]^2 + x[8]^2 - x[5]*x[6]
            cviol6 = x[9] - x[10] - atan(x[8], x[7])

            cnorm = max(abs(cviol1), abs(cviol2), abs(cviol3), abs(cviol4), abs(cviol5), abs(cviol6))

            if cnorm <= eta
                if cnorm <= 1e-6
                    terminate = true
                else
                    param[19,I] += mu*cviol1
                    param[20,I] += mu*cviol2
                    param[21,I] += mu*cviol3
                    param[22,I] += mu*cviol4
                    param[23,I] += mu*cviol5
                    param[31,I] += mu*cviol6

                    eta = eta / mu^0.9
                    omega  = omega / mu
                end
            else
                mu = min(mu_max, mu*10)
                eta = 1 / mu^0.1
                omega = 1 / mu
                param[24,I] = mu
            end

            if it >= max_auglag
                terminate = true
            end
        end

        avg_auglag_it += it
        avg_minor_it += (avg_tron_minor / it)
        u_curr[pij_idx] = x[1]
        u_curr[pij_idx+1] = x[2]
        u_curr[pij_idx+2] = x[3]
        u_curr[pij_idx+3] = x[4]
        u_curr[pij_idx+4] = x[5]
        u_curr[pij_idx+5] = x[6]
        wRIij[2*(I-1)+1] = x[7]
        wRIij[2*I] = x[8]
        u_curr[pij_idx+6] = x[9]
        u_curr[pij_idx+7] = x[10]
        param[24,I] = mu
    end

    return (avg_auglag_it / nline), (avg_minor_it / nline)
end


"""
    generatesignal!(signal::ReplicaSignal, t_length=signal.t_length;
                    doppler_curve=missing, doppler_t=missing)

Generates local GNSS signal using paramters defined in a `ReplicaSignal`
struct.

Generates a signal with carrier, ADC quantization, noise, and Neuman sequence.

No need to specify `isreplica`. Set `isreplica` to `true` to use alternate
method, which ignores all `Bool` flags in `signal`.

Specify `t_length` to be ≲ to `replica.t_length` for different signal generation
lengths. **NOTE:** The size of the `replica.data` array will still be the same
size. You will need to keep track of the `t_length` you passed to
`generatesignal!`.

Required Arguments:

- `signal::ReplicaSignal`: struct containing signal data and parameters

Optional Arguments:

- `t_length::Float64`: signal generation length in seconds
                       `(default = signal.t_length)`
- `doppler_curve::Vector`: Doppler frequency curve in Hz to use instead of
                           `signal.f_d` and `signal.fd_rate` parameters
                           `(default = missing)`
- `doppler_t::Vector`: time vector in seconds corresponding to `doppler_curve`
                       `(default = missing)`

Modifies in Place:

- `signal::ReplicaSignal`: generated signal data stored in `signal.data`
"""
function generatesignal!(signal::ReplicaSignal, t_length=signal.t_length;
                         doppler_curve=missing, doppler_t=missing)
    # Common parmeters used for entire signal
    prn = signal.prn
    Tsys = signal.Tsys
    CN0 = signal.CN0
    f_s = signal.f_s
    f_if = signal.f_if
    f_d = signal.f_d
    fd_rate = signal.fd_rate
    ϕ_init = signal.phi
    if ismissing(doppler_curve)
        get_ϕ(t) = 2π*(f_if + f_d + 0.5*fd_rate*t)*t + ϕ_init
        get_code_val(t) = calc_code_val(signal, t)
    else
        get_code_val, get_ϕ = get_chips_and_ϕ(signal, doppler_curve, doppler_t)
    end
    generatesignal!(signal, t_length, get_code_val, get_ϕ)
end

"""
    generatesignal!(signal::ReplicaSignal, t_length, get_code_val, get_ϕ)

Generates local GNSS signal using paramters defined in a `ReplicaSignal`
struct.

Generates a signal with carrier, ADC quantization, noise, and Neuman sequence.

No need to specify `isreplica`. Set `isreplica` to `true` to use alternate
method, which ignores all `Bool` flags in `signal`.

Specify `t_length` to be ≲ to `replica.t_length` for different signal generation
lengths. **NOTE:** The size of the `replica.data` array will still be the same
size. You will need to keep track of the `t_length` you passed to
`generatesignal!`.

`get_code_val` and `get_ϕ` are both only functions of `t`, where `t` is in
seconds. They return the current code value for all codes and carrier phase,
respectively.

Required Arguments:

- `signal::ReplicaSignal`: struct containing signal data and parameters
- `t_length::Float64`: signal generation length in seconds
- `get_code_val`: a function that calculates the current code value for a given
                  `t`
    * Required Arguments:
        - `t::Float64`: current time in seconds
- `get_ϕ`: a function that calculates the current carrier phase for a given `t`
    * Required Arguments:
        - `t::Float64`: current time in seconds

Modifies in Place and Returns:

- `signal::ReplicaSignal`: generated signal data stored in `signal.data`
"""
function generatesignal!(signal::ReplicaSignal, t_length, get_code_val, get_ϕ)
    # Common parmeters used for entire signal
    prn = signal.prn
    Tsys = signal.Tsys
    CN0 = signal.CN0
    f_s = signal.f_s
    start_t = signal.start_t
    if ~signal.signal_type.include_I && signal.signal_type.include_Q
        B = signal.signal_type.B_Q
    elseif signal.signal_type.include_I && ~signal.signal_type.include_Q
        B = signal.signal_type.B_I
    else  # assume both channels are included and determine highest bandwidth
        B = max(signal.signal_type.B_I, signal.signal_type.B_Q)
    end
    nADC = signal.nADC
    include_carrier = signal.include_carrier
    include_thermal_noise = signal.include_thermal_noise
    include_phase_noise = signal.include_phase_noise
    include_adc = signal.include_adc
    thermal_noise = signal.thermal_noise
    phase_noise = signal.phase_noise
    adc_scale = 2^(nADC-1)
    carrier_amp = sqrt(2*k*Tsys)*10^(CN0/20)  # for sinusoid
    noise_amp = sqrt(k*B*Tsys)
    N = floor(Int, t_length*f_s)
    upsample_factor = denominator(signal.code_start_idx)
    Δt = 1/f_s
    dΔt = Δt/upsample_factor
    @threads for i in 1:N
        t = calc_t_at_i(i, start_t, f_s)
        # Generate code value for given signal type
        cis_sum = complex(0.)
        if include_carrier
            for j in 0:(upsample_factor-1)
                code_val, code_ϕ = get_code_val(t+j*dΔt)
                ϕ = get_ϕ(t+j*dΔt)
                cis_sum += cis(ϕ+code_ϕ)
                # cis_sum += real(code_val)*cos(ϕ) + imag(code_val)*sin(ϕ)*1im
            end
        else
            for j in 0:(upsample_factor-1)
                code_val, code_ϕ = get_code_val(t+j*dΔt)
                cis_sum += cis(code_ϕ)
                # cis_sum += real(code_val) + imag(code_val)*1im
            end
        end
        cis_sum = cis_sum/upsample_factor
        if include_carrier & include_thermal_noise & include_phase_noise
            # Calculate code value with carrier, thermal and phase noise
            ϕ_noise = real(phase_noise[i])
            @inbounds signal.data[i] = carrier_amp * cis_sum * cis(ϕ_noise) +
                                       noise_amp * thermal_noise[i]
        elseif include_carrier & include_thermal_noise & ~include_phase_noise
            # Calculate code value with carrier and noise
            @inbounds signal.data[i] = carrier_amp * cis_sum +
                                       noise_amp * thermal_noise[i]
        elseif include_carrier & ~include_thermal_noise & include_phase_noise
            # Calculate code value with noise and no carrier
            ϕ_noise = real(phase_noise[i])
            @inbounds signal.data[i] = carrier_amp * cis_sum * cis(ϕ_noise)
        elseif include_carrier & ~include_thermal_noise & ~include_phase_noise
            # Calculate code value with carrier and no noise
            @inbounds signal.data[i] = carrier_amp * cis_sum
        else
            # Calculate code value only
            @inbounds signal.data[i] = cis_sum
        end
    end
    # Quantize signal
    if include_adc
        sigmax = sqrt(maximum(abs2, signal.data))
        @threads for i in 1:signal.sample_num
            @inbounds signal.data[i] = round(signal.data[i]*adc_scale/sigmax)
            # Check that the max positive value for I/Q channels is
            # (2^(nADC - 1) - 1)
            if real(signal.data[i]) == adc_scale
                signal.data[i] = signal.data[i] - 1
            end
            if imag(signal.data[i]) == adc_scale
                signal.data[i] = signal.data[i] - 1im
            end
        end
    end
    return signal
end

"""
    generatesignal!(signal::ReplicaSignals, t_length=signal.t_length;
                    doppler_curve=missing, doppler_t=missing)

Generates local GNSS signal using paramters defined in a `ReplicaSignals`
struct.

Generates a signal with carrier, ADC quantization, noise, and Neuman sequence.

No need to specify `isreplica`. Set `isreplica` to `true` to use alternate
method, which ignores all `Bool` flags in `signal`.

Specify `t_length` to be ≲ to `replica.t_length` for different signal generation
lengths. **NOTE:** The size of the `replica.data` array will still be the same
size. You will need to keep track of the `t_length` you passed to
`generatesignal!`.

Required Arguments:

- `signal::ReplicaSignals`: struct containing signal data and parameters

Optional Arguments:

- `t_length::Float64`: signal generation length in seconds
                       `(default = signal.t_length)`
- `doppler_curve::Vector`: Doppler frequency curve in Hz to use instead of
                           `signal.f_d` and `signal.fd_rate` parameters
                           `(default = missing)`
- `doppler_t::Vector`: time vector in seconds corresponding to `doppler_curve`
                       `(default = missing)`

Modifies in Place:

- `signal::ReplicaSignal`: generated signal data stored in `signal.data`
"""
function generatesignal!(signal::ReplicaSignals, t_length=signal.t_length;
                         doppler_curve=missing, doppler_t=missing)
    # Common parmeters used for entire signal
    prn = signal.prn
    Tsys = signal.Tsys
    CN0 = signal.CN0
    f_s = signal.f_s
    f_if = signal.f_if
    f_d = signal.f_d
    fd_rate = signal.fd_rate
    ϕ_init = signal.phi
    if ismissing(doppler_curve)
        get_ϕ(t) = 2π*(f_if + f_d + 0.5*fd_rate*t)*t + ϕ_init
        get_code_val(t) = calc_code_val(signal, t)
    else
        get_code_val, get_ϕ = get_chips_and_ϕ(signal, doppler_curve, doppler_t)
    end
    generatesignal!(signal, t_length, get_code_val, get_ϕ)
end

"""
    generatesignal!(signal::ReplicaSignals, t_length, get_code_val, get_ϕ)

Generates local GNSS signal using paramters defined in a `ReplicaSignals`
struct.

Generates a signal with carrier, ADC quantization, noise, and Neuman sequence.

No need to specify `isreplica`. Set `isreplica` to `true` to use alternate
method, which ignores all `Bool` flags in `signal`.

Specify `t_length` to be ≲ to `replica.t_length` for different signal generation
lengths. **NOTE:** The size of the `replica.data` array will still be the same
size. You will need to keep track of the `t_length` you passed to
`generatesignal!`.

`get_code_val` and `get_ϕ` are both only functions of `t`, where `t` is in
seconds. They return the current code value for all codes and carrier phase,
respectively.

Required Arguments:

- `signal::ReplicaSignals`: struct containing signal data and parameters
- `t_length::Float64`: signal generation length in seconds
- `get_code_val`: a function that calculates the current code value for a given
                  `t`
    * Required Arguments:
        - `t::Float64`: current time in seconds
- `get_ϕ`: a function that calculates the current carrier phase for a given `t`
    * Required Arguments:
        - `t::Float64`: current time in seconds

Modifies in Place and Returns:

- `signal::ReplicaSignal`: generated signal data stored in `signal.data`
"""
function generatesignal!(signal::ReplicaSignals, t_length, get_code_val, get_ϕ)
    # Common parmeters used for entire signal
    prn = signal.prn
    Tsys = signal.Tsys
    CN0 = signal.CN0
    f_s = signal.f_s
    start_t = signal.start_t
    B = signal.B
    nADC = signal.nADC
    include_carrier = signal.include_carrier
    include_thermal_noise = signal.include_thermal_noise
    include_phase_noise = signal.include_phase_noise
    include_adc = signal.include_adc
    thermal_noise = signal.thermal_noise
    phase_noise = signal.phase_noise
    adc_scale = 2^(nADC-1)
    carrier_amp = sqrt(2*k*Tsys)*10^(CN0/20)
    noise_amp = sqrt(k*B*Tsys)
    N = floor(Int, t_length*f_s)
    upsample_factor = denominator(signal.code_start_idx)
    Δt = 1/f_s
    dΔt = Δt/upsample_factor
    @threads for i in 1:N
        t = calc_t_at_i(i, start_t, f_s)
        # Generate code value for given signal type
        cis_sum = complex(0.)
        if include_carrier
            for j in 0:(upsample_factor-1)
                code_val, code_ϕ = get_code_val(t+dΔt)
                ϕ = get_ϕ(t+dΔt)
                cis_sum += cis(ϕ+code_ϕ)
            end
        else
            for j in 0:(upsample_factor-1)
                code_val, code_ϕ = get_code_val(t+dΔt)
                cis_sum += cis(code_ϕ)
            end
        end
        cis_sum = cis_sum/upsample_factor
        if include_carrier & include_thermal_noise & include_phase_noise
            # Calculate code value with carrier, thermal and phase noise
            ϕ = get_ϕ(t)
            ϕ_noise = real(phase_noise[i])
            @inbounds signal.data[i] = carrier_amp * cis_sum * cis(ϕ_noise) +
                                       noise_amp * thermal_noise[i]
        elseif include_carrier & include_thermal_noise & ~include_phase_noise
            # Calculate code value with carrier and noise
            # ϕ = get_ϕ(t)
            @inbounds signal.data[i] = carrier_amp * cis_sum +
                                       noise_amp * thermal_noise[i]
        elseif include_carrier & ~include_thermal_noise & include_phase_noise
            # Calculate code value with noise and no carrier
            ϕ_noise = real(phase_noise[i])
            @inbounds signal.data[i] = carrier_amp * cis_sum * cis(ϕ_noise)
        elseif include_carrier & ~include_thermal_noise & ~include_phase_noise
            # Calculate code value with carrier and no noise
            ϕ = get_ϕ(t)
            @inbounds signal.data[i] = carrier_amp * cis_sum
        else
            # Calculate code value only
            @inbounds signal.data[i] = cis_sum
        end
    end
    # Quantize signal
    if include_adc
        sigmax = sqrt(maximum(abs2, signal.data))
        # for i in 1:signal.sample_num
        @threads for i in 1:signal.sample_num
            @inbounds signal.data[i] = round(signal.data[i]*adc_scale/sigmax)
        end
    end
    return signal
end

"""
generatereplica!(signal::ReplicaSignal)

Generates local GNSS signal using paramters defined in a
`ReplicaSignal` struct.

Generates a signal with carrier, ADC quantization, noise,
and Neuman sequence.

This version is used only when for when this signal struct is
used as a known replica for use in course/fine acquisition and
tracking. When this function is used instead of `generatesignal!`,
all the `include_*` flags in `signal` are ignored. The bool flag,
`noexp` in `signal`, Exponential without the amplitude is included
automatically.

Required Arguments:

- `signal::ReplicaSignal`: struct containing signal data and parameters

Modifies in Place and Returns:

- `signal::ReplicaSignal`: generated signal data stored in `signal.data`
"""
function generatereplica!(signal::ReplicaSignal)
    # Common parmeters used for entire signal
    prn = signal.prn
    f_d = signal.f_d
    f_if = signal.f_if
    f_s = signal.f_s
    start_t = signal.start_t
    fd_rate = signal.fd_rate
    ϕ = signal.phi
    noexp = signal.noexp
    signal.isreplica = true
    @threads for i in 1:signal.sample_num
        t = calc_t_at_i(i, start_t, f_s)
        # Generate code value for given signal type
        code_val, code_ϕ = calc_code_val(signal, t)
        if signal.include_carrier
            # Doppler effected code is modulated onto carrier.
            # Carrier has amplitude of 1. It is not rescaled.
            ϕₜ = 2π*(f_if + f_d + 0.5*fd_rate*t)*t + ϕ
            @inbounds signal.data[i] = cis(ϕₜ + code_ϕ)
            # @inbounds signal.data[i] = real(code_val)*cos(ϕₜ) + imag(code_val)*sin(ϕₜ)*1im
        else
            # Produce the code only with no carrier modulation
            # Code is still generated with Doppler effects
            @inbounds signal.data[i] = cis(code_ϕ)
            # @inbounds signal.data[i] = real(code_val) + imag(code_val)*1im
        end
    end
    signal.isreplica = false
    return signal
end


include("DGModel_kernels.jl")

struct DGModel{BL, G, NFND, NFD, GNF, AS, DS, HDS, D, DD, MD, GF, TF} <:
       SpaceDiscretization
    balance_law::BL
    grid::G
    numerical_flux_first_order::NFND
    numerical_flux_second_order::NFD
    numerical_flux_gradient::GNF
    state_auxiliary::AS
    state_gradient_flux::DS
    states_higher_order::HDS
    direction::D
    diffusion_direction::DD
    modeldata::MD
    gradient_filter::GF
    tendency_filter::TF
    check_for_crashes::Bool
end

function DGModel(
    balance_law,
    grid,
    numerical_flux_first_order,
    numerical_flux_second_order,
    numerical_flux_gradient;
    fill_nan = false,
    check_for_crashes = false,
    state_auxiliary = create_state(
        balance_law,
        grid,
        Auxiliary(),
        fill_nan = fill_nan,
    ),
    state_gradient_flux = create_state(balance_law, grid, GradientFlux()),
    states_higher_order = (
        create_state(balance_law, grid, GradientLaplacian()),
        create_state(balance_law, grid, Hyperdiffusive()),
    ),
    direction = EveryDirection(),
    diffusion_direction = direction,
    modeldata = nothing,
    gradient_filter = nothing,
    tendency_filter = nothing,
)
    state_auxiliary =
        init_state(state_auxiliary, balance_law, grid, direction, Auxiliary())
    DGModel(
        balance_law,
        grid,
        numerical_flux_first_order,
        numerical_flux_second_order,
        numerical_flux_gradient,
        state_auxiliary,
        state_gradient_flux,
        states_higher_order,
        direction,
        diffusion_direction,
        modeldata,
        gradient_filter,
        tendency_filter,
        check_for_crashes,
    )
end

# Include the remainder model for composing DG models and balance laws
include("remainder.jl")

"""
    (dg::DGModel)(tendency, state_prognostic, _, t, α, β)

Uses spectral element discontinuous Galerkin in all direction to compute the
tendency.

Computes the tendency terms compatible with `IncrementODEProblem`

    tendency .= α .* dQdt(state_prognostic, p, t) .+ β .* tendency

The 4-argument form will just compute

    tendency .= dQdt(state_prognostic, p, t)
"""
function (dg::DGModel)(tendency, state_prognostic, _, t, α, β)

    device = array_device(state_prognostic)
    Qhypervisc_grad, Qhypervisc_div = dg.states_higher_order

    FT = eltype(state_prognostic)
    num_state_prognostic = number_states(dg.balance_law, Prognostic())
    num_state_gradient_flux = number_states(dg.balance_law, GradientFlux())
    nhyperviscstate = number_states(dg.balance_law, Hyperdiffusive())
    num_state_tendency = size(tendency, 2)

    @assert num_state_prognostic ≤ num_state_tendency

    if num_state_prognostic < num_state_tendency && β != 1
        # If we don't operate on the full state, then we need to scale here instead of volume_tendency!
        tendency .*= β
        β = β != 0 # if β==0 then we can avoid the memory load in volume_tendency!
    end

    communicate =
        !(
            isstacked(dg.grid.topology) &&
            typeof(dg.direction) <: VerticalDirection
        )

    update_auxiliary_state!(
        dg,
        dg.balance_law,
        state_prognostic,
        t,
        dg.grid.topology.realelems,
    )

    exchange_state_prognostic = NoneEvent()
    exchange_state_gradient_flux = NoneEvent()
    exchange_Qhypervisc_grad = NoneEvent()
    exchange_Qhypervisc_div = NoneEvent()

    comp_stream = Event(device)

    if communicate
        exchange_state_prognostic = MPIStateArrays.begin_ghost_exchange!(
            state_prognostic;
            dependencies = comp_stream,
        )
    end

    if num_state_gradient_flux > 0 || nhyperviscstate > 0
        ########################
        # Gradient Computation #
        ########################

        comp_stream = launch_volume_gradients!(
            dg,
            state_prognostic,
            t;
            dependencies = comp_stream,
        )

        comp_stream = launch_interface_gradients!(
            dg,
            state_prognostic,
            t;
            surface = :interior,
            dependencies = comp_stream,
        )

        if communicate
            exchange_state_prognostic = MPIStateArrays.end_ghost_exchange!(
                state_prognostic;
                dependencies = exchange_state_prognostic,
                check_for_crashes = dg.check_for_crashes,
            )

            # Update_aux may start asynchronous work on the compute device and
            # we synchronize those here through a device event.
            checked_wait(
                device,
                exchange_state_prognostic,
                nothing,
                dg.check_for_crashes,
            )
            update_auxiliary_state!(
                dg,
                dg.balance_law,
                state_prognostic,
                t,
                dg.grid.topology.ghostelems,
            )
            exchange_state_prognostic = Event(device)
        end

        comp_stream = launch_interface_gradients!(
            dg,
            state_prognostic,
            t;
            surface = :exterior,
            dependencies = (comp_stream, exchange_state_prognostic),
        )

        if dg.gradient_filter !== nothing
            comp_stream = Filters.apply_async!(
                dg.state_gradient_flux,
                1:num_state_gradient_flux,
                dg.grid,
                dg.gradient_filter;
                dependencies = comp_stream,
            )
        end

        if communicate
            if num_state_gradient_flux > 0
                exchange_state_gradient_flux =
                    MPIStateArrays.begin_ghost_exchange!(
                        dg.state_gradient_flux,
                        dependencies = comp_stream,
                    )
            end
            if nhyperviscstate > 0
                exchange_Qhypervisc_grad = MPIStateArrays.begin_ghost_exchange!(
                    Qhypervisc_grad,
                    dependencies = comp_stream,
                )
            end
        end

        if num_state_gradient_flux > 0
            # Update_aux_diffusive may start asynchronous work on the compute device
            # and we synchronize those here through a device event.
            checked_wait(device, comp_stream, nothing, dg.check_for_crashes)
            update_auxiliary_state_gradient!(
                dg,
                dg.balance_law,
                state_prognostic,
                t,
                dg.grid.topology.realelems,
            )
            comp_stream = Event(device)
        end
    end

    if nhyperviscstate > 0
        #########################
        # Laplacian Computation #
        #########################

        comp_stream = launch_volume_divergence_of_gradients!(
            dg,
            state_prognostic,
            t;
            dependencies = comp_stream,
        )

        comp_stream = launch_interface_divergence_of_gradients!(
            dg,
            state_prognostic,
            t;
            surface = :interior,
            dependencies = comp_stream,
        )

        if communicate
            exchange_Qhypervisc_grad = MPIStateArrays.end_ghost_exchange!(
                Qhypervisc_grad,
                dependencies = exchange_Qhypervisc_grad,
                check_for_crashes = dg.check_for_crashes,
            )
        end

        comp_stream = launch_interface_divergence_of_gradients!(
            dg,
            state_prognostic,
            t;
            surface = :exterior,
            dependencies = (comp_stream, exchange_Qhypervisc_grad),
        )

        if communicate
            exchange_Qhypervisc_div = MPIStateArrays.begin_ghost_exchange!(
                Qhypervisc_div,
                dependencies = comp_stream,
            )
        end

        ####################################
        # Hyperdiffusive terms computation #
        ####################################

        comp_stream = launch_volume_gradients_of_laplacians!(
            dg,
            state_prognostic,
            t,
            dependencies = (comp_stream,),
        )

        comp_stream = launch_interface_gradients_of_laplacians!(
            dg,
            state_prognostic,
            t;
            surface = :interior,
            dependencies = (comp_stream,),
        )

        if communicate
            exchange_Qhypervisc_div = MPIStateArrays.end_ghost_exchange!(
                Qhypervisc_div,
                dependencies = exchange_Qhypervisc_div,
                check_for_crashes = dg.check_for_crashes,
            )
        end

        comp_stream = launch_interface_gradients_of_laplacians!(
            dg,
            state_prognostic,
            t;
            surface = :exterior,
            dependencies = (comp_stream, exchange_Qhypervisc_div),
        )

        if communicate
            exchange_Qhypervisc_grad = MPIStateArrays.begin_ghost_exchange!(
                Qhypervisc_grad,
                dependencies = comp_stream,
            )
        end
    end

    ###################
    # RHS Computation #
    ###################
    comp_stream = launch_volume_tendency!(
        dg,
        tendency,
        state_prognostic,
        t,
        α,
        β;
        dependencies = (comp_stream,),
    )

    comp_stream = launch_interface_tendency!(
        dg,
        tendency,
        state_prognostic,
        t,
        α,
        β;
        surface = :interior,
        dependencies = (comp_stream,),
    )

    if communicate
        if num_state_gradient_flux > 0 || nhyperviscstate > 0
            if num_state_gradient_flux > 0
                exchange_state_gradient_flux =
                    MPIStateArrays.end_ghost_exchange!(
                        dg.state_gradient_flux;
                        dependencies = exchange_state_gradient_flux,
                        check_for_crashes = dg.check_for_crashes,
                    )

                # Update_aux_diffusive may start asynchronous work on the
                # compute device and we synchronize those here through a device
                # event.
                checked_wait(
                    device,
                    exchange_state_gradient_flux,
                    nothing,
                    dg.check_for_crashes,
                )
                update_auxiliary_state_gradient!(
                    dg,
                    dg.balance_law,
                    state_prognostic,
                    t,
                    dg.grid.topology.ghostelems,
                )
                exchange_state_gradient_flux = Event(device)
            end
            if nhyperviscstate > 0
                exchange_Qhypervisc_grad = MPIStateArrays.end_ghost_exchange!(
                    Qhypervisc_grad;
                    dependencies = exchange_Qhypervisc_grad,
                    check_for_crashes = dg.check_for_crashes,
                )
            end
        else
            exchange_state_prognostic = MPIStateArrays.end_ghost_exchange!(
                state_prognostic;
                dependencies = exchange_state_prognostic,
                check_for_crashes = dg.check_for_crashes,
            )

            # Update_aux may start asynchronous work on the compute device and
            # we synchronize those here through a device event.
            checked_wait(
                device,
                exchange_state_prognostic,
                nothing,
                dg.check_for_crashes,
            )
            update_auxiliary_state!(
                dg,
                dg.balance_law,
                state_prognostic,
                t,
                dg.grid.topology.ghostelems,
            )
            exchange_state_prognostic = Event(device)
        end
    end

    comp_stream = launch_interface_tendency!(
        dg,
        tendency,
        state_prognostic,
        t,
        α,
        β;
        surface = :exterior,
        dependencies = (
            comp_stream,
            exchange_state_prognostic,
            exchange_state_gradient_flux,
            exchange_Qhypervisc_grad,
        ),
    )

    # The synchronization here through a device event prevents CuArray based and
    # other default stream kernels from launching before the work scheduled in
    # this function is finished.
    if dg.tendency_filter !== nothing
        comp_stream = Filters.apply_async!(
            tendency,
            1:num_state_tendency,
            dg.grid,
            dg.tendency_filter;
            dependencies = comp_stream,
        )
    end
    checked_wait(device, comp_stream, nothing, dg.check_for_crashes)
end

function restart_ode_state(dg::DGModel, state_data; init_on_cpu = false)
    bl = dg.balance_law
    grid = dg.grid

    state = create_state(bl, grid, Prognostic())
    state .= state_data

    device = arraytype(dg.grid) <: Array ? CPU() : CUDADevice()
    event = Event(device)
    event = MPIStateArrays.begin_ghost_exchange!(state; dependencies = event)
    event = MPIStateArrays.end_ghost_exchange!(state; dependencies = event)
    wait(device, event)

    return state
end

function indefinite_stack_integral!(
    dg::DGModel,
    m::BalanceLaw,
    state_prognostic::MPIStateArray,
    state_auxiliary::MPIStateArray,
    t::Real,
    elems::UnitRange = dg.grid.topology.elems,
)

    device = array_device(state_prognostic)

    grid = dg.grid
    topology = grid.topology

    dim = dimensionality(grid)
    N = polynomialorders(grid)
    Nq = N .+ 1
    Nqj = dim == 2 ? 1 : Nq[2]

    FT = eltype(state_prognostic)

    # Compute integrals
    nelem = length(elems)
    nvertelem = topology.stacksize
    horzelems = fld1(first(elems), nvertelem):fld1(last(elems), nvertelem)

    event = Event(device)
    event = kernel_indefinite_stack_integral!(device, (Nq[1], Nqj))(
        m,
        Val(dim),
        Val(N),
        Val(nvertelem),
        state_prognostic.data,
        state_auxiliary.data,
        grid.vgeo,
        # Only need the vertical Imat since this kernel is vertically oriented
        grid.Imat[dim],
        horzelems;
        ndrange = (length(horzelems) * Nq[1], Nqj),
        dependencies = (event,),
    )
    checked_wait(device, event, nothing, dg.check_for_crashes)
end

function reverse_indefinite_stack_integral!(
    dg::DGModel,
    m::BalanceLaw,
    state_prognostic::MPIStateArray,
    state_auxiliary::MPIStateArray,
    t::Real,
    elems::UnitRange = dg.grid.topology.elems,
)

    device = array_device(state_auxiliary)

    grid = dg.grid
    topology = grid.topology

    dim = dimensionality(grid)
    N = polynomialorders(grid)
    Nq = N .+ 1
    Nqj = dim == 2 ? 1 : Nq[2]

    FT = eltype(state_auxiliary)

    # Compute integrals
    nelem = length(elems)
    nvertelem = topology.stacksize
    horzelems = fld1(first(elems), nvertelem):fld1(last(elems), nvertelem)

    event = Event(device)
    event = kernel_reverse_indefinite_stack_integral!(device, (Nq[1], Nqj))(
        m,
        Val(dim),
        Val(N),
        Val(nvertelem),
        state_prognostic.data,
        state_auxiliary.data,
        horzelems;
        ndrange = (length(horzelems) * Nq[1], Nqj),
        dependencies = (event,),
    )
    checked_wait(device, event, nothing, dg.check_for_crashes)
end


function set_bounds(info::Union{VariableInfo, ConstraintInfo}, set::MOI.EqualTo)
    set_lower_bound(info, set.value)
    set_upper_bound(info, set.value)
end

function set_bounds(info::Union{VariableInfo, ConstraintInfo}, set::MOI.GreaterThan)
    set_lower_bound(info, set.lower)
end

function set_bounds(info::Union{VariableInfo, ConstraintInfo}, set::MOI.LessThan)
    set_upper_bound(info, set.upper)
end

function set_bounds(info::Union{VariableInfo, ConstraintInfo}, set::MOI.Interval)
    set_lower_bound(info, set.lower)
    set_upper_bound(info, set.upper)
end

MOI.supports_constraint(::Optimizer, ::Type{SV}, ::Type{<:Bounds}) = true

function MOI.add_constraint(model::Optimizer, f::SV, set::S) where {S <: Bounds}
    variable_info = find_variable_info(model, f.variable)
    set_bounds(variable_info, set)
    return CI{SV, S}(f.variable.value)
end

MOI.supports_constraint(::Optimizer, ::Type{<:Union{SAF, SQF}}, ::Type{<:Bounds}) = true

function MOI.add_constraint(model::Optimizer, f::F, set::S) where {F <: Union{SAF, SQF}, S <: Bounds}
    ci = ConstraintInfo()
    ci.expression = to_expr(f)
    set_bounds(ci, set)
    push!(model.inner.constraint_info, ci)
    return CI{F, S}(length(model.inner.constraint_info))
end

MOI.supports(::Optimizer, ::MOI.ConstraintName, ::Type{CI}) = true

function MOI.set(model::Optimizer, attr::MOI.ConstraintName, ci::CI{SV}, value)
    error("No support for naming constraints imposed on variables.")
end
function MOI.set(model::Optimizer, attr::MOI.ConstraintName, ci::CI, value)
    check_constraint_indices(model, ci)
    model.inner.constraint_info[ci.value].name = value
end
function MOI.get(model::Optimizer, ::MOI.ConstraintName, ci::CI)
     return model.inner.constraint_info[ci.value].name
end

function MOI.get(model::Optimizer, ::Type{MathOptInterface.ConstraintIndex}, name::String)
    for (i,c) in enumerate(model.inner.constraint_info)
        if name == c.name
            return CI(i)
        end
    end
    error("Unrecognized constraint name $name.")
end

MOI.supports_constraint(::Optimizer, ::Type{SV}, ::Type{MOI.ZeroOne}) = true
MOI.supports_constraint(::Optimizer, ::Type{SV}, ::Type{MOI.Integer}) = true

function MOI.add_constraint(model::Optimizer, f::SV, set::S) where {S <: Union{MOI.ZeroOne, MOI.Integer}}
    variable_info = find_variable_info(model, f.variable)
    if set isa MOI.ZeroOne
        variable_info.category = :Bin
    elseif set isa MOI.Integer
        variable_info.category = :Int
    else
        error("Unsupported variable type $set.")
    end
    return CI{SV, S}(f.variable.value)
end

# MOI.supports(::Optimizer, ::MOI.NLPBlock) = true

# function MOI.set(model::Optimizer, ::MOI.NLPBlock, nlp_block_data::MOI.NLPBlockData)
#     model.nlp_block_data = nlp_block_data
#     return
# end


using AsmMacro, Test, InteractiveUtils

@testset "@asm" begin
    @asm function add_loop_vec4(x::Ptr{Float64},n::Int,z::Ptr{Float64})
        movq(n, rcx)
        movapd(x[0*16], xmm0)
        movapd(x[1*16], xmm1)
        xorpd(xmm2,xmm2)
        xorpd(xmm3,xmm3)
        @loop
        addpd(xmm0,xmm2)
        addpd(xmm1,xmm3)
        dec(rcx)
        jnz(@loop)
        movapd(xmm2, z[0*16])
        movapd(xmm3, z[1*16])
    end

    x = [1.0,2.0,3.0,4.0]
    n = 10
    z = similar(x)
    add_loop_vec4(pointer(x),n,pointer(z))
    @test z == x*n
    @test occursin("\${:uid}", sprint(code_llvm, add_loop_vec4, (Ptr{Float64}, Int, Ptr{Float64})))

    @asm function add(z::Ptr{Int64}, x::Int64, y::Int64)
        addq(x, y)
        movq(y, z[0])
    end
    z = Int64[100]
    add(pointer(z), Int64(1), Int64(2))
    @test z[1] === Int64(3)

    @asm function add(z::Ptr{Int32}, x::Int32, y::Int32)
        addl(x, y)
        movl(y, z[0])
    end
    z = Int32[100]
    add(pointer(z), Int32(1), Int32(2))
    @test z[1] === Int32(3)

    @asm function add(z::Ptr{Float64}, x::Float64, y::Float64)
        vaddsd(xmm0, xmm1, xmm1)
        movq(xmm1, z[0])
    end
    z = Float64[100]
    add(pointer(z), Float64(1), Float64(2))
    @test z[1] === Float64(3.0)

    @asm function add(z::Ptr{Float32}, x::Float32, y::Float32)
        vaddss(xmm0, xmm1, xmm1)
        movq(xmm1, z[0])
    end
    z = Float32[100]
    add(pointer(z), Float32(1), Float32(2))
    @test z[1] === Float32(3.0)

    @test_throws Any (@eval @asm function add(x::Float64)
        ___
    end)

    @test_throws Any (@eval @asm function add(x::Float64)
        x :: 1
    end)

    @test_throws Any (@eval @asm function add(x::Float64)
                          foo(:: 1)
    end)

    @test_throws Any (@eval @asm function (x::Float64)
                          foo(:: 1)
    end)

    @test_throws Any (@eval @asm function f(x)
                          foo(:: 1)
    end)
end


module DiskArrayTools
import DiskArrays: AbstractDiskArray, eachchunk, haschunks, Chunked,
estimate_chunksize, GridChunks, findints, readblock!, writeblock!
using Interpolations
using IterTools: imap
using Base.Iterators: product
using OffsetArrays: OffsetArray

#Define a new chunk grid type for irregular, but still block-shaped grid
struct IrregularGridChunks{N} <: AbstractArray{CartesianIndices{N,NTuple{N,UnitRange{Int}}},N}
    cs::NTuple{N,Vector{UnitRange{Int}}}
    s::NTuple{N,Int}
end
Base.size(cs::IrregularGridChunks) = cs.s
Base.size(cs::IrregularGridChunks,i) = cs.s[i]
function IrregularGridChunks(cs::Vector{UnitRange{Int}}...)
    s = length.(cs)
    IrregularGridChunks((cs...,),s)
end
Base.eltype(::IrregularGridChunks{N}) where N = CartesianIndices{N,NTuple{N,UnitRange{Int}}}
function Base.show(io::IO,g::IrregularGridChunks)
    stot = last.(g.cs)
    griddes = join(string.(g.s), "x")
    sizedes = join(string.(stot), "x")
    print(io,"Irregular chunk grid of size $griddes over DiskArray of size $stot")
end
function Base.getindex(cs::IrregularGridChunks{N},i::Vararg{Int, N}) where N
    rout = getindex.(cs.cs,i)
    CartesianIndices(rout)
end

export DiskArrayStack, diskstack, ConcatDiskArray, CFDiskArray
struct DiskArrayStack{T,N,M,NO}<:AbstractDiskArray{T,N}
    arrays::Array{M,NO}
end
function diskstack(a::Array{M,N}) where {N,M<:AbstractArray{T,NO}} where {T,NO}
    length(unique(size.(a))) == 1 || error("All arrays in the stack must have the same size")
    DiskArrayStack{T,N+NO,M,N}(a)
end
Base.size(r::DiskArrayStack) = (size(r.arrays[1])...,size(r.arrays)...)
haschunks(a::DiskArrayStack) = haschunks(a.arrays[1])
iscompressed(a::DiskArrayStack) = any(iscompressed,a.arrays)
function eachchunk(a::DiskArrayStack{<:Any,<:Any,<:Any,NO}) where NO
    iterold = eachchunk(a.arrays[1])
    cs = (iterold.chunksize...,ntuple(one,NO)...)
    co = (iterold.offset...,ntuple(zero,NO)...)
    GridChunks(a,cs,offset=co)
end

function readblock!(a::DiskArrayStack{<:Any,N,<:Any,NO},aout,i::AbstractVector...) where {N,NO}

  innerinds = i[1:(N-NO)]

  outerinds = i[(N-NO+1):N]
  innercolon = map(_->(:), innerinds)
  iiter = CartesianIndices(outerinds)
  inum  = CartesianIndices(size(iiter))
  foreach(zip(iiter,inum)) do (iouter,iret)
    arnow = a.arrays[iouter]
    if isa(arnow, AbstractDiskArray)
      readblock!(a.arrays[iouter],view(aout,innercolon...,iret.I...),innerinds...)
    else
      aout[innercolon...,iret.I...] = arnow[innerinds...]
    end
  end
  nothing
end

function writeblock!(a::DiskArrayStack{<:Any,N,<:Any,NO},v,i::AbstractVector...) where {N,NO}
  innerinds = i[1:(N-NO)]

  outerinds = i[(N-NO+1):N]
  innercolon = map(_->(:), innerinds)
  iiter = CartesianIndices(outerinds)
  inum  = CartesianIndices(size(iiter))
  foreach(zip(iiter,inum)) do (iouter,iret)
    arnow = a.arrays[iouter]
    if isa(arnow, AbstractDiskArray)
      writeblock!(a.arrays[iouter],view(v,innercolon...,iret.I...),innerinds...)
    else
      arnow[innerinds...] = v[innercolon...,iret.I...]
    end
  end
  nothing
end

function Base.view(a::DiskArrayStack{<:Any,N,<:Any,NO},i...) where {N,NO}
    iinner = i[1:(N-NO)]
    ashort = map(view(a.arrays,i[(N-NO+1):N]...)) do ai
        view(ai,iinner...)
    end
    if ndims(ashort)==0
      ashort[]
    else
      diskstack(ashort)
    end
end

abstract type ResampledDiskArray{T,N} <: AbstractDiskArray{T,N} end

function readblock!(a::ResampledDiskArray, aout, i::AbstractUnitRange...)
  parentranges = map((ir,ip)->ip[ir],i,a.newinds)
  rr = get_readinds(a,parentranges)
  atemp = a.a[rr...]
  atemp2 = OffsetArray(atemp,map(r->(first(r)-1),rr))
  resample_disk(a,aout,atemp2,parentranges)
end

struct InterpolatedDiskArray{T,N,A<:AbstractArray{T,N},I,O,BC,CS<:Union{Nothing,GridChunks{N}}} <: ResampledDiskArray{T,N}
    a::A
    newinds::I
    meth::O
    bc::BC
    chunksize::CS
end
fixin(i,s) = max(1,min(s,i))
function round_readinds(s,r,an)
  mi,ma = extrema(r)
  if mi == ma
    m = fixin(round(Int,mi),s)
    m:m
  else
    a = if an
      (round(Int,mi,RoundNearestTiesUp), -round(Int,-ma,RoundNearestTiesUp))
    else
      (floor(Int,mi),ceil(Int,ma))
    end
    fixin(a[1],s):fixin(a[2],s)
  end
end

function get_readinds(a::InterpolatedDiskArray,r)
  allnearest = all(i->isa(i,Union{Nothing,BSpline{Constant}}),a.meth)
  round_readinds.(size(a.a),r,allnearest)
end

allmeths(order::Tuple,newinds) = map(order,newinds) do o,ni
    ni === nothing ? NoInterp() : BSpline(o)
end
function allmeths(order, newinds)
    allorders = map(newinds) do _
        order
    end
    allmeths(allorders,newinds)
end
function InterpolatedDiskArray(a::AbstractArray,chunksize,newinds...; order=Linear(), bc=Flat())
    eltype(a) <: Union{Missing,Real,Complex} || error("Can only interpolate real or complex values")
    s = size(a)
    ni2 = map((s,i)->i===nothing ? (1:s) : i,s,newinds)
    me = allmeths(order,newinds)
    InterpolatedDiskArray(a,ni2,me,bc,chunksize)
end
Base.size(a::InterpolatedDiskArray) = map(length,a.newinds)
haschunks(a::InterpolatedDiskArray{<:Any,<:Any,<:Any,<:Any,<:Any,<:Any,<:GridChunks}) = true
haschunks(a::InterpolatedDiskArray{<:Any,<:Any,<:Any,<:Any,<:Any,<:Any,Nothing}) = false
eachchunk(a::InterpolatedDiskArray{<:Any,<:Any,<:Any,<:Any,<:Any,<:Any,<:GridChunks}) = a.chunksize

function resample_disk(a::InterpolatedDiskArray,aout,atemp,parentranges)
  meth = map((s,m)->s==1 ? NoInterp() : m,size(atemp),a.meth)
  if all(isequal(NoInterp()),meth)
      aout .= atemp.parent
  else
      fremap(aout,atemp,parentranges,meth,bc=a.bc)
  end
end

function fremap(xout,xin,newinds,ipmeth;bc = Flat())
  interp = extrapolate(interpolate(xin, ipmeth), bc)
  for (i,ic) in enumerate(Iterators.product(newinds...))
    xout[i]=interp(ic...)
  end
end

#Use of Sentinel missing value
struct CFDiskArray{T,N,ST,P<:AbstractArray{ST,N}} <: AbstractDiskArray{Union{T,Missing},N}
    a::P
    mv::ST
    add_offset::T
    scale_factor::T
end
function CFDiskArray(a::AbstractArray{T}, attr::Dict) where T
  mv = get(attr,"missing_value",get(attr,"_FillValue",typemax(T)))
  offs,sc = if haskey(attr,"add_offset") || haskey(attr,"scale_factor")
    offs = get(attr,"add_offset",zero(Float16))
    sc = get(attr,"scale_factor",one(Float16))
    promote(offs,sc)
  else
    zero(T), one(T)
  end
  CFDiskArray(a, T(mv), offs, sc)
end

Base.size(a::CFDiskArray, args...) = size(a.a, args...)
haschunks(a::CFDiskArray) = haschunks(a.a)
eachchunk(a::CFDiskArray) = eachchunk(a.a)
iscompressed(a::CFDiskArray) = iscompressed(a.a)

function readblock!(a::CFDiskArray, aout, r::AbstractVector...)
    mv = a.mv
    sc,offs = a.scale_factor, a.add_offset
    broadcast!(j->scaleoffs(j,mv,sc,offs), aout, view(a.a,r...))
    nothing
end
scaleoffs(x,mv,sc,offs) = x==mv ? missing : x*sc+offs
function writeblock!(a::CFDiskArray, v, r::AbstractVector...)
    mv = a.mv
    sc,offs = a.scale_factor, a.add_offset
    broadcast!(j->scaleoffsinv(j,mv,sc,offs), view(a.a,r...), v)
    nothing
end
scaleoffsinv(x,mv::Integer,sc,offs) = ismissing(x) ? mv : round(typeof(mv),(x-offs)/sc)
scaleoffsinv(x,mv,sc,offs) = ismissing(x) ? mv : ((x-offs)/sc)

struct ConcatDiskArray{T,N,P} <: AbstractDiskArray{T,N}
    parents::P
    startinds::NTuple{N,Vector{Int}}
    size::NTuple{N,Int}
end

function ConcatDiskArray(arrays::AbstractArray)
    #First do some consistency checks
    arrinfo = map(arrays) do a
        eltype(a), size(a)
    end
    et = first(arrinfo)[1]
    nd = length(first(arrinfo)[2])
    sa = size.(arrays)
    all(i->i[1]==et,arrinfo) || error("Arrays don't have the same element type")
    all(i->length(i[2])==nd, arrinfo) || error("Arrays don't have the same dimensions")
    if ndims(arrays)<nd
        arrays = reshape(arrays,size(arrays)...,ones(Int,nd-ndims(arrays))...)
    end
    function othersize(x,id)
        (x[1:id-1]..., x[id+1:end]...)
    end
    si = map(1:nd) do id
        otherdims = ((1:id-1)...,(id+1:nd)...)
        a = size.(arrays)
        a = reduce(a, dims=id, init=ntuple(zero,nd)) do i,j
                if all(iszero,i)
                    j
                elseif othersize(i,id)==othersize(j,id)
                    j
                else
                    error("Dimensions don't match")
                end
        end
        I = fill!(Array{Union{Int,Colon},1}(undef,nd),1)
        I[id] = Colon()
        ar = sa[I...]
        ari = map(i->i[id],ar)
        sl = sum(ari)
        r = cumsum(ari)
        pop!(pushfirst!(r,0))
        r.+1, sl
    end
    ConcatDiskArray{et,nd,typeof(arrays)}(arrays, (getindex.(si,1)...,),(getindex.(si,2)...,))
end

Base.size(a::ConcatDiskArray) = a.size
haschunks(::ConcatDiskArray) = Chunked()
function readblock!(a::ConcatDiskArray, aout, inds::AbstractUnitRange...)
    #Find affected blocks and indices in blocks
    blockinds  = map(inds, a.startinds, size(a.parents)) do i, si, s
        bi1 = max(searchsortedlast(si,first(i)),1)
        bi2 = min(searchsortedfirst(si,last(i)+1)-1,s)
        bi1:bi2
    end
    map(CartesianIndices(blockinds)) do cI
        myar = a.parents[cI]
        mysize = size(myar)
        array_range = map(cI.I, a.startinds, mysize, inds) do ii, si, ms, indstoread
            max(first(indstoread)-si[ii]+1,1) : min(last(indstoread)-si[ii]+1, ms)
        end
        outer_range = map(cI.I, a.startinds, array_range, inds) do ii, si, ar, indstoread
            (first(ar)+si[ii]-first(indstoread)):(last(ar)+si[ii]-first(indstoread))
        end
        aout[outer_range...] .= a.parents[cI][array_range...]
    end
end
function writeblock!(a::ConcatDiskArray, aout, inds::AbstractUnitRange...)
  error("No method yet for writing into a ConcatDiskArray")
end

function eachchunk_fallback(aconc::ConcatDiskArray)
  nested = imap(zip(aconc.parents, Iterators.product(aconc.startinds...))) do (par, si)
      imap(eachchunk(par)) do cI
          cI .+ CartesianIndex(si.-1)
      end
  end
  Iterators.flatten(nested)
end

#This function will work for DiskArrayStack as well, we only need a fallback_chunksize function
function eachchunk(aconc::ConcatDiskArray)
  fbchunks = eachchunk_fallback(aconc)
  nd = ndims(first(fbchunks))
  dout = ntuple(_->Dict{NTuple{nd-1,UnitRange{Int}},Set{UnitRange{Int}}}(),nd)
  foreach(fbchunks) do c
    map(enumerate(c.indices)) do (ind,r)
        other = (c.indices[1:ind-1]...,c.indices[ind+1:end]...)
        s = get!(dout[ind],other) do
            Set{UnitRange{Int}}()
        end
        push!(s,c.indices[ind])
    end
  end
  #First check if all
  uniqueranges = unique.(values.(dout)) # Here we give up
  if any(length.(uniqueranges) .> 1)
    return fbchunks
  end
  simplifyaxes = map(uniqueranges) do ur
    allr = first(ur)
    allr = sort!(collect(allr),by=first)
    l = length.(allr)
    if length(l)==1
        allr,(cs = l[1], offs = 0, l = l[1])
    elseif all(isequal(l[2]),l[2:end-1]) && l[end]<=l[2]
        allr,(cs = l[2], offs = l[2]-l[1], l = allr[end][end])
    else
        allr,nothing
    end
  end
  if all(!isnothing,getindex.(simplifyaxes,2))
    grid = getindex.(simplifyaxes,2)
    cs,offs,l = getproperty.(grid,:cs),getproperty.(grid,:offs),getproperty.(grid,:l)
    return GridChunks(l,cs, offset = offs)
  else
    return IrregularGridChunks(getindex.(simplifyaxes,1)...)
  end
end
end # module


### Deprecated while v0.7 was latest

function ∇softmax(Δ, x; dims = 1)
    # This 2-arg version recomputes the forward pass, which is slow.
    # Removed from use in 0.7, but only prints a warning during 0.8:
    Base.depwarn("`∇softmax(Δ, x)` without `y = softmax(x)` argument is deprecated, as this is inefficient, please use `∇softmax_data(dy, y)`", :∇softmax)
    ∇softmax(Δ, x, softmax(x; dims); dims)
end
∇softmax!(Δ, x; dims = 1) = Δ .= ∇softmax(Δ, x; dims)
∇softmax!(out, Δ, x; dims = 1) = out .= ∇softmax(Δ, x; dims)

function ∇logsoftmax(Δ, x; dims = 1)
    Base.depwarn("`∇logsoftmax(Δ, x)` without `y = logsoftmax(x)` argument is deprecated, please use `∇logsoftmax_data(dy, y)`", :∇logsoftmax)
    ∇logsoftmax(Δ, x, logsoftmax(x; dims); dims)
end
∇logsoftmax!(Δ, x; dims = 1) = Δ .= ∇logsoftmax(Δ, x; dims)
∇logsoftmax!(out, Δ, x; dims = 1) = out .= ∇logsoftmax(Δ, x; dims)

### Deprecated while v0.8 was latest

export ∇softmax,
    ∇softmax!,
    logsoftmax,
    logsoftmax!,
    ∇logsoftmax,
    ∇logsoftmax!

function ∇softmax!(out::AbstractArray, Δ::AbstractArray,
                    x::AbstractArray, y::AbstractArray; dims = 1)
    Base.depwarn("`∇softmax!(dx, dy, x, y)` is deprecated, just use `∇softmax_data(dy, y)`", :∇softmax!)
    # Removed because using a mutating function blocks 2nd derivatives, and
    # the CUDA overload was slow anyway, https://github.com/FluxML/NNlibCUDA.jl/issues/30
    out .= Δ .* y
    out .= out .- y .* sum(out; dims)
end

function ∇logsoftmax!(out::AbstractArray, Δ::AbstractArray,
                    x::AbstractArray, y::AbstractArray; dims = 1)
    Base.depwarn("`∇logsoftmax!(dx, dy, x, y)` is deprecated, just use `∇logsoftmax_data(dy, y)`", :∇softmax!)
    out .= Δ .- sum(Δ; dims) .* exp.(y)
end

function ∇softmax(dy::AbstractArray{T}, x::AbstractArray, y::AbstractArray{S}; dims = 1) where {T,S}
    # Removed because there's no need to close over `x` here, that was done only to distinguish
    # this from `∇softmax(Δ, x; dims = 1)` which re-computed `y = softmax(x)`, which is slow.
    Base.depwarn("`∇softmax(dy, x, y)` should be replaced with `∇softmax_data(dy, y)`", :∇softmax)
    ∇softmax_data(dy, y)
end

function ∇logsoftmax(dy::AbstractArray, x::AbstractArray, y::AbstractArray; dims = 1)
    Base.depwarn("`∇logsoftmax(dy, x, y)` should be replaced with `∇logsoftmax_data(dy, y)`", :∇softmax)
    ∇logsoftmax_data(dy, y)
end


"""
Author: Mohammad Dehghani Ashkezari <mdehghan@uw.edu>

Date: 2020-05-08

Function: implemantation of matching (colocalizing) data sets.
"""

using Dates
include("./rest.jl")

"""
    _atomic_match(match::Match)

Colocalizes the source variable (from source table) with a single target variable (from target table).
The tolerance parameters set the matching boundaries between the source and target data sets.
Returns a dataframe containing the source variable joined with the target variable.
This function is meant to be used internally.

Examples
≡≡≡≡≡≡≡≡≡≡

_atomic_match(
             "uspMatch",
             "tblKM1314_Cobalmins",
             "Me_PseudoCobalamin_Particulate_pM",
             "tblDarwin_Phytoplankton",
             "picoprokaryote",
             "2013-08-11",
             "2013-09-05",
             22.25,
             450.25,
             -159.25,
             -127.75,
             -5,
             305,
             1,
             0.25,
             0.25,
             5
             );
"""
function _atomic_match(
                       spName::String,
                       sourceTable::String,
                       sourceVariable::String,
                       targetTable::String,
                       targetVariable::String,
                       dt1::String,
                       dt2::String,
                       lat1::Number,
                       lat2::Number,
                       lon1::Number,
                       lon2::Number,
                       depth1::Number,
                       depth2::Number,
                       timeTolerance::Number,
                       latTolerance::Number,
                       lonTolerance::Number,
                       depthTolerance::Number
                      )

    statement = """
                EXEC $spName
                    '$sourceTable',
                    '$sourceVariable',
                    '$targetTable',
                    '$targetVariable',
                    '$dt1',
                    '$dt2',
                    '$lat1',
                    '$lat2',
                    '$lon1',
                    '$lon2',
                    '$depth1',
                    '$depth2',
                    '$timeTolerance',
                    '$latTolerance',
                    '$lonTolerance',
                    '$depthTolerance'
                """
    api = API();
    status, response = query(api, statement);
    return response
end

"""
Loops through the target data sets and match them with the source data set according to the the accosiated tolerance parameters.
Returns a compiled dataframe of the source and matched target data sets.
The matching results rely on the spatio-temporal tolerance parameters.
Notice the source has to be a single variable and cannot be a climatological variable.
You may pass empty string ('') as source variable if you only want to get the time and location info from the source table.
The target variables (one or more) are matched with the source variable, if any match exists.
Please note that the number of matching entries between each target variable and the source variable might vary depending on the temporal and spatial resolution of the target variable.

# Arguments
- `spname::String`: stored procedure name that executes the matching logic.
- `sourceTable::String`: table name of the source data set.
- `sourceVariable::String`: the source variable. The target variables are matched (colocalized) with this variable.
- `targetTables::Array`: table names of the target data sets to be matched with the source data.
- `targetVariables::Array`: variable names to be matched with the source variable.
- `dt1::String`: start date or datetime.
- `dt2::String`: end date or datetime.
- `lat1::Number`: start latitude [degree N]. This parameter sets the lower bound of the meridional cut. Note latitude ranges from -90 to 90.
- `lat2::Number`: end latitude [degree N]. This parameter sets the upper bound of the meridional cut. Note latitude ranges from -90 to 90.
- `lon1::Number`: start longitude [degree E]. This parameter sets the lower bound of the zonal cut. Note latitude ranges from -180 to 180.
- `lon2::Number`: end longitude [degree E]. This parameter sets the upper bound of the zonal cut. Note latitude ranges from -180 to 180.
- `depth1::Number`: start depth [m]. This parameter sets the lower bound of the vertical cut. Note depth is a positive number (it is 0 at surface and grows towards ocean floor).
- `depth2::Number`: end depth [m]. This parameter sets the upper bound of the vertical cut. Note depth is a positive number (it is 0 at surface and grows towards ocean floor).
- `timeTolerance::Number`: integer array of temporal tolerance values between pairs of source and target datasets. The size and order of values in this array should match those of targetTables. This parameter is in day units except when the target variable represents monthly climatology data in which case it is in month units. Note fractional values are not supported in the current version.
- `latTolerance::Number`: float array of spatial tolerance values in meridional direction [deg] between pairs of source and target data sets.
- `lonTolerance::Number`: float array of spatial tolerance values in zonal direction [deg] between pairs of source and target data sets.
- `depthTolerance::Number`: float array of spatial tolerance values in vertical direction [m] between pairs of source and target data sets.

"""
function compile(;
                 spname::String,
                 sourceTable::String,
                 sourceVariable::String,
                 targetTables::Array,
                 targetVariables::Array,
                 dt1::String,
                 dt2::String,
                 lat1::Number,
                 lat2::Number,
                 lon1::Number,
                 lon2::Number,
                 depth1::Number,
                 depth2::Number,
                 timeTolerance::Array,
                 latTolerance::Array,
                 lonTolerance::Array,
                 depthTolerance::Array
                )

    function shift_dt(dt::String, delta::Int)
        dtFormat = "yyyy-mm-dd";
        if occursin("T", dt)
            dt = replace(dt, "T" => " ");
            if occursin(".000Z", dt)
                dt = replace(dt, ".000Z" => "");
            end
            dtFormat = "yyyy-mm-dd HH:MM:SS";
        end
        dt = DateTime(dt, dtFormat);
        dt = dt + Dates.Day(delta);
        # TODO: Handle monthly climatology data sets
        shifted = Dates.format(dt, "yyyy-mm-dd HH:MM:SS");
    end

    df = DataFrame();
    for i =  1:length(targetTables)
        data = _atomic_match(
                            spname,
                            sourceTable,
                            sourceVariable,
                            targetTables[i],
                            targetVariables[i],
                            shift_dt(dt1, -timeTolerance[i]),
                            shift_dt(dt2, timeTolerance[i]),
                            lat1 - latTolerance[i],
                            lat2 + latTolerance[i],
                            lon1 - latTolerance[i],
                            lon2 + latTolerance[i],
                            depth1 - depthTolerance[i],
                            depth2 + depthTolerance[i],
                            timeTolerance[i],
                            latTolerance[i],
                            lonTolerance[i],
                            depthTolerance[i]
                            )
        if nrow(data) < 1
            println("$i: No matching entry associated with $(targetVariables[i]).")
            continue
        end
        println("$i: $(targetVariables[i]) matched.")

        if nrow(df) < 1
            df = data;
        elseif (
                df[!, names(df)[1]] == data[!, names(df)[1]] &&
                df.lat == data.lat &&
                df.lon == data.lon
                )

            df[!, Symbol(targetVariables[i])] = data[!, Symbol(targetVariables[i])]
            df[!, Symbol(targetVariables[i]*"_std")] = data[!, Symbol(targetVariables[i]*"_std")]
        else
            println("The matched dataframe corresponding to $(targetVariables[i]) does not have the same size as the first targert variable. Please change the tolerance parameters.")
        end
    end
    return df
end


const HUMAN1_PUBLICATION_PROJ_NAME = "Human1_Publication_Data_Scripts"
const HUMAN1_PROBLICATION_ZENODO_LINK = "https://zenodo.org/record/3583004/files/Human1_Publication_Data_Scripts.zip?download=1"

# Some usful iders
const BIOMASS_IDER = "biomass_human"
const COST_IDER = "prot_pool_exchange"
const ATPM_IDER = "HMR_3964";
const EXCH_SUBSYS_HINT = "Exchange/demand"

# Human1
const CELL_NAMES = ["HS_578T", "RPMI_8226", "HT29", "MALME_3M", "SR",
               "UO_31", "MDMAMB_231", "HOP62", "NCI_H226", "HOP92", "O_786"];
const CONSTRAINTS_NAMES = ["media", "glucose", "L-lactate", "threonine"];
const CONSTRAINTS_LEVELS = [0, 1, 2, 3]

# Common constants
const MAX_ABS_BOUND = 100
const MAX_CONC = 99999 # Inf conc means that the metabolite will be never limiting the growth
const ZEROTH = 1e-8

const MODEL_TYPE_KEY = :model
const MODEL_TYPE_SUFFIX = "__Model"
const EC_MODEL_TYPE_KEY = :ecModel
const EC_MODEL_TYPE_SUFFIX = "__ecModel"


using FactCheck, Images, Color, FixedPointNumbers, TestImages

facts("OS X reader") do
    context("Autumn leaves") do
        img = testimage("autumn_leaves")
        @fact colorspace(img) --> "RGBA"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> AlphaColorValue{RGB{Ufixed16}, Ufixed16}
    end
    context("Camerman") do
        img = testimage("cameraman")
        @fact colorspace(img) --> "Gray"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> Gray{Ufixed8}
    end
    context("Earth Apollo") do
        img = testimage("earth_apollo17")
        @fact colorspace(img) --> "RGB4"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> RGB4{Ufixed8}
    end
    context("Fabio") do
    img = testimage("fabio")
        @fact colorspace(img) --> "Gray"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> Gray{Ufixed8}
    end
    context("House") do
        img = testimage("house")
        @fact colorspace(img) --> "GrayAlpha"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> AlphaColorValue{Gray{Ufixed8}, Ufixed8}
    end
    context("Jetplane") do
        img = testimage("jetplane")
        @fact colorspace(img) --> "GrayAlpha"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> AlphaColorValue{Gray{Ufixed8}, Ufixed8}
    end
    context("Lighthouse") do
        img = testimage("lighthouse")
        @fact colorspace(img) --> "RGB4"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> RGB4{Ufixed8}
    end
    context("Mandrill") do
        img = testimage("mandrill")
        @fact colorspace(img) --> "RGB"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> RGB{Ufixed8}
    end
    context("Moonsurface") do
        img = testimage("moonsurface")
        @fact colorspace(img) --> "Gray"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> Gray{Ufixed8}
    end
    context("Mountainstream") do
        img = testimage("mountainstream")
        @fact colorspace(img) --> "RGB4"
        @fact ndims(img) --> 2
        @fact colordim(img) --> 0
        @fact eltype(img) --> RGB4{Ufixed8}
    end
end


function training_from_config(filepath)

    (vid_name,frame_list,woi,pad_pos,training_path,epochs,data_path,nn,max_frames) = load_config(filepath)

    set_up_training(nn,vid_name,max_frames,woi,pad_pos,frame_list) #heatmaps, labels, normalize, augment
    save_training(training_path,frame_list,woi,nn)

    if !nn.use_existing_weights
        create_new_weights(han.nn)
    end

    if size(nn.labels,3) != features(nn.hg)
        StackedHourglass.change_hourglass_output(nn.hg,size(nn.labels,1),size(nn.labels,3))
        nn.features = features(nn.hg)
    end

    dtrn=make_training_batch(nn.imgs,nn.labels);

    myadam=Adam(lr=1e-3)
    run_training_no_gui(nn.hg,dtrn,myadam,nn.epochs,nn.losses)

    save_hourglass(string(data_path,"/weights.jld"),nn.hg)

end

function create_config(han::Tracker_Handles,training::Bool)

    filepath=string(han.wt.data_path,"/predict_config.jld")
    file=jldopen(filepath,"w")

    write(file,"Video_Name",han.wt.vid_name)
    write(file,"Tracking_Frames",han.frame_list)
    write(file, "WOI",han.woi)
    write(file,"Pad_Pos",han.wt.pad_pos)
    write(file,"Training_Path",string(han.wt.data_path,"/labels.jld"))
    write(file,"Data_Path",string(han.wt.data_path))
    write(file,"Epochs",han.nn.epochs)
    write(file,"Training",training)

    close(file)

    nothing
end

function read_training_config(filepath)
    file=jldopen(filepath,"r")
    training = read(file,"Training")
    close(file)
    training
end

function load_config(filepath)

    file=jldopen(filepath,"r")
    vid_name = read(file,"Video_Name")
    frame_list = read(file,"Tracking_Frames")
    woi=read(file,"WOI")
    pad_pos=read(file,"Pad_Pos")
    training_path=read(file,"Training_Path")
    epochs=read(file,"Epochs")
    data_path=read(file,"Data_Path")
    close(file)

    nn=NeuralNetwork()
    nn.epochs=epochs
    max_frames = get_max_frames(vid_name)

    (vid_name,frame_list,woi,pad_pos,training_path,epochs,data_path,nn,max_frames)
end

function prediction_from_config(filepath)

    (vid_name,frame_list,woi,pad_pos,training_path,epochs,data_path,nn,max_frames) = load_config(filepath)

    config_path = string(data_path,"/weights.jld")
    load_hourglass_to_nn(nn,config_path)

    set_up_training(nn,vid_name,max_frames,woi,pad_pos,frame_list) #heatmaps, labels, normalize, augment
    save_training(training_path,frame_list,woi,nn)

    nn.predicted = calculate_whiskers(nn,vid_name,total_frames)

    #Save?
end


"""
    parse_file(file; flex_load=true, <keyword arguments>)

Parse a Matpower .m `file` or PTI (PSS(R)E-v33) .raw `file` into a FlexPlan data structure,
including non-dispatchable generators, DC components, storage and flexible loads.

`flex_load` specifies whether to process flexible load data.
Other keyword arguments, if any, are forwarded to `PowerModels.parse_file`.

Mandatory tables: `bus`, `gen`, `branch` (and `load_extra` if `flex_load==true`).

Optional tables: `gencost`, `ndgen`, `branch_oltc`, `storage`, `storage_extra`,
`ne_storage`, and tables used by PowerModelsACDC.
Other tables can be added as well: they will be made available in the returned object.
"""
function parse_file(file::String; flex_load=true, kwargs...)
    data = _PM.parse_file(file; kwargs...)
    add_gen_data!(data)
    if !haskey(data, "ne_branch")
        data["ne_branch"] = Dict{String,Any}()
    end
    if haskey(data, "busdc") || haskey(data, "busdc_ne")
        _PMACDC.process_additional_data!(data)
    end
    add_storage_data!(data)
    if flex_load
        if !haskey(data, "load_extra")
            Memento.error(_LOGGER, "No `load_extra` table found in input file.")
        end
        add_flexible_demand_data!(data)
    end
    return data
end

"Add a `dispatchable` bool field to all generators; add non-dispatchable generators to `data[\"gen\"]`."
function add_gen_data!(data::Dict{String,Any})
    for dgen in values(data["gen"])
        dgen["dispatchable"] = true
    end

    if haskey(data, "ndgen")
        offset = length(data["gen"])
        rescale      = x -> x/data["baseMVA"]
        rescale_dual = x -> x*data["baseMVA"]
        for ndgen in values(data["ndgen"])
            ndgen["dispatchable"] = false

            # Convert to p.u.
            _PM._apply_func!(ndgen, "pref", rescale)
            _PM._apply_func!(ndgen, "qmax", rescale)
            _PM._apply_func!(ndgen, "qmin", rescale)
            _PM._apply_func!(ndgen, "cost_gen", rescale_dual)
            _PM._apply_func!(ndgen, "cost_curt", rescale_dual)

            # Define active power bounds using the same names used by dispatchable
            # generators.
            ndgen["pmin"] = 0.0
            ndgen["pmax"] = ndgen["pref"]
            delete!(ndgen, "pref")

            # Convert the cost of power produced by non-dispatchable generators into
            # polynomial form (the same used by dispatchable generators).
            ndgen["cost"] = [ndgen["cost_gen"], 0.0]
            delete!(ndgen, "cost_gen")

            # Assign to non-dispatchable generators ids contiguous to dispatchable
            # generators so that each generator has an unique id.
            new_id = ndgen["index"] += offset
            data["gen"]["$new_id"] = ndgen
        end
        delete!(data, "ndgen")
    end

    return data
end

function add_storage_data!(data)
    if haskey(data, "storage")
        for (s, storage) in data["storage"]
            rescale_power = x -> x/data["baseMVA"]
            _PM._apply_func!(storage, "max_energy_absorption", rescale_power)
            _PM._apply_func!(storage, "stationary_energy_outflow", rescale_power)
            _PM._apply_func!(storage, "stationary_energy_inflow", rescale_power)
        end
    else
        data["storage"] = Dict{String,Any}()
    end

    if haskey(data, "ne_storage")
        for (s, storage) in data["ne_storage"]
            rescale_power = x -> x/data["baseMVA"]
            _PM._apply_func!(storage, "energy_rating", rescale_power)
            _PM._apply_func!(storage, "thermal_rating", rescale_power)
            _PM._apply_func!(storage, "discharge_rating", rescale_power)
            _PM._apply_func!(storage, "charge_rating", rescale_power)
            _PM._apply_func!(storage, "energy", rescale_power)
            _PM._apply_func!(storage, "ps", rescale_power)
            _PM._apply_func!(storage, "qs", rescale_power)
            _PM._apply_func!(storage, "q_loss", rescale_power)
            _PM._apply_func!(storage, "p_loss", rescale_power)
            _PM._apply_func!(storage, "qmax", rescale_power)
            _PM._apply_func!(storage, "qmin", rescale_power)
            _PM._apply_func!(storage, "max_energy_absorption", rescale_power)
            _PM._apply_func!(storage, "stationary_energy_outflow", rescale_power)
            _PM._apply_func!(storage, "stationary_energy_inflow", rescale_power)
        end
    else
        data["ne_storage"] = Dict{String,Any}()
    end

    return data
end

function add_flexible_demand_data!(data)
    for (le, load_extra) in data["load_extra"]

        # ID of load point
        idx = load_extra["load_id"]

        # Superior bound on voluntary load reduction (not consumed power) as a fraction of the total reference demand (0 ≤ pred_rel_max ≤ 1)
        data["load"]["$idx"]["pred_rel_max"] = load_extra["pred_rel_max"]

        # Superior bound on upward demand shifted as a fraction of the total reference demand (0 ≤ pshift_up_rel_max ≤ 1)
        data["load"]["$idx"]["pshift_up_rel_max"] = load_extra["pshift_up_rel_max"]

        # Superior bound on downward demand shifted as a fraction of the total reference demand (0 ≤ pshift_down_rel_max ≤ 1)
        data["load"]["$idx"]["pshift_down_rel_max"] = load_extra["pshift_down_rel_max"]

        # Superior bound on shifted energy as a fraction of the total reference demand (0 ≤ eshift_rel_max ≤ 1)
        if haskey(load_extra, "eshift_rel_max")
            data["load"]["$idx"]["eshift_rel_max"] = load_extra["eshift_rel_max"]
        end

        # Compensation for consuming less (i.e. voluntary demand reduction) (€/MWh)
        data["load"]["$idx"]["cost_red"] = load_extra["cost_red"]

        # Recovery period for upward demand shifting (h)
        if haskey(load_extra, "tshift_up")
            data["load"]["$idx"]["tshift_up"] = load_extra["tshift_up"]
        end

        # Recovery period for downward demand shifting (h)
        if haskey(load_extra, "tshift_down")
            data["load"]["$idx"]["tshift_down"] = load_extra["tshift_down"]
        end

        # Compensation for demand shifting (€/MWh), applied only to the power shifted upward to avoid double counting
        data["load"]["$idx"]["cost_shift"] = load_extra["cost_shift"]

        # Compensation for load curtailment (i.e. involuntary demand reduction) (€/MWh)
        data["load"]["$idx"]["cost_curt"] = load_extra["cost_curt"]

        # Investment costs for enabling flexible demand (€)
        data["load"]["$idx"]["cost_inv"] = load_extra["cost_inv"]

        # Whether load is flexible (boolean)
        data["load"]["$idx"]["flex"] = load_extra["flex"]

        # Superior bound on voluntary energy reduction as a fraction of the total reference demand (0 ≤ ered_rel_max ≤ 1)
        if haskey(load_extra, "ered_rel_max")
            data["load"]["$idx"]["ered_rel_max"] = load_extra["ered_rel_max"]
        end

        # Expected lifetime of flexibility-enabling equipment (years)
        data["load"]["$idx"]["lifetime"] = load_extra["lifetime"]

        # CO2 costs for enabling flexible demand (€)
        if haskey(load_extra, "co2_cost")
            data["load"]["$idx"]["co2_cost"] = load_extra["co2_cost"]
        end

        # Power factor angle θ, giving the reactive power as Q = P ⨉ tan(θ)
        if haskey(load_extra, "pf_angle")
            data["load"]["$idx"]["pf_angle"] = load_extra["pf_angle"]
        end

        # Rescale cost and power input values to the p.u. values used internally in the model
        rescale_cost = x -> x*data["baseMVA"]
        rescale_power = x -> x/data["baseMVA"]
        _PM._apply_func!(data["load"]["$idx"], "cost_red", rescale_cost)
        _PM._apply_func!(data["load"]["$idx"], "cost_shift", rescale_cost)
        _PM._apply_func!(data["load"]["$idx"], "cost_curt", rescale_cost)
    end
    delete!(data, "load_extra")
    return data
end

function add_generation_emission_data!(data)
    rescale_emission = x -> x * data["baseMVA"]
    for (g, gen) in data["gen"]
        _PM._apply_func!(gen, "emission_factor", rescale_emission)
    end
    return data
end