# using CSV
# using Dates
# using Plots
# using JLD

## Main simulation function

function simulateODEs(model_def, simul_def)

    # First make a folder where to save the results
    cudi = pwd(); #@__DIR__;
    if !isdir(string(cudi,"\\Results"))
        mkdir(string(cudi,"\\Results"))
    end
    if !isdir(string(cudi, "\\Results\\", model_def["NameF"],"_",today()))
        mkdir(string(cudi, "\\Results\\", model_def["NameF"],"_",today()))
    end

    # Check which of the 2 types of simul_def have been given and make a choice on how to proceed
    if haskey(simul_def, "switchT")
        simul_def = checkStructSimul(model_def, simul_def)
    else
        simul_def = extractSimulCSV(model_def, simul_def)
    end

    # Make empty structure where the simulations will be
    simuls = Dict();

    # Get ODEs function
    ODEsFun = Symbol(string(join(model_def["NameF"]),"_SolveAll"));

    # loop over the different experiments to simulate
    for i in 1:simul_def["Nexp"]
        # Define simulation parameters
        global ts = collect(0.0:round(simul_def["finalTime"][i]))'; # 24 hour experiment
        global sp = [convert(Int,v) for v in (round.(simul_def["switchT"][i])')]; # 8 step experiment for now
        global ivss = simul_def["y0"][i]; # Experimental values for Y0 (IPTG, aTc, RFP, GFP)

        if model_def["Y0eqs"] != [] # ON inputs
            global pre = simul_def["preInd"][i];
        else
            global pre = [];
        end
        global samps = convert.(Int, round.(simul_def["tsamps"][i]));
        global theta = simul_def["theta"];

        # Inputs case
        if model_def["nInp"] != 0
            r = convert.(Int, 1:model_def["nInp"]:(length(simul_def["uInd"][i])));
            global inputs = zeros(convert.(Int,length(simul_def["uInd"][i])));
            for j in 1:convert.(Int,length(simul_def["uInd"][i])/model_def["nInp"])
                for k in 0:(model_def["nInp"]-1)
                    inputs[r[j]+k] = simul_def["uInd"][i][j,(k+1)];
                end
            end
        else
            r=0
        inputs=[];
        end

        simul = @eval $ODEsFun(ts, theta, sp, inputs, ivss, samps, pre);

        simuls[string("Exp_", i)] = simul;
    end

    simul_def["savepath"] = string(cudi, "\\Results\\", model_def["NameF"],"_",today());
    simul_def["savename"] = string(model_def["NameF"],"_",today(), "_SimulationResults_",simul_def["flag"],".jld");

    if simul_def["plot"] == true
        plotSimsODE(simuls,model_def,simul_def)
        println("")
        println("----------------------------------------- PLOTS -----------------------------------------")
        println("Simulation PLOTS are saved in the directory: ")
        println(string("                 ", simul_def["savepath"]))
        println(string("Under the name PlotSimulation_Exp(i)_",simul_def["flag"],".png"))
        println("--------------------------------------------------------------------------------------")
        println("")
    end

    save(string(simul_def["savepath"], "\\", simul_def["savename"]), "simuls", simuls, "model_def", model_def, "simul_def", simul_def)

    println("")
    println("----------------------------------------- RESULTS -----------------------------------------")
    println("Simulation results are saved in the directory: ")
    println(string("                 ", simul_def["savepath"]))
    println(string("Under the name ",simul_def["savename"]))
    println("--------------------------------------------------------------------------------------")
    println("")

    return simuls, model_def, simul_def

end

## Simulation Data structure Dict
function defSimulStruct()

    # The structure of things user should give
    simul_def = Dict()

    simul_def["Nexp"] = []; # Integer indicating the number of experiments to be simulated
    simul_def["finalTime"] = []; # -> Vector of final times for each simulation (initial time will allways be asumed as 0, so please consider that)
    simul_def["switchT"] = []; # Array with the switching times of the inducer in the simulation (time 0 and final time need to be considered)
    simul_def["y0"] = []; # Array of Y0s for the simulations for each experiment. If you are computing the steady state this vector might not be used, however you still need to introduce it
    simul_def["preInd"] = []; # Level of the inducer in the ON. This entry can be empty if no Steady State equations are given and only the Y0 given by the user is considered.
    simul_def["uInd"] = []; # Array with the values for the inducer for each experiment at each step
    simul_def["theta"] = []; # Vector/Matrix with the parameter samples or directory and file location of CSV file with them
    simul_def["tsamps"] = []; # Array of Sampling times vectors
    simul_def["plot"] = []; # Bollean or yes/no string to save the resulting simulations in the results directory (false will be considered as default)
    simul_def["flag"] = []; # String to attach a unique flag to the generated files so it is not overwritten. If empty, nothing will be added.

    return(simul_def)

end

# Check Simulations function
# Function to check the content of the fields the user has given
function checkStructSimul(model_def, simul_def)

    # Check taht all the dictionary entries are correct
    entries = ["Nexp", "finalTime", "switchT", "y0", "preInd", "uInd", "theta", "tsamps", "plot", "flag"]
    if symdiff(entries,keys(simul_def))!=[] && symdiff(entries,keys(simul_def)) != ["savepath", "savename"]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the entries of the dictionary, there is something wrong...")
        println(symdiff(entries,keys(simul_def)))
        return
    end

    # Check one to be sure there is no empty entry
    nee = ["Nexp", "finalTime", "switchT", "y0", "uInd", "theta", "tsamps"]; # No empty entries
    for i in 1:length(nee)
        if simul_def[nee[i]] == []
            println("-------------------------- Process STOPPED!!! --------------------------")
            println(string("Plese, check the field ", nee[i], ", this cannot be empty"))
            return
        end
    end

    # Checks to see if the content of the fields is the expected (also consier if single value entries have been given as a number or a vector)
    if ((typeof(simul_def["Nexp"])!=Array{Int,1}) || length(simul_def["Nexp"])!=1) && (typeof(simul_def["Nexp"])!=Int)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field Nexp! This should be an integer. ")
        return
    elseif (typeof(simul_def["finalTime"]) != Array{Int,1}) && (typeof(simul_def["finalTime"]) != Array{Float64,1}) && (typeof(simul_def["finalTime"]) != Array{Float32,1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field finalTime! This should be a vector of final times. ")
        return
    elseif (typeof(simul_def["switchT"]) != Array{Array{Int,1},1}) && (typeof(simul_def["switchT"]) != Array{Array{Float64,1},1}) && (typeof(simul_def["switchT"]) != Array{Array{Float32,1},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field switchT! This should be an array of arrays of numbers. ")
        return
    elseif (typeof(simul_def["y0"]) != Array{Array{Int,1},1}) && (typeof(simul_def["y0"]) != Array{Array{Float64,1},1}) && (typeof(simul_def["y0"]) != Array{Array{Float32,1},1}) &&
        (typeof(simul_def["y0"]) != Array{Array{Int,N} where N,1}) && (typeof(simul_def["y0"]) != Array{Array{Float64,N} where N,1}) && (typeof(simul_def["y0"]) != Array{Array{Float32,N} where N,1}) &&
        (typeof(simul_def["y0"]) != Array{Array{Int,2},1}) && (typeof(simul_def["y0"]) != Array{Array{Float64,2},1}) && (typeof(simul_def["y0"]) != Array{Array{Float32,2},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field y0! This should be an array of arrays of numbers. ")
        return
    elseif (typeof(simul_def["preInd"]) != Array{Array{Int,1},1}) && (typeof(simul_def["preInd"]) != Array{Array{Float64,1},1}) && (typeof(simul_def["preInd"]) != Array{Array{Float32,1},1}) && simul_def["preInd"] != []
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field preInd! This should be an array of arrays of numbers. ")
        return
    # elseif (typeof(simul_def["uInd"]) != Array{Array{Int,1},1}) && (typeof(simul_def["uInd"]) != Array{Array{Float64,1},1}) && (typeof(simul_def["uInd"]) != Array{Array{Float32,1},1})
    #     println("-------------------------- Process STOPPED!!! --------------------------")
    #     println("Please, check the field uInd! This should be an array of arrays of numbers. ")
    #     return
    elseif (typeof(simul_def["theta"]) != Array{Float64,1}) && (typeof(simul_def["theta"]) != Array{Float32,1}) &&
        (typeof(simul_def["theta"]) != Array{Int,1}) && (typeof(simul_def["theta"]) != Array{Int,1}) &&
        (typeof(simul_def["theta"]) != Array{Float64,2}) && (typeof(simul_def["theta"]) != Array{Float32,2}) &&
        (typeof(simul_def["theta"]) != Array{String,1}) && (typeof(simul_def["theta"]) != String)
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field theta! This should be a vector, matrix or string. ")
        return
    elseif (typeof(simul_def["tsamps"]) != Array{Array{Int,1},1}) && (typeof(simul_def["tsamps"]) != Array{Array{Float64,1},1}) && (typeof(simul_def["tsamps"]) != Array{Array{Float32,1},1})
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field tsamps! This should be an array of arrays of numbers. ")
        return
    elseif (simul_def["plot"] != [])
        try
            if (typeof(simul_def["plot"]) != Bool) && (typeof(simul_def["plot"]) != Array{Bool,1}) &&
            (typeof(simul_def["plot"]) != String) && (typeof(simul_def["plot"]) != Array{String,1})
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check the field plot! This should be true, fals, \"Yes\", \"yes\", \"No\", \"no\" or an empty array ")
                return
            end
        catch
             if (typeof(simul_def["plot"]) != Array{Bool,1}) &&
            (typeof(simul_def["plot"]) != Array{String,1})
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check the field plot! This should be true, fals, \"Yes\", \"yes\", \"No\", \"no\" or an empty array ")
                return
            end
        end
    elseif (typeof(simul_def["flag"]) != Array{String,1}) && (typeof(simul_def["flag"]) != String) && ((simul_def["flag"]) != [])
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the field flag! This should be a vector, matrix or string. ")
    return

    end

    # Extract necessary elements to ease generalisation
    if typeof(simul_def["Nexp"]) == Array{Int,1}
        simul_def["Nexp"] = simul_def["Nexp"][1];
    end

    if simul_def["plot"]==[true] || simul_def["plot"]==["Yes"] || simul_def["plot"]==["yes"] || simul_def["plot"]=="Yes" || simul_def["plot"]=="yes"
        simul_def["plot"]=true
    elseif simul_def["plot"]==[false] || simul_def["plot"]==["No"] || simul_def["plot"]==["no"] || simul_def["plot"]=="No" || simul_def["plot"]=="no" || simul_def["plot"]==[]
        simul_def["plot"]=false
    end

    if (typeof(simul_def["theta"]) == Array{String,1})
        simul_def["theta"] = simul_def["theta"][1];
    end

    if typeof(simul_def["flag"]) == Array{String,1}
        simul_def["flag"] = simul_def["flag"][1];
    elseif typeof(simul_def["flag"]) == String
        simul_def["flag"] = simul_def["flag"];
    elseif ((simul_def["flag"]) == [])
        simul_def["flag"] = "";
    end

    # Check that all the contents make sense
    if (simul_def["Nexp"] != length(simul_def["finalTime"])) || (simul_def["Nexp"] != length(simul_def["switchT"])) ||
        (simul_def["Nexp"] != length(simul_def["y0"])) || (simul_def["Nexp"] != length(simul_def["uInd"])) ||
        (simul_def["Nexp"] != length(simul_def["tsamps"]))
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, some field does not match the number of experiments selected. Check that the number of")
        println("entries for finalTime, switchT, y0, uInd or tsamps matches the number of experiments in Nexp.")
        return
    end

    if (model_def["Y0eqs"] != []) && simul_def["preInd"]==[]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("You specified computation of Y0 as steady-state but you have not introduced an inducer value for it!")
        return
    end

    if (typeof(simul_def["theta"]) == Array{Float64,1}) || (typeof(simul_def["theta"]) == Array{Float32,1})
        if length(simul_def["theta"]) != model_def["nPar"]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Number of parameters introduced does not match the specified")
            return
        end
    elseif (typeof(simul_def["theta"]) == Array{Float64,2}) || (typeof(simul_def["theta"]) == Array{Float32,2})
        if size(simul_def["theta"])[1] != model_def["nPar"] && size(simul_def["theta"])[2] != model_def["nPar"]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Number of parameters introduced does not match the specified")
            return
        end
    elseif (typeof(simul_def["theta"]) == String)
        if simul_def["theta"][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the theta file! And no spaces after!")
            return
        end
        if isfile(simul_def["theta"])
            simul_def["theta"] = Matrix(CSV.read(simul_def["theta"]));
        else
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Sorry, but the file path you introduced does not exist!")
            return
        end
    end

    for i in 1:simul_def["Nexp"]
        if simul_def["tsamps"][i][end] > simul_def["finalTime"][i]
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, check finalTime. You have selected a sampling point past it.")
            return
        end
        if model_def["nInp"] != 0
            if length(simul_def["uInd"][i]) != (length(simul_def["switchT"][i])-1)
                println("-------------------------- Process STOPPED!!! --------------------------")
                println("Please, check uInd and switchT. Number of steps does not match the number of values for the inputs.")
                return
            end
        end

        if length(simul_def["y0"][i])/model_def["nStat"] == 1
            if length(simul_def["y0"][i]) != model_def["nStat"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the number or states in experiment ", i))
                return
            end
        else
            if size(simul_def["y0"][i])[1] != model_def["nStat"] && size(simul_def["y0"][i])[2] != model_def["nStat"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the number or states in experiment ", i))
                return
            end
            if length(simul_def["y0"][i])/model_def["nStat"] != length(simul_def["theta"])/model_def["nPar"]
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but Y0 does not match the dimensions of theta "))
                return
            end
            if size(simul_def["y0"][i])[1] == size(simul_def["y0"][i])[2]
                println("-------------------------- WARNING --------------------------")
                println(string("Sorry, but the number of rows and columns of the y0 matrix in experiment ",i," is the same, so the checks on "))
                println("correct orientation will not work. Please make sure that the dimensions follow: ")
                println("y0[samples, states]")
            end
        end
    end

    # Check warning in case the matrix introduced is symmetric
    if length(size(simul_def["theta"])) == 2
        if size(simul_def["theta"])[1] == size(simul_def["theta"])[2]
            println("-------------------------- WARNING --------------------------")
            println("Sorry, but the number of rows and columns of the theta matrix is the same, so the checks on ")
            println("correct orientation will not work. Please make sure that the dimensions follow: ")
            println("theta[samples, parameters]")
        end
    end

    # Check that no step is no smaller than 2 unit of time
    for i in 1:simul_def["Nexp"]
        for j in 1:length(simul_def["uInd"][i])
            if (simul_def["switchT"][i][j+1]-simul_def["switchT"][i][j])<=4
                println("-------------------------- Process STOPPED!!! --------------------------")
                println(string("Sorry, but 2 of the steps in experiment ", i, " are too close. This package cannot "))
                println("handle this for now. We are working on it!")
                return
            end
        end
    end

    # Check that the inputs for the experiment are given as collumns
    try
        if model_def["nInp"]>1
            for i in 1:simul_def["Nexp"]
                if size(simul_def["uInd"][i])[2] != model_def["nInp"];
                    simul_def["uInd"][i] = simul_def["uInd"][i]';
                end
            end
        end
    catch
        println("-------------------------- Process STOPPED!!! --------------------------")
        println(string("Sorry, but there is some issue with the contents of the field uInd."))
        return
    end

    return(simul_def)
end

## CSV introduction description
function fileStructInfo()

    println("ALL THE FILES MUST BE CSV FILES!!!")
    println("")
    println("---------------------------------------------------------------------------------------------------------")
    println("")
    println("The observables file entry should have the following structure: ")
    println("Column 1: Sampling Times for the simulations")
    println("Column 2 + Number of states: Y0 value for each state (in order) in each column. ")
    println("           Value might need to be repeated across all the column, but only the first row will be considered")
    println("")
    println("---------------------------------------------------------------------------------------------------------")
    println("")
    println("The event inputs file entry should have the following structure: ")
    println("Column 1: Each Switching time (final time not considered)")
    println("Column 2: Final time (can be repeated for as many entrances as column 1 has)")
    println("Column 3 + number of inducers: Repeated column for the value of the inducers in the ON. ")
    println("              If no pre-inducer will be used in the simulations, any number could be used")
    println("Column 4 + number of inducers: Value of each inducer at each switching time.")
    println("")
    println("This is an Example of an experiment with 3 steps and 2 inducers: ")
    println(" |Switching|  |FinalTime|  |IPTGpre|  |aTcPre|  |  IPTG  |  |  aTc  |")
    println("      0           1350         1          0          1          0    ")
    println("     400          1350         1          0         0.3         50   ")
    println("     700          1350         1          0         0.7         35   ")
    println("     950          1350         1          0         0.1        100   ")
    println("")
    println("---------------------------------------------------------------------------------------------------------")
    println("")
    println("For the main directory, remember that in Julia you have to type \\ twice!")

end

## CSV introduction structure
function defSimulStructFiles()

    fileStructInfo()

    # The structure of things user should give
    simul_def = Dict()

    simul_def["ObservablesFile"] = []; # File containing information about the states Y0 and sampling times
    simul_def["EventInputsFile"] = []; # -> File containing all the information about the stimuli
    simul_def["theta"] = []; # Vector/Matrix with the parameter samples or directory and file location of CSV file with them
    simul_def["MainDir"] = []; # Directory path where all files are located (to avoid repetition). This can be empty.
    simul_def["plot"] = [];
    simul_def["flag"] = [];

    return(simul_def)

end

## Extract data from CSV
# Function to extract contents of CSV files if user sellects that
function extractSimulCSV(model_def, simul_def)

    # Check taht all the dictionary entries are correct
    entries = ["ObservablesFile", "EventInputsFile", "theta", "MainDir", "plot", "flag"]
    if symdiff(entries,keys(simul_def))!=[]
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Please, check the entries of the dictionary, there is something wrong...")
        println(symdiff(entries,keys(simul_def)))
        return
    end

    # Checks on the different strings introduced
    if simul_def["MainDir"] == [] || simul_def["MainDir"] == ""
        simul_def["MainDir"] = "";
    elseif typeof(simul_def["MainDir"])==Array{String,1}
        simul_def["MainDir"] = simul_def["MainDir"][1]
    elseif typeof(simul_def["MainDir"]) == String
        nothing
    else
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field MainDir")
        return
    end

    if typeof(simul_def["ObservablesFile"]) != Array{String,1}
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field ObservablesFile. This should be a Array{String,1}")
        return
    end

    if length(simul_def["ObservablesFile"]) != length(simul_def["EventInputsFile"])
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there number of entries for ObservablesFile and EventInputsFile should be the same")
        return
    end

    for i in 1:length(simul_def["ObservablesFile"])
        if simul_def["ObservablesFile"][i][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the ObservablesFile file! And no spaces after!")
            return
        end
    end

    if typeof(simul_def["EventInputsFile"]) != Array{String,1}
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, there is something wrong in the type of the field EventInputsFile. This should be a Array{String,1}")
        return
    end

    for i in 1:length(simul_def["EventInputsFile"])
        if simul_def["EventInputsFile"][i][end-3:end] != ".csv"
            println("-------------------------- Process STOPPED!!! --------------------------")
            println("Please, add the .csv termination to the EventInputsFile file! And no spaces after!")
            return
        end
    end

    simstu = defSimulStruct();

    simstu["theta"] = simul_def["theta"];

    tmpInp = Array{Any}(undef, 1,length(simul_def["EventInputsFile"]))
    for i in 1:length(simul_def["EventInputsFile"])
        if simul_def["MainDir"] != [] && simul_def["MainDir"] != ""
            tmpInp[i] = Matrix(CSV.read(string(simul_def["MainDir"], "\\\\",simul_def["EventInputsFile"][i])));
        else
            tmpInp[i] = Matrix(CSV.read(simul_def["EventInputsFile"][i]));
        end
    end

    tmpObs = Array{Any}(undef, 1,length(simul_def["ObservablesFile"]))
    for i in 1:length(simul_def["ObservablesFile"])
        if simul_def["MainDir"] != [] && simul_def["MainDir"] != ""
            tmpObs[i] = Matrix(CSV.read(string(simul_def["MainDir"], "\\\\",simul_def["ObservablesFile"][i])));
        else
            tmpObs[i] = Matrix(CSV.read(simul_def["ObservablesFile"][i]));
        end
    end

    try
        simstu["Nexp"] = length(simul_def["EventInputsFile"]);
        simstu["finalTime"] = [tmpInp[i][1,2] for i in 1:length(simul_def["EventInputsFile"])];
        simstu["switchT"] = [vcat(tmpInp[i][:,1], tmpInp[i][1,2]) for i in 1:length(simul_def["EventInputsFile"])];
        simstu["y0"] = [tmpObs[i][1,2:(1+model_def["nStat"])] for i in 1:length(simul_def["ObservablesFile"])];
        simstu["preInd"] = [tmpInp[i][1,3:(2+model_def["nInp"])] for i in 1:length(simul_def["EventInputsFile"])];
        simstu["uInd"] = [tmpInp[i][:,(3+model_def["nInp"]):(3+model_def["nInp"]+(model_def["nInp"]-1))] for i in 1:length(simul_def["EventInputsFile"])];
        simstu["theta"] = simul_def["theta"];
        simstu["tsamps"] = [tmpObs[i][:,1] for i in 1:length(simul_def["ObservablesFile"])];
        simstu["plot"] = simul_def["plot"];
        simstu["flag"] = simul_def["flag"];
    catch
        println("-------------------------- Process STOPPED!!! --------------------------")
        println("Sorry, but there seems to be some issue with the internal structure of the files you have given...")
        return
    end

    simstu = checkStructSimul(model_def, simstu);

    return(simstu)

end

## Plot simulations function
function plotSimsODE(simuls,model_def,simul_def)
    cudi = pwd(); # @__DIR__;
    for i in 1:simul_def["Nexp"]
        # Title for each subplot
        tit = "";
        yl1 = "";
        for k in 1:model_def["nStat"]
            tit = hcat(tit, string(model_def["stName"][k]));
            yl1 = hcat(yl1, "y");
        end
        tit = tit[:,2:end];
        yl1 = yl1[:,2:end];

        titu = "";
        yl2 = "";
        if model_def["nInp"] != 0
            for k in 1:model_def["nInp"]
                titu = hcat(titu, string(model_def["inpName"][k]));
                yl2 = hcat(yl2, "u");
            end
            titu = titu[:,2:end];
            yl2 = yl2[:,2:end];
        end

        tuu = hcat(tit, titu);
        yuu = hcat(yl1, yl2);

        # Elements for the inducers
        tu = Array{Array{Any,1}}(undef, model_def["nStat"]+model_def["nInp"])
        [tu[k] = [] for k in 1:model_def["nStat"]]
        su = Array{Array{Any,1}}(undef, model_def["nStat"]+model_def["nInp"])
        [su[k] = [] for k in 1:model_def["nStat"]]
        for k in 1:model_def["nInp"]
            tu[k+model_def["nStat"]] = round.(simul_def["switchT"][i]);
            su[k+model_def["nStat"]] = vcat(simul_def["uInd"][i][:,(k)], simul_def["uInd"][i][end,(k)])
        end

        t = round.(simul_def["tsamps"][i]);
        s = simuls[string("Exp_",i)];

        pl = plot(t, s[:,:,1], layout=model_def["nStat"]+model_def["nInp"], label = "", title = tit, xlabel = "time", ylabel = yuu,
        color = "blue", size = [2000, 1200])

        if model_def["nStat"]>1
            for p in 2:size(simuls[string("Exp_",i)])[3]
                pl = plot!(t,s[:,:,p], layout=model_def["nStat"], label = "", color = "blue");
            end
        end

        pl = plot!(tu, su, layout=model_def["nStat"]+model_def["nInp"], label = "", linetype=:step, title = tuu);

        savefig(string(cudi, "\\Results\\", model_def["NameF"],"_",today(), "\\PlotSimulation_Exp", i,"_", simul_def["flag"], ".png"));

    end

end


#using Ipopt: optimize!
using Base: Int64
using XLSX: length
#=using Pkg
Pkg.add("Tables")
Pkg.add("DataFrames")
Pkg.add("XLSX")
Pkg.add("ExcelReaders")
Pkg.add("JuMP")
Pkg.add("Ipopt")
=#
using XLSX, ExcelReaders, DataFrames, Tables, JuMP, Ipopt;
IOSource = XLSX.readdata("IO.xlsx", "io-table-5!A1:DV130");

#filepath cross system compatability code
if Sys.KERNEL === :NT || kern === :Windows
    pathmark = "\\"
else
    pathmark = "/"
end

#indexing vectors for initial data import groups
intermediaryTotalsCol = findall(x -> occursin("T4", x), string.(IOSource[3,:]));
intermediaryTotalsRow = findall(x -> occursin("T1", x), string.(IOSource[:,1]));
finalTotalsCol = findall(x -> occursin("T6", x), string.(IOSource[3,:]));
finalTotalsRow = findall(x -> occursin("Australian Production", x), string.(IOSource[:,2]));
finalDemandCol = findall(x -> occursin('Q', x), string.(IOSource[3,:]));
factorRow = findall(x -> occursin('P', x), string.(IOSource[:,1]));
IOSourceCol = vcat(intermediaryTotalsCol, finalDemandCol, finalTotalsCol);
IOSourceRow = vcat(intermediaryTotalsRow, factorRow, finalTotalsRow);
#initialising IO
IO = zeros(length(IOSourceRow), length(IOSourceCol));
#import numerical data into IO
IO[1:length(IOSourceRow), 1:length(IOSourceCol)] = IOSource[IOSourceRow, IOSourceCol];
#creating vectors of titles for IO
IOCodeRow = IOSource[IOSourceRow, 1];
IOCodeCol = IOSource[3, IOSourceCol];
IONameRow = IOSource[IOSourceRow, 2];
IONameCol = IOSource[2, IOSourceCol];

#code to sum public and private entities into one collumn
investment = findall(x -> occursin("Capital Formation", x), IONameCol);
IO[:, investment[1]]=sum(eachcol(IO[:, investment[1:2]]));
IO = IO[:,Not(investment[2])];
#alter title vectors accordingly (include Q in total investment collumn in IOcode)
IOCodeCol[investment[1]] = "Q3+Q4";
IOCodeCol = IOCodeCol[Not(investment[2])];
IONameCol[investment[1]] = "Private and Public Gross Fixed Capital Formation";
IONameCol = IONameCol[Not(investment[2])];
#creating a dictionary for the index of each collumn and row in IO by IOCode
IOColDict = Dict(IOCodeCol .=> [1:1:8;]);
IORowDict = Dict(IOCodeRow .=> [1:1:8;]);
IOCapForm = findall(x -> occursin("Capital Formation", x), IONameCol);
IOChangeInv = findall(x -> occursin("Changes in Inventories", x), IONameCol);

#importing relevant ASNA data for table 5
ASNAHouseCap = ExcelReaders.readxl("ASNAData"*pathmark*"5204039_Household_Capital_Account.xls", "Data1!A1:T71");
ASNANonFinCap = ExcelReaders.readxl("ASNAData"*pathmark*"5204018_NonFin_Corp_Capital_Account.xls", "Data1!A1:T71");
ASNAFinCap = ExcelReaders.readxl("ASNAData"*pathmark*"5204026_Fin_Corp_Capital_Account.xls", "Data1!A1:S71");
ASNAGovCap = ExcelReaders.readxl("ASNAData"*pathmark*"5204032_GenGov_Capital_Account.xls", "Data1!A1:AV71");
ASNAYearRow = findall(x -> occursin("2019", x), string.(ASNAHouseCap[:,1]));

#table 5
#creating table 5a - allocation of investment expenditure (broken into subsections for dict referencing purposes)
#subsection a is fixed capital expenditure
table5aNameCol = ["Households", "Non-Financial Corporations", "Financial Corporations", "General Government", "Total"];
table5aNameRow = ["Domestic Commodities", "Imported Commodities, complementary", "Imported Commodities, competing",
"Taxes less subsidies on products", "Other taxes less subsidies on investment", "Total indirect taxes",
"Total fixed capital expenditure"];
table5aRowDict = Dict(table5aNameRow .=> [1:1:length(table5aNameRow);]);
table5aColDict = Dict(table5aNameCol .=> [1:1:length(table5aNameCol);]);
table5a = zeros(length(table5aNameRow), length(table5aNameCol));

#filling in totals collumn from corresponding IO data
table5a[table5aRowDict["Domestic Commodities"], table5aColDict["Total"]] = sum(IO[IORowDict["T1"],IOCapForm]);
table5a[table5aRowDict["Imported Commodities, complementary"], table5aColDict["Total"]] = sum(IO[IORowDict["P5"],IOCapForm]);
table5a[table5aRowDict["Imported Commodities, competing"], table5aColDict["Total"]] = sum(IO[IORowDict["P6"],IOCapForm]);
table5a[table5aRowDict["Taxes less subsidies on products"], table5aColDict["Total"]] = sum(IO[IORowDict["P3"],IOCapForm]);
table5a[table5aRowDict["Other taxes less subsidies on investment"], table5aColDict["Total"]] = sum(IO[IORowDict["P4"],IOCapForm]);
table5aTaxes = findall(x -> occursin("taxes", lowercase(x)), table5aNameRow);
table5aTaxes = table5aTaxes[Not(3)];
table5a[table5aRowDict["Total indirect taxes"], table5aColDict["Total"]] = sum(table5a[table5aTaxes,table5aColDict["Total"]]);
table5a[table5aRowDict["Total fixed capital expenditure"], table5aColDict["Total"]] = sum(table5a[Not(table5aRowDict["Total indirect taxes"]),table5aColDict["Total"]]);

#creating index variables for the measurements that we want
ASNAHouseCapTotCapForm = findall(x -> occursin("Gross fixed capital formation ;", x), string.(ASNAHouseCap[1,:]));
ASNANonFinCapTotCapForm = findall(x -> occursin("Gross fixed capital formation ;", x), string.(ASNANonFinCap[1,:]));
ASNAFinCapTotCapForm = findall(x -> occursin("Gross fixed capital formation ;", x), string.(ASNAFinCap[1,:]));
ASNAGenGovCapTotCapForm = findall(x -> occursin("General government ;  Gross fixed capital formation ;", x), string.(ASNAGovCap[1,:]));

#filling in totals row from ASNA Data
table5a[table5aRowDict["Total fixed capital expenditure"], table5aColDict["Households"]] = first(ASNAHouseCap[ASNAYearRow, ASNAHouseCapTotCapForm]);
table5a[table5aRowDict["Total fixed capital expenditure"], table5aColDict["Non-Financial Corporations"]] = first(ASNANonFinCap[ASNAYearRow, ASNANonFinCapTotCapForm]);
table5a[table5aRowDict["Total fixed capital expenditure"], table5aColDict["Financial Corporations"]] = first(ASNAFinCap[ASNAYearRow, ASNAFinCapTotCapForm]);
table5a[table5aRowDict["Total fixed capital expenditure"], table5aColDict["General Government"]] = first(ASNAGovCap[ASNAYearRow, ASNAGenGovCapTotCapForm]);

#filling in non-total values
for ring in [1:1:length(table5aColDict)-1;];
    table5a[table5aRowDict["Domestic Commodities"],ring] = table5a[table5aRowDict["Total fixed capital expenditure"],ring]*IO[IORowDict["T1"],IOCapForm[1]] / IO[IORowDict[missing],IOCapForm[1]];
    table5a[table5aRowDict["Imported Commodities, complementary"],ring] = table5a[table5aRowDict["Total fixed capital expenditure"],ring]*IO[IORowDict["P5"],IOCapForm[1]] / IO[IORowDict[missing],IOCapForm[1]];
    table5a[table5aRowDict["Imported Commodities, competing"],ring] = table5a[table5aRowDict["Total fixed capital expenditure"],ring]*IO[IORowDict["P6"],IOCapForm[1]] / IO[IORowDict[missing],IOCapForm[1]];
    table5a[table5aRowDict["Taxes less subsidies on products"],ring] = table5a[table5aRowDict["Total fixed capital expenditure"],ring]*IO[IORowDict["P3"],IOCapForm[1]] / IO[IORowDict[missing],IOCapForm[1]];
    table5a[table5aRowDict["Other taxes less subsidies on investment"],ring] = table5a[table5aRowDict["Total fixed capital expenditure"],ring]*IO[IORowDict["P4"],IOCapForm[1]] / IO[IORowDict[missing],IOCapForm[1]];
    table5a[table5aRowDict["Total indirect taxes"], ring] = sum(table5a[table5aTaxes, ring]);
end

#creating table 5b - allocation of investment expenditure (broken into subsections for dict referencing purposes)
#subsection b is fixed capital expenditure
table5bNameCol = ["Households", "Non-Financial Corporations", "Financial Corporations", "General Government", "Total"];
table5bNameRow = ["Domestic Commodities", "Imported Commodities, complementary", "Imported Commodities, competing",
"Taxes less subsidies on products", "Total change in inventories"];
table5bRowDict = Dict(table5bNameRow .=> [1:1:length(table5bNameRow);]);
table5bColDict = Dict(table5bNameCol .=> [1:1:length(table5bNameCol);]);
table5b = zeros(length(table5bNameRow), length(table5bNameCol));

#filling in totals collumn from corresponding IO data
table5b[table5bRowDict["Domestic Commodities"], table5bColDict["Total"]] = sum(IO[IORowDict["T1"],IOChangeInv]);
table5b[table5bRowDict["Imported Commodities, complementary"], table5bColDict["Total"]] = sum(IO[IORowDict["P5"],IOChangeInv]);
table5b[table5bRowDict["Imported Commodities, competing"], table5bColDict["Total"]] = sum(IO[IORowDict["P6"],IOChangeInv]);
table5b[table5bRowDict["Taxes less subsidies on products"], table5bColDict["Total"]] = sum(IO[IORowDict["P3"],IOChangeInv]);
table5b[table5bRowDict["Total change in inventories"], table5bColDict["Total"]] = sum(table5b[:,table5bColDict["Total"]]);

#creating index variables for the measurements that we want
ASNAHouseCapChangeInv = findall(x -> occursin("Changes in inventories ;", x), string.(ASNAHouseCap[1,:]));
ASNANonFinCapChangeInv = findall(x -> occursin("Changes in inventories ;", x), string.(ASNANonFinCap[1,:]));
ASNAFinCapChangeInv = findall(x -> occursin("Changes in inventories ;", x), string.(ASNAFinCap[1,:]));
ASNAGenGovCapChangeInv = findall(x -> occursin("General government ;  Changes in inventories ;", x), string.(ASNAGovCap[1,:]));

#filling in totals row from ASNA Data
table5b[table5bRowDict["Total change in inventories"], table5bColDict["Households"]] = first(ASNAHouseCap[ASNAYearRow, ASNAHouseCapChangeInv]);
table5b[table5bRowDict["Total change in inventories"], table5bColDict["Non-Financial Corporations"]] = first(ASNANonFinCap[ASNAYearRow, ASNANonFinCapChangeInv]);
table5b[table5bRowDict["Total change in inventories"], table5bColDict["Financial Corporations"]] = first(ASNAFinCap[ASNAYearRow, ASNAFinCapChangeInv]);
table5b[table5bRowDict["Total change in inventories"], table5bColDict["General Government"]] = first(ASNAGovCap[ASNAYearRow, ASNAGenGovCapChangeInv]);

#calculate non-total values with lagrangian optimisation
table5bScalingFact = abs(minimum(table5b)) * 2;
mod5b = Model(Ipopt.Optimizer);
@variable(mod5b, x[1:(length(table5bNameRow)-1), 1:(length(table5bNameCol)-1)]);
@NLobjective(mod5b, Min, sum((x[i,j] - table5bScalingFact) ^ 2 for i in 1:(length(table5bNameRow)-1), j in 1:(length(table5bNameCol)-1)));
for i in 1:(length(table5bNameRow)-1);
    @constraint(mod5b, sum(x[:,i]) == table5b[table5bRowDict["Total change in inventories"],i]+table5bScalingFact);
end;
for i in 1:(length(table5bNameCol)-1);
    @constraint(mod5b, sum(x[i,:]) == table5b[i,table5bColDict["Total"]]+table5bScalingFact);
end;
optimize!(mod5b);

#plug back into table 5b
table5b[1:(length(table5bNameRow)-1),1:(length(table5bNameCol)-1)]=value.(x).-table5bScalingFact/4;

#creating table 5c - allocation of investment expenditure (broken into subsections for dict referencing purposes)
#subsection c is totals
table5cNameCol = ["Households", "Non-Financial Corporations", "Financial Corporations", "General Government", "Total"];
table5cNameRow = ["Domestic Commodities", "Imported Commodities", "Taxes less subsidies on products", "Other taxes less subsidies on investment", "Total investment expenditure"];
table5cRowDict = Dict(table5cNameRow .=> [1:1:length(table5cNameRow);]);
table5cColDict = Dict(table5cNameCol .=> [1:1:length(table5cNameCol);]);
table5c = zeros(length(table5cNameRow), length(table5cNameCol));

#do totals calcuations to get all values in 5c
table5c[table5cRowDict["Domestic Commodities"],:] = (table5a[table5aRowDict["Domestic Commodities"],:] +
table5b[table5bRowDict["Domestic Commodities"],:]);
table5c[table5cRowDict["Imported Commodities"],:] = sum(eachrow(table5a[[table5aRowDict["Imported Commodities, competing"],table5aRowDict["Imported Commodities, complementary"]],:] +
table5b[[table5bRowDict["Imported Commodities, competing"],table5bRowDict["Imported Commodities, complementary"]],:]));
table5c[table5cRowDict["Taxes less subsidies on products"],:] = table5a[table5aRowDict["Taxes less subsidies on products"],:];
table5c[table5cRowDict["Other taxes less subsidies on investment"],:] = (table5a[table5aRowDict["Other taxes less subsidies on investment"],:] +
table5b[table5bRowDict["Taxes less subsidies on products"],:]);
table5c[table5cRowDict["Total investment expenditure"],:] = (table5a[table5aRowDict["Total fixed capital expenditure"],:] +
table5b[table5bRowDict["Total change in inventories"],:]);

#table 6
#importing relevant ASNA data
ASNAHouseInc = ExcelReaders.readxl("ASNAData"*pathmark*"5204036_Household_Income_Account.xls", "Data1!A1:AN71");
ASNANonFinInc = ExcelReaders.readxl("ASNAData"*pathmark*"5204017_NonFin_Corp_Income_Account.xls", "Data1!A1:AE71");
ASNAFinInc = ExcelReaders.readxl("ASNAData"*pathmark*"5204025_Fin_Corp_Income_Account.xls", "Data1!A1:AD71");
ASNAGovInc = ExcelReaders.readxl("ASNAData"*pathmark*"5204030_GenGov_Income_Account.xls", "Data1!A1:DA71");
ASNAExtInc = ExcelReaders.readxl("ASNAData"*pathmark*"5204043_External_Accounts.xls", "Data1!A1:AI71");
#initialising table
table6Name = ["Households", "Non-Financial Corporations", "Financial Corporations", "General Government", "External", "Total"];
table6Dict = Dict(table6Name .=> [1:1:length(table6Name);]);
table6 = zeros(length(table6Name),length(table6Name));
#allocating total collumn and row data
table6[table6Dict["Total"],table6Dict["Households"]] = (first(ASNAHouseInc[ASNAYearRow,findall(x -> occursin("receivable - Interest", x), string.(ASNAHouseInc[1,:]))])
+first(ASNAHouseInc[ASNAYearRow,findall(x -> occursin("receivable - Imputed interest", x), string.(ASNAHouseInc[1,:]))]));
table6[table6Dict["Total"],table6Dict["Non-Financial Corporations"]] = (first(ASNANonFinInc[ASNAYearRow,findall(x -> occursin("receivable - Interest", x), string.(ASNANonFinInc[1,:]))])
+first(ASNANonFinInc[ASNAYearRow,findall(x -> occursin("Property income attributed to insurance policyholders", x), string.(ASNANonFinInc[1,:]))]));
table6[table6Dict["Total"],table6Dict["Financial Corporations"]] = first(ASNAFinInc[ASNAYearRow,findall(x -> occursin("receivable - Interest", x), string.(ASNAFinInc[1,:]))]);
table6[table6Dict["Total"],table6Dict["General Government"]] = first(ASNAGovInc[ASNAYearRow,findall(x -> occursin("General government ;  Property income receivable - Interest ;", x), string.(ASNAGovInc[1,:]))]);
table6[table6Dict["Total"],table6Dict["External"]] = first(ASNAExtInc[ASNAYearRow,findall(x -> occursin("receivable - Interest", x), string.(ASNAExtInc[1,:]))]);

table6[table6Dict["Households"],table6Dict["Total"]] = sum(ASNAHouseInc[ASNAYearRow,findall(x -> occursin("Property income payable - Interest", x), string.(ASNAHouseInc[1,:]))]);
table6[table6Dict["Non-Financial Corporations"],table6Dict["Total"]] = first(ASNANonFinInc[ASNAYearRow,findall(x -> occursin("Property income payable - Interest", x), string.(ASNANonFinInc[1,:]))]);
table6[table6Dict["Financial Corporations"],table6Dict["Total"]] = (first(ASNAFinInc[ASNAYearRow,findall(x -> occursin("Property income payable - Interest", x), string.(ASNAFinInc[1,:]))])
+first(ASNAFinInc[ASNAYearRow,findall(x -> occursin("payable - Property income attributed to insurance policy holders", x), string.(ASNAFinInc[1,:]))]));
table6[table6Dict["General Government"],table6Dict["Total"]] = first(ASNAGovInc[ASNAYearRow,findall(x -> occursin("General government ;  Property income payable - Total interest ;", x), string.(ASNAGovInc[1,:]))]);
table6[table6Dict["External"],table6Dict["Total"]] = first(ASNAExtInc[ASNAYearRow,findall(x -> occursin("Property income payable - Interest", x), string.(ASNAExtInc[1,:]))]);

if 0.98*sum(table6[:,length(table6Name)])<sum(table6[length(table6Name),:])<1.02*sum(table6[:,length(table6Name)])
else
    error("Large discrepancy in row and collumn total sums in table 6")
end

#=solve missing values with Ipopt
mod6 = Model(Ipopt.Optimizer);
@variable(mod6, x[1:(length(table6Name)-1), 1:(length(table6Name)-1)]>=0);
@NLobjective(mod6, Min, sum((x[i,j]) ^ 2 for i in 1:(length(table6Name)-1), j in 1:(length(table6Name)-1)));
for i in 1:(length(table6Name)-1);
    @constraint(mod6, sum(x[:,i]) == table6[table6Dict["Total"],i]-sum(table6[1:(length(table6Name)-1),i]));
end;
for i in 1:(length(table6Name)-1);
    @constraint(mod6, sum(x[i,:]) == table6[i,table6Dict["Total"]]-sum(table6[i,1:(length(table6Name)-1)]));
end;
for i in 1:(length(table6Name)-1);
    @constraint(mod6, x[i,table6Dict["General Government"]] == 0);
end;
@constraint(mod6, x[table6Dict["Households"],table6Dict["Households"]] == 0);
@constraint(mod6, x[table6Dict["External"],table6Dict["Households"]] == 0);
@constraint(mod6, x[table6Dict["Households"],table6Dict["External"]] == 0);
@constraint(mod6, x[table6Dict["External"],table6Dict["External"]] == 0);
optimize!(mod6);

table6[1:(length(table6Name)-1), 1:(length(table6Name)-1)] = table6[1:(length(table6Name)-1), 1:(length(table6Name)-1)] + value.(x);
=#

#solve for missing values with scaling
table6Step3 = zeros(length(table6Name),length(table6Name));
table6Step3Row = [table6Dict["Non-Financial Corporations"],table6Dict["Financial Corporations"],table6Dict["General Government"]];
table6Step3Col = [table6Dict["Households"],table6Dict["External"]];
for i in table6Step3Col;
    for ring in table6Step3Row;
        table6Step3[ring,i] = (table6[table6Dict["Total"],i]-sum(table6[1:(length(table6Name)-1),i]))*(
            table6[ring,table6Dict["Total"]]-sum(table6[ring,1:(length(table6Name)-1)]))/sum(table6[
            table6Step3Row,table6Dict["Total"]]-sum(eachcol(table6[table6Step3Row,1:(length(table6Name)-1)])));
    end
end
table6 = table6+table6Step3;

table6Step4 = zeros(length(table6Name),length(table6Name));
table6Step4Row = [1:1:(length(table6Name)-1);];
table6Step4Col = [table6Dict["Non-Financial Corporations"],table6Dict["Financial Corporations"]];
for i in table6Step4Col;
    for ring in table6Step4Row;
        table6Step4[ring,i] = (table6[table6Dict["Total"],i]-sum(table6[1:(length(table6Name)-1),i]))*(
            table6[ring,table6Dict["Total"]]-sum(table6[ring,1:(length(table6Name)-1)]))/sum(table6[
            table6Step4Row,table6Dict["Total"]]-sum(eachcol(table6[table6Step4Row,1:(length(table6Name)-1)])));
    end
end
table6 = table6+table6Step4;

#=convert dataframe to dictionary
function increment!( d::Dict{S, T}, k::S, i::T) where {T<:Real, S<:Any}
    if haskey(d, k)
        d[k] += i
    else
        d[k] = i
    end
end
increment!(d::Dict{S, T}, k::S ) where {T<:Real, S<:Any} = increment!( d, k, one(T))

function df2dict( df::DataFrame, key_col::Symbol, val_col::Symbol=:null)
    keytype = typeof(df[1,key_col])
    if val_col == :null
        valtype = Int
    else
        valtype = typeof(df[1,val_col])
    end
    D = Dict{keytype, valtype}()
    for i=1:size(df,1)
        if !ismissing(df[i,key_col])
            if val_col == :null
                increment!( D, df[i,key_col] )
            elseif valtype <: Real
                increment!( D, df[i,key_col], df[i,val_col] )
            else
                if haskey(D, df[i,key_col])
                    @warn("non-unique entry: $(df[i,key_col])")
                else
                    D[df[i,key_col]] = df[i,val_col]
                end
            end
        end
    end
    return D
end
df[!, "IOCode"]=IOcode
insertcols!(df, 2, :name => vector)
D = df2dict(df, :IOCode, :x3)
=#


function place_grid_order!(config, trade_grid, price_str, client, bot_path)
    #
    ## Test Bot?
    test_bot = config["testing"] == "1" ? true : false

    info = trade_grid[price_str]

    if info["side"] == client.SIDE_BUY
        #
        ## Calculate BUY qty depending on thes. volume
        qty = Pinguu.get_buy_value(config, trade_grid) / parse(Float64, price_str)
        qty = floor(qty, digits=config["precision"])
    else
        #
        ## Get SELL qty
        qty = typeof(info["qty"]) != Float64 ? parse(Float64, info["qty"]) : info["qty"]
    end
    tmp = Dict(
        "status" => "check",
        "side" => info["side"],
        "price" => price_str,
        "type" => info["type"],
        "qty" => qty
    )

    current_volume = Pinguu.get_current_volume(config)

    if info["side"] == client.SIDE_BUY
        tmp["current_volume"] = current_volume
    end
    #
    ## Sell of extra coin volume, take buy_order
    if info["type"] == "normal" && info["side"] == client.SIDE_SELL
        tmp["buy_order"] = info["buy_order"]
        if haskey(info, "current_volume")
            tmp["current_volume"] = info["current_volume"]
        else
            tmp["current_volume"] = current_volume
        end
    end

    if test_bot
        #
        ## PLACE BUY TESTING
        order = try client.create_test_order(
                symbol=config["symbol"],
                side=info["side"],
                type=client.ORDER_TYPE_LIMIT,
                timeInForce=client.TIME_IN_FORCE_GTC,
                quantity=tmp["qty"],
                price=tmp["price"]
            )
            catch
            return
        end
        tmp["order"] = Dict(
            "price" => price_str,
            "orderId" => 42,
            "executedQty" => "0.00",
            "origQty" => tmp["qty"],
            "side" => tmp["side"],
            "type" => "limit",
            "symbol" => config["symbol"],
            "status" => "NEW",
            "cummulativeQuoteQty" => "0.00000000",
            "executedQty" => "0.00",
            "time" => string(now())
        )
        ##
        #
    else
        #
        ## PLACE ORDER
        if tmp["side"] == client.SIDE_BUY
            order = try client.order_limit_buy(
                    symbol=config["symbol"],
                    quantity=tmp["qty"],
                    price=tmp["price"]
                );
            catch
                return
            end
        else
            order = try client.order_limit_sell(
                    symbol=config["symbol"],
                    quantity=tmp["qty"],
                    price=tmp["price"]
                );
            catch
                return
            end
        end
        tmp["order"] = order
        tmp["order"]["time"] = string(now())
        ##
        #
    end

    trade_grid[price_str] = tmp
    open(joinpath(bot_path, "trade_grid.json"), "w") do f
        JSON.print(f, trade_grid, 4)
    end
end
export place_grid_order!


@doc raw"""
    add_variable(psi_container::PSIContainer,
                      devices::D,
                      var_name::Symbol,
                      binary::Bool,
                      expression_name::Symbol,
                      sign::Float64)

Adds a variable to the optimization model and to the affine expressions contained
in the psi_container model according to the specified sign. Based on the inputs, the variable can
be specified as binary.

# Bounds

``` lb_value_function <= varstart[name, t] <= ub_value_function ```

If binary = true:

``` varstart[name, t] in {0,1} ```

# LaTeX

``  lb \ge x^{device}_t \le ub \forall t ``

``  x^{device}_t \in {0,1} \forall t iff \text{binary = true}``

# Arguments
* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* devices : Vector or Iterator with the devices
* var_name::Symbol : Base Name for the variable
* binary::Bool : Select if the variable is binary
* expression_name::Symbol : Expression_name name stored in psi_container.expressions to add the variable
* sign::Float64 : sign of the addition of the variable to the expression_name. Default Value is 1.0

# Accepted Keyword Arguments
* ub_value_function : Provides the function over device to obtain the value for a upper_bound
* lb_value_function : Provides the function over device to obtain the value for a lower_bound. If the variable is meant to be positive define lb = x -> 0.0
* initial_value_function : Provides the function over device to obtain the warm start value

"""
function add_variable(
    psi_container::PSIContainer,
    devices::D,
    var_name::Symbol,
    binary::Bool,
    expression_name::Union{Nothing, Symbol} = nothing,
    sign::Float64 = 1.0;
    kwargs...,
) where {D <: Union{Vector{<:PSY.Device}, IS.FlattenIteratorWrapper{<:PSY.Device}}}
    time_steps = model_time_steps(psi_container)
    variable = add_var_container!(
        psi_container,
        var_name,
        (PSY.get_name(d) for d in devices),
        time_steps,
    )

    lb_f = get(kwargs, :lb_value, nothing)
    init_f = get(kwargs, :initial_value, nothing)
    ub_f = get(kwargs, :ub_value, nothing)

    for t in time_steps, d in devices
        name = PSY.get_name(d)
        variable[name, t] = JuMP.@variable(
            psi_container.JuMPmodel,
            base_name = "$(var_name)_{$(name), $(t)}",
            binary = binary
        )

        !isnothing(ub_f) && JuMP.set_upper_bound(variable[name, t], ub_f(d))
        !isnothing(lb_f) && !binary && JuMP.set_lower_bound(variable[name, t], lb_f(d))
        !isnothing(init_f) && JuMP.set_start_value(variable[name, t], init_f(d))

        if !(isnothing(expression_name))
            bus_number = PSY.get_number(PSY.get_bus(d))
            add_to_expression!(
                get_expression(psi_container, expression_name),
                bus_number,
                t,
                variable[name, t],
                sign,
            )
        end
    end

    return

end

@doc raw"""
    set_variable_bounds!(
        psi_container::PSIContainer,
        bounds::DeviceRange,
        var_type::AbstractString,
        device_type::Type{PSY.Device},
    )

Adds a bounds to a variable in the optimization model.

# Bounds

``` bounds.min <= varstart[name, t] <= bounds.max  ```

# LaTeX

``  x^{device}_t >= bound^{min;} \forall t ``

``  x^{device}_t <= bound^{max} \forall t ``

# Arguments
* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* bounds::DeviceRange : contains names and vector of min / max
* var_type::AbstractString : type of the variable
* T: type of the device

"""
function set_variable_bounds!(
    psi_container::PSIContainer,
    bounds::Vector{DeviceRange},
    var_type::AbstractString,
    ::Type{T},
) where {T <: PSY.Device}
    var = get_variable(psi_container, var_type, T)
    for t in model_time_steps(psi_container), bound in bounds
        _var = var[bound.name, t]
        JuMP.set_upper_bound(_var, bound.limits.max)
        JuMP.set_lower_bound(_var, bound.limits.min)
    end
end


"""
Contains stubs for advective, diffusive, and nondiffusive courant number
calculations to be used in [`DGmethods.courant`](@ref). Models should provide
concrete implementations if they wish to use these functions.
"""
module Courant

export advective_courant, diffusive_courant, nondiffusive_courant

function advective_courant end

function nondiffusive_courant end

function diffusive_courant end

function viscous_courant end

end


# TODO: Need to finish
#       Need to test
function fit_fs_imcmc_pt!(cfs::ConstantsFS,
                          dfs::DataFS;
                          nmcmc::Int, nburn::Int,
                          # Args for PT:
                          tempers::Vector{Float64},
                          inits=nothing,
                          save_all_states=false,
                          randpair=0.0,
                          # End of PT Args.
                          # Args are for iMCMC:
                          batchprop::Float64=0.1,
                          batchsizes=nothing,
                          prior_thin::Int=2,
                          imcmc_burn_prop=0.6,
                          swap_freq::Float64=1.0,
                          # End of iMCMC args.
                          tfs::Union{Nothing, Vector{TunersFS}}=nothing,
                          monitors=[monitor1, monitor2],
                          fix::Vector{Symbol}=Symbol[],
                          thins::Vector{Int}=[2, nsamps_to_thin(10, nmcmc)],
                          ndden_samps::Int=200,
                          printFreq::Int=0,
                          checkpoint=0,
                          computeDIC::Bool=true, computeLPML::Bool=true,
                          computedden::Bool=true,
                          sb_ibp::Bool=false,
                          use_repulsive::Bool=true,
                          Z_marg_lamgam::Float64=1.0,
                          Z_marg_lamgam_decay_rate::Float64=100.0,
                          Z_marg_lamgam_min::Float64=0.05,
                          verbose::Int=1,
                          time_updates=false,
                          seed::Int=-1)

  printMsg(iter::Int, msg::String) = if printFreq > 0 && iter % printFreq == 0
    print(msg)
  end

  # How frequently to thin dden
  thin_dden = nsamps_to_thin(ndden_samps, nmcmc)

  # Number of temperatures
  num_tempers = length(tempers)

  @assert mod(num_tempers, 2) == 0
  if tfs != nothing
    @assert num_tempers == length(tfs)
  end

  # Assert swap frequency is in unit interval
  @assert 0 < swap_freq <= 1.0

  # Cache for swap counts
  swapcounts = zeros(Int, num_tempers, num_tempers)

  # Cache for pair counts
  paircounts = zeros(Int, num_tempers, num_tempers)

  # Set random seed if needed
  if seed >= 0
    Random.seed!(seed)
  end

  if verbose >= 1
    fixed_vars_str = join(fix, ", ")
    if fixed_vars_str == ""
      fixed_vars_str = "nothing"
    end
    println("fixing: $fixed_vars_str")
    println("Use stick-breaking IBP: $(sb_ibp)")
    println("Z_marg_lamgam: $(Z_marg_lamgam)")
    println("Z_marg_lamgam_decay_rate: $(Z_marg_lamgam_decay_rate)")
    println("Z_marg_lamgam_min: $(Z_marg_lamgam_min)")
    println("use_repulsive: $(use_repulsive)")
    println("batchprop: $(batchprop)")
    println("batchsizes: $(batchsizes)")
    flush(stdout)
  end

  @assert printFreq >= -1
  if printFreq == 0
    numPrints = 10
    printFreq = Int(ceil((nburn + nmcmc) / numPrints))
  end

  # Instantiate (but not initialize) CPO stream
  if computeLPML
    cpoStream = MCMC.CPOstream{Float64}()
  end

  # DIC
  if computeDIC
    dicStream = initDicStream(dfs.data)
    loglikeDIC(param::DICparam) = computeLoglikeDIC(dfs.data, param)

    convertStateToDicParam(s::State)::DICparam = let
      _convertStateToDicParam(s, cfs.constants, dfs.data)
    end
  end

  # Cache for data density
  dden = Matrix{Vector{Float64}}[]

  # Update function
  function update!(states, args, iter::Int)
    # Whether or not to marginalize over lambda and gamma.
    # We want to do this more often at the beginning, and less at the end.
    zmarg = ((Z_marg_lamgam - Z_marg_lamgam_min) *
             exp(-iter/Z_marg_lamgam_decay_rate) +
             Z_marg_lamgam_min) > rand()

    s_arg_vec = pmap(states, args) do s, arg
      tu = (arg[:c].constants.temper == 1) && time_updates

      # For reproducibility.
      if seed > - 1
        temper_idx = findfirst(tau -> tau == arg[:c].constants.temper, tempers)
        Random.seed!(iter + temper_idx + seed)
      end

      # Update state using trained prior
      imcmc_all_params = let
        iter < imcmc_burn_prop * nburn
      end

      update_via_trained_prior!(s, dfs, arg[:c], arg[:t],
                                batchprop, prior_thin,
                                batchsizes=batchsizes,
                                fix=fix, use_repulsive=use_repulsive,
                                Z_marg_lamgam=zmarg, sb_ibp=sb_ibp,
                                time_updates=time_updates, temper=1.0,
                                minibatch_update_all_params=imcmc_all_params,
                                verbose=verbose-2)
      (s, arg)
    end

    states = [sa[1] for sa in s_arg_vec]
    args = [sa[2] for sa in s_arg_vec]

    # Swap Chains
    if verbose > 0
      println()
    end

    llf(s, tuner) = compute_marg_loglike(s, cfs.constants, dfs.data, tuner)

    if swap_freq > rand()
      swapchains!(states, llf, tempers,
                  paircounts=paircounts, swapcounts=swapcounts,
                  randpair=randpair, verbose=verbose)
    end
    if iter == nburn
      println("swapcounts / paircounts:")
      println(swapcounts ./ (paircounts .+ 1e-6))
      println("Resetting swapcounts, paircounts ...")
      swapcounts .= 0.0
      paircounts .= 0.0
    end

    # Pull out inner componenets for convenience
    s = states[1]
    c = args[1][:c]
    d = dfs
    ll = args[1][:ll]

    # # Append loglike
    append!(ll, compute_marg_loglike(s.theta, c.constants, d.data, 1.0))

    if computedden && iter > nburn && (iter - nburn) % thin_dden == 0
      # NOTE: `datadensity(s, c, d)` returns an (I x J) matrix of vectors of
      # length g.
      append!(dden, [datadensity(s.theta, c.constants, d.data)])
    end

    if computeLPML && iter > nburn
      # Inverse likelihood for each data point
      like = [[compute_like(i, n, s.theta, c.constants, d.data)
               for n in 1:d.data.N[i]] for i in 1:d.data.I]

      # Update (or initialize) CPO
      MCMC.updateCPO(cpoStream, vcat(like...))

      # Add to printMsg
      printMsg(iter, " -- LPML: $(MCMC.computeLPML(cpoStream))")
    end

    if computeDIC && iter > nburn
      # Update DIC
      MCMC.updateDIC(dicStream, s.theta, updateParams,
                     loglikeDIC, convertStateToDicParam)

      # Add to printMsg
      printMsg(iter, " -- DIC: $(MCMC.computeDIC(dicStream, loglikeDIC,
                                                 paramMeanCompute))")

      DICg = MCMC.DIC_gelman(ll[(nburn+1):end])
      printMsg(iter, " -- DIC_gelman: $(DICg)")
    end

    printMsg(iter, "\n")
    flush(stdout)

    return states, args
  end

  # Create vectors of states
  states = if inits == nothing
    println("Using random inits!")
    [let
       s = genInitialState(cfs.constants, dfs.data)
       s.eps .= 0.0
       StateFS{Float64}(s, dfs)
     end for _ in tempers]
  else
    @assert length(inits) == num_tempers
    for _init in inits
      _init.theta.eps .= 0.0
    end
    inits
  end

  # Create Args
  args = [let
            ll = Float64[]  # FIXME
            c = deepcopy(cfs)
            c.constants.temper = tempers[i]
            t = if tfs == nothing
              TunersFS(Tuners(dfs.data.y, cfs.constants.K),
                       states[1].theta, dfs.X)
            else
              tfs[i]
            end
            Dict(:ll => ll, :c => c, :t => t)
          end
          for i in 1:num_tempers]

  println("Running Gibbs sampler ...")
  samples, states, args = let
    MCMC.gibbs_pt(states, args, update!, monitors=monitors,
                  thins=thins, nmcmc=nmcmc, nburn=nburn,
                  printFreq=printFreq,
                  save_all_states=save_all_states,
                  printlnAfterMsg=false)
  end

  out = Dict(:samples => samples,
             :lastState => states[1],
             :inits => inits,
             :c => cfs,
             :d => dfs,
             :lls => [arg[:ll] for arg in args],
             :save_all_states => save_all_states,
             :tempers => tempers,
             :swap_freq => swap_freq,
             :randpair => randpair,
             :paircounts => paircounts,
             :swapcounts => swapcounts)

  if computeDIC || computeLPML
    LPML = computeLPML ? MCMC.computeLPML(cpoStream) : NaN
    Dmean, pD = computeDIC ? MCMC.computeDIC(dicStream, loglikeDIC,
                                             paramMeanCompute,
                                             return_Dmean_pD=true) : (NaN, NaN)

    ll1 = args[1][:ll]
    DICg = MCMC.DIC_gelman(ll1[(nburn+1):end])

    metrics = Dict(:LPML => LPML,
                   :DIC => Dmean + pD,
                   :Dmean => Dmean,
                   :pD => pD,
                   :DICg => DICg)
    println()
    println("metrics:")
    for (k, v) in metrics
      out[k] = v
      println("$k => $v")
    end
    flush(stdout)
  end

  if computedden
    out[:dden] = dden
  end

  out[:nburn] = nburn
  out[:nmcmc] = nmcmc
  out[:batchprop] = batchprop
  out[:batchsizes] = batchsizes
  out[:prior_thin] = prior_thin

  # Return all states if requested
  if save_all_states
    out[:all_last_states] = deepcopy(states)
  end

  return out
end


module TestAudioIO

using FactCheck
using AudioIO
import AudioIO.AudioBuf

const TEST_SAMPLERATE = 44100
const TEST_BUF_SIZE = 1024

include("testhelpers.jl")

type TestAudioStream <: AudioIO.AudioStream
    root::AudioIO.AudioMixer
    info::AudioIO.DeviceInfo

    function TestAudioStream()
        root = AudioMixer()
        new(root, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE))
    end
end

# render the stream and return the next block of audio. This is used in testing
# to simulate the audio callback that's normally called by the device.
function process(stream::TestAudioStream)
    out_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
    in_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
    rendered = AudioIO.render(stream.root, in_array, stream.info)
    out_array[1:length(rendered)] = rendered
    return out_array
end

#### Test playing back various vector types ####

facts("Array playback") do
    # data shared between tests, for convenience
    t = linspace(0, 2, 2 * 44100)
    phase = 2pi * 100 * t

    ## Test Float32 arrays, this is currently the native audio playback format
    context("Playing Float32 arrays") do
        f32 = convert(Array{Float32}, sin(phase))
        test_stream = TestAudioStream()
        player = play(f32, test_stream)
        @fact process(test_stream) => f32[1:TEST_BUF_SIZE]
    end

    context("Playing Float64 arrays") do
        f64 = convert(Array{Float64}, sin(phase))
        test_stream = TestAudioStream()
        player = play(f64, test_stream)
        @fact process(test_stream) => convert(AudioBuf, f64[1:TEST_BUF_SIZE])
    end

    context("Playing Int8(Signed) arrays") do
        i8 = Int8[-127:127]
        test_stream = TestAudioStream()
        player = play(i8, test_stream)
        @fact process(test_stream)[1:255] =>
                mse(convert(AudioBuf, linspace(-1.0, 1.0, 255)))
    end

    context("Playing Uint8(Unsigned) arrays") do
        # for unsigned 8-bit audio silence is represented as 128, so the symmetric range
        # is 1-255
        ui8 = Uint8[1:255]
        test_stream = TestAudioStream()
        player = play(ui8, test_stream)
        @fact process(test_stream)[1:255] =>
                mse(convert(AudioBuf, linspace(-1.0, 1.0, 255)))
   end
end

facts("AudioNode Stopping") do
    test_stream = TestAudioStream()
    node = SinOsc(440)
    play(node, test_stream)
    process(test_stream)
    stop(node)
    @fact process(test_stream) => zeros(AudioIO.AudioSample, TEST_BUF_SIZE)
end

facts("Audio Device Listing") do
    # there aren't any devices on the Travis machine so just test that this doesn't crash
    @fact get_audio_devices() => issubtype(Array)
end

end # module TestAudioIO


# Cbc isn't performant enough and SCIP is too problematic
test_months = (get(ENV, "CI", nothing) == "true") ? 4 : 6
sys_rts = PSB.build_system(PSITestSystems, "modified_RTS_GMLC_DA_sys")
@testset "Decision Model test for Initialization with RTS GMLC system, Case 1" begin
    ######## Test with ThermalStandardUnitCommitment ########
    template = ProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, ThermalStandard, ThermalStandardUnitCommitment)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)
    for init_time in
        DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00")
        @info("Decision Model initial_conditions test with RTS-GMLC for $init_time")
        model = DecisionModel(
            template,
            sys_rts;
            optimizer=HiGHS_optimizer,
            initial_time=init_time,
            horizon=48,
        )
        @test build!(model; output_dir=mktempdir(cleanup=true)) == BuildStatus.BUILT

        ####### Check initialization problem
        check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)
        check_initialization_variable_count(model, OnVariable(), ThermalStandard)
        check_initialization_variable_count(model, StopVariable(), ThermalStandard)
        check_initialization_variable_count(model, StartVariable(), ThermalStandard)
        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)
        ####### Check initial condition from initialization step
        check_duration_on_initial_conditions_values(model, ThermalStandard)
        check_duration_off_initial_conditions_values(model, ThermalStandard)
        check_active_power_initial_condition_values(model, ThermalStandard)
        check_status_initial_conditions_values(model, ThermalStandard)
        ####### Check variables
        check_variable_count(model, ActivePowerVariable(), ThermalStandard)
        check_variable_count(model, StopVariable(), ThermalStandard)
        check_variable_count(model, OnVariable(), ThermalStandard)
        check_variable_count(model, StartVariable(), ThermalStandard)
        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_variable_count(model, ActivePowerVariable(), HydroDispatch)
        ####### Check constraints
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="lb",
        )
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="ub",
        )
        check_constraint_count(model, DurationConstraint(), ThermalStandard)
        check_constraint_count(model, RampConstraint(), ThermalStandard)
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta="aux")
        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            RenewableDispatch;
            meta="ub",
        )

        # @test solve!(model) == RunStatus.SUCCESSFUL
    end
