## Functions to compute the reduced shape

# for reductions that expand 0 dims to 1
reduced_dims(a::AbstractArray, region) = reduced_dims(size(a), region)

# for reductions that keep 0 dims as 0
reduced_dims0(a::AbstractArray, region) = reduced_dims0(size(a), region)
reduced_dims{N}(siz::NTuple{N,Int}, d::Int, rd::Int) = (d == 1 ? tuple(rd, siz[d+1:N]...) :
                                                        d == N ? tuple(siz[1:N-1]..., rd) :
                                                        1 < d < N ? tuple(siz[1:d-1]..., rd, siz[d+1:N]...) :
                                                        siz)::typeof(siz)
reduced_dims{N}(siz::NTuple{N,Int}, d::Int) = reduced_dims(siz, d, 1)
reduced_dims0{N}(siz::NTuple{N,Int}, d::Int) = 1 <= d <= N ? reduced_dims(siz, d, (siz[d] == 0 ? 0 : 1)) : siz

function reduced_dims{N}(siz::NTuple{N,Int}, region)
    rsiz = [siz...]
    for i in region
        if 1 <= i <= N
            rsiz[i] = 1
        end
    end
    tuple(rsiz...)::typeof(siz)
end

function reduced_dims0{N}(siz::NTuple{N,Int}, region)
    rsiz = [siz...]
    for i in region
        if i <= i <= N
            rsiz[i] = (rsiz[i] == 0 ? 0 : 1)
        end
    end
    tuple(rsiz...)::typeof(siz)
end

function regionsize(a, region)
    s = 1
    for d in region
        s *= size(a,d)
    end
    s
end

###### Generic reduction functions #####

## initialization

for (Op, initfun) in ((:AddFun, :zero), (:MulFun, :one), (:MaxFun, :typemin), (:MinFun, :typemax))
    @eval initarray!{T}(a::AbstractArray{T}, ::$(Op), init::Bool) = (init && fill!(a, $(initfun)(T)); a)
end

for (Op, initval) in ((:AndFun, true), (:OrFun, false))
    @eval initarray!(a::AbstractArray, ::$(Op), init::Bool) = (init && fill!(a, $initval); a)
end

reducedim_initarray{R}(A::AbstractArray, region, v0, ::Type{R}) = fill!(similar(A,R,reduced_dims(A,region)), v0)
reducedim_initarray{T}(A::AbstractArray, region, v0::T) = reducedim_initarray(A, region, v0, T)

reducedim_initarray0{R}(A::AbstractArray, region, v0, ::Type{R}) = fill!(similar(A,R,reduced_dims0(A,region)), v0)
reducedim_initarray0{T}(A::AbstractArray, region, v0::T) = reducedim_initarray0(A, region, v0, T)

# TODO: better way to handle reducedim initialization
#
# The current scheme is basically following Steven G. Johnson's original implementation
#
function reducedim_init{T}(f, op::AddFun, A::AbstractArray{T}, region)
    if method_exists(zero, (Type{T},))
        x = evaluate(f, zero(T))
        z = zero(x) + zero(x)
        Tr = typeof(z) == typeof(x) && !isbits(T) ? T : typeof(z)
    else
        z = zero(sum(f, A))
        Tr = typeof(z)
    end
    return reducedim_initarray(A, region, z, Tr)
end

function reducedim_init{T}(f, op::MulFun, A::AbstractArray{T}, region)
    if method_exists(zero, (Type{T},))
        x = evaluate(f, zero(T))
        z = one(x) * one(x)
        Tr = typeof(z) == typeof(x) && !isbits(T) ? T : typeof(z)
    else
        z = one(prod(f, A))
        Tr = typeof(z)
    end
    return reducedim_initarray(A, region, z, Tr)
end

reducedim_init{T}(f, op::MaxFun, A::AbstractArray{T}, region) = reducedim_initarray0(A, region, typemin(evaluate(f, zero(T))))
reducedim_init{T}(f, op::MinFun, A::AbstractArray{T}, region) = reducedim_initarray0(A, region, typemax(evaluate(f, zero(T))))
reducedim_init{T}(f::Union(AbsFun,Abs2Fun), op::MaxFun, A::AbstractArray{T}, region) =
    reducedim_initarray(A, region, zero(evaluate(f, zero(T))))

reducedim_init(f, op::AndFun, A::AbstractArray, region) = reducedim_initarray(A, region, true)
reducedim_init(f, op::OrFun, A::AbstractArray, region) = reducedim_initarray(A, region, false)

# specialize to make initialization more efficient for common cases

typealias CommonReduceResult Union(Uint64,Uint128,Int64,Int128,Float32,Float64,Complex64,Complex128)

for (IT, RT) in ((:CommonReduceResult, :T), (:SmallSigned, :Int), (:SmallUnsigned, :Uint))
    @eval begin
        reducedim_init{T<:$IT}(f::Union(IdFun,AbsFun,Abs2Fun), op::AddFun, A::AbstractArray{T}, region) =
            reducedim_initarray(A, region, zero($RT))
        reducedim_init{T<:$IT}(f::Union(IdFun,AbsFun,Abs2Fun), op::MulFun, A::AbstractArray{T}, region) =
            reducedim_initarray(A, region, one($RT))
    end
end
reducedim_init(f::Union(IdFun,AbsFun,Abs2Fun), op::AddFun, A::AbstractArray{Bool}, region) =
    reducedim_initarray(A, region, 0)

## generic (map)reduction

has_fast_linear_indexing(a::AbstractArray) = false
has_fast_linear_indexing(a::Array) = true

function check_reducdims(R, A)
    # Check whether R has compatible dimensions w.r.t. A for reduction
    #
    # It returns an integer value value (useful for choosing implementation)
    # - If it reduces only along leading dimensions, e.g. sum(A, 1) or sum(A, (1, 2)),
    #   it returns the length of the leading slice. For the two examples above,
    #   it will be size(A, 1) or size(A, 1) * size(A, 2).
    # - Otherwise, e.g. sum(A, 2) or sum(A, (1, 3)), it returns 0.
    #
    lsiz = 1
    had_nonreduc = false
    for i = 1:ndims(A)
        sRi = size(R, i)
        sAi = size(A, i)
        if sRi == 1
            if sAi > 1
                if had_nonreduc
                    lsiz = 0  # to reduce along i, but some previous dimensions were non-reducing
                else
                    lsiz *= sAi  # if lsiz was set to zero, it will stay to be zero
                end
            end
        else
            sRi == sAi ||
                throw(DimensionMismatch("Reduction on array of size $(size(A)) with output of size $(size(R))"))
            had_nonreduc = true
        end
    end
    return lsiz
end

@ngenerate N typeof(R) function _mapreducedim!{T,N}(f, op, R::AbstractArray, A::AbstractArray{T,N})
    lsiz = check_reducdims(R, A)
    isempty(A) && return R
    @nextract N sizeR d->size(R,d)
    sizA1 = size(A, 1)

    if has_fast_linear_indexing(A) && lsiz > 16
        # use mapreduce_impl, which is probably better tuned to achieve higher performance
        nslices = div(length(A), lsiz)
        ibase = 0
        for i = 1:nslices
            @inbounds R[i] = mapreduce_impl(f, op, A, ibase+1, ibase+lsiz)
            ibase += lsiz
        end
    elseif size(R, 1) == 1 && sizA1 > 1
        # keep the accumulator as a local variable when reducing along the first dimension
        @nloops N i d->(d>1? (1:size(A,d)) : (1:1)) d->(j_d = sizeR_d==1 ? 1 : i_d) begin
            @inbounds r = (@nref N R j)
            for i_1 = 1:sizA1
                @inbounds v = evaluate(f, (@nref N A i))
                r = evaluate(op, r, v)
            end
            @inbounds (@nref N R j) = r
        end
    else
        # general implementation
        @nloops N i A d->(j_d = sizeR_d==1 ? 1 : i_d) begin
            @inbounds v = evaluate(f, (@nref N A i))
            @inbounds (@nref N R j) = evaluate(op, (@nref N R j), v)
        end
    end
    return R
end

mapreducedim!(f, op, R::AbstractArray, A::AbstractArray) = _mapreducedim!(f, op, R, A)

function mapreducedim!(f::Function, op, R::AbstractArray, A::AbstractArray)
    is(op, +) ? _mapreducedim!(f, AddFun(), R, A) :
    is(op, *) ? _mapreducedim!(f, MulFun(), R, A) :
    is(op, &) ? _mapreducedim!(f, AndFun(), R, A) :
    is(op, |) ? _mapreducedim!(f, OrFun(), R, A) :
    _mapreducedim!(f, op, R, A)
end

reducedim!{RT}(op, R::AbstractArray{RT}, A::AbstractArray) = mapreducedim!(IdFun(), op, R, A, zero(RT))

mapreducedim(f, op, A::AbstractArray, region, v0) = mapreducedim!(f, op, reducedim_initarray(A, region, v0), A)
mapreducedim{T}(f, op, A::AbstractArray{T}, region) = mapreducedim!(f, op, reducedim_init(f, op, A, region), A)

reducedim(op, A::AbstractArray, region, v0) = mapreducedim(IdFun(), op, A, region, v0)
reducedim(op, A::AbstractArray, region) = mapreducedim(IdFun(), op, A, region)

##### Specific reduction functions #####

for (fname, Op) in [(:sum, :AddFun), (:prod, :MulFun),
                    (:maximum, :MaxFun), (:minimum, :MinFun),
                    (:all, :AndFun), (:any, :OrFun)]

    fname! = symbol(string(fname, '!'))
    @eval begin
        $(fname!)(f::Union(Function,Func{1}), r::AbstractArray, A::AbstractArray; init::Bool=true) =
            mapreducedim!(f, $(Op)(), initarray!(r, $(Op)(), init), A)
        $(fname!)(r::AbstractArray, A::AbstractArray; init::Bool=true) = $(fname!)(IdFun(), r, A; init=init)

        $(fname)(f::Union(Function,Func{1}), A::AbstractArray, region) =
            mapreducedim(f, $(Op)(), A, region)
        $(fname)(A::AbstractArray, region) = $(fname)(IdFun(), A, region)
    end
end

for (fname, fbase, Fun) in [(:sumabs, :sum, :AbsFun),
                            (:sumabs2, :sum, :Abs2Fun),
                            (:maxabs, :maximum, :AbsFun),
                            (:minabs, :minimum, :AbsFun)]
    fname! = symbol(string(fname, '!'))
    fbase! = symbol(string(fbase, '!'))
    @eval begin
        $(fname!)(r::AbstractArray, A::AbstractArray; init::Bool=true) =
            $(fbase!)($(Fun)(), r, A; init=init)
        $(fname)(A::AbstractArray, region) = $(fbase)($(Fun)(), A, region)
    end
end

##### findmin & findmax #####

# Generate the body for a reduction function reduce!(f, Rval, Rind, A), using a comparison operator f
# Rind contains the index of A from which Rval was taken
function gen_findreduction_body(N, f::Function)
    F = Expr(:quote, f)
    quote
        (isempty(Rval) || isempty(A)) && return Rval, Rind
        for i = 1:$N
            (size(Rval, i) == size(A, i) || size(Rval, i) == 1) || throw(DimensionMismatch("Find-reduction on array of size $(size(A)) with output of size $(size(Rval))"))
            size(Rval, i) == size(Rind, i) || throw(DimensionMismatch("Find-reduction: outputs must be of the same size"))
        end
        @nexprs $N d->(sizeR_d = size(Rval,d))
        # If we're reducing along dimension 1, for efficiency we can make use of a temporary.
        # Otherwise, keep the result in Rval/Rind so that we traverse A in storage order.
        k = 0
        @inbounds if size(Rval, 1) < size(A, 1)
            @nloops $N i d->(d>1? (1:size(A,d)) : (1:1)) d->(j_d = sizeR_d==1 ? 1 : i_d) begin
                tmpRv = (@nref $N Rval j)
                tmpRi = (@nref $N Rind j)
                for i_1 = 1:size(A,1)
                    k += 1
                    tmpAv = (@nref $N A i)
                    if ($F)(tmpAv, tmpRv)
                        tmpRv = tmpAv
                        tmpRi = k
                    end
                end
                (@nref $N Rval j) = tmpRv
                (@nref $N Rind j) = tmpRi
            end
        else
            @nloops $N i A d->(j_d = sizeR_d==1 ? 1 : i_d) begin
                k += 1
                tmpAv = (@nref $N A i)
                if ($F)(tmpAv, (@nref $N Rval j))
                    (@nref $N Rval j) = tmpAv
                    (@nref $N Rind j) = k
                end
            end
        end
        Rval, Rind
    end
end

eval(ngenerate(:N, :(typeof((Rval,Rind))), :(_findmin!{T,N}(Rval::AbstractArray, Rind::AbstractArray, A::AbstractArray{T,N})), N->gen_findreduction_body(N, <)))
findmin!{R}(rval::AbstractArray{R}, rind::AbstractArray, A::AbstractArray; init::Bool=true) = _findmin!(initarray!(rval, typemax(R), init), rind, A)
findmin{T}(A::AbstractArray{T}, region) =
    isempty(A) ? (similar(A,reduced_dims0(A,region)), zeros(Int,reduced_dims0(A,region))) :
                  _findmin!(reducedim_initarray0(A, region, typemax(T)), zeros(Int,reduced_dims0(A,region)), A)

eval(ngenerate(:N, :(typeof((Rval,Rind))), :(_findmax!{T,N}(Rval::AbstractArray, Rind::AbstractArray, A::AbstractArray{T,N})), N->gen_findreduction_body(N, >)))
findmax!{R}(rval::AbstractArray{R}, rind::AbstractArray, A::AbstractArray; init::Bool=true) = _findmax!(initarray!(rval, typemin(R), init), rind, A)
findmax{T}(A::AbstractArray{T}, region) =
    isempty(A) ? (similar(A,reduced_dims0(A,region)), zeros(Int,reduced_dims0(A,region))) :
                  _findmax!(reducedim_initarray0(A, region, typemin(T)), zeros(Int,reduced_dims0(A,region)), A)


