import Base: @propagate_inbounds, @inbounds # compiler inlines a function, while retaining the caller's inbounds context
import BenchmarkTools: @btime, @time, @benchmark

#euclid fans have a right to throw tomato at me
@propagate_inbounds sumInterval(a, b) = abs(a + b) +1> # Range (min … max):  0.001 ns … 0.100 ns

@propagate_inbounds function sumInterval(a, b) #sumInterval subtracts 1 (complies with julia logic)
    cond = abs(a + b) - 1
    @inbounds if cond >= 1
        return cond
    end
end

"""isEven1 experimential Performance: always below isEven2
edit: there's nothing called as `always`
"""
@propagate_inbounds function isEven1(a = 1, b = 10) #3 if-statements
    try
        @inbounds if a >= 0 && b >= 0
            distance = sumInterval(a, b) #1. distance
            _isPositive = isPositive(distance) #2. positive distance
            @inbounds if _isPositive #positive?
                isEven = distance % 2 == 0 ? true : false
                @inbounds if isEven           # even?
                    return true
                elseif !isEven # || isPositive
                    return false
                else
                    println(UnexpMsg)
                end
            else
                throw(error("Positive number error"))
            end
            #end
        end
    catch notPositiveInputError
        #Exception
        @error positiveMsg exception = (notPositiveInputError, catch_backtrace())
        println(positiveMsg) #positive arguments error
    end
end
isEven1(-1, 10)
isEven1(1, 10)

#=
@inbounds if lenV < lenA # first assumption
    @inbounds a[i:lenV] = v[i:lenV]
    #elseif lenV > lenA #v can't be larger than original vector a (throws an outofBoundsError )
else
    throw(error("Positive number error"))
    println(UnexpMsg)
end
#return a
#end =#
 #computeRange!(a, α, β) # # TODO: middle(arr, a, b) #or # TODO: middle(a, b, arr)
@propagate_inbounds function replaceVector2(v = [1, 2], a = [2, 3, 4, 5]; i = 1)

"""divideConquer """

#  mit 6.006  #unused #orphaned #to-be-deleted
#choosing a,b is choosing arr , this is a mean, not a goal
# divideConquer(a, range[1], length(range[1])) #check
@propagate_inbounds function divideConquer(a = [1, 2, 3, 4]; α = arr[1], β = length(a) - 1)  ## arr is an initial value, while the goal would be , 1  length(other size of bound to walk on  ) a code that never walks..
    count = 1
    mid = middle(α, β) # first Domino #this presumes middle returns 1 middle # immature
    #check valid Interval
    cond = sumInterval(mid, count) > 0 ? true : false #= mid - count >= 0=#

    q = []

    if cond
        count += 1
        if a[mid] < a[mid-1] # only look at left half 1 . . . n/2 − − − 1 to look for peak
            pushfirst!(q, goleft(arr, a, mid))

        elseif a[mid] < a[mid+1] # only look at right half n/2 + 1 . . . n to look for peak
            pushfirst!(q, goright(a))

        else # a[mid] =?= a[mid+1]
            pushfirst!(q, mid) # the peak!
        end
    end
end
#no errors

#-----------
#---------------

cond = isEven(1, 3)
res = -1

#---test run tryout

#1,3 total range
# 1+3/2 = 2
#yeilds 1:2 , 2:3
#check dist

abs(2 - 3) == 1 # bounds are apart by 1 #sweet!

# final comparison
#doCompare(2,3,arr)
#compare current ranges with their value , do comparison when required
a[1]

#=Plain comparison - flip if lower index has a higher value, otherwise return
given bound values of indicies, find their corresponding value that they weigh,
then compare them together, as well

```input:
st: start bound
ed: ending bound
a: corresponding vector array to traverse
```

```output:
```

=#
#inputs, outputs

"""
```input:
```

```output:
```
"""
compareVector()

#--- q Zone /
a = buildAboveSoBelow(below, above, arr)
#= [1, 2]
 [2, 3]
 [3, 4, 5]=#
l = length(q)
count = 1 #starting from 1
#=
0 -> 3 (3)
1-> 4 (3)
this correctly counts 1 -> 3 (sumRanges 3+1 = 4 but max is 3 )
=#

l = length(q[2]) # 2
array = []; # [firstvalue]
count = 1
temp = deepcopy(q[2])

typeof(q[2][1]) #Vector #int64

res = q[2] # pop!(q[2])
typeof(q[2])

#---repeat
for i = 1:l #enumerate(q) # in 0:l#4 Always!    #enumerate(q) also works fine as Base.Iterator.Enumerate
    #count would be 3 = length(q) A lways
    #count<=l ? count += 1 : break; # count ==3 max
    #array[i] =i
    push!(arr, i)
    item = Base.popfirst!(q[2]) #pop!(collection) -> item # Remove the last item in collection and return it.
    #i<=l  ? count+= 1 : break;
    #count+=1 # counts 4 ( size = length(q) + 1 ) #either need to count -1

end # will count 4

#array arr
count -= 1 # learnd heuristic # length adjustment [of the Interval ] # q. why not measure length(v) directly (after its adjustments)?
#count = 0->3 0 index or 1 ->4 1-index

#q = buildAboveSoBelow(a, below, above, b) # instead: make adjustments on the array arr directtly

#--- end of q
#--- end q Zone

