using CUDA: CUDA, @cuda
using DataFrames: DataFrame, Tables
using LoopVectorization: @avx

export satisfies, satisfies_all

#####################
# Circuit logical evaluation
#####################

# evaluate a circuit as a function
function (root::LogicCircuit)(data...)
    satisfies(root, data...)
end

"Evaluate satisfaction of the circuit bottom-up for a given input"
satisfies(root::LogicCircuit, data::Real...) =
    satisfies(root, collect(data))

satisfies(root::LogicCircuit, data::AbstractVector{Bool}) =
    satisfies(root, DataFrame(reshape(BitVector(data), 1, :), :auto))[1]

satisfies(root::LogicCircuit, data::AbstractVector{<:AbstractFloat}) =
    satisfies(root, DataFrame(reshape(data, 1, :), :auto))[1]

satisfies(circuit::LogicCircuit, data::DataFrame) =
    satisfies(same_device(BitCircuit(circuit, data), data) , data)

function satisfies(circuit::BitCircuit, data::DataFrame)::AbstractVector
    output = satisfies_all(circuit,data)[:,end]
    if isbinarydata(data)
        return AbstractBitVector(output, num_examples(data))
    else
        @assert isfpdata(data) "Only floating point and binary flows are supported"
        return output
    end
end

#####################
# Circuit evaluation of *all* nodes in circuit
#####################

"Evaluate the circuit bottom-up for a given input and return the value of all nodes"
function satisfies_all(circuit::BitCircuit, data, reuse=nothing)
    @assert num_features(data) == num_features(circuit)
    @assert iscomplete(data)
    values = init_satisfies(data, reuse, num_nodes(circuit))
    satisfies_layers(circuit, values)
    return values
end

"Initialize values from the data (data frames)"
function init_satisfies(data, reuse, num_nodes)
    if isbinarydata(data)
        flowtype = isgpu(data) ? CuMatrix{UInt} : Matrix{UInt}
        values = similar!(reuse, flowtype, num_chunks(data), num_nodes)

        @views values[:,TRUE_BITS] .= typemax(UInt)
        @views values[:,FALSE_BITS] .= typemin(UInt)
        nfeatures = num_features(data)
        for i=1:nfeatures
            @views values[:,2+i] .= chunks(data,i)
            @views values[:,2+nfeatures+i] .= .~ chunks(data,i)
        end
        # warning: there are now some 1 bits beyond the BitVector mask.
        # therefore, reset those to 0
        @views values[end,1:2+2*num_features(data)] .&= _msk_end(data)

    else
        @assert isfpdata(data) "Only floating point and binary flows are supported"
        flowtype = isgpu(data) ? CuMatrix{eltype(data)} : Matrix{eltype(data)}
        values = similar!(reuse, flowtype, num_examples(data), num_nodes)
        @views values[:,TRUE_BITS] .= one(eltype(values))
        @views values[:,FALSE_BITS] .= zero(eltype(values))
        nfeatures = num_features(data)
        for i=1:nfeatures
            @views values[:,2+i] .= feature_values(data,i)
            @views values[:,2+nfeatures+i] .= one(eltype(values)) .- feature_values(data,i)
        end

    end
    return values
end

# upward pass helpers on CPU

"Evaluate the layers of a bit circuit on the CPU (SIMD & multi-threaded)"
function satisfies_layers(circuit::BitCircuit, values::Matrix)
    els = circuit.elements
    for layer in circuit.layers[2:end]
        Threads.@threads for dec_id in layer
            j = @inbounds circuit.nodes[1,dec_id]
            els_end = @inbounds circuit.nodes[2,dec_id]
            if j == els_end
                assign_value(values, dec_id, els[2,j], els[3,j])
                j += 1
            else
                assign_value(values, dec_id, els[2,j], els[3,j], els[2,j+1], els[3,j+1])
                j += 2
            end
            while j <= els_end
                if j == els_end
                    accum_value(values, dec_id, els[2,j], els[3,j])
                    j += 1
                else
                    accum_value(values, dec_id, els[2,j], els[3,j], els[2,j+1], els[3,j+1])
                    j += 2
                end
            end
        end
    end
end

assign_value(v::Matrix{<:AbstractFloat}, i, e1p, e1s) =
    @views @. @avx v[:,i] = v[:,e1p] * v[:,e1s]

accum_value(v::Matrix{<:AbstractFloat}, i, e1p, e1s) =
    @views @. v[:,i] += v[:,e1p] * v[:,e1s] # adding @avx crashes macro

assign_value(v::Matrix{<:AbstractFloat}, i, e1p, e1s, e2p, e2s) =
    @views @. @avx v[:,i] = v[:,e1p] * v[:,e1s] + v[:,e2p] * v[:,e2s]

accum_value(v::Matrix{<:AbstractFloat}, i, e1p, e1s, e2p, e2s) =
    @views @. v[:,i] += v[:,e1p] * v[:,e1s] + v[:,e2p] * v[:,e2s] # adding @avx crashes macro

assign_value(v::Matrix{<:Unsigned}, i, e1p, e1s) =
    @views @. @avx v[:,i] = v[:,e1p] & v[:,e1s]

accum_value(v::Matrix{<:Unsigned}, i, e1p, e1s) =
    @views @. v[:,i] |= v[:,e1p] & v[:,e1s] # adding @avx crashes macro

assign_value(v::Matrix{<:Unsigned}, i, e1p, e1s, e2p, e2s) =
    @views @. @avx v[:,i] = v[:,e1p] & v[:,e1s] | v[:,e2p] & v[:,e2s]

accum_value(v::Matrix{<:Unsigned}, i, e1p, e1s, e2p, e2s) =
    @views @. v[:,i] |= v[:,e1p] & v[:,e1s] | v[:,e2p] & v[:,e2s] # adding @avx crashes macro

# upward pass helpers on GPU

"Evaluate the layers of a bit circuit on the GPU"
function satisfies_layers(circuit::BitCircuit, values::CuMatrix)
    CUDA.@sync for layer in circuit.layers[2:end]
        num_examples = size(values, 1)
        num_decision_sets = length(layer)

        kernel = @cuda name="satisfies_layers_cuda" launch=false satisfies_layers_cuda(layer, circuit.nodes, circuit.elements, values)
        config = launch_configuration(kernel.fun)
        threads, blocks = balance_threads_2d(num_examples, num_decision_sets, config.threads)
        kernel(layer, circuit.nodes, circuit.elements, values; threads, blocks)
    end
end

"assign threads to examples and decision nodes so that everything is a power of 2"
function balance_threads(num_examples, num_decisions, total_log2)
    ratio = num_examples / num_decisions
    k = ceil(Int, (log2(ratio) + total_log2)/2)
    k = min(max(0, k), total_log2)
    l = total_log2-k
    (2^k, 2^l)
end

"""

"""
function balance_threads_2d(num_examples, num_decisions, total_threads_per_block)
    lsb(n) = n ⊻ ( n& (n-1))
    ratio_diff(a, b) =  ceil(Int, num_examples/a) - ceil(Int, num_decisions/b)

    n_lsb = lsb(total_threads_per_block)
    options_d1 = [Int32(2^i) for i = 0 : log2(n_lsb)]
    append!(options_d1, [total_threads_per_block / n_lsb * Int32(2^i) for i = 0 : log2(n_lsb)])
    options_d2 = [Int32(total_threads_per_block / d1) for d1 in options_d1]

    best_d1 = options_d1[1]
    best_d2 = options_d2[1]
    best_ratio = ratio_diff(best_d1, best_d2)

    for (d1, d2) in zip(options_d1, options_d2)
        cur_ratio = ratio_diff(d1, d2)
        if abs(best_ratio) > abs(cur_ratio)
            best_d1 = d1
            best_d2 = d2
            best_ratio = cur_ratio
        end
    end
    threads = (best_d1, best_d2)
    blocks = (ceil(Int, num_examples / threads[1]),
                ceil(Int, num_decisions / threads[2]))

    threads, blocks
