using CUDA.CUDNN: CUDNN_BN_MIN_EPSILON, cudnnBatchNormalizationBackward,
                  cudnnBatchNormalizationForwardInference, CUDNN_BATCHNORM_SPATIAL,
                  cudnnBatchNormalizationForwardTraining

# TODO: replace with new cudnn normalization interface
# https://github.com/JuliaGPU/CUDA.jl/blob/master/lib/cudnn/normalization.jl

mutable struct BNCache
  mean
  ivar
end

BNCache() = BNCache(nothing, nothing)

@inline _wsize(x::AbstractArray{<:Any,N}) where N = ntuple(i -> i == N-1 ? size(x, N-1) : 1, N)

function batchnorm(g::Nothing, b::Nothing, x::DenseCuArray,
                   running_mean, running_var, momentum; kws...)
  affine_sz = _wsize(x)
  g = fill!(similar(x, affine_sz), 1)
  b = fill!(similar(x, affine_sz), 0)
  return batchnorm(g, b, x, running_mean, running_var, momentum; kws...)
end

# NOTE: CuDNN supports only 4D and 5D Tensors for BatchNorm Operations
# so reshape a 2D Tensor into 4D
function batchnorm(g::DenseCuArray{T}, b::DenseCuArray{T}, x::DenseCuArray{T,2},
                   running_mean, running_var, momentum; kws...) where T<:CUDNNFloat
  x = reshape(x, 1, 1, size(x, 1), size(x, 2))
  y = batchnorm(g, b, x, running_mean, running_var, momentum; kws...)
  return dropdims(y, dims = (1, 2))
end

function batchnorm(g::DenseCuArray{T}, b::DenseCuArray{T}, x::Union{DenseCuArray{T,4},DenseCuArray{T,5}},
                   running_mean, running_var, momentum; kws...) where T<:CUDNNFloat
  cudnnBNForward!(similar(x), g, b, x, running_mean, running_var, momentum; kws...)
end

function cudnnBNForward!(y::DenseCuArray{T}, g::DenseCuArray{T}, b::DenseCuArray{T}, x::DenseCuArray{T},
                        running_mean, running_var, momentum;
                        cache = nothing,
                        alpha = T(1), beta = T(0),
                        eps = T(1e-5),
                        training = true,
                        affine = true,
                        track_stats = true) where T<:CUDNNFloat
  dims = _wsize(x)
  if eps < CUDNN_BN_MIN_EPSILON
    @warn "eps $eps is too small for CuDNN, setting to CUDNN_BN_MIN_EPSILON=$CUDNN_BN_MIN_EPSILON"
    eps = CUDNN_BN_MIN_EPSILON
  end

  if running_mean === nothing || running_var === nothing
    running_mean !== running_var && throw(ArgumentError("both or neither of running_mean and running_var must be nothing"))
    if track_stats || !training
      running_mean = fill!(similar(x, dims), 0)
      running_var = fill!(similar(x, dims), 1)
    end
  end

  xd = cudnnTensorDescriptor(x)
  yd = cudnnTensorDescriptor(y)
  gd = cudnnTensorDescriptor(CUDNN_TENSOR_NCHW, cudnnDataType(T), Cint(length(dims)), dim4(dims,Val(CUDNN_TENSOR_NCHW)))

  if training
    if !track_stats
      running_mean = CU_NULL
      running_var = CU_NULL
    end

    if cache !== nothing
      mean = fill!(similar(x, dims), 0)
      ivar = fill!(similar(x, dims), 1)
    else
      mean = CU_NULL
      ivar = CU_NULL
    end

    cudnnBatchNormalizationForwardTraining(handle(), CUDNN_BATCHNORM_SPATIAL, scalingParameter(T, alpha), scalingParameter(T, beta), xd, x, yd, y, gd, g, b, momentum, running_mean, running_var, eps, mean, ivar)

    if cache !== nothing
      cache.mean = mean
      cache.ivar = ivar
    end
  else
    cudnnBatchNormalizationForwardInference(handle(), CUDNN_BATCHNORM_SPATIAL, scalingParameter(T, alpha), scalingParameter(T, beta), xd, x, yd, y, gd, g, b, running_mean, running_var, eps)
  end
  return y
end

function ∇batchnorm(g::Nothing, b::Nothing, x::DenseCuArray, dy::DenseCuArray,
                    running_mean, running_var, momentum; kws...)
  affine_sz = _wsize(x)
  g = fill!(similar(x, affine_sz), 1)
  b = fill!(similar(x, affine_sz), 0)
  return ∇batchnorm(g, b, x, dy, running_mean, running_var, momentum; kws...)
end

function ∇batchnorm(g::DenseCuArray{T}, b::DenseCuArray{T}, x::DenseCuArray{T, 2}, dy::DenseCuArray{T, 2},
            running_mean, running_var, momentum;
            kws...) where T<:CUDNNFloat
  dg, db, dx = ∇batchnorm(g, b, reshape(x, 1, 1, size(x, 1), size(x, 2)), reshape(dy, 1, 1, size(dy, 1),
                          size(dy, 2)), running_mean, running_var, momentum; kws...)
  (dg, db, dropdims(dx, dims = (1, 2)))
end

function ∇batchnorm(g::DenseCuArray{T}, b::DenseCuArray{T}, x::DenseCuArray{T}, dy::DenseCuArray{T},
                    running_mean, running_var, momentum;
                    affine=true, kws...) where T<:CUDNNFloat
  dg = similar(g)
  db = similar(b)
  dx = similar(x)
  cudnnBNBackward!(dg, g, db, dx, x, dy, running_mean, running_var, T(momentum); kws...)
  if affine
    (dg, db, dx)
  else
    # CUDNN always calculates dg and db, therefore we just have to drop them
    (nothing, nothing, dx)
  end
end

function cudnnBNBackward!(dg::DenseCuArray{T}, g::DenseCuArray{T}, db::DenseCuArray{T},
                          dx::DenseCuArray{T}, x::DenseCuArray{T}, dy::DenseCuArray{T},
                          running_mean, running_var,
                          momentum; cache = nothing, eps = T(1e-5),
                          alpha = T(1), beta = T(0),
                          dalpha = T(1), dbeta = T(0), training = true,
                          track_stats = true) where T<:CUDNNFloat
  if !track_stats
    running_mean = CU_NULL
    running_var = CU_NULL
  end

  xd = cudnnTensorDescriptor(x)
  dyd = cudnnTensorDescriptor(dy)
  dxd = cudnnTensorDescriptor(dx)
  gd = cudnnTensorDescriptor(CUDNN_TENSOR_NCHW, cudnnDataType(T), Cint(length(_wsize(x))), dim4(_wsize(x),Val(CUDNN_TENSOR_NCHW)))
  if cache !== nothing
    @debug "fetching mean and ivar from the cache"
    mean, ivar = cache.mean, cache.ivar
  else
    mean, ivar = CU_NULL, CU_NULL
  end

  if eps < CUDNN_BN_MIN_EPSILON
    @warn "eps $eps is too small for CuDNN, setting to CUDNN_BN_MIN_EPSILON=$CUDNN_BN_MIN_EPSILON"
    eps = CUDNN_BN_MIN_EPSILON
  end

  cudnnBatchNormalizationBackward(handle(), CUDNN_BATCHNORM_SPATIAL,
        scalingParameter(T, alpha), scalingParameter(T, beta), scalingParameter(T, dalpha), scalingParameter(T, dbeta),
        xd, x, dyd, dy, dxd, dx, gd, g, dg, db, eps, mean, ivar)
end


module TypeDef
export datType, dimType, fxpType, labType, matType, setType,
  indxType, predType, diagsType, paramType, fileNameType

struct datType
  region  ::Vector{Int64}
  qtr     ::Vector{Int64}
  network ::Vector{Int64}
  brand   ::Vector{Int64}
  pop     ::Vector{Float64}
  ms1d    ::Vector{Float64}
  msOO1d  ::Vector{Float64}
  price   ::Vector{Float64}
  # income  ::Vector{Float64}
  prepost ::Vector{Int64}
end

struct dimType
  region  ::Int64
  qtr     ::Int64
  network ::Int64
  brand   ::Int64
  prepost ::Int64
  product ::Int64
  person  ::Int64
  market  ::Int64
  obs     ::Int64
  obsIn   ::Int64
  prodt   ::Vector{Int64}
  θ₂ ::Int64
  θ₁ ::Int64
  θ  ::Int64
  Xᵢ ::Int64
  Dᵢ ::Int64
  νᵢ ::Int64
  Rᵢ ::Int64
end

struct fxpType
  eδ   ::Vector{Vector{Float64}}
  eδ̂   ::Vector{Vector{Float64}}
  eδ1d ::Vector{Float64}
  eδ̂1d ::Vector{Float64}
  eμ   ::Array{Matrix{Float64},1}
  eμ̂   ::Array{Matrix{Float64},1}
  ∂δ∂θ ::Array{Array{Float64,2},1}
  ∂δ̂∂θ ::Array{Array{Float64,2},1}
end

struct labType
  θ₁  ::Array{String,1}
  Σᵢᵢ ::Array{String,1} # diagonal elements of Σ
  L   ::Array{String,1} # Lower triangular Cholesky of Σ
end

struct matType
  X         ::Matrix{Float64}
  X̂         ::Vector{Matrix{Float64}}
  Xᵢ        ::Matrix{Float64}
  X̂ᵢ        ::Matrix{Float64}
  Xexo      ::Matrix{Float64}
  Z         ::Matrix{Float64}
  invPhi    ::Matrix{Float64}
  XZ        ::Matrix{Float64}
  XZPhiZXZ  ::Matrix{Float64}
  p̂         ::Vector{Float64}
  ξ         ::Matrix{Float64}
  Lξ        ::Matrix{Float64}
  Eξ        ::Matrix{Float64}
  νᵢ        ::Matrix{Float64}
  y         ::Matrix{Float64}
  dμdL      ::Vector{Array{Float64,3}}
  dμ̂dL      ::Vector{Array{Float64,3}}
  L         ::Matrix{Float64}
  Lν        ::Matrix{Float64}
  ∂δ̂∂θ      ::Matrix{Float64}
  mask      ::BitArray{1}
  maskBrand ::BitArray{2}
  income    ::Array{Vector{Float64},1}
  logIncome ::Array{Vector{Float64},1}
  Dᵢ        ::Array{Array{Float64,3},1}
  ms        ::Vector{Vector{Float64}}
end

struct setType
  region  ::Vector{Int64}
  qtr     ::Vector{Int64}
  network ::Vector{Int64}
  brand   ::Vector{Int64}
  prepost ::Vector{Int64}
  product ::Matrix{Int64}
end

mutable struct diagsType
  iter     ::Int64
  iterBLP  ::Int64
  tolBLP   ::Float64
  tolOptIV ::Float64
  gtolSimplex ::Float64
  gtolBFGS    ::Float64
end

struct indxType
  mkt   ::BitArray{2}
  diagL ::Vector{Int64}
  L     ::Matrix{Int64}
  idObs ::Vector{Int64}
  idLag ::Vector{Int64}
  expθ  ::Vector{Int64}
end

struct predType
  s   ::Array{Array{Float64,1},1}
  ms  ::Array{Array{Float64,1},1}
  mss ::Array{Array{Float64,2},1}
  num ::Array{Array{Float64,1},1}
  eμ₀ ::Array{Vector{Float64},1}
  adj ::Array{Vector{Float64},1}
  ∂S∂θ::Array{Array{Float64,2},1}
end

struct paramType
  θ₂    ::Vector{Float64}
  θ₁    ::Vector{Float64}
  seθ   ::Vector{Float64}
  Jstat ::Float64
end

struct fileNameType
  CSV1 ::String
  CSV2 ::String
  JLD1 ::String
  JLD2 ::String
  INT1 ::String
  INT2 ::String
end

end


module SpaceMethods

export AbstractSpaceMethod, AbstractDGMethod, odefun!

"""
    AbstractSpaceMethod

Supertype for spatial discretizations
"""
abstract type AbstractSpaceMethod end

"""
    AbstractDGMethod <: AbstractSpaceMethod

Supertype for discontinuous Galerkin spatial discretizations
"""
abstract type AbstractDGMethod <: AbstractSpaceMethod end

"""
    odefun!(disc::AbstractSpaceMethod, dQ, Q, t)

Evaluates the right-hand side of the spatial discretization defined by `disc` at
time `t` with state `Q`. The result is added into `dQ`. Namely, the
semi-discretization is of the form
``
    Q̇ = F(Q, t)
``
and after the call `dQ += F(Q, t)`

!!! note

    There is no generic implementation of this function. This must be
    implemented for each subtype of `AbstractSpaceMethod`
"""
odefun!(m::AbstractSpaceMethod, dQ, Q, t) =
throw(MethodError(odefun!, (m, dQ, Q, t)))

end


