# Test of PhyloNetworklm on trees

###############################################################################
## Caudata dataset - shared paths matrix
###############################################################################
@testset "phyloNetworklm: Caudata Dataset" begin
## Export "caudata" dataset (from geiger)
phy = readTopology(joinpath(@__DIR__, "..", "examples", "caudata_tree.txt"));

V = sharedPathMatrix(phy);
VR = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "caudata_shared_paths.txt")))
VR = convert(Matrix, VR);

# Tips
@test V[:Tips] ≈ VR[1:197, 1:197]

# Internal nodes
@test V[:InternalNodes] ≈ VR[-V.internalNodeNumbers .+ 196, -V.internalNodeNumbers .+ 196]

# Tips Nodes
@test V[:TipsNodes] ≈ VR[1:197, -V.internalNodeNumbers .+ 196]

### R Code to get those results
# library(geiger)
# ## Load data caudata (salamanders)
# data("caudata")
# ## Save tree
# write.tree(caudata$phy, file = "caudata_tree.txt", append = FALSE,
#            digits = 10, tree.names = FALSE)
#
# ## Times shared
# V <- node.depth.edgelength(caudata$phy)
# prac <- mrca(caudata$phy, full = TRUE)
# V <- matrix(V[prac], dim(prac))
# write.table(V,
#             file = "caudata_shared_paths.txt",
#             sep = ",", row.names = FALSE,
#             col.names = TRUE)

###############################################################################
## Caudata dataset - BM
###############################################################################

## Export "caudata" dataset (from geiger)
phy = readTopology(joinpath(@__DIR__, "..", "examples", "caudata_tree.txt"));
dat = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "caudata_trait.txt")));

## Fit a BM
fitBM = phyloNetworklm(@formula(trait ~ 1), dat, phy)

# Tests against results obtained with geiger::fitContinuous or phylolm::phylolm
@test loglikelihood(fitBM) ≈ -78.9611507833 atol=1e-10
@test dof(fitBM) ≈ 2.0 atol=1e-10
@test aic(fitBM) ≈ 161.9223015666 atol=1e-10
@test aicc(fitBM) ≈ 161.9841572367 atol=1e-10
@test coef(fitBM) ≈ [4.6789989001] atol=1e-8
@test vcov(fitBM) ≈ [0.1093144100] atol=1e-10
@test nobs(fitBM) ≈ 197.0 atol=1e-10
@test sum(residuals(fitBM)) ≈ -115.5767321312 atol=1e-8
@test dof_residual(fitBM) ≈ 196.0 atol=1e-10
@test sigma2_estim(fitBM) ≈ 0.0029452097 atol=1e-10
@test stderror(fitBM) ≈ [0.3306272978] atol=1e-10
@test confint(fitBM)[1] ≈ 4.0269551772 atol=1e-10
@test confint(fitBM)[2] ≈ 5.3310426231 atol=1e-10
tmp = predict(fitBM);
# below: print for when the test was broken
#@show length(tmp) # fixit: tests fail here
#@show tmp[1:6], tmp[190:end] # 4.679 except for last 5: 9.31707, 9.33832, 9.33052, 9.34356, 9.31707
#println("are they all 4.6789989001?")
# next: looks random. sometimes passes, most times fails
@test predict(fitBM) ≈ [4.6789989001 for i in 1:197] atol=1e-8
# @test_approx_eq_eps deviance(fitBM)  deviance(fitbis)
# @test_approx_eq_eps nulldeviance(fitBM)  nulldeviance(fitbis)
# @test_approx_eq_eps nullloglikelihood(fitBM)  nullloglikelihood(fitbis)
# @test_approx_eq_eps r2(fitBM)  r2(fitbis)
# @test_approx_eq_eps adjr2(fitBM)  adjr2(fitbis)
# @test_approx_eq_eps bic(fitBM)  bic(fitbis)
# @test_approx_eq_eps mu_estim(fitBM)  mu_estim(fitbis)

### Ancestral state reconstruction (with Rphylopars)
anc = (@test_logs (:warn, r"^These prediction intervals show uncertainty in ancestral values") ancestralStateReconstruction(fitBM));
ancR = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "caudata_Rphylopars.txt")))

## Expectations
expe = expectations(anc)
expeR = ancR[!,:trait]
# Matching tips ?
tipsR = expeR[expe[197:393, :nodeNumber]]
tipsJulia = expe[197:393, :condExpectation]
@test tipsR ≈ tipsJulia
# Matching nodes ?
nodesR = expeR[-expe[1:196, :nodeNumber] .+ 196]
nodesJulia = expe[1:196, :condExpectation]
# below: print for when the test was broken
#@show nodesR[1:6],    nodesR[190:end]
#@show nodesJulia[1:6],nodesJulia[190:end]
@test isapprox(nodesR, nodesJulia)

## Variances
vars = diag(anc.variances_nodes)
# Rphylopars
varsR = ancR[!,:var]
# Matching nodes ?
nodesR = varsR[-expe[1:196, :nodeNumber] .+ 196]
@test nodesR ≈ vars atol=1e-3 ## RK: Small tol !!

### Ancestral state reconstruction (with Phytools)
ancRt = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "caudata_Phytools.txt")));

## Expectations
expe = expectations(anc)
expeRt = ancRt[!,:trait]
# Matching nodes ?
nodesRt = expeRt[-expe[1:196, :nodeNumber] .+ (196 - 197)]
nodesJulia = expe[1:196, :condExpectation]
# below: print for when the test was broken
#@show nodesRt[1:6],   nodesRt[190:end]
#@show nodesJulia[1:6],nodesJulia[190:end]
@test isapprox(nodesRt, nodesJulia)

## Variances
vars = diag(anc.variances_nodes)
# Rphylopars
varsRt = ancRt[!,:var]
# Matching nodes ?
nodesRt = varsRt[-expe[1:196, :nodeNumber] .+ (196 - 197)]
@test nodesRt ≈ vars atol=2e-3 ## RK: Small tol !!

### Comparison between Rphylopars and Phytools:
@test nodesRt ≈ nodesR atol=0.003 ## RK: Small tol !!

### R script to get the above values:
# library(geiger)
#
# ## Load data caudata (salamanders)
# data("caudata")
#
# ## Save tree and data
# write.tree(caudata$phy, file = "caudata_tree.txt", append = FALSE,
#            digits = 10, tree.names = FALSE)
#
# write.table(data.frame(tipsNames = names(caudata$dat),
#                        trait = unname(caudata$dat)),
#             file = "caudata_trait.txt",
#             sep = ",", row.names = FALSE)
#
# ## Fit using Geiger
# fitgeiger <- fitContinuous(caudata$phy, caudata$dat, model = "BM")
#
# ## Fit using phylolm
# library(phylolm)
# fitphylolm <- phylolm(trait ~ 1,
#                       data.frame(trait = caudata$dat),
#                       caudata$phy, model = "BM")
#
# ## Fit using Rphylopars
# library(Rphylopars)
# fitphylopars <- phylopars(data.frame(species = names(caudata$dat),
#                           trait = unname(caudata$dat)),
#                           caudata$phy,
#                           pheno_error = FALSE,
#                           pheno_correlated = FALSE,
#                           REML = FALSE)
#
# # Save results of Rphylopars for ancestral trait reconstruction
# write.table(data.frame(trait = unname(fitphylopars$anc_recon),
#                        var = unname(fitphylopars$anc_var)),
#             file = "caudata_Rphylopars.txt",
#             sep = ",", row.names = FALSE)
#
# ## Ancestral State reconstruction using phytools
# library(phytools)
# fitphytools <- fastAnc(caudata$phy, caudata$dat, vars = TRUE)
#
# # Save results of Rphylopars for ancestral trait reconstruction
# write.table(data.frame(trait = unname(fitphytools$ace),
#                        var = unname(fitphytools$var)),
#             file = "caudata_Phytools.txt",
#             sep = ",", row.names = FALSE)
#
# ## Quantities to compare
# sprintf("%.10f", fitgeiger$opt$ln) # log likelihood
# sprintf("%.10f", fitphylolm$logLik)
# sprintf("%.10f", fitphylopars$logLik)
# sprintf("%.10f", fitgeiger$opt$aic) # aic
# sprintf("%.10f", fitphylolm$aic)
# sprintf("%.10f", fitgeiger$opt$aicc) # aicc
# sprintf("%.10f", fitphylolm$coefficients) # coef
# sprintf("%.10f", fitgeiger$opt$z0)
# sprintf("%.10f", fitphylopars$mu)
# sprintf("%.10f", fitphylolm$vcov) # vcov
# sprintf("%.10f", fitphylolm$n) # nobs
# sprintf("%.10f", sum(fitphylolm$residuals)) # residuals (sum)
# sprintf("%.10f", fitphylolm$n - fitphylolm$d) # df residuals
# sprintf("%.10f", fitphylolm$sigma2) # sigma 2
# sprintf("%.10f", fitgeiger$opt$sigsq)
# sprintf("%.10f", fitphylopars$pars$phylocov)
# sprintf("%.10f", summary(fitphylolm)$coefficients[2]) # std error
# sprintf("%.10f", summary(fitphylolm)$df) # df
# sprintf("%.10f", fitgeiger$opt$k)
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[2] * qt(0.025, 196))
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[2] * qt(0.975, 196))
# sprintf("%.10f", predict(fitphylolm)) # df

###############################################################################
## Caudata dataset - Pagel's lambda
###############################################################################

## Fit Pagel's lambda
fitLambda = (@test_logs (:info, r"^Maximum lambda value") match_mode=:any phyloNetworklm(@formula(trait ~ 1), dat, phy, model = "lambda"));

@test lambda_estim(fitLambda) ≈ 0.9193 atol=1e-4 # Due to convergence issues, tolerance is lower.
@test loglikelihood(fitLambda) ≈ -51.684379 atol=1e-6
@test dof(fitLambda) ≈ 3.0 atol=1e-10
@test aic(fitLambda) ≈ 109.368759 atol=1e-6
@test aicc(fitLambda) ≈ 109.493111 atol=1e-6
@test coef(fitLambda) ≈ [4.66893] atol=1e-5
@test vcov(fitLambda) ≈ [0.05111] atol=1e-5
@test nobs(fitLambda) ≈ 197.0 atol=1e-10
# below: print for when the test was broken
#@show sum(residuals(fitLambda)) # locally: -115.91591040367894
#println("is this -113.594?")
@test isapprox(sum(residuals(fitLambda)), -113.594, atol=1e-2) ## Low Tolerance !!
@test dof_residual(fitLambda) ≈ 196.0 atol=1e-10 ## Correct Definition ?
@test sigma2_estim(fitLambda) ≈ 0.0014756 atol=1e-7
@test stderror(fitLambda) ≈ [0.22608] atol=1e-5
@test confint(fitLambda)[1] ≈ 4.2230 atol=1e-4
@test confint(fitLambda)[2] ≈ 5.114 atol=1e-3
tmp = predict(fitLambda);
# below: print for when the test was broken
#@show length(tmp)
#@show tmp[1:6], tmp[190:end] # all 4.66893 except for last 5: 8.42676, 8.44585, etc.
#println("are they all 4.66893?")
# next: looks random. sometimes passes, most times fails
@test predict(fitLambda) ≈ [4.66893 for i in 1:197] atol=1e-5 norm=x->norm(x,Inf)

### R script to get the above values:
# library(geiger)
#
# ## Load data caudata (salamanders)
# data("caudata")
#
# ## Fit using Geiger
# fitgeiger <- fitContinuous(caudata$phy, caudata$dat, model = "lambda")
#
# ## Fit using phylolm
# library(phylolm)
# fitphylolm <- phylolm(trait ~ 1,
#                       data.frame(trait = caudata$dat),
#                       caudata$phy, model = "lambda",
#                       starting.value = 0.9)
#
# ## Fit using Rphylopars
# library(Rphylopars)
# fitphylopars <- phylopars(data.frame(species = names(caudata$dat),
#                           trait = unname(caudata$dat)),
#                           caudata$phy,
#                           model = "lambda",
#                           model_par_start = 0.9,
#                           pheno_error = FALSE,
#                           pheno_correlated = FALSE,
#                           REML = FALSE)
#
# ## Quantities to compare
# sprintf("%.10f", fitgeiger$opt$ln) # log likelihood
# sprintf("%.10f", fitphylolm$logLik)
# sprintf("%.10f", fitphylopars$logLik)
# sprintf("%.10f", fitgeiger$opt$aic) # aic
# sprintf("%.10f", fitphylolm$aic)
# sprintf("%.10f", fitgeiger$opt$aicc) # aicc
# sprintf("%.10f", fitphylolm$coefficients) # coef
# sprintf("%.10f", fitgeiger$opt$z0)
# sprintf("%.10f", fitphylopars$mu)
# sprintf("%.10f", fitphylolm$vcov) # vcov
# sprintf("%.10f", fitphylolm$n) # nobs
# sprintf("%.10f", sum(fitphylolm$residuals)) # residuals (sum)
# sprintf("%.10f", fitphylolm$n - fitphylolm$d) # df residuals
# sprintf("%.10f", fitphylolm$sigma2) # sigma 2
# sprintf("%.10f", fitgeiger$opt$sigsq)
# sprintf("%.10f", fitphylopars$pars$phylocov)
# sprintf("%.10f", summary(fitphylolm)$coefficients[2]) # std error
# sprintf("%.10f", summary(fitphylolm)$df) # df
# sprintf("%.10f", fitgeiger$opt$k)
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[2] * qt(0.025, 196))
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[2] * qt(0.975, 196))
# sprintf("%.10f", predict(fitphylolm)) # df