end

"CUDA kernel for circuit evaluation"
function satisfies_layers_cuda(layer, nodes, elements, values)
    index_x = (blockIdx().x - 1) * blockDim().x + threadIdx().x
    index_y = (blockIdx().y - 1) * blockDim().y + threadIdx().y
    stride_x = blockDim().x * gridDim().x
    stride_y = blockDim().y * gridDim().y
    for j = index_x:stride_x:size(values,1)
        for i = index_y:stride_y:length(layer)
            decision_id = @inbounds layer[i]
            k = @inbounds nodes[1,decision_id]
            els_end = @inbounds nodes[2,decision_id]
            @inbounds values[j, decision_id] =
                el_value(values[j, elements[2,k]], values[j, elements[3,k]])
            while k < els_end
                k += 1
                @inbounds accum_el_value(values, j, decision_id, values[j,
                            elements[2,k]], values[j, elements[3,k]])
            end # would loop unrolling help here as on CPU? probably not?
        end
    end
    return nothing
end

el_value(p::AbstractFloat, s) = p * s
el_value(p::Unsigned, s) = p & s

accum_el_value(values, j, decision_id, p::AbstractFloat, s) =
    @inbounds values[j, decision_id] += el_value(p, s)
accum_el_value(values, j, decision_id, p::Unsigned, s) =
    @inbounds values[j, decision_id] |= el_value(p, s)


module QuadraticModelsXpress

export xpress

using QuadraticModels, SolverCore, SparseMatricesCOO
using LinearAlgebra, SparseArrays
using Xpress

const xpress_statuses = Dict(0 => :unknown,
                             1 => :acceptable,
                             2 => :infeasible,
                             3 => :exception,
                             4 => :max_eval,
                             5 => :unbounded,
                             6 => :exception,
                             7 => :exception,
                             8 => :exception)

function sparse_csr(I, J, V, m=maximum(I), n=maximum(J))
    csrrowptr = zeros(Int, m+1)
    # Compute the CSR form's row counts and store them shifted forward by one in csrrowptr
    coolen = length(I)
    min(length(J), length(V)) >= coolen || throw(ArgumentError("J and V need length >= length(I) = $coolen"))
    @inbounds for k in 1:coolen
        Ik = I[k]
        if 1 > Ik || m < Ik
            throw(ArgumentError("row indices I[k] must satisfy 1 <= I[k] <= m"))
        end
        csrrowptr[Ik+1] += 1
    end

    # Compute the CSR form's rowptrs and store them shifted forward by one in csrrowptr
    countsum = 1
    csrrowptr[1] = 1
    @inbounds for i in 2:(m+1)
        overwritten = csrrowptr[i]
        csrrowptr[i] = countsum
        countsum += overwritten
    end

    # Counting-sort the column and nonzero values from J and V into csrcolval and csrnzval
    # Tracking write positions in csrrowptr corrects the row pointers
    csrcolval = zeros(Int, length(I))
    csrnzval = zeros(length(I))
    @inbounds for k in 1:coolen
        Ik, Jk = I[k], J[k]
        if 1 > Jk || n < Jk
            throw(ArgumentError("column indices J[k] must satisfy 1 <= J[k] <= n"))
        end
        csrk = csrrowptr[Ik+1]
        csrrowptr[Ik+1] = csrk + 1
        csrcolval[csrk] = Jk
        csrnzval[csrk] = V[k]
    end
    csrrowptr = csrrowptr[1:end-1]
    return csrrowptr, csrcolval, csrnzval
end

function xpress(QM::QuadraticModel{T, S, M1, M2};
                method="b", kwargs...) where {T, S, M1 <: SparseMatrixCOO, M2 <: SparseMatrixCOO}

    Xpress.init()
    prob = Xpress.XpressProblem()
    # use kwargs change to presolve, scaling and crossover mode
    # example: xpress(QM, presolve=0, bargapstop=1e-8)
    for (k, v) in kwargs
        if k==:presolve
            Xpress.setintcontrol(prob, Xpress.Lib.XPRS_PRESOLVE, v)  # 0 no presolve, 1=presolve
        elseif k==:scaling
            Xpress.setintcontrol(prob, Xpress.Lib.XPRS_SCALING, v)  # 0 no scaling
        elseif k==:crossover
            Xpress.setintcontrol(prob, Xpress.Lib.XPRS_CROSSOVER, v)  # 0 no crossover
        elseif k==:bargapstop
            Xpress.setdblcontrol(prob, Xpress.Lib.XPRS_BARGAPSTOP, v)
        elseif k==:barprimalstop
            Xpress.setdblcontrol(prob, Xpress.Lib.XPRS_BARPRIMALSTOP, v)
        elseif k==:bardualstop
            Xpress.setdblcontrol(prob, Xpress.Lib.XPRS_BARDUALSTOP, v)
        end
    end

    srowtypes = fill(Cchar('A'), QM.meta.ncon)
    if length(QM.meta.jinf) > 0
        error("infeasible bounds in A")
    end
    p_low, p_upp, p_rng, p_fix, p_free = 1, 1, 1, 1, 1
    for j=1:QM.meta.ncon
        if length(QM.meta.jlow) > 0 && QM.meta.jlow[p_low] == j
            srowtypes[j] = Cchar('G')
            if (p_low < length(QM.meta.jlow)) p_low += 1 end
        elseif length(QM.meta.jupp) > 0 && QM.meta.jupp[p_upp] == j
            srowtypes[j] = Cchar('L')
            if (p_upp < length(QM.meta.jupp)) p_upp += 1 end
        elseif length(QM.meta.jrng) > 0 && QM.meta.jrng[p_rng] == j
            srowtypes[j] = Cchar('R')
            if (p_rng < length(QM.meta.jrng)) p_rng += 1 end
        elseif length(QM.meta.jfix) > 0 && QM.meta.jfix[p_fix] == j
            srowtypes[j] = Cchar('E')
            if (p_fix < length(QM.meta.jfix)) p_fix += 1 end
        elseif length(QM.meta.jfree) > 0 && QM.meta.jfree[p_free] == j
            srowtypes[j] = Cchar('N')
            if (p_free < length(QM.meta.jfree)) p_free += 1 end
        else
            error("A error")
        end
    end
    rhs = zeros(QM.meta.ncon)
    drange = zeros(QM.meta.ncon)
    for j = 1:QM.meta.ncon
        if QM.meta.lcon[j] != -Inf && QM.meta.ucon[j] != Inf
            rhs[j] = QM.meta.ucon[j]
            drange[j] = QM.meta.ucon[j] - QM.meta.lcon[j]
        elseif QM.meta.lcon[j] != -Inf && QM.meta.ucon[j] == Inf
            rhs[j] = QM.meta.lcon[j]
        elseif QM.meta.lcon[j] == -Inf && QM.meta.ucon[j] != Inf
            rhs[j] = QM.meta.ucon[j]
        else
            rhs[j] = Xpress.Lib.XPRS_PLUSINFINITY
        end
    end

    A = sparse(QM.data.A.rows, QM.data.A.cols, QM.data.A.vals, QM.meta.ncon, QM.meta.nvar)
    lvar, uvar = zeros(QM.meta.nvar), zeros(QM.meta.nvar)

    for i=1:QM.meta.nvar
        QM.meta.lvar[i] == -Inf ? lvar[i] = Xpress.Lib.XPRS_MINUSINFINITY : lvar[i] = QM.meta.lvar[i]
        QM.meta.uvar[i] == Inf ? uvar[i] = Xpress.Lib.XPRS_PLUSINFINITY : uvar[i] = QM.meta.uvar[i]
    end

    if QM.meta.nnzh > 0
        Xpress.loadqp(prob, QM.meta.name, QM.meta.nvar, QM.meta.ncon, srowtypes, rhs, drange,
                      QM.data.c, convert(Array{Cint,1}, A.colptr.-1), C_NULL,
                      convert(Array{Cint,1}, A.rowval.-1), A.nzval, lvar, uvar, QM.meta.nnzh,
                      convert(Array{Cint,1}, QM.data.H.rows.-1), convert(Array{Cint,1}, QM.data.H.cols.-1),
                      QM.data.H.vals)
    else
        Xpress.loadlp(prob, "", QM.meta.nvar, QM.meta.ncon, srowtypes, rhs, drange,
                      QM.data.c,convert(Array{Cint,1}, A.colptr.-1), C_NULL,convert(Array{Cint,1}, A.rowval.-1),
                      A.nzval, lvar, uvar)
    end
    Xpress.chgobj(prob, [0], [-QM.data.c0])

    start_time = time()
    Xpress.lpoptimize(prob, method)
    elapsed_time = time() - start_time

    x, y, s = zeros(QM.meta.nvar), zeros(QM.meta.ncon), zeros(QM.meta.nvar)
    Xpress.getsol(prob, x, C_NULL, y, s)
    baritcnt = Xpress.getintattrib(prob, Xpress.Lib.XPRS_BARITER)
    objval = Xpress.getdblattrib(prob, Xpress.Lib.XPRS_LPOBJVAL)
    status = Xpress.getintattrib(prob, Xpress.Lib.XPRS_LPSTATUS)
    p_feas = Xpress.getdblattrib(prob, Xpress.Lib.XPRS_BARPRIMALINF)
    d_feas = Xpress.getdblattrib(prob, Xpress.Lib.XPRS_BARDUALINF)

    stats = GenericExecutionStats(get(xpress_statuses, status, :unknown),
                                  QM, solution = x,
                                  objective = objval,
                                  primal_feas = p_feas,
                                  dual_feas = d_feas,
                                  iter = Int64(baritcnt),
                                  multipliers = y,
                                  elapsed_time = elapsed_time)
    Xpress.destroyprob(prob)
    Xpress.free()

    return stats
