module Base

using JuMP
using DataFrames
using Interpolations
using CSV
using Printf

# These functions are required for MPC_Module.jl and PrettyPlots.jl (resultsDir!)
export
  evalConstraints!,
  opt2dfs!,
  dvs2dfs,
  plant2dfs!,
  postProcess!,
  optimize!,
  scale_tau,
  try_import,
  intervals,
  interpolateLagrange!,
  interpolateLinear!,
  interpolate_lagrange,
  State,
  Control,
  Constraint,
  Results,
  Settings,
  _Ipopt_defaults,
  _Ipopt_MPC,
  # _KNITRO_defaults,
  # _KNITRO_MPC,
  simulationModes,
  resultsDir!

################################################################################
# Constants
################################################################################
#####################################
# settings for both KNITRO and IPOPT
####################################
#outlev # (c.m.solver==:Ipopt) ? :print_level : :outlev # print level
# (KNITRO) absolute stopping tolerance for the feasibility error
# (Ipopt) Absolute tolerance on the constraint violation.
#feastol_abs # (c.m.solver==:Ipopt) ? :constr_viol_tol : :feastol_abs
# (KNITRO) maximum number of iterations before termination
# (Ipopt) Maximum number of iterations.
#maxit # (c.m.solver==:Ipopt) ? :max_iter : :maxit
# (KNITRO) in seconds, the maximum allowable CPU time before termination
# (Ipopt) A limit on CPU seconds that Ipopt can use to solve one problem
#maxtime_cpu # (c.m.solver==:Ipopt) ? :max_cpu_time : :maxtime_cpu
const _Ipopt_defaults=Dict(
  :print_level                =>0,
  :warm_start_init_point      =>"yes",
  :tol                        =>1e-8,
  :max_iter                   =>3000,
  :max_cpu_time               =>1e6,
  :dual_inf_tol               =>1.,
  :constr_viol_tol            =>0.0001,
  :compl_inf_tol              =>0.0001,
  :acceptable_tol             =>1e-6,
  :acceptable_constr_viol_tol =>0.01,
  :acceptable_dual_inf_tol    =>1e-10,
  :acceptable_compl_inf_tol   =>0.01,
  :acceptable_obj_change_tol  =>1e20,
  :diverging_iterates_tol     =>1e20
)

const _Ipopt_MPC=Dict(
  :print_level                =>0,
  :warm_start_init_point      =>"yes",
  :tol                        =>5e-1,
  :max_iter                   =>500,
  :max_cpu_time               =>0.47,
  :dual_inf_tol               =>5.,
  :constr_viol_tol            =>1e-1,
  :compl_inf_tol              =>1e-1,
  :acceptable_tol             =>1e-2,
  :acceptable_constr_viol_tol =>0.01,
  :acceptable_dual_inf_tol    =>1e10,
  :acceptable_compl_inf_tol   =>0.01,
  :acceptable_obj_change_tol  =>1e20,
  :diverging_iterates_tol     =>1e20
)

# const _KNITRO_defaults=Dict(
#  :outlev                       =>1,
#  :feastol                      =>1.0e-6,
#  :feastol_abs                  =>1e-3,
#  :ftol                         =>1e-15,
#  :ftol_iters                   =>5,
#  :infeastol                    =>1.0e-8,
#  :maxfevals                    =>-1,
#  :maxit                        =>0,
#  :maxtime_cpu                  =>1e8,
#  :maxtime_real                 =>1e8,
#  :opttol                       =>1e-6,
#  :opttol_abs                   =>1e-3,
#  :xtol                         =>1e-12,
#  :xtol_iters                   =>0,
#  :algorithm                    =>0,
#  :bar_initpt                   =>0,
#  :bar_murule                   =>0,
#  :bar_penaltycons              =>0,
#  :bar_penaltyrule              =>0,
#  :bar_switchrule               =>0,
#  :linesearch                   =>0,
#  :linsolver                    =>0,
#  :cg_pmem                      =>10,
#  :bar_initpt                   =>0,
#  :bar_penaltycons              =>0,
#  :bar_penaltyrule              =>0,
#  :bar_switchrule               =>0,
#  :linesearch                   =>0,
#  :tuner                        =>0
# )
#
# const _KNITRO_MPC=Dict(
#  :outlev                       =>0,
#  :feastol                      =>1.0e20,
#  :feastol_abs                  =>7e-2,
#  :ftol                         =>1e-15,
#  :ftol_iters                   =>5,
#  :infeastol                    =>1e-2,
#  :maxfevals                    =>-1,
#  :maxit                        =>0,
#  :maxtime_cpu                  =>1e8,
#  :maxtime_real                 =>0.47,
#  :opttol                       =>1.0e20,
#  :opttol_abs                   =>5e-1,
#  :xtol                         =>1e-12,
#  :xtol_iters                   =>0,
#  :algorithm                    =>1,
#  :bar_initpt                   =>0,
#  :bar_murule                   =>1,
#  :bar_penaltycons              =>0,
#  :bar_penaltyrule              =>0,
#  :bar_switchrule               =>0,
#  :linesearch                   =>0,
#  :linsolver                    =>4,
#  :cg_pmem                      =>0,
#  :bar_initpt                   => 3,
#  :bar_penaltycons              => 1,
#  :bar_penaltyrule              => 2,
#  :bar_switchrule               => 2,
#  :linesearch                   => 1,
#  :tuner                        =>0
# )

const simulationModes = [:OCP,:IP,:IPEP,:EP]

################################################################################
# structs
################################################################################
############################### control ########################################
mutable struct Control
  name::Vector{Any}
  description::Vector{Any}
  num::Int64
  pts::Int64
end
function Control()
  Control([],
          [],
          0,
          0)
end
# ############################# state  ##########################################
mutable struct State
  name::Vector{Any}
  description::Vector{Any}
  num::Int64
  pts::Int64
  model
end
function State()
  State([],
        [],
        0,
        0,
        Any)
end

############################## constraint ######################################
mutable struct Constraint
  name::Vector{Any}
  handle::Vector{Any}
  value::Vector{Any}
  nums  # range of indecies in g(x)
end
function Constraint()
  Constraint([],
             [],
             [],
             []);
end

############################### solver  ########################################
mutable struct Solver
    name
    settings
end

function Solver()
       Solver(:Ipopt,
              _Ipopt_defaults);
end

mutable struct PlantResults
  plant
  X0p
  X0a  # closest one in time to X0e
  X0e
  e
  dfsplant
  dfsplantPts::DataFrames.DataFrame # plant data extracted into a single DataFrame
  dfsX0p
  dfsX0a
  dfsX0e
  dfse
end

function PlantResults()
 PlantResults(
    DataFrame(), # plantdata
  Vector{Any}[],
  Vector{Any}[],
  Vector{Any}[],
  Matrix{Any}[], # e
           [],   # vector of plantdata dfs
  DataFrame(),
  DataFrame(),
  DataFrame(),
  DataFrame(),
  DataFrame()
      )
end

mutable struct OCPResults
  tctr                       # time vector for control
  tst                        # time vector for state
  x                          # JuMP states
  u                          # JuMP controls
  X                          # states
  U                          # controls
  X0                         # initial states for OCP
  CS                         # costates
  tpolyPts                   # time sample points for polynomials  (NOTE these interpolated solutions were developed for calulaing error, between them and a known Optimal solution)
  XpolyPts                   # state evaluated using Lagrange/Linear polynomial
  CSpolyPts                  # costate evaluated using Lagrange/Linear polynomial
  UpolyPts                   # control evaluated using Lagrane/Linear polynomial
  AlltpolyPts                   # time sample points for polynomials
  AllXpolyPts                   # state evaluated using Lagrange/Linear polynomial
  AllCSpolyPts                  # costate evaluated using Lagrange/Linear polynomial
  AllUpolyPts                   # control evaluated using Lagrane/Linear polynomial
  tpts                       # vector time sample points
  Xpts                       # vector state sample points
  Upts                       # vector control sample points
  CSpts                      # vector costate sample points
  x0Con                      # handle for initial state constraints
  x0sCon
  xfCon                      # handle for final state constraints
  xfsCon
  dynCon                     # dynamics constraints
  constraint::Constraint     # constraint handles and data
  evalNum                    # number of times optimization has been run
  iterNum                    # mics. data, perhaps an iteration number for a higher level algorithm
  status                     # optimization status
  tSolve                     # solve time for optimization
  objVal                     # objective function value
  dfs                        # results in DataFrame for plotting
  dfsOpt                     # optimization information in DataFrame for plotting
  dfsCon                     # constraint data
end

# Default Constructor
function OCPResults()
OCPResults( Vector{Any}[],# time vector for control
        Vector{Any}[],    # time vector for state
        Matrix{Any}[],    # JuMP states
        Matrix{Any}[],    # JuMP controls
        Matrix{Any}[],    # states
        Matrix{Any}[],    # controls
        Vector{Any}[],    # initial states for OCP
        [],               # costates
        [],               # time sample points for polynomials
        [],               # state evaluated using Lagrange polynomial
        [],               # costate evaluated using Lagrange polynomial
        [],               # control evaluated using Lagrange polynomial
        [],
        [],
        [],
        [],
        Vector{Any}[],    # vector time sample points
        Vector{Any}[],    # vector state sample points
        Vector{Any}[],    # vector control sample points
        Vector{Any}[],    # vector costate sample points
        nothing,          # handle for initial state constraints
        nothing,
        nothing,          # handle for final state constraints
        nothing,
        nothing,          # dynamics constraint
        Constraint(),     # constraint data
        1,                # current evaluation number
        [],               # mics. data, perhaps an iteration number for a higher level algorithm
        Symbol,           # optimization status
        Float64,          # solve time for optimization
        Float64,          # objective function value
        [],               # results in DataFrame for plotting
        DataFrame(),      # optimization information in DataFrame for plotting
        []                # constraint data
      )
end

mutable struct Results
  ocp::OCPResults
  ip::PlantResults
  ep::PlantResults
  resultsDir                 # string that defines results folder
  mainDir                    # string that defines main folder
end

function Results()
  Results(
  OCPResults(),
  PlantResults(),
  PlantResults(),
  string(pwd(),"/results/"),  # string that defines results folder
  pwd()                       # string that defines main folder
  )
end

# MPC Settings Class
mutable struct MPCSettings
 on::Bool
 mode::Symbol
 predictX0::Bool
 fixedTp::Bool
 IPKnown::Bool
 saveMode::Symbol
 maxSim::Int64                # maximum number of total MPC updates
 shiftX0::Bool                # a bool to indicate that a check on the feasibility of the linear constraints for the initial conditions should be performed
 lastOptimal::Bool            # a bool to indicate that the previous solution should be used if the current solution in not optimal
 printLevel::Int64
 expandGoal::Bool             # bool to indicate if the goal needs to be expanded
 enlargeGoalTolFactor::Int64  # scaling factor to enlare the goal
 onlyOptimal::Bool
end

function MPCSettings()
      MPCSettings(
      false,
      :OCP,
      false,
      true,
      true,
      :all,
      100,
      true,
      true,
      2,
      true,
      2,
      false
      )
end

# OCP Settings Class
mutable struct OCPSettings
  solver::Solver                # solver information
  finalTimeDV::Bool
  integrationMethod::Symbol
  integrationScheme::Symbol
  save::Bool                    # bool for saving data
  reset::Bool                   # bool for reseting data
  evalConstraints::Bool         # bool for evaluating duals of the constraints
  evalCostates::Bool            # bool for evaluating costates
  tfMin::Any                    # minimum final time
  tfMax::Any                    # maximum final time
  tfOptimal::Any                # known optimal final time
  numInterpPts::Int64           # number of points to sample polynomial running through collocation points
  cacheOnly::Bool               # bool for only caching the results when using optimize!()
  linearInterpolation::Bool     # bool for using linear interpolation even if integrationMethod ==:ps
  interpolationOn::Bool         # bool to indicate if user wants solution interpolated for them
  x0slackVariables::Bool
  xFslackVariables::Bool