function _readgml(io::IO, line, ::Type{G}) where G
    mapping = Dict{Int,Int}()
    i = 0
    while startswith(line, "node")
        i += 1
        line = readline(io) |> strip
        line == "[" && (line = readline(io) |> strip)
        while !startswith(line, "]")
            name, valstr = splitgml(line)
            if name == "id"
                mapping[parse(Int, valstr)] = i
            end
            line = readline(io) |> strip
        end
        line = readline(io) |> strip #skip ]
    end
    g = G(i)
    while startswith(line, "edge")
        line = readline(io) |> strip
        line == "[" && (line = readline(io) |> strip)
        u = -1
        v = -1
        while !startswith(line, "]")
            name, valstr = splitgml(line)
            if name == "source"
                id = parse(Int, valstr)
                u = mapping[id]
            elseif name == "target"
                id = parse(Int, valstr)
                v = mapping[id]
            end
            line = readline(io) |> strip
        end
        @assert u > 0 && v > 0
        add_edge!(g, u, v)
        line = readline(io) |> strip #skip ]
    end
    return g
end

function readgml(io::IO, ::Type{G}) where G
    H = G
    line = readline(io) |> strip
    @assert startswith(line, "graph")
    line = readline(io) |> strip
    line == "[" && (line = readline(io) |> strip)
    while !startswith(line, "node") && !isempty(line)
        if startswith(line, "directed")
            H = parse(Int, line[10:end]) == 1 ? digraphtype(G) : graphtype(G)
        end
        line = readline(io) |> strip
    end
    return _readgml(io, line, H)
end

function gmltypeof(valstr)
    str = strip(valstr, '\"')
    if length(str) < length(valstr)
        return String
    else
        return Float64
    end
end

gmlval(::Type{T}, x) where {T} = parse(T, x)
gmlval(::Type{String}, x) = strip(x, '\"')
function splitgml(s::AbstractString)
    i = findfirst(isequal(' '), s)
    return SubString(s, 1, i-1), SubString(s, i+1, length(s))
end

gmlprintval(x) = x
gmlprintval(x::String) = "\"" * x * "\""

function _readnetgml(io::IO, line, g)
    mapping = Dict{Int,Int}()
    i = 0
    while startswith(line, "node")
        i += 1
        line = readline(io) |> strip
        line == "[" && (line = readline(io) |> strip)
        @assert startswith(line, "id")
        name, valstr = splitgml(line)
        mapping[parse(Int, valstr)] = i
        add_vertex!(g)
        line = readline(io) |> strip
        while !startswith(line, "]")
            name, valstr = splitgml(line)
            !has_vprop(g, name) && vprop!(g, name, String)
            T = valtype(vprop(g, name))
            vprop(g, name)[i] = gmlval(T, valstr)
            line = readline(io) |> strip
        end

        line = readline(io) |> strip #skip ]
    end

    while startswith(line, "edge")
        line = readline(io) |> strip
        line == "[" && (line = readline(io) |> strip)
        startswith(line, "id") && (line = readline(io) |> strip)

        @assert startswith(line, "source")
        name, valstr = splitgml(line)
        u = mapping[parse(Int, valstr)]
        line = readline(io) |> strip
        @assert startswith(line, "target")
        name, valstr = splitgml(line)
        v = mapping[parse(Int, valstr)]
        line = readline(io) |> strip
        ok, e = add_edge!(g, u, v)

        while !startswith(line, "]")
            name, valstr = splitgml(line)
            !has_eprop(g, name) && eprop!(g, name, gmltypeof(valstr))
            T = valtype(eprop(g, name))
            eprop(g, name)[e] = gmlval(T, valstr)
            line = readline(io) |> strip
        end

        line = readline(io) |> strip #skip ]
    end
    return g
end

function readnetgml(io::IO, ::Type{G}) where G<:ANetOrDiNet
    H = G
    line = readline(io) |> strip
    @assert startswith(line, "graph")
    line = readline(io) |> strip
    line == "[" && (line = readline(io) |> strip)
    gdict = Dict{String, String}()
    while !startswith(line, "node") && !isempty(line)
        if startswith(line, "directed")
            H = parse(Int, line[10:end]) == 1 ? digraphtype(G) : graphtype(G)
        else
            name, valstr = splitgml(line)
            gdict[name] = gmlval(String, valstr)
        end
        line = readline(io) |> strip
    end
    g = H()
    for (name, val) in gdict
        gprop!(g, name, val)
    end
    return _readnetgml(io, line, g)
end

"""
    writegml(f, g)

Writes a graph `g` to a file `f` in the
[GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) format.
"""
function writegml(io::IO, g::AGraphOrDiGraph)
    println(io, "graph [")
    # length(gname) > 0 && println(io, "label \"$gname\"")
    is_directed(g) && println(io, "\tdirected 1")
    for i=1:nv(g)
        println(io,"\tnode [")
        println(io,"\t\tid $i")
        println(io,"\t]")
    end
    for (s,t) in edges(g)
        println(io,"\tedge [")
        println(io,"\t\tsource $s")
        println(io,"\t\ttarget $t")
        println(io,"\t]")
    end
    println(io, "]")
    return 1
end

function writenetgml(io::IO, g::ANetOrDiNet)
    println(io, "graph [")
    is_directed(g) && println(io, "\tdirected 1")
    for (pname, p) in gprop(g)
        println(io,"\t$pname $(gmlprintval(p))")
    end
    for i=1:nv(g)
        println(io,"\tnode [")
        println(io,"\t\tid $i")
        for (name, val) in vprop(g, i)
            println(io,"\t\t$name $(gmlprintval(val))")
        end
        println(io,"\t]")
    end
    for e in edges(g)
        println(io,"\tedge [")
        println(io,"\t\tsource $(src(e))")
        println(io,"\t\ttarget $(dst(e))")
        for (name, val) in eprop(g, e)
            println(io,"\t\t$name $(gmlprintval(val))")
        end
        println(io,"\t]")
    end
    println(io, "]")
    return 1
end

filemap[:gml] = (readgml, writegml, readnetgml, writenetgml)


#=
Time steps for the MixedSolver module.
Since the FE and FV portions may use different steppers, this must work for both,
but they can be done separately by only passing one set of variables.
For all of them:
input
- fe_var
- fv_var
- vol_lhs
- vol_rhs
- surf_lhs
- surf_rhs
- step_info = (t, dt, dofs_per_node_fe, dofs_per_loop_fe, assemble_func_fe, dofs_per_node_fv, dofs_per_loop_fv, assemble_func_fv)
- fe_storage = (A, b, fe_sol, allocated_vecs, b_order, b_sizes)
- fv_storage = (fv_sol, allocated_vecs_fv)
=#

function mixed_euler_explicit_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    fe_stepper = step_info[1];
    fv_stepper = step_info[2];
    dofs_per_node_fe = step_info[3];
    dofs_per_loop_fe = step_info[4];
    fe_assemble_func = step_info[5];
    dofs_per_node_fv = step_info[6];
    dofs_per_loop_fv = step_info[7];
    fv_assemble_func = step_info[8];
    dt = fe_stepper.dt;
    assemble_t = 0;
    linsolve_t = 0;

    if !(fv_var === nothing) && config.num_partitions > 1 exchange_ghosts(fv_var, fv_grid, i); end
    pre_step_function();

    ### First step the FE part #######################################################################################
    if !(fe_var === nothing)
        # Unpack things
        A = fe_storage[1];
        b = fe_storage[2];
        fe_sol = fe_storage[3];
        allocated_vecs = fe_storage[4];
        b_order = fe_storage[5];
        b_sizes = fe_storage[6];

        N1 = size(grid_data.allnodes,2);

        assemble_t += @elapsed begin
                                    b = assemble(fe_var, nothing, vol_rhs[1], allocated_vecs, dofs_per_node_fe, dofs_per_loop_fe, t, dt; rhs_only = true, assemble_loops=fe_assemble_func);
                                    b = gather_system(nothing, b, N1, dofs_per_node_fe, b_order, b_sizes);
                                end

        linsolve_t += @elapsed(tmpvec = distribute_solution( linear_system_solve(A,b) , N1, dofs_per_node_fe, b_order, b_sizes));

        # At this point tmpvec holds the boundary values
        # directly write them to the variable values and zero sol.
        copy_bdry_vals_to_variables(fe_var, tmpvec, grid_data, dofs_per_node_fe, zero_vals=true);

        for i=1:length(fe_sol)
            fe_sol[i] = fe_sol[i] + dt * tmpvec[i];
        end

        copy_bdry_vals_to_vector(fe_var, fe_sol, grid_data, dofs_per_node_fe);
        place_sol_in_vars(fe_var, fe_sol);

    end

    ### Then step the FV part ######################################################################################
    if !(fv_var === nothing)
        fv_sol = fv_storage[1];
        fv_allocated_vecs = fv_storage[2];

        assemble_t += @elapsed(tmpvec = fv_assemble(fv_var, vol_lhs[2], vol_rhs[2], surf_lhs[2], surf_rhs[2], fv_allocated_vecs, dofs_per_node_fv, dofs_per_loop_fv, t, dt, assemble_loops=fv_assemble_func));

        for i=1:length(fv_sol)
            fv_sol[i] = fv_sol[i] + dt * tmpvec[i];
        end

        FV_copy_bdry_vals_to_vector(fv_var, fv_sol, fv_grid, dofs_per_node_fv);
        place_sol_in_vars(fv_var, fv_sol);
    end

    post_step_function();

    return (assemble_t, linsolve_t);
end

function mixed_euler_implicit_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    fe_stepper = step_info[1];
    fv_stepper = step_info[2];
    dofs_per_node_fe = step_info[3];
    dofs_per_loop_fe = step_info[4];
    fe_assemble_func = step_info[5];
    dofs_per_node_fv = step_info[6];
    dofs_per_loop_fv = step_info[7];
    fv_assemble_func = step_info[8];
    dt = fe_stepper.dt;
    assemble_t = 0;
    linsolve_t = 0;

    if !(fv_var === nothing) && config.num_partitions > 1 exchange_ghosts(fv_var, fv_grid, i); end
    pre_step_function();

    ### First step the FE part #######################################################################################
    if !(fe_var === nothing)
        # Unpack things
        A = fe_storage[1];
        b = fe_storage[2];
        fe_sol = fe_storage[3];
        allocated_vecs = fe_storage[4];
        b_order = fe_storage[5];
        b_sizes = fe_storage[6];

        N1 = size(grid_data.allnodes,2);

        assemble_t += @elapsed begin
                                    b = assemble(fe_var, nothing, vol_rhs[1], allocated_vecs, dofs_per_node_fe, dofs_per_loop_fe, t, dt; rhs_only = true, assemble_loops=fe_assemble_func);
                                    b = gather_system(nothing, b, N1, dofs_per_node_fe, b_order, b_sizes);
                                end

        linsolve_t += @elapsed(tmpvec = distribute_solution( linear_system_solve(A,b) , N1, dofs_per_node_fe, b_order, b_sizes));

        # At this point tmpvec holds the boundary values
        # directly write them to the variable values and zero sol.
        copy_bdry_vals_to_variables(fe_var, tmpvec, grid_data, dofs_per_node_fe, zero_vals=true);

        for i=1:length(fe_sol)
            fe_sol[i] = tmpvec[i];
        end

        copy_bdry_vals_to_vector(fe_var, fe_sol, grid_data, dofs_per_node_fe);
        place_sol_in_vars(fe_var, fe_sol);
    end

    ### Then step the FV part ######################################################################################
    if !(fv_var === nothing)
        fv_sol = fv_storage[1];
        fv_allocated_vecs = fv_storage[2];

        ## TODO

        FV_copy_bdry_vals_to_vector(fv_var, fv_sol, fv_grid, dofs_per_node_fv);
        place_sol_in_vars(fv_var, fv_sol);
    end

    post_step_function();

    return (assemble_t, linsolve_t);
end

function mixed_crank_nicholson_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    fe_stepper = step_info[1];
    fv_stepper = step_info[2];
    dofs_per_node_fe = step_info[3];
    dofs_per_loop_fe = step_info[4];
    fe_assemble_func = step_info[5];
    dofs_per_node_fv = step_info[6];
    dofs_per_loop_fv = step_info[7];
    fv_assemble_func = step_info[8];
    dt = fe_stepper.dt;
    assemble_t = 0;
    linsolve_t = 0;

    if !(fv_var === nothing) && config.num_partitions > 1 exchange_ghosts(fv_var, fv_grid, i); end
    pre_step_function();

    ### First step the FE part #######################################################################################
    if !(fe_var === nothing)
        # Unpack things
        A = fe_storage[1];
        b = fe_storage[2];
        fe_sol = fe_storage[3];
        allocated_vecs = fe_storage[4];
        b_order = fe_storage[5];
        b_sizes = fe_storage[6];

        N1 = size(grid_data.allnodes,2);

        assemble_t += @elapsed begin
            b = assemble(fe_var, nothing, vol_rhs[1], allocated_vecs, dofs_per_node_fe, dofs_per_loop_fe, t, dt; rhs_only = true, assemble_loops=fe_assemble_func);
            b = gather_system(nothing, b, N1, dofs_per_node_fe, b_order, b_sizes);
        end

        linsolve_t += @elapsed(tmpvec = distribute_solution( linear_system_solve(A,b) , N1, dofs_per_node_fe, b_order, b_sizes));

        # At this point tmpvec holds the boundary values
        # directly write them to the variable values and zero sol.
        copy_bdry_vals_to_variables(fe_var, tmpvec, grid_data, dofs_per_node_fe, zero_vals=true);

        for i=1:length(fe_sol)
        fe_sol[i] = tmpvec[i];
        end

        copy_bdry_vals_to_vector(fe_var, fe_sol, grid_data, dofs_per_node_fe);
        place_sol_in_vars(fe_var, fe_sol);
    end

    ### Then step the FV part ######################################################################################
    if !(fv_var === nothing)
        fv_sol = fv_storage[1];
        fv_allocated_vecs = fv_storage[2];

        ## TODO

        FV_copy_bdry_vals_to_vector(fv_var, fv_sol, fv_grid, dofs_per_node_fv);
        place_sol_in_vars(fv_var, fv_sol);
    end

    post_step_function();

    return (assemble_t, linsolve_t);
