################################################################################
###  Introduction to Scientific Programming and Machine Learning with Julia  ###
###                                                                          ###
### Run each script on a new clean Julia session                             ###
### GitHub: https://github.com/sylvaticus/IntroSPMLJuliaCourse               ###
### Licence (apply to all material of the course: scripts, videos, quizes,..)###
### Creative Commons By Attribution (CC BY 4.0), Antonello Lobianco          ###
################################################################################

# # 0202 - Miscellaneous topics

# ## Some stuff to set-up the environment..

cd(@__DIR__)
using Pkg
Pkg.activate(".")
## If using a Julia version different than 1.7 please uncomment and run the following line (reproductibility guarantee will hower be lost)
## Pkg.resolve()
## Pkg.instantiate() # run this if you didn't in Segment 02.01
using Random
Random.seed!(123)

# ## Plotting

# Within the many possible packages to plot in Julia we use here the `Plots` package that allows at run time to choose the plot's backend.
# The defauld backend is `gr` (that, if you want to go back to it after you choosen another backend, you can activate with `gr()`). Other common backends are the Python MatplotLib (`pyplot()`, requiring package `PyPlot`) and Plotly (`plotlyjs()` from package `PlotlyJS`).
# Actually we use `StatsPlots` that just adds a set of convenient functionalities (in `plots` terminology called "_recipes_") on top of `Plots`.

using StatsPlots # no need to `using Plots` as `Plots` elements are reexported by StatsPlots

# The basic idea is that we can draw different "graphical elements" in a Plot figure. The `plot(.)` function will first create a new figure, while `plot!(.)` will modify an existing plot (by drawing new elements on it, such as a new serie) taking the "current" plot as default if no plot object is passed as first argument.

# ### Plotting functions

# Let's start plotting a function. The FIRST TIME you invoke `plot` will take a while. This is the famous "time to first plot" problem due to the JIT compilation, but it refer only to the first plotting in a working session
plot(cos) # default [-5,+5] range
savefig("currentPlot1.svg"); #src
# ![](currentPlot.svg)
plot!(x->x^2, -2,2 ) # more explicit, with ranges
savefig("currentPlot1.svg"); #src
# ![](currentPlot.svg)
plot!(x->max(x,2), label="max function", linestyle=:dot, color=:black, title="Chart title", xlabel= "X axis", ylabel="Y axis", legend=:topleft) # a bit of design
savefig("currentPlot2.svg"); #src
# ![](currentPlot2.svg)
plot!(twinx(),x->20x,colour=RGB(20/255,120/255,13/255)) # secondary axis
savefig("currentPlot3.svg"); #src
# ![](currentPlot3.svg)

# ### Plotting data

using DataFrames
x = 11:15
data = DataFrame(a=[4,8,6,6,3], b=[2,4,5,8,6], c=[20,40,15,5,30])
plot(x, Matrix(data)) # x, series (in column)
savefig("currentPlot4.svg"); #src
# ![](currentPlot4.svg)
@df data plot(x, :a, seriestype=:bar, legend=:topleft)
savefig("currentPlot5.svg"); #src
# ![](currentPlot5.svg)
plot!(x, data.b, seriestype=:line)
savefig("currentPlot6.svg"); #src
# ![](currentPlot6.svg)
scatter!(twinx(), x, data.c) # alias for `plot!(..., seriestype=:scatter)`
savefig("currentPlot7.svg"); #src
# ![](currentPlot7.svg)

# ### Layouts with multiple plots

l = @layout [a ; b c] # a,b,c, here are just placeholders, not related with the df column names..
p1 = plot(x, data.a)
p2 = scatter(x, data.b)
p3 = plot(x, data.c)
plot(p1, p2, p3, layout = l)
savefig("currentPlot8.svg"); #src
# ![](currentPlot8.svg)

# ### Saving the plot..
savefig("myplot.png")
savefig("myplot.pdf")
savefig("myplot.svg")

# ## Probabilistic analysis

# Julia has a very elegant package to deal with probability analysis, the `Distributions` package.

using Distributions

# The idea is that we first create a `Distribution` object, of the distribution family and with the parameter required, and then we operate on it, for example to sample or to retrieve a quantile.
# The following table gives the distribution constructors for some of the most common distributions:

# | Discrete distr.   | Constructor                       |  Continuous distr. | Constructor              |
# | ----------------- | --------------------------------- | ------------------ | ------------------------ |
# | Discrete uniform  | `DiscreteUniform(lRange,uRange)`  | Uniform            | `Uniform(lRange,uRange)` |
# | Bernoulli         | `Bernoulli(p)`                    | Exponential        | `Exponential(rate)`      |
# | Binomial          | `Binomial(n,p)`                   | Laplace            | `Laplace(loc, scale)`    |
# | Categorical       | `Categorical(ps)`                 | Normal             | `Normal(μ,σ)`            |
# | Multinomial       | `Multinomial(n, ps)`              | Erlang             | `Erlang(n,rate)`         |
# | Geometric         | `Geometric(p)`                    | Cauchy             | `Cauchy(μ, σ)`           |
# | Hypergeometric    | `Hypergeometric(nS, nF, nTrials)` | Chisq              | `Chisq(df)`              |
# | Poisson           | `Poisson(rate)`                   | T Dist             | `TDist(df)`              |
# | Negative Binomial | `NegativeBinomial(nSucc,p)`       | F Dist             | `FDist(df1, df2)`        |
# |                   |                                   | Beta Dist          | `Beta(shapeα,shapeβ)`    |
# |                   |                                   | Gamma Dist         | `Gamma(shapeα,1/rateβ)`  |

d = Normal(10,3) # note that the parameter is the standard deviation, not the variance

mean(d)
var(d)
median(d)
quantile(d,0.9)
cdf(d,13.844)
pdf(d,0)
rand(d,3,4,2)

sample = rand(d,1000);
density(sample)
savefig("currentPlot9.svg"); #src
# ![](currentPlot9.svg)
plot!(d)
savefig("currentPlot10.svg"); #src
# ![](currentPlot10.svg)
fit(Normal, sample) # using MLE

# ## Curve fitting

# `LsqFit` is a very flexible data fitting package for linear and nonlinear arbitrary functions (using least squares).
# Let's use it to fit a logistic growth curve (Verhulst model) of volumes for a forest stand.
using LsqFit
data = DataFrame(
    age = 20:5:90,
    vol = [64,112,170,231,293,352,408,459,505,546,582,613,640,663,683]
) # Scots Pine, data from the UK Forestry Commission https://web.archive.org/web/20170119072737/http://forestry.gov.uk/pdf/FCBK048.pdf/$FILE/FCBK048.pdf
plot(data.vol)
savefig("currentPlot11.svg"); #src
# ![](currentPlot11.svg)

logisticModel(age,parameters) = parameters[1]/(1+exp(-parameters[2] * (age-parameters[3]) ))
logisticModelVec(age,parameters) = logisticModel.(age,Ref(parameters))

initialParameters = [1000,0.02,50] #max growth; growth rate, mid age
fitobject         = curve_fit(logisticModelVec, data.age, data.vol, initialParameters)
fitparams         = fitobject.param

fitobject.resid
residuals = logisticModelVec(data.age,fitparams) .- data.vol
fitobject.resid == residuals

sigma            = stderror(fitobject)
confidence_inter = confidence_interval(fitobject, 0.05) # 5% significance level

x = 0:maximum(data.age)*1.5
plot(x->logisticModel(x,fitparams),0,maximum(x), label= "Fitted vols", legend=:topleft)
plot!(data.age, data.vol, seriestype=:scatter, label = "Obs vols")
plot!(data.age, residuals, seriestype=:bar, label = "Residuals")
savefig("currentPlot12.svg"); #src
# ![](currentPlot12.svg)

# ## Constrained optimisation

# `JuMP` is the leading library to express complex optimisation problems in a clear, mathematical friendly syntax, compute the information required by the solver engines to solve the optimisation problem, pass the problem to the aforementioned solver engines and retrieve the solutions.

# JuMP has the same flexibility and expressivity of dedicated _algeabric modelling languages_ as `GAMS` or `AMPL` but with the advantage of being a library within a much more general programming language, with larger community, development tools, language constructs and possibility to interface the specific "optimisation component" of a model with the rest of the model.

# We will see how to specify the variables, the constraints and the objective function of an optimisation model, how to "solve" it and how to retrieve the optimal values

# ### A linear example: the classical "transport" problem

# Obj: minimise transport costs $c$ from several plants $p$ to several markets $m$ under the contraint to satisfy the demand $d_m$ at each market while respecting the production capacity $c_p$ of each plant:
# $min_{x_{p,m}} \sum_p \sum_m c_{p,m} * x_{p,m}$
# subject to:
# $\sum_m x_{p,m} \leq d_m$
# $\sum_p x_{p,m} \geq c_m$

using  JuMP, GLPK, DataFrames, CSV

# #### "Sets" and exogenous parameters definition

# index sets
orig = ["Epinal", "Bordeaux", "Grenoble"] # plant/sawmills origin of timber
dest = ["Paris", "Lyon", "Nantes", "Tulouse", "Lille", "Marseille", "Strasbourg"] # markets
prod = ["Fuelwood", "Sawnwood", "Pannels"]

# Read input data into DataFrames and then into Dictionaries with 2D or 3D tuples of index sets as keys

# supply(prod, orig) amounts available at origins
supplytable = CSV.read(IOBuffer("""
prod     Epinal Bordeaux Grenoble
Fuelwood 400    700      800
Sawnwood 800    1600     1800
Pannels  200    300      300
"""), DataFrame, delim=" ", ignorerepeated=true,copycols=true)
supply = Dict( (r[:prod],o) => r[Symbol(o)] for r in eachrow(supplytable), o in orig)

# demand(prod, dest) amounts required at destinations
demandtable = CSV.read(IOBuffer("""
prod      Paris Lyon Nantes Tulouse Lille Marseille Strasbourg
Fuelwood  300   300  100    75      650   225       250
Sawnwood  500   750  400    250     950   850       500
Pannels   100   100  0      50      200   100       250
"""), DataFrame, delim=" ", ignorerepeated=true,copycols=true)
demand = Dict( (r[:prod],d) => r[Symbol(d)] for r in eachrow(demandtable), d in dest)

# limit(orig, dest) of total units from any origin to destination
defaultlimit = 625.0
limit = Dict((o,d) => defaultlimit for o in orig, d in dest)

# cost(prod, orig, dest) Shipment cost per unit
costtable = CSV.read(IOBuffer("""
prod     orig     Paris Lyon Nantes Tulouse Lille Marseille Strasbourg
Fuelwood Epinal   30    10   8      10      11    71        6
Fuelwood Bordeaux 22    7    10     7       21    82        13
Fuelwood Grenoble 19    11   12     10      25    83        15

Sawnwood Epinal   39    14   11     14      16    82        8
Sawnwood Bordeaux 27    9    12     9       26    95        17
Sawnwood Grenoble 24    14   17     13      28    99        20

Pannels  Epinal   41    15   12     16      17    86        8
Pannels  Bordeaux 29    9    13     9       28    99        18
Pannels  Grenoble 26    14   17     13      31    104       20
"""), DataFrame, delim=" ", ignorerepeated=true,copycols=true)
cost = Dict( (r[:prod],r[:orig],d) => r[Symbol(d)] for r in eachrow(costtable), d in dest)

# #### Optimisation model definition

trmodel = Model(GLPK.Optimizer)
set_optimizer_attribute(trmodel, "msg_lev", GLPK.GLP_MSG_ON)

# #### Model's endogenous variables definition

@variables trmodel begin
    x[p in prod, o in orig, d in dest] >= 0
end;

# #### Constraints definition

@constraints trmodel begin
    supply[p in prod, o in orig], # observe supply limit at plant/sawmill origin o
        sum(x[p,o,d] for d in dest) <= supply[p,o]
    demand[p in prod, d in dest], # satisfy demand at market dest d
        sum(x[p,o,d] for o in orig) >= demand[p,d]
    c_total_shipment[o in orig, d in dest],
        sum(x[p,o,d] for p in prod) <= limit[o,d]
end;

# #### Objective function definition
@objective trmodel Min begin
    sum(cost[p,o,d] * x[p,o,d] for p in prod, o in orig, d in dest)
end

# #### Human-readable visualisation of the model

print(trmodel)

# #### Model resolution
optimize!(trmodel)
status = termination_status(trmodel)

# #### Post-resolution information retrieval

# Here, after the model has been "solved", we can retrieve information as the optimal level of the endogenous variables, the value of the opjective function at these optimal levels and the shadow costs of the contraints.

if (status == MOI.OPTIMAL || status == MOI.LOCALLY_SOLVED || status == MOI.TIME_LIMIT) && has_values(trmodel)
    println("#################################################################")
    if (status == MOI.OPTIMAL)
        println("** Problem solved correctly **")
    else
        println("** Problem returned a (possibly suboptimal) solution **")
    end
    println("- Objective value (total costs): ", objective_value(trmodel))
    println("- Optimal routes:\n")
    optRoutes = value.(x)
    for p in prod
      println("\n* $(p):")
      [println("$o --> $d: $(optRoutes[p,o,d])") for o in orig, d in dest]
      println("- Shadow prices of supply:")
      [println("$o = $(dual(supply[p,o]))") for o in orig]
      println("- Shadow prices of demand:")
      [println("$d = $(dual(demand[p,d]))") for d in dest]
    end
else
    println("The model was not solved correctly.")
    println(status)
end

# ### A nonlinear example: portfolio optimisation

# The problem objective is to choose the shares of different assets in the portfolio (here forest species, but the example is exactly the same considering other assets, for example financial investments) that maximise the portfolio expected returns while minimising its expected variance under the portfolio owner risk aversion risk.
# Here the "returns" are based on the timber production and the covariance between individual species of the portfolio is based on the observed volume growth covariances.
# The idea is that within the infinite possible allocations, the locus of those allocations for which is not possible to increase the portfolio profitability without increasing also its variance and the converse whose variance can not be lowered without at the same time lower its expected profitability are efficient in the Pareto meaning and form an "efficient frontier". Within this frontier the problem is to find the unique point that maximise the utility of the portfolio's owner given its risk aversion characteristic.
# Graphically the problem is depicted i nthe following picture:

# ![The efficient frontier and the owner utility curves](https://raw.githubusercontent.com/sylvaticus/IntroSPMLJuliaCourse/main/lessonsSources/02_-_JULIA2_-_Scientific_programming_with_Julia/graph_eff_frontier_v2.png)

# Data originally from the Institut national de l'information géographique et forestière (IGN) of France. See the paper [A. Dragicevic, A. Lobianco, A. Leblois (2016), ”Forest planning and productivity-risk trade-off through the Markowitz mean-variance model“, Forest Policy and Economics, Volume 64](http://dx.doi.org/10.1016/j.forpol.2015.12.010) for a thorough discussion of this model.

# Declare the packages we are going to use:
using JuMP, Ipopt, StatsPlots

# Forest species names
species   = ["Chêne pédonculé", "Chêne sessile", "Hêtre", "Pin sylvestre"]
nSpecies  = length(species)
# Average productiities by specie
# This is implemented in a dictionary: key->value
y   = Dict( "Chêne pédonculé" => 1.83933333333333,
            "Chêne sessile"   => 2.198,
            "Hêtre"           => 3.286,
            "Pin sylvestre"   => 3.3695)

# Covariance matrix between species
σtable = [[0.037502535947712	0.016082745098039	0.027797176470588	-0.025589882352942]
          [0.016082745098039	0.015177019607843	0.018791960784314	-0.102880470588234]
          [0.027797176470588	0.018791960784314	0.031732078431373	-0.166391058823529]
          [-0.025589882352942	-0.102880470588234	-0.166391058823529	2.02950454411765]]
# We reshape the covariance matrix in a dictionary (sp1,sp2) -> value
# The function (ix,x) = enumerate(X) returns a tuple of index position and element
# for each element of an array
σ = Dict((i,j) => σtable[i_ix,j_ix] for (i_ix,i) in enumerate(species), (j_ix,j) in enumerate(species))

################################################################################
###### Showing the possible mean/variance of the portfolio by simulation #######
################################################################################

nSamples = 1000
shares   = rand(nSamples,nSpecies);
# Converting to probabilities:
import BetaML.Utils:softmax
[shares[i,:]  = softmax(shares[i,:], β=one.(shares[i,:]) .* 5) for i in 1:nSamples]

pScores = Array{Float64,2}(undef,0,2)
for i in 1:nSamples
    global pScores
    pVar    = sum(shares[i,j1] * shares[i,j2] * σ[species[j1],species[j2]] for j1 in 1:nSpecies, j2 in 1:nSpecies)
    pY      = sum(shares[i,j]*y[species[j]] for j in 1:nSpecies)
    pScores = vcat(pScores,[pVar pY])
end

scatter(pScores[:,1],pScores[:,2],colour=:blue)
savefig("currentPlot13.svg"); #src
# ![](currentPlot13.svg)

################################################################################
### Finding (one) optimal portfolio ############################################
################################################################################

# Risk aversion coefficient
α = 0.1

# We declare an optimisation problem, we name it "m" and we let JuMP associate it with the most
# suitable solver within the one installed:
port = Model(Ipopt.Optimizer)

# We declare a set of variables, indicized by the species name:
@variables port begin
    x[i in species] >= 0
end

# We declare the constraint shat the sum of shares must be equal to 1
@constraint(port, c_share, sum(x[i] for i in species) == 1)

#=
@objective port Min begin
  α *  sum(x[i] * x[j] * σ[i,j] for i in species for j in species) - sum(x[i] * y[i] for i in species)
end
=#

@NLobjective port Min α *  sum(x[i] * x[j] * σ[i,j] for i in species for j in species) - sum(x[i] * y[i] for i in species)

# Print the optimisation model in nice human-readable format:
print(port)

# Solve the model and return the solving status:
optimize!(port)
status = termination_status(port)

# Return the objective:
println("Objective value: ", objective_value(port))
# Return the value of the decision variable:
optShares = value.(x)
[println("$sp = $(optShares[sp])") for sp in species];
pOptVar = sum(optShares[species[j1]] * optShares[species[j2]] * σ[species[j1],species[j2]] for j1 in 1:nSpecies, j2 in 1:nSpecies)
pOptY   = sum(optShares[species[j]]*y[species[j]] for j in 1:nSpecies)

function computeOptimalPortfolio(species,y,σ,α)
    port = Model(Ipopt.Optimizer)
    set_optimizer_attribute(port, "print_level", 0)
    @variables port begin
        x[i in species] >= 0
    end
    @constraint(port, c_share, sum(x[i] for i in species) == 1)
    @NLobjective port Min α *  sum(x[i] * x[j] * σ[i,j] for i in species for j in species) - sum(x[i] * y[i] for i in species)
    optimize!(port)
    status = termination_status(port)
    optShares = value.(x)
    pOptVar = sum(optShares[species[j1]] * optShares[species[j2]] * σ[species[j1],species[j2]] for j1 in 1:nSpecies, j2 in 1:nSpecies)
    pOptY   = sum(optShares[species[j]]*y[species[j]] for j in 1:nSpecies)
    return (pOptVar,pOptY)
end

αs = [1000,100,10,1,0.1,0.05,0.02,0.01]
pOptScores = Array{Float64,2}(undef,0,2)
for α in αs
    global pOptScores
    pVar,pY =computeOptimalPortfolio(species,y,σ,α)
    pOptScores = vcat(pOptScores,[pVar pY])
end
scatter!(pOptScores[:,1],pOptScores[:,2],colour=:red)
savefig("currentPlot14.svg"); #src
# ![](currentPlot14.svg)

αs = [82.45,50,30,20,15,12,10,9,8,7,6,5]
pOptScores = Array{Float64,2}(undef,0,2)
for α in αs
    global pOptScores
    pVar,pY =computeOptimalPortfolio(species,y,σ,α)
    pOptScores = vcat(pOptScores,[pVar pY])
end

scatter(pOptScores[:,1],pOptScores[:,2],colour=:red)
savefig("currentPlot15.svg"); #src
# ![](currentPlot15.svg)


@testset "Order tests" begin
    @testset "gtmax" begin
        @test @inferred(gtmax(1:10, 1:11)) == false
        @test @inferred(gtmax(1:11, 1:10)) == true
    end

    @testset "ltmax" begin
        @test @inferred(ltmax(1:10, 1:11)) == true
        @test @inferred(ltmax(1:11, 1:10)) == false
    end

    @testset "eqmax" begin
        @test @inferred(eqmax(1:10, 3:10)) == true
        @test @inferred(eqmax(1:11, 1:10)) == false
    end

    @testset "gtmin" begin
        @test @inferred(gtmin(1:10, 3:11)) == false
        @test @inferred(gtmin(3:11, 1:10)) == true
    end

    @testset "ltmin" begin
        @test @inferred(ltmin(1:10, 3:11)) == true
        @test @inferred(ltmin(3:11, 1:10)) == false
    end

    @testset "eqmin" begin
        @test @inferred(eqmin(3:10, 3:11)) == true
        @test @inferred(eqmin(3:11, 2:10)) == false
    end

    @testset "cmpmin" begin
        @test @inferred(cmpmin(0:10, 3.0:-1.0:1.0)) == -1
    end

    @testset "cmpmax" begin
        @test @inferred(cmpmax(1:10, 3.0:-1.0:1.0)) == 1
    end

    @testset "group_max" begin
        @test @inferred(group_max(1:10, [1,4, 20], 3.0:-1.0:1.0)) == 20
    end

    @testset "group_min" begin
        @test @inferred(group_min(1:10, [1,4, 20], 3.0:-1.0:1.0)) == 1
    end

    @testset "min_of_group_max" begin
        @test @inferred(min_of_group_max(1:10, 3.0:-1.0:1.0)) == min(maximum(1:10), maximum(3.0:-1.0:1.0))
    end

    @testset "max_of_group_min" begin
        @test @inferred(max_of_group_min(1:10, 3.0:-1.0:1.0)) == max(minimum(1:10), minimum(3.0:-1.0:1.0))
    end

    x = 1:10
    y = 10:-2:1
    z = [1.5, 1.7, 3.3]
    a = [1, 3, 2]

    @testset "order" begin
        @test @inferred(order(x)) == Forward
        @test order(y) == Reverse
        @test order(z) == Forward
        @test order(a) == Unordered
    end

    @testset "is_before" begin
        @test @inferred(is_before(2, 3, x)) == true
        @test @inferred(is_before(3, 2, x)) == false
        @test @inferred(is_before(1:2, 3:4)) == true
        @test @inferred(is_before(3:4, 1:2)) == false
        @test @inferred(is_before(2:-1:1, 4:-1:3)) == true
        @test @inferred(is_before(4:-1:3, 2:-1:1)) == false
    end

    @testset "is_after" begin
        @test @inferred(is_after(2, 3, x)) == false
        @test @inferred(is_after(3, 2, x)) == true
        @test @inferred(is_after(1:2, 3:4)) == false
        @test @inferred(is_after(3:4, 1:2)) == true
        @test @inferred(is_after(2:-1:1, 4:-1:3)) == false
        @test @inferred(is_after(4:-1:3, 2:-1:1)) == true
    end

    @testset "is_forward" begin
        @test @inferred(is_forward(x)) == true
        @test @inferred(is_forward(y)) == false
        @test @inferred(is_forward(z)) == true
        @test @inferred(is_forward(a)) == false
    end

    @testset "is_reverse" begin
        @test @inferred(is_reverse(x)) == false
        @test @inferred(is_reverse(y)) == true
        @test @inferred(is_reverse(z)) == false
        @test @inferred(is_reverse(a)) == false
    end

    @testset "is_ordered" begin
        @test @inferred(is_ordered(x)) == true
        @test @inferred(is_ordered(typeof(x))) == true
        @test @inferred(is_ordered(y)) == true
        @test @inferred(is_ordered(z)) == true
        @test @inferred(is_ordered(a)) == false
        @test @inferred(is_ordered(typeof(a))) == false
    end

    @testset "is_within" begin
        for (xo,yo,x,y) in ((Forward, Forward, 2:3, 1:10),
                            (Reverse, Reverse, 3:-1:2, 10:-1:1),
                            (Forward, Reverse, 2:3, 10:-1:1),
                            (Reverse, Forward, 3:-1:2, 1:10))
            @test @inferred(is_within(x, y)) == true
            @test @inferred(is_within(x, xo, y, yo)) == true
            @test @inferred(is_within(y, yo, x, xo)) == false
        end
    end

    @testset "find_max" begin
        @test find_max(x) == findmax(x)
        @test find_max(y) == findmax(y)
        @test find_max(z) == findmax(z)
        @test find_max(a) == findmax(a)
    end

    @testset "find_min" begin
        @test find_min(x) == findmin(x)
        @test find_min(y) == findmin(y)
        @test find_min(z) == findmin(z)
        @test find_min(a) == findmin(a)
    end

    @testset "is_contiguous" begin
        @test @inferred(is_contiguous(1:3, 3:4)) == true
        @test @inferred(is_contiguous(3:-1:1, 3:4)) == true
        @test @inferred(is_contiguous(3:-1:1, 4:-1:3)) == true
        @test @inferred(is_contiguous(1:3, 4:-1:3)) == true
        @test @inferred(is_contiguous(1:3, 2:4)) == false
    end

    @testset "next_type" begin
        @test next_type("a") == "b"
        @test next_type(:a) == :b
        @test next_type('a') == 'b'
        @test next_type(1) == 2
        @test next_type(1.0) == nextfloat(1.0)
        @test next_type("") == ""
    end

    @testset "prev_type" begin
        @test prev_type("b") == "a"
        @test prev_type(:b) == :a
        @test prev_type('b') == 'a'
        @test prev_type(1) == 0
        @test prev_type(nextfloat(1.0)) == prevfloat(nextfloat(1.0))
        @test prev_type("") == ""
    end

    @testset "is_forward" begin
        @test @inferred(is_forward([1, 2, 3])) == true
        @test @inferred(is_forward(Forward)) == true
        @test @inferred(is_forward(Reverse)) == false
        @test @inferred(is_forward(UnitSRange(1, 10))) == true
    end
    @testset "is_reverse" begin
        @test @inferred(is_reverse([1, 2, 3])) == false
        @test @inferred(is_reverse(Forward)) == false
        @test @inferred(is_reverse(Reverse)) == true
        @test @inferred(is_reverse(UnitSRange(1, 10))) == false
    end
end

#=
@testset "grow" begin
    sv_int = sorted([1,2,3])
    sv_float = sorted([1., 2., 3.])
    @test @inferred(grow_last!(sv_int, 2)) == [1, 2, 3, 4, 5]
    @test @inferred(grow_last!(sv_float, 1)) == [1., 2., 3., nextfloat(3.)]

    sr_int = sorted(mrange(1, 10))
    sr_float = sorted(mrange(1., 10.))
    @test @inferred(grow_last!(sr_int, 2)) == sorted(mrange(1, 12))
    @test @inferred(grow_last!(sr_float, 1)) == sorted(mrange(1., 11.))

    @test @inferred(grow_first!(sv_int, 2)) == [-1, 0, 1, 2, 3, 4, 5]
    @test @inferred(grow_first!(sv_float, 1)) == [prevfloat(1.), 1., 2., 3., nextfloat(3.)]

    @test @inferred(grow_first!(sr_int, 2)) == sorted(mrange(-1, 12))
    @test @inferred(grow_first!(sr_float, 1)) == sorted(mrange(0., 11.))
end