end

@testset "Decision Model test for Initialization with RTS GMLC system, Case 2" begin
    ######## Test with HydroCommitmentRunOfRiver ########
    template = ProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, ThermalStandard, ThermalBasicUnitCommitment)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
    set_device_model!(template, HydroDispatch, HydroCommitmentRunOfRiver)

    for init_time in
        DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00")
        @info("Decision Model initial_conditions test with RTS-GMLC for $init_time")
        model = DecisionModel(
            template,
            sys_rts;
            optimizer=HiGHS_optimizer,
            initial_time=init_time,
            horizon=48,
        )
        @test build!(model; output_dir=mktempdir(cleanup=true)) == BuildStatus.BUILT

        ####### Check initialization problem
        check_initialization_variable_count(model, ActivePowerVariable(), ThermalStandard)
        check_initialization_variable_count(model, OnVariable(), ThermalStandard)
        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)
        ####### Check initial condition from initialization step
        check_status_initial_conditions_values(model, ThermalStandard)

        ####### Check variables
        check_variable_count(model, ActivePowerVariable(), ThermalStandard)
        check_variable_count(model, StopVariable(), ThermalStandard)
        check_variable_count(model, OnVariable(), ThermalStandard)
        check_variable_count(model, StartVariable(), ThermalStandard)
        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_variable_count(model, ActivePowerVariable(), HydroDispatch)
        check_variable_count(model, OnVariable(), HydroDispatch)
        ####### Check constraints
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="lb",
        )
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="ub",
        )
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta="aux")

        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            RenewableDispatch;
            meta="ub",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="lb",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )

        # @test solve!(model) == RunStatus.SUCCESSFUL
    end