###############################################################################
## Caudata dataset - BM with shifts
###############################################################################

## Export "caudata" dataset (from geiger)
phy = readTopology(joinpath(@__DIR__, "..", "examples", "caudata_tree.txt"));
dat = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "caudata_trait.txt")));

## Add some shifts in the model
df_shift = regressorShift(phy.edge[[98, 326, 287]], phy)
dat = innerjoin(dat, df_shift, on=:tipNames)
## Fit a BM
fitBM = phyloNetworklm(@formula(trait ~ shift_98 + shift_326 + shift_287), dat, phy)

# Tests against results obtained with geiger::fitContinuous or phylolm::phylolm
@test loglikelihood(fitBM) ≈ -76.1541605207 atol=1e-10
@test dof(fitBM) ≈ 5.0 atol=1e-10
@test aic(fitBM) ≈ 162.3083210414 atol=1e-10
@test coef(fitBM) ≈ [4.8773804547 -0.6664678924 -1.0692950215 -0.0159349201]' atol=1e-10
vcovR = [ 0.1165148753  -0.0431446679 -0.0305707092 0.0000000000
         -0.0431446679 0.3212796435  0.0340202694  -0.0000000000
         -0.0305707092 0.0340202694 0.2472613252  -0.0236868278
          0.0000000000  -0.0000000000 -0.0236868278 0.1263794810]
@test vcov(fitBM) ≈ vcovR atol=1e-8
@test nobs(fitBM) ≈ 197.0 atol=1e-10
@test sum(residuals(fitBM)) ≈ -10.1752334428 atol=1e-10
@test dof_residual(fitBM) ≈ 193.0 atol=1e-10
@test sigma2_estim(fitBM) ≈ 0.0028624636 atol=1e-10
@test stderror(fitBM) ≈ [0.3413427535 0.5668153522 0.4972537835 0.3554989184]' atol=1e-10
@test confint(fitBM)[:,1] ≈ [4.2041393297 -1.7844157659 -2.0500444097 -0.7170966977]' atol=1e-10
@test confint(fitBM)[:,2] ≈ [5.5506215797 0.4514799811 -0.0885456333 0.6852268574]' atol=1e-10
predictR = [4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 4.2109125624, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.7921505131, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 3.8080854332, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547, 4.8773804547]
tmp  = predict(fitBM)
tmp2 = predictR[fitBM.model.ind]
# below: print for when the test was broken
#@show tmp[1:6],  tmp[190:end]
#@show tmp2[1:6],tmp2[190:end] # the last 5 values are different
@test isapprox(predict(fitBM), predictR[fitBM.model.ind], atol=1e-8)

# ## R code to get those results
# library(geiger)
# ## Load data caudata (salamanders)
# data("caudata")
# ## Re-order with the tree
# dat <- caudata$dat[match(caudata$phy$tip.label, names(caudata$dat))]
# ## Incidence matrix
# Tm = PhylogeneticEM::incidence.matrix(caudata$phy) + 0
# ## Fit using phylolm
# library(phylolm)
# fitphylolm <- phylolm(trait ~ shift_37 + shift_105 + shift_222,
#                       data.frame(trait = dat,
#                                  shift_37 = Tm[, 37],
#                                  shift_105 = Tm[, 105],
#                                  shift_222 = Tm[, 222]),
#                       caudata$phy, model = "BM")
# ## Quantities to compare
# sprintf("%.10f", fitphylolm$logLik)
# sprintf("%.10f", fitphylolm$aic)
# sprintf("%.10f", fitphylolm$coefficients) # coef
# sprintf("%.10f", fitphylolm$vcov) # vcov
# sprintf("%.10f", fitphylolm$n) # nobs
# sprintf("%.10f", sum(fitphylolm$residuals)) # residuals (sum)
# sprintf("%.10f", fitphylolm$n - fitphylolm$d) # df residuals
# sprintf("%.10f", fitphylolm$sigma2) # sigma 2
# sprintf("%.10f", summary(fitphylolm)$coefficients[,2]) # std error
# sprintf("%.10f", summary(fitphylolm)$df) # df
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[,2] * qt(0.025, 193))
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[,2] * qt(0.975, 193))
# sprintf("%.10f", predict(fitphylolm))

end

###############################################################################
## Lizard dataset - BM
###############################################################################

@testset "phyloNetworklm: Lizard Dataset" begin

## Export "lizard" dataset (Mahler et al 2013)
phy = readTopology(joinpath(@__DIR__, "..", "examples", "lizard_tree.txt"));
dat = DataFrame!(CSV.File(joinpath(@__DIR__, "..", "examples", "lizard_trait.txt")));
categorical!(dat, :region)

## Fit a BM
fitBM = phyloNetworklm(@formula(AVG_SVL ~ AVG_ltoe_IV + AVG_lfing_IV * region), dat, phy)

# Tests against results obtained with geiger::fitContinuous or phylolm::phylolm
@test loglikelihood(fitBM) ≈ 105.17337853473711 atol=1e-10
@test dof(fitBM) ≈ 10.0 atol=1e-10
@test aic(fitBM) ≈ -190.3467570695 atol=1e-10
# @test aicc(fitBM)
@test coef(fitBM) ≈ [2.7925712673 -0.2010704391 0.9832555589 -0.1021226296 -0.3703658712 0.1557471731 0.0374549036 0.1805667675 -0.0495767233]' atol=1e-10
vcovR =  [0.0200086273  -0.0136717540 0.0084815090  -0.0093192029 -0.0114417825 -0.0113346813 0.0041102304  0.0053787287  0.0050521693
          -0.0136717540 0.0185396965  -0.0169114682 0.0020645005  0.0036352899  0.0026227856 -0.0012281620 -0.0018838231 -0.0014800242
          0.0084815090  -0.0169114682 0.0174647413  0.0016403871  0.0005624488  0.0015003301 -0.0005714235 -0.0003201257 -0.0005423354
          -0.0093192029 0.0020645005  0.0016403871  0.0167953394  0.0078012534  0.0086329399 -0.0080782771 -0.0037333495 -0.0039327836
          -0.0114417825 0.0036352899  0.0005624488  0.0078012534  0.0490482083  0.0092203882 -0.0033670465 -0.0191567265 -0.0040068947
          -0.0113346813 0.0026227856  0.0015003301  0.0086329399  0.0092203882  0.0331395502 -0.0037513830 -0.0041592743 -0.0146108207
          0.0041102304  -0.0012281620 -0.0005714235 -0.0080782771 -0.0033670465 -0.0037513830 0.0045172675  0.0018165174  0.0020857846
          0.0053787287  -0.0018838231 -0.0003201257 -0.0037333495 -0.0191567265 -0.0041592743 0.0018165174  0.0093292284  0.0020427637
          0.0050521693  -0.0014800242 -0.0005423354 -0.0039327836 -0.0040068947 -0.0146108207 0.0020857846  0.0020427637  0.0074817942]
@test vcov(fitBM) ≈ vcovR atol=1e-8
@test nobs(fitBM) ≈ 100.0 atol=1e-10
# below: print for when the test was broken (with @test_skip)
#@show sum(residuals(fitBM)) # looks random, e.g. 1.6091413520064477, or 0.8338189090359597
#println("is this equal to 0.6352899255?") # sometimes NO, yet the test passes below!!
@test sum(residuals(fitBM)) ≈ 0.6352899255 atol=1e-10
@test dof_residual(fitBM) ≈ 91.0 atol=1e-10
@test sigma2_estim(fitBM) ≈ 0.0003025014 atol=1e-10
@test stderror(fitBM) ≈ [0.1414518551,0.1361605540,0.1321542330,0.1295968341,0.2214683008,0.1820427154,0.0672106202,0.0965879311,0.0864973651] atol=1e-10
@test confint(fitBM)[:,1] ≈ [2.5115945339,-0.4715366529,0.7207474097,-0.3595508202,-0.8102854443,-0.2058583178,-0.0960507369,-0.0112932922,-0.2213931131] atol=1e-10 norm=x->norm(x,Inf)
@test confint(fitBM)[:,2] ≈ [3.0735480006,0.0693957746,1.2457637082,0.1553055609,0.0695537019,0.5173526640,0.1709605441,0.3724268272,0.1222396666] atol=1e-10
# @test_approx_eq_eps predict(fitBM)[fitbis.model.ind] predict(fitbis)
# @test_approx_eq_eps deviance(fitBM)  deviance(fitbis)
# @test_approx_eq_eps nulldeviance(fitBM)  nulldeviance(fitbis)
# @test_approx_eq_eps nullloglikelihood(fitBM)  nullloglikelihood(fitbis)
# @test_approx_eq_eps r2(fitBM)  r2(fitbis)
# @test_approx_eq_eps adjr2(fitBM)  adjr2(fitbis)
# @test_approx_eq_eps bic(fitBM)  bic(fitbis)
# @test_approx_eq_eps mu_estim(fitBM)  mu_estim(fitbis)

### R script to get the above values
# ## Data
# dat <- read.csv(file = "GA_Anolis_traits.csv")
# geo <- read.csv(file = "GA_Anolis_biogeography.csv")
#
# dat <- merge(dat, geo, by = "species")
# # keep only toe and hand length
# dat <- dat[, c("species", "AVG.SVL", "AVG.ltoe.IV", "AVG.lfing.IV", "region")]
# colnames(dat)[1] <- "tipsNames"
#
# write.table(dat,
#             file = "lizard_trait.txt",
#             sep = ",", row.names = FALSE)
#
# ## Tree
# phy <- read.tree(file = "GA_Anolis_MCC.tre")
#
# write.tree(phy, file = "lizards_tree.txt", append = FALSE,
#            digits = 10, tree.names = FALSE)
#
# rownames(dat) <- dat$tipsNames
# dat <- dat[, -1]
# dat$region <- as.factor(dat$region)
#
# ## Fit
# fitphylolm <- phylolm(AVG.SVL ~ 1 + AVG.ltoe.IV + AVG.lfing.IV * region, dat, phy, model = "BM")
# ## Quantities to compare
# sprintf("%.10f", fitphylolm$logLik)
# sprintf("%.10f", summary(fitphylolm)$df) # df
# sprintf("%.10f", fitphylolm$aic)
# sprintf("%.10f", fitphylolm$coefficients) # coef
# matrix(sprintf("%.10f", fitphylolm$vcov), 9, 9) # vcov
# sprintf("%.10f", fitphylolm$n) # nobs
# sprintf("%.10f", sum(fitphylolm$residuals)) # residuals (sum)
# sprintf("%.10f", fitphylolm$n - fitphylolm$d) # df residuals
# sprintf("%.10f", fitphylolm$sigma2) # sigma 2
# sprintf("%.10f", summary(fitphylolm)$coefficients[,2]) # std error
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[, 2] * qt(0.025, 91))
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[, 2] * qt(0.975, 91))

###############################################################################
## Lizard dataset - lambda
###############################################################################

## Fit lambda
fitLambda = (@test_logs (:info, r"^Maximum lambda value") match_mode=:any phyloNetworklm(@formula(AVG_SVL ~ AVG_ltoe_IV + AVG_lfing_IV * region), dat, phy, model = "lambda"))

# Tests against results obtained with geiger::fitContinuous or phylolm::phylolm
@test lambda_estim(fitLambda) ≈ 0.9982715594 atol=1e-5
@test loglikelihood(fitLambda) ≈ 105.1769275564 atol=1e-8
@test dof(fitLambda) ≈ 11.0 atol=1e-10
@test aic(fitLambda) ≈ -188.3538551128 atol=1e-8
# @test aicc(fitBM)
@test coef(fitLambda) ≈ [2.7940573420 -0.2066584606 0.9897083949 -0.1004840950 -0.3677991157 0.1576743022 0.0367633665 0.1792502383 -0.0505291142]' atol=1e-5
vcovR =  [0.0200251600  -0.0137474015 0.0085637021  -0.0092973836 -0.0114259722 -0.0113056243 0.0041037877  0.0053740100  0.0050429112
          -0.0137474015 0.0186885224  -0.0170645512 0.0020509207  0.0036334103 0.0026066694  -0.0012237488 -0.0018826836 -0.0014743137
          0.0085637021  -0.0170645512 0.0176200143  0.0016494733  0.0005604169 0.0015116125  -0.0005735573 -0.0003192320 -0.0005457420
          -0.0092973836 0.0020509207  0.0016494733  0.0167461876  0.0077885115 0.0086173037  -0.0080563819 -0.0037287856 -0.0039275469
          -0.0114259722 0.0036334103  0.0005604169  0.0077885115  0.0490092393 0.0092036032  -0.0033631662 -0.0191657329 -0.0040017905
          -0.0113056243 0.0026066694  0.0015116125  0.0086173037  0.0092036032 0.0330248707  -0.0037465110 -0.0041543671 -0.0145663751
          0.0041037877  -0.0012237488 -0.0005735573 -0.0080563819 -0.0033631662 -0.0037465110 0.0045042057  0.0018142470  0.0020823721
          0.0053740100  -0.0018826836 -0.0003192320 -0.0037287856 -0.0191657329 -0.0041543671 0.0018142470  0.0093334212  0.0020404652
          0.0050429112  -0.0014743137 -0.0005457420 -0.0039275469 -0.0040017905 -0.0145663751 0.0020823721  0.0020404652  0.0074600880]