@testset "shrink" begin
    sv = SortedVector([1,2,3])
    @test shrink_last!(sv, 1) == [1,2]
    @test shrink_first!(sv, 1) == [2]
end
=#


"""
    space_dimension(S::AbstractSet)

Return the space dimension of the given set.

### Input

- `S` -- set

### Output

An integer.

### Notes

The `space_dimension` refers to the ambient dimension of the set. For example,
a line in an ``n``-dimensional space has `space_dimension` equal to ``n``, even
if the dimension of its affine hull is ``1``.

See also [`dimension(::AbstractSet)`](@ref) for the dimension of the set.
"""
function space_dimension(::AbstractSet) end

"""
    dimension(S::AbstractSet)

Return the dimension of the given set.

### Input

- `S` -- set

### Output

An integer.

### Notes

The actual notion of `dimension` adopted is left to the package implementing
this function.

For instance, in many applications `dimension` refers to the dimension of the
affine hull of the set. For example, a line in an ``n``-dimensional space has
`dimension` equal to ``1``, even if the dimension of the space is ``n``.
However, in other domains, e.g. semialgebraic sets, `dimension` is not the
dimension of its affine hull.

See also [`space_dimension(::AbstractSet)`](@ref) for the ambient dimension of
the set.
"""
function dimension(::AbstractSet) end


using Plots
using Colors
using Statistics
using MAT
import Random

include("lambs_krylov.jl")

δt = step_size_lamb(load_lambs(mesh=2), 1.0);

# ---------------------------------------------
# Generar figuras condicion inicial singleton
# ---------------------------------------------
generar_figura_malla2_singleton_position()
generar_figura_malla2_singleton_velocity()

# ---------------------------------------------
# Generar datos envolvente
# ---------------------------------------------

vecsamples = [1, 10, 100, 1000, 10_000, 100_000]

# corridas de warm-up
out_flowpipe_pos, out_extrema_malla2_pos, fig_pos = generar_figura_malla2_envolventes_position(vecsamples=[1, 10]);
out_flowpipe_vel, out_extrema_malla2_vel, fig_vel = generar_figura_malla2_envolventes_velocity(vecsamples=[1, 10]);

# corridas largas
Random.seed!(1234)
out_flowpipe_pos, out_extrema_malla2_pos, fig_pos = generar_figura_malla2_envolventes_position(vecsamples=vecsamples);
out_flowpipe_vel, out_extrema_malla2_vel, fig_vel = generar_figura_malla2_envolventes_velocity(vecsamples=vecsamples);

# ---------------------------------------------
# Calculo de normas del flowpipe
# ---------------------------------------------

# dt siempre el mismo para cada tramo, y la ventana de tiempo va hasta 1 segundo,
# entonces el dt se saca de factor comun

# max(max_t |v_max(t)|, max_t |v_min(t)|)
out_flowpipe_vel_norma_inf_envolvente = max(abs(ρ([1.0], out_flowpipe_vel)), abs(ρ([-1.0], out_flowpipe_vel)))
# salida: 0.0012224968252669921

# δt * sum_{i=1}^N max(|v_max(t)|, |v_min(t)|)
δt = step_size_lamb(load_lambs(mesh=2), 1.0)
@assert tspan(out_flowpipe_vel[1]).hi ≈ δt
out_flowpipe_vel_norma_1_envolvente = sum(max(abs(ρ([1.0], set(X))), abs(ρ([-1.0], set(X)))) for X in out_flowpipe_vel) * δt
# salida: 0.0008133221497265329

# ---------------------------------------------
# Calculo de normas de numericas
# ---------------------------------------------

out_extrema_malla2_vel_norma_inf_envolvente = _norma_inf_envolvente_numericas(out_extrema_malla2_vel, vecsamples)
#=
6-element Vector{Float64}:
 0.00056983336205989
 0.0005752808846025316
 0.0005758510647681003
 0.0005821999057203168
 0.0006118261295338235
 0.0006220764784910799
 =#

out_extrema_malla2_vel_norma_1_envolvente = _norma_1_envolvente_numericas(out_extrema_malla2_vel, vecsamples)
#=
julia> out_extrema_malla2_vel_norma_1_envolvente = _norma_1_envolvente_numericas(out_extrema_malla2_vel, vecsamples)
6-element Vector{Float64}:
 9.268765778974471e-5
 0.00013515950042666908
 0.0001661339910196396
 0.00018520059379205858
 0.0001997982127229552
 0.00021423496225308065
 =#

# ------------------------------
# Guardar datos para las tablas
# ------------------------------

# convert the keys of a dictionary into strings; the values might themselves
# be dictionaries, in which case the operation is applied recursively
# moreover, all keys are appended a capital letter N, e.g. "1000", it is converted to "N1000"
function _to_str_keys(d::Dict)
    strkeys = "N" .* string.(keys(d))

    if first(d)[2] isa Dict
        return Dict(strkeys .=> _to_str_keys.(values(d)))
    else
        return Dict(strkeys .=> values(d))
    end
end

using MAT

results  = Dict{String, Any}("vecsamples" => vecsamples,
                             "dt" => δt,
                             "tiempo_flowpipe" => "8.504446 seconds (9.82 k allocations: 292.719 MiB, 1.10% gc time)",

                             "out_flowpipe_pos_lo" => [set(X).dat.lo for X in out_flowpipe_pos],
                             "out_flowpipe_pos_hi" => [set(X).dat.hi for X in out_flowpipe_pos],
                             "out_flowpipe_pos_tstart" => [tstart(X) for X in out_flowpipe_pos],
                             "out_flowpipe_pos_tend" => [tend(X) for X in out_flowpipe_pos],

                             "out_flowpipe_vel_lo" => [set(X).dat.lo for X in out_flowpipe_vel],
                             "out_flowpipe_vel_hi" => [set(X).dat.hi for X in out_flowpipe_vel],
                             "out_flowpipe_vel_tstart" => [tstart(X) for X in out_flowpipe_vel],
                             "out_flowpipe_vel_tend" => [tend(X) for X in out_flowpipe_vel],

                             "out_flowpipe_vel_norma_inf_envolvente" => out_flowpipe_vel_norma_inf_envolvente,
                             "out_flowpipe_vel_norma_1_envolvente" => out_flowpipe_vel_norma_1_envolvente,
                             "out_extrema_malla2_vel_norma_inf_envolvente" => out_extrema_malla2_vel_norma_inf_envolvente,
                             "out_extrema_malla2_vel_norma_1_envolvente" => out_extrema_malla2_vel_norma_1_envolvente,

                             "out_extrema_malla2_pos_times" => out_extrema_malla2_pos.idx,
                             "out_extrema_malla2_pos_U_min" => out_extrema_malla2_pos.U_min,
                             "out_extrema_malla2_pos_U_max" => out_extrema_malla2_pos.U_max,
                             "out_extrema_malla2_pos_Udot_min" => out_extrema_malla2_pos.U′_min,
                             "out_extrema_malla2_pos_Udot_max" => out_extrema_malla2_pos.U′_max,
                             "out_extrema_malla2_pos_hist" => _to_str_keys(out_extrema_malla2_pos.hist),
                             "out_extrema_malla2_pos_t" => collect(out_extrema_malla2_pos.t),
                             "out_extrema_malla2_pos_times" => _to_str_keys(out_extrema_malla2_pos.times),

                             "out_extrema_malla2_vel_times" => out_extrema_malla2_vel.idx,
                             "out_extrema_malla2_vel_U_min" => out_extrema_malla2_vel.U_min,
                             "out_extrema_malla2_vel_U_max" => out_extrema_malla2_vel.U_max,
                             "out_extrema_malla2_vel_Udot_min" => out_extrema_malla2_vel.U′_min,
                             "out_extrema_malla2_vel_Udot_max" => out_extrema_malla2_vel.U′_max,
                             "out_extrema_malla2_vel_hist" => _to_str_keys(out_extrema_malla2_vel.hist),
                             "out_extrema_malla2_vel_t" => collect(out_extrema_malla2_vel.t),
                             "out_extrema_malla2_vel_times" => _to_str_keys(out_extrema_malla2_vel.times)
                             );

matwrite("results.mat", results)

# ---------------
# Generar figura
# ---------------

generar_figura_malla2_envolvente_velocidad()


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

using BAT
using Test

using Statistics
using ArraysOfArrays, StatsBase

@testset "onlinestats" begin
    n = 10
    data1  = [(-1)^x*exp(x-n/2) for x in 1:n]
    data2 = reverse(data1, dims=1)

    w = [exp(x-n/2) for x in 1:n]

    data = VectorOfSimilarVectors(vcat(data1', data2'))
    m = innersize(data, 1)

    @testset "BAT.kbn_add" begin
        bg = +1E5
        sm = +1e-4
        res = bg + sm

        bgT = (bg, 0.0)
        smT = (sm, 0.0)

        @test collect(BAT.kbn_add((0.0,0.0),0.0)) ≈ [0.0,0.0]
        @test sum(BAT.kbn_add((0.0,0.0),eps())) ≈ eps()
        @test sum(BAT.kbn_add((bg,0.0),sm)) ≈ res
        @test sum(BAT.kbn_add((sm,0.0),bg)) ≈ res

        @test sum(BAT.kbn_add(bgT,smT)) ≈ res

        @test typeof(@inferred BAT.kbn_add(bgT,sm)) == NTuple{2, Float64}
        @test typeof(@inferred BAT.kbn_add(bgT,smT)) == NTuple{2, Float64}
        @test typeof(@inferred BAT.kbn_add((0.0f0, 0.0f0), 0.0f0)) == NTuple{2, Float32}
        @test typeof(@inferred BAT.kbn_add((0.0f0, 0.0f0), (0.0f0, 0.0f0))) == NTuple{2, Float32}
    end

    @testset "BAT.OnlineMvMean" begin
        @test typeof(@inferred BAT.OnlineMvMean(n)) <: AbstractVector{Float64}
        @test typeof(@inferred BAT.OnlineMvMean{Float32}(n)) <: AbstractVector{Float32}

        mvmean = BAT.OnlineMvMean(m)
        @test size(mvmean, 1) == m

        countM = 3
        mvmeans = Array{BAT.OnlineMvMean{Float64}}(undef, countM)

        for i in axes(mvmeans, 1)
            mvmeans[i] = BAT.OnlineMvMean(m)
        end

        for i in axes(data, 1)
            push!(mvmean, data[i], w[i]);
            push!(mvmeans[(i % countM) + 1], data[i], w[i]);
        end

        @test mvmean ≈ mean(data, Weights(w))

        res = merge(mvmeans...)
        @test res ≈ mean(data, Weights(w))

        mvmean = BAT.OnlineMvMean(m)
        res = append!(deepcopy(mvmean), data)
        @test res ≈ mean(data)
        res = append!(deepcopy(mvmean), data, w)
        @test res ≈ mean(data, Weights(w))
    end

    @testset "BAT.OnlineMvCov" begin
        @test typeof(@inferred BAT.OnlineMvCov(n)) <: AbstractMatrix{Float64}
        @test typeof(@inferred BAT.OnlineMvCov{Float32, ProbabilityWeights}(n)) <: AbstractMatrix{Float32}

        for wKind in [ProbabilityWeights, FrequencyWeights, AnalyticWeights, Weights]
            mvcov = BAT.OnlineMvCov{Float64, wKind}(m)
            for i in eachindex(data)
                push!(mvcov, data[i], w[i]);
            end

            @test mvcov ≈ cov(data, wKind(w); corrected = (wKind != Weights))
        end

        countMvCovs = 3
        mvcovs = Array{BAT.OnlineMvCov{Float64, ProbabilityWeights}}(undef, countMvCovs)
        for i in axes(mvcovs,1)
            mvcovs[i] = BAT.OnlineMvCov(m)
        end
        mvcovc = BAT.OnlineMvCov(m)

        for i in eachindex(data)
            push!(mvcovs[(i % countMvCovs) + 1], data[i], w[i]);
            push!(mvcovc, data[i], w[i]);
        end

        res = merge(mvcovs...)
        @test res ≈ cov(data, ProbabilityWeights(w); corrected = true)
        @test res ≈ mvcovc
        @test Float64(res.sum_w) ≈ Float64(mvcovc.sum_w)
        @test Float64(res.sum_w2) ≈ Float64(mvcovc.sum_w2)
        @test res.n ≈ mvcovc.n
        @test res.Mean_X ≈ mvcovc.Mean_X
        @test res.New_Mean_X ≈ mvcovc.New_Mean_X
        @test push!(res, data[1], zero(Float64)) ≈ mvcovc

        mvcov = BAT.OnlineMvCov(m)
        res = append!(deepcopy(mvcov), data)
        @test res ≈ cov(data)
        res = append!(deepcopy(mvcov), data, w)
        @test res ≈ cov(data, ProbabilityWeights(w); corrected = true)
    end

    @testset "BAT.BasicMvStatistic" begin
        bmvstats = BAT.BasicMvStatistics{Float64, ProbabilityWeights}(m)

        countBMS = 3
        bmvs = Array{BAT.BasicMvStatistics{Float64, ProbabilityWeights}}(undef, countBMS)
        for i in axes(bmvs,1)
            bmvs[i] = BAT.BasicMvStatistics{Float64, ProbabilityWeights}(m)
        end

        for i in eachindex(data)
            BAT.push!(bmvstats, data[i], w[i]);
            #BAT.push!(bmvs[(i % countBMS) + 1], data[i], w[i]);
        end
    end
end


function get_tensorinfo(tensor_shape)
    tensor_info = ""
    tensor_info_for_title = ""
    for s in tensor_shape
        tensor_info = tensor_info*"_"*string(lpad(s, n_digits, "0"))
    end
    return tensor_info
end

function get_dkl(P, Q ; eps_thred = 1.0e-10)
    tensor_shape = size(P)
    dkl = 0.0
    for idx in CartesianIndices(tensor_shape)
        if P[idx] < eps_thred
            continue
        end
        if Q[idx] < eps_thred
            continue
        end
        dkl += P[idx] * log( P[idx] / Q[idx] )
    end
    return dkl - sum(P) + sum(Q)
    #return sum( P .* log.( P ./ Q ) ) - sum(P) + sum(Q)
