export connect_system, rename_vars

"""
$(SIGNATURES)

Recursively transform `IOSystems` to `IOBlocks`.

- substitute inputs with connected outputs
- try to eliminate equations for internal states which are not used to calculate the specified outputs of the system.
- try to eliminate explicit algebraic equations (i.e. outputs of internal blocks) by substituting each occurrence
  with their rhs. Explicit algebraic states which are marked as system outputs won't be removed.

Arguments:
- `ios`: system to connect
- `verbose=false`: toggle verbosity (show equations at different steps)
- `simplify_eqs=true`: toggle simplification of all equations at the end
"""
function connect_system(ios::IOSystem; verbose=false, simplify_eqs=true)
    # recursive connect all subsystems
    for (i, subsys) in enumerate(ios.systems)
        if subsys isa IOSystem
            ios.systems[i] = connect_system(subsys, verbose=verbose)
        end
    end
    eqs = vcat([namespace_equations(iob.system) for iob in ios.systems]...)
    removed_eqs = vcat([namespace_rem_eqs(iob) for iob in ios.systems]...)

    verbose && @info "Transform IOSystem $(ios.name) to IOBlock" ios.name ios.inputs ios.outputs ios.connections eqs

    # get rid of closed inputs by substituting output states
    substitutions = reverse.(ios.connections)
    for (i, eq) in enumerate(eqs)
        eqs[i] = eq.lhs ~ substitute(eq.rhs, substitutions)
    end
    for (i, eq) in enumerate(removed_eqs)
        removed_eqs[i] = eq.lhs ~ substitute(eq.rhs, substitutions)
    end

    verbose && @info "substitute inputs with outputs" eqs

    # get red of unused states
    # (internal variables which are not used for the outputs)
    nspcd_outputs = [findfirst(v->isequal(v, o), ios.namespace_map) for o in ios.outputs]
    reduced_eqs1 = remove_superfluous_states(eqs, get_iv(ios), nspcd_outputs; verbose)
    verbose && @info "without superfluous states" reduced_eqs1

    # reduce algebraic states of the system
    (reduced_eqs2, new_rem_eqs) = remove_algebraic_states(reduced_eqs1, skip = nspcd_outputs)
    verbose && @info "without explicit algebraic states" reduced_eqs2 new_rem_eqs

    # add all of the removed_eqs of the subsystem
    removed_eqs = vcat(new_rem_eqs, removed_eqs)

    # apply the namespace transformations
    promotion_rules = ios.namespace_map
    promoted_eqs = map(eq->eqsubstitute(eq, promotion_rules), reduced_eqs2)
    removed_eqs  = map(eq->eqsubstitute(eq, promotion_rules), removed_eqs)

    verbose && @info "promoted namespaces" promoted_eqs removed_eqs

    if simplify_eqs
        promoted_eqs = simplify.(promoted_eqs)
        removed_eqs = simplify.(removed_eqs)
        verbose && @info "simplified equations" promoted_eqs removed_eqs
    end

    try
        IOBlock(ios.name, promoted_eqs, ios.inputs, ios.outputs, removed_eqs; iv=get_iv(ios))
    catch e
        @error "Failed to build IOBlock from System" ios.inputs ios.outputs ios.name eqs reduced_eqs1 reduced_eqs2 promoted_eqs removed_eqs
        throw(e)
    end
end

"""
    remove_superfluous_states(eqs::Vector{Equation}, iv, outputs)

This function removes equations, which are not used in order to generate the
given `outputs`. It looks for equations which have no path to the `outputs`
equations in the dependency graph. Returns a new, reduced set of equations
without these states.
"""
function remove_superfluous_states(eqs::Vector{Equation}, iv, outputs; verbose=false)
    neweqs = deepcopy(eqs)
    sys = ODESystem(neweqs, iv; name=:tmp) # will be used for the dependency graph
    neweqs = get_eqs(sys) # the ODESystem might reorder the equations
    # generate dependency graph
    graph = eqeq_dependencies(asgraph(sys), variable_dependencies(sys))
    # find 'main' eq for each output
    output_idx = [findfirst(x->o ∈ Set(get_variables(x.lhs)), neweqs) for o in outputs]

    if any(isnothing, output_idx)
        verbose && @info "Can't remove souperflous states if outputs implicitly defined."
        return neweqs
    end

    # if there is no path from equation to output equation is not necessary
    removable = []
    for eq_node in 1:length(neweqs)
        if !any(has_path(graph, eq_node, out_node) for out_node in output_idx)
            push!(removable, eq_node)
        end
    end

    removed_eqs = neweqs[removable]
    deleteat!(neweqs, sort(removable))

    return neweqs
end

"""
    remove_algebraic_states(eqs:Vector{Equation}, skip=[])

Reduces the number of equations by substituting explicit algebraic equations.
Returns a tuple containing two `Vector{Equations}`
  - the new equations with substituted states
  - the algebraic states which have been removed

The optional skip argument is used to declare states which should not be removed.

This function checks for cyclic dependencies between algebraic equations by generating
a dependency graph between them. All substitutions have to be pairwise cycle free.
```example
julia> @variables i x y o1 o2;
julia> D = Differential(t);
julia> eqs = [D(x) ~ i,
              o1 ~ x + o2,
              D(y) ~ i,
              o2 ~ y + o1];
julia> remove_algebraic_states(eqs)
(Equation[Equation((D'~t)(x), i),
          Equation(o1, x + (o1 + y)),
          Equation((D'~t)(y), i)],
 Equation[Equation(o2, o1 + y)])
```
"""
function remove_algebraic_states(eqs::Vector{Equation}; skip=[])
    # FIXME: implicit euqations x ~ a+b which constraint a and b might be wrongly reduced if x not used elsewhere!
    reduced_eqs = deepcopy(eqs)

    # only consider states for reduction which are explicit algebraic and not in skip
    condition = eq -> begin
        (type, var) = eq_type(eq)
        type == :explicit_algebraic && var ∉ Set(skip)
    end
    algebraic_idx = findall(condition, reduced_eqs)

    # symbols of all algebraic eqs
    symbols = [eq.lhs for eq ∈ reduced_eqs[algebraic_idx]]

    # generate dependency graph
    g = SimpleDiGraph(length(algebraic_idx))
    for (i, eq) in enumerate(reduced_eqs[algebraic_idx])
        rhs_vars = get_variables(eq.rhs)
        for (isym, sym) in enumerate(symbols)
            if Set([sym]) ⊆ Set(rhs_vars)
                add_edge!(g, isym => i)
            end
        end
    end
    removable = algebraic_idx[pairwise_cycle_free(g)]
    @assert allunique(removable)

    rules = Dict(eq.lhs => eq.rhs for eq in reduced_eqs[removable])
    # subsitute all the equations, remove substituted
    for (i, eq) in enumerate(reduced_eqs)
        reduced_eqs[i] = eq.lhs ~ recursive_substitute(eq.rhs, rules)
    end

    removed_eqs = reduced_eqs[removable]
    deleteat!(reduced_eqs, sort(removable))

    return reduced_eqs, removed_eqs
end

"""
    pairwise_cycle_free(g:SimpleDiGraph)

Returns an array of vertices, which pairwise do not belong to any cycle in `g`.
Uses `simplecycles` from `LightGraphs`. The algorithm starts with all vertices
and iteratively removes the vertices is part of most cycles.
"""
function pairwise_cycle_free(g::SimpleDiGraph)
    idx = collect(vertices(g))
    cycles = simplecycles(g)
    while true
        cycles_per_idx = zeros(Int, length(idx))
        for (i, id1) ∈ enumerate(idx)
            for (j, id2) ∈ enumerate(@view idx[i+1 : end])
                for c in cycles
                    if id1 ∈ c && id2 ∈ c
                        cycles_per_idx[i] += 1
                        cycles_per_idx[i+j] += 1
                    end
                end
            end
        end
        if sum(cycles_per_idx) > 0
            worst_idx = findmax(cycles_per_idx)[2]
            deleteat!(idx, worst_idx)
        else
            return idx
        end
    end
end

"""
    rename_vars(blk::IOBLock, kwargs...)
    rename_vars(blk::IOBlock, subs::Dict{Symbolic,Symbolic})

Returns new IOBlock which is similar to blk but with new variable names.
Variable renamings should be provided as keyword arguments, i.e.

    rename_vars(blk; x=:newx, k=:knew)

to rename `x(t)=>newx(t)` and `k=>knew`. Subsitutions can be also provided as
dict of `Symbolic` types (`Sym`s and `Term`s).
"""
function rename_vars(blk::IOBlock; kwargs...)
    substitutions = Dict{Symbolic, Symbolic}()
    for pair in kwargs
        key = remove_namespace(blk.name, getproperty(blk, pair.first))
        val = rename(key, pair.second)
        substitutions[key] = val
    end
    rename_vars(blk, substitutions)
end

function rename_vars(blk::IOBlock, subs::Dict{Symbolic,Symbolic})
    eqs     = map(eq->eqsubstitute(eq, subs), get_eqs(blk.system))
    rem_eqs = map(eq->eqsubstitute(eq, subs), blk.removed_eqs)
    inputs  = map(x->substitute(x, subs), blk.inputs)
    outputs = map(x->substitute(x, subs), blk.outputs)
    IOBlock(blk.name, eqs, inputs, outputs, rem_eqs; iv=get_iv(blk))
end


# Set the Attributes of some "builtin" Protected symbols

## NOTE!: Until the attribute setting is cleaned up, in particular in math_functions.jl,
## This file should be loaded last or almost last in order to correct incorrectly set attributes.
## This happens because big lists of functions are handled by macros that set default attributes,
## but the defaults should not apply to a few of them.

## TODO: The use of arrays, vs. single elements and strings vs symbols is outdated
## set_sysattributes takes two args. either can be a string or a symbol or an array of strings or symbols.

set_sysattributes(["Pattern", "SetJ", "SetAttributes", "ClearAttributes", "TimesBy", "AddTo", "Catch"], :HoldFirst)

## Piecewise is also read protected in Mma
set_sysattributes(["Module","LModule", "Clear", "ClearAll", "Condition", "HoldPattern", "HoldForm", "Hold",
                   "DumpHold", "DownValues", "UpValues", "HAge", "Table", "NTable", "For", "If", "While", "Do",
                   "Jxpr", "JuliaExpression", "Protect", "Unprotect", "Function", "Definition", "DirtyQ", "ToSymata",
                   "NIntegrate", "Compile","SymataCall", "Piecewise", "ExportJ", "OwnValues"],
                  :HoldAll)

set_sysattributes(["HoldComplete", "Unevaluated"], [:HoldAllComplete])

set_sysattributes(["Save", "Last"], [:HoldRest] )

set_sysattributes("Attributes",[:HoldAll,:Listable])

set_sysattributes("DivisorSigma",[:NHoldAll,:Listable])

set_sysattributes("Rule",:SequenceHold)

set_sysattributes(["Set","UpSet"],[:HoldFirst, :SequenceHold])

set_sysattributes(["Increment","Decrement"],[:HoldFirst, :ReadProtected])

set_sysattributes(["RuleDelayed","PatternTest"],[:HoldRest, :SequenceHold])

set_sysattributes(["Timing","Allocated","SetDelayed","UpSetDelayed"], [:HoldAll,:SequenceHold])

set_sysattributes(["Pi","E"],[:ReadProtected,:Constant])
set_sysattributes(["EulerGamma", "GoldenRatio", "Catalan"],[:Constant])

set_sysattributes("I", [:ReadProtected,:Locked]) # Careful with this. We mostly use julia symbol :I bound to complex(0,1)

set_sysattributes(["CompoundExpression","Sum","Product"],[:ReadProtected,:HoldAll])

set_sysattributes(["Part","D","LaplaceTransform","InverseLaplaceTransform",
                 "FourierTransform","InverseFourierTransform", "Integrate", "DSolve", "ReadString",
                 "STDOUT", "STDERR", "DevNull", "HoldAllComplete"
                 ],:ReadProtected)

# We kinda need Exp, see the apprules.
set_sysattributes(["Cos", "ArcCos", "Sin", "ArcSin", "Tan", "ArcTan", "CosPi", "SinPi",
                 "Cot", "Cosh", "Conjugate", "Sinh","Minus","Abs","Re","Im", "ReIm", "Exp", "Sqrt",
                 "PolyGamma", "EllipticE", "EllipticF", "EllipticK", "EllipticPi", "LogIntegral", "Mod", "DivRem",
                 "Sign", "SphericalHarmonicY", "SphericalBesselJ", "SphericalBesselY", "Erf", "Gamma", "GammaRegularized", "Divide",
                 "DirichletEta", "PolyLog", "Sqrt", "CubeRoot", "Surd", "BesselJZero"
                 ],
                [:Listable,:NumericFunction])