using DataFrames

@testset "calibrate" begin
    @testset "basic" begin
        @system SCalibrate(Controller) begin
            a => 0 ~ preserve(parameter)
            b(a) ~ accumulate
        end
        n = 10
        t, a, b = 10.0u"hr", 20, 200
        A = (0.0, 100.0)
        obs = DataFrame(time=[t], b=[b])
        p = calibrate(SCalibrate, obs, stop=n, target=:b, parameters=("SCalibrate.a" => A))
        @test p[:SCalibrate][:a] == a
        r = simulate(SCalibrate, stop=n, config=p)
        @test r[r.time .== t, :][1, :b] == b
    end

    @testset "unit" begin
        @system SCalibrateUnit(Controller) begin
            a => 0 ~ preserve(parameter, u"m/hr")
            b(a) ~ accumulate(u"m")
        end
        n = 10
        t, a, b = 10.0u"hr", 20u"m/hr", 200u"m"
        #FIXME: parameter range units are just ignored
        A = [0.0, 100.0]u"m/hr"
        obs = DataFrame(time=[t], b=[b])
        p = calibrate(SCalibrateUnit, obs, stop=n, target=:b, parameters=("SCalibrateUnit.a" => A))
        @test p[:SCalibrateUnit][:a] == Cropbox.deunitfy(a)
        r = simulate(SCalibrateUnit, stop=n, config=p)
        @test r[r.time .== t, :][1, :b] == b
    end

    @testset "config" begin
        @system SCalibrateConfig(Controller) begin
            a => 0 ~ preserve(parameter)
            w => 1 ~ preserve(parameter)
            b(a, w) => w*a ~ accumulate
        end
        n = 10
        t, a, b = 10.0u"hr", 20, 200
        w1, w2 = 1, 2
        A = (0.0, 100.0)
        obs = DataFrame(time=[t], b=[b])
        params = :SCalibrateConfig => :a => A
        p1 = calibrate(SCalibrateConfig, obs, stop=n, target=:b, config=(:SCalibrateConfig => :w => w1), parameters=params)
        @test p1[:SCalibrateConfig][:a] == a/w1
        p2 = calibrate(SCalibrateConfig, obs, stop=n, target=:b, config=(:SCalibrateConfig => :w => w2), parameters=params)
        @test p2[:SCalibrateConfig][:a] == a/w2
    end

    @testset "configs as index" begin
        @system SCalibrateConfigsIndex(Controller) begin
            a => 0 ~ preserve(parameter)
            w => 1 ~ preserve(parameter)
            b(a, w) => w*a ~ accumulate
        end
        n = 10
        t, w, b = [10.0u"hr", 10.0u"hr"], [1, 2], [100, 200]
        A = (0.0, 100.0)
        obs = DataFrame(; time=t, w, b)
        configs = [
            :SCalibrateConfigsIndex => :w => 1,
            :SCalibrateConfigsIndex => :w => 2,
        ]
        index = [:time => "context.clock.time", :w]
        target = :b
        params = :SCalibrateConfigsIndex => :a => A
        p = calibrate(SCalibrateConfigsIndex, obs, configs; stop=n, index, target, parameters=params)
        @test p[:SCalibrateConfigsIndex][:a] == 10
    end
end


using ArcadeLearningEnvironment

# For this example you need to obtain the Seaquest ROM file from
# https://atariage.com/system_items.html?SystemID=2600&ItemTypeID=ROM

episodes = 50

ale = ALE_new()
rom = string(ENV["ATARI_DIR"], "SEAQUEST.BIN")
println(rom)
loadROM(ale, rom)

S = Array{Int}(undef, episodes)
TR = Array{Float64}(undef, episodes)
for ei = 1:episodes
    ctr = 0.0

    fc = 0
    while game_over(ale) == false
        actions = getLegalActionSet(ale)
        ctr += act(ale, actions[rand(1:length(actions))])
        fc += 1
    end
    reset_game(ale)
    println("Game $ei ended after $fc frames with total reward $(ctr).")

    S[ei] = fc
    TR[ei] = ctr
end
ALE_del(ale)


aligned, aligned_lm, reco = align_principal(images, lms_on_surface, 192)

resized, resized_lm, scales = resize_relevant(aligned, aligned_lm, 128)
aligned = nothing

images1, imnames1 = load_imgs("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/bonobo_volumes_large", (256,256,256), true)
images2, imnames2 = load_imgs("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/chimp_volumes_large", (256,256,256), true)
images = cat(images1, images2, dims=4)
images = Float32.(images)
images1 = nothing
images2 = nothing

landmark_data = read_landmarks("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/bonobo_volumes_large", 22, "@1")
landmark_data2 = read_landmarks("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/chimp_volumes_large", 22, "@1")
landmarks = cat(landmark_data, landmark_data2, dims=2)./10

X_train, X_test, y_train, y_test = regular_train_test_split_2d(images, landmarks)

X_train, y_train = (mirror_vol(X_train, y_train))

X_train = Float32.(X_train)

flip1, lm_flip1 = flip_3D(X_train, y_train)
flip2, lm_flip2 = flip_volume_front(flip1, lm_flip1)
flip3, lm_flip3 = flip_volume_side(flip2, lm_flip2)

rot, lm_rot = rotate_volumes(flip2, lm_flip2, 10)
rot2, lm_rot2 = rotate_volumes(X_train, y_train, 20)
rot3, lm_rot3 = rotate_volumes(flip3, lm_flip3, -15)

jit, lm_jit = jitter_3D(flip1, lm_flip1, 10)
jit2, lm_jit2 = jitter_3D(X_train, y_train, 10)
jit3, lm_jit3 = jitter_3D(flip3, lm_flip3, 10)
jit4, lm_jit4 = jitter_3D(rot2, lm_rot2, 10)

using ImageView
ImageView.imshow(X_test)

complete1 = depth_map_all_sides(X_train)
X_train=nothing
complete2 = depth_map_all_sides(flip1)
flip1=nothing
complete3 = depth_map_all_sides(flip2)
flip2=nothing
complete4 = depth_map_all_sides(flip3)
flip3=nothing
complete5 = depth_map_all_sides(rot)
rot=nothing
complete6 = depth_map_all_sides(rot2)
rot2=nothing
complete7 = depth_map_all_sides(rot3)
rot3=nothing
complete8 = depth_map_all_sides(jit)
jit=nothing
complete9 = depth_map_all_sides(jit2)
jit2=nothing
complete10 = depth_map_all_sides(jit3)
jit3=nothing
complete11 = depth_map_all_sides(jit4)

X_train = cat(complete1, complete2, complete3, complete4, complete5, complete6,
    complete7, complete8, complete9, complete10, complete11, dims=4)
y_train = cat(y_train, lm_flip1, lm_flip2, lm_flip3, lm_rot, lm_rot2, lm_rot3, lm_jit, lm_jit2, lm_jit3, lm_jit4, dims=2)

X_train = image_gradients(X_train)
# make depthmaps of the testing data
X_test = depth_map_all_sides(X_test)
X_test = image_gradients(X_test)

# define the cost function
cost(x, y) = sum((model(x)-y).^2)|>gpu

#define the model
model = Flux.mapleaves(cu, AutoLandmarking.vgg19)

# define the trainingrate and optimiser
opt = Flux.ADAM(0.000015)

# redefinition of the dropout function to work with testmode!() since this
# functionality is not working at the moment. This step only needs to be done
# if you wish to perform uncertainty estimation using the dropout method (AutoLM.response_distribution())
# after training.
using Random
function Flux.dropout(x, p; dims = :)
    q = 1 - p
    y = rand!(similar(x, Flux._dropout_shape(x, dims)))
    y .= Flux._dropout_kernel.(y, p, q)
    x .* y
end

import Zygote
Zygote.@adjoint function Flux.dropout(x, p; dims = :)
   q = 1 - p
   y = rand!(similar(x, Flux._dropout_shape(x, dims)))
   y .= Flux._dropout_kernel.(y, p, q)
   return x .* y, Δ -> (Δ .* y, nothing)
end
Zygote.refresh()

function run_model(modell, X, y)
  train_data = Flux.mapleaves(cu, X)
  train_labels = Flux.mapleaves(cu, y)
  dataset = Flux.Data.DataLoader(train_data, train_labels, batchsize = 4, shuffle=true)
  Flux.train!(cost, params(model), dataset, opt)
  testmode!(model)
  cosima = cost_whole_data_2D(train_data, train_labels, cost)
  testmode!(model, false)
  return cosima
end

# create lists to store metrics
accs = []
costs = []

# run the training for 300 epochs. batches of 128 will be loaded onto the GPU,
# which will be further subdevided into minibatches of 4 (as defined in AutoML.run_model).
for i in 1:150
  costr = 0
  for j in 1:128:size(X_train, 4)
    if j+127 > size(X_train, 4)
      train_data = X_train[:,:,:,j:end]
      train_labels = y_train[:,j:end]
    else
      train_data = X_train[:,:,:,j:j+127]
      train_labels = y_train[:,j:j+127]
    end
    cosima = run_model(model, train_data, train_labels)
    costr = costr + cosima
    train_data = nothing
    train_labels = nothing
    # GC.gc()
  end
  push!(costs, costr)
  println("epoch ", i, " finished")
  println(costr)
  testmode!(model)
  acc2, max1, min1 = AutoLandmarking.avg_accuracy_per_point(model, gpu(X_test[:,:,:,:]), gpu(y_test[:,:]), 3)
  testmode!(model, false)
  println("median deviation per point on testing dataset: ", acc2, "with maximum", max1)
  println("median sum is ", sum(acc2))
  push!(accs, sum(acc2))
  if sum(acc2)[1]<23.0
    break
  end
end

#solve issue with arrays inside vector
aks = []
for i in accs
  push!(aks, i[1])
end

# check what the best accuracy is
minimum(aks)

#plot the development of cost and accuracy
import Plots
Plots.plot(aks, legend=:topright, label="sum of deviations per point", color= :red, xlabel="epochs", ylabel = "acuracy")
plt = Plots.plot!(Plots.twinx(), costs, label="training loss", legend=:topleft, ylabel = "loss")
Plots.png(plt, "C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/mandibles_training.png")

# Data analysis mandibles ==================
response = cpu(predict_set(gpu(X_test), model))
X_train2, X_test2, y_train2, y_test2 = regular_train_test_split_2d(images, landmarks)
on_vol = AutoLandmarking.landmark_to_surface(X_test2, response, 6)
on_vol_no4 = on_vol[:, cat(1:3,5:19,dims=1)]
response_no4 = response[:, cat(1:3,5:19,dims=1)]

function print_costs(y, resp)
  costs=[]
  for i in 1:size(resp,2)
    dev = sum((y[:,i] .- resp[:,i]) .^2)
    push!(costs, dev)
    println(i, "    ", dev)
  end
  return costs
end

costs_resp = print_costs(y_test_no4, response_no4)

# puting on volume makes it worse

three_d_train = AutoLandmarking.to_3d_array(y_train2)
three_d_test = AutoLandmarking.to_3d_array(response_no4)

aligned_train = AutoLandmarking.align_all(three_d_train)
aligned_test = AutoLandmarking.align_all(three_d_test)

mean_train, stdevs = mean_shape(aligned_train)

distances = proc_distance(mean_train, aligned_test, stdevs)
dists = []
for i in 1:18
  push!(dists, distances[i])
end

for i in 1:18
  println("ind $i cost is ", costs_resp[i], "  and dist is ", dists[i])
end

uncertainties = []
for i in 1:18
  means, stddev = AutoLandmarking.response_distribution(model, gpu(X_test_no4[:,:,:,i:i]), gpu(y_test_no4[:,i:i]), 100)
  println("$i has uncevertaty", sum(stddev), "  and cost  ", costs_resp[i])
  push!(uncertainties, sum(stddev))
end
# 6 are over 40

dists_conf = dists[findall(x->x<4,dists)]
resp_conf = response_no4[:,findall(x->x<4,dists)]
volumes_conf = X_test2[:,:,:,findall(x->x<4,dists)]
y_test_conf = y_test_no4[:,findall(x->x<4,dists)]
X_test_conf = X_test_no4[:,:,:,findall(x->x<4,dists)]
AutoLandmarking.change_values!(y_test_conf, 26.0, 0, >)
ImageView.imshow(X_test_conf)

is_bonobo = zeros(97,1)
is_bonobo[1:37,1] .= 1
imnames = cat(imnames1, imnames2, dims=1)
test_names = []
train_names = []
is_bonobo_train = []
is_bonobo_test = []
for i in 1:97
  if i % 5 == 0
    push!(test_names, imnames[i])
    push!(is_bonobo_test, is_bonobo[i])
  else
    push!(train_names, imnames[i])
    push!(is_bonobo_train, is_bonobo[i])
  end
end