end

# Default Constructor NOTE currently not using these, they get overwritten
function OCPSettings()
      OCPSettings(
         Solver(),           # default solver
         false,              # finalTimeDV
         :ts,                # integrationMethod
         :bkwEuler,          # integrationScheme
         true,               # bool for saving data
         false,              # bool for reseting data
         false,              # bool for evaluating duals of the constraints
         false,              # bool for evaluating costates
         0.001,              # minimum final time
         400.0,              # maximum final time
         false,              # known optimal final time
         250,                # number of points to sample polynomial running through collocation points
         false,              # bool for only caching the results when using optimize!()
         false,              # bool for using linear interpolation even if integrationMethod ==:ps
         false,              # bool to indicate if user wants solution interpolated for them
         false,
         false
                )
end

mutable struct Settings
 ocp::OCPSettings
 mpc::MPCSettings
end

function Settings()
 Settings(
  OCPSettings(),
  MPCSettings()
       )
end

"""
L = lagrange_basis_poly(x,x_data,Nc,j)
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 12/26/2016, Last Modified: 1/25/2016
Citations: This function was influenced by the lagrange() function [located here](https://github.com/pjabardo/Jacobi.jl/blob/master/src/gauss_quad.jl)
--------------------------------------------------------------------------------------\n
# Input Arguments
* `x`: point to approximate function value at
* `x_data`: x data to used calculate basis polynomials
* `Nc`: order of Lagrange interpolating polynomial
* `j`: index of interest

# Output Arguments
* `L`: Lagrange basis polynomials

A basic description of Lagrange interpolating polynomials is provided [here](http://127.0.0.1:8000/lagrange_poly.html#lagrange-poly)

"""
function lagrange_basis_poly(x,x_data,Nc,j)
    L = 1;
    for idx in 1:Nc+1 # use all of the data
      if idx!=j
        L = L*(x - x_data[idx])/(x_data[j]-x_data[idx]);
      end
    end
  return L
end

"""
y=interpolate_lagrange(ts[int],ts[int],stateMatrix[int][:,st])
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Created: 1/2/2017, Last Modified: 9/19/2017 \n
--------------------------------------------------------------------------------------\n
"""
function interpolate_lagrange(x::AbstractArray{T},x_data,y_data) where {T<:Number}
  Nc = length(x_data) - 1

  if length(x_data)!=length(y_data)
      error(string("\n",
                    "-------------------------------------------------------", "\n",
                    "There is an error with the data vector lengths!!", "\n",
                    "-------------------------------------------------------", "\n",
                    "The following variables should be equal:", "\n",
                    "length(x_data) = ",length(x_data),"\n",
                    "length(y_data) = ",length(y_data),"\n"
                    )
            )
    end
    ns = length(x)
    L = zeros(Float64,Nc+1,ns)
    x = x[:]; x_data = x_data[:]; y_data = y_data[:]; # make sure data is in a column
    for idx in 1:Nc+1
      for j in 1:ns
          L[idx,j] = lagrange_basis_poly(x[j],x_data,Nc,idx);
      end
    end
    return y_data'*L
end

"""
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 6/29/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function intervals(n,int,x,u)

  if typeof(x[1,1]) == JuMP.Variable
    # states
    x_int = Array{JuMP.Variable}(length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]),n.ocp.state.num)
    for st in 1:n.ocp.state.num # +1 adds the DV in the next interval
      x_int[:,st] = x[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1,st];
    end

    # controls
    if int!=n.ocp.Ni
      u_int = Matrix{JuMP.Variable}(undef, length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]),n.ocp.control.num)
    else                    # -1 -> removing control in last mesh interval
      u_int = Matrix{JuMP.Variable}(undef, length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]-1),n.ocp.control.num)
    end
    for ctr in 1:n.ocp.control.num
      if int!=n.ocp.Ni          # +1 adds the DV in the next interval
        u_int[:,ctr] = u[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1,ctr];
      else
        u_int[:,ctr] = u[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1],ctr];
      end
    end
  else
    # states
    x_int=Matrix{Any}(undef, length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]),n.ocp.state.num);
    for st in 1:n.ocp.state.num # +1 adds the DV in the next interval
      x_int[:,st] = x[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1,st];
    end

    # controls
    if int!=n.ocp.Ni
      u_int = Matrix{Any}(undef, length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]),n.ocp.control.num)
    else                    # -1 -> removing control in last mesh interval
      u_int = Matrix{Any}(undef, length(n.ocp.Nck_full[int]+1:n.ocp.Nck_full[int+1]-1),n.ocp.control.num)
    end
    for ctr in 1:n.ocp.control.num
      if int!=n.ocp.Ni          # +1 adds the DV in the next interval
        u_int[:,ctr] = u[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1,ctr]
      else
        u_int[:,ctr] = u[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1],ctr]
      end
    end
  end

  return x_int,u_int
end

"""
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Created: 9/19/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function interpolateLagrange!(n; numPts::Int64=250, tfOptimal::Any=false)
  # TODO throw an error if tfOptimal does not make sense given current solution
  if isa(tfOptimal,Bool)
    if n.s.ocp.finalTimeDV
      tf = getvalue(n.ocp.tf)
    else n.s.ocp.finalTimeDV
      tf = n.ocp.tf
    end
  else  # if there is a known optimal final time, then it is useful to evaluate the Lagrange polynomial at particular points to determine the error in the solution
    tf = tfOptimal
  end

  if isnan(tf)
      @warn "tf is a NaN cannot use it in interpolateLagrange!().\n
            Exiting interpolateLagrange!() without an interpolated solution."
    return nothing
  end

  if tf < 0.01
    @warn "tf needs to be greater than 0.01 to interpolate over solution.\n
          Exiting interpolateLagrange!() without an interpolated solution."
     return nothing
  end

  # sample points
  n.r.ocp.tpolyPts = [range(tf/n.ocp.Ni*(int-1),tf/n.ocp.Ni*int;length=numPts) .+ n.r.ocp.tst[1] for int in 1:n.ocp.Ni]
  n.r.ocp.XpolyPts = [[zeros(numPts) for int in 1:n.ocp.Ni] for st in 1:n.ocp.state.num]
  n.r.ocp.UpolyPts = [[zeros(numPts) for int in 1:n.ocp.Ni] for ctr in 1:n.ocp.control.num]
  if n.s.ocp.evalCostates; n.r.ocp.CSpolyPts = [[zeros(numPts) for int in 1:n.ocp.Ni] for st in 1:n.ocp.state.num] end

  # time data points
  t_st_int = [n.r.ocp.tst[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1] for int in 1:n.ocp.Ni]
  t_ctr_int = [n.r.ocp.tctr[n.ocp.Nck_cum[int]+1:n.ocp.Nck_cum[int+1]+1] for int in 1:n.ocp.Ni-1]
  t_ctr_int = push!(t_ctr_int, n.r.ocp.tst[n.ocp.Nck_cum[n.ocp.Ni]+1:n.ocp.Nck_cum[n.ocp.Ni+1]]) # -1 -> removing control in last mesh interval

  for int in 1:n.ocp.Ni
    # controls and states for this interval
    x_int, u_int = intervals(n, int, copy(n.r.ocp.X), copy(n.r.ocp.U))

     # sample polynomial in interval at n.r.ocp.tpolyPts
     for st in 1:n.ocp.state.num
      n.r.ocp.XpolyPts[st][int] = interpolate_lagrange(n.r.ocp.tpolyPts[int], t_st_int[int], x_int[:,st])'
     end

     for ctr in 1:n.ocp.control.num
      n.r.ocp.UpolyPts[ctr][int] = interpolate_lagrange(n.r.ocp.tpolyPts[int], t_ctr_int[int], u_int[:,ctr])'
     end

      # sample polynomial in interval at n.r.ocp.tpolyPts NOTE costate is missing the last point, that is the t_st_int[int][1:end-1]
      if n.s.ocp.evalCostates && n.s.ocp.evalConstraints
         for st in 1:n.ocp.state.num
          n.r.ocp.CSpolyPts[st][int] = interpolate_lagrange(n.r.ocp.tpolyPts[int], t_st_int[int][1:end-1], n.r.ocp.CS[st][int])'
         end
      end
  end

  # extract result into vectors
  temp = [n.r.ocp.tpolyPts[int][1:end] for int in 1:n.ocp.Ni] # time
  n.r.ocp.tpts = [idx for tempM in temp for idx=tempM]
  totalPts = length(n.r.ocp.tpts)

  n.r.ocp.Xpts = Matrix{Float64}(undef, totalPts, n.ocp.state.num)
  for st in 1:n.ocp.state.num # states
    temp = [n.r.ocp.XpolyPts[st][int][1:end,:] for int in 1:n.ocp.Ni]
    n.r.ocp.Xpts[:,st] = [idx for tempM in temp for idx=tempM]
  end

  n.r.ocp.Upts = Matrix{Float64}(undef, totalPts, n.ocp.control.num)
  for ctr in 1:n.ocp.control.num # controls
    temp = [n.r.ocp.UpolyPts[ctr][int][1:end,:] for int in 1:n.ocp.Ni]
    n.r.ocp.Upts[:,ctr] = [idx for tempM in temp for idx=tempM]
  end

  if n.s.ocp.evalCostates && n.s.ocp.evalConstraints
    n.r.ocp.CSpts = Matrix{Float64}(undef, totalPts, n.ocp.state.num)
    for st in 1:n.ocp.state.num # states
      temp = [n.r.ocp.CSpolyPts[st][int][1:end,:] for int in 1:n.ocp.Ni]
      n.r.ocp.CSpts[:,st] = [idx for tempM in temp for idx=tempM]
    end
  end

  return nothing
end

"""
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Created: 10/4/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function interpolateLinear!(n; numPts::Int64=250, tfOptimal::Any=false)
  # TODO throw an error if tfOptimal does not make sense given current solution
  if isa(tfOptimal,Bool)
    if n.s.ocp.finalTimeDV
      tf = getvalue(n.ocp.tf)
    else n.s.ocp.finalTimeDV
      tf = n.ocp.tf
    end
  else  # if there is a known optimal final time, then it is useful to evaluate the Lagrange polynomial at particular points to determine the error in the solution
    tf = tfOptimal
  end

  if isnan(tf)
      @warn "tf is a NaN cannot use it in interpolateLinear!().\n
            Exiting interpolateLinear!() without an interpolated solution."
      return nothing
  end

  if tf < 0.01
    @warn "tf needs to be greater than 0.01 to interpolate over solution.\n
          Exiting interpolateLinear!() without an interpolated solution."
    return nothing
  end

  # sample points
  t = range(0,tf,length=numPts) .+ n.r.ocp.tst[1] # NOTE not tested
  n.r.ocp.tpts = convert(Array{Float64,1},t)
  n.r.ocp.Xpts = Matrix{Float64}(undef, numPts, n.ocp.state.num)
  n.r.ocp.Upts = Matrix{Float64}(undef, numPts, n.ocp.control.num)
  knots = (n.r.ocp.tst,)
  for st in 1:n.ocp.state.num
    sp_st = interpolate(knots,n.r.ocp.X[:,st],Gridded(Linear()))
    n.r.ocp.Xpts[:,st] = sp_st[n.r.ocp.tpts]
  end

  for ctr in 1:n.ocp.control.num
    if isequal(n.s.ocp.integrationMethod,:ps)
      sp_ctr = interpolate(knots,[n.r.ocp.U[:,ctr];0],Gridded(Linear()))
    else
      sp_ctr = interpolate(knots,n.r.ocp.U[:,ctr],Gridded(Linear()))
    end
    n.r.ocp.Upts[:,ctr] = sp_ctr[n.r.ocp.tpts]
  end
  return nothing
end
"""
scale_tau(tau,ta,tb)
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 12/23/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function scale_tau(tau,ta,tb)
  (tb - ta)/2*tau .+ (ta + tb)/2