end

@testset "Decision Model test for Initialization with RTS GMLC system, Case 4" begin
    ######## Test with ThermalCompactUnitCommitment ########
    template = ProblemTemplate(CopperPlatePowerModel)
    set_device_model!(template, ThermalStandard, ThermalCompactUnitCommitment)
    set_device_model!(template, PowerLoad, StaticPowerLoad)
    set_device_model!(template, RenewableDispatch, RenewableFullDispatch)
    set_device_model!(template, HydroDispatch, HydroDispatchRunOfRiver)

    for init_time in
        DateTime("2020-01-01T00:00:00"):Month(test_months):DateTime("2020-12-31T00:00:00")
        @info("Decision Model initial_conditions test with RTS-GMLC for $init_time")
        model = DecisionModel(
            template,
            sys_rts;
            optimizer=HiGHS_optimizer,
            initial_time=init_time,
            horizon=48,
        )
        setup_ic_model_container!(model)
        ####### Check initialization problem constraints #####
        check_initialization_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="lb",
        )
        check_initialization_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="ub",
        )
        check_initialization_constraint_count(
            model,
            CommitmentConstraint(),
            ThermalStandard,
        )
        check_initialization_constraint_count(
            model,
            CommitmentConstraint(),
            ThermalStandard;
            meta="aux",
        )
        check_initialization_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            RenewableDispatch;
            meta="ub",
        )
        check_initialization_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="lb",
        )
        check_initialization_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )
        check_initialization_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )
        PSI.reset!(model)
        @test build!(model; output_dir=mktempdir(cleanup=true)) == BuildStatus.BUILT

        ####### Check initialization problem
        check_initialization_variable_count(
            model,
            PSI.PowerAboveMinimumVariable(),
            ThermalStandard,
        )
        check_initialization_variable_count(model, OnVariable(), ThermalStandard)
        check_initialization_variable_count(model, StopVariable(), ThermalStandard)
        check_initialization_variable_count(model, StartVariable(), ThermalStandard)
        check_initialization_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_initialization_variable_count(model, ActivePowerVariable(), HydroDispatch)

        ####### Check initial condition from initialization step
        check_duration_on_initial_conditions_values(model, ThermalStandard)
        check_duration_off_initial_conditions_values(model, ThermalStandard)
        check_active_power_abovemin_initial_condition_values(model, ThermalStandard)
        check_status_initial_conditions_values(model, ThermalStandard)

        ####### Check variables
        check_variable_count(model, PSI.PowerAboveMinimumVariable(), ThermalStandard)
        check_variable_count(model, OnVariable(), ThermalStandard)
        check_variable_count(model, StopVariable(), ThermalStandard)
        check_variable_count(model, StartVariable(), ThermalStandard)
        check_variable_count(model, ActivePowerVariable(), RenewableDispatch)
        check_variable_count(model, ActivePowerVariable(), HydroDispatch)

        ####### Check constraints
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="lb",
        )
        check_constraint_count(
            model,
            ActivePowerVariableLimitsConstraint(),
            ThermalStandard;
            meta="ub",
        )
        check_constraint_count(model, RampConstraint(), ThermalStandard)
        check_constraint_count(model, DurationConstraint(), ThermalStandard)
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard)
        check_constraint_count(model, CommitmentConstraint(), ThermalStandard; meta="aux")
        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            RenewableDispatch;
            meta="ub",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableTimeSeriesLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="lb",
        )
        check_constraint_count(
            model,
            PSI.ActivePowerVariableLimitsConstraint(),
            HydroDispatch;
            meta="ub",
        )

        # @test solve!(model) == RunStatus.SUCCESSFUL
    end