@test vcov(fitLambda) ≈ vcovR atol=3e-7
@test nobs(fitLambda) ≈ 100.0 atol=1e-10
# below: print for when the test was broken (with @test_skip)
#@show sum(residuals(fitLambda)) # looks random, eg 0.033126277561337916 or 0.644941961666333
#println("is this equal to 0.6369008979?")
@test sum(residuals(fitLambda)) ≈ 0.6369008979 atol=1e-5
@test dof_residual(fitLambda) ≈ 91.0 atol=1e-10
@test sigma2_estim(fitLambda) ≈ 0.0003009914 atol=1e-9
@test stderror(fitLambda) ≈ [0.1415102824,0.1367059706,0.1327404019,0.1294070617,0.2213803048,0.1817274626,0.0671133793,0.0966096332,0.0863718011] atol=1e-6
@test confint(fitLambda)[:,1] ≈ [2.5129645499,-0.4782080775,0.7260358930,-0.3575353260,-0.8075438955,-0.2033049779,-0.0965491169,-0.0126529301,-0.2220960868] atol=1e-5
@test confint(fitLambda)[:,2] ≈ [3.0751501341,0.0648911562,1.2533808968,0.1565671360,0.0719456641,0.5186535822,0.1700758500,0.3711534067,0.1210378584] atol=1e-5
# @test_approx_eq_eps predict(fitLambda)[fitbis.model.ind] predict(fitbis)
# @test_approx_eq_eps deviance(fitLambda)  deviance(fitbis)
# @test_approx_eq_eps nulldeviance(fitLambda)  nulldeviance(fitbis)
# @test_approx_eq_eps nullloglikelihood(fitLambda)  nullloglikelihood(fitbis)
# @test_approx_eq_eps r2(fitLambda)  r2(fitbis)
# @test_approx_eq_eps adjr2(fitLambda)  adjr2(fitbis)
# @test_approx_eq_eps bic(fitLambda)  bic(fitbis)
# @test_approx_eq_eps mu_estim(fitLambda)  mu_estim(fitbis)

### R script to get the above values
# ## Data
# dat <- read.csv(file = "GA_Anolis_traits.csv")
# geo <- read.csv(file = "GA_Anolis_biogeography.csv")
#
# dat <- merge(dat, geo, by = "species")
# # keep only toe and hand length
# dat <- dat[, c("species", "AVG.SVL", "AVG.ltoe.IV", "AVG.lfing.IV", "region")]
# colnames(dat)[1] <- "tipsNames"
#
# write.table(dat,
#             file = "lizard_trait.txt",
#             sep = ",", row.names = FALSE)
#
# ## Tree
# phy <- read.tree(file = "GA_Anolis_MCC.tre")
#
# write.tree(phy, file = "lizards_tree.txt", append = FALSE,
#            digits = 10, tree.names = FALSE)
#
# rownames(dat) <- dat$tipsNames
# dat <- dat[, -1]
# dat$region <- as.factor(dat$region)
#
# ## Fit
# fitphylolm <- phylolm(AVG.SVL ~ 1 + AVG.ltoe.IV + AVG.lfing.IV * region, dat, phy, model = "lambda")
# ## Quantities to compare
# sprintf("%.10f", fitphylolm$logLik)
# sprintf("%.10f", summary(fitphylolm)$df) # df
# sprintf("%.10f", fitphylolm$aic)
# sprintf("%.10f", fitphylolm$coefficients) # coef
# matrix(sprintf("%.10f", fitphylolm$vcov), 9, 9) # vcov
# sprintf("%.10f", fitphylolm$n) # nobs
# sprintf("%.10f", sum(fitphylolm$residuals)) # residuals (sum)
# sprintf("%.10f", fitphylolm$n - fitphylolm$d) # df residuals
# sprintf("%.10f", fitphylolm$sigma2) # sigma 2
# sprintf("%.10f", summary(fitphylolm)$coefficients[,2]) # std error
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[, 2] * qt(0.025, 91))
# sprintf("%.10f", coef(fitphylolm) + summary(fitphylolm)$coefficients[, 2] * qt(0.975, 91))

end


#=
priority_queue:
- Julia version: 1.5.0
- Author: shisa
- Date: 2020-10-18
=#

module PQ
    using StatsBase

    buffer = []

    # move added node to root
    function upheap(buffer, n)
        while true
            p = Int(trunc((n - 1)/2))
            if (p < -1) || (buffer[p+1] <= buffer[n+1])
                break
            end
            tmp = buffer[n+1]
            buffer[n+1] = buffer[p+1]
            buffer[p+1] = tmp
            n = p
        end
    end

    # move root node to leaf
    function downheap(buffer, n)
        len = length(buffer)
        while true
            c = 2 * n + 1
            if c >= len
                break
            end
            # exchange parent and smaller child
            if (c + 1) < len
                if buffer[c+1] > buffer[c+2]
                    c += 1
                end
            end
            if buffer[n+1] <= buffer[c+1]
                break
            end
            tmp = buffer[n+1]
            buffer[n+1] = buffer[c+1]
            buffer[c+1] = tmp
            n = c
        end
    end

    # add data to tail of buffer
    # after adding, sort as upheap
    function push(buffer, data)
        push!(buffer, data)
        upheap(buffer, length(buffer)-1)
    end

    # get minimum data
    function peek(buffer)
        return buffer[1]
    end

    # get and remove minimum data
    function pop(buffer)
        min_val = buffer[1]
        last_val = buffer[end]
        deleteat!(buffer, length(buffer))
        if length(buffer) > 0
            # create heap again
            buffer[1] = last_val
            downheap(buffer, 0)
        end
        return min_val
    end

    function main()
        input = sample(1:100, 10, replace=false)
        # create by upheap
        for n in input
            push(buffer, n)
            println("Add $(n), Min data = $(peek(buffer))")
        end
        # remove minimum data and create by downheap
        while length(buffer) > 0
            print("$(pop(buffer)) ")
        end
    end
end

if abspath(PROGRAM_FILE) == @__FILE__
    using .PQ
    PQ.main()
end


function HHG(f::TVF) where {TVF<:AbstractVector{<:AbstractFloat}}
    n = length(f)
    spec = fft(f)
    return abs.(spec) .^ 2 / n^2
end
function HHGfreq(f::TVF, ncyc) where {TVF<:AbstractVector{<:AbstractFloat}}
    x = collect(range(1, length(f), length = length(f)))
    return x ./ ncyc
end


## --- test Interpolations.jl

    # Interpolation
    @test linterp1(1:10, 1:10, 5.5) == 5.5
    @test linterp1(1:10, collect(1:10.), 3:7) == 3:7
    @test linterp1(1:10,21:30,5:0.5:6) == [25.0, 25.5, 26.0]
    @test linterp1s(10:-1:1,21:30,5:0.5:6) == [26.0, 25.5, 25.0]
    @test linterp_at_index(1:100,10) == 10

    # Extrapolation
    @test linterp1(1:10, 1:10, 15) == 15 # Default is to extrapolate
    @test linterp1(1:10, 1:10, 15, extrapolate=-5) == -5
    @test linterp1(1:10, 1:10, 5, extrapolate=-5) == 5
    @test isnan(linterp1(1:10, 1:10, 15, extrapolate=NaN))
    @test linterp1(1:10,1:10,0:11) == 0:11 # Default is to extrapolate
    @test linterp1(1:10,1:10,0:11, extrapolate=:Linear) == 0:11
    @test linterp1(1:10,1:10,0.5:10.5, extrapolate=:Linear) == 0.5:10.5
    @test linterp1(1:10,1:10,0.5:10.5, extrapolate=-5) == [-5; 1.5:9.5; -5]
    @test all(linterp1(1:10,1:10,0.5:10.5, extrapolate=NaN) .=== [NaN; 1.5:9.5; NaN])
    @test isnan(linterp_at_index(1:100,-10))
    @test linterp_at_index(1:100,-10, 0) == 0


@testset "647.palindromic-substrings.jl" begin
    @test count_substrings("abc") == 3
    @test count_substrings("aaa") == 6
    @test count_substrings("a" ^ 10) == sum(1:10)
    @test count_substrings("abcdefghijklmnnmlkjihgfedcba") == 42
end


"""
    is_leap_year(year)

Return `true` if `year` is a leap year in the gregorian calendar.

"""
function is_leap_year(year)
    rem(year, 4) == 0 &&
        (rem(year, 4) > 0 && rem(year, 4) > 0)
end


"""
    matex(input::Vector)
Takes a one-dimensional vector as input and formats it for LaTeX display.
"""
function matex(input::Vector)
	S = ""
	for i in input[1:end-1]
		S = string(S, " ", i, " \\\\")
	end
	S = string(
		"\\begin{bmatrix}",
		S, " ", input[end],
		"\\end{bmatrix}")
	S
end

"""
    matex(input::Matrix)
Takes a two-dimensional matrix as input and formats it for LaTeX display.
"""
function matex(input::Matrix)
	S = ""
	rows = collect(eachrow(input))
	for row in rows
		for i in row[1:end-1]
			S = string(S, i, " & ")
		end
		S = string(S, last(row))
		if row != last(rows)
			S = string(S, " \\\\ ")
		end
	end
	S = string("\\begin{bmatrix}", S, "\\end{bmatrix}")
	S
end


# Before running this, please make sure to activate and instantiate the environment
# corresponding to [this `Project.toml`](https://raw.githubusercontent.com/alan-turing-institute/DataScienceTutorials.jl/master/Project.toml) and [this `Manifest.toml`](https://raw.githubusercontent.com/alan-turing-institute/DataScienceTutorials.jl/master/Manifest.toml)
# so that you get an environment which matches the one used to generate the tutorials:
#
# ```julia
# cd("DataScienceTutorials") # cd to folder with the *.toml
# using Pkg; Pkg.activate("."); Pkg.instantiate()
# ```

using RDatasets
using MLJ

iris = dataset("datasets", "iris")

first(iris, 3) |> pretty

iris2 = coerce(iris, :PetalWidth => OrderedFactor)
first(iris2[:, [:PetalLength, :PetalWidth]], 1) |> pretty

y, X = unpack(iris, ==(:Species), colname -> true)
first(X, 1) |> pretty

y, X = unpack(iris, ==(:Species), !=(:PetalLength))
first(X, 1) |> pretty

X, y = @load_iris;

for m in models(matching(X, y))
    if m.prediction_type == :probabilistic
        println(rpad(m.name, 30), "($(m.package_name))")
    end
end

knc = @load KNeighborsClassifier

linreg = @load LinearRegressor pkg="GLM"

# This file was generated using Literate.jl, https://github.com/fredrikekre/Literate.jl


#generate community
p = random_micrm_params(5,10,0.5)

#check dimensions
@testset "random_micrm_params dimensions" begin
    @test p[:N] == 5
    @test p[:M] == 10
    @test size(p[:u]) == (5,10)
    @test size(p[:m]) == (5,)
    @test size(p[:ρ]) == (10,)
    @test size(p[:ω]) == (10,)
    @test size(p[:l]) == (10,10)
end

@testset "random_micrm_params values" begin
    @test p[:N] == 5
    @test p[:M] == 10
    @test all(sum(p[:l], dims = 2) .≈ 0.5)
    @test all(sum(p[:u], dims = 2) .≈ 1.0)
end