end

"""
plant2dfs!(n,sol)
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/14/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function plant2dfs!(n,sol,U)
  tSample = range(sol.t[1],sol.t[end],length=n.mpc.ip.state.pts)
  dfs = DataFrame()
  if isempty(n.r.ip.plant)
    dfs[:t] = tSample
    for st in 1:n.mpc.ip.state.num
      dfs[n.mpc.ip.state.name[st]] = [sol(t)[st] for t in tSample]
    end
    for ctr in 1:n.mpc.ip.control.num
      dfs[n.mpc.ip.control.name[ctr]] = [U[ctr][t] for t in tSample]
    end
  else
    dfs[:t] = [n.r.ip.plant[:t]; tSample]
    for st in 1:n.mpc.ip.state.num
      dfs[n.mpc.ip.state.name[st]] = [n.r.ip.plant[n.mpc.ip.state.name[st]]; [sol(t)[st] for t in tSample]]
    end
    for ctr in 1:n.mpc.ip.control.num
      dfs[n.mpc.ip.control.name[ctr]] = [n.r.ip.plant[n.mpc.ip.control.name[ctr]]; [U[ctr][t] for t in tSample] ]
    end
  end
  n.r.ip.plant = dfs

  # TODO only run this if saving, it is redundant data, or maybe put time stamps or something in the above data
  dfs = DataFrame()
  dfs[:t] = tSample
  for st in 1:n.mpc.ip.state.num
    dfs[n.mpc.ip.state.name[st]] = [sol(t)[st] for t in tSample]
  end
  for ctr in 1:n.mpc.ip.control.num
    dfs[n.mpc.ip.control.name[ctr]] = [U[ctr][t] for t in tSample]
  end
  push!(n.r.ip.dfsplant,dfs)
  return nothing
end

"""
dvs2dfs(n)
# funtionality to save state, costate, and control data from optimization
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/10/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function dvs2dfs(n)
  dfs = DataFrame()
  dfs[:t] = n.r.ocp.tst
  for st in 1:n.ocp.state.num
    dfs[n.ocp.state.name[st]] = n.r.ocp.X[:,st]
  end
  for ctr in 1:n.ocp.control.num
    if n.s.ocp.integrationMethod==:tm
      dfs[!,n.ocp.control.name[ctr]] = n.r.ocp.U[:,ctr]
    else
      dfs[!,n.ocp.control.name[ctr]] = [n.r.ocp.U[:,ctr];NaN]
    end
  end

  if n.s.ocp.evalCostates && n.s.ocp.integrationMethod == :ps && n.s.ocp.evalConstraints
    CS_vector = Matrix{Float64}(undef, n.ocp.state.pts, n.ocp.state.num)
    for st in 1:n.ocp.state.num # states
      temp = [n.r.ocp.CS[st][int][1:end,:] for int in 1:n.ocp.Ni]
      CS_vector[1:end-1,st] = [idx for tempM in temp for idx=tempM]
      CS_vector[end,st] = NaN
    end
    for st in 1:n.ocp.state.num # states
      dfs[Symbol(n.ocp.state.name[st],:cs)] = CS_vector[:,st]
    end
  end

  return dfs
end

"""
opt2dfs!(n)
# funtionality to save optimization data
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/10/2017, Last Modified: 4/13/2018 \n
--------------------------------------------------------------------------------------\n
"""
function opt2dfs!(n;kwargs...)
  kw = Dict(kwargs);

  if !haskey(kw,:statusUpdate); statusUpdate=false;
  else; statusUpdate=get(kw,:statusUpdate,0);
  end

  # make sure that the feildnames are initialized
  if isempty(n.r.ocp.dfsOpt)
      n.r.ocp.dfsOpt = DataFrame(tSolve = [], objVal = [], status = [], iterNum = [], evalNum = [])
  end

  if !statusUpdate
    push!(n.r.ocp.dfsOpt[:tSolve], n.r.ocp.tSolve)
    push!(n.r.ocp.dfsOpt[:objVal], n.r.ocp.objVal)
    push!(n.r.ocp.dfsOpt[:status], n.r.ocp.status)
  else  # TODO consider removing and cleaning this up
    push!(n.r.ocp.dfsOpt[:tSolve], NaN)
    push!(n.r.ocp.dfsOpt[:objVal], NaN)
    if statusUpdate && (typeof(n.r.ocp.status)==Symbol)
      push!(n.r.ocp.dfsOpt[:status], n.r.ocp.status)
    else
      push!(n.r.ocp.dfsOpt[:status], NaN)
    end
  end
  if n.s.mpc.on
    push!(n.r.ocp.dfsOpt[:iterNum], n.r.ocp.iterNum) # can set iter_nums for a higher/lower level algoritm
  end
  push!(n.r.ocp.dfsOpt[:evalNum], n.r.ocp.evalNum-1)

  return nothing
end

"""
con2dfs(n)
# funtionality to save constraint data
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/20/2017, Last Modified: 4/13/2018 \n
--------------------------------------------------------------------------------------\n
"""
function con2dfs(n)
  dfs_con=DataFrame()
  dfs_con[:conVal]=n.r.ocp.constraint.value;
  return dfs_con
end

"""
postProcess!(n)
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 1/27/2017, Last Modified: 12/06/2019 \n
--------------------------------------------------------------------------------------\n
"""
function postProcess!(n;kwargs...)
  kw = Dict(kwargs)
  # check to see if the user is initializing while compensating for control delay
  if !haskey(kw,:Init); Init = false;
  else; Init = get(kw,:Init,0);
  end

  if n.s.ocp.save
    opt2dfs!(n)
  end

  # even if n.r.ocp.status==:Infeasible try to get solution. For the case that user may want to look at results to see where constraints where violated
  # in this case set =>  n.s.ocp.evalConstraints = true
  # http://jump.readthedocs.io/en/latest/refmodel.html#solve-status
  if !Init #&& (n.s.ocp.evalConstraints || ((n.r.ocp.status==:Optimal) || (n.r.ocp.status==:UserLimit)))
    if n.s.ocp.integrationMethod==:ps
      if n.s.ocp.finalTimeDV
        t = [scale_tau(n.ocp.ts[int],0.0,getvalue(n.ocp.tf)) for int in 1:n.ocp.Ni]     # scale time from [-1,1] to [t0,tf]
      else
        t = [scale_tau(n.ocp.ts[int],0.0,n.ocp.tf) for int in 1:n.ocp.Ni]
      end
      n.r.ocp.tctr = [idx for tempM in t for idx = tempM[1:end-1]] .+ getvalue(n.ocp.t0)
      n.r.ocp.tst = [n.r.ocp.tctr; t[end][end] .+ getvalue(n.ocp.t0)]
      # TODO check the above line... is t0 getting added on twice?
    elseif n.s.ocp.integrationMethod==:tm
      if n.s.ocp.finalTimeDV
        n.r.ocp.tctr = append!([0.0],cumsum(getvalue(n.ocp.dt))) .+ getvalue(n.ocp.t0)
      else
        n.r.ocp.tctr = append!([0.0],cumsum(n.ocp.dt)) .+ getvalue(n.ocp.t0)
      end
      n.r.ocp.tst = n.r.ocp.tctr
    end

    stateDataExists = false
    if n.r.ocp.status==:Optimal || (!n.s.mpc.onlyOptimal && n.s.mpc.on && isequal(n.mpc.v.evalNum,1))
      stateDataExists = true
      if (!isequal(n.r.ocp.status,:Optimal) && n.s.mpc.on && isequal(n.mpc.v.evalNum,1))
        @warn "There is no previous :Optimal solution to use since isequal(n.mpc.v.evalNum,1). \n
            Attemting to extract: ",n.r.ocp.status," solution. \n
            Setting: n.f.mpc.simFailed = [true, n.r.ocp.status] "
         n.f.mpc.simFailed = [true, n.r.ocp.status]
      end
      n.r.ocp.X = zeros(Float64,n.ocp.state.pts,n.ocp.state.num)
      n.r.ocp.U = zeros(Float64,n.ocp.control.pts,n.ocp.control.num)
      for st in 1:n.ocp.state.num
        n.r.ocp.X[:,st] = getvalue(n.r.ocp.x[:,st])
      end
      for ctr in 1:n.ocp.control.num
        n.r.ocp.U[:,ctr] = getvalue(n.r.ocp.u[:,ctr])
      end

    elseif n.s.mpc.on && n.s.mpc.lastOptimal && !n.s.mpc.onlyOptimal
      if !n.s.ocp.save
        error("This functionality currently needs to have n.s.ocp.save==true")
      end
      optIdx = findall(n.r.ocp.dfsOpt[:status].==:Optimal)[end]  # use the last :Optimal solution
      @show n.r.ocp.dfsOpt
      @show optIdx
      @show n.r.ocp.dfs[optIdx]
      @show n.mpc.v.t
      if n.r.ocp.dfs[optIdx][:t][1] > n.mpc.v.t
        timeIdx = 1
      elseif n.r.ocp.dfs[optIdx][:t][end] < n.mpc.v.t
        @warn "the current time is past the final time in the last :Optimal soultion.\n
              Setting: n.f.mpc.simFailed = [true, n.r.ocp.status]"
        n.f.mpc.simFailed = [true, n.r.ocp.status]
        return nothing
      else
        timeIdx = findall(n.r.ocp.dfs[optIdx][:t] .- n.mpc.v.t .<= 0)[end]     # find the nearest index in time
      end
      # TODO make an error message or fix      ERROR: LoadError: BoundsError: attempt to access 0-element Array{Int64,1} at index [0]
      n.r.ocp.tst = n.r.ocp.dfs[optIdx][:t][timeIdx:end]
      n.r.ocp.X = zeros(Float64,length(n.r.ocp.dfs[optIdx][n.ocp.state.name[1]][timeIdx:end]),n.ocp.state.num)
      if n.s.ocp.integrationMethod==:tm  # TODO try to
        n.r.ocp.tctr = n.r.ocp.dfs[optIdx][:t][timeIdx:end]
        n.r.ocp.U = zeros(Float64,length(n.r.ocp.dfs[optIdx][:,n.ocp.control.name[1]][timeIdx:end]),n.ocp.control.num)
      else
        n.r.ocp.tctr = n.r.ocp.dfs[optIdx][:t][timeIdx:end-1]
        n.r.ocp.U = zeros(Float64,length(n.r.ocp.dfs[optIdx][:,n.ocp.control.name[1]][timeIdx:end-1]),n.ocp.control.num)
      end
      for st in 1:n.ocp.state.num
        n.r.ocp.X[:,st] = n.r.ocp.dfs[optIdx][n.ocp.state.name[st]][timeIdx:end]
      end
      for ctr in 1:n.ocp.control.num
        if n.s.ocp.integrationMethod==:tm
          n.r.ocp.U[:,ctr] = n.r.ocp.dfs[optIdx][:,n.ocp.control.name[ctr]][timeIdx:end]
        else
          n.r.ocp.U[:,ctr] = n.r.ocp.dfs[optIdx][:,n.ocp.control.name[ctr]][timeIdx:end-1]
        end
      end
    else
      @warn "The solution is not Optimal \n
          Setting: n.f.mpc.simFailed = [true, n.r.ocp.status] "
       n.f.mpc.simFailed = [true, n.r.ocp.status]
    end
    if n.s.ocp.evalConstraints && n.r.ocp.status!=:Error  # note may want to remove the && arg
      evalConstraints!(n)
      # TODO make a note that costates can only be evaluated if .....
      if n.s.ocp.evalCostates && n.s.ocp.integrationMethod == :ps
        L1 = 0       # find index where dynamics constraints start
        for i in 1:length(n.r.ocp.constraint.name)
          if n.r.ocp.constraint.name[i] == :dyn_con
            L1 = n.r.ocp.constraint.nums[i][end][1]
          end
        end
        mpb = JuMP.internalmodel(n.ocp.mdl)
        c = MathProgBase.getconstrduals(mpb)
        # NOTE for now since costates are not defined for :tm methods, n.r.ocp.CS is in a different format than n.r.ocp.X
        # in the future if costates are defined for :tm methods this can be changed
        n.r.ocp.CS = [[zeros(Float64,n.ocp.Nck[int]) for int in 1:n.ocp.Ni] for st in 1:n.ocp.state.num]
        for int in 1:n.ocp.Ni
          b = 0
          for st in 1:n.ocp.state.num
            a = L1 + n.ocp.Nck[int]*(st-1)  # n.ocp.Nck[int]*(st-1) adds on indices for each additional state within the interval
            b = a + n.ocp.Nck[int] - 1      # length of the state within this interval
            n.r.ocp.CS[st][int] = -c[a:b]./n.ocp.ws[int]
          end
          L1 = b + 1 # adds indicies due to a change in the interval
        end
      end
    end
    if n.s.ocp.save && stateDataExists
      push!(n.r.ocp.dfs,dvs2dfs(n))
      push!(n.r.ocp.dfsCon,con2dfs(n))
      if n.s.ocp.interpolationOn
        if (n.r.ocp.status != :Error) # (n.r.ocp.status != :Infeasible) &&
          if n.s.ocp.integrationMethod==:ps && (n.r.ocp.status != :Infeasible) && !n.s.ocp.linearInterpolation
            interpolateLagrange!(n;numPts = n.s.ocp.numInterpPts, tfOptimal = n.s.ocp.tfOptimal)
            push!(n.r.ocp.AlltpolyPts,n.r.ocp.tpolyPts)
            push!(n.r.ocp.AllXpolyPts,n.r.ocp.XpolyPts)
            push!(n.r.ocp.AllUpolyPts,n.r.ocp.UpolyPts)
            if n.s.ocp.evalCostates && n.s.ocp.evalConstraints
              push!(n.r.ocp.AllCSpolyPts,n.r.ocp.CSpolyPts)
            end
          else
            interpolateLinear!(n;numPts = n.s.ocp.numInterpPts, tfOptimal = n.s.ocp.tfOptimal)
            if n.s.ocp.integrationMethod==:ps && !n.s.ocp.linearInterpolation
              push!(n.r.ocp.AlltpolyPts,nothing)
              push!(n.r.ocp.AllXpolyPts,nothing)
              push!(n.r.ocp.AllUpolyPts,nothing)
              if n.s.ocp.evalCostates && n.s.ocp.evalConstraints
                push!(n.r.ocp.AllCSpolyPts,nothing)
              end
            end
          end
        end
      end  # TODO save [] of interpolated pts for :tm methods
    end
  end
  return nothing
