function cpu_reduce_partial_sum(
    partial_sum::StructArray
)
    return sum(replace_storage(Array, partial_sum), dims=1)
end

function cuda_reduce_partial_sum(
    partial_sum::StructArray
)
    return CUDA.sum(partial_sum.re, dims=1), CUDA.sum(partial_sum.im, dims=1)
end

function gen_code_replica_kernel!(
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]
    thread_idx = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    if thread_idx <= num_samples
        @inbounds code_replica[thread_idx] = codes[1+mod(floor(Int32, code_frequency/sampling_frequency * (thread_idx + latest_shift) + start_code_phase), code_length), prn]
    end

    return nothing
end

function gen_code_replica_strided_kernel!(
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]
    stride = blockDim().x * gridDim().x
    thread_idx = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    @inbounds for i = thread_idx:stride:num_samples
        code_replica[i] = codes[1+mod(floor(Int32, code_frequency/sampling_frequency * (i + latest_shift) + start_code_phase), code_length), prn]
    end

    return nothing
end

function gen_code_replica_texture_mem_strided_kernel!(
    code_replica,
    codes, # texture memory codes
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn::Int,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]

    thread_idx = (blockIdx().x - 1) * blockDim().x + threadIdx().x
    if thread_idx <= num_samples
        code_replica[thread_idx] = codes[(code_frequency/sampling_frequency * (thread_idx + latest_shift) + start_code_phase) / code_length, prn]
    end

    return nothing
end

function gen_code_replica_texture_mem_strided_nsat_kernel!(
    code_replica,
    codes, # texture memory codes
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]
    sat_idx = blockIdx().y
    thread_idx = (blockIdx().x - 1) * blockDim().x + threadIdx().x
    if thread_idx <= num_samples
        code_replica[thread_idx, sat_idx] = codes[(code_frequency/sampling_frequency * (thread_idx + latest_shift) + start_code_phase) / code_length, prn[sat_idx]]
    end

    return nothing
end

function gen_code_replica_texture_mem_strided_kernel!(
    code_replica,
    codes, # texture memory codes
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]
    thread_idx = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    if thread_idx <= num_samples
        code_replica[thread_idx] = codes[(code_frequency/sampling_frequency * (thread_idx + latest_shift) + start_code_phase) / code_length, prn]
    end

    return nothing
end

function gen_code_replica_texture_mem_kernel!(
    code_replica,
    codes, # texture memory codes
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    latest_shift,
    code_length
)
    # thread_idx goes from 1:2502
    # sample_idx converted to -1:2500 -> [-1 0 +1]
    thread_idx = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    if thread_idx <= num_samples
        code_replica[thread_idx] = codes[(code_frequency/sampling_frequency * (thread_idx + latest_shift) + start_code_phase) / code_length, prn]
    end

    return nothing
end

