# ---------------NEW ------------------
using DataFrames
dfd=readtable("orig.csv")
names!(dfd,[Symbol(replace(string(n),"jennie_rf_pos_exposure2_","")) for n in names(dfd)])
rename!(dfd,Symbol("3rdparty"),:thirdparty)

# --------------- NEW END -----------------

# ---------------- REMOVE OTLIERS -------------
using DataFrames
dfd=readtable("out.csv")

isNum{T<:Number}(::AbstractArray{T}) = true
isNum(::Any) = false
isFloat{T<:Float64}(::AbstractArray{T}) = true
isFloat(::Any) = false
isInt{T<:Int64}(::AbstractArray{T}) = true
isInt(::Any) = false
isString{T<:String}(::AbstractArray{T}) = true
isString(::Any) = false
isBool{T<:Bool}(::AbstractArray{T}) = true
isBool(::Any) = false
isSymbol{T<:Symbol}(::AbstractArray{T}) = true
isSymbol(::Any) = false

describe(dfd[:venue_dim_key])
countmap(dfd[:venue_dim_key])

 :venue_dim_key
 :tm_dim_key_day
-- don't change :date1
 :trans_time
???? there is exactly 10000? this should either be unique, or the same/defaulted  :trans_num
 :store_num
 :card_num
 :upc
--- not populated  :ndc_upc_ind
 :quantity
--- not populated :weight
--- overfit :item_list_price
 :net_price
--- not populated :card_discount
--- not populated  :ad_discount
--- not populated  :other_discount
--- not populated :item_type
--- not populated :tender_type
--- not populated :ad_event
--- not populated  :ad_version
:tm_dim_key_week  -- only in control
:operating_company -- only "BJS" in control  & COMSCORE in exp
:item_dim_key - only populated for control
 :upc10 - only populated for control
 :units - only populated for control
 :cents - only populated for control
 :baseline_units  - only populated for control
 :baseline_cents - only populated for control
 :feature - only populated for control
 :display - only populated for control
 :totl_prc_reduc
 :registration_request_id
 :product_id_jennieo6

for c in setdiff(names(dfd[1:33]),[:])
    #b = 2.8*(sqrt(sum((dfd[:quantity].- mean(dfd[:quantity])).^2)/(length(dfd[:quantity])-1)))
    b = 2.8*(sqrt(sum((dfd[(dfd[:gross_imps].==0),:quantity].- mean(dfd[(dfd[:gross_imps].==0),:quantity])).^2)/(length(dfd[(dfd[:gross_imps].==0),:quantity])-1)))

    length(dfd[(dfd[:quantity].> b)|(dfd[:quantity].< (b*-1)),:quantity])
end

function Pre_out(df_in::DataFrame)
     df_cat_pre = df_in[df_in[:Buyer_Pre_P0] .==1 , [:Prd_0_Net_Pr_PRE,:experian_id]]
     df_cat_pos = df_in[(df_in[:Buyer_Pre_P0] .==0) & (df_in[:Buyer_Pos_P0] .==1) , [:experian_id]]
     median_df = median(df_cat_pre[:Prd_0_Net_Pr_PRE])
     df_cat_pre[:Prd_0_Net_Pr_PRE_med1] = abs(df_cat_pre[:Prd_0_Net_Pr_PRE]-median_df)
     MAD=median(df_cat_pre[:Prd_0_Net_Pr_PRE_med1])
     df_cat_pre[:Prd_0_Net_Pr_PRE_med2] = (0.6745*(df_cat_pre[:Prd_0_Net_Pr_PRE]-median_df))/MAD
     df_cat_pre_zsc = df_cat_pre[abs(df_cat_pre[:Prd_0_Net_Pr_PRE_med2]) .< 3.5,:]
     df_cat_pre_zsc_1 = df_cat_pre_zsc[:,[:experian_id]]
     df_cat_pre_zsc_f = vcat(df_cat_pos,df_cat_pre_zsc_1)
     df_in_pout =  join(df_in, df_cat_pre_zsc_f, on =  :experian_id , kind = :inner);
end
df_in = Pre_out(df_in);

# ---------------- END REMOVE OTLIERS -------------

"""
# DataSet
drop table if exists jennie.jennie6_breaks_RF;
CREATE EXTERNAL TABLE IF NOT EXISTS jennie.jennie6_breaks_RF
  ( household_id string,
    iri_week string,
    date1 string,
    creative_id int,
    placement_id int,
    gross_imps bigint,
    val_imps bigint,
    placement_nm string,
    creative_nm string,
    publisher string
   )
ROW FORMAT DELIMITED FIELDS TERMINATED BY ',' LINES TERMINATED BY ‘\n’ STORED AS TEXTFILE LOCATION '/mapr/mapr04p/analytics0001/analytic_users/Models/trees/tables/';

# show create table jennie.jennie6_breaks_RF;

Insert Overwrite Table jennie.jennie6_breaks_RF
select distinct
b.household_id ,
a.iri_week ,
a.date1 ,
a.creative_id ,
a.placement_id ,
a.gross_imps ,
a.val_imps ,
c.placement_nm,
d.creative_nm,
c.publisher
from daily.daily_unioned a
left join jennie.placements6 c on a.placement_id = c.id
left join jennie.creatives6 d on a.creative_id = d.id
inner JOIN WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP b on a.lr_id = b.dependent_id
where b.dependent_type = 'LVRAMP_ID'
      and b.household_type = 'EXPERIAN'
      and b.retailer = 'COMSCORE'
      and b.data_supplier = 'EXPERIAN'
      and b.current_state = 'MATCHED'
      and a.clientid in('21884504')
      and iri_week < 1934
order by household_id, date1;

=============================================================================

select distinct
b.household_id ,
a.iri_week ,
a.date1 ,
a.creative_id ,
a.placement_id ,
a.gross_imps ,
a.val_imps
from daily.daily_unioned a,
WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP b
where a.lr_id = b.dependent_id
      and b.dependent_type = 'LVRAMP_ID'
      and b.household_type = 'EXPERIAN'
      and b.retailer = 'COMSCORE'
      and b.data_supplier = 'COMSCORE'
      and b.current_state = 'MATCHED'
      and a.clientid in('21884504')
      and iri_week < 1934
order by household_id, date1;

select count(*)
from daily.daily_unioned a,
     WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP b
where a.lr_id = b.dependent_id
      and a.clientid in('21884504')
      and iri_week < 1934
      and b.current_state = 'MATCHED'
      and b.retailer = 'COMSCORE'
      and b.data_supplier = 'EXPERIAN'
;

select count(*)
from WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP b
where b.retailer = 'COMSCORE'
      and b.data_supplier = 'COMSCORE'
;

select distinct(retailer) from WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP;

select distinct(data_supplier) from WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP;
"""

# ========================================================== DATASET AGREGATION =============================================

# ================ START HERE ==============================
export JULIA_NUM_THREADS=4

Threads.nthreads()
#addprocs([("10.106.128.212", 1)])
addprocs([("10.106.128.213", 1)])
addprocs([("10.106.128.214", 1)])
addprocs([("10.106.128.215", 1)])
addprocs([("10.106.128.216", 1)])
addprocs([("10.106.128.217", 1)])
addprocs([("10.106.128.218", 1)])
addprocs([("10.106.128.219", 1)])
addprocs([("10.106.128.220", 1)])
addprocs([("10.106.128.221", 1)])
addprocs(1)

#Base.LOAD_CACHE_PATH[1]="/mapr/mapr04p/analytics0001/analytic_users/jpkg/v0.5"
#ENV["JULIA_PKGDIR"]  = "/mapr/mapr04p/analytics0001/analytic_users/jpkg"
#push!(LOAD_PATH, "/mapr/mapr04p/analytics0001/analytic_users/jpkg/v0.5")
#pop!(LOAD_PATH)

#using DataStructures, DataFrames, StatsFuns, GLM , JuMP,NLopt, HDF5, JLD, Distributions, MixedModels, RCall, StatsBase, xCommon, Feather
using DataFrames

#dfd = readtable("/mnt/resource/analytics/trees/exposure.csv",header=false);
dfd = readtable("orig.csv",header=true);
#names!(dfdx, [:household_id, :iri_week, :date1, :creative_id, :placement_id, :gross_imps, :val_imps ,:placement, :creative, :publisher] )
#names!(dfd, [:hh_id, :week, :date, :creative_id, :placement_id, :gross_imps, :imps ,:placement, :creative, :publisher] )
names!(dfd,[Symbol(replace(string(n),"jennie_rf_pos_exposure2_","")) for n in names(dfd)])
rename!(dfd,Symbol("3rdparty"), :thirdrdparty )
rename!(dfd,:household_id, :panid )

#dfd[:household_id]
dformat = Dates.DateFormat("y-m-d");
#dfd[:date] = map(x-> x=="NULL" ? NA : Date(replace(x, " 00:00:00",""),dformat), dfd[:date1])
dfd[:date] = map(x-> Date(string(x),Dates.DateFormat("yyyymmdd") ), dfd[:date1])

dfd = dfd[!isna(dfd[:date]),:]
dfd[:date] = convert(Array{Date},dfd[:date])
dfd[:datelag] = dfd[:date] .- Dates.Day(7)
dfd[:datenum] = map(x->Int64(x),dfd[:date])
dfd[:datenumlag] = map(x->Int64(x),dfd[:datelag])

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

#Date("2016-07-06",Dates.DateFormat("y-m-d"))

# ----------------- CHUNKS  ------------------
#Date("2016-06-30",Dates.DateFormat("y-m-d"))-Dates.Day(7)
#Date("2016-06-30",Dates.DateFormat("y-m-d"))
#dfd[:date] = map(x-> Date(replace(x, " 00:00:00",""),dformat), dfd[:date1])
#Date(dfd[:date1],dformat)
#Date(dfd[:date1],dformat)
#Date("2016-06-30",dformat)
#dfd=sort(dfd,cols=[:date])
#dfd[:chunks]=-1

panlst = unique(dfd[:panid])
panchunks=DataFrame( panid=panlst,chunks=-1 )
panlen=length(panlst)
chunksize = Int(floor(length(panlst)/100))
chunknum=1
while (chunknum*chunksize) <= panlen
    s=chunknum*chunksize
    e=s+chunksize
    println("start: ",s,"   end:",e,"   :=  chunk : ",chunknum)
    if e > panlen
        panchunks[s:end,:chunks] = chunknum
    else
        panchunks[s:e,:chunks] = chunknum
    end
    chunknum=chunknum+1
end
panchunks[panchunks[:chunks].==-1,:chunks]=chunknum

countmap(panchunks[:chunks])

dfd2 = join(dfd, panchunks, on = :panid)

root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
writetable(root*"/total.csv", dfd2)
for p in sort(unique(dfd2[:chunks]))
    println(p)
    writetable(root*"/"*string(p)*".csv",  dfd2[dfd2[:chunks].==p,:]    )
end

# ---- END CHUNKS ------

#---  Generate Lags ---

export JULIA_NUM_THREADS=4
Threads.nthreads()

#addprocs([("10.106.128.212", 1)])
addprocs([("10.106.128.213", 1)])
addprocs([("10.106.128.214", 1)])
addprocs([("10.106.128.215", 1)])
addprocs([("10.106.128.216", 1)])
addprocs([("10.106.128.217", 1)])
addprocs([("10.106.128.218", 1)])
addprocs([("10.106.128.219", 1)])
addprocs([("10.106.128.220", 1)])
addprocs([("10.106.128.221", 1)])
addprocs(1)

@everywhere insert!(Base.LOAD_CACHE_PATH, 1, "/mapr/mapr04p/analytics0001/analytic_users/jpkg/lib/v0.5")
@everywhere pop!(Base.LOAD_CACHE_PATH)
@everywhere println((Base.LOAD_CACHE_PATH))

using DataFrames
@everywhere using DataFrames
p=1

@everywhere function genLags(p::Int64)
    brks=[:html5
         ,:static
         ,:native_creative
         ,:video
         ,:content
         ,:contextual
         ,:direct
         ,:prospecting
          ,:retargeting
          ,:behavorial
          ,:native
          ,:predictive
          ,:thirdrdparty
          ,:pmp
          ,:buzzfeed
          ,:hulu
          ,:popsugarus
          ,:rachaelraymag
          ,:shape
          ,:blank
          ,:eatingwell
          ,:meredithcorporation
          ,:realsimple
          ,:turndsp
          ,:cookinglight
          ,:dataxu
          ,:allrecipes
          ,:amazon
          ,:myfitnesspal
          ,:womenshealth
          ,:yummly
         ,:youtube
        ]

    root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
    dfx = readtable(root*"/"*string(p)*".csv",header=true)

    for brk in brks
        lag=Symbol(string(brk)*"_lag")
        println("Generating : ",brk,"  ~ ",lag)
        dfx[lag]=0
        for row in eachrow(dfx)
            #println(r[:video])
            row[lag] = sum(dfx[(dfx[:panid].==row[:panid])&(dfx[:datenum].<=row[:datenum] ) & ( dfx[:datenum].>= row[:datenumlag]  ) ,brk])
        end
    end
    writetable(root*"/"*string(p)*"_out.csv", dfx)
    println("COMPLETE for : ",p,"  in ","/"*string(p)*"_out.csv")
end

"""
@async remotecall_fetch(genLags, 2, 2)
@spawnat 2 genLags(2)

@async remotecall_fetch(genLags, 2, 2)
@async remotecall_fetch(genLags, 3, 3)
@async remotecall_fetch(genLags, 4, 4)
@async remotecall_fetch(genLags, 5, 5)
@async remotecall_fetch(genLags, 6, 6)
@async remotecall_fetch(genLags, 7, 7)
@async remotecall_fetch(genLags, 8, 8)
@async remotecall_fetch(genLags, 9, 9)
@async remotecall_fetch(genLags, 10, 10)
@async remotecall_fetch(genLags, 11, 10)
"""
"""
@async remotecall_fetch(genLags, 2, 11)
@async remotecall_fetch(genLags, 3, 12)
@async remotecall_fetch(genLags, 4, 13)
@async remotecall_fetch(genLags, 5, 14)
@async remotecall_fetch(genLags, 6, 15)
@async remotecall_fetch(genLags, 7, 16)
@async remotecall_fetch(genLags, 8, 17)
@async remotecall_fetch(genLags, 9, 18)
@async remotecall_fetch(genLags, 10, 19)
@async remotecall_fetch(genLags, 11, 20)
"""

x=99
x=59

for p in 2:11
    x=x+1
    @async remotecall_fetch(genLags, p,  x)
end

# ---------------- de-chunk files ----------------------
using DataFrames
root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
#dfx = readtable(root*"/1_out.csv",header=true);

#for i in 31:60   #2:101
#    println("Loading : ",i,"_out.csv")
#    t = readtable(root*"/"*string(i)*"_out.csv",header=true);
#    dfx=vcat(dfx,t)
#end

#writetable(root*"/1_60_tmp.csv", dfx)