end
# TODO add some "finalPostProcess" to sace time
"""
optimize!(n)
# solves JuMP model and saves optimization data
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/6/2017, Last Modified: 4/13/2018 \n
--------------------------------------------------------------------------------------\n
"""
function optimize!(n;Iter::Int64=0)
  t1 = time(); status = JuMP.solve(n.ocp.mdl); t2 = time();
  if !n.s.ocp.cacheOnly
    n.r.ocp.status = status
    n.r.ocp.tSolve = t2 - t1
    n.r.ocp.objVal = getobjectivevalue(n.ocp.mdl)
    n.r.ocp.iterNum = Iter    # possible iteration number for a higher level algorithm
    n.r.ocp.evalNum = n.r.ocp.evalNum + 1
    postProcess!(n)      # temporarily save data
  end
  return nothing
end
"""
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 2/7/2017, Last Modified: 4/13/2018 \n
--------------------------------------------------------------------------------------\n
"""
function evalConstraints!(n)
  n.r.ocp.constraint.value=[];   # reset values
  n.r.ocp.constraint.nums=[]; s=1;
  for i = 1:length(n.r.ocp.constraint.handle)
    if n.r.ocp.constraint.name[i]==:dyn_con  # state constraits
      dfs=Vector{DataFrame}(n.ocp.state.num);
      con=DataFrame(step=1);
      l=0;
      for st in 1:n.ocp.state.num
        if n.s.ocp.integrationMethod==:ps
          temp=[getdual(n.r.ocp.constraint.handle[i][int][:,st]) for int in 1:n.ocp.Ni];
          vals=[idx for tempM in temp for idx=tempM];
          dfs[st]=DataFrame(step=1:sum(n.ocp.Nck);Dict(n.ocp.state.name[st] => vals)...);
          l=l+length(vals);
        else
          dfs[st]=DataFrame(step=1:n.ocp.N;Dict(n.ocp.state.name[st] => getdual(n.r.ocp.constraint.handle[i][:,st]))...);
          l=l+length(n.r.ocp.constraint.handle[i][:,st]);
        end
        if st==1; con=dfs[st]; else; con=join(con,dfs[st],on=:step); end
      end
    else
      S=0;
      try
        S=JuMP.size(n.r.ocp.constraint.handle[i])
      catch
        error("\n For now, the constraints cannot be in this form: \n
        con=@NLconstraint(mdl,n.r.ocp.u[1,1]==param); \n
        Write it in array form: \n
          con=@NLconstraint(mdl,[i=1],n.r.ocp.u[i,1]==param); \n")
      end
      if length(S)==1
        con = DataFrame(step=1:length(n.r.ocp.constraint.handle[i]);Dict(n.r.ocp.constraint.name[i] => getdual(n.r.ocp.constraint.handle[i][:]))...);
        l=S[1];
      elseif length(S)==2
        dfs=Vector{DataFrame}(S[1]);
        con=DataFrame(step=1);
        for idx in 1:S[1]
          try
            dfs[idx] = DataFrame(step=1:S[2];Dict(n.r.ocp.constraint.name[i] => getdual(n.r.ocp.constraint.handle[i][idx,:]))...);
          catch
            dfs[idx] = DataFrame(step=1:S[2];Dict(n.r.ocp.constraint.name[i] => NaN)...); # fix for case where all of the states are not being constrainted, but some are within some XF_tol
          end
          if idx==1;
            con = dfs[idx]
          else
            con = join(con,dfs[idx],on=:step,makeunique=true)
          end
        end
        l = S[1]*S[2]
      end
    end
    f = s + l - 1
    num = (i,n.r.ocp.constraint.name[i],@sprintf("length = %0.0f",l),string("indecies in g(x) = "),(s,f))
    push!(n.r.ocp.constraint.nums,num)
    push!(n.r.ocp.constraint.value,con)
    s = f + 1
  end
  return nothing
end

"""
description = string(
" *  \n ")

resultsDir!(r;resultsName=resultsName,description=description)
# removes results folder and creates a new one
# TODO consider putting in a warning or some sort of an interaction with user
--------------------------------------------------------------------------------------\n
Author: Huckleberry Febbo, Graduate Student, University of Michigan
Date Create: 3/26/2017, Last Modified: 5/29/2017 \n
--------------------------------------------------------------------------------------\n
"""
function resultsDir!(n;resultsName::String = "",description::DataFrame = DataFrame())
 results_dir=string(n.r.mainDir,"/results/",resultsName)  # define directories
 n.r.resultsDir=results_dir;

 if isdir(n.r.resultsDir)
   rm(n.r.resultsDir; recursive=true)
   print("\n The old results have all been deleted! \n \n")
 end
 mkdir(n.r.resultsDir)# create directory

 cd(n.r.resultsDir)
   CSV.write("description.csv", description; quotechar = ' ')
 cd(n.r.mainDir)
 return nothing
end

end # module


using DataFrames, CSV

const CCLE = CSV.read("../gene_expression/tpm_values.csv", DataFrame)

function get_tpm(gene::String, cell_line::String)::Float64
    return CCLE[CCLE[!,Symbol("Column1")].==gene,Symbol(cell_line)][1]
end

function init_E1(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_E1] * get_tpm("EGFR", cell_line)
end

function init_E2(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_E2] * get_tpm("ERBB2", cell_line)
end

function init_E3(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_E3] * get_tpm("ERBB3", cell_line)
end

function init_E4(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_E4] * get_tpm("ERBB4", cell_line)
end

function init_G(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_G] * get_tpm("GRB2", cell_line)
end

function init_S(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_SHC1] * get_tpm("SHC1", cell_line) +
            p[C.a_SHC2] * get_tpm("SHC2", cell_line) +
            p[C.a_SHC3] * get_tpm("SHC3", cell_line) +
            p[C.a_SHC4] * get_tpm("SHC4", cell_line))
end

function init_I(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_PIK3CA] * get_tpm("PIK3CA", cell_line) +
            p[C.a_PIK3CB] * get_tpm("PIK3CB", cell_line) +
            p[C.a_PIK3CD] * get_tpm("PIK3CD", cell_line) +
            p[C.a_PIK3CG] * get_tpm("PIK3CG", cell_line))
end

function init_PTEN(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_PTEN] * get_tpm("PTEN", cell_line)
end

function init_R(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_RASA1] * get_tpm("RASA1", cell_line) +
            p[C.a_RASA2] * get_tpm("RASA2", cell_line) +
            p[C.a_RASA3] * get_tpm("RASA3", cell_line))
end

function init_O(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_SOS1] * get_tpm("SOS1", cell_line) +
            p[C.a_SOS2] * get_tpm("SOS2", cell_line))
end

function init_A(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_A] * get_tpm("GAB1", cell_line)
end

function init_Akt(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_AKT1] * get_tpm("AKT1", cell_line) +
            p[C.a_AKT2] * get_tpm("AKT2", cell_line))
end

function init_RsD(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_HRAS] * get_tpm("HRAS", cell_line) +
            p[C.a_KRAS] * get_tpm("KRAS", cell_line) +
            p[C.a_NRAS] * get_tpm("NRAS", cell_line))
end

function init_Raf(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_ARAF] * get_tpm("ARAF", cell_line) +
            p[C.a_BRAF] * get_tpm("BRAF", cell_line) +
            p[C.a_RAF1] * get_tpm("RAF1", cell_line))
end

function init_MEK(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_MAP2K1] * get_tpm("MAP2K1", cell_line) +
            p[C.a_MAP2K2] * get_tpm("MAP2K2", cell_line))
end

function init_T(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_T] * get_tpm("PTPN1", cell_line)
end

function init_CREBn(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_CREBn] * get_tpm("CREB1", cell_line)
end

function init_ERKc(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_MAPK1] * get_tpm("MAPK1", cell_line) +
            p[C.a_MAPK3] * get_tpm("MAPK3", cell_line))
end

function init_Elk1n(p::Vector{Float64}, cell_line::String)::Float64
    return p[C.scale_Elk1n] * get_tpm("ELK1", cell_line)
end

function init_RSKc(p::Vector{Float64}, cell_line::String)::Float64
    return (p[C.a_RPS6KA1] * get_tpm("RPS6KA1", cell_line) +
            p[C.a_RPS6KA2] * get_tpm("RPS6KA2", cell_line) +
            p[C.a_RPS6KA3] * get_tpm("RPS6KA3", cell_line) +
            p[C.a_RPS6KA6] * get_tpm("RPS6KA6", cell_line))
end