## These were given Listable attribute in math_functions.jl
clear_attributes(:MeijerG, :HypergeometricPFQ)
set_sysattributes([:MeijerG, :HypergeometricPFQ], [:NumericFunction])

set_sysattributes([:HeavisideTheta],
                [:Listable, :Orderless])

set_sysattributes(["Plus", "Times"],
                [:Flat,:Listable,:NumericFunction,:OneIdentity,:Orderless])

set_sysattributes(["Max", "Min"],
                [:Flat,:NumericFunction,:OneIdentity,:Orderless])

set_sysattributes(["LCM", "GCD"],
                [:Flat,:Listable,:OneIdentity,:Orderless])

set_sysattributes(["Composition"],
            [:Flat,:OneIdentity,])

set_sysattributes("Power",[:Listable,:NumericFunction,:OneIdentity])

# This is not quite what Mma has for these. I don't understand why. Eg. CosIntegral is not a NumericFunction
# Many are not Listable in the docs, although they are in practice.
set_sysattributes(["Pochhammer", "LogIntegral", "LerchPhi", "CosIntegral", "SinIntegral", "FresnelC", "FresnelS", "MittagLefflerE",
                 "HarmonicNumber", "InverseErf", "Log"],
                [:Listable,:NumericFunction,:ReadProtected])

# BellB should be split into BellB and BellY
clear_attributes(:BellB)  ## FIXME: set these attributes only once
set_sysattributes(["BellB"],
                [:NumericFunction,:ReadProtected])

# Note: Simplify is not Listable in Mma
set_sysattributes(["Boole", "EvenQ","OddQ", "PrimeQ", "Range","Limit", "Together", "Apart", "Cyclotomic", "MoebiusMu", "EulerPhi", "Divisors", "DivisorCount",
                 "ToExpression", "Simplify", "FullSimplify", "StringReverse", "ToJExpression", "LetterNumber"]
                ,[:Listable])

set_sysattributes(["DirectedInfinity"], [:Listable, :ReadProtected])

# Some symbols that may be particular to SymPy
# We should use GaussianIntegers instead of Gaussian
set_sysattributes(["PolarLift", "ExpPolar", "ExpandFunc", "Modulus","Force", "Deep", "Gaussian"],
                :Protected)

set_sysattributes(["And", "Or"], [:Flat, :HoldAll, :OneIdentity])

set_sysattributes(["UnicodeForm", "JupyterForm", "InputForm"], :Protected)

## Do we still need these ?
#set_sysattributes(["Plain","Unicode", "IJulia" ], :Protected)

set_sysattributes(["Return","Break","Continue"], :Protected)

set_sysattributes(["HoldFirst","HoldAll","HoldRest","NHoldFirst","NHoldAll","NHoldRest"], :Protected)

# FIXME: Subscript cannot have Attribute 'Protected', because the user must be able to assign, e.g. Subscript(a,1) = 1.
# But, without Protected, we do not get TAB completion.
set_attributes(:Subscript, :NHoldRest)

set_sysattributes(["Int64", "Int", "Float64",  "BigFloat", "BigInt", "AbstractString", "Real", "Float"], :Protected)

set_sysattributes(["O","OO","OOO","OOOO","OOOOO","OOOOOO","OOOOOOO","OOOOOOOO","OOOOOOOOOO"], :Protected)

set_sysattributes(["System", "Main"], :Protected)

# True and False are actually parsed as Julia true and false. But, this is a quick way to get TAB completion for them
set_sysattributes(["True", "False"], :Protected)

set_sysattributes(["Optional","Repeated", "RepeatedNull" ], :Protected)

set_sysattributes([
    "!=","//","<","<=","==",">","===",">=","Age","Algebraics","All","Alternatives","Apply","Array","ArrayDepth",
    "AtomQ","BF","BI","Big","Binary","Blank",
    "BlankNullSequence","BlankSequence","Booleans","BuiltIns","ByteCount","Cancel",
    "Chop", "Collect","Comparison","CompiledFunction","Complex", "ComplexInfinity",
    "Complexes","ConditionalExpression",
    "Conditions","ConstantArray","ConstantQ","Depth","DirtyQ","Dump","Evaluate","Example","Except",
    "ExpToTrig", "Expand", "ExpandA", "ExpandCos", "ExpandCosRule",
    "ExpandSin","ExpandSinCos","ExpandSinRule","Factor","FactorInteger",
    "FixedQ","Flat","FullForm","Head", "Help",
    "Indeterminate","Infinity","Integer","IntegerDigits","Integers",
    "JVar","Keys","LeafCount","Length","List","ListQ","Listable",
    "Map","MatchQ","Missing","N","None","Not",
    "Null","Numerator","NumericFunction","NumericQ","OneIdentity","Order","Orderless",
    "Pack","Permutations", "PossibleClosedForm","Precision","Primes","Print",
    "PrintTests","Println", "Protected",
    "RatSimp","Rational","Rationals","RealRoots","Reals", "Replace", "ReplaceAll",
    "ReplaceRepeated","Reverse","Roots","Sequence","SequenceHold",
    "Series","Solve","Span","String","StringJoin","StringLength","StringQ","Subdivide","Symbol",
    "Syms","Temporary","TensorRank","Throw",
    "Time","ToLowerCase","ToString","ToSymPy","ToUpperCase", "Trace","TraceDownValues",
    "TraceUpValues","TrigSimp", "Undefined", "Unfix", "Unpack", "UserSyms", "Values", "Normal",
    "ans", "nothing" ], :Protected)


export LgbmModel, LgbmRegressor, LgbmClassifier

@with_kw mutable struct LgbmModel <: BaseEstimator
    pyo::PyObject = PyNULL()
    so::Vector{UInt8} = UInt8[]
    max_bin::Int = 16
    objective::String = "binary:logistic"
    warm_start::Bool = false
    num_leaves::Int = 31
    min_child_samples::Int = 1000
    n_estimators::Int = 50
    reg_lambda::Float32 = 0.1
    subsample::Float32 = 1
    colsample_bytree::Float32 = 1
end

is_classifier(m::LgbmModel) = occursin(r"binary|multi", m.objective)

function gridparams(m::LgbmModel)
    grid = [
        "num_leaves" => [15, 31],
        "min_child_samples" => [1000, 100, 5000],
        "n_estimators" => [20, 50],
        "reg_lambda" => [0.1, 1, 10],
        "subsample" => [1, 0.8],
        "colsample_bytree" => [1, 0.8],
    ]
    params = gridparams(grid)
end

function fit!(m::LgbmModel, x, y, w = nothing; columns = nothing)
    columns = something(columns, string.(1:size(x, 1)))
    @unpack max_bin, objective, warm_start, num_leaves, min_child_samples = m
    @unpack n_estimators, reg_lambda, subsample, colsample_bytree = m
    dump_lgbm_data(x, y, w, max_bin = max_bin, columns = columns)
    if occursin(r"binary|multi", objective)
        num_class = length(unique(y))
    end
    if occursin("binary", objective) && num_class > 2
        objective = "multi:softmax"
    end
    min_child_samples = min(length(y) ÷ 10, min_child_samples)
    # get node lists
    nodes = unique(pmap(n -> gethostname(), workers()))
    nnode, hosts = length(nodes), join(nodes, ',')
    # make cmd args
    args = String[]
    push!(args, "task=train")
    objective = denormalize_objective(objective, "lightgbm")
    push!(args, "objective=$objective")
    occursin("multiclass", objective) &&
    push!(args, "num_class=$num_class")
    push!(args, "tree_learner=data")
    push!(args, "n_estimators=$n_estimators")
    push!(args, "n_jobs=20")
    push!(args, "train_data=train.lgbm")
    push!(args, "num_machines=$nnode")
    push!(args, "output_model=lgbm")
    warm_start && isfile("lgbm") &&
    push!(args, "input_model=lgbm")
    push!(args, "num_leaves=$num_leaves")
    push!(args, "min_child_samples=$min_child_samples")
    push!(args, "reg_lambda=$reg_lambda")
    push!(args, "subsample=$subsample")
    push!(args, "colsample_bytree=$colsample_bytree")
    push!(args, "max_bin=$(max_bin - 1)")
    if isnothing(Sys.which("mpiexec"))
        run(`$LIGHTGBM $args`)
    else
        run(`mpiexec --host $hosts $LIGHTGBM $args`)
    end
    @from lightgbm imports Booster
    @from treelite imports Model
    pyo = Booster(model_file = "lgbm")
    em = eachmatch(r"Tree=(\d+)", read("lgbm", String))
    ntree = collect(em)[end].captures[1]
    if parse(Int, ntree) > 0
        model = Model.load("lgbm", model_format = "lightgbm")
        rm("lgbm.so", force = true)
        model.export_lib(toolchain = "gcc", libpath = "lgbm.so")
        so = read("lgbm.so")
    else
        so = UInt8[]
    end
    @pack! m = pyo, so
    BSON.bson("model.bson", model = m)
    visualize(m, columns)
    return m
end

function predict_lgbm(m::LgbmModel, x; treelite)
    @unpack pyo, so = m
    if treelite && !isempty(so)
        write("lgbm.so", so)
        @imports treelite.runtime as rt
        batch = rt.Batch.from_npy2d(pymat(x))
        pyo = rt.Predictor("lgbm.so")
        pyo.predict(batch)
    else
        pyo.predict(pymat(x))
    end
end

function predict_proba(m::LgbmModel, x; treelite = false)
    ŷ = predict_lgbm(m, x, treelite = treelite)
    ndims(ŷ) == 1 ? hcat(1 .- ŷ,  ŷ) : ŷ
end

function predict(m::LgbmModel, x; treelite = false)
    @unpack objective = m
    ŷ = predict_lgbm(m, x, treelite = treelite)
    !is_classifier(m) && return ŷ
    if ndims(ŷ) == 1
        signone.(ŷ .- 0.5)
    else
        [I[2] - 1 for I in argmax(ŷ, dims = 2)]
    end
end

function visualize(m::LgbmModel, columns)
    @unpack pyo = m
    gbm = GbmModel(pyo = pyo)
    visualize(gbm, columns)
end

reset!(m::LgbmModel) = foreach(c -> rm(c, force = true), ["train.lgbm", "lgbm", "lgbm.so", "ref.bson"])

istree(m::LgbmModel) = true

modelhash(m::LgbmModel) = hash(m.pyo)

function dump_lgbm_data(x, y, w = nothing; max_bin = 16, columns)
    w = isnothing(w) ? fill!(similar(y), 1) : w
    if isfile("ref.bson") && isfile("train.lgbm")
        BSON.@load "ref.bson" xseg yoff woff
    else
        xseg, yoff, woff = zeros(Float32), -1, -1
    end
    if xseg == x[1:min(end, 100000)] && length(unique(y)) <= 2
        open("train.lgbm", "r+") do fid
            @assert yoff > 0
            seek(fid, yoff - 1)
            write(fid, y)
            if woff > 0
                seek(fid, woff - 1)
                write(fid, w)
            end
        end
    else
        xseg = x[1:min(end, 100000)]
        x, y, w = pymat(x), vec(y), vec(w)
        @from lightgbm imports Dataset
        @from unidecode imports unidecode
        dset = Dataset(data = x, label = y, weight = w, params = Dict("max_bin" => max_bin - 1))
        columns′ = [filter(!isspace, unidecode(c)) for c in columns]
        dset.set_feature_name(columns′)
        isfile("train.lgbm") && rm("train.lgbm")
        @time dset.save_binary("train.lgbm")
        @gc dset
        data = Mmap.mmap("train.lgbm")
        yoff = align(data, reinterpret(UInt8, y))
        woff = align(data, reinterpret(UInt8, w))
        BSON.@save "ref.bson" xseg yoff woff
    end
end

LgbmRegressor(;ka...) = LgbmModel(;objective = "reg:mse", ka...)

LgbmClassifier(;ka...) = LgbmModel(;objective = "binary:logistic", ka...)


# utils for Graff / Caesar style BigData

function getPointCloudFromKinect(data, dcamjl, imshape)
  ri,ci = imshape[1], imshape[2] # TODO -- hack should be removed since depth is array and should have rows and columns stored in Mongo
  arr = bin2arr(data, dtype=Float32) # should also store dtype for arr in Mongo
  img = reshape(arr, ci, ri)'
  reconstruct(dcamjl, Array{Float64,2}(img))