end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

@doc doc"""
    abstract type AbstractPosteriorDensity <: AbstractDensity end

Abstract super-type for posterior probability densities.
"""
abstract type AbstractPosteriorDensity <: AbstractDensity end
export AbstractPosteriorDensity

@doc doc"""
    getlikelihood(posterior::AbstractPosteriorDensity)::AbstractDensity

*BAT-internal, not part of stable public API.*

The likelihood density of `posterior`. The likelihood may or may not be
normalized.
"""
function getlikelihood end

@doc doc"""
    getprior(posterior::AbstractPosteriorDensity)::AbstractDensity

*BAT-internal, not part of stable public API.*

The prior density of `posterior`. The prior may or may not be normalized.
"""
function getprior end

@doc doc"""
    BAT.apply_bounds_and_eval_posterior_logval!(
        T::Type{<:Real},
        density::AbstractDensity,
        params::AbstractVector{<:Real}
    )

*BAT-internal, not part of stable public API.*

First apply bounds to the parameters, then compute and return posterior log
value. May modify `params`.

Guarantees that  :

* If parameters are still out of bounds after applying bounds,
  `density_logval` is not called for either prior or likelihood.
* If `density_logval` for prior returns `-Inf`, `density_logval` is not called
  for likelihood.

In both cases, `T(-Inf)` is returned for both prior and posterior.
"""
function apply_bounds_and_eval_posterior_logval! end