end

end


#=##############################################################################################
Filename: solve_vlm_bem.jl
Author: Ryan Anderson
Contact: rymanderson@gmail.com
README: `<: Action` function calculates aerodynamic forces on a blade-element momentum-vortex
        lattice system; only accounts for rotor-on-wing effects
=###############################################################################################

"""
    solve_vlm_bem(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol) <: Action

Solves for the aerodynamic forces at each step.

# Arguments:

* `aircraft::Aircraft` : `Aircraft` system object
* `parameters<:Parameters` `Parameters` struct
* `freestream::Freestream` : `Freestream` object
* `environment::Environment` `Environment` object
* `step_range::AbstractArray` : array of steps for which the simulation is run
* `stepi::Int` : index of the current step
* `step_symbol::String` : defines the step, e.g. `alpha` or `time`

`parameters <: Parameters` requires the following elements:

* `omegas::Vector{Float64}` : a vector of length `length(step_range)` containing a vector of rotational velocities for each rotor
* `Js::Array{Float64,2}` : a vector of length `length(step_range)` containing a vector of advance ratios for each rotor
* `Ts::Array{Float64,2}` : a vector of length `length(step_range)` containing a vector of dimensional thrust values for each rotor
* `Qs::Array{Float64,2}` : a vector of length `length(step_range)` containing a vector of dimensional torque values for each rotor
* `us::Vector{Vector{Vector{Float64}}}` : each [i][j][k]th element is the axial induced velocity at ith step of the jth rotor at the kth radial section
* `vs::Vector{Vector{Vector{Float64}}}` : each [i][j][k]th element is the swirl induced velocity at ith step of the jth rotor at the kth radial section
* `wake_function::Vector{Function}`: [i]th element is a function vwake(X::Vector{Float64}) describing the wake induced velocity at `X` at the ith step
* `wake_shape_functions::Vector{Function}` : [i]th element is a function f(Rtip, x) describing the radial distance from the rotor axis to the boundary of the wake of the ith rotor
* `axial_interpolations::Vector{Function}` : [i]th element is a function f(rs, us, r, Rtip) that returns the axial component of rotor-induced velocity at distance r from the rotor axis based on the calculated axial induced velocities output from CCBlade of the ith rotor
* `swirl_interpolations::Vector{Function}` : [i]th element is a function f(rs, us, r, Rtip) that returns the swirl component of rotor-induced velocity at distance r from the rotor axis based on the calculated axial induced velocities output from CCBlade of the ith rotor
* `axial_multipliers::Vector{Function}` : [i]th element is a function f(distance2plane, Rtip) that is multiplied by the axial induced velocity function of the ith rotor
* `swirl_multipliers::Vector{Function}` : [i]th element is a function f(distance2plane, Rtip) that is multiplied by the swirl induced velocity function of the ith rotor
* `CFs::Array{Float64,2}` : [:,j]th element is the [CD,CY,CL] force coefficients of the aircraft at the jth step
* `CMs::Array{Float64,2}` : [:,j]th element is the [CMx,CMy,CMz] moment coefficients of the aircraft at the jth step
* `cfs::Vector{Vector{Array{Float64,2}}}`: [i][j]th element is an array of size (3,nspanwisepanels) of force coefficients cd, cy, cl corresponding to the ith step, jth lifting surface
* `cms::Vector{Vector{Array{Float64,2}}}`: [i][j]th element is an array of size (3,nspanwisepanels) of moment coefficients cmx, cmy, cmz corresponding to the ith step, jth lifting surface

"""
function solve_vlm_bem(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)

    flags = Vector{Bool}(undef,4)
    # solves rotors
    flags[1] = solve_rotor_nondimensional(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)
    # get wake function
    flags[2] = solve_rotor_wake(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)
    # solve VLM
    flags[3] = solve_wing_CF_CM(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)
    # extract lift and moment distribution
    flags[4] = lift_moment_distribution_blownwing(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)
    # flags[4] = lift_moment_distribution_blownwing(aircraft, parameters, freestream, environment, step_range, stepi, step_symbol)

    return prod(flags)
end