function downconvert_and_correlate_kernel_1330!(
    res_re,
    res_im,
    signal_re,
    signal_im,
    codes,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    start_code_phase,
    carrier_phase,
    code_length,
    prn,
    num_samples,
    num_ants,
    num_corrs
)
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, num_ants, num_corrs))
    sample_idx   = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    antenna_idx  = 1 + ((blockIdx().y - 1) * blockDim().y + (threadIdx().y - 1))
    corr_idx     = 1 + ((blockIdx().z - 1) * blockDim().z + (threadIdx().z - 1))
    iq_offset = blockDim().x
    cache_index = threadIdx().x - 1

    code_phase = accum_re = accum_im = dw_re = dw_im = carrier_re = carrier_im = 0.0f0
    mod_floor_code_phase = Int(0)

    if sample_idx <= num_samples && antenna_idx <= num_ants && corr_idx <= num_corrs
        # generate carrier
        carrier_im, carrier_re = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconvert with the conjugate of the carrier
        dw_re = signal_re[sample_idx, antenna_idx] * carrier_re + signal_im[sample_idx, antenna_idx] * carrier_im
        dw_im = signal_im[sample_idx, antenna_idx] * carrier_re - signal_re[sample_idx, antenna_idx] * carrier_im

        # calculate the code phase
        code_phase = code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase

        # wrap the code phase around the code length e.g. phase = 1024 -> modfloorphase = 1
        mod_floor_code_phase = 1 + mod(floor(Int32, code_phase), code_length)

        # multiply elementwise with the code
        accum_re += codes[mod_floor_code_phase, prn] * dw_re
        accum_im += codes[mod_floor_code_phase, prn] * dw_im
    end

    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = accum_re
    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = accum_im

    ## Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
            cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
        end
        sync_threads()
        i ÷= 2
    end

    if (threadIdx().x - 1) == 0
        res_re[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
        res_im[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
    end
    return nothing
end

function downconvert_and_correlate_kernel_1331!(
    res_re,
    res_im,
    signal_re,
    signal_im,
    codes,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    start_code_phase,
    carrier_phase,
    code_length,
    prn,
    num_samples,
    num_ants,
    num_corrs
)
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, num_ants, num_corrs))
    sample_idx   = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))
    antenna_idx  = 1 + ((blockIdx().y - 1) * blockDim().y + (threadIdx().y - 1))
    corr_idx     = 1 + ((blockIdx().z - 1) * blockDim().z + (threadIdx().z - 1))
    iq_offset = blockDim().x
    cache_index = threadIdx().x - 1

    accum_re = accum_im = dw_re = dw_im = carrier_re = carrier_im = 0.0f0

    if sample_idx <= num_samples && antenna_idx <= num_ants && corr_idx <= num_corrs
        # generate carrier
        carrier_im, carrier_re = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconvert with the conjugate of the carrier
        dw_re = signal_re[sample_idx, antenna_idx] * carrier_re + signal_im[sample_idx, antenna_idx] * carrier_im
        dw_im = signal_im[sample_idx, antenna_idx] * carrier_re - signal_re[sample_idx, antenna_idx] * carrier_im

        # multiply elementwise with the code
        accum_re += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_re
        accum_im += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_im
    end

    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = accum_re
    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = accum_im

    ## Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
            cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
        end
        sync_threads()
        i ÷= 2
    end

    if (threadIdx().x - 1) == 0
        res_re[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
        res_im[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
    end
    return nothing
end

function downconvert_and_correlate_kernel_1431!(
    res_re,
    res_im,
    signal_re,
    signal_im,
    codes,
    code_frequency,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    start_code_phase,
    carrier_phase,
    code_length,
    prn,
    num_samples,
    num_ants,
    num_corrs
)
    block_dim_x = 2 * blockDim().x # launched with half the grid
    sample_idx   = 1 + ((blockIdx().x - 1) * block_dim_x  + (threadIdx().x - 1))
    antenna_idx  = 1 + ((blockIdx().y - 1) * blockDim().y + (threadIdx().y - 1))
    corr_idx     = 1 + ((blockIdx().z - 1) * blockDim().z + (threadIdx().z - 1))
    iq_offset = blockDim().x
    cache_index = threadIdx().x - 1

    # allocate shared memory
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, num_ants, num_corrs))
    # wipe values
    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = 0.0f0
    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = 0.0f0

    # define local variables
    accum_re_1 = accum_im_1 = dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
    accum_re_2 = accum_im_2 = dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0

    if sample_idx <= num_samples && antenna_idx <= num_ants && corr_idx <= num_corrs
        # generate carrier
        carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconvert with the conjugate of the carrier
        dw_re_1 = signal_re[sample_idx, antenna_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx] * carrier_im_1
        dw_im_1 = signal_im[sample_idx, antenna_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx] * carrier_im_1

        # multiply elementwise with the code
        accum_re_1 += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_re_1
        accum_im_1 += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_im_1

        # write results to shared memory
        cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = accum_re_1
        cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = accum_im_1

        if sample_idx + blockDim().x <= num_samples
            # generate carrier for the remaining samples
            carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

            # downconvert with the conjugate of the carrier for the remaining samples
            dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
            dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_im_2

            # multiply elementwise with the code for the remaining samples
            accum_re_2 += codes[(code_frequency / sampling_frequency * ((sample_idx + blockDim().x - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_re_2
            accum_im_2 += codes[(code_frequency / sampling_frequency * ((sample_idx + blockDim().x - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_im_2

            # append results to shared memory
            cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += accum_re_2
            cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += accum_im_2
        end
    end

    ## Reduction in shared memory
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
            cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
        end
        sync_threads()
        i ÷= 2
    end

    if (threadIdx().x - 1) == 0
        res_re[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
        res_im[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
    end
    return nothing
end

# function downconvert_and_correlate_kernel_3431!(
#     res_re,
#     res_im,
#     signal_re,
#     signal_im,
#     codes,
#     code_frequency,
#     correlator_sample_shifts,
#     carrier_frequency,
#     sampling_frequency,
#     start_code_phase,
#     carrier_phase,
#     code_length,
#     prn,
#     num_samples,
#     num_ants,
#     num_corrs
# )
#     block_dim_x = 2 * blockDim().x # launched with half the grid
#     sample_idx   = 1 + ((blockIdx().x - 1) * block_dim_x  + (threadIdx().x - 1))
#     antenna_idx  = 1 + ((blockIdx().y - 1) * blockDim().y + (threadIdx().y - 1))
#     corr_idx     = 1 + ((blockIdx().z - 1) * blockDim().z + (threadIdx().z - 1))
#     iq_offset = blockDim().x
#     cache_index = threadIdx().x - 1

#     # allocate shared memory
#     cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, num_ants, num_corrs))
#     # wipe values
#     cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = 0.0f0
#     cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = 0.0f0

#     # define local variables
#     accum_re_1 = accum_im_1 = dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
#     accum_re_2 = accum_im_2 = dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0

#     if sample_idx <= num_samples && antenna_idx <= num_ants && corr_idx <= num_corrs
#         # generate carrier
#         carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

#         # downconvert with the conjugate of the carrier
#         dw_re_1 = signal_re[sample_idx, antenna_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx] * carrier_im_1
#         dw_im_1 = signal_im[sample_idx, antenna_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx] * carrier_im_1

#         # multiply elementwise with the code
#         accum_re_1 += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_re_1
#         accum_im_1 += codes[(code_frequency / sampling_frequency * ((sample_idx - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_im_1

#         # write results to shared memory
#         cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = accum_re_1
#         cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = accum_im_1

#         if sample_idx + blockDim().x <= num_samples
#             # generate carrier for the remaining samples
#             carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

#             # downconvert with the conjugate of the carrier for the remaining samples
#             dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
#             dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_im_2

#             # multiply elementwise with the code for the remaining samples
#             accum_re_2 += codes[(code_frequency / sampling_frequency * ((sample_idx + blockDim().x - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_re_2
#             accum_im_2 += codes[(code_frequency / sampling_frequency * ((sample_idx + blockDim().x - 1) + correlator_sample_shifts[corr_idx]) + start_code_phase) / code_length, prn] * dw_im_2

#             # append results to shared memory
#             cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += accum_re_2
#             cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += accum_im_2
#         end
#     end

#     ## Reduction in shared memory
#     # wait until all the accumulators have done writing the results to the cache
#     sync_threads()

#     i::Int = blockDim().x ÷ 2
#     @inbounds while i != 0
#         if cache_index < i
#             cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
#             cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
#         end
#         sync_threads()
#         i ÷= 2
#     end

#     if (threadIdx().x - 1) == 0
#         res_re[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
#         res_im[blockIdx().x, antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
#     end
#     return nothing
# end

function downconvert_and_correlate_kernel_3431!(
    partial_sum_re,
    partial_sum_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    code_replica,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    sampling_frequency,
    carrier_phase,
    num_samples::Int,
    num_ants::NumAnts{NANT}
)  where {NCOR, NANT}
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, NANT, NCOR))
    sample_idx   = 1 + ((blockIdx().x - 1) * (2 * blockDim().x) + (threadIdx().x - 1)) # double the grid
    iq_offset = blockDim().x # indexing offset for complex values I/Q samples
    cache_index = threadIdx().x - 1

    dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
    dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0

    @inbounds if sample_idx <= num_samples
        # carrier replica generation, sin->im , cos->re
        carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconversion / carrier wipe off
        for antenna_idx = 1:NANT
            dw_re_1 = signal_re[sample_idx, antenna_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx] * carrier_im_1
            dw_im_1 = signal_im[sample_idx, antenna_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx] * carrier_im_1
            for corr_idx = 1:NCOR
                sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]

                # write to shared memory cache
                cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_re_1
                cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_im_1
            end
        end

        if sample_idx + blockDim().x <= num_samples
            # carrier replica generation, sin->im , cos->re
            carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

            # downconversion / carrier wipe off
            for antenna_idx = 1:NANT
                dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                for corr_idx = 1:NCOR
                    sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]

                    # write to shared memory cache
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_re_2
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_im_2
                end
            end
        end
    end
    ## Partial Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            for antenna_idx = 1:NANT
                for corr_idx = 1:NCOR
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
                end
            end
        end
        sync_threads()
        i ÷= 2
    end

    @inbounds if (threadIdx().x - 1) == 0
         for antenna_idx = 1:NANT
            for corr_idx = 1:NCOR
                partial_sum_re[blockIdx().x, antenna_idx, corr_idx] = cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
                partial_sum_im[blockIdx().x, antenna_idx, corr_idx] = cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
            end
        end
    end
    return nothing
end

function downconvert_and_correlate_kernel_4431!(
    accum_re,
    accum_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    code_replica,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    sampling_frequency,
    carrier_phase,
    num_samples::Int,
    num_ants::NumAnts{NANT}
)  where {NCOR, NANT}
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, NANT, NCOR))
    sample_idx   = 1 + ((blockIdx().x - 1) * (2 * blockDim().x) + (threadIdx().x - 1)) # double the grid
    iq_offset = blockDim().x # indexing offset for complex values I/Q samples
    cache_index = threadIdx().x - 1

    dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
    dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0

    @inbounds if sample_idx <= num_samples
        # carrier replica generation, sin->im , cos->re
        carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconversion / carrier wipe off
        for antenna_idx = 1:NANT
            dw_re_1 = signal_re[sample_idx, antenna_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx] * carrier_im_1
            dw_im_1 = signal_im[sample_idx, antenna_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx] * carrier_im_1
            for corr_idx = 1:NCOR
                sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]

                # write to shared memory cache
                cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_re_1
                cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_im_1
            end
        end

        if sample_idx + blockDim().x <= num_samples
            # carrier replica generation, sin->im , cos->re
            carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

            # downconversion / carrier wipe off
            for antenna_idx = 1:NANT
                dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                for corr_idx = 1:NCOR
                    sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]

                    # write to shared memory cache
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_re_2
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_im_2
                end
            end
        end
    end

    ## Partial Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            for antenna_idx = 1:NANT
                for corr_idx = 1:NCOR
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
                end
            end
        end
        sync_threads()
        i ÷= 2
    end

    @inbounds if threadIdx().x == 1
        for antenna_idx = 1:NANT
            for corr_idx = 1:NCOR
                CUDA.@atomic accum_re[antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
                CUDA.@atomic accum_im[antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
            end
        end
    end

    return nothing
end

function downconvert_and_correlate_kernel_3d_4431!(
    accum_re,
    accum_im,
    signal_re,
    signal_im,
    code_replica,
    correlator_sample_shifts,
    carrier_frequency,
    sampling_frequency,
    carrier_phase,
    num_samples::Int,
    num_ants::NumAnts{NANT}
)  where {NANT}
    cache = CuDynamicSharedArray(Float32, (2 * blockDim().x, NANT))
    sample_idx = 1 + ((blockIdx().x - 1) * (2 * blockDim().x) + (threadIdx().x - 1)) # double the grid
    iq_offset = blockDim().x # indexing offset for complex values I/Q samples
    cache_index = threadIdx().x - 1
    antenna_idx = threadIdx().y
    corr_idx = blockIdx().y
    sat_idx = blockIdx().z

    dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
    dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0
    sample_shift = correlator_sample_shifts[corr_idx]

    @inbounds if sample_idx <= num_samples
        # carrier replica generation, sin->im , cos->re
        carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconversion / carrier wipe off
        for antenna_idx = 1:NANT
            dw_re_1 = signal_re[sample_idx, antenna_idx, sat_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx, sat_idx] * carrier_im_1
            dw_im_1 = signal_im[sample_idx, antenna_idx, sat_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx, sat_idx] * carrier_im_1

            # write to shared memory cache
            cache[1 + cache_index + 0 * iq_offset, antenna_idx] = code_replica[sample_idx + sample_shift, sat_idx] * dw_re_1
            cache[1 + cache_index + 1 * iq_offset, antenna_idx] = code_replica[sample_idx + sample_shift, sat_idx] * dw_im_1
        end

        if sample_idx + blockDim().x <= num_samples
            # carrier replica generation, sin->im , cos->re
            carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

            # downconversion / carrier wipe off
            for antenna_idx = 1:NANT
                dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx, sat_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx, sat_idx] * carrier_im_2
                dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx, sat_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx, sat_idx] * carrier_im_2

                # write to shared memory cache
                cache[1 + cache_index + 0 * iq_offset, antenna_idx] += code_replica[sample_idx + blockDim().x + sample_shift, sat_idx] * dw_re_2
                cache[1 + cache_index + 1 * iq_offset, antenna_idx] += code_replica[sample_idx + blockDim().x + sample_shift, sat_idx] * dw_im_2
            end
        end
    end

    ## Partial Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            for antenna_idx = 1:NANT
                cache[1 + cache_index + 0 * iq_offset, antenna_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx]
                cache[1 + cache_index + 1 * iq_offset, antenna_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx]
            end
        end
        sync_threads()
        i ÷= 2
    end

    @inbounds if threadIdx().x == 1
        for antenna_idx = 1:NANT
            CUDA.@atomic accum_re[antenna_idx, corr_idx, sat_idx] += cache[1 + 0 * iq_offset, antenna_idx]
            CUDA.@atomic accum_im[antenna_idx, corr_idx, sat_idx] += cache[1 + 1 * iq_offset, antenna_idx]
        end
    end

    return nothing
end

function downconvert_and_correlate_kernel_5431!(
    accum_re,
    accum_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    codes,
    code_length,
    code_replica,
    prn,
    correlator_sample_shifts::SVector{NCOR, Int64},
    num_of_shifts,
    code_frequency,
    carrier_frequency,
    sampling_frequency,
    start_code_phase,
    carrier_phase,
    num_samples::Int,
    num_ants::NumAnts{NANT},
)  where {NCOR, NANT}
    cache = @cuDynamicSharedMem(Float32, (2 * blockDim().x, NANT, NCOR))
    sample_idx   = 1 + ((blockIdx().x - 1) * (2 * blockDim().x) + (threadIdx().x - 1))
    iq_offset = blockDim().x # indexing offset for complex values I/Q samples
    cache_index = threadIdx().x - 1

    # local
    dw_re_1 = dw_im_1 = carrier_re_1 = carrier_im_1 = 0.0f0
    dw_re_2 = dw_im_2 = carrier_re_2 = carrier_im_2 = 0.0f0

    # Code replica generation
    if sample_idx <= num_samples + num_of_shifts
        @inbounds code_replica[sample_idx] = codes[(code_frequency/sampling_frequency * (sample_idx - num_of_shifts) + start_code_phase)/code_length, prn]
        if sample_idx + blockDim().x <= num_samples + num_of_shifts
            @inbounds code_replica[sample_idx + blockDim().x] = codes[(code_frequency/sampling_frequency * (sample_idx + blockDim().x - num_of_shifts) + start_code_phase)/code_length, prn]
        end
    end

    sync_threads()

    @inbounds if sample_idx <= num_samples
        # carrier replica generation, sin->im , cos->re
        carrier_im_1, carrier_re_1 = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        # downconversion / carrier wipe off
        for antenna_idx = 1:NANT
            dw_re_1 = signal_re[sample_idx, antenna_idx] * carrier_re_1 + signal_im[sample_idx, antenna_idx] * carrier_im_1
            dw_im_1 = signal_im[sample_idx, antenna_idx] * carrier_re_1 - signal_re[sample_idx, antenna_idx] * carrier_im_1
            for corr_idx = 1:NCOR
                sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]
                # write to shared memory cache
                cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_re_1
                cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] = code_replica[sample_idx + sample_shift] * dw_im_1
            end
        end

        if sample_idx + blockDim().x <= num_samples
            # carrier replica generation, sin->im , cos->re
            carrier_im_2, carrier_re_2 = CUDA.sincos(2π * ((sample_idx + blockDim().x - 1) * carrier_frequency / sampling_frequency + carrier_phase))

            # downconversion / carrier wipe off
            for antenna_idx = 1:NANT
                dw_re_2 = signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 + signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                dw_im_2 = signal_im[sample_idx + blockDim().x, antenna_idx] * carrier_re_2 - signal_re[sample_idx + blockDim().x, antenna_idx] * carrier_im_2
                for corr_idx = 1:NCOR
                    sample_shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]
                    # write to shared memory cache
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_re_2
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += code_replica[sample_idx + blockDim().x + sample_shift] * dw_im_2
                end
            end
        end
    end

    ## Partial Reduction
    # wait until all the accumulators have done writing the results to the cache
    sync_threads()

    i::Int = blockDim().x ÷ 2
    @inbounds while i != 0
        if cache_index < i
            for antenna_idx = 1:NANT
                for corr_idx = 1:NCOR
                    cache[1 + cache_index + 0 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 0 * iq_offset + i, antenna_idx, corr_idx]
                    cache[1 + cache_index + 1 * iq_offset, antenna_idx, corr_idx] += cache[1 + cache_index + 1 * iq_offset + i, antenna_idx, corr_idx]
                end
            end
        end
        sync_threads()
        i ÷= 2
    end

    # Last block reduction via atomic add
    @inbounds if (threadIdx().x - 1) == 0
        for antenna_idx = 1:NANT
            for corr_idx = 1:NCOR
                CUDA.@atomic accum_re[antenna_idx, corr_idx] += cache[1 + 0 * iq_offset, antenna_idx, corr_idx]
                CUDA.@atomic accum_im[antenna_idx, corr_idx] += cache[1 + 1 * iq_offset, antenna_idx, corr_idx]
            end
        end
    end

    return nothing