end

function getPointCloudFromBSON(data)
  buf = IOBuffer(data)
  st = takebuf_string(buf)
  bb = BSONObject(st)
  return map(x -> convert(Array, x), bb["pointcloud"])
end


struct TwoLoopVars{T,TR,M}
    d::T # search direction
    S::TR # change in x
    Y::TR # change in successive gradients
    α::M
    ρ::M
end
function TwoLoopVars(x, memory)
    d = copy(x)

    S = [copy(x) for i = 1:memory]
    Y = [copy(x) for i = 1:memory]
    α = similar(x, memory)
    ρ = similar(x, memory)
    TwoLoopVars(d, S, Y, α, ρ)
end
lbfgs_vars(method, x) = TwoLoopVars(x, method.memory)
function solve(
    problem::OptimizationProblem,
    x0,
    approach::LineSearch{<:LBFGS{<:Inverse,<:TwoLoop},<:Any,<:QNScaling},
    options::OptimizationOptions,
    cache = preallocate_qn_caches(mstyle(problem), x0),
)
    solve(problem, (x0, nothing), approach, options, cache)
end
function solve(
    problem::OptimizationProblem,
    s0::Tuple,
    approach::LineSearch{<:LBFGS{<:Inverse,<:TwoLoop},<:Any,<:QNScaling},
    options::OptimizationOptions,
    cache = preallocate_qn_caches(mstyle(problem), first(s0)),
)
    _solve(mstyle(problem), problem, s0, approach, options, cache)
end

function _solve(
    mstyle,
    problem::OptimizationProblem,
    s0::Tuple,
    approach::LineSearch{<:LBFGS{<:Inverse,<:TwoLoop},<:Any,<:QNScaling},
    options::OptimizationOptions,
    cache,
)
    t0 = time()

    #==============
         Setup
    ==============#
    x0, B0 = s0
    T = eltype(x0)
    # Remove B from all of this
    objvars = prepare_variables(problem, approach, x0, copy(x0), B0)

    qnvars = lbfgs_vars(modelscheme(approach), objvars.x)
    ∇f0 = norm(objvars.∇fz, Inf)
    f0 = objvars.fz
    P = initial_preconditioner(approach, x0)

    #========================
         First iteration
    ========================#
    objvars, qnvars, P =
        iterate(mstyle, 1, qnvars, objvars, P, approach, problem, problem, options)
    iter = 1
    # Check for gradient convergence
    is_converged = converged(approach, objvars, ∇f0, options)
    while iter <= options.maxiter && !any(is_converged)
        iter += 1
        # take a step and update approximation
        objvars, qnvars, P = iterate(
            mstyle,
            iter,
            qnvars,
            objvars,
            P,
            approach,
            problem,
            problem,
            options,
            false,
        )
        # Check for gradient convergence
        is_converged = converged(approach, objvars, ∇f0, options)
    end
    x, fx, ∇fx, z, fz, ∇fz, B, Pg = objvars
    return ConvergenceInfo(
        approach,
        (
            P = P,
            B = B,
            ρs = norm(x .- z),
            ρx = norm(x),
            solution = z,
            fx = fx,
            minimum = fz,
            ∇fz = ∇fz,
            f0 = f0,
            ∇f0 = ∇f0,
            iter = iter,
            time = time() - t0,
        ),
        options,
    )
end
function iterate(
    mstyle::InPlace,
    iter::Integer,
    qnvars::TwoLoopVars,
    objvars,
    P,
    approach::LineSearch{<:LBFGS{<:Inverse,<:TwoLoop},<:Any,<:QNScaling},
    prob::OptimizationProblem,
    problem::OptimizationProblem,
    options::OptimizationOptions,
    is_first = nothing,
)
    # split up the approach into the hessian approximation scheme and line search
    scheme, linesearch, K = modelscheme(approach), algorithm(approach), approach.scaling # make grabber and get a better name than K
    current_memory = min(iter - 1, scheme.memory)
    x, fx, ∇fx, z, fz, ∇fz, B, Pg = objvars
    Tf = typeof(fz)

    # Move nexts into currs
    fx = fz
    copyto!(x, z)
    copyto!(∇fx, ∇fz)

    # Update preconditioner
    P = update_preconditioner(scheme, x, P) # returns nothing????

    # Update current gradient and calculate the search direction
    d = find_direction!(scheme, copy(∇fz), qnvars, current_memory, K, P) # solve Bd = -∇fx
    φ = _lineobjective(mstyle, problem, ∇fz, z, x, d, fx, real(dot(∇fx, d))) # real is needed to convert complex dots to actually being real

    # Perform line search along d
    α, f_α, ls_success = find_steplength(mstyle, linesearch, φ, Tf(1))

    s = current_memory == length(qnvars.S) ? qnvars.S[1] : qnvars.S[1+current_memory]
    @. s = α * d
    z = retract(problem, z, x, s)

    # Update approximation
    fz, ∇fz, qnvars =
        update_obj!(problem, qnvars, α, x, ∇fx, z, ∇fz, current_memory, scheme)
    return (
        x = x,
        fx = fx,
        ∇fx = ∇fx,
        z = z,
        fz = fz,
        ∇fz = ∇fz,
        B = nothing,
        Pg = nothing,
    ),
    qnvars,
    P
end

function iterate(
    mstyle::OutOfPlace,
    cache,
    objvars,
    P,
    approach::LineSearch{<:LBFGS{<:Inverse,<:TwoLoop},<:Any,<:QNScaling},
    prob::OptimizationProblem,
    problem::OptimizationProblem,
    options::OptimizationOptions,
    is_first = nothing,
)
    # split up the approach into the hessian approximation scheme and line search
    scheme, linesearch = modelscheme(approach), algorithm(approach)
    # Move nexts into currs
    x, fx, ∇fx, z, fz, ∇fz, B, Pg = objvars
    Tf = typeof(fz)
    fx = fz
    x = copy(z)
    ∇fx = copy(∇fz)
    # Update preconditioner
    P = update_preconditioner(scheme, x, P)
    # Update current gradient and calculate the search direction
    d = find_direction(B, P, ∇fx, scheme) # solve Bd = -∇fx
    φ = _lineobjective(mstyle, problem, problem, ∇fz, z, x, d, fx, dot(∇fx, d))

    # # Perform line search along d
    α, f_α, ls_success = find_steplength(linesearch, φ, Tf(1))

    # # Calculate final step vector and update the state
    s = @. α * d
    z = retract(problem, z, x, s)

    # Update approximation
    fz, ∇fz, B = update_obj(problem, s, ∇fx, z, ∇fz, B, scheme, is_first)

    return x, fx, ∇fx, z, fz, ∇fz, B, P
end


using TupleVectors
using AbstractMCMC

using AbstractDifferentiation

struct IsCube end

struct IsFlat end

struct HasDeriv end
struct NoDeriv end

export sample

samplertype(::Type) = ArgumentError("samplertype not specified")

include(joinpath(@__DIR__, "pullbacks.jl"))

function AbstractMCMC.sample(post::Posterior, sampler::S, args...; init_params=nothing, kwargs...) where {S}
    θ0 = init_params
    if isnothing(init_params)
        θ0 = prior_sample(post)
    end
    return _sample(samplertype(S), post, sampler, args...; init_params=θ0, kwargs...)
end

function _sample(::IsFlat, post, sampler, args...; init_params, kwargs...)
    tpost = asflat(post)
    θ0 = HypercubeTransform.inverse(tpost, init_params)
    return sample(tpost, sampler, args...; init_params=θ0, kwargs...)
end

function _sample(::IsCube, post, sampler, args...; init_params, kwargs...)
    tpost = ascube(post)
    θ0 = HypercubeTransform.inverse(tpost, init_params)
    return sample(tpost, sampler, args...; init_params=θ0, kwargs...)
end