function tpm2ival!(p::Vector{Float64}, u0::Vector{Float64}, cell_line::String)
    p[C.PTEN] *= init_PTEN(p, cell_line)

    u0[V.E1] *= init_E1(p, cell_line)
    u0[V.E2] *= init_E2(p, cell_line)
    u0[V.E3] *= init_E3(p, cell_line)
    u0[V.E4] *= init_E4(p, cell_line)
    u0[V.G] *= init_G(p, cell_line)
    u0[V.S] *= init_S(p, cell_line)
    u0[V.I] *= init_I(p, cell_line)
    u0[V.R] *= init_R(p, cell_line)
    u0[V.O] *= init_O(p, cell_line)
    u0[V.A] *= init_A(p, cell_line)
    u0[V.Akt] *= init_Akt(p, cell_line)
    u0[V.RsD] *= init_RsD(p, cell_line)
    u0[V.Raf] *= init_Raf(p, cell_line)
    u0[V.MEK] *= init_MEK(p, cell_line)
    u0[V.T] *= init_T(p, cell_line)
    u0[V.CREBn] *= init_CREBn(p, cell_line)
    u0[V.ERKc] *= init_ERKc(p, cell_line)
    u0[V.Elk1n] *= init_Elk1n(p, cell_line)
    u0[V.RSKc] *= init_RSKc(p, cell_line)

    return p, u0
end

function mul_ratio2mcf7!(p::Vector{Float64}, u0::Vector{Float64}, cell_line_2::String)
    p[C.PTEN] *= init_PTEN(p, cell_line_2) / init_PTEN(p, "MCF7")

    u0[V.E1] *= init_E1(p, cell_line_2) / init_E1(p, "MCF7")
    u0[V.E2] *= init_E2(p, cell_line_2) / init_E2(p, "MCF7")
    u0[V.E3] *= init_E3(p, cell_line_2) / init_E3(p, "MCF7")
    u0[V.E4] *= init_E4(p, cell_line_2) / init_E4(p, "MCF7")
    u0[V.G] *= init_G(p, cell_line_2) / init_G(p, "MCF7")
    u0[V.S] *= init_S(p, cell_line_2) / init_S(p, "MCF7")
    u0[V.I] *= init_I(p, cell_line_2) / init_I(p, "MCF7")
    u0[V.R] *= init_R(p, cell_line_2) / init_R(p, "MCF7")
    u0[V.O] *= init_O(p, cell_line_2) / init_O(p, "MCF7")
    u0[V.A] *= init_A(p, cell_line_2) / init_A(p, "MCF7")
    u0[V.Akt] *= init_Akt(p, cell_line_2) / init_Akt(p, "MCF7")
    u0[V.RsD] *= init_RsD(p, cell_line_2) / init_RsD(p, "MCF7")
    u0[V.Raf] *= init_Raf(p, cell_line_2) / init_Raf(p, "MCF7")
    u0[V.MEK] *= init_MEK(p, cell_line_2) / init_MEK(p, "MCF7")
    u0[V.T] *= init_T(p, cell_line_2) / init_T(p, "MCF7")
    u0[V.CREBn] *= init_CREBn(p, cell_line_2) / init_CREBn(p, "MCF7")
    u0[V.ERKc] *= init_ERKc(p, cell_line_2) / init_ERKc(p, "MCF7")
    u0[V.Elk1n] *= init_Elk1n(p, cell_line_2) / init_Elk1n(p, "MCF7")
    u0[V.RSKc] *= init_RSKc(p, cell_line_2) / init_RSKc(p, "MCF7")

    return p, u0
end


using Distributions, SpecialFunctions, Random
"""
Estimation of the transition matrix for outlier task via a Particle filter
"""
struct TParticleFilterJump <: TPs1a0s0
    nr_states::Int
    nr_actions::Int
    nparticles::Int # Per state and action
    Neffthrs::Float64 # Neff = approx nr of particles that have a weight which
                    # meaningfully contributes to the probability distribution.
    surpriseprobability::Float64
    stochasticity::Float64
    particlesjump::Array{Bool, 3} # wannabe nr_states x nr_actions x nparticles.
    weights::Array{Float64, 3} # wannabe nr_states x nr_actions x nparticles.
    Sgmparticles::Array{Float64, 1} # wannabe nparticles.
    Sgm::Array{Float64, 1}
    m::Float64
    γ_surprise::Array{Float64, 1}
    Ps1a0s0::Array{Dict{Tuple{Int, Int}, Float64}, 1}
    SPE::Array{Float64, 1}
    counts::Array{Array{Float64,1}, 3}
    seed::Any
    rng::MersenneTwister
    terminalstates::Array{Int,1}
end
function TParticleFilterJump(;nr_states = 10, nr_actions = 4, nparticles = 6,
                        surpriseprobability = .01,
                        stochasticity = .01, seed = 3)
    Neffthrs = nparticles/2.
    particlesjump = Array{Bool, 3}(undef, nr_actions, nr_states, nparticles)
    weights = Array{Float64, 3}(undef, nr_actions, nr_states, nparticles)
    Sgmparticles = Array{Float64, 1}(undef, nparticles)
    Ps1a0s0 = [Dict{Tuple{Int, Int}, Float64}() for _ in 1:nr_states]
    [Ps1a0s0[sprime][(a, s)] = 1. /nr_states for sprime in 1:nr_states for a in 1:nr_actions for s in 1:nr_states]
    counts = Array{Array{Float64,1}}(undef, nr_actions, nr_states, nparticles)
    rng = MersenneTwister(seed)
    [weights[a0, s0, i] = 1. /nparticles for a0 in 1:nr_actions for s0 in 1:nr_states for i in 1:nparticles]
    Sgmparticles .= 1.; Sgm = [1.]
    [counts[a0, s0, i] = zeros(nr_states) for a0 in 1:nr_actions for s0 in 1:nr_states for i in 1:nparticles]
    m = surpriseprobability/ (1. - surpriseprobability)
    γ_surprise = [m/(1. + m)]
    SPE = [1. - 1. / nr_states]

    TParticleFilterJump(nr_states, nr_actions, nparticles, Neffthrs, surpriseprobability, stochasticity,
                    particlesjump, weights, Sgmparticles, Sgm, m, γ_surprise, Ps1a0s0, SPE, counts, seed, rng, Int[])
end
export TParticleFilterJump
function updatet!(learnerT::TParticleFilterJump, s0, a0, s1, done)
    learnerT.particlesjump[a0, s0, :] .= false
    calcSPE!(learnerT, s0, a0, s1) # Calculate a hypothetical SPE
    calcSgm!(learnerT, s0, a0, s1)
    calcγ!(learnerT)
    stayterms = computestayterms(learnerT, s0, a0, s1)
    getweights!(learnerT, s0, a0, stayterms, 1. / learnerT.nr_states)
    sampleparticles!(learnerT, s0, a0, stayterms, 1. / learnerT.nr_states)
    Neff = 1. /sum(learnerT.weights[a0, s0, :] .^2)
    if Neff <= learnerT.Neffthrs; resample!(learnerT, s0, a0); end
    updatecounts!(learnerT, s0, a0, s1)
    computePs1a0s0!(learnerT, s0, a0)
    computeterminalPs1a0s0!(learnerT, s1, done)
end
export updatet!
function computestayterms(learnerT::TParticleFilterJump, s0, a0, s1)
    stayterms = zeros(learnerT.nparticles)
    for i in 1:learnerT.nparticles
        stayterms[i] = (learnerT.stochasticity + learnerT.counts[a0, s0, i][s1])
        stayterms[i] /= sum(learnerT.stochasticity .+ learnerT.counts[a0, s0, i])
    end
    stayterms
end
export computestayterms
function getweights!(learnerT::TParticleFilterJump, s0, a0, stayterms, jumpterm)
    for i in 1:learnerT.nparticles #firstratio = B(s + a(h_t-1)') / B(s + a(h_t-1)). secondratio = B(s + a(h_t=h_t-1 + 1)) / B(s)
        particleweightupdate = (1. - learnerT.surpriseprobability) * stayterms[i]
        particleweightupdate += learnerT.surpriseprobability * jumpterm
        learnerT.weights[a0, s0, i] *= particleweightupdate
    end

    learnerT.weights[a0, s0, :] ./= sum(learnerT.weights[a0, s0, :]) # Normalize

end
export getweights!
function sampleparticles!(learnerT::TParticleFilterJump,
                        s0, a0, stayterms, jumpterm)
    for i in 1:learnerT.nparticles
        particlestayprobability = computeproposaldistribution(learnerT, stayterms[i], jumpterm)

        r = rand(learnerT.rng) # Draw and possibly update
        if r > particlestayprobability
            # println("jump!")
            learnerT.particlesjump[a0, s0, i] = true
        end
    end
end
export sampleparticles!
function computeproposaldistribution(learnerT::TParticleFilterJump,
                                    istayterm, jumpterm)
    particlestayprobability = 1. /(1. + ((learnerT.surpriseprobability * jumpterm) /
                                ((1. - learnerT.surpriseprobability) * istayterm)))
end
export computeproposaldistribution
function updatecounts!(learnerT::TParticleFilterJump, s0, a0, s1)
    for i in 1:learnerT.nparticles
        if !learnerT.particlesjump[a0, s0, i] # if it is not a surprise trial: Integrate
            learnerT.counts[a0, s0, i][s1] += 1 # +1 for s'
        end
    end
end
export updatecounts!
function resample!(learnerT::TParticleFilterJump, s0, a0)
    tempcopyparticleweights = deepcopy(learnerT.weights[a0, s0, :])
    d = Categorical(tempcopyparticleweights)
    tempcopyparticlesjump = copy(learnerT.particlesjump[a0, s0, :])
    tempcopycounts = deepcopy(learnerT.counts[a0, s0, :])
    for i in 1:learnerT.nparticles
        sampledindex = rand(learnerT.rng, d)
        learnerT.particlesjump[a0, s0, i] = copy(tempcopyparticlesjump[sampledindex])
        learnerT.weights[a0, s0, i] = 1. /learnerT.nparticles
        learnerT.counts[a0, s0, i] = deepcopy(tempcopycounts[sampledindex])
    end
end
export resample!
function computePs1a0s0!(learnerT::TParticleFilterJump, s0, a0)
    thetasweighted = zeros(learnerT.nparticles, learnerT.nr_states)
    for i in 1:learnerT.nparticles
        thetas = (learnerT.stochasticity .+ learnerT.counts[a0, s0, i])
        thetas /= sum(learnerT.stochasticity .+ learnerT.counts[a0, s0, i])
        thetasweighted[i,:] = learnerT.weights[a0, s0, i] .* thetas
    end
    expectedvaluethetas = sum(thetasweighted, dims = 1)
    for s in 1:learnerT.nr_states
        learnerT.Ps1a0s0[s][(a0, s0)] = copy(expectedvaluethetas[s])
        # @show learnerT.Ps1a0s0[s][(a0, s0)]
    end
end
""" Outgoing transitions from goal state: set all to 0, except for 1 to itself
(absorbing state) """
function computeterminalPs1a0s0!(learnerT::TPs1a0s0, s1, done)
    if done
        if !in(s1, learnerT.terminalstates)
            push!(learnerT.terminalstates, s1)
            for a in 1:learnerT.nr_actions
                for s in 1:learnerT.nr_states
                    if s == s1
                        learnerT.Ps1a0s0[s][(a, s1)] = 1.
                    else
                        learnerT.Ps1a0s0[s][(a, s1)] = 0.
                    end
                end
            end
        end
    end
end
function calcSgmi(αt, α0, s1)
    p0 = α0[s1] / sum(α0)
    pt = αt[s1] / sum(αt)
    p0 / pt
end
function calcSgmparticles!(learnerT::TParticleFilterJump, s0, a0, s1)
    for i in 1:learnerT.nparticles
        learnerT.Sgmparticles[i] = calcSgmi(
                            learnerT.stochasticity .+ learnerT.counts[a0, s0, i],
                            learnerT.stochasticity .* ones(learnerT.nr_states),
                            s1)
    end
end
function calcSgm!(learnerT::TParticleFilterJump, s0, a0, s1)

    calcSgmparticles!(learnerT, s0, a0, s1)

    if any(isequal.(learnerT.Sgmparticles, 0.))
        learnerT.Sgm[1] = 0.
    else
        SgmInvWeighted = learnerT.weights[a0, s0, :] ./ learnerT.Sgmparticles
        learnerT.Sgm[1] = 1. / sum(SgmInvWeighted)
    end