"""
    solve_vlm_bem(aircraft, step_range)

Method returns initialized elements required for the `parameters <: Parameters` struct during simulation.

# Arguments:

* `aircraft::Aircraft` : system to be simulated
* `step_range::AbstractArray` : defines each step of the simulation

# Returns:

* `omegas::Vector{Float64}` : a vector of rotational speeds in rad/s at the current step
* `Js::Array{Float64,2}` : each [i,j]th element is the advance ratio of the ith rotor at the jth step
* `Ts::Array{Float64,2}` : each [i,j]th element is the thrust of the ith rotor at the jth step
* `Qs::Array{Float64,2}` : each [i,j]th element is the torque of the ith rotor at the jth step
* `CTs::Array{Float64,2}` : each [i,j]th element is the thrust coefficient of the ith rotor at the jth step
* `CQs::Array{Float64,2}` : each [i,j]th element is the torque coefficient of the ith rotor at the jth step
* `ηs::Array{Float64,2}` : each [i,j]th element is the propulsive efficiency of the ith rotor at the jth step
* `us::Vector{Vector{Vector{Float64}}}` : each [i][j][k]th element is the axial induced velocity at ith step of the jth rotor at the kth radial section
* `vs::Vector{Vector{Vector{Float64}}}` : each [i][j][k]th element is the swirl induced velocity at ith step of the jth rotor at the kth radial section
* `wake_function::Vector{Function}`: [i]th element is a function vwake(X::Vector{Float64}) describing the wake induced velocity at `X` at the ith step
* `wake_shape_functions::Vector{Function}` : [i]th element is a function f(Rtip, x) describing the radial distance from the rotor axis to the boundary of the wake of the ith rotor
* `axial_interpolations::Vector{Function}` : [i]th element is a function f(rs, us, r, Rtip) that returns the axial component of rotor-induced velocity at distance r from the rotor axis based on the calculated axial induced velocities output from CCBlade of the ith rotor
* `swirl_interpolations::Vector{Function}` : [i]th element is a function f(rs, us, r, Rtip) that returns the swirl component of rotor-induced velocity at distance r from the rotor axis based on the calculated axial induced velocities output from CCBlade of the ith rotor
* `axial_multipliers::Vector{Function}` : [i]th element is a function f(distance2plane, Rtip) that is multiplied by the axial induced velocity function of the ith rotor
* `swirl_multipliers::Vector{Function}` : [i]th element is a function f(distance2plane, Rtip) that is multiplied by the swirl induced velocity function of the ith rotor
* `CFs::Array{Float64,2}` : [:,j]th element is the [CD,CY,CL] force coefficients of the aircraft at the jth step
* `CMs::Array{Float64,2}` : [:,j]th element is the [CMx,CMy,CMz] moment coefficients of the aircraft at the jth step
* `cfs::Vector{Vector{Array{Float64,2}}}`: [i][j]th element is an array of size (3,nspanwisepanels) of force coefficients cd, cy, cl corresponding to the ith step, jth lifting surface
* `cms::Vector{Vector{Array{Float64,2}}}`: [i][j]th element is an array of size (3,nspanwisepanels) of moment coefficients cmx, cmy, cmz corresponding to the ith step, jth lifting surface

"""
function solve_vlm_bem(aircraft, step_range)

    params_solve_rotor_nondimensional = solve_rotor_nondimensional(aircraft, step_range) # omegas, Js, Ts, Qs, CTs, CQs, ηs, us, vs
    params_solve_rotor_wake = solve_rotor_wake(aircraft, step_range) # wake_function, us, vs
    params_solve_wing_CF_CM = solve_wing_CF_CM(aircraft, step_range) # wake_function, CFs, CMs
    params_lift_moment_distribution = lift_moment_distribution(aircraft, step_range) # cfs, cms

    return params_solve_rotor_nondimensional..., params_solve_rotor_wake[[1,4,5,6,7,8]]..., params_solve_wing_CF_CM[2:end]..., params_lift_moment_distribution...
end


m = @model y ~ x1 fe = pn  vcov = cluster(pid1)
x = reg(df, m, drop_singletons = false)


function H!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4, ˍ₋arg5, ˍ₋arg6, ˍ₋arg7; )
    let c_p = @inbounds(ˍ₋arg1[1]), q_p = @inbounds(ˍ₋arg1[2]), i_p = @inbounds(ˍ₋arg1[3]), c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), c_ss = @inbounds(ˍ₋arg3[1]), q_ss = @inbounds(ˍ₋arg3[2]), i_ss = @inbounds(ˍ₋arg3[3]), k_p = @inbounds(ˍ₋arg4[1]), z_p = @inbounds(ˍ₋arg4[2]), k = @inbounds(ˍ₋arg5[1]), z = @inbounds(ˍ₋arg5[2]), k_ss = @inbounds(ˍ₋arg6[1]), z_ss = @inbounds(ˍ₋arg6[2]), α = @inbounds(ˍ₋arg7[1]), β = @inbounds(ˍ₋arg7[2]), ρ = @inbounds(ˍ₋arg7[3]), δ = @inbounds(ˍ₋arg7[4]), σ = @inbounds(ˍ₋arg7[5]), Ω_1 = @inbounds(ˍ₋arg7[6])
        @inbounds begin
                ˍ₋out[1] = (+)((/)(1, c), (/)((*)(-1, β, (+)(1, (*)(-1, δ), (*)(α, (^)(k_p, (+)(-1, α)), (exp)(z_p)))), c_p))
                ˍ₋out[2] = (+)(c, k_p, (*)(-1, q), (*)(-1, k, (+)(1, (*)(-1, δ))))
                ˍ₋out[3] = (+)(q, (*)(-1, (^)(k, α), (exp)(z)))
                ˍ₋out[4] = (+)(z_p, (*)(-1, z, ρ))
                ˍ₋out[5] = (+)(i, (*)(-1, k_p), (*)(k, (+)(1, (*)(-1, δ))))
                nothing
            end
    end
end

function H_yp!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3; )
    let c = @inbounds(ˍ₋arg1[1]), q = @inbounds(ˍ₋arg1[2]), i = @inbounds(ˍ₋arg1[3]), k = @inbounds(ˍ₋arg2[1]), z = @inbounds(ˍ₋arg2[2]), α = @inbounds(ˍ₋arg3[1]), β = @inbounds(ˍ₋arg3[2]), ρ = @inbounds(ˍ₋arg3[3]), δ = @inbounds(ˍ₋arg3[4]), σ = @inbounds(ˍ₋arg3[5]), Ω_1 = @inbounds(ˍ₋arg3[6])
        @inbounds begin
                ˍ₋out[1] = (*)(-1, (/)((*)(-1, β, (+)(1, (*)(-1, δ), (*)(α, (^)(k, (+)(-1, α)), (exp)(z)))), (^)(c, 2)))
                nothing
            end
    end
end

function H_y!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3; )
    let c = @inbounds(ˍ₋arg1[1]), q = @inbounds(ˍ₋arg1[2]), i = @inbounds(ˍ₋arg1[3]), k = @inbounds(ˍ₋arg2[1]), z = @inbounds(ˍ₋arg2[2]), α = @inbounds(ˍ₋arg3[1]), β = @inbounds(ˍ₋arg3[2]), ρ = @inbounds(ˍ₋arg3[3]), δ = @inbounds(ˍ₋arg3[4]), σ = @inbounds(ˍ₋arg3[5]), Ω_1 = @inbounds(ˍ₋arg3[6])
        @inbounds begin
                ˍ₋out[1] = (*)(-1, (/)(1, (^)(c, 2)))
                ˍ₋out[2] = 1
                ˍ₋out[7] = -1
                ˍ₋out[8] = 1
                ˍ₋out[15] = 1
                nothing
            end
    end
end

function H_xp!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3; )
    let c = @inbounds(ˍ₋arg1[1]), q = @inbounds(ˍ₋arg1[2]), i = @inbounds(ˍ₋arg1[3]), k = @inbounds(ˍ₋arg2[1]), z = @inbounds(ˍ₋arg2[2]), α = @inbounds(ˍ₋arg3[1]), β = @inbounds(ˍ₋arg3[2]), ρ = @inbounds(ˍ₋arg3[3]), δ = @inbounds(ˍ₋arg3[4]), σ = @inbounds(ˍ₋arg3[5]), Ω_1 = @inbounds(ˍ₋arg3[6])
        @inbounds begin
                ˍ₋out[1] = (/)((*)(-1, α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), c)
                ˍ₋out[2] = 1
                ˍ₋out[5] = -1
                ˍ₋out[6] = (/)((*)(-1, α, β, (^)(k, (+)(-1, α)), (exp)(z)), c)
                ˍ₋out[9] = 1
                nothing
            end
    end
end

function H_x!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3; )
    let c = @inbounds(ˍ₋arg1[1]), q = @inbounds(ˍ₋arg1[2]), i = @inbounds(ˍ₋arg1[3]), k = @inbounds(ˍ₋arg2[1]), z = @inbounds(ˍ₋arg2[2]), α = @inbounds(ˍ₋arg3[1]), β = @inbounds(ˍ₋arg3[2]), ρ = @inbounds(ˍ₋arg3[3]), δ = @inbounds(ˍ₋arg3[4]), σ = @inbounds(ˍ₋arg3[5]), Ω_1 = @inbounds(ˍ₋arg3[6])
        @inbounds begin
                ˍ₋out[2] = (+)(-1, δ)
                ˍ₋out[3] = (*)(-1, α, (^)(k, (+)(-1, α)), (exp)(z))
                ˍ₋out[5] = (+)(1, (*)(-1, δ))
                ˍ₋out[8] = (*)(-1, (^)(k, α), (exp)(z))
                ˍ₋out[9] = (*)(-1, ρ)
                nothing
            end
    end
