"""
    NegativeBinomialExpert(n, p)

PMF:

```math
P(X = k) = \\frac{\\Gamma(k+r)}{k! \\Gamma(r)} p^r (1 - p)^k, \\quad \\text{for } k = 0,1,2,\\ldots.
```

See also: [Negative Binomial Distribution](https://reference.wolfram.com/language/ref/NegativeBinomialDistribution.html) (Wolfram)

"""
struct NegativeBinomialExpert{T<:Real} <: NonZIDiscreteExpert
    n::T
    p::T
    NegativeBinomialExpert{T}(n, p) where {T<:Real} = new{T}(n, p)
end

function NegativeBinomialExpert(n::T, p::T; check_args=true) where {T <: Real}
    check_args && @check_args(NegativeBinomialExpert, 0 <= p <= 1 && n > zero(n))
    return NegativeBinomialExpert{T}(n, p)
end

## Outer constructors
NegativeBinomialExpert(n::Real, p::Real) = NegativeBinomialExpert(promote(n, p)...)
NegativeBinomialExpert(n::Integer, p::Integer) = NegativeBinomialExpert(float(n), float(p))
NegativeBinomialExpert() = NegativeBinomialExpert(1, 0.5)

## Conversion
function convert(::Type{NegativeBinomialExpert{T}}, n::S, p::S) where {T <: Real, S <: Real}
    NegativeBinomialExpert(T(n), T(p))
end
function convert(::Type{NegativeBinomialExpert{T}}, d::NegativeBinomialExpert{S}) where {T <: Real, S <: Real}
    NegativeBinomialExpert(T(d.n), T(d.p), check_args=false)
end
copy(d::NegativeBinomialExpert) = NegativeBinomialExpert(d.n, d.p, check_args=false)

## Loglikelihood of Expoert
logpdf(d::NegativeBinomialExpert, x...) = isinf(x...) ? -Inf : Distributions.logpdf.(Distributions.NegativeBinomial(d.n, d.p), x...)
pdf(d::NegativeBinomialExpert, x...) = isinf(x...) ? 0.0 : Distributions.pdf.(Distributions.NegativeBinomial(d.n, d.p), x...)
logcdf(d::NegativeBinomialExpert, x...) = isinf(x...) ? 0.0 : Distributions.logcdf.(Distributions.NegativeBinomial(d.n, d.p), x...)
cdf(d::NegativeBinomialExpert, x...) = isinf(x...) ? 1.0 : Distributions.cdf.(Distributions.NegativeBinomial(d.n, d.p), x...)

## expert_ll, etc
expert_ll_exact(d::NegativeBinomialExpert, x::Real) = LRMoE.logpdf(LRMoE.NegativeBinomialExpert(d.n, d.p), x)
function expert_ll(d::NegativeBinomialExpert, tl::Real, yl::Real, yu::Real, tu::Real)
    # d_exp = NegativeBinomialExpert(d.n*exposure, d.p)
    expert_ll = (yl == yu) ? logpdf.(d, yl) : logcdf.(d, yu) + log1mexp.(logcdf.(d, ceil.(yl) .- 1) - logcdf.(d, yu))
    return expert_ll
end
function expert_tn(d::NegativeBinomialExpert, tl::Real, yl::Real, yu::Real, tu::Real)
    # d_exp = NegativeBinomialExpert(d.n*exposure, d.p)
    expert_tn = (tl == tu) ? logpdf.(d, tl) : logcdf.(d, tu) + log1mexp.(logcdf.(d, ceil.(tl) .- 1) - logcdf.(d, tu))
    return expert_tn
end
function expert_tn_bar(d::NegativeBinomialExpert, tl::Real, yl::Real, yu::Real, tu::Real)
    # d_exp = NegativeBinomialExpert(d.n*exposure, d.p)
    expert_tn_bar = (tl == tu) ? log1mexp.(logpdf.(d, tl)) : log1mexp.(logcdf.(d, tu) + log1mexp.(logcdf.(d, ceil.(tl) .- 1) - logcdf.(d, tu)))
    return expert_tn_bar
end

exposurize_expert(d::NegativeBinomialExpert; exposure = 1) = NegativeBinomialExpert(d.n*exposure, d.p)

## Parameters
params(d::NegativeBinomialExpert) = (d.n, d.p)
function params_init(y, d::NegativeBinomialExpert)
    μ, σ2 = mean(y), var(y)
    p_init = μ / σ2
    n_init = μ*p_init/(1-p_init)
    try
        NegativeBinomialExpert(n_init, p_init)
    catch;
        NegativeBinomialExpert()
    end
end

## Simululation
sim_expert(d::NegativeBinomialExpert) = Distributions.rand(Distributions.NegativeBinomial(d.n, d.p), 1)[1]

## penalty
penalty_init(d::NegativeBinomialExpert) = [2.0 10.0]
no_penalty_init(d::NegativeBinomialExpert) = [1.0 Inf]
penalize(d::NegativeBinomialExpert, p) = (p[1]-1)*log(d.n) - d.n/p[2]

## statistics
mean(d::NegativeBinomialExpert) = mean(Distributions.NegativeBinomial(d.n, d.p))
var(d::NegativeBinomialExpert) = var(Distributions.NegativeBinomial(d.n, d.p))
quantile(d::NegativeBinomialExpert, p) = quantile(Distributions.NegativeBinomial(d.n, d.p), p)

## Misc functions for E-Step

function _sum_densy_series(d::NegativeBinomialExpert, yl, yu)
    if isinf(yu)
        series = 0:(max(yl-1, 0))
        return d.n*(1-d.p)/d.p - sum(pdf.(d, series) .* series)[1]
    else
        series = yl:yu
        return sum(pdf.(d, series) .* series)[1]
    end
end

function _int_obs_Y_raw(d::NegativeBinomialExpert, yl, yu)
    return _sum_densy_series(d, yl, yu)
end

function _int_lat_Y_raw(d::NegativeBinomialExpert, tl, tu)
    return d.n*(1-d.p)/d.p - _sum_densy_series(d, tl, tu)
end

function _sum_denslogy_series(n_new, d::NegativeBinomialExpert, yl, yu, exposure)
    upper_finite = isinf(yu) ? quantile(d, 1-1e-8) : yu
    series = yl:(max(yl, min(yu, upper_finite+1)))
    # series = yl:yu
    return sum(pdf.(d, series) .* loggamma.(series .+ n_new*exposure))[1]
end

function _int_obs_logY_raw(n_new, d::NegativeBinomialExpert, yl, yu, exposure)
    if yl == yu
        return pdf.(d, yl) .* loggamma.(yl .+ n_new*exposure)
    else
        return _sum_denslogy_series.(n_new, d, yl, yu, exposure)
    end
end

function _int_lat_logY_raw(n_new, d::NegativeBinomialExpert, tl, tu, exposure)
    # return (tl==0 ? 0.0 : _sum_denslogy_series.(n_new, d, 0, tl)) + (isinf(tu) ? 0.0 : _sum_denslogy_series.(n_new, d, tu, Inf))
    return (tl==0 ? 0.0 : _sum_denslogy_series.(n_new, d, 0, ceil(tl)-1, exposure)) +
        (isinf(tu) ? 0.0 : _sum_denslogy_series.(n_new, d, floor(tu)+1, Inf, exposure))
end

function _negativebinomial_n_to_p(n, sum_term_zkz, sum_term_zkzy;
                        penalty = true, pen_pararms_jk = [])
    return 1 - sum_term_zkzy/(sum_term_zkz + sum_term_zkzy)
end

function _negativebinomial_optim_n(logn,
                        d_old,
                        tl, yl, yu, tu,
                        exposure,
                        z_e_obs, z_e_lat, k_e,
                        Y_e_obs, Y_e_lat;
                        penalty = true, pen_pararms_jk = [])
    # Optimization in log scale for unconstrained computation
    n_tmp = exp(logn)

    # Further E-Step
    # yl_yu_unique = unique_bounds(yl, yu)
    # int_obs_logY_tmp = _int_obs_logY_raw.(n_tmp, d_old, yl_yu_unique[:,1], yl_yu_unique[:,2])
    # logY_e_obs = exp.(-expert_ll_pos) .* int_obs_logY_tmp[match_unique_bounds(hcat(vec(yl), vec(yu)), yl_yu_unique)]
    # nan2num(logY_e_obs, 0.0) # get rid of NaN

    # tl_tu_unique = unique_bounds(tl, tu)
    # int_lat_logY_tmp = _int_lat_logY_raw.(n_tmp, d_old, tl_tu_unique[:,1], tl_tu_unique[:,2])
    # logY_e_lat = exp.(-expert_tn_bar_pos) .* int_lat_logY_tmp[match_unique_bounds(hcat(vec(tl), vec(tu)), tl_tu_unique)]
    # nan2num(logY_e_lat, 0.0) # get rid of NaN

    logY_e_obs = fill(0.0, length(yl))
    logY_e_lat = fill(0.0, length(yl))

    for i in 1:length(yl)
        d_expo = exposurize_expert(d_old, exposure = exposure[i])
        expert_ll_pos = expert_ll.(d_expo, tl[i], yl[i], yu[i], tu[i])
        expert_tn_bar_pos = expert_tn_bar.(d_expo, tl[i], yl[i], yu[i], tu[i])
        logY_e_obs[i] = exp(-expert_ll_pos) * _int_obs_logY_raw(n_tmp, d_expo, yl[i], yu[i], exposure[i])
        logY_e_lat[i] = exp(-expert_tn_bar_pos) * _int_lat_logY_raw(n_tmp, d_expo, tl[i], tu[i], exposure[i])
    end

    nan2num(logY_e_obs, 0.0) # get rid of NaN
    nan2num(logY_e_lat, 0.0) # get rid of NaN
    inf2num(logY_e_obs, 0.0) # get rid of inf
    inf2num(logY_e_lat, 0.0) # get rid of inf

    term_zkz = (z_e_obs .* n_tmp .* exposure) .+ (z_e_lat .* k_e .* n_tmp .* exposure)
    term_zkz_Y = (z_e_obs .* Y_e_obs) .+ (z_e_lat .* k_e .* Y_e_lat)
    term_zkz_logY = (z_e_obs .* logY_e_obs) .+ (z_e_lat .* k_e .* logY_e_lat)

    sum_term_zkz = sum(term_zkz)[1]
    sum_term_zkzy = sum(term_zkz_Y)[1]
    sum_term_zkzlogy = sum(term_zkz_logY)[1]

    p_tmp = _negativebinomial_n_to_p(n_tmp, sum_term_zkz, sum_term_zkzy, penalty = penalty, pen_pararms_jk = pen_pararms_jk)

    obj = sum_term_zkzlogy - sum(z_e_obs .* loggamma.(n_tmp .* exposure))[1] + sum_term_zkz*log(p_tmp) + sum_term_zkzy*log(1-p_tmp)
    p = penalty ? (pen_pararms_jk[1]-1)*log(n_tmp) - n_tmp/pen_pararms_jk[2] : 0.0
    return (obj + p) * (-1.0)
end

## EM: M-Step
function EM_M_expert(d::NegativeBinomialExpert,
                    tl, yl, yu, tu,
                    exposure,
                    z_e_obs, z_e_lat, k_e;
                    penalty = true, pen_pararms_jk = [2.0 1.0])

    # Further E-Step
    # yl_yu_unique = unique_bounds(yl, yu)
    # int_obs_Y_tmp = _int_obs_Y_raw.(d, yl_yu_unique[:,1], yl_yu_unique[:,2])
    # Y_e_obs = exp.(-expert_ll_pos) .* int_obs_Y_tmp[match_unique_bounds(hcat(vec(yl), vec(yu)), yl_yu_unique)]
    # nan2num(Y_e_obs, 0.0) # get rid of NaN

    # tl_tu_unique = unique_bounds(tl, tu)
    # int_lat_Y_tmp = _int_lat_Y_raw.(d, tl_tu_unique[:,1], tl_tu_unique[:,2])
    # Y_e_lat = exp.(-expert_tn_bar_pos) .* int_lat_Y_tmp[match_unique_bounds(hcat(vec(tl), vec(tu)), tl_tu_unique)]
    # nan2num(Y_e_lat, 0.0) # get rid of NaN
    Y_e_obs = fill(0.0, length(yl))
    Y_e_lat = fill(0.0, length(yl))

    for i in 1:length(yl)
        d_expo = exposurize_expert(d, exposure = exposure[i])
        expert_ll_pos = expert_ll.(d_expo, tl[i], yl[i], yu[i], tu[i])
        expert_tn_bar_pos = expert_tn_bar.(d_expo, tl[i], yl[i], yu[i], tu[i])
        Y_e_obs[i] = exp(-expert_ll_pos) * _int_obs_Y_raw(d_expo, yl[i], yu[i])
        Y_e_lat[i] = exp(-expert_tn_bar_pos) * _int_lat_Y_raw(d_expo, tl[i], tu[i])
    end

    nan2num(Y_e_obs, 0.0) # get rid of NaN
    nan2num(Y_e_lat, 0.0) # get rid of NaN
    inf2num(Y_e_obs, 0.0) # get rid of inf
    inf2num(Y_e_lat, 0.0) # get rid of inf

    # Update parameters
    logn_new = Optim.minimizer( Optim.optimize(x -> _negativebinomial_optim_n(x, d,
                                                tl, yl, yu, tu,
                                                exposure,
                                                z_e_obs, z_e_lat, k_e,
                                                Y_e_obs, Y_e_lat,
                                                penalty = penalty, pen_pararms_jk = pen_pararms_jk),
                                                log(d.n)-0.5, log(d.n)+0.5 )) # ,
                                                # max(log(d.n)-2.0, 0.0), log(d.n)+2.0 )) # ,
                                                # log(d.k)-2.0, log(d.k)+2.0 )) # ,
                                                # GoldenSection() ))
                                                # , rel_tol = 1e-8) )
    n_new = exp(logn_new)

    term_zkz = (z_e_obs .* n_new .* exposure ) .+ (z_e_lat .* k_e  .* n_new .* exposure)
    term_zkz_Y = (z_e_obs .* Y_e_obs) .+ (z_e_lat .* k_e .* Y_e_lat)

    sum_term_zkz = sum(term_zkz)[1]
    sum_term_zkzy = sum(term_zkz_Y)[1]

    p_new = _negativebinomial_n_to_p(n_new, sum_term_zkz, sum_term_zkzy, penalty = penalty, pen_pararms_jk = pen_pararms_jk)

    # Deal with zero mass
    if (p_new^n_new > 0.999999) || (isnan(p_new^n_new))
        p_new, n_new = d.p, d.n
    end

    return NegativeBinomialExpert(n_new, p_new)

end