end
function calcγ!(learnerT::TParticleFilterJump)
    learnerT.γ_surprise[1] = learnerT.m * learnerT.Sgm[1]/(1. + learnerT.m * learnerT.Sgm[1])
end
function calcExpectedHiddenState(learnerT::TParticleFilterJump, s0, a0)
    expectedhiddenstate = sum(Float64.(learnerT.particlesjump[a0, s0, :]) .*
                        learnerT.weights[a0, s0, :])
    expectedhiddenstate = round(expectedhiddenstate)
end
function calcExpectedHiddenStateNotRounded(learnerT::TParticleFilterJump, s0, a0)
    expectedhiddenstate = sum(Float64.(learnerT.particlesjump[a0, s0, :]) .*
                        learnerT.weights[a0, s0, :])
end
function calcSPE!(learnerT::TParticleFilterJump, s0, a0, s1)
    learnerT.SPE[1] = 1. - learnerT.Ps1a0s0[s1][(a0, s0)]
end


include("BMA.jl")
include("GLM.jl")
include("RFM.jl")


using DataFrames
using Test

for testscen in 1:2
    valdir, scenario, use_permafrost, use_seaice = get_scenario(testscen)
    println(scenario)

    m = page_model()
    include("../src/components/NonMarketDamages.jl")

    nonmarketdamages = addnonmarketdamages(m)

    set_param!(m, :NonMarketDamages, :rtl_realizedtemperature, readpagedata(m, "test/validationdata/$valdir/rtl_realizedtemperature.csv"))
    set_param!(m, :NonMarketDamages, :rcons_per_cap_MarketRemainConsumption, readpagedata(m, "test/validationdata/$valdir/rcons_per_cap_MarketRemainConsumption.csv"))
    set_param!(m, :NonMarketDamages, :rgdp_per_cap_MarketRemainGDP, readpagedata(m, "test/validationdata/$valdir/rgdp_per_cap_MarketRemainGDP.csv"))
    set_param!(m, :NonMarketDamages, :atl_adjustedtolerableleveloftemprise, readpagedata(m, "test/validationdata/atl_adjustedtolerableleveloftemprise_nonmarket.csv"))
    set_param!(m, :NonMarketDamages, :imp_actualreduction, readpagedata(m, "test/validationdata/$valdir/imp_actualreduction_nonmarket.csv"))
    set_param!(m, :NonMarketDamages, :isatg_impactfxnsaturation, 17.0)

    p = load_parameters(m)
    p["y_year_0"] = 2015.
    p["y_year"] = Mimi.dim_keys(m.md, :time)
    set_leftover_params!(m, p)

    run(m)
    rcons_per_cap = m[:NonMarketDamages, :rcons_per_cap_NonMarketRemainConsumption]
    rcons_per_cap_compare = readpagedata(m, "test/validationdata/$valdir/rcons_per_cap_NonMarketRemainConsumption.csv")
    @test rcons_per_cap ≈ rcons_per_cap_compare rtol=1e-2

    rgdp_per_cap = m[:NonMarketDamages, :rgdp_per_cap_NonMarketRemainGDP]
    rgdp_per_cap_compare = readpagedata(m, "test/validationdata/$valdir/rgdp_per_cap_NonMarketRemainGDP.csv")
    @test rgdp_per_cap ≈ rgdp_per_cap_compare rtol=1e-2
end


struct ∂☆recurse{N}; end

struct ∂vararg{N}; end

(::∂vararg{N})() where {N} = ZeroBundle{N}(())
function (::∂vararg{N})(a::AbstractTangentBundle{N}...) where N
    CompositeBundle{N, Tuple{map(x->basespace(typeof(x)), a)...}}(a)
end

struct ∂☆new{N}; end

(::∂☆new{N})(B::Type, a::AbstractTangentBundle{N}...) where {N} =
    CompositeBundle{N, B}(a)

@generated (::∂☆new{N})(B::Type) where {N} = return :(ZeroBundle{$N}($(Expr(:new, :B))))

# Sometimes we don't know whether or not we need to the ZeroBundle when doing
# the transform, so this can happen - allow it for now.
(this::∂☆new{N})(B::ATB{N, <:Type}, args::ATB{N}...) where {N} = this(primal(B), args...)

function transform_fwd!(ci, meth, nargs, sparams, N)
    new_code = Any[]
    new_codelocs = Any[]
    ssa_mapping = Int[]
    loc_mapping = Int[]

    function emit!(stmt)
        (isexpr(stmt, :call) || isexpr(stmt, :(=)) || isexpr(stmt, :new)) || return stmt
        push!(new_code, stmt)
        push!(new_codelocs, isempty(new_codelocs) ? 0 : new_codelocs[end])
        SSAValue(length(new_code))
    end

    function mapstmt!(stmt)
        if isexpr(stmt, :(=))
            return Expr(stmt.head, emit!(mapstmt!(stmt.args[1])), emit!(mapstmt!(stmt.args[2])))
        elseif isexpr(stmt, :call)
            args = map(stmt.args) do stmt
                emit!(mapstmt!(stmt))
            end
            return Expr(:call, ∂☆{N}(), args...)
        elseif isexpr(stmt, :new)
            args = map(stmt.args) do stmt
                emit!(mapstmt!(stmt))
            end
            return Expr(:call, ∂☆new{N}(), args...)
        elseif isexpr(stmt, :splatnew)
            args = map(stmt.args) do stmt
                emit!(mapstmt!(stmt))
            end
            return Expr(:call, Core._apply_iterate, FwdIterate(ZeroBundle{N}(iterate)), ∂☆new{N}(), emit!(Expr(:call, tuple, args[1])), args[2:end]...)
        elseif isa(stmt, SSAValue)
            return SSAValue(ssa_mapping[stmt.id])
        elseif isa(stmt, Core.SlotNumber)
            return SlotNumber(2 + stmt.id)
        elseif isa(stmt, Argument)
            return SlotNumber(2 + stmt.n)
        elseif isa(stmt, ReturnNode)
            return ReturnNode(emit!(mapstmt!(stmt.val)))
        elseif isa(stmt, GotoNode)
            return stmt
        elseif isa(stmt, GotoIfNot)
            return GotoIfNot(emit!(Expr(:call, primal, emit!(mapstmt!(stmt.cond)))), stmt.dest)
        elseif isexpr(stmt, :static_parameter)
            return ZeroBundle{N}(sparams[stmt.args[1]])
        elseif isexpr(stmt, :foreigncall)
            return Expr(:call, error, "Attempted to AD a foreigncall. Missing rule?")
        elseif isexpr(stmt, :meta) || isexpr(stmt, :inbounds)
            # Can't trust that meta annotations are still valid in the AD'd
            # version.
            return nothing
        else
            return Expr(:call, ZeroBundle{N}, stmt)
        end
    end

    for i = 1:meth.nargs
        if meth.isva && i == meth.nargs
            args = map(i:(nargs+1)) do j
                emit!(Expr(:call, getfield, SlotNumber(2), j))
            end
            emit!(Expr(:(=), SlotNumber(2 + i), Expr(:call, ∂vararg{N}(), args...)))
        else
            emit!(Expr(:(=), SlotNumber(2 + i), Expr(:call, getfield, SlotNumber(2), i)))
        end
    end

    for (stmt, codeloc) in zip(ci.code, ci.codelocs)
        push!(loc_mapping, length(new_code)+1)
        push!(new_codelocs, codeloc)
        push!(new_code, mapstmt!(stmt))
        push!(ssa_mapping, length(new_code))
    end

    # Rewrite control flow
    for (i, stmt) in enumerate(new_code)
        if isa(stmt, GotoNode)
            new_code[i] = GotoNode(loc_mapping[stmt.label])
        elseif isa(stmt, GotoIfNot)
            new_code[i] = GotoIfNot(stmt.cond, loc_mapping[stmt.dest])
        end
    end

    ci.code = new_code
    ci.codelocs = new_codelocs
    ci
end

π(::Type{<:AbstractTangentBundle{N, B}} where N) where {B} = B

∂☆passthrough(args::Tuple{Vararg{ATB{N}}}) where {N} =
    ZeroBundle{N}(primal(getfield(args, 1))(map(primal, Base.tail(args))...))

function ∂☆nomethd(@nospecialize(args))
    throw(MethodError(primal(args[1]), map(primal, Base.tail(args))))
end

function perform_fwd_transform(@nospecialize(ff::Type{∂☆recurse{N}}), @nospecialize(args)) where {N}
    if all(x->x <: ZeroBundle, args)
        return :(∂☆passthrough(args))
    end

    # Check if we have an rrule for this function
    sig = Tuple{map(π, args)...}
    mthds = Base._methods_by_ftype(sig, -1, typemax(UInt))
    if length(mthds) != 1
        return :(∂☆nomethd(args))
    end
    match = mthds[1]

    mi = Core.Compiler.specialize_method(match)
    ci = Core.Compiler.retrieve_code_info(mi)

    ci′ = copy(ci)
    ci′.edges = MethodInstance[mi]

    transform_fwd!(ci′, mi.def, length(args) - 1, match.sparams, N)

    ci′.ssavaluetypes = length(ci′.code)
    ci′.method_for_inference_limit_heuristics = match.method
    slotnames = Symbol[Symbol("#self#"), :args, ci.slotnames...]
    slotflags = UInt8[(0x00 for i = 1:2)..., ci.slotflags...]
    slottypes = Any[(Any for i = 1:2)..., ci.slotflags...]
    ci′.slotnames = slotnames
    ci′.slotflags = slotflags
    ci′.slottypes = slottypes

    ci′
end

@eval function (ff::∂☆recurse)(args...)
    $(Expr(:meta, :generated_only))
    $(Expr(:meta,
            :generated,
            Expr(:new,
                Core.GeneratedFunctionStub,
                :perform_fwd_transform,
                Any[:ff, :args],
                Any[],
                @__LINE__,
                QuoteNode(Symbol(@__FILE__)),
                true)))
end


"""
    readbin(fname::String,prec::Type,iosize::Tuple,fldidx=1)

Read in a binary file to an Array.
"""
function readbin(fname::String,prec::Type,iosize::Tuple,fldidx=1)
    n3 = 1
    if length(iosize) == 3
        n1,n2,n3 = iosize
    else
        n1,n2 = iosize
    end

    if prec == Float64
        reclen = 8
    else
        reclen = 4
    end

    field = Array{prec,1}(undef,(n1*n2*n3))
    fid = open(fname)
    if fldidx != 1
        seek(fid,n1*n2*n3*(fldidx-1)*reclen)
    end
    read!(fid,field)
    close(fid)
    field = hton.(field)

    n3>1 ? s=(n1,n2,n3) : s=(n1,n2)
    return reshape(field,s)

end

"""
    readbin(flddata::BinData,tidx=1)

Read in a binary file as an array as specified by BinData
"""
readbin(flddata::BinData,tidx=1) = readbin(flddata.fnames[tidx],flddata.precision,flddata.iosize,flddata.fldidx)

"""
    readncdata(var::NCData,i::Union{Colon,Integer}=:)

Read netcdf file as specified in `NCData` argument. Optional
argument `i` can be used to read a specific records / times.
"""
function readncdata(var::NCData,i::Union{Colon,Integer}=:)
    ds = Dataset(var.fname)
    ndims = length(size(ds[var.varname]))

    if ndims == 1
        values = ds[var.varname][i]
    elseif ndims == 2
        values = ds[var.varname][:,i]
    elseif ndims == 3
        values = ds[var.varname][:,:,i]
    elseif ndims == 4
        values = ds[var.varname][:,:,:,i]
    else
        println("Variables greater than four dimensions not currently supported")
    end
    close(ds)
    return values
end