@testset "feature_transform" begin
    function ind2cart(F)
        s = CartesianIndices(axes(F))
        return map(i -> CartesianIndex(s[i]), F)
    end
    @testset "Square Images" begin
        # (1)
        A = [true false; false true]
        F = feature_transform(A)
        @test F == ind2cart([1 1; 1 4])
        D = distance_transform(F)
        @test D == [0 1; 1 0]

        # (2)
        A = [true true; false true]
        F = feature_transform(A)
        @test F == ind2cart([1 3; 1 4])
        D = distance_transform(F)
        @test D == [0 0; 1 0]

        # (3)
        A = [false false; false true]
        F = feature_transform(A)
        @test F == ind2cart([4 4; 4 4])
        D = distance_transform(F)
        @test D ≈ [sqrt(2) 1.0; 1.0 0.0]

        # (4)
        A = [true false true; false true false; true true false]
        F = feature_transform(A)
        @test F == ind2cart([1 1 7; 1 5 5; 3 6 6])
        D = distance_transform(F)
        @test D == [0 1 0; 1 0 1; 0 0 1]

        # (5)
        A = [false false true; true true false; true true true]
        F = feature_transform(A)
        @test F == ind2cart([2 5 7; 2 5 5; 3 6 9])
        D = distance_transform(F)
        @test D == [1 1 0; 0 0 1; 0 0 0]

        # (6)
        A = [
            true false true true
            false true false false
            false true true false
            true false false false
        ]
        F = feature_transform(A)
        @test F == ind2cart([1 1 9 13; 1 6 6 13; 4 7 11 11; 4 4 11 11])
        D = distance_transform(F)
        @test D ≈ [0.0 1.0 0.0 0.0; 1.0 0.0 1.0 1.0; 1.0 0.0 0.0 1.0; 0.0 1.0 1.0 sqrt(2)]
    end

    @testset "Rectangular Images" begin
        # (1)
        A = [true false true; false true false]
        F = feature_transform(A)
        @test F == ind2cart([1 1 5; 1 4 4])
        D = distance_transform(F)
        @test D == [0 1 0; 1 0 1]

        # (2)
        A = [true false; false false; false true]
        F = feature_transform(A)
        @test F == ind2cart([1 1; 1 6; 6 6])
        D = distance_transform(F)
        @test D == [0 1; 1 1; 1 0]

        # (3)
        A = [
            true false false
            true false false
            false true true
            true true true
            false true false
        ]
        F = feature_transform(A)
        @test F == ind2cart([1 1 1; 2 2 13; 2 8 13; 4 9 14; 4 10 10])
        D = distance_transform(F)
        @test D == [0.0 1.0 2.0; 0.0 1.0 1.0; 1.0 0.0 0.0; 0.0 0.0 0.0; 1.0 0.0 1.0]

        @test feature_transform(Gray.(A)) == F
    end

    @testset "Corner Case Images" begin
        null1 = CartesianIndex((typemin(Int),))
        null2 = CartesianIndex((typemin(Int), typemin(Int)))
        # (1)
        A = [false]
        F = feature_transform(A)
        @test F == [null1]
        D = distance_transform(F)
        @test D == [Inf]

        # (2)
        A = [true]
        F = feature_transform(A)
        @test F == ind2cart([1])
        D = distance_transform(F)
        @test D == [0]

        # (3)
        A = [true false]
        F = feature_transform(A)
        @test F == ind2cart([1 1])
        D = distance_transform(F)
        @test D == [0 1]

        # (4)
        A = [false; false]
        F = feature_transform(A)
        @test F == [null1; null1]
        D = distance_transform(F)
        @test D == [Inf; Inf]

        # (5)
        A = [true; true]
        F = feature_transform(A)
        @test F == ind2cart([1; 2])
        D = distance_transform(F)
        @test D == [0; 0]

        # (6)
        A = [true; true; false]
        F = feature_transform(A)
        @test F == ind2cart([1; 2; 2])
        D = distance_transform(F)
        @test D == [0; 0; 1]

        # (7)
        A = falses(3, 3)
        F = feature_transform(A)
        @test all(x -> x == null2, F)
        D = distance_transform(F)
        @test all(x -> x == Inf, D)

        # (8)
        A = trues(4, 2, 3)
        F = feature_transform(A)
        @test F == ind2cart(reshape(1:length(A), size(A)))
        D = distance_transform(F)
        @test all(x -> x == 0, D)

        # (9)
        A = falses(4, 2, 3)
        A[3, 1, 2] = true
        @test all(==(CartesianIndex(3, 1, 2)), feature_transform(A))
    end

    @testset "Anisotropic images" begin
        A, w = [false false; false true], (3, 1)
        F = feature_transform(A; weights=w)
        @test F == ind2cart([4 4; 4 4])
        D = distance_transform(F, w)
        @test D ≈ [sqrt(10) 3.0; 1.0 0.0]

        A, w = [false false; false true], (1, 3)
        F = feature_transform(A; weights=w)
        @test F == ind2cart([4 4; 4 4])
        D = distance_transform(F, w)
        @test D ≈ [sqrt(10) 1.0; 3.0 0.0]

        A, w = [true false; false true], (3, 1)
        F = feature_transform(A; weights=w)
        @test F == ind2cart([1 1; 4 4])
        D = distance_transform(F, w)
        @test D == [0 1; 1 0]

        A, w = [true false; false true], (1, 3)
        F = feature_transform(A; weights=w)
        @test F == ind2cart([1 4; 1 4])
        D = distance_transform(F, w)
        @test D == [0 1; 1 0]
    end
end

nothing


# mask operator

export joMask
"""
    julia> op = joMask(n,idx;[DDT=...,][RDT=...,][name=...])

Mask operator with index array

# Signature

    function joMask(n::Integer,idx::Vector{VDT};
        DDT::DataType=joFloat,RDT::DataType=DDT,name::String="joMask") where {VDT<:Integer}

# Arguments

- `n`: size of square operator
- `idx`: vector of true indecies
- keywords
    - `DDT`: domain data type
    - `RDT`: range data type
    - `name`: custom name

# Examples

    A=joMask(3,[1,3])

examples with DDT/RDT

    A=joMask(3,[1,3]; DDT=Float32)
    A=joMask(3,[1,3]; DDT=Float32,RDT=Float64)

"""
function joMask(n::Integer,idx::Vector{VDT};
    DDT::DataType=joFloat,RDT::DataType=DDT,name::String="joMask") where {VDT<:Integer}

    l=length(idx)
    n>=l || throw(joLinearFunctionException("joMask length(idx) must be <= n"))
    n>=max(idx...) || throw(joLinearFunctionException("joMask max(idx) must be <= n"))
    mask=falses(n)
    mask[idx].=true
    return joLinearFunctionFwd(n,n,
        v1->jo_convert(RDT,mask.*v1,false),
        v2->jo_convert(DDT,mask.*v2,false),
        v3->jo_convert(DDT,mask.*v3,false),
        v4->jo_convert(RDT,mask.*v4,false),
        DDT,RDT;
        name=name,
        fMVok=true
        )
end
"""
    julia> op = joMask(mask;[makecopy=...,][DDT=...,][RDT=...,][name=...])

Mask operator with BitArray mask

# Signature

    function joMask(mask::BitArray{1};
        makecopy::Bool=true,DDT::DataType=joFloat,RDT::DataType=DDT,name::String="joMask")

# Arguments
- `mask`: BitArray mask of true indecies
- keywords
    - `makecopy`: copy mask array
    - `DDT`: domain data type
    - `RDT`: range data type
    - `name`: custom name

# Examples

define BitArray mask and operator

    mask=falses(3)
    mask[[1,3]]=true

    A=joMask(mask)

examples with RDT/DDT

    A=joMask(mask; DDT=Float32)
    A=joMask(mask; DDT=Float32,RDT=Float64)

"""
function joMask(mask::BitArray{1};
    makecopy::Bool=true,DDT::DataType=joFloat,RDT::DataType=DDT,name::String="joMask")

    n=length(mask)
    mymask= makecopy ? Base.deepcopy(mask) : mask
    return joLinearFunctionFwd(n,n,
        v1->jo_convert(RDT,mymask.*v1,false),
        v2->jo_convert(DDT,mymask.*v2,false),
        v3->jo_convert(DDT,mymask.*v3,false),
        v4->jo_convert(RDT,mymask.*v4,false),
        DDT,RDT;
        name=name,
        fMVok=true
        )
end


@testset "$(rpad("Jacobian",80))" begin

    n = 1
    T = Float64

    x = [T(π),]
    j = reshape(2x, 1, 1)

    function F(x::Vector, b::Vector)
        b[:] = x.^2
    end

    function J(x::Vector, A::Matrix)
        A[:,:] = 2x
    end

    set_config(:jacobian_autodiff, true)
    JPAD = getJacobianParameters(nothing, F, T, n)

    set_config(:jacobian_autodiff, false)
    JPFD = getJacobianParameters(nothing, F, T, n)

    JPUS = getJacobianParameters(J, F, T, n)

    @test typeof(JPAD) <: JacobianParametersAD
    @test typeof(JPFD) <: JacobianParametersFD
    @test typeof(JPUS) <: JacobianParametersUser

    function test_jac(j1, j2, atol)
        for i in eachindex(j1,j2)
            @test j1[i] ≈ j2[i] atol=atol
        end
    end

    jad = zero(j)
    jfd = zero(j)
    jus = zero(j)

    computeJacobian(x, jad, JPAD)
    computeJacobian(x, jfd, JPFD)
    computeJacobian(x, jus, JPUS)

    test_jac(jad, j, eps())
    test_jac(jfd, j, 1E-7)
    test_jac(jus, j, 0)

    jad2 = zero(j)
    jfd2 = zero(j)

    computeJacobianAD(x, jad2, F)
    computeJacobianFD(x, jfd2, F, get_config(:jacobian_fd_ϵ))

    test_jac(jad, jad2, 0)
    test_jac(jfd, jfd2, 0)

end