using DataFrames
root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
function consolidateChunks(s::Int64,e::Int64)
    println("Loading : ",string(s),"_out.csv")
    dfx = readtable(root*"/"*string(s)*"_out.csv",header=true);
    for i in s+1:e   #2:101
        println("Loading : ",i,"_out.csv")
        t = readtable(root*"/"*string(i)*"_out.csv",header=true);
        dfx=vcat(dfx,t)
    end
    return dfx
end

dfx1 = consolidateChunks(1,30);
dfx2 = consolidateChunks(31,60);
dfx3 = consolidateChunks(61,90);
dfx4 = consolidateChunks(91,101);

dfout=vcat(dfx1,dfx2,dfx3,dfx4)
writetable(root*"/out.csv", dfout)

# ------------------ XGB --------------------------

using BinDeps, DataFrames, XGBoost
#using Dates
#root="/mnt/resource/analytics/models"
root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
#dfd=readtable(root*"/out.csv")
#dfd=readtable(root*"/48_out.csv")
dfd=readtable(root*"/1_60_tmp.csv")
#:datenum, :datenumlag , :chunks

dfd=dfd[setdiff(names(dfd),[:date,:datelag,:date1,:chunks])]
dfd[:trans_time]=pool(dfd[:trans_time])
dfd[:operating_company]=pool(dfd[:operating_company])

#just to get it working - these are strings
dfd=dfd[setdiff(names(dfd),[:trans_time,:ndc_upc_ind,:item_type,:ad_event,:operating_company,:upc10])]

net_price=convert(Array{Float32}, dfd[:net_price])

dfd=dfd[setdiff(names(dfd),[:net_price])]

lags=Symbol[:html5_lag,:static_lag,:native_creative_lag,:video_lag,:content_lag,:contextual_lag,:direct_lag,:prospecting_lag,:retargeting_lag,:behavorial_lag,:native_lag,:predictive_lag,:thirdrdparty_lag,:pmp_lag,:buzzfeed_lag,:hulu_lag,:popsugarus_lag,:rachaelraymag_lag,:shape_lag,:blank_lag,:eatingwell_lag,:meredithcorporation_lag,:realsimple_lag,:turndsp_lag,:cookinglight_lag,:dataxu_lag,:allrecipes_lag,:amazon_lag,:myfitnesspal_lag,:womenshealth_lag,:yummly_lag,:youtube_lag]
dfdc=deepcopy(dfd)
for l in lags
    dfdc[l] = 0.0
end

function DMdata(dfx::DataFrame,lbl::Float32[]=Float32[1.1])
    arr = convert(Array{Float32}, dfx )
    if length(lbl) == 0
        return DMatrix(arr)
    else
        return DMatrix(arr, label = lbl)
    end
end
#Transform data to XGBoost matrices
#trainArray = convert(Array{Float32}, dfd )
#dtrain = DMatrix(trainArray, label = net_price)
#testArray = convert(Array{Float32}, dfd )
#dtest = DMatrix(testArray)
#ctrlArray = convert(Array{Float32}, dfdc )
#dctrl = DMatrix(ctrlArray)
dtrain = DMatrix(convert(Array{Float32},dfd), label = net_price)
dtest = DMatrix(convert(Array{Float32},dfd))
dctrl = DMatrix(convert(Array{Float32},dfdc))

num_round = 250; param = ["eta" => 0.2, "max_depth" => 20, "objective" => "reg:linear", "silent" => 1]
XGBoostModel = xgboost(dtrain, num_round, param = param)

or

#random forrest
param = ["eta" => 0.2, "max_depth" => 5, "objective" => "reg:linear", "silent" => 1,
         "num_parallel_tree" => 1000, "subsample" => 0.5, "colsample_bytree" => 0.5
        ]
XGBoostModel = xgboost(dtrain, 10, param = param)

ptest = XGBoost.predict(XGBoostModel, dtest)
pctrl = XGBoost.predict(XGBoostModel, dctrl)
mean(convert(Array{Float64},ptest))
mean(convert(Array{Float64},pctrl))
mean(ptest) - mean(pctrl)

#importance(XGBoostModel)
im = [ parse(x.fname[2:end]) for x in importance(XGBoostModel)]
icols = [names(dfd)[c] for c in [ parse(x.fname[2:end]) for x in importance(XGBoostModel)]]

# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************
# ************************* TEST **** TEST ********************************************

using BinDeps, DataFrames, XGBoost
root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
dfx=readtable(root*"/out.csv")  #dfx=readtable(root*"/1_60_tmp.csv")
net_price=convert(Array{Float32}, dfx[:net_price])
dfxc=deepcopy(dfx)
for l in lags dfxc[l] = 0.0 end
xvars = [:panid, :net_price, :date,:datelag,:date1,:chunks, :trans_time, :trans_num, :operating_company, :ndc_upc_ind,:item_type,:ad_event,   ]

"""
:product_id_jennieo6
 :panid
 :datenum
 :datenumlag
 :trans_num
 :card_num
 :registration_request_id
 :upc

 :venue_dim_key
 :tm_dim_key_day
 :item_list_price

 :tm_dim_key_week

 :item_dim_key
 :units
 :cents
 :baseline_units
 :baseline_cents
 :totl_prc_reduc

 :gross_imps
"""

v_covar=[ :feature,
          :display,
          :ad_version,
          :card_discount,
          :ad_discount,
          :other_discount,
          :tender_type,
          :quantity,
          :weight,
          :store_num
]

v_breaks=[ :html5,
 :static,
 :native_creative,
 :video,
 :content,
 :contextual,
 :direct,
 :prospecting,
 :retargeting,
 :behavorial,
 :native,
 :predictive,
 :thirdrdparty,
 :pmp,
 :buzzfeed,
 :hulu,
 :popsugarus,
 :rachaelraymag,
 :shape,
 :blank,
 :eatingwell,
 :meredithcorporation,
 :realsimple,
 :turndsp,
 :cookinglight,
 :dataxu,
 :allrecipes,
 :amazon,
 :myfitnesspal,
 :womenshealth,
 :yummly,
 :youtube
]

v_lags=[ :html5_lag,
 :static_lag,
 :native_creative_lag,
 :video_lag,
 :content_lag,
 :contextual_lag,
 :direct_lag,
 :prospecting_lag,
 :retargeting_lag,
 :behavorial_lag,
 :native_lag,
 :predictive_lag,
 :thirdrdparty_lag,
 :pmp_lag,
 :buzzfeed_lag,
 :hulu_lag,
 :popsugarus_lag,
 :rachaelraymag_lag,
 :shape_lag,
 :blank_lag,
 :eatingwell_lag,
 :meredithcorporation_lag,
 :realsimple_lag,
 :turndsp_lag,
 :cookinglight_lag,
 :dataxu_lag,
 :allrecipes_lag,
 :amazon_lag,
 :myfitnesspal_lag,
 :womenshealth_lag,
 :yummly_lag,
 :youtube_lag
]

#dfx[v_lags]

#TEST
cols=vcat(v_lags,v_breaks, v_covar)
dtrain = DMatrix(convert(Array{Float32},dfx[cols]), label = net_price)
dtest = DMatrix(convert(Array{Float32},dfx[cols]))
dctrl = DMatrix(convert(Array{Float32},dfxc[cols]))
#param = ["eta" => 0.2, "max_depth" => 20, "objective" => "reg:linear", "silent" => 1]
xgb = xgboost(dtrain, 250, param = ["eta" => 0.2, "max_depth" => 20, "objective" => "reg:linear", "silent" => 1] )
ptest = XGBoost.predict(xgb, dtest)
pctrl = XGBoost.predict(xgb, dctrl)
mean(convert(Array{Float64},ptest))
mean(convert(Array{Float64},pctrl))

mean(ptest) - mean(pctrl)

dfx[dfx[:panid].==1321364138,:]
dfx[dfx[:net_price].!=0.0,:]
julia> mean(dfx[dfx[:gross_imps].==0.0,:net_price])
14.720302357882089
julia> mean(dfx[dfx[:gross_imps].!=0.0,:net_price])
0.0
julia> mean(dfx[dfx[:gross_imps].>0.0,:net_price])
0.0
julia> length(dfx[dfx[:gross_imps].>0.0,:net_price])
6123200
julia> length(dfx[dfx[:gross_imps].==0.0,:net_price])
1091488

# *********************************************************************

"""  """

results[    findin(results[:parameter],map(x->string(x),    setdiff(m[:finalvars],[:group])  ) )         ,[:parameter,:coef]]

findin(results[:parameter],map(x->string(x),    setdiff(m[:finalvars],[:group])  ) )

bst <- xgboost(data = train$data, label = train$label, max.depth = 4,
               num_parallel_tree = 1000, subsample = 0.5, colsample_bytree =0.5,
               nround = 1
               , objective = "binary:logistic")
"""  """

function genData(dfx::DataFrame,ycol::Symbol,z::Symbol[])
    #dfy = deepcopy
    y = convert(Array{Float32},dfx[ycol] )
    x=convert(Array{Float32}, dfx[setdiff(names(dfx),[ycol])])
    return DMatrix(x, label = y), y, x
end
dtrain,y,x = genData(dfd,:net_price)

num_round = 250
param = ["eta" => 0.2, "max_depth" => 20, "objective" => "reg:linear", "silent" => 1]
xgb = xgboost(dtrain, num_round, param=param)
pred = XGBoost.predict(xgb,dtrain)

dtest,ty,tx = genData(dfdc,:net_price)
DMatrix(tx)

pred_ctrl = XGBoost.predict(xgb,dtest)

# ------------------ END XGB --------------------------

==================  TREES WORKING ==================== examples https://github.com/bensadeghi/DecisionTree.jl
using DecisionTree
y=dfd[:dol_per_trip_pre_p1]
x=dfd[setdiff(names(dfd),[:dol_per_trip_pre_p1])]

model = build_tree(Array(y), Array(x), 5)
or
m2 = build_forest(Array(y), Array(x), 2, 10, 5, 0.7)

apply_tree(model, Array(x))

xtest=x[1:1000,:]
xtrain=x[1000:end,:]
ytest=y[1:1000]
ytrain=y[1000:end]
model = build_tree(Array(ytrain), Array(xtrain), 5)
res = apply_tree(model, Array(xtest))
map((x,y)->x-y, res,ytest)

r2 = nfoldCV_tree(Array(ytrain), Array(xtrain), 3, 5)

m2 = build_forest(Array(ytrain), Array(xtrain), 2, 10, 5, 0.7)
res = apply_tree(m2, Array(xtest))

# Symbol in expression
a=:xyz
QuoteNode(a)
:(5 - $t)
:( :a in $( :(:a + :b) ) )

s=:ssss
:( :a in $( :(:($s) + $s) ) )

a=:(:aaaa)
b=:(:bbb)
:( :a in $( :($a + $b) ) )

function t(s::Symbol)
           println(s," ~~ ",typeof(s))
       end
t(:xyz)
:(t(:xyz))
ex = :(t(:xyz))

:(t(:xyz))

v=:xyz
:(t( :($v) ))

ai=:aaa
bi=:bbb
a= QuoteNode(a)
b= QuoteNode(b)
:( :a in $( :($a + $b) ) )

macro tst(v, exp)
    println("v : ",v," ~~ ",typeof(v))
    println("exp : ",exp," ~~ ",typeof(exp))
    return exp
end

vi=:v1
@tst vi :(println("hello"))

#---------

#----
@everywhere function genx(p::Int64)
        brks=[:html5
         ,:static
         ,:native_creative
         ,:video
         ,:content
         ,:contextual
         ,:direct
         ,:prospecting
          ,:retargeting
          ,:behavorial
          ,:native
          ,:predictive
          ,:thirdrdparty
          ,:pmp
          ,:buzzfeed
          ,:hulu
          ,:popsugarus
          ,:rachaelraymag
          ,:shape
          ,:blank
          ,:eatingwell
          ,:meredithcorporation
          ,:realsimple
          ,:turndsp
          ,:cookinglight
          ,:dataxu
          ,:allrecipes
          ,:amazon
          ,:myfitnesspal
          ,:womenshealth
          ,:yummly
         ,:youtube
        ]
    dfx = readtable("/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks/"*string(p)*".csv",header=true)
    println("xyz : ",p)
end
@spawnat 5  genx(64)

p=1
root="/mapr/mapr04p/analytics0001/analytic_users/Models/RF/chunks"
dfx = readtable(root*"/"*string(p)*".csv",header=true)
y=:net_price
#----

  brk=:youtube
  lag=:youtube_lag
  dfx[lag]=0
  for row in eachrow(dfx)
        #println(r[:video])
        row[lag] = sum(dfx[(dfx[:panid].==row[:panid])&(dfx[:datenum].<=row[:datenum] ) & ( dfx[:datenum].>= row[:datenumlag]  ) ,brk])
  end

dfx[(dfx[brk].!=dfx[lag]) ,[:panid,:date,:datelag,:video_lag,:datenum,:datenumlag,brk]]
t = dfx[5634,[:panid,:date,:datelag,lag,:datenum,:datenumlag,brk]]      #...... 1321364407 │ "2016-07-18" │ "2016-07-11" │ 2         │ 736163  │ 736156
t = dfx[(dfx[:datenum].==736174)&(dfx[:panid].==1338983878),[:panid,:date,:datelag,lag,:datenum,:datenumlag,brk]]
dfx[(dfx[:panid].==t[:panid][1]) ,brk]
dfx[(dfx[:panid].==t[:panid][1])&(dfx[:datenum].<=t[:datenum][1] ) & (dfx[:datenum].>= t[:datenumlag][1]  ) ,brk]
sum(dfx[(dfx[:panid].==t[:panid][1])&(dfx[:datenum].<=t[:datenum][1]) & (  dfx[:datenum].>= t[:datenumlag][1]  ) ,brk])

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

dfd[1,:panid]
dfd[1,[:panid,:date,:datelag]]       == 1305262312 │ 2016-07-06 │ 2016-06-29
dfd[dfd[:panid].==1305262312,:]
sum(dfd[dfd[:panid].==1305262312,:video])

dfd[(dfd[:panid].==1305262312)&(dfd[:date].<=Date("2016-07-06",Dates.DateFormat("y-m-d")) ) & (  dfd[:date].>=Date("2016-06-29",Dates.DateFormat("y-m-d"))  ) ,:]

dfd[1,[:panid,:date,:datelag,:datenum,:datenumlag]]
sum(dfd[(dfd[:panid].==1305262312)&(dfd[:datenum].<=736151 ) & (  dfd[:datenum].>= 736144  ) ,:video])

dfd[:video_lag]=0
for row in eachrow(dfd)
    #println(r[:video])
    row[:video_lag] = sum(dfd[(dfd[:panid].==row[:panid])&(dfd[:datenum].<=row[:datenum] ) & (  dfd[:datenum].>= row[:datenumlag]  ) ,:video])
end

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