end


using SparseDiffTools
using Graphs
using Graphs: SimpleGraph
using Random
#=
            Graph g0
vertex nuumber followed by color in parentheses

            6(3)
           /    \
          /      \
         /        \
       5(1)--------3(2)
       |  \        |
       |   \       |
       |    \      |
       |   1(2)----2(1)
       |   /
       |  /
      4(3)
=#

g0 = SimpleGraph(6)
add_edge!(g0, 1,2)
add_edge!(g0, 1,4)
add_edge!(g0, 1,5)
add_edge!(g0, 3,2)
add_edge!(g0, 3,5)
add_edge!(g0, 3,6)
add_edge!(g0, 4,5)
add_edge!(g0, 5,6)

#=
        Graph g1

          1
          |
          |
     5----2----3
         / \
        /   \
      6      4

=#
g1 = SimpleGraph(6)
add_edge!(g1, 2,1)
add_edge!(g1, 3,2)
add_edge!(g1, 4,2)
add_edge!(g1, 5,2)
add_edge!(g1, 6,2)

#=
        Graph g2

            1------2
          / |    / |
         /  |   /  |
        /   | /    |
       3----4------5

=#
g2 = SimpleGraph(5)
add_edge!(g2, 1,2)
add_edge!(g2, 1,3)
add_edge!(g2, 1,4)
add_edge!(g2, 4,2)
add_edge!(g2, 5,2)
add_edge!(g2, 3,4)
add_edge!(g2, 4,5)

#test custom graphs first
coloring0 = SparseDiffTools.color_graph(g0, SparseDiffTools.BacktrackingColor())
coloring1 = SparseDiffTools.color_graph(g1, SparseDiffTools.BacktrackingColor())
coloring2 = SparseDiffTools.color_graph(g2, SparseDiffTools.BacktrackingColor())

for v = 1:nv(g0)
    color = coloring0[v]
    for j in inneighbors(g0, v)
        if coloring0[j] == color
             @test false
        end
    end
end

for v = 1:nv(g1)
    color = coloring1[v]
    for j in inneighbors(g1, v)
        if coloring1[j] == color
             @test false
        end
    end
end

for v = 1:nv(g2)
    color = coloring2[v]
    for j in inneighbors(g2, v)
        if coloring2[j] == color
             @test false
        end
    end
end

test_graphs = Array{SimpleGraph, 1}(undef, 0)
for _ in 1:5
    nv = rand(5:20)
    ne = rand(1:100)
    graph = SimpleGraph(nv)
    for e in 1:ne
        v1 = rand(1:nv)
        v2 = rand(1:nv)
        while v1 == v2
            v2 = rand(1:nv)
        end
        add_edge!(graph, v1, v2)
    end
    push!(test_graphs, copy(graph))
end

for i in 1:5
    g = test_graphs[i]
    out_colors = SparseDiffTools.color_graph(g,SparseDiffTools.BacktrackingColor())

    for v = vertices(g)
        color = out_colors[v]
        for j in inneighbors(g, v)
            @test out_colors[j] != color
        end
    end
end


# This is a Node in the Binning Analysis tree that averages two values. There
# is one of these for each binning level. When two values should be
# compressed, this is done immediately, so that only one value needs to be saved.
# switch indicates whether value should be written to or averaging should happen.
mutable struct Compressor{T}
    value::T
    switch::Bool
end

struct LogBinner{N, T}
    # list of Compressors, one per level
    compressors::NTuple{N, Compressor{T}}

    # sum(x) for all values on a given lvl
    x_sum::Vector{T}
    # sum(x.^2) for all values on a given lvl
    x2_sum::Vector{T}
    # number of values that are summed on a given lvl
    count::Vector{Int64}
end

"""
    LogBinner([T = Float64, N = 32])

Creates a Binning Analysis which can take 2^N-1 value sof type T.
"""
LogBinner(T::Type = Float64, N::Int64 = 32) = LogBinner(zero(T), N)

# TODO
# Currently, the Binning Analysis requires a "zero" to initialize x_sum and
# x2_sum. It's not necessary for the Compressors.
# Since Arrays do not have a static size, we cannot generate a fitting zero
# automatically. So currently we let the user supply it.
# Other possibilities:
# > generate x_sum, x2_sum with #undef
# bad: requires many checks (if isassigned ... else ...)
# > force StaticArrays (and/or tuples)
# good: faster for small arrays
# bad: requires user to use another package
# > initialize is first push!
# bad: also requires frequent checks (if first push ... else ...)
# ...?
"""
    LogBinner([zero = 0.0, N = 32])

Creates a new Binning Analysis which can take 2^N-1 values of type T. The type
is inherited by the given zero. Returns a Binning Analysis object.

Values can be added using `push!(LogBinner, value)`.
"""
function LogBinner(_zero::T = zero(Float64), N::Int64 = 32) where {T}
    # heuristic to set sum type (#2)
    D = ndims(_zero)
    S = if eltype(T)<:Real
        D > 0 ? Array{Float64, D} : Float64
    else
        D > 0 ? Array{ComplexF64, D} : ComplexF64
    end

    LogBinner{N, S}(
        tuple([Compressor{S}(copy(_zero), false) for i in 1:N]...),
        [copy(_zero) for _ in 1:N],
        [copy(_zero) for _ in 1:N],
        zeros(Int64, N)
    )
end

# TODO typing?
"""
    append!(LogBinner, values)

Adds an array of values to the Binning Analysis by applying push! to each
element.
"""
function append!(B::LogBinner, values::AbstractArray)
    for value in values
        push!(B, value)
    end
    nothing
end

"""
    push!(LogBinner, value)

Pushes a new value into the Binning Analysis.
"""
function push!(B::LogBinner{N, T}, value::S) where {N, T, S}
    ndims(T) == ndims(S) || throw(DimensionMismatch("Expected $(ndims(T)) dimensions but got $(ndims(S))."))

    _push!(B, 1, value)
end

_square(x) = x^2
_square(x::Complex) = Complex(real(x)^2, imag(x)^2)
_square(x::AbstractArray) = _square.(x)

# recursion, back-end function
function _push!(B::LogBinner{N, T}, lvl::Int64, value::S) where {N, T <: Number, S}
    C = B.compressors[lvl]

    # any value propagating through this function is new to lvl. Therefore we
    # add it to the sums. Note that values pushed to the output arrays are not
    # added here until the array drops to the next level. (New compressors are
    # added)
    B.x_sum[lvl] += value
    B.x2_sum[lvl] += _square(value)
    B.count[lvl] += 1

    if !C.switch
        # Compressor has space -> save value
        C.value = value
        C.switch = true
        return nothing
    else
        # Do averaging
        if lvl == N
            # No more propagation possible -> throw error
            throw(OverflowError("The Binning Analysis ha exceeddd its maximum capacity."))
        else
            # propagate to next lvl
            C.switch = false
            _push!(B, lvl+1, 0.5 * (C.value + value))
            return nothing
        end
    end
    return nothing
end

function _push!(
        B::LogBinner{N, T},
        lvl::Int64,
        value::S
    ) where {N, T <: AbstractArray, S}

    C = B.compressors[lvl]
    B.x_sum[lvl] .+= value
    B.x2_sum[lvl] .+= _square(value)
    B.count[lvl] += 1

    if !C.switch
        C.value = value
        C.switch = true
        return nothing
    else
        if lvl == N
            throw(OverflowError("The Binning Analysis ha exceeddd its maximum capacity."))
        else
            C.switch = false
            _push!(B, lvl+1, 0.5 * (C.value .+ value))
            return nothing
        end
    end
    return nothing
end


"""
The *LaplaceModule* contains all functionality to work with the Laplace transformation
1/x ≈ ∑ₜweight[t] * exp(-x node[t]).
The nodes and weights are contained within the *LaplacePoints* type. Hackbusch and coworkers
have optimized and pretabulated these for different x-ranges according to minmax
(Takatsuka, Ten-No, Hackbusch, JCP, 129 (2008), 044112).
Their pretabulated files can be obtained with *downloadLaplacePointsHackbusch*. Use *readLaplacePointsHackbusch* to
generate a LaplacePoints object from that file. After downloading, one can also use *findLaplacePointsHackbuschPretableLarger*/*findLaplacePointsHackbuschPretableSmaller* to obtain the best guesses for a given range. The ideal Laplace
problem considers x∈ [1,R]. For the transformation to the x∈ [A,B]-type Laplace problem utility functions are provided (*transformRangeToIdealLaplace*,*transformWeightFromIdealLaplace*,*transformNodeFromIdealLaplace*,*transformLaplacePointFromIdealLaplace*).
"""
module LaplaceModule
export LaplacePoints, downloadLaplacePointsHackbusch, findLaplacePointsHackbuschPretableLarger, findLaplacePointsHackbuschPretableSmaller, transformRangeToIdealLaplace, transformNodeFromIdealLaplace, transformWeightFromIdealLaplace, transformLaplacePointFromIdealLaplace
using ..DocumentationModule
using ..BaseModule
using ZipFile
using Printf
import Base.show
import Base.==

"""
LaplacePoints contain the collection of weights and corresponding nodes for the quadrature
according to
1/x = ∫ exp(-xt) ≈ ∑ₜweight[t] * exp(-x node[t])
"""
struct LaplacePoints
  weights::Array{Float64,1}
  nodes::Array{Float64,1}
end

function show(io::IO,::MIME"text/plain",lp::LaplacePoints)
  @assert length(lp.weights) == length(lp.nodes)
  println(io,"  Exponential Factor         Weight")
  println(io,"  ------------------         ------")
  for (node,weight) in zip(lp.nodes,lp.weights)
    print(io,@sprintf("  %.16f         %f\n",node,weight))
  end
end

"""
Evaluates to the name of the Laplace point file which corresponds to the given number of points and limit
in Hackbusch nomenclature. These files are contained in the zip-file obtained via downloadLaplacePointsHackbusch.
"""
function computeFilenameHackbusch(numberOfPoints::Integer,R::Real)
  k = numberOfPoints
  kstr = @sprintf("k%02d",k)
  Rstr = replace(replace(@sprintf(".%.0E",R),r"\+0*(\d+)"=>s"\1"),r"(\d+)E(\d+)"=>s"\1_\2")
  filename = "1_x$kstr$Rstr"
  return filename
end

"""
Extract the number of points (k) and the range (1,R) from the filename generated with computeFilenameHackbusch
"""
function computeFilenameHackbuschReverse(filename::AbstractString)
  m = match(r"1_xk(\d+).(\d+)_(\d+)",filename)
  k = parse(Int64,m.captures[1])
  R = parse(Float64,"$(m.captures[2])E$(m.captures[3])")
  return (k,R)
end

"""
Download a zip-file of pretabulated Laplace points to target location (default:"hackbusch")
"""
function downloadLaplacePointsHackbusch(target::AbstractString="hackbusch")
  download("https://www.mis.mpg.de/scicomp/EXP_SUM/1_x/1_xData.zip",target)
end
@doc GenericCitation("https://www.mis.mpg.de/scicomp/EXP_SUM/1_x/") downloadLaplacePointsHackbusch

"""
Generate a LaplacePoints object by extracting from the pretable zip-file obtained with downloadLaplacePointsHackbusch.
"""
function readLaplacePointsHackbusch(numberOfPoints::Integer,R::Real,zipfile::AbstractString="hackbusch")
  k = numberOfPoints
  dir = ZipFile.Reader(zipfile)
  file = ZipFile.ReadableFile
  for f in dir.files
    if f.name==computeFilenameHackbusch(k,R)
      file = f
    end
  end

  result = LaplacePoints(Array{Float64,1}(),Array{Float64,1}())
  lines = readlines(file)
  for line in 1:k
    regmatch = match(Regex(" *(?P<weight>$floatregex) .*omega.*"),lines[line])
    push!(result.weights,parse(Float64,regmatch[:weight]))
  end
  for line in k+1:2k
    regmatch = match(Regex(" *(?P<node>$floatregex) .*alpha.*"),lines[line])
    push!(result.nodes,parse(Float64,regmatch[:node]))
  end
  close(file)
  close(dir)
  return result
end

"""
Returns the LaplacePoints for the smallest range larger than the requested one for which
pretabulated Laplace points can be found. If the requested range is larger than any pretabulated one,
we expect the largest pretabulated one to be Rₖ, so the function issues a warning and
returns the LaplacePoints for that range.
"""
function findLaplacePointsHackbuschPretableLarger(numberOfPoints::Integer,R::Float64,zipfile::AbstractString="hackbusch")
  dir = ZipFile.Reader(zipfile)
  Rtight = Inf
  Rmax = 0
  for f in dir.files
    if occursin(r"1_xk(\d+).(\d+)_(\d+)",f.name)
      k,range = computeFilenameHackbuschReverse(f.name)
      if k == numberOfPoints
	Rmax = (Rmax > range) ? Rmax : range;
	Rtight = (R <= range < Rtight) ? range : Rtight;
      end
    end
  end

  if Rtight < Inf
    return (readLaplacePointsHackbusch(numberOfPoints,Rtight,zipfile),Rtight)
  else
    # Expect R > Rₖ, so just return largest available table for the given number of points
    @warn "numberOfPoints not sufficiently large ?!"
    return (readLaplacePointsHackbusch(numberOfPoints,Rmax,zipfile),Rmax)
  end
end