"""
    readdata(flddata,tidx=1)

Generic wrapper function to read data from a file regardless of file/data type.
"""
function readdata(flddata,tidx=1)
    if isa(flddata,BinData)
        res = readbin(flddata,tidx)
    elseif isa(flddata,NCData)
        res = readncdata(flddata,tidx)
    elseif isa(flddata,TileData)
        if isa(flddata.vals,BinData)
            iosize = flddata.vals.iosize
            grid = flddata.tileinfo["XC"].grid
            if length(iosize) == 3
                f = Array{Array{flddata.vals.precision,2},2}(undef,grid.nFaces,iosize[3])
                exarray = MeshArray(grid,f)
            else
                exarray = flddata.tileinfo["XC"]
            end
            res = read(readbin(flddata.vals,tidx),exarray)
        else
            if isa(flddata.vals,MeshArray) || isa(flddata.vals,MeshArrays.gcmfaces)
                res = flddata.vals
            else
                res = flddata.vals[tidx]
            end
        end
    end
    return res
end

"""
    readncfile(fname,backend::Module=NCDatasets)

Read in a NetCDF file and return variables/dimensions as `NCvar` structs, and
    file attributes as `Dict`. Large variables/dimensions are not loaded into
    memory. This can use either `NCDatasets.jl` or `NetCDF.jl`
"""
function readncfile(fname,backend::Module=NCDatasets)

    ds = Dataset(fname)

    dims = Dict{AbstractString,NCvar}()
    vars = Dict{AbstractString,NCvar}()

    for k in keys(ds)
        if ~haskey(dims,k)
            k_units = get(ds[k].attrib,"units","")
            k_atts = Dict(ds[k].attrib)
            if length(dimnames(ds[k])) == 1 && k == dimnames(ds[k])[1] # this variable is also a dimension
                k_dims = length(ds[k])
                if length(size(ds[k])) == 1
                    k_values = ds[k][:]
                elseif length(size(ds[k])) == 2
                    k_values = ds[k][:,:]
                else
                    k_values = NCData(fname,k,backend,typeof(ds[k].var[:][1])) # Pointer to this variable in the file
                end
                if !isa(k_values,NCData) && !any(isa.(k_values,Ref(Missing)))
                    k_values = typeof(k_values[1]).(k_values)
                end
                dims[k] = NCvar(k,k_units,k_dims,k_values,k_atts,backend)
            else # Not a dimension
                k_dims = Array{NCvar,1}()
                for d in dimnames(ds[k])
                    #global k_dims,dims
                    if !haskey(dims,d)
                        if haskey(ds,d) # Add the dimension
                            d_units = get(ds[d].attrib,"units","")
                            d_dims = length(ds[d])
                            d_values = NCData(fname,d,backend,typeof(ds[d][1])) # Pointer to this variable in the file
                            d_atts = Dict(ds[d].attrib)
                            dims[d] = NCvar(d,d_units,d_dims,d_values,d_atts,backend)
                        else
                            dims[d] = NCvar(d,"",ds.dim[d],[],Dict(),backend)
                        end

                    end
                    k_dims = cat(k_dims,dims[d],dims=1)
                end
                hasTimeDim = hastimedim(k_dims)
                if  ~hasTimeDim && length(k_dims) == 1# extra variable, not main field, just load the data now
                    k_values = ds[k][:]
                elseif ~hasTimeDim && length(k_dims) == 2
                    k_values = ds[k][:,:]
                elseif ~hasTimeDim && length(k_dims) == 3
                    k_values = ds[k][:,:,:]
                elseif hasTimeDim
                    k_values = NCData(fname,k,backend,eltype(ds[k].var[:])) # Pointer to this variable in the file
                end

                if !hasTimeDim && !any(isa.(k_values,Ref(Missing)))
                    k_values = typeof(k_values[1]).(k_values)
                end

                vars[k] = NCvar(k,k_units,k_dims,k_values,k_atts,backend)
            end
        end
    end

    atts = ["_FillValue","missing_value","itile","ntile"]
    fileatts = Dict()
    for a in atts
        if haskey(ds.attrib,a)
            fileatts[a] = ds.attrib[a]
        end
    end
    close(ds)
    return vars,dims,fileatts
end


# macros for accessing the auxiliary variables
# note that each variable needs two macros, a get and a set

#=
macro getPressure(aux_vars, node, element)
  pressure_index = 1
  return :($aux_vars[$pressure_index, $node, $element])
end
=#

@doc """
  These macros set and retrieve values of auxiliary variables.  Users
   should not acess values in the auxiliary variables directly.
"""

@doc """
### EulerEquationMod.@getPressure

  This macro returns pressure from the vector of auxiliary variables at
  a node.  The user should use this function to access pressure rather
  than accessing the vector of auxiliary variables directly
"""->
macro getPressure(vars)
  pressure_index = 1
  return esc(:($vars[$pressure_index]))
end

function getPressure(aux_vars)
  return aux_vars[1]
end

@doc """
### EulerEquationMod.@setPressure

  This function tests the pressure for the specified node and element,
  wheer aux_vars is the 3D array of auxiliary variables.  The users should
   use this function to set the value rather than accessing the array of
   auxiliary variables directly
"""->
macro setPressure(aux_vars, node, element, val)
  pressure_index = 1
  return esc(:($aux_vars[$pressure_index, $node, $element] = $val))
end


struct Visitante
    visitante

    function Visitante(ast::Node)

    	function mivisitante(x::Array{Node,1},rule::Rule)
    		for c in x
		     mivisitante(c,rule)
			end
		end

		function mivisitante(x::Node,rules::Rule)
			t = typeof(x)
			rules.enter(x)
			for f in fieldnames(t)
				subarbol= getfield(x, f)
				if( (typeof(subarbol )<: Node) & !(isa(subarbol, Diana.Name) ) )
					mivisitante(subarbol,rules)
				elseif (typeof(subarbol) <: Array{Node,1})
				    mivisitante(subarbol,rules)
				end
			end
			rules.leave(x)
		end

	    function visitante(rule::Rule)
			mivisitante(ast,rule)
	    end
      new(visitante)
    end
end

#=
struct VisitanteParallel
    visitante

    function VisitanteParallel(ast::Node)

    	function mivisitante(x::Array{Node,1},rule::Rule)
    		for c in x  # paralelizacion
		     mivisitante(c,rule)
			end
		end

		function mivisitante(x::Node,rules::Rule)
			t = typeof(x)
			rules.enter(x)
			for f in fieldnames(t)  # paralelizacion
				subarbol= getfield(x, f)
				if( (typeof(subarbol )<: Node) & !(isa(subarbol, Diana.Name) ) )
					mivisitante(subarbol,rules)
				elseif (typeof(subarbol) <: Array{Node,1})
				    mivisitante(subarbol,rules)
				end
			end
			rules.leave(x)
		end

	    function visitante(rule::Rule)
			mivisitante(ast,rule)
	    end
      new(visitante)
    end
end=#


using Test
using QContinuousMeasurement
using HDF5
using Random

typedict(x) = Dict(string(fn) => getfield(x, fn) for fn ∈ fieldnames(typeof(x)))

# Opens the FileWriter
@testset "FileWriter" begin
    Ntraj = 100

    params = ModelParameters(outpoints=200)

    timevector = collect(get_time(params))

    filename = tempname() # Get a temporary filename

    @info "Temporary file: $filename"
    FI = rand(length(timevector), Ntraj)
    QFI = rand(length(timevector), Ntraj)
    xi2y = rand(length(timevector), Ntraj)
    FIstrong = rand(length(timevector), Ntraj)

    writer = FileWriter(filename, params, Ntraj, ["FI", "QFI", "xi2y", "something", "FIstrong"])

    for i = 1:Ntraj
        tuple = (FI = FI[:, i],
                 QFI = QFI[:, i],
                 xi2y = xi2y[:, i],
                 FIstrong = FIstrong[:, i],
                 extrafield = xi2y[:, i]) # This field should be ignored
        put!(writer, tuple)
    end

    close(writer)

    fid = h5open(filename, "r")

    @testset "datasets" begin
        @test read(fid["t"]) == timevector
        @test read(fid["FI"]) == FI
        @test read(fid["QFI"]) == QFI
        @test read(fid["xi2y"]) == xi2y
        @test iszero(read(fid["something"]))
        @test read(attributes(fid)["stored_traj"]) == Ntraj
        close(fid)
    end
    @testset "attrs" begin
        h5attr = h5readattr(filename, "/")
        pop!(h5attr, "stored_traj")
        @test h5attr == typedict(params)
    end

end


module WavGen

export writeToWav

# writes sound data to WAV file
# - fname : name of output file
# - data : array of sound signal
# - samplerate : number of samples per second
function writeToWav(fname :: String, data :: Array{<:Number, 1}, samplerate :: Integer)
    out =  open(fname, "w")
    outputType = Int16
    bytenum = sizeof(outputType)
    subchunk2size = length(data) * bytenum
    chunksize = 36 + subchunk2size

    write(out, "RIFF")
    write(out, Int32(chunksize))
    write(out, "WAVE")

    write(out, "fmt ")
    write(out, Int32(16)) # PCM
    write(out, Int16(1)) # linear quantization
    write(out, Int16(1)) # mono
    write(out, Int32(samplerate))
    write(out, Int32(samplerate*bytenum))
    write(out, Int16(bytenum))
    write(out, Int16(8*bytenum))

    write(out, "data")
    write(out, Int32(subchunk2size))
    write(out, Array{Int16,1}(map(floor, data)))

    close(out)
end

end


##
## Interface functions
##

function sample_init(
    rng::Union{AbstractRNG,AbstractVector{<:AbstractRNG}},
    h::Hamiltonian,
    θ::AbstractVecOrMat{<:AbstractFloat}
)
    # Ensure h.metric has the same dim as θ.
    h = update(h, θ)
    # Initial transition
    t = Transition(phasepoint(rng, θ, h), NamedTuple())
    return h, t
end

# A step is a momentum refreshment plus a transition
function step(
    rng::Union{AbstractRNG,AbstractVector{<:AbstractRNG}},
    h::Hamiltonian,
    τ::AbstractProposal,
    z::PhasePoint
)
    # Refresh momentum
    z = refresh(rng, z, h)
    # Make transition
    return transition(rng, τ, h, z)
end

adapt!(
    h::Hamiltonian,
    τ::AbstractProposal,
    adaptor::Adaptation.NoAdaptation,
    i::Int,
    n_adapts::Int,
    θ::AbstractVecOrMat{<:AbstractFloat},
    α::AbstractScalarOrVec{<:AbstractFloat}
) = h, τ, false

function adapt!(
    h::Hamiltonian,
    τ::AbstractProposal,
    adaptor::Adaptation.AbstractAdaptor,
    i::Int,
    n_adapts::Int,
    θ::AbstractVecOrMat{<:AbstractFloat},
    α::AbstractScalarOrVec{<:AbstractFloat}
)
    isadapted = false
    if i <= n_adapts
        i == 1 && Adaptation.initialize!(adaptor, n_adapts)
        adapt!(adaptor, θ, α)
        i == n_adapts && finalize!(adaptor)
        h, τ = update(h, τ, adaptor)
        isadapted = true
    end
    return h, τ, isadapted
end

"""
Progress meter update with all trajectory stats, iteration number and metric shown.
"""
function pm_next!(pm, stat::NamedTuple, i::Int, metric::AbstractMetric)
    # Add current iteration and mass matrix
    stat = (iterations=i, stat..., mass_matrix=metric)
    ProgressMeter.next!(pm; showvalues=[tuple(s...) for s in pairs(stat)])
end

"""
Simple progress meter update without any show values.
"""
simple_pm_next!(pm, stat::NamedTuple, ::Int, ::AbstractMetric) = ProgressMeter.next!(pm)

##
## Sampling functions
##