end

function mixed_lsrk4_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    fe_stepper = step_info[1];
    fv_stepper = step_info[2];
    dofs_per_node_fe = step_info[3];
    dofs_per_loop_fe = step_info[4];
    fe_assemble_func = step_info[5];
    dofs_per_node_fv = step_info[6];
    dofs_per_loop_fv = step_info[7];
    fv_assemble_func = step_info[8];
    dt = fe_stepper.dt;
    assemble_t = 0;
    linsolve_t = 0;

    ### First step the FE part #######################################################################################
    if !(fe_var === nothing)
        # Unpack things
        A = fe_storage[1];
        b = fe_storage[2];
        fe_sol = fe_storage[3];
        allocated_vecs = fe_storage[4];
        b_order = fe_storage[5];
        b_sizes = fe_storage[6];
        tmppi = fe_storage[7];
        tmpki = fe_storage[8];

        N1 = size(grid_data.allnodes,2);

        # Low storage RK4:
        # p0 = u
        #   ki = ai*k(i-1) + dt*f(p(i-1), t+ci*dt)
        #   pi = p(i-1) + bi*ki
        # u = p5

        tmppi = get_var_vals(fe_var, tmppi);
        for rki=1:fe_stepper.stages
            rktime = t + fe_stepper.c[rki]*fe_stepper.dt;
            # p(i-1) is currently in u
            pre_step_function();

            ### First step the FE part #######################################################################################
            assemble_t += @elapsed begin
                                b = assemble(fe_var, nothing, vol_rhs[1], allocated_vecs, dofs_per_node_fe, dofs_per_loop_fe, rktime, fe_stepper.dt; rhs_only = true, assemble_loops=fe_assemble_func);
                                b = gather_system(nothing, b, N1, dofs_per_node_fe, b_order, b_sizes);
                            end

            linsolve_t += @elapsed(fe_sol = distribute_solution( linear_system_solve(A,b) , N1, dofs_per_node_fe, b_order, b_sizes));

            # At this point sol holds the boundary values
            # directly write them to the variable values and zero sol.
            copy_bdry_vals_to_variables(fe_var, fe_sol, grid_data, dofs_per_node_fe, zero_vals=true);

            if rki == 1 # because a1 == 0
                tmpki = fe_stepper.dt .* fe_sol;
            else
                tmpki = fe_stepper.a[rki].*tmpki + fe_stepper.dt.*fe_sol;
            end
            tmppi = tmppi + fe_stepper.b[rki].*tmpki

            copy_bdry_vals_to_vector(fe_var, tmppi, grid_data, dofs_per_node_fe);
            place_sol_in_vars(fe_var, tmppi);

            post_step_function();
        end

    end

    ### Then step the FV part ######################################################################################
    if !(fv_var === nothing)
        fv_sol = fv_storage[1];
        fv_allocated_vecs = fv_storage[2];
        fv_tmppi = fv_storage[3];
        fv_tmpki = fv_storage[4];

        fv_tmppi = get_var_vals(fv_var, fv_tmppi);
        for rki=1:fv_stepper.stages
            rktime = t + fv_stepper.c[rki]*fv_stepper.dt;
            # p(i-1) is currently in u
            if config.num_partitions > 1 exchange_ghosts(fv_var, fv_grid, i); end
            pre_step_function();

            assemble_t += @elapsed(fv_sol = fv_assemble(fv_var, vol_lhs[2], vol_rhs[2], surf_lhs[2], surf_rhs[2], fv_allocated_vecs, dofs_per_node_fv, dofs_per_loop_fv, rktime, dt, assemble_loops=fv_assemble_func));

            if rki == 1 # because a1 == 0
                fv_tmpki = fv_stepper.dt .* fv_sol;
            else
                fv_tmpki = fv_stepper.a[rki].*fv_tmpki + fv_stepper.dt.*fv_sol;
            end
            fv_tmppi = fv_tmppi + fv_stepper.b[rki].*fv_tmpki

            FV_copy_bdry_vals_to_vector(fv_var, fv_tmppi, fv_grid, dofs_per_node_fv);
            place_sol_in_vars(fv_var, fv_tmppi);

            post_step_function();
        end
    end

    return (assemble_t, linsolve_t);
end

# This may be removed.
function mixed_rk4_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    return mixed_multistage_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage);
end

function mixed_multistage_step(fe_var, fv_var, vol_lhs, vol_rhs, surf_lhs, surf_rhs, t, step_info, fe_storage, fv_storage)
    fe_stepper = step_info[1];
    fv_stepper = step_info[2];
    dofs_per_node_fe = step_info[3];
    dofs_per_loop_fe = step_info[4];
    fe_assemble_func = step_info[5];
    dofs_per_node_fv = step_info[6];
    dofs_per_loop_fv = step_info[7];
    fv_assemble_func = step_info[8];
    dt = fe_stepper.dt;
    assemble_t = 0;
    linsolve_t = 0;

    ### First step the FE part #######################################################################################
    if !(fe_var === nothing)
        # Unpack things
        A = fe_storage[1];
        b = fe_storage[2];
        fe_sol = fe_storage[3];
        allocated_vecs = fe_storage[4];
        b_order = fe_storage[5];
        b_sizes = fe_storage[6];
        tmpresult = fe_storage[7];
        tmpki = fe_storage[8];

        N1 = size(grid_data.allnodes,2);

        # solution will be placed in var.values for each stage
        for stage=1:fe_stepper.stages
            stime = t + fe_stepper.c[stage]*fe_stepper.dt;

            # Update the values in vars to be used in this stage
            if stage > 1
                initialized_tmpresult = false;
                for j=1:stage
                    if fe_stepper.a[stage, j] > 0
                        if !initialized_tmpresult
                            initialized_tmpresult = true;
                            for k=1:length(fe_sol)
                                tmpresult[k] = fe_sol[k] + fe_stepper.dt * fe_stepper.a[stage, j] * tmpki[k,j];
                            end
                        else
                            for k=1:length(fe_sol)
                                tmpresult[k] += fe_stepper.dt * fe_stepper.a[stage, j] * tmpki[k,j];
                            end
                        end
                    end
                end

                if initialized_tmpresult
                    copy_bdry_vals_to_vector(fe_var, tmpresult, grid_data, dofs_per_node_fe);
                    place_sol_in_vars(fe_var, tmpresult);
                end
                post_step_function(); # seems weird, but imagine this is happening after stage-1
            end

            pre_step_function();

            ### First step the FE part #######################################################################################
            assemble_t += @elapsed begin
                                b = assemble(fe_var, nothing, vol_rhs[1], allocated_vecs, dofs_per_node_fe, dofs_per_loop_fe, stime, fe_stepper.dt; rhs_only = true, assemble_loops=fe_assemble_func);
                                b = gather_system(nothing, b, N1, dofs_per_node_fe, b_order, b_sizes);
                            end

            linsolve_t += @elapsed(tmpki[:,stage] = distribute_solution( linear_system_solve(A,b) , N1, dofs_per_node_fe, b_order, b_sizes));

            # At this point tmpki[:,stage] holds the boundary values
            # directly write them to the variable values and zero sol.
            copy_bdry_vals_to_variables(fe_var, tmpki[:,stage], grid_data, dofs_per_node_fe, zero_vals=true);

        end
        for i=1:length(fe_sol)
            for stage=1:fe_stepper.stages
                fe_sol[i] += fe_stepper.dt * fe_stepper.b[stage] * tmpki[i, stage];
            end
        end

        copy_bdry_vals_to_vector(fe_var, fe_sol, grid_data, dofs_per_node_fe);
        place_sol_in_vars(fe_var, fe_sol);

        post_step_function();
    end

    ### Then step the FV part ######################################################################################
    if !(fv_var === nothing)
        fv_sol = fv_storage[1];
        fv_allocated_vecs = fv_storage[2];
        fv_tmpresult = fv_storage[3];
        fv_tmpki = fv_storage[4];

        # solution will be placed in var.values for each stage
        for stage=1:fv_stepper.stages
            stime = t + fv_stepper.c[stage]*fv_stepper.dt;

            # Update the values in vars to be used in this stage
            if stage > 1
                initialized_tmpresult = false;
                for j=1:stage
                    if fv_stepper.a[stage, j] > 0
                        if !initialized_tmpresult
                            initialized_tmpresult = true;
                            for k=1:length(fv_sol)
                                fv_tmpresult[k] = fv_sol[k] + fv_stepper.dt * fv_stepper.a[stage, j] * fv_tmpki[k,j];
                            end
                        else
                            for k=1:length(fv_sol)
                                fv_tmpresult[k] += fv_stepper.dt * fv_stepper.a[stage, j] * fv_tmpki[k,j];
                            end
                        end
                    end
                end

                if initialized_tmpresult
                    FV_copy_bdry_vals_to_vector(fv_var, fv_tmpresult, fv_grid, dofs_per_node_fv);
                    place_sol_in_vars(fv_var, fv_tmpresult);
                end
                post_step_function(); # seems weird, but imagine this is happening after stage-1
            end

            if config.num_partitions > 1 exchange_ghosts(fv_var, fv_grid, i); end
            pre_step_function();

            tmpki[:,stage] = fv_assemble(fv_var, vol_lhs[2], vol_rhs[2], surf_lhs[2], surf_rhs[2], fv_allocated_vecs, dofs_per_node_fv, dofs_per_loop_fv, stime, fv_stepper.dt, assemble_loops=fv_assemble_func);
        end
        for i=1:length(fv_sol)
            for stage=1:fv_stepper.stages
                fv_sol[i] += fv_stepper.dt * fv_stepper.b[stage] * fv_tmpki[i, stage];
            end
        end
        FV_copy_bdry_vals_to_vector(fv_var, fv_sol, fv_grid, dofs_per_node_fv);
        place_sol_in_vars(fv_var, fv_sol);

        post_step_function();
    end

    return (assemble_t, linsolve_t);
end


# Replace Flux's LSTMCell with BasicLSTMCell from TensorFlow 1.
# It's implementation is slightly different.
# https://github.com/tensorflow/tensorflow/blob/v2.4.1/tensorflow/python/keras/layers/legacy_rnn/rnn_cell_impl.py#L786-L787
# https://github.com/FluxML/Flux.jl/blob/master/src/layers/recurrent.jl#L141-L142
# The replacement is required because the pre-trained models use BasicLSTMCell.
struct BasicLSTMCell{A,V,S}
    Wi::A
    Wh::A
    b::V
    state0::S
end

function BasicLSTMCell(in::Integer, out::Integer;
                       init = Flux.glorot_uniform,
                       initb = zeros,
                       init_state = zeros)
    cell = BasicLSTMCell(init(out * 4, in), init(out * 4, out), initb(out * 4), (init_state(out,1), init_state(out,1)))
    cell.b[gate(out, 2)] .= 1
    return cell
end

function (m::BasicLSTMCell)((h, c), x) where {A,V,T}
    b, o = m.b, size(h, 1)
    g = m.Wi*x .+ m.Wh*h .+ b
    input = σ.(gate(g, o, 1))
    cell = tanh.(gate(g, o, 2))
    forget = σ.(gate(g, o, 3))
    output = σ.(gate(g, o, 4))
    c = forget .* c .+ input .* cell
    h′ = output .* tanh.(c)
    sz = size(x)
    return (h′, c), reshape(h′, :, sz[2:end]...)
end

Flux.@functor BasicLSTMCell

Base.show(io::IO, l::BasicLSTMCell) =
    print(io, "BasicLSTMCell(", size(l.Wi, 2), ", ", size(l.Wi, 1)÷4, ")")

"""
    BasicLSTM(in::Integer, out::Integer)
Implements the BasicLSTMCell from TensorFlow 1
https://github.com/tensorflow/tensorflow/blob/v2.4.1/tensorflow/python/keras/layers/legacy_rnn/rnn_cell_impl.py#L648-L814
"""
BasicLSTM(a...; ka...) = Flux.Recur(BasicLSTMCell(a...; ka...))
Flux.Recur(m::BasicLSTMCell) = Flux.Recur(m, m.state0)


#------- SET UP DESIGN PARAMETERS --------#

struct plyproperties
    names#::Array{String,1}
    plies::Array{Composites.material,1}
end

struct geometry
    t::Array{Float64,1}
    s::Array{Float64,1}
    x::Array{Float64,1} #x-location of airfoil section (positive x = downstream)
    y::Array{Float64,1} #y-location of airfoil section (positive y = spanwise)
    z::Array{Float64,1} #z-location of airfoil section (positive z = up)
    chord::Array{Float64,1}
    normalchord::Array{Float64,1} #normal chord length
    twist::Array{Float64,1} #airfoil twist in degrees
    sweep::Array{Float64,1}
    dihedral::Array{Float64,1}
    airfoilthickness::Array{Float64,1}
    xaf::Array{Float64,2}
    yaf::Array{Float64,2}
end

struct compstructure
    material::Array{Array{Composites.material,1},1}
    laminate::Array{Composites.laminate,1}
    A::Array{Array{Float64,2},1}
    B::Array{Array{Float64,2},1}
    D::Array{Array{Float64,2},1}
    bucklingstrain::Array{Float64,2}
    precompinput::Array{PreComp.input,1}
    precompoutput::Array{PreComp.output,1}
end