end

function Ψ!(ˍ₋out, ˍ₋arg1, ˍ₋arg2, ˍ₋arg3; )
    let c = @inbounds(ˍ₋arg1[1]), q = @inbounds(ˍ₋arg1[2]), i = @inbounds(ˍ₋arg1[3]), k = @inbounds(ˍ₋arg2[1]), z = @inbounds(ˍ₋arg2[2]), α = @inbounds(ˍ₋arg3[1]), β = @inbounds(ˍ₋arg3[2]), ρ = @inbounds(ˍ₋arg3[3]), δ = @inbounds(ˍ₋arg3[4]), σ = @inbounds(ˍ₋arg3[5]), Ω_1 = @inbounds(ˍ₋arg3[6])
        @inbounds begin
                nothing
            end
        begin
            @inbounds begin
                    (ˍ₋out[1])[1] = (*)(2, c, (/)((*)(-1, β, (+)(1, (*)(-1, δ), (*)(α, (^)(k, (+)(-1, α)), (exp)(z)))), (^)(c, 4)))
                    (ˍ₋out[1])[7] = (/)((*)(α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), (^)(c, 2))
                    (ˍ₋out[1])[8] = (/)((*)(α, β, (^)(k, (+)(-1, α)), (exp)(z)), (^)(c, 2))
                    (ˍ₋out[1])[34] = (*)(2, c, (/)(1, (^)(c, 4)))
                    (ˍ₋out[1])[61] = (/)((*)(α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), (^)(c, 2))
                    (ˍ₋out[1])[67] = (/)((*)(-1, α, β, (^)(k, (+)(-3, α)), (+)(-1, α), (+)(-2, α), (exp)(z)), c)
                    (ˍ₋out[1])[68] = (/)((*)(-1, α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), c)
                    (ˍ₋out[1])[71] = (/)((*)(α, β, (^)(k, (+)(-1, α)), (exp)(z)), (^)(c, 2))
                    (ˍ₋out[1])[77] = (/)((*)(-1, α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), c)
                    (ˍ₋out[1])[78] = (/)((*)(-1, α, β, (^)(k, (+)(-1, α)), (exp)(z)), c)
                    nothing
                end
        end
        begin
            @inbounds begin
                    nothing
                end
        end
        begin
            @inbounds begin
                    (ˍ₋out[3])[89] = (*)(-1, α, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z))
                    (ˍ₋out[3])[90] = (*)(-1, α, (^)(k, (+)(-1, α)), (exp)(z))
                    (ˍ₋out[3])[99] = (*)(-1, α, (^)(k, (+)(-1, α)), (exp)(z))
                    (ˍ₋out[3])[100] = (*)(-1, (^)(k, α), (exp)(z))
                    nothing
                end
        end
        begin
            @inbounds begin
                    nothing
                end
        end
        begin
            @inbounds begin
                    nothing
                end
        end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:α}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((*)(β, (+)((*)((^)(k, (+)(-1, α)), (exp)(z)), (*)(α, (^)(k, (+)(-1, α)), (log)(k), (exp)(z)))), (^)(c, 2))
                nothing
            end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:ρ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:σ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:Ω_1}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:δ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((*)(-1, β), (^)(c, 2))
                nothing
            end
    end
end

function H_yp_p!(ˍ₋out, ::Val{:β}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((+)(1, (*)(-1, δ), (*)(α, (^)(k, (+)(-1, α)), (exp)(z))), (^)(c, 2))
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:α}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:ρ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:σ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:Ω_1}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:δ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_y_p!(ˍ₋out, ::Val{:β}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:α}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((+)((*)(-1, α, β, (^)(k, (+)(-2, α)), (exp)(z)), (*)(-1, β, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), (*)(-1, α, β, (^)(k, (+)(-2, α)), (+)(-1, α), (log)(k), (exp)(z))), c)
                ˍ₋out[6] = (/)((+)((*)(-1, β, (^)(k, (+)(-1, α)), (exp)(z)), (*)(-1, α, β, (^)(k, (+)(-1, α)), (log)(k), (exp)(z))), c)
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:ρ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:σ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:Ω_1}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:δ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_xp_p!(ˍ₋out, ::Val{:β}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((*)(-1, α, (^)(k, (+)(-2, α)), (+)(-1, α), (exp)(z)), c)
                ˍ₋out[6] = (/)((*)(-1, α, (^)(k, (+)(-1, α)), (exp)(z)), c)
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:α}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[3] = (+)((*)(-1, (^)(k, (+)(-1, α)), (exp)(z)), (*)(-1, α, (^)(k, (+)(-1, α)), (log)(k), (exp)(z)))
                ˍ₋out[8] = (*)(-1, (^)(k, α), (log)(k), (exp)(z))
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:ρ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[9] = -1
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:σ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:Ω_1}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:δ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[2] = 1
                ˍ₋out[5] = -1
                nothing
            end
    end
end

function H_x_p!(ˍ₋out, ::Val{:β}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:α}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((*)(-1, β, (+)((*)((^)(k, (+)(-1, α)), (exp)(z)), (*)(α, (^)(k, (+)(-1, α)), (log)(k), (exp)(z)))), c)
                ˍ₋out[3] = (*)(-1, (^)(k, α), (log)(k), (exp)(z))
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:ρ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[4] = (*)(-1, z)
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:σ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:Ω_1}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:δ}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)(β, c)
                ˍ₋out[2] = k
                ˍ₋out[5] = (*)(-1, k)
                nothing
            end
    end
end

function H_p!(ˍ₋out, ::Val{:β}, ˍ₋arg2, ˍ₋arg3, ˍ₋arg4; )
    let c = @inbounds(ˍ₋arg2[1]), q = @inbounds(ˍ₋arg2[2]), i = @inbounds(ˍ₋arg2[3]), k = @inbounds(ˍ₋arg3[1]), z = @inbounds(ˍ₋arg3[2]), α = @inbounds(ˍ₋arg4[1]), β = @inbounds(ˍ₋arg4[2]), ρ = @inbounds(ˍ₋arg4[3]), δ = @inbounds(ˍ₋arg4[4]), σ = @inbounds(ˍ₋arg4[5]), Ω_1 = @inbounds(ˍ₋arg4[6])
        @inbounds begin
                ˍ₋out[1] = (/)((+)(-1, δ, (*)(-1, α, (^)(k, (+)(-1, α)), (exp)(z))), c)
                nothing
            end
    end
end


import ElasticFusion
import Surfels
import Pangolin
import SLAMData
import CameraModel