#=
"""a valid middle - Classic #old #depreciated ,why it uses q """
function middle(α, β) #
    condition = isEven(α, β) #  not | b-a
    #q = []
    if condition == true
        #return true #a.s. #eucledian Distance divided by 2 returing a whole integer
        check = Int(sumInterval(α, β) // 2) # | b + a | // 2 isa Integer #sumInterval -to-> ϟ#5
        #middleExtraction(condition, check) # Here we didn't get anything ! <------------- # check not defined here
        #return condition, check
        #push!(q, check)
    elseif !condition # == false
        #return false #a.s.# check = sumInterval(a,b)//2*1.0
        #GET Ceil & Floor

        #call middle
        #sumInterval(α,β)/2
        #does other things as well : # warning  #old-thinking
        check = sumInterval(α , β)/2 # floating-point division sumInterval(a, b) / 2 * 1.0 # freely allowing floats, to be ceiled & floored
        if isEven2(check)
            above = Int(ceil(check)) #nearest index above
            below = Int(floor(check))

      #  push!(q, below)#old thinking detected # to be changed
       # push!(q, above)

    else # faulty Input or Unexpected Error Occured
        #    return check  # nothing
        return #q #condition, check, above, below
    end
    return #q #condition, check, above, below
end
resultvector = middle(1,2)
#check length
length(resultvector) > 1 #there's atleast 1 middle

isEven2(length(resultvector )) #divisible /2
=#


__precompile__(false)
module MicroBench

# The main reason for this module's existence
using CachedArrays

# stdlib
using Dates
using LinearAlgebra
using Random
using Statistics

# "internal" dependencies
using MaxLFSR
using KernelBenchmarks  # for fast initialization of arrays.
using SystemSnoop
using CounterTools
using MattDaemon
using ExperimentsDB

# external deps
using DataStructures
using ProgressMeter
using StructArrays

# debug mode enables much smaller kernels that don't exceed the size of the Local
const DEBUG = false

function __init__()
    if DEBUG
        @warn """
        Running in debug mode!!

        Make sure you aren't trying to record data!!
        """
    end
end

# Will throw an error if this is not defined
const IS_2LM = parse(Bool, ENV["JULIA_IS_2LM"])
const IS_1LM = !IS_2LM

# More Prefetch definitions
maybesuper(x...) = CachedArrays.maybesuper(x...)

function LinearAlgebra.mul!(Y::AbstractCachedArray, A::AbstractCachedArray, B::AbstractCachedArray)
    CachedArrays.shallowfetch!(Y)
    invoke(
        LinearAlgebra.mul!,
        Tuple{maybesuper(Y), maybesuper(A), maybesuper(B)},
        CachedArrays.unlock(Y),
        A,
        B,
    )
    return nothing
end

include("counters.jl")
include("1d/top_1d.jl")
include("2d/top_2d.jl")

end # module


##  Copyright 2022 Myrl Marmarelis
##
##  Licensed under the Apache License, Version 2.0 (the "License");
##  you may not use this file except in compliance with the License.
##  You may obtain a copy of the License at
##
##    http://www.apache.org/licenses/LICENSE-2.0
##
##  Unless required by applicable law or agreed to in writing, software
##  distributed under the License is distributed on an "AS IS" BASIS,
##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##  See the License for the specific language governing permissions and
##  limitations under the License.

using ForwardDiff

abstract type Constraint{ T <: Real } end # do I really need the `T` for the supertype?

draw_instance(::Constraint{T}, ::Predictor{In,POut,T}, ::PredictionParam{T},
    ::Vector{Instance{In,Out,T}}) where {In,POut,Out,T} = #::ConstraintInstance{PIn,Out,T}
  error("unimplemented")

apply_operator(::Constraint{T}, ::Predictor{In,POut,T}, ::PredictionParam{T},
    ::ConstraintInstance{In,COut,T}) where {In,POut,COut,T} =
  error("unimplemented")

constraint_instance_type(::Constraint{T}, ::PredictionParam{T}, ::Predictor{In,POut,T}
    ) where {In,POut,T} =
  error("unimplemented")

struct ConservationConstraint{ T } <: Constraint{T}
  input_indices :: Matrix{Int} # batched as columns so that gradient only has to be taken once
  coefficients :: Matrix{T}
  positive :: Vector{Bool} # per column
  absolute :: Vector{Bool}
  mimicked_indices :: Vector{Int} # indices to grab from a random instance; recycled
end

constraint_target(::LomaxParam, (alpha, beta)::AbstractVector) =
  (alpha - 1) / beta # mode of the rate parameter

@generated function constraint_instance_type(::ConservationConstraint{T},
    ::Predictor{In,POut,T}, ::PredictionParam{T}) where {In,POut,T} # make this a macro? (harder to know types of arguments)
  ConstraintInstance{In,0,T} # do I need to quote this even though it's just a type value?
end

function draw_instance(constraint::ConservationConstraint{T}, predictor::Predictor{In,POut,T},
    parametrization::PredictionParam{T}, instances::Vector{Instance{In,Out,T}}
    )::ConstraintInstance{In,0,T} where {In,POut,Out,T}
  grabbed_instance = sample(instances)
  input = @MVector rand(T, In)
  for i in constraint.mimicked_indices # just overwrite previously randomized
    input[i] = grabbed_instance.input[i]
  end
  # I guess I could always subclass the ConstraintInstance?
  # generic structure seems to work fine for now.
  ConstraintInstance(
    convert(SVector, input),
    SVector{0,T}() )
end

function apply_operator(constraint::ConservationConstraint{T}, predictor::Predictor{In,POut,T},
    parametrization::PredictionParam{T}, instance::ConstraintInstance{In,0,T})::T where {In,POut,T}
  n_indices, n_laws = size(constraint.input_indices)
  @assert (n_indices, n_laws) == size(constraint.coefficients)
  n_targets = n_instance_outputs(predictor, parametrization)
  sum(1:n_targets) do target_index
    # recent versions of Zygote can ALMOST differentiate their own gradients
    derivatives = ForwardDiff.gradient(instance.input) do input # input -> begin ... end
      map_prediction(params -> constraint_target(parametrization, params),
        predictor, parametrization, input, target_index)
    end
    #derivatives, = grad(central_fdm(diff_order, 1), differentiable, instance.input)
    sum(1:n_laws) do law
      operator = sum(1:n_indices) do index
        input = constraint.input_indices[index, law]
        coefficient = constraint.coefficients[index, law]
        metric = derivatives[input]
        if constraint.absolute[law]
          metric = abs(metric)
        end
        coefficient * metric
      end
      if constraint.positive[law]
        min(operator, 0) ^ 2
      else
        operator ^ 2 # penalize nonzero-ness
      end
    end
  end
end


include("dynamic_piomelli.jl")

@par function realspace_dynamic_les_calculation!(s::A) where {A<:@par(AbstractSimulation)}
    real!(s.lesmodel.û)
    real!(s.lesmodel.M)
    if is_dynP_les(A)
        real!(s.lesmodel.ŵ)
    end
    if is_piomelliSmag_les(A)
        dt = get_dt(s)
        dtm1 = s.lesmodel.dtm1[]
        dtf = dt == 0.0 ? 1.0 : dt
        dtm1f = dtm1 == 0.0 ? dtf : dtm1

        if s.iteration[] == 0
            copyto!(s.lesmodel.cm1.rr,s.lesmodel.c.rr)
        end

        calc_cstar!(s.lesmodel.c.rr,s.lesmodel.cm1.rr,dtf,dtm1f)
        s.lesmodel.dtm1[] = dt
    end

    if is_piomelliP_les(A)
        dt = get_dt(s)
        dtm1 = s.lesmodel.dtpm1[]
        dtf = dt == 0.0 ? 1.0 : dt
        dtm1f = dtm1 == 0.0 ? dtf : dtm1

        if s.iteration[] == 0
            copyto!(s.lesmodel.cpm1.rr,s.lesmodel.cp.rr)
        end

        calc_cstar!(s.lesmodel.cp.rr,s.lesmodel.cpm1.rr,dtf,dtm1f)
        s.lesmodel.dtpm1[] = dt
    end

    @mthreads for j in TRANGE
        realspace_dynamic_les_calculation!(s,j)
    end
    return nothing
end

@par function realspace_dynamic_les_calculation!(s::A,j::Integer) where {A<:@par(AbstractSimulation)}
    u = s.u.rr
    L = s.lesmodel.L.rr
    tau = s.lesmodel.tau.rr
    Sa = s.lesmodel.S.rr
    Δ² = s.lesmodel.Δ²

    if is_piomelliSmag_les(A)
        c = s.lesmodel.c.rr
    end

    if is_dynP_les(A)
        w = s.rhs.rr
        P = s.lesmodel.P.rr
        if is_piomelliP_les(A)
            cp = s.lesmodel.cp.rr
        end
    end

    @inbounds @msimd for i in REAL_RANGES[j]
        S = tau[i]
        if is_piomelliSmag_les(A)
            Sa[i] = 2*c[i]*Δ²*norm(S)*S
        else
            Sa[i] = Δ²*norm(S)*S
        end
        v = u[i]
        L[i] = symouter(v,v)
        if is_piomelliP_les(A)
            P[i] = cp[i]*Δ²*Lie(S,AntiSymTen(0.5*w[i]))
        elseif is_dynSandP_les(A)
            P[i] = Δ²*Lie(S,AntiSymTen(0.5*w[i]))
        end
    end

    return nothing
end

@par function fourierspace_dynamic_les_calculation!(s::A) where {A<:@par(AbstractSimulation)}
    fullfourier!(s.lesmodel.S)
    fullfourier!(s.lesmodel.L)
    is_dynP_les(A) && fullfourier!(s.lesmodel.P)

    ind = CartesianIndices((Base.OneTo(NY),Base.OneTo(NZ)))
    @mthreads for jk in ind
        j,k = Tuple(jk)
        fourierspace_dynamic_les_calculation!(s,j,k)
    end

    real!(s.lesmodel.S)
    real!(s.lesmodel.L)
    is_dynP_les(A) && real!(s.lesmodel.P)
    return nothing
end

@par function fourierspace_dynamic_les_calculation!(s::A,j::Integer, k::Integer) where {A<:@par(AbstractSimulation)}
    S = s.lesmodel.S.c
    L = s.lesmodel.L.c
    Δ̂² = s.lesmodel.Δ̂²
    if is_dynP_les(A)
        P = s.lesmodel.P.c
    end

    @inbounds begin
        @msimd for i in Base.OneTo(NX)
            G = Gaussfilter(Δ̂²,i,j,k)
            S[i,j,k] *= G
            L[i,j,k] *= G
            if is_dynP_les(A)
                P[i,j,k] *= G
            end
        end
    end
end

@par function coefficient_les_calculation!(s::A) where {A<:@par(AbstractSimulation)}
    @mthreads for j in TRANGE
        coefficient_les_calculation!(s,j)
    end

    s.lesmodel.avg && average_c!(s.lesmodel.c,s.lesmodel.Δ̂²)

    if is_dynP_les(s)
        s.lesmodel.avg && average_c!(s.lesmodel.cp,s.lesmodel.Δ̂²)
    end

    return nothing
end

@par function coefficient_les_calculation!(s::A,j::Integer) where {A<:@par(AbstractSimulation)}

    rhs = s.rhs.rr
    τ = s.lesmodel.tau.rr
    Δ² = s.lesmodel.Δ²

    if haspassivescalarles(A)
        φ = s.passivescalar.φ.field.data
        fφ = s.passivescalar.flux.rr
        has_les_scalar_vorticity_model(A) && (cφ = s.passivescalar.lesmodel.c*Δ²)
    end

    if hasdensityles(A)
        ρ = s.densitystratification.ρ.field.data
        f = s.densitystratification.flux.rr
        has_les_density_vorticity_model(A) && (cρ = s.densitystratification.lesmodel.c*Δ²)
    end

    ca = s.lesmodel.c.rr
    û = s.lesmodel.û.rr
    La = s.lesmodel.L.rr
    Ma = s.lesmodel.M.rr
    Sa = s.lesmodel.S.rr
    Δ̂² = s.lesmodel.Δ̂² + Δ²
    #cmin = s.lesmodel.cmin

    if is_dynP_les(A)
        Pa = s.lesmodel.P.rr
        ŵ = s.lesmodel.ŵ.rr
        cpa = s.lesmodel.cp.rr
    end

    @inbounds @msimd for i in REAL_RANGES[j]
        w = rhs[i]
        S = τ[i]

        L = traceless(symouter(û[i],û[i]) - La[i])
        La[i] = L
        Sh = Ma[i]

        if is_dynSmag_les(A)
            M = Δ̂²*norm(Sh)*Sh - Sa[i]
            #c = max(0.0,0.5*((L:M)/(M:M)))
            c = max(-1.0,min(1.0,0.5*((L:M)/(M:M))))
        elseif is_piomelliSmag_les(A)

            τ̂ = Sa[i]

            if is_piomelliP_les(A)
                τ̂ += Pa[i]
            end

            ah = norm(Sh)

            M = 2*Δ̂²*ah*Sh
            Mn = M/(M:M)
            c = (τ̂ + L):Mn
            c = max(-1.0,min(1.0,c))
            #c = ifelse(ah<10.0,0.0,max(0.0,c))
        end
        ca[i] = c

        if is_dynP_les(A)
            wh = ŵ[i]
            Ph = Lie(Sh,AntiSymTen(0.5*wh))

            if is_dynSandP_les(A)
                Mp = Δ̂²*Ph - Pa[i]
                cp = max(-1.0,min(1.0,(L:Mp)/(Mp:Mp))) # Should I clip negative values?
                #cp = max(0.0,cp)
            elseif is_piomelliP_les(A)
                if !is_piomelliSmag_les(A)
                    τ̂ = Pa[i]
                end
                Mp = Δ̂²*Ph
                Mpn = Mp/(Mp:Mp)
                cp = max(-1.0,min(1.0,(τ̂ + L):Mpn))
            end

            cpa[i] = cp
        end

    end

    return nothing
end


using Secp256k1
using Base58: base58checkdecode

@testset "Transaction" begin
    raw_tx = hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600")
    @testset "Parsing" begin
        @testset "Version" begin
            stream = IOBuffer(raw_tx)
            tx = parse(stream)::Tx
            @test tx.version == 1
        end
        @testset "Inputs" begin
            stream = IOBuffer(raw_tx)
            tx = parse(stream)::Tx
            @test length(tx.tx_ins) == 1
            want = hex2bytes("d1c789a9c60383bf715f3f6ad9d14b91fe55f3deb369fe5d9280cb1a01793f81")
            @test tx.tx_ins[1].prev_tx == want
            @test tx.tx_ins[1].prev_index == 0
            want = hex2bytes("6b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278a")
            @test Bitcoin.serialize(tx.tx_ins[1].script_sig) == want
            @test tx.tx_ins[1].sequence == 0xfffffffe
        end
        @testset "Outputs" begin
            stream = IOBuffer(raw_tx)
            tx = parse(stream)::Tx
            @test length(tx.tx_outs) == 2
            want = 32454049
            @test tx.tx_outs[1].amount == want
            want = hex2bytes("1976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac")
            @test Bitcoin.serialize(tx.tx_outs[1].script_pubkey) == want
            want = 10011545
            @test tx.tx_outs[2].amount == want
            want = hex2bytes("1976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac")
            @test Bitcoin.serialize(tx.tx_outs[2].script_pubkey) == want
        end
        @testset "Locktime" begin
            stream = IOBuffer(raw_tx)
            tx = parse(stream)::Tx
            @test tx.locktime == 410393
        end
    end
    @testset "Serialize" begin
        stream = IOBuffer(raw_tx)
        tx = Bitcoin.parse_legacy(stream)
        @test serialize(tx) == raw_tx
    end
    @testset  "Input Value" begin
        tx_hash = "d1c789a9c60383bf715f3f6ad9d14b91fe55f3deb369fe5d9280cb1a01793f81"
        index = 0
        want = 42505594
        tx_in = Bitcoin.TxIn(hex2bytes(tx_hash), index)
        @test Bitcoin.value(tx_in) == want
    end
    @testset "Input PubKey" begin
        tx_hash = "d1c789a9c60383bf715f3f6ad9d14b91fe55f3deb369fe5d9280cb1a01793f81"
        index = 0
        want = hex2bytes("1976a914a802fc56c704ce87c42d7c92eb75e7896bdc41ae88ac")
        tx_in = Bitcoin.TxIn(hex2bytes(tx_hash), index)
        @test Bitcoin.serialize(Bitcoin.script_pubkey(tx_in)) == want
    end
    @testset "Fee" begin
        raw_tx = hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        @test Bitcoin.fee(tx) == 40000
        raw_tx = hex2bytes("010000000456919960ac691763688d3d3bcea9ad6ecaf875df5339e148a1fc61c6ed7a069e010000006a47304402204585bcdef85e6b1c6af5c2669d4830ff86e42dd205c0e089bc2a821657e951c002201024a10366077f87d6bce1f7100ad8cfa8a064b39d4e8fe4ea13a7b71aa8180f012102f0da57e85eec2934a82a585ea337ce2f4998b50ae699dd79f5880e253dafafb7feffffffeb8f51f4038dc17e6313cf831d4f02281c2a468bde0fafd37f1bf882729e7fd3000000006a47304402207899531a52d59a6de200179928ca900254a36b8dff8bb75f5f5d71b1cdc26125022008b422690b8461cb52c3cc30330b23d574351872b7c361e9aae3649071c1a7160121035d5c93d9ac96881f19ba1f686f15f009ded7c62efe85a872e6a19b43c15a2937feffffff567bf40595119d1bb8a3037c356efd56170b64cbcc160fb028fa10704b45d775000000006a47304402204c7c7818424c7f7911da6cddc59655a70af1cb5eaf17c69dadbfc74ffa0b662f02207599e08bc8023693ad4e9527dc42c34210f7a7d1d1ddfc8492b654a11e7620a0012102158b46fbdff65d0172b7989aec8850aa0dae49abfb84c81ae6e5b251a58ace5cfeffffffd63a5e6c16e620f86f375925b21cabaf736c779f88fd04dcad51d26690f7f345010000006a47304402200633ea0d3314bea0d95b3cd8dadb2ef79ea8331ffe1e61f762c0f6daea0fabde022029f23b3e9c30f080446150b23852028751635dcee2be669c2a1686a4b5edf304012103ffd6f4a67e94aba353a00882e563ff2722eb4cff0ad6006e86ee20dfe7520d55feffffff0251430f00000000001976a914ab0c0b2e98b1ab6dbf67d4750b0a56244948a87988ac005a6202000000001976a9143c82d7df364eb6c75be8c80df2b3eda8db57397088ac46430600")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        @test Bitcoin.fee(tx) == 140500
    end
    @testset "Sig Hash" begin
        raw_tx = hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        want = hex2bytes("27e0c5994dec7824e56dec6b2fcb342eb7cdb0d0957c2fce9882f715e85d81a6")
        @test Bitcoin.sig_hash(tx, 0) == want
    end
    @testset "hash_prevouts" begin
        tx = get_tx("d869f854e1f8788bcff294cc83b280942a8c728de71eb709a2c29d10bfe21b7c", testnet=true)
        want = hex2bytes("7fe72cb4866759324e1f158d0482fcc49015b0987966d80624dcc2db24638999")
        @test Bitcoin.hash_prevouts(tx) == want
    end
    @testset "Verify" begin
        @testset "P2PKH" begin
            tx = get_tx("452c629d67e41baec3ac6f04fe744b4b9617f8f859c63b3002f8684e7a4fee03")
            @test verify(tx)
            tx = get_tx("5418099cc755cb9dd3ebc6cf1a7888ad53a1a3beb5a025bce89eb1bf7f1650a2", testnet=true)
            @test verify(tx)
        end
        @testset "P2SH" begin
            println("Testing P2SH...")
            tx = get_tx("46df1a9484d0a81d03ce0ee543ab6e1a23ed06175c104a178268fad381216c2b")
            @test verify(tx)
        end
        @testset "P2WPKH" begin
            println("Testing P2WPKH...")
            tx = get_tx("d869f854e1f8788bcff294cc83b280942a8c728de71eb709a2c29d10bfe21b7c", testnet=true)
            @test verify(tx)
        end
        @testset "P2SH-P2WPKH" begin
            println("Testing P2SH-P2WPKH...")
            tx = get_tx("c586389e5e4b3acb9d6c8be1c19ae8ab2795397633176f5a6442a261bbdefc3a")
            @test verify(tx)
        end
        @testset "P2WSH" begin
            println("Testing P2WSH...")
            tx = get_tx("78457666f82c28aa37b74b506745a7c7684dc7842a52a457b09f09446721e11c", testnet=true)
            @test verify(tx)
        end
        @testset "P2SH-P2WSH" begin
            println("Testing P2SH-P2WSH...")
            tx = get_tx("954f43dbb30ad8024981c07d1f5eb6c9fd461e2cf1760dd1283f052af746fc88", testnet=true)
            @test verify(tx)
        end
    end
    @testset "Sign Input" begin
        keypair = KeyPair{:ECDSA}(8675309)
        tx_ins = TxIn[]
        prev_tx = hex2bytes("0025bc3c0fa8b7eb55b9437fdbd016870d18e0df0ace7bc9864efc38414147c8")
        push!(tx_ins, TxIn(prev_tx, 0))
        tx_outs = TxOut[]
        h160 = base58checkdecode(b"mzx5YhAH9kNHtcN481u6WkjeHjYtVeKVh2")[2:end]
        push!(tx_outs, TxOut(Int(0.99 * 100000000), Bitcoin.p2pkh_script(h160)))
        h160 = base58checkdecode(b"mnrVtF8DWjMu839VW3rBfgYaAfKk8983Xf")[2:end]
        push!(tx_outs, TxOut(Int(0.1 * 100000000), Bitcoin.p2pkh_script(h160)))
        tx = Tx(1, tx_ins, tx_outs, 0, true)
        @test txsigninput(tx, 0, keypair)
    end
    @testset "Is CoinbaseTx" begin
        raw_tx = hex2bytes("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5e03d71b07254d696e656420627920416e74506f6f6c20626a31312f4542312f4144362f43205914293101fabe6d6d678e2c8c34afc36896e7d9402824ed38e856676ee94bfdb0c6c4bcd8b2e5666a0400000000000000c7270000a5e00e00ffffffff01faf20b58000000001976a914338c84849423992471bffb1a54a8d9b1d69dc28a88ac00000000")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        @test iscoinbase(tx)
    end
    @testset "coinbase_height" begin
        raw_tx = hex2bytes("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff5e03d71b07254d696e656420627920416e74506f6f6c20626a31312f4542312f4144362f43205914293101fabe6d6d678e2c8c34afc36896e7d9402824ed38e856676ee94bfdb0c6c4bcd8b2e5666a0400000000000000c7270000a5e00e00ffffffff01faf20b58000000001976a914338c84849423992471bffb1a54a8d9b1d69dc28a88ac00000000")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        @test coinbase_height(tx) == 465879
        raw_tx = hex2bytes("0100000001813f79011acb80925dfe69b3def355fe914bd1d96a3f5f71bf8303c6a989c7d1000000006b483045022100ed81ff192e75a3fd2304004dcadb746fa5e24c5031ccfcf21320b0277457c98f02207a986d955c6e0cb35d446a89d3f56100f4d7f67801c31967743a9c8e10615bed01210349fc4e631e3624a545de3f89f5d8684c7b8138bd94bdd531d2e213bf016b278afeffffff02a135ef01000000001976a914bc3b654dca7e56b04dca18f2566cdaf02e8d9ada88ac99c39800000000001976a9141c4bc762dd5423e332166702cb75f40df79fea1288ac19430600")
        stream = IOBuffer(raw_tx)
        tx = parse(stream)::Tx
        @test coinbase_height(tx) == nothing
    end
end
@testset "SegWit Transactions" begin
    raw = hex2bytes("0100000000010115e180dc28a2327e687facc33f10f2a20da717e5548406f7ae8b4c811072f85601000000000fffffff0100b4f505000000001976a9141d7cd6c75c2e86f4cbf98eaed221b30bd9a0b92888ac02483045022100df7b7e5cda14ddf91290e02ea10786e03eb11ee36ec02dd862fe9a326bbcb7fd02203f5b4496b667e6e281cc654a2da9e4f08660c620a1051337fa8965f727eb19190121038262a6c6cec93c2d3ecd6c6072efea86d02ff8e3328bbd0242b20af3425990ac00000000")
    @testset "Parse Segwit" begin
        tx = Bitcoin.parse(IOBuffer(raw), false)::Tx
        @test tx.version == 1
        @test tx.flag == 0x01
        hex1 = "3045022100df7b7e5cda14ddf91290e02ea10786e03eb11ee36ec02dd862fe9a326bbcb7fd02203f5b4496b667e6e281cc654a2da9e4f08660c620a1051337fa8965f727eb191901"
        hex2 = "038262a6c6cec93c2d3ecd6c6072efea86d02ff8e3328bbd0242b20af3425990ac"
        @test tx.tx_ins[1].witness.instructions == Script([hex2bytes(hex1), hex2bytes(hex2)]).instructions
    end
    @testset "Serialize" begin
        tx = Bitcoin.parse(IOBuffer(raw), false)::Tx
        @test Bitcoin.serialize(tx) == raw
    end
end


function tvdrk3!(system::LegSystem,times::CVTimer,n::Int64,splits::Vector{Int64},terms::Vector{Int64},uprox::Float64)
    # allocation for invariant/boundary value RK3 loop
    dW = zeros(2);
    W10 = zeros(2,length(system.branches.ID));
    W20 = zeros(2,length(system.branches.ID));
    W11 = zeros(2,length(system.branches.ID));
    W21 = zeros(2,length(system.branches.ID));
    W12 = zeros(2,length(system.branches.ID));
    W22 = zeros(2,length(system.branches.ID));
    lf = zeros(length(system.branches.ID));
    lb = zeros(length(system.branches.ID));
    ret1 = zeros(2);
    ret2 = zeros(2);
    ret3 = zeros(1);
    ret4 = zeros(1);

    # allocation for interior value RK3 loop
    dU = zeros((length(system.solverparams.colsint))*2);
    Q1 = zeros(length(system.solverparams.colsint),length(system.branches.ID));
    A1 = zeros(length(system.solverparams.colsint),length(system.branches.ID));
    Q2 = zeros(length(system.solverparams.colsint),length(system.branches.ID));
    A2 = zeros(length(system.solverparams.colsint),length(system.branches.ID));
    Acat = zeros(length(system.solverparams.colsint)+2);
    Qcat = zeros(length(system.solverparams.colsint)+2);

    # allocation for WENO3 spatial derivative subroutine
    ISA = zeros(3);
    ISQ = zeros(3);
    PA = Vector{Function}(length(system.solverparams.colsint));
    PQ = Vector{Function}(length(system.solverparams.colsint));
    Aint = Vector{Float64}(length(system.solverparams.colsint));
    Qint = Vector{Float64}(length(system.solverparams.colsint));
    wA = Vector{Float64}(3);
    wQ = Vector{Float64}(3);
    c = Vector{Float64}(3);
    a = Vector{Float64}(2);
    p = Vector{Float64}(2);
    x = Vector{Float64}(2);
    Al = Vector{Float64}(2);
    Ar = Vector{Float64}(2);
    Ql = Vector{Float64}(2);
    Qr = Vector{Float64}(2);
    Fl = Vector{Float64}(2);
    Fr = Vector{Float64}(2);
    Hl = Vector{Float64}(2);
    Hr = Vector{Float64}(2);
    Pi = Vector{Float64}(3);
    σ = Vector{Float64}(2);
    eigs = Vector{Float64}(2);
    abso = Vector{Float64}(2);
    J = zeros(2,2);
    Pret = zeros(1);
    s = zeros(1);

    # first RK step
    # println("First RK:")
    tic();
    for i = 1:length(system.branches.ID)
        # println("First step, artery $i, time step $n.")
        # scalings
        # println("Scalings:")
        As = ((system.solverparams.rho.*system.branches.c0[i][end].^2)./system.branches.beta[i][end]).^2;
        Qs = As.*system.branches.c0[i][end];
        xs = system.branches.lengthinmm[i].*LegHemodynamics.mmTom .- system.branches.k[i];
        ts = As.*xs./Qs;
        Acat .= system.branches.A[i][:,n+1]/As;
        Qcat .= system.branches.Q[i][:,n+1]/Qs;
        # println("Initial area: $(system.branches.A[i][:,n+1]./As)")
        # println("Initial flowrate: $(system.branches.Q[i][:,n+1]./Qs)")
        # invariant step
        # println("Invariant calculation:")
        invariants!(ret1,ret2,ret3,ret4,Acat,Qcat,
            system.branches.c0[i][end],system.branches.beta[i][end],system.solverparams.rho,
            system.solverparams.mu,system.solverparams.diffusioncoeff,Qs,As)
        W10[:,i] .= ret1;
        W20[:,i] .= ret2;
        lf[i] = ret3[1];
        lb[i] = ret4[1];
        # println("Forward invariant at time step $n, artery $i: $(W1n[i]) m/s")
        # println("Invariant update:")
        invariantodes!(dW,W10[:,i],W20[:,i],lf[i],lb[i],Qcat,Acat,
            system.solverparams.rho,system.solverparams.mu,system.solverparams.diffusioncoeff,
            system.branches.k[i]./xs,ts,xs,Qs,As);
        # println("Invariant assignment:")
        if i == 1 # aortic root
            system.branches.W1[i] = (W10[end,i] .+ system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2root = (W20[1,i] .+ system.solverparams.h./ts.*dW[2]).*Qs./As;
        elseif isempty(system.branches.children[i]) # terminals
            system.branches.W1end[i] = (W10[end,i] .+ system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] = (W20[1,i] .+ system.solverparams.h./ts.*dW[2]).*Qs./As;
        else # all others
            system.branches.W1[i] = (W10[end,i] .+ system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] = (W20[1,i] .+ system.solverparams.h./ts.*dW[2]).*Qs./As;
        end
        # primitive step
        # println("WENO3:")
        weno3!(dU,system.branches.k[i]./xs,Acat,
            Qcat,system.branches.beta[i][end],
            system.solverparams.rho,system.solverparams.mu,
            system.solverparams.diffusioncoeff,ts,xs,Qs,As,ISA,ISQ,PA,PQ,wA,wQ,
            c,a,p,x,Al,Ar,Ql,Qr,Hl,Hr,Fl,Fr,Pi,σ,eigs,abso,J,Aint,Qint,Pret,s)
        # println("Intermediate step:")
        A1[:,i] .= (system.branches.A[i][2:end-1,n+1]./As .+
            system.solverparams.h./ts.*dU[system.solverparams.acols[1:end-2]]).*As;
        Q1[:,i] .= (system.branches.Q[i][2:end-1,n+1]./Qs .+
            system.solverparams.h./ts.*dU[system.solverparams.qcols[1:end.-2].-2]).*Qs;
    end
    times.tfd += toq();

    # first set of boundary values
    # println("Split solve:")
    tic();
    for i = 1:length(splits)
        # println("Solving interior boundary, parent ID: $(splits[i])")
        children = system.branches.children[splits[i]];
        Q = zeros(length(children)+1);
        A = zeros(length(children)+1);
        Q[1] = system.branches.Q[splits[i]][system.solverparams.JL,n+1];
        A[1] = system.branches.A[splits[i]][system.solverparams.JL,n+1];
        for j = 1:length(children)
            Q[j+1] = system.branches.Q[children[j]][1,n+1];
            A[j+1] = system.branches.A[children[j]][1,n+1];
        end
        LegHemodynamics.interiorbcs!(system,n,splits[i],Q,A,children);
    end
    times.ts += toq();
    # println("0D-1D coupling:")
    tic();
    LegHemodynamics.coupling!(system,n,system.solverparams.h,terms,uprox);
    times.tc += toq();

    # second RK step
    # println("Second RK:")
    tic()
    for i = 1:length(system.branches.ID)
        # println("Second step, artery $i, time step $n.")
        Acat[1] = system.branches.A[i][1,n+2];
        Acat[2:end-1] .= A1[:,i];
        Acat[end] = system.branches.A[i][end,n+2];
        Qcat[1] = system.branches.Q[i][1,n+2];
        Qcat[2:end-1] .= Q1[:,i];
        Qcat[end] = system.branches.Q[i][end,n+2];
        # scalings
        As = ((system.solverparams.rho.*system.branches.c0[i][end].^2)./system.branches.beta[i][end]).^2;
        Qs = As.*system.branches.c0[i][end];
        xs = system.branches.lengthinmm[i].*LegHemodynamics.mmTom .- system.branches.k[i];
        ts = As.*xs./Qs;
        Acat /= As;
        Qcat /= Qs;
        # println("Area after first step: $(Acat./As)")
        # println("Flowrate after first step: $(Qcat./Qs)")
        # invariant step
        invariants!(ret1,ret2,ret3,ret4,Acat,Qcat,
            system.branches.c0[i][end],system.branches.beta[i][end],system.solverparams.rho,
            system.solverparams.mu,system.solverparams.diffusioncoeff,Qs,As)
        W11[:,i] .= ret1;
        W21[:,i] .= ret2;
        lf[i] = ret3[1];
        lb[i] = ret4[1];
        invariantodes!(dW,W11[:,i],W21[:,i],lf[i],lb[i],Qcat,Acat,
            system.solverparams.rho,system.solverparams.mu,system.solverparams.diffusioncoeff,
            system.branches.k[i]./xs,ts,xs,Qs,As);
        if i == 1 # aortic root
            system.branches.W1[i] = (0.75.*W10[end,i] .+ 0.25.*W11[end,i] .+
                0.25.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2root = (0.75.*W20[1,i] .+ 0.25.*W21[1,i] .+
                0.25.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        elseif isempty(system.branches.children[i]) # terminals
            system.branches.W1end[i] = (0.75.*W10[end,i] .+ 0.25.*W11[end,i] .+
                0.25.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] = (0.75.*W20[1,i] .+ 0.25.*W21[1,i] .+
                0.25.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        else # all others
            system.branches.W1[i] = (0.75.*W10[end,i] .+ 0.25.*W11[end,i] .+
                0.25.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] = (0.75.*W20[1,i] .+ 0.25.*W21[1,i] .+
                0.25.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        end
        # primitive step
        weno3!(dU,system.branches.k[i]./xs,Acat,
            Qcat,system.branches.beta[i][end],
            system.solverparams.rho,system.solverparams.mu,
            system.solverparams.diffusioncoeff,ts,xs,Qs,As,ISA,ISQ,PA,PQ,wA,wQ,
            c,a,p,x,Al,Ar,Ql,Qr,Hl,Hr,Fl,Fr,Pi,σ,eigs,abso,J,Aint,Qint,Pret,s)
        A2[:,i] .= (0.75.*system.branches.A[i][2:end-1,n+1]./As .+ 0.25.*A1[:,i]./As .+
            0.25.*system.solverparams.h./ts.*dU[system.solverparams.acols[1:end-2]]).*As;
        Q2[:,i] .= (0.75.*system.branches.Q[i][2:end-1,n+1]./Qs .+ 0.25.*Q1[:,i]./Qs .+
            0.25.*system.solverparams.h./ts.*dU[system.solverparams.qcols[1:end.-2].-2]).*Qs;
    end
    times.tfd += toq();

    # second set of boundary values
    tic();
    for i = 1:length(splits)
        children = system.branches.children[splits[i]];
        Q = zeros(length(children)+1);
        A = zeros(length(children)+1);
        Q[1] = system.branches.Q[splits[i]][system.solverparams.JL,n+2];
        A[1] = system.branches.A[splits[i]][system.solverparams.JL,n+2];
        for j = 1:length(children)
            Q[j+1] = system.branches.Q[children[j]][1,n+2];
            A[j+1] = system.branches.A[children[j]][1,n+2];
        end
        LegHemodynamics.interiorbcs!(system,n,splits[i],Q,A,children);
    end
    times.ts += toq();
    tic()
    LegHemodynamics.coupling!(system,n,0.5*system.solverparams.h,terms,uprox);
    times.tc += toq();

    # third RK step
    # println("Third RK:")
    tic();
    for i = 1:length(system.branches.ID)
        # println("Third step, artery $i, time step $n.")
        Acat[1] = system.branches.A[i][1,n+2];
        Acat[2:end-1] .= A2[:,i];
        Acat[end] = system.branches.A[i][end,n+2];
        Qcat[1] = system.branches.Q[i][1,n+2];
        Qcat[2:end-1] .= Q2[:,i];
        Qcat[end] = system.branches.Q[i][end,n+2];
        # scalings
        As = ((system.solverparams.rho.*system.branches.c0[i][end].^2)./system.branches.beta[i][end]).^2;
        Qs = As.*system.branches.c0[i][end];
        xs = system.branches.lengthinmm[i].*LegHemodynamics.mmTom .- system.branches.k[i];
        ts = As.*xs./Qs;
        Acat /= As;
        Qcat /= Qs;
        # println("Area after second step: $(Acat./As)")
        # println("Flowrate after second step: $(Qcat./Qs)")
        # invariant step
        invariants!(ret1,ret2,ret3,ret4,Acat,Qcat,
            system.branches.c0[i][end],system.branches.beta[i][end],system.solverparams.rho,
            system.solverparams.mu,system.solverparams.diffusioncoeff,Qs,As)
        W12[:,i] .= ret1;
        W22[:,i] .= ret2;
        lf[i] = ret3[1];
        lb[i] = ret4[1];
        invariantodes!(dW,W12[:,i],W22[:,i],lf[i],lb[i],Qcat,Acat,
            system.solverparams.rho,system.solverparams.mu,system.solverparams.diffusioncoeff,
            system.branches.k[i]./xs,ts,xs,Qs,As);
        if i == 1 # aortic root
            system.branches.W1[i] .= (1/3.*W10[end,i] .+ 2/3.*W12[end,i] .+
                2/3.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2root = (1/3.*W20[1,i] .+ 2/3.*W22[1,i] .+
                2/3.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        elseif isempty(system.branches.children[i]) # terminals
            system.branches.W1end[i] .= (1/3.*W10[end,i] .+ 2/3.*W12[end,i] .+
                2/3.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] .= (1/3.*W20[1,i] .+ 2/3.*W22[1,i] .+
                2/3.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        else # all others
            system.branches.W1[i] .= (1/3.*W10[end,i] .+ 2/3.*W12[end,i] .+
                2/3.*system.solverparams.h./ts.*dW[1]).*Qs./As;
            system.branches.W2[i] .= (1/3.*W20[1,i] .+ 2/3.*W22[1,i] .+
                2/3.*system.solverparams.h./ts.*dW[2]).*Qs./As;
        end
        # primitive step
        weno3!(dU,system.branches.k[i]./xs,Acat,
            Qcat,system.branches.beta[i][end],
            system.solverparams.rho,system.solverparams.mu,
            system.solverparams.diffusioncoeff,ts,xs,Qs,As,ISA,ISQ,PA,PQ,wA,wQ,
            c,a,p,x,Al,Ar,Ql,Qr,Hl,Hr,Fl,Fr,Pi,σ,eigs,abso,J,Aint,Qint,Pret,s)
        system.branches.A[i][2:end-1,n+2] .= (1/3.*system.branches.A[i][2:end-1,n+1]./As .+ 2/3.*A2[:,i]./As .+
            2/3.*system.solverparams.h./ts.*dU[system.solverparams.acols[1:end-2]]).*As;
        system.branches.Q[i][2:end-1,n+2] .= (1/3.*system.branches.Q[i][2:end-1,n+1]./Qs .+ 2/3.*Q2[:,i]./Qs .+
            2/3.*system.solverparams.h./ts.*dU[system.solverparams.qcols[1:end.-2].-2]).*Qs;
    end
    times.tfd += toq();

    # third set of boundary values
    # println("Third interior boundaries:")
    tic();
    for i = 1:length(splits)
        # println("Interior boundary allocation:")
        children = system.branches.children[splits[i]];
        Q = zeros(length(children)+1);
        A = zeros(length(children)+1);
        Q[1] = system.branches.Q[splits[i]][system.solverparams.JL,n+2];
        A[1] = system.branches.A[splits[i]][system.solverparams.JL,n+2];
        for j = 1:length(children)
            Q[j+1] = system.branches.Q[children[j]][1,n+2];
            A[j+1] = system.branches.A[children[j]][1,n+2];
        end
        # println("Interior boundary solution:")
        LegHemodynamics.interiorbcs!(system,n,splits[i],Q,A,children);
    end
    times.ts += toq();
    # println("Third coupling:")
    tic();
    LegHemodynamics.coupling!(system,n,system.solverparams.h,terms,uprox);
    times.tc += toq();

end


@testset "cube" begin

    x = reshape(1:24, 2, 3, 4)
    @test is_transformable(x)

    @testset "apply" begin

        @testset "OneToOne" begin
            T = FakeOneToOneTransform()
            @test FeatureTransforms.apply(x, T) == ones(2, 3, 4)
            @test FeatureTransforms.apply(x, T; inds=[1, 2]) == ones(2)
            @test FeatureTransforms.apply(x, T; dims=2, inds=[2, 3]) == ones(2, 2, 4)
        end

        @testset "OneToMany" begin
            T = FakeOneToManyTransform()
            @test FeatureTransforms.apply(x, T) == ones(2, 6, 4)
            @test FeatureTransforms.apply(x, T; inds=[1, 2]) == ones(2, 2)
            @test FeatureTransforms.apply(x, T; dims=2, inds=[2, 3]) == ones(2, 4, 4)
        end

        @testset "ManyToOne" begin
            T = FakeManyToOneTransform()
            @test FeatureTransforms.apply(x, T; dims=1) == ones(3, 4)
            @test FeatureTransforms.apply(x, T; dims=2, inds=[1, 2]) == ones(2, 4)
            @test FeatureTransforms.apply(x, T; dims=3, inds=[1, 3]) == ones(2, 3)
        end

        @testset "ManyToMany" begin
            T = FakeManyToManyTransform()
            @test FeatureTransforms.apply(x, T) == ones(2, 6, 4)
            @test FeatureTransforms.apply(x, T; inds=[1, 2]) == ones(2, 2)
            @test FeatureTransforms.apply(x, T; dims=2, inds=[2, 3]) == ones(2, 4, 4)
        end

    end

    @testset "apply!" begin

        T = FakeOneToOneTransform()

        _x = copy(x)
        FeatureTransforms.apply!(_x, T)
        @test _x == ones(2, 3, 4)

        # https://github.com/invenia/FeatureTransforms.jl/issues/68
        _x = copy(x)
        @test_broken FeatureTransforms.apply!(_x, T; dims=2, inds=[1, 2])
        @test_broken _x == cat(
            [1 1 5; 1 1 6], [1 1 11; 1 1 12], [1 1 17; 1 1 18], [1 1 23; 1 1 24]; dims=3
        )
    end

    @testset "apply_append" begin

        @testset "OneToOne" begin

            T = FakeOneToOneTransform()

            @test FeatureTransforms.apply_append(x, T; append_dim=1) == vcat(x, ones(2, 3, 4))
            @test FeatureTransforms.apply_append(x, T; append_dim=2) == hcat(x, ones(2, 3, 4))
            @test FeatureTransforms.apply_append(x, T; append_dim=3) == cat(x, ones(2, 3, 4), dims=3)

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=1, inds=[1], append_dim=1),
                vcat(x, ones(1, 3, 4))
            )

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=2, inds=[1, 2], append_dim=2),
                hcat(x, ones(2, 2, 4))
            )
        end

        @testset "OneToMany" begin

            T = FakeOneToManyTransform()

            @test_throws DimensionMismatch FeatureTransforms.apply_append(x, T; append_dim=1)
            @test_throws DimensionMismatch FeatureTransforms.apply_append(x, T; append_dim=3)

            @test isequal(
                FeatureTransforms.apply_append(x, T; append_dim=2),
                hcat(x, ones(2, 6, 4))
            )

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=2, inds=[1, 2], append_dim=2),
                hcat(x, ones(2, 4, 4))
            )
        end

        @testset "ManyToOne" begin

            T = FakeManyToOneTransform()

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=1, append_dim=1),
                vcat(x, ones(1, 3, 4))
            )

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=2, append_dim=2),
                hcat(x, ones(2, 1, 4))
            )

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=3, append_dim=3),
                cat(x, ones(2, 3, 1); dims=3)
            )
        end

        @testset "ManyToMany" begin

            T = FakeManyToManyTransform()

            @test_throws DimensionMismatch FeatureTransforms.apply_append(x, T; append_dim=1)
            @test_throws DimensionMismatch FeatureTransforms.apply_append(x, T; append_dim=3)

            @test isequal(
                FeatureTransforms.apply_append(x, T; append_dim=2),
                hcat(x, ones(2, 6, 4))
            )

            @test isequal(
                FeatureTransforms.apply_append(x, T; dims=2, inds=[1, 2], append_dim=2),
                hcat(x, ones(2, 4, 4))
            )
        end

    end