function apply_bounds_and_eval_posterior_logval!(
    T::Type{<:Real},
    posterior::AbstractPosteriorDensity,
    params::AbstractVector{<:Real}
)
    bounds = param_bounds(posterior)
    apply_bounds!(params, bounds)

    parshapes = params_shape(posterior)
    zero_prob_logval = convert(T, -Inf)

    prior_logval = if !isoob(params)
        convert(T, eval_density_logval(getprior(posterior), params, parshapes))
    else
        zero_prob_logval
    end

    likelihood_logval = if prior_logval > zero_prob_logval
        convert(T, eval_density_logval(getlikelihood(posterior), params, parshapes))
    else
        zero_prob_logval
    end

    convert(T, prior_logval + likelihood_logval)
end

@doc doc"""
    BAT.apply_bounds_and_eval_posterior_logval_strict!(
        posterior::AbstractPosteriorDensity,
        params::AbstractVector{<:Real}
    )

*BAT-internal, not part of stable public API.*

First apply bounds to the parameters, then compute and return posterior log
value. May modify `params`.

Guarantees that  :

* If parameters are still out of bounds after applying bounds,
  `density_logval` is not called for either prior or likelihood.
* If `density_logval` for prior returns `-Inf`, `density_logval` is not called
  for likelihood.

In both cases, an exception is thrown.
"""
function apply_bounds_and_eval_posterior_logval_strict!(
    posterior::AbstractPosteriorDensity,
    params::AbstractVector{<:Real}
)
    bounds = param_bounds(posterior)
    apply_bounds!(params, bounds)

    parshapes = params_shape(posterior)

    prior_logval = if !isoob(params)
        eval_density_logval(getprior(posterior), params, parshapes)
    else
        throw(ArgumentError("Parameter(s) out of bounds"))
    end

    likelihood_logval = if prior_logval > convert(typeof(prior_logval), -Inf)
        eval_density_logval(getlikelihood(posterior), params, parshapes)
    else
        throw(ErrorException("Prior density must not be zero."))
    end

    T = typeof(likelihood_logval)

    convert(T, convert(T, prior_logval) + likelihood_logval)