is_bonobo_test_no4 = is_bonobo_test[cat(1:3,5:19,dims=1)]
is_bonobo_conf = is_bonobo_test_no4[findall(x->x<41,dists)]
test_names = test_names[cat(1:3,5:19,dims=1)]
names_conf = test_names[findall(x->x<41,dists)]
AutoLandmarking.array_to_lm_file("C:/Users/immanueldiener/Desktop/Master/worst_pred.landmarkAscii", response_no4[:,18])

import ImageView
using DelimitedFiles
using Statistics

y_test_no4 = y_test[:,cat(1:3,5:19,dims=1)]
X_test_no4 = X_test[:,:,:,cat(1:3,5:19,dims=1)]
ImageView.imshow(X_test_no4)

ImageView.imshow(depth_map_all_sides(X_test_no4))
test_names[14]
all_devs = zeros(14, 4)
devs_per_point = zeros(14*22, 2)

AutoLandmarking.change_values!(y_test_no4, 26.0, 0.0, >)

testmode!(model)
for i in 1:14
  acc2, max1, min1 = AutoLandmarking.avg_accuracy_per_point(model, gpu(X_test_conf[:,:,:,i:i]), gpu(y_test_conf[:,i:i]), 3)
  all_devs[i,1] = sum(acc2)
  all_devs[i,2] = dists_conf[i]
  all_devs[i,3] = i
  if i in findall(x->x<41,dists)
    all_devs[i,4] = 1
  end
end
minimum(all_devs[:,1])
all_devs
using Distances
for i in 1:14
  for j in 1:22
    devs_per_point[(i-1)*22+j, 1] = euclidean(resp_conf[j*3-2:j*3, i], y_test_conf[j*3-2:j*3, i])
    devs_per_point[(i-1)*22+j, 2] = j
    if j == 2
      println(i, "   ", euclidean(resp_conf[j*3-2:j*3, i], y_test_conf[j*3-2:j*3, i]))
    end
  end
end

devs_per_point

AutoLandmarking.change_values!(y_test, 25.8, 25.6, >)
test_names[18]

out = hcat(all_devs, names_conf)

writedlm("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/devs_per_ind.txt", out)
writedlm("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/devs_per_point.txt", devs_per_point)

# timing GPU : 12000/min, CPU: 110/min
@time begin
  predict_set(X_train[:,:,:,1:50], model_cpu)
end

