using Base.Test, MathProgComplex, OPFInstances

## Application of the Moment–SOS Approach to Global Optimization of the OPF Problem
# C. Josz, J. Maeght, P. Panciatici, and J. Ch. Gilbert
# https://arxiv.org/pdf/1311.6370.pdf
# Data from table 3, p. 13.

testfolder = joinpath(Pkg.dir("MathProgComplex"), "Mosek_runs", "tests", "sos_example5")

@testset "SDP hierarchy ACOPFs relaxations - sparse" begin
    @testset "$pbsolved tests" for pbsolved in [:SOSRelaxation, :MomentRelaxation]

        @testset "WB5 real formulation - order 1 - two cliques" begin
            instance = "WB5"

            problem_c, point = import_from_dat(getinstancepath("Matpower", "QCQP", instance))
            problem = pb_cplx2real(problem_c)

            logpath = joinpath(testfolder, "$instance")

            relax_ctx = set_relaxation(problem; hierarchykind=:Real,
                                                d = 1,
                                                issparse = true,
                                                params = Dict(:opt_outlev=>1,
                                                            :opt_logpath=>logpath,
                                                            :opt_solver=>testsolver,
                                                            :opt_pbsolved=>pbsolved))

            max_cliques = get_WB5cliques(relax_ctx, problem)
            @assert length(max_cliques) == 2

            primobj, dualobj = run_hierarchy(problem, relax_ctx, max_cliques=max_cliques)

            ε_rel = OPFsols[("WB5", 1)].rel_opt_gap
            ε_abs = max(primobj, dualobj) * ε_rel

            @show primobj, dualobj,  OPFsols[("WB5", 1)].primal_solvalue, ε_abs

            @test primobj ≈ OPFsols[("WB5", 1)].primal_solvalue atol=ε_abs
            @test dualobj ≈ primobj atol=ε_abs
        end

        @testset "case9 real formulation - order $i - three cliques" for i in 1:1
            instance = "case9"

            problem_c, point = import_from_dat(getinstancepath("Matpower", "QCQP", instance))
            problem = pb_cplx2real(problem_c)

            logpath = joinpath(testfolder, "$instance")

            relax_ctx = set_relaxation(problem; hierarchykind=:Real,
                                                d = i,
                                                issparse = true,
                                                params = Dict(:opt_outlev=>1,
                                                            :opt_logpath=>logpath,
                                                            :opt_solver=>testsolver,
                                                            :opt_pbsolved=>pbsolved))

            max_cliques = get_case9cliques(relax_ctx, problem)
            @assert length(max_cliques) == 3

            primobj, dualobj = run_hierarchy(problem, relax_ctx, save_pbs=false,
                                                                max_cliques=max_cliques);

            ε_rel = OPFsols[("case9", i)].rel_opt_gap
            ε_abs = max(primobj, dualobj) * ε_rel

            @show primobj, dualobj,  OPFsols[("case9", i)].primal_solvalue, ε_abs

            @test primobj ≈ OPFsols[("case9", i)].primal_solvalue atol=ε_abs
            @test dualobj ≈ primobj atol=ε_abs
        end
    end
end


###########################################################################################################################################################
# Declaring parameter and model descriptives
# Call functions to solve the model
#IMPORTANT: The model needs ONE steady state parameter for each variable
using DsgeSolves
# Set Seed
srand(1234)
include(joinpath(Pkg.dir("DsgeSolves"),"src","projection_method.jl"))

# Define the exogenous variables and the calibrated parameters
const Rstar = 0.01
const PFstar = 1.0
const PXstar = 1.0
const eta = 1.5
const omega = 0.25
const beta = 0.990099009901
const alpha1 = 0.15
const kappa = 0.1
const chis = 0.1
const phi0 = 0.9
const phi1 = 1.5
const Θ_s = [Rstar PFstar PXstar eta omega beta alpha1 kappa chis phi0 phi1]

# Reduced form parameters
const ρ = 0.9
const σ = 0.01
const Θ_rf = [ρ σ]

# Set out the initial steady-state values
const Blam_ss = 3.22696890071349
const C_ss = 0.45793462256871
const F_ss = 0.0
const G_ss = 0.0
const K_ss = 0.02729049017873
const L_ss = 0.74732008502373
const P_ss = 0.99999998539410
const Pk_ss = 1.0000
const R_ss = 0.0100
const S_ss = 0.99999999315490
const W_ss = 0.28812562001981
const X_ss = 0.02729049020372
const Y_ss = 0.48522511277243
const Z_ss = 1.0
const Θ_ss = [Blam_ss C_ss F_ss G_ss K_ss L_ss P_ss Pk_ss R_ss S_ss W_ss X_ss Y_ss Z_ss]

# Aproximating function
const State = 3 # number state variables
const Euler = 2  # number euler equations
const Neuron = 1 # number of neurons
const Neuronx1 = Neuron + 1
const Param = State*Euler*Neuron # number of parameters for the Neural Network function
const Start = 4
const Other = 4

# Create the shocks
const T = 1000 # length of simulated data
const K = 300  # number of simulations
const B = Int(ceil(T*0.25)) #burn-in
# const shock = initialize_shock(T,K,σ)

const Θ = Parameter(Θ_s,Θ_rf,Θ_ss)
const vDes = Descriptive(T, K, B, State, Euler, Neuron, Neuronx1, Param, Start,Other)
const d1 = Normal(0,σ)
const m = Model(Θ,vDes,d1)

gammaf = [2.8342 -1.6406 -0.0868 0.5264 -0.0000 3.0591] # starting values
model = optimize(minimize_euler_error!, vec(gammaf), BFGS())

# Evaluate the model for the bestσlected parameters
vParam_star = reshape(Optim.minimizer(model),size(gammaf))
m = evaluate_model!(vParam_star,m)

# Model assessment
irf = plot_irf!(m,0.1,50,25)
jg = judd_gaspar_stat(m) # I AM NOT SURE THIS IS CORRECT
DM = haan_marcet_statistics(m) # I AM NOT SURE THIS IS CORRECT

# @unpack endo, state_endo, euler_error, other, shock = m.Var
# (Blam,C,F,G,κ,L,P,Pk,R,S,W,X,Y,Z) = endo

# plot


################################################################################
#                      TEST: Compose Rotations using DCMs
################################################################################

for i = 1:samples
    # Sample 8 DCMs.
    D1 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D2 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D3 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D4 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D5 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D6 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D7 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    D8 = angle2dcm(EulerAngles(-pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               -pi + 2*pi*rand(),
                               rand(rot_seq_array)))

    # Test the function `compose_rotation`.
    @test                   D2*D1 ≈ compose_rotation(D1,D2)                   atol=1e-14
    @test                D3*D2*D1 ≈ compose_rotation(D1,D2,D3)                atol=1e-14
    @test             D4*D3*D2*D1 ≈ compose_rotation(D1,D2,D3,D4)             atol=1e-14
    @test          D5*D4*D3*D2*D1 ≈ compose_rotation(D1,D2,D3,D4,D5)          atol=1e-14
    @test       D6*D5*D4*D3*D2*D1 ≈ compose_rotation(D1,D2,D3,D4,D5,D6)       atol=1e-14
    @test    D7*D6*D5*D4*D3*D2*D1 ≈ compose_rotation(D1,D2,D3,D4,D5,D6,D7)    atol=1e-14
    @test D8*D7*D6*D5*D4*D3*D2*D1 ≈ compose_rotation(D1,D2,D3,D4,D5,D6,D7,D8) atol=1e-14

end


function Ground(;name)
    @named g = Pin()
    eqs = [g.v ~ 0]
    ODESystem(eqs, t, [], [], systems=[g], name=name)
end

function Resistor(;name, R = 1.0)
    val = R

    @named p = Pin()
    @named n = Pin()
    @parameters R
    @variables v(t)

    eqs = [
           v ~ p.v - n.v
           0 ~ p.i + n.i
           v ~ p.i * R
          ]
    ODESystem(eqs, t, [v], [R], systems=[p, n], defaults=Dict(R => val), name=name)
end

function Capacitor(; name, C = 1.0)
    val = C

    @named p = Pin()
    @named n = Pin()
    @parameters C
    @variables v(t)

    D = Differential(t)
    eqs = [
           v ~ p.v - n.v
           0 ~ p.i + n.i
           D(v) ~ p.i / C
          ]
    ODESystem(eqs, t, [v], [C], systems=[p, n], defaults=Dict(C => val), name=name)
end

function Inductor(; name, L = 1.0)
    val = L

    @named p = Pin()
    @named n = Pin()
    @parameters L
    @variables v(t)

    D = Differential(t)
    eqs = [
           v ~ p.v - n.v
           0 ~ p.i + n.i
           D(p.i) ~ v / L
          ]
    ODESystem(eqs, t, [v], [L], systems=[p, n], defaults=Dict(L => val), name=name)
end

function IdealOpAmp(; name)
    @named p1 = Pin()
    @named p2 = Pin()
    @named n1 = Pin()
    @named n2 = Pin()
    @variables v1(t) v2(t) # u"v"
    @variables i1(t) i2(t) # u"A"

    eqs = [
        v1 ~ p1.v - n1.v
        v2 ~ p2.v - n2.v
        0 ~ p1.i + n1.i
        0 ~ p2.i + n2.i
        i1 ~ p1.i
        i2 ~ p2.i
        v1 ~ 0
        i1 ~ 0
    ]
    ODESystem(eqs, t, [i1, i2, v1, v2], [], systems=[p1, p2, n1, n2], name=name)
end


using Distributions
using GaussianProcesses
using Plots, StatPlots
using LaTeXStrings
#  include("X3_empirical.jl")
include("utilities.jl")
include("self_confidence.jl")

function annotate_pct(coords::Tuple{Float64,Float64},txt::Plots.PlotText,plt::Plots.Plot)
    xmin = plt.subplots[1].attr[:xaxis][:extrema].emin
    xmax = plt.subplots[1].attr[:xaxis][:extrema].emax
    ymin = plt.subplots[1].attr[:yaxis][:extrema].emin
    ymax = plt.subplots[1].attr[:yaxis][:extrema].emax
    #  println("$xmin, $xmax, $ymin, $ymax")
    Plots.annotate!(coords[1]*(xmax-xmin)+xmin,coords[2]*(ymax-ymin)+ymin,txt)
end

srand(12345)
trusted_n=5;                          #number of training points
trusted_x = squeeze([0. 1. rand(1,trusted_n-2)],1);              #predictors
#  trusted_y = ones(trusted_n);    #regressors
trusted_y = zeros(trusted_n);    #regressors

candidate_n=5;
candidate_x = squeeze([0. 1. 0.1464466 rand(1,candidate_n-3)],1);              #predictors
#  candidate_y = (candidate_x-0.5).^2 - 0.125 + 1.;    #regressors
candidate_y = (candidate_x-0.5).^2 - 0.125;    #regressors

#Select mean and covariance function
#  mZero = MeanConst(1.0)
mZero = MeanZero()
kern = SE(-0.5,0.0)                   #Sqaured exponential kernel (note that hyperparameters are on the log scale)

logObsNoise_trust = -4.0                        # log standard deviation of observation noise (this is optional)
logObsNoise_cand = -4.0                        # log standard deviation of observation noise (this is optional)
trusted_gp = GP(trusted_x,trusted_y,mZero,kern,logObsNoise_trust)       #Fit the GP
candidate_gp = GP(candidate_x,candidate_y,mZero,kern,logObsNoise_cand)       #Fit the GP

#  points_of_interest_x = [0.150,0.5,0.84,0.985]
points_of_interest_x = [0.15,0.5,0.84,0.985]
#  points_of_interest_y = [1.,0.89,1.04,1.116]
points_of_interest_y = [0.,-0.125,0.03,0.116]

μ_trust, σ²_trust = predict_y(trusted_gp,points_of_interest_x);
μ_cand, σ²_cand = predict_y(candidate_gp,points_of_interest_x);

dist_trust = []
for (m,s) in zip(μ_trust,sqrt.(σ²_trust))
    push!(dist_trust,Normal(m,s))
end
dist_cand = []
for (m,s) in zip(μ_cand,sqrt.(σ²_cand))
    push!(dist_cand,Normal(m,s))
end

SQ = []
SQ_raw = []
solver_reward_range = [[-0.,5.], [-0.,0.5], [-0.,0.05], [-0.,0.005]]
for (c,t) in zip(dist_cand,dist_trust)

    sqs = []
    sq_raw = []
    for i = 1:length(solver_reward_range)
        println("#################################")
        println("i: $i")
        (sq,sqr) = X3(c,t,global_rwd_range=solver_reward_range[i],return_raw_sq=true)
        println("using $(solver_reward_range[i]) reward range, got $sq")
        push!(sqs,sq)
        push!(sq_raw,sqr)
    end
    push!(SQ,sqs)
    push!(SQ_raw,sq_raw)
end

show_obs = false
beta_val = 0.999

Plots.scalefontsizes(1.0)