function fuse_RGBD_to_3D_model(
    rgbd_reader::T,
    id2Twc, # traj
    cam::CameraModel.RgbdCamParams,
    output_ply_fpath::String
) where T <: SLAMData.RgbdReader

    # hidden_window = Surfels.create_hidden_gl_window(cam.width, cam.height)
    window_name = "EFusion Window"
    Pangolin.create_window(window_name, cam.width, cam.height)

    ef = ElasticFusion.Fusion(
        w=cam.width,
        h=cam.height,
        fx=cam.fx,
        fy=cam.fy,
        cx=cam.cx,
        cy=cam.cy,
        timeDelta = 200,
        countThresh = 35000,
        errThresh = 5e-05,
        covThresh = 1e-05,
        closeLoops = false,
        iclnuim = false,
        reloc = false,
        photoThresh = 115,
        confidence = 10,
        depthCut = 5,
        icpThresh = 10,
        fastOdom = false,
        fernThresh = 0.3095,
        so3 = true,
        frameToFrameRGB = false,
        fileName = "fsd_2021sep"
    )

    frame_ids = sort!(collect(keys(id2Twc)))
    @assert frame_ids[1] == 0

    for f_id = frame_ids

        # TODO: how to get timestamp from id
        time_stamp = Int64(f_id)

        # pose = convert(Matrix{Float32}, inv(all_Twc0[f_id]))
        pose = convert(Matrix{Float32}, id2Twc[f_id])

        img, depth16 = SLAMData.readRGBD(rgbd_reader, id=time_stamp)

        # note depth_factor only support 1000.0 for now
        @assert cam.depth_factor == 1000.0

        ElasticFusion.process_frame!(
            ef, img, depth16, time_stamp, pose=pose, depth_factor=1000.0
        )

        # function process_frame!(
        #     ef::ElasticFusion,
        #     image::Matrix{RGB{Normed{UInt8,8}}},
        #     depth::Matrix{UInt16},
        #     timestamp::Int64,
        #     pose::Matrix{Float32};
        #     weightMultiplier::Union{Float32, Float64}
        #     bootstrap::Bool
        #     )

        # Pangolin.activate(surfel_viewer.d_cam, surfel_viewer.s_cam)
        # glClearColor(0.05 , 0.05 , 0.3 , 0.0);
        # glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        # # end
        # mvp = Pangolin.get_projection_view_mat(surfel_viewer.s_cam)
        # # mvp32 = convert(Matrix{Float32}, mvp)

        # ElasticFusion.render(
        #     ef,
        #     mvp,
        #     threshold=10,#::Union{Float64, Float32},10
        #     drawUnstable=true,#//bool
        #     drawNormals=false,#, //bool
        #     drawColors=true,#,  //bool
        #     drawPoints=true,#,  //bool
        #     drawWindow=false,#,  //bool
        #     drawTimes=false,#,   //bool
        #     time=time_stamp,
        #     timeDelta=200
        # )
        # Pangolin.finish_frame()
    end

    ElasticFusion.save_ply(ef, output_ply_fpath)
    finalize(ef)
    Pangolin.destroy_window(window_name)
end


## DEFAULT SETTING FOR TESTING

# Data Settings
settings = Dict(
    :filepath_ssp => joinpath(@__DIR__, "sample_ssp.jld2"),
    :filepath_ppp => joinpath(@__DIR__, "sample_ppp.jld2"),
    # :roi_ssp => [1:3,1:301],        # region of interst [dl, wn]
    # :roi_ppp => [1:3,1:301],
    :roi_ssp => [1:2,10:290],
    :roi_ppp => [1:2,10:290],
    :size_ssp => (2,281),           # size of final data (dl, wn)
    :size_ppp => (2,281),
    :reference_ssp => 1,            # index of delays which's spectrum
    :reference_ppp => 1,            # should serve as reference
    :bleach_weight => 3,
    :datatype => Float64,
    :N => 3,                        # number of resonances

    # Optimization Settings
    :method => :LBFGS,
    :iterations => 100_000_000_000,
    :n_particles => 50,
    :start => [
        -3, 4, -9,
        7, -2, 5,
        2872, 2911, 2938,
        fill(8.0, 3)...,
        fill(0.9, 3*2)...,
        1.0,
        1.0
    ],
    :lower => [
        fill(-10, 3)...,
        fill(-10, 3)...,
        fill(2860, 3)...,
        fill(6.0, 3)...,
        fill(0.7, 3*2)...,
        -3.0,
        -5.0
    ],
    :upper => [
        fill(10, 3)...,
        fill(10, 3)...,
        fill(2950, 3)...,
        fill(12.0, 3)...,
        fill(1.1, 3*2)...,
        3.0,
        5.0
    ],
    :time_limit => 360,

    # Saving
    :savedir => joinpath(@__DIR__, "results"),
)


using ImageMagick  # maybe 16.04 specific requirement
using Arena.Amphitheatre
using RoME
using GraffSDK

#Point the giraffe in the right direction
config = loadGraffConfig()
config.userId = ""
config.robotId = ""
config.sessionId = ""

#Set up amphis
visdatasets = AbstractAmphitheatre[]

# visualize factor graph
push!(visdatasets, BasicGraffPose(config,
                                    meanmax=:max,
                                    poseProp = plDrawProp(0.15, 0.05, RGBA(1,1,0,0.5)),
                                    landmarkProp = plDrawProp(0.2, 0.1, RGBA(0,1,0,0.5))))

# visualize april tags on poses
push!(visdatasets, GraffTagOnPose(config) )

# visualize point cloud on poses
push!(visdatasets, Amphitheatre.GraffCloudOnPose(config, CameraModel(640, 480, 387.205, [322.042, 238.544])))

# run visualzer
vis, vistask = visualize(visdatasets, quat=Amphitheatre.Quat(0.0,1.0,0.0,0.0))
vistask

## stop visualizer
stopAmphiVis!()


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
function add_export_constraints(m, p; _n="")

    ##Constraint (8e): Production export and curtailment no greater than production
    @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid],
        p.production_factor[t,ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts]
        >= sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t]) +
           m[Symbol("dvCurtail"*_n)][t, ts]
    )

    binNEM = 0
    binWHL = 0
    NEM_benefit = 0
    EXC_benefit = 0
    WHL_benefit = 0
    NEM_techs = String[t for t in p.techs.elec if :NEM in p.export_bins_by_tech[t]]
    WHL_techs = String[t for t in p.techs.elec if :WHL in p.export_bins_by_tech[t]]

    if !isempty(NEM_techs)
        # Constraint (9c): Net metering only -- can't sell more than you purchase
        # hours_per_timestep is cancelled on both sides, but used for unit consistency (convert power to energy)
        @constraint(m,
            p.hours_per_timestep * sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts]
            for t in NEM_techs, ts in p.time_steps)
            <= p.hours_per_timestep * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier]
                for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers)
        )

        if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(WHL_techs)
            # no need for binNEM nor binWHL
            binNEM = 1
            @constraint(m,
                sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw
            )
            NEM_benefit = @expression(m, p.pwf_e * p.hours_per_timestep *
                sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts]
                    for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps)
            )
            if :EXC in p.s.electric_tariff.export_bins
                EXC_benefit = @expression(m, p.pwf_e * p.hours_per_timestep *
                    sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts]
                        for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps)
                )
            end
        else
            if !(isempty(_n))
                @error """Binaries decisions for net metering capacity limit is not implemented for multinode models to keep
                            them linear. Please set the net metering limit to zero or equal to the interconnection limit."""
            end

            binNEM = @variable(m, binary = true)
            @warn "Adding binary variable for net metering choice. Some solvers are slow with binaries."

            # Good to bound the benefit
            max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:NEM])])*10
            NEM_benefit = @variable(m, lower_bound = max_bene)

            # If choosing to take advantage of NEM, must have total capacity less than net_metering_limit_kw
            @constraint(m,
                binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.net_metering_limit_kw}
            )
            @constraint(m,
                !binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw}
            )

            # binary choice for NEM benefit
            @constraint(m,
                binNEM => {NEM_benefit >= p.pwf_e * p.hours_per_timestep *
                    sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts]
                        for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps)
                }
            )
            @constraint(m, !binNEM => {NEM_benefit >= 0})

            EXC_benefit = 0
            if :EXC in p.s.electric_tariff.export_bins
                EXC_benefit = @variable(m, lower_bound = max_bene)
                @constraint(m,
                    binNEM => {EXC_benefit >= p.pwf_e * p.hours_per_timestep *
                        sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts]
                            for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps)
                    }
                )
                @constraint(m, !binNEM => {EXC_benefit >= 0})
            end
        end
    end

    if !isempty(WHL_techs)

        if typeof(binNEM) <: Real  # no need for wholesale binary
            binWHL = 1
            WHL_benefit = @expression(m, p.pwf_e * p.hours_per_timestep *
                sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
                        for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
            )
        else
            binWHL = @variable(m, binary = true)
            @warn "Adding binary variable for wholesale export choice. Some solvers are slow with binaries."
            max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])])*10
            WHL_benefit = @variable(m, lower_bound = max_bene)

            @constraint(m, binNEM + binWHL == 1)  # can either NEM or WHL export, not both

            @constraint(m,
                binWHL => {WHL_benefit >= p.pwf_e * p.hours_per_timestep *
                    sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
                            for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
                }
            )
            @constraint(m, !binWHL => {WHL_benefit >= 0})
        end
    end

    # register the benefits in the model
    m[Symbol("NEM_benefit"*_n)] = NEM_benefit
    m[Symbol("EXC_benefit"*_n)] = EXC_benefit
    m[Symbol("WHL_benefit"*_n)] = WHL_benefit
    nothing