end


module Topologies
import ..BrickMesh
import MPI
using DocStringExtensions

export AbstractTopology, BrickTopology, StackedBrickTopology,
    CubedShellTopology, StackedCubedSphereTopology, isstacked

export grid1d, SingleExponentialStretching, InteriorStretching

"""
    AbstractTopology{dim}

Represents the connectivity of individual elements, with local dimension `dim`.
"""
abstract type AbstractTopology{dim} end

"""
    BoxElementTopology{dim, T} <: AbstractTopology{dim}

The local topology of a larger MPI-distributed topology, represented by
`dim`-dimensional box elements.

This contains the necessary information for the connectivity elements of the
elements on the local process, along with "ghost" elements from neighbouring
processes.

# Fields

$(DocStringExtensions.FIELDS)
"""
struct BoxElementTopology{dim, T} <: AbstractTopology{dim}

  """
  MPI communicator for communicating with neighbouring processes.
  """
  mpicomm::MPI.Comm

  """
  Range of element indices
  """
  elems::UnitRange{Int64}

  """
  Range of real (aka nonghost) element indices
  """
  realelems::UnitRange{Int64}

  """
  Range of ghost element indices
  """
  ghostelems::UnitRange{Int64}

  """
  Ghost element to face is received; `ghostfaces[f,ge] == true` if face `f` of
  ghost element `ge` is received.
  """
  ghostfaces::BitArray{2}

  """
  Array of send element indices
  """
  sendelems::Array{Int64, 1}

  """
  Send element to face is sent; `sendfaces[f,se] == true` if face `f` of send
  element `se` is sent.
  """
  sendfaces::BitArray{2}

  """
  Element to vertex coordinates; `elemtocoord[d,i,e]` is the `d`th coordinate of
  corner `i` of element `e`

  !!! note
      currently coordinates always are of size 3 for `(x1, x2, x3)`
  """
  elemtocoord::Array{T, 3}

  """
  Element to neighboring element; `elemtoelem[f,e]` is the number of the element
  neighboring element `e` across face `f`.  If there is no neighboring element
  then `elemtoelem[f,e] == e`.
  """
  elemtoelem::Array{Int64, 2}

  """
  Element to neighboring element face; `elemtoface[f,e]` is the face number of
  the element neighboring element `e` across face `f`.  If there is no
  neighboring element then `elemtoface[f,e] == f`."
  """
  elemtoface::Array{Int64, 2}

  """
  element to neighboring element order; `elemtoordr[f,e]` is the ordering number
  of the element neighboring element `e` across face `f`.  If there is no
  neighboring element then `elemtoordr[f,e] == 1`.
  """
  elemtoordr::Array{Int64, 2}

  """
  Element to boundary number; `elemtobndy[f,e]` is the boundary number of face
  `f` of element `e`.  If there is a neighboring element then `elemtobndy[f,e]
  == 0`.
  """
  elemtobndy::Array{Int64, 2}

  """
  List of the MPI ranks for the neighboring processes
  """
  nabrtorank::Array{Int64, 1}

  """
  Range in ghost elements to receive for each neighbor
  """
  nabrtorecv::Array{UnitRange{Int64}, 1}

  """
  Range in `sendelems` to send for each neighbor
  """
  nabrtosend::Array{UnitRange{Int64}, 1}

  """
  original order in partitioning
  """
  origsendorder::Array{Int64,1}

  """
  boolean for whether or not this topology has a boundary
  """
  hasboundary::Bool