"""
Returns the LaplacePoints for the largest range smaller than the requested one for which
pretabulated Laplace points can be found.
"""
function findLaplacePointsHackbuschPretableSmaller(numberOfPoints::Integer,R::Float64,zipfile::AbstractString="hackbusch")
  dir = ZipFile.Reader(zipfile)
  Rtight = 0.
  for f in dir.files
    if occursin(r"1_xk(\d+).(\d+)_(\d+)",f.name)
      k,range = computeFilenameHackbuschReverse(f.name)
      if k == numberOfPoints
	Rtight = (Rtight < range <= R) ? range : Rtight;
      end
    end
  end

  if Rtight > 0
    return (readLaplacePointsHackbusch(numberOfPoints,Rtight,zipfile),Rtight)
  else
    error("No pretabulated value for $numberOfPoints points and a Range of [1., $R] or smaller.")
  end
end

"""
For the ideal Laplace problem the values x must be known to be in the range [1,R].
If 1/x can in general be within [A,B], then all nodes and weights must be scaled with 1/A,
so that x' := x/A lies within [1,B/A (=:R)].

Consider: 1/x = ∑ w exp(-α x)  =>  A/x = ∑ A w exp(-α x)
                               =>  A/x = ∑ (A w) exp(-(A α) x/A)
                   =>  1/x' = ∑ w' exp(-α' x')
"""
function transformRangeToIdealLaplace(A::Float64,B::Float64)
  return (1,B/A)
end

"""
cmp. help(transformRangeToIdealLaplace)
"""
function transformNodeFromIdealLaplace(α::Float64,A::Float64)
  return α/A
end

"""
cmp. help(transformRangeToIdealLaplace)
"""
function transformWeightFromIdealLaplace(w::Float64,A::Float64)
  return w/A
end

"""
cmp. help(transformRangeToIdealLaplace)
"""
function transformLaplacePointFromIdealLaplace(lp::LaplacePoints, A)
  LaplacePoints(map(x->transformWeightFromIdealLaplace(x,A),lp.weights), map(x->transformNodeFromIdealLaplace(x,A),lp.nodes))
end

"""
1/x ≈ ∑ᵢ wᵢ exp(-αᵢ x)
"""
function computeInverseByLaplaceApproximation(x,lp::LaplacePoints)
  sum=0.
  for (node,weight) in zip(lp.nodes,lp.weights)
    sum += weight*exp(-node*x)
  end
  return sum
end
@doc JournalCitation(["A. Takatsuka", "S. Ten-No", "W. Hackbusch"],"J. Chem. Phys.",129,044112,2008) computeInverseByLaplaceApproximation

"""
1/(x² + ω²) ≈ ∑ᵢ wᵢ sin(ω αᵢ)/ω exp(-αᵢ x)
"""
function computeRPADenominatorByDoubleLaplace(x::Float64,ω::Float64,lp::LaplacePoints)
  sum = 0.
  for (node,weight) in zip(lp.nodes,lp.weights)
    sum += weight * sin(ω*node)/ω * exp(-node*x)
  end
  return sum
end
@doc JournalCitation(["H. F. Schurkus","C. Ochsenfeld"],"J. Chem. Phys.",144,031101,2016) computeRPADenominatorByDoubleLaplace

==(x::LaplacePoints, y::LaplacePoints) = x.weights == y.weights && x.nodes == y.nodes

end # LaplaceModule


# 2D arrays are confusing, wonder if I can do this better?
room = open("a11.txt") do file
  hcat([collect(line) for line in eachline(file)]...)
end

cartesian = CartesianIndices(room)
linear = LinearIndices(room)
Ifirst = first(cartesian)
Ilast = last(cartesian)
I1 = CartesianIndex(1,1)

seats = linear[findall((c) -> c == 'L', room)]

function simulate(affecting, tolerance)
  occupied = BitSet()
  while true
    change = BitSet()
    for seat in seats
      neighbours = length(intersect(affecting[seat], occupied))
      if in(seat, occupied)
        neighbours >= tolerance && push!(change, seat)
      else
        neighbours == 0 && push!(change, seat)
      end
    end
    length(change) == 0 && break
    symdiff!(occupied, change)
  end
  length(occupied)
end

function part1()
  adjacent = fill(BitSet(), axes(room))
  for I in cartesian
    if room[I] != 'L'
      continue
    end
    adjacent[I] = BitSet([linear[J] for J in max(Ifirst, I-I1):min(Ilast, I+I1) if I != J && room[J] == 'L'])
  end
  simulate(adjacent, 4)
end

using BenchmarkTools: @btime
ans1 = @btime part1()
println("$ans1")

function part2()
  visible = fill(BitSet(), axes(room))
  directions = [
    CartesianIndex(x,y) for x in -1:1 for y in -1:1 if (x,y) != (0,0)
  ]
  for I in cartesian
    if room[I] != 'L'
      continue
    end
    visibles = []
    for D in directions
      J = I
      while true
        J += D
        checkbounds(Bool, room, J) || break
        room[J] == 'L' || continue
        push!(visibles, linear[J])
        break
      end
    end
    visible[I] = BitSet(visibles)
  end
  simulate(visible, 5)
end

ans2 = @btime part2()
println("$ans2")


# ---
# title: "`mgradient`"
# id: op_mgradient
# cover: assets/mgradient.png
# ---

# There are three commonly used morphological gradient definitions: the beucher gradident
# `dilate(img) - erode(img)`, the external half-gradient `dilate(img) - img`, and the
# internal half-gradident `img - erode(img)`.

using ImageMorphology
using TestImages
using ImageBase
using ImageShow

img = restrict(testimage_dip3e("fig0940")) # rice
img01 = @. Gray(img > 0.5)
mosaic(img, img01; nrow=1)

# ## Beucher gradient

# The default mode is beucher gradient defined as `dilate(img) - erode(img)`.

mosaic(mgradient(img), mgradient(img01); nrow=1)

# There are two nice properties of the beucher gradient:
#
# - it is always positive if the structuring element is symmetric
# - it is a self-complementary operator -- `mgradient(img) == mgradient(complement.(img))`

all(mgradient(rand(32, 32)) .> 0) # always positive

#-

A = rand(32, 32)
mgradient(A) == mgradient(1 .- A) # self-complementary

# ## Half gradient

# If we look closely, we can see that the beucher gradient boundary is always two-pixel wide
# -- one pixel per each side.

A = falses(7, 7)
A[3:5, 3:5] .= true
A

#-

Int.(mgradient(A))

# This is because `dilate` extends the boundary for one pixel, while `erode` shrinks for one
# pixel. This observation introduces so-called _half-gradients_.
#
# The _external half-gradient_, also known as _half-gradient by dilation_, is defined as
# `dilate(img) - img`:

Int.(mgradient(A; mode=:external)) # external boundary

# The _internal half-gradient_, also known as _half-gradient by erosion_, is defined as
# `img - erode(img)`:

Int.(mgradient(A; mode=:internal)) # internal boundary

# Note that the result of internal gradient is inside the original image, and that of
# external gradient is outside the original image. Also, external gradient and internal
# are complementary to each other:

A = rand(32, 32)
mgradient(A; mode=:external) == mgradient(1 .- A; mode=:internal)

# ## Thick gradient

# The gradient thickness is controlled by the SE size, or the `r` parameter. When `r>1`, it
# is usually called _thick gradient_.

mosaic([mgradient(img01; r) for r in 1:3]; nrow=1)

# ## See also

# There are many useful gradient definitions in image processing. For instance, ImageBase.jl
# provides the finite-difference version of gradients `fdiff` and `fgradient`.
# ImageFiltering.jl provides `sobel`, `prewitt` and other gradient filters.

fout = abs.(ImageBase.FiniteDiff.fdiff(img; dims=1))
mout = mgradient(img; dims=1)
mosaic(fout, mout; nrow=1)

# For a comprehensive and more accurate documentation, please check the [`mgradient`](@ref)
# reference page.

# save cover image #src
using FileIO #src
mkpath("assets") #src
img = Gray.(restrict(testimage("blobs"))) #src
save("assets/mgradient.png", mgradient(img; r=2)) #src


function acyclic(network::AbstractNetwork, linkcosts::Vector{<:Real}, order=orderednodes(network); kind=:short)
    nnodes = length(order)
    r = order[1]

    nodecosts = fill((kind == :short) ? Inf : (kind == :long) ? -Inf : error("Only short and long kinds allowed!"), nnodes)
    parentvec = fill(-1, nnodes)
    nodecosts[r] = 0; parentvec[r] = r

    condn = ((kind == :short) ? (i,j) -> (nodecosts[j] > nodecosts[i] + linkcosts[idx(network, i, j)])
                              : (i,j) -> (nodecosts[j] < nodecosts[i] + linkcosts[idx(network, i, j)]))

    for j in order[2:end]
        for i in inneighbors(network, j)
            if condn(i,j)
                nodecosts[j] = nodecosts[i] + linkcosts[idx(network, i, j)]
                parentvec[j] = i
            end
        end
    end

    return nodecosts, parentvec
end


# Run line by line in the REPL, or ctrl + enter in VSCode
# First code section
using MechanicalUnits
m_air = 1000kg; c_p = 1.00kJ/(kg*K)
@import_expand ~W   # Watt = Joule / Second is not exported by default. Several: (u1, u2,..)
Q_cp(T1, T2) = m_air * c_p * (T2 - T1) |> (kW*h)
Q_cp(20°C, 985°C)
dm |> upreferred
preferunits(m) # No effect, since upreferred was called once this session
m_s = [30kg/m 28.8lb/ft]
l_s = 93ft*[3 4]m/s
m_s.*l_s .|> (kg*m)
E=206GPa; h_y = 100mm; b = 30mm; I = 1/12 * b * h_y^3
L = 2m; F=100kg*g |> N
F*L^3/(3E*I) |> upreferred
l_wire = 20m
k(d) = E * 0.691 * π/4 * d^2 / l_wire |> N/mm
k.([5 6 8]mm)
δ(d)= F / k(d) |> mm
δ.([5, 6, 8]mm)
d = 6mm
dimension(d)
1d |> s
@import_expand ~V ~W ~A  G
sqrt(1G²)
[1V*12.0A 2W 1kg*g*1m/2s]*30minute .|> kJ
ω = 50*2π*rad/s
t = (0:0.006:0.02)s
u = 220V*exp.(im∙(ω∙t))
u*1.5A .|> J

# Second code section
import MechanicalUnits: @import_expand, ∙
@import_expand ~m dyn     # ~ : also import SI prefixes for metre
(1.0cm², 2.0mm∙m, 3.0dm⁴/m² ) .|> mm²
typeof(dyn)
1dyn |> μm

# Third code section
strinp = "2 [s]\t11364.56982421875 [N]\t-44553.50244140625 [N]\t-26.586366176605225 [N]\t0.0[N mm]\t0.0[N mm]\t0.0[N mm]\t1561.00350618362 [mm]\t-6072.3729133606 [mm]\t2825.15907287598 [mm]";

time, Fx, Fy, Fz, Mx, My, Mz, px, py, pz = parse.(Quantity{Float64}, split(strinp, '\t'))

# Fourth code section
strain = 10.6μm/m
strain |> upreferred
strain *m/μm
strain |> NoUnits


export Diff

"""
    Diff(f)

Create the derivative of the scalar function `f`, based on `ForwardDiff`.
"""
Diff(f) = x -> ForwardDiff.derivative(f, float(x))

"""
    Diff(f,k)

By recurrence, create the k-th derivative of the scalar function `f`, based on `ForwardDiff`.
"""
Diff(f, k) = k == 0 ? f : k == 1 ? Diff(f) : k>1 ? Diff(Diff(f),k-1) : print("Differential operator not defined for k<0")


# ==============================
#  ======== Tested code ========
using Eirene
using Plots
using StatsPlots

#%%
"""
    get_bettis(results_eirene::Dict, max_dim::Integer; min_dim::Int=1)

Calls Eirene.betticurve for 'dim' in range from `min_dim` up to 'max_dim' and
stack the resulting Arrays into a vector.

The returned value is a Vector of Arrays{Float64,2}. Each array is of size
(n,2), where n is the maximal number of steps taken to compute Betti curve of dimensions
ranging form `min_dim` to `max_dim`. First column of each array contains numbered steps.
Second column are the Betti curve values for corresponding step.

Arrays in returned vector correspond to Betti curve dimensions form range
`min_dim` up to 'max_dim'.

"""
function get_bettis(results_eirene::Dict, max_dim::Integer; min_dim::Int = 1)
    bettis = Matrix{Float64}[]
    for d = min_dim:max_dim
        result = betticurve(results_eirene, dim = d)
        if isempty(result) && d > 1
            result = zeros(size(bettis[d-1]))
        end
        push!(bettis, result)
    end
    return bettis
end
# TODO add get_bettis_from_matrix, to wrap C= eirene...; get bettis

#%%

"""
  normalise_bettis(bettis::Vector)
    normalise_bettis(bettis::Array)

Normalise the number of steps for Betti curves. 'bettis' can be either vector of
arrays (each array contain Betti curve of different dimension) or an array
containing Betti curve of a single dimension.
"""
function normalise_bettis(bettis::Vector)

    @debug "Vector version"
    norm_bettis = copy(bettis)
    @debug "norm_bettis size :" size(norm_bettis)[1][1]

    max_dim = size(norm_bettis)[1]
    @debug "typeof(max_dim) :" typeof(max_dim[1])

    for d = 1:(max_dim)
        if !isempty(norm_bettis[d])
            norm_bettis[d][:, 1] /= findmax(norm_bettis[d][:, 1])[1]
        end
    end
    return norm_bettis

end

#%%
function normalise_bettis(bettis::Array)
    @debug "Array version"
    norm_bettis = copy(bettis)
    @debug "norm_bettis size :" size(norm_bettis)

    if !isempty(norm_bettis)
        norm_bettis[:, 1] /= findmax(norm_bettis[:, 1])[1]
    end
    return norm_bettis
end