#  p1 = Plots.plot(trusted_gp,β=beta_val,linecolor=:blue,fillcolor=:blue,lw=3,obsv=show_obs,fillalpha=0.2,label=L"test",legend=:topleft)
#  Plots.plot!(candidate_gp,β=beta_val,linecolor=:red,fillcolor=:red,lw=2,obsv=show_obs,fillalpha=0.2,label=L"test2")
p1 = Plots.plot(trusted_gp,β=beta_val,linecolor=:blue,fillcolor=:blue,lw=3,obsv=show_obs,fillalpha=0.2,label=L"trusted $\mathcal{S}^T$",legend=:topleft)
Plots.plot!(candidate_gp,β=beta_val,linecolor=:red,fillcolor=:red,lw=2,obsv=show_obs,fillalpha=0.2,label=L"candidate $\mathcal{S}^C$")
Plots.scatter!(points_of_interest_x,points_of_interest_y,marker=:star,markersize=10,label="points of interest")
xlabel!("task parameter")
ylabel!("reward")
for i = 1:length(points_of_interest_x)
    point_label = '@' + i
    Plots.annotate!(points_of_interest_x[i],points_of_interest_y[i],Plots.text("$point_label",:black,:right,:bottom))
end

p2 = Plots.plot(dist_trust[1],linecolor=:blue,fill=(0,0.2,:blue),legend=false,title="A")
Plots.plot!(dist_cand[1],linecolor=:red,fill=(0,0.2,:red))
annotate_pct((0.65,0.8),Plots.text(LaTeXString(@sprintf("\$x_Q(r=5):%0.3f\$\n\$x_Q(r=0.5):%0.3f\$\n\$x_Q(r=0.05):%0.3f\$\n\$x_Q(r=0.005)%0.3f\$",SQ[1][1],SQ[1][2],SQ[1][3],SQ[1][4])),14,:center,:left),p2)
#  annotate_pct((0.65,0.8),Plots.text(LaTeXString(@sprintf("\$x^2\$")),14,:center,:left),p2)
xlabel!("reward")
ylabel!("p(rwd)")
#  annotate_pct((0.8,0.8),text("SQ: $(@sprintf("%0.3f",SQ[1][1]))",:center,:center),p2)
#  annotate!(0.07,10.,text(L"SQ\rightarrow High?",:black,:center))

p3 = Plots.plot(dist_trust[2],linecolor=:blue,fill=(0,0.2,:blue),legend=false,title="B")
Plots.plot!(dist_cand[2],linecolor=:red,fill=(0,0.2,:red))
annotate_pct((0.15,0.8),Plots.text(LaTeXString(@sprintf("\$x_Q(r=5):%0.3f\$\n\$x_Q(r=0.5):%0.3f\$\n\$x_Q(r=0.05):%0.3f\$\n\$x_Q(r=0.005)%0.3f\$",SQ[2][1],SQ[2][2],SQ[2][3],SQ[2][4])),14,:center,:left),p3)
xlabel!("reward")
ylabel!("p(rwd)")
#  annotate_pct((0.8,0.8),text("SQ: $(@sprintf("%0.3f",SQ[2][1]))",:center,:center),p3)
#  annotate!(-0.15,12.,text(L"SQ\rightarrow Very Low",:black,:center))

p4 = Plots.plot(dist_trust[3],linecolor=:blue,fill=(0,0.2,:blue),legend=false,title="C")
Plots.plot!(dist_cand[3],linecolor=:red,fill=(0,0.2,:red))
annotate_pct((0.65,0.8),Plots.text(LaTeXString(@sprintf("\$x_Q(r=5):%0.3f\$\n\$x_Q(r=0.5):%0.3f\$\n\$x_Q(r=0.05):%0.3f\$\n\$x_Q(r=0.005)%0.3f\$",SQ[3][1],SQ[3][2],SQ[3][3],SQ[3][4])),14,:center,:left),p4)
xlabel!("reward")
ylabel!("p(rwd)")
#  annotate_pct((0.8,0.8),text("SQ: $(@sprintf("%0.3f",SQ[3][1]))",:center,:center),p4)
#  annotate!(0.15,12.,text(L"SQ\rightarrow Low?",:black,:center))

p5 = Plots.plot(dist_trust[4],linecolor=:blue,fill=(0,0.2,:blue),legend=false,title="D")
Plots.plot!(dist_cand[4],linecolor=:red,fill=(0,0.2,:red))
annotate_pct((0.65,0.8),Plots.text(LaTeXString(@sprintf("\$x_Q(r=5):%0.3f\$\n\$x_Q(r=0.5):%0.3f\$\n\$x_Q(r=0.05):%0.3f\$\n\$x_Q(r=0.005)%0.3f\$",SQ[4][1],SQ[4][2],SQ[4][3],SQ[4][4])),14,:center,:left),p5)
#  annotate_pct((0.65,0.8),Plots.text(@sprintf("SQ(r=5):%0.3f\nSQ(r=0.5):%0.3f\nSQ(r=0.05):%0.3f\nSQ(r=0.005)%0.3f",SQ[4][1],SQ[4][2],SQ[4][3],SQ[4][4]),14,:center,:left),p5)
xlabel!("reward")
ylabel!("p(rwd)")
#  annotate_pct((0.8,0.8),text("SQ: $(@sprintf("%0.3f",SQ[4][1]))",:center,:center),p5)
#  annotate!(0.15,12.,text(L"SQ\rightarrow Very High",:black,:center))

# empty filler frames
pe1 = Plots.plot(framestyle = :none)
annotate!(0.1,0.5,text("Solver Quality:\n  0->Poor Quality\n  1-> Equal to Trusted\n  2->Better than Trusted\nThink of as fraction of quality\nthresholded at 2",10,:center,:left))
#  annotate!(0.,0.,text("empty",:black,:center))
pe2 = Plots.plot(framestyle = :none)
Plots.annotate!(0.1,0.65,Plots.text("* Calculate mean difference weighted by std",10,:center,:left))
Plots.annotate!(0.5,0.5,Plots.text(L"wd=\frac{m(c)/s(c)-m(t)/s(t)}{range(rwd)}",10,:center,:center))
Plots.annotate!(0.1,0.3,Plots.text("* SQ is a scaled Sigmoid on wd",10,:center,:left))
Plots.annotate!(0.5,0.15,Plots.text(L"SQ=\frac{2exp(wd)}{(exp(wd)+1)}",10,:center,:center))
#  annotate!(0.,0.,text("empty",:black,:center))
pe3 = Plots.plot(framestyle = :none)
Plots.annotate!(0.1,0.65,Plots.text("* In these plots:",10,:center,:left))
Plots.annotate!(0.5,0.5,Plots.text(LaTeXString(@sprintf("range(rwd) = %s",solver_reward_range)),10,:center,:center))
#  annotate!(0.,0.,text("empty",:black,:center))

p1notes = Plots.plot(framestyle = :none)
Plots.annotate!(0.5,0.5,Plots.text(@sprintf("SQ: %0.3f\n%0.3f\n%0.3f\n%0.3f",SQ[1][1],SQ[1][2],SQ[1][3],SQ[1][4]),10,:center,:center))

p2notes = Plots.plot(framestyle = :none)
Plots.annotate!(0.5,0.5,Plots.text("SQ: $(@sprintf("%0.3f",SQ[2][1]))",10,:center,:center))

p3notes = Plots.plot(framestyle = :none)
Plots.annotate!(0.5,0.5,Plots.text("SQ: $(@sprintf("%0.3f",SQ[3][1]))",10,:center,:center))

p4notes = Plots.plot(framestyle = :none)
Plots.annotate!(0.5,0.5,Plots.text("SQ: $(@sprintf("%0.3f",SQ[4][1]))",10,:center,:center))

savefig(p1,"figs/p1.pdf")
savefig(p2,"figs/p2.pdf")
savefig(p3,"figs/p3.pdf")
savefig(p4,"figs/p4.pdf")
savefig(p5,"figs/p5.pdf")
paper_layout = Plots.plot(p2,p3,p4,p5,size=(1000,800))
savefig(paper_layout,"figs/point_compare.pdf")
Plots.plot(p1,p2,pe1,p3,pe2,p4,pe3,p5,layout=(4,2),size=(800,800))


import NonUniformRandomVariateGeneration.samplePoisson
import Random.GLOBAL_RNG

## linear is essentially an implementation of the Huber (2016) algorithm
"""
    linear(f::F, C::Float64, ϵ::Float64, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```C >= 0```
  - ```ϵ ∈ (0,1)``` satisfying C*p < 1-ϵ.
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(C*p) variate
  - ```flips``` is the number of calls of f()
"""
function linear(f::F, C::Float64, ϵ::Float64, rng::RNG=GLOBAL_RNG) where
  {F<:Function, RNG<:AbstractRNG}
  ϵ <= 0.0 || ϵ >= 1.0 && throw(DomainError(ϵ, "ϵ must be in (0,1)"))
  if C > 1.0
    return _huber2016(f, C, ϵ, rng)
  end
  if rand(rng) < C
    return f(), 1
  else
    return false, 0
  end
end

## inverse is an implementation of Algorithm 6 of
## Lee, A., Doucet, A. and Łatuszyński, K., 2014. Perfect simulation using
## atomic regeneration with application to Sequential Monte Carlo.
## arXiv:1407.5770
"""
    inverse(f::F, C::Float64, ϵ::Float64, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```C ∈ [0,p)```
  - ```ϵ ∈ (0,1)``` satisfying p > C + ϵ.
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(C/p) variate
  - ```flips``` is the number of calls of f()
"""
function inverse(f::F, C::Float64, ϵ::Float64, rng::RNG=GLOBAL_RNG) where
  {F<:Function, RNG<:AbstractRNG}

  (C < 0.0 || C >= 1.0) && throw(DomainError(C, "C must be in [0,1)"))
  ϵ <= 0.0 && throw(DomainError(ϵ, "ϵ must be positive"))
  C+ϵ > 1.0 && throw(DomainError(C+ϵ, "C+ϵ cannot exceed 1.0"))

  fq = !f
  Cprime = 1.0/(1.0-C)
  ϵprime = ϵ/(1.0-C)
  flips::Int64 = 0
  k::Int64 = _sampleGeometric(C, rng)
  i::Int64 = 1
  while i < k
    X, newFlips = linear(fq, Cprime, ϵprime, rng)
    flips += newFlips
    if !X
      return false, flips
    end
    i += 1
  end
  return true, flips
end

## use Mendo's power algorithm
"""
    power(f::F, a::Float64, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```a >= 0```
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(p^a) variate
  - ```flips``` is the number of calls of f()
"""
function power(f::F, a::Float64, rng::RNG=GLOBAL_RNG) where
  {F<:Function, RNG<:AbstractRNG}
  a < 0.0 && throw(DomainError(a, "a cannot be negative"))
  a == 0.0 && return true, 0
  a < 1.0 && return _mendoPower(f, a, rng)
  a == 1.0 && return f(), 1
  k = ceil(Int64, a)
  flips::Int64 = 0
  for i in 1:k
    X, newFlips = _mendoPower(f, a/k, rng)
    flips += newFlips
    !X && return false, flips
  end
  return true, flips
end

## use Mendo's power algorithm
"""
    sqrt(f::F, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(sqrt(p)) variate
  - ```flips``` is the number of calls of f()
"""
function sqrt(f::F, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}
  return _mendoPower(f, 0.5, rng)
end

## expMinus is based on series expansion of exp(-λp) = exp(λ(1-p))/exp(λ),
## which is a natural extension of Wastlund's observation for λ = 1
"""
    expMinus(f::F, λ::Float64, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```λ >= 0```
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(exp(-λ*p)) variate
  - ```flips``` is the number of calls of f()
"""
function expMinus(f::F, λ::Float64, rng::RNG=GLOBAL_RNG) where
  {F<:Function, RNG<:AbstractRNG}
  λ < 0.0 && throw(DomainError(λ, "λ cannot be negative"))
  flips::Int64 = 0
  K::Int64 = samplePoisson(λ, rng)
  for i in 1:K
    if f()
      return false, flips + 1
    end
    flips += 1
  end
  return true, flips
end

## use the algorithm described in Huber (2017)
"""
    logistic(f::F, C::Float64, rng::RNG=GLOBAL_RNG) where {F<:Function, RNG<:AbstractRNG}

Input:
  - ```f()``` simulates Bernoulli(p) random variates (of type Bool)
  - ```C >= 0```
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(C*p/(1.0+C*p)) variate
  - ```flips``` is the number of calls of f()
"""
function logistic(f::F, C::Float64, rng::RNG=GLOBAL_RNG) where
  {F<:Function, RNG<:AbstractRNG}
  return _logistic(f, C, rng)
end

## described in
## Gonçalves, F.B., Łatuszyński, K.G. and Roberts, G.O., 2017. Exact Monte Carlo
## likelihood-based inference for jump-diffusion processes. arXiv:1707.00332
"""
    twocoin(f1::F1, f2::F2, C1::Float64, C2::Float64, rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function, RNG<:AbstractRNG}

Input:
  - ```f1()``` simulates Bernoulli(p1) random variates (of type Bool)
  - ```f2()``` simulates Bernoulli(p2) random variates (of type Bool)
  - ```C1, C2 >= 0```
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```X, flips```
  - ```X``` is a Bernoulli(C1*p1/(C1*p1+C2*p2)) variate
  - ```flips``` is the number of calls of f()
"""
function twocoin(f1::F1, f2::F2, C1::Float64, C2::Float64,
  rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function, RNG<:AbstractRNG}
  Cprob::Float64 = C1/(C1+C2)
  flips1::Int64 = 0
  flips2::Int64 = 0
  while true
    if rand(rng) < Cprob
      flips1 += 1
      if f1()
        return true, flips1, flips2
      end
    else
      flips2 += 1
      if f2()
        return false, flips1, flips2
      end
    end
  end