function test_backend(run::Function, name::String; check_object::Bool = true)
    function check(code::String, expected, expected_output::String = "")
        c = IOCapture.capture() do
            run(code)
        end

        if check_object
            test_object(c.value, expected)
        else
            @test c.value == expected
        end

        @test c.output == expected_output
    end

    @testset "Empty program" begin
        @test isnothing(run(""))
    end

    @testset "Integer expression" begin
        for (code, expected) in [
            ("5", 5),
            ("10", 10),
            ("-5", -5),
            ("-10", -10),
            ("5 + 5 + 5 + 5 - 10", 10),
            ("2 * 2 * 2 * 2 * 2", 32),
            ("-50 + 100 + -50", 0),
            ("5 * 2 + 10", 20),
            ("5 + 2 * 10", 25),
            ("20 + 2 * -10", 0),
            ("50 / 2 * 2 + 10", 60),
            ("2 * (5 + 10)", 30),
            ("3 * 3 * 3 + 10", 37),
            ("3 * (3 * 3) + 10", 37),
            ("(5 + 10 * 2 + 15 / 3) * 2 + -10", 50),
        ]
            check(code, expected)
        end
    end

    @testset "Boolean expression" begin
        for (code, expected) in [
            ("false", false),
            ("true", true),
            ("1 < 2", true),
            ("1 > 2", false),
            ("1 < 1", false),
            ("1 > 1", false),
            ("1 == 1", true),
            ("1 != 1", false),
            ("1 == 2", false),
            ("1 != 2", true),
            ("true == true", true),
            ("false == false", true),
            ("true == false", false),
            ("true != false", true),
            ("false != true", true),
            ("(1 < 2) == true", true),
            ("(1 < 2) == false", false),
            ("(1 > 2) == true", false),
            ("(1 > 2) == false", true),
            ("\"a\" == \"a\"", true),
            ("\"a\" != \"a\"", false),
            ("\"a\" == \"b\"", false),
            ("\"a\" != \"b\"", true),
        ]
            check(code, expected)
        end
    end

    @testset "String expression" begin
        for (code, expected) in [
            ("\"Hello World!\"", "Hello World!"),
            ("\"newline\n\n\t\"", "newline\n\n\t"),
            (""""Hello" + " " + "World!\"""", "Hello World!"),
        ]
            check(code, expected)
        end
    end

    @testset "Array" begin
        @testset "Literal" begin
            for (code, expected) in
                [("[]", []), ("[1, 2, 3]", [1, 2, 3]), ("[1, 2 * 2, 3 + 3]", [1, 4, 6])]
                check(code, expected)
            end
        end

        @testset "Index" begin
            for (code, expected) in [
                ("[1, 2, 3][0]", 1),
                ("[1, 2, 3][1]", 2),
                ("[1, 2, 3][2]", 3),
                ("let i = 0; [1][i]", 1),
                ("[1, 2, 3][1 + 1]", 3),
                ("let myArray = [1, 2, 3]; myArray[2];", 3),
                ("let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2]", 6),
                ("let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]", 2),
                ("[1, 2, 3][3]", nothing),
                ("[1, 2, 3][-1]", nothing),
            ]
                check(code, expected)
            end
        end
    end

    @testset "Hash" begin
        @testset "Literal" begin
            for (input, expected) in [
                ("""{"foo": "bar", true: false}""", Dict("foo" => "bar", true => false)),
                ("{1: 2, 1: 3, 1: 4}", Dict(1 => 4)),
                (
                    "{null: [], [2]: null, {3: 5}: {\"1\": 2}}",
                    Dict(nothing => [], [2] => nothing, Dict(3 => 5) => Dict("1" => 2)),
                ),
            ]
                check(input, expected)
            end
        end

        @testset "Index" begin
            for (code, expected) in [
                ("{\"foo\": 5}[\"foo\"]", 5),
                ("{\"foo\": 5}[\"bar\"]", nothing),
                ("let key = \"foo\"; {\"foo\": 5}[key]", 5),
                ("{}[\"foo\"]", nothing),
                ("{5: 5}[5]", 5),
                ("{true: 5}[true]", 5),
                ("{false: 5}[false]", 5),
                ("let a = [1, 2]; {a: 2}[[1, 2]]", 2),
                ("let a = {1: 2, 3: 4}; {a: 2}[{3: 4, 1: 2}]", 2),
            ]
                check(code, expected)
            end

            @testset "Maybe broken" begin
                for (code, expected) in [
                    ("let a = {1: 2, true: 3}; a[1] + a[true]", 5), # Julia backend fails this test since in Julia `true == 1` holds
                ]
                    if name == "julia"
                        @test_broken run(code) == expected
                    else
                        check(code, expected)
                    end
                end
            end
        end
    end

    @testset "Bang operator" begin
        for (code, expected) in [
            ("!true", false),
            ("!false", true),
            ("!5", false),
            ("!!true", true),
            ("!!false", false),
            ("!!5", true),
            ("!\"hello\"", false),
            ("!null", true),
            ("![1, 2, 3]", false),
            ("!{1: 2}", false),
            ("!(fn(x) {x})", false),
        ]
            check(code, expected)
        end
    end

    @testset "If-else expression" begin
        for (code, expected) in [
            ("if (true) { 10 }", 10),
            ("if (false) { 10 }", nothing),
            ("if (null) { 2 } else { 3 }", 3),
            ("if (1) { 10 }", 10),
            ("if (1 < 2) { 10 }", 10),
            ("if (1 > 2) { 10 }", nothing),
            ("if (1 > 2) { 10 } else { 20 }", 20),
            ("if (1 < 2) { 10 } else { 20 }", 10),
        ]
            check(code, expected)
        end
    end

    @testset "While statement" begin
        @testset "Plain" begin
            for (code, expected) in [
                ("let x = 1; while (false) { x = x + 1; } x", 1),
                ("let x = 1; let y = 1; while (y > 0) { y = y - 1; x = x + 1; } x", 2),
            ]
                check(code, expected)
            end
        end

        @testset "Nested" begin
            for (code, expected) in [(
                """
                let a = 6;
                let b = a;
                let x = 1;
                while (x > 0) {
                    x = x - 1;
                    a = 5;
                    b = b + a;
                    let c = 8;
                    c = c / 2;
                    b = b + c;
                    let y = 1;
                    while (y > 0) {
                        y = y - 1;
                        a = a + 3;
                        b = b + a;
                    }
                }
                b = b + a;
                """,
                31,
            )]
                check(code, expected)
            end
        end

        @testset "Inside functions" begin
            for (code, expected) in [
                (
                    """
                    let f = fn(x) {
                        while (x > 0) {
                            if (x == 3) {
                                return 4;
                            }
                            x = x - 1;
                        }
                    }
                    f(4);
                    """,
                    4,
                ),
                (
                    """
                    let f = fn(x) {
                        while (x > 0) {
                            if (x == 3) {
                                return 4;
                            }
                            x = x - 1;
                        }
                    }
                    f(2);
                    """,
                    nothing,
                ),
                (
                    """
                    let x = 5;
                    let y = 0;
                    while (x > 0) {
                        x = x - 1;
                        let z = 5;
                        let f = fn() {
                            let w = 1;
                            while (z > 0) {
                                w = w * 2;
                                y = y + z * w;
                                z = z - 1;
                                f();
                            }
                        }
                        f();
                    }
                    y;
                    """,
                    150,
                ),
            ]
                check(code, expected)
            end
        end
    end

    @testset "Break statement" begin
        for (code, expected) in [(
            """
            let a = 2;
            let b = 0;
            while (a > 0) {
                b = b + a;
                if (a == 2) {
                    break;
                }
                a = a - 1;
            }
            b;
            """,
            2,
        )]
            check(code, expected)
        end
    end

    @testset "Continue statement" begin
        for (code, expected) in [(
            """
            let a = 4;
            let b = 0;
            while (a > 0) {
                a = a - 1;
                if (a == 2) {
                    continue;
                }
                b = b + a;
            }
            b;
            """,
            4,
        )]
            check(code, expected)
        end
    end

    @testset "Return statement" begin
        for (code, expected) in [
            ("return 10;", 10),
            ("return 10; 9;", 10),
            ("return 2 * 5; 9;", 10),
            ("9; return 2 * 5; 9;", 10),
            (
                """
                if (10 > 1) {
                    if (10 > 1) {
                        return 10;
                    }

                    return 1;
                }
                """,
                10,
            ),
            ("while (true) { return 10; }", 10),
        ]
            check(code, expected)
        end
    end

    @testset "Let statement" begin
        @testset "Plain" begin
            for (code, expected) in [
                ("let a = 5; a;", 5),
                ("let a = 5 * 5; a;", 25),
                ("let a = 5; let b = a; b;", 5),
                ("let a = 5; let b = a; let c = a + b + 5; c;", 15),
                ("let a = 5; a = a + 1; a;", 6),
            ]
                check(code, expected)
            end
        end

        @testset "Reassign" begin
            for (code, expected) in [("let a = 5; a = a + 1; a;", 6)]
                check(code, expected)
            end
        end

        @testset "Redefine" begin
            for (code, expected) in [(
                """
                let x = 5;
                let y = 1;
                let z = 0;
                while (y > 0) {
                    z = z + x;
                    let x = 9;
                    z = z + x;
                    y = y - 1;
                }
                z = z + x;
                z;
                """,
                19,
            )]
                if name == "julia"
                    @test_broken run(code) == expected
                else
                    check(code, expected)
                end
            end
        end
    end

    @testset "Function" begin
        if name == "evaluator"
            @testset "Definition" begin
                code = "fn(x) { x + 2; };"
                result = run(code)

                @test isa(result, m.FunctionObj)
                @test string(result.parameters[1]) == "x"
                @test string(result.body) == "(x + 2)"
            end
        end

        @testset "Application" begin
            for (code, expected) in [
                ("let identity = fn(x) { x; }; identity(5);", 5),
                ("let identity = fn(x) { return x; }; identity(5);", 5),
                ("let double = fn(x) { x * 2; }; double(5);", 10),
                ("let add = fn(x, y) { x + y; }; add(5, 5);", 10),
                ("let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20),
                ("fn(x) { x; }(5)", 5),
            ]
                check(code, expected)
            end
        end

        @testset "Closure" begin
            for (code, expected) in [(
                """
               let newAdder = fn(x) {
                 fn(y) { x + y };
               }

               let addTwo = newAdder(2);
               addTwo(2);
               """,
                4,
            ), (
                """
                let a = 2;
                let ans = [];

                let g = fn() {
                    let b = 2;
                    let f = fn() {
                        b = b - 1;
                        return b;
                    }
                    return f;
                }

                let f = g();

                ans = push(ans, f());
                ans = push(ans, f());

                let ff = g();

                ans = push(ans, ff());
                ans = push(ans, ff());

                ans;
                """,
                [1, 0, 1, 0],
            )]
                check(code, expected)
            end
        end

        @testset "Recursion" begin
            for (code, expected) in [(
                """
                let fibonacci = fn(x) {
                if (x == 0) {
                    0
                } else {
                    if (x == 1) {
                        return 1;
                    } else {
                        fibonacci(x - 1) + fibonacci(x - 2);
                    }
                    }
                };

                fibonacci(10);
                """,
                55,
            )]
                check(code, expected)
            end
        end
    end

    @testset "Built-in functions" begin
        @testset "len" begin
            for (code, expected) in [
                ("len(\"\")", 0),
                ("len(\"four\")", 4),
                ("len([1, 2, 3])", 3),
                ("len([])", 0),
                ("len([[1, 2, 3], [4, 5, 6]])", 2),
                ("len({\"a\": 1, \"b\": 2})", 2),
                ("len({})", 0),
            ]
                check(code, expected)
            end
        end

        @testset "first" begin
            for (code, expected) in [
                ("first([1, 2, 3])", 1),
                ("first([])", nothing),
                ("first(\"hello\")", "h"),
                ("first(\"\")", nothing),
            ]
                check(code, expected)
            end
        end

        @testset "last" begin
            for (code, expected) in [
                ("last([1, 2, 3])", 3),
                ("last([])", nothing),
                ("last(\"hello\")", "o"),
                ("last(\"\")", nothing),
            ]
                check(code, expected)
            end
        end

        @testset "rest" begin
            for (code, expected) in [
                ("rest([1, 2, 3])", [2, 3]),
                ("rest([1])", []),
                ("rest([])", nothing),
                ("rest(\"hello\")", "ello"),
                ("rest(\"h\")", ""),
                ("rest(\"\")", nothing),
            ]
                check(code, expected)
            end
        end

        @testset "push" begin
            for (code, expected) in [("push([], 2)[0]", 2), ("push({2: 3}, 4, 5)[4]", 5)]
                check(code, expected)
            end
        end

        @testset "type" begin
            for (code, expected) in [
                ("type(false)", m.BOOLEAN_OBJ),
                ("type(true)", m.BOOLEAN_OBJ),
                ("type(null)", m.NULL_OBJ),
                ("type(0)", m.INTEGER_OBJ),
                ("type(fn (x) { x })", name == "vm" ? m.CLOSURE_OBJ : m.FUNCTION_OBJ),
                ("type(\"hello\")", m.STRING_OBJ),
                ("type([1, 2])", m.ARRAY_OBJ),
                ("type({1:2})", m.HASH_OBJ),
            ]
                check(code, expected)
            end
        end

        @testset "puts" begin
            for (code, expected_output) in [
                ("puts()", ""),
                ("puts(1)", "1\n"),
                ("puts(\"hello\")", "hello\n"),
                ("puts([23, \"hello\"])", "[23, \"hello\"]\n"),
            ]
                check(code, nothing, expected_output)
            end
        end

        @testset "Complex combination" begin
            for (code, expected) in [
                (
                    """
                    let map = fn(arr, f) {
                        let iter = fn(arr, accumulated) {
                            if (len(arr) == 0) {
                                accumulated
                            } else {
                                iter(rest(arr), push(accumulated, f(first(arr))));
                            }
                        };

                        iter(arr, []);
                    };

                    let a = [1, 2, 3, 4];
                    let double = fn(x) { x * 2 };
                    map(a, double)[3]
                    """,
                    8,
                ),
                (
                    """
                    let reduce = fn(arr, initial, f) {
                        let iter = fn(arr, result) {
                            if (len(arr) == 0) {
                                result
                            } else {
                                iter(rest(arr), f(result, first(arr)))
                            }
                        }

                        iter(arr, initial)
                    }

                    let sum = fn(arr) {
                        reduce(arr, 0, fn(initial, el) { initial + el })
                    }

                    sum([1, 2, 3, 4, 5])
                    """,
                    15,
                ),
            ]
                check(code, expected)
            end
        end
    end

    @testset "Macro" begin
        for (code, expected) in [
            (
                """
                let double_len_macro = macro(x) { quote(len(unquote(x)) * 2); };
                let double_len = double_len_macro([1, 2, 3]);
                    double_len;
                """,
                6,
            ),
            (
                """
                let unless = macro(condition, consequence, alternative) {
                    quote(if (!(unquote(condition))) {
                        unquote(consequence);
                    } else {
                        unquote(alternative);
                    });
                };

                unless(10 > 5, "not greater", "greater");
                """,
                "greater",
            ),
        ]
            check(code, expected)
        end
    end

    @testset "Error handling" begin
        @testset "Invalid expression" begin
            for (code, expected) in [
                # `INTEGER + BOOLEAN` works well in Julia
                ("5 + null", "type mismatch: INTEGER + NULL"),
                ("5 + null; 5;", "type mismatch: INTEGER + NULL"),
                ("-null", "unknown operator: -NULL"),
                ("null + null;", "unknown operator: NULL + NULL"),
                ("5; null + null; 5", "unknown operator: NULL + NULL"),
                ("if (10 > 1) { null + null; }", "unknown operator: NULL + NULL"),
                (
                    """
                    if (10 > 1) {
                        if (10 > 1) {
                            return null + null;
                        }

                        return 1;
                    }
                   """,
                    "unknown operator: NULL + NULL",
                ),
                ("\"Hello\" - \"World\"", "unknown operator: STRING - STRING"),
                ("5 / 0", "divide error: division by zero"),
                ("[5 / 0]", "divide error: division by zero"),
                ("{5 / 0: 2}", "divide error: division by zero"),
                ("{2: 5 / 0}", "divide error: division by zero"),
                ("(5 / 0) + (5 / 0)", "divide error: division by zero"),
                ("if (5 / 0) { 2 }", "divide error: division by zero"),
                ("while (5 / 0) { 2 }", "divide error: division by zero"),
                ("(5 / 0)()", "divide error: division by zero"),
                ("let a = fn(x) { x }; a(5 / 0);", "divide error: division by zero"),
                ("{1:2}[5 / 0]", "divide error: division by zero"),
                ("(5 / 0)[2]", "divide error: division by zero"),
                ("let a = 5 / 0", "divide error: division by zero"),
                ("if (true) { 5 / 0; 2 + 3; 4; }", "divide error: division by zero"),
                ("\"str\"[1]", "index operator not supported: STRING"),
                ("[1, 2, 3][\"23\"]", "unsupported index type: STRING"),
                ("2(3)", "not a function: INTEGER"),
                ("fn() { 1; }(1);", "argument error: wrong number of arguments: got 1"),
                ("fn(a) { a; }();", "argument error: wrong number of arguments: got 0"),
                (
                    "fn(a, b) { a + b; }(1);",
                    "argument error: wrong number of arguments: got 1",
                ),
            ]
                if check_object
                    check(code, expected, "ERROR: $expected\n")
                else
                    check(code, nothing, "ERROR: $expected\n")
                end
            end

            @testset "Semantic analysis" begin
                for (code, expected) in [
                    ("foobar", "identifier not found: foobar"),
                    ("x = 2;", "identifier not found: x"),
                    ("let a = 2; let a = 4;", "a is already defined"),
                ]
                    if check_object
                        check(code, expected, "ERROR: $expected\n")
                    else
                        check(code, nothing, "ERROR: $expected\n")
                    end
                end
            end
        end

        @testset "Built-in function" begin
            for (code, expected) in [
                (
                    "len(1)",
                    "argument error: argument to `len` is not supported, got INTEGER",
                ),
                (
                    "len(\"one\", \"two\")",
                    "argument error: wrong number of arguments. Got 2 instead of 1",
                ),
                (
                    "first([1, 2], [2])",
                    "argument error: wrong number of arguments. Got 2 instead of 1",
                ),
                (
                    "first(1)",
                    "argument error: argument to `first` is not supported, got INTEGER",
                ),
                (
                    "last(1)",
                    "argument error: argument to `last` is not supported, got INTEGER",
                ),
                (
                    "last([1, 2], [2])",
                    "argument error: wrong number of arguments. Got 2 instead of 1",
                ),
                (
                    "rest(1)",
                    "argument error: argument to `rest` is not supported, got INTEGER",
                ),
                (
                    "rest([1, 2], [2])",
                    "argument error: wrong number of arguments. Got 2 instead of 1",
                ),
                (
                    "push()",
                    "argument error: wrong number of arguments. Got 0 instead of 2 or 3",
                ),
                (
                    "push({}, 2)",
                    "argument error: argument to `push` is not supported, got HASH",
                ),
                (
                    "push([], 2, 3)",
                    "argument error: argument to `push` is not supported, got ARRAY",
                ),
                (
                    "type(1, 2)",
                    "argument error: wrong number of arguments. Got 2 instead of 1",
                ),
            ]
                if check_object
                    check(code, expected, "ERROR: $expected\n")
                else
                    check(code, nothing, "ERROR: $expected\n")
                end
            end
        end
    end
end


abstract type Algorithm end
# Base.copy(o::Algorithm) = deepcopy(o)
init!(o::Algorithm, p) = o
update!(o::Algorithm, gx) = nothing
Base.merge!(o::T, o2::T, γ) where {T<:Algorithm} = o
Base.show(io::IO, o::Algorithm) = print(io, name(o, false, false))

abstract type SGAlgorithm <: Algorithm end

#-----------------------------------------------------------------------# SGD
"""
    SGD()

Stochastic Gradient Descent.
"""
struct SGD <: SGAlgorithm end

# #-----------------------------------------------------------------------# NSGD
# """
#     NSGD(α)

# Nesterov accelerated Proximal Stochastic Gradient Descent.
# """
# struct NSGD <: SGUpdater
#     α::Float64
#     v::Vector{Float64}
#     θ::Vector{Float64}
#     NSGD(α = 0.0, p = 0) = new(α, zeros(p), zeros(p))
# end
# init(u::NSGD, p::Int) = NSGD(u.α, p)
# function Base.merge!(o::NSGD, o2::NSGD, γ::Float64)
#     o.α == o2.α || error("Merge Failed.  NSGD objects use different α.")
#     smooth!(o.v, o2.v, γ)
#     smooth!(o.θ, o2.θ, γ)
# end

#-----------------------------------------------------------------------# ADAGRAD
"""
    ADAGRAD()

A variation of [`SGD`](@ref) with element-wise weights generated by the average of the squared gradients.
"""
mutable struct ADAGRAD <: SGAlgorithm
    h::Vector{Float64}
    n::Int
    ADAGRAD() = new(zeros(0), 0)
end
init!(o::ADAGRAD, p) = (o.h = zeros(p))
Base.merge!(o::ADAGRAD, o2::ADAGRAD, γ) = (smooth!(o.h, o2.h, γ); o)
function update!(o::ADAGRAD, gx)
    o.n += 1
    for j in eachindex(o.h)
        o.h[j] = smooth(o.h[j], gx[j]^2, 1 / o.n)
    end
end

#-----------------------------------------------------------------------# RMSPROP
"""
    RMSPROP(α = .9)

A Variation of [`ADAGRAD`](@ref) that uses element-wise weights generated by an exponentially
weighted mean of the squared gradients.
"""
mutable struct RMSPROP <: SGAlgorithm
    α::Float64
    h::Vector{Float64}
    RMSPROP(α = .9) = new(α, zeros(0))
end
init!(a::RMSPROP, p) = (a.h = zeros(p))
function Base.merge!(o::RMSPROP, o2::RMSPROP, γ)
    o.α = smooth(o.α, o2.α, γ)
    smooth!(o.h, o2.h, γ)
end
function update!(o::RMSPROP, gx)
    for j in eachindex(o.h)
        o.h[j] = smooth(gx[j]^2, o.h[j], o.α)
    end
end

#-----------------------------------------------------------------------# ADADELTA
"""
    ADADELTA(ρ = .95)

An extension of [`ADAGRAD`](@ref).
"""
mutable struct ADADELTA <: SGAlgorithm
    v::Vector{Float64}
    Δ::Vector{Float64}
    δ::Vector{Float64}
    ρ::Float64
    ADADELTA(ρ = .95) = new(Float64[], Float64[], Float64[], ρ)
end
init!(o::ADADELTA, p) = (o.v = zeros(p); o.δ = zeros(p); o.Δ = zeros(p))
function Base.merge!(o::ADADELTA, o2::ADADELTA, γ)
    smooth!(o.v, o2.v, γ)
    smooth!(o.Δ, o2.Δ, γ)
    o.ρ = smooth(o.ρ, o2.ρ, γ)
    o
end
function update!(o::ADADELTA, gx)
    for j in eachindex(o.v)
        g2 = gx[j] ^ 2
        o.v[j] = smooth(g2 + ϵ, o.v[j], o.ρ)
        o.δ[j] = sqrt(o.Δ[j] / o.v[j] + ϵ)  # not multiplied by gx[j] as in paper
        o.Δ[j] = smooth(o.δ[j]^2 * g2, o.Δ[j], o.ρ)
    end
end

#-----------------------------------------------------------------------# ADAM
"""
    ADAM(β1 = .99, β2 = .999)

A variant of [`SGD`](@ref) with element-wise learning rates generated by exponentially weighted
first and second moments of the gradient.
"""
mutable struct ADAM <: SGAlgorithm
    m::Vector{Float64}
    v::Vector{Float64}
    β1::Float64
    β2::Float64
    ADAM(β1 = .99, β2 = .999) = new(Float64[], Float64[], β1, β2)
end
init!(o::ADAM, p) = (o.m = zeros(p); o.v = zeros(p))
function Base.merge!(o::ADAM, o2::ADAM, γ)
    smooth!(o.m, o2.m, γ)
    smooth!(o.v, o2.v, γ)
    o.β1 = smooth(o.β1, o2.β1, γ)
    o.β2 = smooth(o.β2, o2.β2, γ)
    o
end
function update!(o::ADAM, gx)
    for j in eachindex(o.m)
        g = gx[j]
        o.m[j] = smooth(g, o.m[j], o.β1)
        o.v[j] = smooth(g * g, o.v[j], o.β2)
    end
end

#-----------------------------------------------------------------------# ADAMAX
"""
    ADAMAX(η, β1 = .9, β2 = .999)

ADAMAX with momentum parameters `β1`, `β2`.  ADAMAX is an extension of [`ADAM`](@ref).
"""
mutable struct ADAMAX <: SGAlgorithm
    β1::Float64
    β2::Float64
    m::Vector{Float64}
    v::Vector{Float64}
    function ADAMAX(β1::Float64 = 0.9, β2::Float64 = .999)
        0 < β1 < 1 || error("β1 must be in (0, 1)")
        0 < β2 < 1 || error("β2 must be in (0, 1)")
        new(β1, β2, zeros(0), zeros(0))
    end
end
init!(o::ADAMAX, p) = (o.m = zeros(p); o.v = zeros(p))
function Base.merge!(o::ADAMAX, o2::ADAMAX, γ)
    smooth!(o.m, o2.m, γ)
    smooth!(o.v, o2.v, γ)
    o.β1 = smooth(o.β1, o2.β1, γ)
    o.β2 = smooth(o.β2, o2.β2, γ)
    o
end
function update!(o::ADAMAX, gx)
    for j in eachindex(o.m)
        o.m[j] = smooth(gx[j], o.m[j], o.β1)
        o.v[j] = max(abs(gx[j]) + ϵ, o.β2 * o.v[j])
    end
end

# #-----------------------------------------------------------------------# NADAM
# """
#     NADAM(α1 = .99, α2 = .999)

# Adaptive Moment Estimation with momentum parameters `α1` and `α2`.
# """
# mutable struct NADAM <: SGUpdater
#     β1::Float64
#     β2::Float64
#     M::Vector{Float64}
#     V::Vector{Float64}
#     nups::Int
#     function NADAM(β1::Float64 = 0.99, β2::Float64 = .999, p::Integer = 0)
#         0 < β1 < 1 || error("β1 must be in (0, 1)")
#         0 < β2 < 1 || error("β2 must be in (0, 1)")
#         new(β1, β2, zeros(p), zeros(p), 0)
#     end
# end
# init(u::NADAM, p) = NADAM(u.β1, u.β2, p)
# function Base.merge!(o::NADAM, o2::NADAM, γ::Float64)
#     (o.β1 == o2.β1) && (o.β2 == o2.β2) ||
#         error("Merge failed.  NADAM objects use different momentum parameters.")
#     o.nups += o2.nups
#     smooth!(o.M, o2.M, γ)
#     smooth!(o.V, o2.V, γ)
# end

#-----------------------------------------------------------------------# MSPI, OMAS, OMAP
"""
    MSPI()

Majorized Stochastic Proximal Iteration.
"""
struct MSPI <: Algorithm end

"""
    OMAP()

Online MM via Averaged Parameter.
"""
struct OMAP <: Algorithm end

"""
    OMAS()

Online MM via Averaged Surrogate.
"""
mutable struct OMAS <: Algorithm
    a::Vector{Float64}
    b::Vector{Float64}
    A::Matrix{Float64}
    B::Matrix{Float64}
    OMAS() = new(zeros(0), zeros(0), zeros(0, 0), zeros(0, 0))
end
init!(o::OMAS, p) = (o.a = zeros(p); o.b = zeros(p))


ToVec3(x::Vec{3}) = x
ToVec3(x::Vec{2}) = Vec(x[1], x[2], 0)

@testset "Quaternion" begin
    for T in (Float32, Float64)
        # basic constructors
        @test (@inferred Quaternion{T}((1,2,3,4)))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion{T}(1,2,3,4))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion{T}(Vec(1,2,3,4)))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion{T}(Vec(1,2,3)))::Quaternion{T} |> Tuple === map(T, (0,1,2,3))
        @test (@inferred Quaternion{T}(Vec(1,2)))::Quaternion{T} |> Tuple === map(T, (0,1,2,0))
        @test (@inferred Quaternion{T}(Vec(1)))::Quaternion{T} |> Tuple === map(T, (0,1,0,0))
        @test (@inferred Quaternion{T}(4, Vec(1,2,3)))::Quaternion{T} |> Tuple === map(T, (4,1,2,3))
        @test (@inferred Quaternion{T}(4, Vec(1,2)))::Quaternion{T} |> Tuple === map(T, (4,1,2,0))
        @test (@inferred Quaternion{T}(4, Vec(1)))::Quaternion{T} |> Tuple === map(T, (4,1,0,0))
        @test (@inferred Quaternion{T}(4))::Quaternion{T} |> Tuple === map(T, (4,0,0,0))
        @test (@inferred Quaternion((T(1),2,3,4)))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion(T(1),2,3,4))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion(Vec(T(1),2,3,4)))::Quaternion{T} |> Tuple === map(T, (1,2,3,4))
        @test (@inferred Quaternion(Vec(T(1),2,3)))::Quaternion{T} |> Tuple === map(T, (0,1,2,3))
        @test (@inferred Quaternion(Vec(T(1),2)))::Quaternion{T} |> Tuple === map(T, (0,1,2,0))
        @test (@inferred Quaternion(Vec(T(1))))::Quaternion{T} |> Tuple === map(T, (0,1,0,0))
        @test (@inferred Quaternion(T(4), Vec(1,2,3)))::Quaternion{T} |> Tuple === map(T, (4,1,2,3))
        @test (@inferred Quaternion(T(4), Vec(1,2)))::Quaternion{T} |> Tuple === map(T, (4,1,2,0))
        @test (@inferred Quaternion(T(4), Vec(1)))::Quaternion{T} |> Tuple === map(T, (4,1,0,0))
        @test (@inferred Quaternion(T(4)))::Quaternion{T} |> Tuple === map(T, (4,0,0,0))

        # properties
        q = Quaternion{T}(1,2,3,4)
        @test propertynames(q) == (:scalar, :vector, :data)
        get_scalar = q -> q.scalar
        get_vector = q -> q.vector
        get_data = q -> q.data
        @test (@inferred get_scalar(q))::T == T(1)
        @test (@inferred get_vector(q))::Vec{3, T} == Vec{3, T}(2,3,4)
        @test (@inferred get_data(q))::NTuple{4, T} == map(T, (1,2,3,4))

        # quaternion
        @test (@inferred quaternion(T(π/4), Vec{2, T}(1,2)))::Quaternion{T} == (@inferred quaternion(T(π/4), Vec{3, T}(1,2,0)))::Quaternion{T}
        @test (@inferred quaternion(T, π/4, Vec(1,2)))::Quaternion{T} == (@inferred quaternion(T, π/4, Vec(1,2,0)))::Quaternion{T}
        q = (@inferred quaternion(T(π/4), Vec{3, T}(1,2,3)))::Quaternion{T}
        q = (@inferred quaternion(T, π/4, Vec(1,2,3)))::Quaternion{T}
        @test length(q) == 4
        @test size(q) == (4,)
        @test norm(q) ≈ 1
        @test q/q ≈ 1
        q = (@inferred quaternion(T(π/4), Vec{3, T}(1,2,3), normalize = false))::Quaternion{T}
        q = (@inferred quaternion(T, π/4, Vec(1,2,3), normalize = false))::Quaternion{T}
        @test !(norm(q) ≈ 1)
        @test q/q ≈ 1
        @test q ≈ (@inferred quaternion(T(45), Vec{3, T}(1,2,3), normalize = false, degree = true))::Quaternion{T}
        @test q ≈ (@inferred quaternion(T, 45, Vec(1,2,3), normalize = false, degree = true))::Quaternion{T}

        q = quaternion(rand(T), rand(Vec{3, T}))
        p = quaternion(rand(T), rand(Vec{3, T}), normalize = false)

        # conversion
        @test (@inferred convert(Quaternion{T}, q))::Quaternion{T} == q
        @test (@inferred convert(Quaternion{T}, 3))::Quaternion{T} == Quaternion{T}(3,0,0,0)

        # promotion
        @test (@inferred promote_rule(Quaternion{T}, T)) == Quaternion{T}
        @test (@inferred promote_rule(Quaternion{T}, Int)) == Quaternion{T}
        @test (@inferred promote_rule(Quaternion{T}, Quaternion{T})) == Quaternion{T}
        @test (@inferred promote_rule(Quaternion{T}, Quaternion{Int})) == Quaternion{T}
        @test ((@inferred promote(q, T(3)))::NTuple{2, Quaternion{T}})[2] == Quaternion(3.0,0,0,0)
        @test ((@inferred promote(q, 3))::NTuple{2, Quaternion{T}})[2] == Quaternion(3.0,0,0,0)

        # math operations
        @test (@inferred +q)::Quaternion{T} === q
        @test (@inferred +p)::Quaternion{T} === p
        @test (@inferred -q)::Quaternion{T} == -1 * q
        @test (@inferred -p)::Quaternion{T} == -1 * p
        @test (@inferred q + p)::Quaternion{T} == Quaternion(Vec(q) + Vec(p))
        @test (@inferred q - p)::Quaternion{T} == Quaternion(Vec(q) - Vec(p))
        @test (@inferred 2 * q)::Quaternion{T} == Quaternion(2 * Vec(q))
        @test (@inferred 2 * p)::Quaternion{T} == Quaternion(2 * Vec(p))
        @test (@inferred q * 2)::Quaternion{T} == Quaternion(Vec(q) * 2)
        @test (@inferred p * 2)::Quaternion{T} == Quaternion(Vec(p) * 2)
        @test (@inferred q / 2)::Quaternion{T} == Quaternion(Vec(q) / 2)
        @test (@inferred p / 2)::Quaternion{T} == Quaternion(Vec(p) / 2)
        @test (@inferred norm(q))::T ≈ (@inferred abs(q))::T
        @test (@inferred norm(p))::T ≈ (@inferred abs(p))::T

        # angleaxis
        @test (quaternion((@inferred angleaxis(q))::Tuple{T, Vec{3, T}}...)) ≈ q

        a = rand(T)
        @test (@inferred exp(log(q)))::Quaternion{T} ≈ q
        @test (@inferred exp(log(p)))::Quaternion{T} ≈ p
        @test (@inferred exp(a + q))::Quaternion{T} ≈ exp(a) * exp(q)
        @test (@inferred exp(a + p))::Quaternion{T} ≈ exp(a) * exp(p)
        @test (@inferred log(a * q))::Quaternion{T} ≈ log(a) + log(q)
        @test (@inferred log(a * p))::Quaternion{T} ≈ log(a) + log(p)
        @test (@inferred exp(Quaternion{T}(1,0,0,0)))::Quaternion{T} ≈ exp(1)

        Rq = rotmat(q)
        Rp = rotmat(p)
        r = p * q

        for dim in (2, 3)
            # check multiplications
            x = rand(Vec{dim, T})
            x3 = ToVec3(x)
            @test (q * x / q).vector ≈ Rq ⋅ x3
            @test (p * x / p).vector ≈ Rp ⋅ x3
            @test (r * x / r).vector ≈ Rp ⋅ Rq ⋅ x3
            @test (q * x * inv(q)).vector ≈ Rq ⋅ x3
            @test (p * x * inv(p)).vector ≈ Rp ⋅ x3
            @test (r * x * inv(r)).vector ≈ Rp ⋅ Rq ⋅ x3
            # inverse of rotation
            @test (inv(q) * x * q).vector ≈ inv(Rq) ⋅ x3
            # check order of multiplications
            @test ((q * x) / q).vector ≈ Rq ⋅ x3
            @test (q * (x / q)).vector ≈ Rq ⋅ x3
            # rotate
            @test rotate(x, q) ≈ rotate(x, Rq)
            @test rotate(x, p) ≈ rotate(x, Rp)
            @test rotate(x, r) ≈ rotate(x, Rp ⋅ Rq)
            @test rotate(x, inv(q)) ≈ rotate(x, inv(Rq))
            @test rotate(x, inv(p)) ≈ rotate(x, inv(Rp))
            @test rotate(x, inv(r)) ≈ rotate(x, inv(Rp ⋅ Rq))
        end
        # test with rotmat(θ, n)
        θ = rand(T)
        n = rand(Vec{3, T})
        @test rotmat(θ, n) ≈ rotmat(quaternion(θ, n))
    end