# export of coordinates
out_test = hcat(names_conf, is_bonobo_conf, resp_conf')
out_train = hcat(train_names, is_bonobo_train, y_train')
out_all = vcat(out_train, out_test)
is_train = zeros(92, 1)
is_train[1:78] .= 1
output_all = hcat(out_all[:,1:2], is_train, out_all[:,3:end])

writedlm("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/coords_pred.txt", out_test)
writedlm("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/coords_train.txt", out_train)
writedlm("C:/Users/immanueldiener/Desktop/Master/master_data/mandibles/coords_all.txt", output_all)
# ===========================================

using BSON
testmode!(model)
model_cpu = cpu(model)
BSON.@save "C:/Users/immanueldiener/Desktop/Master/master_data/model_mandibles.bson" model_cpu
BSON.@load "C:/Users/immanueldiener/Desktop/Master/master_data/model_mandibles.bson" model_cpu
model = gpu(model_cpu)


localIP =  Sockets.localhost
DG = dg_new(localIP,8888,"QRS")
DG.data["QPoint"].data[2]("0","500","500")
Int32.(DG.data["WidthQRS"].data[1]("0","10",""))

info1 = helloIBox.findnode(tree, "ClassQRS") #запрос группы
mask =helloIBox.getMask(helloIBox.parseType(info1["attrs"]))

a,b,c,d = helloIBox.getSegBegsType(DG)

using StructArrays
newSeg = StructArray(ibeg = [10], iend = [20], type = [1])
helloIBox.addSeg!(DG,newSeg,"simpleAdd")

DG = dg_new(localIP,8888,"QRS")
allObj = Dict{String,Any}()
allObj["history"] = []
allObj["state"] = 0
allObj["dataStorage"] = Dict{String,Any}()
allObj["dataStorage"]["QRS"] = DG
command1Path = joinpath(Base.@__DIR__, "files","oxy115829.002","command_FT.json")
command_FT= String(read(command1Path))
helloIBox.parseCommand(localIP, 8888, allObj,command_FT)
@test allObj["dataStorage"]["QRS"].result.QPoint[1:2]==[10,30]
@test allObj["dataStorage"]["QRS"].result.WidthQRS[1:2]==[10,15]

#добавление с расширением границ, если тип одинаковый
command1Path = joinpath(Base.@__DIR__, "files","oxy115829.002","command_FT_addRaw.json")
command_FT= String(read(command1Path))
helloIBox.parseCommand(localIP, 8888, allObj,command_FT)
@test allObj["dataStorage"]["QRS"].result.QPoint[1:2]==[10,50]
@test allObj["dataStorage"]["QRS"].result.WidthQRS[1:2]==[35,5]
ibeg =  DG.data[DG.ibegdata].data[1]("0","","")
#удаление сегента
command1Path = joinpath(Base.@__DIR__, "files","oxy115829.002","command_FT_delete.json")
command_FT= String(read(command1Path))
helloIBox.parseCommand(localIP, 8888, allObj,command_FT)
@test allObj["dataStorage"]["QRS"].result.QPoint[1:2]==[101,221]
@test allObj["dataStorage"]["QRS"].result.WidthQRS[1:2]==[23,21]


import GitHub
import Dates
import Plots

if length(ARGS) <= 2
    return
end

owner = ARGS[1]
auth = GitHub.authenticate(ENV["GITHUB_TOKEN"])

params = Dict("type" => "public", "sort" => "created")
repos, next = GitHub.repos(owner; auth = auth, params = params)

range = Dict()
date = floor(Dates.now() + Dates.Month(1), Dates.Month)
for repo in repos
    if date > repo.created_at
       global date -= Dates.Month(1)
    end
    if haskey(range, date)
        push!(range[date], repo)
    else
        range[date] = [repo]
    end
end

arr = collect(range)
sort!(arr, by = x -> x.first)

count = map(x -> Pair(x.first, length(x.second)), arr)
println(count)

x = map(x -> x.first, count)
y = map(x -> x.second, count)

plot = Plots.plot(x, y)
bar = Plots.bar(rand(50))

Plots.png(plot, "plot.png")
Plots.png(bar, "bar.png")


fm1 = lmm(Yield ~ 1 + (1|Batch), ds)
fm1w = lmm(Yield ~ 1 + (1|Batch), ds; wt = ones(size(ds, 1)))

@test size(fm1.A) == (3, 3)
@test size(fm1.wttrms) == (3,)
@test size(fm1.R) == (3, 3)
@test size(fm1.Λ) == (1,)
@test lowerbd(fm1) == zeros(1)
@test fm1[:θ] == ones(1)

fm1[:θ] = [0.713]
@test objective(fm1) ≈ 327.34216280955366
MixedModels.describeblocks(IOBuffer(), fm1)

fit!(fm1);
@test_approx_eq_eps objective(fm1) 327.3270598811428 1e-3
@test_approx_eq_eps fm1[:θ] [0.752580] 1.e-5
@test_approx_eq_eps deviance(fm1) 327.32705988 1.e-3
@test_approx_eq_eps AIC(fm1) 333.3270598811394 1.e-3
@test_approx_eq_eps BIC(fm1) 337.5306520261259 1.e-3
@test fixef(fm1) ≈ [1527.5]
@test MixedModels.fixef!(zeros(1),fm1) ≈ [1527.5]
@test coef(fm1) ≈ [1527.5]
@test cond(fm1) == ones(1)
cm = coeftable(fm1)
@test size(cm.mat) == (1,3)
@test MixedModels.fnames(fm1) == [:Batch]
@test model_response(fm1) == convert(Vector,ds[:Yield])
@test abs(sum(ranef(fm1,true)[1])) < 1.e-5

@test_approx_eq_eps logdet(fm1) 8.06014522999825 1.e-3
@test_approx_eq_eps varest(fm1) 2451.2501089607676 1.e-3
@test_approx_eq_eps pwrss(fm1) 73537.50326882303 1.e-1
@test_approx_eq_eps stderr(fm1) [17.69455188898009] 1.e-4

vc = VarCorr(fm1)
show(IOBuffer(), vc)
@test vc.s == sdest(fm1)
srand(1234321)
simulate!(fm1)   # changes the fit
@test_approx_eq_eps deviance(fm1) 339.0218639362958 1.e-3
bootstrap(fm1,1,(i,x) -> fixef(x)) # bootstrap restores the fit
@test_approx_eq_eps deviance(fm1) 339.0218639362958 1.e-3

fm2 = lmm(Yield ~ 1 + (1|Batch),ds2)
@test lowerbd(fm2) == zeros(1)
#redirect_stdout(IOBuffer())
fit!(fm2,true)
#redirect_stdout()
show(IOBuffer(), fm2)
@test abs(fm2[:θ][1]) < 1.0e-9
@test objective(fm2) ≈ 162.87303665382575
@test abs(std(fm2)[1][1]) < 1.0e-9
@test std(fm2)[2] ≈ [3.653231351374652]
@test stderr(fm2) ≈ [0.6669857396443261]
@test coef(fm2) ≈ [5.6656]
@test logdet(fm2) ≈ 0.0
refit!(fm2,convert(Vector,ds[:Yield]))

fm3 = lmm(Reaction ~ 1+Days + (1+Days|Subject),slp)
@test lowerbd(fm3) == [0.,-Inf,0.]
@test isa(fm3.A[1,1],MixedModels.HBlkDiag{Float64})
@test size(fm3.A[1,1]) == (36,36)
@test fm3.A[1,1][1,1] == 10.
@test fm3.A[1,1][6,1] == 0.
@test_throws BoundsError size(fm3.A[1,1],0)
@test size(fm3.A[1,1],1) == 36
@test full(fm3.A[1,1])[1:2,1:2] == reshape([10.,45,45,285],(2,2))

fit!(fm3)

@test objective(fm3) ≈ 1751.9393444647046
@test_approx_eq_eps fm3[:θ] [0.9292213074888169,0.01816838485113137,0.22264487095998978] 1.e-6
@test pwrss(fm3) ≈ 117889.46144025437
@test_approx_eq_eps logdet(fm3) 73.90322021999222 1e-3
@test_approx_eq_eps stderr(fm3) [6.632257721914501,1.5022354739749826]  1.e-4
@test coef(fm3) ≈ [251.40510484848477,10.4672859595959]
@test fixef(fm3) ≈ [251.40510484848477,10.4672859595959]
@test_approx_eq_eps stderr(fm3) [6.632246393963571,1.502190605041084] 1.e-2
@test_approx_eq_eps std(fm3)[1] [23.780468100188497,5.716827903196682] 1.e-2
@test_approx_eq_eps logdet(fm3) 73.90337187545992 1.e-3
@test_approx_eq diag(cor(fm3)[1]) ones(2)
@test_approx_eq_eps cond(fm3) [4.1752507630514915] 1.e-4
@test loglikelihood(fm3) ≈ -875.9696722323523
@test eltype(fm3.wttrms[1]) === Float64
const u3 = ranef(fm3,true)
@test length(u3) == 1
@test size(u3[1]) == (2,18)
@test_approx_eq_eps u3[1][1,1] 3.030300122575336 1.e-3
const b3 = ranef(fm3)
@test length(b3) == 1
@test size(b3[1]) == (2,18)
@test_approx_eq_eps b3[1][1,1] 2.815819441982976 1.e-3

fm4 = lmm(Reaction ~ Days + (1|Subject) + (0+Days|Subject), slp);
@test size(fm4) == (180,2,36,2)
@test fm4[:θ] == ones(2)
@test lowerbd(fm4) == zeros(2)

fit!(fm4);

@test_approx_eq_eps deviance(fm4) 1752.0032551398835 1.e-3
@test_approx_eq_eps objective(fm4) 1752.0032551398835 1.e-3
@test_approx_eq coef(fm4) [251.40510484848585,10.467285959595715]
@test_approx_eq fixef(fm4) [251.40510484848585,10.467285959595715]
@test_approx_eq_eps stderr(fm4) [6.707710260366577,1.5193083237479683] 1.e-3
@test_approx_eq_eps fm4[:θ] [0.9458106880922268,0.22692826607677266] 1.e-4
@test std(fm4)[1] ≈ [24.171449463289047]
@test std(fm4)[2] ≈ [5.799379721123582]
@test std(fm4)[3] ≈ [25.556130034081047]
@test_approx_eq_eps logdet(fm4) 74.46952585564611 1.e-3

tbl = MixedModels.lrt(fm4,fm3)

@test_approx_eq_eps tbl[:Deviance] [1752.0032551398835,1751.9393444636157] 1e-3
@test tbl[:Df] == [5,6]

simulate!(fm3)  # to test one of the unscaledre methods

fm5 = lmm(Diameter ~ (1|Plate) + (1|Sample), pen);
@test size(fm5) == (144,1,30,2)
@test fm5[:θ] == ones(2)
@test lowerbd(fm5) == zeros(2)

fit!(fm5)

@test_approx_eq_eps objective(fm5) 332.18834867227616 1.e-3
@test_approx_eq_eps coef(fm5) [22.97222222222222] 1.e-3
@test_approx_eq_eps fixef(fm5) [22.97222222222222] 1.e-3
@test coef(fm5)[1] ≈ mean(pen[:Diameter])
@test_approx_eq_eps stderr(fm5) [0.7445960346851368] 1.e-4
@test_approx_eq_eps fm5[:θ] [1.5375772376554968,3.219751321180035] 1.e-3
@test_approx_eq_eps std(fm5)[1] [0.8455645948223015] 1.e-4
@test_approx_eq_eps std(fm5)[2] [1.770647779277388] 1.e-4
@test_approx_eq_eps varest(fm5) 0.3024263987592062 1.e-4
@test_approx_eq_eps logdet(fm5) 95.74614821367786 1.e-3
@test length(ranef(fm5)) == 2

fm6 = lmm(Strength ~ (1|Sample) + (1|Batch), psts);
@test size(fm6) == (60,1,40,2)
@test fm6[:θ] == ones(2)
@test lowerbd(fm6) == zeros(2)

fit!(fm6);

@test_approx_eq_eps objective(fm6) 247.99446586289676 1.e-3
@test_approx_eq_eps coef(fm6) [60.05333333333329] 1.e-3
@test_approx_eq_eps fixef(fm6) [60.05333333333329] 1.e-3
@test_approx_eq_eps stderr(fm6) [0.6421359883527029] 1.e-4
@test_approx_eq_eps fm6[:θ] [3.5268858714382905,1.3299230213750168] 1.e-3
@test_approx_eq_eps std(fm6)[1] [2.904069002535747] 1.e-3
@test_approx_eq_eps std(fm6)[2] [1.095070371687089] 1.e-4
@test_approx_eq_eps std(fm6)[3] [0.8234088395243269] 1.e-4
@test_approx_eq_eps varest(fm6) 0.6780020742644107 1.e-4
@test_approx_eq_eps logdet(fm6) 101.0381339953986 1.e-3


##### Get prognostic variable list

import ..BalanceLaws: prognostic_vars

prognostic_vars(::DryModel) = ()
prognostic_vars(::EquilMoist) = (TotalMoisture(),)
prognostic_vars(::NonEquilMoist) =
    (TotalMoisture(), LiquidMoisture(), IceMoisture())

prognostic_vars(::NoPrecipitation) = ()
prognostic_vars(::RainModel) = (Rain(),)
prognostic_vars(::RainSnowModel) = (Rain(), Snow())

prognostic_vars(::NoTracers) = ()
prognostic_vars(::NTracers{N}) where {N} = (Tracers{N}(),)

prognostic_vars(m::AtmosModel) = (
    Mass(),
    Momentum(),
    Energy(),
    prognostic_vars(m.moisture)...,
    prognostic_vars(m.precipitation)...,
    prognostic_vars(m.tracers)...,
    prognostic_vars(m.turbconv)...,
)


using Dagger
import Dagger:DomainBlocks

import Dagger: DArray, chunks

function onchunks(X::Indexing)
    # TODO: handle IndexConsts
    idx = X.idx
    Indexing(map(delayed(x -> Indexing(x, idx)), chunks(X.array)), idx)
end

function onchunks(X::Map)
    f = X.f
    Map(delayed((x...) -> Map(f, x)), map(onchunks, X.arrays))
end

function arrayop!{D<:DArray}(::Type{D}, t::Assign)
    lhs = onchunks(t.lhs)
    rhs = onchunks(t.rhs)

    idx = t.lhs.idx
    combine_chunks = delayed() do a, b
        Map(t.reducefn, (a, b))
    end
    ct = Assign(lhs, rhs, combine_chunks, delayed(()->nothing))
    cs = arrayop_treereduce(ct)
    f = delayed() do l, r
        arrayop!(Assign(l, r, t.reducefn, t.empty))
    end
    t.lhs.array.chunks = map(f, onchunks(t.lhs).array, cs)
    t.lhs.array
end

@generated function arrayop_treereduce{L,R,F,E}(op::Assign{L,R,F,E})

    rspaces = index_spaces(:(op.rhs), R)
    lspaces = index_spaces(:(op.lhs), L)
    expr = kernel_expr(:(op.rhs), R)

    reduceddims = setdiff(keys(rspaces), keys(lspaces))
    for sym in reduceddims
        spaces = rspaces[sym]
        T,dim,nm = first(spaces)
        valx = gensym("valx")
        valy = gensym("valy")
        expr = quote
            Dagger.treereduce(indices($nm, $dim)) do x, y
                x, y
                $sym = x

                $valx = $expr
                $sym = y
                $valy = $expr
                op.reducefn($valx, $valy)
            end
        end
    end

    lhs_expr = kernel_expr(:(op.lhs), L) # :() will be ignored
    expr = :($lhs_expr = $expr)

    checks = :()
    for (sym, spaces) in lspaces
        if length(spaces) > 1
            # check dimensions for equality
            equal_dims = [:(size($(d[3]), $(d[2]))) for d in spaces]
            checks = :($checks; @assert allequal($(equal_dims...),))
        end
        T,dim,nm = first(spaces)
        expr = quote
            for $sym = indices($nm, $dim)
                $expr
            end
        end
    end

    :($checks; $expr; op.lhs.array)
end

function Base.indices(x::DArray)
    Dagger.domainchunks(x)
end

function Base.indices(x::DArray, i)
    idxs = indices(x)
    Dagger.DomainBlocks((idxs.start[i],), (idxs.cumlength[i],))
end

function allocarray{T,N}(::Type{DArray{T,N}}, default, idxs...)
    dmnchunks = DomainBlocks(map(i->1, idxs),
                             map(i->isa(i, DomainBlocks) ?
                                 i.cumlength[1] : (length(i),), idxs))

    chnks = map(delayed(subd -> allocarray(Array{T,1}, default, size(subd))), dmnchunks)
    sz = map((x,y)->x-y+1, map(last, dmnchunks.cumlength), dmnchunks.start)
    dmn = ArrayDomain(map(x->1:x, sz))
    DArray{T, length(idxs)}(dmn, dmnchunks, chnks)
end


# Unit tests for judiRHS and judiWavefield (without PDE solves)
# Philipp Witte (pwitte.slim@gmail.com)
# May 2018
#

using JUDI.TimeModeling, SegyIO, Test, LinearAlgebra

# Example structures

example_info(; n=(120,100), nsrc=2, ntComp=1000) = Info(prod(n), nsrc, ntComp)
example_model(; n=(120,100), d=(10f0, 10f0), o=(0f0, 0f0), m=randn(Float32, n)) = Model(n, d, o, m)

function example_rec_geometry(; nsrc=2, nrec=120)
    xrec = range(50f0, stop=1150f0, length=nrec)
    yrec = 0f0
    zrec = range(50f0, stop=50f0, length=nrec)
    return Geometry(xrec, yrec, zrec; dt=4f0, t=1000f0, nsrc=nsrc)
end

function example_src_geometry(; nsrc=2)
    xrec = range(100f0, stop=1000f0, length=nsrc)
    yrec = 0f0
    zrec = range(50f0, stop=50f0, length=nsrc)
    return Geometry(xrec, yrec, zrec; dt=4f0, t=1000f0, nsrc=nsrc)
end

########################################################### judiRHS ####################################################

@testset "judiRHS Unit test" begin

    # Constructor
    nsrc = 2
    info = example_info(nsrc=nsrc)
    rec_geometry = example_rec_geometry(nsrc=nsrc)
    data = Array{Array}(undef, nsrc)
    for j=1:nsrc
        data[j] = randn(Float32, rec_geometry.nt[j], length(rec_geometry.xloc[j]))
    end
    rhs = judiRHS(info, rec_geometry, data)

    @test isequal(typeof(rhs), judiRHS{Float32})
    @test isequal(rhs.geometry, rec_geometry)

    # conj, transpose, adjoint
    @test isequal(size(rhs), size(conj(rhs)))
    @test isequal(reverse(size(rhs)), size(transpose(rhs)))
    @test isequal(reverse(size(rhs)), size(adjoint(rhs)))

    # +, -
    info = example_info(nsrc=nsrc)
    rec_geometry = example_rec_geometry(nsrc=nsrc)
    src_geometry = example_src_geometry(nsrc=nsrc)
    data1 = Array{Array}(undef, nsrc)
    data2 = Array{Array}(undef, nsrc)
    for j=1:nsrc
        data1[j] = randn(Float32, rec_geometry.nt[j], length(rec_geometry.xloc[j]))
        data2[j] = randn(Float32, src_geometry.nt[j], length(src_geometry.xloc[j]))
    end
    rhs1 = judiRHS(info, rec_geometry, data1)
    rhs2 = judiRHS(info, src_geometry, data2)

    rhs_sum = rhs1 + rhs2
    rhs_sub = rhs1 - rhs2

    @test isequal(size(rhs_sum), size(rhs1))
    @test isequal(size(rhs_sub), size(rhs1))

    @test isequal(length(rhs_sum.geometry.xloc[1]), length(rhs1.geometry.xloc[1]) + length(rhs2.geometry.xloc[1]))
    @test isequal(length(rhs_sub.geometry.xloc[1]), length(rhs1.geometry.xloc[1]) + length(rhs2.geometry.xloc[1]))

    @test isequal(size(rhs_sum.data[1])[2], size(rhs1.data[1])[2] + size(rhs2.data[1])[2])
    @test isequal(size(rhs_sub.data[1])[2], size(rhs1.data[1])[2] + size(rhs2.data[1])[2])

    # get index
    rhs_sub = rhs[1]
    @test isequal(rhs_sub.info.nsrc, 1)
    @test isequal(typeof(rhs_sub.geometry), GeometryIC)
    @test isequal(typeof(rhs.data), Array{Array, 1})
    @test isequal(length(rhs_sub), Int(length(rhs)/2))

    rhs_sub = rhs[1:2]
    @test isequal(rhs_sub.info.nsrc, 2)
    @test isequal(typeof(rhs_sub.geometry), GeometryIC)
    @test isequal(typeof(rhs.data), Array{Array, 1})
    @test isequal(length(rhs_sub), length(rhs))

end

######################################################### judiWavefield ################################################


@testset "ThermalGen data misspecification" begin
    # See https://discourse.julialang.org/t/how-to-use-test-warn/15557/5 about testing for warning throwing
    warn_message = "The data doesn't include devices of type ThermalStandard, consider changing the device models"
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)
    c_sys5_re_only = build_system("c_sys5_re_only")
    op_problem =
        OperationsProblem(TestOpProblem, DCPPowerModel, build_system("c_sys5_re_only"))
    @test_logs (:warn, warn_message) construct_device!(op_problem, :Thermal, model)
end

################################### Unit Commitment tests ##################################
@testset "Thermal UC With DC - PF" begin
    bin_variable_names = [
        PSI.variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.variable_name(PSI.START, PSY.ThermalStandard),
        PSI.variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    uc_constraint_names = [
        PSI.constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
        PSI.constraint_name(PSI.DURATION_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.DURATION_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)

    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_uc)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, false, 480, 0, 480, 120, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem =
        OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_uc; use_parameters = true)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, true, 480, 0, 480, 120, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 480, 0, 240, 120, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal UC With AC - PF" begin
    bin_variable_names = [
        PSI.variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.variable_name(PSI.START, PSY.ThermalStandard),
        PSI.variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    uc_constraint_names = [
        PSI.constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
        PSI.constraint_name(PSI.DURATION_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.DURATION_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalStandardUnitCommitment)

    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    op_problem = OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5_uc)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, false, 600, 0, 600, 240, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem =
        OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5_uc; use_parameters = true)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, true, 600, 0, 600, 240, 120, true)
    psi_constraint_test(op_problem, uc_constraint_names)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 600, 0, 360, 240, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

################################### Basic Unit Commitment tests ############################
@testset "Thermal Basic UC With DC - PF" begin
    bin_variable_names = [
        PSI.variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.variable_name(PSI.START, PSY.ThermalStandard),
        PSI.variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)

    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    op_problem = OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_uc)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, false, 480, 0, 240, 120, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem =
        OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_uc; use_parameters = true)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, true, 480, 0, 240, 120, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 480, 0, 240, 120, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Basic UC With AC - PF" begin
    bin_variable_names = [
        PSI.variable_name(PSI.ON, PSY.ThermalStandard),
        PSI.variable_name(PSI.START, PSY.ThermalStandard),
        PSI.variable_name(PSI.STOP, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalBasicUnitCommitment)

    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    op_problem = OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5_uc)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, false, 600, 0, 360, 240, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    op_problem =
        OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5_uc; use_parameters = true)
    construct_device!(op_problem, :Thermal, model)
    moi_tests(op_problem, true, 600, 0, 360, 240, 120, true)
    psi_checkbinvar_test(op_problem, bin_variable_names)
    psi_checkobjfun_test(op_problem, GAEVF)

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 600, 0, 360, 240, 120, true)
        psi_checkbinvar_test(op_problem, bin_variable_names)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

