#OVERVIEW
#----------------
#Any JuMP model that describes an optimization problem must have four parts:

#Model Object,
#Variables,
#Objective,
#Constraints.

#PACKAGE IMPORT
#----------------
println("Initializing...")

using JuMP  # Need to say it whenever we use JuMP

using GLPK

using CSV # Import data

using DataFrames # For results export to CSV

using Plots

#=
electrolyzer_tea.jl is a function used to evaluate the TEA of the electrochemical
system, including separations, stack cost, and other operational parameters.
This function takes a series of inputs specific to an electrochemical pathway,
as shown below:

function electrolyzer_TEA(
    flow, product flow in [kg/year]
    product_phase, phase of product ["Gas" or "Liquid"]
    product_mw, molecular weight of product [g/mol]
    product_density, product density [kg/m^3]
    electrons_product, [# e-]
    electrons_co2, [#e-/co2]
    system_potential, whole cell potential [V]
    selectivity, selectivity, or faradaic efficiency of CO2R system to product [%]
    hours_year, hours in a given year [hours/year]
    current_density, [A/cm^2]
    single_pass_conversion, single pass conversion of CO2 to product [%]
    electrolyzer_cost, electrolyzer capital cost [$/m^2]
    electricity_price, [$/kWh]
    water_price, process water cost [$/kg]
    water_rate, rate of water consusumption [kg water/kg product]
    crf capital recovery factor [1/years]
    )
=#
include("./electrolyzer_tea.jl")

#=
The electrolyzer tea function operates identically to the electrolyzer tea function
discussed above, however it returns an array of results for costs rather
than a single value. This allows for a more detailed analysis of costs
included in the TEA. Input values are identical, output values are as follows:

    return:
    electrolyzer_capex + electrolyzer_BOP, electrolyzer costs [$/year]
    PSA_capex + PSA_opex + distil_capex + distil_opex, separations costs [$/year]
    battery_capex, battery energy storage costs [$/year]
    maintenance_cost, system maintenance costs [$/year]
    water_opex annual process water costs [$/year]
=#
include("./electrolyzer_tea_array.jl")

__precompile__()

# Define function to perform optimization routine - decreases runtime
function co2_optimizer()

    #DATA IMPORT
    #--------------------
    input_filename = "input_data_full.csv"
    input_filepath = joinpath(@__DIR__, input_filename)

    # Import point source and point sink data
    data = DataFrame!(CSV.File(input_filepath))

    #=
    Option to exclude specific point types from either sink or
    source data in optimizer. To use, fill the array with the proper name
    of the point source to be excluded, e.g. ["COAL", "GAS"]
    =#
    #--------------------
    source_exclusions =  ["COAL","GAS"]#["None"]#["None"]#["None"]
    sink_exclusions = ["None"]#["Import","Export","Acetic Acid Plant", "Petroleum Terminal"]#["Ethane Crackers"]#["Import","Export"]#["None"]

    for i = 1:length(source_exclusions)

        data = data[data[!,8].!= source_exclusions[i] ,:]

    end

    for i = 1:length(sink_exclusions)

        data = data[data[!,8].!= sink_exclusions[i] ,:]

    end

    println("Source and sink points to be considered:")
    println(size(data,1))

    #VARIABLES and generic functions
    #---------

    # A variable is modeled using @defVar(name of the model object, variable name and bound, variable type)
    # Bound can be lower bound, upper bound or both. If no variable type is defined, then it is treated as
    #real. For binary variable write Bin and for integer use Int.

    # Sink/source point datasets
    #_____________________________________________________________________________

    #= Isolate consumption point data by including only points where
    demand for the molecule is not equal to zero =#
    sinks_C2H4 = data[data[:,4].!= 0  ,:]
    sinks_HCOO = data[data[:,5].!= 0  ,:]
    sinks_CO = data[data[:,6].!= 0  ,:]

    #= Isolate CO2 point source data by including only points where
    feedstock supply is not equal to zero =#
    source_pts = data[data[:,7].!= 0  ,:]
    sink_pts = data[data[:,7].== 0  ,:]

    # Number of chemical products considered
    num_species = 3

    # Find number of CO2 point sources
    num_source_pts = size(data[data[:,7].!= 0  ,:],1)

    # Find number of sink points
    num_sink_pts = size(data[data[:,7].== 0  ,:],1)

    # Find total number of points
    num_pts = size(data,1)

    # Faradays constant
    F = 96485 # [C/mol]

    # Sink/source distance evaluation datasets
    # Goal: build an array showing distance between sink and source points
    #_____________________________________________________________________________

    # Array of lat/long for sources
    lat_source = source_pts[:, 2]
    long_source = source_pts[:, 3]

    # Array of lat/long for sinks
    lat_sink = sink_pts[:, 2]
    long_sink = sink_pts[:, 3]

    # Molecular weights
    #_____________________________________________________________________________
    C_mw = 12.011 # [g/mol]
    CO2_mw = 44 # [g/mol]
    C2H4_mw = 28.05 # [g/mol]
    HCOO_mw = 45.017 # [g/mol]
    CO_mw = 28.01 # [g/mol]
    H2O_mw = 18 # [g/mol]

    product_mw = [C2H4_mw , HCOO_mw, CO_mw]

    # Molecule carbons
    #_____________________________________________________________________________
    C2H4_carbons = 2 # [carbons/molecule]
    HCOO_carbons = 1 # [carbons/molecule]
    CO_carbons = 1 # [carbons/molecule]

    carbons = [C2H4_carbons , HCOO_carbons, CO_carbons]

    # Electrons per mol product
    #_____________________________________________________________________________
    C2H4_electrons_product = 12  # [mol e- / mol product]
    HCOO_electrons_product = 2 # [mol e- / mol product]
    CO_electrons_product = 2 # [mol e- / mol product]
    H2O_electrons = 4 # [mol e- / mol product]

    electrons_product = [C2H4_electrons_product , HCOO_electrons_product, CO_electrons_product]

    # Product state
    #_____________________________________________________________________________
    C2H4_phase = "Gas"
    HCOO_phase = "Liquid"
    CO_phase = "Gas"

    product_phase = [C2H4_phase, HCOO_phase, CO_phase]

    # Product densities
    #_____________________________________________________________________________
    C2H4_density = 1.18 # [kg/m^3]
    HCOO_density = 1221 # [kg/m^3]
    CO_density = 1.14 # [kg/m^3]

    product_density = [C2H4_density , HCOO_density, CO_density]

    # Electrons per mol CO2
    #_____________________________________________________________________________
    C2H4_electrons_co2 = 6  # [mol e- / mol co2]
    HCOO_electrons_co2 = 2 # [mol e- / mol co2]
    CO_electrons_co2 = 2 # [mol e- / mol co2]

    electrons_co2 = [C2H4_electrons_co2 , HCOO_electrons_co2, CO_electrons_co2]

    # Current densities
    #_____________________________________________________________________________
    C2H4_cd = 1.55 # [A/cm^2] 10.1126/science.aay4217
    HCOO_cd = 0.5 # [A/cm^2] 10.1021/acsenergylett.0c00860
    CO_cd = 0.35 # [A/cm^2] http://xlink.rsc.org/?DOI=C9EE02410G

    current_density = [C2H4_cd , HCOO_cd, CO_cd]

    # single pass conversion
    #_____________________________________________________________________________
    C2H4_spc = 0.5 # [%] assumed
    HCOO_spc = 0.5 # [%] assumed
    CO_spc = 0.5 # [%] assumed

    single_pass_conversion = [C2H4_spc , HCOO_spc, CO_spc]

    # Selectivities
    #___________________________________________________________________________
    C2H4_selectivity = 0.6 # 10.1126/science.aay4217
    HCOO_selectivity = 0.93 #  10.1021/acsenergylett.0c00860
    CO_selectivity = 0.95  #  http://xlink.rsc.org/?DOI=C9EE02410G

    selectivity = [C2H4_selectivity , HCOO_selectivity, CO_selectivity]

    # Potentials
    #_____________________________________________________________________________
    C2H4_system_potential = 3.9 #[V] 10.1126/science.aay4217 taken from fig s33, durability study
    HCOO_system_potential = 5.8 #[V] 10.1021/acsenergylett.0c00860 table 1
    CO_system_potential = 3.0 #[V] 10.1021/acs.iecr.7b03514

    system_potential = [C2H4_system_potential , HCOO_system_potential, CO_system_potential]

    # Market Values
    #_____________________________________________________________________________
    C2H4_value = 0.555 # [$/kg] C2H4 value from PEP yearbook and: https://www.spglobal.com/platts/en/market-insights/podcasts/focus/040720-north-america-lng-exports-price-plunge
    HCOO_value = 0.735 # [$/kg] CO value https://pubs.acs.org/doi/10.1021/acs.iecr.7b03514
    CO_value = 0.6 # [$/kg] CO value https://pubs.acs.org/doi/10.1021/acs.iecr.7b03514

    value = [C2H4_value , HCOO_value, CO_value]

    # Market Demand
    #_____________________________________________________________________________
    C2H4_demand = sink_pts[:,4]  # consumption at each point in [kg/year]
    HCOO_demand = sink_pts[: ,5] # consumption at each point in [kg/year]
    CO_demand = sink_pts[: ,6] # consumption at each point in [kg/year]

    demand = hcat(C2H4_demand, HCOO_demand, CO_demand)

    # Water rate of consumption
    #_____________________________________________________________________________
    C2H4_water = 1.28 #[kg H2O/kg product]
    HCOO_water = 0.391 #[kg H2O/kg product]
    CO_water = 0.6426 #[kg H2O/kg product]

    water_rate = [C2H4_water, HCOO_water, CO_water]

    #_____________________________________________________________________________
    capacity = data[data[:,7].!= 0  ,7] * 1e3 * (C_mw / CO2_mw) # Feedstock carbon at each point in [kg C/year]

    transport_A = 1e-5 # Best fit line for CO2 transportation costs from IPCC (see Excel sheet)
    transport_B = -0.0002 # Best fit line for CO2 transportation costs from IPCC (see Excel sheet)

    #CO2 flue gas capture cost for each location [$/kg C]
    CO2_cap_cost = data[data[:,7].!= 0  ,12] / 1e3 * (C_mw / CO2_mw) #[$/kg C]

    # Spatially distinct electricity prices in [$/kWh] (sink pts)
    electricity_price = data[data[:,7].!= 0  ,15]

    # Economic assumptions
    #_____________________________________________________________________________

    hours_year = 8000 # [hours of operation annually]
    electrolyzer_cost = 10000 # [$/m^2]
    interest_rate = 0.08 # [%]
    facility_lifetime = 15 #[years]
    water_price = 8.929e-4 # [$/kg] https://www.energy.gov/sites/prod/files/2017/10/f38/water_wastewater_escalation_rate_study.pdf
    crf = (interest_rate * (1 + interest_rate)^facility_lifetime) / ((1 + interest_rate)^facility_lifetime - 1)

    # Define an array with rows = number of sources, columns = number of sinks, and sheets = number of products
    D = zeros(Float16, num_source_pts, num_sink_pts, num_species)

    # Distance constraint evaluation.
    #_____________________________________________________________________________
    #=
    Define an array with rows = source points, columns = sink points,
    sheets = number of species. Default values are zeros. The loop below changes
    the value to 1 if distance between sink/source is <1,000 km.
    This is then incorporated in the flow variable definition as an upper
    bound on the variable.
    =#
    up_bound = zeros(num_source_pts,num_sink_pts,num_species)
    for i in 1:num_source_pts
        for j in 1:num_sink_pts
            for k in 1:num_species
                pt1 = [long_source[i]; lat_source[i]]
                pt2 = [long_sink[j]; lat_sink[j]]

                D[i, j, k] = (abs(pt1[1] - pt2[1]) + abs(pt1[2] - pt2[2])) / 1e3

                if (abs(source_pts[i,3] - sink_pts[j,3]) + abs(source_pts[i,2] - sink_pts[j,2])) / 1e3 < 1000
                    up_bound[i, j, k] = 1.0

                end
            end
        end
    end

    #MODEL CONSTRUCTION
    #--------------------

    # Preparing an optimization model
    eco2RR = Model(GLPK.Optimizer)

    #OPTIMIZATION VARIABLES
    #--------------------
    #Initialize flow array [kg/year]
    @variable(eco2RR, flow[i = 1:num_source_pts , j = 1:num_sink_pts , k = 1:num_species] >= 0, upper_bound = up_bound[i, j, k].*1e10) # distance constraint
    #@variable(eco2RR, flow[i = 1:num_source_pts , j = 1:num_sink_pts , k = 1:num_species] >= 0)

    #OPTIMIZATION CONSTRAINTS
    #--------------------

    # Ship no more than plant capacity, in units of [kg carbon]
    @constraint(eco2RR, capacity_con[i in 1:num_source_pts],
    + sum(flow[i, : , 1]) * carbons[1] * C_mw / product_mw[1] / selectivity[1]
    + sum(flow[i, : , 2]) * carbons[2] * C_mw / product_mw[2] / selectivity[2]
    + sum(flow[i, : , 3]) * carbons[3] * C_mw / product_mw[3] / selectivity[3]
    <= capacity[i])

    #=
    Constrain total electrochemical system power consumption at a source point
    to less than 300MW. Provides a realistic constraint on system size

    Modifying this equality only applies when the system is feedstock constrained
    i.e. total demand is greater than carbon feedstock available

    Two key options are available:

    == equality - model required to export 300MW worth at each CO2 point.

    <= equality - model can provide anything up to 300MW system at a CO2 point source.

    =#

    @constraint(eco2RR, elec_con[i in 1:num_source_pts],
    sum(
    + sum(flow[i, :, 1]) / hours_year / 3600 * 1e3 / product_mw[1] * electrons_product[1] * F * system_potential[1] / selectivity[1] / 1e6
    + sum(flow[i, :, 2]) / hours_year / 3600 * 1e3 / product_mw[2] * electrons_product[2] * F * system_potential[2] / selectivity[2] / 1e6
    + sum(flow[i, :, 3]) / hours_year / 3600 * 1e3 / product_mw[3] * electrons_product[3] * F * system_potential[3] / selectivity[3] / 1e6
    )
    <= 500)

    #=
    Choose how the model attempts to meet demand for different molecules.
    This constraint can cause the model to be unable to find a SOLUTION
    if feedstock inputs are not sufficient to meet demand.

    Modifying this equality only applies when the system is demand constrained
    i.e. total demand is less than carbon feedstock available

    Two key options are available:

    == equality - model required to meet demand at each point. In this scenario,
        if feedstock inputs are not sufficient then optimizer will not find a
        solution

    <= equality - model can provide anything up to market demand at a point.
        Using this equality allows the optimizer to only make flows it deems
        economically sustainable (positive NPV).

    =#
    @constraint(eco2RR, demand_con[j in 1:num_sink_pts, k in 1:num_species],
    sum(flow[:, j , k]) == demand[j,k])

    println("Constraints Defined")

    #OPTIMIZATION OBJECTIVE FUNCTION
    #--------------------
    println("Beginning Optimization...")

    @objective(
        eco2RR, # Designate model name
        Max, # Tell JuMP to minimize/optimize
        sum(
        (

        - (D[i, j, k] * transport_A + transport_B) * flow[i, j, k] # Transportation cost
        - electrolyzer_TEA(
            flow[i, j, k],
            product_phase[k],
            product_mw[k],
            product_density[k],
            electrons_product[k],
            electrons_co2[k],
            system_potential[k],
            selectivity[k],
            hours_year,
            current_density[k],
            single_pass_conversion[k],
            electrolyzer_cost,
            electricity_price[i],
            water_price,
            water_rate[k],
            crf) # System TEA
        - flow[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
        - flow[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical system electricity cost
        + flow[i, j, k] * value[k] # Product value
        )

        for
            i in 1:num_source_pts, # Iterate over source points
            j in 1: num_sink_pts, # Iterate over sink points
            k in 1: num_species#, # Iterate over number of species
    ))

    # Solving the optimization problem
    optimize!(eco2RR)

    # Set objective value
    obj_value = JuMP.objective_value(eco2RR)

    #OPTIMIZATION SOLUTION WRANGLING
    #--------------------
    println("Optimizer termination status:")
    println(termination_status(eco2RR))
    println(primal_status(eco2RR))
    println("Objective function value:")
    println(obj_value)

    #Output filepath and variable name
    #--------------------

    C2H4_result_filepath = joinpath(@__DIR__, "results/flows/C2H4_flows.csv")
    HCOO_result_filepath = joinpath(@__DIR__, "results/flows/HCOO_flows.csv")
    CO_result_filepath = joinpath(@__DIR__, "results/flows/CO_flows.csv")

    C2H4_NPV_filepath = joinpath(@__DIR__, "results/npv/C2H4_NPV.csv")
    HCOO_NPV_filepath = joinpath(@__DIR__, "results/npv/HCOO_NPV.csv")
    CO_NPV_filepath = joinpath(@__DIR__, "results/npv/CO_NPV.csv")

    C2H4_trans_filepath = joinpath(@__DIR__, "results/econ/trans/C2H4_trans.csv")
    HCOO_trans_filepath = joinpath(@__DIR__, "results/econ/trans/HCOO_trans.csv")
    CO_trans_filepath = joinpath(@__DIR__, "results/econ/trans/CO_trans.csv")

    C2H4_tea_filepath = joinpath(@__DIR__, "results/econ/tea/C2H4_tea.csv")
    HCOO_tea_filepath = joinpath(@__DIR__, "results/econ/tea/HCOO_tea.csv")
    CO_tea_filepath = joinpath(@__DIR__, "results/econ/tea/CO_tea.csv")

    C2H4_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/C2H4_ccu.csv")
    HCOO_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/HCOO_ccu.csv")
    CO_ccu_filepath = joinpath(@__DIR__, "results/econ/ccu/CO_ccu.csv")

    C2H4_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/C2H4_elec.csv")
    HCOO_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/HCOO_elec.csv")
    CO_elec_filepath = joinpath(@__DIR__, "results/econ/electricity/CO_elec.csv")

    C2H4_prf_filepath = joinpath(@__DIR__, "results/econ/prf/C2H4_prf.csv")
    HCOO_prf_filepath = joinpath(@__DIR__, "results/econ/prf/HCOO_prf.csv")
    CO_prf_filepath = joinpath(@__DIR__, "results/econ/prf/CO_prf.csv")

    C2H4_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/C2H4_water_rsrc.csv")
    HCOO_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/HCOO_water_rsrc.csv")
    CO_water_rsrc_filepath = joinpath(@__DIR__, "results/resource/water/CO_water_rsrc.csv")

    C2H4_power_filepath = joinpath(@__DIR__, "results/resource/power/C2H4_power.csv")
    HCOO_power_filepath = joinpath(@__DIR__, "results/resource/power/HCOO_power.csv")
    CO_power_filepath = joinpath(@__DIR__, "results/resource/power/CO_power.csv")

    C2H4_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/C2H4_electrolyzer.csv")
    HCOO_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/HCOO_electrolyzer.csv")
    CO_electrolyzer_filepath = joinpath(@__DIR__, "results/econ/electrolyzer/CO_electrolyzer.csv")

    C2H4_separations_filepath = joinpath(@__DIR__, "results/econ/separations/C2H4_separations.csv")
    HCOO_separations_filepath = joinpath(@__DIR__, "results/econ/separations/HCOO_separations.csv")
    CO_separations_filepath = joinpath(@__DIR__, "results/econ/separations/CO_separations.csv")

    C2H4_battery_filepath = joinpath(@__DIR__, "results/econ/battery/C2H4_battery.csv")
    HCOO_battery_filepath = joinpath(@__DIR__, "results/econ/battery/HCOO_battery.csv")
    CO_battery_filepath = joinpath(@__DIR__, "results/econ/battery/CO_battery.csv")

    C2H4_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/C2H4_maintenance.csv")
    HCOO_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/HCOO_maintenance.csv")
    CO_maintenance_filepath = joinpath(@__DIR__, "results/econ/maintenance/CO_maintenance.csv")

    C2H4_water_filepath = joinpath(@__DIR__, "results/econ/water/C2H4_water.csv")
    HCOO_water_filepath = joinpath(@__DIR__, "results/econ/water/HCOO_water.csv")
    CO_water_filepath = joinpath(@__DIR__, "results/econ/water/CO_water.csv")

    # Initialize flow output array
    flow_results = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Populate each output array with JuMP varaible value found in optimizer
    for i = 1:num_source_pts
        for j = 1:num_sink_pts
            for k in 1:num_species
                flow_results[i, j, k] = JuMP.value(flow[i, j, k])
            end
        end
    end

    # Initialize NPV output arrays for each molecule
    results_NPV = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_elec = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_trans = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_ccu = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_tea = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Initialize electricity cost output arrays for each molecule
    results_prf = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize water consumption output arrays for each molecule
    results_water = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize power consumption output arrays for each molecule
    results_power = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize electrolyzer cost output arrays for each molecule
    results_electrolyzer = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize separations cost output arrays for each molecule
    results_separations = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize battery cost output arrays for each molecule
    results_battery = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize electrolyzer cost output arrays for each molecule
    results_maintenance = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    #Initialize water cost output arrays for each molecule
    results_water_rsrc = zeros(Float64, num_source_pts, num_sink_pts, num_species)

    # Populate each output array with JuMP varaible value found in optimizer
    for i = 1:num_source_pts
        for j = 1:num_sink_pts
            for k = 1:num_species
                results_NPV[i, j, k] = sum(
                (
                - (D[i, j, k] * transport_A + transport_B) * flow_results[i, j, k] # Transportation cost
                -  electrolyzer_TEA(
                    flow_results[i, j, k],
                    product_phase[k],
                    product_mw[k],
                    product_density[k],
                    electrons_product[k],
                    electrons_co2[k],
                    system_potential[k],
                    selectivity[k],
                    hours_year,
                    current_density[k],
                    single_pass_conversion[k],
                    electrolyzer_cost,
                    electricity_price[i],
                    water_price,
                    water_rate[k],
                    crf)  # TEA system cost
                - flow_results[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
                - flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical electricity price
                + flow_results[i, j, k] * value[k] # Product value
                )
                )

                if flow_results[i, j, k] != 0
                    results_electrolyzer[i, j, k],
                    results_separations[i, j, k],
                    results_battery[i, j, k],
                    results_maintenance[i, j, k],
                    results_water[i, j, k] =
                    electrolyzer_TEA_array(
                        flow_results[i, j, k],
                        product_phase[k],
                        product_mw[k],
                        product_density[k],
                        electrons_product[k],
                        electrons_co2[k],
                        system_potential[k],
                        selectivity[k],
                        hours_year,
                        current_density[k],
                        single_pass_conversion[k],
                        electrolyzer_cost,
                        electricity_price[i],
                        water_price,
                        water_rate[k],
                        crf)  # TEA system cost
                    results_trans[i, j, k] = (D[i, j, k] * transport_A + transport_B) * flow_results[i, j, k] # Transportation cost
                    results_tea[i, j, k] =  electrolyzer_TEA(
                        flow_results[i, j, k],
                        product_phase[k],
                        product_mw[k],
                        product_density[k],
                        electrons_product[k],
                        electrons_co2[k],
                        system_potential[k],
                        selectivity[k],
                        hours_year,
                        current_density[k],
                        single_pass_conversion[k],
                        electrolyzer_cost,
                        electricity_price[i],
                        water_price,
                        water_rate[k],
                        crf)  # TEA system cost
                    results_ccu[i, j, k] =  flow_results[i, j, k] * carbons[k] * C_mw / product_mw[k] * CO2_cap_cost[i] # Carbon capture cost
                    results_elec[i, j, k] =  flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 * hours_year * electricity_price[i] # Electrochemical electricity price
                    results_prf[i, j, k] = flow_results[i, j, k] * value[k] # Product value
                    results_water_rsrc[i, j, k] = flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F / selectivity[k] / H2O_electrons / F * H2O_mw / 1e3 * hours_year * 3600
                    results_power[i, j, k] = flow_results[i, j, k] / hours_year / 3600 * 1e3 / product_mw[k] * electrons_product[k] * F * system_potential[k] / selectivity[k] / 1e3 #kW
                end
            end
        end
    end

    # Write sink/source data to CSV files for data visualization
    # Useful for runs where points are excluded from optimization routine
    source_index_path = joinpath(@__DIR__, "results/index/source_index.csv")
    sink_index_path = joinpath(@__DIR__, "results/index/sink_index.csv")

    CSV.write(source_index_path, DataFrame(source_pts))
    CSV.write(sink_index_path, DataFrame(sink_pts))

    # Write output flow files to CSV
    CSV.write(C2H4_result_filepath, DataFrame(flow_results[:, :, 1]))
    CSV.write(HCOO_result_filepath, DataFrame(flow_results[:, :, 2]))
    CSV.write(CO_result_filepath, DataFrame(flow_results[:, :, 3]))

    # write output NPV dataframes to CSV
    CSV.write(C2H4_NPV_filepath, DataFrame(results_NPV[:, :, 1]))
    CSV.write(HCOO_NPV_filepath, DataFrame(results_NPV[:, :, 2]))
    CSV.write(CO_NPV_filepath, DataFrame(results_NPV[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_trans_filepath, DataFrame(results_trans[:, :, 1]))
    CSV.write(HCOO_trans_filepath, DataFrame(results_trans[:, :, 2]))
    CSV.write(CO_trans_filepath, DataFrame(results_trans[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_tea_filepath, DataFrame(results_tea[:, :, 1]))
    CSV.write(HCOO_tea_filepath, DataFrame(results_tea[:, :, 2]))
    CSV.write(CO_tea_filepath, DataFrame(results_tea[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_ccu_filepath, DataFrame(results_ccu[:, :, 1]))
    CSV.write(HCOO_ccu_filepath, DataFrame(results_ccu[:, :, 2]))
    CSV.write(CO_ccu_filepath, DataFrame(results_ccu[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_elec_filepath, DataFrame(results_elec[:, :, 1]))
    CSV.write(HCOO_elec_filepath, DataFrame(results_elec[:, :, 2]))
    CSV.write(CO_elec_filepath, DataFrame(results_elec[:, :, 3]))

    # write output electricity cost dataframes to CSV
    CSV.write(C2H4_prf_filepath, DataFrame(results_prf[:, :, 1]))
    CSV.write(HCOO_prf_filepath, DataFrame(results_prf[:, :, 2]))
    CSV.write(CO_prf_filepath, DataFrame(results_prf[:, :, 3]))

    # write output water consumption dataframes to CSV
    CSV.write(C2H4_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 1]))
    CSV.write(HCOO_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 2]))
    CSV.write(CO_water_rsrc_filepath, DataFrame(results_water_rsrc[:, :, 3]))

    # write output power consumption dataframes to CSV
    CSV.write(C2H4_power_filepath, DataFrame(results_power[:, :, 1]))
    CSV.write(HCOO_power_filepath, DataFrame(results_power[:, :, 2]))
    CSV.write(CO_power_filepath, DataFrame(results_power[:, :, 3]))

    # write output electrolyzer cost dataframes to CSV
    CSV.write(C2H4_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 1]))
    CSV.write(HCOO_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 2]))
    CSV.write(CO_electrolyzer_filepath, DataFrame(results_electrolyzer[:, :, 3]))

    # write output separations cost dataframes to CSV
    CSV.write(C2H4_separations_filepath, DataFrame(results_separations[:, :, 1]))
    CSV.write(HCOO_separations_filepath, DataFrame(results_separations[:, :, 2]))
    CSV.write(CO_separations_filepath, DataFrame(results_separations[:, :, 3]))

    # write output battery cost dataframes to CSV
    CSV.write(C2H4_battery_filepath, DataFrame(results_battery[:, :, 1]))
    CSV.write(HCOO_battery_filepath, DataFrame(results_battery[:, :, 2]))
    CSV.write(CO_battery_filepath, DataFrame(results_battery[:, :, 3]))

    # write output maintenance cost dataframes to CSV
    CSV.write(C2H4_maintenance_filepath, DataFrame(results_maintenance[:, :, 1]))
    CSV.write(HCOO_maintenance_filepath, DataFrame(results_maintenance[:, :, 2]))
    CSV.write(CO_maintenance_filepath, DataFrame(results_maintenance[:, :, 3]))

    # write output water cost dataframes to CSV
    CSV.write(C2H4_water_filepath, DataFrame(results_water[:, :, 1]))
    CSV.write(HCOO_water_filepath, DataFrame(results_water[:, :, 2]))
    CSV.write(CO_water_filepath, DataFrame(results_water[:, :, 3]))

    println("Optimization Routine Completed!")

end

@time co2_optimizer()


module Curves

import ..CryptoGroups: <|, tobits, validate, a, b, cofactor, order

include("elliptic_curves.jl")
include("ecpoint.jl")

end


# We will be wrapping MLJ machines/resampling machines
# (cf. MLJBase/src/resampling.jl). It is for the wrapped type that we
# will be overloading the methods of `IterationControl`.

const ERR_EVALUATION =
    ArgumentError("There are no evaluation objects if `resampling=nothing`. ")

mlj_model(mach::Machine) = mach.model
mlj_model(mach::Machine{<:Resampler}) = mach.model.model

# training machine wrappper (constructor resets mlj model iterations
# to zero):
struct ICModel{M}
    mach::M
    iteration_parameter::Union{Symbol,Expr}
    verbosity::Integer
    n::Base.RefValue{Int64} # num calls to train!
    i::Base.RefValue{Int64} # num iterations
    Δi::Base.RefValue{Int64}
    function ICModel(mach::M, iteration_parameter, verbosity) where M
        model = mlj_model(mach)
        rset!(model, iteration_parameter, 0)
        return new{M}(mach,
                      iteration_parameter,
                      verbosity,
                      Ref(0),
                      Ref(0),
                      Ref(0))
    end
end

mlj_model(ic_model::ICModel) = mlj_model(ic_model.mach)

# overloading `train!`:
function IterationControl.train!(m::ICModel, n::Int)

    # increment training call counter:
    m.n[] = m.n[] + 1

    model = mlj_model(m)

    # update iteration parameter value stored on ic_model:
    m.i[] = rget(model, m.iteration_parameter) + n

    # update the iteration parameter in the mlj model:
    rset!(model, m.iteration_parameter, m.i[])

    fit!(m.mach, verbosity=m.verbosity - 2)
    m.Δi[] = n
end

# overloading `expose`- for `resampling === nothing`:
IterationControl.expose(ic_model::ICModel) = ic_model.mach

# overloading `expose`- for `resampling isa Holdout` or
# other resampling strategy:
IterationControl.expose(ic_model::ICModel{<:Machine{<:Resampler}}) =
    MLJBase.fitted_params(ic_model.mach).machine

# overloading `loss` - for `resampling === nothing`:
function IterationControl.loss(m::ICModel)
    losses = training_losses(IterationControl.expose(m))
    losses isa Nothing && return nothing
    return last(losses)
end

# overloading `loss` - for `resampling isa Holdout`:
IterationControl.loss(m::ICModel{<:Machine{<:Resampler}}) =
    evaluate(m.mach).measurement |> first

# overloading `training_loss`:
function IterationControl.training_losses(m::ICModel)
    mach = IterationControl.expose(m)
    losses = training_losses(mach)
    losses isa Nothing && return nothing
    s = length(losses)
    return view(losses, (s - m.Δi[] + 1):s)
end

_evaluation(ic_model) = throw(ERR_EVALUATION)
_evaluation(ic_model::ICModel{<:Machine{<:Resampler}}) =
    MLJBase.evaluate(ic_model.mach)

# interface for user-defined controls:
function Base.getproperty(m::ICModel, property::Symbol)
    property === :machine && return IterationControl.expose(m)
    property === :model && return mlj_model(m)
    property === :n_cycles && return m.n[]
    property === :n_iterations && return m.i[]
    property === :Δiterations && return m.Δi[]
    property === :loss && return IterationControl.loss(m)
    property === :training_losses && return IterationControl.training_losses(m)
    property === :evaluation && return _evaluation(m)
    return getfield(m, property)
end
Base.propertynames(::ICModel) = (:machine,
                                 :model,
                                 :n_cycles,
                                 :n_iterations,
                                 :Δiterations,
                                 :loss,
                                 :training_losses,
                                 :evaluation)


using WAV
using SpeechAugment

dB = (5.0, 15.0)
period = 10
# read a wav file
wav = wavread("xxx.wav")[1]
# init a addnoise function with dir only having noise audios
addnoise = initAddNoise("XXPathFullOfNoiseWAVs", period, dB)
# every period times the addnoise function would change a background noise audio
noisy = addnoise(wav);


# TODO: rename it to something since "Interval Tree" means a specific data structure.
# NestedIntervals? CallTree?
struct IntervalTree
    eventid::EventID
    branches::Union{Vector{IntervalTree},Nothing}
    height::Int  # TODO: maybe `level` is a better name (as its 0-origin)
end

IntervalTree(eventid::EventID) = IntervalTree(eventid, nothing, 0)

function IntervalTree(eventid::EventID, branches::Vector{IntervalTree})
    if isempty(branches)
        return IntervalTree(eventid)
    else
        return IntervalTree(eventid, branches, maximum(i -> i.height, branches) + 1)
    end
end

Base.:(==)(x::IntervalTree, y::IntervalTree) =
    isequal(x.eventid, y.eventid) &&
    isequal(x.height, y.height) &&
    isequal(x.branches, y.branches)

struct IntervalStacks
    trees::Vector{IntervalTree}
    points::Vector{EventID}
    events::Dict{EventID,Event}
    locations::Dict{UInt64,Location}
end

function interval_tree(etbl::AbstractVector{Event}, events::Dict{EventID,Event})
    intervals_data = StructVector(
        (
            approx_taskid = e.approx_taskid,
            eventid = e.eventid,
            start = e.start,
            stop = e.stop,
        ) for e in etbl if e.stop isa TimeNS
    )
    intervals_df = DataFrame(intervals_data; copycols = false)
    trees = IntervalTree[]
    for task in groupby(intervals_df, :approx_taskid)
        intervals = Tables.rowtable(task[:, [:eventid, :start, :stop]])
        sort!(intervals; by = i -> i.start)
        append!(trees, _interval_trees(intervals))
    end
    sort!(trees, by = t -> events[t.eventid].start)
    return trees
end

# Optimization ideas: It's straightforward to parallelize this.  Also, maybe
# galloping search is better than binary search.
function _interval_trees(intervals)
    length(intervals) == 1 && return [IntervalTree(intervals[1].eventid)]
    k1 = firstindex(intervals)
    trees = IntervalTree[]
    while true
        root = intervals[k1]
        offset = searchsortedlast(
            @view(intervals[k1:end]),
            (start = root.stop,),
            by = i -> i.start,
        )
        @assert offset > 0   # no time travel `start <= stop`
        k2 = k1 + offset - 1
        if offset == 1
            t = IntervalTree(root.eventid)
        else
            t = IntervalTree(root.eventid, _interval_trees(view(intervals, k1+1:k2)))
        end
        push!(trees, t)
        k2 == lastindex(intervals) && break
        k1 = k2 + 1
    end
    return trees
end

# [...](@ref EventTrackerBase.@recordinterval) didn't work somehow
"""
    EventTracker.stacks()

Analyze nestings of the recorded intervals to recover the "call trees" of the
functions instrumented with [`@recordinterval`](@ref
EventTracker.@recordinterval) macro.

Currently, the returned object only supports `Plots.plot` API.
"""
EventTracker.stacks
EventTracker.stacks() =
    EventTracker.stacks(EventTracker.event_table(), EventTracker.location_table())

function EventTracker.stacks(etbl::AbstractVector{Event}, ltbl::AbstractVector{Location})
    events = Dict{EventID,Event}(e.eventid => e for e in etbl)
    return IntervalStacks(
        interval_tree(etbl, events),
        EventID[e.eventid for e in etbl if e.eventkind === EVENT_POINT],
        events,
        Dict{UInt64,Location}(l.locationid => l for l in ltbl),
    )
end

function append_owntime!(table, stks::IntervalStacks)
    for tree in stks.trees
        append_owntime!(table, tree, stks.events)
    end
    return table
end

function duration_of(tree::IntervalTree, events::AbstractDict{EventID,Event})
    e = events[tree.eventid]
    return e.stop - e.start
end

function owntime_of(tree::IntervalTree, events::AbstractDict{EventID,Event})
    cumtime = duration_of(tree, events)
    tree.branches === nothing && return cumtime
    subtime = sum(tree.branches) do sub
        duration_of(sub, events)
    end
    return cumtime - subtime
end

function append_owntime!(table, tree::IntervalTree, events::AbstractDict{EventID,Event})
    push!(table, (eventid = tree.eventid, owntime = owntime_of(tree, events)))
    if tree.branches !== nothing
        for sub in tree.branches
            append_owntime!(table, sub, events)
        end
    end
    return table
end

const OwntimeAccumulatorDict = Dict{EventID,typeof((owntime = TimeNS(0), count = 0))}

struct OwntimeAccumulator
    dict::OwntimeAccumulatorDict
end
OwntimeAccumulator() = OwntimeAccumulator(OwntimeAccumulatorDict())

function Base.push!(acc::OwntimeAccumulator, row)
    dict, _ = modify!!(acc.dict, row.eventid) do entry
        if entry === nothing
            Some((owntime = row.owntime, count = 1))
        else
            v = something(entry)
            Some((owntime = row.owntime + v.owntime, count = v.count + 1))
        end
    end
    @assert dict === acc.dict
    return acc
end

function owntime_dict(stks::IntervalStacks)
    acc = append_owntime!(OwntimeAccumulator(), stks)
    return acc.dict
end


module PSCOPFio

using ..PSCOPF
using ..Networks

using Dates
using Printf
using DataStructures

OUTPUT_PREFIX = "pscopf_out_"

##########################
#   Readers
##########################

function read_buses!(network::Network, data::String)
    buses_ids = Set{String}();
    open(joinpath(data, "pscopf_ptdf.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);
                push!(buses_ids, buffer[2])
            end
        end
    end

    open(joinpath(data, "pscopf_units.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);
                push!(buses_ids, buffer[3])
            end
        end
    end

    Networks.add_new_buses!(network, collect(buses_ids));
end

function read_branches!(network::Network, data::String)
    branches = Dict{String,Float64}();
    default_limit = 0.
    open(joinpath(data, "pscopf_ptdf.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);
                branch_id = buffer[1]
                push!(branches, branch_id => default_limit)
            end
        end
    end

    open(joinpath(data, "pscopf_limits.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);
                branch_id = buffer[1]
                limit = parse(Float64, buffer[2]);
                push!(branches, branch_id=>limit)
            end
        end
    end

    for (id,limit) in branches
        Networks.add_new_branch!(network, id, limit);
    end
end

function read_ptdf!(network::Network, data::String, filename="pscopf_ptdf.txt")
    open(joinpath(data, filename), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);
                branch_id = buffer[1]
                bus_id = buffer[2]
                ptdf_value = parse(Float64, buffer[3])
                Networks.add_ptdf_elt!(network, branch_id, bus_id, ptdf_value)
            end
        end
    end
end

function read_generators!(network, data)
    open(joinpath(data, "pscopf_units.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);

                generator_id = buffer[1]
                gen_type = parse(Networks.GeneratorType, buffer[2])
                gen_bus_id = buffer[3]
                pmin = parse(Float64, buffer[4])
                pmax = parse(Float64, buffer[5])
                start_cost = parse(Float64, buffer[6])
                prop_cost = parse(Float64, buffer[7])
                dmo = Dates.Second(parse(Float64, buffer[8]))
                dp = Dates.Second(parse(Float64, buffer[9]))

                Networks.add_new_generator_to_bus!(network, gen_bus_id,
                                        generator_id, gen_type, pmin, pmax, start_cost, prop_cost, dmo, dp)
            end
        end
    end
end

function read_uncertainties_distributions(data)
    result = PSCOPF.UncertaintiesDistribution()
    open(joinpath(data, "uncertainties_distribution.txt"), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln);

                id = buffer[1]
                min_value = parse(Float64, buffer[2])
                max_value = parse(Float64, buffer[3])
                mu = parse(Float64, buffer[4])
                sigma = parse(Float64, buffer[5])
                time_factor = parse(Float64, buffer[6])

                PSCOPF.add_uncertainty_distribution!(result,
                                        id, min_value, max_value, mu, sigma,
                                        time_factor)
            end
        end
    end

    return result
end

function read_uncertainties(data, filename="pscopf_uncertainties.txt")
    result = PSCOPF.Uncertainties()
    open(joinpath(data, filename), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln)
                # "name", "ts", "ech", "scenario", "value"))
                name = buffer[1]
                ts = Dates.DateTime(buffer[2])
                ech = Dates.DateTime(buffer[3])
                scenario = buffer[4]
                value = parse(Float64, buffer[5])
                PSCOPF.add_uncertainty!(result, ech, name, ts, scenario, value)
            end
        end
    end
    return result
end

function read_initial_state(data, filename="pscopf_init.txt")
    result = SortedDict{String, PSCOPF.GeneratorState}()
    open(joinpath(data, filename), "r") do file
        for ln in eachline(file)
            # don't read commentted line
            if ln[1] != '#'
                buffer = PSCOPF.split_with_space(ln)
                gen_id = buffer[1]
                state = parse(PSCOPF.GeneratorState, buffer[2])
                result[gen_id] = state
            end
        end
    end
    return result
end

##########################
#   Writers
##########################

function write(dir_path::String, network::Networks.Network)
    mkpath(dir_path)
    # if !isdir(dir_path)
    #     mkpath(dir_path)
    # else
    #     msg = @sprintf("data folder `%s` already exists!", dir_path)
    #     error(msg)
    # end
    #units
    write(dir_path, network.generators)
    #limits
    write(dir_path, network.branches)
    #ptdf
    write(dir_path, network.ptdf)
end

function write(dir_path::String, generators::SortedDict{String, Networks.Generator})
    output_file_l = joinpath(dir_path, "pscopf_units.txt")
    open(output_file_l, "w") do file_l
        Base.write(file_l, @sprintf("#%24s%16s%25s%16s%16s%16s%16s%16s%16s\n",
                    "name", "type", "bus_id", "minP","maxP", "start", "prop", "dmo(s)", "dp(s)"))
        for (id_l, generator_l) in generators
            Base.write(file_l, @sprintf("%25s%16s%25s%16.8E%16.8E%16.8E%16.8E%16.8E%16.8E\n",
                                    Networks.get_id(generator_l),
                                    Networks.get_type(generator_l),
                                    Networks.get_bus_id(generator_l),
                                    Networks.get_p_min(generator_l),
                                    Networks.get_p_max(generator_l),
                                    Networks.get_start_cost(generator_l),
                                    Networks.get_prop_cost(generator_l),
                                    Dates.value(Networks.get_dmo(generator_l)),
                                    Dates.value(Networks.get_dp(generator_l))
                                    )
                    )
        end
    end
end

function write(dir_path::String, branches::SortedDict{String, Networks.Branch})
    output_file_l = joinpath(dir_path, "pscopf_limits.txt")
    open(output_file_l, "w") do file_l
        Base.write(file_l, @sprintf("#%24s%16s\n", "branch", "limit"))
        for (id_l, branch_l) in branches
            Base.write(file_l, @sprintf("%25s%16.8E\n",
                                    Networks.get_id(branch_l),
                                    Networks.get_limit(branch_l)
                                    )
                    )
        end
    end
end

function write(dir_path::String, ptdf::SortedDict{String,SortedDict{String, Float64}})
    output_file_l = joinpath(dir_path, "pscopf_ptdf.txt")
    open(output_file_l, "w") do file_l
        Base.write(file_l, @sprintf("#%24s%16s\n", "REF_BUS", "unknown"))
        Base.write(file_l, @sprintf("#%24s%25s%16s\n", "branch", "bus", "value"))
        for (branch_id_l, _) in ptdf
            for (bus_id_l, val_l) in ptdf[branch_id_l]
                Base.write(file_l, @sprintf("%25s%25s%16.8E\n",
                                        branch_id_l,
                                        bus_id_l,
                                        val_l
                                        )
                            )
            end
        end
    end
end

function write(dir_path::String, gen_init::SortedDict{String, PSCOPF.GeneratorState})
    mkpath(dir_path)
    output_file_l = joinpath(dir_path, "pscopf_init.txt")
    open(output_file_l, "w") do file_l
        Base.write(file_l, @sprintf("#%24s%10s\n", "name", "state"))
        for (gen_id, state) in gen_init
            Base.write(file_l, @sprintf("%25s%10s\n", gen_id, state))
        end
    end
end

function write(dir_path::String, uncertainties::PSCOPF.Uncertainties)
    mkpath(dir_path)
    output_file_l = joinpath(dir_path, "pscopf_uncertainties.txt")
    open(output_file_l, "w") do file_l

        Base.write(file_l, @sprintf("#%24s%20s%20s%10s%16s\n", "name", "ts", "ech", "scenario", "value"))
        for (ech, _) in uncertainties
            for (nodal_injection_name, _) in uncertainties[ech]
                for (ts, _) in uncertainties[ech][nodal_injection_name]
                    for (scenario, value_l) in uncertainties[ech][nodal_injection_name][ts]
                        Base.write(file_l, @sprintf("%25s%20s%20s%10s%16.8E\n",
                                        nodal_injection_name,
                                        ts,
                                        ech,
                                        scenario,
                                        value_l
                                        )
                            )
                    end
                end
            end
        end

    end
end

function write_commitment_schedule(dir_path::String, schedule::PSCOPF.Schedule, prefix="")
    ech = schedule.decision_time
    commitment_filename_l = joinpath(dir_path, OUTPUT_PREFIX*prefix*"commitment_schedule.txt")
    open(commitment_filename_l, "a") do commitment_file_l
        if filesize(commitment_file_l) == 0
            Base.write(commitment_file_l, @sprintf("#%19s%10s%25s%20s%10s%6s%10s\n", "ech", "decider", "name", "ts", "scenario", "value", "firmness"))
        end
        for (gen_id, gen_schedule) in schedule.generator_schedules
            for (ts, uncertain_value) in gen_schedule.commitment
                firmness = PSCOPF.is_definitive(uncertain_value) ? "FIRM" : "FREE"
                for (scenario, value_l) in uncertain_value.anticipated_value
                    value_l = ismissing(value_l) ? -1. : value_l
                    Base.write(commitment_file_l, @sprintf("%20s%10s%25s%20s%10s%6s%10s\n",
                                        ech,
                                        schedule.decider_type,
                                        gen_id,
                                        ts,
                                        scenario,
                                        value_l,
                                        firmness
                                        )
                            )
                end
            end
        end
    end
end

function write_production_schedule(dir_path::String, schedule::PSCOPF.Schedule, prefix="")
    ech = schedule.decision_time
    schedule_filename_l = joinpath(dir_path, OUTPUT_PREFIX*prefix*"schedule.txt")
    open(schedule_filename_l, "a") do schedule_file_l
        if filesize(schedule_file_l) == 0
            Base.write(schedule_file_l, @sprintf("#%19s%10s%25s%20s%10s%16s%10s\n", "ech", "decider", "name", "ts", "scenario", "value", "firmness"))
        end
        for (gen_id, gen_schedule) in schedule.generator_schedules
            for (ts, uncertain_value) in gen_schedule.production
                firmness = PSCOPF.is_definitive(uncertain_value) ? "FIRM" : "FREE"
                for (scenario, value_l) in uncertain_value.anticipated_value
                    value_l = ismissing(value_l) ? -1. : value_l
                    Base.write(schedule_file_l, @sprintf("%20s%10s%25s%20s%10s%16.8E%10s\n",
                                        ech,
                                        schedule.decider_type,
                                        gen_id,
                                        ts,
                                        scenario,
                                        value_l,
                                        firmness
                                        )
                            )
                end
            end
        end
    end
end

function write_flows(dir_path::String, context::PSCOPF.AbstractContext, schedule::PSCOPF.Schedule, prefix="")
    ech = schedule.decision_time
    flows_filename_l = joinpath(dir_path, OUTPUT_PREFIX*prefix*"flows.txt")

    flows = PSCOPF.compute_flows(context, schedule)
    open(flows_filename_l, "a") do flows_file_l
        if filesize(flows_file_l) == 0
            Base.write(flows_file_l, @sprintf("#%19s%10s%25s%20s%10s%16s\n", "ech", "decider", "branch_name", "ts", "scenario", "value"))
        end
        for ((branch_id, ts, scenario), flow_value) in flows
            Base.write(flows_file_l, @sprintf("%20s%10s%25s%20s%10s%16.8E\n",
                                ech,
                                schedule.decider_type,
                                branch_id,
                                ts,
                                scenario,
                                flow_value
                                )
                    )
        end
    end
end

function write(context::PSCOPF.AbstractContext, schedule::PSCOPF.Schedule, prefix="")
    dir_path = context.out_dir
    if !isnothing(dir_path)
        mkpath(dir_path)
        write_commitment_schedule(dir_path, schedule, prefix)
        write_production_schedule(dir_path, schedule, prefix)
        write_flows(dir_path, context, schedule, prefix)
    end
end

function write_full(context::PSCOPF.AbstractContext, schedule::PSCOPF.Schedule, prefix="")
    dir_path = context.out_dir
    if !isnothing(dir_path)
        mkpath(dir_path)
        _write(dir_path, schedule, context, prefix)
    end
end

function _write(dir_path::String, schedule::PSCOPF.Schedule, context::PSCOPF.AbstractContext, prefix="")
    ech = schedule.decision_time
    decider = schedule.decider_type
    schedule_filename_l = joinpath(dir_path, OUTPUT_PREFIX*prefix*"full_schedule.txt")
    open(schedule_filename_l, "a") do schedule_file_l
        if filesize(schedule_file_l) == 0
            Base.write(schedule_file_l, @sprintf("#%19s%10s%25s%10s%20s%10s%16s%16s%16s%16s%15s%8s%15s\n",
                                            "ech", "decider", "unit/bus", "PROD/LOAD", "ts", "scenario",
                                            "inject_value", "power_value", "load_shed", "power_capped", "DP_firmness",
                                            "ON/OFF", "DMO_firmness",
                                            ))
        end

        TS =  PSCOPF.get_target_timepoints(context)
        scenarios = PSCOPF.get_scenarios(context)
        uncertainties_at_ech = PSCOPF.get_uncertainties(context, ech)
        network = PSCOPF.get_network(context)

        for generator in Networks.get_generators(network)
            gen_id = Networks.get_id(generator)
            gen_schedule = PSCOPF.get_sub_schedule(schedule, gen_id)

            load_shed = 0. #this is for buses only
            for ts in TS
                power_uncertain_value = PSCOPF.get_prod_uncertain_value(gen_schedule, ts)
                power_firmness = PSCOPF.is_definitive(power_uncertain_value) ? "FIRM" : "FREE"
                if Networks.needs_commitment(generator)
                    commitment_uncertain_value = PSCOPF.get_commitment_uncertain_value(gen_schedule, ts)
                    commitment_firmness = PSCOPF.is_definitive(commitment_uncertain_value) ? "FIRM" : "FREE"
                else
                    commitment_firmness = "X"
                end
                for scenario in scenarios
                    prod_value = PSCOPF.get_prod_value(gen_schedule, ts, scenario)
                    if Networks.is_limitable(generator)
                        power_value = PSCOPF.get_uncertainties(uncertainties_at_ech, gen_id, ts, scenario)
                        power_capped = PSCOPF.get_capping(schedule, gen_id, ts, scenario)
                        inject_value = prod_value - power_capped #TODO : add this to schedule and compute it by each Model
                                                                 #FIXME : for TSOModel the prod_value take account of capping already
                    else
                        inject_value = power_value = prod_value
                        power_capped = 0.
                    end
                    if Networks.needs_commitment(generator)
                        commitment_value = PSCOPF.get_commitment_value(gen_schedule, ts, scenario)
                    else
                        commitment_value = "X"
                    end

                    Base.write(schedule_file_l, @sprintf("%20s%8s%25s%10s%20s%10s%16.8E%16.8E%16.8E%16.8E%15s%8s%15s\n",
                                                    ech, decider, gen_id, "PROD", ts, scenario,
                                                    inject_value, power_value, load_shed, power_capped, power_firmness,
                                                    commitment_value, commitment_firmness)
                                )
                end
            end
        end

        for bus_id in Networks.get_id.(Networks.get_buses(network))
            for ts in TS
                for scenario in scenarios
                    power_value = PSCOPF.get_uncertainties(uncertainties_at_ech, bus_id, ts, scenario)
                    load_shed = PSCOPF.get_loss_of_load(schedule, bus_id, ts, scenario)
                    inject_value = power_value - load_shed

                    Base.write(schedule_file_l, @sprintf("%20s%8s%25s%8s%20s%10s%16.8E%16.8E%16.8E%16.8E%8s%6s%8s\n",
                                                        ech, decider, bus_id, "LOAD", ts, scenario,
                                                        inject_value, power_value, load_shed, 0., "X",
                                                        "X", "X")
                        )
                end
            end
        end
    end
end

end #module PSCOPFio


# Base type from which all solvers inherit from
abstract type AbstractSolver{NDIMS} end

# "eval is evil"
# This is a temporary hack until we have switched to a library based approach
# with pure Julia code instead of parameter files.
# This hack enables users to define functions outside of Trixi.jl and pass
# them as keyword arguments to Trixi.run etc.
eval_if_not_function(func) = eval(Symbol(func))
eval_if_not_function(func::Function) = func

# Create an instance of a solver based on a given name
function make_solver(name::String, equations::AbstractEquation, mesh::TreeMesh;
                     surface_flux_function=nothing, volume_flux_function=nothing)
  if name == "dg"
    # "eval is evil"
    # This is a temporary hack until we have switched to a library based approach
    # with pure Julia code instead of parameter files.
    if isnothing(surface_flux_function)
      surface_flux_type = parameter("surface_flux", "flux_lax_friedrichs")
      surface_flux_function = eval_if_not_function(surface_flux_type)
    end
    if isnothing(volume_flux_function)
      volume_flux_type = parameter("volume_flux", "flux_central")
      volume_flux_function = eval_if_not_function(volume_flux_type)
    end

    initial_conditions_type = parameter("initial_conditions")
    initial_conditions = eval_if_not_function(initial_conditions_type)

    source_terms_type = parameter("source_terms", "nothing")
    source_terms = eval_if_not_function(source_terms_type)

    if ndims(equations) == 2
      return Dg2D(equations, surface_flux_function, volume_flux_function, initial_conditions, source_terms, mesh, parameter("polydeg"))
    elseif ndims(equations) == 3
      return Dg3D(equations, surface_flux_function, volume_flux_function, initial_conditions, source_terms, mesh, parameter("polydeg"))
    else
      error("Unsupported number of spatial dimensions: ", ndims(equations))
    end
  else
    error("'$name' does not name a valid solver")
  end
end

"""
    calc_error_norms([func=(u,equation)->u,] solver, t)

Calculate the discrete L2 and L∞ errors of `func` applied to the conservative variables of
the problem encapsulated by `solver` at time `t`, where `func` is called as `func(u, equation)`.
"""
function calc_error_norms end

@inline calc_error_norms(solver::AbstractSolver, t) = calc_error_norms(cons2cons, solver, t)

####################################################################################################
# Include files with actual implementations for different systems of equations.

# DG
include("dg/dg.jl")


# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.

using UnitCommitment, Test, LinearAlgebra
import UnitCommitment: _Violation, _offer, _query

@testset "_ViolationFilter" begin
    instance = UnitCommitment.read_benchmark("test/case14")
    filter = UnitCommitment._ViolationFilter(max_per_line = 1, max_total = 2)

    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[1],
            outage_line = nothing,
            amount = 100.0,
        ),
    )
    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[1],
            outage_line = instance.lines[1],
            amount = 300.0,
        ),
    )
    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[1],
            outage_line = instance.lines[5],
            amount = 500.0,
        ),
    )
    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[1],
            outage_line = instance.lines[4],
            amount = 400.0,
        ),
    )
    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[2],
            outage_line = instance.lines[1],
            amount = 200.0,
        ),
    )
    _offer(
        filter,
        _Violation(
            time = 1,
            monitored_line = instance.lines[2],
            outage_line = instance.lines[8],
            amount = 100.0,
        ),
    )

    actual = _query(filter)
    expected = [
        _Violation(
            time = 1,
            monitored_line = instance.lines[2],
            outage_line = instance.lines[1],
            amount = 200.0,
        ),
        _Violation(
            time = 1,
            monitored_line = instance.lines[1],
            outage_line = instance.lines[5],
            amount = 500.0,
        ),
    ]
    @test actual == expected
end


using Dates
using Base.Threads

"""
    struct KeyHeader

Helper struct to store header information of .key files.

# Fields

| Name                   | Meaning                                        |
| :---                   | :------                                        |
| `nkeys_file`           | Number of keys in this .key file               |
| `domain_corners`       | Corners of the domains defined by these keys   |
| `domain_fac`           | Factor needed for reconstructung int positions |
| `bits`                 | Size of PH keys it bits                        |
| `nkeys_total`          | Total number of keys in all files              |
| `nkeys_total_highword` | Total number of keys in all files              |
"""
struct KeyHeader
    nkeys_file::Vector{Int32}
    domain_corners::Vector{Float64}
    domain_fac::Float64
    bits::Int32
    nkeys_total::Vector{UInt32}
    nkeys_total_highword::Vector{UInt32}
end

"""
    read_keyheader(filename::String)

Reads the header of a .key file.
"""
function read_keyheader(filename::String)

    f = open(filename)
    blocksize = read(f, Int32)

    if blocksize == 8
        swap = 0
        snap_format = 2
    elseif blocksize == 256
        swap = 0
        snap_format = 1
    else
        blocksize = bswap(blocksize)
        if blocksize == 8
            swap = 1
            snap_format = 2
        elseif blocksize == 256
            swap = 1
            snap_format = 1
        else
            error("incorrect file format encountered when reading header of $filename")
        end
    end

    if snap_format == 2
        seek(f, 16)
        skip_line = read(f, Int32)
    end

    nkeys = read!(f, Array{Int32,1}(undef,6))
    domain_corners = read!(f, Array{Float64,1}(undef,3))
    domain_fac = read(f, Float64)

    read!(f, Array{Float64,1}(undef,2))
    read(f, Float64)
    read(f, Float64)
    read(f, Int32)

    bits = read(f, Int32)
    nkeys_total = read!(f, Array{UInt32,1}(undef,6))

    read(f, Int32)
    read(f, Int32)
    read(f, Float64)
    read(f, Float64)
    read(f, Float64)
    read(f, Float64)
    read(f, Int32)
    read(f, Int32)

    nkeys_total_highword = read!(f, Array{UInt32,1}(undef,6))

    close(f)

    return KeyHeader( nkeys, domain_corners, domain_fac, bits, nkeys_total, nkeys_total_highword )
end

"""
    read_key_index(file_key_index::String)

Reads the .key.index file.
"""
function read_key_index(file_key_index::String)

    finfo = stat(file_key_index)

    f = open(file_key_index)
    n = read(f, Int32)

    if finfo.size == 4+n*8*2+n*4
        int_type = UInt64
    else
        int_type = UInt32
    end

    low_list = read!(f, Array{int_type,1}(undef,n))
    high_list = read!(f, Array{int_type,1}(undef,n))
    file_list = read!(f, Array{UInt32,1}(undef,n))

    close(f)

    return low_list, high_list, file_list
end


"""
A generally useful event scheduler class.
Each instance of this class manages its own queue.
No multi-threading is implied; you are supposed to hack that
yourself, or use a single instance per application.
Each instance is parametrized with two functions, one that is
supposed to return the current time, one that is supposed to
implement a delay.    You can implement real-time scheduling by
substituting time and sleep from built-in module time, or you can
implement simulated time by writing your own functions.    This can
also be used to integrate scheduling with STDWIN events; the delay
function is allowed to modify the queue.    Time can be expressed as
integers or floating point numbers, as long as it is consistent.
Events are specified by tuples (time, priority, action, argument, kwargs).
As in UNIX, lower priority numbers mean higher priority; in this
way the queue can be maintained as a priority queue.    Execution of the
event means calling the action function, passing it the argument
sequence in "argument" (remember that in Python, multiple function
arguments are be packed in a sequence) and keyword parameters in "kwargs".
The action function may be an instance method so it
has another way to reference private data (besides global variables).
"""
module Sched

    export Scheduler, enter, enterabs, cancel, queue
    export FloatTimeFunc, UTCDateTimeFunc
    # Base exports: run, isempty
    import Base: run, isempty

    using DataStructures: PriorityQueue, peek, dequeue!, dequeue_pair!
    using Dates

    """
    Abstract type for struct that returns real-time or simulated time
    when called (functor)
    """
    abstract type TimeFunc end

    """
        UTCDateTimeFuncStruct()

    Functor that return real-time as DateTime (UTC) when called
    """
    struct UTCDateTimeFuncStruct <: TimeFunc
        func::Function
        args

        UTCDateTimeFuncStruct() = new(now, [Dates.UTC])
    end
    function (timefunc::UTCDateTimeFuncStruct)()
        timefunc.func(timefunc.args...)
    end
    UTCDateTimeFunc = UTCDateTimeFuncStruct()

    """
        FloatTimeFuncStruct()

    Functor that return real-time as Float when called
    """
    struct FloatTimeFuncStruct <: TimeFunc
        func::Function

        FloatTimeFuncStruct() = new(time)
    end
    function (timefunc::FloatTimeFuncStruct)()
        timefunc.func()
    end
    FloatTimeFunc = FloatTimeFuncStruct()

    """
    Default time function
    """
    global _time = UTCDateTimeFunc

    # Time as Float64
    #_time = FloatTimeFunc

    # Time as DateTime (UTC)
    #_time = UTCDateTimeFunc

    """
        Event(time_, priority, action, args...; kwargs...)

    Event structure

     - `time_`: Numeric type compatible with the return value of the timefunc function passed to the constructor.'
     - `priority`: Events scheduled for the same time will be executed in the order of their priority.
     - `action`: Executing the event means executing action(args...; kwargs...)
     - `args`: args is a sequence holding the positional arguments for the action.
     - `kwargs`: kwargs is a dictionary holding the keyword arguments for the action.

    """
    struct Event
        time_
        priority
        action
        args
        kwargs

        Event(time_, priority, action, args...; kwargs...) = new(time_, priority, action, args, kwargs)
    end
    run(event::Event) = event.action(event.args...; event.kwargs...)

    """
        Scheduler(; timefunc=_time, delayfunc=sleep)

    Initialize a new Scheduler instance, passing optionaly
    the time and delay functions

    The scheduler struct defines a generic interface to scheduling events.
    It needs two functions to actually deal with the “outside world”

    - The `timefunc` should be callable without arguments, and return a number (the “time”, in any units whatsoever). `timefunc` default is `UTCDateTimeFunc`.

    - The `delayfunc` function should be callable with one argument, compatible  with the output of `timefunc`, and should delay that many time units. delayfunc  will also be called with the argument 0 after each event is run to allow other threads an opportunity to run in multi-threaded applications.
    """
    struct Scheduler
        timefunc::TimeFunc
        delayfunc::Function

            _queue::PriorityQueue
            _lock::ReentrantLock

            function Scheduler(; timefunc=_time, delayfunc=sleep)
                q = PriorityQueue{Event, Priority}(Base.Order.Reverse)
                l = ReentrantLock()
                new(timefunc, delayfunc, q, l)
            end
    end

    """
        Priority(time_, priority)

    Priority of events
    """
    struct Priority
        time_
        priority
    end
    function Base.isless(p1::Sched.Priority, p2::Sched.Priority)
        if p1.time_ > p2.time_
            true
        elseif p1.time_ < p2.time_
            false
        else
            p1.priority > p2.priority
        end
    end

    """
        enterabs(sched, time_, priority, action, args...; kwargs...)

    Enter a new event in the queue at an absolute time.
    Returns an ID for the event which can be used to remove it,
    if necessary.
    """
    function enterabs(sched::Scheduler, time_, priority, action, args...; kwargs...)
        #println("enterabs $sched $time_ $priority $action with args=$args kwargs=$kwargs")
        event = Event(time_, priority, action, args...; kwargs...)
        l = sched._lock
        lock(l)
        try
            sched._queue[event] = Priority(time_, priority)
        finally
            unlock(sched._lock)
        end
        event # The ID
    end

    """
        enter(sched, delay, priority, action, args...; kwargs...)

    Enter a new event in the queue at a relative time.
    A variant of enterabs that specifies the time as a relative time.
    This is actually the more commonly used interface.
    """
    function enter(sched::Scheduler, delay, priority, action, args...; kwargs...)
        next_time = sched.timefunc() + delay
        event = enterabs(sched, next_time, priority, action, args...; kwargs...)
        event
    end

    """
        run(sched; blocking=true)

    Execute events until the queue is empty.
    If blocking is False executes the scheduled events due to
    expire soonest (if any) and then return the deadline of the
    next scheduled call in the scheduler.
    When there is a positive delay until the first event, the
    delay function is called and the event is left in the queue;
    otherwise, the event is removed from the queue and executed
    (its action function is called, passing it the argument).  If
    the delay function returns prematurely, it is simply
    restarted.
    It is legal for both the delay function and the action
    function to modify the queue or to raise an exception;
    exceptions are not caught but the scheduler's state remains
    well-defined so run() may be called again.
    A questionable hack is added to allow other threads to run:
    just after an event is executed, a delay of 0 is executed, to
    avoid monopolizing the CPU when other threads are also
    runnable.
    """
    function run(sched::Scheduler; blocking=true)
        l = sched._lock
        q = sched._queue
        delayfunc = sched.delayfunc
        timefunc = sched.timefunc
        while(true)
            lock(l)
            if length(q) == 0
                break
            end
            next_event, priority = peek(q)
            now_ = timefunc()
            if next_event.time_ > now_
                delay = true
            else
                delay = false
                event = dequeue!(q)
            end
            unlock(l)
            if delay
                if !blocking
                    return next_event.time_ - now_
                end
                delayfunc(next_event.time_ - now_)
            else
                run(event)
                delayfunc(0)     # Let other threads run
            end
        end
    end

    """
        cancel(sched, event)

    Remove an event from the queue.
    This must be presented the ID as returned by enter().
    If the event is not in the queue, this raises ValueError.
    """
    function cancel(sched::Scheduler, event::Event)
        lock(sched._lock)
        dequeue_pair!(sched._queue, event)
        unlock(sched._lock)
    end

    """
        isempty(sched) -> Bool

    Check whether the queue is empty.
    """
    function isempty(sched::Scheduler)
        lock(sched._lock)
        _isempty = isempty(sched._queue)
        unlock(sched._lock)
        _isempty
    end

    """
        queue(sched)

    Return an ordered list of upcoming events.
    """
    function queue(sched::Scheduler)
        q = sched._queue
        q2 = deepcopy(q)
        [dequeue!(q) for i in 1:length(q)]
    end

end # module


using Adapt

struct Field{X, Y, Z, A, D, G, T, B} <: AbstractDataField{X, Y, Z, A, G, T}
                   data :: D
           architecture :: A
                   grid :: G
    boundary_conditions :: B

    function Field{X, Y, Z}(data::D, arch::A, grid::G, bcs::B) where {X, Y, Z, D, A, G, B}
        T = eltype(grid)
        return new{X, Y, Z, A, D, G, T, B}(data, arch, grid, bcs)
    end
end

"""
    Field(X, Y, Z, [arch = CPU()], grid,
          [ bcs = FieldBoundaryConditions(grid, (X, Y, Z)),
           data = new_data(eltype(grid), arch, grid, (X, Y, Z))])

Construct a `Field` on `grid` with `data` on architecture `arch` with
boundary conditions `bcs`. Each of `(X, Y, Z)` is either `Center` or `Face` and determines
the field's location in `(x, y, z)`.

Example
=======

```jldoctest
julia> using Oceananigans, Oceananigans.Grids

julia> ω = Field(Face, Face, Center, CPU(), RegularRectilinearGrid(size=(1, 1, 1), extent=(1, 1, 1)))
Field located at (Face, Face, Center)
├── data: OffsetArrays.OffsetArray{Float64,3,Array{Float64,3}}, size: (3, 3, 3)
├── grid: RegularRectilinearGrid{Float64, Periodic, Periodic, Bounded}(Nx=1, Ny=1, Nz=1)
└── boundary conditions: x=(west=Periodic, east=Periodic), y=(south=Periodic, north=Periodic), z=(bottom=ZeroFlux, top=ZeroFlux)
```
"""
function Field(X, Y, Z,
               arch::AbstractArchitecture,
               grid::AbstractGrid,
               bcs = FieldBoundaryConditions(grid, (X, Y, Z)),
               data = new_data(eltype(grid), arch, grid, (X, Y, Z)))

    validate_field_data(X, Y, Z, data, grid)

    return Field{X, Y, Z}(data, arch, grid, bcs)
end

# Default CPU architecture
Field(X, Y, Z, grid::AbstractGrid, args...) = Field(X, Y, Z, CPU(), grid, args...)

# Canonical `similar` for AbstractField (doesn't transfer boundary conditions)
Base.similar(f::AbstractField{X, Y, Z, Arch}) where {X, Y, Z, Arch} = Field(X, Y, Z, Arch(), f.grid)

# Type "destantiation": convert Face() to Face and Center() to Center if needed.
destantiate(X) = typeof(X)
destantiate(X::DataType) = X

"""
    Field(L::Tuple, arch, grid, data, bcs)

Construct a `Field` at the location defined by the 3-tuple `L`,
whose elements are `Center` or `Face`.
"""
Field(L::Tuple, args...) = Field(destantiate.(L)..., args...)

#####
##### Special constructors for tracers and velocity fields
#####

"""
    CenterField([arch=CPU()], grid
                bcs = TracerBoundaryConditions(grid),
                data = new_data(eltype(grid), arch, grid, (Center, Center, Center)))

Return a `Field{Center, Center, Center}` on architecture `arch` and `grid` containing `data`
with field boundary conditions `bcs`.
"""
function CenterField(arch::AbstractArchitecture,
                     grid,
                     bcs = TracerBoundaryConditions(grid),
                     data = new_data(eltype(grid), arch, grid, (Center, Center, Center)))

    return Field(Center, Center, Center, arch, grid, bcs, data)
end

"""
    XFaceField([arch=CPU()], grid
               bcs = UVelocityBoundaryConditions(grid),
               data = new_data(eltype(grid), arch, grid, (Face, Center, Center)))

Return a `Field{Face, Center, Center}` on architecture `arch` and `grid` containing `data`
with field boundary conditions `bcs`.
"""
function XFaceField(arch::AbstractArchitecture,
                    grid,
                    bcs = UVelocityBoundaryConditions(grid),
                    data = new_data(eltype(grid), arch, grid, (Face, Center, Center)))

    return Field(Face, Center, Center, arch, grid, bcs, data)
end

"""
    YFaceField([arch=CPU()], grid,
               bcs = VVelocityBoundaryConditions(grid),
               data = new_data(eltype(grid), arch, grid, (Center, Face, Center)))

Return a `Field{Center, Face, Center}` on architecture `arch` and `grid` containing `data`
with field boundary conditions `bcs`.
"""
function YFaceField(arch::AbstractArchitecture,
                    grid,
                    bcs = VVelocityBoundaryConditions(grid),
                    data = new_data(eltype(grid), arch, grid, (Center, Face, Center)))

    return Field(Center, Face, Center, arch, grid, bcs, data)
end

"""
    ZFaceField([arch=CPU()], grid
               bcs = WVelocityBoundaryConditions(grid),
               data = new_data(eltype(grid), arch, grid, (Center, Center, Face)))

Return a `Field{Center, Center, Face}` on architecture `arch` and `grid` containing `data`
with field boundary conditions `bcs`.
"""
function ZFaceField(arch::AbstractArchitecture,
                    grid,
                    bcs = WVelocityBoundaryConditions(grid),
                    data = new_data(eltype(grid), arch, grid, (Center, Center, Face)))

    return Field(Center, Center, Face, arch, grid, bcs, data)
end

# Default CPU architectures...
CenterField(grid::AbstractGrid, args...) = CenterField(CPU(), grid, args...)
 XFaceField(grid::AbstractGrid, args...) =  XFaceField(CPU(), grid, args...)
 YFaceField(grid::AbstractGrid, args...) =  YFaceField(CPU(), grid, args...)
 ZFaceField(grid::AbstractGrid, args...) =  ZFaceField(CPU(), grid, args...)

Adapt.adapt_structure(to, field::Field) = Adapt.adapt(to, field.data)


#  MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
#  Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
#  Released under the modified BSD license. See COPYING.md for more details.

using Statistics
using DataStructures
import Base.Threads: threadid

function take(
    pool::NodePool;
    suggestions::Array{Node} = [],
    time_remaining::Float64,
    gap_limit::Float64,
    node_limit::Int,
)::Union{Symbol,Node}
    t = threadid()
    lock(pool.lock) do
        n_processing = length(pool.processing)
        if (
            (pool.gap < gap_limit) ||
            (n_processing + pool.processed >= node_limit) ||
            (time_remaining < 0)
        )
            return :END
        end
        if isempty(pool.pending)
            if isempty(pool.processing)
                return :END
            else
                return :WAIT
            end
        else
            # If one of the suggested nodes is still pending, return it.
            # This is known in the literature as plunging.
            for s in suggestions
                if s in keys(pool.pending)
                    delete!(pool.pending, s)
                    pool.processing[s] = s.obj
                    return s
                end
            end

            # If all suggestions have already been processed
            # or pruned, find another node based on best bound.
            node = dequeue!(pool.pending)
            pool.processing[node] = node.obj
            return node
        end
    end
end

function offer(
    pool::NodePool;
    parent_node::Union{Nothing,Node},
    child_nodes::Vector{Node},
    time_elapsed::Float64 = 0.0,
    print_interval::Int = 100,
    detailed_output::Bool = false,
)::Nothing
    lock(pool.lock) do
        primal_update = false

        # Update node.processing and node.processed
        pool.processed += 1
        if parent_node !== nothing
            delete!(pool.processing, parent_node)
        end

        # Queue child nodes
        for node in child_nodes
            if node.status == :Infeasible
                continue
            end
            if node.obj >= pool.primal_bound
                continue
            end
            if isempty(node.fractional_variables)
                pool.primal_bound = min(pool.primal_bound, node.obj)
                primal_update = true
                continue
            end
            pool.pending[node] = node.obj
        end

        # Update dual bound
        pool.dual_bound = pool.primal_bound
        if !isempty(pool.pending)
            pool.dual_bound = min(pool.dual_bound, peek(pool.pending)[2])
        end
        if !isempty(pool.processing)
            pool.dual_bound = min(pool.dual_bound, peek(pool.processing)[2])
        end

        # Update gap
        if pool.primal_bound == pool.dual_bound
            pool.gap = 0
        else
            pool.gap = abs((pool.primal_bound - pool.dual_bound) / pool.primal_bound)
        end

        if parent_node !== nothing
            # Update branching variable history
            branch_var = child_nodes[1].branch_variables[end]
            offset = findfirst(isequal(branch_var), parent_node.fractional_variables)
            x = parent_node.fractional_values[offset]
            obj_change_up = child_nodes[1].obj - parent_node.obj
            obj_change_down = child_nodes[2].obj - parent_node.obj
            _update_var_history(
                pool = pool,
                var = branch_var,
                x = x,
                obj_change_down = obj_change_down,
                obj_change_up = obj_change_up,
            )

            # Update global history
            pool.history.avg_pseudocost_up =
                mean(vh.pseudocost_up for vh in values(pool.var_history))
            pool.history.avg_pseudocost_down =
                mean(vh.pseudocost_down for vh in values(pool.var_history))

            # Print progress
            print_progress(
                pool,
                parent_node,
                time_elapsed = time_elapsed,
                print_interval = print_interval,
                detailed_output = detailed_output,
                primal_update = primal_update,
            )
        end
    end
    return
end

function _update_var_history(;
    pool::NodePool,
    var::Variable,
    x::Float64,
    obj_change_down::Float64,
    obj_change_up::Float64,
)::Nothing
    # Create new history entry
    if var ∉ keys(pool.var_history)
        pool.var_history[var] = VariableHistory()
    end
    varhist = pool.var_history[var]

    # Push fractional value
    push!(varhist.fractional_values, x)

    # Push objective value changes
    push!(varhist.obj_change_up, obj_change_up)
    push!(varhist.obj_change_down, obj_change_down)

    # Push objective change ratios
    f_up = x - floor(x)
    f_down = ceil(x) - x
    if isfinite(obj_change_up)
        push!(varhist.obj_ratio_up, obj_change_up / f_up)
    end
    if isfinite(obj_change_down)
        push!(varhist.obj_ratio_down, obj_change_down / f_down)
    end

    # Update variable pseudocosts
    varhist.pseudocost_up = 0.0
    varhist.pseudocost_down = 0.0
    if !isempty(varhist.obj_ratio_up)
        varhist.pseudocost_up = sum(varhist.obj_ratio_up) / length(varhist.obj_ratio_up)
    end
    if !isempty(varhist.obj_ratio_down)
        varhist.pseudocost_down =
            sum(varhist.obj_ratio_down) / length(varhist.obj_ratio_down)
    end
    return
end

function generate_indices(pool::NodePool, n::Int)::Vector{Int}
    lock(pool.lock) do
        result = Int[]
        for i = 1:n
            push!(result, pool.next_index)
            pool.next_index += 1
        end
        return result
    end
end


@testset "Krylov K1" begin
  for kmethod in [:cg, :cg_lanczos, :cr]
    stats4 = ripqp(
      QuadraticModel(qps4),
      display = false,
      iconf = InputConfig(
        sp = K1KrylovParams(kmethod = kmethod),
        solve_method = :IPF,
        history = true,
      ),
      itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
    )
    @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
    @test stats4.status == :acceptable
  end

  stats4 = ripqp(
    QuadraticModel(qps4),
    display = false,
    iconf = InputConfig(sp = K1KrylovParams(uplo = :U), solve_method = :PC, history = true),
    itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
  )
  @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
  @test stats4.status == :acceptable
end

@testset "Krylov K1.1 Structured" begin
  for kmethod in [:lsqr, :lsmr]
    stats4 = ripqp(
      QuadraticModel(qps4),
      display = false,
      iconf = InputConfig(
        sp = K1_1StructuredParams(kmethod = kmethod),
        solve_method = :IPF,
        history = true,
      ),
      itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
    )
    @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-1)
    @test stats4.status == :acceptable
  end

  stats4 = ripqp(
    QuadraticModel(qps4),
    display = false,
    iconf = InputConfig(sp = K1_1StructuredParams(uplo = :U), solve_method = :IPF, history = true),
    itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
  )
  @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
  @test stats4.status == :acceptable
end

@testset "Krylov K1.2 Structured" begin
  for kmethod in [:lnlq, :craig, :craigmr]
    stats4 = ripqp(
      QuadraticModel(qps4),
      display = false,
      iconf = InputConfig(
        sp = K1_2StructuredParams(kmethod = kmethod),
        solve_method = :IPF,
        history = true,
      ),
      itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
    )
    @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-1)
    @test stats4.status == :acceptable
  end

  stats4 = ripqp(
    QuadraticModel(qps4),
    display = false,
    iconf = InputConfig(sp = K1_2StructuredParams(uplo = :U), solve_method = :IPF, history = true),
    itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
  )
  @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
  @test stats4.status == :acceptable
end

@testset "Krylov K1 Dense" begin
  sm_dense4 = SlackModel(QuadraticModel(qps4))
  qm_dense4 = QuadraticModel(
    sm_dense4.data.c,
    Matrix(sm_dense4.data.H),
    A = Matrix(sm_dense4.data.A),
    lcon = sm_dense4.meta.lcon,
    ucon = sm_dense4.meta.ucon,
    lvar = sm_dense4.meta.lvar,
    uvar = sm_dense4.meta.uvar,
  )
  stats4 = ripqp(
    qm_dense4,
    display = false,
    iconf = InputConfig(
      sp = K1CholDenseParams(),
      solve_method = :PC,
      presolve = false,
      scaling = false,
      history = false,
    ),
    itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
  )

  @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
  @test stats4.status == :acceptable

  stats4 = ripqp(
    qm_dense4,
    display = true,
    iconf = InputConfig(
      sp = K1CholDenseParams(),
      solve_method = :PC,
      mode = :multi,
      presolve = false,
      scaling = false,
      history = false,
    ),
    itol = InputTol(max_iter = 50, max_time = 20.0, ϵ_rc = 1.0e-4, ϵ_rb = 1.0e-4, ϵ_pdd = 1.0e-4),
  )

  @test isapprox(stats4.objective, -4.6475314286e02, atol = 1e-2)
  @test stats4.status == :acceptable
end


module Cases
using DataFrames, CSV

include("../src/DistSystem.jl")
import .DistSystem

export case3_dist, case33, case3_dist_no_dgs

function case3_dist()
    VN = 12500
    Sb = 1e6
    Yb = Sb / (VN^2)
    Vsub = 1.0
    P_limt = 10
    Q_limit = 10
    bus_sub = 1
    nbuses = 3
    VL = 0.90
    VH = 1.05
    sub = DistSystem.Substation(VN, bus_sub, Vsub, P_limt, Q_limit, [0.003, 12, 240])
    data = DataFrame(CSV.File("case3_dist.csv"))

    sys = DistSystem.factory_system(data, VL, VH, sub)

    sys.dgs = [
        DistSystem.DER(3, 0.001, 0.001, 0.05, 1.0, [0.0, 0.03], [-0.03, 0.03], [0.0, 1.0], [0.0026, 10.26, 210]), #DG dispatchable
        DistSystem.DER(2, 0.002, 0.002, 0.05, 1.0, [0.0, 0.03], [-0.03, 0.03], [-1.0, 0.0, 1.0], [0.0026, 10.26, 210]), #DG dispatchable
    ]
    sys.m_load = [0.5, 0.8, 1.0]
    sys.m_new_dg = [-1.0, 0.0, 1]

    return sys, "case3_dist"
end

function case3_dist_no_dgs()
    VN = 12500
    Sb = 1e6
    Yb = Sb / (VN^2)
    Vsub = 1.0
    P_limt = 10
    Q_limit = 10
    bus_sub = 1
    nbuses = 3
    VL = 0.90
    VH = 1.05
    sub = DistSystem.Substation(VN, bus_sub, Vsub, P_limt, Q_limit, [0.003, 12, 240])
    data = DataFrame(CSV.File("case3_dist.csv"))

    sys = DistSystem.factory_system(data, VL, VH, sub)
    sys.m_load = [0.5, 0.8, 1.0]
    sys.m_new_dg = [-1.0, 0.0, 1]

    return sys, "case3_dist_no_dgs"
end

function case33()
    VN = 12660
    Sb = 1e6
    Yb = Sb / (VN^2)
    Vsub = 1.0
    P_limt = 4
    Q_limit = 2.5
    bus_sub = 1
    nbuses = 33
    VL = 0.90
    VH = 1.05
    sub = DistSystem.Substation(VN, bus_sub, Vsub, P_limt, Q_limit, [0.003, 12, 240])
    data = DataFrame(CSV.File("case33.csv"))
    sys = DistSystem.factory_system(data, VL, VH, sub)
    sys.dgs = [
        DistSystem.DER(18, 0.02, 0.02, 0.05, 0.05, [0.0, 0.03], [-0.03, 0.03], [0.0, 1.0], [0.0026, 10.26, 210]), #DG dispatchable
        DistSystem.DER(22, 0.02, 0.02, 0.05, 0.05, [0.0, 0.0], [-0.03, 0.03], [0.0, 1.0], [0.0026, 10.26, 210]), #DG non-active dispatchable
        DistSystem.DER(33, 0.02, 0.02, 0.15, 0.05, [-0.01, 0.01], [-0.01, 0.01], [-1.0, 0.0, 1.0], [0.0026, 10.26, 210]), #ESS
        DistSystem.DER(25, 0.02, 0.02, 0.0, 0.05, [0.0, 0.0], [0.0, 0.0], [0.0, 1.0], [0.0026, 10.26, 210]), #DG non-dispatchable
    ]
    sys.m_load = [0.5, 0.8, 1.0]
    sys.m_new_dg = [-1.0, 0.0, 1]
    return sys, "case33"
end

end


#Add structure/type that defined actuators and their effects

#Define AbstractActuator type
abstract type AbstractActuator end

#= Define a Realstic Thruster type which includes:
    - Specific impulse (only used when infinitely throttable, to determine fuel used)
                        (set to inf if no fuel is to be used)
    - [x,y,z] position w.r.t. centre of mass of vehicle
    - Transformation matrix between body-axis of vehicle and thruster (thrust in x-direction)
    - NozzleType;
    - TypeFlag; indicates whether or not the thruster is throttable (0 = on/off, 1 = throttable)
                (if this flag is zero, a non-zero thrust setting is interpreted as on)
    - ThrottableFlag; indicates if actuator is finely throttable (0) or has limits (1)
                     if it has limits, the nozzle type will be used to determine thrust
                     as function of mass flow and pressure. If its infinitely throttable,
                     the force generated is the control variable, else its the mass flow.
    - ConstantFlag; indicates whether or not the thruster generates a constant thrust
                    (0 = constant, 1 = variable)
                    (if the flag is zero, the specific impulse is constant
                    (if the flag is one, the altitude function will be used to compute the
                    atmospheric pressure that determines the variation in generated thrust)
    -ControlGroupFlag; indicates if used for translational (0) or rotational control (1)
    - Force generated; Computed
    - Moment generated; Computed

=#
struct RealisticThruster <: AbstractActuator

end #RealisticThruster

#allow for simplistic thruster models
struct SimpleThruster <: AbstractActuator
    ForceMoment::Vector{Float64} #Vector in body-axis frame
    TypeFlag::Bool #0 means that it generates a force, 1 means it generates a moment
end #SimpleThruster

#Define an AerodynamicEffector type
struct AerodynamicEffector <: AbstractActuator

end #AerodynamicEffector


export PoolDims

"""
    PoolDims(x_size::NTuple{M}, k::Union{NTuple{L, Int}, Int};
            stride=k, padding=0, dilation=1)  where {M, L}

Dimensions for a "pooling" operation that can have an arbitrary input size, kernel size,
stride, dilation, and channel count.  Used to dispatch onto efficient implementations at
compile-time.
"""
struct PoolDims{N, K, S, P, D} <: ConvDims{N}
    input_size::NTuple{N, Int}

    kernel_size::NTuple{K, Int}
    channels_in::Int

    stride::NTuple{S, Int}
    padding::NTuple{P, Int}
    dilation::NTuple{D, Int}
end

function PoolDims(
    x_size::NTuple{M}, k::Union{NTuple{L, Int}, Int};
    stride = k, padding = 0, dilation = 1,
) where {M, L}
    _check_kernel(k::Number, N::Int) = ntuple(_ -> Int(k), N)
    _check_kernel(k::NTuple, ::Int) = k

    kernel = _check_kernel(k, M - 2)
    spdf_kernel = NTuple{M, Int}([kernel..., 1, 1])

    sstride, ppadding, ddilation = check_spdf(
        x_size, spdf_kernel, stride, padding, dilation)
    PoolDims(
        x_size[1:(end - 2)], kernel, x_size[end - 1],
        sstride, ppadding, ddilation)
end

PoolDims(x::AbstractArray, k; kwargs...) = PoolDims(size(x), k; kwargs...)

# Useful for constructing a new PoolDims that has only a few elements different
# from the original progenitor object that it inherits shapes from.
PoolDims(
    c::C; I=input_size(c), K=kernel_size(c),
    C_in=channels_in(c), S=stride(c), P=padding(c), D=dilation(c),
) where C <: ConvDims = PoolDims(I, K, C_in, S, P, D)

@inline channels_in(c::PoolDims) = c.channels_in
@inline channels_out(c::PoolDims) = c.channels_in

@inline input_size(c::PoolDims) = c.input_size
@inline kernel_size(c::PoolDims) = c.kernel_size

@inline stride(c::PoolDims) = c.stride
@inline padding(c::PoolDims) = c.padding
@inline dilation(c::PoolDims) = c.dilation
@inline flipkernel(c::PoolDims) = false

function check_dims(x::NTuple{M}, y::NTuple{M}, pdims::PoolDims) where {M}
    # First, check that channel counts are all correct:
    @assert x[end-1] == channels_in(pdims) DimensionMismatch("Data input channel count ($(x[end-1]) vs. $(channels_in(pdims)))")
    @assert y[end-1] == channels_out(pdims) DimensionMismatch("Data output channel count ($(y[end-1]) vs. $(channels_out(pdims)))")

    # Next, check that the spatial dimensions match up
    @assert x[1:end-2] == input_size(pdims) DimensionMismatch("Data input spatial size ($(x[1:end-2]) vs. $(input_size(pdims)))")
    @assert y[1:end-2] == output_size(pdims) DimensionMismatch("Data output spatial size ($(y[1:end-2]) vs. $(output_size(pdims)))")

    # Finally, check that the batch size matches
    @assert x[end] == y[end] DimensionMismatch("Batch size ($(x[end]) vs. $(y[end]))")
end