end


using MultipleScattering
using Plots; pyplot()
using Random
using LinearAlgebra

function box_size_convergence(  m = 4,
                                volfrac = 0.05,
                                radius = 1.0,
                                times = [20.0,30.0,40.0,50.0,60.0,70.0],
                                ω = collect(LinRange(0.01,1.0,100))
                                )

    host_medium = Acoustic(1.0, 1.0, 2)
    particle_medium = Acoustic(1.0, 0.0, 2)
    particle_shape = Circle(radius)

    listener_position = [-10.0; 0.0]
    bigshape = TimeOfFlightPlaneWaveToPoint(listener_position, maximum(times))

    seed = MersenneTwister(1).seed
    allparticles = random_particles(particle_medium, particle_shape;
                                            region_shape = bigshape,
                                            volume_fraction = volfrac,
                                            seed = seed
                                    )

    source =  plane_source( host_medium; position = [0.0, 0.0], direction = [1.0, 0.0] )

    simulations = map(times) do t
        println("Calculating response with particles at a maximum of $t away")
        shape = TimeOfFlightPlaneWaveToPoint(listener_position, t)
        particles = filter(p -> p ⊆ shape, allparticles)
        return run(particles, source, [listener_position], ω; basis_order = m)
    end

    return map(x -> frequency_to_time(x; t_vec = LinRange(0.0,100.0,201)), simulations)