#%%
# function vectorize_bettis(betti_curves::Array{Matrix{Float64,2}})
"""
    vectorize_bettis(betti_curves::Matrix{Float64})

Reshapes the 'betti_curves' from type Array{Matrices{Float64,2}} into
Matrix{Float64}.

The resulting matrix size is (n, k), where 'n' is equal to the number of
rows in each matrix, 'k' is equal to the number of matrices.

TODO: Change the name- it takse vector and returns a matrix.
TODO: get bettis could have an arguent betti_type which would determine resulting type
"""
function vectorize_bettis(betti_curves::Vector{Array{Float64,2}})

    first_betti = 1
    last_betti = size(betti_curves,1)
    return hcat([betti_curves[k][:, 2] for k = first_betti:last_betti]...)
end

#%%
@deprecate vectorize_bettis(eirene_results::Dict, maxdim::Integer, mindim::Integer) vectorize_bettis(betti_curves)

# ===
#%%
"""
  get_vectorized_bettis(results_eirene::Dict, max_dim::Integer; min_dim::Int=1)

Takes the eirene result and computes Betti curves for dimensions in range
'mindim:maxdim'. Every Betti curve is stored in successive column of the
resulting array.
TODO: this should return a matrix, where first col are indices and rest are B values (1st col is missing now)
"""
function get_vectorized_bettis(results_eirene::Dict, max_dim::Integer; min_dim::Int = 1)

    all_bettis = get_bettis(results_eirene, max_dim, min_dim = min_dim)
    bettis_vector = vectorize_bettis(all_bettis)

    return bettis_vector
end

# ==
#%%
"""
  plot_bettis(bettis; min_dim::Integer=1, betti_labels::Bool=true, default_labels::Bool=true kwargs...)

Creates a plot for set of betti numbers stored in `bettis` and return the
handler to the plot.

'kwargs' are plot parameters

Some of the possible 'kwargs' are:
  - title::String
  - legend:Bool
  - size::Tuple{T, T} where {T::Number}
  - lw::Integer or linewidth:Integer
(for more, see plots documentation):
TODO: min_dim is not included in all_dims variable
TODO: add change of x label based on x values- so it is either edge density for 0:1 range values or Filtration step otherwise
"""
function plot_bettis(bettis::Vector;
                        min_dim::Integer = 1,
                        use_edge_density::Bool=true,
                        betti_labels::Bool = true,
                        default_labels::Bool = true,
                        kwargs...)#; plot_size = (width=1200, height=800),
    max_dim = size(bettis, 1)
    all_dims = 1:max_dim

    if min_dim > max_dim
        throw(DomainError(
            min_dim,
            "\'min_dim\' must be greater that maximal dimension.",
        ))
    end

    lw_pos = findfirst(x -> x == :lw || x == :linewidth, keys(kwargs))
    if !isnothing(lw_pos)
        lw = kwargs[lw_pos]
    else
        lw = 2
    end

    # Create iterator for all loops
    all_iterations = 1:(max_dim) #TODO ths can not be starting from min_dim, because it may be 0

    if use_edge_density
        for p = all_iterations
            max_step = findmax(bettis[p][:, 1])[1]
            bettis[p][:, 1] ./= max_step
        end
    end

    colors_set = get_bettis_color_palete(min_dim=min_dim)
    plot_ref = plot(; kwargs...)
    # for p = min_dim:(max_dim) #TODO ths can not be starting from min_dim, because it may be 0
    # for p = all_iterations
    for (index, p) in enumerate(min_dim:max_dim)
        args = (lc = colors_set[index], linewidth = lw)
        if betti_labels
            args = (args..., label = "β$(p)")
        end
        plot!(bettis[index][:, 1], bettis[index][:, 2]; args...)
    end

    legend_pos = findfirst(x -> x == :legend, keys(kwargs))
    if !isnothing(legend_pos)
        plot!(legend = kwargs[legend_pos])
    else
        plot!(legend = betti_labels)
    end

    x_pos = findfirst(x -> x == :xlabel, keys(kwargs))
    y_pos = findfirst(x -> x == :ylabel, keys(kwargs))
    if !isnothing(x_pos)
        xlabel!(kwargs[x_pos])
    elseif default_labels
        xlabel!("Edge density")
    end
    if !isnothing(y_pos)
        ylabel!(kwargs[y_pos])
    elseif default_labels
        ylabel!("Number of cycles")
    end

    # set tlims to integer values
    max_ylim = findmax(ceil.(Int, ylims(plot_ref)))[1]
    if max_ylim <=3
        ylims!((0, 3))
    end

    if use_edge_density
        xlims!((0, 1))
    end

    return plot_ref
end

"""
  plot_bettis(bettis::Array; min_dim::Integer=1, betti_labels::Bool=true, default_labels::Bool=true kwargs...)

Creates a plot for set of betti numbers stored in `bettis` and return the
handler to the plot.

'kwargs' are plot parameters

Some of the possible 'kwargs' are:
  - title::String
  - legend:Bool
  - size::Tuple{T, T} where {T::Number}
  - lw::Integer or linewidth:Integer
(for more, see plots documentation):
TODO: add change of x label based on x values- so it is either edge density for 0:1 range values or Filtration step otherwise
"""
function plot_bettis(bettis::Array;
                        # min_dim::Integer = 1,
                        dims_range=1:size(bettis,2),
                        use_edge_density::Bool=true,
                        betti_labels::Bool = true,
                        default_labels::Bool = true,
                        normalised=true,
                        kwargs...)#; plot_size = (width=1200, height=800),
    max_dim = dims_range[end]
    min_dim = dims_range[1]

    if min_dim > max_dim
        throw(DomainError(
            min_dim,
            "\'min_dim\' must be greater that maximal dimension in \'bettis\'",
        ))
    end

    total_steps = size(bettis, 1)
    if normalised
        x_vals = range(0, stop=1, length=total_steps)
    else
        x_vals = range(0, stop=total_steps)
    end

    lw_pos = findfirst(x -> x == :lw || x == :linewidth, keys(kwargs))
    if !isnothing(lw_pos)
        lw = kwargs[lw_pos]
    else
        lw = 2
    end

    # if use_edge_density
    #     # for p = 1:(max_dim) #TODO ths can not be starting from min_dim, because it may be 0
    #     for (index, p) in enumerate(min_dim:max_dim)
    #         max_step = findmax(bettis[:, 1])[1]
    #         bettis[p][:, 1] ./=max_step
    #     end
    # end

    colors_set = get_bettis_color_palete(min_dim=min_dim)
    plot_ref = plot(; kwargs...)
    # for p = min_dim:(max_dim) #TODO ths can not be starting from min_dim, because it may be 0
    # for p = 1:(max_dim) #TODO ths can not be starting from min_dim, because it may be 0
    for (index, p) in enumerate(min_dim:max_dim)
        args = (lc = colors_set[index], linewidth = lw)
        if betti_labels
            args = (args..., label = "β$(p)")
        end
        plot!(x_vals, bettis[:, index]; args...)
    end

    legend_pos = findfirst(x -> x == :legend, keys(kwargs))
    if !isnothing(legend_pos)
        plot!(legend = kwargs[legend_pos])
    else
        plot!(legend = betti_labels)
    end

    x_pos = findfirst(x -> x == :xlabel, keys(kwargs))
    y_pos = findfirst(x -> x == :ylabel, keys(kwargs))
    if !isnothing(x_pos)
        xlabel!(kwargs[x_pos])
    elseif default_labels
        xlabel!("Edge density")
    end
    if !isnothing(y_pos)
        ylabel!(kwargs[y_pos])
    elseif default_labels
        ylabel!("Number of cycles")
    end

    # set tlims to integer values
    max_ylim = findmax(ceil.(Int, ylims(plot_ref)))[1]
    if max_ylim <=3
        ylims!((0, 3))
    end

    if use_edge_density
        xlims!((0, 1))
    end

    return plot_ref
end
#  ======= Untested code
# TODO add default kwargs paring function -> parse_kwargs()

"""
    plot_all_bettis ...
"""
function plot_all_bettis(bettis_collection;
                        min_dim::Integer = 1,
                        betti_labels::Bool = true,
                        default_labels::Bool = true,
                        normalised=true,
                        kwargs...)#; plot_size = (width=1200, height=800),
    total_dims = size(bettis_collection[1],2)

    lw_pos = findfirst(x -> x == :lw || x == :linewidth, keys(kwargs))
    if !isnothing(lw_pos)
        lw = kwargs[lw_pos]
    else
        lw = 2
    end

    colors_set = get_bettis_color_palete(min_dim=min_dim)
    max_y_val = find_max_betti(bettis_collection)

    plot_ref = plot(; kwargs...)
    for b = 1:total_dims
        args = (lc = colors_set[b], linewidth = lw, alpha=0.12,label=false, ylims=(0,max_y_val))
        for bettis = bettis_collection
            betti_vals = bettis[:,b]

            total_steps = size(bettis, 1)
            x_vals = range(0, stop=1, length=total_steps)

            plot!(x_vals, betti_vals; args...)
        end
        # my_label = "β$(b)"
        # betti_vals = results_d["bettis_collection"][:hc][end]
        # x_vals = range(0, stop=1, length=size(betti_vals, 1))
        # plot!(x_vals, betti_vals; lc = colors_set[b], linewidth = 1, alpha=0.1,label=my_label, ylims=(0,max_y_val))
    end
    plot!(legend=true)

    legend_pos = findfirst(x -> x == :legend, keys(kwargs))
    if !isnothing(legend_pos)
        plot!(legend = kwargs[legend_pos])
    else
        plot!(legend = betti_labels)
    end

    x_pos = findfirst(x -> x == :xlabel, keys(kwargs))
    y_pos = findfirst(x -> x == :ylabel, keys(kwargs))
    if !isnothing(x_pos)
        xlabel!(kwargs[x_pos])
    elseif default_labels
        xlabel!("Edge density")
    end
    if !isnothing(y_pos)
        ylabel!(kwargs[y_pos])
    elseif default_labels
        ylabel!("Number of cycles")
    end

    return plot_ref
end

"""
    find_max_betti(bettis_collection::Array)

Returns the highest Betti curve value from all dimensions.
"""
function find_max_betti(bettis_collection::Array)
    if typeof(bettis_collection) == Vector
        bettis_collection = vectorize_bettis(bettis_collection)
    end

    max_y_val = 0
    for betti_set in bettis_collection
        local_max = findmax(betti_set)[1]
        if local_max > max_y_val
            max_y_val = local_max
        end
    end
    return max_y_val
end

#  ======= Untested code == end
#%%

"""
  printready_plot_bettis(kwargs)

Creates a plot using 'plot_bettis' with arguments which were tested to be very
good for using them in prints. Used arguments are:

"""
function printready_plot_bettis(kwargs)
    return nothing
end

#%%
"""
  function get_bettis_color_palete()

Generates vector with colours used for Betti plots. Designed for Betti plots consistency.
"""
function get_bettis_color_palete(; min_dim = 1, use_set::Integer = 1)
    # TODO what does the number in the function below is used for?

    if use_set == 1
        cur_colors = [Gray(bw) for bw = 0.0:0.025:0.5]
        if min_dim == 0
            colors_set = [RGB(87 / 256, 158 / 256, 0 / 256)]
        else
            colors_set = []
        end
        max_RGB = 256
        colors_set = vcat(
            colors_set,
            [
                RGB(255 / max_RGB, 206 / max_RGB, 0 / max_RGB),
                RGB(248 / max_RGB, 23 / max_RGB, 0 / max_RGB),
                RGB(97 / max_RGB, 169 / max_RGB, 255 / max_RGB),
                RGB(163 / max_RGB, 0 / max_RGB, 185 / max_RGB),
                RGB(33 / max_RGB, 96 / max_RGB, 45 / max_RGB),
                RGB(4 / max_RGB, 0 / max_RGB, 199 / max_RGB),
                RGB(135 / max_RGB, 88 / max_RGB, 0 / max_RGB),
            ],
            cur_colors,
        )
    else
        use_set == 2
        cur_colors = get_color_palette(:auto, 1)
        cur_colors3 = get_color_palette(:lightrainbow, 1)
        cur_colors2 = get_color_palette(:cyclic1, 1)
        if min_dim == 0
            # colors_set =  [cur_colors[3], cur_colors[5], [:red], cur_colors[1]] #cur_colors[7],
            colors_set = [cur_colors3[3], cur_colors[5], cur_colors3[end], cur_colors[1]] #cur_colors[7],
        else
            colors_set = [cur_colors[5], cur_colors3[end], cur_colors[1]] #cur_colors[7],
            # colors_set =  [cur_colors[5], [:red], cur_colors[1], cur_colors[14]]
        end
        # for c =  [collect(11:25);]
        #     push!(colors_set, cur_colors2[c])
        # end
        colors_set = vcat(colors_set, [cur_colors2[c] for c in [collect(11:25);]])
    end

    return colors_set
end

# ==============================
#  ======= Untested code =======

# using Measures
# using Plots.PlotMeasures

#
# # Source: https://github.com/JuliaPlots/Plots.jl/issues/897
# function setdefaultplottingparams(;upscale=2)
#     #8x upscaling in resolution
#     fntsm = Plots.font("sans-serif", pointsize=round(12.0*upscale))
#     fntlg = Plots.font("sans-serif", pointsize=round(18.0*upscale))
#     default(titlefont=fntlg, guidefont=fntlg, tickfont=fntsm, legendfont=fntsm)
#     default(size=(800*upscale,600*upscale)) #Plot canvas size
#     default(dpi=500) #Only for PyPlot - presently broken
# end