end

function density_logval(density::AbstractPosteriorDensity, params::AbstractVector{<:Real})
    parshapes = params_shape(density)
    eval_density_logval(getprior(density), params, parshapes) +
    eval_density_logval(getlikelihood(density), params, parshapes)
end

function density_logval(density::AbstractPosteriorDensity, params::Any)
    density_logval(getprior(density), params) +
    density_logval(getlikelihood(density), params)
end

function param_bounds(density::AbstractPosteriorDensity)
    li_bounds = param_bounds(getlikelihood(density))
    pr_bounds = param_bounds(getprior(density))
    if ismissing(li_bounds)
        pr_bounds
    else
        li_bounds ∩ pr_bounds
    end
end

function nparams(density::AbstractPosteriorDensity)
    li_np = nparams(getlikelihood(density))
    pr_np = nparams(getprior(density))
    ismissing(li_np) || li_np == pr_np || error("Likelihood and prior have different number of parameters")
    pr_np
end

@doc doc"""
    PosteriorDensity{
        Li<:AbstractDensity,
        Pr<:DistLikeDensity
    } <: AbstractPosteriorDensity

A representation of a PosteriorDensity, based a likelihood and prior, a
representation of the posterior density is cached internally. The densities
be accessed via

```julia
getlikelihood(posterior::PosteriorDensity)::Li
getprior(posterior::PosteriorDensity)::Pr
```

Constructors:

```julia
PosteriorDensity(likelihood::AbstractDensity, prior::AbstractDensity)
PosteriorDensity(likelihood::Any, prior::Any)
PosteriorDensity(log_likelihood::Function, prior::Any)
```
"""
struct PosteriorDensity{
    L<:AbstractDensity,
    P<:DistLikeDensity,
    B<:AbstractParamBounds,
    S<:AbstractValueShape
} <: AbstractPosteriorDensity
    likelihood::L
    prior::P
    parbounds::B
    parshapes::S