#hhlen=length(hhlst)
hhlen = Int(floor(length(hhlst)/10000))
for hhcnt in 0:hhlen
    lb=(hhcnt*50000)+1
    if hhcnt < hhlen
        ub=(hhcnt+1)*50000
        println("chuncking : (",lb,":",ub,")")
        #dfd[findin(dfd[:panid],panids) ,:iso] = true
        dfd[ findin(dfd[:panid],hhlst[lb:ub])  ,:chunks] = hhcnt+1
    else
        println("chuncking : (",lb,":end)")
        #dfd[ findin(dfd[:panid],hhlst[lb:end])  ,:chunks] = hhcnt+1
        dfd[dfd[:chunks].<0,:chunks]=hhcnt+1
    end
end

#dfd[:dateX] = map(x->    Date(string(x),Dates.DateFormat("yyyymmdd"))   ,dfd[:date])
#dfd[:dateX] = convert(Array{Date}, dfd[:dateX])

#lag_cnt=Dict(:one=>1,:four=>4,:eight=>8,:sixteen=>16)

for brk in [:placement, :creative, :publisher]
    for lvl in unique(dfd[brk])
        for lag in [1,4] #[:one,:four,:eight]
            col=Symbol(string(brk)*"_"*replace(string(lvl)," ","_")*"_"*string(lag))
            println(col)
            dfd[col]=0
        end
    end
end

for chunk in 1:2   #maximum(dfd[:chunks])
    for row in eachrow(dfd[dfd[:chunks].==chunk,:])
        println(row[:hh_id]," ~~~ ",row[:date])
    end
end

x=by(dfd,[:date,:hh_id], df-> sum(     df[ ()&()  :imps]       ))
x=by(dfd,[:date,:hh_id], df-> sum(  df[ ()&()  :imps]       ))

y=by(dfd, [:date,:hh_id,:publisher], nrow)

by(dfd, [:date,:hh_id]) do df
    DataFrame(m = mean(df[:PetalLength]), s² = var(df[:PetalLength]))
end

gb = groupby(dfd, [:date])

by(iris, :Species) do df
    DataFrame(m = mean(df[:PetalLength]), s² = var(df[:PetalLength]))
end

#=========================== END =============================

x=by(dfd,:hh_id, df-> sum(df[:imps] ))
or : x=by(dfd,:hh_id, df-> sum(df[:gross_imps] ))
x[x[:x1].>0,:]
x[x[:hh_id].==1000120003,:]

function oliers(dfd::DataFrame, k::Symbol, c::Symbol)
    m = median(dfd[c])
    #dfd[c_med1] = abs(dfd[c]-m)
    #MAD=median(dfd[c_med1])
    MAD = median(abs(dfd[c]-m))
    dfd[c_med2] =  ((dfd[c]-m)*0.6745) / MAD

    dfd_zsc = dfd[abs(dfd[c_med2]) .< 3.5 ,:]
    df_in_pout =  join(df_in, dfd_zsc, on = [ k, k ], kind = :inner)
end
oliers(dfd,:household_id, :val_imps)

# by(dfd, :, df -> sum(df[:PetalLength]))
function Pre_out(df_in::DataFram)
    df_cat_pre = df_in[df_in[:Buyer_Pre_P0] .==1 , [:Prd_0_Net_Pr_PRE,:experian_id]]
    median_df = median(df_cat_pre[:Prd_0_Net_Pr_PRE])
    df_cat_pre[:Prd_0_Net_Pr_PRE_med1] = abs(df_cat_pre[:Prd_0_Net_Pr_PRE]-median_df)
    MAD=median(df_cat_pre[:Prd_0_Net_Pr_PRE_med1])
    df_cat_pre[:Prd_0_Net_Pr_PRE_med2] = (0.6745*(df_cat_pre[:Prd_0_Net_Pr_PRE]-median_df))/MAD
    df_cat_pre_zsc = df_cat_pre[abs(df_cat_pre[:Prd_0_Net_Pr_PRE_med2]) .< 3.5 ,:]
    df_in_pout =  join(df_in, df_cat_pre_zsc, on = [ :experian_id, :experian_id ], kind = :inner)
end
Pre_out(df_in)

# --- parallel RF

function build_forest(labels, features, nsubfeatures, ntrees, ncpu=1)
                  if ncpu > nprocs()
                       addprocs(ncpu - nprocs())
                  end
                  Nlabels = length(labels)
                  Nsamples = int(0.7 * Nlabels)
                  forest = @parallel (vcat) for i in [1:ntrees]
                      inds = rand(1:Nlabels, Nsamples)
                      build_tree(labels[inds], features[inds,:], nsubfeatures)
                  end
                  return [forest]
              end

+++++++++Distribute Macro ++++++++++++++++++++++++++++
function sync_add(r)
    spawns = get(task_local_storage(), :SPAWNS, ())
    if spawns !== ()
        push!(spawns[1], r)
        if isa(r, Task)
            tls_r = get_task_tls(r)
            tls_r[:SUPPRESS_EXCEPTION_PRINTING] = true
        end
    end
    r
end

spawnat(p, thunk) = sync_add(remotecall(thunk, p))

macro spawnat(p, expr)
    expr = localize_vars(esc(:(()->($expr))), false)
    :(spawnat($(esc(p)), $expr))
end

macro run(p, expr)
    expr = localize_vars(esc(:(()->($expr))), false)
    :(spawnat($(esc(p)), $expr))
end
+++++++++++++++++++++++++++++++++++++

================== XGBoost ============================================= https://www.kaggle.com/wacaxx/rossmann-store-sales/julia-xgboost-starter-code
using BinDeps, DataFrames, XGBoost
#using Dates

y=convert(Array{Float32},dfd[:dol_per_trip_pre_p1] )
x=dfd[setdiff(names(dfd),[:dol_per_trip_pre_p1])]
#Define target
#y = convert(Array{Float32}, train[:Sales])

#Transform data to XGBoost matrices
#trainArray = convert(Array{Float32},  x[:, vcat(numericalColumns, categoricalColumns)])
#testArray = convert(Array{Float32}, test[:, vcat(numericalColumns, categoricalColumns)])
#dtrain = DMatrix(trainArray, label = costTrainingLog)
#dtest = DMatrix(testArray)

num_round = 250
param = ["eta" => 0.2, "max_depth" => 20, "objective" => "reg:linear", "silent" => 1]

XGBoostModel = xgboost(dtrain, num_round, param = param)

#Predictions using test data
preds = predict(XGBoostModel, dtest)
#Round to zero closed stores
preds[closedStoreIdx] = 0

#Write Results
sampleSubmission = DataFrame(Id = tesdIdx, Sales = preds)

==================  TREES WORKING ==================== examples https://github.com/bensadeghi/DecisionTree.jl
using DecisionTree
y=dfd[:dol_per_trip_pre_p1]
x=dfd[setdiff(names(dfd),[:dol_per_trip_pre_p1])]

model = build_tree(Array(y), Array(x), 5)
or
m2 = build_forest(Array(y), Array(x), 2, 10, 5, 0.7)

apply_tree(model, Array(x))

xtest=x[1:1000,:]
xtrain=x[1000:end,:]
ytest=y[1:1000]
ytrain=y[1000:end]
model = build_tree(Array(ytrain), Array(xtrain), 5)
res = apply_tree(model, Array(xtest))
map((x,y)->x-y, res,ytest)

r2 = nfoldCV_tree(Array(ytrain), Array(xtrain), 3, 5)

m2 = build_forest(Array(ytrain), Array(xtrain), 2, 10, 5, 0.7)
res = apply_tree(m2, Array(xtest))

====================== END ===========================

df_in[:Dol_per_Trip_PRE_P1]

setdiff(names(df_in),:Dol_per_Trip_PRE_P1)

y=df_in[:Dol_per_Trip_PRE_P1]
x=df_in[setdiff(names(df_in),[:Dol_per_Trip_PRE_P1])]
#model = build_forest(y,x, 20, 50, 1.0)
model = build_tree(y, Array(x),20, 50, 1.0);

using DecisionTree

model = build_forest(df_in[:Dol_per_Trip_PRE_P1], df_in[[setdiff(names(df_in),:Dol_per_Trip_PRE_P1)]], 20, 50, 1.0)

x=Array(x[setdiff(names(x),[:core_based_statistical_areas,:person_1_birth_year_and_month])])

model = build_tree(Array(y), Array(x),20, 50, 1.0);

========================================================
#https://github.com/dmlc/XGBoost.jl/blob/master/demo/basic_walkthrough.jl

using XGBoost

function readlibsvm(fname::ASCIIString, shape)
    dmx = zeros(Float32, shape)
    label = Float32[]
    fi = open(fname, "r")
    cnt = 1
    for line in eachline(fi)
        line = split(line, " ")
        push!(label, float(line[1]))
        line = line[2:end]
        for itm in line
            itm = split(itm, ":")
            dmx[cnt, int(itm[1]) + 1] = float(int(itm[2]))
        end
        cnt += 1
    end
    close(fi)
    return (dmx, label)
end

train_X, train_Y = readlibsvm("/home/rmadmin/.julia/v0.4/XGBoost/data/agaricus.txt.train", (6513, 126))
test_X, test_Y = readlibsvm("/home/rmadmin/.julia/v0.4/XGBoost/data/agaricus.txt.test", (1611, 126))

=============================
using DecisionTree

#Train random forest with
#20 for number of features chosen at each random split,
#50 for number of trees,
#and 1.0 for ratio of subsampling.
model = build_forest(yTrain, xTrain, 20, 50, 1.0)

using DataStructures, DataFrames, StatsFuns, GLM , JuMP,NLopt, HDF5, JLD, Distributions, MixedModels, RCall, StatsBase, xCommon

function loadDF()
    cd("/media/u01/analytics/scoring/CDW5_792/")
    df_data = readtable("csv_final_cdw5_792.csv",header=false);
    df_h = readtable("Headers_cdw5.csv",header=false);
    names!(df_data, convert(Array{Symbol}, df_h[:x1]) )
end
df_in=loadDF()

# --===========================================================================================================================================
# --===================================== SQL =================================================================================================
# --===========================================================================================================================================

select count(a.household_id), count(distinct a.household_id), b.operating_company
from wh_supplmental.household_dependent_id_map a
join wh_fsp.partitioned_fsp_wkly_fact b
     on a.dependent_id = b.card_num
     and UPPER(trim(b.Operating_company)) = trim(UPPER(case when a.retailer='FOODLION' then 'FOOD LION' else a.retailer end ))
where a.current_state = 'MATCHED'
  and a.household_type = 'EXPERIAN'
  and a.dependent_type = 'FSP_CARD'
  and TM_DIM_KEY_WEEK>=1909
group by b.operating_company
"""
149243  17012   FREDS
196025906       7033205 RITEAID
330798388       994861  WEGMANS
38008898        221081  KEYFOOD
6020735400      15722602        KROGER
16025680        100830  SUPERVALU
302291297       2137143 BJS
"""

hive -e 'SELECT distinct household_id FROM WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP where household_type = "EXPERIAN" and dependent_type = "FSP_CARD" and  dependent_id is not null' | sed 's/[\t]/,/g' > /mapr/mapr04p/analytics0001/analytic_users/mddak/fsp_ids_retailer2.csv

set hive.cli.print.header=true;
SELECT retailer, count(distinct household_id) as ids FROM WH_SUPPLMENTAL.HOUSEHOLD_DEPENDENT_ID_MAP where household_type = "EXPERIAN" and dependent_type = "FSP_CARD" and  dependent_id is not null GROUP BY retailer;

hadoop fs -put /mapr/mapr04p/analytics0001/analytic_users/mddak/Experian/IRI_Key_Food_Shipment.txt /externaldata01/prd/experian/xwalk/raw/

set hive.cli.print.header=true; select * from wh_fsp.partitioned_fsp_wkly_fact limit 10;
hive -e 'set hive.cli.print.header=true; select * from MEDIA_LIFT_CAMPAIGN.lift_product WHERE registration_request_id=931 limit 10' | sed 's/[\t]/,/g' > /mapr/mapr04p/analytics0001/analytic_users/mddak/lift_product_example10.csv
set hive.cli.print.header=true; select * from MEDIA_LIFT_CAMPAIGN.lift_product_item_filter_map WHERE registration_request_id=931 limit 10;
set hive.cli.print.header=true; select * from MEDIA_LIFT_CAMPAIGN.POS_WKLY_FACT POS WHERE registration_request_id=931 limit 10;

set hive.cli.print.header=true; select * from Jennieo6.Kantar_FSP_POS_Jennieo6_931 limit 10;


module SlaterKoster

using Reexport

# Functionality we need - by order of
#  (2) AD derivatives
#        - w.r.t. U
#        - w.r.t. hop
#        all further derivatives are handles outside
#  (3) correct f and higher orbitals : implemented -> need to check!
#  (4) fast evaluation
#  (5) fast derivatives

# my old codes from the previous TB package
#include("oldsk.jl")

# basic SK machinery
include("basics.jl")

# evaluate the SK blocks
include("code_generation.jl")
include("skcore.jl")

# hamiltonian matrix assembly
include("matrixassembly.jl")
# exports: hamiltonian

# Some basic models - mostly for testing?
include("models.jl")
@reexport using ACEtb.SlaterKoster.Models
# This exports
#  - KwonTB

end # module


export
    ALTROSolverOptions,
    ALTROSolver

"""$(TYPEDEF) Solver options for the ALTRO solver.
$(FIELDS)
"""
@with_kw mutable struct ALTROSolverOptions{T} <: AbstractSolverOptions{T}

    verbose::Bool=false

    "Augmented Lagrangian solver options."
    opts_al::AugmentedLagrangianSolverOptions=AugmentedLagrangianSolverOptions{Float64}()

    "constraint tolerance"
    constraint_tolerance::T = 1e-5

    # Infeasible Start
    "Use infeasible model (augment controls to make it fully actuated)"
    infeasible::Bool = false

    # "regularization term for infeasible controls."
    # R_inf::T = 1.0

    "project infeasible results to feasible space using TVLQR."
    dynamically_feasible_projection::Bool = true

    "resolve feasible problem after infeasible solve."
    resolve_feasible_problem::Bool = true

    "initial penalty term for infeasible controls."
    penalty_initial_infeasible::T = 1.0

    "penalty update rate for infeasible controls."
    penalty_scaling_infeasible::T = 10.0

    # Projected Newton
    "finish with a projecte newton solve."
    projected_newton::Bool = true

    "options for projected newton solver."
    opts_pn::ProjectedNewtonSolverOptions{T} = ProjectedNewtonSolverOptions{Float64}()

    "constraint satisfaction tolerance that triggers the projected newton solver.
    If set to a non-positive number it will kick out when the maximum penalty is reached."
    projected_newton_tolerance::T = 1.0e-3

end

function ALTROSolverOptions(opts::SolverOptions)
    ALTROSolverOptions(
        opts_al=AugmentedLagrangianSolverOptions(opts),
        opts_pn=ProjectedNewtonSolverOptions(opts),
        verbose=opts.verbose,
        constraint_tolerance=opts.constraint_tolerance
    )