#%%
"""
  plot_bettis_collection(bettis_collection, bett_num; step=1, show_plt=true, R=0., G=0.4, B=1.0)

PLots collection of Betti curves of rank 'bett-num'. Every successive plot has
lower opacity than predecessor.  'step' defines step between collection elements
that are ploted. By default, plot is displayed after carteation. This can be
disabled by setting 'show_plt' to false.

Color of the plot can be set with 'R', 'G', 'B' parameters.
"""
function plot_bettis_collection(bettis_collection,
                                bett_num,
                                max_rank;
                                step = 1,
                                show_plt = true,
                                R = 0.0,
                                G = 0.4,
                                B = 1.0)
    step > 0 || error("Steps should be natural number!")
    bettis_total = size(bettis_collection, 1)
    colors_set = zeros(Float64, bettis_total, 4)
    colors_set[:, 1] .= R
    colors_set[:, 2] .= G
    colors_set[:, 3] .= B
    max_betti = get_max_betti_from_collection(bettis_collection)
    @info "max_betti" max_betti

    x = 0
    y = bettis_total * 0.1
    va_range = collect(range(bettis_total + x, y, length = bettis_total))

    colors_set[:, 4] .= va_range / findmax(va_range)[1]
    rgba_set = RGBA[]
    for k = 1:size(colors_set, 1)
        push!(
            rgba_set,
            RGBA(colors_set[k, 1], colors_set[k, 2], colors_set[k, 3], colors_set[k, 4]),
        )
    end

    plt_reference = plot(1, title = "Betti curves collection, rank $(bett_num)", label = "")
    for b = 1:step:bettis_total
        betti = bettis_collection[b]
        x_vals_1 = (1:size(betti[:, bett_num], 1)) / size(betti[:, bett_num], 1)
        plot!(x_vals_1, betti[:, bett_num], lc = rgba_set[b], label = "rank=$(max_rank-b)")
        plot!(ylim = (0, max_betti))
    end
    xlabel!("Normalised steps")
    ylabel!("Number of cycles")
    plot!(legend = true)

    show_plt && display(plt_reference)
    return plt_reference
end

#%%

"""
    get_max_bettis(bettis)

Returns the maximal bettis of Betti curves for all dimensions.
"""
function get_max_bettis(bettis)

    all_max_bettis = findmax(bettis, dims=1)[1]

    return all_max_bettis
end

# TODO change name
# TODO check what for dim is used, change to min dim
function get_max_betti_from_collection(bettis_collection; dim = 1)
    max_betti = 0
    for betti in bettis_collection
        # global max_betti
        local_max = findmax(betti)[1]
        if (local_max > max_betti)
            max_betti = local_max
        end
    end
    return max_betti
end

#%%
"""
  plot_and_save_bettis(eirene_results, plot_title::String,
                results_path::String; extension = ".png",
                data_size::String="", do_save=true,
                extend_title=true, do_normalise=true, max_dim=3,
                legend_on=true)

Plot Betti curves from 0 up to `max_dim` using `eirene_results` from Eirene library and
returns handler for figure. Optionally, if `do_save` is set, saves the figure
or if `do_normalise` is set, sets the steps range to be normalised to the
horizontal axis maximal value.
"""
function plot_and_save_bettis(bettis,
                                plot_title::String,
                                results_path::String;
                                file_name = "",
                                extension = ".png",
                                do_save = true,
                                do_normalise = true,
                                min_dim = 0,
                                max_dim = 3,
                                legend_on = true,
                                kwargs...)
    bettis = get_bettis(eirene_results, max_dim)
    if do_normalise
        bettis = normalise_bettis(bettis)
    end
    plot_ref =
        plot_bettis(bettis, plot_title, legend_on = legend_on, min_dim = min_dim, kwargs...)

    if do_save
        if isempty(file_name)
            file_name = plot_title * extension
        elseif isempty(findall(x -> x == extension[2:end], split(file_name, ".")))
            #check for the extension in file name
            file_name *= extension
        end

        save_figure_with_params(
            plot_ref,
            results_path;
            extension = extension,
            prefix = split(file_name, ".")[1],
        )

    end
    return plot_ref
end

# TODO merge functions for getting betti curves
# Original function returns 2 different types of betti curves. If no default
# value parameters is given, it returns vector of matrices. If num of steps is
# given, then it return matrix maxdim x numsteps.
# """
# bettis_eirene(matr, maxdim; mintime=-Inf, maxtime=Inf, numofsteps=Inf, mindim=1)
#
# Takes the `matr` and computes Betti curves up to `maxdim`. Return matrix only
# with betti curve values
#
#
# Function taken from: https://github.com/alexyarosh/hyperbolic
# """
#%%

@deprecate bettis_eirene(matr, maxdim; mintime = -Inf, maxtime = Inf, numofsteps = Inf, mindim = 1) get_bettis(results_eirene, max_dim; min_dim = 1)
@deprecate get_bettis_from_image(img_name, plot_params; file_path = "", plot_heatmaps = true, save_heatmaps = false, plot_betti_figrues = true) get_bettis(results_eirene, max_dim; min_dim = 1)
@deprecate get_bettis_from_image2(img_name;file_path = "",plot_heatmaps = true, save_heatmaps = false, plot_betti_figrues = true) get_bettis_from_image(img_name, plot_params; file_path = "", plot_heatmaps = true, save_heatmaps = false, plot_betti_figrues = true)
@deprecate plot_and_save_bettis2(eirene_results, plot_title::String, results_path::String; file_name = "", extension = ".png", data_size::String = "", do_save = true, extend_title = true, do_normalise = true, min_dim = 0, max_dim = 3, legend_on = true) plot_and_save_bettis(bettis, plot_title::String, results_path::String; file_name = "", extension = ".png", do_save = true, do_normalise = true, min_dim = 0, max_dim = 3, legend_on = true, kwargs...)
@deprecate get_and_plot_bettis(eirene_results; max_dim = 3, min_dim = 1, plot_title = "", legend_on = false) get_bettis(results_eirene, max_dim; min_dim = 1)

#%%
"""
  lower_ordmat_resolution(ordered_matrix::Array, total_bins::Int)

Takes ordered matrix 'input_matrix' and reduces the resolution of values in the
matrix into 'total_bins' bins.
"""
function lower_ordmat_resolution(ordered_matrix::Array, total_bins::Int)
    new_ordered_matrix = zeros(size(ordered_matrix))
    max_val = findmax(ordered_matrix)[1]
    min_val = findmin(ordered_matrix)[1]

    bin_step = max_val ÷ total_bins
    old_bins = min_val:bin_step:max_val

    for bin = 1:total_bins
        @debug "First step threshold is $(old_bins[bin])"
        indices = findall(x -> (x >= old_bins[bin]), ordered_matrix)
        new_ordered_matrix[indices] .= bin - 1
    end

    @debug "Max_val in new matrix is " findmax(new_ordered_matrix)
    @debug "And should be " total_bins - 1

    return new_ordered_matrix
end

#%%
"""
  average_bettis(bettis_matrix; up_factor=8)

Takes the average values of betti curves stored in 'bettis_matrix'.

'bettis_matrix' consist of different simulations(first index of the matrix),
different ranks (third index of the matrix). Second index of the matrices
(saples) may vary accross many simulations and for this reason, all betti curves
are upsampled by a factor of 'upsample_factor' and then the average for every
dimension is computed.
"""
function average_bettis(bettis_matrix::Matrix; up_factor = 8)

    bettis_matrix_backup = copy(bettis_matrix)

    simulations = size(bettis_matrix, 1)
    dimensions = size(bettis_matrix[1], 1)

    max_samples = 0
    for k = 1:simulations
        # global max_samples
        current_len = length(bettis_matrix[k][1][:, 1])
        if max_samples < current_len
            max_samples = current_len
        end
    end

    bettis_size = size(bettis_matrix)

    total_upsamples = (max_samples - 1) * up_factor + 1
    x_resampled = range(0, 1, step = total_upsamples)

    avg_bettis = zeros(total_upsamples, dimensions)
    std_bettis = copy(avg_bettis)
    resampled_bettis = zeros(simulations, total_upsamples, dimensions)

    # resample betti curves
    for simulation = 1:simulations, betti = 1:dimensions
        resampled_bettis[simulation, :, betti] =
            upsample_vector2(bettis_matrix[simulation][betti][:, 2], total_upsamples)
    end

    # average and std Betti
    for dimension = 1:dimensions
        avg_bettis[:, dimension] = mean(resampled_bettis[:, :, dimension], dims = 1)
        std_bettis[:, dimension] = mean(resampled_bettis[:, :, dimension], dims = 1)
    end

    return avg_bettis, std_bettis
end

#%%
function upsample_vector2(input_vector, total_upsamples)
    total_orig_samples = size(input_vector, 1) - 1

    x_vals = range(0, 1, length = total_orig_samples + 1)
    spl = Spline1D(x_vals, input_vector)

    x_upsampled = range(0, 1, length = total_upsamples)
    y_upsampled = spl(x_upsampled)

    # ref = plot(range(0, 1, length=total_orig_samples), input_vector);
    # plot!(x_vals, y_upsampled);
    # display(ref)

    return y_upsampled
end

#%%
"""
  upsample_vector(input_vector; upsample_factor::Int=8)

Takes an 'input_vector' and returns a vector which has 'upsample_factor' many
times more samples. New samples are interpolated with 'spl' function from
'Dierckx' package.

"""
function upsample_vector(input_vector; upsample_factor::Int = 8)
    total_orig_samples = size(input_vector, 1) - 1
    total_samples = upsample_factor * total_orig_samples + 1

    x_vals = range(0, 1, length = total_orig_samples + 1)
    spl = Spline1D(x_vals, input_vector)

    x_upsampled = range(0, 1, length = total_samples)
    y_upsampled = spl(x_upsampled)

    # ref = plot(range(0, 1, length=total_orig_samples), input_vector);
    # plot!(x_vals, y_upsampled);
    # display(ref)

    return y_upsampled
end

# =========--=======-========-==========-=======-
# From bettis areas
# Area under Betti curve functions
#%%
"""
    get_area_under_betti_curve(betti_curves, min_dim, max_dim)

Computes the area under Betti curves stored in 'betti_curves', where each row is
a Betti curve and each column is a value.
"""
function get_area_under_betti_curve(betti_curves::Union{Matrix{Float64}, Array{Array{Float64,2}}};do_normalised::Bool=false)
    #TODO check this part
    if size(betti_curves,2) < 2
        bettis_vector = vectorize_bettis(betti_curves)
    else
        bettis_vector = betti_curves
    end
    # @info sum(bettis_vector, dims=1)
    bettis_area = sum(bettis_vector, dims=1)

    if do_normalised
        total_steps = size(bettis_vector,1)
        bettis_area ./= total_steps
    end
    # @info bettis_area
    return bettis_area
end

@deprecate get_area_under_betti_curve(C, min_dim, max_dim) get_area_under_betti_curve(betti_curves; do_normalised=false)

#%%
"""
    get_dataset_bettis_areas(dataset; min_dim::Integer=1, max_dim::Integer=3, return_matrix::Bool=true)

Computes topology of every matrix in dataset, computes Betti curves for dimensions
min_dim up to max_dim and returns vector (or matrix) of areas under Betti curves.
"""
function get_dataset_bettis_areas(dataset; min_dim::Integer=1, max_dim::Integer=3, return_matrix::Bool=true)
    areas_vector = Array[]
    for data = dataset
        @info "Computing topology."
        C = eirene(data, maxdim=max_dim,)
        matrix_bettis = get_bettis(C,max_dim, min_dim=min_dim)
        push!(areas_vector, get_area_under_betti_curve(matrix_bettis))
    end
    if return_matrix
        return vcat([areas_vector[k] for k=1:10]...)
    else
        return areas_vector
    end
end

#%%
"""
    get_area_boxes(areas_matrix, min_dim::Integer, max_dim::Integer)

Plots the boxplot of area under betti curves.
"""
function get_area_boxes(areas_matrix, min_dim::Integer, max_dim::Integer)
    bplot = StatsPlots.boxplot()

    data_colors = get_bettis_color_palete()

    for (index, value) in enumerate(min_dim:max_dim)
        StatsPlots.boxplot!(bplot, areas_matrix[:,index], labels="β$(value)", color=data_colors[value])
    end

    return bplot
end

# function get_bettis_collection_from_matrices(ordered_matrices_collection; max_dim::Int=3, min_dim::Int=1)
#     bettis_collection = Array[]
#
#     for matrix = ordered_matrices_collection
# 		@debug "Computing Bettis..."
# 		eirene_geom = eirene(matrix,maxdim=max_B_dim,model="vr")
#
# 		bettis = reshape_bettis(get_bettis(eirene_geom, max_B_dim))
# 		push!(bettis_collection, bettis)
#     end
#
#     return bettis_collection
# end
#
# #%%
# TODO find what are the alternative functions for the functions below
# @deprecate get_dataset_topology(dataset; min_dim::Integer=1, max_dim::Integer=3, get_curves::Bool=true, get_areas::Bool=true, get_persistence_diagrams::Bool=true, do_normalise::Bool=true)
# @deprecate get_bettis_collection(ordered_matrices_collection; max_B_dim=3)
# @deprecate reshape_bettis(bettis)
# @deprecate print_hmap_with_bettis(ordered_matrices_collection, bettis_collection, plot_data::PlottingData)
# @deprecate make_hm_and_betti_plot(ordered_geom_gr, bettis, title_hmap, title_bettis, max_betti)
# @deprecate matrix_analysis(test_data::PlottingData;generation_function=get_geom_matrix)
# @deprecate multiscale_matrix_testing(sample_space_dims = 3, maxsim = 5, min_B_dim = 1, max_B_dim = 3, size_start = 10, size_step = 5, size_stop = 50; do_random = true, control_saving = false, perform_eavl = false)
# @deprecate plot_betti_numbers(betti_numbers, edge_density, title="Geometric  matrix"; stop=0.6)