end

"""
    add_monthly_peak_constraint(m, p; _n="")

Only used if ElectricTariff has monthly demand rates.
Sets dvPeakDemandMonth to greater than dvGridPurchase across each month.
If the monthly demand rate is tiered than also adds binMonthlyDemandTier and constraints.
"""
function add_monthly_peak_constraint(m, p; _n="")

	## Constraint (11d): Monthly peak demand is >= demand at each hour in the month
    if (!isempty(p.techs.chp)) && !(p.s.chp.reduces_demand_charges)
        @constraint(m, [mth in p.months, ts in p.s.electric_tariff.time_steps_monthly[mth]],
            sum(m[Symbol("dvPeakDemandMonth"*_n)][mth, t] for t in 1:p.s.electric_tariff.n_monthly_demand_tiers)
            >= sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) +
            sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t, ts] for t in p.techs.chp) -
            sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types) for t in p.techs.chp) -
            sum(sum(m[Symbol("dvProductionToGrid")][t,u,ts] for u in p.export_bins_by_tech[t]) for t in p.techs.chp)

        )
    else
        @constraint(m, [mth in p.months, ts in p.s.electric_tariff.time_steps_monthly[mth]],
            sum(m[Symbol("dvPeakDemandMonth"*_n)][mth, t] for t in 1:p.s.electric_tariff.n_monthly_demand_tiers)
                >= sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers)
        )
    end

    if p.s.electric_tariff.n_monthly_demand_tiers > 1  # only need binaries if more than one tier
        @warn "Adding binary variables to model monthly demand tiers."
        ntiers = p.s.electric_tariff.n_monthly_demand_tiers
        dv = "binMonthlyDemandTier" * _n
        m[Symbol(dv)] = @variable(m, [p.months, 1:ntiers], binary = true, base_name = dv)
        b = m[Symbol(dv)]
        # Upper bound on peak electrical power demand by month, tier; if tier is selected (0 o.w.)
        @constraint(m, [mth in p.months, tier in 1:ntiers],
            m[Symbol("dvPeakDemandMonth"*_n)][mth, tier] <= p.s.electric_tariff.monthly_demand_tier_limits[tier] *
                b[mth, tier]
        )

        # Monthly peak electrical power demand tier ordering
        @constraint(m, [mth in p.months, tier in 2:ntiers], b[mth, tier] <= b[mth, tier-1])

        # One monthly peak electrical power demand tier must be full before next one is active
        @constraint(m, [mth in p.months, tier in 2:ntiers],
        b[mth, tier] * p.s.electric_tariff.monthly_demand_tier_limits[tier-1] <=
            m[Symbol("dvPeakDemandMonth"*_n)][mth, tier-1]
        )
        # TODO implement NewMaxDemandMonthsInTier, which adds mth index to monthly_demand_tier_limits
    end
end

function add_tou_peak_constraint(m, p; _n="")
    ## Constraint (12d): Ratchet peak demand is >= demand at each hour in the ratchet`
    @constraint(m, [r in p.ratchets, ts in p.s.electric_tariff.tou_demand_ratchet_timesteps[r]],
        sum(m[Symbol("dvPeakDemandTOU"*_n)][r, tier] for tier in 1:p.s.electric_tariff.n_tou_demand_tiers) >=
        sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers)
    )

    if p.s.electric_tariff.n_tou_demand_tiers > 1
        @warn "Adding binary variables to model TOU demand tiers."
        ntiers = p.s.electric_tariff.n_tou_demand_tiers
        dv = "binTOUDemandTier" * _n
        m[Symbol(dv)] = @variable(m, [p.ratchets, 1:ntiers], binary = true, base_name = dv)
        b = m[Symbol(dv)]

        # Upper bound on peak electrical power demand by tier, by ratchet, if tier is selected (0 o.w.)
        @constraint(m, [r in p.ratchets, tier in 1:ntiers],
            m[Symbol("dvPeakDemandTOU"*_n)][r, tier] <= p.s.electric_tariff.tou_demand_tier_limits[tier] * b[r, tier]
        )

        # Ratchet peak electrical power ratchet tier ordering
        @constraint(m, [r in p.ratchets, tier in 2:ntiers],
            b[r, tier] <= b[r, tier-1]
        )

        # One ratchet peak electrical power demand tier must be full before next one is active
        @constraint(m, [r in p.ratchets, tier in 2:ntiers],
            b[r, tier] * p.s.electric_tariff.tou_demand_tier_limits[tier-1]
            <= m[Symbol("dvPeakDemandTOU"*_n)][r, tier-1]
        )

        # Ratchet peak demand is >= demand at each hour in the ratchet
        @constraint(m, [r in p.ratchets, ts in p.s.electric_tariff.tou_demand_ratchet_timesteps[r]],
            sum(m[Symbol("dvPeakDemandTOU"*_n)][r, tier] for tier in 1:ntiers)
            >= sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers )
        )
    end
    # TODO implement NewMaxDemandInTier
end

function add_mincharge_constraint(m, p; _n="")
    @constraint(m,
        m[Symbol("MinChargeAdder"*_n)] >= m[Symbol("TotalMinCharge"*_n)] - ( m[Symbol("TotalEnergyChargesUtil"*_n)] +
        m[Symbol("TotalDemandCharges"*_n)] + m[Symbol("TotalExportBenefit"*_n)] + m[Symbol("TotalFixedCharges"*_n)] )
    )
end

function add_simultaneous_export_import_constraint(m, p; _n="")
    @constraint(m, NoGridPurchasesBinary[ts in p.time_steps],
        m[Symbol("binNoGridPurchases"*_n)][ts] => {
          sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) +
          sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types) <= 0
        }
    )
    @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps],
        !m[Symbol("binNoGridPurchases"*_n)][ts] => {
            sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= 0
        }
    )
end

"""
    add_energy_tier_constraints(m, p; _n="")

Only necessary if n_energy_tiers > 1
"""
function add_energy_tier_constraints(m, p; _n="")
    @warn "Adding binary variables to model energy cost tiers."
    ntiers = p.s.electric_tariff.n_energy_tiers
    dv = "binEnergyTier" * _n
    m[Symbol(dv)] = @variable(m, [p.months, 1:ntiers], binary = true, base_name = dv)
    b = m[Symbol(dv)]
    ##Constraint (10a): Usage limits by pricing tier, by month
    @constraint(m, [mth in p.months, tier in 1:p.s.electric_tariff.n_energy_tiers],
        p.hours_per_timestep * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.s.electric_tariff.time_steps_monthly[mth] )
        <= b[mth, tier] * p.s.electric_tariff.energy_tier_limits[tier]
    )
    ##Constraint (10b): Ordering of pricing tiers
    @constraint(m, [mth in p.months, tier in 2:p.s.electric_tariff.n_energy_tiers],
        b[mth, tier] - b[mth, tier-1] <= 0
    )
    ## Constraint (10c): One tier must be full before any usage in next tier
    @constraint(m, [mth in p.months, tier in 2:p.s.electric_tariff.n_energy_tiers],
        b[mth, tier] * p.s.electric_tariff.energy_tier_limits[tier-1] -
        sum( m[Symbol("dvGridPurchase"*_n)][ts, tier-1] for ts in p.s.electric_tariff.time_steps_monthly[mth])
        <= 0
    )
    # TODO implement NewMaxUsageInTier