################################### Basic Dispatch tests ###################################
@testset "Thermal Dispatch With DC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatch)
    @info "5-Bus testing"
    c_sys5 = build_system("c_sys5")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Dispatch With AC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatch)
    @info "5-Bus testing"
    c_sys5 = build_system("c_sys5")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

################################### No Minimum Dispatch tests ##############################

@testset "Thermal Dispatch NoMin With DC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)
    @info "5-Bus testing"
    c_sys5 = build_system("c_sys5")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        moi_lbvalue_test(
            op_problem,
            PSI.constraint_name(PSI.ACTIVE_RANGE_LB, PSY.ThermalStandard),
            0.0,
        )
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        moi_lbvalue_test(
            op_problem,
            PSI.constraint_name(PSI.ACTIVE_RANGE_LB, PSY.ThermalStandard),
            0.0,
        )
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Dispatch NoMin With AC - PF" begin
    model = DeviceModel(ThermalStandard, ThermalDispatchNoMin)
    @info "5-Bus testing"
    c_sys5 = build_system("c_sys5")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        moi_lbvalue_test(
            op_problem,
            PSI.constraint_name(PSI.ACTIVE_RANGE_LB, PSY.ThermalStandard),
            0.0,
        )
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        moi_lbvalue_test(
            op_problem,
            PSI.constraint_name(PSI.ACTIVE_RANGE_LB, PSY.ThermalStandard),
            0.0,
        )
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

################################### Ramp Limited Testing ##################################
@testset "Thermal Ramp Limited Dispatch With DC - PF" begin
    constraint_names = [
        PSI.constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalRampLimited)
    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys5_uc; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 216, 120, 0, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, DCPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 120, 0, 120, 120, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

@testset "Thermal Ramp Limited Dispatch With AC - PF" begin
    constraint_names = [
        PSI.constraint_name(PSI.RAMP_UP, PSY.ThermalStandard),
        PSI.constraint_name(PSI.RAMP_DOWN, PSY.ThermalStandard),
    ]
    model = DeviceModel(ThermalStandard, ThermalRampLimited)
    @info "5-Bus testing"
    c_sys5_uc = build_system("c_sys5_uc")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys5_uc; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 336, 240, 0, false)
        psi_constraint_test(op_problem, constraint_names)
        psi_checkobjfun_test(op_problem, GAEVF)
    end

    @info "14-Bus testing"
    c_sys14 = build_system("c_sys14")
    for p in [true, false]
        op_problem =
            OperationsProblem(TestOpProblem, ACPPowerModel, c_sys14; use_parameters = p)
        construct_device!(op_problem, :Thermal, model)
        moi_tests(op_problem, p, 240, 0, 240, 240, 0, false)
        psi_checkobjfun_test(op_problem, GQEVF)
    end
end

############################# UC validation tests ##########################################
branches = Dict{Symbol, DeviceModel}()
services = Dict{Symbol, ServiceModel}()
ED_devices = Dict{Symbol, DeviceModel}(
    :Generators => DeviceModel(ThermalStandard, ThermalRampLimited),
    :Loads => DeviceModel(PowerLoad, StaticPowerLoad),
)
UC_devices = Dict{Symbol, DeviceModel}(
    :Generators => DeviceModel(ThermalStandard, ThermalStandardUnitCommitment),
    :Loads => DeviceModel(PowerLoad, StaticPowerLoad),
)
# Testing Ramping Constraint
@testset "Solving ED with CopperPlate for testing Ramping Constraints" begin
    node = Bus(1, "nodeA", "PV", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing)
    load = PowerLoad("Bus1", true, node, nothing, 0.4, 0.9861, 1.0, 2.0)
    DA_ramp = collect(
        DateTime("1/1/2024  0:00:00", "d/m/y  H:M:S"):Hour(1):DateTime(
            "1/1/2024  4:00:00",
            "d/m/y  H:M:S",
        ),
    )
    gen_ramp = [
        ThermalStandard(
            "Alta",
            true,
            true,
            node,
            0.20,
            0.010,
            0.5,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.0, max = 0.40),
            nothing,
            nothing,
            nothing,
            ThreePartCost((0.0, 1400.0), 0.0, 4.0, 2.0),
        ),
        ThermalStandard(
            "Park City",
            true,
            true,
            node,
            0.70,
            0.20,
            2.0,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.7, max = 2.20),
            nothing,
            (up = 0.010625, down = 0.010625),
            nothing,
            ThreePartCost((0.0, 1500.0), 0.0, 1.5, 0.75),
        ),
    ]
    ramp_load = [0.9, 1.1, 2.485, 2.175, 0.9]
    load_forecast_ramp = Deterministic("get_maxactivepower", TimeArray(DA_ramp, ramp_load))
    ramp_test_sys = System(100.0)
    add_component!(ramp_test_sys, node)
    add_component!(ramp_test_sys, load)
    add_component!(ramp_test_sys, gen_ramp[1])
    add_component!(ramp_test_sys, gen_ramp[2])
    add_forecast!(ramp_test_sys, load, load_forecast_ramp)

    template =
        OperationsProblemTemplate(CopperPlatePowerModel, ED_devices, branches, services)
    ED = OperationsProblem(
        TestOpProblem,
        template,
        ramp_test_sys;
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    psi_checksolve_test(ED, [MOI.OPTIMAL], 11191.00)
    moi_tests(ED, true, 10, 0, 20, 10, 5, false)
end

#= Disabled temporarily due to the elimination of initial conditions passing
# Testing Duration Constraints
@testset "Solving UC with CopperPlate for testing Duration Constraints" begin
    node = Bus(1, "nodeA", "PV", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing)
    load = PowerLoad("Bus1", true, node, nothing, 0.4, 0.9861, 1.0, 2.0)
    DA_dur = collect(
        DateTime("1/1/2024  0:00:00", "d/m/y  H:M:S"):Hour(1):DateTime(
            "1/1/2024  6:00:00",
            "d/m/y  H:M:S",
        ),
    )
    gens_dur = [
        ThermalStandard(
            "Alta",
            true,
            true,
            node,
            0.40,
            0.010,
                0.5,
                PrimeMovers.ST,
                ThermalFuels.COAL,
                (min = 0.3, max = 0.9),
                nothing,
                nothing,
                (up = 4, down = 2),
            ThreePartCost((0.0, 1400.0), 0.0, 4.0, 2.0),
        ),
        ThermalStandard(
            "Park City",
            true,
            true,
            node,
            1.70,
            0.20,
                2.2125,
                PrimeMovers.ST,
                ThermalFuels.COAL,
                (min = 0.7, max = 2.2),
                nothing,
                nothing,
                (up = 6, down = 4),
            ThreePartCost((0.0, 1500.0), 0.0, 1.5, 0.75),
        ),
    ]

    duration_load = [0.3, 0.6, 0.8, 0.7, 1.7, 0.9, 0.7]
    load_forecast_dur =
        Deterministic("get_maxactivepower", TimeArray(DA_dur, duration_load))
    duration_test_sys = System(100.0)
    add_component!(duration_test_sys, node)
    add_component!(duration_test_sys, load)
    add_component!(duration_test_sys, gens_dur[1])
    add_component!(duration_test_sys, gens_dur[2])
    add_forecast!(duration_test_sys, load, load_forecast_dur)

    status = [1.0, 0.0]
    up_time = [2.0, 0.0]
    down_time = [0.0, 3.0]

    alta = gens_dur[1]
    init_cond = PSI.InitialConditions()
    PSI.set_initial_conditions!(
        init_cond,
        PSI.ICKey(DeviceStatus, typeof(alta)),
        build_init(gens_dur, status),
    )
    PSI.set_initial_conditions!(
        init_cond,
        PSI.ICKey(TimeDurationON, typeof(alta)),
        build_init(gens_dur, up_time),
    )
    PSI.set_initial_conditions!(
        init_cond,
        PSI.ICKey(TimeDurationOFF, typeof(alta)),
        build_init(gens_dur, down_time),
    )

    template =
        OperationsProblemTemplate(CopperPlatePowerModel, UC_devices, branches, services)
    UC = OperationsProblem(
        TestOpProblem,
        template,
        duration_test_sys;
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    psi_checksolve_test(UC, [MOI.OPTIMAL], 8223.50)
    moi_tests(UC, true, 56, 0, 56, 14, 21, true)
end
=#

## PWL linear Cost implementation test
@testset "Solving UC with CopperPlate testing Linear PWL" begin
    node = Bus(1, "nodeA", "PV", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing)
    load = PowerLoad("Bus1", true, node, nothing, 0.4, 0.9861, 1.0, 2.0)
    gens_cost = [
        ThermalStandard(
            "Alta",
            true,
            true,
            node,
            0.52,
            0.010,
            0.5,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.22, max = 0.55),
            nothing,
            nothing,
            nothing,
            ThreePartCost(
                [(589.99, 0.220), (884.99, 0.33), (1210.04, 0.44), (1543.44, 0.55)],
                532.44,
                5665.23,
                0.0,
            ),
        ),
        ThermalStandard(
            "Park City",
            true,
            true,
            node,
            0.62,
            0.20,
            2.2125,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.62, max = 1.55),
            nothing,
            nothing,
            nothing,
            ThreePartCost(
                [(1264.80, 0.62), (1897.20, 0.93), (2594.4787, 1.24), (3433.04, 1.55)],
                235.397,
                5665.23,
                0.0,
            ),
        ),
    ]
    DA_cost = collect(
        DateTime("1/1/2024  0:00:00", "d/m/y  H:M:S"):Hour(1):DateTime(
            "1/1/2024  1:00:00",
            "d/m/y  H:M:S",
        ),
    )
    cost_load = [1.3, 2.1]
    load_forecast_cost = Deterministic("get_maxactivepower", TimeArray(DA_cost, cost_load))
    cost_test_sys = System(100.0)
    add_component!(cost_test_sys, node)
    add_component!(cost_test_sys, load)
    add_component!(cost_test_sys, gens_cost[1])
    add_component!(cost_test_sys, gens_cost[2])
    add_forecast!(cost_test_sys, load, load_forecast_cost)

    template =
        OperationsProblemTemplate(CopperPlatePowerModel, UC_devices, branches, services)
    UC = OperationsProblem(
        TestOpProblem,
        template,
        cost_test_sys;
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    psi_checksolve_test(UC, [MOI.OPTIMAL], 9336.736919354838)
    moi_tests(UC, true, 32, 0, 8, 4, 10, true)
end

## PWL SOS-2 Cost implementation test
@testset "Solving UC with CopperPlate testing SOS2 implementation" begin
    node = Bus(1, "nodeA", "PV", 0, 1.0, (min = 0.9, max = 1.05), 230, nothing, nothing)
    load = PowerLoad("Bus1", true, node, nothing, 0.4, 0.9861, 1.0, 2.0)
    gens_cost_sos = [
        ThermalStandard(
            "Alta",
            true,
            true,
            node,
            0.52,
            0.010,
            0.5,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.22, max = 0.55),
            nothing,
            nothing,
            nothing,
            ThreePartCost(
                [(1122.43, 0.22), (1617.43, 0.33), (1742.48, 0.44), (2075.88, 0.55)],
                0.0,
                5665.23,
                0.0,
            ),
        ),
        ThermalStandard(
            "Park City",
            true,
            true,
            node,
            0.62,
            0.20,
            2.2125,
            PrimeMovers.ST,
            ThermalFuels.COAL,
            (min = 0.62, max = 1.55),
            nothing,
            nothing,
            nothing,
            ThreePartCost(
                [(1500.19, 0.62), (2132.59, 0.929), (2829.875, 1.24), (2831.444, 1.55)],
                0.0,
                5665.23,
                0.0,
            ),
        ),
    ]
    DA_cost_sos = collect(
        DateTime("1/1/2024  0:00:00", "d/m/y  H:M:S"):Hour(1):DateTime(
            "1/1/2024  1:00:00",
            "d/m/y  H:M:S",
        ),
    )
    cost_sos_load = [1.3, 2.1]
    load_forecast_cost_sos =
        Deterministic("get_maxactivepower", TimeArray(DA_cost_sos, cost_sos_load))
    cost_test_sos_sys = System(100.0)
    add_component!(cost_test_sos_sys, node)
    add_component!(cost_test_sos_sys, load)
    add_component!(cost_test_sos_sys, gens_cost_sos[1])
    add_component!(cost_test_sos_sys, gens_cost_sos[2])
    add_forecast!(cost_test_sos_sys, load, load_forecast_cost_sos)

    for g in gens_cost_sos
        @test PSI._pwlparamcheck(PSY.get_op_cost(g).variable) == false
    end

    template =
        OperationsProblemTemplate(CopperPlatePowerModel, UC_devices, branches, services)
    UC = OperationsProblem(
        TestOpProblem,
        template,
        cost_test_sos_sys;
        optimizer = Cbc_optimizer,
        use_parameters = true,
    )
    psi_checksolve_test(UC, [MOI.OPTIMAL], 8500.89716, 10.0)
    moi_tests(UC, true, 32, 0, 8, 4, 14, true)