struct designparameters
    # configuration assumptions
    p_n_akima::Int64
    p_radii::Array{Float64,1}
    p_webloc::Array{Float64,1}

    # operating point assumptions
    altitude::Float64 #m

    # material properties
    plyprops::plyproperties
    p_usedmaterials
    p_orientation::Array{Float64,1}

    # Propeller Design
    p_lam_t::Array{Float64,1}

    p_chord::Array{Float64,1}
    p_sweep_d::Array{Float64,1}
    p_twist_d::Array{Float64,1}
    p_pitch_d::Float64
    p_dihedral_d::Array{Float64,1}
    p_airfoilthickness::Array{Float64,1}
    p_aerocenter::Array{Float64,1}
    p_x_offset::Array{Float64,1}
    velocity::Float64
    p_Rtip::Float64
    blades::Float64
    numprops::Int64
    machtip::Float64
    p_rpm::Float64
    kv::Float64
    i0::Float64
    batt_mass::Float64

    # Wing Design
    w_n_linear::Int64
    w_webloc::Array{Float64,1}
    w_lam_t::Array{Float64,1}
    w_chord::Array{Float64,1}
    w_aoa_d::Float64
    w_twist_d::Array{Float64,1}
    w_halfspan::Float64
    w_nondim_halfspany::Array{Float64,1}
    w_sweep_d::Array{Float64,1}
    w_dihedral_d::Array{Float64,1}
    w_usedmaterials #array of strings???
    w_orientation::Array{Float64,1}
    w_x_offset::Array{Float64,1}
    w_aerocenter::Array{Float64,1}

    n_CP::Float64
    payload::Float64
    max_mass::Float64
    gravity::Float64
    h_set::Float64
    specif_energy::Float64
    required_range::Float64
    required_takeoff_dist::Float64
    altitude_accel::Float64
    altitude_climb::Float64
    altitude_cruise::Float64
    required_noise::Float64
    required_cruise::Float64

end

function designparameters(;
    p_n_akima = 20,
    p_radii::Array{Float64,1}=Float64[0.0,0.2,.4,.6,.8,1.0],
    p_webloc::Array{Float64,1}=[1.0],
    altitude::Float64=18288.0,
    plyprops::plyproperties=plyproperties(),
    p_usedmaterials = ["highmodulus_uni","highmodulus_weave","taylor_foam"],
    p_orientation=[0.0,-45],
    # structural design variables
    p_lam_t = ones(length(p_radii)*length(p_usedmaterials)),
    # geometric design variables
    p_chord = Float64[0.1, 0.1, 0.1, 0.1, 0.1],
    p_sweep_d = Float64[0.0,0.0,0.0,0.0,0.0],
    p_twist_d = Float64[30.0, 30.0, 30.0, 30.0],
    p_pitch_d = 0.0,
    p_dihedral_d = fill(0.0,5),
    p_airfoilthickness = Float64[0.147538, 0.132307, 0.127404, 0.130604, 0.245836, 0.0629818],
    p_aerocenter = ones(p_radii)*0.25,
    p_x_offset = zeros(p_radii),
    # operating point design variables
    velocity = 15.40662113981876,
    # propulsion design variables
    p_Rtip = 2.25,
    blades = 2,
    numprops = 4,
    machtip = 0.8,
    p_rpm = 1020.45,
    kv = 4.271513933940295,
    i0 = 0.1,
    batt_mass = 1E8,

    w_n_linear = 20,
    w_halfspan = 10.0,
    w_webloc::Array{Float64,1}=[0.40],
    w_usedmaterials = ["highmodulus_uni","highmodulus_weave","taylor_foam"],
    w_nondim_halfspany = linspace(0,w_halfspan,length(w_chord)),
    w_lam_t = ones(length(w_nondim_halfspany)*length(w_usedmaterials)),
    w_chord = [0.5,0.5],
    w_aoa_d = 0.0,
    w_twist_d = [0.0,0.0],
    w_sweep_d = [0.0,0.0],
    w_dihedral_d = [0.0,0.0],
    w_orientation = [0.0,45.0,45.0], #uni, weave, webweave
    w_x_offset = ones(w_nondim_halfspany)*0.25,
    w_aerocenter = ones(w_nondim_halfspany)*0.275,

    n_CP = 60.0,
    payload = 684.0,
    max_mass = 1500.0,
    gravity = 9.81,
    h_set = 20.0, #meters
    specif_energy = 300.0, #wh/kg
    required_range = 150000.0, #km
    required_takeoff_dist = 395.0, #meters
    altitude_accel = 0.0,
    altitude_climb = 0.0,
    altitude_cruise = 3000.0,
    required_noise = 68.0,
    required_cruise = 28.0,

    )

    parameters = designparameters(p_n_akima,p_radii,p_webloc,altitude,plyprops,p_usedmaterials,p_orientation,p_lam_t,
    p_chord,p_sweep_d,p_twist_d,p_pitch_d,p_dihedral_d,p_airfoilthickness,p_aerocenter,p_x_offset,velocity,p_Rtip,
    blades,numprops,machtip,p_rpm,kv,i0,batt_mass,w_n_linear,w_webloc,w_lam_t,w_chord,w_aoa_d,w_twist_d,w_halfspan,w_nondim_halfspany,w_sweep_d,w_dihedral_d,w_usedmaterials,
    w_orientation,w_x_offset,w_aerocenter,n_CP,payload,max_mass,gravity,h_set,
    specif_energy,required_range,required_takeoff_dist,altitude_accel,altitude_climb,
    altitude_cruise,required_noise,required_cruise)
    return parameters
end

struct propwingdesign
    #Design Variables at this scope
    batt_mass::Float64
    w_aoa::Float64
    p_rpm::Float64
    p_pitch::Float64
    velocity::Float64

    #Design Parameters at this scope
    constraints
    af
    p_xaf
    p_yaf
    p_xafstrain
    p_yafstrain
    w_xaf
    w_yaf
    w_xafstrain
    w_yafstrain
    spline_3D_freestream
    spline_3D_blown

    # Design variables unique to the system function
    p_lam_tin
    p_orientation::Array{Float64,1}
    p_chord_pts::Array{Float64,1}
    p_twist_pts::Array{Float64,1}
    p_Rtip::Float64
    kv::Float64
    i0::Float64
    w_chord_pts::Array{Float64,1}
    w_twist_pts::Array{Float64,1}
    w_halfspan::Float64
    w_sweep_pts::Array{Float64,1}
    w_dihedral_pts::Array{Float64,1}
    w_lam_tin
    w_orientation::Array{Float64,1}
    w_airfoilthickness::Array{Float64,1}

    #Design variables unique to the system function
    etaprop_center::Array{Float64,1}
    p_n_akima::Int64
    p_radii::Array{Float64,1}
    altitude::Float64
    numprops::Int64
    p_x_offset::Array{Float64,1}
    p_aerocenter::Array{Float64,1}
    p_usedmaterials
    p_webloc::Array{Float64,1}
    plyprops
    blades::Float64
    w_n_linear::Int64
    w_x_offset::Array{Float64,1}
    w_aerocenter::Array{Float64,1}
    w_nondim_halfspany::Array{Float64,1}
    w_usedmaterials
    w_webloc::Array{Float64,1}
    prop_tilt::Float64

    #Control parameters
    detailed::Bool
    VTKfilename
    savefinalvtk::Bool
    verification::Bool
    plots::Bool
    n_CP::Int64

end

function propwingdesign(;
    #Design Variables at this scope
    batt_mass = 0.0,
    w_aoa = 0.0,
    p_rpm = 0.0,
    p_pitch = 0.0,
    velocity = 0.0,

    #Design Parameters at this scope
    constraints = [],
    af = [],
    p_xaf = [],
    p_yaf = [],
    p_xafstrain = [],
    p_yafstrain = [],
    w_xaf = [],
    w_yaf = [],
    w_xafstrain = [],
    w_yafstrain = [],
    spline_3D_freestream = [],
    spline_3D_blown = [],

    # Design variables unique to the system function
    p_lam_tin= [],
    p_orientation = zeros(2),
    p_chord_pts = zeros(2),
    p_twist_pts = zeros(2),
    p_Rtip =0.0,
    kv =0.0,
    i0 =0.0,
    w_chord_pts = zeros(2),
    w_twist_pts = zeros(2),
    w_halfspan =0.0,
    w_sweep_pts = zeros(2),
    w_dihedral_pts = zeros(2),
    w_lam_tin= [],
    w_orientation = zeros(2),
    w_airfoilthickness = zeros(2),

    #Design variables unique to the system function
    etaprop_center = zeros(2),
    p_n_akima = 10 ,
    p_radii = zeros(2),
    altitude =0.0,
    numprops = 10 ,
    p_x_offset = zeros(2),
    p_aerocenter = zeros(2),
    p_usedmaterials = [],
    p_webloc = zeros(2),
    plyprops = [],
    blades = 10 ,
    w_n_linear = 10 ,
    w_x_offset = zeros(2),
    w_aerocenter = zeros(2),
    w_nondim_halfspany = zeros(2),
    w_usedmaterials = [],
    w_webloc = zeros(2),
    prop_tilt = 0.0,

    #Control parameters
    detailed = false,
    VTKfilename = "notdefined.vtk",
    savefinalvtk = false,
    verification = false,
    plots = false,
    n_CP = 60,
    )

    return propwingdesign(batt_mass, w_aoa, p_rpm, p_pitch, velocity,
        constraints, af, p_xaf, p_yaf, p_xafstrain, p_yafstrain, w_xaf, w_yaf,
        w_xafstrain, w_yafstrain, spline_3D_freestream, spline_3D_blown,
        p_lam_tin, p_orientation, p_chord_pts, p_twist_pts, p_Rtip, kv, i0,
        w_chord_pts, w_twist_pts, w_halfspan, w_sweep_pts, w_dihedral_pts,
         w_lam_tin, w_orientation, w_airfoilthickness, etaprop_center,
        p_n_akima, p_radii, altitude, numprops, p_x_offset, p_aerocenter,
        p_usedmaterials, p_webloc, plyprops, blades, w_n_linear, w_x_offset,
        w_aerocenter, w_nondim_halfspany, w_usedmaterials, w_webloc, prop_tilt,
        detailed, VTKfilename, savefinalvtk, verification, plots, n_CP)
end

struct propwing_out
    #Propulsion
    ETA_total::Float64
    ETA_m::Float64
    ETA_p::Float64
    # Propeller
    CT::Float64
    CQ::Float64
    Np::Array{Float64,1}
    Tp::Array{Float64,1}
    p_alphas::Array{Float64,1}
    uprop::Array{Float64,1}
    vprop::Array{Float64,1}
    total_thrust::Float64
    advance_ratio::Float64
    torque::Float64
    db_test::Array{Float64,1}

    # Motor
    V_m::Float64
    I_m::Float64
    # Masses
    propwing_mass::Float64
    mass_prop::Float64
    mass_ESC::Float64
    mass_motor::Float64
    mass_wing::Float64

    # Wing
    panels#::Array{Float64,1}
    Lift::Float64
    Drag::Float64
    viscousDrag::Float64
    a::Float64
    Fp#::Array{Float64,1}
    cllocal::Array{Float64,1}
    cl::Array{Float64,1}
    Vinfeff::Array{Float64,1}
    alphaeff::Array{Float64,1}

    #Structures
    p_c_stress #::Array{Float64,2}
    w_c_stress #::Array{Float64,2}
    p_c_buckling #::Array{Float64,2}
    w_c_buckling #::Array{Float64,2}

    fail::Bool
end

function propwing_out(;
    #Propulsion
    ETA_total = 0.0,
    ETA_m = 0.0,
    ETA_p = 0.0,
    # Propeller
    CT = 0.0,
    CQ = 0.0,
    Np = zeros(2),
    Tp = zeros(2),
    p_alphas = zeros(2),
    uprop = zeros(2),
    vprop = zeros(2),
    total_thrust = 0.0,
    advance_ratio = 0.0,
    torque = 0.0,
    db_test = zeros(2),

    # Motor
    V_m = 0.0,
    I_m = 0.0,
    # Masses
    propwing_mass = 0.0,
    mass_prop = 0.0,
    mass_ESC = 0.0,
    mass_motor = 0.0,
    mass_wing = 0.0,

    # Wing
    panels = zeros(2),
    Lift = 0.0,
    Drag = 0.0,
    viscousDrag = 0.0,
    a = 0.0,
    Fp = [],# = zeros(2)
    cllocal = zeros(2),
    cl = zeros(2),
    Vinfeff = zeros(2),
    alphaeff = zeros(2),

    #Structures
    p_c_stress = zeros(2),
    w_c_stress = zeros(2),
    p_c_buckling = zeros(2),
    w_c_buckling = zeros(2),
    fail = false,
)

    return propwing_out(ETA_total,ETA_m,ETA_p,CT,CQ,Np,Tp,p_alphas,uprop,vprop,total_thrust,
    advance_ratio,torque,db_test,V_m,I_m,propwing_mass,mass_prop,mass_ESC,
    mass_motor,mass_wing,panels,Lift,Drag,viscousDrag,a,Fp,cllocal,cl,Vinfeff,alphaeff,
    p_c_stress,w_c_stress,p_c_buckling,w_c_buckling, fail)

end

struct system_output
    range_true::Float64
    dist_accel::Float64
    dist_climb::Float64

    energy_accel::Float64
    energy_climb::Float64
    energy_cruise::Float64

    t_accel::Float64
    t_transition::Float64
    t_climb::Float64
    t_cruise::Float64

    total_mass::Float64
    total_weight::Float64

    gama::Float64
    radius::Float64
    xtr::Float64
    ytr::Float64
    H_left::Float64
    x_left::Float64

    optconvals::Dict{Symbol,Array{Float64,1}}

    payload::Float64
    max_mass::Float64
    gravity::Float64
    h_set::Float64
    specif_energy::Float64
    required_takeoff_dist::Float64
    altitude_accel::Float64
    altitude_climb::Float64
    altitude_cruise::Float64
    required_noise::Float64
    etaprop_center::Array{Float64,1}
    batt_mass_accel::Float64
    batt_mass_climb::Float64
    batt_mass_cruise::Float64
    Fl::Float64