end

export PosteriorDensity

function PosteriorDensity(likelihood::Any, prior::Any)
    li = convert(AbstractDensity, likelihood)
    pr = convert(AbstractDensity, prior)

    parbounds = _posterior_parbounds(
        param_bounds(li),
        param_bounds(pr)
    )

    parshapes = _posterior_parshapes(
        params_shape(li),
        params_shape(pr)
    )

    PosteriorDensity(li, pr, parbounds, parshapes)
end

getlikelihood(posterior::PosteriorDensity) = posterior.likelihood

getprior(posterior::PosteriorDensity) = posterior.prior

param_bounds(posterior::PosteriorDensity) = posterior.parbounds

params_shape(posterior::PosteriorDensity) = posterior.parshapes

function _posterior_parshapes(li_ps::AbstractValueShape, pr_ps::AbstractValueShape)
    if li_ps == pr_ps
        li_ps
    else
        throw(ArgumentError("Variable shapes of likelihood and prior are incompatible"))
    end
end

function _posterior_parshapes(li_ps::AbstractValueShape, pr_ps::ArrayShape{T,1}) where T
    n = totalndof(li_ps)
    if n == totalndof(pr_ps)
        @assert size(pr_ps) == (n,)
        li_ps
    else
        throw(ArgumentError("Likelihood and prior have different number of free parameters"))
    end
end

_posterior_parshapes(li_ps::ArrayShape{T,1}, pr_ps::AbstractValueShape) where T =
    _posterior_parshapes(pr_ps, li_ps)

function _posterior_parshapes(li_ps::ArrayShape{T,1}, pr_ps::ArrayShape{U,1}) where {T,U}
    if size(li_ps) == size(pr_ps)
        li_ps
    else
        throw(ArgumentError("Likelihood and prior have different number of free parameters"))
    end
end

_posterior_parshapes(li_ps::Missing, pr_ps::AbstractValueShape) = pr_ps

_posterior_parshapes(li_ps::Union{AbstractValueShape,Missing}, pr_ps::Missing) =
    throw(ArgumentError("Parameter shapes of prior must not be missing"))

_posterior_parbounds(li_bounds::AbstractParamBounds, pr_bounds::AbstractParamBounds) =
     li_bounds ∩ pr_bounds

_posterior_parbounds(li_bounds::Missing, pr_bounds::AbstractParamBounds) = pr_bounds


using p7zip_jll
using Colors
using Mmap