end

"""
    hasboundary(topology::AbstractTopology)

query function to check whether a topology has a boundary (i.e., not fully
periodic)
"""
hasboundary(topology::AbstractTopology) = topology.hasboundary

if VERSION >= v"1.2-"
  isstacked(::T) where {T<:AbstractTopology} = hasfield(T, :stacksize)
else
  isstacked(::T) where {T<:AbstractTopology} = Base.fieldindex(T, :stacksize,
                                                               false) > 0
end

"""
    BrickTopology{dim, T} <: AbstractTopology{dim}

A simple grid-based topology. This is a convenience wrapper around
[`BoxElementTopology`](@ref).
"""
struct BrickTopology{dim, T} <: AbstractTopology{dim}
  topology::BoxElementTopology{dim, T}
end
Base.getproperty(a::BrickTopology, p::Symbol) =
getproperty(getfield(a, :topology), p)

"""
    CubedShellTopology{T} <: AbstractTopology{2}

A cube-shell topology. This is a convenience wrapper around
[`BoxElementTopology`](@ref).
"""
struct CubedShellTopology{T} <: AbstractTopology{2}
  topology::BoxElementTopology{2, T}
end
Base.getproperty(a::CubedShellTopology, p::Symbol) =
getproperty(getfield(a, :topology), p)