end

"""$(TYPEDEF)
Augmented Lagrangian Trajectory Optimizer (ALTRO) is a solver developed by the Robotic Exploration Lab at Stanford University.
    The solver is special-cased to solve Markov Decision Processes by leveraging the internal problem structure.

ALTRO consists of two "phases":
1) AL-iLQR: iLQR is used with an Augmented Lagrangian framework to solve the problem quickly to rough constraint satisfaction
2) Projected Newton: A collocation-flavored active-set solver projects the solution from AL-iLQR onto the feasible subspace to achieve machine-precision constraint satisfaction.
"""
struct ALTROSolver{T,S} <: ConstrainedSolver{T}
    opts::ALTROSolverOptions{T}
    solver_al::AugmentedLagrangianSolver{T,S}
    solver_pn::ProjectedNewtonSolver{T}
end

AbstractSolver(prob::Problem, opts::ALTROSolverOptions) = ALTROSolver(prob, opts)

function ALTROSolver(prob::Problem{Q,T},
        opts::SolverOptions=SolverOptions{T}();
        infeasible=false,
        R_inf=1.0,
        solver_uncon=iLQRSolver) where {Q,T}
    if infeasible
        # Convert to an infeasible problem
        prob = InfeasibleProblem(prob, prob.Z, R_inf/prob.Z[1].dt)

        # Set infeasible constraint parameters
        # con_inf = get_constraints(prob).constraints[end]
        # con_inf::ConstraintVals{T,Control,<:InfeasibleConstraint}
        # con_inf.params.μ0 = opts.penalty_initial_infeasible
        # con_inf.params.ϕ = opts.penalty_scaling_infeasible
    end
    opts_altro = ALTROSolverOptions(opts)
    solver_al = AugmentedLagrangianSolver(prob, opts, solver_uncon=solver_uncon)
    solver_pn = ProjectedNewtonSolver(prob, opts)
    S = typeof(solver_al.solver_uncon)
    ALTROSolver{T,S}(opts_altro, solver_al, solver_pn)
end

@inline Base.size(solver::ALTROSolver) = size(solver.solver_pn)
@inline get_trajectory(solver::ALTROSolver)::Traj = get_trajectory(solver.solver_al)
@inline get_objective(solver::ALTROSolver) = get_objective(solver.solver_al)
function iterations(solver::ALTROSolver)
    if !solver.opts.projected_newton
        iterations(solver.solver_al)
    else
        iterations(solver.solver_al) + iterations(solver.solver_pn)
    end
end

function get_constraints(solver::ALTROSolver)
    if solver.opts.projected_newton
        get_constraints(solver.solver_pn)
    else
        get_constraints(solver.solver_al)
    end
end

function solve!(solver::ALTROSolver)
    conSet = get_constraints(solver)

    # Set terminal condition if using projected newton
    opts = solver.opts
    if opts.projected_newton
        opts_al = opts.opts_al
        if opts.projected_newton_tolerance >= 0
            opts_al.constraint_tolerance = opts.projected_newton_tolerance
        else
            opts_al.constraint_tolerance = 0
            opts_al.kickout_max_penalty = true
        end
    end

    # Solve with AL
    solve!(solver.solver_al)

    # Check convergence
    i = solver.solver_al.stats.iterations
    c_max = solver.solver_al.stats.c_max[i]

    if opts.projected_newton && c_max > opts.constraint_tolerance
        solve!(solver.solver_pn)
    end

end

function InfeasibleProblem(prob::Problem, Z0::Traj, R_inf::Real)
    @assert !isnan(sum(sum.(states(Z0))))

    n,m,N = size(prob)  # original sizes

    # Create model with augmented controls
    model_inf = InfeasibleModel(prob.model)

    # Get a trajectory that is dynamically feasible for the augmented problem
    #   and matches the states and controls of the original guess
    Z = infeasible_trajectory(model_inf, Z0)

    # Convert constraints so that they accept new dimensions
    conSet = change_dimension(get_constraints(prob),n, m+n)

    # Constrain additional controls to be zero
    inf = InfeasibleConstraint(model_inf)
    add_constraint!(conSet, inf, 1:N-1)

    # Infeasible Objective
    obj = infeasible_objective(prob.obj, R_inf)

    # Create new problem
    Problem(model_inf, obj, conSet, prob.x0, prob.xf, Z, N, prob.t0, prob.tf)
end

function infeasible_objective(obj::Objective, regularizer)
    n,m = state_dim(obj.cost[1]), control_dim(obj.cost[1])
    Rd = [@SVector zeros(m); @SVector fill(regularizer,n)]
    R = Diagonal(Rd)
    cost_inf = QuadraticCost(Diagonal(@SVector zeros(n)),R,checks=false)
    costs = map(obj.cost) do cost
        cost_idx = change_dimension(cost, n, n+m)
        cost_idx + cost_inf
    end
    Objective(costs, copy(obj.J))
end

get_model(solver::ALTROSolver) = get_model(solver.solver_al)


# CSI
# ===
#
# Specification:
#   1: https://github.com/samtools/hts-specs/blob/master/CSIv1.pdf
#   2: https://github.com/samtools/hts-specs/blob/master/CSIv2.pdf
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/Bio.jl/blob/master/LICENSE.md

struct CSIBin
    # distinct bin
    bin::UInt32

    # virtual file offset
    loffset::UInt64

    # number of records stored in the bin
    n_rec::UInt64

    # chunks
    chunks::Vector{Chunk}
end

struct CSI
    # CSI version (0x01 or 0x02)
    version::UInt8

    # number of bits for the minimal interval
    minshift::Int32

    # depth of the binning index
    depth::Int32

    # auxilary data
    aux::Vector{UInt8}

    # bins
    bins::Vector{Vector{CSIBin}}

    # number of unmapped reads
    n_no_coor::Nullable{Int}
end

function Base.show(io::IO, csi::CSI)
    print(io, summary(csi), "(<version=$(csi.version),#seqs=$(length(csi.bins))>)")
end

"""
    CSI(input::AbstractString)

Load a CSI index from `input`.
"""
function CSI(input::AbstractString)
    return open(x->CSI(x), input)
end

"""
    CSI(input::IO)

Load a CSI index from `input`.
"""
function CSI(input_::IO)
    input = CodecBGZF.BGZFDecompressorStream(input_)

    # check magic bytes
    C = read(input, UInt8)
    S = read(input, UInt8)
    I = read(input, UInt8)
    v = read(input, UInt8)
    if C != UInt32('C') || S != UInt32('S') || I != UInt8('I') || v ∉ (0x01, 0x02)
        error("invalid csi magic bytes")
    end

    minshift = read(input, Int32)
    depth = read(input, Int32)
    l_aux = read(input, Int32)
    aux = read(input, l_aux)
    n_ref = read(input, Int32)
    bins = Vector{CSIBin}[]
    for _ in 1:n_ref
        n_bin = read(input, Int32)
        refbins = CSIBin[]
        for _ in 1:n_bin
            bin = read(input, UInt32)
            loffset = read(input, UInt64)
            if v == 0x02
                n_rec = read(input, UInt64)
            else
                n_rec = typemax(UInt64)
            end
            n_chunk = read(input, Int32)
            chunks = Chunk[]
            for _ in 1:n_chunk
                chunk_beg = read(input, UInt64)
                chunk_end = read(input, UInt64)
                push!(chunks, (chunk_beg, chunk_end))
            end
            push!(refbins, CSIBin(bin, loffset, n_rec, chunks))
        end
        push!(bins, refbins)
    end

    if !eof(input)
        n_no_coor = Nullable{Int}(read(input, UInt64))
    else
        n_no_coor = Nullable{Int}()
    end

    return CSI(v, minshift, depth, aux, bins, n_no_coor)
end

function overlapchunks(csi::CSI, seqid::Integer, interval::UnitRange{<:Integer})
    if !(1 ≤ seqid ≤ endof(csi.bins))
        throw(ArgumentError("sequence id $(seqid) is out of range"))
    elseif isempty(interval)
        return Chunk[]
    end
    chunks = Chunk[]
    bins = reg2bins(interval, csi.minshift, csi.depth)
    binindex = csi.bins[seqid]
    for bin in bins
        for x in binindex
            if x.bin == bin
                for chunk in x.chunks
                    if chunk[2] > x.loffset
                        push!(chunks, chunk)
                    end
                end
            end
        end
    end
    sort!(chunks)
    reduce!(chunks)
    return chunks
end

# Calculate bins overlapping an interval (inclusive, one-based).
reg2bins(interval::UnitRange{<:Integer}, minshift::Integer, depth::Integer) =
    reg2bins(convert(UnitRange{Int}, interval),
             convert(Int, minshift),
             convert(Int, depth))
function reg2bins(interval::UnitRange{Int}, minshift::Int, depth::Int)
    bins = Int[]
    s = minshift + depth * 3
    bin_start = 0
    for d in 0:depth
        for k in ((first(interval)-1)>>s):((last(interval)-1)>>s)
            push!(bins, bin_start + k)
        end
        s -= 3
        bin_start = 8 * bin_start + 1
    end
    return bins
end


#####
##### Training hyperparameters
#####

Network = NetLib.ResNet

netparams = NetLib.ResNetHP(
  num_filters=128,
  num_blocks=5,
  conv_kernel_size=(3, 3),
  num_policy_head_filters=32,
  num_value_head_filters=32,
  batch_norm_momentum=0.1)

self_play = SelfPlayParams(
  sim=SimParams(
    num_games=1024,
    num_workers=32,
    batch_size=32,
    use_gpu=true,
    reset_every=2,
    flip_probability=0.,
    alternate_colors=false),
  mcts=MctsParams(
    num_iters_per_turn=200,
    cpuct=2.0,
    prior_temperature=1.0,
    temperature=PLSchedule([0, 20, 30], [1.0, 1.0, 0.3]),
    dirichlet_noise_ϵ=0.25,
    dirichlet_noise_α=0.3))

arena = ArenaParams(
  sim=SimParams(
    num_games=128,
    num_workers=32,
    batch_size=32,
    use_gpu=true,
    reset_every=2,
    flip_probability=0.0,
    alternate_colors=true),
  mcts=MctsParams(
    self_play.mcts,
    temperature=ConstSchedule(0.2),
    dirichlet_noise_ϵ=0.05),
  update_threshold=0.05)

learning = LearningParams(
  use_gpu=true,
  use_position_averaging=true,
  samples_weighing_policy=LOG_WEIGHT,
  batch_size=128,
  loss_computation_batch_size=128,
  optimiser=Adam(lr=2e-3),
  l2_regularization=1e-4,
  nonvalidity_penalty=1.,
  min_checkpoints_per_epoch=1,
  max_batches_per_checkpoint=2000,
  num_checkpoints=1)

params = Params(
  arena=arena,
  self_play=self_play,
  learning=learning,
  num_iters=45,
  ternary_rewards=true,
  use_symmetries=false,
  memory_analysis=nothing,
  mem_buffer_size=PLSchedule(
  [      0,        30],
  [50000, 180000]))

#####
##### Evaluation benchmark
#####

mcts_baseline =
  Benchmark.MctsRollouts(
    MctsParams(
      arena.mcts,
      num_iters_per_turn=1000,
      cpuct=1.))

minmax_baseline = Benchmark.MinMaxTS(
  depth=5,
  τ=0.2,
  amplify_rewards=true)

alphazero_player = Benchmark.Full(arena.mcts)

network_player = Benchmark.NetworkOnly(τ=0.5)

benchmark_sim = SimParams(
  arena.sim;
  num_games=128,
  num_workers=64,
  batch_size=64,
  alternate_colors=true)

benchmark = [
  Benchmark.Duel(alphazero_player, mcts_baseline,   benchmark_sim),
# Benchmark.Duel(alphazero_player, minmax_baseline, benchmark_sim),
  Benchmark.Duel(network_player,   mcts_baseline,   benchmark_sim),
# Benchmark.Duel(network_player,   minmax_baseline, benchmark_sim)
]

#####
##### Wrapping up in an experiment
#####

experiment = Experiment("amazon_full",
  GameSpec(), params, Network, netparams, benchmark)


## Multivariate ##

"""
    TimeSeriesModelStorage

Preallocated storage for model manipulation including preplanned ffts.
"""
abstract type TimeSeriesModelStorage end

struct Acv2EIStorage{N,M,D,L,P} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    hermitianarray::Base.ReinterpretArray{
        SHermitianCompact{D,ComplexF64,L},
        N,
        ComplexF64,
        Array{ComplexF64,N},
    }
    planned_fft::P
    kernel::Vector{Float64}
    function Acv2EIStorage(
        allocatedarray::Array{ComplexF64,M},
        hermitianarray::Base.ReinterpretArray{
            SHermitianCompact{D,ComplexF64,L},
            N,
            ComplexF64,
            Array{ComplexF64,N},
        },
        kernel,
        fft_flags,
    ) where {N,M,D,L}
        planned_fft = plan_fft!(allocatedarray, M, flags = fft_flags)
        new{N,M,D,L,typeof(planned_fft)}(allocatedarray, hermitianarray, planned_fft, kernel)
    end
end

struct Sdf2AcvStorage{M,Q} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    planned_ifft::Q
    function Sdf2AcvStorage(allocatedarray::Array{ComplexF64,M}, fft_flags) where {M}
        planned_ifft = plan_ifft!(allocatedarray, M, flags = fft_flags)
        new{M,typeof(planned_ifft)}(allocatedarray, planned_ifft)
    end
end

# Return the type of the fftplan from Acv2EIStorage and Sdf2AcvStorage types.
fft_plan_type(::Acv2EIStorage{N,M,D,L,P}) where {N,M,D,L,P} = P
fft_plan_type(::Sdf2AcvStorage{M,Q}) where {M,Q} = Q

# Struct to store required storage for computing EI when acv is not specified.
struct Sdf2EIStorage{N,M,D,L,Q,P} <: TimeSeriesModelStorage
    sdf2acv::Sdf2AcvStorage{M,Q}
    acv2EI::Acv2EIStorage{N,M,D,L,P}
    function Sdf2EIStorage(
        sdf_array::Array{ComplexF64,M},
        acv_array::Array{ComplexF64,M},
        hermitianarray::Base.ReinterpretArray{
            SHermitianCompact{D,ComplexF64,L},
            N,
            ComplexF64,
            Array{ComplexF64,N},
        },
        kernel,
        fft_flags,
    ) where {N,M,D,L}
        sdf2acv = Sdf2AcvStorage(sdf_array, fft_flags)
        acv2EI = Acv2EIStorage(acv_array, hermitianarray, kernel, fft_flags)
        new{N,M,D,L,fft_plan_type(sdf2acv),fft_plan_type(acv2EI)}(sdf2acv, acv2EI)
    end
end