end

function system_output(;
    range_true = 0.0,
    dist_accel = 0.0,
    dist_climb = 0.0,

    energy_accel = 0.0,
    energy_climb = 0.0,
    energy_cruise = 0.0,

    t_accel = 0.0,
    t_transition = 0.0,
    t_climb = 0.0,
    t_cruise = 0.0,

    total_mass = 0.0,
    total_weight = 0.0,

    gama = 0.0,
    radius = 0.0,
    xtr = 0.0,
    ytr = 0.0,
    H_left = 0.0,
    x_left = 0.0,

    optconvals = Dict{Symbol,Array{Float64,1}}(),

    payload = 0.0,
    max_mass = 0.0,
    gravity = 0.0,
    h_set = 0.0,
    specif_energy = 0.0,
    required_takeoff_dist = 0.0,
    altitude_accel = 0.0,
    altitude_climb = 0.0,
    altitude_cruise = 0.0,
    required_noise = 0.0,
    etaprop_center = [0.0,0.0],
    batt_mass_accel = 0.0,
    batt_mass_climb = 0.0,
    batt_mass_cruise = 0.0,
    Fl = 0.0,
    )

    return system_output(range_true,dist_accel,dist_climb, energy_accel,
    energy_climb,energy_climb, t_accel,t_transition,t_climb,t_cruise,
    total_mass,total_weight, gama,radius,xtr,ytr,H_left,x_left, optconvals,
    payload,max_mass,gravity,h_set,specif_energy,required_takeoff_dist,
    altitude_accel,altitude_climb,altitude_cruise,required_noise,
    etaprop_center,batt_mass_accel,batt_mass_climb,batt_mass_cruise,Fl)

end