end

function plot_box_size_convergence(simulations; times = [20,30,40,50,60,70])

    plot(xguide = "Time (t)", yguide ="Response")
    for s in eachindex(simulations)
        plot!(simulations[s], label = "Box cut at t=$(times[s])")
        plot!(size=(800,600))
    end
    plot!(title="Time response from random particles from increasingly large boxes")
end

simulations = box_size_convergence()
plot_box_size_convergence(simulations)


"""
   probability_check(pomdp::POMDP)
Checks if the transition and observation function of the discrete `pomdp`
have probability mass that sums up to unity for all state-action pairs.
"""
function probability_check(pomdp::POMDP)
    obs_prob_consistency_check(pomdp)
    trans_prob_consistency_check(pomdp)
end

"""
    obs_prob_consistency_check(pomdp::POMDP)
Checks if the observation function of the discrete `pomdp`
has probability mass that sums up to unity for all state-action pairs.
"""
function obs_prob_consistency_check(pomdp::POMDP)
    # initalize space
    sspace = states(pomdp)
    aspace = actions(pomdp)
    ospace = observations(pomdp)
    # iterate through all s-a pairs
    for s in sspace
        for a in aspace
            obs = observation(pomdp, a, s)
            psum = 0.0
            for o in ospace
                p = pdf(obs, o)
                @assert p ≥ 0 "Probability is negative for state: $s, action: $a, observation: $o"
                psum += p
            end
            @assert isapprox(psum, 1.0) "Observation probability does not sum to unity for state: $s, action: $a"
        end
    end
end

"""
    trans_prob_consistency_check(pomdp::Union{MDP, POMDP})
Checks if the transition function of the discrete problem
has probability mass that sums up to unity for all state-action pairs.
"""
function trans_prob_consistency_check(pomdp::Union{MDP, POMDP})
    # initalize space
    sspace = states(pomdp)
    aspace = actions(pomdp)
    # iterate through all s-a pairs
    for s in sspace
        for a in aspace
            tran = transition(pomdp, s, a)
            psum = 0.0
            for sp in sspace
                p = pdf(tran, sp)
                @assert p ≥ 0 "Probability is negative for state: $s, action: $a, next state: $sp"
                psum += p
            end
            @assert isapprox(psum, 1.0) "Transition probability does not sum to unity for state: $s, action: $a"
        end
    end
end


using Random
using Distributions
using BenchmarkTools
using RandomDataODE

n_samples = 10^4;
Tmax = 6.;

# generate initial condition data
ϵ = 0.2;
xdist = Uniform(0.5-ϵ, 0.5 + ϵ);
ydist = Uniform(2.0-ϵ, 2.0 + ϵ);
Random.seed!(100)
u0data = [[rand(xdist), rand(ydist)] for _ in 1:n_samples];

f = sol->sol(Tmax)[1]; # observable function

@btime samples = run_obs_ensemble(u0data, Tmax, f);


export eigs, entropy, polar, random_orthog, random_unitary, Spectrum, svd, truncerror

#
# Linear Algebra of order 2 NDTensors
#
# Even though DenseTensor{_,2} is strided
# and passable to BLAS/LAPACK, it cannot
# be made <: StridedArray

function (
  T1::Tensor{ElT1,2,StoreT1} * T2::Tensor{ElT2,2,StoreT2}
) where {ElT1,StoreT1<:Dense,IndsT1,ElT2,StoreT2<:Dense,IndsT2}
  RM = matrix(T1) * matrix(T2)
  indsR = (ind(T1, 1), ind(T2, 2))
  return tensor(Dense(vec(RM)), indsR)
end

function LinearAlgebra.exp(T::DenseTensor{ElT,2}) where {ElT<:Union{Real,Complex}}
  expTM = exp(matrix(T))
  return tensor(Dense(vec(expTM)), inds(T))
end

function LinearAlgebra.exp(
  T::Hermitian{ElT,<:DenseTensor{ElT,2}}
) where {ElT<:Union{Real,Complex}}
  # exp(::Hermitian/Symmetric) returns Hermitian/Symmetric,
  # so extract the parent matrix
  expTM = parent(exp(matrix(T)))
  return tensor(Dense(vec(expTM)), inds(T))
end

"""
  Spectrum
contains the (truncated) density matrix eigenvalue spectrum which is computed during a
decomposition done by `svd` or `eigen`. In addition stores the truncation error.
"""
struct Spectrum{VecT<:Union{AbstractVector,Nothing}}
  eigs::VecT
  truncerr::Float64
end

eigs(s::Spectrum) = s.eigs
truncerror(s::Spectrum) = s.truncerr

function entropy(s::Spectrum)
  S = 0.0
  eigs_s = eigs(s)
  isnothing(eigs_s) &&
    error("Spectrum does not contain any eigenvalues, cannot compute the entropy")
  for p in eigs_s
    p > 1e-13 && (S -= p * log(p))
  end
  return S
end

function svd_catch_error(A; kwargs...)
  USV = try
    svd(A; kwargs...)
  catch
    return nothing
  end
  return USV
end

function lapack_svd_error_message(alg)
  return "The SVD algorithm `\"$alg\"` has thrown an error,\n" *
         "likely because of a convergance failure. You can try\n" *
         "other SVD algorithms that may converge better using the\n" *
         "`alg` (or `svd_alg` if called through `factorize` or MPS/MPO functionality) keyword argument:\n\n" *
         " - \"divide_and_conquer\" is a divide-and-conquer algorithm\n" *
         "   (LAPACK's `gesdd`). It is fast, but may lead to some innacurate\n" *
         "   singular values for very ill-conditioned matrices.\n" *
         "   It also may sometimes fail to converge, leading to errors\n" *
         "   (in which case `\"qr_iteration\"` or `\"recursive\"` can be tried).\n\n" *
         " - `\"qr_iteration\"` (LAPACK's `gesvd`) is typically slower \n" *
         "   than \"divide_and_conquer\", especially for large matrices,\n" *
         "   but is more accurate for very ill-conditioned matrices \n" *
         "   compared to `\"divide_and_conquer\"`.\n\n" *
         " - `\"recursive\"` is ITensor's custom SVD algorithm. It is very\n" *
         "   reliable, but may be slow if high precision is needed.\n" *
         "   To get an `svd` of a matrix `A`, an eigendecomposition of\n" *
         "   ``A^{\\dagger} A`` is used to compute `U` and then a `qr` of\n" *
         "   ``A^{\\dagger} U`` is used to compute `V`. This is performed\n" *
         "   recursively to compute small singular values.\n\n" *
         "Returning `nothing`. For an output `F = svd(A, ...)` you can check if\n" *
         "`isnothing(F)` in your code and try a different algorithm.\n\n" *
         "To suppress this message in the future, you can wrap the `svd` call in the\n" *
         "`@suppress` macro from the `Suppressor` package.\n"