# Generate required storage for computing the EI for a general time series model.
function EIstorage_function(model::Type{<:TimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, vec(acv_array))
    return Acv2EIStorage(acv_array, hermitianarray, kernel, fft_flags)
end
function EIstorage_function(model::Type{<:UnknownAcvTimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    sdf_array = zeros(ComplexF64, nlowertriangle_dimension(model), max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, vec(acv_array))
    return Sdf2EIStorage(sdf_array, acv_array, hermitianarray, kernel, fft_flags)
end

# Generate required storage for computing the gradient of EI for a general time series model.
function EIstorage_gradient(model::Type{<:TimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), npars(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, reshape(acv_array, (:, 2n)))
    return Acv2EIStorage(acv_array, hermitianarray, kernel, fft_flags)
end
function EIstorage_gradient(model::Type{<:UnknownAcvTimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    sdf_array = zeros(ComplexF64, nlowertriangle_dimension(model), npars(model), max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), npars(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, reshape(acv_array, (:, 2n)))
    return Sdf2EIStorage(sdf_array, acv_array, hermitianarray, kernel, fft_flags)
end

# Generate required storage for computing the hessian of EI for a general time series model.
function EIstorage_hessian(model::Type{<:TimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), nlowertriangle_parameter(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, reshape(acv_array, (:, 2n)))
    return Acv2EIStorage(acv_array, hermitianarray, kernel, fft_flags)
end
function EIstorage_hessian(model::Type{<:UnknownAcvTimeSeriesModel{D,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {D,T}
    sdf_array = zeros(ComplexF64, nlowertriangle_dimension(model), nlowertriangle_parameter(model), max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, nlowertriangle_dimension(model), nlowertriangle_parameter(model), 2n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(acv_array),nlowertriangle_dimension(model)}, reshape(acv_array, (:, 2n)))
    return Sdf2EIStorage(sdf_array, acv_array, hermitianarray, kernel, fft_flags)
end

#  Storage for useful quanties associated with the EI memory when estimating the acv, namely n, Δ and Ωₘ.
struct FreqAcvEst
    n::Int
    Δ::Float64
    Ωₘ::Frequencies{Float64}
    function FreqAcvEst(model::Type{<:UnknownAcvTimeSeriesModel},n,Δ)
        Ωₘ = fftfreq(max(2n, minbins(model)), 2π/Δ)
        new(n,Δ,Ωₘ)
    end
    function FreqAcvEst(n,Δ)
        Ωₘ = fftfreq(n, 2π/Δ)
        new(n,Δ,Ωₘ)
    end
end

# Storage for useful quanties associated with the EI memory when the acv is known, namely n, Δ and Ωₘ.
struct LagsEI
    n::Int
    Δ::Float64
    lags::Frequencies{Float64}
    function LagsEI(n,Δ)
        lags = fftfreq(2n, 2n*Δ)
        new(n,Δ,lags)
    end
end

# Function to choose additional storage depending on whether the acv is known or not.
encodetimescale(model::Type{<:TimeSeriesModel},n,Δ) = LagsEI(n,Δ)
encodetimescale(model::Type{<:UnknownAcvTimeSeriesModel},n,Δ) = FreqAcvEst(model,n,Δ)

abstract type TimeSeriesModelStorageFunction <: TimeSeriesModelStorage end
abstract type TimeSeriesModelStorageGradient <: TimeSeriesModelStorageFunction end
abstract type TimeSeriesModelStorageHessian  <: TimeSeriesModelStorageGradient end

# Structure to store memory if only the EI is required.
struct PreallocatedEI{T,F} <: TimeSeriesModelStorageFunction
    encodedtime::T
    funcmemory::F
    function PreallocatedEI(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = FFTW.MEASURE)
        time = encodetimescale(model, n, Δ)
        func = EIstorage_function(model, n, kernel, fft_flags)
        new{typeof(time),typeof(func)}(time,func)
    end
end

# Structure to store memory if the EI and its gradient is required.
struct PreallocatedEIGradient{T,F,G} <: TimeSeriesModelStorageGradient
    encodedtime::T
    funcmemory::F
    gradmemory::G
    function PreallocatedEIGradient(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = FFTW.MEASURE)
        time = encodetimescale(model, n, Δ)
        func = EIstorage_function(model, n, kernel, fft_flags)
        grad = EIstorage_gradient(model, n, kernel, fft_flags)
        new{typeof(time),typeof(func),typeof(grad)}(time, func, grad)
    end
end

# Structure to store memory if the EI, its gradient and its hessian are required.
struct PreallocatedEIHessian{T,F,G,H} <: TimeSeriesModelStorageHessian
    encodedtime::T
    funcmemory::F
    gradmemory::G
    hessmemory::H
    function PreallocatedEIHessian(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = FFTW.MEASURE)
        time = encodetimescale(model, n, Δ)
        func = EIstorage_function(model, n, kernel, fft_flags)
        grad = EIstorage_gradient(model, n, kernel, fft_flags)
        hess = EIstorage_hessian(model, n, kernel, fft_flags)
        new{typeof(time),typeof(func),typeof(grad),typeof(hess)}(time, func, grad, hess)
    end
end

# Storage for additive models.
struct AdditiveStorage{S₁<:TimeSeriesModelStorage,S₂<:TimeSeriesModelStorage} <: TimeSeriesModelStorage
    store1::S₁
    store2::S₂
    npar1::Int
end

# Helper function for extracting the function memory from additive storage (which is only stored in the left most storage).
extract_S(store::TimeSeriesModelStorageFunction) = store.funcmemory
extract_S(store::AdditiveStorage) = extract_S(store.store1) # returns the left hand store if additive storage

# tapering kernel computation
function taper2timedomainkernel(taper::AbstractVector{Float64},n)
    length(taper) == n || throw(ArgumentError("Taper is not of length n."))
    kernel = Vector{Float64}(undef,2n)
    for (i,τ) ∈ enumerate(0:n-1)
        kernel[i] = sum(taper[t]*taper[t+τ] for t ∈ 1:length(taper)-τ)
    end
    kernel[n+1] = 0
    for i in n+2:2n
        kernel[i] = kernel[2n-i+2]
    end
    return kernel
end
function taper2timedomainkernel(taper::Nothing,n)
    kernel = Vector{Float64}(undef,2n)
    for i ∈ 1:n
        kernel[i] = (1 - (i - 1) / n)
    end
    for i ∈ n+1:2n
        kernel[i] = (1 - (2n - i + 1) / n)
    end
    return kernel
end

# Interface level function for allocating the memory for EI computation.
function allocate_memory_EI_F(model::Type{<:TimeSeriesModel}, n, Δ; taper = nothing, fft_flags = FFTW.MEASURE)
    kernel = taper2timedomainkernel(taper,n)
    return _allocate_memory_EI_F(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_F(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags)
    return PreallocatedEI(model, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_F(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ, kernel; fft_flags) where {M₁,M₂,D,T}
    return AdditiveStorage( _allocate_memory_EI_F(M₁, n, Δ, kernel; fft_flags = fft_flags),
                            _allocate_memory_EI_F(M₂, n, Δ, kernel; fft_flags = fft_flags),
                            npars(M₁))
end

# Interface level function for allocating the memory for EI computation with gradient.
function allocate_memory_EI_FG(model::Type{<:TimeSeriesModel}, n, Δ; taper = nothing, fft_flags = FFTW.MEASURE)
    kernel = taper2timedomainkernel(taper,n)
    return _allocate_memory_EI_FG(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_FG(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags)
    return PreallocatedEIGradient(model, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_FG(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ, kernel; fft_flags) where {M₁,M₂,D,T}
    return AdditiveStorage( _allocate_memory_EI_FG(M₁, n, Δ, kernel; fft_flags = fft_flags),
                            _allocate_memory_EI_FG(M₂, n, Δ, kernel; fft_flags = fft_flags),
                            npars(M₁))
end

# Interface level function for allocating the memory for EI computation with gradient and hessian.
function allocate_memory_EI_FGH(model::Type{<:TimeSeriesModel}, n, Δ; taper = nothing, fft_flags = FFTW.MEASURE)
    kernel = taper2timedomainkernel(taper,n)
    return _allocate_memory_EI_FGH(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_FGH(model::Type{<:TimeSeriesModel}, n, Δ, kernel; fft_flags)
    return PreallocatedEIHessian(model, n, Δ, kernel; fft_flags = fft_flags)
end
function _allocate_memory_EI_FGH(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ, kernel; fft_flags) where {M₁,M₂,D,T}
    return AdditiveStorage( _allocate_memory_EI_FGH(M₁, n, Δ, kernel; fft_flags = fft_flags),
                            _allocate_memory_EI_FGH(M₂, n, Δ, kernel; fft_flags = fft_flags),
                            npars(M₁))
end

## sdf only storage

struct SdfStorage{N,M,D,L} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    hermitianarray::Base.ReinterpretArray{
        SHermitianCompact{D,ComplexF64,L},
        N,
        ComplexF64,
        Array{ComplexF64,N},
    }
    function SdfStorage(allocatedarray::Array{ComplexF64,M}, hermitianarray::Base.ReinterpretArray{
            SHermitianCompact{D,ComplexF64,L},
            N,
            ComplexF64,
            Array{ComplexF64,N},
        }) where {N,M,D,L}
        new{N,M,D,L}(allocatedarray,hermitianarray)
    end
end
# Function to allocate storage for the function part of sdf calculations.
function sdfstorage_function(model::Type{<:TimeSeriesModel{D,T}}, n) where {D,T}
    allocatedarray = zeros(ComplexF64, nlowertriangle_dimension(model), n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(allocatedarray),nlowertriangle_dimension(model)}, vec(allocatedarray))
    return SdfStorage(allocatedarray,hermitianarray)
end
# Function to allocate storage for the gradient part of sdf calculations.
function sdfstorage_gradient(model::Type{<:TimeSeriesModel{D,T}}, n) where {D,T}
    allocatedarray = zeros(ComplexF64, nlowertriangle_dimension(model), npars(model), n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(allocatedarray),nlowertriangle_dimension(model)}, reshape(allocatedarray, (:, n)))
    return SdfStorage(allocatedarray,hermitianarray)
end
# Function to allocate storage for the hessian part of sdf calculations.
function sdfstorage_hessian(model::Type{<:TimeSeriesModel{D,T}}, n) where {D,T}
    allocatedarray = zeros(ComplexF64, nlowertriangle_dimension(model), nlowertriangle_parameter(model), n)
    hermitianarray = reinterpret(SHermitianCompact{D,eltype(allocatedarray),nlowertriangle_dimension(model)}, reshape(allocatedarray, (:, n)))
    return SdfStorage(allocatedarray,hermitianarray)
end

# Structure to store memory if the sdf and its gradient is required.
struct PreallocatedSdf{F} <: TimeSeriesModelStorageFunction
    encodedtime::FreqAcvEst
    funcmemory::F
    function PreallocatedSdf(model::Type{<:TimeSeriesModel}, n, Δ)
        time = FreqAcvEst(n, Δ)
        func = sdfstorage_function(model, n)
        new{typeof(func)}(time, func)
    end
end

# Structure to store memory if the sdf and its gradient is required.
struct PreallocatedSdfGradient{F,G} <: TimeSeriesModelStorageGradient
    encodedtime::FreqAcvEst
    funcmemory::F
    gradmemory::G
    function PreallocatedSdfGradient(model::Type{<:TimeSeriesModel}, n, Δ)
        time = FreqAcvEst(n, Δ)
        func = sdfstorage_function(model, n)
        grad = sdfstorage_gradient(model, n)
        new{typeof(func),typeof(grad)}(time, func, grad)
    end
end

# Structure to store memory if the sdf, its gradient and its hessian are required.
struct PreallocatedSdfHessian{F,G,H} <: TimeSeriesModelStorageHessian
    encodedtime::FreqAcvEst
    funcmemory::F
    gradmemory::G
    hessmemory::H
    function PreallocatedSdfHessian(model::Type{<:TimeSeriesModel}, n, Δ)
        time = FreqAcvEst(n, Δ)
        func = sdfstorage_function(model, n)
        grad = sdfstorage_gradient(model, n)
        hess = sdfstorage_hessian(model, n)
        new{typeof(func),typeof(grad),typeof(hess)}(time, func, grad, hess)
    end
end

# Interface level function for allocating the memory for EI computation.
function allocate_memory_sdf_F(model::Type{<:TimeSeriesModel}, n, Δ)
    return PreallocatedSdf(model, n, Δ)
end
function allocate_memory_sdf_F(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ) where {M₁,M₂,D,T}
    return AdditiveStorage( allocate_memory_sdf_F(M₁, n, Δ),
                            allocate_memory_sdf_F(M₂, n, Δ),
                            npars(M₁))
end

# Interface level function for allocating the memory for sdf computation with gradient.
function allocate_memory_sdf_FG(model::Type{<:TimeSeriesModel}, n, Δ)
    return PreallocatedSdfGradient(model, n, Δ)
end
function allocate_memory_sdf_FG(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ) where {M₁,M₂,D,T}
    return AdditiveStorage( allocate_memory_sdf_FG(M₁, n, Δ),
                            allocate_memory_sdf_FG(M₂, n, Δ),
                            npars(M₁))
end

# Interface level function for allocating the memory for sdf computation with gradient and hessian.
function allocate_memory_sdf_FGH(model::Type{<:TimeSeriesModel}, n, Δ)
    return PreallocatedSdfHessian(model, n, Δ)
end
function allocate_memory_sdf_FGH(model::Type{AdditiveTimeSeriesModel{M₁,M₂,D,T}}, n, Δ) where {M₁,M₂,D,T}
    return AdditiveStorage( allocate_memory_sdf_FGH(M₁, n, Δ),
                            allocate_memory_sdf_FGH(M₂, n, Δ),
                            npars(M₁))
end

## Univariate ##

struct Acv2EIStorageUni{M,P} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    planned_fft::P
    kernel::Vector{Float64}
    function Acv2EIStorageUni(
        allocatedarray::Array{ComplexF64,M},
        kernel,
        fft_flags,
    ) where {M}
        planned_fft = plan_fft!(allocatedarray, M, flags = fft_flags)
        new{M,typeof(planned_fft)}(allocatedarray, planned_fft, kernel)
    end
end

struct Sdf2AcvStorageUni{M,Q} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    planned_ifft::Q
    function Sdf2AcvStorageUni(allocatedarray::Array{ComplexF64,M}, fft_flags) where {M}
        planned_ifft = plan_ifft!(allocatedarray, M, flags = fft_flags)
        new{M,typeof(planned_ifft)}(allocatedarray, planned_ifft)
    end
end

# Return the type of the fftplan from Acv2EIStorageUni and Sdf2AcvStorageUni types.
fft_plan_type(::Acv2EIStorageUni{M,P}) where {M,P} = P
fft_plan_type(::Sdf2AcvStorageUni{M,Q}) where {M,Q} = Q

# Struct to store required storage for computing EI when acv is not specified.
struct Sdf2EIStorageUni{M,Q,P} <: TimeSeriesModelStorage
    sdf2acv::Sdf2AcvStorageUni{M,Q}
    acv2EI::Acv2EIStorageUni{M,P}
    function Sdf2EIStorageUni(
        sdf_array::Array{ComplexF64,M},
        acv_array::Array{ComplexF64,M},
        kernel,
        fft_flags,
    ) where {M}
        sdf2acv = Sdf2AcvStorageUni(sdf_array, fft_flags)
        acv2EI = Acv2EIStorageUni(acv_array, kernel, fft_flags)
        new{M,fft_plan_type(sdf2acv),fft_plan_type(acv2EI)}(sdf2acv, acv2EI)
    end
end

# Generate required storage for computing the EI for a univariate time series model.
function EIstorage_function(model::Type{<:TimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    acv_array = zeros(ComplexF64, 2n)
    return Acv2EIStorageUni(acv_array, kernel, fft_flags)
end
function EIstorage_function(model::Type{<:UnknownAcvTimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    sdf_array = zeros(ComplexF64, max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, 2n)
    return Sdf2EIStorageUni(sdf_array, acv_array, kernel, fft_flags)
end

# Generate required storage for computing the gradient of EI for a univariate time series model.
function EIstorage_gradient(model::Type{<:TimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    acv_array = zeros(ComplexF64, npars(model), 2n)
    return Acv2EIStorageUni(acv_array, kernel, fft_flags)
end
function EIstorage_gradient(model::Type{<:UnknownAcvTimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    sdf_array = zeros(ComplexF64, npars(model), max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, npars(model), 2n)
    return Sdf2EIStorageUni(sdf_array, acv_array, kernel, fft_flags)
end

# Generate required storage for computing the hessian of EI for a univariate time series model.
function EIstorage_hessian(model::Type{<:TimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    acv_array = zeros(ComplexF64, nlowertriangle_parameter(model), 2n)
    return Acv2EIStorageUni(acv_array, kernel, fft_flags)
end
function EIstorage_hessian(model::Type{<:UnknownAcvTimeSeriesModel{1,T}}, n, kernel, fft_flags = FFTW.MEASURE) where {T}
    sdf_array = zeros(ComplexF64, nlowertriangle_parameter(model), max(2n, minbins(model)))
    acv_array = zeros(ComplexF64, nlowertriangle_parameter(model), 2n)
    return Sdf2EIStorageUni(sdf_array, acv_array, kernel, fft_flags)
end

## sdf only store
struct SdfStorageUni{M} <: TimeSeriesModelStorage
    allocatedarray::Array{ComplexF64,M}
    function SdfStorageUni(allocatedarray::Array{ComplexF64,M}) where {M}
        new{M}(allocatedarray)
    end
end

# Function to allocate storage for the function part of sdf calculations for univariate case.
function sdfstorage_function(model::Type{<:TimeSeriesModel{1,T}}, n) where {T}
    allocatedarray = zeros(ComplexF64, n)
    return SdfStorageUni(allocatedarray)
end
# Function to allocate storage for the gradient part of sdf calculations for univariate case.
function sdfstorage_gradient(model::Type{<:TimeSeriesModel{1,T}}, n) where {T}
    allocatedarray = zeros(ComplexF64, npars(model), n)
    return SdfStorageUni(allocatedarray)
end
# Function to allocate storage for the hessian part of sdf calculations for univariate case.
function sdfstorage_hessian(model::Type{<:TimeSeriesModel{1,T}}, n) where {T}
    allocatedarray = zeros(ComplexF64, nlowertriangle_parameter(model), n)
    return SdfStorageUni(allocatedarray)
end

## Unitily functions for pulling out desired vector from storage for Whittle
unpack(store::Sdf2EIStorage)    = store.acv2EI.hermitianarray
unpack(store::Acv2EIStorage)    = store.hermitianarray
unpack(store::SdfStorage)       = store.hermitianarray
unpack(store::Sdf2EIStorageUni) = store.acv2EI.allocatedarray
unpack(store::Acv2EIStorageUni) = store.allocatedarray
unpack(store::SdfStorageUni)    = store.allocatedarray


struct Cdouble6
    x1::Cdouble
    x2::Cdouble
    x3::Cdouble
    x4::Cdouble
    x5::Cdouble
    x6::Cdouble
end

struct Cdouble15
    x1::Cdouble
    x2::Cdouble
    x3::Cdouble
    x4::Cdouble
    x5::Cdouble
    x6::Cdouble
    x7::Cdouble
    x8::Cdouble
    x9::Cdouble
    x10::Cdouble
    x11::Cdouble
    x12::Cdouble
    x13::Cdouble
    x14::Cdouble
    x15::Cdouble
end

struct Cdouble21
    x1::Cdouble
    x2::Cdouble
    x3::Cdouble
    x4::Cdouble
    x5::Cdouble
    x6::Cdouble
    x7::Cdouble
    x8::Cdouble
    x9::Cdouble
    x10::Cdouble
    x11::Cdouble
    x12::Cdouble
    x13::Cdouble
    x14::Cdouble
    x15::Cdouble
    x16::Cdouble
    x17::Cdouble
    x18::Cdouble
    x19::Cdouble
    x20::Cdouble
    x21::Cdouble
end

mutable struct geod_geodesic <: _geodesic
    a::Cdouble
    f::Cdouble
    f1::Cdouble
    e2::Cdouble
    ep2::Cdouble
    n::Cdouble
    b::Cdouble
    c2::Cdouble
    etol2::Cdouble

    # Arrays of parameters must be expanded manually,
    # currently (either inline, or in an immutable helper-type)
    # In the future, some of these restrictions may be reduced/eliminated.
    A3x::Cdouble6
    C3x::Cdouble15
    C4x::Cdouble21

    geod_geodesic() = new()
end

"""
Construct an ellipsoid of revolution with

    equatorial radius a,
    flattening f,

Remark, we construct it when constructing a [Projection] using the formulas:

    f = (a − b)/a = 1 − sqrt(1-es)
    n = (a − b)/(a + b) = f /(2 − f)
    es = (a^2 − b^2)/a^2 = f(2 − f)

Reference: equations (1)-(3) of
    Algorithms for geodesics (arXiv:1109.4448v2 [physics.geo-ph] 28 Mar 2012)
"""
function geod_geodesic(a::Cdouble, f::Cdouble)
    geod = geod_geodesic()
    ccall((:geod_init, libproj), Cvoid, (Ptr{Cvoid},Cdouble,Cdouble),
          pointer_from_objref(geod), a, f)
    geod
end

"""
Solve the direct geodesic problem.

Args:

    g        - the geod_geodesic object specifying the ellipsoid.
    lonlat   - where lat ∈ [-90, 90], lon ∈ [-540, 540), modified in-place to [dest] (described below)
    azimuth  - azimuth (degrees) ∈ [-540, 540)
    distance - distance (metres) to move from (lon,lat); can be negative

Returns:

    dest     - destination after moving for [distance] metres in [azimuth] direction.
    azi      - forward azimuth (degrees) at destination [dest].

Remarks:

    If either point is at a pole, the azimuth is defined by keeping the longitude fixed,
    writing lat = 90 +/- eps, and taking the limit as eps -> 0+. An arc length greater than 180deg
    signifies a geodesic which is not a shortest path.
"""
function _geod_direct!(geod::geod_geodesic, lonlat::Vector{Cdouble}, azimuth::Cdouble, distance::Cdouble)
    p = pointer(lonlat)
    azi = Ref{Cdouble}() # the (forward) azimuth at the destination
    ccall((:geod_direct, libproj),Cvoid,(Ptr{Cvoid},Cdouble,Cdouble,Cdouble,Cdouble,Ptr{Cdouble},Ptr{Cdouble},
          Ptr{Cdouble}), pointer_from_objref(geod), lonlat[2], lonlat[1], azimuth, distance, p+sizeof(Cdouble), p, azi)
    lonlat, azi[]
end

"""
Solve the inverse geodesic problem.

Args:

    g       - the geod_geodesic object specifying the ellipsoid.
    lonlat1 - point 1 (degrees), where lat ∈ [-90, 90], lon ∈ [-540, 540)
    lonlat2 - point 2 (degrees), where lat ∈ [-90, 90], lon ∈ [-540, 540)

Returns:

    dist    - distance between point 1 and point 2 (meters).
    azi1    - azimuth at point 1 (degrees) ∈ [-180, 180)
    azi2    - (forward) azimuth at point 2 (degrees) ∈ [-180, 180)

Remarks:

    If either point is at a pole, the azimuth is defined by keeping the longitude fixed,
    writing lat = 90 +/- eps, and taking the limit as eps -> 0+.
"""
function _geod_inverse(geod::geod_geodesic, lonlat1::Vector{Cdouble}, lonlat2::Vector{Cdouble})
    dist = Ref{Cdouble}()
    azi1 = Ref{Cdouble}()
    azi2 = Ref{Cdouble}()
    ccall((:geod_inverse, libproj), Cvoid, (Ptr{Cvoid},Cdouble,Cdouble,Cdouble,
          Cdouble,Ptr{Cdouble},Ptr{Cdouble},Ptr{Cdouble}),
          pointer_from_objref(geod), lonlat1[2], lonlat1[1], lonlat2[2], lonlat2[1], dist, azi1, azi2)
    dist[], azi1[], azi2[]
end


function testend()
  testelement(End(),".end\n")
end
testend()

function testnode()
  testelement(Node(1,1,1),
  "N_1 x=1.000000000e+00 y=1.000000000e+00 z=1.000000000e+00\n" )
  testelement(Node(:abc,2,2,2),
  "Nabc x=2.000000000e+00 y=2.000000000e+00 z=2.000000000e+00\n")
  testelement(Node([3.0,3.0,3.0]),
  "N_1 x=3.000000000e+00 y=3.000000000e+00 z=3.000000000e+00\n")
  testelement(Node(x=4, y=4, z=4),
  "N_1 x=4.000000000e+00 y=4.000000000e+00 z=4.000000000e+00\n")
  testelement(Node(name=:abc, x=5, y=5, z=5),
  "Nabc x=5.000000000e+00 y=5.000000000e+00 z=5.000000000e+00\n")
  coordinate = [1.0,2.0,3.0]
  n1 = Node(coordinate)
  @test xyz(n1) == coordinate
end
testnode()

function testmatrices()
  result = [1.0   0.0       0.0       0.0
         0.0   0.707107  0.707107  0.0
         0.0  -0.707107  0.707107  0.0
         0.0   0.0       0.0       1.0]
  @test sum(rx(π / 4) - result) ≈ 0.0 atol=1.0e-5

  result = [0.707107  0.0  -0.707107  0.0
   0.0       1.0   0.0       0.0
   0.707107  0.0   0.707107  0.0
   0.0       0.0   0.0       1.0]
  @test sum(ry(π / 4) - result) ≈ 0.0 atol=1.0e-5

  result = [0.707107  0.707107  0.0  0.0
   -0.707107  0.707107  0.0  0.0
    0.0       0.0       1.0  0.0
    0.0       0.0       0.0  1.0]
  @test sum(rz(π / 4) - result) ≈ 0.0 atol=1.0e-5

  result = [1.0  0.0  0.0  1.0
   0.0  1.0  0.0  2.0
   0.0  0.0  1.0  3.0
   0.0  0.0  0.0  1.0]
  @test sum(txyz(1.0, 2.0, 3.0) - result) ≈ 0.0 atol=1.0e-5

  result = [ 1.0  0.0  0.0  0.0
   0.0  2.0  0.0  0.0
   0.0  0.0  3.0  0.0
   0.0  0.0  0.0  1.0]
  @test sum(scalexyz(1.0, 2.0, 3.0) - result) ≈ 0.0 atol=1.0e-5
end
testmatrices()

function testsigmarho()
  @test_throws(ArgumentError,FastHenryHelper.SigmaRho(sigma=1.0, rho=2.0))
  sr1 = FastHenryHelper.SigmaRho(sigma=1.0)
  @test sr1.sigma == 1.0
  @test isnan(sr1.rho)
  sr2 = FastHenryHelper.SigmaRho(rho=1.0)
  @test sr2.rho == 1.0
  @test isnan(sr2.sigma)
end
testsigmarho()

function testwxwywz()
  w1 = FastHenryHelper.WxWyWz(wx=1.0,wy=2.0,wz=3.0)
  @test w1.xyz == [1.0, 2.0, 3.0]
  @test w1.isdefault == false
  w2 = FastHenryHelper.WxWyWz()
  @test isnan(w2.xyz[1]) && isnan(w2.xyz[2]) && isnan(w2.xyz[3])
  @test w2.isdefault == true
end
testwxwywz()

function testwh()
  @test_throws(ArgumentError,FastHenryHelper.WH(nhinc=-1))
  @test_throws(ArgumentError,FastHenryHelper.WH(nwinc=-1))
  @test_throws(ArgumentError,FastHenryHelper.WH(rh=-1))
  @test_throws(ArgumentError,FastHenryHelper.WH(rw=-1))
  wh1 = FastHenryHelper.WH()
  @test isnan(wh1.w)
  @test isnan(wh1.h)
  @test wh1.nhinc == 0
  @test wh1.nwinc == 0
  @test isnan(wh1.rh)
  @test isnan(wh1.rw)
  wh2 = FastHenryHelper.WH(w=1.0,h=2.0,nhinc=3.0,nwinc=4.0,rh=5.0,rw=6.0)
  @test wh2.w == 1.0
  @test wh2.h == 2.0
  @test wh2.nhinc == 3.0
  @test wh2.nwinc == 4.0
  @test wh2.rh == 5.0
  @test wh2.rw == 6.0
end
testwh()

function testsegmentparameters()
  sp1 = SegmentParameters(w=1.0,h=2.0,sigma=3.0,wx=4.0,wy=5.0,wz=6.0,
                          nhinc=7.0,nwinc=8.0,rh=9,rw=10)
  sp2 = SegmentParameters(sp1,)
  sp3 = SegmentParameters(sp1,w=21.0,h=22.0,sigma=23.0,wx=24.0,wy=25.0,wz=26.0,
                          nhinc=27.0,nwinc=28.0,rh=29,rw=210)
  @test FastHenryHelper.w(sp2) == 1.0
  @test FastHenryHelper.h(sp2) == 2.0
  @test FastHenryHelper.sigma(sp2) == 3.0
  @test FastHenryHelper.wx(sp2) == 4.0
  @test FastHenryHelper.wy(sp2) == 5.0
  @test FastHenryHelper.wz(sp2) == 6.0
  @test FastHenryHelper.nhinc(sp2) == 7.0
  @test FastHenryHelper.nwinc(sp2) == 8.0
  @test FastHenryHelper.rh(sp2) == 9
  @test FastHenryHelper.rw(sp2) == 10
  @test isnan(FastHenryHelper.rho(sp2))
  @test FastHenryHelper.w(sp3) == 21.0
  @test FastHenryHelper.h(sp3) == 22.0
  @test FastHenryHelper.sigma(sp3) == 23.0
  @test FastHenryHelper.wx(sp3) == 24.0
  @test FastHenryHelper.wy(sp3) == 25.0
  @test FastHenryHelper.wz(sp3) == 26.0
  @test FastHenryHelper.nhinc(sp3) == 27.0
  @test FastHenryHelper.nwinc(sp3) == 28.0
  @test FastHenryHelper.rh(sp3) == 29
  @test FastHenryHelper.rw(sp3) == 210
  @test isnan(FastHenryHelper.rho(sp3))
end
testsegmentparameters()

function testsegment()
  sp1 = SegmentParameters(w=1.0,h=2.0,sigma=3.0,wx=4.0,wy=5.0,wz=6.0,
                          nhinc=7.0,nwinc=8.0,rh=9,rw=10)
  n1 = Node(1,2,3)
  n2 = Node(4,5,6)
  s1 = Segment(n1,n2,sp1)
  verified =
  """
  E_1 N_2 N_3
  +  w=1.000000000e+00 h=2.000000000e+00 nhinc=7 nwinc=8 rh=9.000000000e+00 rw=1.000000000e+01
  +  sigma=3.000000000e+00
  +  wx=-7.071067812e-01 wy=0.000000000e+00 wz=7.071067812e-01
  """
  @test_logs (:warn,"wx,wy,wz not perpendicular to segment length") testelement(s1,verified)
  n3 = Node(0,0,0)
  n4 = Node(1,0,0)
  s2 = Segment(n3,n4,w=.5,h=.2)
  verified =
  """
  E_1 N_2 N_3
  +  w=5.000000000e-01 h=2.000000000e-01
  """
  testelement(s2,verified)
  s3 = Segment(n3,n4,w=.5,h=.2,wx=0,wy=1,wz=0)
  verified =
  """
  E_1 N_2 N_3
  +  w=5.000000000e-01 h=2.000000000e-01
  +  wx=0.000000000e+00 wy=1.000000000e+00 wz=0.000000000e+00
  """
  testelement(s3,verified)
  s4 = Segment(n3,n4,w=.5,h=.2,wx=1,wy=1,wz=0)
  verified =
  """
  E_1 N_2 N_3
  +  w=5.000000000e-01 h=2.000000000e-01
  +  wx=0.000000000e+00 wy=1.000000000e+00 wz=0.000000000e+00
  """
  @test_logs (:warn,"wx,wy,wz not perpendicular to segment length") testelement(s4,verified)
  s5 = Segment(n3,n4,w=.5,h=.2,wx=0,wy=0,wz=1)
  verified =
  """
  E_1 N_2 N_3
  +  w=5.000000000e-01 h=2.000000000e-01
  +  wx=0.000000000e+00 wy=0.000000000e+00 wz=1.000000000e+00
  """
  testelement(s5,verified)
  s6 = Segment(n3,n4,w=.5,h=.2,wx=1,wy=1,wz=1)
  verified =
  """
  E_1 N_2 N_3
  +  w=5.000000000e-01 h=2.000000000e-01
  +  wx=-0.000000000e+00 wy=7.071067812e-01 wz=7.071067812e-01
  """
  @test_logs (:warn,"wx,wy,wz not perpendicular to segment length") testelement(s6,verified)
  @test_throws(ArgumentError,Segment(n1,n2,w=1,h=2,wx=0,wy=0,wz=0))
end
testsegment()

function testcomment()
  c = Comment("abcd")
  testelement(c,"* abcd\n")
end
testcomment()

function testdefault()
  sp1 = SegmentParameters(w=1.0,h=2.0,sigma=3.0,wx=4.0,wy=5.0,wz=6.0,
                          nhinc=7.0,nwinc=8.0,rh=9,rw=10)
  def1 = Default(sp1)
  verified =
  """
  .default
  +  w=1.000000000e+00 h=2.000000000e+00 nhinc=7 nwinc=8 rh=9.000000000e+00 rw=1.000000000e+01
  +  sigma=3.000000000e+00
  """
  testelement(def1,verified)
  def2 = Default(rho=1.0)
  verified =
  """
  .default
  +  rho=1.000000000e+00
  """
  testelement(def2,verified)
end
testdefault()

function testequiv()
  n3 = Node(:abc,3,3,3)
  n4 = Node(:def,4,4,4)
  eq1 = Equiv([n3,n4])
  testelement(eq1,".equiv Nabc Ndef\n")
  n5 = Node(5,5,5)
  n6 = Node(6,6,6)
  eq2 = Equiv([n5,n6])
  testelement(eq2,".equiv N_1 N_2\n")
end
testequiv()

function testexternal()
  n3 = Node(:abc,3,3,3)
  n4 = Node(:def,4,4,4)
  ex1 = External(n3,n4,"portname")
  testelement(ex1,".external Nabc Ndef portname\n")
  n5 = Node(5,5,5)
  n6 = Node(6,6,6)
  ex2 = External(n5,n6,"portname")
  testelement(ex2,".external N_1 N_2 portname\n")
end
testexternal()

function testfreq()
  @test_throws(ArgumentError,Freq())
  @test_throws(ArgumentError,Freq(min=100,max=10))
  @test_throws(ArgumentError,Freq(max=1000, ndec=-10))
  f1 = Freq(min=1,max=2,ndec=3)
  testelement(f1,".freq fmin=1.000000000e+00 fmax=2.000000000e+00 ndec=3.000000000e+00\n")
end
testfreq()

function testunits()
  @test_throws(ArgumentError,Units("kk"))
  u = Units("in")
  testelement(u,".units in\n")
end
testunits()

function testuniformplane()
  p = Point(x=1,y=2,z=3)
  testelement(p,"+ hole point (1.000000000e+00, 2.000000000e+00, 3.000000000e+00)\n")
  r = Rect(x1=1,y1=2,z1=3,x2=4,y2=5,z2=6)
  testelement(r,"+ hole rect (1.000000000e+00, 2.000000000e+00, 3.000000000e+00, 4.000000000e+00, 5.000000000e+00, 6.000000000e+00)\n")
  c = Circle(x=1,y=2,z=3,r=4)
  testelement(c,"+ hole circle (1.000000000e+00, 2.000000000e+00, 3.000000000e+00, 4.000000000e+00)\n")
  @test_throws(ArgumentError,UniformPlane(thick=1, seg1=2, seg2=3, nhinc=4,rh=5,sigma=6,rho=7))
  @test_throws(ArgumentError,UniformPlane(thick=1, seg1=2, seg2=3, nhinc=4,rh=-5,sigma=6))
  @test_throws(ArgumentError,UniformPlane(thick=1, seg1=2, seg2=3, nhinc=-4,rh=5,rho=7))
  @test_throws(ArgumentError,UniformPlane(thick=1, seg1=2, seg2=0, nhinc=4,rh=5,sigma=6))
  @test_throws(ArgumentError,UniformPlane(thick=1, seg1=0, seg2=3, nhinc=4,rh=5,sigma=6))
  @test_throws(ArgumentError,UniformPlane(seg1=2, seg2=3, nhinc=4,rh=5,sigma=6))
  @test_throws(ArgumentError,
    UniformPlane(
    x1=1, y1=2, z1=10,
    x2=1, y2=1, z2=10,
    x3=2, y3=1, z3=10,
    thick=2, seg1=11, seg2=12,
    segwid1 = 13, segwid2 = 14,
    sigma = 15,
    nhinc=16, rh=17,
    relx=18, rely=19, relz=20,
    nodes=[Node(1,1,11.5)]))
  n10 = Node(10,11,1)
  n11 = Node(13,14,1)
  up = UniformPlane(
  x1=1, y1=2, z1=1,
  x2=1, y2=1, z2=1,
  x3=2, y3=1, z3=1,
  thick=10, seg1=11, seg2=12,
  segwid1 = 13, segwid2 = 14,
  sigma = 15,
  nhinc=16, rh=17,
  relx=18, rely=19, relz=20,
  nodes=[n10,n11],
  holes=[p,r,c])
  verified =
  """
  G_1
  + x1=1.000000000e+00 y1=2.000000000e+00 z1=1.000000000e+00
  + x2=1.000000000e+00 y2=1.000000000e+00 z2=1.000000000e+00
  + x3=2.000000000e+00 y3=1.000000000e+00 z3=1.000000000e+00
  + thick=1.000000000e+01 seg1=11 seg2=12
  + segwid1=1.300000000e+01
  + segwid2=1.400000000e+01
  + sigma=1.500000000e+01
  + nhinc=16
  + rh=17
  + relx=1.800000000e+01
  + rely=1.900000000e+01
  + relz=2.000000000e+01
  + N_2 (1.000000000e+01,1.100000000e+01,1.000000000e+00)
  + N_3 (1.300000000e+01,1.400000000e+01,1.000000000e+00)
  + hole point (1.000000000e+00, 2.000000000e+00, 3.000000000e+00)
  + hole rect (1.000000000e+00, 2.000000000e+00, 3.000000000e+00, 4.000000000e+00, 5.000000000e+00, 6.000000000e+00)
  + hole circle (1.000000000e+00, 2.000000000e+00, 3.000000000e+00, 4.000000000e+00)
  """
  testelement(up,verified)
  up2 = deepcopy(up)
  transform!(up2,txyz(1,2,3))
  verified =
  """
  G_1
  + x1=2.000000000e+00 y1=4.000000000e+00 z1=4.000000000e+00
  + x2=2.000000000e+00 y2=3.000000000e+00 z2=4.000000000e+00
  + x3=3.000000000e+00 y3=3.000000000e+00 z3=4.000000000e+00
  + thick=1.000000000e+01 seg1=11 seg2=12
  + segwid1=1.300000000e+01
  + segwid2=1.400000000e+01
  + sigma=1.500000000e+01
  + nhinc=16
  + rh=17
  + relx=1.800000000e+01
  + rely=1.900000000e+01
  + relz=2.000000000e+01
  + N_2 (1.100000000e+01,1.300000000e+01,4.000000000e+00)
  + N_3 (1.400000000e+01,1.600000000e+01,4.000000000e+00)
  + hole point (2.000000000e+00, 4.000000000e+00, 6.000000000e+00)
  + hole rect (2.000000000e+00, 4.000000000e+00, 6.000000000e+00, 5.000000000e+00, 7.000000000e+00, 9.000000000e+00)
  + hole circle (2.000000000e+00, 4.000000000e+00, 6.000000000e+00, 4.000000000e+00)
  """
  testelement(up2,verified)
end
testuniformplane()

function testgroup()
  title = Comment("test title")
  n20 = Node(1,0,0)
  n21 = Node(10,0,0)
  sp20 = SegmentParameters(h=3,w=5,sigma=0.1)
  seg20 = Segment(n20,n21,sp20)
  g1 = Group([title,n20,n21,seg20],Dict(:a=>n20,:b=>n21))
  g2 = transform(g1,rz(π/4))
  g3 = Group([g1,g2],Dict(:c=>g1[:a], :d=>g1[:b], :e=>g2[:a], :f=>g2[:b]))
  testelement(g3[:c],"N_1 x=1.000000000e+00 y=0.000000000e+00 z=0.000000000e+00\n")
  testelement(g3[:d],"N_1 x=1.000000000e+01 y=0.000000000e+00 z=0.000000000e+00\n")
  testelement(g3[:e],"N_1 x=7.071067812e-01 y=-7.071067812e-01 z=0.000000000e+00\n")
  testelement(g3[:f],"N_1 x=7.071067812e+00 y=-7.071067812e+00 z=0.000000000e+00\n")
  verified =
  """
  * test title
  N_1 x=1.000000000e+00 y=0.000000000e+00 z=0.000000000e+00
  N_2 x=1.000000000e+01 y=0.000000000e+00 z=0.000000000e+00
  E_3 N_1 N_2
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  * test title
  N_4 x=7.071067812e-01 y=-7.071067812e-01 z=0.000000000e+00
  N_5 x=7.071067812e+00 y=-7.071067812e+00 z=0.000000000e+00
  E_6 N_4 N_5
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  +  wx=7.071067812e-01 wy=7.071067812e-01 wz=0.000000000e+00
  """
  testelement(g3,verified)
  u = Units("in")
  def1 = Default(sigma = 1.234)
  n3 = Node(:abc,3,3,3)
  n4 = Node(:def,4,4,4)
  eq1 = Equiv([n3,n4])
  n5 = Node(:geh,3,3,3)
  n6 = Node(:ijk,4,4,4)
  ex1 = External(n5,n6,"portname")
  f1 = Freq(min=1,max=2,ndec=3)
  p = FastHenryHelper.Point(x=1,y=2,z=3)
  r = FastHenryHelper.Rect(x1=1,y1=2,z1=3,x2=4,y2=5,z2=6)
  c = FastHenryHelper.Circle(x=1,y=2,z=3,r=4)
  n10 = Node(10,11,1)
  n11 = Node(13,14,1)
  up = UniformPlane(
  x1=1, y1=2, z1=1,
  x2=1, y2=1, z2=1,
  x3=2, y3=1, z3=1,
  thick=10, seg1=11, seg2=12,
  segwid1 = 13, segwid2 = 14,
  sigma = 15,
  nhinc=16, rh=17,
  relx=18, rely=19, relz=20,
  nodes=[n10,n11],
  holes=[p,r,c])
  g4 = Group([title,u,n20,n21,n3,n4,n5,n6,seg20,def1,eq1,up,ex1,f1])
  g5 = transform(g4,rx(π/2))
  verified =
  """
  * test title
  .units in
  N_1 x=1.000000000e+00 y=0.000000000e+00 z=0.000000000e+00
  N_2 x=1.000000000e+01 y=0.000000000e+00 z=0.000000000e+00
  Nabc x=3.000000000e+00 y=3.000000000e+00 z=-3.000000000e+00
  Ndef x=4.000000000e+00 y=4.000000000e+00 z=-4.000000000e+00
  Ngeh x=3.000000000e+00 y=3.000000000e+00 z=-3.000000000e+00
  Nijk x=4.000000000e+00 y=4.000000000e+00 z=-4.000000000e+00
  E_3 N_1 N_2
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  +  wx=0.000000000e+00 wy=6.123233996e-17 wz=-1.000000000e+00
  .default
  +  sigma=1.234000000e+00
  .equiv Nabc Ndef
  G_4
  + x1=1.000000000e+00 y1=1.000000000e+00 z1=-2.000000000e+00
  + x2=1.000000000e+00 y2=1.000000000e+00 z2=-1.000000000e+00
  + x3=2.000000000e+00 y3=1.000000000e+00 z3=-1.000000000e+00
  + thick=1.000000000e+01 seg1=11 seg2=12
  + segwid1=1.300000000e+01
  + segwid2=1.400000000e+01
  + sigma=1.500000000e+01
  + nhinc=16
  + rh=17
  + relx=1.800000000e+01
  + rely=1.900000000e+01
  + relz=2.000000000e+01
  + N_5 (1.000000000e+01,1.000000000e+00,-1.100000000e+01)
  + N_6 (1.300000000e+01,1.000000000e+00,-1.400000000e+01)
  + hole point (1.000000000e+00, 3.000000000e+00, -2.000000000e+00)
  + hole rect (1.000000000e+00, 3.000000000e+00, -2.000000000e+00, 4.000000000e+00, 6.000000000e+00, -5.000000000e+00)
  + hole circle (1.000000000e+00, 3.000000000e+00, -2.000000000e+00, 4.000000000e+00)
  .external Ngeh Nijk portname
  .freq fmin=1.000000000e+00 fmax=2.000000000e+00 ndec=3.000000000e+00
  """
  testelement(g5,verified)
  c1 = Comment("this is a comment")
  # Default and Freq are immutable
  g6 = Group([title,c1,u,n20,n21,n3,n4,n5,n6,seg20,eq1,up,ex1])
  deepcopyg6 = deepcopy(g6)
  for i in (4,5,6,7,8,9,10) # only verify mutable elements
    @test ~(elements(deepcopyg6)[i] === elements(g6)[i])
  end

  g7 = Group()
  elements!(g7,[title,n20,n21,seg20])
  terms!(g7,Dict(:a=>n20,:b=>n21))
  g7[:c] = n20
  @test g7[:a] === g7[:c]
  deepcopyg7 = deepcopy(g7)
  @test elements(deepcopyg7)[4].node1 === elements(deepcopyg7)[2]
  @test elements(deepcopyg7)[4].node2 === elements(deepcopyg7)[3]
  push!(g1,n20)
  @test pop!(g1) === n20
  pushfirst!(g1,n20)
  @test popfirst!(g1) === n20
  append!(g1,g2)
  prepend!(g1,g2)
  merge!(g1,g2,g3)
  verified =
  """
  * test title
  N_1 x=7.071067812e-01 y=-7.071067812e-01 z=0.000000000e+00
  N_2 x=7.071067812e+00 y=-7.071067812e+00 z=0.000000000e+00
  E_3 N_1 N_2
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  +  wx=7.071067812e-01 wy=7.071067812e-01 wz=0.000000000e+00
  * test title
  N_4 x=1.000000000e+00 y=0.000000000e+00 z=0.000000000e+00
  N_5 x=1.000000000e+01 y=0.000000000e+00 z=0.000000000e+00
  E_6 N_4 N_5
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  * test title
  N_1 x=7.071067812e-01 y=-7.071067812e-01 z=0.000000000e+00
  N_2 x=7.071067812e+00 y=-7.071067812e+00 z=0.000000000e+00
  E_3 N_1 N_2
  +  w=5.000000000e+00 h=3.000000000e+00
  +  sigma=1.000000000e-01
  +  wx=7.071067812e-01 wy=7.071067812e-01 wz=0.000000000e+00
  """
  testelement(g1,verified)
  g7 = Group([Node(1,2,3)])
  g8 = Group([g7])
  g9 = Group([g8])
  g10 = Group([g9])
  g11 = Group([g10])
  g12 = Group([g11])
  testelement(g12,"N_1 x=1.000000000e+00 y=2.000000000e+00 z=3.000000000e+00\n")
  for element in FastHenryHelper.traverseTree(g12)
    @test typeof(element) != Group
  end
end
testgroup()

function testgroup2()
  g1 = Group()
  g2 = Group([Node(1,1,1)])
  a=Node(1,1,1)
  b=Node(2,2,2)
  g3 = Group([a],Dict(:terma=>a))
  g4 = Group([a,b],Dict(:terma=>[a,b]))
  g5 = Group([a],Dict(:terma=>a),Units("mm"))
end
testgroup2()

function testrecursivedeepcopy()
  g1 = Group([Node(1,1,1)])
  e = elements(g1)
  push!(e,Node(2,2,2))
  push!(e,g1);
  g2 = deepcopy(g1);
  @test ~(g2===g1)
end
testrecursivedeepcopy()


function __init__emnist()
    DEPNAME = "EMNIST"

    register(DataDep(
        DEPNAME,
        """
        Dataset: The EMNIST Dataset
        Authors: Gregory Cohen, Saeed Afshar, Jonathan Tapson, and Andre van Schaik
        Website: https://www.nist.gov/itl/products-and-services/emnist-dataset

        [Cohen et al., 2017]
            Cohen, G., Afshar, S., Tapson, J., & van Schaik, A. (2017).
            EMNIST: an extension of MNIST to handwritten letters.
            Retrieved from http://arxiv.org/abs/1702.05373

        The EMNIST dataset is a set of handwritten character digits derived from the
        NIST Special Database 19 (https://www.nist.gov/srd/nist-special-database-19)
        and converted to a 28x28 pixel image format and dataset structure that directly
        matches the MNIST dataset (http://yann.lecun.com/exdb/mnist/). Further information
        on the dataset contents and conversion process can be found in the paper available
        at https://arxiv.org/abs/1702.05373v1.

        The files are available for download at the official
        website linked above. Note that using the data
        responsibly and respecting copyright remains your
        responsibility. For example the website mentions that
        the data is for non-commercial use only. Please read
        the website to make sure you want to download the
        dataset.
        """,
        "http://www.itl.nist.gov/iaui/vip/cs_links/EMNIST/matlab.zip",
        "e1fa805cdeae699a52da0b77c2db17f6feb77eed125f9b45c022e7990444df95",
        post_fetch_method = file -> (run(BinDeps.unpack_cmd(file, dirname(file), ".zip", "")); rm(file))
    ))
end

"""
    EMNIST(name; Tx=Float32, split=:train, dir=nothing)
    EMNIST(name, [Tx, split])

The EMNIST dataset is a set of handwritten character digits derived from the
NIST Special Database 19 (https://www.nist.gov/srd/nist-special-database-19)
and converted to a 28x28 pixel image format and dataset structure that directly
matches the MNIST dataset (http://yann.lecun.com/exdb/mnist/). Further information
on the dataset contents and conversion process can be found in the paper available
at https://arxiv.org/abs/1702.05373v1.

# Arguments

- `name`: name of the EMNIST dataset. Possible values are: `:balanced, :byclass, :bymerge, :digits, :letters, :mnist`.
- `split`: selects the data partition. Can take the values `:train:` or `:test`.
$ARGUMENTS_SUPERVISED_ARRAY

# Fields

- `name`.
- `split`.
$FIELDS_SUPERVISED_ARRAY

# Methods

$METHODS_SUPERVISED_ARRAY
- [`convert2image`](@ref) converts features to `Gray` images.

# Examples

The images are loaded as a multi-dimensional array of eltype `Tx`.
If `Tx <: Integer`, then all values will be within `0` and `255`,
otherwise the values are scaled to be between `0` and `1`.
`EMNIST().features` is a 3D array (i.e. a `Array{Tx,3}`), in
WHN format (width, height, num_images). Labels are stored as
a vector of integers in `EMNIST().targets`.

```julia-repl
julia> using MLDatasets: EMNIST

julia> dataset = EMNIST(:letters, split=:train)
EMNIST:
  metadata    =>    Dict{String, Any} with 3 entries
  split       =>    :train
  features    =>    28×28×60000 Array{Float32, 3}
  targets     =>    60000-element Vector{Int64}

julia> dataset[1:5].targets
5-element Vector{Int64}:
7
2
1
0
4

julia> X, y = dataset[];

julia> dataset = EMNIST(:balanced, Tx=UInt8, split=:test)
EMNIST:
  metadata    =>    Dict{String, Any} with 3 entries
  split       =>    :test
  features    =>    28×28×10000 Array{UInt8, 3}
  targets     =>    10000-element Vector{Int64}
```
"""
struct EMNIST <: SupervisedDataset
    metadata::Dict{String, Any}
    name::Symbol
    split::Symbol
    features::Array{<:Any,3}
    targets::Vector{Int}
end

EMNIST(name; split=:train, Tx=Float32, dir=nothing) = EMNIST(name, Tx, split; dir)

function EMNIST(name, Tx::Type, split::Symbol=:train; dir=nothing)
    @assert split ∈ [:train, :test]
    @assert name ∈ [:balanced, :byclass, :bymerge, :digits, :letters, :mnist]
    path = "matlab/emnist-$name.mat"

    path = datafile("EMNIST", path, dir)
    vars = matread(path)
    features = reshape(vars["dataset"]["$split"]["images"], :, 28, 28)
    features = permutedims(features, (3, 2, 1))
    targets = Int.(vars["dataset"]["$split"]["labels"] |> vec)
    features = bytes_to_type(Tx, features)

    metadata = Dict{String,Any}()
    metadata["n_observations"] = size(features)[end]

    return EMNIST(metadata, name, split, features, targets)
end

convert2image(::Type{<:EMNIST}, x::AbstractArray) = convert2image(MNIST, x)


# internal objects, placed to ASTs in compiling time
module Runtime
using MLStyle
using CamJulia.IdrisList
using CamJulia.IdrisForeignCollections

struct FileHandler
    filename :: String
    mode     :: String
end

simple_open(filename::IdrisString) = FileHandler(string(filename), "r")
simple_read(f::FileHandler) = open(f.filename) do f
    from_text(read(f, String))
end

read_all_text(f::IOStream) = read(f, String)

filesystem_pipe(::Nothing) = (1, 2)

@inline function is(a :: T , b :: T) where T
    a === b
end

is(_, _) = false

@inline function to_list_str(s)
    from_text(string(s))
end

@inline function parse_from_list_str(::Type{T}, s::IdrisString) where T
    parse(T, string(s))
end

function parse_int_from_list_str(s::IdrisString)
    parse_from_list_str(Int, s)
end

function parse_double_from_list_str(s::IdrisString)
    parse_from_list_str(Float64, s)
end

@inline function module_property(m :: Module, s::String)
    getproperty(m, Symbol(s))
end

export rt_support
rt_support = Dict{String, Any}(
    "cam-rt.cmp" => (==),
    "cam-rt.err" => throw,
    "cam-rt.is" => is,

    "prim-plus" => (+),
    "prim-minus" => (-),
    "prim-times" => (*),
    "prim-udiv" => div,
    "prim-sdiv" => div,
    "prim-urem" => rem,
    "prim-srem" => rem,
    "prim-eq" => (==),
    "prim-slt" => (<),
    "prim-sle" => (<=),
    "prim-sgt" => (>),
    "prim-sge" => (>=),
    "prim-and" => (&),
    "prim-or" => (|),

    # str methods1
    "prim-streq" => (==),
    "prim-strlen" => string_len,
    "prim-strindex" => getindex,
    "prim-strlt" => (<=),

    # effect
    "prim-external" => throw,
    "prim-readstr" => readline,
    "prim-writestr" => print,

    # conversion
    "prim-floatstr" => to_list_str,
    "prim-strfloat" => x -> parse(Float64, x) ∘ string,
    "prim-intstr" => to_list_str,
    "prim-strint" => x -> parse(Int, x) ∘ string,
    "prim-intch" => Char,
    "prim-chint" => Int,

    # str method2
    "prim-strhead" => string_head,
    "prim-strtail" => string_tail,
    "prim-strcons" => string_cons,
    "prim-strconcat" => string_concat,
    "prim-crash" => throw,

    # io
    "builtin-println" => println,
    "builtin-simple_open" => simple_open,
    "builtin-simple_read" => simple_read,
    "builtin-filesystem_pipe" => filesystem_pipe,
    "builtin-filesystem_open_file" => open,
    "builtin-filesystem_close_file" => close,
    "builtin-filesystem_read_all_text" => read_all_text,

    # flist
    "builtin-reverse_flist" => reverse,
    "builtin-flist_to_native" => from_flist,
    "builtin-list_to_foreign" => to_flist,

    # fvect
    "builtin-reverse_fvect" => reverse,
    "builtin-fvect_to_native" => from_fvect,
    "builtin-vect_to_foreign" => to_fvect,

    # fhvect
    "builtin-reverse_fhvect" => reverse,
    "builtin-fhvect_to_native" => from_fhvect,
    "builtin-hvect_to_foreign" => to_fhvect,

    # string
    "builtin-to_text" => string,
    "builtin-fstr_to_native" => from_text,
    "builtin-str_to_foreign" => string,

    "builtin-map_hvect" => map_hvect,
    "builtin-module_property" => module_property,

)
end


module TestMacroMLJ

using MLJBase, Test, Distances

# No type, no default
@mlj_model mutable struct A1
    a
end
a = A1()
@test ismissing(a.a)
a.a = 5
@test a.a == 5

# No type, with default
@mlj_model mutable struct A1b
    a = 5
end
a = A1b()
@test a.a == 5
a.a = "hello"
@test a.a == "hello"

# If a type is given but no default value is given, then the macro tries to fill
# a default value; either 0 if it's a Number type, or an empty string and otherwise fails.
@mlj_model mutable struct A1c
    a::Int
end
a = A1c()
@test a.a == 0
a = A1c(a=7)
@test a.a == 7
@test_throws InexactError A1c(a=5.3)
@test_throws MethodError A1c(a="hello")

# Type is given and default is given
@mlj_model mutable struct A1d
    a::Int = 5
end
a = A1d()
@test a.a == 5
a = A1d(a=7)
@test a.a == 7

# No type is given but a default and constraint
@mlj_model mutable struct A1e
    a = 5::(_ > 0)
end
a = A1e()
@test a.a == 5
a = A1e(a=7)
@test a.a == 7
@test @test_logs (:warn, "Constraint `model.a > 0` failed; using default: a=5.") A1e(a=-1).a==5
a = A1e(a=7.5)
@test a.a == 7.5

# Type is given with default and constraint
@mlj_model mutable struct A1f
    a::Int = 5::(_ > 0)
end
a = A1f()
@test a.a == 5
a = A1f(a=7)
@test a.a == 7
@test_throws InexactError A1f(a=7.5)
@test @test_logs (:warn, "Constraint `model.a > 0` failed; using default: a=5.") A1f(a=-1).a==5

abstract type FooBar end
@mlj_model mutable struct B1a <: FooBar
    a::Symbol = :auto::(_ in (:auto, :semi))
end
b = B1a()
@test b.a == :auto
b = B1a(a=:semi)
@test b.a == :semi
@test @test_logs (:warn, "Constraint `model.a in (:auto, :semi)` failed; using default: a=:auto.") B1a(a=:autos).a == :auto
@test_throws MethodError B1a(b="blah")

# == dependence on other types
@mlj_model mutable struct B1b
    a::SemiMetric = Euclidean()::(_ isa Metric)
end
@test B1b().a isa Euclidean
@test @test_logs (:warn, "Constraint `model.a isa Metric` failed; using default: a=Euclidean().") B1b(a=BhattacharyyaDist()).a isa Euclidean

@mlj_model mutable struct B1c
    a::SemiMetric = Euclidean()
end
@test B1c().a isa Euclidean

# Implicit defaults
@mlj_model mutable struct Ca
    a::String
end
@test Ca().a == ""
@mlj_model mutable struct Cb
    a::Any
end
@test Cb().a === missing
@mlj_model mutable struct Cc
    a::Union{Nothing,Int}
end
@test Cc().a === nothing
@mlj_model mutable struct Cd
    a::Union{Missing,Int}
end
@test Cd().a === missing

end
true