"""
    StackedBrickTopology{dim, T} <: AbstractTopology{dim}

A simple grid-based topology, where all elements on the trailing dimension are
stacked to be contiguous. This is a convenience wrapper around
[`BoxElementTopology`](@ref).
"""
struct StackedBrickTopology{dim, T} <: AbstractTopology{dim}
  topology::BoxElementTopology{dim, T}
  stacksize::Int64
end
function Base.getproperty(a::StackedBrickTopology, p::Symbol)
  return p == :stacksize ? getfield(a, p) :
                           getproperty(getfield(a, :topology), p)
end

"""
    StackedCubedSphereTopology{3, T} <: AbstractTopology{3}

A cube-sphere topology. All elements on the same "vertical" dimension are
stacked to be contiguous. This is a convenience wrapper around
[`BoxElementTopology`](@ref).
"""
struct StackedCubedSphereTopology{T} <: AbstractTopology{3}
  topology::BoxElementTopology{3, T}
  stacksize::Int64
end
function Base.getproperty(a::StackedCubedSphereTopology, p::Symbol)
  return p == :stacksize ? getfield(a, p) :
                           getproperty(getfield(a, :topology), p)
end

""" A wrapper for the BrickTopology """
BrickTopology(mpicomm, Nelems::NTuple{N, Integer}; kw...) where N =
BrickTopology(mpicomm, map(Ne->0:Ne, Nelems); kw...)

"""
    BrickTopology{dim, T}(mpicomm, elemrange; boundary, periodicity)

Generate a brick mesh topology with coordinates given by the tuple `elemrange`
and the periodic dimensions given by the `periodicity` tuple.

The elements of the brick are partitioned equally across the MPI ranks based
on a space-filling curve.

By default boundary faces will be marked with a one and other faces with a
zero.  Specific boundary numbers can also be passed for each face of the brick
in `boundary`.  This will mark the nonperiodic brick faces with the given
boundary number.

# Examples

We can build a 3 by 2 element two-dimensional mesh that is periodic in the
\$x2\$-direction with
```jldoctest brickmesh

using CLIMA.Topologies
using MPI
MPI.Init()
topology = BrickTopology(MPI.COMM_SELF, (2:5,4:6);
                         periodicity=(false,true),
                         boundary=((1,2),(3,4)))
```
This returns the mesh structure for

             x2

              ^
              |
             6-  +-----+-----+-----+
              |  |     |     |     |
              |  |  3  |  4  |  5  |
              |  |     |     |     |
             5-  +-----+-----+-----+
              |  |     |     |     |
              |  |  1  |  2  |  6  |
              |  |     |     |     |
             4-  +-----+-----+-----+
              |
              +--|-----|-----|-----|--> x1
                 2     3     4     5

For example, the (dimension by number of corners by number of elements) array
`elemtocoord` gives the coordinates of the corners of each element.
```jldoctest brickmesh
julia> topology.elemtocoord
2×4×6 Array{Int64,3}:
[:, :, 1] =
 2  3  2  3
 4  4  5  5

[:, :, 2] =
 3  4  3  4
 4  4  5  5

[:, :, 3] =
 2  3  2  3
 5  5  6  6

[:, :, 4] =
 3  4  3  4
 5  5  6  6

[:, :, 5] =
 4  5  4  5
 5  5  6  6

[:, :, 6] =
 4  5  4  5
 4  4  5  5
```
Note that the corners are listed in Cartesian order.

The (number of faces by number of elements) array `elemtobndy` gives the
boundary number for each face of each element.  A zero will be given for
connected faces.
```jldoctest brickmesh
julia> topology.elemtobndy
4×6 Array{Int64,2}:
 1  0  1  0  0  0
 0  0  0  0  2  2
 0  0  0  0  0  0
 0  0  0  0  0  0
```
Note that the faces are listed in Cartesian order.

"""
function BrickTopology(mpicomm, elemrange;
                       boundary=ntuple(j->(1,1), length(elemrange)),
                       periodicity=ntuple(j->false, length(elemrange)),
                       connectivity=:face, ghostsize=1)

  if boundary isa Matrix
    boundary = tuple(mapslices(x -> tuple(x...), boundary, dims=1)...)
  end

  # We cannot handle anything else right now...
  @assert connectivity == :face
  @assert ghostsize == 1

  mpirank = MPI.Comm_rank(mpicomm)
  mpisize = MPI.Comm_size(mpicomm)
  topology = BrickMesh.brickmesh(elemrange, periodicity, part=mpirank+1,
                              numparts=mpisize, boundary=boundary)
  topology = BrickMesh.partition(mpicomm, topology...)
  origsendorder = topology[5]
  topology = BrickMesh.connectmesh(mpicomm, topology[1:4]...)

  dim = length(elemrange)
  T = eltype(topology.elemtocoord)
  return BrickTopology{dim, T}(BoxElementTopology{dim, T}(
              mpicomm, topology.elems, topology.realelems,
              topology.ghostelems, topology.ghostfaces, topology.sendelems,
              topology.sendfaces, topology.elemtocoord, topology.elemtoelem,
              topology.elemtoface, topology.elemtoordr, topology.elemtobndy,
              topology.nabrtorank, topology.nabrtorecv, topology.nabrtosend,
              origsendorder, !minimum(periodicity)))