sample(
    h::Hamiltonian,
    τ::AbstractProposal,
    θ::AbstractVecOrMat{<:AbstractFloat},
    n_samples::Int,
    adaptor::Adaptation.AbstractAdaptor=Adaptation.NoAdaptation(),
    n_adapts::Int=min(div(n_samples, 10), 1_000);
    drop_warmup=false,
    verbose::Bool=true,
    progress::Bool=false,
    (pm_next!)::Function=pm_next!
) = sample(
    GLOBAL_RNG,
    h,
    τ,
    θ,
    n_samples,
    adaptor,
    n_adapts;
    drop_warmup=drop_warmup,
    verbose=verbose,
    progress=progress,
    (pm_next!)=pm_next!,
)

"""
    sample(
        rng::AbstractRNG,
        h::Hamiltonian,
        τ::AbstractProposal,
        θ::AbstractVecOrMat{T},
        n_samples::Int,
        adaptor::Adaptation.AbstractAdaptor=Adaptation.NoAdaptation(),
        n_adapts::Int=min(div(n_samples, 10), 1_000);
        drop_warmup::Bool=false,
        verbose::Bool=true,
        progress::Bool=false
    )

Sample `n_samples` samples using the proposal `τ` under Hamiltonian `h`.
- The randomness is controlled by `rng`.
    - If `rng` is not provided, `GLOBAL_RNG` will be used.
- The initial point is given by `θ`.
- The adaptor is set by `adaptor`, for which the default is no adaptation.
    - It will perform `n_adapts` steps of adaptation, for which the default is the minimum of `1_000` and 10% of `n_samples`
- `drop_warmup` controls to drop the samples during adaptation phase or not
- `verbose` controls the verbosity
- `progress` controls whether to show the progress meter or not
"""
function sample(
    rng::Union{AbstractRNG,AbstractVector{<:AbstractRNG}},
    h::Hamiltonian,
    τ::AbstractProposal,
    θ::T,
    n_samples::Int,
    adaptor::Adaptation.AbstractAdaptor=Adaptation.NoAdaptation(),
    n_adapts::Int=min(div(n_samples, 10), 1_000);
    drop_warmup=false,
    verbose::Bool=true,
    progress::Bool=false,
    (pm_next!)::Function=pm_next!
) where {T<:AbstractVecOrMat{<:AbstractFloat}}
    @assert !(drop_warmup && (adaptor isa Adaptation.NoAdaptation)) "Cannot drop warmup samples if there is no adaptation phase."
    # Prepare containers to store sampling results
    n_keep = n_samples - (drop_warmup ? n_adapts : 0)
    θs, stats = Vector{T}(undef, n_keep), Vector{NamedTuple}(undef, n_keep)
    # Initial sampling
    h, t = sample_init(rng, h, θ)
    # Progress meter
    pm = progress ? ProgressMeter.Progress(n_samples, desc="Sampling", barlen=31) : nothing
    time = @elapsed for i = 1:n_samples
        # Make a step
        t = step(rng, h, τ, t.z)
        # Adapt h and τ; what mutable is the adaptor
        tstat = stat(t)
        h, τ, isadapted = adapt!(h, τ, adaptor, i, n_adapts, t.z.θ, tstat.acceptance_rate)
        tstat = merge(tstat, (is_adapt=isadapted,))
        # Update progress meter
        if progress
            pm_next!(pm, tstat, i, h.metric)
        # Report finish of adapation
        elseif verbose && isadapted && i == n_adapts
            @info "Finished $n_adapts adapation steps" adaptor τ.integrator h.metric
        end
        # Store sample
        if !drop_warmup || i > n_adapts
            j = i - drop_warmup * n_adapts
            θs[j], stats[j] = t.z.θ, tstat
        end
    end
    # Report end of sampling
    if verbose
        EBFMI_est = EBFMI(map(s -> s.hamiltonian_energy, stats))
        average_acceptance_rate = mean(map(s -> s.acceptance_rate, stats))
        if θ isa AbstractVector
            n_chains = 1
        else
            n_chains = size(θ, 2)
            # TODO: see if there is other trick to make
            EBFMI_est = "[" * join(EBFMI_est, ", ") * "]"
            average_acceptance_rate = "[" * join(average_acceptance_rate, ", ") * "]"
        end
        @info "Finished $n_samples sampling steps for $n_chains chains in $time (s)" h τ EBFMI_est average_acceptance_rate
    end
    return θs, stats
end


# Génère un certain nombre d'instances de test
#
# TODO : MODULE LE CONSTRUCTEUR DE TYPE Instance POUR LIRE UNE CHAINE
# DANS UN DES FORMATS ACCEPTÉ
#

"""
La description d'un avion est de la forme :

#    name kind  at   E    T   L    ep    tp
plane  p1    1  55  130  156 560  10.0  10.0

ou bien
#    name kind  at  E     T   L    dt1  c1   dt2 c2    dt3 c3
plane  p1    1  55  130  156 560  -26 260.0   0 0.0    404 4040.0
"""
function instance_build_mini10()
    inst = Instance()
    inst.name = "mini10"
    inst.nb_kinds = 2
    inst.nb_planes = 10
    inst.freeze_time = 10

    inst.planes = Vector{Plane}()

    # inst.sep_mat = Matrix{Int}(undef, 2, 2) # ok
    inst.sep_mat = zeros(Int, 2, 2) # ok
    inst.sep_mat[1, 1] = 3
    inst.sep_mat[1, 2] = 15
    inst.sep_mat[2, 1] = 15
    inst.sep_mat[2, 2] = 8

    add_plane(inst, "  p1    1    55   130   156   560    10.0 10.0")
    add_plane(inst, "  p2    1   121   196   259   745    10.0 10.0")
    add_plane(inst, "  p3    2    15    90    99   511    30.0 30.0")
    add_plane(inst, "  p4    2    22    97   107   522    30.0 30.0")
    add_plane(inst, "  p5    2    36   111   124   556    30.0 30.0")
    add_plane(inst, "  p6    2    46   121   136   577    30.0 30.0")
    add_plane(inst, "  p7    2    50   125   139   578    30.0 30.0")
    add_plane(inst, "  p8    2    52   127   141   574    30.0 30.0")
    add_plane(inst, "  p9    2    61   136   151   592    30.0 30.0")
    add_plane(inst, " p10    2    86   161   181   658    30.0 30.0")

    # # Mise à jour des attributs dérivés de l'avion (juste costs pour l'instant)
    # update_costs!.(inst.planes)

    return inst
end


using StatsBase
using DataFrames
using CSV
include("SupportFunction.jl")
println("Leeeeroooy Jenkins")

industryArr = ["NoDur", "Durbl", "Manuf", "Enrgy", "HiTec", "Telcm", "Shops", "Hlth", "Utils", "Other"]
folderArr = [12, 24, 36, 48, 120, 240]

totalLog = zeros(6,10)

#Getting best data within a time period with all predictos
for a = 1:6
    folder = folderArr[a]
    for b = 1:10
        industry = industryArr[b]
        localBestClassRate = zeros(1,10)
        path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"*industry*"/"
        #path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
        cd(path)
        prediction = CSV.read(string(folder)*"-1/"*string(folder)*"_VIX_MacroTimeExpTA_Predicted.CSV", delim = ',', nullable=false)
        real = CSV.read(string(folder)*"-1/"*string(folder)*"_VIX_MacroTimeExpTA_real.CSV", delim = ',', nullable=false)
        predictionLength = 800
        for i = 1:10
            trueClassCounter = 0
            for j = 1:predictionLength
                predSign = sign(prediction[j,3+i])
                realSign = sign(real[j,4])
                if predSign == realSign
                    trueClassCounter += 1
                end
            end
            localBestClassRate[1,i] = trueClassCounter/800
        end
        totalLog[a,b] = maximum(localBestClassRate)
    end
end

#end

folder = 12
industry = "Durbl"
function logMaxes(industry, folder)
    path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"*industry*"/"
    cd(path)
    summary = CSV.read("Summary "*string(folder)*".CSV", delim = ',', nullable=false)
    classificationRate, index = findmax(summary[:,2])
    bestGamma = summary[index,6]
    ModelName = summary[index,1]
    path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"*industry*"/"*string(folder)*"-1/"
    cd(path)
    indicators = CSV.read(string(folder)*"_"*ModelName*"_Indi.CSV", delim = ',', nullable=false)
    indicatorArray = indicators[:,3+bestGamma]
    #=bestGamma = string(names(indicators)[3+bestGamma])
    if bestGamma == "0_1"
        bestGamma = 0
    else
        bestGamma = parse(Float64, bestGamma)
    end
    =#
    return bestGamma, classificationRate, ModelName, indicatorArray
end

bestGammaArr = zeros(6,10)
classificationRateArr = zeros(6,10)
ModelNameArr = Array{String}(6,10)
indicatorValues12 = zeros(1073, 10)
indicatorValues24 = zeros(1061, 10)
indicatorValues36 = zeros(1049, 10)
indicatorValues48 = zeros(1037, 10)
indicatorValues120= zeros( 965, 10)
indicatorValues240= zeros( 845, 10)

for i = 1:6
    for j = 1:10
        bestGammaArr[i,j], classificationRateArr[i,j], ModelNameArr[i,j], indicatorVals = logMaxes(industryArr[j],folderArr[i])
        if i == 1
            indicatorValues12[:,j] = indicatorVals
        end

        if i == 2
            indicatorValues24[:,j] = indicatorVals
        end

        if i == 3
            indicatorValues36[:,j] = indicatorVals
        end

        if i == 4
            indicatorValues48[:,j] = indicatorVals
        end

        if i == 5
            indicatorValues120[:,j] = indicatorVals
        end

        if i == 6
            indicatorValues240[:,j] = indicatorVals
        end
    end
end

industryArr = ["NoDur", "Durbl", "Manuf", "Enrgy", "HiTec", "Telcm", "Shops", "Hlth", "Utils", "Other"]
industryArrFile = Array{String}(1,10)
for i=1:10
    industryArrFile[1,i] = industryArr[i]
end

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"GammaInfo.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,bestGammaArr)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"ClassificationInfo.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,classificationRateArr)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"ModelNameInfo.csv", "w")
writecsv(f, industryArrFile)
writecsv(f, ModelNameArr)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators12.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues12)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators24.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues24)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators36.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues36)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators48.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues48)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators120.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues120)
close(f)

path = "/Users/SkipperAfRosenborg/Google Drive/DTU/10. Semester/Thesis/GitHubCode/Results/IndexData/LassoTest/"
f = open(path*"Indicators240.csv", "w")
writecsv(f, industryArrFile)
writecsv(f,indicatorValues240)
close(f)

println("Done")


module SignalAnalysis

using Requires
using DocStringExtensions

using SignalBase
using SignalBase.Units
export nframes, nchannels, sampletype, framerate, duration
export 𝓈

const 𝓈 = Units.s

include("signals.jl")
include("basic.jl")
include("dsp.jl")
include("tfa.jl")
include("generate.jl")
include("array.jl")
include("rand.jl")

function __init__()
  @require Plots="91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plot.jl")
  @require InteractiveViz="d14badfc-0adb-4d57-980e-37858d990fa5" include("iplot.jl")
end

end # module


#
# Copyright (c) 2021 Tobias Thummerer, Lars Mikelsons
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

module FMIFlux

using FMI
include("FMI_neural.jl")
include("FMI_plot.jl")
#include("FMI_cache.jl")
include("misc.jl")

# FMI2_neural.jl
export fmi2DoStepME, fmi2DoStepCS
export fmi2InputDoStepCSOutput
export ME_NeuralFMU, CS_NeuralFMU, NeuralFMU, NeuralFMUInputLayer, NeuralFMUOutputLayer

# FMI_neural.jl
export fmiDoStepME, fmiDoStepCS
export fmiInputDoStepCSOutput
export NeuralFMUCacheTime, NeuralFMUCacheState

# misc.jl
export mse_interpolate, transferParams!

end # module