end

"""
signedEstimate(μ::F1, φ::F2, c::Float64, δ::Float64, n::Int64, rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function, RNG<:AbstractRNG} where {F1<:Function, F2<:Function, RNG<:AbstractRNG}

Input:
  - ```μ()``` simulates a random variate X
  - ```φ(X)``` is real-valued
  - ```c``` satisfies sup_x |φ(x)| <= c
  - ```δ > 0``` satisfies μ(φ) >= δ
  - ```n``` specifies a number of X variables to average
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```Y, flips, calls```
  - ```Y``` is almost surely valued in [0.0,c] with E[Y] = μ(φ)
  - ```flips``` is the number of coin tosses by the Bernoulli Factory algorithm
  - ```calls``` is the number of calls of μ() in total
"""
function signedEstimate(μ::F1, φ::F2, c::Float64, δ::Float64, n::Int64,
  rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function, RNG<:AbstractRNG}
  δ >= c && throw(DomainError("requires δ < c"))
  return _signedEstimate(μ, φ, c, δ, n, rng)
end

"""
signedEstimate(μ::F1, φ::F2, a::Float64, b::Float64, δ::Float64, c::Float64, n::Int64, rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function, RNG<:AbstractRNG}

Input:
  - ```μ()``` simulates a random variate X
  - ```φ(X)``` is real-valued
  - ```a,b,δ,c``` satisfy a <= inf_x φ(x) < b < δ <= μ(φ) < sup_x φ(x) <= c
  - ```n``` specifies a number of X variables to average
  - ```[rng]``` the RNG to used by the algorithm (does not affect f)
Output: ```Y, flips, calls```
  - ```Y``` is almost surely valued in [b, max{2b-a,c}] with E[Y] = μ(φ)
  - ```flips``` is the number of coin tosses by the Bernoulli Factory algorithm
  - ```calls``` is the number of calls of μ() in total
"""
function signedEstimate(μ::F1, φ::F2, a::Float64, b::Float64, δ::Float64,
  c::Float64, n::Int64, rng::RNG=GLOBAL_RNG) where {F1<:Function, F2<:Function,
  RNG<:AbstractRNG}
  !(a < b < δ < c) && throw(DomainError("requires a < b < δ < c"))
  return _signedEstimate(μ, φ, a, b, δ, c, n, rng)
end


export solve_modelARCDiag_HO

function solve_modelARCDiag_HO(nlp_stop, PData :: PDataFact, α:: T; ho_correction :: Symbol = :Shamanskii, λfact = 1.0) where T
    # Solve the ARC subproblem once diagonalized into Δ
    # printstyled("On est dans solve_modelARCDiag ⇊ \n", color = :red)
    nlp_at_x = nlp_stop.current_state
    # Setup the problem
    # M = ones(PData.Δ)
    # M = fill(1, size(PData.Δ))
    M = ones(T, size(PData.Δ))
    # printstyled("On a M ↢ \n", color = :red)

    # ϵ = 1.0e-10 * (1.0 + PData.λ)
    ϵ1 = sqrt(eps(T)) / 100.0
    # printstyled("On a ϵ1 ↢  \n", color = :red)
    ϵ = T.(ϵ1 * (1.0 + PData.λ))
    # printstyled("On a ϵ ↢  \n", color = :red)
    λ = max(ϵ,PData.λ+ϵ)
    # printstyled("On a λ ↢  \n", color = :red)

    d̃ = -(PData.Δ+λ*M) .\ PData.g̃
    # printstyled("On a d̃ ↢  \n", color = :red)
    normd̃ = sqrt(d̃⋅d̃)
    # printstyled("On a normd̃ ↢  \n", color = :red)
    seuil = normd̃/α
    # printstyled("On a seuil ↢  \n", color = :red)

    # Solve the subproblem (Δ + λ I) d̃ = -g̃ such that λ = ||d̃||/α
    d̃,λ = solve_diag(λ,PData.Δ,PData.g̃,seuil,α,ϵ)
    # printstyled("On a d̃, λ ↢  \n", color = :red)

    PData.λ = λ
    # Transform back d̃ into d

    d = AInv(PData,d̃)

    if PData.λ <= λfact
        # println("ici ! 🍆")
        # @show λfact
        # @show ho_correction
        dHO = eval(ho_correction)(nlp_stop, PData, d, PData.g̃)
        # @show dHO
        # @show (norm(dHO) < 2.0 .* α)
        # @show ((-(nlp_at_x.gx + 0.5 .* nlp_at_x.Hx * dHO)⋅dHO) > 0.0)
        # @show nlp_at_x.gx
        # @show nlp_at_x.Hx
        # @show -(nlp_at_x.gx + 0.5 .* nlp_at_x.Hx * dHO)
        # @show (-(nlp_at_x.gx + 0.5 .* nlp_at_x.Hx * dHO)⋅dHO)

        if (norm(dHO) < 2.0 .* α) && ((-(nlp_at_x.gx + 0.5 * nlp_at_x.Hx * dHO)⋅dHO) > 0.0)
            # printstyled("on prend dHO 🐣\n", color = :green)
            return dHO, dHO, PData.λ
        else
            # printstyled("on prend d 🐲 \n", color = :green)
            return d, dHO, PData.λ
        end
    end

    return d, NaN * rand(length(d)), λ
end


module Discretisers

using FymEnvs
using LinearAlgebra
# using Debugger
include("Linearisers.jl")
using .Linearisers: get_jacobian

export Discretiser, ZeroOrderHoldDiscretiser
export discretise

abstract type Discretiser end

mutable struct ZeroOrderHoldDiscretiser <: Discretiser
    env
    nonlinear_dynamics  # f(x, u)
    x_k  # function of t
    u_k  # function of t
    ZeroOrderHoldDiscretiser(args...; kwargs...) = init!(new(), args...; kwargs...)
end

function init!(disc::ZeroOrderHoldDiscretiser, nonlinear_dynamics;
               ts=nothing, x_k=nothing, u_k=nothing, kwargs...)
    dt = ts[2] - ts[1]
    if -dt >= 0.0
        error("Invalid order of time")
    elseif ts == nothing || x_k == nothing || u_k == nothing
        error("No linearisation points")
    elseif length(size(x_k)) > 2 || length(size(u_k)) > 2
        error("Only one dimensional state and input are available")
    end
    n, m = size(x_k)[2], size(u_k)[2]
    disc.nonlinear_dynamics = nonlinear_dynamics
    disc.x_k = time_indexing(x_k, ts, dt)
    disc.u_k = time_indexing(u_k, ts, dt)

    function set_dyn(env, t)  # values at t
        # x_k, u_k: linearisation point
        Ad = env.systems["Ad"]
        Bd = env.systems["Bd"]
        cd = env.systems["cd"]
        jacob = get_jacobian(disc.nonlinear_dynamics, disc.x_k(t), disc.u_k(t))
        A = jacob[:, 1:n]
        B = jacob[:, n+1:n+m]
        c = disc.nonlinear_dynamics(disc.x_k(t), disc.u_k(t)) - A*disc.x_k(t) - B*disc.u_k(t)
        Ad.dot = - Ad.state * A
        Bd.dot = - Ad.state * B
        cd.dot = - Ad.state * c
    end
    function step(env)
        update!(env)
        # after update
        Ad = env.systems["Ad"]
        Bd = env.systems["Bd"]
        cd = env.systems["cd"]
        info = Dict(
                    "Ad" => Ad.state,
                    "Bd" => Bd.state,
                    "cd" => cd.state,
                   )
        done = time_over(env.clock)
        return done, info
    end
    disc.env = BaseEnv(;initial_time=ts[end], dt=-dt, max_t=ts[1], kwargs...)
    env = disc.env
    systems = Dict(
                   "Ad" => BaseSystem(initial_state=Matrix(I, n, n), name="Ad"),
                   "Bd" => BaseSystem(initial_state=zeros(n, m), name="Bd"),
                   "cd" => BaseSystem(initial_state=zeros(n), name="cd"),
                  )  # initial state: values at t_{i+1}
    systems!(env, systems)
    dyn!(env, set_dyn)
    step!(env, step)
    return disc
end

function time_indexing(xs, ts, dt)
    if size(xs)[1] != length(ts)
        error("Length mismatched")
    elseif dt < 0.0
        error("Invalid time step for time indexing")
    end
    func = function(t)
        idx = findfirst(x -> abs(x-t) < dt , ts)
        return xs[idx, :]
    end
    return func
end

"""
    zero_order_hold

# Equations
x_{i+1} = Ad x_i + Bd u_i + cd
where
Ad = Φ(t_{i+1}, t_i)
Bd = ∫_{t_i}^{t_{i+1}} Φ(t_{i+1}, τ) B(τ) dτ
cd = ∫_{t_i}^{t_{i+1}} Φ(t_{i+1}, τ) c(τ) dτ
"""
function discretise(disc::ZeroOrderHoldDiscretiser)
    env = disc.env
    reset!(env)

    Ad = []
    Bd = []
    cd = []
    while true
        reinit!(disc)  # need to be reinitialised for every step
        done, info = env.step()  # backward integration
        push!(Ad, info["Ad"])
        push!(Bd, info["Bd"])
        push!(cd, info["cd"])
        if done
            reverse!(Ad)
            reverse!(Bd)
            reverse!(cd)
            break
        end
    end
    return Ad, Bd, cd
end

"""
    reinit!(disc)

Re-initialise the state of disc's systems while preserving time.
"""
function reinit!(disc::ZeroOrderHoldDiscretiser)
    env = disc.env
    for name in ["Ad", "Bd", "cd"]
        env.systems[name].state = env.systems[name].initial_state
    end
end

end  # module


using ScikitLearn.CrossValidation: cross_val_score
@sk_import naive_bayes: GaussianNB