end

""" A wrapper for the StackedBrickTopology """
StackedBrickTopology(mpicomm, Nelems::NTuple{N, Integer}; kw...) where N =
StackedBrickTopology(mpicomm, map(Ne->0:Ne, Nelems); kw...)

"""
    StackedBrickTopology{dim, T}(mpicomm, elemrange; boundary, periodicity)

Generate a stacked brick mesh topology with coordinates given by the tuple
`elemrange` and the periodic dimensions given by the `periodicity` tuple.

The elements are stacked such that the elements associated with range
`elemrange[dim]` are contiguous in the element ordering.

The elements of the brick are partitioned equally across the MPI ranks based
on a space-filling curve.  Further, stacks are not split at MPI boundaries.

By default boundary faces will be marked with a one and other faces with a
zero.  Specific boundary numbers can also be passed for each face of the brick
in `boundary`.  This will mark the nonperiodic brick faces with the given
boundary number.

# Examples

We can build a 3 by 2 element two-dimensional mesh that is periodic in the
\$x2\$-direction with
```jldoctest brickmesh

using CLIMA.Topologies
using MPI
MPI.Init()
topology = StackedBrickTopology(MPI.COMM_SELF, (2:5,4:6);
                                periodicity=(false,true),
                                boundary=((1,2),(3,4)))
```
This returns the mesh structure stacked in the \$x2\$-direction for

             x2

              ^
              |
             6-  +-----+-----+-----+
              |  |     |     |     |
              |  |  2  |  4  |  6  |
              |  |     |     |     |
             5-  +-----+-----+-----+
              |  |     |     |     |
              |  |  1  |  3  |  5  |
              |  |     |     |     |
             4-  +-----+-----+-----+
              |
              +--|-----|-----|-----|--> x1
                 2     3     4     5

For example, the (dimension by number of corners by number of elements) array
`elemtocoord` gives the coordinates of the corners of each element.
```jldoctest brickmesh
julia> topology.elemtocoord
2×4×6 Array{Int64,3}:
[:, :, 1] =
 2  3  2  3
 4  4  5  5

[:, :, 2] =
 2  3  2  3
 5  5  6  6

[:, :, 3] =
 3  4  3  4
 4  4  5  5

[:, :, 4] =
 3  4  3  4
 5  5  6  6

[:, :, 5] =
 4  5  4  5
 4  4  5  5

[:, :, 6] =
 4  5  4  5
 5  5  6  6
```
Note that the corners are listed in Cartesian order.

The (number of faces by number of elements) array `elemtobndy` gives the
boundary number for each face of each element.  A zero will be given for
connected faces.
```jldoctest brickmesh
julia> topology.elemtobndy
4×6 Array{Int64,2}:
 1  0  1  0  0  0
 0  0  0  0  2  2
 0  0  0  0  0  0
 0  0  0  0  0  0
```
Note that the faces are listed in Cartesian order.
"""
function StackedBrickTopology(mpicomm, elemrange;
                       boundary=ntuple(j->(1,1), length(elemrange)),
                       periodicity=ntuple(j->false, length(elemrange)),
                       connectivity=:face, ghostsize=1)

  if boundary isa Matrix
    boundary = tuple(mapslices(x -> tuple(x...), boundary, dims=1)...)
  end

  dim = length(elemrange)

  dim <= 1 && error("Stacked brick topology works for 2D and 3D")

  # Build the base topology
  basetopo = BrickTopology(mpicomm, elemrange[1:dim-1];
                     boundary=boundary[1:dim-1],
                     periodicity=periodicity[1:dim-1],
                     connectivity=connectivity,
                     ghostsize=ghostsize)

  # Use the base topology to build the stacked topology
  stack = elemrange[dim]
  stacksize = length(stack) - 1

  nvert = 2^dim
  nface = 2dim

  nreal = length(basetopo.realelems)*stacksize
  nghost = length(basetopo.ghostelems)*stacksize

  elems=1:(nreal+nghost)
  realelems=1:nreal
  ghostelems=nreal.+(1:nghost)

  sendelems = similar(basetopo.sendelems,
                      length(basetopo.sendelems)*stacksize)
  for i=1:length(basetopo.sendelems), j=1:stacksize
    sendelems[stacksize*(i-1) + j] = stacksize*(basetopo.sendelems[i]-1) + j
  end

  ghostfaces = similar(basetopo.ghostfaces, nface, length(ghostelems))
  ghostfaces .= false

  for i=1:length(basetopo.ghostelems), j=1:stacksize
    e = stacksize*(i-1) + j
    for f = 1:2(dim-1)
      ghostfaces[f, e] = basetopo.ghostfaces[f, i]
    end
  end

  sendfaces = similar(basetopo.sendfaces, nface, length(sendelems))
  sendfaces .= false

  for i=1:length(basetopo.sendelems), j=1:stacksize
    e = stacksize*(i-1) + j
    for f = 1:2(dim-1)
      sendfaces[f, e] = basetopo.sendfaces[f, i]
    end
  end

  elemtocoord = similar(basetopo.elemtocoord, dim, nvert, length(elems))

  for i=1:length(basetopo.elems), j=1:stacksize
    e = stacksize*(i-1) + j

    for v = 1:2^(dim-1)
      for d = 1:(dim-1)
        elemtocoord[d, v, e] = basetopo.elemtocoord[d, v, i]
        elemtocoord[d, 2^(dim-1) + v, e] = basetopo.elemtocoord[d, v, i]
      end

      elemtocoord[dim, v, e] = stack[j]
      elemtocoord[dim, 2^(dim-1) + v, e] = stack[j+1]
    end
  end

  elemtoelem = similar(basetopo.elemtoelem, nface, length(elems))
  elemtoface = similar(basetopo.elemtoface, nface, length(elems))
  elemtoordr = similar(basetopo.elemtoordr, nface, length(elems))
  elemtobndy = similar(basetopo.elemtobndy, nface, length(elems))

  for e=1:length(basetopo.elems)*stacksize, f=1:nface
    elemtoelem[f, e] = e
    elemtoface[f, e] = f
    elemtoordr[f, e] = 1
    elemtobndy[f, e] = 0
  end

  for i=1:length(basetopo.realelems), j=1:stacksize
    e1 = stacksize*(i-1) + j

    for f = 1:2(dim-1)
      e2 = stacksize*(basetopo.elemtoelem[f, i]-1) + j

      elemtoelem[f, e1] = e2
      elemtoface[f, e1] = basetopo.elemtoface[f, i]

      # We assume a simple orientation right now
      @assert basetopo.elemtoordr[f, i] == 1
      elemtoordr[f, e1] = basetopo.elemtoordr[f, i]
    end

    et = stacksize*(i-1) + j + 1
    eb = stacksize*(i-1) + j - 1
    ft = 2(dim-1) + 1
    fb = 2(dim-1) + 2
    ot = 1
    ob = 1

    if j == stacksize
      et = periodicity[dim] ? stacksize*(i-1) + 1 : e1
      ft = periodicity[dim] ? ft : 2(dim-1) + 2
    end
    if j == 1
      eb = periodicity[dim] ? stacksize*(i-1) + stacksize : e1
      fb = periodicity[dim] ? fb : 2(dim-1) + 1
    end

    elemtoelem[2(dim-1)+1, e1] = eb
    elemtoelem[2(dim-1)+2, e1] = et
    elemtoface[2(dim-1)+1, e1] = fb
    elemtoface[2(dim-1)+2, e1] = ft
    elemtoordr[2(dim-1)+1, e1] = ob
    elemtoordr[2(dim-1)+2, e1] = ot
  end

  for i=1:length(basetopo.elems), j=1:stacksize
    e1 = stacksize*(i-1) + j

    for f = 1:2(dim-1)
      elemtobndy[f, e1] = basetopo.elemtobndy[f, i]
    end

    bt = bb = 0

    if j == stacksize
      bt = periodicity[dim] ? bt : boundary[dim][2]
    end
    if j == 1
      bb = periodicity[dim] ? bb : boundary[dim][1]
    end

    elemtobndy[2(dim-1)+1, e1] = bb
    elemtobndy[2(dim-1)+2, e1] = bt
  end

  nabrtorank = basetopo.nabrtorank
  nabrtorecv =
    UnitRange{Int}[UnitRange(stacksize*(first(basetopo.nabrtorecv[n])-1)+1,
                             stacksize*last(basetopo.nabrtorecv[n]))
                   for n = 1:length(nabrtorank)]
  nabrtosend =
    UnitRange{Int}[UnitRange(stacksize*(first(basetopo.nabrtosend[n])-1)+1,
                             stacksize*last(basetopo.nabrtosend[n]))
                   for n = 1:length(nabrtorank)]

  T = eltype(basetopo.elemtocoord)

  StackedBrickTopology{dim, T}(
    BoxElementTopology{dim, T}(
      mpicomm, elems, realelems, ghostelems, ghostfaces, sendelems, sendfaces,
      elemtocoord, elemtoelem, elemtoface, elemtoordr, elemtobndy,
      nabrtorank, nabrtorecv, nabrtosend, basetopo.origsendorder, !minimum(periodicity)),
    stacksize)
end

"""
    CubedShellTopology(mpicomm, Nelem, T) <: AbstractTopology{dim}

Generate a cubed shell mesh with the number of elements along each dimension of
the cubes being `Nelem`. This topology actual creates a cube mesh, and the
warping should be done after the grid is created using the `cubedshellwarp`
function. The coordinates of the points will be of type `T`.

The elements of the shell are partitioned equally across the MPI ranks based
on a space-filling curve.

Note that this topology is logically 2-D but embedded in a 3-D space

# Examples

We can build a cubed shell mesh with 10 elements on each cube, total elements is
`10 * 10 * 6 = 600`, with
```jldoctest brickmesh
using CLIMA.Topologies
using MPI
MPI.Init()
topology = CubedShellTopology(MPI.COMM_SELF, 10, Float64)

# Typically the warping would be done after the grid is created, but the cell
# corners could be warped with...

# Shell radius = 1
x1, x2, x3 = ntuple(j->topology.elemtocoord[j, :, :], 3)
for n = 1:length(x1)
   x1[n], x2[n], x3[n] = Topologies.cubedshellwarp(x1[n], x2[n], x3[n])
end

# Shell radius = 10
x1, x2, x3 = ntuple(j->topology.elemtocoord[j, :, :], 3)
for n = 1:length(x1)
  x1[n], x2[n], x3[n] = Topologies.cubedshellwarp(x1[n], x2[n], x3[n], 10)
end
```
"""
function CubedShellTopology(mpicomm, Neside, T; connectivity=:face,
                            ghostsize=1)

  # We cannot handle anything else right now...
  @assert connectivity == :face
  @assert ghostsize == 1

  mpirank = MPI.Comm_rank(mpicomm)
  mpisize = MPI.Comm_size(mpicomm)

  topology = cubedshellmesh(Neside, part=mpirank+1, numparts=mpisize)

  topology = BrickMesh.partition(mpicomm, topology...)#topology(1), topology(2), topology(3), topology(4) ) #topology...)
  origsendorder = topology[5]
  dim, nvert = 3, 4
  elemtovert = topology[1]
  nelem = size(elemtovert, 2)
  elemtocoord = Array{T}(undef, dim, nvert, nelem)
  ind2vert = CartesianIndices((Neside+1, Neside+1, Neside+1))
  for e in 1:nelem
    for n = 1:nvert
      v = elemtovert[n, e]
      i, j, k = Tuple(ind2vert[v])
      elemtocoord[:, n, e] = (2*[i-1, j-1, k-1] .- Neside) / Neside
    end
  end

  topology = BrickMesh.connectmesh(mpicomm, topology[1], elemtocoord, topology[3],
                                topology[4]; dim = 2)

  CubedShellTopology{T}(
    BoxElementTopology{2, T}(
      mpicomm, topology.elems, topology.realelems,
      topology.ghostelems, topology.ghostfaces, topology.sendelems,
      topology.sendfaces, topology.elemtocoord, topology.elemtoelem,
      topology.elemtoface, topology.elemtoordr, topology.elemtobndy,
      topology.nabrtorank, topology.nabrtorecv, topology.nabrtosend, origsendorder, false))