#-------------------------------------------------------------------
#* EMSO Model Library (EML) Copyright (C) 2004 - 2007 ALSOC.
#*
#* This LIBRARY is free software; you can distribute it and/or modify
#* it under the therms of the ALSOC FREE LICENSE as available at
#* http://www.enq.ufrgs.br/alsoc.
#*
#* EMSO Copyright (C) 2004 - 2007 ALSOC, original code
#* from http://www.rps.eng.br Copyright (C) 2002-2004.
#* All rights reserved.
#*
#* EMSO is distributed under the therms of the ALSOC LICENSE as
#* available at http://www.enq.ufrgs.br/alsoc.
#*--------------------------------------------------------------------
#* Author: Gerson Balbueno Bicca
#* $Id: DoublePipeIncr.mso								$
#*------------------------------------------------------------------
type DoublePipeIncr
	DoublePipeIncr()=begin
		PP=outers.PP
		NComp=outers.NComp
		new(
			DanaPlugin(Dict{Symbol,Any}(
				:Brief=>"External Physical Properties",
				:Type=>"PP"
			)),
			DanaInteger(Dict{Symbol,Any}(
				:Brief=>"Number of Components"
			)),
			DanaInteger(Dict{Symbol,Any}(
				:Brief=>"Number of zones",
				:Default=>2
			)),
			DanaInteger(Dict{Symbol,Any}(
				:Brief=>"Number of incremental points",
				:Default=>3
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Options for convergence Calculations ",
				:Valid=>["Simplified","Full"],
				:Default=>"Full"
			)),
			power(Dict{Symbol,Any}(
				:Brief=>"Estimated Duty",
				:Default=>70,
				:Lower=>1e-6,
				:Upper=>1e10
			)),
			fill(molweight(Dict{Symbol,Any}(
				:Brief=>"Component Mol Weight"
			)),(NComp)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Flow Direction",
				:Valid=>["counter","cocurrent"],
				:Default=>"cocurrent"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Flag for Fluid Alocation ",
				:Valid=>["outer","inner"],
				:Default=>"outer"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Inner Flow Regime ",
				:Valid=>["laminar","transition","turbulent"],
				:Default=>"laminar"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Outer Flow Regime ",
				:Valid=>["laminar","transition","turbulent"],
				:Default=>"laminar"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Laminar Flow for the Inner Side",
				:Valid=>["Hausen","Schlunder"],
				:Default=>"Hausen"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Transition Flow for the Inner Side",
				:Valid=>["Gnielinski","Hausen"],
				:Default=>"Gnielinski"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Turbulent Flow for the Inner Side",
				:Valid=>["Petukhov","SiederTate"],
				:Default=>"Petukhov"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Laminar Flow for the Outer Side",
				:Valid=>["Hausen","Schlunder"],
				:Default=>"Hausen"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Transition Flow for the OuterSide",
				:Valid=>["Gnielinski","Hausen"],
				:Default=>"Gnielinski"
			)),
			DanaSwitcher(Dict{Symbol,Any}(
				:Brief=>"Heat Transfer Correlation in Turbulent Flow for the Outer Side",
				:Valid=>["Petukhov","SiederTate"],
				:Default=>"Petukhov"
			)),
			constant(Dict{Symbol,Any}(
				:Brief=>"Pi Number",
				:Default=>3.14159265,
				:Symbol=>"\\pi"
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Outside Diameter of Inner Pipe",
				:Lower=>1e-6
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Inside Diameter of Inner Pipe",
				:Lower=>1e-10
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Inside Diameter of Outer pipe",
				:Lower=>1e-10
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Effective Tube Length of one segment of Pipe",
				:Lower=>0.1,
				:Symbol=>"L_{pipe}"
			)),
			conductivity(Dict{Symbol,Any}(
				:Brief=>"Tube Wall Material Thermal Conductivity",
				:Default=>1.0,
				:Symbol=>"K_{wall}"
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Inside Fouling Resistance",
				:Unit=>"m^2*K/kW",
				:Default=>1e-6,
				:Lower=>0
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Outside Fouling Resistance",
				:Unit=>"m^2*K/kW",
				:Default=>1e-6,
				:Lower=>0
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Inner Side Outlet Nozzle Diameter",
				:Default=>0.036,
				:Lower=>10e-6
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Inner Side Inlet Nozzle Diameter",
				:Default=>0.036,
				:Lower=>10e-6
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Outer Side Outlet Nozzle Diameter",
				:Default=>0.036,
				:Lower=>10e-6
			)),
			length(Dict{Symbol,Any}(
				:Brief=>"Outer Side Inlet Nozzle Diameter",
				:Default=>0.036,
				:Lower=>10e-6
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Inner Side Inlet Nozzle Pressure Loss Coeff",
				:Default=>1.1
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Inner Side Outlet Nozzle Pressure Loss Coeff",
				:Default=>0.7
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Outer Side Inlet Nozzle Pressure Loss Coeff",
				:Default=>1.1
			)),
			positive(Dict{Symbol,Any}(
				:Brief=>"Outer Side Outlet Nozzle Pressure Loss Coeff",
				:Default=>0.7
			)),
			stream(Dict{Symbol,Any}(
				:Brief=>"Inlet Inner Stream",
				:PosX=>0,
				:PosY=>0.5225,
				:Symbol=>"_{inInner}"
			)),
			stream(Dict{Symbol,Any}(
				:Brief=>"Inlet Outer Stream",
				:PosX=>0.2805,
				:PosY=>0,
				:Symbol=>"_{inOuter}"
			)),
			streamPH(Dict{Symbol,Any}(
				:Brief=>"Outlet Inner Stream",
				:PosX=>1,
				:PosY=>0.5225,
				:Symbol=>"_{outInner}"
			)),
			streamPH(Dict{Symbol,Any}(
				:Brief=>"Outlet Outer Stream",
				:PosX=>0.7264,
				:PosY=>1,
				:Symbol=>"_{outOuter}"
			)),
			Details_Main(Dict{Symbol,Any}(
				:Brief=>"Some Details in the Heat Exchanger",
				:Symbol=>" "
			)),
			Main_DoublePipe(Dict{Symbol,Any}(
				:Brief=>"Inner Side of the Heat Exchanger",
				:Symbol=>"_{Inner}"
			)),
			Main_DoublePipe(Dict{Symbol,Any}(
				:Brief=>"Outer Side of the Heat Exchanger",
				:Symbol=>"_{Outer}"
			)),
			fill(length(Dict{Symbol,Any}(
				:Brief=>"Incremental Tube Length",
				:Symbol=>"L_{incr}"
			)),(Npoints)),
			[
				:(Outer.Properties.Average.T(1:N) = 0.5*Outer.HeatTransfer.Tlocal(1:N) + 0.5*Outer.HeatTransfer.Tlocal(2:Npoints)),
				:(Inner.Properties.Average.T(1:N) = 0.5*Inner.HeatTransfer.Tlocal(1:N) + 0.5*Inner.HeatTransfer.Tlocal(2:Npoints)),
				:(Outer.Properties.Average.P(1:N) = 0.5*Outer.PressureDrop.Plocal(1:N) + 0.5*Outer.PressureDrop.Plocal(2:Npoints)),
				:(Inner.Properties.Average.P(1:N) = 0.5*Inner.PressureDrop.Plocal(1:N) + 0.5*Inner.PressureDrop.Plocal(2:Npoints)),
				:(Inner.Properties.Wall.Twall = 0.5*Outer.Properties.Average.T + 0.5*Inner.Properties.Average.T),
				:(Outer.Properties.Wall.Twall = 0.5*Outer.Properties.Average.T + 0.5*Inner.Properties.Average.T),
				:(Outer.Properties.Average.Mw = sum(M*InletOuter.z)),
				:(Inner.Properties.Average.Mw = sum(M*InletInner.z)),
				:(Inner.Properties.Inlet.rho = PP.LiquidDensity(InletInner.T,InletInner.P,InletInner.z)),
				:(Inner.Properties.Outlet.rho = PP.LiquidDensity(OutletInner.T,OutletInner.P,OutletInner.z)),
				:(Inner.Properties.Inlet.rho = PP.VapourDensity(InletInner.T,InletInner.P,InletInner.z)),
				:(Inner.Properties.Outlet.rho = PP.VapourDensity(OutletInner.T,OutletInner.P,OutletInner.z)),
				:(Outer.Properties.Inlet.rho = PP.LiquidDensity(InletOuter.T,InletOuter.P,InletOuter.z)),
				:(Outer.Properties.Outlet.rho = PP.LiquidDensity(OutletOuter.T,OutletOuter.P,OutletOuter.z)),
				:(Outer.Properties.Inlet.rho = PP.VapourDensity(InletOuter.T,InletOuter.P,InletOuter.z)),
				:(Outer.Properties.Outlet.rho = PP.VapourDensity(OutletOuter.T,OutletOuter.P,OutletOuter.z)),
				:(Inner.Properties.Average.Cp([1:N]) = PP.LiquidCp(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.rho([1:N]) = PP.LiquidDensity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.Mu([1:N]) = PP.LiquidViscosity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.K([1:N]) = PP.LiquidThermalConductivity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Wall.Mu([1:N]) = PP.LiquidViscosity(Inner.Properties.Wall.Twall([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.Cp([1:N]) = PP.VapourCp(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.rho([1:N]) = PP.VapourDensity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.Mu([1:N]) = PP.VapourViscosity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Average.K([1:N]) = PP.VapourThermalConductivity(Inner.Properties.Average.T([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Inner.Properties.Wall.Mu([1:N]) = PP.VapourViscosity(Inner.Properties.Wall.Twall([1:N]),Inner.Properties.Average.P([1:N]),InletInner.z)),
				:(Outer.Properties.Average.Cp([1:N]) = PP.LiquidCp(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.rho([1:N]) = PP.LiquidDensity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.Mu([1:N]) = PP.LiquidViscosity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.K([1:N]) = PP.LiquidThermalConductivity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Wall.Mu([1:N]) = PP.LiquidViscosity(Outer.Properties.Wall.Twall([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.Cp([1:N]) = PP.VapourCp(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.rho([1:N]) = PP.VapourDensity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.Mu([1:N]) = PP.VapourViscosity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Average.K([1:N]) = PP.VapourThermalConductivity(Outer.Properties.Average.T([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Outer.Properties.Wall.Mu([1:N]) = PP.VapourViscosity(Outer.Properties.Wall.Twall([1:N]),Outer.Properties.Average.P([1:N]),InletOuter.z)),
				:(Details.Q(1:N) = InletOuter.F*(Outer.HeatTransfer.Enth(1:N) - Outer.HeatTransfer.Enth(2:Npoints))),
				:(Details.Q(1:N) = InletOuter.F*(Outer.HeatTransfer.Enth(2:Npoints) - Outer.HeatTransfer.Enth(1:N))),
				:(Details.Q(1:N) = -InletInner.F*(Inner.HeatTransfer.Enth(1:N) - Inner.HeatTransfer.Enth(2:Npoints))),
				:(Details.Q(1:N) = InletInner.F*(Inner.HeatTransfer.Enth(1:N)-Inner.HeatTransfer.Enth(2:Npoints))),
				:(Details.Q(1:N) = -InletOuter.F*(Outer.HeatTransfer.Enth(1:N) - Outer.HeatTransfer.Enth(2:Npoints))),
				:(Details.Q(1:N) = -InletOuter.F*(Outer.HeatTransfer.Enth(2:Npoints) - Outer.HeatTransfer.Enth(1:N))),
				:(Inner.Properties.Inlet.Fw = sum(M*InletInner.z)*InletInner.F),
				:(Inner.Properties.Outlet.Fw = sum(M*OutletInner.z)*OutletInner.F),
				:(Outer.Properties.Inlet.Fw = sum(M*InletOuter.z)*InletOuter.F),
				:(Outer.Properties.Outlet.Fw = sum(M*OutletOuter.z)*OutletOuter.F),
				:(OutletOuter.F = InletOuter.F),
				:(OutletInner.F = InletInner.F),
				:(OutletOuter.z=InletOuter.z),
				:(OutletInner.z=InletInner.z),
				:(Details.A=Pi*DoInner*Lpipe),
				:(Lincr(1) = 0*"m"),
				:(Lincr([1:N]+1) = [1:N]*abs(Lpipe)/N),
				:(Inner.PressureDrop.fi([1:N])*Inner.PressureDrop.Re([1:N]) = 16),
				:((Inner.PressureDrop.fi([1:N])-0.0035)*(Inner.PressureDrop.Re([1:N])^0.42) = 0.264),
				:((Inner.PressureDrop.fi([1:N])-0.0035)*(Inner.PressureDrop.Re([1:N])^0.42) = 0.264),
				:(Outer.PressureDrop.fi([1:N])*Outer.PressureDrop.Re([1:N]) = 16),
				:((Outer.PressureDrop.fi([1:N])-0.0035)*(Outer.PressureDrop.Re([1:N])^0.42) = 0.264),
				:((Outer.PressureDrop.fi([1:N])-0.0035)*(Outer.PressureDrop.Re([1:N])^0.42) = 0.264),
				:(Inner.HeatTransfer.fi([1:N]) = 1/(0.79*ln(Inner.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Inner.HeatTransfer.Nu([1:N]) = 3.665 + ((0.19*((DiInner/Lpipe)*Inner.HeatTransfer.Re([1:N])*Inner.HeatTransfer.PR([1:N]))^0.8)/(1+0.117*((DiInner/Lpipe)*Inner.HeatTransfer.Re([1:N])*Inner.HeatTransfer.PR([1:N]))^0.467))),
				:(Inner.HeatTransfer.Nu([1:N]) = (49.027896+4.173281*Inner.HeatTransfer.Re([1:N])*Inner.HeatTransfer.PR([1:N])*(DiInner/Lpipe))^(1/3)),
				:(Inner.HeatTransfer.fi([1:N]) = 1/(0.79*ln(Inner.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Inner.HeatTransfer.Nu([1:N])*(1+(12.7*sqrt(0.125*Inner.HeatTransfer.fi([1:N]))*((Inner.HeatTransfer.PR([1:N]))^(2/3) -1))) = 0.125*Inner.HeatTransfer.fi([1:N])*(Inner.HeatTransfer.Re([1:N])-1000)*Inner.HeatTransfer.PR([1:N])),
				:(Inner.HeatTransfer.Nu([1:N]) =0.116*(Inner.HeatTransfer.Re([1:N])^(0.667)-125)*Inner.HeatTransfer.PR([1:N])^(0.333)*(1+(DiInner/Lpipe)^0.667)),
				:(Inner.HeatTransfer.fi([1:N]) = 1/(1.82*log(Inner.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Inner.HeatTransfer.Nu([1:N])*(1.07+(12.7*sqrt(0.125*Inner.HeatTransfer.fi([1:N]))*((Inner.HeatTransfer.PR([1:N]))^(2/3) -1))) = 0.125*Inner.HeatTransfer.fi([1:N])*Inner.HeatTransfer.Re([1:N])*Inner.HeatTransfer.PR([1:N])),
				:(Inner.HeatTransfer.Nu([1:N]) = 0.027*(Inner.HeatTransfer.PR([1:N]))^(1/3)*(Inner.HeatTransfer.Re([1:N]))^(4/5)),
				:(Inner.HeatTransfer.fi([1:N]) = 1/(1.82*log(Inner.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Outer.HeatTransfer.fi([1:N]) = 1/(0.79*ln(Outer.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Outer.HeatTransfer.Nu([1:N]) = 3.665 + ((0.19*((Outer.HeatTransfer.Dh/Lpipe)*Outer.HeatTransfer.Re([1:N])*Outer.HeatTransfer.PR([1:N]))^0.8)/(1+0.117*((Outer.HeatTransfer.Dh/Lpipe)*Outer.HeatTransfer.Re([1:N])*Outer.HeatTransfer.PR([1:N]))^0.467))),
				:(Outer.HeatTransfer.Nu([1:N]) = (49.027896+4.173281*Outer.HeatTransfer.Re([1:N])*Outer.HeatTransfer.PR([1:N])*(Outer.HeatTransfer.Dh/Lpipe))^(1/3)),
				:(Outer.HeatTransfer.fi([1:N]) = 1/(0.79*ln(Outer.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Outer.HeatTransfer.Nu([1:N])*(1+(12.7*sqrt(0.125*Outer.HeatTransfer.fi([1:N]))*((Outer.HeatTransfer.PR([1:N]))^(2/3) -1))) = 0.125*Outer.HeatTransfer.fi([1:N])*(Outer.HeatTransfer.Re([1:N])-1000)*Outer.HeatTransfer.PR([1:N])),
				:(Outer.HeatTransfer.Nu([1:N]) = 0.116*(Outer.HeatTransfer.Re([1:N])^(0.667)-125)*Outer.HeatTransfer.PR([1:N])^(0.333)*(1+(Outer.HeatTransfer.Dh/Lpipe)^0.667)),
				:(Outer.HeatTransfer.fi([1:N]) = 1/(0.79*ln(Outer.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Outer.HeatTransfer.fi([1:N]) = 1/(1.82*log(Outer.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Outer.HeatTransfer.Nu([1:N])*(1.07+(12.7*sqrt(0.125*Outer.HeatTransfer.fi([1:N]))*((Outer.HeatTransfer.PR([1:N]))^(2/3) -1))) = 0.125*Outer.HeatTransfer.fi([1:N])*Outer.HeatTransfer.Re([1:N])*Outer.HeatTransfer.PR([1:N])),
				:(Outer.HeatTransfer.Nu([1:N]) = 0.027*(Outer.HeatTransfer.PR([1:N]))^(1/3)*(Outer.HeatTransfer.Re([1:N]))^(4/5)),
				:(Outer.HeatTransfer.fi([1:N]) = 1/(1.82*log(Outer.HeatTransfer.Re([1:N]))-1.64)^2),
				:(Inner.HeatTransfer.hcoeff = (Inner.HeatTransfer.Nu*Inner.Properties.Average.K/DiInner)*Inner.HeatTransfer.Phi),
				:(Outer.HeatTransfer.hcoeff= (Outer.HeatTransfer.Nu*Outer.Properties.Average.K/Outer.HeatTransfer.Dh)*Outer.HeatTransfer.Phi),
				:(Outer.HeatTransfer.Phi = (Outer.Properties.Average.Mu/Outer.Properties.Wall.Mu)^0.14),
				:(Inner.HeatTransfer.Phi = (Inner.Properties.Average.Mu/Inner.Properties.Wall.Mu)^0.14),
				:(Outer.HeatTransfer.PR = ((Outer.Properties.Average.Cp/Outer.Properties.Average.Mw)*Outer.Properties.Average.Mu)/Outer.Properties.Average.K),
				:(Inner.HeatTransfer.PR = ((Inner.Properties.Average.Cp/Inner.Properties.Average.Mw)*Inner.Properties.Average.Mu)/Inner.Properties.Average.K),
				:(Outer.HeatTransfer.Re = (Outer.Properties.Average.rho*Outer.HeatTransfer.Vmean*Outer.HeatTransfer.Dh)/Outer.Properties.Average.Mu),
				:(Outer.PressureDrop.Re = (Outer.Properties.Average.rho*Outer.HeatTransfer.Vmean*Outer.PressureDrop.Dh)/Outer.Properties.Average.Mu),
				:(Inner.HeatTransfer.Re = (Inner.Properties.Average.rho*Inner.HeatTransfer.Vmean*Inner.HeatTransfer.Dh)/Inner.Properties.Average.Mu),
				:(Inner.PressureDrop.Re = Inner.HeatTransfer.Re),
				:(Outer.HeatTransfer.Vmean*(Outer.HeatTransfer.As*Outer.Properties.Average.rho) = Outer.Properties.Inlet.Fw),
				:(Inner.HeatTransfer.Vmean*(Inner.HeatTransfer.As*Inner.Properties.Average.rho) = Inner.Properties.Inlet.Fw),
				:(Details.Uc*((DoInner/(Inner.HeatTransfer.hcoeff*DiInner) )+(DoInner*ln(DoInner/DiInner)/(2*Kwall))+(1/(Outer.HeatTransfer.hcoeff)))=1),
				:(Details.Ud*(Rfi*(DoInner/DiInner) + Rfo + (DoInner/(Inner.HeatTransfer.hcoeff*DiInner) )+(DoInner*ln(DoInner/DiInner)/(2*Kwall))+(1/(Outer.HeatTransfer.hcoeff)))=1),
				:(Details.Qtotal = sum(Details.Q)),
				:(Details.Q = Details.Ud*Pi*DoInner*(Lpipe/N)*(Outer.Properties.Average.T - Inner.Properties.Average.T)),
				:(Details.Q = Details.Ud*Pi*DoInner*(Lpipe/N)*(Inner.Properties.Average.T - Outer.Properties.Average.T)),
				:(Details.Q = Qestimated/N),
				:(Inner.HeatTransfer.Enth([2:N]) = (1-InletInner.v)*PP.LiquidEnthalpy(Inner.HeatTransfer.Tlocal([2:N]), Inner.PressureDrop.Plocal([2:N]), InletInner.z) + InletInner.v*PP.VapourEnthalpy(Inner.HeatTransfer.Tlocal([2:N]), Inner.PressureDrop.Plocal([2:N]), InletInner.z)),
				:(Outer.HeatTransfer.Enth([2:N]) = (1-InletOuter.v)*PP.LiquidEnthalpy(Outer.HeatTransfer.Tlocal([2:N]), Outer.PressureDrop.Plocal([2:N]), InletOuter.z) + InletOuter.v*PP.VapourEnthalpy(Outer.HeatTransfer.Tlocal([2:N]), Outer.PressureDrop.Plocal([2:N]), InletOuter.z)),
				:(Inner.HeatTransfer.Enth(1) = InletInner.h),
				:(Inner.HeatTransfer.Enth(Npoints) = OutletInner.h),
				:(Inner.HeatTransfer.Tlocal(1) = InletInner.T),
				:(Inner.HeatTransfer.Tlocal(Npoints) = OutletInner.T),
				:(Inner.PressureDrop.Plocal(1) = InletInner.P),
				:(Inner.PressureDrop.Plocal(Npoints) = OutletInner.P),
				:(Outer.HeatTransfer.Enth(1) = InletOuter.h),
				:(Outer.HeatTransfer.Enth(Npoints) = OutletOuter.h),
				:(Outer.HeatTransfer.Tlocal(1) = InletOuter.T),
				:(Outer.HeatTransfer.Tlocal(Npoints) = OutletOuter.T),
				:(Outer.PressureDrop.Plocal(1) = InletOuter.P),
				:(Outer.PressureDrop.Plocal(Npoints) = OutletOuter.P),
				:(Outer.HeatTransfer.Enth(Npoints) = InletOuter.h),
				:(Outer.HeatTransfer.Enth(1) = OutletOuter.h),
				:(Outer.HeatTransfer.Tlocal(Npoints) = InletOuter.T),
				:(Outer.HeatTransfer.Tlocal(1) = OutletOuter.T),
				:(Outer.PressureDrop.Plocal(Npoints) = InletOuter.P),
				:(Outer.PressureDrop.Plocal(1) = OutletOuter.P),
				:(Outer.PressureDrop.Pdrop = Outer.PressureDrop.Pd_fric(Npoints)+Outer.PressureDrop.Pdnozzle_in+Outer.PressureDrop.Pdnozzle_out),
				:(Outer.PressureDrop.Pd_fric(2:Npoints) = (2*Outer.PressureDrop.fi*Lincr(2:Npoints)*Outer.Properties.Average.rho*Outer.HeatTransfer.Vmean^2)/(Outer.PressureDrop.Dh*Outer.HeatTransfer.Phi)),
				:(Outer.PressureDrop.Pd_fric(1) = 0*"kPa"),
				:(Outer.PressureDrop.Plocal([1:N]+1) = Outer.PressureDrop.Plocal(1) - Outer.PressureDrop.Pd_fric([1:N]+1)),
				:(Outer.PressureDrop.Pdrop = Outer.PressureDrop.Pd_fric(1)+Outer.PressureDrop.Pdnozzle_in+Outer.PressureDrop.Pdnozzle_out),
				:(Outer.PressureDrop.Pd_fric([1:N]) = (2*Outer.PressureDrop.fi([1:N])*Lincr(1+N-[1:N])*Outer.Properties.Average.rho([1:N])*Outer.HeatTransfer.Vmean([1:N])^2)/(Outer.PressureDrop.Dh*Outer.HeatTransfer.Phi([1:N]))),
				:(Outer.PressureDrop.Pd_fric(Npoints) = 0*"kPa"),
				:(Outer.PressureDrop.Plocal([1:N]) = Outer.PressureDrop.Plocal(Npoints) - Outer.PressureDrop.Pd_fric([1:N]+1)),
				:(Inner.PressureDrop.Pdrop = Inner.PressureDrop.Pd_fric(Npoints)+Inner.PressureDrop.Pdnozzle_in+Inner.PressureDrop.Pdnozzle_out),
				:(Inner.PressureDrop.Pd_fric(2:Npoints) = (2*Inner.PressureDrop.fi*Lincr(2:Npoints)*Inner.Properties.Average.rho*Inner.HeatTransfer.Vmean^2)/(DiInner*Inner.HeatTransfer.Phi)),
				:(Inner.PressureDrop.Pd_fric(1) = 0*"kPa"),
				:(Inner.PressureDrop.Plocal([1:N]+1) = Inner.PressureDrop.Plocal(1) - Inner.PressureDrop.Pd_fric([1:N]+1)),
				:(Inner.PressureDrop.Vnozzle_in = Inner.Properties.Inlet.Fw/(Inner.Properties.Inlet.rho*(0.25*Pi*Dinozzle_Inner^2))),
				:(Inner.PressureDrop.Vnozzle_out = Inner.Properties.Outlet.Fw/(Inner.Properties.Outlet.rho*(0.25*Pi*Donozzle_Inner^2))),
				:(Outer.PressureDrop.Vnozzle_in = Outer.Properties.Inlet.Fw/(Outer.Properties.Inlet.rho*(0.25*Pi*Dinozzle_Outer^2))),
				:(Outer.PressureDrop.Vnozzle_out = Outer.Properties.Outlet.Fw/(Outer.Properties.Outlet.rho*(0.25*Pi*Donozzle_Outer^2))),
				:(Inner.PressureDrop.Pdnozzle_in = 0.5*InnerKinlet*Inner.Properties.Inlet.rho*Inner.PressureDrop.Vnozzle_in^2),
				:(Inner.PressureDrop.Pdnozzle_out = 0.5*InnerKoutlet*Inner.Properties.Outlet.rho*Inner.PressureDrop.Vnozzle_out^2),
				:(Outer.PressureDrop.Pdnozzle_in = 0.5*OuterKinlet*Outer.Properties.Inlet.rho*Outer.PressureDrop.Vnozzle_in^2),
				:(Outer.PressureDrop.Pdnozzle_out = 0.5*OuterKoutlet*Outer.Properties.Outlet.rho*Outer.PressureDrop.Vnozzle_out^2),
				:(Inner.PressureDrop.RVsquare_in = Inner.Properties.Inlet.rho*(Inner.PressureDrop.Vnozzle_in)^2),
				:(Inner.PressureDrop.RVsquare_out = Inner.Properties.Outlet.rho*(Inner.PressureDrop.Vnozzle_out)^2),
				:(Outer.PressureDrop.RVsquare_in = Outer.Properties.Inlet.rho*(Outer.PressureDrop.Vnozzle_in)^2),
				:(Outer.PressureDrop.RVsquare_out = Outer.Properties.Outlet.rho*(Outer.PressureDrop.Vnozzle_out)^2),
			],
			[
				"Outer	Stream Average Temperature","Inner Stream Average Temperature","Outer Stream Average Pressure","Inner Stream Average Pressure","Inner Stream Wall Temperature","Outer Stream Wall Temperature","Outer Stream Average Molecular Weight","Inner Stream Average Molecular Weight","Inlet Mass Density Inner Stream","Outlet Mass Density Inner Stream","Inlet Mass Density Inner Stream","Outlet Mass Density Inner Stream","Inlet Mass Density Outer Stream","Outlet Mass Density Outer Stream","Inlet Mass Density Outer Stream","Outlet Mass Density Outer Stream","Average Heat Capacity Inner Stream","Average Mass Density Inner Stream","Average Viscosity Inner Stream","Average	Conductivity Inner Stream","Viscosity Inner Stream at wall temperature","Average Heat Capacity InnerStream","Average Mass Density Inner Stream","Average Viscosity Inner Stream","Average Conductivity Inner Stream","Viscosity Inner Stream at wall temperature","Average Heat Capacity Outer Stream","Average Mass Density Outer Stream","Average Viscosity Outer Stream","Average Conductivity Outer Stream","Viscosity Outer Stream at wall temperature","Average Heat Capacity Outer Stream","Average Mass Density Outer Stream","Average Viscosity Outer Stream","Average Conductivity Outer Stream","Viscosity Outer Stream at wall temperature","Energy Balance Outer Stream in cocurrent flow","Energy Balance Outer Stream in counter flow","Energy Balance Inner Stream","Energy Balance Hot Stream","Energy Balance Cold Stream in cocurrent flow","Energy Balance Cold Stream in counter flow","Flow Mass Inlet Inner Stream","Flow Mass Outlet Inner Stream","Flow Mass Inlet Outer Stream","Flow Mass Outlet Outer Stream","Molar Balance Outer Stream","Molar Balance Inner Stream","Outer Stream Molar Fraction Constraint","InnerStream Molar Fraction Constraint","Total Exchange Surface Area for one segment of pipe","Pipe Initial Length from Left to Right - OBS: Left: Always Inlet inner side","Incremental Length","Inner Side Friction Factor for Pressure Drop - laminar Flow","using Turbulent Flow - to be implemented","Inner Side Friction Factor - Turbulent Flow","Outer Side Friction Factor - laminar Flow","using Turbulent Flow - Transition Flow must be implemented","Outer Side Friction Factor - Turbulent Flow","Inner Side Friction Factor for Heat Transfer - laminar Flow","Nusselt Number","Nusselt Number","Inner Side Friction Factor for Heat Transfer - transition Flow","Nusselt Number","Nusselt Number","Inner Side Friction Factor for Heat Transfer - turbulent Flow","Nusselt Number","Nusselt Number","Inner Side Friction Factor for Heat Transfer - turbulent Flow","Outer Side Friction Factor for Heat Transfer - laminar Flow","Nusselt Number","Nusselt Number","Outer Side Friction Factor for Heat Transfer - transition Flow","Nusselt Number","Nusselt Number","Outer Side Friction Factor for Heat Transfer - transition Flow","Outer Side Friction Factor for Heat Transfer - turbulent Flow","Nusselt Number","Nusselt Number","Outer Side Friction Factor for Heat Transfer - turbulent Flow","Inner Pipe Film Coefficient","Outer Pipe Film Coefficient","Outer Pipe Phi correction","Inner Pipe Phi correction","Outer Pipe Prandtl Number","Inner Pipe Prandtl Number","Outer Pipe Reynolds Number for Heat Transfer","Outer Pipe Reynolds Number for Pressure Drop","Inner Pipe Reynolds Number for Heat Transfer","Inner Pipe Reynolds Number for Pressure Drop","Outer Pipe Velocity","Inner Pipe Velocity","Overall Heat Transfer Coefficient Clean","Overall Heat Transfer Coefficient Dirty","Total Duty","Incremental Duty","Incremental Duty","Duty","Incremental Enthalpy Inner Stream","Incremental Enthalpy Outer Stream","Enthalpy of Inner Side - Inlet Boundary","Enthalpy of inner Side - Outlet Boundary","Temperature of Inner Side - Inlet Boundary","Temperature of Inner Side - Outlet Boundary","Pressure of Inner Side - Inlet Boundary","Pressure of Inner Side - Outlet Boundary","Enthalpy of Outer Side - Inlet Boundary","Enthalpy of Outer Side - Outlet Boundary","Temperature of Outer Side - Inlet Boundary","Temperature of Outer Side - Outlet Boundary","Pressure of Outer Side - Inlet Boundary","Pressure of Outer Side - Outlet Boundary","Enthalpy of Outer Side - Inlet Boundary","Enthalpy of Outer Side - Outlet Boundary","Temperature of Outer Side - Inlet Boundary","Temperature of Outer Side - Outlet Boundary","Pressure of Outer Side - Inlet Boundary","Pressure of Outer Side - Outlet Boundary","Total Pressure Drop Outer Stream","Outer Pipe Pressure Drop for friction","Outer Pipe Pressure Drop for friction","Outer Pipe Local Pressure","Total Pressure Drop Outer Stream","Outer Pipe Pressure Drop for friction","Outer Pipe Pressure Drop for friction","Outer Pipe Local Pressure","Total Pressure Drop Inner Stream","Inner Pipe Pressure Drop for friction","Inner Pipe Pressure Drop for friction","Inner Pipe Local Pressure","Velocity Inner Side Inlet Nozzle","Velocity Inner Side Outlet Nozzle","Velocity Outer Side Inlet Nozzle","Velocity Outer Side Outlet Nozzle","Pressure Drop Inner Side Inlet Nozzle","Pressure Drop Inner Side Outlet Nozzle","Pressure Drop Outer Side Inlet Nozzle","Pressure Drop Outer Side Outlet Nozzle","Inner Side Inlet Nozzle rho-V^2","Inner Side Outlet Nozzle rho-V^2","Outer Side Inlet Nozzle rho-V^2","Outer Side Outlet Nozzle rho-V^2",
			],
			[:PP,:NComp,:N,:Npoints,:CalculationApproach,:Qestimated,:M,:FlowDirection,:HotSide,:innerFlowRegime,:outerFlowRegime,:InnerLaminarCorrelation,:InnerTransitionCorrelation,:InnerTurbulentCorrelation,:OuterLaminarCorrelation,:OuterTransitionCorrelation,:OuterTurbulentCorrelation,:Pi,:DoInner,:DiInner,:DiOuter,:Lpipe,:Kwall,:Rfi,:Rfo,:Donozzle_Inner,:Dinozzle_Inner,:Donozzle_Outer,:Dinozzle_Outer,:InnerKinlet,:InnerKoutlet,:OuterKinlet,:OuterKoutlet,],
			[:InletInner,:InletOuter,:OutletInner,:OutletOuter,:Details,:Inner,:Outer,:Lincr,]
		)
	end
	PP::DanaPlugin
	NComp::DanaInteger
	N::DanaInteger
	Npoints::DanaInteger
	CalculationApproach::DanaSwitcher
	Qestimated::power
	M::Array{molweight}
	FlowDirection::DanaSwitcher
	HotSide::DanaSwitcher
	innerFlowRegime::DanaSwitcher
	outerFlowRegime::DanaSwitcher
	InnerLaminarCorrelation::DanaSwitcher
	InnerTransitionCorrelation::DanaSwitcher
	InnerTurbulentCorrelation::DanaSwitcher
	OuterLaminarCorrelation::DanaSwitcher
	OuterTransitionCorrelation::DanaSwitcher
	OuterTurbulentCorrelation::DanaSwitcher
	Pi::constant
	DoInner::length
	DiInner::length
	DiOuter::length
	Lpipe::length
	Kwall::conductivity
	Rfi::positive
	Rfo::positive
	Donozzle_Inner::length
	Dinozzle_Inner::length
	Donozzle_Outer::length
	Dinozzle_Outer::length
	InnerKinlet::positive
	InnerKoutlet::positive
	OuterKinlet::positive
	OuterKoutlet::positive
	InletInner::stream
	InletOuter::stream
	OutletInner::streamPH
	OutletOuter::streamPH
	Details::Details_Main
	Inner::Main_DoublePipe
	Outer::Main_DoublePipe
	Lincr::Array{length}
	equations::Array{Expr,1}
	equationNames::Array{String,1}
	parameters::Array{Symbol,1}
	variables::Array{Symbol,1}
	attributes::Dict{Symbol,Any}
end
export DoublePipeIncr
function set(in::DoublePipeIncr)
	#"Number of incremental points"
	Npoints = N+1
	 #"Component Molecular Weight"
	M = PP.MolecularWeight()
	 #"Pi Number"
	Pi = 3.14159265
	 #"Inner Pipe Cross Sectional Area for Flow"
	Inner.HeatTransfer.As=0.25*Pi*DiInner*DiInner
	 #"Outer Pipe Cross Sectional Area for Flow"
	Outer.HeatTransfer.As=0.25*Pi*(DiOuter*DiOuter - DoInner*DoInner)
	 #"Inner Pipe Hydraulic Diameter for Heat Transfer"
	Inner.HeatTransfer.Dh=DiInner
	 #"Outer Pipe Hydraulic Diameter for Heat Transfer"
	Outer.HeatTransfer.Dh=(DiOuter*DiOuter-DoInner*DoInner)/DoInner
	 #"Inner Pipe Hydraulic Diameter for Pressure Drop"
	Inner.PressureDrop.Dh=DiInner
	 #"Outer Pipe Hydraulic Diameter for Pressure Drop"
	Outer.PressureDrop.Dh=DiOuter-DoInner

end
function setEquationFlow(in::DoublePipeIncr)
	addEquation(1)
	addEquation(2)
	addEquation(3)
	addEquation(4)
	addEquation(5)
	addEquation(6)
	addEquation(7)
	addEquation(8)
	if InletInner.v == 0
		addEquation(9)
		addEquation(10)
	else
		addEquation(11)
		addEquation(12)
	end
	if InletOuter.v == 0
		addEquation(13)
		addEquation(14)
	else
		addEquation(15)
		addEquation(16)
	end
	if InletInner.v == 0
		addEquation(17)
		addEquation(18)
		addEquation(19)
		addEquation(20)
		addEquation(21)
	else
		addEquation(22)
		addEquation(23)
		addEquation(24)
		addEquation(25)
		addEquation(26)
	end
	if InletOuter.v == 0
		addEquation(27)
		addEquation(28)
		addEquation(29)
		addEquation(30)
		addEquation(31)
	else
		addEquation(32)
		addEquation(33)
		addEquation(34)
		addEquation(35)
		addEquation(36)
	end
	let switch=HotSide
		if HotSide==InletInner.T > InletOuter.T
			set(switch,"inner")
		end
		if HotSide==InletInner.T < InletOuter.T
			set(switch,"outer")
		end
		if switch=="outer"
			let switch=FlowDirection
				if switch=="cocurrent"
					addEquation(37)
				elseif switch=="counter"
					addEquation(38)
				end
			end
			addEquation(39)
		elseif switch=="inner"
			addEquation(40)
			let switch=FlowDirection
				if switch=="cocurrent"
					addEquation(41)
				elseif switch=="counter"
					addEquation(42)
				end
			end
		end
	end
	addEquation(43)
	addEquation(44)
	addEquation(45)
	addEquation(46)
	addEquation(47)
	addEquation(48)
	addEquation(49)
	addEquation(50)
	addEquation(51)
	addEquation(52)
	addEquation(53)
	let switch=innerFlowRegime
		if innerFlowRegime==Inner.PressureDrop.Re(i) > 2300
			set(switch,"transition")
		end
		if innerFlowRegime==Inner.PressureDrop.Re(i) < 2300
			set(switch,"laminar")
		end
		if innerFlowRegime==Inner.PressureDrop.Re(i) > 10000
			set(switch,"turbulent")
		end
		if innerFlowRegime==Inner.PressureDrop.Re(i) < 10000
			set(switch,"transition")
		end
		if switch=="laminar"
			addEquation(54)
		elseif switch=="transition"
			addEquation(55)
		elseif switch=="turbulent"
			addEquation(56)
		end
	end
	let switch=outerFlowRegime
		if outerFlowRegime==Outer.PressureDrop.Re(i) > 2300
			set(switch,"transition")
		end
		if outerFlowRegime==Outer.PressureDrop.Re(i) < 2300
			set(switch,"laminar")
		end
		if outerFlowRegime==Outer.PressureDrop.Re(i) > 10000
			set(switch,"turbulent")
		end
		if outerFlowRegime==Outer.PressureDrop.Re(i) < 10000
			set(switch,"transition")
		end
		if switch=="laminar"
			addEquation(57)
		elseif switch=="transition"
			addEquation(58)
		elseif switch=="turbulent"
			addEquation(59)
		end
	end
	let switch=innerFlowRegime
		if innerFlowRegime==Inner.HeatTransfer.Re(i) > 2300
			set(switch,"transition")
		end
		if innerFlowRegime==Inner.HeatTransfer.Re(i) < 2300
			set(switch,"laminar")
		end
		if innerFlowRegime==Inner.HeatTransfer.Re(i) > 10000
			set(switch,"turbulent")
		end
		if innerFlowRegime==Inner.HeatTransfer.Re(i) < 10000
			set(switch,"transition")
		end
		if switch=="laminar"
			addEquation(60)
			let switch=InnerLaminarCorrelation
				if switch=="Hausen"
					addEquation(61)
				elseif switch=="Schlunder"
					addEquation(62)
				end
			end
		elseif switch=="transition"
			addEquation(63)
			let switch=InnerTransitionCorrelation
				if switch=="Gnielinski"
					addEquation(64)
				elseif switch=="Hausen"
					addEquation(65)
				end
			end
		elseif switch=="turbulent"
			let switch=InnerTurbulentCorrelation
				if switch=="Petukhov"
					addEquation(66)
					addEquation(67)
				elseif switch=="SiederTate"
					addEquation(68)
					addEquation(69)
				end
			end
		end
	end
	let switch=outerFlowRegime
		if outerFlowRegime==Outer.HeatTransfer.Re(i) > 2300
			set(switch,"transition")
		end
		if outerFlowRegime==Outer.HeatTransfer.Re(i) < 2300
			set(switch,"laminar")
		end
		if outerFlowRegime==Outer.HeatTransfer.Re(i) > 10000
			set(switch,"turbulent")
		end
		if outerFlowRegime==Outer.HeatTransfer.Re(i) < 10000
			set(switch,"transition")
		end
		if switch=="laminar"
			addEquation(70)
			let switch=OuterLaminarCorrelation
				if switch=="Hausen"
					addEquation(71)
				elseif switch=="Schlunder"
					addEquation(72)
				end
			end
		elseif switch=="transition"
			let switch=OuterTransitionCorrelation
				if switch=="Gnielinski"
					addEquation(73)
					addEquation(74)
				elseif switch=="Hausen"
					addEquation(75)
					addEquation(76)
				end
			end
		elseif switch=="turbulent"
			let switch=OuterTurbulentCorrelation
				if switch=="Petukhov"
					addEquation(77)
					addEquation(78)
				elseif switch=="SiederTate"
					addEquation(79)
					addEquation(80)
				end
			end
		end
	end
	addEquation(81)
	addEquation(82)
	addEquation(83)
	addEquation(84)
	addEquation(85)
	addEquation(86)
	addEquation(87)
	addEquation(88)
	addEquation(89)
	addEquation(90)
	addEquation(91)
	addEquation(92)
	addEquation(93)
	addEquation(94)
	addEquation(95)
	let switch=CalculationApproach
		if switch=="Full"
			let switch=HotSide
				if HotSide==InletInner.T > InletOuter.T
					set(switch,"inner")
				end
				if HotSide==InletInner.T < InletOuter.T
					set(switch,"outer")
				end
				if switch=="outer"
					addEquation(96)
				elseif switch=="inner"
					addEquation(97)
				end
			end
		elseif switch=="Simplified"
			addEquation(98)
		end
	end
	addEquation(99)
	addEquation(100)
	addEquation(101)
	addEquation(102)
	addEquation(103)
	addEquation(104)
	addEquation(105)
	addEquation(106)
	let switch=FlowDirection
		if switch=="cocurrent"
			addEquation(107)
			addEquation(108)
			addEquation(109)
			addEquation(110)
			addEquation(111)
			addEquation(112)
		elseif switch=="counter"
			addEquation(113)
			addEquation(114)
			addEquation(115)
			addEquation(116)
			addEquation(117)
			addEquation(118)
		end
	end
	let switch=FlowDirection
		if switch=="cocurrent"
			addEquation(119)
			addEquation(120)
			addEquation(121)
			# FIXME: NOZZLE PRESSURE DROP MUST BE ADDED
			addEquation(122)
		elseif switch=="counter"
			addEquation(123)
			addEquation(124)
			addEquation(125)
			# FIXME: NOZZLE PRESSURE DROP MUST BE ADDED
			addEquation(126)
		end
	end
	addEquation(127)
	addEquation(128)
	addEquation(129)
	# FIXME: NOZZLE PRESSURE DROP MUST BE ADDED
	addEquation(130)
	addEquation(131)
	addEquation(132)
	addEquation(133)
	addEquation(134)
	addEquation(135)
	addEquation(136)
	addEquation(137)
	addEquation(138)
	addEquation(139)
	addEquation(140)
	addEquation(141)
	addEquation(142)
end
function atributes(in::DoublePipeIncr,_::Dict{Symbol,Any})
	fields::Dict{Symbol,Any}=Dict{Symbol,Any}()
	fields[:Pallete]=true
	fields[:Icon]="icon/DoublePipe"
	fields[:Brief]="Incremental Double Pipe Heat Exchanger. "
	fields[:Info]="Incremental approach for a single double pipe heat exchanger. "
	drive!(fields,_)
	return fields
end
DoublePipeIncr(_::Dict{Symbol,Any})=begin
	newModel=DoublePipeIncr()
	newModel.attributes=atributes(newModel,_)
	newModel
end


function run_agent_simulation(simulation::AgentSimulation, simulation_years::Int64)

    total_horizon = get_total_horizon(get_case(simulation))
    rolling_horizon = get_rolling_horizon(get_case(simulation))

    installed_capacity =  zeros(simulation_years)

    capacity_forward_years = get_capacity_forward_years(simulation)

    # Set initial capacity market profits considering forward capacity auctions
    if get_markets(simulation)[:Capacity]
        initial_existing_projects = vcat(get_existing.(get_investors(simulation))...)
        initial_capacity_prices = [70000.0, 80000.0]              # initial capacity prices - arbitrarily selected here - #TODO: need some meachanism to generate these
        for y in 1:capacity_forward_years - 1
            for project in initial_existing_projects
                if get_end_life_year(project) >= y
                    for product in get_products(project)
                        update_initial_capacity_revenues!(project, product, initial_capacity_prices, y)
                    end
                end
            end
        end
    end

    sys_UC = get_system_UC(simulation)
    sys_ED = get_system_ED(simulation)

    investors = get_investors(simulation)

    average_capital_cost_multiplier = Statistics.mean(get_cap_cost_multiplier.(investors))

    services = get_system_services(sys_UC)

    for iteration_year = 1:simulation_years

        yearly_horizon = min(total_horizon - iteration_year + 1, rolling_horizon)

        println("Year $(iteration_year)")
        set_iteration_year!(simulation, iteration_year)

        active_projects = deepcopy(get_activeprojects(simulation))

        installed_capacity = update_installed_cap!(installed_capacity,
                                                   active_projects,
                                                   iteration_year,
                                                   simulation_years)

        create_investor_predictions(investors,
                                          active_projects,
                                          iteration_year,
                                          yearly_horizon,
                                          get_data_dir(get_case(simulation)),
                                          get_results_dir(simulation),
                                          average_capital_cost_multiplier,
                                          get_zones(simulation),
                                          get_lines(simulation),
                                          get_peak_load(simulation),
                                          get_solver(get_case(simulation)),
                                          get_parallel_investors(get_case(simulation)),
                                          get_parallel_scenarios(get_case(simulation))
                                          )

        for investor in investors
            run_investor_iteration(investor,
                                    active_projects,
                                    iteration_year,
                                    yearly_horizon,
                                    simulation_years,
                                    get_start_year(get_case(simulation)),
                                    capacity_forward_years,
                                    get_data_dir(get_case(simulation)),
                                    sys_UC,
                                    services,
                                    get_solver(get_case(simulation))
                                    )
        end

        update_simulation_derating_data!(simulation)

        #Get all existing projects to calculate realized profits for energy and REC markets.
        all_existing_projects = vcat(get_existing.(get_investors(simulation))...)

        # Get all projects which are expected to be online for the forward capacity market auction.
        capacity_market_year = iteration_year + capacity_forward_years - 1
        capacity_market_projects = Project[]

        for project in get_activeprojects(simulation)
            end_life_year = get_end_life_year(project)
            construction_year = get_construction_year(project)
            if end_life_year >= capacity_market_year && construction_year <= capacity_market_year
                push!(capacity_market_projects, project)
            end
        end

        installed_capacity = update_installed_cap!(installed_capacity,
                                                   all_existing_projects,
                                                   iteration_year,
                                                   simulation_years)

        println("Current Installed Capacity = $(round(installed_capacity[iteration_year])) MW")

        #Find which markets to simulate.
        markets = union(hcat(get_markets.(get_investors(simulation))...))

        #Create realzed market prices for existing projects.
        realized_market_prices,
        realized_capacity_factors,
        realized_reserve_up_perc,
        realized_reserve_down_perc,
        capacity_accepted_bids,
        rec_accepted_bids = create_realized_marketdata(simulation,
                                                             sys_UC,
                                                             sys_ED,
                                                             markets,
                                                             all_existing_projects,
                                                             capacity_market_projects,
                                                             capacity_forward_years,
                                                             iteration_year,
                                                             simulation_years,
                                                             get_solver(get_case(simulation)),
                                                             get_results_dir(simulation))

        existing_project_types = unique(get_type.(get_tech.(all_existing_projects)))

        #Update forecasts and realized profits of all existing projects for each investor.

        for investor in get_investors(simulation)
            if iteration_year < simulation_years
                update_forecast!(get_forecast(investor), get_annual_growth(simulation)[:, iteration_year], iteration_year)
            end

            projects = get_projects(investor)
            for (i, project) in enumerate(projects)

                update_realized_profits!(project,
                                         realized_market_prices,
                                         realized_capacity_factors,
                                         realized_reserve_up_perc,
                                         realized_reserve_down_perc,
                                         capacity_accepted_bids,
                                         rec_accepted_bids,
                                         get_hour_weight(simulation),
                                         iteration_year,
                                         capacity_forward_years)

                update_annual_cashflow!(project, iteration_year)

                retire_old!(projects,
                            i,
                            project,
                            sys_UC,
                            get_data_dir(get_case(simulation)),
                            iteration_year)

            end
        end

        println("COMPLETED YEAR $(iteration_year)")
    end

    final_portfolio = vcat(get_existing.(get_investors(simulation))...)

    for project in final_portfolio
        extrapolate_profits!(project, simulation_years)
    end

    FileIO.save(joinpath(get_results_dir(simulation), "simulation_data.jld2"), "simulation_data", simulation)

    return
end


import StatsBase:sample

"""
      ExperienceSampleModel <: AbstractSampleModel

Generate a turn sample based on previous experiences.
"""
mutable struct ExperienceSampleModel <: AbstractSampleModel
   experiences::Dict{Any, Dict{Any, NamedTuple{(:reward, :isdone, :nextstate), Tuple{Float64, Bool, Any}}}}
   sample_count::Int
   ExperienceSampleModel() = new(Dict{Any, Dict{Any, NamedTuple{(:reward, :isdone, :nextstate), Tuple{Float64, Bool, Any}}}}(), 0)
end

update!(m::ExperienceSampleModel, buffer, learner) = update!(m, buffer)

function update!(m::ExperienceSampleModel, buffer::EpisodeSARDBuffer)
   s, a, r, d, s′ = buffer.state[end-1], buffer.action[end-1], buffer.reward[end], buffer.isdone[end], buffer.state[end]
   if haskey(m.experiences, s)
         m.experiences[s][a] = (reward=r, isdone=d, nextstate=s′)
   else
         m.experiences[s] = Dict{Any, NamedTuple{(:reward, :isdone, :nextstate), Tuple{Float64, Bool, Any}}}(a => (reward=r, isdone=d, nextstate=s′))
   end
end

function sample(model::ExperienceSampleModel)
    s = rand(keys(model.experiences))
    a = rand(keys(model.experiences[s]))
    model.sample_count += 1
    s, a, model.experiences[s][a]...
end


module RLAlgorithms
    include("envs/RLEnvironments.jl")
    include("helpers/RLHelpers.jl")
    using Reexport
    @reexport using .RLHelpers
    @reexport using .RLEnvironments
    using StatsBase: sample, Weights

    export episode!, episodes!

    function episodes!(f, memory, env, steps, maxt)
        j = 0
        while j < steps
            j += episode!(f, memory, env, min(maxt, steps-j))
        end
    end

    function episode!(f, memory::ReplayMemory{S, A, R},
            env::Environment{S, B, R}, maxt) where {S, A, B, R}
        reset!(env)
        done = false
        t = 0
        while t < maxt && !done
            s = copy(env.current)
            a = f(s)
            _, r, done = step!(env, a)
            append!(memory, s, a, r, done)
            t += 1
        end
        return t
    end

    function episode!(f, episode::PGEpisode{S, A, R},
            env::Environment{S, B, R}, maxt) where {S, A, B, R}
        reset!(env)
        done = false
        for _ in 1:maxt-1
            s = copy(env.current)
            p, v = f(s)
            a = sample(Weights(p))
            _, r, done = step!(env, a)
            append!(episode, s, a, r, v)
            done && break
        end
        if !done
            s = env.current
            p, v = f(s)
            append!(episode, s, argmax(p), v, v)
        end
        nothing
    end

    function episode!(f, memory::PGReplayMemory{S, A, R},
            env::Environment{S, B, R}, maxt) where {S, A, B, R}
        t = 0
        done = false
        reset!(env)
        while t < maxt && !done
            s = copy(env.current)
            a, loga, v = f(s)
            _, r, done = step!(env, a)
            append!(memory, s, a, loga, r, v)
            t += 1
        end
        lastval = done ? zero(R) : f(env.current)[3]
        finish!(memory, t, lastval)
        return t
    end
end


@testset "Looks like in CoreModel, detailed test" begin
    cp = test_LP()
    @test isempty(filter(looks_like_exchange_reaction, reactions(cp)))

    cp = test_simpleLP()
    @test isempty(filter(looks_like_exchange_reaction, reactions(cp)))

    cp = CoreModel(
        [-1.0 -1 -2; 0 -1 0; 0 0 0],
        zeros(3),
        ones(3),
        ones(3),
        ones(3),
        ["EX_m1"; "r2"; "r3"],
        ["m1"; "m2"; "m3"],
    )
    @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1"]

    cp = CoreModel(
        [-1.0 0 0; 0 0 -1; 0 -1 0],
        zeros(3),
        ones(3),
        ones(3),
        ones(3),
        ["EX_m1"; "Exch_m3"; "Ex_m2"],
        ["m1"; "m2"; "m3_e"],
    )
    @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1", "Exch_m3", "Ex_m2"]
    @test filter(x -> looks_like_exchange_reaction(x; exchange_prefixes = ["Exch_"]), reactions(cp)) == ["Exch_m3"]

    # this is originally the "toyModel1.mat"
    cp = test_toyModel()

    @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1(e)", "EX_m3(e)", "EX_biomass(e)"]
    @test filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(cp)) == ["EX_m1(e)", "EX_m3(e)"]
    @test filter(looks_like_exchange_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"]
    @test filter(looks_like_biomass_reaction, reactions(cp)) == ["EX_biomass(e)", "biomass1"]
    @test filter(x -> looks_like_biomass_reaction(x; exclude_exchanges=true), reactions(cp)) == ["biomass1"]
end

@testset "Looks like functions, basic" begin
    model = load_model(model_paths["e_coli_core.json"])
    @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20
    @test length(filter(looks_like_exchange_metabolite, metabolites(model))) == 20
    @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1

    model = load_model(model_paths["e_coli_core.xml"])
    @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20
    @test length(filter(looks_like_exchange_metabolite, metabolites(model))) == 20
    @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1

    model = load_model(model_paths["e_coli_core.mat"])
    @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20
    @test length(filter(looks_like_exchange_metabolite, metabolites(model))) == 20
    @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1

    model = convert(StandardModel, model)
    @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20
    @test length(filter(looks_like_exchange_metabolite, metabolites(model))) == 20
    @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1

    model = convert(CoreModelCoupled, model)
    @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20
    @test length(filter(looks_like_exchange_metabolite, metabolites(model))) == 20
    @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1

end


#########################TimeSeries Data Updating###########################################
function get_simulation_time(sim::Simulation, stage_number::Int64)
    return sim.ref.date_ref[stage_number]
end

function parameter_update!(param_reference::UpdateRef{T},
                           param_array::JuMPParamArray,
                           stage_number::Int64,
                           sim::Simulation) where T <: PSY.Component

    devices = PSY.get_components(T, sim.stages[stage_number].sys)
    initial_forecast_time = get_simulation_time(sim, stage_number)
    horizon = length(model_time_steps(sim.stages[stage_number].canonical))
    for d in devices
        forecast = PSY.get_forecast(PSY.Deterministic,
                                    d,
                                    initial_forecast_time,
                                    "$(param_reference.access_ref)",
                                    horizon)
        ts_vector = TS.values(PSY.get_data(forecast))
        device_name = PSY.get_name(d)
        for (ix, val) in enumerate(param_array[device_name,:])
            value = ts_vector[ix]
            JuMP.fix(val, value)
        end
    end

    return

end

# This makes the choice in which variable to get from the results.
function _get_stage_variable(chron::Type{RecedingHorizon},
                           from_stage::_Stage,
                           device_name::String,
                           var_ref::UpdateRef,
                           to_stage_execution_count::Int64)

    variable = get_value(from_stage.canonical, var_ref)
    step = axes(variable)[2][1]
    return JuMP.value(variable[device_name, step])
end

function _get_stage_variable(chron::Type{Sequential},
                             from_stage::_Stage,
                             device_name::String,
                             var_ref::UpdateRef,
                             to_stage_execution_count::Int64)

    variable = get_value(from_stage.canonical, var_ref)
    step = axes(variable)[2][end]
    return JuMP.value(variable[device_name, step])
end

function _get_stage_variable(chron::Type{Synchronize},
                            from_stage::_Stage,
                            device_name::String,
                            var_ref::UpdateRef,
                            to_stage_execution_count::Int64)

    variable = get_value(from_stage.canonical, var_ref)
    step = axes(variable)[2][to_stage_execution_count + 1]
    return JuMP.value(variable[device_name, step])
end

################################Cache Update################################################
function _update_cache!(c::TimeStatusChange, stage::_Stage)
    parameter = get_value(stage.canonical, c.ref)

    for name in parameter.axes[1]
        param_status = PJ.value(parameter[name])
        if c.value[name][:status] == param_status
            c.value[name][:count] += 1.0
        elseif c.value[name][:status] != param_status
            c.value[name][:count] = 1.0
            c.value[name][:status] = param_status
        end
    end

    return
end

#########################FeedForward Variables Updating#####################################
function feedforward_update(synch::Chron,
                            param_reference::UpdateRef{JuMP.VariableRef},
                            param_array::JuMPParamArray,
                            to_stage::_Stage,
                            from_stage::_Stage) where Chron <: Chronology

    !(to_stage.execution_count % synch.to_steps == 0) && return

    var_count = to_stage.execution_count ÷ synch.to_steps

    for device_name in axes(param_array)[1]
        var_value = _get_stage_variable(Chron, from_stage, device_name, param_reference, var_count)
        PJ.fix(param_array[device_name], var_value)
    end

    return

end

#########################Initial Condition Updating#########################################
# TODO: Consider when more than one UC model is used for the stages that the counts need
# to be scaled.
function _calculate_ic_quantity(initial_condition_key::ICKey{TimeDurationOFF, PSD},
                                ic::InitialCondition,
                                var_value::Float64,
                                cache::Union{Nothing,AbstractCache}) where PSD <: PSY.Device

    name = device_name(ic)
    time_cache = cache_value(cache, name)

    current_counter = time_cache[:count]
    last_status = time_cache[:status]
    var_status = isapprox(var_value, 0.0, atol = 1e-4) ? 0.0 : 1.0
    @assert abs(last_status - var_status) < eps()

    if last_status >= 1.0
        return current_counter
    end

    if last_status < 1.0
        return 0.0
    end
end

function _calculate_ic_quantity(initial_condition_key::ICKey{TimeDurationON, PSD},
                                ic::InitialCondition,
                                var_value::Float64,
                                cache::Union{Nothing,AbstractCache}) where PSD <: PSY.Device

    name = device_name(ic)
    time_cache = cache_value(cache, name)

    current_counter = time_cache[:count]
    last_status = time_cache[:status]
    var_status = isapprox(var_value, 0.0, atol = 1e-4) ? 0.0 : 1.0
    @assert abs(last_status - var_status) < eps()

    if last_status >= 1.0
        return 0.0
    end

    if last_status < 1.0
        return current_counter
    end

end

function _calculate_ic_quantity(initial_condition_key::ICKey{DeviceStatus, PSD},
                                ic::InitialCondition,
                                var_value::Float64,
                                cache::Union{Nothing,AbstractCache}) where PSD <: PSY.Device
    return isapprox(var_value, 0.0, atol = 1e-4) ? 1.0 : 0.0
end

function _calculate_ic_quantity(initial_condition_key::ICKey{DevicePower, PSD},
                               ic::InitialCondition,
                               var_value::Float64,
                               cache::Union{Nothing,AbstractCache}) where PSD <: PSY.ThermalGen

    if isnothing(cache)
        status_change_to_on = value(ic) <= eps() && var_value >= eps()
        status_change_to_off = value(ic) >= eps() && var_value <= eps()
    else
        name = device_name(ic)
        time_cache = cache_value(cache, name)
        status_change_to_on = time_cache[:status] >= eps() && var_value <= eps()
        status_change_to_off = time_cache[:status] <= eps() && var_value >= eps()
    end

    if status_change_to_on
        return ic.device.tech.activepowerlimits.min
    end

    if status_change_to_off
        return 0.0
    end

    return var_value
end

function _initial_condition_update!(initial_condition_key::ICKey,
                                    synch::Chron,
                                    ini_cond_vector::Vector{InitialCondition},
                                    to_stage::_Stage,
                                    from_stage::_Stage) where Chron <: Chronology

    to_stage_execution_count = to_stage.execution_count
    for ic in ini_cond_vector
        name = device_name(ic)
        update_ref = ic.update_ref
        var_value = _get_stage_variable(Chron, from_stage, name, update_ref, to_stage_execution_count)
        cache = get(from_stage.cache, ic.cache, nothing)
        quantity = _calculate_ic_quantity(initial_condition_key, ic, var_value, cache)
        PJ.fix(ic.value, quantity)
    end

    return

end

function _initial_condition_update!(initial_condition_key::ICKey,
                                    ::Nothing,
                                    ini_cond_vector::Vector{InitialCondition},
                                    to_stage::_Stage,
                                    from_stage::_Stage)
        # Meant to do nothing
    return
end

#############################Interfacing Functions##########################################
function cache_update!(stage::_Stage)

    for (_, cache) in stage.cache
        _update_cache!(cache, stage)
    end

    return

end

"""Updates the forecast parameter value"""
function parameter_update!(param_reference::UpdateRef{JuMP.VariableRef},
                           param_array::JuMPParamArray,
                           stage_number::Int64,
                           sim::Simulation)

    chronology_ref = sim.stages[stage_number].chronology_ref
    current_stage = sim.stages[stage_number]
    for (k, ref) in chronology_ref
        feedforward_update(ref, param_reference, param_array, current_stage, sim.stages[k])
    end

    return

end

function intial_condition_update!(initial_condition_key::ICKey,
                                  ini_cond_vector::Vector{InitialCondition},
                                  stage_number::Int64,
                                  step::Int64,
                                  sim::Simulation)

    chronology_ref = nothing
    current_stage = sim.stages[stage_number]
    #checks if current stage is the first in the step and the execution is the first to
    # look backwards on the previous step
    intra_step_update = (stage_number == 1 && current_stage.execution_count == 0)
    #checks if current execution is the first execution to look into the previuous stage
    intra_stage_update = (stage_number > 1 && current_stage.execution_count == 0)
    #checks that the current run and stage ininital conditions is based on the current results
    inner_stage_update = (stage_number > 1 && current_stage.execution_count > 0)
    # makes the update based on the last stage.
    if intra_step_update
        from_stage = sim.stages[end]
        chronology_ref = sim.stages[stage_number].ini_cond_chron
    # Updates the next stage in the same step
    elseif intra_stage_update
        from_stage = sim.stages[stage_number-1]
        chronology_ref = sim.stages[stage_number].chronology_ref[stage_number-1]
    # Update is done on the current stage
    elseif inner_stage_update
        from_stage = current_stage
        chronology_ref = sim.stages[stage_number].ini_cond_chron
    else
        error("Condition not implemented")
    end
    _initial_condition_update!(initial_condition_key,
                               chronology_ref,
                               ini_cond_vector,
                               current_stage,
                               from_stage)

    return

end

function update_stage!(stage::_Stage{M}, step::Int64, sim::Simulation) where M<:AbstractOperationsProblem
    # Is first run of first stage? Yes -> do nothing
    (step == 1 && stage.key == 1 && stage.execution_count == 0) && return
    for (k, v) in stage.canonical.parameters
        parameter_update!(k, v, stage.key, sim)
    end

    cache_update!(stage)

    # Set initial conditions of the stage I am about to run.
    for (k, v) in stage.canonical.initial_conditions
        intial_condition_update!(k, v, stage.key, step, sim)
    end

    return

end


@testset "Print/show" begin
    # Test whether we can reconstruct the objects from their show values

    "Simulates a user printing an object in the REPL, copying and pasting the result and pressing enter"
    function show_and_eval(object)
        io = IOBuffer()
        show(io, object)
        io |> take! |> String |> Meta.parse |> eval
    end

    acoustic = Acoustic(1.89, 1.5, 2)
    circle = Circle(2.0)
    particle = Particle(acoustic, circle)
    simulation = FrequencySimulation([particle], plane_source(acoustic))

    @test acoustic == show_and_eval(acoustic)
    @test circle == show_and_eval(circle)
    @test particle == show_and_eval(particle)

    # Currently can't do this with FrequencySimulations because of Source type
    @test show(devnull, simulation) == nothing

end