function update_weights(data, e, closest_same, closest_other, weights,
						weights_mult, m, k, max_f_vals, min_f_vals, mu_vals, tinfo)

	# Compute ReliefF weights.
	for t = 1:size(data, 2)

		# Penalty term
		penalty = sum(abs.(e[t] .- closest_same[:, t])/(max_f_vals[t] .- min_f_vals[t] .+ eps(Float64)))

		# Reward term
		reward = sum(weights_mult .* (abs.(e[t] .- closest_other[:, t])/(max_f_vals[t] .- min_f_vals[t] .+ eps(Float64))))

		# Weights update
		weights[t] = weights[t] - penalty/(m*k) + reward/(m*k)
	end

	# Get maximal ReliefF weight and compute epsilon values.
	max_weight = maximum(weights)
	eps = (max_weight .- weights)./max_weight

	### Grid search for best temperature from previous step ###

	best_tinfo = tinfo
	idx_removed_feature = -1

	for tinfo_nxt = tinfo-0.3:0.1:tinfo+0.3

		# Compute weights for next value of tinfo.
		weights_nxt = eps .- tinfo.*mu_vals

		# Rank features.
		enumerated_weights = [weights_nxt; collect(1:size(weights_nxt, 2))[:,:]']
		rank = zeros(Int64, size(weights_nxt, 2))
		s = enumerated_weights[:, sortperm(enumerated_weights[1,:], rev=true)]
		rank[convert.(Int64, s[2, :])] = 1:size(weights_nxt, 2)

		# Remove lowest ranked feature.
		msk_rm = rank .!= length(weights_nxt)

		# Perform 5-fold cross validation.
		data_filt = data[:, msk_rm]

		# Compare to current maximal CV value.
		cv_val_nxt = Statistics.mean(cross_val_score(GaussianNB(), X, y; cv=5))
		# If CV value greater than current maximal, save current
		# CV score, tinfo and feature weights.
		if cv_val_nxt > max_cv_val
			# Set current tinfo value as best.
			best_tinfo = tinfo_nxt
			# Get index of removed feature.
			idx_removed_feature = findall(msk_rm)[1][2]
		end
	end

	###########################################################

	return idx_removed_feature, best_tinfo

end

# Todo: Assign lowest rank to feature that is evaporated in next step.
#


# ------------------------------------------------------------------------------
# finite element bases
# ------------------------------------------------------------------------------

"""
Finite element basis for function spaces and test spaces
"""
abstract type AbstractBasis end

# ------------------------------------------------------------------------------
# basis types
# ------------------------------------------------------------------------------

"""
```julia
TensorBasis(
    numbernodes1d,
    numberquadraturepoints1d,
    numbercomponents,
    dimension,
    nodes1d,
    quadraturepoints1d,
    quadratureweights1d,
    interpolation1d,
    gradient1d
)
```

Tensor product basis

# Arguments:
- `numbernodes1d`:             number of nodes in 1 dimension
- `numberquadraturepoints1d`:  number of quadrature points in 1 dimension
- `numbercomponents`:          number of components
- `dimension`:                 dimension of the basis
- `nodes1d`:                   coordinates of the nodes in 1 dimension
- `quadraturepoints1d`:        coordinates of the quadrature points in 1
                                   dimension
- `quadratureweights1d`:       quadrature weights in 1 dimension
- `interpolation1d`:           interpolation matrix from nodes to quadrature
                                   points in 1 dimension
- `gradient1d`:                gradient matrix from nodes to quadrature points in
                                   1 dimension

# Returns:
- Tensor product basis object
"""
mutable struct TensorBasis <: AbstractBasis
    # data never changed
    numbernodes1d::Int
    numberquadratuepoints1d::Int
    numbercomponents::Int
    dimension::Int
    nodes1d::AbstractArray{Float64,1}
    quadraturepoints1d::AbstractArray{Float64,1}
    quadratureweights1d::AbstractArray{Float64,1}
    interpolation1d::AbstractArray{Float64,2}
    gradient1d::AbstractArray{Float64,2}
    volume::Float64
    numberelements1d::Int

    # data empty until assembled
    nodes::AbstractArray{Float64}
    quadraturepoints::AbstractArray{Float64}
    quadratureweights::AbstractArray{Float64,1}
    interpolation::AbstractArray{Float64,2}
    gradient::AbstractArray{Float64,2}
    numbermodes::Int
    modemap::AbstractArray{Int,1}
    primalnodes::AbstractArray{Int,1}
    interfacenodes::AbstractArray{Int,1}

    # inner constructor
    TensorBasis(
        numbernodes1d::Int,
        numberquadraturepoints1d::Int,
        numbercomponents::Int,
        dimension::Int,
        nodes1d::AbstractArray{Float64,1},
        quadraturepoints1d::AbstractArray{Float64,1},
        quadratureweights1d::AbstractArray{Float64,1},
        interpolation1d::AbstractArray{Float64,2},
        gradient1d::AbstractArray{Float64,2};
        numberelements1d::Int = 1,
    ) = (
        # validity checking
        if numbernodes1d < 1
            error("number of basis nodes in 1d must be at least 1") # COV_EXCL_LINE
        end;
        if numberquadraturepoints1d < 1
            error("number of quadrature points in 1d must be at least 1") # COV_EXCL_LINE
        end;
        if numbercomponents < 1
            error("number of components must be at least 1") # COV_EXCL_LINE
        end;
        if dimension < 1
            error("dimension must be at least 1") # COV_EXCL_LINE
        end;
        if length(nodes1d) != numbernodes1d
            error("must include numbernodes1d nodes") # COV_EXCL_LINE
        end;
        if length(quadraturepoints1d) != numberquadraturepoints1d
            error("must include numberquadraturepoints1d quadrature points") # COV_EXCL_LINE
        end;
        if length(quadratureweights1d) != numberquadraturepoints1d
            error("must include numberquadraturepoints1d quadrature weights") # COV_EXCL_LINE
        end;
        if size(interpolation1d) != (numberquadraturepoints1d, numbernodes1d)
            # COV_EXCL_START
            error(
                "interpolation matrix must have dimensions (numberquadraturepoints1d, numbernodes1d)",
            )
            # COV_EXCL_STOP
        end;
        if size(gradient1d) != (numberquadraturepoints1d, numbernodes1d)
            # COV_EXCL_START
            error(
                "gradient matrix must have dimensions (numberquadraturepoints1d, numbernodes1d)",
            ) # COV_EXCL_STOP
        end;

        # constructor
        new(
            numbernodes1d,
            numberquadraturepoints1d,
            numbercomponents,
            dimension,
            nodes1d,
            quadraturepoints1d,
            quadratureweights1d,
            interpolation1d,
            gradient1d,
            (maximum(nodes1d) - minimum(nodes1d))^dimension,
            numberelements1d,
        )
    )
end

# printing
# COV_EXCL_START
function Base.show(io::IO, basis::TensorBasis)
    print(
        io,
        basis.numberelements1d == 1 ? "" : "macro-element ",
        "tensor product basis:\n    numbernodes1d: ",
        basis.numbernodes1d,
        "\n    numberquadraturepoints1d: ",
        basis.numberquadratuepoints1d,
        "\n    numbercomponents: ",
        basis.numbercomponents,
    )
    if basis.numberelements1d != 1
        print(io, "\n    numberelements1d: ", basis.numberelements1d)
    end
    print(io, "\n    dimension: ", basis.dimension)
end
# COV_EXCL_STOP

"""
```julia
NonTensorBasis(
    numbernodes,
    numberquadraturepoints,
    numbercomponents,
    dimension,
    nodes,
    quadraturepoints,
    quadratureweights,
    interpolation,
    gradient
)
```
Non-tensor basis

# Arguments:
- `numbernodes`:             number of nodes
- `numberquadraturepoints`:  number of quadrature points
- `numbercomponents`:        number of components
- `dimension`:               dimension of the basis
- `nodes`:                   coordinates of the nodes
- `quadraturepoints`:        coordinates of the quadrature points
- `quadratureweights`:       quadrature weights
- `interpolation`:           interpolation matrix from nodes to quadrature points
- `gradient`:                gradient matrix from nodes to quadrature points

# Returns:
- Non-tensor product basis object
"""
mutable struct NonTensorBasis <: AbstractBasis
    # data never changed
    numbernodes::Int
    numberquadraturepoints::Int
    numbercomponents::Int
    dimension::Int
    nodes::AbstractArray{Float64}
    quadraturepoints::AbstractArray{Float64,1}
    quadratureweights::AbstractArray{Float64,1}
    interpolation::AbstractArray{Float64,2}
    gradient::AbstractArray{Float64,2}
    volume::Float64
    numbermodes::Int
    modemap::AbstractArray{Int,1}
    numberelements::Int
    primalnodes::AbstractArray{Int,1}
    interfacenodes::AbstractArray{Int,1}

    # inner constructor
    NonTensorBasis(
        numbernodes::Int,
        numberquadraturepoints::Int,
        numbercomponents::Int,
        dimension::Int,
        nodes::Int,
        quadraturepoints::AbstractArray{Float64},
        quadratureweights::AbstractArray{Float64,1},
        interpolation::AbstractArray{Float64,2},
        gradient::AbstractArray{Float64,2};
        numberelements::Int = 1,
    ) = (
        # validity checking
        if numbernodes < 1
            error("number of nodes must be at least 1") # COV_EXCL_LINE
        end;
        if numberquadraturepoints < 1
            error("number of quadrature points must be at least 1") # COV_EXCL_LINE
        end;
        if numbercomponents < 1
            error("number of components must be at least 1") # COV_EXCL_LINE
        end;
        if dimension < 1
            error("dimension must be at least 1") # COV_EXCL_LINE
        end;
        if size(nodes) != (numbernodes, dimension)
            error("must include (numbernodes, dimension) nodal coordinates") # COV_EXCL_LINE
        end;
        if length(quadraturepoints) != (numberquadraturepoints, dimension)
            error("must include (numberquadraturepoints, dimension) quadrature points") # COV_EXCL_LINE
        end;
        if length(quadratureweights) != numberquadraturepoints
            error("must include sufficient quadrature weights") # COV_EXCL_LINE
        end;
        if size(interpolation) != (numberquadraturepoints, numbernodes)
            # COV_EXCL_START
            error(
                "interpolation matrix must have dimensions (numberquadraturepoints, numbernodes)",
            )
            # COV_EXCL_STOP
        end;
        if size(gradient) != (q * dimension, numbernodes)
            # COV_EXCL_START
            error(
                "gradient matrix must have dimensions (numberquadraturepoints*dimension, numbernodes)",
            )
            # COV_EXCL_STOP
        end;
        if length(modemap) != numbercomponents * numbernodes
            error("must map the modes for each basis node") # COV_EXCL_LINE
        end;

        # compute volume
        volume = 1;
        for d = 1:dimension
            volume *= (maximum(nodes[:, d]) - minimum(nodes[:, d]))
        end;

        # constructor
        new(
            numbernodes,
            numberquadraturepoints,
            numbercomponents,
            dimension,
            nodes,
            quadraturepoints,
            quadratureweights,
            interpolation,
            gradient,
            volume,
            maximum(modemap),
            modemap,
            numberelements,
        )
    )
end

# printing
# COV_EXCL_START
function Base.show(io::IO, basis::NonTensorBasis)
    print(
        io,
        basis.numberelements == 1 ? "" : "macro-element ",
        "non-tensor product basis:\n    numbernodes: ",
        basis.numbernodes,
        "\n    numberquadraturepoints: ",
        basis.numberquadratuepoints,
        "\n    numbercomponents: ",
        basis.numbercomponents,
    )
    if basis.numberelements != 1
        print(io, "\n    numberelements: ", basis.numberelements)
    end
    print(io, "\n    dimension: ", basis.dimension)
end
# COV_EXCL_STOP

# ------------------------------------------------------------------------------
# basis properties
# ------------------------------------------------------------------------------

"""
```julia
getnumbernodes(basis)
```

Get the number of nodes for the basis

# Arguments:
- `basis`:  basis to compute number of nodes

# Returns:
- Integer number of basis nodes

# Example:
```jldoctest
# get number of nodes for basis
basis = TensorH1LagrangeBasis(4, 3, 2, 2);

# note: either syntax works
numbernodes = LFAToolkit.getnumbernodes(basis);
numbernodes = basis.numbernodes;

# verify
@assert numbernodes == 4^2

# output

```
"""
function getnumbernodes(basis::TensorBasis)
    return basis.numbernodes1d^basis.dimension
end

"""
```julia
getnodes(basis)
```

Get nodes for basis

# Returns:
- Basis nodes array

# Arguments:
- `basis`:  basis to compute nodes

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get basis quadrature weights
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    nodes = LFAToolkit.getnodes(basis);
    nodes = basis.nodes;

    # verify
    truenodes1d = [-1, -√(1/5), √(1/5), 1];
    truenodes = [];
    if dimension == 1
        truenodes = truenodes1d;
    elseif dimension == 2
        truenodes =
            transpose(hcat([[[x, y] for x in truenodes1d, y in truenodes1d]...]...));
    elseif dimension == 3
        truenodes = transpose(hcat([[
            [x, y, z] for x in truenodes1d, y in truenodes1d, z in truenodes1d
        ]...]...));
    end

    @assert truenodes ≈ nodes
end

# output

```
"""
function getnodes(basis::TensorBasis)
    # assembled if needed
    if !isdefined(basis, :nodes)
        nodes = []
        if basis.dimension == 1
            # 1D
            nodes = basis.nodes1d
        elseif basis.dimension == 2
            # 2D
            nodes =
                transpose(hcat([[[x, y] for x in basis.nodes1d, y in basis.nodes1d]...]...))
        elseif basis.dimension == 3
            # 3D
            nodes = transpose(
                hcat(
                    [
                        [
                            [x, y, z] for x in basis.nodes1d, y in basis.nodes1d,
                            z in basis.nodes1d
                        ]...,
                    ]...,
                ),
            )
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        basis.nodes = nodes
    end

    # return
    return getfield(basis, :nodes)
end

"""
```julia
getnumberquadraturepoints(basis)
```

Get the number of quadrature points for the basis

# Arguments:
- `basis`:  basis to compute number of quadrature points

# Returns:
- Integer number of basis quadrature points

# Example:
```jldoctest
# get number of quadrature points for basis
basis = TensorH1LagrangeBasis(4, 3, 2, 2);

# note: either syntax works
numberquadraturepoints = LFAToolkit.getnumberquadraturepoints(basis);
numberquadraturepoints = basis.numberquadraturepoints;

# verify
@assert numberquadraturepoints == 3^2

# output

```
"""
function getnumberquadraturepoints(basis::TensorBasis)
    return basis.numberquadratuepoints1d^basis.dimension
end

"""
```julia
getquadraturepoints(basis)
```

Get quadrature points for basis

# Returns:
- Basis quadrature points array

# Arguments:
- `basis`: basis to compute quadrature points

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get basis quadrature weights
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    quadraturepoints = LFAToolkit.getquadraturepoints(basis);
    quadraturepoints = basis.quadraturepoints;

    # verify
    truequadraturepoints1d = [-√(3/5), 0, √(3/5)];
    truequadraturepoints = [];
    if dimension == 1
        truequadraturepoints = truequadraturepoints1d;
    elseif dimension == 2
        truequadraturepoints = transpose(hcat([[
            [x, y] for x in truequadraturepoints1d, y in truequadraturepoints1d
        ]...]...));
    elseif dimension == 3
        truequadraturepoints = transpose(hcat([[
            [x, y, z]
            for
            x in truequadraturepoints1d,
            y in truequadraturepoints1d, z in truequadraturepoints1d
        ]...]...));
    end

    @assert truequadraturepoints ≈ quadraturepoints
end

# output

```
"""
function getquadraturepoints(basis::TensorBasis)
    # assembled if needed
    if !isdefined(basis, :quadraturepoints)
        quadraturepoints = []
        if basis.dimension == 1
            # 1D
            quadraturepoints = basis.quadraturepoints1d
        elseif basis.dimension == 2
            # 2D
            quadraturepoints = transpose(
                hcat(
                    [
                        [
                            [x, y] for x in basis.quadraturepoints1d,
                            y in basis.quadraturepoints1d
                        ]...,
                    ]...,
                ),
            )
        elseif basis.dimension == 3
            # 3D
            quadraturepoints = transpose(
                hcat(
                    [
                        [
                            [x, y, z] for x in basis.quadraturepoints1d,
                            y in basis.quadraturepoints1d, z in basis.quadraturepoints1d
                        ]...,
                    ]...,
                ),
            )
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        basis.quadraturepoints = quadraturepoints
    end

    # return
    return getfield(basis, :quadraturepoints)
end

"""
```julia
getquadratureweights(basis)
```

Get full quadrature weights vector for basis

# Returns:
- Basis quadrature weights vector

# Arguments:
- `basis`:  basis to compute quadrature weights

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get basis quadrature weights
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    quadratureweights = LFAToolkit.getquadratureweights(basis);
    quadratureweights = basis.quadratureweights;

    # verify
    trueweights1d = [5/9, 8/9, 5/9];
    trueweights = [];
    if dimension == 1
        trueweights = trueweights1d;
    elseif dimension == 2
        trueweights = kron(trueweights1d, trueweights1d);
    elseif dimension == 3
        trueweights = kron(trueweights1d, trueweights1d, trueweights1d);
    end

    @assert trueweights ≈ quadratureweights
end

# output

```
"""
function getquadratureweights(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :quadratureweights)
        quadratureweights = []
        if basis.dimension == 1
            # 1D
            quadratureweights = basis.quadratureweights1d
        elseif basis.dimension == 2
            # 2D
            quadratureweights = kron(basis.quadratureweights1d, basis.quadratureweights1d)
        elseif basis.dimension == 3
            # 3D
            quadratureweights = kron(
                basis.quadratureweights1d,
                basis.quadratureweights1d,
                basis.quadratureweights1d,
            )
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        basis.quadratureweights = quadratureweights
    end

    # return
    return getfield(basis, :quadratureweights)
end

"""
```julia
getinterpolation(basis)
```

Get full interpolation matrix for basis

# Arguments:
- `basis`:  basis to compute interpolation matrix

# Returns:
- Basis interpolation matrix

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get basis interpolation matrix
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    interpolation = LFAToolkit.getinterpolation(basis);
    interpolation = basis.interpolation;

    # verify
    for i in 1:3^dimension
        total = sum(interpolation[i, :]);
        @assert total ≈ 1.0
    end
end

# output

```
"""
function getinterpolation(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :interpolation)
        interpolation = []
        if basis.dimension == 1
            # 1D
            interpolation = kron(I(basis.numbercomponents), basis.interpolation1d)
        elseif basis.dimension == 2
            # 2D
            interpolation = kron(
                I(basis.numbercomponents),
                basis.interpolation1d,
                basis.interpolation1d,
            )
        elseif basis.dimension == 3
            # 3D
            interpolation = kron(
                I(basis.numbercomponents),
                basis.interpolation1d,
                basis.interpolation1d,
                basis.interpolation1d,
            )
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        basis.interpolation = interpolation
    end

    # return
    return getfield(basis, :interpolation)
end

"""
```julia
getgradient(basis)
```

Get full gradient matrix for basis

# Arguments:
- `basis`:  basis to compute gradient matrix

# Returns:
- Basis gradient matrix

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get basis gradient matrix
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    gradient = LFAToolkit.getgradient(basis);
    gradient = basis.gradient;

    # verify
    for i in 1:dimension*3^dimension
        total = sum(gradient[i, :]);
        @assert abs(total) < 1e-14
    end
end

# output

```
"""
function getgradient(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :gradient)
        gradient = []
        if basis.dimension == 1
            # 1D
            gradient = kron(I(basis.numbercomponents), basis.gradient1d)
        elseif basis.dimension == 2
            # 2D
            gradient = kron(
                I(basis.numbercomponents),
                [
                    kron(basis.gradient1d, basis.interpolation1d)
                    kron(basis.interpolation1d, basis.gradient1d)
                ],
            )
        elseif basis.dimension == 3
            # 3D
            gradient = kron(
                I(basis.numbercomponents),
                [
                    kron(basis.gradient1d, basis.interpolation1d, basis.interpolation1d)
                    kron(basis.interpolation1d, basis.gradient1d, basis.interpolation1d)
                    kron(basis.interpolation1d, basis.interpolation1d, basis.gradient1d)
                ],
            )
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        basis.gradient = gradient
    end

    # return
    return getfield(basis, :gradient)
end

"""
```julia
getnumbermodes(basis)
```

Get number of modes for basis

# Arguments:
- `basis`:  basis to compute number of modes

# Returns:
- Number of modes for basis

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get number of basis modes
    basis = TensorH1LagrangeBasis(4, 3, 2, dimension);

    # note: either syntax works
    numbermodes = LFAToolkit.getnumbermodes(basis);
    numbermodes = basis.numbermodes;

    # verify
    @assert numbermodes == 2*3^dimension
end

# output

```
"""
function getnumbermodes(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :modemap)
        basis.numbermodes = maximum(basis.modemap)
    end

    # return
    return getfield(basis, :numbermodes)
end

"""
```julia
getmodemap(basis)
```

Get mode mapping vector for basis

# Arguments:
- `basis`:  basis to compute mode map vector

# Returns:
- Basis mode map vector

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get mode map vector
    basis = TensorH1LagrangeBasis(4, 3, 1, dimension);

    # note: either syntax works
    modemap = LFAToolkit.getmodemap(basis);
    modemap = basis.modemap;

    # verify
    truemodemap1d = [1, 2, 3, 1];
    truemodemap = [];
    if dimension == 1
        truemodemap = truemodemap1d;
    elseif dimension == 2
        truemodemap = [[
            i + (j - 1)*3 for i in truemodemap1d, j in truemodemap1d
        ]...];
    elseif dimension == 3
        truemodemap = [[
            i +
            (j - 1)*3 +
            (k - 1)*3^2
            for i in truemodemap1d, j in truemodemap1d, k in truemodemap1d
        ]...];
    end

    @assert truemodemap == modemap
end

# output

```
"""
function getmodemap(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :modemap)
        modemap1d = [1:basis.numbernodes1d;]
        modemap1d[end] = 1
        modemap = []
        if basis.dimension == 1
            # 1D
            modemap = modemap1d
        elseif basis.dimension == 2
            # 2D
            modemap = [
                [
                    i + (j - 1) * (basis.numbernodes1d - 1) for i in modemap1d,
                    j in modemap1d
                ]...,
            ]
        elseif basis.dimension == 3
            # 3D
            modemap = [
                [
                    i +
                    (j - 1) * (basis.numbernodes1d - 1) +
                    (k - 1) * (basis.numbernodes1d - 1)^2 for i in modemap1d,
                    j in modemap1d, k in modemap1d
                ]...,
            ]
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        numbermodes1component = maximum(modemap)
        modemap = vcat(
            [
                ((i - 1) * numbermodes1component) .+ modemap for
                i = 1:basis.numbercomponents
            ]...,
        )
        basis.modemap = modemap
        basis.numbermodes = maximum(modemap)
    end

    # return
    return getfield(basis, :modemap)
end

"""
```julia
getprimalnodes(basis)
```

Get primal nodes for basis

# Arguments:
- `basis`:  basis to compute primal nodes

# Returns:
- Basis primal nodes vector

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get mode map vector
    p = 4
    basis = TensorH1LagrangeBasis(p, p, 1, dimension);

    # note: either syntax works
    primalnodes = LFAToolkit.getprimalnodes(basis);
    primalnodes = basis.primalnodes;

    # verify
    trueprimalnodes = []
    if dimension == 1
        trueprimalnodes = [1, p];
    elseif dimension == 2
        trueprimalnodes = [1, p, p^2-p+1, p^2]
    elseif dimension == 3
        trueprimalnodes = [1, p, p^2-p+1, p^2, p^3-p^2+1, p^3-p^2+p, p^3-p+1, p^3]
    end

    @assert trueprimalnodes == primalnodes
end

# output

```
"""
function getprimalnodes(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :primalnodes)
        primalnodes1component = []
        p = basis.numbernodes1d
        if basis.dimension == 1
            # 1D
            primalnodes1component = [1, p]
        elseif basis.dimension == 2
            # 2D
            primalnodes1component = [1, p, p^2 - p + 1, p^2]
        elseif basis.dimension == 3
            # 3D
            primalnodes1component =
                [1, p, p^2 - p + 1, p^2, p^3 - p^2 + 1, p^3 - p^2 + p, p^3 - p + 1, p^3]
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        primalnodes = vcat(
            [
                (i - 1) * basis.numbernodes .+ primalnodes1component for
                i = 1:basis.numbercomponents
            ]...,
        )
        basis.primalnodes = primalnodes
    end

    # return
    return getfield(basis, :primalnodes)
end

"""
```julia
getinterfacenodes(basis)
```

Get interface nodes for basis

# Arguments:
- `basis`:  basis to compute primal nodes

# Returns:
- Basis primal nodes vector

# Example:
```jldoctest
# test for all supported dimensions
for dimension in 1:3
    # get mode map vector
    p = 4
    basis = TensorH1LagrangeBasis(p, p, 1, dimension);

    # note: either syntax works
    interfacenodes = LFAToolkit.getinterfacenodes(basis);
    interfacenodes = basis.interfacenodes;

    # verify
    trueinterfacenodes = []
    if dimension == 1
        trueinterfacenodes = [1, p];
    elseif dimension == 2
        trueinterfacenodes = [1:p..., p^2-p+1:p^2...]
        for i = 1:p-2
            push!(trueinterfacenodes, i*p+1)
            push!(trueinterfacenodes, (i+1)*p)
        end
    elseif dimension == 3
        trueinterfacenodes = [1:p^2..., p^3-p^2+1:p^3...]
        for i = 1:p-2
            push!(trueinterfacenodes, i*p^2+1:i*p^2+p...)
            push!(trueinterfacenodes, (i+1)*p^2-p+1:(i+1)*p^2...)
            push!(trueinterfacenodes, i*p^2+p+1:p:(i+1)*p^2-2*p+1...)
            push!(trueinterfacenodes, i*p^2+2*p:p:(i+1)*p^2-p...)
        end
    end
    trueinterfacenodes = sort(trueinterfacenodes)

    @assert trueinterfacenodes == interfacenodes
end

# output

```
"""
function getinterfacenodes(basis::TensorBasis)
    # assemble if needed
    if !isdefined(basis, :interfacenodes)
        interfacenodes1component = []
        p = basis.numbernodes1d
        if basis.dimension == 1
            # 1D
            interfacenodes1component = [1, p]
        elseif basis.dimension == 2
            # 2D
            interfacenodes1component = [1:p..., p^2-p+1:p^2...]
            for i = 1:p-2
                push!(interfacenodes1component, i * p + 1)
                push!(interfacenodes1component, (i + 1) * p)
            end
        elseif basis.dimension == 3
            # 3D
            interfacenodes1component = [1:p^2..., p^3-p^2+1:p^3...]
            for i = 1:p-2
                push!(interfacenodes1component, i*p^2+1:i*p^2+p...)
                push!(interfacenodes1component, (i+1)*p^2-p+1:(i+1)*p^2...)
                push!(interfacenodes1component, i*p^2+p+1:p:(i+1)*p^2-2*p+1...)
                push!(interfacenodes1component, i*p^2+2*p:p:(i+1)*p^2-p...)
            end
        else
            throw(DomainError(basis.dimension, "Dimension must be less than or equal to 3")) # COV_EXCL_LINE
        end
        interfacenodes1component = sort(interfacenodes1component)
        interfacenodes = vcat(
            [
                (i - 1) * basis.numbernodes .+ interfacenodes1component for
                i = 1:basis.numbercomponents
            ]...,
        )
        basis.interfacenodes = interfacenodes
    end

    # return
    return getfield(basis, :interfacenodes)
end

"""
```julia
getnumberelements(basis)
```

Get the number of elements for the basis

# Arguments:
- `basis`:  basis to compute number of micro-elements

# Returns:
- Integer number of basis micro-elements

# Example:
```jldoctest
# get number of nodes for basis
basis = TensorH1LagrangeMacroBasis(4, 4, 1, 2, 2);

# note: either syntax works
numbernodes = LFAToolkit.getnumberelements(basis);
numbernodes = basis.numberelements;

# verify
@assert numbernodes == 2^2

# output

```
"""
function getnumberelements(basis::TensorBasis)
    return basis.numberelements1d^basis.dimension
end

# ------------------------------------------------------------------------------
# get/set property
# ------------------------------------------------------------------------------

function Base.getproperty(basis::TensorBasis, f::Symbol)
    if f == :numbernodes
        return getnumbernodes(basis)
    elseif f == :numberquadraturepoints
        return getnumberquadraturepoints(basis)
    elseif f == :nodes
        return getnodes(basis)
    elseif f == :quadratureweights
        return getquadratureweights(basis)
    elseif f == :quadraturepoints
        return getquadraturepoints(basis)
    elseif f == :interpolation
        return getinterpolation(basis)
    elseif f == :gradient
        return getgradient(basis)
    elseif f == :numbermodes
        return getnumbermodes(basis)
    elseif f == :modemap
        return getmodemap(basis)
    elseif f == :primalnodes
        return getprimalnodes(basis)
    elseif f == :interfacenodes
        return getinterfacenodes(basis)
    elseif f == :numberelements
        return getnumberelements(basis)
    else
        return getfield(basis, f)
    end
end

function Base.setproperty!(basis::TensorBasis, f::Symbol, value)
    if f == :numbernodes1d ||
       f == :numberquadraturepoints1d ||
       f == :numbercomponents ||
       f == :dimension ||
       f == :nodes1d ||
       f == :quadraturepoints1d ||
       f == :quadratureweights1d ||
       f == :interpolation1d ||
       f == :gradient1d ||
       f == :volume
        throw(ReadOnlyMemoryError()) # COV_EXCL_LINE
    else
        return setfield!(basis, f, value)
    end
end

function Base.setproperty!(basis::NonTensorBasis, f::Symbol, value)
    if f == :numbernodes ||
       f == :numberquadraturepoints ||
       f == :numbercomponents ||
       f == :dimension ||
       f == :nodes ||
       f == :quadraturepoints ||
       f == :quadratureweights ||
       f == :interpolation ||
       f == :gradient ||
       f == :volume ||
       f == :numbermodes ||
       f == :modemap
        throw(ReadOnlyMemoryError()) # COV_EXCL_LINE
    else
        return setfield!(basis, f, value)
    end
end

# ------------------------------------------------------------------------------
# gradient on stretched mesh
# ------------------------------------------------------------------------------

"""
```julia
getdXdxgradient(basis, mesh)
```

Get gradient adjusted for mesh stretching

# Arguments:
- `basis`:  basis to compute gradient
- `mesh`:   mesh to compute gradient

# Returns:
- gradient matrix multiplied by change of coordinates adjoint

# Example:
```jldoctest
for dimension = 1:3
    # mesh
    mesh = []
    if dimension == 1
        mesh = Mesh1D(2.0)
    elseif dimension == 2
        mesh = Mesh2D(2.0, 3.0)
    elseif dimension == 3
        mesh = Mesh3D(2.0, 3.0, 4.0)
    end

    # basis
    basis = TensorH1LagrangeBasis(4, 3, 1, dimension);

    # get gradient on mesh
    gradient = LFAToolkit.getdXdxgradient(basis, mesh);

    # verify
    nodes = basis.nodes;
    linearfunction = [];
    truegradient = [];
    if dimension == 1
        linearfunction = nodes;
        truegradient = [1/2*ones(basis.numberquadraturepoints)...]
    elseif dimension == 2
        linearfunction = (nodes[:, 1] + nodes[:, 2]);
        truegradient = [
            1/2*ones(basis.numberquadraturepoints)...
            1/3*ones(basis.numberquadraturepoints)...
        ]
    elseif dimension == 3
        linearfunction = (nodes[:, 1] + nodes[:, 2] + nodes[:, 3]);
        truegradient = [
            1/2*ones(basis.numberquadraturepoints)...
            1/3*ones(basis.numberquadraturepoints)...
            1/4*ones(basis.numberquadraturepoints)...
        ]
    end

    @assert gradient*linearfunction ≈ truegradient
end

# output

```
"""
function getdXdxgradient(basis::TensorBasis, mesh::Mesh)
    # check compatibility
    dimension = basis.dimension
    if (dimension == 1 && typeof(mesh) != Mesh1D) ||
       (dimension == 2 && typeof(mesh) != Mesh2D) ||
       (dimension == 3 && typeof(mesh) != Mesh3D)
        error("mesh dimension must match basis dimension") # COV_EXCL_LINE
    end

    # coordinate transformation
    gradient = basis.gradient

    # adjust for mesh
    if dimension == 1
        # 1D
        return gradient / mesh.dx
    elseif dimension == 2
        # 2D
        numberquadraturepoints = basis.numberquadraturepoints
        return [
            gradient[1:numberquadraturepoints, :] / mesh.dx
            gradient[numberquadraturepoints+1:end, :] / mesh.dy
        ]
    elseif dimension == 3
        # 3D
        numberquadraturepoints = basis.numberquadraturepoints
        return [
            gradient[1:numberquadraturepoints, :] / mesh.dx
            gradient[numberquadraturepoints+1:2*numberquadraturepoints, :] / mesh.dy
            gradient[2*numberquadraturepoints+1:end, :] / mesh.dz
        ]
    end
end

function getdXdxgradient(basis::NonTensorBasis, mesh::Mesh)
    throw(error("dXdxgradient unimplemented for non-tensor bases"))
end

"""
```julia
getdxdXquadratureweights(basis, mesh)
```

Get quadrature weights adjusted for mesh stretching

# Arguments:
- `basis`:  basis to compute quadratureweights
- `mesh`:   mesh to compute quadratureweights

# Returns:
- quadrature weights multiplied by change of coordinates adjoint

# Example:
```jldoctest
mesh = Mesh1D(2.0)

# basis
basis = TensorH1LagrangeBasis(4, 3, 1, 1);

# get gradient on mesh
weights = LFAToolkit.getdxdXquadratureweights(basis, mesh);

# verify
@assert basis.quadratureweights * mesh.volume / basis.volume ≈ weights

# output

```
"""
function getdxdXquadratureweights(basis::TensorBasis, mesh::Mesh)
    # check compatibility
    dimension = basis.dimension
    if (dimension == 1 && typeof(mesh) != Mesh1D) ||
       (dimension == 2 && typeof(mesh) != Mesh2D) ||
       (dimension == 3 && typeof(mesh) != Mesh3D)
        error("mesh dimension must match basis dimension") # COV_EXCL_LINE
    end

    # adjust for mesh
    return basis.quadratureweights * mesh.volume / basis.volume
end

function getdxdXquadratureweights(basis::NonTensorBasis, mesh::Mesh)
    throw(error("dxdXquadratureweights unimplemented for non-tensor bases"))
end

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


function setup_unconstrained()
    options = Dict(:verbose => false,
                   :eps_abs => 1e-08,
                   :eps_rel => 1e-08,
                   :eps_dual_inf => 1e-18)
    return options
end

tol = 1e-5

@testset "unconstrained" begin

    @testset "unconstrained_problem" begin
        srand(1)

        n = 30
        m = 0
        P = sparse(Diagonal(rand(n)) + 0.2 * sparse(I, n, n))
        q = randn(n)
        A = spzeros(m, n)
        u = Float64[]
        l = Float64[]

        # Explicit solution
        invP = inv(Array(P))
        x_test = - invP * q
        y_test = zeros(m)
        obj_test = - .5 * q' * invP * q

        options = setup_unconstrained()

        model = OSQP.Model()
        OSQP.setup!(model; P=P, q=q, A=A, l=l, u=u, options...)
        results = OSQP.solve!(model)

        @test isapprox(results.x, x_test, atol=tol)
        @test isapprox(results.y, y_test, atol=tol)
        @test isapprox(results.info.obj_val, obj_test, atol=tol)
        @test results.info.status == :Solved

    end

end


# tests for segment selection and StatsModels @formula+df interface
using Random
@testset "segment/model selection" begin

datapath = joinpath(dirname(@__FILE__), "data")

# NOTE: 1. we use intercept=false because of a StatsModels issue where predict with drop_intercept model didn't work
# NOTE: 2. we skip the poisson because it does not converge without an intercept
@testset "$family" for (family, dist, link) in (("gaussian", Normal(), IdentityLink()), ("binomial", Binomial(), LogitLink()))
    data = CSV.read(joinpath(datapath,"gamlr.$family.data.csv"); header=[:y, :x1, :x2, :x3])
    offset = fill(0.001,length(data.y))

    @testset "$L" for L in [LassoModel, GammaLassoModel]
        R = Lasso.pathtype(L)

        @testset "$f" for (f,intercept) in ((@formula(y~x1+x2+x3), true), (@formula(y~0+x1+x2+x3), false))
            path = fit(R, f, data, dist, link; intercept=intercept, offset=offset)

            @testset "$(typeof(select))" for select in [MinAIC(), MinAICc(), MinBIC(), MinCVmse(path), MinCV1se(path)]
                Random.seed!(421)
                m = fit(L, f, data, dist, link; select=select, intercept=intercept, offset=offset)

                Random.seed!(421)
                pathcoefs = coef(path, select)

                Random.seed!(421)
                pathpredict = Lasso.predict(path, data; select=select, offset=offset)

                @test pathcoefs == coef(m)
                if isa(dist, Normal)
                    @test pathpredict == GLM.predict(m, data) + offset
                else
                    @test pathpredict == GLM.predict(m, data; offset=offset)
                end
            end
        end
    end
end

end


using WAVI, Plots
function plot_MISMIP1_melt_pattern()
    # Grid and boundary conditions
    nx = 320
    ny = 40
    nσ = 4
    x0 = 0.0
    y0 = -40000.0
    dx = 2000.0
    dy = 2000.0
    h_mask = trues(nx, ny)
    u_iszero = falses(nx + 1, ny); u_iszero[1,:] .= true
    v_iszero = falses(nx, ny + 1); v_iszero[:,1] .= true; v_iszero[:,end] .= true
    grid = Grid(nx=nx,
                ny=ny,
                nσ=nσ,
                x0=x0,
                y0=y0,
                dx=dx,
                dy=dy,
                h_mask=h_mask,
                u_iszero=u_iszero,
                v_iszero=v_iszero)

    # Bed
    bed = WAVI.mismip_plus_bed # function definition

    # Inputing thickness profile, reading from binary file
    fname = "examples\\Favier2019_melt_params\\data\\MISMIP_ice0_2km_SteadyThickness.bin";
    # fname = joinpath(dirname(@__FILE__), "data",  "MISMIP_ice0_2km_SteadyThickness.bin")
    h = Array{Float64,2}(undef, nx, ny)
    read!(fname, h)
    h = ntoh.(h)

    # melt rate Model
    melt_rate = MISMIPMeltRateOne();

    # make the model
    initial_conditions = InitialConditions(initial_thickness=h) # set thickness
    model = Model(grid=grid,
                bed_elevation=bed,
                initial_conditions=initial_conditions,
                melt_rate=melt_rate,
                solver_params=SolverParams(maxiter_picard=1) # we'll update state to get basal melt, but don't need other info accurate
                )

    # update the configuration
    update_state!(model)

    # make the plot
    x = model.grid.xxh[:,1]; y = model.grid.yyh[1,:];
    m = deepcopy(model.fields.gh.basal_melt);
    m[model.fields.gh.grounded_fraction .== 1.] .= NaN;
    msat = deepcopy(m)
    msat[msat .> 50] .= 50
    plt = contour(x ./ 1e3,y / 1e3, msat',
                  fill=true,
                  linewidth=0,
                  colorbar=true,
                  colorbar_title="melt rate (m/yr)",
                  framestyle=:box);
    xlims!((420, 600))
    xlabel!("x (km)")
    ylabel!("y (km)")
    @show plt

    #savefig(plt,  joinpath(dirname(@__FILE__), "MISMIP1_melt_pattern.png"))

    mean_melt_rate = sum(m[model.fields.gh.grounded_fraction .< 1]) / sum(model.fields.gh.grounded_fraction .< 1)
    println("Mean melt rate in the shelf is ", mean_melt_rate, " m/yr")

    return model
end

model = plot_MISMIP1_melt_pattern()


#=
  (Simple) True skill problem.

  Example inspired by
  Johannes Borgstrom, Andrew D. Gordon, Michael Greenberg, James Margetson, and Jurgen Van Gael:
  "Measure Transformer Semantics for Bayesian Machine Learning"
  https://www.microsoft.com/en-us/research/publication/measure-transformer-semantics-for-bayesian-machine-learning-2011/?from=http%3A%2F%2Fresearch.microsoft.com%2Fpubs%2F135344%2Fmsr-tr-2011-18.pdf

  Note that we constraint the generated worlds so they satisfies the
  observation constraints (a > b > c).

  For num_people = 5:

Summary Statistics
      parameters       mean       std   naive_se      mcse        ess      rhat   ess_per_sec
          Symbol    Float64   Float64    Float64   Float64    Float64   Float64       Float64

        skill[1]   102.2371    2.8288     0.0283    0.1671   231.3914    0.9999        5.5389
        skill[2]   100.8802    2.5754     0.0258    0.1498   281.8016    1.0053        6.7455
        skill[3]    99.7618    2.5047     0.0250    0.1523   215.7630    1.0022        5.1648
        skill[4]    99.1769    2.7811     0.0278    0.1690   206.5924    1.0058        4.9452
        skill[5]    97.8249    2.8215     0.0282    0.1792   185.3145    1.0026        4.4359
  performance[1]   105.4878    3.5954     0.0360    0.2291   198.0813    1.0005        4.7415
  performance[2]   102.3919    2.5597     0.0256    0.1493   184.1535    1.0264        4.4081
  performance[3]   100.0782    2.5635     0.0256    0.1485   201.1306    1.0179        4.8145
  performance[4]    97.8592    2.7721     0.0277    0.1637   196.4140    1.0023        4.7016
  performance[5]    94.4979    3.2780     0.0328    0.2024   158.5955    1.0044        3.7963

skill[1] > skill[2]: 0.6344
skill[1] > skill[3]: 0.7565
skill[1] > skill[4]: 0.7846
skill[1] > skill[5]: 0.8963
skill[2] > skill[3]: 0.6219
skill[2] > skill[4]: 0.7113
skill[2] > skill[5]: 0.7706
skill[3] > skill[4]: 0.5337
skill[3] > skill[5]: 0.6878
skill[4] > skill[5]: 0.6122
performance[1] > performance[2]:1.0
performance[1] > performance[3]:1.0
performance[1] > performance[4]:1.0
performance[1] > performance[5]:1.0
performance[2] > performance[3]:0.9999
performance[2] > performance[4]:0.9999
performance[2] > performance[5]:0.9995
performance[3] > performance[4]:0.9995
performance[3] > performance[5]:0.9992
performance[4] > performance[5]:0.9992

  Cf: ~/cplint/trueskill.pl
      ~/blog/true_skill.blog
      ~/psi/true_skill.psi
      ~/webppl/true_skill.wpl

   Compare with tug_of_war.jl

=#

using Turing, StatsPlots, DataFrames
include("jl_utils.jl")

@model function true_skill(num_people)
    # Each person has an unknown (latent) Skill and a
    # known performance, where the skill is
    # reflected in the performance (with uncertainties).
    skill       = tzeros(num_people)
    performance = tzeros(num_people)
    for p in 1:num_people
        skill[p] ~ Normal(100,sqrt(10))
        performance[p] ~ Normal(skill[p], sqrt(15))
    end

    # Nicer (and slower), but it's not supported by IS or PG!
    # skill .~ Normal(100,sqrt(10))
    # @. performance ~ Normal(skill, sqrt(15)) # Note The @. since we refer to skill[p]
    for p1 in 1:num_people-1, p2 in p1+1:num_people
        true ~ Dirac(performance[p1] > performance[p2])
    end

    # What are their performance and (underlying) skills?

end

# num_people = 3
num_people = 5
model = true_skill(num_people)

num_chns = 4

# chns = sample(model, Prior(), 10_000)
# chns = sample(model, MH(), 10_000)
chns = sample(model, PG(15), 10_000)
# chns = sample(model, SMC(), 10_000)
# chns = sample(model, IS(), 10_000)

display(chns)

# display(plot(chns))
# display(histogram(chns))
# display(gelmandiag(chns)) # requires number of chns >= 2

df = DataFrame(chns);

# Show the latent skills
for p1 in 1:num_people-1, p2 in p1+1:num_people
    println("skill[$p1] > skill[$p2]: ", mean(df[!,"skill[$p1]"] .>df[!,"skill[$p2]"]))
end

# Show the observed performance
for p1 in 1:num_people-1, p2 in p1+1:num_people
    println("performance[$p1] > performance[$p2]:", mean(df[!,"performance[$p1]"] .>df[!,"performance[$p2]"]))
end


const EPS_REL = Base.rtoldefault(Float64)  # machine epsilon

# Define drawshape() and drawshape!() functions.
# hres and vres are the number of sampling points in the corresponding Cartesion derctions.
# They should be at least 2 because range(start, end, length) with start≠end requires length
# ≥ 2.  The default values of vres and hres are intentionally invalid.
@recipe(DrawShape) do scene
    Attributes(res=101, vres=1, hres=1)
end

# Implement drawshape!() for 2D shapes.
function Makie.plot!(ds::DrawShape{<:Tuple{Shape{2}}})
    res₀ = ds.res.val  # fields found by examining typeof(ds) and fieldnames(typeof(ds)), etc
    hres = ds.hres.val
    vres = ds.vres.val

    # If hres and vres are set to valid values, use them; otherwise use res₀.
    hres≤1 && (hres = res₀)
    vres≤1 && (vres = res₀)

    hres>1 || @error "hres = $hres should be at least 2."
    vres>1 || @error "vres = $vres should be at least 2."

    shp = ds[1]
    res = (hres, vres)

    # Makie.convert_arguments() defined below handles this new signature.
    contour!(ds, shp, res, levels=SVector(0.0); ds.attributes...)

    return ds
end

# Implement drawshape!() for 3D shapes.
function Makie.plot!(ds::DrawShape{<:Tuple{Shape{3},Tuple{Symbol,Real}}})
    res₀ = ds.res.val  # fields found by examining typeof(ds) and fieldnames(typeof(ds)), etc
    hres = ds.hres.val
    vres = ds.vres.val

    # If hres and vres are set to valid values, use them; otherwise use res₀.
    hres≤1 && (hres = res₀)
    vres≥1 && (vres = res₀)

    hres>1 || @error "hres = $hres should at least 2."
    vres>1 || @error "vres = $vres should at least 2."

    shp = ds[1]
    cs = ds[2]
    res = (hres, vres)

    # Makie.convert_arguments() defined below handles this new signature.
    contour!(ds, shp, cs, res, levels=SVector(0.0); ds.attributes...)

    return ds
end

# Define the new signature of contour!() used in drawshape!() for 2D shapes.
function Makie.convert_arguments(P::SurfaceLike, shp::Shape{2}, res::Tuple{Integer,Integer})
    lower, upper = bounds(shp)
    ∆ = upper - lower

    ϵrel = EPS_REL
    nw = 1; xs = range(lower[nw] - ϵrel*∆[nw], upper[nw] + ϵrel*∆[nw], length=res[nw])
    nw = 2; ys = range(lower[nw] - ϵrel*∆[nw], upper[nw] + ϵrel*∆[nw], length=res[nw])

    lvs = [level(@SVector([x,y]), shp) for x = xs, y = ys]

    return convert_arguments(P, xs, ys, lvs)
end

# Define the new signature of contour!() used in drawshape!() for 3D shapes.
function Makie.convert_arguments(P::SurfaceLike, shp::Shape{3},
                                 cs::Tuple{Symbol,Real},  # (:x or :y or :z, intercept): cross section spec
                                 res::Tuple{Integer,Integer})
    ax, cept = cs  # axis normal to cross section, intercept

    ax==:x || ax==:y || ax==:z || @error "cs[1] = $(cs[1]) should be :x or :y or :z."
    nw = (ax==:x) + 2(ax==:y) + 3(ax==:z)  # nw = 1, 2, 3 for ax = :x, :y, :z
    nu, nv = mod1(nw+1,3), mod1(nw+2,3)

    # Set the unit vectors along the u-, v-, w-axes.
    û = SVector(ntuple(identity,Val(3))) .== nu
    v̂ = SVector(ntuple(identity,Val(3))) .== nv
    ŵ = SVector(ntuple(identity,Val(3))) .== nw

    lower, upper = bounds(shp)
    ∆ = upper - lower

    ϵrel = EPS_REL
    us = range(lower[nu] - ϵrel*∆[nu], upper[nu] + ϵrel*∆[nu], length=res[1])
    vs = range(lower[nv] - ϵrel*∆[nv], upper[nv] + ϵrel*∆[nv], length=res[2])

    lvs = [level(u*û + v*v̂ + cept*ŵ, shp) for u = us, v = vs]

    return convert_arguments(P, us, vs, lvs)
end


const regex = r"<x=(-?\d+), y=(-?\d+), z=(-?\d+)>";
const input = readlines(length(ARGS) > 0 ? ARGS[1] : "example1.txt");
pos = hcat(map(line -> parse.(Int,match(regex, line).captures), input)...);
const start = copy(pos)

function gravity(p,i,d)
    dv = 0
    for j in p[d,:]
        if p[d,i] > j
            dv -= 1
        elseif p[d,i] < j
            dv += 1
        end
    end
    return dv
end

function energy(positions, velocities, moon)
    potential = sum(abs.(positions[:,moon]))
    kinetic = sum(abs.(velocities[:,moon]))
    return (potential, kinetic, potential * kinetic)
end

vel = zeros(Int,3,4);
const period_length = zeros(Int,3)

const show_output = false

for i in 1:500000
    dv = [gravity(pos,body,dimension) for dimension in 1:3, body in 1:size(pos)[2]]
    global vel += dv
    global pos += vel
    if (show_output)
        println("After $(i) step$(i != 1 ? "s" : ""):")
        println("Positions:")
        display(pos')
        println()
        println("Velocities:")
        display(vel')
        println()
    end

    for j in 1:3
        if period_length[j] == 0 && pos[j,:] == start[j,:]
            period_length[j] = i + 1
        end
    end

    if i == 1000
        println("Day 12 Part 1: $(sum([energy(pos,vel,i)[3] for i in 1:4]))")
    end

    if i > 1000 && !(0 in period_length)
        break;
    end
end

println("Day 12 Part 2: $(foldl(lcm, period_length))")


zero_map(::Type{LinearMap}, M::Integer, N::Integer) =
    LinearMap((y,x) -> (y .= false), M, N,
              ismutating=true, issymmetric=true, ishermitian=true)

block_eltype(::Type{LinearMap{T}}) where T = T
block_spy_block(b::LinearMaps.WrappedMap{<:Any,<:AbstractMatrix}) =
    block_spy_block(b.lmap)

isblockentryzero(b::LinearMaps.WrappedMap{<:Any,<:AbstractMatrix}, args...) =
    isblockentryzero(b.lmap, args...)

unit_vec(::Type{T}, m, i) where T = Vcat(Zeros{T}(i-1), one(T), Zeros{T}(m-i))

function block_getindex(A::LinearMap{T}, i, j) where T
    m,n = size(A)

    ej = unit_vec(T, n, j)
    tmp = Vector{T}(undef, m)

    mul!(tmp, A, ej)

    tmp[i]
end

# This is necessary type piracy since SparseArrays checks if entry
# iszero on assignment. Should be upstreamed.
Base.iszero(::LinearMap) = false


#######################
## Timematr get mean ##
#######################

import Base.mean
function mean(tm::AbstractTimematr, dim::Int)
    ## output: DataFrame, since date dimension is lost
    if dim == 2
        error("For rowwise mean use rowmeans function")
    end
    meanVals = DataFrame()
    for (nam, col) in eachcol(tm.vals)
        meanVals[nam] = mean(col)
    end
    return meanVals
end

function mean(tn::AbstractTimenum, dim::Int)
    ## output: DataFrame, since date dimension is lost
    ## NAs are ignored!
    if dim == 2
        error("For rowwise mean use rowmeans function")
    end
    nObs, nAss = size(tn)
    meanVals = DataFrame()
    nVals = zeros(Int, nObs)
    for (nam, col) in eachcol(tn.vals)
        if isa(col, DataArray)
            meanVals[nam] = mean(col[!col.na])
        else
            meanVals[nam] = mean(col)
        end
    end
    return meanVals
end

## rowmeans
##---------

function rowmeans(tm::AbstractTimematr)
    ## output: Timematr
    nObs, nAss = size(tm)
    meanVals = zeros(Float64, nObs)
    for (nam, col) in eachcol(tm.vals)
        meanVals += col
    end
    return Timematr(DataFrame(mean_values = meanVals./nAss),
                    idx(tm))
end

function rowmeans(tn::Timenum)
    ## output: Timematr, NAs are ignored
    nObs, nAss = size(tn)
    meanVals = DataArray(zeros(Float64, nObs))
    nVals = zeros(Int, nObs)
    for (nam, col) in eachcol(tn.vals)
        if isa(col, DataArray)
            nVals[!col.na] += 1
            meanVals[!col.na] += col.data[!col.na]
        else
            nVals += 1
            meanVals += col
        end
    end
    return Timematr(DataFrame(mean_values = meanVals./nVals),
                    idx(tn))
end

#######################
## Timematr get prod ##
#######################

import Base.prod
function prod(tm::AbstractTimematr, dim::Int = 1)
    ## output: DataFrame, since date dimension is lost
    if dim == 2
        error("For rowwise prod use rowprods function")
    end
    prodVals = DataFrame()
    for (nam, col) in eachcol(tm.vals)
        prodVals[nam] = prod(col)
    end
    return prodVals
end

function rowprods(tm::AbstractTimematr)
    ## output: Timematr
    prodVals = prod(core(tm), 2)
    prods = Timematr(prodVals, idx(tm))
end
## function rowprods2(tm::AbstractTimematr)
##     ## output: Timematr
##     nObs, nAss = size(tm)
##     prodVals = ones(Float64, nObs)
##     for (nam, col) in eachcol(tm.vals)
##         prodVals = prodVals .* col
##     end
##     return Timematr(composeDataFrame(prodVals, [:prod_values]),
##                     idx(tm))
## end

######################################
## Timematr get row and column sums ##
######################################

import Base.sum
function sum(tm::AbstractTimematr, dim::Int = 1)
    ## output: DataFrame, since date dimension is lost
    if dim == 2
        error("For rowwise sum use rowsums function")
    end
    sumVals = sum(core(tm), dim)
    sums = composeDataFrame(sumVals, names(tm))
end

function rowsums(tm::AbstractTimematr)
    ## output: Timematr
    sumVals = sum(core(tm), 2)
    sums = Timematr(sumVals, idx(tm))
end

#########################
## Timematr covariance ##
#########################

import Base.cov
function cov(tm::AbstractTimematr)
    ## output: DataFrame
    covDf = composeDataFrame(cov(core(tm)), names(tm.vals))
    return covDf
end

##########################
## Timematr correlation ##
##########################

import Base.cor
function cor(tm::AbstractTimematr)
    ## output: DataFrame
    corDf = composeDataFrame(cor(core(tm)), names(tm.vals))
    return corDf
end

##########################
## Timematr std ##
##########################

import Base.std
function std(tm::AbstractTimematr)
    ## output: DataFrame
    stdDf = composeDataFrame(std(core(tm), 1), names(tm.vals))
    return stdDf
end

function std(tm::AbstractTimematr, dim::Integer)
    ## output: DataFrame
    if dim == 2
        error("For rowwise standard deviations use rowstds")
    end
    stdDf = composeDataFrame(std(core(tm), 1), names(tm.vals))
    return stdDf
end

function rowstds(tm::AbstractTimematr)
    rowStd = Timematr(std(core(tm), 2), idx(tm))
    return rowStd
end

#########################
## minimum and maximum ##
#########################

import Base.minimum
function minimum(tm::AbstractTimematr)
    return minimum(core(tm))
end
function minimum(tm::AbstractTimematr, dim::Integer)
    if dim == 2
        error("For rowwise minimum use rowmin function")
    end
    return composeDataFrame(minimum(core(tm), 1), names(tm))
end

############
## cumsum ##
############

import Base.cumsum
function cumsum(tm::AbstractTimematr, dim::Integer)
    cumulated = cumsum(core(tm), dim)
    return Timematr(composeDataFrame(cumulated, names(tm)), idx(tm))
end

import Base.cumprod
function cumprod(tm::AbstractTimematr, dim::Integer)
    cumulated = cumprod(core(tm), dim)
    return Timematr(composeDataFrame(cumulated, names(tm)), idx(tm))
end

####################
## geometric mean ##
####################

function geomMean(x::AbstractTimematr; percent = true)
    nObs = size(x, 1)
    if percent
        vals = (prod(1 + core(x)./100, 1).^(1/nObs) - 1)*100
    else
        vals = prod(1 + core(x), 1).^(1/nObs) - 1
    end
    return composeDataFrame(vals, names(x))
end

function geomMean(x; percent = true)
    nObs = size(x, 1)
    if percent
        vals = (prod(1 + x./100, 1).^(1/nObs) - 1)*100
    else
        vals = prod(1 + x./100, 1).^(1/nObs) - 1
    end
    return vals
end

#####################
## moving averages ##
#####################

function movAvg(tm::AbstractTimematr, nPeriods::Integer)
    (nObs, nAss) = size(tm)
    nMAs = nObs - nPeriods + 1
    vals = core(tm)

    ## get start and ending indices
    startInd = ceil(nPeriods/2)
    finInd = startInd + nMAs - 1

    ## preallocation
    movAvgs = ones(nMAs, nAss)

    movAvgs[1, :] = sum(vals[1:nPeriods, :], 1)

    for ii=2:nMAs
        movAvgs[ii, :] = movAvgs[ii-1, :] - vals[(ii-1), :] +
        vals[(ii-1)+nPeriods, :]
    end

    ## divide
    scaledVals = movAvgs / nPeriods
    dats = idx(tm)[startInd:finInd]

    movAvgTm = Timematr(scaledVals, names(tm), dats)
    return movAvgTm
end


using Test
using Distributions
using KernelDensity

import KernelDensity: kernel_dist, default_bandwidth, kde_boundary, kde_range, tabulate

for D in [Normal,Uniform,Logistic]
    d = kernel_dist(D,0.5)
    @test isa(d,D)
    @test mean(d) == 0.0
    @test std(d) ≈ 0.5
end

r = kde_range((-2.0,2.0), 128)
@test step(r) > 0
r2 = kde_range((0.12698109160784082, 0.9785547869337731), 256)
@test length(r2) == 256

for X in ([0.0], [0.0,0.0], [0.0,0.5], [-0.5:0.1:0.5;])
    w = default_bandwidth(X)
    @test w > 0
    lo, hi = kde_boundary(X,w)
    @test lo < hi
    kr = kde_range((lo,hi), 10)
    @test step(kr) > 0

    for D in (Normal, )
        k1 = tabulate(X,r)
        @test isa(k1,UnivariateKDE)
        @test length(k1.density) == length(k1.x)
        @test all(k1.density .>= 0.0)
        @test sum(k1.density)*step(k1.x) ≈ 1.0

        k2 = KernelDensity.conv(k1,kernel_dist(D,0.1))
        @test isa(k2,UnivariateKDE)
        @test length(k2.density) == length(k2.x)
        @test all(k2.density .>= 0.0)
        @test sum(k2.density)*step(k2.x) ≈ 1.0

        k3 = kde(X;kernel=D)
        @test isa(k3,UnivariateKDE)
        @test length(k3.density) == length(k3.x)
        @test all(k3.density .>= 0.0)
        @test sum(k3.density)*step(k3.x) ≈ 1.0

        k4 = kde(X,r;kernel=D)
        @test isa(k4,UnivariateKDE)
        @test length(k4.density) == length(k4.x)
        @test all(k4.density .>= 0.0)
        @test sum(k4.density)*step(k4.x) ≈ 1.0

        k5 = kde_lscv(X)
        @test isa(k5,UnivariateKDE)
        @test length(k5.density) == length(k5.x)
        @test all(k5.density .>= 0.0)
        @test sum(k5.density)*step(k5.x) ≈ 1.0

        k6 = kde(X,r;kernel=D, weights=fill(1.0/length(X),length(X)))
        @test k4.density ≈ k6.density
    end
end

k11 = kde([0.0, 1.], r, bandwidth=1, weights=[0,1])
k12 = kde([1.], r, bandwidth=1)
@test k11.density ≈ k12.density


α = abs.(rand(Uniform(-1.0, 1.0), (ns))) .+ 0.5

n_nodes = ns;
nn_W = Chain(x -> x,
              Dense(ns, n_nodes, gelu),
              Dense(n_nodes, n_nodes, gelu),
              Dense(n_nodes, n_nodes, gelu),
              Dense(n_nodes, n_nodes, gelu),
              Dense(n_nodes, ns))
pW, re = Flux.destructure(nn_W);

p = vcat(α, pW)

function cellbox!(du, u, p, t)
    Wu = re(@view(p[ns+1:end]))(u)
    pα = @view(p[1:ns])
    # du = Wu - μ - u
    @. du = pα * tanh(Wu - μ) - u
end

tspan = (0, tfinal);
ts = 0:tspan[2]/ntotal:tspan[2];
ts = ts[2:end];
u0 = zeros(ns);
prob = ODEProblem(cellbox!, u0, tspan, saveat=ts);

predict_neuralode(u0, p, 1);
@show loss_neuralode(p, 1);

cbi = function (p, i_exp)
    ode_data = ode_data_list[i_exp, :, :]
    _ts = 0:ts[end]/nplot:ts[end]
    pred = predict_neuralode(u0, p, i_exp, nothing, false)
    # gold = predict_neuralode(u0, p_gold, i_exp, nothing, false)
    l_plt = []
    for i in 1:minimum([10, ns])
        plt = Plots.scatter(ts, ode_data[i,:], label="Data");
        plot!(plt, _ts, pred[i,:], label="Prediction");
        # plot!(plt, _ts, gold[i,:], label="Ground truth");
        ylabel!(plt, "x$i")
        xlabel!(plt, "Time")
        push!(l_plt, plt)
    end
    plt_all = plot(l_plt..., legend=false, size=(1000, 1000));
    png(plt_all, string(fig_path, "/conditions/i_exp_", i_exp))
    return false
end

function show_network(p)
    nothing
end

function loss_network(p)
     return 1.0, 1.0
end

cbp = function (p, iter)
    nothing
end

cbi(p, 1)


module Turf

import Base: contains

using Reexport
@reexport using GeoInterface
@reexport using GeoJSON

export radians_to_length, length_to_radians, length_to_degrees, convert_length, convert_area,
    angle_adjacent, distance, distance_to_segment, rhumb_distance, rhumb_bearing, rhumb_destination,
    destination, bearing, bearing_to_azimuth, bbox, bbox_polygon, center, centroid, transform_rotate, transform_scale,
    bezier, concave, clockwise, nearestpoint, earth_radius, area_factors, units_factors, parallel,
    Spline, linesegment, point_on_line, point_in_polygon, mediancenter, masscenter, meancenter, circle,
    pnorm_distance, distance_weight, ellipse, explode, midpoint, square, flip, lineclip, polygonclip,
    linearc, sector, planepoint, to_WGS84, to_mercator, point_grid, rectangle_grid, hexgrid, polygon_tangents,
    area, contains, within, disjoint, line_intersects, polygon_to_line, transform_translate, valid, crosses, convert_to,
    combine, scale, square_grid, triangle_grid, tag, point_to_line_distance, simplify, simplify!, clean, clean!, overlap

include("Constants.jl")
include("Utils.jl")
include("lib/Angle.jl")
include("lib/Distance.jl")
include("lib/Destination.jl")
include("lib/Bearing.jl")
include("lib/BBox.jl")
include("lib/Lines.jl")
include("lib/Centering.jl")
include("lib/Transformations.jl")
include("lib/Splines.jl")
include("lib/Circle.jl")
include("lib/Ellipse.jl")
include("lib/Planes.jl")
include("lib/Square.jl")
include("lib/Grids.jl")
include("lib/BezierSpline.jl")
include("lib/Booleans.jl")

end


using Graphs

function create_graph(start_node, end_node)
    @assert Base.length(start_node)==Base.length(end_node)

    no_node = max(maximum(start_node), maximum(end_node))
    no_arc = Base.length(start_node)

    graph = simple_inclist(no_node)
    for i=1:no_arc
        add_edge!(graph, start_node[i], end_node[i])
    end
    return graph
end

function get_vector(state, origin, destination, link_dic)
    current = destination
    parent = -1
    x = zeros(Int, maximum(link_dic))

    while parent != origin
        parent = state.parents[current]
        link_idx = link_dic[parent,current]
        if link_idx != 0
            x[link_idx] = 1
        end
        current = parent
    end
    return x
end

# preparing a graph
graph = create_graph(start_node, end_node)
link_dic = sparse(start_node, end_node, 1:numLinks)

function BPR(flowVec, fcoeffs)
    bpr = similar(flowVec)
    for a = 1:length(bpr)
        bpr[a] = free_flow_time[a] * sum([fcoeffs[i] * (flowVec[a]/capacity[a])^(i-1) for i = 1:length(fcoeffs)])
    end
    return bpr
end

function BPRSocial(flowVec, fcoeffs)
    bpr = similar(flowVec)
    # refer to [Page 50; Patriksson 1994, 2015]
    for a = 1:length(bpr)
        bpr[a] = free_flow_time[a] * sum([fcoeffs[i] *
        (flowVec[a]/capacity[a])^(i-1) for i = 1:length(fcoeffs)])
        + free_flow_time[a] * sum([fcoeffs[i] * (i-1) *
        (flowVec[a]/capacity[a])^(i-1) for i = 2:length(fcoeffs)])
    end
    return bpr
end

function all_or_nothing(travel_time, demands)
    state = []
    path = []
    x = zeros(size(start_node))

    for r=1:numZones
        # for each origin node r, find shortest paths to all destination nodes
        state = dijkstra_shortest_paths(graph, travel_time, r)

        for s=1:numZones
            # for each destination node s, find the shortest-path vector
            # load travel demand
            x = x + demands[(r,s)] * get_vector(state, r, s, link_dic)
        end
    end

    return x
end

function tapMSA(demands, fcoeffs, numIter=500, tol=1e-6)
    # Finding a starting feasible solution
    travel_time = BPR(zeros(numLinks), fcoeffs)
    xl = all_or_nothing(travel_time, demands)

    l = 1

    while l < numIter
        l += 1

        xl_old = xl

        # Finding yl
        travel_time = BPR(xl, fcoeffs)

        yl = all_or_nothing(travel_time, demands)

        # assert(yl != xl)

        xl = xl + (yl - xl)/l

        xl_new = xl

        relative_gap = norm(xl_new - xl_old, 1) / norm(xl_new, 1)

        if relative_gap < tol
            break
        end

    end

    tapFlows = Dict()

    for i = 1:length(ta_data.start_node)
        key = (ta_data.start_node[i], ta_data.end_node[i])
        tapFlows[key] = xl[i]
    end

    tapFlowVect = xl

    return tapFlows, tapFlowVect
end

function tapMSASocial(demands, fcoeffs, numIter=1000, tol=1e-6)
    # Finding a starting feasible solution
    travel_time = BPRSocial(zeros(numLinks), fcoeffs)
    xl = all_or_nothing(travel_time, demands)

    l = 1

    while l < numIter
        l += 1

        xl_old = xl

        # Finding yl
        travel_time = BPRSocial(xl, fcoeffs)

        yl = all_or_nothing(travel_time, demands)

        # assert(yl != xl)

        xl = xl + (yl - xl)/l

        xl_new = xl

        relative_gap = norm(xl_new - xl_old, 1) / norm(xl_new, 1)

        if relative_gap < tol
            break
        end

    end

    tapFlows = Dict()

    for i = 1:length(ta_data.start_node)
        key = (ta_data.start_node[i], ta_data.end_node[i])
        tapFlows[key] = xl[i]
    end

    tapFlowVect = xl

    return tapFlows, tapFlowVect
end