end


#
# Powell.jl --
#
# Mike Powell's derivative free optimization methods for Julia.
#
# ----------------------------------------------------------------------------
#
# This file is part of OptimPack.jl which is licensed under the MIT
# "Expat" License:
#
# Copyright (C) 2015, Éric Thiébaut.
#
# ----------------------------------------------------------------------------

# Possible status values returned by COBYLA.
const COBYLA_INITIAL_ITERATE       = convert(Cint,  2)
const COBYLA_ITERATE               = convert(Cint,  1)
const COBYLA_SUCCESS               = convert(Cint,  0)
const COBYLA_ROUNDING_ERRORS       = convert(Cint, -1)
const COBYLA_TOO_MANY_EVALUATIONS  = convert(Cint, -2)
const COBYLA_BAD_ADDRESS           = convert(Cint, -3)
const COBYLA_CORRUPTED             = convert(Cint, -4)

# Get a textual explanation of the status returned by COBYLA.
function cobyla_reason(status::Integer)
    ptr = ccall((:cobyla_reason, opklib), Ptr{UInt8}, (Cint,), status)
    if ptr == C_NULL
        error("unknown COBYLA status: ", status)
    end
    bytestring(ptr)
end

# Wrapper for the objective function in COBYLA, the actual objective
# function is provided by the client data.
function cobyla_objfun(n::Cptrdiff_t, m::Cptrdiff_t, x_::Ptr{Cdouble},
                       c_::Ptr{Cdouble}, f_::Ptr{Any})
    x = pointer_to_array(x_, n)
    f = unsafe_pointer_to_objref(f_)
    fx::Cdouble = (m > 0 ? f(x, pointer_to_array(c_, m)) : f(x))
    return fx
end
const cobyla_objfun_c = cfunction(cobyla_objfun, Cdouble,
                                  (Cptrdiff_t, Cptrdiff_t, Ptr{Cdouble},
                                   Ptr{Cdouble}, Ptr{Any}))

function cobyla_check(n::Integer, m::Integer, rhobeg::Real, rhoend::Real)
    if n < 2
        "bad number of variables"
    elseif m < 0
        "bad number of constraints"
    elseif rhoend < 0 || rhoend > rhobeg
        "bad trust region radius settings"
    end
end

function cobyla!(f::Function, x::Vector{Cdouble},
                 m::Integer, rhobeg::Real, rhoend::Real;
                 verbose::Integer=0, maxeval::Integer=500)
    n = length(x)
    reason = cobyla_check(n, m, rhobeg, rhoend)
    reason == nothing || error(reason)
    w = Array(Cdouble, n*(3*n + 2*m + 11) + 4*m + 6)
    iact = Array(Cptrdiff_t, m + 1)
    maxfun = Cptrdiff_t[maxeval]
    status = ccall((:cobyla, opklib), Cint,
                   (Cptrdiff_t, Cptrdiff_t, Ptr{Void}, Ptr{Any},
                    Ptr{Cdouble}, Cdouble, Cdouble, Cptrdiff_t,
                    Ptr{Cptrdiff_t}, Ptr{Cdouble}, Ptr{Cptrdiff_t}),
                   n, m, cobyla_objfun_c, pointer_from_objref(f),
                   x, rhobeg, rhoend, verbose, maxfun, w, iact)
    if status != COBYLA_SUCCESS
        error(cobyla_reason(status))
    end
    #return w[1]
end

# Context for reverse communication variant of COBYLA.
type CobylaContext
    ptr::Ptr{Void}
    n::Int
    m::Int
    rhobeg::Cdouble
    rhoend::Cdouble
    verbose::Int
    maxeval::Int
end

# Create a new reverse communication workspace for COBYLA algorithm.
# A typical usage is:
# ```
# x = Array(Cdouble, n)
# c = Array(Cdouble, m)
# x[...] = ... # initial solution
# ctx = cobyla_create(n, m, rhobeg, rhoend, verbose=1, maxeval=500)
# status = cobyla_get_status(ctx)
# while status == COBYLA_ITERATE
#     fx = ...       # compute function value at X
#     c[...] = ...   # compute constraints at X
#     status = cobyla_iterate(ctx, fx, x, c)
# end
# if status != COBYLA_SUCCESS
#     println("Something wrong occured in COBYLA: ", cobyla_reason(status))
# end
# ```
function cobyla_create(n::Integer, m::Integer,
                       rhobeg::Real, rhoend::Real;
                       verbose::Integer=0, maxeval::Integer=500)
    reason = cobyla_check(n, m, rhobeg, rhoend)
    reason == nothing || error(reason)
    ptr = ccall((:cobyla_create, opklib), Ptr{Void},
                (Cptrdiff_t, Cptrdiff_t, Cdouble, Cdouble,
                 Cptrdiff_t, Cptrdiff_t),
                n, m, rhobeg, rhoend, verbose, maxeval)
    if ptr == C_NULL
        reason = (errno() == Base.Errno.ENOMEM
                  ? "insufficient memory"
                  : "unexpected error")
        error(reason)
    end
    ctx = CobylaContext(ptr, n, m, rhobeg, rhoend, verbose, maxeval)
    finalizer(ctx, ctx -> ccall((:cobyla_delete, opklib), Void,
                                (Ptr{Void},), ctx.ptr))
    return ctx
end

# Perform the next iteration of the reverse communication variant of the
# COBYLA algorithm.  On entry, the workspace status must be
# `COBYLA_ITERATE`, `f` and `c` are the function value and the constraints
# at `x`.  On exit, the returned value (the new workspace status) is:
# `COBYLA_ITERATE` if a new trial point has been stored in `x` and if user
# is requested to compute the function value and the constraints on the new
# point; `COBYLA_SUCCESS` if algorithm has converged and `x` has been set
# with the variables at the solution (the corresponding function value can
# be retrieved with `cobyla_get_last_f`); anything else indicates an error
# (see `cobyla_reason` for an explanatory message).
function cobyla_iterate(ctx::CobylaContext, f::Real, x::Vector{Cdouble},
                        c::Vector{Cdouble})
    length(x) == ctx.n || error("bad number of variables")
    length(c) == ctx.m || error("bad number of constraints")
    ccall((:cobyla_iterate, opklib), Cint,
          (Ptr{Void}, Cdouble, Ptr{Cdouble}, Ptr{Cdouble}),
          ctx.ptr, f, x, c)
end
# The same but without constraints.
function cobyla_iterate(ctx::CobylaContext, f::Real, x::Vector{Cdouble})
    length(x) == ctx.n || error("bad number of variables")
    ctx.m == 0 || error("bad number of constraints")
    ccall((:cobyla_iterate, opklib), Cint,
          (Ptr{Void}, Cdouble, Ptr{Cdouble}, Ptr{Void}),
          ctx.ptr, f, x, C_NULL)
end

# Restart COBYLA algorithm using the same parameters.  The return value is
# the new status of the algorithm, see `cobyla_get_status` for details.
function cobyla_restart(ctx::CobylaContext)
    ccall((:cobyla_restart, opklib), Cint, (Ptr{Void},), ctx.ptr)
end

# Get the current status of the algorithm.  Result is: `COBYLA_ITERATE` if
# user is requested to compute F(X) and C(X); `COBYLA_SUCCESS` if algorithm
# has converged; anything else indicates an error (see `cobyla_reason` for
# an explanatory message).
function cobyla_get_status(ctx::CobylaContext)
    ccall((:cobyla_get_status, opklib), Cint, (Ptr{Void},), ctx.ptr)
end

# Get the current number of function evaluations.  Result is -1 if
# something is wrong (e.g. CTX is NULL), nonnegative otherwise.
function cobyla_get_nevals(ctx::CobylaContext)
    ccall((:cobyla_get_nevals, opklib), Cptrdiff_t, (Ptr{Void},), ctx.ptr)
end

# Get the current size of the trust region.  Result is 0 if algorithm not
# yet started (before first iteration), -1 if something is wrong (e.g. CTX
# is NULL), strictly positive otherwise.
function cobyla_get_rho(ctx::CobylaContext)
    ccall((:cobyla_get_rho, opklib), Cdouble, (Ptr{Void},), ctx.ptr)
end

# Get the last function value.  Upon convergence of `cobyla_iterate`
# (i.e. return with status `COBYLA_SUCCESS`), this value corresponds to the
# function at the solution; otherwise, this value corresponds to the
# previous set of variables.
function cobyla_get_last_f(ctx::CobylaContext)
    ccall((:cobyla_get_last_f, opklib), Cdouble, (Ptr{Void},), ctx.ptr)
end

# Get a textual explanation of the current status.
cobyla_get_reason(ctx::CobylaContext) = cobyla_reason(cobyla_get_status(ctx))