"""
    readptx(
        fn::AbstractString,
        scale::EnergyScale, # Energy scale for hyperspectrum
        props::Dict{Symbol,Any},
        nch::Int; # Number of channels in hyperspectrum
        blocksize = 1,  # Size of blocks to sum to create averaged hyperspectrum
        dets=[true,true,true,true], # Which detectors to include
        frames=1:typemax(Int)) # which frames to include in hyperspectrum
    readptx(
        fn::AbstractString,
        scale::EnergyScale, # Energy scale for hyperspectrum
        nch::Int; # Number of channels in hyperspectrum
        blocksize = 1,  # Size of blocks to sum to create averaged hyperspectrum
        dets=[true,true,true,true], # Which detectors to include
        frames=1:typemax(Int)) # which frames to include in hyperspectrum

Read a SEMantics .ptx file into a HyperSpectrum.
"""
function readptx(
    fn::AbstractString,
    scale::EnergyScale,
    props::Dict{Symbol,<:Any},
    nch::Int;
    blocksize = 1,
    frames = 1:typemax(Int),
    dets = ( true, true, true, true ),
    datatype = UInt16
)
    function readmsg(io::IO)
        hdr = Array{UInt8}(undef, 32)
        if readbytes!(io, hdr) == 32
            name = String(hdr[1:findfirst(c -> c == 0x0, hdr)-1])
            len = reinterpret(UInt32, hdr[17:20])[1]
            msg = Array{UInt8}(undef, len)
            if readbytes!(io, msg) == len
                return (name, msg)
            end
        end
        return ("End", nothing)
    end
    mktempdir() do tmp_dir
        p7zip() do p7z_exe
            # Use pkzip to unzip the contents to a temporary directory
            run(`$p7z_exe -bsp0 -bso0 e "$fn" -o"$tmp_dir"`)
        end
        # Read and interpret the header
        xml = read(joinpath(tmp_dir,"Header"))
        hdr = readxml(IOBuffer(xml))
        nprops = Dict{Symbol,Any}(props)
        nprops[:Name] = splitpath(fn)[end]
        dwell = 1.0e-6 * parse(Float64, findfirst("/OpticState/mDwell", hdr).content)
        fov = 0.1 * parse(Float64, findfirst("/OpticState/mFieldOfView", hdr).content)
        width = parse(Int, findfirst("/OpticState/mWidth", hdr).content)
        height = parse(Int, findfirst("/OpticState/mHeight", hdr).content)
        rx = parse(Int, findfirst("/OpticState/mRaster/x", hdr).content)
        ry = parse(Int, findfirst("/OpticState/mRaster/y", hdr).content)
        rw = parse(Int, findfirst("/OpticState/mRaster/width", hdr).content)
        rh = parse(Int, findfirst("/OpticState/mRaster/height", hdr).content)
        hss = HyperSpectrum(
            scale, # EnergyScale
            nprops, # Properties
            ((rh - ry)÷blocksize, (rw - rx)÷blocksize), # Pixel dimensions
            nch, # Channel depth
            datatype, # Default UInt16,
            axisnames = ( "X", "Y" ),
            fov=(fov*(rw/width),fov*(rh/height)) # Image scale
        )
        open(joinpath(tmp_dir, "Data"),"r", lock=true) do data
            realtime, deadtime = zero(UInt64), zero(UInt64)
            maxchannel, maxframeid, minframeid = zero(Int32), zero(Int32), typemax(Int32)
            # elapsetime, prevelapse = zero(UInt64), typemax(UInt32)
            seen = Set{String}()
            images = Dict{Tuple{Int32,Int32},Array}()
            cmd, msg = readmsg(data)
            bpp=8
            while cmd != "End"
                # println("$cmd[$(isnothing(msg) ? "nothing" : length(msg))]")
                if cmd == "GetMapData"
                    # The output array consists of list of datagrams, each has got five double-word (32-bit) values (DW0 to
                    # DW4). Little endian is storage is applied. Most significant byte (bits 31-24) of W0 tells the event type:
                    # err = reinterpret(Int32, msg[1:4])[1]
                    (reinterpret(UInt32, msg[5:8])[1]!=0) && warn("A buffer overflow occured during the acquisition of the map data.")
                    # usage = reinterpret(Float32, msg[9:12])[1]
                    # @assert reinterpret(UInt32, msg[13:16])[1]==0
                    ws = reinterpret(UInt32, msg[13:end])
                    frame = zero(eltype(ws))
                    for i = 1:length(ws)÷5
                        w0, w1, w2, w3, w4 = ws[5*(i-1)+1:5*i]
                        # println("$(ws[5*(i-1)+1:5*i])")
                        # msg[2] # timestamp in 10 ns units
                        msgid = (w0 >>> 24) & 0xFF
                        if msgid == 0x80 # dead time
                            # W0 = 0x80xxxxxx – dead-time measurement
                            # W0 – bits 0-7: filter index (1= filter 1, 2 = filter 2, …) bits 8-11: detector index (0=1st detector, 1=2nd detector, …)
                            # W1 – real-time timer value in 10 ns
                            # W2 – dead-time timer value in 10 ns
                            # W3 – event counter
                            x_det = 1 + ((w0 >>> 8) & 0x0F)
                            if (x_det>=1) && (x_det<=length(dets)) && dets[x_det] && frame in frames
                                realtime += w1
                                deadtime += w2
                            end
                        elseif msgid == 0x00 # measured energy
                            # W0 = 0x00xxxxxx – measured energy
                            # W0 – bits 0-7: filter index (1= filter 1, 2 = filter 2, …)  bits 8-11: detector index (0=1st detector, 1=2nd detector, ...)
                            # W1 – energy in eV (24.8 signed fixed point number)
                            # W2 – timestamp in 10ns
                            # W3 – pixel index
                            # W4 – frame index
                            x_det, frame = 1 + ((w0 >>> 8) & 0x0F), w4
                            if (x_det>=1) && (x_det<=length(dets)) && dets[x_det] && frame in frames
                                ch = channel(Float64(w1 >>> 8), scale)
                                r, c = Int(w3 ÷ rw - ry + 1)÷blocksize, Int(w3 % rw - rx + 1)÷blocksize
                                #elapsetime = ( prevelapse == typemax(UInt64) ? 0 : elapsetime + (w2 >= prevelapse ? UInt64(w2 - prevelapse) : zero(typeof(elapsetime))) # w2 + (prevelapse - typemax(UInt32)))
                                #prevelapse = w2
                                if checkbounds(Bool, hss.counts, ch, r, c)
                                    @inbounds hss.counts[ch, r, c] += 1
                                end
                            end
                        end
                        # W0 = 0x10xxxxxx – line sync
                        # W0 – bit 0: “Line” signal state bits 8-11: detector index (0=1st detector, 1=2nd detector, ...)
                        # W2 – timestamp in 10ns
                        # W4 – frame index
                        # W0 = 0x11xxxxxx – pileup
                        # W0 – bits 8-11: detector index (0=1st detector, 1=2nd detector, …)
                        # W2 – timestamp in 10ns
                        # W4 – frame index
                        # W0 = 0x13xxxxxx – preamplifier reset
                        # W0 – bits 8-11: detector index (0=1st detector, 1=2nd detector, …)
                        # W2 – timestamp in 10ns
                        # W4 – frame index
                    end
                elseif cmd == "ScData"
                    # See SharkSEM API ScScanLine documentation
                    # ScData(in unsigned int frameid, in int channel, in unsigned int index, in int bpp, in char[] data);
                    frameid = reinterpret(Int32, msg[1:4])[1]+1
                    maxframeid = max(frameid, maxframeid)
                    minframeid = min(frameid, minframeid)
                    ch = reinterpret(Int32, msg[5:8])[1]+1
                    maxchannel = max(ch, maxchannel)
                    index  = reinterpret(Int32, msg[9:12])[1]
                    bpp = reinterpret(Int32, msg[13:16])[1]
                    @assert bpp==8 || bpp==16
                    if !haskey(images,(frameid, ch))
                        images[(frameid, ch)] = (bpp == 8 ? zeros(UInt8, (rh - ry, rw - rx)) : zeros(UInt16, (rh - ry, rw - rx)))
                    end
                    img=images[(frameid, ch)]
                    imgdata = (bpp==8 ? reinterpret(UInt8, msg[21:end]) : reinterpret(UInt16, msg[21:end]))
                    img[index-ry+1:index-rx+length(imgdata)] .= imgdata
                else
                    if !(cmd in seen)
                        println("Skipping: $cmd")
                        push!(seen, cmd)
                    end
                end
                cmd, msg = readmsg(data)
            end
            if (maxchannel>0) && (maxframeid>0)
                for frameid in minframeid:maxframeid
                    image = zeros(eltype(images[(minframeid,1)]),  (rh - ry, rw - rx, 1:maxchannel))
                    for ch in 1:maxchannel
                        if haskey(images, (frameid, ch))
                            image[:,:,ch] .= images[(frameid,ch)]
                        end
                    end
                    hss[Symbol("Image$(maxframeid-minframeid+1)")] = Gray.(reinterpret(bpp==8 ? N0f8 : N0f16, permutedims(image, (2, 1, 3))))
                end
            end
            hss[:Dwell] = dwell * blocksize^2
            # Elapse time is broken!!!
            # hss[:Elapse] = 1.0e-8 * elapsetime # second
            hss[:RealTime] = (1.0e-8 * blocksize^2 / ((rh - ry) * (rw - rx))) * realtime  # seconds
            hss.livetime .= (1.0e-8 * blocksize^2 / ((rh - ry) * (rw - rx))) * (realtime - deadtime)  # seconds
            hss[:DeadFraction] = deadtime / realtime
            return hss
        end # close the file
    end # delete the temporary directory
end
readptx(
    fn::AbstractString,
    scale::EnergyScale,
    nch::Int;
    blocksize = 1,
    frames = 1:typemax(Int),
    dets = ( true, true, true, true ),
    datatype = UInt16
) = readptx(fn, scale, Dict{Symbol,Any}(), nch, blocksize=blocksize, frames=frames, dets=dets, datatype=datatype)


abstract type AbstractDataGenerator end
function launch(launchable::AbstractDataGenerator) error("unimplemented") end

abstract type  AbstractContext end
abstract type  AbstractSchedule end

"""
    AbstractRunnable

an abstract type interface for all decisional and utilitary processes (TSO, Market and assessment steps).
Such structure might need to overload the following functions :
- `compute_firmness(runnable::AbstractRunnable, ech, next_ech, TS, context::AbstractContext)`
- `run(runnable::AbstractRunnable, ech, firmness, TS, context::AbstractContext)`
- `affects_market_schedule(runnable::AbstractRunnable)`
- `update_market_schedule!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable)`
- `affects_tso_schedule(runnable::AbstractRunnable)`
- `update_tso_schedule!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable)`
- `affects_tso_actions(runnable::AbstractRunnable)`
- `update_tso_actions!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable)`
"""
abstract type  AbstractRunnable end
"""
Determines the firmnes of commitment and powerlevel decisions for each timestep
"""
function compute_firmness(runnable::AbstractRunnable, ech, next_ech, TS, context::AbstractContext) error("unimplemented") end
"""
Launches the runnable :
    get relevant data from context
    build the optimization model
    solve the model
    return a result object to be exploited by the updating methods
"""
function run(runnable::AbstractRunnable, ech, firmness, TS, context::AbstractContext) error("unimplemented") end
"""
If returns True, will launch the market_schedule related updates during sequence execution
"""
function affects_market_schedule(runnable::AbstractRunnable) return false end
"""
Updates the market schedule using the result of run()
"""
function update_market_schedule!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable) error("unimplemented") end
"""
If returns True, will launch the tso_schedule related updates during sequence execution
"""
function affects_tso_schedule(runnable::AbstractRunnable) return false end
"""
Updates the TSO schedule using the result of run()
"""
function update_tso_schedule!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable) error("unimplemented") end
"""
If returns True, will launch the tso actions related updates during sequence execution
"""
function affects_tso_actions(runnable::AbstractRunnable) return false end
"""
Updates the TSO actions using the result of run()
"""
function update_tso_actions!(context::AbstractContext, ech, result, firmness, runnable::AbstractRunnable) end
#FIXME are these compatible with the Assessment step ?

abstract type  AbstractTSO <: AbstractRunnable  end
function affects_tso_schedule(runnable::AbstractTSO) return true end
function affects_tso_actions(runnable::AbstractTSO) return true end

abstract type  AbstractMarket <: AbstractRunnable  end
function affects_market_schedule(runnable::AbstractMarket) return true end

abstract type DeciderType end