# This file sets up default values and bounds for design variables
function getoptxbounds!(paramdict,designparams)

    ##############################################################################
    #----------------------- Prop Design Variables ------------------------#
    ##############################################################################

    lb_scale = 1.0 #1E-6
    ub_scale = 1E3
    # Skin Fabric Ply Thickness Design Variables
    p_nstations = length(designparams.p_radii)
    matnames = designparams.plyprops.names
    p_nplies = length(designparams.p_usedmaterials)

    name = :p_lam_t
    default = zeros(designparams.p_lam_t)
    lowerbound = zeros(designparams.p_lam_t)
    upperbound = zeros(designparams.p_lam_t)
    scaling = zeros(designparams.p_lam_t)
    for i = 1:p_nplies
        idx = find(matnames -> matnames == designparams.p_usedmaterials[i],matnames)
        material = designparams.plyprops.plies[idx[1]]

        default1 = designparams.p_lam_t[(i-1)*p_nstations+1:(i)*p_nstations]
        lowerbound1 = fill(designparams.plyprops.plies[idx[1]].t*lb_scale,p_nstations)
        upperbound1 = fill(designparams.plyprops.plies[idx[1]].t*ub_scale,p_nstations)
        scaling1 = fill(0.01/designparams.plyprops.plies[idx[1]].t,p_nstations)

        default[(i-1)*p_nstations+1:(i)*p_nstations] = default1
        lowerbound[(i-1)*p_nstations+1:(i)*p_nstations] = lowerbound1
        upperbound[(i-1)*p_nstations+1:(i)*p_nstations] = upperbound1
        scaling[(i-1)*p_nstations+1:(i)*p_nstations] = scaling1

    end
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :p_orientation
    default = designparams.p_orientation
    lowerbound = fill(-90.0,length(default))
    upperbound = fill(90.0,length(default))
    scaling = fill(1.0./90.0,length(default))
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :p_chord
    default = designparams.p_chord
    lowerbound = fill(0.01,p_nstations)
    lowerbound[end] = 0.001 #tip, works well for splines
    upperbound = fill(0.8,p_nstations)
    scaling = fill(1E2,p_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :p_sweep_d
    default = designparams.p_sweep_d
    lowerbound = fill(0.0,p_nstations-1)
    upperbound = fill(45.0,p_nstations-1)
    scaling = fill(pi/180.0,p_nstations-1)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    OptParams.addparam!(paramdict,:p_pitch_d_accel, designparams.p_pitch_d*10.0, -25.0, 45.0, 1E1)
    OptParams.addparam!(paramdict,:p_pitch_d_climb, designparams.p_pitch_d*10.0, -25.0, 45.0, 1E0)
    OptParams.addparam!(paramdict,:p_pitch_d_cruise, designparams.p_pitch_d*-2, -5.0, 80.0, 1E2)

    name = :p_twist_d
    default = designparams.p_twist_d
    lowerbound = fill(-0.0,p_nstations)
    upperbound = fill(45.0,p_nstations)
    scaling = fill(0.1,p_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :p_dihedral_d
    default = designparams.p_dihedral_d
    lowerbound = fill(0.0,p_nstations-1)
    upperbound = fill(10.0,p_nstations-1)
    scaling = fill(pi/180.0,p_nstations-1)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :p_airfoilthickness
    default = designparams.p_airfoilthickness
    lowerbound = fill(0.050,p_nstations)
    upperbound = fill(0.250,p_nstations)
    scaling = fill(10.0,p_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    OptParams.addparam!(paramdict,:p_Rtip,designparams.p_Rtip,0.1,5.0,1E-1)

    ##############################################################################
    #----------------------- Wing Design Variables ------------------------#
    ##############################################################################

    # Skin Fabric Ply Thickness Design Variables
    w_nstations = length(designparams.w_chord)

    name = :w_lam_t
    default = zeros(designparams.w_lam_t)
    lowerbound = zeros(designparams.w_lam_t)
    upperbound = zeros(designparams.w_lam_t)
    scaling = zeros(designparams.w_lam_t)
    for i = 1:length(designparams.w_usedmaterials)
        idx = find(matnames -> matnames == designparams.w_usedmaterials[i],matnames)
        material = designparams.plyprops.plies[idx[1]]

        default1 = designparams.w_lam_t[(i-1)*w_nstations+1:(i)*w_nstations]
        lowerbound1 = fill(designparams.plyprops.plies[idx[1]].t*lb_scale,w_nstations)
        upperbound1 = fill(designparams.plyprops.plies[idx[1]].t*ub_scale,w_nstations)
        scaling1 = fill(0.01/designparams.plyprops.plies[idx[1]].t,w_nstations)

        default[(i-1)*w_nstations+1:(i)*w_nstations] = default1
        lowerbound[(i-1)*w_nstations+1:(i)*w_nstations] = lowerbound1
        upperbound[(i-1)*w_nstations+1:(i)*w_nstations] = upperbound1
        scaling[(i-1)*w_nstations+1:(i)*w_nstations] = scaling1

    end
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_orientation
    default = designparams.w_orientation
    lowerbound = fill(-90.0,length(default))
    upperbound = fill(90.0,length(default))
    scaling = fill(1.0./90.0,length(default))
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_chord
    default = designparams.w_chord
    lowerbound = fill(0.1,w_nstations)
    upperbound = fill(5.0,w_nstations)
    scaling = fill(100.0/designparams.w_halfspan,w_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    OptParams.addparam!(paramdict,:w_aoa_d_accel,designparams.w_aoa_d*1.1,2.0,40.0,1E-2)
    OptParams.addparam!(paramdict,:w_aoa_d_climb,designparams.w_aoa_d*0.5,5.0,40.0,1E0)
    OptParams.addparam!(paramdict,:w_aoa_d_cruise,designparams.w_aoa_d*0.5,0.20,20.0,1E0)

    name = :w_twist_d
    default = designparams.w_twist_d
    lowerbound = fill(-10.0,w_nstations)
    upperbound = fill(20.0,w_nstations)
    scaling = fill(10.0,w_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_halfspan
    default = designparams.w_halfspan
    lowerbound = 5.699
    upperbound = 12.0
    scaling = 1.0/designparams.w_halfspan
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_sweep_d
    default = designparams.w_sweep_d
    lowerbound = fill(0.0,w_nstations)
    upperbound = fill(20.0,w_nstations)
    scaling = fill(10.0,w_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_dihedral_d
    default = designparams.w_dihedral_d
    lowerbound = fill(0.0,w_nstations)
    upperbound = fill(20.0,w_nstations)
    scaling = fill(10.0,w_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    name = :w_airfoilthickness
    default = designparams.p_airfoilthickness
    lowerbound = fill(0.050,p_nstations)
    upperbound = fill(0.250,p_nstations)
    scaling = fill(10.0,p_nstations)
    OptParams.addparam!(paramdict,name,default,lowerbound,upperbound,scaling)

    ##############################################################################
    #-----------------  Operating Point Design Variables ------------------------#
    ##############################################################################

    OptParams.addparam!(paramdict,:velocity_accel,designparams.velocity/1.4,5.0,28.0,1E1)
    OptParams.addparam!(paramdict,:velocity_climb,designparams.velocity*1.0,5.0,60.0,1E-2)
    OptParams.addparam!(paramdict,:velocity_cruise,designparams.velocity*1.0,20.0,100.0,1E1)

    ##############################################################################
    #-------------------------  Propulsion Design Variables ---------------------#
    ##############################################################################

    OptParams.addparam!(paramdict,:p_rpm_accel,designparams.p_rpm*4.5,200.0,40000.0,1E-1)
    OptParams.addparam!(paramdict,:p_rpm_climb,designparams.p_rpm*4.6,200.0,40000.0,1E-2)
    OptParams.addparam!(paramdict,:p_rpm_cruise,designparams.p_rpm*2.5,100.0,10000.0,1E0)
    OptParams.addparam!(paramdict,:kv,designparams.kv,5.0,5000.0,1E-0)
    OptParams.addparam!(paramdict,:i0,designparams.i0,0.1,6.0,1E0)
    OptParams.addparam!(paramdict,:batt_mass_accel,designparams.batt_mass,0.001,100.0,1E0)
    OptParams.addparam!(paramdict,:batt_mass_climb,designparams.batt_mass,0.01,100.0,1E-1)
    OptParams.addparam!(paramdict,:batt_mass_cruise,designparams.batt_mass,5.0,1000.0,1E-1)
    OptParams.addparam!(paramdict,:number_blades,2.0,2.0,5.0,1E-0)
    OptParams.addparam!(paramdict,:radius,100.0,2.0,2000.0,1E-1)
    OptParams.addparam!(paramdict,:gama_d,30.0,0.1,89.9,1E-1)
end

#------ SET UP OPTIMIZATION PARAMETERS -------#

mutable struct controlparams
    objective::Symbol
    printfreq::Int
    savevtk::Int
    printobjective::Bool
    printdetailedoutput::Bool
    printconstraintviolations::Bool
end

function controlparams(;
    objective::Symbol = :mass,
    printfreq::Int = 1,
    savevtk::Int = 3,
    printobjective::Bool = true,
    printdetailedoutput::Bool = true,
    printconstraintviolations::Bool = true,
    )
    controlparams(objective,printfreq,savevtk,printobjective,printdetailedoutput,
    printconstraintviolations)
end

# Define design variables
function getoptparams()
    designvar =  [
    :p_lam_t, #1-12
    # :p_orientation,
    :p_chord, #13-18
    :p_twist_d, #19-24
    :p_pitch_d_accel, #25
    :p_pitch_d_climb, #25
    :p_pitch_d_cruise, #25

    :velocity_accel,
    :velocity_climb,
    :velocity_cruise,
    :p_Rtip,
    :p_rpm_accel,
    :p_rpm_climb,
    :p_rpm_cruise,
    # :kv, #28
    # :i0, #29

    # :w_orientation,
    :w_chord, #30-32
    :w_aoa_d_accel,   #multi
    :w_aoa_d_climb,   #multi
    :w_aoa_d_cruise,   #multi
    # :w_twist_d, #33-35
    # :w_halfspan,
    # :w_sweep_d,
    # :w_dihedral_d,
    # :w_lam_t, #36-end
    :batt_mass_accel,
    :batt_mass_climb,
    :batt_mass_cruise,
    ]

    # Define general constraints
    constraints = [
    :p_materialfailure,
    :p_localbuckling,
    # :w_materialfailure,
    # :w_localbuckling,

    :totalmass,
    :machtip,
    :Lift,
    :Drag,
    :noise,
    # :p_chordlamthick,
    # :range,
    :takeoff_dist,
    :stall,
    :capacity,
    :p_alphas,
    ]

    # define run controls
    ctlparams = controlparams(
    objective= :range, #:mass,#:range #:ETA_total_cruise,
    printfreq = 50,
    savevtk = 500,
    printobjective=true,
    printdetailedoutput=true,
    printconstraintviolations=true,
    )

    return designvar, constraints, ctlparams
end

#------- MATERIAL PROPERTIES -------#
function plyproperties()
    names = ["highmodulus_uni",
    "standard_uni",
    "MR60H",
    "T3900_uni",
    "T700_uni",
    "ELT5500",
    "UDCarbon",
    "highmodulus_weave",
    "standard_weave",
    "T3900_weave",
    "T700_weave",
    "Gelcoat",
    "Triax",
    "Saertex",
    "taylor_foam",
    "SNL_foam"]

    e1  =zeros(16)
    e2  =zeros(16)
    g12 =zeros(16)
    anu =zeros(16)
    rho =zeros(16)
    xt  =zeros(16)
    xc  =zeros(16)
    yt  =zeros(16)
    yc  =zeros(16)
    s   =zeros(16)
    plythickness =zeros(16)

    # "highmodulus_uni"
    e1[1]  = 175.0e9
    e2[1]  = 8.0e9
    g12[1] = 5.0e9
    anu[1] = 0.30
    rho[1] = 1600.0
    xt[1]  = min(1.0,1355.656/1682.011)*1000e6 #mean -> A-basis
    xc[1]  = min(1.0,1103.943/1396.504)*850e6 #mean -> A-basis
    yt[1]  = min(1.0,39.226/52.975)*40e6 #mean -> A-basis
    yc[1]  = min(1.0,235.434/282.439)*200e6 #mean -> A-basis
    s[1]   = min(1.0,142.411/159.516)*60e6 #mean -> A-basis
    plythickness[1] = 0.152e-3
    # "standard_uni"
    e1[2]  = 135.0e9
    e2[2]  = 10.0e9
    g12[2] = 5.0e9
    anu[2] = 0.30
    rho[2] = 1600.0
    xt[2]  = min(1.0,1355.656/1682.011)*1500e6 #mean -> A-basis
    xc[2]  = min(1.0,1103.943/1396.504)*1200e6 #mean -> A-basis
    yt[2]  = min(1.0,39.226/52.975)*50e6 #mean -> A-basis
    yc[2]  = min(1.0,235.434/282.439)*250e6 #mean -> A-basis
    s[2]   = min(1.0,142.411/159.516)*70e6 #mean -> A-basis
    plythickness[2] = 0.152e-3
    # "MR60H"
    e1[3]  = (165e9+150e9)/2.0
    e2[3]  = 8.56e9
    g12[3] = 4.39e9
    anu[3] = 0.326
    rho[3] = 1810.0
    xt[3]  = min(1.0,1355.656/1682.011)*3190e6 #mean -> A-basis
    xc[3]  = min(1.0,1103.943/1396.504)*1440e6 #mean -> A-basis
    yt[3]  = min(1.0,39.226/52.975)*82.0e6 #mean -> A-basis
    yc[3]  = min(1.0,235.434/282.439)*200.0e6 #mean -> A-basis
    s[3]   = min(1.0,142.411/159.516)*141e6 #mean -> A-basis
    plythickness[3] = 0.152e-3
    # "T3900_uni"
    e1[4]  = (148e9+131e9)/2.0
    e2[4]  = (9.7e9+9.7e9)/2.0
    g12[4] = 4.83e9
    anu[4] = 0.33
    rho[4] = 1573.0
    xt[4]  = min(1.0,1355.656/1682.011)*2830e6 #CTD mean -> A-basis
    xc[4]  = min(1.0,1103.943/1396.504)*1772e6 #CTD mean -> A-basis
    yt[4]  = min(1.0,39.226/52.975)*56.9e6 #CTD mean -> A-basis
    yc[4]  = min(1.0,235.434/282.439)*303e6 #CTD mean -> A-basis
    s[4]   = min(1.0,142.411/159.516)*89.6e6 #CTD mean -> A-basis
    plythickness[4] = 0.191e-3
    # "T700_uni"
    e1[5]=120.8e9
    e2[5]=11.57e9
    g12[5]=5.219e9
    anu[5]=0.350
    rho[5]=1525.0
    xt[5]=1356e6
    xc[5]=1104e6
    yt[5]=39.23e6
    yc[5]=235.4e6
    s[5]=142.4e6
    plythickness[5] = 0.152e-3
    # "ELT5500"
    e1[6]=41.8e9
    e2[6]=14.00e9
    g12[6]=2.630e9
    anu[6]=0.280
    rho[6]=1920.0
    xt[6]=972.0e6
    xc[6]=702.0e6
    yt[6]=100.0e6 #made up
    yc[6]=100.0e6 #made up
    s[6]=100.0e6 #made up
    plythickness[6] = 0.47e-3
    # "UDCarbon"
    e1[7]=114.5e9
    e2[7]=8.39e9
    g12[7]=5.990e9
    anu[7]=0.270
    rho[7]=1220.0
    xt[7]=1546.0e6
    xc[7]=1047.0e6
    yt[7]=100.0e6 #made up
    yc[7]=100.0e6 #made up
    s[7]=100.0e6 #made up
    plythickness[7] = 0.47e-3

    # FABRICS
    # "highmodulus_weave"
    e1[8]  = 85.0e9
    e2[8]  = 85.0e9
    g12[8] = 5.0e9
    anu[8] = 0.10
    rho[8] = 1600.0
    xt[8]  = min(1.0,1355.656/1682.011)*350e6 #mean -> A-basis
    xc[8]  = min(1.0,1103.943/1396.504)*150e6 #mean -> A-basis
    yt[8]  = min(1.0,39.226/52.975)*350e6 #mean -> A-basis
    yc[8]  = min(1.0,235.434/282.439)*150e6 #mean -> A-basis
    s[8]   = min(1.0,142.411/159.516)*35e6 #mean -> A-basis
    plythickness[8] = 0.218e-3
    # "standard_weave"
    e1[9]  = 70.0e9
    e2[9]  = 70.0e9
    g12[9] = 5.0e9
    anu[9] = 0.10
    rho[9] = 1600.0
    xt[9]  = min(1.0,1355.656/1682.011)*600e6 #mean -> A-basis
    xc[9]  = min(1.0,1103.943/1396.504)*570e6 #mean -> A-basis
    yt[9]  = min(1.0,39.226/52.975)*600e6 #mean -> A-basis
    yc[9]  = min(1.0,235.434/282.439)*570e6 #mean -> A-basis
    s[9]   = min(1.0,142.411/159.516)*90e6 #mean -> A-basis
    plythickness[9] = 0.218e-3
    # "T3900_weave"
    e1[10]  = (70.3e9+71.0e9)/2.0
    e2[10]  = (68.9e9+67.6e9)/2.0
    g12[10] = 4.6e9
    anu[10] = 0.032
    rho[10] = 1551.0
    xt[10]  = min(1.0,701.302/803.236)*1055e6 #CTD mean -> A-basis
    xc[10]  = min(1.0,549.748/749.955)*676e6 #CTD mean -> A-basis
    yt[10]  = min(1.0,557.575/722.602)*945e6 #CTD mean -> A-basis
    yc[10]  = min(1.0,604.067/741.866)*614e6 #CTD mean -> A-basis
    s[10]   = min(1.0,138.440/154.888)*79.3e6 #CTD mean -> A-basis
    plythickness[10] = 0.218e-3
    # "T700_weave"
    e1[11]=55.82e9
    e2[11]=52.10e9
    g12[11]=4.295e9
    anu[11]=0.085
    rho[11]=1501.0
    xt[11]=701.32e6
    yt[11]=557.59e6
    xc[11]=549.77e6
    yc[11]=604.08e6
    s[11]=138.44e6
    plythickness[11] = 0.218e-3
    # "Gelcoat"
    e1[12]=3.44e9
    e2[12]=3.44e9
    g12[12]=1.38e9
    anu[12]=0.3
    rho[12]=1235.0
    xt[12]=100.0e6 #made up
    xc[12]=100.0e6 #made up
    yt[12]=100.0e6 #made up
    yc[12]=100.0e6 #made up
    s[12]=100.0e6 #made up
    plythickness[12] = 0.05e-3
    # "Triax"
    e1[13]=27.7e9
    e2[13]=13.65e9
    g12[13]=7.2e9
    anu[13]=0.39
    rho[13]=1850.0
    xt[13]=700.0e6
    xc[13]=100.0e6 #made up
    yt[13]=700.0e6
    yc[13]=100.0e6 #made up
    s[13]=100.0e6 #made up
    plythickness[13] = 0.94e-3
    # "Saertex"
    e1[14]=13.6e9
    e2[14]=13.3e9
    g12[14]=11.8e9
    anu[14]=0.49
    rho[14]=1780.0
    xt[14]=144.0e6
    xc[14]=213.0e6
    yt[14]=144.0e6
    yc[14]=213.0e6
    s[14]=100.0e6 #made up
    plythickness[14] = 1.0e-3

    #FOAMS
    # "taylor_foam"
    e1[15]=48.0e6
    e2[15]=48.0e6
    g12[15]=28.0e6
    anu[15]=0.3
    rho[15]=75.0
    xt[15]=100.0e6 #made up
    yt[15]=100.0e6 #made up
    xc[15]=100.0e6 #made up
    yc[15]=100.0e6 #made up
    s[15]=100.0e6 #made up
    plythickness[15]=1.0E-3
    # "SNL_foam"
    e1[16]=256.0e6
    e2[16]=256.0e6
    g12[16]=22.0e6
    anu[16]=0.3
    rho[16]=200.0
    xt[16]=100.0e6 #made up
    yt[16]=100.0e6 #made up
    xc[16]=100.0e6 #made up
    yc[16]=100.0e6 #made up
    s[16]=100.0e6 #made up
    plythickness[16]=1.0E-3

    return plyproperties(names,Composites.material(e1,e2,g12,anu,rho,xt,xc,yt,yc,s,plythickness))
end


## Expressions ##

### Expression templates ###

"Defines branch indicator as a function of corresponding ne_branch indicator variables."
function expression_branch_indicator(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    if !haskey(_PM.var(pm, nw), :z_branch)
        _PM.var(pm, nw)[:z_branch] = Dict{Int,Any}()
    end

    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]

    expression_branch_indicator(pm, nw, br_idx, f_bus, t_bus)
end

### Actual expressions ###

function expression_branch_indicator(pm::_PM.AbstractPowerModel, n::Int, br_idx, f_bus, t_bus)
    branch_ne_sum = sum(_PM.var(pm, n, :branch_ne, l) for l in _PM.ref(pm, n, :ne_buspairs, (f_bus,t_bus), "branches"))

    _PM.var(pm, n, :z_branch)[br_idx] = 1 - branch_ne_sum
end

## Constraints ##

### Constraint templates ###

""
function constraint_ohms_yt_from_repl(pm::_PM.AbstractPowerModel, i::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, i)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (i, f_bus, t_bus)
    t_idx = (i, t_bus, f_bus)

    g, b = _PM.calc_branch_y(branch)
    tr, ti = _PM.calc_branch_t(branch)
    g_fr = branch["g_fr"]
    b_fr = branch["b_fr"]
    tm = branch["tap"]

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    # track if a certain candidate branch is replacing a line
    replace, ne_br_idx = replace_branch(pm, nw, f_bus, t_bus)
    # If lines is to be repalced use formulations below, else use PowerModels constraint for existing branches
    if replace  == 0
        _PM.constraint_ohms_yt_from(pm, nw, f_bus, t_bus, f_idx, t_idx, g, b, g_fr, b_fr, tr, ti, tm)
    else
        constraint_ohms_yt_from_repl(pm, nw, ne_br_idx, f_bus, t_bus, f_idx, b, vad_min, vad_max)
    end
end

""
function constraint_ohms_yt_to_repl(pm::_PM.AbstractPowerModel, i::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, i)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (i, f_bus, t_bus)
    t_idx = (i, t_bus, f_bus)

    g, b = _PM.calc_branch_y(branch)
    tr, ti = _PM.calc_branch_t(branch)
    g_to = branch["g_to"]
    b_to = branch["b_to"]
    tm = branch["tap"]

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    # track if a certain candidate branch is replacing a line
    replace, ne_br_idx = replace_branch(pm, nw, f_bus, t_bus)
    # If lines is to be repalced use formulations below, else use PowerModels constraint for existing branches
    if replace  == 0
        _PM.constraint_ohms_yt_to(pm, nw, f_bus, t_bus, f_idx, t_idx, g, b, g_to, b_to, tr, ti, tm)
    else
        constraint_ohms_yt_to_repl(pm, nw, ne_br_idx, f_bus, t_bus, t_idx, b, vad_min, vad_max)
    end
end

function constraint_voltage_angle_difference_repl(pm::_PM.AbstractPowerModel, i::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, i)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (i, branch["f_bus"], branch["t_bus"])

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    # track if a certain candidate branch is replacing a line
    replace, ne_br_idx = replace_branch(pm, nw, f_bus, t_bus)
    # If lines is to be repalced use formulations below, else use PowerModels constraint for existing branches
    if replace  == 0
        _PM.constraint_voltage_angle_difference(pm, nw, f_idx, branch["angmin"], branch["angmax"])
    else
        constraint_voltage_angle_difference_repl(pm, nw, ne_br_idx, f_idx, branch["angmin"], branch["angmax"], vad_min, vad_max)
    end
end

function constraint_thermal_limit_from_repl(pm::_PM.AbstractPowerModel, i::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, i)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (i, f_bus, t_bus)

    if !haskey(branch, "rate_a")
        Memento.error(_LOGGER, "constraint_thermal_limit_from_ne requires a rate_a value on all branches, calc_thermal_limits! can be used to generate reasonable values")
    end

    # track if a certain candidate branch is replacing a line
    replace, ne_br_idx = replace_branch(pm, nw, f_bus, t_bus)
    # If lines is to be repalced use formulations below, else use PowerModels constraint for existing branches
    if replace  == 0
        _PM.constraint_thermal_limit_from(pm, nw, f_idx, branch["rate_a"])
    else
        constraint_thermal_limit_from_repl(pm, nw, ne_br_idx, f_idx, branch["rate_a"])
    end
end

""
function constraint_thermal_limit_to_repl(pm::_PM.AbstractPowerModel, i::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, i)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    t_idx = (i, t_bus, f_bus)

    if !haskey(branch, "rate_a")
        Memento.error(_LOGGER, "constraint_thermal_limit_to_ne requires a rate_a value on all branches, calc_thermal_limits! can be used to generate reasonable values")
    end
    # track if a certain candidate branch is replacing a line
    replace, ne_br_idx = replace_branch(pm, nw, f_bus, t_bus)
    # If lines is to be repalced use formulations below, else use PowerModels constraint for existing branches
    if replace  == 0
        _PM.constraint_thermal_limit_to(pm, nw, t_idx, branch["rate_a"])
    else
        constraint_thermal_limit_to_repl(pm, nw, ne_br_idx, t_idx, branch["rate_a"])
    end
end

#### Constraint templates used in radial networks ####

"States that at most one of the ne_branches sharing the same bus pair must be built."
function constraint_branch_complementarity(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]

    constraint_branch_complementarity(pm, nw, br_idx, f_bus, t_bus)
end

""
function constraint_power_losses_on_off(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    tm = branch["tap"]
    g_sh_fr = branch["g_fr"]
    g_sh_to = branch["g_to"]
    b_sh_fr = branch["b_fr"]
    b_sh_to = branch["b_to"]

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    constraint_power_losses_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm, vad_min, vad_max)
end

""
function constraint_power_losses_frb_on_off(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    tm = branch["tap"]
    g_sh_fr = branch["g_fr"]
    g_sh_to = branch["g_to"]
    b_sh_fr = branch["b_fr"]
    b_sh_to = branch["b_to"]

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    constraint_power_losses_frb_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, tm, vad_min, vad_max)
end

""
function constraint_power_losses_oltc_on_off(pm::_PM.AbstractBFModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    g_sh_to = branch["g_to"]
    b_sh_fr = branch["b_fr"]
    b_sh_to = branch["b_to"]

    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    constraint_power_losses_oltc_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, vad_min, vad_max)
end

""
function constraint_ne_power_losses_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int=_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus = ne_branch["f_bus"]
    t_bus = ne_branch["t_bus"]
    f_idx = (ne_br_idx, f_bus, t_bus)
    t_idx = (ne_br_idx, t_bus, f_bus)
    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    ne_r       = ne_branch["br_r"]
    ne_x       = ne_branch["br_x"]
    ne_tm      = ne_branch["tap"]
    ne_g_sh_fr = ne_branch["g_fr"]
    ne_g_sh_to = ne_branch["g_to"]
    ne_b_sh_fr = ne_branch["b_fr"]
    ne_b_sh_to = ne_branch["b_to"]

    br_idx  = branch_idx(pm, nw, f_bus, t_bus)
    branch  = _PM.ref(pm, nw, :branch, br_idx)
    r       = branch["br_r"]
    x       = branch["br_x"]
    tm      = branch["tap"]
    g_sh_fr = branch["g_fr"]
    g_sh_to = branch["g_to"]
    b_sh_fr = branch["b_fr"]
    b_sh_to = branch["b_to"]

    if ne_tm != tm
        Memento.error(_LOGGER, "ne_branch $(ne_br_idx) cannot be built in parallel to branch $(br_idx) because has a different tap ratio")
    end

    constraint_ne_power_losses_parallel(pm, nw, br_idx, ne_br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, ne_r, ne_x, ne_g_sh_fr, ne_g_sh_to, ne_b_sh_fr, ne_b_sh_to, tm, vad_min, vad_max)
end

""
function constraint_ne_power_losses_frb_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int=_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus = ne_branch["f_bus"]
    t_bus = ne_branch["t_bus"]
    f_idx = (ne_br_idx, f_bus, t_bus)
    t_idx = (ne_br_idx, t_bus, f_bus)
    vad_min = _PM.ref(pm, nw, :off_angmin)
    vad_max = _PM.ref(pm, nw, :off_angmax)

    ne_r       = ne_branch["br_r"]
    ne_x       = ne_branch["br_x"]
    ne_tm      = ne_branch["tap"]
    ne_g_sh_fr = ne_branch["g_fr"]
    ne_g_sh_to = ne_branch["g_to"]
    ne_b_sh_fr = ne_branch["b_fr"]
    ne_b_sh_to = ne_branch["b_to"]

    br_idx  = branch_idx(pm, nw, f_bus, t_bus)
    branch  = _PM.ref(pm, nw, :branch, br_idx)
    r       = branch["br_r"]
    x       = branch["br_x"]
    tm      = branch["tap"]
    g_sh_fr = branch["g_fr"]
    g_sh_to = branch["g_to"]
    b_sh_fr = branch["b_fr"]
    b_sh_to = branch["b_to"]

    if ne_tm != tm
        Memento.error(_LOGGER, "ne_branch $(ne_br_idx) cannot be built in parallel to branch $(br_idx) because has a different tap ratio")
    end

    constraint_ne_power_losses_frb_parallel(pm, nw, br_idx, ne_br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, g_sh_to, b_sh_fr, b_sh_to, ne_r, ne_x, ne_g_sh_fr, ne_g_sh_to, ne_b_sh_fr, ne_b_sh_to, tm, vad_min, vad_max)
end

""
function constraint_voltage_magnitude_difference_on_off(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    b_sh_fr = branch["b_fr"]
    tm = branch["tap"]

    constraint_voltage_magnitude_difference_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm)
end

""
function constraint_voltage_magnitude_difference_frb_on_off(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    b_sh_fr = branch["b_fr"]
    tm = branch["tap"]

    constraint_voltage_magnitude_difference_frb_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm)
end

""
function constraint_voltage_magnitude_difference_oltc_on_off(pm::_PM.AbstractBFModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    f_idx = (br_idx, f_bus, t_bus)
    t_idx = (br_idx, t_bus, f_bus)

    r = branch["br_r"]
    x = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    b_sh_fr = branch["b_fr"]
    tm_min = branch["tm_min"]
    tm_max = branch["tm_max"]

    constraint_voltage_magnitude_difference_oltc_on_off(pm, nw, br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm_min, tm_max)
end

""
function constraint_ne_voltage_magnitude_difference_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int =_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus = ne_branch["f_bus"]
    t_bus = ne_branch["t_bus"]
    f_idx = (ne_br_idx, f_bus, t_bus)
    t_idx = (ne_br_idx, t_bus, f_bus)

    ne_r       = ne_branch["br_r"]
    ne_x       = ne_branch["br_x"]
    ne_g_sh_fr = ne_branch["g_fr"]
    ne_b_sh_fr = ne_branch["b_fr"]
    ne_tm      = ne_branch["tap"]

    br_idx  = branch_idx(pm, nw, f_bus, t_bus)
    branch  = _PM.ref(pm, nw, :branch, br_idx)
    r       = branch["br_r"]
    x       = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    b_sh_fr = branch["b_fr"]
    tm      = branch["tap"]
    if is_oltc_branch(pm, br_idx, nw = nw)
        Memento.error(_LOGGER, "ne_branch $ne_br_idx cannot be built in parallel to an OLTC (branch $br_idx)")
    end
    if ne_tm != tm
        Memento.error(_LOGGER, "ne_branch $ne_br_idx cannot be built in parallel to branch $br_idx because has a different tap ratio")
    end

    constraint_ne_voltage_magnitude_difference_parallel(pm, nw, br_idx, ne_br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, ne_r, ne_x, ne_g_sh_fr, ne_b_sh_fr, tm)
end

""
function constraint_ne_voltage_magnitude_difference_frb_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int =_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus = ne_branch["f_bus"]
    t_bus = ne_branch["t_bus"]
    f_idx = (ne_br_idx, f_bus, t_bus)
    t_idx = (ne_br_idx, t_bus, f_bus)

    ne_r       = ne_branch["br_r"]
    ne_x       = ne_branch["br_x"]
    ne_g_sh_fr = ne_branch["g_fr"]
    ne_b_sh_fr = ne_branch["b_fr"]
    ne_tm      = ne_branch["tap"]

    br_idx  = branch_idx(pm, nw, f_bus, t_bus)
    branch  = _PM.ref(pm, nw, :branch, br_idx)
    r       = branch["br_r"]
    x       = branch["br_x"]
    g_sh_fr = branch["g_fr"]
    b_sh_fr = branch["b_fr"]
    tm      = branch["tap"]

    if is_oltc_branch(pm, br_idx, nw = nw)
        Memento.error(_LOGGER, "ne_branch $ne_br_idx cannot be built in parallel to an OLTC (branch $br_idx)")
    end
    if ne_tm != tm
        Memento.error(_LOGGER, "ne_branch $ne_br_idx cannot be built in parallel to branch $br_idx because has a different tap ratio")
    end

    constraint_ne_voltage_magnitude_difference_frb_parallel(pm, nw, br_idx, ne_br_idx, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, ne_r, ne_x, ne_g_sh_fr, ne_b_sh_fr, tm)
end

""
function constraint_ne_thermal_limit_from_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int=_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus     = ne_branch["f_bus"]
    t_bus     = ne_branch["t_bus"]
    f_idx     = (ne_br_idx, f_bus, t_bus)
    if !haskey(ne_branch, "rate_a")
        Memento.error(_LOGGER, "constraint_ne_thermal_limit_from_parallel requires a rate_a value on all ne_branches, calc_thermal_limits! can be used to generate reasonable values")
    end
    ne_rate_a = ne_branch["rate_a"]

    br_idx = branch_idx(pm, nw, f_bus, t_bus)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    rate_a = branch["rate_a"]

    constraint_ne_thermal_limit_from_parallel(pm, nw, br_idx, ne_br_idx, f_idx, rate_a, ne_rate_a)
end

""
function constraint_ne_thermal_limit_to_parallel(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int=_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    f_bus     = ne_branch["f_bus"]
    t_bus     = ne_branch["t_bus"]
    t_idx     = (ne_br_idx, t_bus, f_bus)
    if !haskey(ne_branch, "rate_a")
        Memento.error(_LOGGER, "constraint_ne_thermal_limit_to_parallel requires a rate_a value on all ne_branches, calc_thermal_limits! can be used to generate reasonable values")
    end
    ne_rate_a = ne_branch["rate_a"]

    br_idx = branch_idx(pm, nw, f_bus, t_bus)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    rate_a = branch["rate_a"]

    constraint_ne_thermal_limit_to_parallel(pm, nw, br_idx, ne_br_idx, t_idx, rate_a, ne_rate_a)
end

### Actual constraints ###

function constraint_ohms_yt_from_repl(pm::_PM.AbstractDCPModel, n::Int, ne_br_idx, f_bus, t_bus, f_idx, b, vad_min, vad_max)
    p_fr  = _PM.var(pm, n,   :p, f_idx)
    va_fr = _PM.var(pm, n,   :va, f_bus)
    va_to = _PM.var(pm, n,   :va, t_bus)
    z = _PM.var(pm, n, :branch_ne, ne_br_idx)

    JuMP.@constraint(pm.model, p_fr <= -b*(va_fr - va_to + vad_max*z))
    JuMP.@constraint(pm.model, p_fr >= -b*(va_fr - va_to + vad_min*z))
end

"nothing to do, this model is symetric"
function constraint_ohms_yt_to_repl(pm::_PM.AbstractAPLossLessModels, n::Int, ne_br_idx, f_bus, t_bus, t_idx, b, vad_min, vad_max)
end

function constraint_voltage_angle_difference_repl(pm::_PM.AbstractDCPModel, n::Int, ne_br_idx, f_idx, angmin, angmax, vad_min, vad_max)
    i, f_bus, t_bus = f_idx

    va_fr = _PM.var(pm, n, :va, f_bus)
    va_to = _PM.var(pm, n, :va, t_bus)
    z = _PM.var(pm, n, :branch_ne, ne_br_idx)

    JuMP.@constraint(pm.model, va_fr - va_to <= angmax*(1-z) + vad_max*z)
    JuMP.@constraint(pm.model, va_fr - va_to >= angmin*(1-z) + vad_min*z)
end

""
function constraint_thermal_limit_from_repl(pm::_PM.AbstractActivePowerModel, n::Int, ne_br_idx, f_idx, rate_a)
    p_fr = _PM.var(pm, n, :p, f_idx)
    z = _PM.var(pm, n, :branch_ne, ne_br_idx)

    JuMP.@constraint(pm.model, p_fr <=  rate_a*(1-z))
    JuMP.@constraint(pm.model, p_fr >= -rate_a*(1-z))
end

""
function constraint_thermal_limit_to_repl(pm::_PM.AbstractActivePowerModel, n::Int, ne_br_idx, t_idx, rate_a)
    p_to = _PM.var(pm, n, :p, t_idx)
    z = _PM.var(pm, n, :branch_ne, ne_br_idx)

    JuMP.@constraint(pm.model, p_to <=  rate_a*(1-z))
    JuMP.@constraint(pm.model, p_to >= -rate_a*(1-z))
end

#### Actual constraints used in radial networks ####

"States that at most one of the ne_branches sharing the same bus pair must be built."
function constraint_branch_complementarity(pm::_PM.AbstractPowerModel, n::Int, i, f_bus, t_bus)
    JuMP.@constraint(pm.model, sum(_PM.var(pm, n, :branch_ne, l) for l in ne_branch_ids(pm, n, f_bus, t_bus)) <= 1)
end

""
function constraint_voltage_magnitude_difference_on_off(pm::_PM.AbstractBFAModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm)
    branch = _PM.ref(pm, n, :branch, i)
    fr_bus = _PM.ref(pm, n, :bus, f_bus)
    to_bus = _PM.ref(pm, n, :bus, t_bus)
    M_hi   =  fr_bus["vmax"]^2/tm^2 - to_bus["vmin"]^2
    M_lo   = -fr_bus["vmin"]^2/tm^2 + to_bus["vmax"]^2

    p_fr = _PM.var(pm, n, :p, f_idx)
    q_fr = _PM.var(pm, n, :q, f_idx)
    w_fr = _PM.var(pm, n, :w, f_bus)
    w_to = _PM.var(pm, n, :w, t_bus)
    z    = _PM.var(pm, n, :z_branch, i)

    JuMP.@constraint(pm.model, (w_fr/tm^2) - w_to <= 2*(r*p_fr + x*q_fr) + M_hi*(1-z) )
    JuMP.@constraint(pm.model, (w_fr/tm^2) - w_to >= 2*(r*p_fr + x*q_fr) - M_lo*(1-z) )
end

""
function constraint_voltage_magnitude_difference_frb_on_off(pm::_PM.AbstractBFAModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm)
    branch = _PM.ref(pm, n, :branch, i)
    fr_bus = _PM.ref(pm, n, :bus, f_bus)
    to_bus = _PM.ref(pm, n, :bus, t_bus)
    M_hi   =  1.0^2/tm^2 - to_bus["vmin"]^2
    M_lo   = -1.0^2/tm^2 + to_bus["vmax"]^2

    p_fr = _PM.var(pm, n, :p, f_idx)
    q_fr = _PM.var(pm, n, :q, f_idx)
    w_to = _PM.var(pm, n, :w, t_bus)
    z    = _PM.var(pm, n, :z_branch, i)
    # w_fr is assumed equal to 1.0

    JuMP.@constraint(pm.model, (1.0/tm^2) - w_to <= 2*(r*p_fr + x*q_fr) + M_hi*(1-z) )
    JuMP.@constraint(pm.model, (1.0/tm^2) - w_to >= 2*(r*p_fr + x*q_fr) - M_lo*(1-z) )
end

""
function constraint_voltage_magnitude_difference_oltc_on_off(pm::_PM.AbstractBFAModel, n::Int, i, f_bus, t_bus, f_idx, t_idx, r, x, g_sh_fr, b_sh_fr, tm_min, tm_max)
    branch = _PM.ref(pm, n, :branch, i)
    fr_bus = _PM.ref(pm, n, :bus, f_bus)
    to_bus = _PM.ref(pm, n, :bus, t_bus)
    M_hi   =  1.0^2/tm_min^2 - to_bus["vmin"]^2
    M_lo   = -1.0^2/tm_max^2 + to_bus["vmax"]^2

    p_fr = _PM.var(pm, n, :p, f_idx)
    q_fr = _PM.var(pm, n, :q, f_idx)
    ttmi = _PM.var(pm, n, :ttmi, i)
    w_to = _PM.var(pm, n, :w, t_bus)
    z    = _PM.var(pm, n, :z_branch, i)
    # w_fr is assumed equal to 1.0 to preserve the linearity of the model

    JuMP.@constraint(pm.model, 1.0*ttmi - w_to <= 2*(r*p_fr + x*q_fr) + M_hi*(1-z) )
    JuMP.@constraint(pm.model, 1.0*ttmi - w_to >= 2*(r*p_fr + x*q_fr) - M_lo*(1-z) )
end

""
function constraint_ne_voltage_magnitude_difference_parallel(pm::_PM.AbstractBFAModel, n::Int, br_idx_e, br_idx_c, f_bus, t_bus, f_idx_c, t_idx_c, r_e, x_e, g_sh_fr_e, b_sh_fr_e, r_c, x_c, g_sh_fr_c, b_sh_fr_c, tm)
    # Suffixes: _e: existing branch; _c: candidate branch; _p: parallel equivalent
    r_p  = (r_e*(r_c^2+x_c^2)+r_c*(r_e^2+x_e^2)) / ((r_e+r_c)^2+(x_e+x_c)^2)
    x_p  = (x_e*(r_c^2+x_c^2)+x_c*(r_e^2+x_e^2)) / ((r_e+r_c)^2+(x_e+x_c)^2)

    constraint_ne_voltage_magnitude_difference(pm::_PM.AbstractBFAModel, n::Int, br_idx_c, f_bus, t_bus, f_idx_c, t_idx_c, r_p, x_p, 0, 0, tm)
end

""
function constraint_ne_voltage_magnitude_difference_frb_parallel(pm::_PM.AbstractBFAModel, n::Int, br_idx_e, br_idx_c, f_bus, t_bus, f_idx_c, t_idx_c, r_e, x_e, g_sh_fr_e, b_sh_fr_e, r_c, x_c, g_sh_fr_c, b_sh_fr_c, tm)
    # Suffixes: _e: existing branch; _c: candidate branch; _p: parallel equivalent
    r_p  = (r_e*(r_c^2+x_c^2)+r_c*(r_e^2+x_e^2)) / ((r_e+r_c)^2+(x_e+x_c)^2)
    x_p  = (x_e*(r_c^2+x_c^2)+x_c*(r_e^2+x_e^2)) / ((r_e+r_c)^2+(x_e+x_c)^2)

    constraint_ne_voltage_magnitude_difference_frb(pm::_PM.AbstractBFAModel, n::Int, br_idx_c, f_bus, t_bus, f_idx_c, t_idx_c, r_p, x_p, 0, 0, tm)
end

## Auxiliary functions ##

function replace_branch(pm, nw, f_bus, t_bus)
    replace = 0
    ne_br_idx = 0
    for (br, ne_branch) in _PM.ref(pm, nw, :ne_branch)
        if ((ne_branch["f_bus"] == f_bus && ne_branch["t_bus"] == t_bus) || (ne_branch["f_bus"] == t_bus && ne_branch["t_bus"] == f_bus)) && ne_branch["replace"] == true
            replace = 1
            ne_br_idx = br
        end
    end
    return replace, ne_br_idx
end

"Returns the index of `branch` connecting `f_bus` to `t_bus`, if such a `branch` exists; 0 otherwise"
function branch_idx(pm::_PM.AbstractPowerModel, nw::Int, f_bus, t_bus)
    buspairs = _PM.ref(pm, nw, :buspairs)
    buspair = get(buspairs, (f_bus,t_bus), Dict("branch"=>0))
    return buspair["branch"]
end

"Returns a list of indices of `ne_branch`es relative to `branch` `br_idx`"
function ne_branch_ids(pm::_PM.AbstractPowerModel, br_idx::Int; nw::Int=_PM.nw_id_default)
    branch = _PM.ref(pm, nw, :branch, br_idx)
    f_bus = branch["f_bus"]
    t_bus = branch["t_bus"]
    ne_branch_ids(pm, nw, f_bus, t_bus)
end

"Returns a list of indices of `ne_branch`es connecting `f_bus` to `t_bus`"
function ne_branch_ids(pm::_PM.AbstractPowerModel, nw::Int, f_bus, t_bus)
    ne_buspairs = _PM.ref(pm, nw, :ne_buspairs)
    ne_buspair = get(ne_buspairs, (f_bus,t_bus), Dict("branches"=>Int[]))
    return ne_buspair["branches"]
end

"Returns whether a `ne_branch` is intended to replace the existing branch or to be added in parallel"
function ne_branch_replace(pm::_PM.AbstractPowerModel, ne_br_idx::Int; nw::Int=_PM.nw_id_default)
    ne_branch = _PM.ref(pm, nw, :ne_branch, ne_br_idx)
    if !haskey(ne_branch, "replace")
        Memento.error(_LOGGER, "a `replace` value is required on all `ne_branch`es")
    end
    return ne_branch["replace"] == 1
end


# This file is a part of Julia. License is MIT: https://julialang.org/license

## general machinery for irrational mathematical constants

"""
    AbstractIrrational <: Real

Number type representing an exact irrational value.
"""
abstract type AbstractIrrational <: Real end

"""
    Irrational{sym} <: AbstractIrrational

Number type representing an exact irrational value denoted by the
symbol `sym`.
"""
struct Irrational{sym} <: AbstractIrrational end

show(io::IO, x::Irrational{sym}) where {sym} = print(io, sym)

function show(io::IO, ::MIME"text/plain", x::Irrational{sym}) where {sym}
    if get(io, :compact, false)
        print(io, sym)
    else
        print(io, sym, " = ", string(float(x))[1:15], "...")
    end
end

promote_rule(::Type{<:AbstractIrrational}, ::Type{Float16}) = Float16
promote_rule(::Type{<:AbstractIrrational}, ::Type{Float32}) = Float32
promote_rule(::Type{<:AbstractIrrational}, ::Type{<:AbstractIrrational}) = Float64
promote_rule(::Type{<:AbstractIrrational}, ::Type{T}) where {T<:Real} = promote_type(Float64, T)
promote_rule(::Type{S}, ::Type{T}) where {S<:AbstractIrrational,T<:Number} = promote_type(promote_type(S, real(T)), T)

AbstractFloat(x::AbstractIrrational) = Float64(x)
Float16(x::AbstractIrrational) = Float16(Float32(x))
Complex{T}(x::AbstractIrrational) where {T<:Real} = Complex{T}(T(x))

@pure function Rational{T}(x::AbstractIrrational) where T<:Integer
    o = precision(BigFloat)
    p = 256
    while true
        setprecision(BigFloat, p)
        bx = BigFloat(x)
        r = rationalize(T, bx, tol=0)
        if abs(BigFloat(r) - bx) > eps(bx)
            setprecision(BigFloat, o)
            return r
        end
        p += 32
    end
end
(::Type{Rational{BigInt}})(x::AbstractIrrational) = throw(ArgumentError("Cannot convert an AbstractIrrational to a Rational{BigInt}: use rationalize(Rational{BigInt}, x) instead"))

@pure function (t::Type{T})(x::AbstractIrrational, r::RoundingMode) where T<:Union{Float32,Float64}
    setprecision(BigFloat, 256) do
        T(BigFloat(x), r)
    end
end

float(::Type{<:AbstractIrrational}) = Float64

==(::Irrational{s}, ::Irrational{s}) where {s} = true
==(::AbstractIrrational, ::AbstractIrrational) = false

<(::Irrational{s}, ::Irrational{s}) where {s} = false
function <(x::AbstractIrrational, y::AbstractIrrational)
    Float64(x) != Float64(y) || throw(MethodError(<, (x, y)))
    return Float64(x) < Float64(y)
end

<=(::Irrational{s}, ::Irrational{s}) where {s} = true
<=(x::AbstractIrrational, y::AbstractIrrational) = x==y || x<y

# Irrationals, by definition, can't have a finite representation equal them exactly
==(x::AbstractIrrational, y::Real) = false
==(x::Real, y::AbstractIrrational) = false

# Irrational vs AbstractFloat
<(x::AbstractIrrational, y::Float64) = Float64(x,RoundUp) <= y
<(x::Float64, y::AbstractIrrational) = x <= Float64(y,RoundDown)
<(x::AbstractIrrational, y::Float32) = Float32(x,RoundUp) <= y
<(x::Float32, y::AbstractIrrational) = x <= Float32(y,RoundDown)
<(x::AbstractIrrational, y::Float16) = Float32(x,RoundUp) <= y
<(x::Float16, y::AbstractIrrational) = x <= Float32(y,RoundDown)
<(x::AbstractIrrational, y::BigFloat) = setprecision(precision(y)+32) do
    big(x) < y
end
<(x::BigFloat, y::AbstractIrrational) = setprecision(precision(x)+32) do
    x < big(y)
end

<=(x::AbstractIrrational, y::AbstractFloat) = x < y
<=(x::AbstractFloat, y::AbstractIrrational) = x < y

# Irrational vs Rational
@pure function rationalize(::Type{T}, x::AbstractIrrational; tol::Real=0) where T
    return rationalize(T, big(x), tol=tol)
end
@pure function lessrational(rx::Rational{<:Integer}, x::AbstractIrrational)
    # an @pure version of `<` for determining if the rationalization of
    # an irrational number required rounding up or down
    return rx < big(x)
end
function <(x::AbstractIrrational, y::Rational{T}) where T
    T <: Unsigned && x < 0.0 && return true
    rx = rationalize(T, x)
    if lessrational(rx, x)
        return rx < y
    else
        return rx <= y
    end
end
function <(x::Rational{T}, y::AbstractIrrational) where T
    T <: Unsigned && y < 0.0 && return false
    ry = rationalize(T, y)
    if lessrational(ry, y)
        return x <= ry
    else
        return x < ry
    end
end
<(x::AbstractIrrational, y::Rational{BigInt}) = big(x) < y
<(x::Rational{BigInt}, y::AbstractIrrational) = x < big(y)

<=(x::AbstractIrrational, y::Rational) = x < y
<=(x::Rational, y::AbstractIrrational) = x < y

isfinite(::AbstractIrrational) = true
isinteger(::AbstractIrrational) = false
iszero(::AbstractIrrational) = false
isone(::AbstractIrrational) = false

hash(x::Irrational, h::UInt) = 3*objectid(x) - h

widen(::Type{T}) where {T<:Irrational} = T

-(x::AbstractIrrational) = -Float64(x)
for op in Symbol[:+, :-, :*, :/, :^]
    @eval $op(x::AbstractIrrational, y::AbstractIrrational) = $op(Float64(x),Float64(y))
end
*(x::Bool, y::AbstractIrrational) = ifelse(x, Float64(y), 0.0)

round(x::Irrational, r::RoundingMode) = round(float(x), r)

"""
	@irrational sym val def
	@irrational(sym, val, def)

Define a new `Irrational` value, `sym`, with pre-computed `Float64` value `val`,
and arbitrary-precision definition in terms of `BigFloat`s given be the expression `def`.
"""
macro irrational(sym, val, def)
    esym = esc(sym)
    qsym = esc(Expr(:quote, sym))
    bigconvert = isa(def,Symbol) ? quote
        function Base.BigFloat(::Irrational{$qsym}, r::MPFR.MPFRRoundingMode=MPFR.ROUNDING_MODE[]; precision=precision(BigFloat))
            c = BigFloat(;precision=precision)
            ccall(($(string("mpfr_const_", def)), :libmpfr),
                  Cint, (Ref{BigFloat}, MPFR.MPFRRoundingMode), c, r)
            return c
        end
    end : quote
        function Base.BigFloat(::Irrational{$qsym}; precision=precision(BigFloat))
            setprecision(BigFloat, precision) do
                $(esc(def))
            end
        end
    end
    quote
        const $esym = Irrational{$qsym}()
        $bigconvert
        Base.Float64(::Irrational{$qsym}) = $val
        Base.Float32(::Irrational{$qsym}) = $(Float32(val))
        @assert isa(big($esym), BigFloat)
        @assert Float64($esym) == Float64(big($esym))
        @assert Float32($esym) == Float32(big($esym))
    end
end

big(x::AbstractIrrational) = BigFloat(x)
big(::Type{<:AbstractIrrational}) = BigFloat

# align along = for nice Array printing
function alignment(io::IO, x::AbstractIrrational)
    m = match(r"^(.*?)(=.*)$", sprint(show, x, context=io, sizehint=0))
    m === nothing ? (length(sprint(show, x, context=io, sizehint=0)), 0) :
    (length(m.captures[1]), length(m.captures[2]))
end

# inv
inv(x::AbstractIrrational) = 1/x