function cobyla_test(revcom::Bool=false)
    # Beware that order of operations may affect the result (whithin
    # rounding errors).  I have tried to keep the same ordering as F2C
    # which takes care of that, in particular when converting expressions
    # involving powers.
    prt(s) = println("\n       "*s)
    for nprob in 1:10
        if nprob == 1
            # Minimization of a simple quadratic function of two variables.
            prt("Output from test problem 1 (Simple quadratic)")
            n = 2
            m = 0
            xopt = Array(Cdouble, n)
            xopt[1] = -1.0
            xopt[2] = 0.0
            function ftest(x::Vector{Cdouble})
                r1 = x[1] + 1.0
                r2 = x[2]
                fc = 10.0*(r1*r1) + (r2*r2)
                return fc
            end
        elseif nprob == 2
            # Easy two dimensional minimization in unit circle.
            prt("Output from test problem 2 (2D unit circle calculation)")
            n = 2
            m = 1
            xopt = Array(Cdouble, n)
            xopt[1] = sqrt(0.5)
            xopt[2] = -xopt[1]
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                fc = x[1]*x[2]
                con[1] = 1.0 - x[1]*x[1] - x[2]*x[2]
                return fc
            end
        elseif nprob == 3
            # Easy three dimensional minimization in ellipsoid.
            prt("Output from test problem 3 (3D ellipsoid calculation)")
            n = 3
            m = 1
            xopt = Array(Cdouble, n)
            xopt[1] = 1.0/sqrt(3.0)
            xopt[2] = 1.0/sqrt(6.0)
            xopt[3] = -0.33333333333333331
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                fc = x[1]*x[2]*x[3]
                con[1] = 1.0 - (x[1]*x[1]) - 2.0*(x[2]*x[2]) - 3.0*(x[3]*x[3])
                return fc
            end
        elseif nprob == 4
            # Weak version of Rosenbrock's problem.
            prt("Output from test problem 4 (Weak Rosenbrock)")
            n = 2
            m = 0
            xopt = Array(Cdouble, n)
            xopt[1] = -1.0
            xopt[2] = 1.0
            function ftest(x::Vector{Cdouble})
                r2 = x[1]
                r1 = r2*r2 - x[2]
                r3 = x[1] + 1.0
                fc = r1*r1 + r3*r3
                return fc
            end
        elseif nprob == 5
            # Intermediate version of Rosenbrock's problem.
            prt("Output from test problem 5 (Intermediate Rosenbrock)")
            n = 2
            m = 0
            xopt = Array(Cdouble, n)
            xopt[1] = -1.0
            xopt[2] = 1.0
            function ftest(x::Vector{Cdouble})
                r2 = x[1]
                r1 = r2*r2 - x[2]
                r3 = x[1] + 1.0
                fc = r1*r1*10.0 + r3*r3
                return fc
            end
        elseif nprob == 6
            # This problem is taken from Fletcher's book Practical Methods
            # of Optimization and has the equation number (9.1.15).
            prt("Output from test problem 6 (Equation (9.1.15) in Fletcher)")
            n = 2
            m = 2
            xopt = Array(Cdouble, n)
            xopt[1] = sqrt(0.5)
            xopt[2] = xopt[1]
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                fc = -x[1] - x[2]
                r1 = x[1]
                con[1] = x[2] - r1*r1
                r1 = x[1]
                r2 = x[2]
                con[2] = 1.0 - r1*r1 - r2*r2
                return fc
            end
        elseif nprob == 7
            # This problem is taken from Fletcher's book Practical Methods
            # of Optimization and has the equation number (14.4.2).
            prt("Output from test problem 7 (Equation (14.4.2) in Fletcher)")
            n = 3
            m = 3
            xopt = Array(Cdouble, n)
            xopt[1] = 0.0
            xopt[2] = -3.0
            xopt[3] = -3.0
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                fc = x[3]
                con[1] = x[1]*5.0 - x[2] + x[3]
                r1 = x[1]
                r2 = x[2]
                con[2] = x[3] - r1*r1 - r2*r2 - x[2]*4.0
                con[3] = x[3] - x[1]*5.0 - x[2]
                return fc
            end
        elseif nprob == 8
            # This problem is taken from page 66 of Hock and Schittkowski's
            # book Test Examples for Nonlinear Programming Codes. It is
            # their test problem Number 43, and has the name Rosen-Suzuki.
            prt("Output from test problem 8 (Rosen-Suzuki)")
            n = 4
            m = 3
            xopt = Array(Cdouble, n)
            xopt[1] = 0.0
            xopt[2] = 1.0
            xopt[3] = 2.0
            xopt[4] = -1.0
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                r1 = x[1]
                r2 = x[2]
                r3 = x[3]
                r4 = x[4]
                fc = (r1*r1 + r2*r2 + r3*r3*2.0 + r4*r4 - x[1]*5.0
                      - x[2]*5.0 - x[3]*21.0 + x[4]*7.0)
                r1 = x[1]
                r2 = x[2]
                r3 = x[3]
                r4 = x[4]
                con[1] = (8.0 - r1*r1 - r2*r2 - r3*r3 - r4*r4 - x[1]
                          + x[2] - x[3] + x[4])
                r1 = x[1]
                r2 = x[2]
                r3 = x[3]
                r4 = x[4]
                con[2] = (10.0 - r1*r1 - r2*r2*2.0 - r3*r3 - r4*r4*2.0
                          + x[1] + x[4])
                r1 = x[1]
                r2 = x[2]
                r3 = x[3]
                con[3] = (5.0 - r1*r1*2.0 - r2*r2 - r3*r3 - x[1]*2.0
                          + x[2] + x[4])
                return fc
            end
        elseif nprob == 9
            # This problem is taken from page 111 of Hock and
            # Schittkowski's book Test Examples for Nonlinear Programming
            # Codes. It is their test problem Number 100.
            prt("Output from test problem 9 (Hock and Schittkowski 100)")
            n = 7
            m = 4
            xopt = Array(Cdouble, n)
            xopt[1] =  2.330499
            xopt[2] =  1.951372
            xopt[3] = -0.4775414
            xopt[4] =  4.365726
            xopt[5] = -0.624487
            xopt[6] =  1.038131
            xopt[7] =  1.594227
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                r1 = x[1] - 10.0
                r2 = x[2] - 12.0
                r3 = x[3]
                r3 *= r3
                r4 = x[4] - 11.0
                r5 = x[5]
                r5 *= r5
                r6 = x[6]
                r7 = x[7]
                r7 *= r7
                fc = (r1*r1 + r2*r2*5.0 + r3*r3 + r4*r4*3.0
                      + r5*(r5*r5)*10.0 + r6*r6*7.0 + r7*r7
                      - x[6]*4.0*x[7] - x[6]*10.0 - x[7]*8.0)
                r1 = x[1]
                r2 = x[2]
                r2 *= r2
                r3 = x[4]
                con[1] = (127.0 - r1*r1*2.0 - r2*r2*3.0 - x[3]
                          - r3*r3*4.0 - x[5]*5.0)
                r1 = x[3]
                con[2] = (282.0 - x[1]*7.0 - x[2]*3.0 - r1*r1*10.0
                          - x[4] + x[5])
                r1 = x[2]
                r2 = x[6]
                con[3] = (196.0 - x[1]*23.0 - r1*r1 - r2*r2*6.0
                          + x[7]*8.0)
                r1 = x[1]
                r2 = x[2]
                r3 = x[3]
                con[4] = (r1*r1*-4.0 - r2*r2 + x[1]*3.0*x[2]
                          - r3*r3*2.0 - x[6]*5.0 + x[7]*11.0)
                return fc
            end
        elseif nprob == 10
            # This problem is taken from page 415 of Luenberger's book
            # Applied Nonlinear Programming. It is to maximize the area of
            # a hexagon of unit diameter.
            prt("Output from test problem 10 (Hexagon area)")
            n = 9
            m = 14
            xopt = Array(Cdouble, n)
            function ftest(x::Vector{Cdouble}, con::Vector{Cdouble})
                fc = -0.5*(x[1]*x[4] - x[2]*x[3] + x[3]*x[9] - x[5]*x[9]
                           + x[5]*x[8] - x[6]*x[7])
                r1 = x[3]
                r2 = x[4]
                con[1] = 1.0 - r1*r1 - r2*r2
                r1 = x[9]
                con[2] = 1.0 - r1*r1
                r1 = x[5]
                r2 = x[6]
                con[3] = 1.0 - r1*r1 - r2*r2
                r1 = x[1]
                r2 = x[2] - x[9]
                con[4] = 1.0 - r1*r1 - r2*r2
                r1 = x[1] - x[5]
                r2 = x[2] - x[6]
                con[5] = 1.0 - r1*r1 - r2*r2
                r1 = x[1] - x[7]
                r2 = x[2] - x[8]
                con[6] = 1.0 - r1*r1 - r2*r2
                r1 = x[3] - x[5]
                r2 = x[4] - x[6]
                con[7] = 1.0 - r1*r1 - r2*r2
                r1 = x[3] - x[7]
                r2 = x[4] - x[8]
                con[8] = 1.0 - r1*r1 - r2*r2
                r1 = x[7]
                r2 = x[8] - x[9]
                con[9] = 1.0 - r1*r1 - r2*r2
                con[10] = x[1]*x[4] - x[2]*x[3]
                con[11] = x[3]*x[9]
                con[12] = -x[5]*x[9]
                con[13] = x[5]*x[8] - x[6]*x[7]
                con[14] = x[9]
                return fc
            end
        else
            error("bad problem number ($nprob)")
        end

        x = Array(Cdouble, n)
        for icase in 1:2
            for i in 1:n
                x[i] = 1.0
            end
            rhobeg = 0.5
            rhoend = (icase == 2 ? 1e-4 : 0.001)
            if revcom
                # Test the reverse communication variant.
                c = (m > 0 ? Array(Cdouble, m) : nothing)
                ctx = cobyla_create(n, m, rhobeg, rhoend, verbose=1, maxeval=2000)
                status = cobyla_get_status(ctx)
                while status == COBYLA_ITERATE
                    if m > 0
                        # Some constraints.
                        fx = ftest(x, c)
                        status = cobyla_iterate(ctx, fx, x, c)
                    else
                        # No constraints.
                        fx = ftest(x)
                        status = cobyla_iterate(ctx, fx, x)
                    end
                end
                if status != COBYLA_SUCCESS
                    println("Something wrong occured in COBYLA: ", cobyla_reason(status))
                end
            else
                cobyla!(ftest, x, m, rhobeg, rhoend, verbose=1, maxeval=2000)
            end
            if nprob == 10
                tempa = x[1] + x[3] + x[5] + x[7]
                tempb = x[2] + x[4] + x[6] + x[8]
                tempc = 0.5/sqrt(tempa*tempa + tempb*tempb)
                tempd = tempc*sqrt(3.0)
                xopt[1] = tempd*tempa + tempc*tempb
                xopt[2] = tempd*tempb - tempc*tempa
                xopt[3] = tempd*tempa - tempc*tempb
                xopt[4] = tempd*tempb + tempc*tempa
                for i in 1:4
                    xopt[i + 4] = xopt[i]
                end
            end
            temp = 0.0
            for i in 1:n
                r1 = x[i] - xopt[i]
                temp += r1*r1
            end
            @printf("\n     Least squares error in variables =%16.6E\n", sqrt(temp))
        end
        @printf("  ------------------------------------------------------------------\n")
    end
end

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

# Possible status values returned by NEWUOA.
const NEWUOA_INITIAL_ITERATE      = convert(Cint,  2)
const NEWUOA_ITERATE              = convert(Cint,  1)
const NEWUOA_SUCCESS              = convert(Cint,  0)
const NEWUOA_BAD_NPT              = convert(Cint, -1)
const NEWUOA_ROUNDING_ERRORS      = convert(Cint, -2)
const NEWUOA_TOO_MANY_EVALUATIONS = convert(Cint, -3)
const NEWUOA_STEP_FAILED          = convert(Cint, -4)
const NEWUOA_BAD_ADDRESS          = convert(Cint, -5)
const NEWUOA_CORRUPTED            = convert(Cint, -6)

# Get a textual explanation of the status returned by NEWUOA.
function newuoa_reason(status::Integer)
    ptr = ccall((:newuoa_reason, opklib), Ptr{UInt8}, (Cint,), status)
    if ptr == C_NULL
        error("unknown NEWUOA status: ", status)
    end
    bytestring(ptr)
end

# Wrapper for the objective function in NEWUOA, the actual objective
# function is provided by the client data.
function newuoa_objfun(n::Cptrdiff_t, x_::Ptr{Cdouble}, f_::Ptr{Any})
    x = pointer_to_array(x_, n)
    f = unsafe_pointer_to_objref(f_)
    fx::Cdouble = f(x)
    return fx
end
const newuoa_objfun_c = cfunction(newuoa_objfun, Cdouble,
                                  (Cptrdiff_t, Ptr{Cdouble}, Ptr{Any}))

function newuoa_check(n::Integer, npt::Integer, rhobeg::Real, rhoend::Real)
    if n < 2
        "bad number of variables"
    elseif npt < n + 2 || npt > div((n + 2)*(n + 1),2)
        "NPT is not in the required interval"
    elseif rhoend < 0 || rhoend > rhobeg
        "bad trust region radius settings"
    end
end

function newuoa!(f::Function, x::Vector{Cdouble},
                 npt::Integer, rhobeg::Real, rhoend::Real;
                 verbose::Integer=0, maxeval::Integer=500)
    n = length(x)
    reason = newuoa_check(n, npt, rhobeg, rhoend)
    reason == nothing || error(reason)
    w = Array(Cdouble, (npt + 13)*(npt + n) + div(3*n*(n + 3),2))
    status = ccall((:newuoa, opklib), Cint,
                   (Cptrdiff_t, Cptrdiff_t, Ptr{Void}, Ptr{Any},
                    Ptr{Cdouble}, Cdouble, Cdouble, Cptrdiff_t,
                    Cptrdiff_t, Ptr{Cdouble}),
                   n, npt, newuoa_objfun_c, pointer_from_objref(f),
                   x, rhobeg, rhoend, verbose, maxeval, w)
    if status != NEWUOA_SUCCESS
        error(newuoa_reason(status))
    end
    return nothing
end

# Context for reverse communication variant of NEWUOA.
type NewuoaContext
    ptr::Ptr{Void}
    n::Int
    npt::Int
    rhobeg::Cdouble
    rhoend::Cdouble
    verbose::Int
    maxeval::Int
end

# Create a new reverse communication workspace for NEWUOA algorithm.
# A typical usage is:
# ```
# x = Array(Cdouble, n)
# x[...] = ... # initial solution
# ctx = newuoa_create(n, npt, rhobeg, rhoend, verbose=0, maxeval=500)
# status = newuoa_get_status(ctx)
# while status == NEWUOA_ITERATE
#   fx = ... # compute function value at X
#   status = newuoa_iterate(ctx, fx, x)
# end
# if status != NEWUOA_SUCCESS
#   println("Something wrong occured in NEWUOA: ", newuoa_reason(status))
# end
# ```
function newuoa_create(n::Integer, npt::Integer,
                       rhobeg::Real, rhoend::Real;
                       verbose::Integer=0, maxeval::Integer=500)
    reason = newuoa_check(n, npt, rhobeg, rhoend)
    reason == nothing || error(reason)
    ptr = ccall((:newuoa_create, opklib), Ptr{Void},
                (Cptrdiff_t, Cptrdiff_t, Cdouble, Cdouble,
                 Cptrdiff_t, Cptrdiff_t),
                n, npt, rhobeg, rhoend, verbose, maxeval)
    if ptr == C_NULL
        reason = (errno() == Base.Errno.ENOMEM
                  ? "insufficient memory"
                  : "unexpected error")
        error(reason)
    end
    ctx = NewuoaContext(ptr, n, npt, rhobeg, rhoend, verbose, maxeval)
    finalizer(ctx, ctx -> ccall((:newuoa_delete, opklib), Void,
                                (Ptr{Void},), ctx.ptr))
    return ctx
end