end

function downconvert_and_accumulate_strided_kernel!(
    accum_re,
    accum_im,
    code_replica,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    carrier_frequency,
    sampling_frequency,
    carrier_phase,
    num_samples::Int,
    num_ants::NumAnts{NANT},
    correlator_sample_shifts::SVector{NCOR, Int64}
) where {NANT, NCOR}
    stride = blockDim().x * gridDim().x
    thread_idx = 1 + ((blockIdx().x - 1) * blockDim().x + (threadIdx().x - 1))

    @inbounds for sample_idx = thread_idx:stride:num_samples
        # gen carrier replica
        carrier_replica_im[sample_idx], carrier_replica_re[sample_idx] = CUDA.sincos(2π * ((sample_idx - 1) * carrier_frequency / sampling_frequency + carrier_phase))

        for antenna_idx = 1:NANT
            # downconvert
            downconverted_signal_re[sample_idx, antenna_idx] = signal_re[sample_idx, antenna_idx] * carrier_replica_re[sample_idx] + signal_im[sample_idx, antenna_idx] * carrier_replica_im[sample_idx]
            downconverted_signal_im[sample_idx, antenna_idx] = signal_im[sample_idx, antenna_idx] * carrier_replica_re[sample_idx] - signal_re[sample_idx, antenna_idx] * carrier_replica_im[sample_idx]
            for corr_idx = 1:NCOR
                # accumulate
                shift = correlator_sample_shifts[corr_idx] - correlator_sample_shifts[1]
                accum_re[sample_idx, antenna_idx, corr_idx] = downconverted_signal_re[sample_idx, antenna_idx] * code_replica[sample_idx + shift]
                accum_im[sample_idx, antenna_idx, corr_idx] = downconverted_signal_im[sample_idx, antenna_idx] * code_replica[sample_idx + shift]
            end
        end
    end

    return nothing
end

# KERNEL 1_3_cplx_multi
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    partial_sum,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{1330};
) where {NANT, NCOR}
    @cuda threads=threads_per_block[1] blocks=blocks_per_grid shmem=shmem_size[1] downconvert_and_correlate_kernel_1330!(
        partial_sum.re,
        partial_sum.im,
        signal_re,
        signal_im,
        codes,
        code_frequency,
        correlator_sample_shifts,
        carrier_frequency,
        sampling_frequency,
        start_code_phase,
        carrier_phase,
        code_length,
        prn,
        num_samples,
        NANT,
        NCOR
    )
    @cuda threads=512 blocks=1 shmem=shmem_size[2] reduce_cplx_multi_3(
        partial_sum.re,
        partial_sum.im,
        partial_sum.re,
        partial_sum.im,
        blocks_per_grid,
        num_ants,
        correlator_sample_shifts
    )
end

# KERNEL 1_3_cplx_multi_textmem
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    partial_sum,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{1331};
) where {NANT, NCOR}
    NVTX.@range "downconvert_and_correlate_kernel_1331!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid shmem=shmem_size[1] downconvert_and_correlate_kernel_1331!(
            partial_sum.re,
            partial_sum.im,
            signal_re,
            signal_im,
            codes,
            code_frequency,
            correlator_sample_shifts,
            carrier_frequency,
            sampling_frequency,
            start_code_phase,
            carrier_phase,
            code_length,
            prn,
            num_samples,
            NANT,
            NCOR
        )
    end
    NVTX.@range "reduce_cplx_multi_3" begin
        @cuda threads=threads_per_block[2] blocks=1 shmem=shmem_size[2] reduce_cplx_multi_3(
            partial_sum.re,
            partial_sum.im,
            partial_sum.re,
            partial_sum.im,
            blocks_per_grid,
            num_ants,
            correlator_sample_shifts
        )
    end