end

"""
    cubedshellmesh(T, Ne; part=1, numparts=1)

Generate a cubed mesh with each of the "cubes" has an `Ne X Ne` grid of
elements.

The mesh can optionally be partitioned into `numparts` and this returns
partition `part`.  This is a simple Cartesian partition and further partitioning
(e.g, based on a space-filling curve) should be done before the mesh is used for
computation.

This mesh returns the cubed spehere in a flatten fashion for the vertex values,
and a remapping is needed to embed the mesh in a 3-D space.

The mesh structures for the cubes is as follows:

```
x2
   ^
   |
4Ne-           +-------+
   |           |       |
   |           |   6   |
   |           |       |
3Ne-           +-------+
   |           |       |
   |           |   5   |
   |           |       |
2Ne-           +-------+
   |           |       |
   |           |   4   |
   |           |       |
 Ne-   +-------+-------+-------+
   |   |       |       |       |
   |   |   1   |   2   |   3   |
   |   |       |       |       |
  0-   +-------+-------+-------+
   |
   +---|-------|-------|------|-> x1
       0      Ne      2Ne    3Ne
```

"""
function cubedshellmesh(Ne; part=1, numparts=1)
  dim = 2
  @assert 1 <= part <= numparts

  globalnelems = 6 * Ne^2

  # How many vertices and faces per element
  nvert = 2^dim # 4
  nface = 2dim  # 4

  # linearly partition to figure out which elements we own
  elemlocal = BrickMesh.linearpartition(prod(globalnelems), part, numparts)

  # elemen to vertex maps which we own
  elemtovert = Array{Int}(undef, nvert, length(elemlocal))
  elemtocoord = Array{Int}(undef, dim, nvert, length(elemlocal))

  nelemcube = Ne^dim # Ne^2

  etoijb = CartesianIndices((Ne, Ne, 6))
  bx = [0 Ne 2Ne  Ne  Ne  Ne]
  by = [0  0   0  Ne 2Ne 3Ne]

  vertmap = LinearIndices((Ne+1, Ne+1, Ne+1))
  for (le, e) = enumerate(elemlocal)
    i, j, blck = Tuple(etoijb[e])
    elemtocoord[1, :, le] = bx[blck] .+ [i-1 i i-1 i]
    elemtocoord[2, :, le] = by[blck] .+ [j-1 j-1 j j]

    for n = 1:4
      ix = i + mod(n-1, 2)
      jx = j + div(n-1, 2)
      # set the vertices like they are the face vertices of a cube
      if blck == 1
        elemtovert[n, le] = vertmap[   1   , Ne+2-ix,      jx]
      elseif blck == 2
        elemtovert[n, le] = vertmap[     ix,   1    ,      jx]
      elseif blck == 3
        elemtovert[n, le] = vertmap[Ne+1   ,      ix,      jx]
      elseif blck == 4
        elemtovert[n, le] = vertmap[     ix,      jx,  Ne+1  ]
      elseif blck == 5
        elemtovert[n, le] = vertmap[     ix, Ne+1   , Ne+2-jx]
      elseif blck == 6
        elemtovert[n, le] = vertmap[     ix, Ne+2-jx,    1   ]
      end
    end
  end

  # no boundaries for a shell
  elemtobndy = zeros(Int, nface, length(elemlocal))

  # no faceconnections for a shell
  faceconnections = Array{Array{Int, 1}}(undef, 0)

  (elemtovert, elemtocoord, elemtobndy, faceconnections, collect(elemlocal))
end

"""
    cubedshellwarp(a, b, c, R = max(abs(a), abs(b), abs(c)))

Given points `(a, b, c)` on the surface of a cube, warp the points out to a
spherical shell of radius `R` based on the equiangular gnomonic grid proposed by
Ronchi, Iacono, Paolucci (1996) <https://doi.org/10.1006/jcph.1996.0047>

```
@article{RonchiIaconoPaolucci1996,
  title={The ``cubed sphere'': a new method for the solution of partial
         differential equations in spherical geometry},
  author={Ronchi, C. and Iacono, R. and Paolucci, P. S.},
  journal={Journal of Computational Physics},
  volume={124},
  number={1},
  pages={93--114},
  year={1996},
  doi={10.1006/jcph.1996.0047}
}
```

"""
function cubedshellwarp(a, b, c, R = max(abs(a), abs(b), abs(c)))

  function f(sR, ξ, η)
    X, Y = tan(π * ξ / 4), tan(π * η / 4)
    x1 = sR / sqrt(X^2 + Y^2 + 1)
    x2, x3 = X * x1, Y * x1
    x1,x2,x3
  end

  fdim = argmax(abs.((a, b, c)))
  if fdim == 1 && a < 0
    # (-R, *, *) : Face I from Ronchi, Iacono, Paolucci (1996)
    x1,x2,x3 = f(-R, b/a, c/a)
  elseif fdim == 2 && b < 0
    # ( *,-R, *) : Face II from Ronchi, Iacono, Paolucci (1996)
    x2,x1,x3 = f(-R, a/b, c/b)
  elseif fdim == 1 && a > 0
    # ( R, *, *) : Face III from Ronchi, Iacono, Paolucci (1996)
    x1,x2,x3 = f(R, b/a, c/a)
  elseif fdim == 2 && b > 0
    # ( *, R, *) : Face IV from Ronchi, Iacono, Paolucci (1996)
    x2,x1,x3 = f(R, a/b, c/b)
  elseif fdim == 3 && c > 0
    # ( *, *, R) : Face V from Ronchi, Iacono, Paolucci (1996)
    x3,x2,x1 = f(R, b/c, a/c)
  elseif fdim == 3 && c < 0
    # ( *, *,-R) : Face VI from Ronchi, Iacono, Paolucci (1996)
    x3,x2,x1 = f(-R, b/c, a/c)
  else
    error("invalid case for cubedshellwarp: $a, $b, $c")
  end

  return x1, x2, x3
end

"""
    cubedshellunwarp(x1, x2, x3)

The inverse of [`cubedshellwarp`](@ref).
"""
function cubedshellunwarp(x1,x2,x3)

  function g(R, X, Y)
    ξ = atan(X) * 4 / pi
    η = atan(Y) * 4 / pi
    R, R*ξ, R*η
  end

  R = hypot(x1,x2,x3)
  fdim = argmax(abs.((x1, x2, x3)))

  if fdim == 1 && x1 < 0
    # (-R, *, *) : Face I from Ronchi, Iacono, Paolucci (1996)
    a,b,c = g(-R, x2/x1, x3/x1)
  elseif fdim == 2 && x2 < 0
    # ( *,-R, *) : Face II from Ronchi, Iacono, Paolucci (1996)
    b,a,c = g(-R, x1/x2, x3/x2)
  elseif fdim == 1 && x1 > 0
    # ( R, *, *) : Face III from Ronchi, Iacono, Paolucci (1996)
    a,b,c = g(R, x2/x1, x3/x1)
  elseif fdim == 2 && x2 > 0
    # ( *, R, *) : Face IV from Ronchi, Iacono, Paolucci (1996)
    b,a,c = g(R, x1/x2, x3/x2)
  elseif fdim == 3 && x3 > 0
    # ( *, *, R) : Face V from Ronchi, Iacono, Paolucci (1996)
    c,b,a = g(R, x2/x3, x1/x3)
  elseif fdim == 3 && x3 < 0
    # ( *, *,-R) : Face VI from Ronchi, Iacono, Paolucci (1996)
    c,b,a = g(-R, x2/x3, x1/x3)
  else
    error("invalid case for cubedshellunwarp: $a, $b, $c")
  end

  return a,b,c
end