#
# A simple steepest descent algorithm implementation
#
export subGradientMethod
@doc doc"""
    subGradientMethod(M, F, ∂F, x)
perform a subgradient method $x_{k+1} = \mathrm{retr}(x_k, s_k∂F(x_k))$,

where $\mathrm{retr}$ is a retraction, $s_k$ can be specified as a function but is
usually set to a constant value. Though the subgradient might be set valued,
the argument `∂F` should always return _one_ element from the subgradient.

# Input
* `M` – a manifold $\mathcal M$
* `F` – a cost function $F\colon\mathcal M\to\mathbb R$ to minimize
* `∂F`: the (sub)gradient $\partial F\colon\mathcal M\to T\mathcal M$ of F
  restricted to always only returning one value/element from the subgradient
* `x` – an initial value $x\in\mathcal M$

# Optional
* `stepsize` – ([`ConstantStepsize`](@ref)`(1.)`) specify a [`Stepsize`](@ref)
* `retraction` – (`exp`) a `retraction(M,x,ξ)` to use.
* `stoppingCriterion` – ([`stopWhenAny`](@ref)`(`[`stopAfterIteration`](@ref)`(200), `[`stopWhenGradientNormLess`](@ref)`(10.0^-8))`)
  a functor, see[`StoppingCriterion`](@ref), indicating when to stop.

and the ones that are passed to [`decorateOptions`](@ref) for decorators.

# Output
* `xOpt` – the resulting (approximately critical) point of gradientDescent
* `record` - if activated (using the `record` key, see [`RecordOptions`](@ref)
  an array containing the recorded values.
"""
function subGradientMethod(M::mT,
        F::Function, ∂F::Function, x::MP;
        retraction::Function = exp,
        stepsize::Stepsize = DecreasingStepsize( typicalDistance(M)/5),
        stoppingCriterion::StoppingCriterion = stopAfterIteration(5000),
        kwargs... #especially may contain debug
    ) where {mT <: Manifold, MP <: MPoint}
    p = SubGradientProblem(M,F,∂F)
    o = SubGradientMethodOptions(x,stoppingCriterion, stepsize, retraction)

    o = decorateOptions(o; kwargs...)
    resultO = solve(p,o)
    if hasRecord(resultO)
        return getSolverResult(p,getOptions(resultO)), getRecord(resultO)
    end
    return getSolverResult(p,resultO)
end
function initializeSolver!(p::SubGradientProblem, o::SubGradientMethodOptions)
    o.xOptimal = o.x
    o.∂ = zeroTVector(p.M,o.x)
end
function doSolverStep!(p::SubGradientProblem, o::SubGradientMethodOptions,iter)
    o.∂ = getSubGradient(p,o.x)
    s = getStepsize!(p,o,iter)
    o.x = o.retraction(p.M,o.x,-s*o.∂)
    if getCost(p,o.x) < getCost(p,o.xOptimal)
        o.xOptimal = o.x
    end
end
function getSolverResult(p::SubGradientProblem, o::SubGradientMethodOptions)
    return o.xOptimal
end

#
# TODO specific debugs and records.
#


# ---
# title: 673. Number of Longest Increasing Subsequence
# id: problem673
# author: Indigo
# date: 2021-06-27
# difficulty: Medium
# categories: Dynamic Programming
# link: <https://leetcode.com/problems/number-of-longest-increasing-subsequence/description/>
# hidden: true
# ---
#
# Given an integer array `nums`, return _the number of longest increasing
# subsequences._
#
# **Notice** that the sequence has to be **strictly** increasing.
#
#
#
# **Example 1:**
#
#
#
#     Input: nums = [1,3,5,4,7]
#     Output: 2
#     Explanation: The two longest increasing subsequences are [1, 3, 4, 7] and [1, 3, 5, 7].
#
#
# **Example 2:**
#
#
#
#     Input: nums = [2,2,2,2,2]
#     Output: 5
#     Explanation: The length of longest continuous increasing subsequence is 1, and there are 5 subsequences' length is 1, so output 5.
#
#
#
#
#
# **Constraints:**
#
#   * `1 <= nums.length <= 2000`
#   * `-106 <= nums[i] <= 106`
#
#
## @lc code=start
using LeetCode

function find_number_of_LIS(nums::Vector{Int})
    len = length(nums)
    len <= 1 && return len
    dp = fill(1, len)
    cnt = copy(dp)
    for i in 2:len, j in 1:(i - 1)
        nums[i] <= nums[j] && continue
        if dp[i] <= dp[j]
            dp[i] = dp[j] + 1
            cnt[i] = cnt[j]
        elseif dp[i] == dp[j] + 1
            cnt[i] += cnt[j]
        end
    end
    max_len = maximum(dp)
    return sum(c for (idx, c) in enumerate(cnt) if dp[idx] == max_len)
end
## @lc code=end


println("Test misc")

@testset "misc" begin
  @testset "wsample_logprob" begin
    p = [.3, .5, .2]
    logp = log.(p)

    Random.seed!(1234)
    x = wsample_logprob(logp, 100_000)
    @test isapprox(mean(x .== 1), p[1], atol=1e-2)
    @test isapprox(mean(x .== 2), p[2], atol=1e-2)
    @test isapprox(mean(x .== 3), p[3], atol=1e-2)
  end

  @testset "quantiles" begin
    Random.seed!(0)
    X = randn(1_000_000, 5)
    @test all(isapprox.(quantiles(X, .025, dims=1), -1.96, atol=1e-2))
    @test all(isapprox.(quantiles(X, .975, dims=1), 1.96, atol=1e-2))
  end
end


macro forwardrule(x)
    :(sqrt(eps(eltype($x))) * max(one(eltype($x)), abs($x)))
end

macro centralrule(x)
    :(cbrt(eps(eltype($x))) * max(one(eltype($x)), abs($x)))
end

macro hessianrule(x)
    :(eps(eltype($x))^(1//4) * max(one(eltype($x)), abs($x)))
end

macro complexrule(x)
    :(eps($x))
end


using ConvDiffMIPDECO
using jInv.Mesh
using jInv.ForwardShare
using jInv.InverseSolve
using jInv.LinearSolvers
using MUMPSjInv
using MAT
using LinearAlgebra
using SparseArrays

# filename= "2DmodelLShaped.mat"
filename= joinpath(dirname(pathof(ConvDiffMIPDECO)),"..","examples","Sources3D")
file = matread(filename * ".mat")

# problem description
domain = file["domain"]
mf     = round.(Int64,file["m"])
v      = file["v"]
sig    = file["sig"]

# data and receiver location
dtrue  = file["dtrue"]
rec    = file["rec"]

Mfine = getRegularMesh(domain,mf);
x1c,x2c,x3c = getCellCenteredAxes(Mfine)
rec3D = [kron(ones(length(x3c)),rec) kron(x3c,ones(size(rec,1)))]

# set up experiments
Meshes       = [ getRegularMesh(domain,[96 48 48]);];
alphas       = exp10.(range(0,stop=-6,length=30))
regularizersName = ["wTVReg"];
regularizers = [wTVReg];
noiseLevel = 0.1;

for mk = 1:length(Meshes)
	M      = Meshes[mk]
	x1,x2,x3 = getNodalAxes(M)
	P = interpmat(x1,x2,x3, rec3D);
	for rk = 1:length(regularizers)
		regFun= regularizers[rk]
		regName= regularizersName[rk]

		# build linear interpolation matrix from nodes to receiver locations

		dnoise = randn(size(dtrue));
		dnoise /= norm(dnoise)
		dnoise *= noiseLevel*norm(dtrue)
		dobs = dtrue + dnoise

		pFor = getConvDiffFEMParam(M,v=v,sig=sig,P=P,Ainv=getMUMPSsolver());

		## configure misfit
		Wt         = ones(size(dobs))/sqrt(mf[3])
		sigback    = 0.0
		pMis       = getMisfitParam(pFor,Wt,dobs,SSDFun)

		## Configure regularization
		mref       = zeros(M.nc)
		alpha      = alphas[1]
		reg        = (m,mr,M,I=1.0) -> regFun(m,mr,M,eps=1e-8)
		# reg        = (m,mr,M,I=1.0) -> wTVReg(m,mr,M,eps=1e-8)

		## Configure optimization
		maxIter    = 20
		minUpdate  = 1e-3
		HesPrec    = getSSORRegularizationPreconditioner(1.0,1e-15,50)
		cgit       = 5
		pcgTol     = 1e-1
		modFun     = identityMod
		boundsLow  = 0*ones(M.nc)
		boundsHigh = 1*ones(M.nc)
		maxStep	   = 0.1*maximum(boundsHigh)

		## store the configuration
		pInv       = getInverseParam(M,modFun,reg,alpha,mref,
		                             boundsLow,boundsHigh,maxStep=maxStep,
		                            pcgMaxIter=cgit,pcgTol=pcgTol,minUpdate=minUpdate,maxIter=maxIter,
		                            HesPrec=HesPrec);

		mc = mref.+0.1

		Mis    = zeros(length(alphas))
		Reg    = zeros(length(alphas))
		Fields = zeros(size(P,2),length(alphas))
		Sources = zeros(length(mc),length(alphas))
	    PredictedData   = zeros(size(P,1),length(alphas))

		nPDEsolves = 0;

		runtimeLcurve = @elapsed begin
			for ak = 1:length(alphas)
				pInv.alpha = alphas[ak]
				println("\n\n--- Inversion on $(M.n) grid with $(regName) and alpha=$(pInv.alpha) --- ")
				# mc = max.(.01,min.(.99,mc))
				mc,Dc,flag,His = projGNCG(mc,pInv,pMis)
				uc = pFor.Fields;          # u variables
				wr = pInv.modelfun(mc)[1]; # w variables

				Fields[:,ak] = uc;
				Sources[:,ak] = wr
				PredictedData[:,ak] = Dc

				iter = findlast(His.F .> 0)
				Mis[ak] = His.F[iter]
				Reg[ak] = His.Rc[iter]/alphas[ak]
		    end
		end
		println("no. PDE solves:\t$(pFor.Ainv.nSolve)")
		println("time PDE factor:\t$(pFor.Ainv.facTime)")
		println("time PDE solves:\t$(pFor.Ainv.solveTime)")
		println("\n overall runtime: $runtimeLcurve")

		results = Dict("n"=>M.n, "Mis"=>Mis, "Reg"=>Reg, "alphas"=> alphas,
		"noiseLevel"=>noiseLevel,"Fields"=> Fields, "Sources" => Sources,
		"PredictedData" => PredictedData, "dobs" => dobs,"runtime"=>runtimeLcurve)
		resFile = "$(filename)-$regName-noise-$(noiseLevel)-$(M.n[1])x$(M.n[2])x$(M.n[3]).mat"
		matwrite(resFile,results)
	end
end


using Plots
plotly()

using simulator.fresnelltools
using simulator.materials.spesifics

θs = 0:1e-3:π/2
θ = 0.82
λs = 300e-9:1e-9:1200e-9
λ = 797e-9
ds = 1e-9:1e-9:100e-9
d = 53e-9

n_gold = Au
n_fiber = 1.4
n_air = 1

R_te(λ, θ, d) = abs((ThreeLayerSystem(n_fiber, n_gold, n_air, λ, θ, d)[1] * [1, 0])[2])^2
R_tm(λ, θ, d) = abs((ThreeLayerSystem(n_fiber, n_gold, n_air, λ, θ, d)[2] * [1, 0])[2])^2
R_ratio(λ, θ, d) = R_te(λ, θ, d) / R_tm(λ, θ, d)

function Inside(λ, θ, d)
    border_1_te, border_1_tm, θg = FresnellBoundrary(n_fiber, n_gold(λ), θ)
    border_2_te, border_2_tm, θa = FresnellBoundrary(n_gold(λ), n_air, θg)

    bulk_fiber(d) = FresnellSlab(n_fiber, 2*π/λ, d, θ)
    bulk_gold(d) = FresnellSlab(n_gold(λ), 2*π/λ, d, θg)
    bulk_air(d) = FresnellSlab(n_air, 2*π/λ, d, θa)

    border_1_te_f(d) = border_1_te
    border_1_tm_f(d) = border_1_tm
    border_2_te_f(d) = border_2_te
    border_2_tm_f(d) = border_2_tm

    search = (Up, Un) -> abs(Up + Un)^2

    u_te, d_te = Interrogator(
        [bulk_fiber, border_1_te_f, bulk_gold, border_2_te_f, bulk_air],
        [20e-9, 0, d, 0, 20e-9],
        1e-10,
        search)

    u_tm, d_tm = Interrogator(
        [bulk_fiber, border_1_tm_f, bulk_gold, border_2_tm_f, bulk_air],
        [20e-9, 0, d, 0, 20e-9],
        1e-10,
        search)

    d_te, u_te, u_tm
end

# plot(λs, R_ratio.(λs, θ, d), label="θ=0.856", title="TE Reflection / TM Reflection at d=53nm")
# plot(θs, (θ) -> (R_ratio.(λ, θ + 1e-9im, d)))
# plot(ds, R_ratio.(λ, θ, ds))

# plot(λs, R_te.(λs, θ, d), label="te", title="Reflection at θ=0.82, d=53nm", legend=:bottomright)
# plot!(λs, R_tm.(λs, θ, d), label="tm", xlabel="wavelength", ylabel="reflected power")

# ds, u_te, u_tm = Inside(650e-9, θ, d)
# ds = ds .* 1e9
# _, u_tep, u_tmp = Inside(786e-9, θ, d)
#
# plot(ds, u_te, label="te normal", title="Gold thin film, power distribution",
#     xlabel="distance in nm", ylabel="all propagating power",
#     legend=:topleft)
# plot!(ds, u_tm, label="tm normal")
# plot!(ds, u_tep, label="te plasmone")
# plot!(ds, u_tmp, label="tm plasmone")


const l2 = 0.6931471805599453 # log(2)

function vpcprior(m::Float64, v::Float64, a::Float64, b::Float64, λ::Float64)
  d = digamma(a+b)-m*digamma(a)-(1-m)*digamma(b)

  z = log(λ)-
    λ*d^0.5+
    log(m*(1-m)*abs(trigamma(a)*m^2+trigamma(b)*(1-m)^2-trigamma(a+b)))-
    (l2+2*log(v)+0.5*log(d))

  return z
end

logdvdw(w::Float64, m::Float64) = w-2*log(1+exp(w))+log(m*(1-m))