end

# KERNEL 1_4_cplx_multi_textmem
# 1431
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    partial_sum,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{1431};
) where {NANT, NCOR}
    NVTX.@range "downconvert_and_correlate_kernel_1431!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid shmem=shmem_size[1] downconvert_and_correlate_kernel_1431!(
            partial_sum.re,
            partial_sum.im,
            signal_re,
            signal_im,
            codes,
            code_frequency,
            correlator_sample_shifts,
            carrier_frequency,
            sampling_frequency,
            start_code_phase,
            carrier_phase,
            code_length,
            prn,
            num_samples,
            NANT,
            NCOR
        )
    end
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[2] blocks=1 shmem=shmem_size[2] reduce_cplx_multi_4(
            partial_sum.re,
            partial_sum.im,
            partial_sum.re,
            partial_sum.im,
            blocks_per_grid,
            num_ants,
            correlator_sample_shifts
        )
    end
end

# KERNEL 2_3_cplx_multi
# 2330
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    phi_re,
    phi_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{2330}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_accumulate!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] downconvert_and_accumulate_strided_kernel!(
            accum_re,
            accum_im,
            code_replica,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    NVTX.@range "reduce_cplx_multi_3" begin
        @cuda threads=threads_per_block[3] blocks=blocks_per_grid[3] shmem=shmem_size reduce_cplx_multi_3(
            phi_re,
            phi_im,
            accum_re,
            accum_im,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
    NVTX.@range "reduce_cplx_multi_3" begin
        @cuda threads=threads_per_block[3] blocks=1 shmem=shmem_size reduce_cplx_multi_3(
            phi_re,
            phi_im,
            phi_re,
            phi_im,
            blocks_per_grid[3],
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
end

# KERNEL 2_3_cplx_multi_textmem
# 2331
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    phi_re,
    phi_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{2331}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_texture_mem_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_accumulate!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] downconvert_and_accumulate_strided_kernel!(
            accum_re,
            accum_im,
            code_replica,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    NVTX.@range "reduce_cplx_multi_3" begin
        @cuda threads=threads_per_block[3] blocks=blocks_per_grid[3] shmem=shmem_size reduce_cplx_multi_3(
            phi_re,
            phi_im,
            accum_re,
            accum_im,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
    NVTX.@range "reduce_cplx_multi_3" begin
        @cuda threads=threads_per_block[3] blocks=1 shmem=shmem_size reduce_cplx_multi_3(
            phi_re,
            phi_im,
            phi_re,
            phi_im,
            blocks_per_grid[3],
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
end

# KERNEL 2_4_cplx_multi
# 2430
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    phi_re,
    phi_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{2430}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_accumulate!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] downconvert_and_accumulate_strided_kernel!(
            accum_re,
            accum_im,
            code_replica,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[3] blocks=cld(blocks_per_grid[3], 2) shmem=shmem_size reduce_cplx_multi_4(
            phi_re,
            phi_im,
            accum_re,
            accum_im,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[3] blocks=1 shmem=shmem_size reduce_cplx_multi_4(
            phi_re,
            phi_im,
            phi_re,
            phi_im,
            blocks_per_grid[3],
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
end

# KERNEL 2_4_cplx_multi_textmem
# 2431
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    phi_re,
    phi_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{2431}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_texture_mem_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_accumulate!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] downconvert_and_accumulate_strided_kernel!(
            accum_re,
            accum_im,
            code_replica,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[3] blocks=cld(blocks_per_grid[3], 2) shmem=shmem_size reduce_cplx_multi_4(
            phi_re,
            phi_im,
            accum_re,
            accum_im,
            num_samples,
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[3] blocks=1 shmem=shmem_size reduce_cplx_multi_4(
            phi_re,
            phi_im,
            phi_re,
            phi_im,
            blocks_per_grid[3],
            num_ants,
            correlator_sample_shifts
        )
    end
    # print(Array(phi_re))
end

# KERNEL 3_4_cplx_multi_textmem
# 3431
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{3431}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_texture_mem_strided_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_texture_mem_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_correlate_kernel_3431!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] shmem=shmem_size[1] downconvert_and_correlate_kernel_3431!(
            accum_re,
            accum_im,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            code_replica,
            correlator_sample_shifts,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants
        )
    end
    NVTX.@range "reduce_cplx_multi_4" begin
        @cuda threads=threads_per_block[3] blocks=1 shmem=shmem_size[2] reduce_cplx_multi_4(
            accum_re,
            accum_im,
            accum_re,
            accum_im,
            blocks_per_grid[2],
            num_ants,
            correlator_sample_shifts
        )
    end
end

# KERNEL 4_4_cplx_multi_textmem
# 4431
function kernel_algorithm(
    threads_per_block,
    blocks_per_grid,
    shmem_size,
    code_replica,
    codes,
    code_frequency,
    sampling_frequency,
    start_code_phase,
    prn,
    num_samples,
    num_of_shifts,
    code_length,
    accum_re,
    accum_im,
    carrier_replica_re,
    carrier_replica_im,
    downconverted_signal_re,
    downconverted_signal_im,
    signal_re,
    signal_im,
    correlator_sample_shifts::SVector{NCOR, Int64},
    carrier_frequency,
    carrier_phase,
    num_ants::NumAnts{NANT},
    num_corrs,
    algorithm::KernelAlgorithm{4431}
) where {NANT, NCOR}
    NVTX.@range "gen_code_replica_texture_mem_strided_kernel!" begin
        @cuda threads=threads_per_block[1] blocks=blocks_per_grid[1] gen_code_replica_texture_mem_strided_kernel!(
            code_replica,
            codes,
            code_frequency,
            sampling_frequency,
            start_code_phase,
            prn,
            num_samples,
            num_of_shifts,
            code_length
        )
    end
    NVTX.@range "downconvert_and_correlate_kernel_3431!" begin
        @cuda threads=threads_per_block[2] blocks=blocks_per_grid[2] shmem=shmem_size[1] downconvert_and_correlate_kernel_4431!(
            accum_re,
            accum_im,
            carrier_replica_re,
            carrier_replica_im,
            downconverted_signal_re,
            downconverted_signal_im,
            signal_re,
            signal_im,
            code_replica,
            correlator_sample_shifts,
            carrier_frequency,
            sampling_frequency,
            carrier_phase,
            num_samples,
            num_ants
        )
    end
end


using CubicEoSDatabase

"""
    load(MBWREoSComponent; name::AbstractString[, component_dbs])

Return `BrusilovskyEoSComponent` named `name` with parameters from `component_dbs`.

# Arguments
- the first argument is `BrusilovskyEoSComponent`

# Keywords
- `name::AbstractString`: the component name
- `component_dbs`: an iterable of `ComponentDatabase` objects (by default,
(Data.martinez(), Data.brusilovsky_comp()))

# Returns
- `BrusilovskyEoSComponent`: the component generated by merging the EoS properties for the
given name from databases
"""
function load(
    ::Type{<:MBWREoSComponent};
    name::AbstractString,
    component_dbs = (ComponentDatabase("data/nist.csv"),)
)
    comp_properties = foldl(
        (dict, db) -> merge!(dict, getentry(db, name)),
        component_dbs;
        init = Dict{Symbol, Any}()
    )

    return __load_mbwr_comp__(; comp_properties...)
end

function __load_mbwr_comp__(
    ;
    name::AbstractString,
    molecular_mass,
    number_carbons,
    critical_temperature,
    critical_pressure,
    critical_compressibility,
    acentric_factor,
    extra_kw...  # unneeded keywords to construct component
)
    __load_mbwr_comp__(
        name,
        molecular_mass,
        number_carbons,
        critical_temperature,
        critical_pressure,
        critical_compressibility,
        acentric_factor,
    )
end

function __load_mbwr_comp__(
    name::AbstractString,
    molecular_mass,
    number_carbons,
    critical_temperature,
    critical_pressure,
    critical_compressibility,
    acentric,
)

    return MBWREoSComponent(
        ;
        name = name,
        critical_pressure = critical_pressure,
        critical_temperature = critical_temperature,
        acentric_factor = acentric,
        Zc = critical_compressibility,
        molar_mass = molecular_mass,
        carbon_number = number_carbons
    )
end

"""
    load(BrusilovskyEoSMixture; names[, component_dbs])

Return `BrusilovskyEoSMixture` by loading parameters for individual substances named `names`
from `component_dbs` and their interaction parameters from `mix_eos_db`.

# Arguments
- the first argument is `BrusilovskyEoSMixture`

# Keywords
- `names`: an iterable of `AbstractString`s containing the names of components to load
- `component_dbs`: an iterable of `ComponentDatabase` objects (by default,
`(Data.martinez(), Data.brusilovsky_comp())`)
- `mix_eos_db::MixtureDatabase`: the database with the cross-component interaction
parameters (by default, `Data.brusilovsky_mix()`)

# Returns
- `BrusilovskyEoSMixture`: the EoS for the mixture of the specified substances
"""
function load(
    ::Type{<:MBWREoSMixture};
    names,
    component_dbs = (Data.martinez(), Data.brusilovsky_comp()),
    mix_eos_db::MixtureDatabase = Data.brusilovsky_mix_adjusted(),
)
    components = [
        load(
            BrusilovskyEoSComponent;
            name = name,
            component_dbs = component_dbs
        ) for name in names
    ]
    corrections = getmatrix(mix_eos_db, names)
    return MBWREoSMixture(;
        components = components,
        corrections...
    )
end


# _Constant--DecisionRef
Base.:(+)(lhs::_Constant, rhs::DecisionRef) = DAE(_VAE(convert(Float64, lhs)), _DAE(0.0, rhs => +one(Float64)), _KAE(0.0))
Base.:(-)(lhs::_Constant, rhs::DecisionRef) = DAE(_VAE(convert(Float64, lhs)), _DAE(0.0, rhs => -one(Float64)), _KAE(0.0))
Base.:(*)(lhs::_Constant, rhs::DecisionRef) = DAE(_VAE(0.0), _DAE(0.0, rhs => lhs), _KAE(0.0))

# _Constant--KnownRef
Base.:(+)(lhs::_Constant, rhs::KnownRef) = DAE(_VAE(convert(Float64, lhs)), _DAE(0.0), _KAE(0.0, rhs => +one(Float64)))
Base.:(-)(lhs::_Constant, rhs::KnownRef) = DAE(_VAE(convert(Float64, lhs)), _DAE(0.0), _KAE(0.0, rhs => -one(Float64)))
Base.:(*)(lhs::_Constant, rhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0), _KAE(0.0, rhs => lhs))

# _Constant--DecisionAffExpr{C}
Base.:(+)(lhs::_Constant, rhs::DecisionAffExpr{C}) where C =
    DecisionAffExpr(convert(C,lhs) + rhs.variables,
                    copy(rhs.decisions),
                    copy(rhs.knowns))
Base.:(-)(lhs::_Constant, rhs::DecisionAffExpr{C}) where C =
    DecisionAffExpr(convert(C,lhs) - rhs.variables,
                    -rhs.decisions,
                    -rhs.knowns)
Base.:(*)(lhs::_Constant, rhs::DecisionAffExpr{C}) where C =
    DecisionAffExpr(convert(C,lhs) * rhs.variables,
                    convert(C,lhs) * rhs.decisions,
                    convert(C,lhs) * rhs.knowns)

# _Constant--DecisionQuadExpr{C}
Base.:(+)(lhs::_Constant, rhs::DecisionQuadExpr{C}) where C =
    DecisionQuadExpr(convert(C,lhs) + rhs.variables,
                     copy(rhs.decisions),
                     copy(rhs.knowns),
                     copy(rhs.cross_terms),
                     copy(rhs.known_variable_terms),
                     copy(rhs.known_decision_terms))
Base.:(-)(lhs::_Constant, rhs::DecisionQuadExpr{C}) where C =
    DecisionQuadExpr(convert(C,lhs) - rhs.variables,
                     copy(rhs.decisions),
                     copy(rhs.knowns),
                     copy(rhs.cross_terms),
                     copy(rhs.known_variable_terms),
                     copy(rhs.known_decision_terms))
Base.:(*)(lhs::_Constant, rhs::DecisionQuadExpr{C}) where C =
    DecisionQuadExpr(convert(C,lhs) * rhs.variables,
                     convert(C,lhs) * rhs.decisions,
                     convert(C,lhs) * rhs.knowns,
                     _map_cross_terms(c -> convert(C,lhs) * c,
                                      copy(rhs.cross_terms)),
                     _map_cross_terms(c -> convert(C,lhs) * c,
                                      copy(rhs.known_variable_terms)),
                     _map_cross_terms(c -> convert(C,lhs) * c,
                                      copy(rhs.known_decision_terms)))

#=
    VariableRef
=#

# VariableRef--DecisionRef
Base.:(+)(lhs::VariableRef, rhs::DecisionRef) = DAE(_VAE(0.0, lhs => 1.0), _DAE(0.0, rhs =>  1.0), _KAE(0.0))
Base.:(-)(lhs::VariableRef, rhs::DecisionRef) = DAE(_VAE(0.0, lhs => 1.0), _DAE(0.0, rhs => -1.0), _KAE(0.0))
function Base.:(*)(lhs::VariableRef, rhs::DecisionRef)
    result = zero(DQE)
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# VariableRef--KnownRef
Base.:(+)(lhs::VariableRef, rhs::KnownRef) = DAE(_VAE(0.0, lhs => 1.0), _DAE(0.0), _KAE(0.0, rhs =>  1.0))
Base.:(-)(lhs::VariableRef, rhs::KnownRef) = DAE(_VAE(0.0, lhs => 1.0), _DAE(0.0), _KAE(0.0, rhs => -1.0))
function Base.:(*)(lhs::VariableRef, rhs::KnownRef)
    result = zero(DQE)
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# VariableRef--_DecisionAffExpr{C}
Base.:(+)(lhs::VariableRef, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(_VariableAffExpr{C}(zero(C), lhs => 1.), copy(rhs), zero(_KnownAffExpr{C}))
Base.:(-)(lhs::VariableRef, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(_VariableAffExpr{C}(zero(C), lhs => 1.), -rhs, zero(_KnownAffExpr{C}))
function Base.:(*)(lhs::VariableRef, rhs::_DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# VariableRef--_DecisionQuadExpr{C}
function Base.:(+)(lhs::VariableRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::VariableRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# VariableRef--_KnownAffExpr{C}
Base.:(+)(lhs::VariableRef, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(_VariableAffExpr{C}(zero(C), lhs => 1.), zero(_DecisionAffExpr{C}), copy(rhs))
Base.:(-)(lhs::VariableRef, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(_VariableAffExpr{C}(zero(C), lhs => 1.), zero(_DecisionAffExpr{C}), -rhs)
function Base.:(*)(lhs::VariableRef, rhs::_KnownAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# VariableRef--_KnownQuadExpr{C}
function Base.:(+)(lhs::VariableRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::VariableRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# VariableRef--DecisionAffExpr{C}
Base.:(+)(lhs::VariableRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(lhs + rhs.variables, copy(rhs.decisions), copy(rhs.knowns))
Base.:(-)(lhs::VariableRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(lhs - rhs.variables, -rhs.decisions, -rhs.knowns)
function Base.:(*)(lhs::VariableRef, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end
Base.:/(lhs::VariableRef, rhs::DecisionAffExpr) = error("Cannot divide a variable by an affine expression")

# VariableRef--DecisionQuadExpr{C}
function Base.:(+)(lhs::VariableRef, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::VariableRef, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::VariableRef, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by a variable")

#=
    DecisionRef
=#

# DecisionRef
Base.:(-)(lhs::DecisionRef) = DAE(_VAE(0.0), _DAE(0.0, lhs => -1.0), _KAE(0.0))

# DecisionRef--_Constant
Base.:(+)(lhs::DecisionRef, rhs::_Constant) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionRef, rhs::_Constant) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionRef, rhs::_Constant) = (*)(rhs, lhs)
Base.:(/)(lhs::DecisionRef, rhs::_Constant) = (*)(1.0 / rhs, lhs)

# DecisionRef--VariableRef
Base.:(+)(lhs::DecisionRef, rhs::VariableRef) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionRef, rhs::VariableRef) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionRef, rhs::VariableRef) = (*)(rhs, lhs)

# DecisionRef--DecisionRef
Base.:(+)(lhs::DecisionRef, rhs::DecisionRef) = DAE(_VAE(0.0), _DAE(0.0, lhs => 1.0, rhs => +1.0), _KAE(0.0))
Base.:(-)(lhs::DecisionRef, rhs::DecisionRef) = DAE(_VAE(0.0), _DAE(0.0, lhs => 1.0, rhs => -1.0), _KAE(0.0))
function Base.:(*)(lhs::DecisionRef, rhs::DecisionRef)
    result = zero(DQE)
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionRef--KnownRef
Base.:(+)(lhs::DecisionRef, rhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0, lhs => 1.0), _KAE(0.0, rhs => +1.0))
Base.:(-)(lhs::DecisionRef, rhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0, lhs => 1.0), _KAE(0.0, rhs => -1.0))
function Base.:(*)(lhs::DecisionRef, rhs::KnownRef)
    result = zero(DQE)
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionRef--_VariableAffExpr{C}
Base.:(+)(lhs::DecisionRef, rhs::_VariableAffExpr{C}) where C = DecisionAffExpr{C}(rhs, _DecisionAffExpr{C}(zero(C), lhs => 1.), zero(_KnownAffExpr{C}))
Base.:(-)(lhs::DecisionRef, rhs::_VariableAffExpr{C}) where C = DecisionAffExpr{C}(-rhs, _DecisionAffExpr{C}(zero(C), lhs => 1.), zero(_KnownAffExpr{C}))
function Base.:(*)(lhs::DecisionRef, rhs::_VariableAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionRef--_VariableQuadExpr{C}
function Base.:(+)(lhs::DecisionRef, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionRef, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# DecisionRef--_DecisionAffExpr{C}
Base.:(+)(lhs::DecisionRef, rhs::_DecisionAffExpr{C}) where C = (+)(_DecisionAffExpr{C}(zero(C), lhs => 1.0),  rhs)
Base.:(-)(lhs::DecisionRef, rhs::_DecisionAffExpr{C}) where C = (+)(_DecisionAffExpr{C}(zero(C), lhs => 1.0), -rhs)
function Base.:(*)(lhs::DecisionRef, rhs::_DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionRef--_DecisionQuadExpr{C}
function Base.:(+)(lhs::DecisionRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expressoin(result, lhs)
    JuMP.add_to_expressoin(result, -1., rhs)
    return result
end

# DecisionRef--_KnownAffExpr{C}
Base.:(+)(lhs::DecisionRef, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), _DecisionAffExpr{C}(zero(C), lhs => 1), copy(rhs))
Base.:(-)(lhs::DecisionRef, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), _DecisionAffExpr{C}(zero(C), lhs => 1.), -rhs)
function Base.:(*)(lhs::DecisionRef, rhs::_KnownAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionRef--_KnownQuadExpr{C}
function Base.:(+)(lhs::DecisionRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# DecisionRef--DecisionAffExpr{C}
Base.:(+)(lhs::DecisionRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(rhs.variables), lhs+rhs.decisions, copy(rhs.knowns))
Base.:(-)(lhs::DecisionRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(-rhs.variables, lhs-rhs.decisions, -rhs.knowns)
function Base.:(*)(lhs::DecisionRef, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end
Base.:/(lhs::DecisionRef, rhs::DecisionAffExpr) = error("Cannot divide a decision by an affine expression")

# DecisionRef--DecisionQuadExpr{C}
function Base.:(+)(lhs::DecisionRef, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::DecisionRef, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::DecisionRef, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by a variable")

#=
    KnownRef
=#

# KnownRef
Base.:(-)(lhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0), _KAE(0.0, lhs => -1.0))

# KnownRef--_Constant
Base.:(+)(lhs::KnownRef, rhs::_Constant) = (+)(rhs, lhs)
Base.:(-)(lhs::KnownRef, rhs::_Constant) = (+)(-rhs, lhs)
Base.:(*)(lhs::KnownRef, rhs::_Constant) = (*)(rhs, lhs)
Base.:(/)(lhs::KnownRef, rhs::_Constant) = (*)(1.0 / rhs, lhs)

# KnownRef--VariableRef
Base.:(+)(lhs::KnownRef, rhs::VariableRef) = (+)(rhs, lhs)
Base.:(-)(lhs::KnownRef, rhs::VariableRef) = (+)(-rhs, lhs)
Base.:(*)(lhs::KnownRef, rhs::VariableRef) = (*)(rhs, lhs)

# KnownRef--DecisionRef
Base.:(+)(lhs::KnownRef, rhs::DecisionRef) = (+)(rhs, lhs)
Base.:(-)(lhs::KnownRef, rhs::DecisionRef) = (+)(-rhs, lhs)
Base.:(*)(lhs::KnownRef, rhs::DecisionRef) = (*)(rhs, lhs)

# KnownRef--KnownRef
Base.:(+)(lhs::KnownRef, rhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0), _KAE(0.0, lhs => 1.0, rhs => +1.0))
Base.:(-)(lhs::KnownRef, rhs::KnownRef) = DAE(_VAE(0.0), _DAE(0.0), _KAE(0.0, lhs => 1.0, rhs => -1.0))
function Base.:(*)(lhs::KnownRef, rhs::KnownRef)
    result = zero(DQE)
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# KnownRef--_VariableAffExpr{C}
Base.:(+)(lhs::KnownRef, rhs::_VariableAffExpr{C}) where C = DecisionAffExpr{C}(copy(rhs), zero(_DecisionAffExpr{C}), _KnownAffExpr{C}(zero(C), lhs => 1.))
Base.:(-)(lhs::KnownRef, rhs::_VariableAffExpr{C}) where C = DecisionAffExpr{C}(-rhs, zero(_DecisionAffExpr{C}), _KnownAffExpr{C}(zero(C), lhs => 1.))
function Base.:(*)(lhs::KnownRef, rhs::_VariableAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# KnownRef--_VariableQuadExpr{C}
function Base.:(+)(lhs::KnownRef, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::KnownRef, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# KnownRef--_DecisionAffExpr{C}
Base.:(+)(lhs::KnownRef, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), rhs, _KnownAffExpr{C}(zero(C), lhs => 1.))
Base.:(-)(lhs::KnownRef, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), -rhs, _KnownAffExpr{C}(zero(C), lhs => 1.))
function Base.:(*)(lhs::KnownRef, rhs::_DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# KnownRef--_DecisionQuadExpr{C}
function Base.:(+)(lhs::KnownRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::KnownRef, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expressoin(result, lhs)
    JuMP.add_to_expressoin(result, -1., rhs)
    return result
end

# KnownRef--_KnownAffExpr{C}
Base.:(+)(lhs::KnownRef, rhs::_KnownAffExpr{C}) where C = (+)(_KnownAffExpr{C}(zero(C), lhs => 1.0),  rhs)
Base.:(-)(lhs::KnownRef, rhs::_KnownAffExpr{C}) where C = (+)(_KnownAffExpr{C}(zero(C), lhs => 1.0), -rhs)
function Base.:(*)(lhs::KnownRef, rhs::_KnownAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# KnownRef--_KnownQuadExpr{C}
function Base.:(+)(lhs::KnownRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::KnownRef, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# KnownRef--DecisionAffExpr{C}
Base.:(+)(lhs::KnownRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(rhs.variables), copy(rhs.decisions), lhs+rhs.knowns)
Base.:(-)(lhs::KnownRef, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(-rhs.variables, -rhs.decisions, lhs-rhs.knowns)
function Base.:(*)(lhs::KnownRef, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end
Base.:/(lhs::KnownRef, rhs::DecisionAffExpr) = error("Cannot divide a known decision by an affine expression")

# KnownRef--DecisionQuadExpr{C}
function Base.:(+)(lhs::KnownRef, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::KnownRef, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::KnownRef, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by a variable")

#=
    _VariableAffExpr{C}
=#

# _VariableAffExpr--DecisionRef/KnownRef
Base.:(+)(lhs::_VariableAffExpr, rhs::Union{DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_VariableAffExpr, rhs::Union{DecisionRef, KnownRef}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_VariableAffExpr, rhs::Union{DecisionRef, KnownRef}) = (*)(rhs, lhs)

# _VariableAffExpr{C}--_DecisionAffExpr{C}
Base.:(+)(lhs::_VariableAffExpr{C}, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(lhs), copy(rhs), zero(_KnownAffExpr{C}))
Base.:(-)(lhs::_VariableAffExpr{C}, rhs::_DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(lhs), -rhs, zero(_KnownAffExpr{C}))
function Base.:(*)(lhs::_VariableAffExpr{C}, rhs::_DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _VariableAffExpr{C}--_DecisionQuadExpr{C}
function Base.:(+)(lhs::_VariableAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::_VariableAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _VariableAffExpr{C}--_KnownAffExpr{C}
Base.:(+)(lhs::_VariableAffExpr{C}, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(copy(lhs), zero(_DecisionAffExpr{C}), copy(rhs))
Base.:(-)(lhs::_VariableAffExpr{C}, rhs::_KnownAffExpr{C}) where C = DecisionAffExpr{C}(copy(lhs), zero(_DecisionAffExpr{C}), -rhs)
function Base.:(*)(lhs::_VariableAffExpr{C}, rhs::_KnownAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _VariableAffExpr{C}--_KnownQuadExpr{C}
function Base.:(+)(lhs::_VariableAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_VariableAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end
Base.:(*)(lhs::_VariableAffExpr, rhs::_KnownQuadExpr) = error("Cannot multiply a quadratic expression by an affine expression")

# VariableRef{C}--DecisionAffExpr{C}
Base.:(+)(lhs::_VariableAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(lhs+rhs.variables, copy(rhs.decisions), copy(rhs.knowns))
Base.:(-)(lhs::_VariableAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(lhs-rhs.variables, -rhs.decisions, -rhs.knowns)
function Base.:(*)(lhs::_VariableAffExpr{C}, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _VariableAffExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::_VariableAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::_VariableAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::_VariableAffExpr, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by an affine expression")

#=
    _DecisionAffExpr{C}
=#

# _DecisionAffExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::_DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (*)(rhs, lhs)

# _DecisionAffExpr--_VariableAffExpr
Base.:(+)(lhs::_DecisionAffExpr, rhs::_VariableAffExpr) = (+)(rhs, lhs)
Base.:(-)(lhs::_DecisionAffExpr, rhs::_VariableAffExpr) = (+)(-rhs, lhs)
Base.:(*)(lhs::_DecisionAffExpr, rhs::_VariableAffExpr) = (*)(rhs, lhs)

# _DecisionAffExpr{C}--_VariableQuadExpr{C}
function Base.:(+)(lhs::_DecisionAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_DecisionAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _DecisionAffExpr--_KnownAffExpr
Base.:(+)(lhs::_DecisionAffExpr, rhs::_KnownAffExpr) = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), copy(lhs), copy(rhs))
Base.:(-)(lhs::_DecisionAffExpr, rhs::_KnownAffExpr) = DecisionAffExpr{C}(zero(_VariableAffExpr{C}), copy(lhs), -rhs)
function Base.:(*)(lhs::_DecisionAffExpr{C}, rhs::_KnownAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _DecisionAffExpr{C}--_KnownQuadExpr{C}
function Base.:(+)(lhs::_DecisionAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_DecisionAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _DecisionAffExpr{C}--DecisionAffExpr{C}
Base.:(+)(lhs::_DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(rhs.variables), lhs+rhs.decisions, copy(rhs.knowns))
Base.:(-)(lhs::_DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(-rhs.variables, lhs-rhs.decisions, -rhs.knowns)
function Base.:(*)(lhs::_DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _DecisionAffExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::_DecisionAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::_DecisionAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::_DecisionAffExpr, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by an affine expression")

#=
    _KnownAffExpr{C}
=#

# _KnownAffExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::_KnownAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_KnownAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (*)(rhs, lhs)

# _KnownAffExpr--_VariableAffExpr
Base.:(+)(lhs::_KnownAffExpr, rhs::_VariableAffExpr) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownAffExpr, rhs::_VariableAffExpr) = (+)(-rhs, lhs)
Base.:(*)(lhs::_KnownAffExpr, rhs::_VariableAffExpr) = (*)(rhs, lhs)

# _KnownAffExpr{C}--_VariableQuadExpr{C}
function Base.:(+)(lhs::_KnownAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_KnownAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _KnownAffExpr--_DecisionAffExpr
Base.:(+)(lhs::_KnownAffExpr, rhs::_DecisionAffExpr) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownAffExpr, rhs::_DecisionAffExpr) = (+)(-rhs, lhs)
Base.:(*)(lhs::_KnownAffExpr, rhs::_DecisionAffExpr) = (*)(rhs, lhs)

# _KnownAffExpr{C}--_DecisionQuadExpr{C}
function Base.:(+)(lhs::_KnownAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::_KnownAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expressoin(result, lhs)
    JuMP.add_to_expressoin(result, -1., rhs)
    return result
end

# _KnownAffExpr{C}--DecisionAffExpr{C}
Base.:(+)(lhs::_KnownAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(copy(rhs.variables), copy(rhs.decisions), lhs+rhs.knowns)
Base.:(-)(lhs::_KnownAffExpr{C}, rhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(-rhs.variables, -rhs.decisions, lhs-rhs.knowns)
function Base.:(*)(lhs::_KnownAffExpr{C}, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# _KnownAffExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::_KnownAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::_KnownAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end

#=
    DecisionAffExpr{C}
=#

Base.:(-)(lhs::DecisionAffExpr{C}) where C = DecisionAffExpr{C}(-lhs.variables, -lhs.decisions, -lhs.knowns)

# DecisionAffExpr--_Constant
Base.:(+)(lhs::DecisionAffExpr, rhs::_Constant) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionAffExpr, rhs::_Constant) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionAffExpr, rhs::_Constant) = (*)(rhs, lhs)
Base.:/(lhs::DecisionAffExpr, rhs::_Constant) = map_coefficients(c -> c/rhs, lhs)
function Base.:^(lhs::DecisionAffExpr{C}, rhs::Integer) where C
    if rhs == 2
        return lhs*lhs
    elseif rhs == 1
        return convert(DecisionQuadExpr{C}, lhs)
    elseif rhs == 0
        return one(GenericQuadExpr{C})
    else
        error("Only exponents of 0, 1, or 2 are currently supported.")
    end
end
Base.:^(lhs::DecisionAffExpr, rhs::_Constant) = error("Only exponents of 0, 1, or 2 are currently supported.")

# DecisionAffExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionAffExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (*)(rhs, lhs)
Base.:/(lhs::DecisionAffExpr, rhs::VariableRef) = error("Cannot divide affine expression by a variable")
Base.:/(lhs::DecisionAffExpr, rhs::DecisionRef) = error("Cannot divide affine expression by a decision")
Base.:/(lhs::DecisionAffExpr, rhs::KnownRef) = error("Cannot divide affine expression by a known decision s")

# DecisionAffExpr--_VariableAffExpr/_DecisionAffExpr/_KnownAffExpr
Base.:(+)(lhs::DecisionAffExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionAffExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr}) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionAffExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr}) = (*)(rhs, lhs)

# DecisionAffExpr{C}--DecisionAffExpr{C}
Base.:(+)(lhs::DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C =
    DecisionAffExpr{C}(lhs.variables+rhs.variables,
                       lhs.decisions+rhs.decisions,
                       lhs.knowns+rhs.knowns)
Base.:(-)(lhs::DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C =
    DecisionAffExpr{C}(lhs.variables-rhs.variables,
                       lhs.decisions-rhs.decisions,
                       lhs.knowns-rhs.knowns)
function Base.:(*)(lhs::DecisionAffExpr{C}, rhs::DecisionAffExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs, rhs)
    return result
end

# DecisionAffExpr{C}--_VariableQuadExpr{C}
function Base.:(+)(lhs::DecisionAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionAffExpr{C}, rhs::_VariableQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# DecisionAffExpr{C}--_DecisionQuadExpr{C}
function Base.:(+)(lhs::DecisionAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionAffExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# DecisionAffExpr{C}--_KnownQuadExpr{C}
function Base.:(+)(lhs::DecisionAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::DecisionAffExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

Base.:(*)(lhs::DecisionAffExpr, rhs::Union{_VariableQuadExpr, _DecisionQuadExpr, _KnownQuadExpr}) = error("Cannot multiply a quadratic expression by an affine expression")

# DecisionAffExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::DecisionAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::DecisionAffExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end
Base.:(*)(lhs::DecisionAffExpr, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by an affine expression")

#=
    _VariableQuadExpr{C}
=#

# _VariableQuadExpr--DecisionRef/KnownRef
Base.:(+)(lhs::_VariableQuadExpr, rhs::Union{DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_VariableQuadExpr, rhs::Union{DecisionRef, KnownRef}) = (+)(-rhs, lhs)

# _VariableQuadExpr--_DecisionAffExpr/_KnownAffExpr/DecisionAffExpr
Base.:(+)(lhs::_VariableQuadExpr, rhs::Union{_DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::_VariableQuadExpr, rhs::Union{_DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_VariableQuadExpr, rhs::DecisionAffExpr) = error("Cannot multiply a quadratic expression by an affine expression")

# _VariableQuadExpr{C}--_DecisionQuadExpr{C}
function Base.:(+)(lhs::_VariableQuadExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expressoin(result, rhs)
    return result
end
function Base.:(-)(lhs::_VariableQuadExpr{C}, rhs::_DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _VariableQuadExpr{C}--_KnownQuadExpr{C}
function Base.:(+)(lhs::_VariableQuadExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_VariableQuadExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _VariableQuadExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::_VariableQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::_VariableQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end

#=
    _DecisionQuadExpr{C}
=#

# _DecisionQuadExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::_DecisionQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_DecisionQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)

# _DecisionQuadExpr--_VariableAffExpr
Base.:(+)(lhs::_DecisionQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::_DecisionQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_DecisionQuadExpr, rhs::DecisionAffExpr) = error("Cannot multiply a quadratic expression by an affine expression")

# _DecisionQuadExpr{C}--_VariableQuadExpr{C}
Base.:(+)(lhs::_DecisionQuadExpr, rhs::_VariableQuadExpr) = (+)(rhs, lhs)
Base.:(-)(lhs::_DecisionQuadExpr, rhs::_VariableQuadExpr) = (+)(-rhs, lhs)

# _DecisionQuadExpr{C}--_KnownQuadExpr{C}
function Base.:(+)(lhs::_DecisionQuadExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, rhs)
    return result
end
function Base.:(-)(lhs::_DecisionQuadExpr{C}, rhs::_KnownQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, lhs)
    JuMP.add_to_expression!(result, -1., rhs)
    return result
end

# _DecisionQuadExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::_DecisionQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(rhs)
    JuMP.add_to_expression!(result, lhs)
    return result
end
function Base.:(-)(lhs::_DecisionQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs)
    return result
end

#=
    _KnownQuadExpr{C}
=#

# _KnownQuadExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::_KnownQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)

# _KnownQuadExpr--_VariableAffExpr/_DecisionAffExpr/_KnownAffExpr/DecisionAffExpr
Base.:(+)(lhs::_KnownQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(-rhs, lhs)
Base.:(*)(lhs::_KnownQuadExpr, rhs::DecisionAffExpr) = error("Cannot multiply a quadratic expression by an affine expression")

# _KnownQuadExpr--_VariableQuadExpr/_DecisionQuadExpr
Base.:(+)(lhs::_KnownQuadExpr, rhs::Union{_VariableQuadExpr, _DecisionQuadExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownQuadExpr, rhs::Union{_VariableQuadExpr, _DecisionQuadExpr}) = (+)(-rhs, lhs)

# _KnownQuadExpr{C}--DecisionQuadExpr{C}
Base.:(+)(lhs::_KnownQuadExpr, rhs::DecisionQuadExpr) = (+)(rhs, lhs)
Base.:(-)(lhs::_KnownQuadExpr, rhs::DecisionQuadExpr) = (+)(-rhs, lhs)

#=
    DecisionQuadExpr{C}
=#

function Base.:(-)(lhs::DecisionQuadExpr{C}) where C
    result = zero(DecisionQuadExpr{C})
    JuMP.add_to_expression!(result, -1., lhs.variables)
    JuMP.add_to_expression!(result, -1., lhs.decisions)
    JuMP.add_to_expression!(result, -1., lhs.knowns)
    for (vars, coef) in lhs.cross_terms
        JuMP._add_or_set!(result.cross_terms, vars, -coef)
    end
    for (vars, coef) in lhs.known_variable_terms
        JuMP._add_or_set!(result.known_variable_terms, vars, -coef)
    end
    for (vars, coef) in lhs.known_decision_terms
        JuMP._add_or_set!(result.known_decision_terms, vars, -coef)
    end
    return result
end

# DecisionQuadExpr--_Constant
Base.:(+)(lhs::DecisionQuadExpr, rhs::_Constant) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionQuadExpr, rhs::_Constant) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionQuadExpr, rhs::_Constant) = (*)(rhs, lhs)
Base.:(/)(lhs::DecisionQuadExpr, rhs::_Constant) = (*)(inv(rhs), lhs)

# DecisionQuadExpr--VariableRef/DecisionRef/KnownRef
Base.:(+)(lhs::DecisionQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = (+)(-rhs, lhs)
Base.:(/)(lhs::DecisionQuadExpr, rhs::Union{VariableRef, DecisionRef, KnownRef}) = error("Cannot divide a quadratic expression by a variable")

# DecisionQuadExpr--_VariableAffExpr/_DecisionAffExpr/_KnownAffExpr
Base.:(+)(lhs::DecisionQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = (+)(-rhs, lhs)
Base.:(*)(lhs::DecisionQuadExpr, rhs::Union{_VariableAffExpr, _DecisionAffExpr, _KnownAffExpr, DecisionAffExpr}) = error("Cannot multiply a quadratic expression by an affine expression")
Base.:(/)(lhs::DecisionQuadExpr, rhs::Union{GenericAffExpr, DecisionAffExpr}) = error("Cannot divide a quadratic expression by an affine expression")

# DecisionQuadExpr{C}--_VariableQuadExpr/_DecisionQuadExpr/_KnownQuadExpr
Base.:(+)(lhs::DecisionQuadExpr, rhs::Union{_VariableQuadExpr, _DecisionQuadExpr, _KnownQuadExpr}) = (+)(rhs, lhs)
Base.:(-)(lhs::DecisionQuadExpr, rhs::Union{_VariableQuadExpr, _DecisionQuadExpr, _KnownQuadExpr}) = (+)(-rhs, lhs)

# DecisionQuadExpr{C}--DecisionQuadExpr{C}
function Base.:(+)(lhs::DecisionQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = copy(lhs)
    JuMP.add_to_expression!(result, rhs.variables)
    JuMP.add_to_expression!(result, rhs.decisions)
    JuMP.add_to_expression!(result, rhs.knowns)
    add_cross_terms!(result_terms, terms) = begin
        for (vars, coef) in terms
            JuMP._add_or_set!(result_terms, vars, coef)
        end
    end
    add_cross_terms!(result.cross_terms, rhs.cross_terms)
    add_cross_terms!(result.known_variable_terms, rhs.known_variable_terms)
    add_cross_terms!(result.known_decision_terms, rhs.known_decision_terms)
    return result
end
function Base.:(-)(lhs::DecisionQuadExpr{C}, rhs::DecisionQuadExpr{C}) where C
    result = -rhs
    JuMP.add_to_expression!(result, lhs.variables)
    JuMP.add_to_expression!(result, lhs.decisions)
    JuMP.add_to_expression!(result, lhs.knowns)
    add_cross_terms!(result_terms, terms) = begin
        for (vars, coef) in terms
            JuMP._add_or_set!(result_terms, vars, -coef)
        end
    end
    add_cross_terms!(result.cross_terms, lhs.cross_terms)
    add_cross_terms!(result.known_variable_terms, lhs.known_variable_terms)
    add_cross_terms!(result.known_decision_terms, lhs.known_decision_terms)
    return result
end
Base.:(*)(lhs::DecisionQuadExpr, rhs::DecisionQuadExpr) = error("Cannot multiply a quadratic expression by a quadratic expression")


function getoutcomes(train_data::Data{Normalized}, new_data::Data{Normalized},
                     method::Val)
  model = train(train_data, method)
  getoutcomes(new_data, model)
end
function getoutcomes(train_X, train_y, train_T, test_X, method::Symbol)
  train_d = Data(train_X, train_y, train_T)
  n_test = size(test_X, 1)
  # Put junk in the test data container
  test_d = Data(test_X, fill(NaN, n_test), zeros(Int, n_test))

  train_d_norm, μ, σ = normalize_data(train_d)
  test_d_norm = normalize_data(test_d, μ, σ)

  getoutcomes(train_d_norm, test_d_norm, Val{method}())
end

function makeprescriptions(
    outcomes::Matrix{Float64},
    allowed_prescriptions::Vector{Vector{Int}}=[collect(1:size(outcomes, 2))
                                                for _ in 1:size(outcomes, 1)],
  )
  map(1:size(outcomes, 1)) do i
    idx = argmin(outcomes[i, allowed_prescriptions[i]])
    allowed_prescriptions[i][idx]
  end
end

function predictoutcomes(outcomes::Matrix{Float64}, prescriptions::Vector{Int})
  map(1:length(prescriptions)) do i
    outcomes[i, prescriptions[i]]
  end
end