# Perform the next iteration of the reverse communication version of the
# NEWUOA algorithm.  On entry, the wokspace status must be
# `NEWUOA_ITERATE`, `f` is the function value at `x`.  On exit, the
# returned value (the new wokspace status) is: `NEWUOA_ITERATE` if a new
# trial point has been stored in `x` and if user is requested to compute
# the function value for the new point; `NEWUOA_SUCCESS` if algorithm has
# converged; anything else indicates an error (see `newuoa_reason` for an
# explanatory message).
function newuoa_iterate(ctx::NewuoaContext, f::Real, x::Vector{Cdouble})
    length(x) == ctx.n || error("bad number of variables")
    ccall((:newuoa_iterate, opklib), Cint,
          (Ptr{Void}, Cdouble, Ptr{Cdouble}),
          ctx.ptr, f, x)
end

# Restart NEWUOA algorithm using the same parameters.  The return value is the
# new status of the algorithm, see `newuoa_get_status` for details.
function newuoa_restart(ctx::NewuoaContext)
    ccall((:newuoa_restart, opklib), Cint, (Ptr{Void},), ctx.ptr)
end

# Get the current status of the algorithm.  Result is: `NEWUOA_ITERATE` if
# user is requested to compute F(X); `NEWUOA_SUCCESS` if algorithm has
# converged; anything else indicates an error (see `newuoa_reason` for an
# explanatory message).
function newuoa_get_status(ctx::NewuoaContext)
    ccall((:newuoa_get_status, opklib), Cint, (Ptr{Void},), ctx.ptr)
end

# Get the current number of function evaluations.  Result is -1 if
# something is wrong (e.g. CTX is NULL), nonnegative otherwise.
function newuoa_get_nevals(ctx::NewuoaContext)
    ccall((:newuoa_get_nevals, opklib), Cptrdiff_t, (Ptr{Void},), ctx.ptr)
end

# Get the current size of the trust region.  Result is 0 if algorithm not
# yet started (before first iteration), -1 if something is wrong (e.g. CTX
# is NULL), strictly positive otherwise.
function newuoa_get_rho(ctx::NewuoaContext)
    ccall((:newuoa_get_rho, opklib), Cdouble, (Ptr{Void},), ctx.ptr)
end

function newuoa_test(revcom::Bool=false)
    # The Chebyquad test problem (Fletcher, 1965) for N = 2,4,6 and 8, with
    # NPT = 2N+1.
    function ftest(x::Vector{Cdouble})
        n = length(x)
        np = n + 1
        y = Array(Cdouble, np, n)
        for j in 1:n
            y[1,j] = 1.0
            y[2,j] = x[j]*2.0 - 1.0
        end
        for i in 2:n
            for j in 1:n
                y[i+1,j] = y[2,j]*2.0*y[i,j] - y[i-1,j]
            end
        end
        f = 0.0
        iw = 1
        for i in 1:np
            sum = 0.0
            for j in 1:n
                sum += y[i,j]
            end
            sum /= n
            if iw > 0
                sum += 1.0/(i*i - 2*i)
            end
            iw = -iw
            f += sum*sum
        end
        return f
    end

    # Run the tests.
    rhoend = 1e-6
    for n = 2:2:8
        npt = 2*n + 1
        x = Array(Cdouble, n)
        for i in 1:n
            x[i] = i/(n + 1)
        end
        rhobeg = x[1]*0.2
        @printf("\n\n    Results with N =%2d and NPT =%3d\n", n, npt)
        if revcom
            # Test the reverse communication variant.
            ctx = newuoa_create(n, npt, rhobeg, rhoend, verbose=2, maxeval=5000)
            status = newuoa_get_status(ctx)
            while status == NEWUOA_ITERATE
                fx = ftest(x)
                status = newuoa_iterate(ctx, fx, x)
            end
            if status != NEWUOA_SUCCESS
                println("Something wrong occured in NEWUOA: ", newuoa_reason(status))
            end
        else
            newuoa!(ftest, x, npt, rhobeg, rhoend, verbose=2, maxeval=5000)
        end
    end
end

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

const BOBYQA_SUCCESS              = convert(Cint,  0)
const BOBYQA_BAD_NPT              = convert(Cint, -1)
const BOBYQA_TOO_CLOSE            = convert(Cint, -2)
const BOBYQA_ROUNDING_ERRORS      = convert(Cint, -3)
const BOBYQA_TOO_MANY_EVALUATIONS = convert(Cint, -4)
const BOBYQA_STEP_FAILED          = convert(Cint, -5)

function bobyqa_reason(status::Integer)
    if status == BOBYQA_SUCCESS
        return "algorithm converged"
    elseif status == BOBYQA_BAD_NPT
        return "NPT is not in the required interval"
    elseif status == BOBYQA_TOO_CLOSE
        return "insufficient space between the bounds"
    elseif status == BOBYQA_ROUNDING_ERRORS
        return "too much cancellation in a denominator"
    elseif status == BOBYQA_TOO_MANY_EVALUATIONS
        return "maximum number of function evaluations exceeded"
    elseif status == BOBYQA_STEP_FAILED
        return "a trust region step has failed to reduce quadratic approximation"
    else
        return "unknown BOBYQA status"
    end
end

# Wrapper for the objective function in BOBYQA, the actual objective
# function is provided by the client data.
function bobyqa_objfun(n::Cptrdiff_t, x_::Ptr{Cdouble}, f_::Ptr{Any})
    x = pointer_to_array(x_, n)
    f = unsafe_pointer_to_objref(f_)
    fx::Cdouble = f(x)
    return fx
end
const bobyqa_objfun_c = cfunction(bobyqa_objfun, Cdouble,
                                  (Cptrdiff_t, Ptr{Cdouble}, Ptr{Any}))

function bobyqa!(f::Function, x::Vector{Cdouble},
                 xl::Vector{Cdouble}, xu::Vector{Cdouble},
                 rhobeg::Real, rhoend::Real;
                 npt::Union{Integer,Void}=nothing,
                 verbose::Integer=0, maxeval::Integer=500)
    n = length(x)
    length(xl) == n || error("bad length for inferior bound")
    length(xu) == n || error("bad length for superior bound")
    if npt == nothing
        npt = 2*n + 1
    end
    w = Array(Cdouble, (npt + 5)*(npt + n) + div(3*n*(n + 5),2))
    status = ccall((:bobyqa, opklib), Cint,
                (Cptrdiff_t, Cptrdiff_t, Ptr{Void}, Ptr{Any},
                 Ptr{Cdouble}, Ptr{Cdouble}, Ptr{Cdouble},
                 Cdouble, Cdouble, Cptrdiff_t, Cptrdiff_t,
                 Ptr{Cdouble}),
                   n, npt, bobyqa_objfun_c, pointer_from_objref(f),
                   x, xl, xu, rhobeg, rhoend, verbose, maxeval, w)
    if status != BOBYQA_SUCCESS
        error(bobyqa_reason(status))
    end
    return w[1]
end

function bobyqa_test()
    # The test function.
    function f(x::Vector{Cdouble})
        fx = 0.0
        n = length(x)
        for i in 4:2:n
            for j in 2:2:i-2
                tempa = x[i - 1] - x[j - 1]
                tempb = x[i] - x[j]
                temp = tempa*tempa + tempb*tempb
                temp = max(temp,1e-6)
                fx += 1.0/sqrt(temp)
            end
        end
        return fx
    end

    # Run the tests.
    bdl = -1.0
    bdu =  1.0
    rhobeg = 0.1
    rhoend = 1e-6
    for m in (5,10)
        q = 2.0*pi/m
        n = 2*m
        x = Array(Cdouble, n)
        xl = Array(Cdouble, n)
        xu = Array(Cdouble, n)
        for i in 1:n
            xl[i] = bdl
            xu[i] = bdu
        end
        for jcase in 1:2
            if jcase == 2
                npt = 2*n + 1
            else
                npt = n + 6
            end
            @printf("\n\n     2D output with M =%4ld,  N =%4ld  and  NPT =%4ld\n",
                    m, n, npt)
            for j in 1:m
                temp = q*j
                x[2*j - 1] = cos(temp)
                x[2*j]     = sin(temp)
            end
            fx = bobyqa!(f, x, xl, xu, rhobeg, rhoend, npt=npt, verbose=2, maxeval=500000)
            @printf("\n***** least function value: %.15e\n", fx)
        end
    end
end


using Random

mutable struct Species
    # A subpopulation containing similar individiduals
    id::Int64                         # species's id
    age::Int64                        # species's age
    subpopulation::Vector{MLPChromosome} # species's individuals
    hasBest::Bool                     # Does this species has the best individual of the population?
    spawn_amount::Int64
    no_improvement_age::Int64         # the age species has shown no improvements on average
    last_avg_fitness::Float64
    representant::MLPChromosome
    function Species(g::Global, first_individual::MLPChromosome, previous_id=0)
        # A species requires at least one individual to come to existence
        id  = previous_id == 0 ? get_new_id(g) : previous_id
        first_individual.species_id = id
        new(id,
            0,     # species's age
            [first_individual],    # species' individuals
            false, # hasBest: Does this species has the best individual of the population?
            0,     # spawn_amount
            0,     # the age species has shown no improvements on average
            0.,    # last_avg_fitness
            first_individual)
    end
end

function get_new_id(g::Global)
    g.speciesCnt += 1
    return g.speciesCnt
end

function add(s::Species, individual::MLPChromosome)
    individual.species_id = s.id
    push!(s.subpopulation, individual)
    # choose a new random representant for the species
    s.representant = s.subpopulation[rand(1:length(s.subpopulation))]
end

# Returns the total number of individuals in this species
Base.length(s::Species) = length(s.subpopulation)

function Base.show(io::IO, s::Species)
    str = @sprintf("   Species: %2d   size: %3d   age: %3d   spawn: %3d  ",
                    s.id, length(s.subpopulation), s.age, s.spawn_amount)
    str = @sprintf("%1s  No improvement: %3d   avg. fitness: %1.8f",
                    str,s.no_improvement_age, s.last_avg_fitness)
    @printf(io,"%6s", str)
end

function tournamentSelection(s::Species, k=2)
    # Tournament selection with size k (default k=2).
    # randomly select k competitors
    selected = randperm(length(s.subpopulation))[1:k]
    chs = s.subpopulation[selected]
    best = chs[1]
    for ch in chs # choose best among randomly selected
        best = ch.fitness > best.fitness ? ch : best
    end
    return best
end

function average_fitness(s::Species)
    # Returns the raw average fitness for this species
    @assert length(s) > 0
    fitness_sum = 0.0
    fitness_sum = sum((ch)-> ch.fitness, s.subpopulation)
    current = fitness_sum / length(s)

    # if no_improvement_age > threshold, species will be removed
    if current > s.last_avg_fitness
        s.last_avg_fitness = current
        s.no_improvement_age = 0
    else
        s.no_improvement_age += 1
    end
    return current
end

function reproduce(g::Global, s::Species, fitness::Evaluation)
    # Returns a list of 'spawn_amount' new individuals

    offspring = Tuple{MLPChromosome, Bool}[] # new offspring for this species
    s.age += 1  # increment species age

#     @printf("Reproducing species %d with %d members\n", s.id, length(s))

    # this condition is useless since no species with spawn_amount < 0 will
    # reach this point - at least it shouldn't happen.
    @assert s.spawn_amount > 0

    sort!(s.subpopulation, by = ch-> ch.fitness, rev = true)

    if g.cf.elitism
        # TODO: Wouldn't it be better if we set elitism=2,3,4...
        # depending on the size of each species?
        push!(offspring, (s.subpopulation[1], true))
        s.spawn_amount -= 1
    end

    survivors = floor(Int, length(s) * g.cf.survival_threshold) # keep a % of the best individuals
#     println("survivors = $survivors $(length(s))")

    s.subpopulation = survivors > 0 ? s.subpopulation[1:survivors] : [s.subpopulation[1]]

    while(s.spawn_amount > 0)

        s.spawn_amount -= 1

        if length(s) > 1
            # Selects two parents from the remaining species and produces a single individual
            # Stanley selects at random, here we use tournament selection (although it is not
            # clear if has any advantages)
            parent1 = tournamentSelection(s)
            parent2 = tournamentSelection(s)

            @assert parent1.species_id == parent2.species_id
            child = crossover(g, parent1, parent2)
            child = mutate!(child,g)
            fitness(child)
            push!(offspring, (child, false))
        else
            # mutate only
            parent1 = s.subpopulation[1]
            # TODO: temporary hack - the child needs a new id (not the father's)
            child = crossover(g, parent1, parent1)
            child = mutate!(child,g)
            fitness(child)
            #push!(offspring, child)
            push!(offspring, (child, false))
        end
    end

    # reset species (new members will be added again when speciating)
    s.subpopulation = []

    # select a new random representant member
    representant_index = rand(1:length(offspring))
    s.representant = offspring[representant_index][1]
    #offspring[representant_index][2] = true
    offspring[representant_index] = (offspring[representant_index][1], true)

    return offspring
end