## EM: M-Step, exact observations
function _negativebinomial_optim_n_exact(logn, d_old,
                        ye, exposure,
                        z_e_obs;
                        penalty = true, pen_pararms_jk = [])
    # Optimization in log scale for unconstrained computation
    n_tmp = exp(logn)

    Y_e_obs = ye

    # Further E-Step
    logY_e_obs = loggamma.(ye .+ n_tmp .* exposure)
    nan2num(logY_e_obs, 0.0) # get rid of NaN

    term_zkz = z_e_obs .* n_tmp .* exposure
    term_zkz_Y = z_e_obs .* Y_e_obs
    term_zkz_logY = z_e_obs .* logY_e_obs

    sum_term_zkz = sum(term_zkz)[1]
    sum_term_zkzy = sum(term_zkz_Y)[1]
    sum_term_zkzlogy = sum(term_zkz_logY)[1]

    p_tmp = _negativebinomial_n_to_p(n_tmp, sum_term_zkz, sum_term_zkzy, penalty = penalty, pen_pararms_jk = pen_pararms_jk)

    obj = sum_term_zkzlogy - sum(z_e_obs .* loggamma.(n_tmp .* exposure))[1] + sum_term_zkz*log(p_tmp) + sum_term_zkzy*log(1-p_tmp)
    p = penalty ? (pen_pararms_jk[1]-1)*log(n_tmp) - n_tmp/pen_pararms_jk[2] : 0.0
    return (obj + p) * (-1.0)
end

function EM_M_expert_exact(d::NegativeBinomialExpert,
                    ye, exposure,
                    # expert_ll_pos,
                    z_e_obs;
                    penalty = true, pen_pararms_jk = [1.0 Inf])

    # Update parameters
    logn_new = Optim.minimizer( Optim.optimize(x -> _negativebinomial_optim_n_exact(x, d,
                                                ye, exposure,
                                                z_e_obs,
                                                penalty = penalty, pen_pararms_jk = pen_pararms_jk),
                                                log(d.n)-0.5, log(d.n)+0.5 ))
    n_new = exp(logn_new)

    term_zkz = z_e_obs .* n_new .* exposure
    term_zkz_Y = z_e_obs .* ye

    sum_term_zkz = sum(term_zkz)[1]
    sum_term_zkzy = sum(term_zkz_Y)[1]

    p_new = _negativebinomial_n_to_p(n_new, sum_term_zkz, sum_term_zkzy, penalty = penalty, pen_pararms_jk = pen_pararms_jk)

    # Deal with zero mass
    if (p_new^n_new > 0.999999) || (isnan(p_new^n_new))
        p_new, n_new = d.p, d.n
    end

    return NegativeBinomialExpert(n_new, p_new)

end


function mass(u, q, w, v, x, t, mu, eta)
    m = [1.0, 1.0, 1.0, 1.0];
    return m;
end
function flux(u, q, w, v, x, t, mu, eta)
    gam = mu[1];
    gam1 = gam - 1.0;
    r = u[1];
    ru = u[2];
    rv = u[3];
    rE = u[4];
    r1 = 1/r;
    uv = ru*r1;
    vv = rv*r1;
    E = rE*r1;
    p = gam1*(rE-r*0.5*(uv*uv+vv*vv));
    h = E+p*r1;
    f = [ru, ru*uv+p, rv*uv, ru*h, rv, ru*vv, rv*vv+p, rv*h];
    f = reshape(f,(4,2));
    return f;
end
function source(u, q, w, v, x, t, mu, eta)
    s = [0.0, 0.0, 0.0, 0.0];
    return s;
end
function ubou(u, q, w, v, x, t, mu, eta, uhat, n, tau)
    ub = [0.0, 0.0, 0.0, 0.0];
    return ub;
end
function fbou(u, q, w, v, x, t, mu, eta, uhat, n, tau)
    f = flux(u, q, w, v, x, t, mu, eta);
    fb = f[:,1]*n[1] + f[:,2]*n[2] + tau[1]*(u-uhat);
    return fb;
end
function initu(x, mu, eta)

    t = 0.0;
    phi = 5.0;
    x1 = x[1];
    x2 = x[2];
    gam = mu[1];
    M_ref = mu[2];

    r = sqrt((x1-t)^2 + x2^2);
    u01 = (1 - ((gam-1)/(16*pi^2)) * phi^2 * exp(2*(1-r^2)) )^(1/(gam-1));
    u02 = u01 * (1 - M_ref^(-1) * phi * exp(1-r^2) * x2/(2*pi));
    u03 = u01 * M_ref^(-1) * phi * exp(1-r^2) * x1/(2*pi);
    p = u01^gam/(gam*M_ref^2);
    u04 = p/(gam-1) + 0.5 * (u02*u02 + u03*u03) / u01;

    u0 = [u01, u02, u03, u04];
    return u0;
end


"""
    Quadratic(trans::AffineMap, length, ay, by, cy, az, bz, cz)

Construct a `Quadratic` object. The `AffineMap` brings the quadratic into a
frame in which the `x` axis is approximately aligned with the quadratic. In this frame,
the `x` values of the quadratic lie between zero and `length`, and the `y` and `z`
components are parameterized by `x` as:

y = ay + by*x + cy*x^2
z = az + bz*x + cz*x^2
"""
struct Quadratic{Tfm <: AffineTransformation, T <: Real}
    transform::Tfm # transformation from global frame to quadratic's frame
    length::T  # replace with xmin, xmax?
    ay::T
    by::T
    cy::T
    az::T
    bz::T
    cz::T
end

"""
    Quadratic(p_start, p_mid, p_end)

Takes three coordinates for the start, somewhere in the middle, and the end of a quadratic
and constructs the appropriate `Quadratic` object.
"""
function Quadratic(p_start::StaticVector{3,T}, p_mid::StaticVector{3,T}, p_end::StaticVector{3,T}) where {T <: Real}
    dir = SVector(p_end[1] - p_start[1], p_end[2] - p_start[2])
    len = norm(dir) # horizontal length
    @static if VERSION < v"0.7"
        θ = atan2(dir[2], dir[1]) # rotation in x-y plane
    else
        θ = atan(dir[2], dir[1]) # rotation in x-y plane
    end
    tform = inv(AffineMap(RotZ(θ), p_start))

    p2_mid = tform(p_mid)
    p2_end = tform(p_end)

    x1 = p2_mid[1]
    y1 = p2_mid[2]
    z1 = p2_mid[3]

    x2 = p2_end[1]
    y2 = p2_end[2]
    z2 = p2_end[3]

    by = (x1*y2/x2 - x2*y1/x1)/(x1 - x2)
    bz = (x1*z2/x2 - x2*z1/x1)/(x1 - x2)

    cy = (y2/x2 - y1/x1)/(x2 - x1)
    cz = (z2/x2 - z1/x1)/(x2 - x1)

    ay = zero(cy)
    az = zero(cz)

    return Quadratic(tform, len, ay, by, cy, az, bz, cz)
end

# mop up other types
function Quadratic(p1::AbstractVector{T}, p2::AbstractVector{T}, p3::AbstractVector{T}) where {T <: Real}
    Quadratic(SVector{3,T}(p1), SVector{3,T}(p2), SVector{3,T}(p3))
end

eltype(::Quadratic{T}) where {T} = SVector{3,T}
eltype(::Type{<:Quadratic{T}}) where {T} = SVector{3,T}

# Use getindex to get points along the line
function getindex(q::Quadratic, x)
    inv(q.transform)(SVector(x, q.ay+q.by*x+q.cy*x*x, q.az+q.bz*x+q.cz*x*x))
end
length(q::Quadratic) = q.length  # matches getindex and makes sense...
if VERSION < v"0.7"
    endof(q::Quadratic) = length(q) # e.g. q[end - 1] is the point 1 metre from top of quadratic
else
    lastindex(q::Quadratic) = length(q) # e.g. q[end - 1] is the point 1 metre from top of quadratic
end

getindex(q::Quadratic, v::AbstractVector) = map(x -> q[x], v)

# Transformations (which compose into AffineMap's)
# Note: Julia's dispatch system makes it difficult to support all `Transformations`.
function (trans::Translation{V})(q::Quadratic) where {V}
    Quadratic(q.transform ∘ inv(trans), q.length, q.ay, q.by, q.cy, q.az, q.bz, q.cz)
end

function (trans::LinearMap{M})(q::Quadratic) where {M}
    Quadratic(q.transform ∘ inv(trans), q.length, q.ay, q.by, q.cy, q.az, q.bz, q.cz)
end

function (trans::AffineMap{M,V})(q::Quadratic) where {M,V}
    Quadratic(q.transform ∘ inv(trans), q.length, q.ay, q.by, q.cy, q.az, q.bz, q.cz)
end


# script for Fig.5

using MultiscaleGraphSignalTransforms, LightGraphs, Plots; gr(dpi = 200)
import WaveletsExt: wiggle

## Build Graph
N = 512; G = path_graph(N)
X = zeros(N,2); X[:, 1] = 1:N
L = Matrix(laplacian_matrix(G))
𝛌, 𝚽 = eigen(L); 𝚽 = 𝚽 .* sign.(𝚽[1,:])'
W = 1.0 * adjacency_matrix(G)

## Build NGWPs
Gstar_Sig = GraphSig(W)
G_Sig = GraphSig(W, xy = X)
GP_dual = partition_tree_fiedler(Gstar_Sig; swapRegion = false)
GP_primal = pairclustering(𝚽, GP_dual)

@time VM_NGWP = vm_ngwp(𝚽, GP_dual)