end

"""
    svd(T::DenseTensor{<:Number,2}; kwargs...)

svd of an order-2 DenseTensor
"""
function LinearAlgebra.svd(T::DenseTensor{ElT,2,IndsT}; kwargs...) where {ElT,IndsT}
  truncate = haskey(kwargs, :maxdim) || haskey(kwargs, :cutoff)

  #
  # Keyword argument deprecations
  #
  use_absolute_cutoff = false
  if haskey(kwargs, :absoluteCutoff)
    @warn "In svd, keyword argument absoluteCutoff is deprecated in favor of use_absolute_cutoff"
    use_absolute_cutoff = get(kwargs, :absoluteCutoff, use_absolute_cutoff)
  end

  use_relative_cutoff = true
  if haskey(kwargs, :doRelCutoff)
    @warn "In svd, keyword argument doRelCutoff is deprecated in favor of use_relative_cutoff"
    use_relative_cutoff = get(kwargs, :doRelCutoff, use_relative_cutoff)
  end

  if haskey(kwargs, :fastsvd) || haskey(kwargs, :fastSVD)
    error(
      "In svd, fastsvd/fastSVD keyword arguments are removed in favor of alg, see documentation for more details.",
    )
  end

  maxdim::Int = get(kwargs, :maxdim, minimum(dims(T)))
  mindim::Int = get(kwargs, :mindim, 1)
  cutoff::Float64 = get(kwargs, :cutoff, 0.0)
  use_absolute_cutoff::Bool = get(kwargs, :use_absolute_cutoff, use_absolute_cutoff)
  use_relative_cutoff::Bool = get(kwargs, :use_relative_cutoff, use_relative_cutoff)
  alg::String = get(kwargs, :alg, "divide_and_conquer")

  #@timeit_debug timer "dense svd" begin
  if alg == "divide_and_conquer"
    MUSV = svd_catch_error(matrix(T); alg=LinearAlgebra.DivideAndConquer())
  elseif alg == "qr_iteration"
    MUSV = svd_catch_error(matrix(T); alg=LinearAlgebra.QRIteration())
  elseif alg == "recursive"
    MUSV = svd_recursive(matrix(T))
  else
    error(
      "svd algorithm $alg is not currently supported. Please see the documentation for currently supported algorithms.",
    )
  end
  if isnothing(MUSV)
    if any(isnan, T)
      println("SVD failed, the matrix you were trying to SVD contains NaNs.")
    else
      println(lapack_svd_error_message(alg))
    end
    return nothing
  end
  MU, MS, MV = MUSV
  conj!(MV)
  #end # @timeit_debug

  P = MS .^ 2
  if truncate
    truncerr, _ = truncate!(
      P;
      mindim=mindim,
      maxdim=maxdim,
      cutoff=cutoff,
      use_absolute_cutoff=use_absolute_cutoff,
      use_relative_cutoff=use_relative_cutoff,
    )
  else
    truncerr = 0.0
  end
  spec = Spectrum(P, truncerr)
  dS = length(P)
  if dS < length(MS)
    MU = MU[:, 1:dS]
    resize!(MS, dS)
    MV = MV[:, 1:dS]
  end

  # Make the new indices to go onto U and V
  u = eltype(IndsT)(dS)
  v = eltype(IndsT)(dS)
  Uinds = IndsT((ind(T, 1), u))
  Sinds = IndsT((u, v))
  Vinds = IndsT((ind(T, 2), v))
  U = tensor(Dense(vec(MU)), Uinds)
  S = tensor(Diag(MS), Sinds)
  V = tensor(Dense(vec(MV)), Vinds)
  return U, S, V, spec
end

function LinearAlgebra.eigen(
  T::Hermitian{ElT,<:DenseTensor{ElT,2,IndsT}}; kwargs...
) where {ElT<:Union{Real,Complex},IndsT}
  # Keyword argument deprecations
  use_absolute_cutoff = false
  if haskey(kwargs, :absoluteCutoff)
    @warn "In svd, keyword argument absoluteCutoff is deprecated in favor of use_absolute_cutoff"
    use_absolute_cutoff = get(kwargs, :absoluteCutoff, use_absolute_cutoff)
  end
  use_relative_cutoff = true
  if haskey(kwargs, :doRelCutoff)
    @warn "In svd, keyword argument doRelCutoff is deprecated in favor of use_relative_cutoff"
    use_relative_cutoff = get(kwargs, :doRelCutoff, use_relative_cutoff)
  end

  truncate = haskey(kwargs, :maxdim) || haskey(kwargs, :cutoff)
  maxdim::Int = get(kwargs, :maxdim, minimum(dims(T)))
  mindim::Int = get(kwargs, :mindim, 1)
  cutoff::Float64 = get(kwargs, :cutoff, 0.0)
  use_absolute_cutoff::Bool = get(kwargs, :use_absolute_cutoff, use_absolute_cutoff)
  use_relative_cutoff::Bool = get(kwargs, :use_relative_cutoff, use_relative_cutoff)

  DM, VM = eigen(matrix(T))

  # Sort by largest to smallest eigenvalues
  p = sortperm(DM; rev=true, by=abs)
  DM = DM[p]
  VM = VM[:, p]

  if truncate
    truncerr, _ = truncate!(
      DM;
      maxdim=maxdim,
      cutoff=cutoff,
      use_absolute_cutoff=use_absolute_cutoff,
      use_relative_cutoff=use_relative_cutoff,
    )
    dD = length(DM)
    if dD < size(VM, 2)
      VM = VM[:, 1:dD]
    end
  else
    dD = length(DM)
    truncerr = 0.0
  end
  spec = Spectrum(DM, truncerr)

  # Make the new indices to go onto V
  l = eltype(IndsT)(dD)
  r = eltype(IndsT)(dD)
  Vinds = IndsT((dag(ind(T, 2)), dag(r)))
  Dinds = IndsT((l, dag(r)))
  V = tensor(Dense(vec(VM)), Vinds)
  D = tensor(Diag(DM), Dinds)
  return D, V, spec
end

"""
    random_unitary(n::Int,m::Int)::Matrix{ComplexF64}
    random_unitary(::Type{ElT},n::Int,m::Int)::Matrix{ElT}

Return a random matrix U of dimensions (n,m)
such that if n >= m, U'*U is the identity, or if
m > n U*U' is the identity. Optionally can pass a numeric
type as the first argument to obtain a matrix of that type.

Sampling is based on https://arxiv.org/abs/math-ph/0609050
such that in the case `n==m`, the unitary matrix will be sampled
according to the Haar measure.
"""
function random_unitary(::Type{ElT}, n::Int, m::Int) where {ElT<:Number}
  if n < m
    return Matrix(random_unitary(ElT, m, n)')
  end
  F = qr(randn(ElT, n, m))
  Q = Matrix(F.Q)
  # The upper triangle of F.factors
  # are the elements of R.
  # Multiply cols of Q by the signs
  # that would make diagonal of R
  # non-negative:
  for c in 1:size(Q, 2)
    Q[:, c] .*= sign(F.factors[c, c])
  end
  return Q
end

random_unitary(n::Int, m::Int) = random_unitary(ComplexF64, n, m)

"""
    random_orthog(n::Int,m::Int)::Matrix{Float64}
    random_orthog(::Type{ElT},n::Int,m::Int)::Matrix{ElT}

Return a random, real matrix O of dimensions (n,m)
such that if n >= m, transpose(O)*O is the
identity, or if m > n O*transpose(O) is the
identity. Optionally can pass a real number type
as the first argument to obtain a matrix of that type.
"""
random_orthog(::Type{ElT}, n::Int, m::Int) where {ElT<:Real} = random_unitary(ElT, n, m)

random_orthog(n::Int, m::Int) = random_orthog(Float64, n, m)

"""
    qr_positive(M::AbstractMatrix)

Compute the QR decomposition of a matrix M
such that the diagonal elements of R are
non-negative. Such a QR decomposition of a
matrix is unique. Returns a tuple (Q,R).
"""
function qr_positive(M::AbstractMatrix)
  sparseQ, R = qr(M)
  Q = convert(Matrix, sparseQ)
  nc = size(Q, 2)
  for c in 1:nc
    if real(R[c, c]) < 0.0
      R[c, c:end] *= -1
      Q[:, c] *= -1
    end
  end
  return (Q, R)
end

function LinearAlgebra.eigen(
  T::DenseTensor{ElT,2,IndsT}; kwargs...
) where {ElT<:Union{Real,Complex},IndsT}
  # Keyword argument deprecations
  use_absolute_cutoff = false
  if haskey(kwargs, :absoluteCutoff)
    @warn "In svd, keyword argument absoluteCutoff is deprecated in favor of use_absolute_cutoff"
    use_absolute_cutoff = get(kwargs, :absoluteCutoff, use_absolute_cutoff)
  end
  use_relative_cutoff = true
  if haskey(kwargs, :doRelCutoff)
    @warn "In svd, keyword argument doRelCutoff is deprecated in favor of use_relative_cutoff"
    use_relative_cutoff = get(kwargs, :doRelCutoff, use_relative_cutoff)
  end

  truncate = haskey(kwargs, :maxdim) || haskey(kwargs, :cutoff)
  maxdim::Int = get(kwargs, :maxdim, minimum(dims(T)))
  mindim::Int = get(kwargs, :mindim, 1)
  cutoff::Float64 = get(kwargs, :cutoff, 0.0)
  use_absolute_cutoff::Bool = get(kwargs, :use_absolute_cutoff, use_absolute_cutoff)
  use_relative_cutoff::Bool = get(kwargs, :use_relative_cutoff, use_relative_cutoff)

  DM, VM = eigen(matrix(T))

  # Sort by largest to smallest eigenvalues
  #p = sortperm(DM; rev = true)
  #DM = DM[p]
  #VM = VM[:,p]

  if truncate
    truncerr, _ = truncate!(
      DM;
      maxdim=maxdim,
      cutoff=cutoff,
      use_absolute_cutoff=use_absolute_cutoff,
      use_relative_cutoff=use_relative_cutoff,
    )
    dD = length(DM)
    if dD < size(VM, 2)
      VM = VM[:, 1:dD]
    end
  else
    dD = length(DM)
    truncerr = 0.0
  end
  spec = Spectrum(abs.(DM), truncerr)

  i1, i2 = inds(T)

  # Make the new indices to go onto D and V
  l = typeof(i1)(dD)
  r = dag(sim(l))
  Dinds = (l, r)
  Vinds = (dag(i2), r)
  D = complex(tensor(Diag(DM), Dinds))
  V = complex(tensor(Dense(vec(VM)), Vinds))
  return D, V, spec
end

function LinearAlgebra.qr(T::DenseTensor{ElT,2,IndsT}; kwargs...) where {ElT,IndsT}
  positive = get(kwargs, :positive, false)
  # TODO: just call qr on T directly (make sure
  # that is fast)
  if positive
    QM, RM = qr_positive(matrix(T))
  else
    QM, RM = qr(matrix(T))
  end
  # Make the new indices to go onto Q and R
  q, r = inds(T)
  q = dim(q) < dim(r) ? sim(q) : sim(r)
  Qinds = IndsT((ind(T, 1), q))
  Rinds = IndsT((q, ind(T, 2)))
  Q = tensor(Dense(vec(Matrix(QM))), Qinds)
  R = tensor(Dense(vec(RM)), Rinds)
  return Q, R
end

# TODO: support alg keyword argument to choose the svd algorithm
function polar(T::DenseTensor{ElT,2,IndsT}) where {ElT,IndsT}
  QM, RM = polar(matrix(T))
  dim = size(QM, 2)
  # Make the new indices to go onto Q and R
  q = eltype(IndsT)(dim)
  # TODO: use push/pushfirst instead of a constructor
  # call here
  Qinds = IndsT((ind(T, 1), q))
  Rinds = IndsT((q, ind(T, 2)))
  Q = tensor(Dense(vec(QM)), Qinds)
  R = tensor(Dense(vec(RM)), Rinds)
  return Q, R
end


function bubble_sort(x) #function declaration
	for i in range(1,length(x)-1) #outer for loop
		for j in range(1,length(x)-1) #inner for loop
            		if x[j]>x[j+1]  #comparison to test acesending
                		t=x[j]       #exchange of variable
                		x[j]=x[j+1]  # ...
                		x[j+1]=t     #using a second one
            		end
    		end
    end
    for i in range(1,length(x)) #loop for printing
        println(x[i])
    end
end

x = [1,4,6,8,7,9,5,6]
bubble_sort(x)