end

"""
    add_demand_lookback_constraints(m, p; _n="")

Only necessary if ElectricTariff.demand_lookback_percent > 0
"""
function add_demand_lookback_constraints(m, p; _n="")
    dv = "dvPeakDemandLookback" * _n
    m[Symbol(dv)] = @variable(m, [p.months], base_name = dv, lower_bound = 0)

	if p.s.electric_tariff.demand_lookback_range != 0  # then the dvPeakDemandLookback varies by month

		##Constraint (12e): dvPeakDemandLookback is the highest peak demand in DemandLookbackMonths
		for mth in p.months
			if mth > p.s.electric_tariff.demand_lookback_range
				@constraint(m, [lm in 1:p.s.electric_tariff.demand_lookback_range, ts in p.s.electric_tariff.time_steps_monthly[mth - lm]],
					m[Symbol(dv)][mth] ≥ sum( m[Symbol("dvGridPurchase"*_n)][ts, tier]
                                              for tier in 1:p.s.electric_tariff.n_energy_tiers )
				)
			else  # need to handle rollover months
				for lm in 1:p.s.electric_tariff.demand_lookback_range
					lkbkmonth = mth - lm
					if lkbkmonth ≤ 0
						lkbkmonth += 12
					end
					@constraint(m, [ts in p.s.electric_tariff.time_steps_monthly[lkbkmonth]],
						m[Symbol(dv)][mth] ≥ sum( m[Symbol("dvGridPurchase"*_n)][ts, tier]
                                                  for tier in 1:p.s.electric_tariff.n_energy_tiers )
					)
				end
			end
		end

		##Constraint (12f): Ratchet peak demand charge is bounded below by lookback
		@constraint(m, [mth in p.months],
			sum( m[Symbol("dvPeakDemandMonth"*_n)][mth, tier] for tier in 1:p.s.electric_tariff.n_monthly_demand_tiers ) >=
			p.s.electric_tariff.demand_lookback_percent * m[Symbol(dv)][mth]
		)

	else  # dvPeakDemandLookback does not vary by month

		##Constraint (12e): dvPeakDemandLookback is the highest peak demand in DemandLookbackMonths
		@constraint(m, [lm in p.s.electric_tariff.demand_lookback_months],
			m[Symbol(dv)][1] >= sum(m[Symbol("dvPeakDemandMonth"*_n)][lm, tier] for tier in 1:p.s.electric_tariff.n_monthly_demand_tiers)
		)

		##Constraint (12f): Ratchet peak demand charge is bounded below by lookback
		@constraint(m, [mth in p.months],
			sum( m[Symbol("dvPeakDemandMonth"*_n)][mth, tier] for tier in 1:p.s.electric_tariff.n_monthly_demand_tiers ) >=
			p.s.electric_tariff.demand_lookback_percent * m[Symbol(dv)][1]
		)
	end
end

function add_coincident_peak_charge_constraints(m, p; _n="")
	## Constraint (14a): in each coincident peak period, charged CP demand is the max of demand in all CP timesteps
    dv = "dvPeakDemandCP" * _n
    m[Symbol(dv)] = @variable(m, [p.s.electric_tariff.coincpeak_periods], lower_bound = 0, base_name = dv)
	@constraint(m,
        [prd in p.s.electric_tariff.coincpeak_periods,
         ts in p.s.electric_tariff.coincident_peak_load_active_timesteps[prd]],
		m[Symbol("dvPeakDemandCP"*_n)][prd] >= sum(m[Symbol("dvGridPurchase"*_n)][ts, tier]
                                                                      for tier in 1:p.s.electric_tariff.n_energy_tiers)
	)
end

function add_elec_utility_expressions(m, p; _n="")

    if !isempty(p.s.electric_tariff.export_bins) && !isempty(p.techs.all)
        # NOTE: levelization_factor is baked into dvProductionToGrid
        m[Symbol("TotalExportBenefit"*_n)] = m[Symbol("NEM_benefit"*_n)] + m[Symbol("WHL_benefit"*_n)] +
                                             m[Symbol("EXC_benefit"*_n)]
    else
        m[Symbol("TotalExportBenefit"*_n)] = 0
    end

    m[Symbol("TotalEnergyChargesUtil"*_n)] = @expression(m, p.pwf_e * p.hours_per_timestep *
        sum( p.s.electric_tariff.energy_rates[ts, tier] * m[Symbol("dvGridPurchase"*_n)][ts, tier]
            for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers)
    )

    if !isempty(p.s.electric_tariff.tou_demand_rates)
        m[Symbol("DemandTOUCharges"*_n)] = @expression(m,
            p.pwf_e * sum( p.s.electric_tariff.tou_demand_rates[r] * m[Symbol("dvPeakDemandTOU"*_n)][r, tier]
            for r in p.ratchets, tier in p.s.electric_tariff.n_tou_demand_tiers)
        )
    else
        m[Symbol("DemandTOUCharges"*_n)] = 0
    end

    if !isempty(p.s.electric_tariff.monthly_demand_rates)
        m[Symbol("DemandFlatCharges"*_n)] = @expression(m, p.pwf_e *
            sum( p.s.electric_tariff.monthly_demand_rates[mth, t] * m[Symbol("dvPeakDemandMonth"*_n)][mth, t]
                for mth in p.months, t in 1:p.s.electric_tariff.n_monthly_demand_tiers)
        )
    else
        m[Symbol("DemandFlatCharges"*_n)] = 0
    end

    m[Symbol("TotalDemandCharges"*_n)] = m[Symbol("DemandTOUCharges"*_n)] + m[Symbol("DemandFlatCharges"*_n)]

    m[Symbol("TotalFixedCharges"*_n)] = p.pwf_e * p.s.electric_tariff.fixed_monthly_charge * 12

    if p.s.electric_tariff.annual_min_charge > 12 * p.s.electric_tariff.min_monthly_charge
        m[Symbol("TotalMinCharge"*_n)] = p.s.electric_tariff.annual_min_charge
    else
        m[Symbol("TotalMinCharge"*_n)] = 12 * p.s.electric_tariff.min_monthly_charge
    end

	if m[Symbol("TotalMinCharge"*_n)] >= 1e-2
		add_mincharge_constraint(m, p)
	else
		@constraint(m, m[Symbol("MinChargeAdder"*_n)] == 0)
	end

    if !isempty(p.s.electric_tariff.coincpeak_periods)
        m[Symbol("TotalCPCharges"*_n)] = @expression(m, p.pwf_e *
            sum( p.s.electric_tariff.coincident_peak_load_charge_per_kw[prd] * m[Symbol("dvPeakDemandCP"*_n)][prd]
                for prd in p.s.electric_tariff.coincpeak_periods )
        )
    else
        m[Symbol("TotalCPCharges"*_n)] = 0
    end

    m[Symbol("TotalElecBill"*_n)] = (
        m[Symbol("TotalEnergyChargesUtil"*_n)]
        + m[Symbol("TotalDemandCharges"*_n)]
        + m[Symbol("TotalExportBenefit"*_n)]
        + m[Symbol("TotalFixedCharges"*_n)]
        + m[Symbol("TotalCPCharges"*_n)]
        + 0.999 * m[Symbol("MinChargeAdder"*_n)]
    )
    #= Note: 0.999 * MinChargeAdder in Objective b/c when
        TotalMinCharge > (TotalEnergyCharges + TotalDemandCharges + TotalExportBenefit + TotalFixedCharges)
		it is arbitrary where the min charge ends up (eg. could be in TotalDemandCharges or MinChargeAdder).
		0.001 * MinChargeAdder is added back into LCC when writing to results.
    =#
    nothing
end