#################### Fig.5
j = 5
for k in [1, 2, 5]
   WW = sort_wavelets(VM_NGWP[GP_dual.rs[k, j]:(GP_dual.rs[k + 1, j] - 1), j, :]')
   if k == 2
      WW[:, end] *= -1
   end
   plt = wiggle(WW; sc = 0.75)
   # savefig(plt, joinpath(@__DIR__, "../paperfigs/Path512_VM_NGWP_j$(j-1)k$(k-1).png"))
end
current()


using Test
using LinearAlgebra
using SparseArrays
using CompScienceMeshes

for T in [Float32, Float64]
    tol = eps(T) * 10^3
    third = one(T)/3

    # test creation of a rectangle
    local m = meshrectangle(T(1.0),T(1.0),T(0.5));
    @test numvertices(m) == 9
    @test numcells(m) == 8

    # test edge detection
    edges = skeleton(m, 1)
    @test numcells(edges) == 16

    # test volume pair construction from a list of edges
    vp = cellpairs(m,edges)
    internal_edges = count(x->x>0, vp[2,:])
    @test internal_edges == 8

    # test the relative orientation routine
    @test CompScienceMeshes.relorientation(index(1,2,3),index(1,2,3,4)) == +4
    @test CompScienceMeshes.relorientation(index(2,3,4),index(1,2,3,4)) == -1
    @test CompScienceMeshes.relorientation(index(1,3,2),index(1,2,3,4)) == -4
    @test CompScienceMeshes.relorientation(index(2,4,3),index(1,2,3,4)) == +1

    o, e1, e2, e3 = euclidianbasis(3)
    cell = simplex(o, e1, e2)

    A  = volume(cell)
    @test abs(A - T(0.5)) < tol

    mp = neighborhood(cell,[third, third]);
    r = cartesian(mp)
    @test norm(r - point(third, third, zero(T))) < tol

    # repeat the test but now on a random cell
    randpoint() = point(2*rand(T)-1, 2*rand(T)-1, 2*rand(T)-1)
    v1 = randpoint()
    v2 = randpoint()
    v3 = randpoint()
    cell = simplex(v1, v2, v3)

    mp = neighborhood(cell,[third, third]);
    r = cartesian(mp)

    @test norm(third*(v1 + v2 + v3) - r) < tol
    @test abs(dot(cross(r-v1, r-v2), r-v3)) < tol

    # test that the normal is directed outwards
    m = meshcircle(T(1.0), T(2π/51))
    #for i in 1:numcells(m)
    for cl in cells(m)
        #p = chart(m, cells(m,i))
        v = vertices(m, cl)
        p = simplex(v)
        _c = (p.vertices[1] + p.vertices[2]) / 2
        @test dot(_c, p.normals[1]) > 0
    end
end

## test cellpairs on non-oriented meshes
using CompScienceMeshes
using Test

for T in [Float32, Float64]
    p1 = point(T,1,0,0)
    p2 = point(T,0,1,0)
    p3 = point(T,-1,0,0)
    p4 = point(T,0,-1,0)

    i1 = index(1,2,3)
    i2 = index(1,4,2)

    local m = Mesh([p1,p2,p3,p4], [i1,i2])
    e = skeleton(m,1)

    @test numcells(m) == 2
    @test numcells(e) == 5

    cps = cellpairs(m,e)

    #Test unstructed mesh rectangle
    r1 = meshrectangle(T(1.0),T(1.0),T(1/1))
    # r2 = meshrectangle(1.0,1.0,1/1,structured=false)
    r2 = readmesh(joinpath(@__DIR__,"assets/rectangle1.in"),T=T)

    @test numcells(r1) == 2
    @test numcells(r2) == 4
    @test numvertices(r2) == 5
    @test numcells(boundary(r2)) == 4
    @test numcells(CompScienceMeshes.interior(r2)) == 4
    @test numcells(boundary(r1)) == numcells(boundary(r2))

    #Test mesh cuboid parts
    deltaT = T(1/1)
    box = meshcuboid(T(1.0),T(1.0),T(1.0),deltaT)
    top = meshcuboid(T(1.0),T(1.0),T(1.0),deltaT,physical="TopPlate")
    sides = meshcuboid(T(1.0),T(1.0),T(1.0),deltaT,physical="SidePlates")
    bot = meshcuboid(T(1.0),T(1.0),T(1.0),deltaT,physical="BottomPlate")
    obox = meshcuboid(T(1.0),T(1.0),T(1.0),deltaT,physical="OpenBox")

    @test numcells(boundary(box)) == 0
    @test numcells(boundary(top)) == numcells(boundary(bot))  == 4
    @test numvertices(box) == 14
    @test numcells(box) == 24
    @test numcells(skeleton(box,1)) == 36
    @test numcells(top) + numcells(bot) + numcells(sides) == numcells(box)
    @test numcells(obox) == numcells(box) - numcells(top)

    e1 = skeleton(top,1)
    E1 = skeleton(box,1)

    e1_cells = cells(e1)
    E1_cells = cells(E1)

    # cols = vec([6 16 17 13 18 19 20 21])
    # Z11 = sparse(collect(1:8),cols,ones(8),numcells(e1),numcells(E1))
    if T == Float64
        S11 = CompScienceMeshes.embedding(e1,E1)
        @test size(S11) == (length(e1), length(E1))
        vals = nonzeros(S11)
        rows = rowvals(S11)
        for col in axes(S11,2)
            for k in nzrange(S11,col)
                row = rows[k]
                val = vals[k]
                val == 0 && continue
                @test e1_cells[row] == E1_cells[col]
            end
        end
    end
# @test S11 == Z11
end


module Brownian

using Distributions
using PDMats
using StatsBase
using FFTW

import Base: convert
import Random: rand!, rand
import Distributions: VariateForm, Univariate, Multivariate, ValueSupport, Discrete, Continuous, Sampleable
import StatsBase: IntegerVector, autocov!, autocov

export BrownianMotion, FBM, FGN, rand!, rand, autocov, chol_update

include("BrownianMotion.jl")
include("FBM.jl")

end


using DSGE
using Test, BenchmarkTools
using JLD2

path = dirname(@__FILE__)

m = BondLabor()

# Steady-state computation
steadystate!(m)
@btime steadystate!(m)

file = jldopen("$path/reference/steady_state.jld2", "r")
saved_ell  = read(file, "ell")
saved_c    = read(file, "c")
saved_η    = read(file, "eta")
saved_μ    = read(file, "mu")
saved_β    = read(file, "beta")
saved_χss  = read(file, "chi_ss")
close(file)

@testset "Check steady state outputs" begin
    @test saved_ell ≈ m[:lstar].value
    @test saved_c   ≈ m[:cstar].value
    @test saved_η   ≈ m[:ηstar].value
    @test saved_μ   ≈ m[:μstar].value
    @test saved_β   ≈ m[:βstar].value
    @test saved_χss ≈ m[:χstar].value
end

# Jacobian computation
m.testing = true        # So that it will test against the unnormalized Jacobian
JJ = DSGE.jacobian(m)
@btime JJ = DSGE.jacobian(m)

file = jldopen("$path/reference/jacobian.jld2", "r")
saved_JJ  = read(file, "JJ")
close(file)

nx = get_setting(m, :nx)
ns = get_setting(m, :ns)

# we will always order things XP YP X Y
# convention is that capital letters generally refer to indices
MUP  = 1:nx*ns
ZP   = nx*ns+1
ELLP = nx*ns+2:2*nx*ns+1
RP   = 2*nx*ns+2

MU   = 2*nx*ns+3:3*nx*ns+2
Z    = 3*nx*ns+3
ELL  = 3*nx*ns+4:4*nx*ns+3
R    = 4*nx*ns+4

# create objects needed for solve.jl
F1 = 1:nx*ns # euler eqn
F2 = nx*ns+1:2*nx*ns # KF
F3 = 2*nx*ns+1:2*nx*ns+1 # mkt ckr
F4 = 2*nx*ns+2:2*nx*ns+2 # z

@testset "Check jacobian outputs" begin
    @testset "Euler Equation" begin
        @test saved_JJ[F1, ZP] ≈ JJ[F1, ZP]
        @test saved_JJ[F1, ELLP] ≈ JJ[F1, ELLP]
        @test saved_JJ[F1, RP] ≈ JJ[F1, RP]
        @test saved_JJ[F1, Z] ≈ JJ[F1, Z]
        @test saved_JJ[F1, ELL] ≈ JJ[F1, ELL]
        @test saved_JJ[F1, R] ≈ JJ[F1, R]
    end

    @testset "Kolmogorov Forward Equation" begin
        @test saved_JJ[F2, MUP] ≈ JJ[F2, MUP]
        @test saved_JJ[F2, MU] ≈ JJ[F2, MU]
        @test saved_JJ[F2, Z] ≈ JJ[F2, Z]
        @test saved_JJ[F2, ELL] ≈ JJ[F2, ELL]
        @test saved_JJ[F2, R] ≈ JJ[F2, R]
    end

    @testset "Market clearing condition" begin
        @test saved_JJ[F3, MU] ≈ JJ[F3, MU]
        @test saved_JJ[F3, Z] ≈ JJ[F3, Z]
        @test saved_JJ[F3, ELL] ≈ JJ[F3, ELL]
        @test saved_JJ[F3, R] ≈ JJ[F3, R]
    end

    @testset "Technology process" begin
        @test saved_JJ[F4, ZP] ≈ JJ[F4, ZP]
        @test saved_JJ[F4, Z] ≈ JJ[F4, Z]
    end
end

# Solve
m.testing = false      # So the Jacobian will be normalized within the klein solution
gx, hx = klein(m)
@btime klein(m)

@load "$path/reference/solve.jld2" saved_gx saved_hx

@testset "Check solve outputs" begin
    @test saved_gx  ≈ gx
    @test saved_hx  ≈ hx
end


# test greeks

let

    # Setup
    d1 = today()
    d2 = d1 + Day(150)
    c1 = EuropeanCall(d2, SingleStock(), 56.5USD)
    core1 = CoreModel(d1, 47.32USD, 0.0, 0.0)
    c2 = EuropeanCall(d2, SingleStock(), 56.5)
    core2 = CoreModel(d1, 47.32, 0.0, 0.0)
    gbm1 = GeomBMModel(core1, 0.1)
    gbm2 = GeomBMModel(core2, 0.1)
    Random.seed!(1)
    mc1 = montecarlo(gbm1, d1:Day(1):d2, 10000)
    Random.seed!(1)
    mc2 = montecarlo(gbm2, d1:Day(1):d2, 10000)

    # Compare with math formulae
    @test delta(gbm1, c1; autodiff = true) ≈ delta(gbm1, c1)
    @test vega(gbm1, c1; autodiff = true) ≈ vega(gbm1, c1)
    @test delta(gbm2, c2; autodiff = true) ≈ delta(gbm2, c2)
    @test vega(gbm2, c2; autodiff = true) ≈ vega(gbm2, c2)
    @test greek(gbm1, c1; metric = :vol) ≈ vega(gbm1, c1)
    @test greek(gbm2, c2; metric = :vol) ≈ vega(gbm2, c2)
    @test greek(gbm1, c1; metric = :strike) ≈ delta(gbm1, c1)
    @test greek(gbm2, c2; metric = :strike) ≈ delta(gbm2, c2)

    # Monte Carlo models greeks - vega
    let
        vol = gbm1.volatility + 1e-8
        gbm3 = GeomBMModel(gbm1.core, vol)
        Random.seed!(1)
        mc3 = montecarlo(gbm3, mc1.dates, size(mc1.paths, 1))
        x = (value(mc3, c1) - value(mc1, c1)) / 1e-8
        Random.seed!(1)
        @test isapprox(x, vega(gbm1, c1, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x, greek(mc1, c1; metric = :vol), rtol = 1e-4)

        vol = gbm2.volatility + 1e-8
        gbm4 = GeomBMModel(gbm2.core, vol)
        Random.seed!(1)
        mc4 = montecarlo(gbm4, mc2.dates, size(mc2.paths, 1))
        x = (value(mc4, c2) - value(mc2, c2)) / 1e-8
        Random.seed!(1)
        @test isapprox(x, vega(gbm2, c2, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x, greek(mc2, c2; metric = :vol), rtol = 1e-4)
    end

    # Monte Carlo models greeks - delta
    let
        s = gbm1.core.startprice + (1e-8 * USD)
        vol = gbm1.volatility
        gbm3 = GeomBMModel(gbm1.core.carrycurve.reference_date, s,
                            gbm1.core.yieldcurve.rate, gbm1.core.carrycurve.rate, vol)
        Random.seed!(1)
        mc3 = montecarlo(gbm3, mc1.dates, size(mc1.paths, 1))
        x = (value(mc3, c1) - value(mc1, c1)) / 1e-8
        Random.seed!(1)
        @test isapprox(x.val, delta(gbm1, c1, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x.val, greek(mc1, c1; metric = :strike), rtol = 1e-4)

        s = gbm2.core.startprice + 1e-8
        vol = gbm2.volatility
        gbm4 = GeomBMModel(gbm2.core.carrycurve.reference_date, s,
                            gbm2.core.yieldcurve.rate, gbm2.core.carrycurve.rate, vol)
        Random.seed!(1)
        mc4 = montecarlo(gbm4, mc2.dates, size(mc2.paths, 1))
        x = (value(mc4, c2) - value(mc2, c2)) / 1e-8
        Random.seed!(1)
        @test isapprox(x, delta(gbm2, c2, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x, greek(mc2, c2; metric = :strike), rtol = 1e-4)
    end

    # Compare rho with forward difference
    let
        r = gbm1.core.yieldcurve.rate + 1e-8
        gbm3 = GeomBMModel(gbm1.core.carrycurve.reference_date, gbm1.core.startprice,
                                 r, gbm1.core.carrycurve.rate, gbm1.volatility)
        x = (value(gbm3, c1) - value(gbm1,c1)) / 1e-8
        @test isapprox(x, rho(gbm1, c1), rtol = 1e-4)
        @test isapprox(x, greek(gbm1, c1; metric = :interest), rtol = 1e-4)

        Random.seed!(1)
        mc3 = montecarlo(gbm3, mc1.dates, size(mc1.paths,1))
        x = (value(mc3, c1) - value(mc1,c1)) / 1e-8
        Random.seed!(1)
        @test isapprox(x, rho(gbm1, c1, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x, greek(mc1, c1; metric = :interest), rtol = 1e-4)

        gbm4 = GeomBMModel(gbm2.core.carrycurve.reference_date, gbm2.core.startprice,
                                 r, gbm2.core.carrycurve.rate, gbm2.volatility)
        x = (value(gbm4, c2) - value(gbm2,c2)) / 1e-8
        @test isapprox(x, rho(gbm2, c2), rtol = 1e-4)
        @test isapprox(x, greek(gbm2, c2; metric = :interest), rtol = 1e-4)

        Random.seed!(1)
        mc4 = montecarlo(gbm4, mc2.dates, size(mc2.paths,1))
        x = (value(mc4, c2) - value(mc2,c2)) / 1e-8
        Random.seed!(1)
        @test isapprox(x, rho(gbm2, c2, MonteCarloModel, d1:Day(1):d2, 10000), rtol = 1e-4)
        #srand(1)
        #@test isapprox(x, greek(mc2, c2; metric = :interest), rtol = 1e-4)
    end

    # Compare gamma with central difference
    let
        s1 = gbm1.core.startprice + (1e-4 * USD)
        s2 = gbm1.core.startprice - (1e-4 * USD)
        gbm31 = GeomBMModel(gbm1.core.carrycurve.reference_date, s1,
                            gbm1.core.yieldcurve.rate, gbm1.core.carrycurve.rate, gbm1.volatility)
        gbm32 = GeomBMModel(gbm1.core.carrycurve.reference_date, s2,
                            gbm1.core.yieldcurve.rate, gbm1.core.carrycurve.rate, gbm1.volatility)
        x = (value(gbm31,c1) + value(gbm32,c1) - (2 * value(gbm1, c1))) / (1e-4)^2
        @test isapprox(x.val, Miletus.gamma(gbm1, c1), rtol = 1e-4)
        @test isapprox(x.val, greek(gbm1, c1; metric = :strike, n = 2), rtol = 1e-4)

        Random.seed!(1)
        mc31 = montecarlo(gbm31, mc1.dates, size(mc1.paths, 1))
        Random.seed!(1)
        mc32 = montecarlo(gbm32, mc1.dates, size(mc1.paths, 1))
        x = (value(mc31,c1) + value(mc32,c1) - (2 * value(mc1, c1))) / (1e-4)^2
        Random.seed!(1)
        @test x.val - Miletus.gamma(gbm1, c1, MonteCarloModel, d1:Day(1):d2, 10000) < 1e-8
        Random.seed!(1)
        @test x.val - greek(gbm1, c1; metric = :strike, n = 2) < 1e-8

        s1 = gbm2.core.startprice + 1e-4
        s2 = gbm2.core.startprice - 1e-4
        gbm41 = GeomBMModel(gbm2.core.carrycurve.reference_date, s1,
                            gbm2.core.yieldcurve.rate, gbm2.core.carrycurve.rate, gbm2.volatility)
        gbm42 = GeomBMModel(gbm2.core.carrycurve.reference_date, s2,
                            gbm2.core.yieldcurve.rate, gbm2.core.carrycurve.rate, gbm2.volatility)
        x = (value(gbm41,c2) + value(gbm42,c2) - (2 * value(gbm2, c2))) / (1e-4)^2
        @test isapprox(x, Miletus.gamma(gbm2, c2), rtol = 1e-4)
        @test isapprox(x, greek(gbm2, c2; metric = :strike, n = 2), rtol = 1e-4)

        Random.seed!(1)
        mc41 = montecarlo(gbm41, mc2.dates, size(mc2.paths, 1))
        Random.seed!(1)
        mc42 = montecarlo(gbm42, mc2.dates, size(mc2.paths, 1))
        x = (value(mc41,c2) + value(mc42,c2) - (2 * value(mc2, c2))) / (1e-4)^2
        Random.seed!(1)
        @test x - Miletus.gamma(gbm2, c2, MonteCarloModel, d1:Day(1):d2, 10000) < 1e-8
        Random.seed!(1)
        @test x - greek(gbm2, c2; metric = :strike, n = 2) < 1e-8
    end

end


using StatsModels
using GLM
using DataFrames
using CSV
using Statistics
using MLBase

# 1. Priprema i provera podataka
# Ucitavanje podataka
data = DataFrame(CSV.File("tacke40c.csv"))
fm = @formula(y ~ x)

# Podesavanje KFold kros validacije
k = 5
a = collect(Kfold(length(data.x), k))
averageAbsMeanErrorTest = 0.0
for i in 1:k
    # Podela na skup za obuku i testiranja, i predvidjanje
    dataTrain = data[a[i], :]
    dataTest = data[setdiff(1:end, a[i]), :]
    linearRegressor = lm(fm, dataTrain)
    dataPredictedTest = predict(linearRegressor, dataTest)
    # Racunanje gresaka za skup za obuku
    errorsTest = dataTest.y-dataPredictedTest
    absMeanErrorTest = mean(abs.(errorsTest))
    # Racunanje MSE i RMSE (Root Mean Square Error)
    println("Prosecna absolutna greska za test $i je: $absMeanErrorTest")
    global averageAbsMeanErrorTest += absMeanErrorTest
end
averageAbsMeanErrorTest /= k
println("Prosecna absolutna greska za sva testiranja je: $averageAbsMeanErrorTest")


"""
---
Structure for OFDM
# --- Syntax
- nFFT		: FFT size [Int]
- nCP		: Cyclic prefix size [Int]
- allocatedSubcarriers	: Vector of allocated subbcarriers [Array{Int}]
# ---
# v 1.0 - Robin Gerzaguet.
"""
struct StrucOFDM <: Waveform
	nFFT::Int;
	nCP::Int;
	allocatedSubcarriers::Array{Int};
end

"""
---
Create OFDM structure
# --- Syntax
ofdm	= initOFDM(nFFT,nCP,allocatedSubcarriers)
# --- Input parameters
- nFFT	  : FFT size [Int]
- nCP	  : Cyclic prefix size [Int]
- allocatedSubcarrier	: Vector of allocated subbcarriers [Array{Int}]
# --- Output parameters
- ofdm	  : OFDM structure [StrucOFDM]
# ---
# v 1.0 - Robin Gerzaguet.
"""
function initOFDM(nFFT,nCP,allocatedSubcarriers)
	# ---Checking FFT size
	if maximum(allocatedSubcarriers) > nFFT || length(allocatedSubcarriers) > nFFT
		error("Subcarrier allocation is impossible");
	end
	# --- Create the OFDM structure
	return StrucOFDM(nFFT,nCP,allocatedSubcarriers)
end

"""
---
Create OFDM signal in time domain based on input T/F matrix and OFDM parameters

qamMat is a complex symbol matrix (for instance QPSK) of size length(allocatedSubcarriers) x nbSymb

The output signal in time domain is of size (nFFT+nCP)xnbSymb.
# --- Syntax
  sigId = ofdmSigGen(qamMat,nFFT,nCP,allocatedSubcarriers)
# --- Input parameters
- qamMat  : Complex T/F matrix to map [Array{Float64},length(allocatedSubcarriers),nbSymb]
- nFFT	  : FFT size [Int]
- nCP	  : Cylic prefix size [Int]
- allocatedSubcarrier	: Vector of allocated subbcarriers [Array{Int}]
# --- Output parameters
- sigId	  : Signal in time domain [Array{Complex{Float64}},(nFFT+nCP)xnbSymb]
# ---
# v 1.0 - Robin Gerzaguet.
"""
function ofdmSigGen(qamMat,nFFT,nCP,allocatedSubcarriers)
	nbSymb = size(qamMat,2);
	sigId  = zeros(Complex{Float64},(nFFT+nCP)*nbSymb);
	ofdmSigGen!(sigId,qamMat,nFFT,nCP,allocatedSubcarriers);
	return sigId;
end

"""
---
Populate a OFDM signal in time domain based on input T/F matrix and OFDM parameters

qamMat is a complex symbol matrix (for instance QPSK) of size length(allocatedSubcarriers) x nbSymb

The output signal in time domain is of size (nFFT+nCP)xnbSymb.
# --- Syntax
  ofdmSigGen!(sigId,qamMat,nFFT,nCP,allocatedSubcarriers)
# --- Input parameters
- sigId	  : Signal in time domain [Array{Complex{Float64}},(nFFT+nCP)xnbSymb]
- qamMat  : Complex T/F matrix to map [Array{Float64},length(allocatedSubcarriers),nbSymb]
- nFFT	  : FFT size [Int]
- nCP	  : Cylic prefix size [Int]
- allocatedSubcarrier	: Vector of allocated subbcarriers [Array{Int}]
# --- Output parameters
- []
# ---
# v 1.0 - Robin Gerzaguet.
"""
function ofdmSigGen!(sigId,qamMat,nFFT,nCP,allocatedSubcarriers)
	# Set 4 core for FFT computation
	#FFTW.set_num_threads(4)
	# --- Getting parameters
	nbSymb		= size(qamMat,2); 		# --- Applied on x symbols
	symbSize	= nFFT + nCP; 			# --- Symbol size
	nSize 		= symbSize*nbSymb; 		# --- Total burst length
	# --- Mapping to nFFT elements
	qamCurr		= zeros(Complex{Float64},nFFT,nbSymb);
	qamCurr[allocatedSubcarriers,:] .= qamMat;
	# --- Switch to time domain
	ifft!(qamCurr,1);
	# --- Inserting Cyclic prefix
	#sigId	 .= reshape([qamCurr[end+1-nCP:end,:] ; qamCurr], symbSize*nbSymb,1);
	for iN = 1 : 1 : nbSymb
		# --- Classic cyclic
		sigId[ (iN-1)*symbSize +  nCP .+ (1:nFFT)] .= qamCurr[:,iN];
		# --- CP insertion
		sigId[ (iN-1)*symbSize .+ (1:nCP)] .= qamCurr[end-nCP+1:end,iN];
	end
end

# --- MD is waveform structure is given
"""
---
Create OFDM signal in time domain based on input T/F matrix and OFDM structure

qamMat is a complex symbol matrix (for instance QPSK) of size length(allocatedSubcarriers) x nbSymb

The output signal in time domain is of size (nFFT+nCP)xnbSymb.
# --- Syntax
  sigId = ofdmSigGen(qamMat,nFFT,nCP,allocatedSubcarriers)
# --- Input parameters
- qamMat  : Complex T/F matrix to map [Array{Float64},length(allocatedSubcarriers),nbSymb]
- ofdm	  : OFDM structure [StrucOFDM]
# --- Output parameters
- sigId	  : Signal in time domain [Array{Complex{Float64}},(nFFT+nCP)xnbSymb]
# ---
# v 1.0 - Robin Gerzaguet.
"""
function ofdmSigGen(qamMat,ofdm::StrucOFDM)
	return ofdmSigGen(qamMat,ofdm.nFFT,ofdm.nCP,ofdm.allocatedSubcarriers);
end

# --- MD is waveform structure is given
"""
---
Populate a  OFDM signal in time domain based on input T/F matrix and OFDM structure

qamMat is a complex symbol matrix (for instance QPSK) of size length(allocatedSubcarriers) x nbSymb

The output signal in time domain is of size (nFFT+nCP)xnbSymb.
# --- Syntax
  ofdmSigGen!(sigId,qamMat,nFFT,nCP,allocatedSubcarriers)
# --- Input parameters
- sigId	  : Signal in time domain [Array{Complex{Float64}},(nFFT+nCP)xnbSymb]
- qamMat  : Complex T/F matrix to map [Array{Float64},length(allocatedSubcarriers),nbSymb]
- ofdm	  : OFDM structure [StrucOFDM]
# --- Output parameters
- []
# ---
# v 1.0 - Robin Gerzaguet.
"""
function ofdmSigGen!(sigId,qamMat,ofdm::StrucOFDM)
	return ofdmSigGen!(sigId,qamMat,ofdm.nFFT,ofdm.nCP,ofdm.allocatedSubcarriers);
end


export pca, qpca, nnmf, rpca, kmeans

# principal components analysis
# minimize ||A - XY||^2
function pca(A::AbstractArray, k::Int; kwargs...)
	loss = QuadLoss()
	r = ZeroReg()
	return GLRM(A,loss,r,r,k; kwargs...)
end

# quadratically regularized principal components analysis
# minimize ||A - XY||^2 + scale*||X||^2 + scale*||Y||^2
function qpca(A::AbstractArray, k::Int; scale=1.0::Float64, kwargs...)
	loss = QuadLoss()
	r = QuadReg(scale)
	return GLRM(A,loss,r,r,k; kwargs...)
end

# nonnegative matrix factorization
# minimize_{X>=0, Y>=0} ||A - XY||^2
function nnmf(A::AbstractArray, k::Int; kwargs...)
	loss = QuadLoss()
	r = NonNegConstraint()
	GLRM(A,loss,r,r,k; kwargs...)
end

# k-means
# minimize_{columns of X are unit vectors} ||A - XY||^2
function kmeans(A::AbstractArray, k::Int; kwargs...)
	loss = QuadLoss()
	ry = ZeroReg()
	rx = UnitOneSparseConstraint()
	return GLRM(A,loss,rx,ry,k; kwargs...)
end

# robust PCA
# minimize HuberLoss(A - XY) + scale*||X||^2 + scale*||Y||^2
function rpca(A::AbstractArray, k::Int; scale=1.0::Float64, kwargs...)
	loss = HuberLoss()
	r = QuadReg(scale)
	return GLRM(A,loss,r,r,k; kwargs...)
end


push!(LOAD_PATH, string(pwd(),"/src"))

using Revise
using PyPlot
using ModStokesFastDirect
import AnalyticDomains
import PUX
include("../src/plottools.jl")

mss = ModifiedStokesSolver
unitcircle = AnalyticDomains.starfish(n_arms=3, amplitude=0.00)

function estimate_fourier_resolution(U)
    Uhat = fftshift(fft(U))
    Ures = max( norm(Uhat[1:2,:], Inf),
                norm(Uhat[end-1:end,:], Inf),
                norm(Uhat[:,1:2], Inf),
                norm(Uhat[:,end-1:end], Inf) ) / maximum(abs.(Uhat))
    return Ures
end

curve = AnalyticDomains.starfish()
Ngrid = 800
numpanels = 400
pux_R = 0.15

# Dev
# curve = unitcircle
# Ngrid = 100
# numpanels = 100

panelorder = 16
method = :dense_gmres
pux_ep = 2.0

plotext = false
ploterror=true

# Define ref problem (linear oscillatory)
k1 = 1.0
k2 = 1.0
ksq = k1^2 + k2^2
phi_k(x, y) = exp(1im*(k1*x + k2*y))
fhat1 = 1.0
fhat2 = -2.0
kdotfhat = k1*fhat1 + k2*fhat2
# rhs
ffunc1(x, y) = real(fhat1*phi_k(x,y))
ffunc2(x, y) = real(fhat2*phi_k(x,y))
# solution
ufunc1(x, y, alpha) = real( (ksq*fhat1 - k1*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y) )
ufunc2(x, y, alpha) = real( (ksq*fhat2 - k2*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y) )
# derivatives of solution
ufunc1x(x, y, alpha) = real( (ksq*fhat1 - k1*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y)*1im*k1 )
ufunc1y(x, y, alpha) = real( (ksq*fhat1 - k1*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y)*1im*k2 )
ufunc2x(x, y, alpha) = real( (ksq*fhat2 - k2*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y)*1im*k1 )
ufunc2y(x, y, alpha) = real( (ksq*fhat2 - k2*kdotfhat)/(ksq*(ksq+alpha^2))*phi_k(x,y)*1im*k2 )

function solve(pux_R, alpha)
    info(">>>>>>>>>>>>>>>>>>>>>> alpha=$alpha")
    @show Ngrid

    # Discretize
    println("* Discretizing")
    dcurve = mss.discretize(curve, numpanels, panelorder, equal_arclength=true)

    if method==:fastdirect
        # Fast direct prep
        println("* Fast direct build")
        @time Ainv = compressed_system_matrix_inverse(dcurve, alpha, 64, fastdirect_TOL)
    elseif method==:fmm
        # GMRES prep
        println("* GMRES FMM prep")
        @time flhs = mss.system_matvec(dcurve, alpha)
    elseif method==:dense_lu
        println("* Dense matrix assembly")
        @time MAT = mss.system_matrix(dcurve, alpha)
        println("* Dense matrix LU")
        @time MATL, MATU, MATp = lu(MAT)
    elseif method==:dense_gmres
        println("* Dense matrix assembly")
        @time MAT = mss.system_matrix(dcurve, alpha)
    else
        error("unknown method")
    end

    println("* PUX precompute")
    @time PUXStor, X, Y, Lgrid, Ngrid_new, interior = PUX.pux_precompute(curve, dcurve, Ngrid, pux_ep, R=pux_R)
    xbdry = dcurve.points[1,:]
    ybdry = dcurve.points[2,:]
    interior_points = [X[interior]' ; Y[interior]']
    numeval = size(interior_points, 2)
    println("* FMM tree precompute")
    @time fmm_precomp = mss.doublelayer_precomp(dcurve, interior_points, alpha)
    println("* Near eval precompute")
    @time near_weights = mss.doublelayer_near_weights(dcurve, interior_points, alpha; ifgrad=true)
    hgrid = Lgrid/Ngrid_new

    # Setup test problem: BCs and RHS
    F1 = ffunc1.(X,Y)
    F2 = ffunc2.(X,Y)
    ubc1 = ufunc1.(xbdry, ybdry, alpha)
    ubc2 = ufunc2.(xbdry, ybdry, alpha)

    # Extend RHS
    #println("* PUX eval")
    tic()
    FEXT1, FEXT2 = PUX.pux_eval_vec(F1, F2, PUXStor)
    time_pux = toq()
    # Compute particular solution
    #println("* Periodic FFT Solve")
    tic()
    U1, U2, upbdry1, upbdry2, dU1dx, dU1dy, dU2dx, dU2dy =
        mss.per_modstokes(FEXT1, FEXT2, Lgrid, alpha, xbdry, ybdry, ifgrad=true, zerok0=true)
    time_fft = toq()
    # Estimate particular solution resolution
    UP_resolution =  max(estimate_fourier_resolution(U1),
                         estimate_fourier_resolution(U2))
    UPgrad_resolution = max( estimate_fourier_resolution(dU1dx),
                             estimate_fourier_resolution(dU1dy),
                             estimate_fourier_resolution(dU2dx),
                             estimate_fourier_resolution(dU2dy) )
    # Integral equation RHS
    rhs1 = ubc1 - upbdry1
    rhs2 = ubc2 - upbdry2
    rhs = vec([rhs1'; rhs2'])
    # Solve integral equation
    tic()
    if method==:fastdirect
        #println("* Fast direct solve")
        dsol = apply_compressed_matrix(Ainv, rhs)
        density = reshape(dsol, 2, :)
    elseif method==:fmm
        #println("* GMRES FMM solve")
        density = mss.gmres_solve(dcurve, rhs, alpha, flhs=flhs)
    elseif method==:dense_lu
        #println("* LU apply")
        dsol = MATU\(MATL\rhs[MATp])
        density = reshape(dsol, 2, :)
    elseif method==:dense_gmres
        println("* GMRES dense solve")
        @time dsol, gmlog = IterativeSolvers.gmres(MAT, rhs; tol=1e-15, restart=100, maxiter=100,
                                             verbose=false, log=true)
        if !gmlog.isconverged
            warn("GMRES did not converge, residual = $(gmlog.data[:resnorm][end])")
        end
        density = reshape(dsol, 2, :)
    end
    time_solve = toq()
    # Estimate density resolution
    L = mss.legendre_matrix(panelorder)
    density_resolution = 0.0;
    for i=1:numpanels
        xcoeff = L*density[1, (1:panelorder) + panelorder*(i-1)]
        ycoeff = L*density[2, (1:panelorder) + panelorder*(i-1)]
        xres = max(abs(xcoeff[end]), abs(xcoeff[end-1]))
        yres = max(abs(ycoeff[end]), abs(ycoeff[end-1]))
        res = max(xres, yres)
        density_resolution = max(density_resolution, res)
    end
    # Evaluate layer potential at interior points
    #println("* FMM eval")
    tic()
    u, u1grad, u2grad = mss.doublelayer_fast(dcurve, density, interior_points, alpha; specquad=true, weights=near_weights, ifgrad=true, precomp=fmm_precomp)
    time_fmm = toq()
    @. begin
        # Add hom sol to part sol on grid
        U1[interior]   += u[1,:]
        U2[interior]   += u[2,:]
        dU1dx[interior] += u1grad[1,:]
        dU1dy[interior] += u1grad[2,:]
        dU2dx[interior] += u2grad[1,:]
        dU2dy[interior] += u2grad[2,:]
        # Clear exterior
        U1[!interior] = 0.0
        U2[!interior] = 0.0
        dU1dx[!interior] = 0.0
        dU1dy[!interior] = 0.0
        dU2dx[!interior] = 0.0
        dU2dy[!interior] = 0.0
    end
    println(@sprintf("Timings: pux=%.3f, fft=%.3f, solve=%.3f, fmm=%.3f", time_pux, time_fft, time_solve, time_fmm))
    info(@sprintf("Resolution est.: den=%.1e, uP=%.1e, uP'=%.1e",
                  density_resolution, UP_resolution, UPgrad_resolution))
    if plotext
        figure(4)
        clf()
        pcolor(X, Y, FEXT1)
        plot(xbdry, ybdry,"r")
        axis("equal")
    end
    sol = mss.Solution(U1, U2, dU1dx, dU1dy, dU2dx, dU2dy,
                       copy(F1), copy(F2), copy(ubc1), copy(ubc2))
    return hgrid, errcheck(sol, X, Y, interior_points, interior, alpha)
end

function errcheck(sol, X, Y, interior_points, interior, alpha)
    ### END STUFF TO PUT IN TIMESTEP LOOP
    # Check errors in test problem
    println("* Computing error")
    # Reference
    xint = interior_points[1,:]
    yint = interior_points[2,:]
    uref1 = ufunc1.(xint, yint, alpha)
    uref2 = ufunc2.(xint, yint, alpha)
    numeval = size(interior_points, 2)

    uref = [uref1 uref2]'

    maxnorm(v) = maximum(abs.(v))
    l2norm(v) = sqrt(sum(vec(v.^2)))

    # Error
    @show size(sol.U1)
    @show size(interior)
    u = [sol.U1[interior] sol.U2[interior]]'
    err = u .- uref
    maxerr = Array{Float64}(numeval)
    for i=1:numeval
        maxerr[i] = norm(err[:, i], Inf)
    end
    max_rel_err = maxnorm(err) / maxnorm(uref)
    l2_rel_err = l2norm(err) / l2norm(uref)
    println("Max rel err on grid: ", max_rel_err)
    println("L2 rel err on grid: ",  l2_rel_err)

    println("* Derivative errors (max rel)")
    grad = max(     norm(vec(sol.dU1dx[interior]), Inf),
                    norm(vec(sol.dU1dy[interior]), Inf),
                    norm(vec(sol.dU2dx[interior]), Inf),
                    norm(vec(sol.dU2dy[interior]), Inf) )

    gradnorm = max( norm(vec(sol.dU1dx[interior]), Inf),
                    norm(vec(sol.dU1dy[interior]), Inf),
                    norm(vec(sol.dU2dx[interior]), Inf),
    norm(vec(sol.dU2dy[interior]), Inf) )

    grad = [sol.dU1dx[interior]; sol.dU1dy[interior]; sol.dU2dx[interior]; sol.dU2dy[interior]]
    gradref = [ufunc1x.(xint, yint, alpha)
               ufunc1y.(xint, yint, alpha)
               ufunc2x.(xint, yint, alpha)
               ufunc2y.(xint, yint, alpha)]

    Egrad_max = maxnorm(grad.-gradref) / maxnorm(gradref)
    Egrad_l2 = l2norm(grad.-gradref) / l2norm(gradref)

    @show Egrad_max
    @show Egrad_l2

    if ploterror
        println("* Displaying")
        figure(1, figsize=(4,3))
        clf()
        # Put on grid
        E = zeros(sol.U1)
        E[interior] = maxerr / maxnorm(uref)

        logE = log10.(E+1e-16)
        #interior_pcolor(X, Y, logE, interior, vmin=-16, vmax=0)
        interior_pcolor(X, Y, logE, interior, cmap=PyPlot.cm[:coolwarm])
        clim(log10(l2_rel_err)-1, log10(l2_rel_err))
        #plot(dcurve.points[1,:], dcurve.points[2,:], ".k")
        axis("off")
        axis("equal")
        xlim(0.6, 1.3)
        ylim(-0.35, 0.35)
        cbar = colorbar(pad=0.0, shrink=0.8)
        cbar[:set_label]("Relative error")
        tight_layout(0.2)
        savefig(@sprintf("figs/starfish_errfield_alpha%.3f.png", alpha))
    end

    return max_rel_err, l2_rel_err, Egrad_max, Egrad_l2
end

alpha_list = 10 .^ (-2.0:0.25:4.0)

Ntests = length(alpha_list)
max_rel_err = zeros(Ntests)
l2_rel_err = zeros(Ntests)
grad_max_rel_err = zeros(Ntests)
grad_l2_rel_err = zeros(Ntests)
hgrid = zeros(Ntests)

include("alpha_variation_plot.jl")

tic()
for i=1:Ntests
    hgrid[i], errors = solve(pux_R, alpha_list[i])
    max_rel_err[i], l2_rel_err[i], grad_max_rel_err[i], grad_l2_rel_err[i] = errors
    plot_results()
end
toq()

include("alpha_variation_plot.jl")
plot_results(paperplots=true)
savefig("../docs/paper/figs/alpha_variation.pdf")


using RootFindGF
using Base.Test
using Nemo

function test_all()
    test_Γ()
    test_multi_eval()
    test_interpolation()
    test_resultant()
    test_SRA()
    test_flint()
end

K, x = FiniteField(2, 10, "x")
P, X = PolynomialRing(K, "X")

function test_Γ()
    Γ(map(i -> x^i, 0:9))
end

function test_multi_eval()
    list = map(i -> x^i, 0:400)
    f = X^400 + X^30 + x
    @test RootFindGF.multi_eval(f, list, 10) == [f(p) for p in list]
end

function test_interpolation()
    xs = map(i -> x^i, 0:400)
    f = X^400 + X^30 + x
    ys = map(f, xs)
    @test RootFindGF.interpolate(P, xs, ys) == f
end

function test_resultant()
    PP, Y = PolynomialRing(P, "Y")
    f = X^400 + X^30 + x
    @test RootFindGF.resultant(f, x) == resultant(compose(f, Y), X - Y^2 + x*Y)
end

function test_SRA()
    roots = [x^i for i in 0:300]
    pol = prod([X - r for r in roots])
    @test Set(roots) == Set(SRA(pol))
end

function test_flint()
    roots = [x^i for i in 0:300]
    pol = prod([X - r for r in roots])
    @test Set(roots) == Set(keys(flint_root_find(pol)))
end

test_all()


################################# sample #######################################
# IDEA: pre-allocation?
# IDEA: lazy GP sampling?
using LinearAlgebraExtensions: vecofvec # converts a matrix into a vector of vectors
sample(G::Univariate{T}) where T = mean(G) + std(G) * randn(T)

function sample(G::Univariate{T}, k::Int) where T
    x = randn(T, k)
    x .= mean(G) .+ std(G) .* x
end

# sample(G::AbstractGaussian{<:Real}) = sample(G, 1)[]
function sample(G::Multivariate; tol::Real = tol)
    vec(sample(G, 1, tol = tol)[1])
end

# TODO: sample(G, k) should return a vector with k elements, each of which is a sample
# multi-variate Gaussian sample, falls back to cholesky
function sample(G::Multivariate{<:AbstractVector, <:AbstractMatrix}, k::Int; tol::Real = tol)
    C = cholesky(G, tol = tol)
    return sample(C, k, tol = tol)
end
# requires that Σ.L*Σ.L' = Σ of the original multivariate Gaussian
function sample(G::Gaussian{<:AbstractVector, <:Cholesky}, k::Int; tol::Real = tol)
    vecofvec(mean(G) .+ cov(G).L * randn(length(G), k))
end
function sample(G::Gaussian{<:AbstractVector, <:CholeskyPivoted}, k::Int; tol::Real = tol)
    C = cov(G)
    C.tol ≤ tol || throw("CholeskyPivoted does not have sufficient error tolerance: $(C.tol) ≰ $tol")
    L = LowRank(C)
    sample(Gaussian(mean(G), L), k, tol = tol)
end
using LinearAlgebraExtensions: LowRank
function sample(G::Gaussian{<:AbstractVector, <:LowRank}, k::Int; tol::Real = tol)
    L = cov(G)
    L.tol ≤ tol || throw("LowRank does not have sufficient error tolerance: $(L.tol) ≰ $tol")
    L.U ≡ L.V' || throw("sampling with LowRank only supported if U ≡ V'")
    vecofvec(mean(G) .+ L.U * randn(rank(L), k))
end

function sample(G::Gaussian{<:AbstractVector, <:Union{Diagonal, UniformScaling}},
                k::Int; tol::Real = tol)
    vecofvec(mean(G) .+ sqrt(cov(G)) * randn((length(G), k)))
end

function sample(G::Gaussian{<:AbstractVector, <:Woodbury}, k::Int; tol::Real = tol)
    W = cov(G)
    W.U ≡ W.V' || throw("sampling with Woodbury only supported if U ≡ V'")
    s(A) = sample(Gaussian(A), k, tol = tol)
    s(W.A) .+ .*((W.U,), s(W.C)) .+ (mean(G),)
end


################################################################################
## Loss utility functions

function vector_mse(x, x̂)
    res = zero(eltype(x[1]))
    @inbounds for i in eachindex(x)
        res += sum((x[i] .- x̂[i]).^2)
    end
    # divide per number of time steps and batch size
    res /= size(x, 1) * size(x[1], 2)
    return res
end

# KL divergence
kl(μ, logσ²) = (exp(logσ²) + μ^2 - logσ² - 1) / 2

function vector_kl(μ::T, logσ²::T) where T <: Tuple{Matrix, Matrix}
    P = eltype(μ[1])
    s = zero(P)
    # go through initial conditions and parameters
    @inbounds for i in 1:2
        s1 = zero(P)
        @inbounds for k in eachindex(μ[i])
            s1 += kl(μ[i][k], logσ²[i][k])
        end
        # divide per batch size
        s1 /= size(μ[i], 2)
        s += s1
    end
    return s
end

function vector_kl(μ::T, logσ²::T) where T <: Matrix
    P = eltype(μ)
    s = zero(P)
    # go through initial conditions
    @inbounds for k in eachindex(μ)
        s += kl(μ[k], logσ²[k])
    end
    # divide per batch size
    s /= size(μ, 2)
    return s
end

function vector_kl(μ::T, logσ²::T) where T <: Tuple{CuArray, CuArray}
	return sum( [ mean(sum(kl.(μ[i], logσ²[i]), dims=1)) for i in 1:length(μ) ] )
end

## annealing factor scheduler
# based on https://github.com/haofuml/cyclical_annealing
function frange_cycle_linear(n_iter, start::T=0.0f0, stop::T=1.0f0,  n_cycle=4, ratio=0.5) where T
    L = ones(n_iter) * stop
    period = n_iter/n_cycle
    step = T((stop-start)/(period*ratio)) # linear schedule

    for c in 0:n_cycle-1
        v, i = start, 1
        while (v ≤ stop) & (Int(round(i+c*period)) < n_iter)
            L[Int(round(i+c*period))] = v
            v += step
            i += 1
        end
    end
    return T.(L)
end

################################################################################
## Data pre-processing

function normalize_to_unit_segment(X)
    min_val = minimum(X)
    max_val = maximum(X)

    X̂ = (X .- min_val) ./ (max_val - min_val)
    return X̂, min_val, max_val
end

denormalize_unit_segment(X̂, min_val, max_val) = X̂ .* (max_val .- min_val) .+ min_val

################################################################################
## Training help function

function time_loader(x, full_seq_len, seq_len)

    x_ = Array{Float32, 3}(undef, (size(x,1), size(x,2), seq_len))
    idxs = rand_time(full_seq_len, seq_len)
    for i in 1:size(x,2)
        x_[:,i,:] = x[:, i, idxs]
    end
    return x_
end

function rand_time(full_seq_len, seq_len)
    start_time = rand(1:full_seq_len - seq_len)
    idxs = start_time:start_time+seq_len-1
    return idxs
end


#########################################################
# Elementary actions of Abstract permutations

Base.eachindex(p::AbstractPerm) = Base.OneTo(degree(p))

@inline fixes(p::GroupElement, pt, op=^) = op(pt, p) == pt
@inline fixes(p::AbstractPerm, v::AbstractVector, op=^) =
    all( i-> v[i] == v[op(i, p)], eachindex(v))

fixedpoints(p::AbstractPerm, range=eachindex(p), op=^) =
    [i for i in range if fixes(p, i, op)]
nfixedpoints(p::AbstractPerm, range=eachindex(p), op=^) = count(i->fixes(p, i, op), range)

for (fname, findname) in [(:firstmoved, :findfirst), (:lastmoved, :findlast)]
    @eval begin
        function $fname(p::AbstractPerm, op=^)
            k = $findname(i -> i != op(i, p), eachindex(p))
            k === nothing && return k
            return eltype(p)(k)
        end
    end
end

@inline Base.:^(v::Tuple, p::AbstractPerm) = ntuple(i->v[i^p], length(v))
@inline function Base.:^(v::AbstractVector, p::AbstractPerm)
    res = similar(v);
    @inbounds for i in eachindex(v)
        res[i] = v[i^p]
    end
    return res
end

# specific definitions

@inline @inbounds function Base.:^(n::Integer, p::Perm)
    if 1 <= n <= length(p.d)
        return oftype(n, p.d[n])
    end
    return n
end

Base.:^(n::Integer, p::Permutation) = n^perm(p)


# This file is a part of Julia. License is MIT: https://julialang.org/license

module TestDiagonal

using Test, LinearAlgebra, Random
using LinearAlgebra: BlasFloat, BlasComplex

n=12 #Size of matrix problem to test
Random.seed!(1)

@testset for relty in (Float32, Float64, BigFloat), elty in (relty, Complex{relty})
    dd=convert(Vector{elty}, randn(n))
    vv=convert(Vector{elty}, randn(n))
    UU=convert(Matrix{elty}, randn(n,n))
    if elty <: Complex
        dd+=im*convert(Vector{elty}, randn(n))
        vv+=im*convert(Vector{elty}, randn(n))
        UU+=im*convert(Matrix{elty}, randn(n,n))
    end
    D = Diagonal(dd)
    DM = Matrix(Diagonal(dd))

    @testset "constructor" begin
        for x in (dd, GenericArray(dd))
            @test Diagonal(x)::Diagonal{elty,typeof(x)} == DM
            @test Diagonal(x).diag === x
            @test Diagonal{elty}(x)::Diagonal{elty,typeof(x)} == DM
            @test Diagonal{elty}(x).diag === x
            @test Diagonal{elty}(D) === D
        end
        @test eltype(Diagonal{elty}([1,2,3,4])) == elty
        @test isa(Diagonal{elty,Vector{elty}}(GenericArray([1,2,3,4])), Diagonal{elty,Vector{elty}})
        DI = Diagonal([1,2,3,4])
        @test Diagonal(DI) === DI
        @test isa(Diagonal{elty}(DI), Diagonal{elty})
        # issue #26178
        @test_throws MethodError convert(Diagonal, [1, 2, 3, 4])
    end

    @testset "Basic properties" begin
        @test_throws ArgumentError size(D,0)
        @test typeof(convert(Diagonal{ComplexF32},D)) <: Diagonal{ComplexF32}
        @test typeof(convert(AbstractMatrix{ComplexF32},D)) <: Diagonal{ComplexF32}

        @test Array(real(D)) == real(DM)
        @test Array(abs.(D)) == abs.(DM)
        @test Array(imag(D)) == imag(DM)

        @test parent(D) == dd
        @test D[1,1] == dd[1]
        @test D[1,2] == 0

        @test issymmetric(D)
        @test isdiag(D)
        @test isdiag(Diagonal([[1 0; 0 1], [1 0; 0 1]]))
        @test !isdiag(Diagonal([[1 0; 0 1], [1 0; 1 1]]))
        @test istriu(D)
        @test istriu(D, -1)
        @test !istriu(D, 1)
        @test istriu(Diagonal(zero(diag(D))), 1)
        @test istril(D)
        @test !istril(D, -1)
        @test istril(D, 1)
        @test istril(Diagonal(zero(diag(D))), -1)
        if elty <: Real
            @test ishermitian(D)
        end
    end

    @testset "diag" begin
        @test_throws ArgumentError diag(D,  n+1)
        @test_throws ArgumentError diag(D, -n-1)
        @test (@inferred diag(D))::typeof(dd) == dd
        @test (@inferred diag(D, 0))::typeof(dd) == dd
        @test (@inferred diag(D, 1))::typeof(dd) == zeros(elty, n-1)
        DG = Diagonal(GenericArray(dd))
        @test (@inferred diag(DG))::typeof(GenericArray(dd)) == GenericArray(dd)
        @test (@inferred diag(DG, 1))::typeof(GenericArray(dd)) == GenericArray(zeros(elty, n-1))
    end

    @testset "Simple unary functions" begin
        for op in (-,)
            @test op(D)==op(DM)
        end

        for func in (det, tr)
            @test func(D) ≈ func(DM) atol=n^2*eps(relty)*(1+(elty<:Complex))
        end
        if relty <: BlasFloat
            for func in (exp, cis, sinh, cosh, tanh, sech, csch, coth)
                @test func(D) ≈ func(DM) atol=n^3*eps(relty)
            end
            @test log(Diagonal(abs.(D.diag))) ≈ log(abs.(DM)) atol=n^3*eps(relty)
        end
        if elty <: BlasComplex
            for func in (logdet, sqrt, sin, cos, tan, sec, csc, cot,
                         asin, acos, atan, asec, acsc, acot,
                         asinh, acosh, atanh, asech, acsch, acoth)
                @test func(D) ≈ func(DM) atol=n^2*eps(relty)*2
            end
        end
    end

    @testset "Two-dimensional Euler formula for Diagonal" begin
        @test cis(Diagonal([π, π])) ≈ -I
    end

    @testset "Linear solve" begin
        for (v, U) in ((vv, UU), (view(vv, 1:n), view(UU, 1:n, 1:2)))
            @test D*v ≈ DM*v atol=n*eps(relty)*(1+(elty<:Complex))
            @test D*U ≈ DM*U atol=n^2*eps(relty)*(1+(elty<:Complex))

            @test transpose(U)*D ≈ transpose(U)*Array(D)
            @test U'*D ≈ U'*Array(D)

            if relty != BigFloat
                atol_two = 2n^2 * eps(relty) * (1 + (elty <: Complex))
                atol_three = 2n^3 * eps(relty) * (1 + (elty <: Complex))
                @test D\v ≈ DM\v atol=atol_two
                @test D\U ≈ DM\U atol=atol_three
                @test ldiv!(D, copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(transpose(D), copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(adjoint(conj(D)), copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(D, copy(U)) ≈ DM\U atol=atol_three
                @test ldiv!(transpose(D), copy(U)) ≈ DM\U atol=atol_three
                @test ldiv!(adjoint(conj(D)), copy(U)) ≈ DM\U atol=atol_three
                # this method tests AbstractMatrix/AbstractVec for second arg
                Usym_bad = Symmetric(ones(elty, n+1, n+1))
                @test_throws DimensionMismatch ldiv!(D, copy(Usym_bad))

                @test ldiv!(zero(v), D, copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(zero(v), transpose(D), copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(zero(v), adjoint(conj(D)), copy(v)) ≈ DM\v atol=atol_two
                @test ldiv!(zero(U), D, copy(U)) ≈ DM\U atol=atol_three
                @test ldiv!(zero(U), transpose(D), copy(U)) ≈ DM\U atol=atol_three
                @test ldiv!(zero(U), adjoint(conj(D)), copy(U)) ≈ DM\U atol=atol_three

                Uc = copy(U')
                target = rmul!(Uc, Diagonal(inv.(D.diag)))
                @test rdiv!(Uc, D) ≈ target atol=atol_three
                @test_throws DimensionMismatch rdiv!(Matrix{elty}(I, n-1, n-1), D)
                @test_throws SingularException rdiv!(Uc, Diagonal(fill!(similar(D.diag), 0)))
                @test rdiv!(Uc, transpose(D)) ≈ target atol=atol_three
                @test rdiv!(Uc, adjoint(conj(D))) ≈ target atol=atol_three
                @test ldiv!(D, Matrix{eltype(D)}(I, size(D))) ≈ D \ Matrix{eltype(D)}(I, size(D)) atol=atol_three
                @test_throws DimensionMismatch ldiv!(D, fill(elty(1), n + 1))
                @test_throws SingularException ldiv!(Diagonal(zeros(relty, n)), copy(v))
                b = rand(elty, n, n)
                @test ldiv!(D, copy(b)) ≈ Array(D)\Array(b)
                @test_throws SingularException ldiv!(Diagonal(zeros(elty, n)), copy(b))
                b = view(rand(elty, n), Vector(1:n))
                b2 = copy(b)
                c = ldiv!(D, b)
                d = Array(D)\b2
                @test c ≈ d
                @test_throws SingularException ldiv!(Diagonal(zeros(elty, n)), b)
                b = rand(elty, n+1, n+1)
                @test_throws DimensionMismatch ldiv!(D, copy(b))
                b = view(rand(elty, n+1), Vector(1:n+1))
                @test_throws DimensionMismatch ldiv!(D, b)
            end
        end
    end
    d = convert(Vector{elty}, randn(n))
    D2 = Diagonal(d)
    DM2= Matrix(Diagonal(d))
    @testset "Binary operations" begin
        for op in (+, -, *)
            @test Array(op(D, D2)) ≈ op(DM, DM2)
        end
        @testset "with plain numbers" begin
            a = rand()
            @test Array(a*D) ≈ a*DM
            @test Array(D*a) ≈ DM*a
            @test Array(D/a) ≈ DM/a
            if elty <: Real
                @test Array(abs.(D)^a) ≈ abs.(DM)^a
            else
                @test Array(D^a) ≈ DM^a
            end
            @test Diagonal(1:100)^2 == Diagonal((1:100).^2)
            p = 3
            @test Diagonal(1:100)^p == Diagonal((1:100).^p)
            @test Diagonal(1:100)^(-1) == Diagonal(inv.(1:100))
            @test Diagonal(1:100)^2.0 == Diagonal((1:100).^2.0)
            @test Diagonal(1:100)^(2.0+0im) == Diagonal((1:100).^(2.0+0im))
        end

        if relty <: BlasFloat
            for b in (rand(elty,n,n), rand(elty,n))
                @test lmul!(copy(D), copy(b)) ≈ Array(D)*Array(b)
                @test lmul!(transpose(copy(D)), copy(b)) ≈ transpose(Array(D))*Array(b)
                @test lmul!(adjoint(copy(D)), copy(b)) ≈ Array(D)'*Array(b)
            end
        end

        #a few missing mults
        bd = Bidiagonal(D2)
        @test D*transpose(D2) ≈ Array(D)*transpose(Array(D2))
        @test D2*transpose(D) ≈ Array(D2)*transpose(Array(D))
        @test D2*D' ≈ Array(D2)*Array(D)'

        #division of two Diagonals
        @test D/D2 ≈ Diagonal(D.diag./D2.diag)
        @test D\D2 ≈ Diagonal(D2.diag./D.diag)

        # QR \ Diagonal
        A = rand(elty, n, n)
        qrA = qr(A)
        @test qrA \ D ≈ A \ D

        # HermOrSym
        A     = rand(elty, n, n)
        Asym  = Symmetric(A + transpose(A), :U)
        Aherm = Hermitian(A + adjoint(A), :U)
        for op in (+, -)
            @test op(Asym, D) isa Symmetric
            @test Array(op(Asym, D)) ≈ Array(Symmetric(op(Array(Asym), Array(D))))
            @test op(D, Asym) isa Symmetric
            @test Array(op(D, Asym)) ≈ Array(Symmetric(op(Array(D), Array(Asym))))
            if !(elty <: Real)
                Dr = real(D)
                @test op(Aherm, Dr) isa Hermitian
                @test Array(op(Aherm, Dr)) ≈ Array(Hermitian(op(Array(Aherm), Array(Dr))))
                @test op(Dr, Aherm) isa Hermitian
                @test Array(op(Dr, Aherm)) ≈ Array(Hermitian(op(Array(Dr), Array(Aherm))))
            end
        end
        @test Array(D*transpose(Asym)) ≈ Array(D) * Array(transpose(Asym))
        @test Array(D*adjoint(Asym)) ≈ Array(D) * Array(adjoint(Asym))
        @test Array(D*transpose(Aherm)) ≈ Array(D) * Array(transpose(Aherm))
        @test Array(D*adjoint(Aherm)) ≈ Array(D) * Array(adjoint(Aherm))
        @test Array(transpose(Asym)*transpose(D)) ≈ Array(transpose(Asym)) * Array(transpose(D))
        @test Array(transpose(D)*transpose(Asym)) ≈ Array(transpose(D)) * Array(transpose(Asym))
        @test Array(adjoint(Aherm)*adjoint(D)) ≈ Array(adjoint(Aherm)) * Array(adjoint(D))
        @test Array(adjoint(D)*adjoint(Aherm)) ≈ Array(adjoint(D)) * Array(adjoint(Aherm))

        # Performance specialisations for A*_mul_B!
        vvv = similar(vv)
        @test (r = Matrix(D) * vv   ; mul!(vvv, D, vv)  ≈ r ≈ vvv)
        @test (r = Matrix(D)' * vv  ; mul!(vvv, adjoint(D), vv) ≈ r ≈ vvv)
        @test (r = transpose(Matrix(D)) * vv ; mul!(vvv, transpose(D), vv) ≈ r ≈ vvv)

        UUU = similar(UU)
        for transformA in (identity, adjoint, transpose)
            for transformD in (identity, adjoint, transpose)
                @test mul!(UUU, transformA(UU), transformD(D)) ≈  transformA(UU) * Matrix(transformD(D))
                @test mul!(UUU, transformD(D), transformA(UU)) ≈  Matrix(transformD(D)) * transformA(UU)
            end
        end

        alpha = elty(randn())  # randn(elty) does not work with BigFloat
        beta = elty(randn())
        @test begin
            vvv = similar(vv)
            vvv .= randn(size(vvv))  # randn!(vvv) does not work with BigFloat
            r = alpha * Matrix(D) * vv + beta * vvv
            mul!(vvv, D, vv, alpha, beta)  ≈ r ≈ vvv
        end
        @test begin
            vvv = similar(vv)
            vvv .= randn(size(vvv))  # randn!(vvv) does not work with BigFloat
            r = alpha * Matrix(D)' * vv + beta * vvv
            mul!(vvv, adjoint(D), vv, alpha, beta) ≈ r ≈ vvv
        end
        @test begin
            vvv = similar(vv)
            vvv .= randn(size(vvv))  # randn!(vvv) does not work with BigFloat
            r = alpha * transpose(Matrix(D)) * vv + beta * vvv
            mul!(vvv, transpose(D), vv, alpha, beta) ≈ r ≈ vvv
        end

        @test begin
            UUU = similar(UU)
            UUU .= randn(size(UUU))  # randn!(UUU) does not work with BigFloat
            r = alpha * Matrix(D) * UU + beta * UUU
            mul!(UUU, D, UU, alpha, beta) ≈ r ≈ UUU
        end
        @test begin
            UUU = similar(UU)
            UUU .= randn(size(UUU))  # randn!(UUU) does not work with BigFloat
            r = alpha * Matrix(D)' * UU + beta * UUU
            mul!(UUU, adjoint(D), UU, alpha, beta) ≈ r ≈ UUU
        end
        @test begin
            UUU = similar(UU)
            UUU .= randn(size(UUU))  # randn!(UUU) does not work with BigFloat
            r = alpha * transpose(Matrix(D)) * UU + beta * UUU
            mul!(UUU, transpose(D), UU, alpha, beta) ≈ r ≈ UUU
        end

        # make sure that mul!(A, {Adj|Trans}(B)) works with B as a Diagonal
        VV = Array(D)
        DD = copy(D)
        r  = VV * Matrix(D)
        @test Array(rmul!(VV, DD)) ≈ r ≈ Array(D)*Array(D)
        DD = copy(D)
        r  = VV * transpose(Array(D))
        @test Array(rmul!(VV, transpose(DD))) ≈ r
        DD = copy(D)
        r  = VV * Array(D)'
        @test Array(rmul!(VV, adjoint(DD))) ≈ r

        # kron
        D3 = Diagonal(convert(Vector{elty}, rand(n÷2)))
        DM3= Matrix(D3)
        @test Matrix(kron(D, D3)) ≈ kron(DM, DM3)
        M4 = rand(elty, n÷2, n÷2)
        @test kron(D3, M4) ≈ kron(DM3, M4)
        @test kron(M4, D3) ≈ kron(M4, DM3)
        X = [ones(1,1) for i in 1:2, j in 1:2]
        @test kron(I(2), X)[1,3] == zeros(1,1)
        X = [ones(2,2) for i in 1:2, j in 1:2]
        @test kron(I(2), X)[1,3] == zeros(2,2)
    end
    @testset "iszero, isone, triu, tril" begin
        Dzero = Diagonal(zeros(elty, 10))
        Done = Diagonal(ones(elty, 10))
        Dmix = Diagonal(zeros(elty, 10))
        Dmix[end,end] = one(elty)
        @test iszero(Dzero)
        @test !isone(Dzero)
        @test !iszero(Done)
        @test isone(Done)
        @test !iszero(Dmix)
        @test !isone(Dmix)
        @test istriu(D)
        @test istril(D)
        @test iszero(triu(D,1))
        @test triu(D,0)  == D
        @test triu(D,-1) == D
        @test tril(D,1)  == D
        @test iszero(tril(D,-1))
        @test tril(D,0)  == D
        @test_throws ArgumentError tril(D, -n - 2)
        @test_throws ArgumentError tril(D, n)
        @test_throws ArgumentError triu(D, -n)
        @test_throws ArgumentError triu(D, n + 2)
    end

    # factorize
    @test factorize(D) == D

    @testset "Eigensystem" begin
        eigD = eigen(D)
        @test Diagonal(eigD.values) ≈ D
        @test eigD.vectors == Matrix(I, size(D))
    end

    @testset "ldiv" begin
        v = rand(n + 1)
        @test_throws DimensionMismatch D\v
        v = rand(n)
        @test D\v ≈ DM\v
        V = rand(n + 1, n)
        @test_throws DimensionMismatch D\V
        V = rand(n, n)
        @test D\V ≈ DM\V
    end

    @testset "conj and transpose" begin
        @test transpose(D) == D
        if elty <: BlasComplex
            @test Array(conj(D)) ≈ conj(DM)
            @test adjoint(D) == conj(D)
        end
        # Translates to Ac/t_mul_B, which is specialized after issue 21286
        @test(D' * vv == conj(D) * vv)
        @test(transpose(D) * vv == D * vv)
    end

    # logdet and logabsdet
    if relty <: Real
        lD = Diagonal(convert(Vector{relty}, rand(n)))
        lM = Matrix(lD)
        @test logdet(lD) ≈ logdet(lM)
        d1, s1 = @inferred logabsdet(lD)
        d2, s2 = logabsdet(lM)
        @test d1 ≈ d2
        @test s1 == s2
        @test logdet(Diagonal(relty[-1,-2])) ≈ log(2)
        @test_throws DomainError logdet(Diagonal(relty[-1,-2,-3]))
    end

    @testset "similar" begin
        @test isa(similar(D), Diagonal{elty})
        @test isa(similar(D, Int), Diagonal{Int})
        @test isa(similar(D, (3,2)), Matrix{elty})
        @test isa(similar(D, Int, (3,2)), Matrix{Int})
    end

    # Issue number 10036
    # make sure issymmetric/ishermitian work for
    # non-real diagonal matrices
    @testset "issymmetric/hermitian for complex Diagonal" begin
        @test issymmetric(D2)
        @test ishermitian(D2)
        if elty <: Complex
            dc = d .+ elty(1im)
            D3 = Diagonal(dc)
            @test issymmetric(D3)
            @test !ishermitian(D3)
        end
    end

    @testset "svd (#11120/#11247)" begin
        U, s, V = svd(D)
        @test (U*Diagonal(s))*V' ≈ D
        @test svdvals(D) == s
        @test svd(D).V == V
    end

end

@testset "rdiv! (#40887)" begin
    @test rdiv!(Matrix(Diagonal([2.0, 3.0])), Diagonal(2:3)) == Diagonal([1.0, 1.0])
    @test rdiv!(fill(3.0, 3, 3), 3.0I(3)) == ones(3,3)
end

@testset "kron (issue #40595)" begin
    # custom array type to test that kron on Diagonal matrices preserves types of the parents if possible
    struct KronTestArray{T, N, AT} <: AbstractArray{T, N}
        data::AT
    end
    KronTestArray(data::AbstractArray) = KronTestArray{eltype(data), ndims(data), typeof(data)}(data)
    Base.size(A::KronTestArray) = size(A.data)
    LinearAlgebra.kron(A::KronTestArray, B::KronTestArray) = KronTestArray(kron(A.data, B.data))
    Base.getindex(K::KronTestArray{<:Any,N}, i::Vararg{Int,N}) where {N} = K.data[i...]

    A = KronTestArray([1, 2, 3]);
    @test kron(A, A) isa KronTestArray
    Ad = Diagonal(A);
    @test kron(Ad, Ad).diag isa KronTestArray
    @test kron(Ad, Ad).diag == kron([1, 2, 3], [1, 2, 3])
end

@testset "svdvals and eigvals (#11120/#11247)" begin
    D = Diagonal(Matrix{Float64}[randn(3,3), randn(2,2)])
    @test sort([svdvals(D)...;], rev = true) ≈ svdvals([D.diag[1] zeros(3,2); zeros(2,3) D.diag[2]])
    @test sort([eigvals(D)...;], by=LinearAlgebra.eigsortby) ≈ eigvals([D.diag[1] zeros(3,2); zeros(2,3) D.diag[2]])
end

@testset "eigmin (#27847)" begin
    for _ in 1:100
        d = randn(rand(1:10))
        D = Diagonal(d)
        @test eigmin(D) == minimum(d)
    end
end

@testset "isposdef" begin
    @test isposdef(Diagonal(1.0 .+ rand(n)))
    @test !isposdef(Diagonal(-1.0 * rand(n)))
    @test isposdef(Diagonal(complex(1.0, 0.0) .+ rand(n)))
    @test !isposdef(Diagonal(complex(1.0, 1.0) .+ rand(n)))
    @test isposdef(Diagonal([[1 0; 0 1], [1 0; 0 1]]))
    @test !isposdef(Diagonal([[1 0; 0 1], [1 0; 1 1]]))
end

@testset "getindex" begin
    d = randn(n)
    D = Diagonal(d)
    # getindex bounds checking
    @test_throws BoundsError D[0, 0]
    @test_throws BoundsError D[-1, -2]
    @test_throws BoundsError D[n, n + 1]
    @test_throws BoundsError D[n + 1, n]
    @test_throws BoundsError D[n + 1, n + 1]
    # getindex on and off the diagonal
    for i in 1:n, j in 1:n
        @test D[i, j] == (i == j ? d[i] : 0)
    end
end

@testset "setindex!" begin
    d = randn(n)
    D = Diagonal(d)
    # setindex! bounds checking
    @test_throws BoundsError D[0, 0] = 0
    @test_throws BoundsError D[-1 , -2] = 0
    @test_throws BoundsError D[n, n + 1] = 0
    @test_throws BoundsError D[n + 1, n] = 0
    @test_throws BoundsError D[n + 1, n + 1] = 0
    for i in 1:n, j in 1:n
        if i == j
            # setindex on! the diagonal
            @test ((D[i, j] = i) == i; D[i, j] == i)
        else
            # setindex! off the diagonal
            @test ((D[i, j] = 0) == 0; iszero(D[i, j]))
            @test_throws ArgumentError D[i, j] = 1
        end
    end
end

@testset "inverse" begin
    for d in (randn(n), [1, 2, 3], [1im, 2im, 3im])
        D = Diagonal(d)
        @test inv(D) ≈ inv(Array(D))
    end
    @test_throws SingularException inv(Diagonal(zeros(n)))
    @test_throws SingularException inv(Diagonal([0, 1, 2]))
    @test_throws SingularException inv(Diagonal([0im, 1im, 2im]))
end

# allow construct from range
@test all(Diagonal(range(1, stop=3, length=3)) .== Diagonal([1.0,2.0,3.0]))

# Issue 12803
for t in (Float32, Float64, Int, ComplexF64, Rational{Int})
    @test Diagonal(Matrix{t}[fill(t(1), 2, 2), fill(t(1), 3, 3)])[2,1] == zeros(t, 3, 2)
end

# Issue 15401
@test Matrix(1.0I, 5, 5) \ Diagonal(fill(1.,5)) == Matrix(I, 5, 5)

@testset "Triangular and Diagonal" begin
    function _test_matrix(type)
        if type == Int
            return rand(1:9, 5, 5)
        else
            return randn(type, 5, 5)
        end
    end
    types = (Float64, Int, ComplexF64)
    for ta in types
        D = Diagonal(_test_matrix(ta))
        for tb in types
            B = _test_matrix(tb)
            Tmats = (LowerTriangular(B), UnitLowerTriangular(B), UpperTriangular(B), UnitUpperTriangular(B))
            restypes = (LowerTriangular, LowerTriangular, UpperTriangular, UpperTriangular)
            for (T, rtype) in zip(Tmats, restypes)
                adjtype = (rtype == LowerTriangular) ? UpperTriangular : LowerTriangular

                # Triangular * Diagonal
                R = T * D
                @test R ≈ Array(T) * Array(D)
                @test isa(R, rtype)

                # Diagonal * Triangular
                R = D * T
                @test R ≈ Array(D) * Array(T)
                @test isa(R, rtype)

                # Adjoint of Triangular * Diagonal
                R = T' * D
                @test R ≈ Array(T)' * Array(D)
                @test isa(R, adjtype)

                # Diagonal * Adjoint of Triangular
                R = D * T'
                @test R ≈ Array(D) * Array(T)'
                @test isa(R, adjtype)

                # Transpose of Triangular * Diagonal
                R = transpose(T) * D
                @test R ≈ transpose(Array(T)) * Array(D)
                @test isa(R, adjtype)

                # Diagonal * Transpose of Triangular
                R = D * transpose(T)
                @test R ≈ Array(D) * transpose(Array(T))
                @test isa(R, adjtype)
            end
        end
    end
end

let D1 = Diagonal(rand(5)), D2 = Diagonal(rand(5))
    @test LinearAlgebra.rmul!(copy(D1),D2) == D1*D2
    @test LinearAlgebra.lmul!(D1,copy(D2)) == D1*D2
    @test LinearAlgebra.rmul!(copy(D1),transpose(D2)) == D1*transpose(D2)
    @test LinearAlgebra.lmul!(transpose(D1),copy(D2)) == transpose(D1)*D2
    @test LinearAlgebra.rmul!(copy(D1),adjoint(D2)) == D1*adjoint(D2)
    @test LinearAlgebra.lmul!(adjoint(D1),copy(D2)) == adjoint(D1)*D2
end

@testset "multiplication of a Diagonal with a Matrix" begin
    A = collect(reshape(1:8, 4, 2));
    B = BigFloat.(A);
    DL = Diagonal(collect(axes(A, 1)));
    DR = Diagonal(Float16.(collect(axes(A, 2))));

    @test DL * A == collect(DL) * A
    @test A * DR == A * collect(DR)
    @test DL * B == collect(DL) * B
    @test B * DR == B * collect(DR)

    A = reshape([ones(2,2), ones(2,2)*2, ones(2,2)*3, ones(2,2)*4], 2, 2)
    Ac = collect(A)
    D = Diagonal([collect(reshape(1:4, 2, 2)), collect(reshape(5:8, 2, 2))])
    Dc = collect(D)
    @test A * D == Ac * Dc
    @test D * A == Dc * Ac
    @test D * D == Dc * Dc

    AS = similar(A)
    mul!(AS, A, D, true, false)
    @test AS == A * D

    D2 = similar(D)
    mul!(D2, D, D)
    @test D2 == D * D

    copyto!(D2, D)
    lmul!(D, D2)
    @test D2 == D * D
    copyto!(D2, D)
    rmul!(D2, D)
    @test D2 == D * D
end

@testset "multiplication of QR Q-factor and Diagonal (#16615 spot test)" begin
    D = Diagonal(randn(5))
    Q = qr(randn(5, 5)).Q
    @test D * Q' == Array(D) * Q'
    Q = qr(randn(5, 5), ColumnNorm()).Q
    @test_throws ArgumentError lmul!(Q, D)
end

@testset "block diagonal matrices" begin
    D = Diagonal([[1 2; 3 4], [1 2; 3 4]])
    Dherm = Diagonal([[1 1+im; 1-im 1], [1 1+im; 1-im 1]])
    Dsym = Diagonal([[1 1+im; 1+im 1], [1 1+im; 1+im 1]])
    @test adjoint(D) == Diagonal([[1 3; 2 4], [1 3; 2 4]])
    @test transpose(D) == Diagonal([[1 3; 2 4], [1 3; 2 4]])
    @test adjoint(Dherm) == Dherm
    @test transpose(Dherm) == Diagonal([[1 1-im; 1+im 1], [1 1-im; 1+im 1]])
    @test adjoint(Dsym) == Diagonal([[1 1-im; 1-im 1], [1 1-im; 1-im 1]])
    @test transpose(Dsym) == Dsym

    v = [[1, 2], [3, 4]]
    @test Dherm' * v == Dherm * v
    @test transpose(D) * v == [[7, 10], [15, 22]]

    @test issymmetric(D) == false
    @test issymmetric(Dherm) == false
    @test issymmetric(Dsym) == true

    @test ishermitian(D) == false
    @test ishermitian(Dherm) == true
    @test ishermitian(Dsym) == false

    @test exp(D) == Diagonal([exp([1 2; 3 4]), exp([1 2; 3 4])])
    @test cis(D) == Diagonal([cis([1 2; 3 4]), cis([1 2; 3 4])])
    @test log(D) == Diagonal([log([1 2; 3 4]), log([1 2; 3 4])])
    @test sqrt(D) == Diagonal([sqrt([1 2; 3 4]), sqrt([1 2; 3 4])])

    @test tr(D) == 10
    @test det(D) == 4
end

@testset "linear solve for block diagonal matrices" begin
    D = Diagonal([rand(2,2) for _ in 1:5])
    b = [rand(2,2) for _ in 1:5]
    B = [rand(2,2) for _ in 1:5, _ in 1:5]
    @test ldiv!(D, copy(b)) ≈ Diagonal(inv.(D.diag)) * b
    @test ldiv!(D, copy(B)) ≈ Diagonal(inv.(D.diag)) * B
    @test rdiv!(copy(B), D) ≈ B * Diagonal(inv.(D.diag))
end

@testset "multiplication with Symmetric/Hermitian" begin
    for T in (Float64, ComplexF64)
        D = Diagonal(randn(T, n))
        A = randn(T, n, n); A = A'A
        S = Symmetric(A)
        H = Hermitian(A)
        for (transform1, transform2) in ((identity,  identity),
                (identity,  adjoint  ), (adjoint,   identity ), (adjoint,   adjoint  ),
                (identity,  transpose), (transpose, identity ), (transpose, transpose) )
            @test *(transform1(D), transform2(S)) ≈ *(transform1(Matrix(D)), transform2(Matrix(S)))
            @test *(transform1(D), transform2(H)) ≈ *(transform1(Matrix(D)), transform2(Matrix(H)))
            @test *(transform1(S), transform2(D)) ≈ *(transform1(Matrix(S)), transform2(Matrix(D)))
            @test *(transform1(S), transform2(H)) ≈ *(transform1(Matrix(S)), transform2(Matrix(H)))
        end
    end
end

@testset "multiplication of transposes of Diagonal (#22428)" begin
    for T in (Float64, ComplexF64)
        D = Diagonal(randn(T, 5, 5))
        B = Diagonal(randn(T, 5, 5))
        DD = Diagonal([randn(T, 2, 2), rand(T, 2, 2)])
        BB = Diagonal([randn(T, 2, 2), rand(T, 2, 2)])
        fullDD = copyto!(Matrix{Matrix{T}}(undef, 2, 2), DD)
        fullBB = copyto!(Matrix{Matrix{T}}(undef, 2, 2), BB)
        for (transform1, transform2) in ((identity,  identity),
                (identity,  adjoint  ), (adjoint,   identity ), (adjoint,   adjoint  ),
                (identity,  transpose), (transpose, identity ), (transpose, transpose))
            @test *(transform1(D), transform2(B))::typeof(D) ≈ *(transform1(Matrix(D)), transform2(Matrix(B))) atol=2 * eps()
            @test *(transform1(DD), transform2(BB))::typeof(DD) == *(transform1(fullDD), transform2(fullBB))
        end
        M = randn(T, 5, 5)
        MM = [randn(T, 2, 2) for _ in 1:2, _ in 1:2]
        for transform in (identity, adjoint, transpose)
            @test lmul!(transform(D), copy(M)) ≈ *(transform(Matrix(D)), M)
            @test rmul!(copy(M), transform(D)) ≈ *(M, transform(Matrix(D)))
            @test lmul!(transform(DD), copy(MM)) ≈ *(transform(fullDD), MM)
            @test rmul!(copy(MM), transform(DD)) ≈ *(MM, transform(fullDD))
        end
    end
end

@testset "Diagonal of adjoint/transpose vectors (#23649)" begin
    @test Diagonal(adjoint([1, 2, 3])) == Diagonal([1 2 3])
    @test Diagonal(transpose([1, 2, 3])) == Diagonal([1 2 3])
end

@testset "Multiplication with adjoint and transpose vectors (#26863)" begin
    x = collect(1:2)
    xt = transpose(x)
    A = reshape([[1 2; 3 4], zeros(Int,2,2), zeros(Int, 2, 2), [5 6; 7 8]], 2, 2)
    D = Diagonal(A)
    @test x'*D == x'*A == collect(x')*D == collect(x')*A
    @test xt*D == xt*A == collect(xt)*D == collect(xt)*A
    outadjxD = similar(x'*D); outtrxD = similar(xt*D);
    mul!(outadjxD, x', D)
    @test outadjxD == x'*D
    mul!(outtrxD, xt, D)
    @test outtrxD == xt*D

    D1 = Diagonal([[1 2; 3 4]])
    @test D1 * x' == D1 * collect(x') == collect(D1) * collect(x')
    @test D1 * xt == D1 * collect(xt) == collect(D1) * collect(xt)
    outD1adjx = similar(D1 * x'); outD1trx = similar(D1 * xt);
    mul!(outadjxD, D1, x')
    @test outadjxD == D1*x'
    mul!(outtrxD, D1, xt)
    @test outtrxD == D1*xt

    y = [x, x]
    yt = transpose(y)
    @test y'*D*y == (y'*D)*y == (y'*A)*y
    @test yt*D*y == (yt*D)*y == (yt*A)*y
    outadjyD = similar(y'*D); outtryD = similar(yt*D);
    outadjyD2 = similar(collect(y'*D)); outtryD2 = similar(collect(yt*D));
    mul!(outadjyD, y', D)
    mul!(outadjyD2, y', D)
    @test outadjyD == outadjyD2 == y'*D
    mul!(outtryD, yt, D)
    mul!(outtryD2, yt, D)
    @test outtryD == outtryD2 == yt*D
end

@testset "Multiplication of single element Diagonal (#36746, #40726)" begin
    @test_throws DimensionMismatch Diagonal(randn(1)) * randn(5)
    @test_throws DimensionMismatch Diagonal(randn(1)) * Diagonal(randn(3, 3))
    A = [1 0; 0 2]
    v = [3, 4]
    @test Diagonal(A) * v == A * v
    @test Diagonal(A) * Diagonal(A) == A * A
    @test_throws DimensionMismatch [1 0;0 1] * Diagonal([2 3])   # Issue #40726
    @test_throws DimensionMismatch lmul!(Diagonal([1]), [1,2,3]) # nearby
end

@testset "Triangular division by Diagonal #27989" begin
    K = 5
    for elty in (Float32, Float64, ComplexF32, ComplexF64)
        U = UpperTriangular(randn(elty, K, K))
        L = LowerTriangular(randn(elty, K, K))
        D = Diagonal(randn(elty, K))
        @test (U / D)::UpperTriangular{elty} == UpperTriangular(Matrix(U) / Matrix(D))
        @test (L / D)::LowerTriangular{elty} == LowerTriangular(Matrix(L) / Matrix(D))
        @test (D \ U)::UpperTriangular{elty} == UpperTriangular(Matrix(D) \ Matrix(U))
        @test (D \ L)::LowerTriangular{elty} == LowerTriangular(Matrix(D) \ Matrix(L))
    end
end

@testset "(Sym)Tridiagonal division by Diagonal" begin
    for K in (5, 1), elty in (Float64, ComplexF32), overlength in (1, 0)
        S = SymTridiagonal(randn(elty, K), randn(elty, K-overlength))
        T = Tridiagonal(randn(elty, K-1), randn(elty, K), randn(elty, K-1))
        D = Diagonal(randn(elty, K))
        D0 = Diagonal(zeros(elty, K))
        @test (D \ S)::Tridiagonal{elty} == Tridiagonal(Matrix(D) \ Matrix(S))
        @test (D \ T)::Tridiagonal{elty} == Tridiagonal(Matrix(D) \ Matrix(T))
        @test (S / D)::Tridiagonal{elty} == Tridiagonal(Matrix(S) / Matrix(D))
        @test (T / D)::Tridiagonal{elty} == Tridiagonal(Matrix(T) / Matrix(D))
        @test_throws SingularException D0 \ S
        @test_throws SingularException D0 \ T
        @test_throws SingularException S / D0
        @test_throws SingularException T / D0
    end
    # 0-length case
    S = SymTridiagonal(Float64[], Float64[])
    T = Tridiagonal(Float64[], Float64[], Float64[])
    D = Diagonal(Float64[])
    @test (D \ S)::Tridiagonal{Float64} == T
    @test (D \ T)::Tridiagonal{Float64} == T
    @test (S / D)::Tridiagonal{Float64} == T
    @test (T / D)::Tridiagonal{Float64} == T
    # matrix eltype case
    K = 5
    for elty in (Float64, ComplexF32), overlength in (1, 0)
        S = SymTridiagonal([rand(elty, 2, 2) for _ in 1:K], [rand(elty, 2, 2) for _ in 1:K-overlength])
        T = Tridiagonal([rand(elty, 2, 2) for _ in 1:K-1], [rand(elty, 2, 2) for _ in 1:K], [rand(elty, 2, 2) for _ in 1:K-1])
        D = Diagonal(randn(elty, K))
        SM = fill(zeros(elty, 2, 2), K, K)
        TM = copy(SM)
        SM[1,1] = S[1,1]; TM[1,1] = T[1,1]
        for j in 2:K
            SM[j,j-1] = S[j,j-1]; SM[j,j] = S[j,j]; SM[j-1,j] = S[j-1,j]
            TM[j,j-1] = T[j,j-1]; TM[j,j] = T[j,j]; TM[j-1,j] = T[j-1,j]
        end
        for (M, Mm) in ((S, SM), (T, TM))
            DS = D \ M
            @test DS isa Tridiagonal
            DM = D \ Mm
            for i in -1:1; @test diag(DS, i) ≈ diag(DM, i) end
        end
    end
    # eltype promotion case
    S = SymTridiagonal(rand(-20:20, K), rand(-20:20, K-1))
    T = Tridiagonal(rand(-20:20, K-1), rand(-20:20, K), rand(-20:20, K-1))
    D = Diagonal(rand(1:20, K))
    @test (D \ S)::Tridiagonal{Float64} == Tridiagonal(Matrix(D) \ Matrix(S))
    @test (D \ T)::Tridiagonal{Float64} == Tridiagonal(Matrix(D) \ Matrix(T))
    @test (S / D)::Tridiagonal{Float64} == Tridiagonal(Matrix(S) / Matrix(D))
    @test (T / D)::Tridiagonal{Float64} == Tridiagonal(Matrix(T) / Matrix(D))
end

@testset "eigenvalue sorting" begin
    D = Diagonal([0.4, 0.2, -1.3])
    @test eigvals(D) == eigen(D).values == [0.4, 0.2, -1.3] # not sorted by default
    @test eigvals(Matrix(D)) == eigen(Matrix(D)).values == [-1.3, 0.2, 0.4] # sorted even if diagonal special case is detected
    E = eigen(D, sortby=abs) # sortby keyword supported for eigen(::Diagonal)
    @test E.values == [0.2, 0.4, -1.3]
    @test E.vectors == [0 1 0; 1 0 0; 0 0 1]
end

@testset "sum, mapreduce" begin
    D = Diagonal([1,2,3])
    Ddense = Matrix(D)
    @test sum(D) == 6
    @test_throws ArgumentError sum(D, dims=0)
    @test sum(D, dims=1) == sum(Ddense, dims=1)
    @test sum(D, dims=2) == sum(Ddense, dims=2)
    @test sum(D, dims=3) == sum(Ddense, dims=3)
    @test typeof(sum(D, dims=1)) == typeof(sum(Ddense, dims=1))
    @test mapreduce(one, min, D, dims=1) == mapreduce(one, min, Ddense, dims=1)
    @test mapreduce(one, min, D, dims=2) == mapreduce(one, min, Ddense, dims=2)
    @test mapreduce(one, min, D, dims=3) == mapreduce(one, min, Ddense, dims=3)
    @test typeof(mapreduce(one, min, D, dims=1)) == typeof(mapreduce(one, min, Ddense, dims=1))
    @test mapreduce(zero, max, D, dims=1) == mapreduce(zero, max, Ddense, dims=1)
    @test mapreduce(zero, max, D, dims=2) == mapreduce(zero, max, Ddense, dims=2)
    @test mapreduce(zero, max, D, dims=3) == mapreduce(zero, max, Ddense, dims=3)
    @test typeof(mapreduce(zero, max, D, dims=1)) == typeof(mapreduce(zero, max, Ddense, dims=1))

    D = Diagonal(Int[])
    Ddense = Matrix(D)
    @test sum(D) == 0
    @test_throws ArgumentError sum(D, dims=0)
    @test sum(D, dims=1) == sum(Ddense, dims=1)
    @test sum(D, dims=2) == sum(Ddense, dims=2)
    @test sum(D, dims=3) == sum(Ddense, dims=3)
    @test typeof(sum(D, dims=1)) == typeof(sum(Ddense, dims=1))

    D = Diagonal(Int[2])
    Ddense = Matrix(D)
    @test sum(D) == 2
    @test_throws ArgumentError sum(D, dims=0)
    @test sum(D, dims=1) == sum(Ddense, dims=1)
    @test sum(D, dims=2) == sum(Ddense, dims=2)
    @test sum(D, dims=3) == sum(Ddense, dims=3)
    @test typeof(sum(D, dims=1)) == typeof(sum(Ddense, dims=1))
end

@testset "logabsdet for generic eltype" begin
    d = Any[1, -2.0, -3.0]
    D = Diagonal(d)
    d1, s1 = logabsdet(D)
    @test d1 ≈ sum(log ∘ abs, d)
    @test s1 == prod(sign, d)
end

@testset "Empty (#35424)" begin
    @test zeros(0)'*Diagonal(zeros(0))*zeros(0) === 0.0
    @test transpose(zeros(0))*Diagonal(zeros(Complex{Int}, 0))*zeros(0) === 0.0 + 0.0im
    @test dot(zeros(Int32, 0), Diagonal(zeros(Int, 0)), zeros(Int16, 0)) === 0
end

@testset "Diagonal(undef)" begin
    d = Diagonal{Float32}(undef, 2)
    @test length(d.diag) == 2
end

@testset "permutedims (#39447)" begin
    for D in (Diagonal(zeros(5)), Diagonal(zeros(5) .+ 1im), Diagonal([[1,2],[3,4]]))
        @test permutedims(D) === permutedims(D,(1,2)) === permutedims(D,(2,1)) === D
        @test_throws ArgumentError permutedims(D,(1,3))
    end
end

@testset "Inner product" begin
    A = Diagonal(rand(10) .+ im)
    B = Diagonal(rand(10) .+ im)
    @test dot(A, B) ≈ dot(Matrix(A), B)
    @test dot(A, B) ≈ dot(A, Matrix(B))
    @test dot(A, B) ≈ dot(Matrix(A), Matrix(B))
    @test dot(A, B) ≈ conj(dot(B, A))
end

@testset "eltype relaxation(#41015)" begin
    A = rand(3,3)
    for trans in (identity, adjoint, transpose)
        @test ldiv!(trans(I(3)), A) == A
        @test rdiv!(A, trans(I(3))) == A
    end
end

const BASE_TEST_PATH = joinpath(Sys.BINDIR, "..", "share", "julia", "test")
isdefined(Main, :ImmutableArrays) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "ImmutableArrays.jl"))
using .Main.ImmutableArrays

@testset "Conversion to AbstractArray" begin
    # tests corresponding to #34995
    d = ImmutableArray([1, 2, 3, 4])
    D = Diagonal(d)

    @test convert(AbstractArray{Float64}, D)::Diagonal{Float64,ImmutableArray{Float64,1,Array{Float64,1}}} == D
    @test convert(AbstractMatrix{Float64}, D)::Diagonal{Float64,ImmutableArray{Float64,1,Array{Float64,1}}} == D
end

@testset "divisions functionality" for elty in (Int, Float64, ComplexF64)
    B = Diagonal(rand(elty,5,5))
    x = rand(elty)
    @test \(x, B) == /(B, x)
end

@testset "promotion" begin
    for (v1, v2) in (([true], [1]), ([zeros(2,2)], [zeros(Int, 2,2)]))
        T = promote_type(eltype(v1), eltype(v2))
        V = promote_type(typeof(v1), typeof(v2))
        d1 = Diagonal(v1)
        d2 = Diagonal(v2)
        v = [d1, d2]
        @test (@inferred eltype(v)) == Diagonal{T, V}
    end
    # test for a type for which promote_type doesn't lead to a concrete eltype
    struct MyArrayWrapper{T,N,A<:AbstractArray{T,N}} <: AbstractArray{T,N}
       a :: A
    end
    Base.size(M::MyArrayWrapper) = size(M.a)
    Base.axes(M::MyArrayWrapper) = axes(M.a)
    Base.length(M::MyArrayWrapper) = length(M.a)
    Base.getindex(M::MyArrayWrapper, i::Int...) = M.a[i...]
    Base.setindex!(M::MyArrayWrapper, v, i::Int...) = M.a[i...] = v
    d1 = Diagonal(MyArrayWrapper(1:3))
    d2 = Diagonal(MyArrayWrapper(1.0:3.0))
    c = [d1, d2]
    @test c[1] == d1
    @test c[2] == d2
end

@testset "zero and one" begin
    D1 = Diagonal(rand(3))
    @test D1 + zero(D1) == D1
    @test D1 * one(D1) == D1
    @test D1 * oneunit(D1) == D1
    @test oneunit(D1) isa typeof(D1)
    D2 = Diagonal([collect(reshape(1:4, 2, 2)), collect(reshape(5:8, 2, 2))])
    @test D2 + zero(D2) == D2
    @test D2 * one(D2) == D2
    @test D2 * oneunit(D2) == D2
    @test oneunit(D2) isa typeof(D2)
    D3 = Diagonal([D2, D2]);
    @test D3 + zero(D3) == D3
    @test D3 * one(D3) == D3
    @test D3 * oneunit(D3) == D3
    @test oneunit(D3) isa typeof(D3)
end

@testset "AbstractTriangular" for (Tri, UTri) in ((UpperTriangular, UnitUpperTriangular), (LowerTriangular, UnitLowerTriangular))
    A = randn(4, 4)
    TriA = Tri(A)
    UTriA = UTri(A)
    D = Diagonal(1.0:4.0)
    DM = Matrix(D)
    DMF = factorize(DM)
    outTri = similar(TriA)
    out = similar(A)
    # 2 args
    for fun in (*, rmul!, rdiv!, /)
        @test fun(copy(TriA), D)::Tri == fun(Matrix(TriA), D)
        @test fun(copy(UTriA), D)::Tri == fun(Matrix(UTriA), D)
    end
    for fun in (*, lmul!, ldiv!, \)
        @test fun(D, copy(TriA))::Tri == fun(D, Matrix(TriA))
        @test fun(D, copy(UTriA))::Tri == fun(D, Matrix(UTriA))
    end
    # 3 args
    @test outTri === ldiv!(outTri, D, TriA)::Tri == ldiv!(out, D, Matrix(TriA))
    @test outTri === ldiv!(outTri, D, UTriA)::Tri == ldiv!(out, D, Matrix(UTriA))
    @test outTri === mul!(outTri, D, TriA)::Tri == mul!(out, D, Matrix(TriA))
    @test outTri === mul!(outTri, D, UTriA)::Tri == mul!(out, D, Matrix(UTriA))
    @test outTri === mul!(outTri, TriA, D)::Tri == mul!(out, Matrix(TriA), D)
    @test outTri === mul!(outTri, UTriA, D)::Tri == mul!(out, Matrix(UTriA), D)
    # 5 args
    @test outTri === mul!(outTri, D, TriA, 2, 1)::Tri == mul!(out, D, Matrix(TriA), 2, 1)
    @test outTri === mul!(outTri, D, UTriA, 2, 1)::Tri == mul!(out, D, Matrix(UTriA), 2, 1)
    @test outTri === mul!(outTri, TriA, D, 2, 1)::Tri == mul!(out, Matrix(TriA), D, 2, 1)
    @test outTri === mul!(outTri, UTriA, D, 2, 1)::Tri == mul!(out, Matrix(UTriA), D, 2, 1)
end

end # module TestDiagonal


"""
    PseudoTiledMatrix(data::Matrix,sz::Int)

Wrap a `Matrix` in a tiled structure of size `sz`, where `getindex(A,i,j)`
returns a view of the `(i,j)` block (of size `(sz × sz)`). No copy of `data` is
made, but the elements in a block are not continguos in memory. If `sz` is not a
divisor of the matrix size, one last row/column block will be included of size
given by the remainder fo the division.
"""
struct PseudoTiledMatrix{T}
    data::Matrix{T}
    # rows/columns of block i are given by partition[i]:partition[i+1]-1
    partition::Vector{Int}
    function PseudoTiledMatrix(data::Matrix,s::Int)
        T = eltype(data)
        m,n = size(data)
        p = 1:s:m |> collect
        push!(p,m+1)
        new{T}(data,p)
    end
end

function Base.size(A::PseudoTiledMatrix)
    length(A.partition)-1,length(A.partition)-1
end

function Base.getindex(A::PseudoTiledMatrix,i::Int,j::Int)
    @assert 1 ≤ i ≤ size(A)[1]
    @assert 1 ≤ j ≤ size(A)[2]
    p = A.partition
    I = p[i]:(p[i+1]-1)
    J = p[j]:(p[j+1]-1)
    view(A.data,I,J)
end