"""
   StackedCubedSphereTopology(mpicomm, Nhorz, Rrange;
                              boundary=(1,1)) <: AbstractTopology{3}

Generate a stacked cubed sphere topology with `Nhorz` by `Nhorz` cells for each
horizontal face and `Rrange` is the radius edges of the stacked elements.  This
topology actual creates a cube mesh, and the warping should be done after the
grid is created using the `cubedshellwarp` function. The coordinates of the
points will be of type `eltype(Rrange)`. The inner boundary condition type is
`boundary[1]` and the outer boundary condition type is `boundary[2]`.

The elements are stacked such that the vertical elements are contiguous in the
element ordering.

The elements of the brick are partitioned equally across the MPI ranks based
on a space-filling curve. Further, stacks are not split at MPI boundaries.

# Examples

We can build a cubed sphere mesh with 10 x 10 x 5 elements on each cube, total
elements is `10 * 10 * 5 * 6 = 3000`, with
```jldoctest brickmesh
using CLIMA.Topologies
using MPI
MPI.Init()
Nhorz = 10
Nstack = 5
Rrange = Float64.(accumulate(+,1:Nstack+1))
topology = StackedCubedSphereTopology(MPI.COMM_SELF, Nhorz, Rrange)

x1, x2, x3 = ntuple(j->reshape(topology.elemtocoord[j, :, :],
                            2, 2, 2, length(topology.elems)), 3)
for n = 1:length(x1)
   x1[n], x2[n], x3[n] = Topologies.cubedshellwarp(x1[n], x2[n], x3[n])
end
```
Note that the faces are listed in Cartesian order.
"""
function StackedCubedSphereTopology(mpicomm, Nhorz, Rrange; boundary = (1, 1),
                                    connectivity=:face, ghostsize=1)
  T = eltype(Rrange)

  basetopo = CubedShellTopology(mpicomm, Nhorz, T; connectivity=connectivity,
                                   ghostsize=ghostsize)

  dim = 3
  nvert = 2^dim
  nface = 2dim
  stacksize = length(Rrange) - 1

  nreal = length(basetopo.realelems)*stacksize
  nghost = length(basetopo.ghostelems)*stacksize

  elems=1:(nreal+nghost)
  realelems=1:nreal
  ghostelems=nreal.+(1:nghost)

  sendelems = similar(basetopo.sendelems,
                      length(basetopo.sendelems)*stacksize)

  for i=1:length(basetopo.sendelems), j=1:stacksize
    sendelems[stacksize*(i-1) + j] = stacksize*(basetopo.sendelems[i]-1) + j
  end

  ghostfaces = similar(basetopo.ghostfaces, nface, length(ghostelems))
  ghostfaces .= false

  for i=1:length(basetopo.ghostelems), j=1:stacksize
    e = stacksize*(i-1) + j
    for f = 1:2(dim-1)
      ghostfaces[f, e] = basetopo.ghostfaces[f, i]
    end
  end

  sendfaces = similar(basetopo.sendfaces, nface, length(sendelems))
  sendfaces .= false

  for i=1:length(basetopo.sendelems), j=1:stacksize
    e = stacksize*(i-1) + j
    for f=1:2(dim-1)
      sendfaces[f, e] = basetopo.sendfaces[f, i]
    end
  end

  elemtocoord = similar(basetopo.elemtocoord, dim, nvert, length(elems))

  for i=1:length(basetopo.elems), j=1:stacksize
    # i is base element
    # e is stacked element
    e = stacksize*(i-1) + j

    # v is base vertex
    for v = 1:2^(dim-1)
      for d = 1:dim # dim here since shell is embedded in 3-D
        # v is lower stacked vertex
        elemtocoord[d, v, e] = basetopo.elemtocoord[d, v, i] * Rrange[j]
        # 2^(dim-1) + v is higher stacked vertex
        elemtocoord[d, 2^(dim-1) + v, e] = basetopo.elemtocoord[d, v, i] *
                                           Rrange[j+1]
      end
    end
  end

  elemtoelem = similar(basetopo.elemtoelem, nface, length(elems))
  elemtoface = similar(basetopo.elemtoface, nface, length(elems))
  elemtoordr = similar(basetopo.elemtoordr, nface, length(elems))
  elemtobndy = similar(basetopo.elemtobndy, nface, length(elems))

  for e=1:length(basetopo.elems)*stacksize, f=1:nface
    elemtoelem[f, e] = e
    elemtoface[f, e] = f
    elemtoordr[f, e] = 1
    elemtobndy[f, e] = 0
  end

  for i=1:length(basetopo.realelems), j=1:stacksize
    e1 = stacksize*(i-1) + j

    for f = 1:2(dim-1)
      e2 = stacksize*(basetopo.elemtoelem[f, i]-1) + j

      elemtoelem[f, e1] = e2
      elemtoface[f, e1] = basetopo.elemtoface[f, i]

      # since the basetopo is 2-D we only need to worry about two orientations
      @assert basetopo.elemtoordr[f, i] ∈ (1,2)
      #=
      orientation 1:
      2---3     2---3
      |   | --> |   |
      0---1     0---1
      same:
      (a,b) --> (a,b)

      orientation 3:
      2---3     3---2
      |   | --> |   |
      0---1     1---0
      reverse first index:
      (a,b) --> (N+1-a,b)
      =#
      elemtoordr[f, e1] = basetopo.elemtoordr[f, i] == 1 ? 1 : 3
    end

    # If top or bottom of stack set neighbor to self on respective face
    elemtoelem[2(dim-1)+1, e1] = j == 1         ? e1 : stacksize*(i-1) + j - 1
    elemtoelem[2(dim-1)+2, e1] = j == stacksize ? e1 : stacksize*(i-1) + j + 1

    elemtoface[2(dim-1)+1, e1] = j == 1         ? 2(dim-1) + 1 : 2(dim-1) + 2
    elemtoface[2(dim-1)+2, e1] = j == stacksize ? 2(dim-1) + 2 : 2(dim-1) + 1

    elemtoordr[2(dim-1)+1, e1] = 1
    elemtoordr[2(dim-1)+2, e1] = 1
  end

  # Set the top and bottom boundary condition
  for i=1:length(basetopo.elems)
    eb = stacksize*(i-1) + 1
    et = stacksize*(i-1) + stacksize

    elemtobndy[2(dim-1)+1, eb] = boundary[1]
    elemtobndy[2(dim-1)+2, et] = boundary[2]
  end

  nabrtorank = basetopo.nabrtorank
  nabrtorecv =
    UnitRange{Int}[UnitRange(stacksize*(first(basetopo.nabrtorecv[n])-1)+1,
                             stacksize*last(basetopo.nabrtorecv[n]))
                   for n = 1:length(nabrtorank)]
  nabrtosend =
    UnitRange{Int}[UnitRange(stacksize*(first(basetopo.nabrtosend[n])-1)+1,
                             stacksize*last(basetopo.nabrtosend[n]))
                   for n = 1:length(nabrtorank)]

  StackedCubedSphereTopology{T}(
    BoxElementTopology{3, T}(
      mpicomm, elems, realelems, ghostelems, ghostfaces, sendelems,
      sendfaces, elemtocoord, elemtoelem, elemtoface, elemtoordr, elemtobndy,
      nabrtorank, nabrtorecv, nabrtosend, basetopo.origsendorder, true),
    stacksize)
end

"""
    grid1d(a, b[, stretch::AbstractGridStretching]; elemsize, nelem)

Discretize the 1D interval [`a`,`b`] into elements.
Exactly one of the following keyword arguments must be provided:
- `elemsize`: the average element size, or
- `nelem`: the number of elements.

The optional `stretch` argument allows stretching, otherwise the element sizes will be uniform.

Returns either a range object or a vector containing the element boundaries.
"""
function grid1d(a, b, stretch=nothing; elemsize=nothing, nelem=nothing)
  xor(nelem === nothing, elemsize === nothing) || error("Either `elemsize` or `nelem` arguments must be provided")
  if elemsize !== nothing
    nelem = round(Int,abs(b-a)/elemsize)
  end
  grid1d(a, b, stretch, nelem)
end
function grid1d(a, b, ::Nothing, nelem)
  range(a, stop=b, length=nelem+1)
end

# TODO: document these
abstract type AbstractGridStretching end

"""
    SingleExponentialStretching(A)

Apply single-exponential stretching: `A > 0` will increase the density of points at the lower boundary, `A < 0` will increase the density at the upper boundary.

# Reference
* "Handbook of Grid Generation" J. F. Thompson, B. K. Soni, N. P. Weatherill (Editors) RCR Press 1999, §3.6.1 Single-Exponential Function
"""
struct SingleExponentialStretching{T} <: AbstractGridStretching
  A::T
end
function grid1d(a::A, b::B, stretch::SingleExponentialStretching, nelem) where {A,B}
  F = float(promote_type(A,B))
  s = range(zero(F), stop=one(F), length=nelem+1)
  a .+ (b-a) .* expm1.(stretch.A .* s) ./ expm1(stretch.A)
end

struct InteriorStretching{T} <: AbstractGridStretching
  attractor::T
end
function grid1d(a::A, b::B, stretch::InteriorStretching, nelem) where {A,B}
  F = float(promote_type(A,B))
  coe = F(2.5)
  s = range(zero(F), stop=one(F), length=nelem+1)
  range(a, stop=b, length=nelem+1) .+ coe .* (stretch.attractor .- (b-a).*s) .* (1 .-s) .* s
end

end


using Revise

using PathTracing

using ImageView, Gtk, GtkReactive

# Create a canvas `c`. There are other approaches, like stealing one from a previous call
# to `imshow`, or using GtkReactive directly.
guidict = imshow_gui((512, 384), name="Path Tracer")
frame = guidict["frame"]
canvas = guidict["canvas"]
# To see anything you have to call `showall` on the window (once)
Gtk.showall(guidict["window"])

# This dance with the frame and zoom region signal `zrsig` is to preserve the
# aspect ratio. Why is this *soo* annoying!? :-( :-(
img = PathTracing.main()
zrsig = Signal(ZoomRegion(img))
imgsig = Signal(img)
imshow(frame, canvas, imgsig, zrsig)

@async Revise.entr([], all=true, postpone=true) do
    global img = PathTracing.main()
    push!(imgsig, img)
    push!(zrsig, ZoomRegion(img))
end


using .Networks

using Dates
using DataStructures
using Printf

"""
    Determines whether a decision should be :
    - already decided : DECIDED
    - to decide firmly (setting a common value for all scenarios) : TO_DECIDE
    - to decide freely (possibly setting different values for different scenarios): FREE
    The decision is based on the characteristic time period `delta` (delta can represent the DMO or DP)
"""
function compute_firmness(ech::Dates.DateTime, next_ech::Union{Nothing,Dates.DateTime},
                        ts::Dates.DateTime, delta::Dates.Period)
    if ( !isnothing(next_ech) && (next_ech < ech) )
        throw( error("next_ech (", next_ech, ") must be later than ech (", ech,").") )
    end

    final_decision_time = ts - delta

    if final_decision_time < ech
        return DECIDED
    elseif ( isnothing(next_ech) || (final_decision_time < next_ech) )
        return TO_DECIDE
    else
        return FREE
    end
end

function compute_firmness(ech::Dates.DateTime, next_ech::Union{Nothing,Dates.DateTime},
                    TS::Vector{Dates.DateTime}, generators::Vector{Networks.Generator})
    firmness = Firmness()
    for generator in generators
        gen_id = Networks.get_id(generator)
        dmo = Networks.get_dmo(generator)
        dp = Networks.get_dp(generator)

        for ts in TS
            if Networks.get_p_min(generator) > eps()
                #commitment
                commitment_firmness = compute_firmness(ech, next_ech, ts, dmo)
                set_commitment_firmness!(firmness, gen_id, ts, commitment_firmness)
            end

            #power level
            power_level_firmness = compute_firmness(ech, next_ech, ts, dp)
            set_power_level_firmness!(firmness, gen_id, ts, power_level_firmness)
        end
    end
    return firmness
end

#########################################
#       Schedule Firmness
#########################################

function verify_firmness(firmness::Firmness, schedule::Schedule;
                        excluded_ids::Union{Set{String},Vector{String}}=Set{String}())
    excluded_ids = Set{String}(excluded_ids)
    return ( verify_commitment_firmness(get_commitment_firmness(firmness), schedule, excluded_ids=excluded_ids)
            && verify_production_firmness(get_power_level_firmness(firmness), schedule, excluded_ids=excluded_ids) )
end

function verify_commitment_firmness(firmness::SortedDict{String, SortedDict{Dates.DateTime, DecisionFirmness} },
                                    schedule::Schedule;
                                    excluded_ids::Set{String}=Set{String}())
    for (gen_id, generator_firmness) in firmness
        if gen_id in excluded_ids
            continue
        end
        if !verify_firmness(generator_firmness, get_sub_schedule(schedule, gen_id).commitment)
            @warn(@sprintf("commitment schedule of generator %s violates firmness", gen_id))
            return false
        end
    end
    return true
end

function verify_production_firmness(firmness::SortedDict{String, SortedDict{Dates.DateTime, DecisionFirmness} },
                                    schedule::Schedule;
                                    excluded_ids::Set{String}=Set{String}())
    for (gen_id, generator_firmness) in firmness
        if gen_id in excluded_ids
            continue
        end
        if !verify_firmness(generator_firmness, get_sub_schedule(schedule, gen_id).production)
            @warn(@sprintf("production schedule of generator %s violates firmness", gen_id))
            return false
        end
    end
    return true
end

"""
    Verfies if a given schedule respects the firmness constraints at a given timestep
"""
function verify_firmness(generator_firmness::SortedDict{Dates.DateTime, DecisionFirmness},
                        scheduled_values::SortedDict{Dates.DateTime, UncertainValue{T}}) where T

    for (ts, decision_firmness) in generator_firmness
        #if firmness is defined, a schedule value must exist
        if !haskey(scheduled_values, ts)
            @warn(@sprintf("firmness violation : missing required schedule value at timestep %s", ts))
            return false
        end

        uncertain_value = scheduled_values[ts]
        if ((decision_firmness == TO_DECIDE) || (decision_firmness == DECIDED))
            if !is_definitive(uncertain_value)
                @warn(@sprintf("firmness violation : non-definitive value at timestep %s while a firm value is required", ts))
                return false
            end

        elseif (decision_firmness == FREE)
            # FIXME: are definitive values accepted as FREE ? (might be useful for EnergyMarketAtFO, which considers one scenario)
            # if (is_definitive(uncertain_value))
            #     @warn(@sprintf("firmness violation : definitive value at timestep %s while a non-firm value is required", ts))
            #     return false
            # end
            if (is_missing_values(uncertain_value))
                @warn(@sprintf("firmness violation : missing value at timestep %s while a non-firm value is required", ts))
                return false
            end

        else
            throw( error("Non-handled DecisionFirmness value : ", decision_firmness) )
        end
    end

    return true
end