#=
    Port of SPPL model
    https://github.com/probcomp/sppl/blob/master/examples/fairness-hiring-model-1.ipynb

    The SPPL model gives the following exact probabilities:
        p_hire_given_minority:0.007131626828051439
        p_hire_given_majority:0.01945024229170891
        min/maj: 0.3666600508668959

    This Turing.jl model:
    Summary Statistics
            parameters      mean       std   naive_se      mcse         ess      rhat   ess_per_sec
                Symbol   Float64   Float64    Float64   Float64     Float64   Float64       Float64

           ethnicity    1.8531    0.3540     0.0035    0.0046   6492.4337    1.0000     1198.5294
    years_experience    7.5026    1.9193     0.0192    0.0230   6416.1713    0.9999     1184.4510
        college_rank   20.6405    7.0939     0.0709    0.0827   6775.2100    1.0000     1250.7310
                hire    0.0199    0.1397     0.0014    0.0018   7133.8377    1.0002     1316.9351

    Quantiles
            parameters      2.5%     25.0%     50.0%     75.0%     97.5%
                Symbol   Float64   Float64   Float64   Float64   Float64

           ethnicity    1.0000    2.0000    2.0000    2.0000    2.0000
    years_experience    4.0000    6.0000    8.0000    9.0000   11.0000
        college_rank    5.9163   16.9625   20.4955   24.4271   35.7348
                hire    0.0000    0.0000    0.0000    0.0000    0.0000

    Distributions of variable ethnicity
    majority   =>    8531  (0.853100)
    minority   =>    1469  (0.146900)
    ethnicity: 1 minority, 2: majority:
    Distributions of variable (num:0)
    [2, 0]  =>      8345 (0.8345)
    [1, 0]  =>      1456 (0.1456)
    [2, 1]  =>      186 (0.0186)
    [1, 1]  =>      13 (0.0013)
    hire | minority: 0.008849557522123894
    hire | majority: 0.021802836713163756
    min / maj: 0.4058901893614997

=#

using Turing
include("jl_utils.jl")

#=
SPPL model:
# Prior on ethnicity, experience, and college rank.
ethnicity ~= choice({'minority': .15, 'majority': .85})
years_experience ~= binom(n=15, p=.5)
if (ethnicity == 'minority'):
    college_rank ~= dlaplace(loc=25, a=1/5)
else:
    college_rank ~= dlaplace(loc=20, a=1/5)

# Top 50 colleges and at most 20 years of experience.
condition((0 <= college_rank) <= 50)
condition((0 <= years_experience) <= 20)

# Hiring decision.
if college_rank <= 5:
    hire ~= atomic(loc=1)
else:
    switch (years_experience) cases (years in range(0, 20)):
        if ((years - 5) > college_rank):
            hire ~= atomic(loc=1)
        else:
            hire ~= atomic(loc=0)
=#
@model function fairness_model1()
    minority = 1
    majority = 2

    ethnicity ~ Categorical([0.15,0.85])
    years_experience ~ Binomial(15,0.5)
    college_rank ~ ethnicity == minority ? Laplace(25,5) : Laplace(20,5)

    # Top 50 colleges and at most 20 years of experience.
    true ~ Dirac(college_rank <= 50)
    true ~ Dirac(years_experience <= 20)

    # Hiring decision.
    if college_rank <= 5
        hire ~ Dirac(1)
    else
        # hakank: I'm not sure that I understand this: Why years_experience related to college rank?
        # Some seconds later: Ah, that's from the underlying decision tree!
        if ((years_experience - 5) > college_rank)
            hire ~ Dirac(1)
        else
            hire ~ Dirac(0)
        end
    end

    return [ethnicity, hire]

end

model = fairness_model1()

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

display(chns)

show_var_dist_pct(chns,:ethnicity,["minority","majority"])
eth = group(chns,:ethnicity).value.data |> make_hash

println("ethnicity: 1 minority, 2: majority:")
chains_params = Turing.MCMCChains.get_sections(chns, :parameters)
genq = generated_quantities(model, chains_params)
show_var_dist_pct(genq)

hhh = genq[:,1] |> make_hash
hire_min = get(hhh,[1,1],0) / eth[1]
hire_maj = get(hhh,[2,1],0) / eth[2]
println("hire | minority: $hire_min")
println("hire | majority: $hire_maj")
println("min / maj: $(hire_maj > 0 ? hire_min / hire_maj : 0)")


#
# This short script demonstrates that we should really be using
# StaticPolynomials to evaluate the final NBodyIP
#

import StaticPolynomials

using StaticArrays, BenchmarkTools
using NBodyIPs

using NBodyIPs.Polys: monomial, monomial_d, gen_tuples

function eval_poly(A::Vector{NTuple{7,Int}},
                   C::Vector{Float64},
                   x::SVector{12, Float64})
   E = 0.0
   x6 = x[SVector(1,2,3,4,5,6)]
   for (α, c) in zip(A, C)
      E += c * x[7+α[end]] * monomial(α, x6)
   end
   return E
end

function eval_poly_d(A::Vector{NTuple{7,Int}},
                     C::Vector{Float64},
                     x::SVector{12, Float64})
   E = zero(Float64)
   dM = zero(SVector{6, Float64})
   dE = zero(SVector{6, Float64})
   x6 = x[SVector(1,2,3,4,5,6)]
   #
   for (α, c) in zip(A, C)
      m, m_d = monomial_d(α, x6)
      E += c * x[7+α[end]] * m        # just the value of the function itself
      dM += (c * x[7+α[end]]) * m_d   # the I2 * ∇m term without the chain rule
      dE += (c * m) * x6              # a made-up line
   end
   # chain rule
   dE += dM
   return dE
end

function sp_exps(A)
   exps = zeros(Int, (length(A), 12))
   for (i, a) in enumerate(A)
      exps[i,1:6] .= a[1:6]
      exps[i,7+a[end]] = 1
   end
   return exps
end

A = NBodyIPs.Polys.gen_tuples(4, 10)
C = rand(length(A))
SP = StaticPolynomials.Polynomial(C, sp_exps(A)')

x = @SVector rand(12)

print("   hand-coded:"); @btime eval_poly($A, $C, $x)
print("  StaticPolys:"); @btime StaticPolynomials.evaluate($SP, $x)
print(" ∇-hand-coded:"); @btime eval_poly_d($A, $C, $x)
print("∇-StaticPolys:"); @btime StaticPolynomials.gradient($SP, $x)


main() = let part1 = 0, part2 = 0
    groups = split.(split(read("input.txt", String), "\n\n"))
    part1 = sum(length(union(g...)) for g in groups)
    part2 = sum(length(intersect(g...)) for g in groups)
    part1, part2
end


# Note: this code more or less comes directly from https://en.wikipedia.org/wiki/MD5
# I believe this is fair use. and does not have license implications

const kk =  floor.(UInt32, Int64(2)^32 * abs.(sin.(1:64)))

const ss = UInt64[
7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,  7, 12, 17, 22,
5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,
4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,  4, 11, 16, 23,
6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,
]

const MD5_initial_hash_value = UInt32[0x67452301, 0xefcdab89, 0x98badcfe,  0x10325476] # A,B,C,D


using CPLEX

struct MO_GAP_Inst
    m::Model
    a::Matrix{Int}
    b::Vector{Int}
    c1::Matrix{Int}
    c2::Matrix{Int}
end

function parseGAP(fname, solver=CplexSolver())
    f = open(fname)
    n,m = parse.(Int, split(readline(f)))

    a = zeros(Int, n, m)
    c = zeros(Int, n, m)

    for i = 1:n
        cij = Int[]
        while length(cij) < m
            append!(cij, parse.(Int, split(readline(f))))
        end
        @assert length(cij) == m
        c[i, :] .= cij
    end

    for i = 1:n
        aij = Int[]
        while length(aij) < m
            append!(aij, parse.(Int, split(readline(f))))
        end
        @assert length(aij) == m
        a[i, :] .= aij
    end

    b = Int[]
    while length(b) < n
        append!(b, parse.(Int, split(readline(f))))
    end

    c2 = reshape(shuffle(MersenneTwister(0), vec(c)), n, m)

    #m = m÷4

    mod = vModel(solver=solver)
    @variable(mod, x[1:n, 1:m], Bin)
    @addobjective(mod, Min, sum(c[i,j]*x[i,j] for i = 1:n, j = 1:m))
    @addobjective(mod, Min, sum(c2[i,j]*x[i,j] for i = 1:n, j = 1:m))
    @constraint(mod, [i=1:n], sum(a[i,j]*x[i,j] for j = 1:m) <= b[i])
    @constraint(mod, [j=1:m], sum(x[i,j] for i = 1:n) == 1)

    mod,a,b,c,c2
end

function parseGAP_orlib(fname, solver=CplexSolver(CPX_PARAM_SCRIND = 0))
    f = open(fname)
    nbinst = parse(Int, chomp(readline(f)))
    res = MO_GAP_Inst[]

    for _ = 1:nbinst

        n,m = parse.(Int, split(readline(f)))
        a = zeros(Int, n, m)
        c = zeros(Int, n, m)

        for i = 1:n
            cij = Int[]
            while length(cij) < m
                append!(cij, parse.(Int, split(readline(f))))
            end
            @assert length(cij) == m
            c[i, :] .= cij
        end

        for i = 1:n
            aij = Int[]
            while length(aij) < m
                append!(aij, parse.(Int, split(readline(f))))
            end
            @assert length(aij) == m
            a[i, :] .= aij
        end

        b = Int[]
        while length(b) < n
            append!(b, parse.(Int, split(readline(f))))
        end

        c2 = reshape(shuffle(MersenneTwister(0), vec(c)), n, m)
        #m = m÷4

        mod = vModel(solver=solver)
        @variable(mod, x[1:n, 1:m], Bin)
        @addobjective(mod, Min, sum(c[i,j]*x[i,j] for i = 1:n, j = 1:m))
        @addobjective(mod, Min, sum(c2[i,j]*x[i,j] for i = 1:n, j = 1:m))
        @constraint(mod, [i=1:n], sum(a[i,j]*x[i,j] for j = 1:m) <= b[i])
        @constraint(mod, [j=1:m], sum(x[i,j] for i = 1:n) == 1)

        push!(res, MO_GAP_Inst(mod,a,b,c,c2))
    end
    close(f)
    res
end


export Dykstra_prox_parallel
function Dykstra_prox_parallel(
                                        x::Vector{TF},
                                        P::Vector{Any},
                                        P_sub,
                                        TD_OP,
                                        closed_form,
                                        maxit_dyk,
                                        dyk_feas_tol,
                                        obj_dyk_tol
                                        ) where {TF<:Real}

#this script computes the projection of x0 onto the intersection of and
#arbitrary number of closed and convex sets. May still work in case
#nonconvex sets are used.
# Solves: min_x 0.5||x-x0||_2^2 s.t. x in intersection of m convex sets.
#
# This algorithm allows for parallel evaluation of the prox part
# (projections). This version does it in serial anyway, so no Matlab
# Parallel computing toolbox is required.
#
# input:
#       x                   -   vector to be projection onto the intersection
#       P                   -   contains projectors. P{1},P{2}...P{m} as generated by setup_constraint.m in folder :
#       options_PARSDMM.
#           options_PARSDMM.tol     -   tolerance based on residual as stopping condition
#           options_PARSDMM.maxIt   -   maximum number of Parallel proximal Dykstra-like iterations
#           options_PARSDMM.minIt   -   minimum number of Parallel proximal Dykstra-like iterations
#           options_PARSDMM.log_vec -   if (=1), saves all vectors at every iteration for analysis purposes.
#           options_PARSDMM.feas_tol-   feasibility tolerance for warning message, not for stopping condition
#           options_PARSDMM.evol_rel_tol- tolerance on relative evolution between iterations: exit if norm(x-x_old)/norm(x_old) becomes too small
#
# output:
#       x           -   result
#       res         -   relative residual
#       prog_rel    -   relative evolution (movement) per iteration

# Author: Bas Peters

# Initialize
N       = length(x)
m = length(P) #number of constraints
ptp   = Vector{TF}(undef,N)
x_old = Vector{TF}(undef,N)
copyto!(ptp,x)
copyto!(x_old,x)
z=Vector{Vector{TF}}(undef,m)
p=Vector{Vector{TF}}(undef,m)

obj=zeros(TF,maxit_dyk)
rel_feasibility_err=zeros(TF,maxit_dyk+1,m)
evol_x = zeros(TF,maxit_dyk+1,m);

l1_P=zeros(Int64,maxit_dyk)
bounds_P=zeros(Int64,maxit_dyk)
ARADMM_it=zeros(Int64,maxit_dyk)
cg_it=zeros(Int64,maxit_dyk)

for i=1:m
    z[i]=zeros(TF,N)
    copyto!(z[i],x)
    p[i]=zeros(TF,N)
end
omega   = 1.0 ./ m; #weights are hardcored here

#initial feasibility (transform-domain, relative)
#check distance to feasibility
for i=1:m
  rel_feasibility_err[1,i]=norm(P_sub[i](TD_OP[i]*x)-TD_OP[i]*x)/norm(TD_OP[i]*x)
end

## Main loop
for n=1:maxit_dyk

    #evaluate prox
    temp1=0
    temp2=0
    temp3=0
    temp4=0
    for i=1:m #make this loop parallel for actual parallel proximal evaluation
        if closed_form[i]==false
          (p[i],log_PARSDMM) = P[i](z[i])
          if m==2; temp1= length(log_PARSDMM.obj); end
          if m==3; temp2= length(log_PARSDMM.obj); end
          if m==2; temp3= sum(log_PARSDMM.cg_it); end
          if m==3; temp4= sum(log_PARSDMM.cg_it); end
          if m==2; bounds_P[n]= length(log_PARSDMM.obj); end
          if m==3; l1_P[n] = length(log_PARSDMM.obj); end
        else
          copyto!(p[i],P[i](z[i]))
        end
  end
#   [ (p[i],log_PARSDMM) = P[i](z[i])
#         if m==2; temp1= length(log_PARSDMM.obj); end
#         if m==3; temp2= length(log_PARSDMM.obj); end
#         if m==2; temp3= sum(log_PARSDMM.cg_it); end
#         if m==3; temp4= sum(log_PARSDMM.cg_it); end
#         if m==2; bounds_P[n]= length(log_PARSDMM.obj); end
#         if m==3; l1_P[n] = length(log_PARSDMM.obj); end
#       else
#         copy!(p[i],P[i](z[i]))
#       end
# end
  ARADMM_it[n] = max(temp1,temp2)
  cg_it[n] = max(temp3,temp4)

    #averaging step
    if n>1; copyto!(x_old,x); end;
    fill!(x,TF(0.0))
    for i=1:m
        x .= x .+ omega.*p[i];
    end

    #log objective
    obj[n] = 0.5*norm(ptp-x,2).^2;
    evol_x[n]=norm(x_old-x)/norm(x)

    #updating
    for i=1:m
        z[i] .= x .+ z[i] .- p[i];
    end

    #check distance to feasibility
    for i=1:m
      rel_feasibility_err[n+1,i]=norm(P_sub[i](TD_OP[i]*x)-TD_OP[i]*x)/norm(TD_OP[i]*x)
    end

    #stop if objective value does not change and x is sufficiently feasible for all sets
    if n>1 && (maximum(rel_feasibility_err[n+1,:])<dyk_feas_tol) && ( ( (obj[n]-obj[n-1])./obj[n] ) < obj_dyk_tol )
        println("stationary objective and reached feasibility, exiting Dykstra (iteration ",n,")")
        obj=obj[1:n]
        cg_it=cg_it[1:n]
        l1_P=l1_P[1:n]
        ARADMM_it=ARADMM_it[1:n]
        evol_x = evol_x[1:n]
        rel_feasibility_err=rel_feasibility_err[1:n+1,:]
        return x,obj,rel_feasibility_err,cg_it,ARADMM_it,l1_P,bounds_P,evol_x
    end
end

#obj=obj[1:n]
#rel_feasibility_err=rel_feasibility_err[1:n,:]

return x,obj,rel_feasibility_err,cg_it,ARADMM_it,l1_P,bounds_P,evol_x
end


export Polynomial

"""
    Polynomial{T<:Number}(coeffs::AbstractVector{T}, var=:x)

Construct a polynomial from its coefficients `a`, lowest order first, optionally in
terms of the given variable `x`. `x` can be a character, symbol, or string.

If ``p = a_n x^n + \\ldots + a_2 x^2 + a_1 x + a_0``, we construct this through
`Polynomial([a_0, a_1, ..., a_n])`.

The usual arithmetic operators are overloaded to work with polynomials as well as
with combinations of polynomials and scalars. However, operations involving two
polynomials of different variables causes an error except those involving a constant polynomial.

# Examples
```@meta
DocTestSetup = quote
    using Polynomials
end
```

```jldoctest
julia> Polynomial([1, 0, 3, 4])
Polynomial(1 + 3*x^2 + 4*x^3)

julia> Polynomial([1, 2, 3], :s)
Polynomial(1 + 2*s + 3*s^2)

julia> one(Polynomial)
Polynomial(1.0)
```
"""
struct Polynomial{T <: Number} <: StandardBasisPolynomial{T}
    coeffs::Vector{T}
    var::Symbol
    function Polynomial{T}(coeffs::Vector{T}, var::Symbol) where {T <: Number}
        length(coeffs) == 0 && return new{T}(zeros(T, 1), var)
        last_nz = findlast(!iszero, coeffs)
        last = max(1, last_nz === nothing ? 0 : last_nz)
        return new{T}(coeffs[1:last], var)
    end
end

@register Polynomial

"""
    (p::Polynomial)(x)

Evaluate the polynomial using [Horner's Method](https://en.wikipedia.org/wiki/Horner%27s_method), also known as synthetic division, as implemented in `evalpoly` of base `Julia`.

```@meta
DocTestSetup = quote
    using Polynomials
end
```

```@meta
DocTestSetup = quote
    using Polynomials
end
```

# Examples
```jldoctest
julia> p = Polynomial([1, 0, 3])
Polynomial(1 + 3*x^2)

julia> p(0)
1

julia> p.(0:3)
4-element Array{Int64,1}:
  1
  4
 13
 28
```
"""
(p::Polynomial{T})(x::S) where {T,S} = evalpoly(x, coeffs(p))

function Base.:+(p1::Polynomial{T}, p2::Polynomial{S}) where {T, S}
    n1, n2 = length(p1), length(p2)
    R = promote_type(T,S)
    if n1 > 1 && n2 > 1
       p1.var != p2.var && error("Polynomials must have same variable")
       if n1 >= n2
          c = R.(copy(p1.coeffs))
          for i = 1:n2
            c[i] += p2.coeffs[i]
          end
        else
            c = R.(copy(p2.coeffs))
            for i = 1:n1
              c[i] += p1.coeffs[i]
            end
        end
        return Polynomial(c, p1.var)
    elseif n1 <= 1
       c = R.(copy(p2.coeffs))
       c[1] += p1[0]
       return Polynomial(c, p2.var)
    else
       c = R.(copy(p1.coeffs))
       c[1] += p2[0]
       return Polynomial(c, p1.var)
    end
end

function Base.:*(p1::Polynomial{T}, p2::Polynomial{S}) where {T,S}

    n, m = length(p1)-1, length(p2)-1 # not degree, so pNULL works
    if n > 0 && m > 0
        p1.var != p2.var && error("Polynomials must have same variable")
        R = promote_type(T, S)
        c = zeros(R, m + n + 1)
        for i in 0:n, j in 0:m
            @inbounds c[i + j + 1] += p1[i] * p2[j]
        end
        return Polynomial(c, p1.var)
    elseif n <= 0
        return Polynomial(copy(p2.coeffs) * p1[0], p2.var)
    else
        return Polynomial(copy(p1.coeffs) * p2[0], p1.var)
    end

end


using Memorization
@static if VERSION < v"0.7.0-DEV.2005"
    using Base.Test
else
    using Test
end

immutable Dataset
    X::Matrix{Bool}
    y::Vector{Bool}
    function Dataset(X::Matrix, y::Vector)
        @assert size(X, 2) == length(y)
        new(X, y)
    end
end

tie = Dataset([true true], [true, false])
net = Network(1, [1], [1], Memorization.positive)
@test all(net(tie.X, tie.y) .== trues(length(tie.y)))
net = Network(1, [1], [1], Memorization.negative)
@test all(net(tie.X, tie.y) .== falses(length(tie.y)))

net = Network(2, [1], [2], Memorization.negative)
net([true true]', [true])
@test net([true false]')[1] == false
net([true false]', [true])
@test net([true false]')[1] == true

test = Dataset(
    [true false; true true; false true; false false]',
    [true, false, true, false]
)
net = Network(2, [1], [2])
@test all(net(test.X, test.y) .== test.y)

test = Dataset(
    [true false; true true; false true; false false]',
    [true, true, false, false]
)
net = Network(2, [1], [2])
@test all(net(test.X, test.y) .== test.y)

random = Dataset(
    convert(Matrix{Bool}, rand(4, 20) .> .3),
    convert(Vector{Bool}, rand(20) .> .3)
)
net = Network(4, [2, 1], [2, 2])
@test all(net(random.X, random.y) .== net(random.X))

net = Network(4, [1024, 1], [2, 2])
@test all(Memorization.numberofneurons(net) .== [2, 1])


println("=====================test_pricing_problem=====================")
# P = Problem(
#     [0:1, 0:1],             # Capacity of edges
#     [3, 5],                 # Selling period end of edges
#     5,                      # Number of timesteps (Start at 1)
#     [10, 20],               # Actions (prices)
#     [(1,), (2,), (1, 2)],   # Products (seqeuences of edge indeces)
#     Dict((1,) => 0.5,       # λ: Dictionary of demand intensities for products
#         (2,) => 0.5,
#         (1, 2) => 0.4),
# )

@testset "PricingProblem.jl" begin
using Mdp.PricingProblem

um = price -> prob_user_accept(price, slope_start=5., slope_end=30.)
e1, e2 = Edge(1, 3), Edge(1, 5)
prd1, prd2, prd12 = Product([e1,], 0.5, um),
                    Product([e2,], 0.5, um),
                    Product([e1,e2], 0.4, um)
P = Problem(
    [e1,e2],
    [prd1, prd2, prd12],
    [10,20],
    5
)

@testset "prob_user_accept" begin
    @test prob_user_accept(5.0; slope_start = 5.0, slope_end = 10.0) == 1.0
    @test prob_user_accept(10.0; slope_start = 5.0, slope_end = 10.0) == 0.0
    @test prob_user_accept(7.5; slope_start = 5.0, slope_end = 10.0) == 0.5
    @test prob_user_accept(3.0; slope_start = 5.0, slope_end = 10.0) == 1.0
    @test prob_user_accept(15.0; slope_start = 5.0, slope_end = 10.0) == 0.0
end

@testset "len_product_selling_period" begin
    @test len_product_selling_period(prd1) == 3
    @test len_product_selling_period(prd2) == 5
    @test len_product_selling_period(prd12) == 3
end

@testset "prob_prod_req" begin
    k = 3
    @test prob_prod_req(prd1, k, P) == 0.16666666666666666
    @test prob_prod_req(prd2, k, P) == 0.1
    @test prob_prod_req(prd12, k, P) == 0.13333333333333333
    k = 4
    @test prob_prod_req(prd1, k, P) == 0.
    @test prob_prod_req(prd2, k, P) == 0.1
    @test prob_prod_req(prd12, k, P) == 0.
end

@testset "product_generator" begin
    all_possible_products([e1, e2], 0.5, um)==[Product([e1], 0.5,um),
                                                Product([e2], 0.5,um),
                                                Product([e1,e2], 0.5,um)]
end

end


ENV["GKS_WSTYPE"] = "mov"  # or "webm", "mp4"
ENV["GKS_VIDEO_OPTS"] = "600x450@25@2x"

using GR

x = LinRange(0, 2π, 200)
for i = 0:100
    plot(x, sin.(x .+ i / 10.0))
end


using Test

include("perfect-numbers.jl")

@testset "Perfect numbers" begin

    @testset "Smallest perfect number is classified correctly" begin
        @test isperfect(6)
    end

    @testset "Medium perfect number is classified correctly" begin
        @test isperfect(28)
    end

    @testset "Large perfect number is classified correctly" begin
        @test isperfect(33550336)
    end
end

@testset "Abundant numbers" begin

    @testset "Smallest abundant number is classified correctly" begin
        @test isabundant(12)
    end

    @testset "Medium abundant number is classified correctly" begin
        @test isabundant(30)
    end

    @testset "Large abundant number is classified correctly" begin
        @test isabundant(33550335)
    end
end

@testset "Deficient numbers" begin

    @testset "Smallest prime deficient number is classified correctly" begin
        @test isdeficient(2)
    end

    @testset "Smallest non-prime deficient number is classified correctly" begin
        @test isdeficient(4)
    end

    @testset "Medium deficient number is classified correctly" begin
        @test isdeficient(32)
    end

    @testset "Large deficient number is classified correctly" begin
        @test isdeficient(33550337)
    end

    @testset "Edge case (no factors other than itself) is classified correctly" begin
        @test isdeficient(1)
    end
end

@testset "Invalid inputs" begin

    @testset "Zero is rejected (not a natural number)" begin
        @test_throws DomainError isdeficient(0)
        @test_throws DomainError isperfect(0)
        @test_throws DomainError isabundant(0)
    end

    @testset "Negative integer is rejected (not a natural number)" begin
        @test_throws DomainError isdeficient(-1)
        @test_throws DomainError isperfect(-1)
        @test_throws DomainError isabundant(-1)
    end
end


using Pkg; Pkg.activate("..")
using Turing
using Turkie
using GLMakie # You could also use CairoMakie or another backend
using CairoMakie
GLMakie.activate!()
CairoMakie.activate!()
Turing.@model function demo(x) # Some random Turing model
    m0 ~ Normal(0, 2)
    s ~ InverseGamma(2, 3)
    m ~ Normal(m0, √s)
    for i in eachindex(x)
        x[i] ~ Normal(m, √s)
    end
end

xs = randn(100) .+ 1;
m = demo(xs);
cb = TurkieCallback(m) # Create a callback function to be given to the sample function
chain = sample(m, NUTS(0.65), 30; callback = cb)

record(cb.figure, joinpath(@__DIR__, "video.gif")) do io
    addIO!(cb, io)
    sample(m,  NUTS(0.65), 50; callback = cb)
end

## Let's test if Soss work as well!
using Soss
using Random

sossdemo = Soss.@model x begin
    m0 ~ Normal(0, 2)
    s ~ InverseGamma(2, 3)
    m ~ Normal(m0, √s)
    x ~ For(eachindex(x)) do i
        Normal(m, √s)
    end
end

cb = TurkieCallback(Dict(:m0 => [:trace, :mean],
                        :s => [:autocov, :var]))

advancedHMC(sossdemo(), (x=xs,), 100; callback = cb)

## Test for array of parameters
using LinearAlgebra
D = 1
N = 20
Turing.@model function vectordemo(x, y, σ)
    m ~ Normal(0, 10)
    β ~ MvNormal(m * ones(D + 1), ones(D + 1))
    for i in eachindex(y)
        y[i] ~ Normal(dot(β, vcat(1, x[i])), σ)
    end
end

x = [rand(D) for _ in 1:N]
β = randn(D + 1) * 2
σ = 0.1
y = dot.(Ref(β), vcat.(1, x)) .+ σ * randn(N)

m = vectordemo(x, y, σ)

cb = TurkieCallback(m) # Create a callback function to be given to the sample function

chain = sample(m, NUTS(0.65), 200; callback = cb)


# Julia code
# Copyright (c) 2022 the SLICOTMath.jl developers
# Portions extracted from SLICOT-Reference distribution:
# Copyright (c) 2002-2020 NICONET e.V.

function run_mc01vd(datfile, io=stdout)
    f = open(datfile,"r")
    readline(f)
    vs = split(readline(f))
    A = parse(Float64, replace(vs[1],'D'=>'E'))
    B = parse(Float64, replace(vs[2],'D'=>'E'))
    C = parse(Float64, replace(vs[3],'D'=>'E'))
# interp call 1

    Z1RE, Z1IM, Z2RE, Z2IM, INFO = SLICOT.mc01vd!(A, B, C)
    println(io, "Z1RE = $Z1RE")
    println(io, "Z1IM = $Z1IM")
    println(io, "Z2RE = $Z2RE")
    println(io, "Z2IM = $Z2IM")
    @test INFO == 0
    INFO == 0 || return

if ( INFO!=0 )
else
end # if
    close(f)
end # run_X()


# ---
# title: 797. All Paths From Source to Target
# id: problem797
# author: Tian Jun
# date: 2020-10-31
# difficulty: Medium
# categories: Backtracking, Depth-first Search, Graph
# link: <https://leetcode.com/problems/all-paths-from-source-to-target/description/>
# hidden: true
# ---
#
# Given a directed acyclic graph ( **DAG** ) of `n` nodes labeled from 0 to n -
# 1, find all possible paths from node `0` to node `n - 1`, and return them in
# any order.
#
# The graph is given as follows: `graph[i]` is a list of all nodes you can visit
# from node `i` (i.e., there is a directed edge from node `i` to node
# `graph[i][j]`).
#
#
#
# **Example 1:**
#
# ![](https://assets.leetcode.com/uploads/2020/09/28/all_1.jpg)
#
#
#
#     Input: graph = [[1,2],[3],[3],[]]
#     Output: [[0,1,3],[0,2,3]]
#     Explanation: There are two paths: 0 -> 1 -> 3 and 0 -> 2 -> 3.
#
#
# **Example 2:**
#
# ![](https://assets.leetcode.com/uploads/2020/09/28/all_2.jpg)
#
#
#
#     Input: graph = [[4,3,1],[3,2,4],[3],[4],[]]
#     Output: [[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
#
#
# **Example 3:**
#
#
#
#     Input: graph = [[1],[]]
#     Output: [[0,1]]
#
#
# **Example 4:**
#
#
#
#     Input: graph = [[1,2,3],[2],[3],[]]
#     Output: [[0,1,2,3],[0,2,3],[0,3]]
#
#
# **Example 5:**
#
#
#
#     Input: graph = [[1,3],[2],[3],[]]
#     Output: [[0,1,2,3],[0,3]]
#
#
#
#
# **Constraints:**
#
#   * `n == graph.length`
#   * `2 <= n <= 15`
#   * `0 <= graph[i][j] < n`
#   * `graph[i][j] != i` (i.e., there will be no self-loops).
#   * The input graph is **guaranteed** to be a **DAG**.
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


#---------------------------------------------------------------------#
#This routine advances the solution in time using
#a simple General Order RK method.
#Written by F.X. Giraldo / P.R. Mugg on 9/21/19
#           Department of Applied Mathematics
#           Naval Postgraduate School
#           Monterey, CA 93943-5216
#---------------------------------------------------------------------#
function WS_general_rk(q0,c,I,Δt,time)

    #build RK Coefficients
    αi = zeros(I+1,1)
    for i = 1:I
        αi[i+1] = 1.0/(I-i+1)
    end

    qi = q0
    (rhs,f,g) = rhs_function(qi,c,time)
    for i = 1:I
        #Update
        qi = q0 + Δt*αi[i+1]*rhs

        #Construct RHS
        (rhs,f,g) = rhs_function(qi,c,(time + Δt*αi[i+1]))
    end #for i

    #final update
    return qp = qi

end #end function


function sparse_random_factory(;
    states=10,
    state_type=Float64,
    actions=2,
    action_type=Float64,
)
    return MDPs.Examples.sprandom(state_type, action_type, states, actions)
end

"sparse random called without type arguments  should return a vector of "
"sparse Float64 transition matrices"
let
    transition, reward = MDPs.Examples.sprandom(10, 2)
    @test typeof(transition) == Vector{SparseMatrixCSC{Float64,Int64}}
end

"sparse random should return a sparse Float64 reward"
let
    transition, reward = sparse_random_factory()
    @test typeof(reward) == SparseMatrixCSC{Float64,Int64}
end

"sparse random with Float32 types should return Float32 sparse matrices"
let
    transition, reward = sparse_random_factory(
        state_type=Float32,
        action_type=Float32,
    )
    @test typeof(transition) == Vector{SparseMatrixCSC{Float32,Int64}}
    @test typeof(reward) == SparseMatrixCSC{Float32,Int64}
end

"sparse random transition matrix should be square"
let
    transition, reward = sparse_random_factory()
    rows, columns = size(transition[1])
    @test rows == columns
end

"sparse random transition rows should sum to one"
let
    transition, reward = sparse_random_factory()
    @test_approx_eq sum(transition[1], 1) ones(size(transition[1])[1])
end

"sparse random transition probabilities should be non-negative"
let
    transition, reward = sparse_random_factory()
    @test all(nonzeros(transition[1]) .>= 0)
end

"sparse random transition matrices should be randomly generated"
let
    transition1, reward = sparse_random_factory()
    transition2, reward = sparse_random_factory()
    @test transition1[1] != transition2[1]
end


using Test

import MutableArithmetics
const MA = MutableArithmetics

using DynamicPolynomials

@testset "Mutable Arithmetics with $T" for T in [Int, BigInt]
    @polyvar x y z
    p = T(2)x^2 + T(4)y^2
    q = T(3)x * y
    @test p === MA.add!(p, q)
    @test q == T(3)x * y
    @test nterms(p) == 3
    @test issorted(monomials(p), rev=true)
    @test p == T(2)x^2 + T(3)x * y + T(4)y^2
    @test p === MA.add!(p, x * y)
    @test nterms(p) == 3
    @test issorted(monomials(p), rev=true)
    @test p == T(2)x^2 + T(4)x * y + T(4)y^2
    @test p === MA.add!(p, T(2)x^3 + T(3)x^2 + T(5)x + T(2))
    @test nterms(p) == 6
    @test issorted(monomials(p), rev=true)
    @test p == T(2)x^3 + T(5)x^2 + T(4)x * y + T(4)y^2 + T(5)x + T(2)
    q = T(5)z^3 + T(1)x*z + T(4)z^2 + T(3)z
    @test p === MA.add!(p, q)
    @test nterms(p) == 10
    @test issorted(monomials(p), rev=true)
    @test p == T(2)x^3 + T(5)x^2 + T(4)x * y + T(4)y^2 + T(5)x + T(2) + q

    @testset "Issue #62" begin
        @polyvar x y
        p = x^2 + x + 1
        q = rem(p, [x^2-y])
        @test q == x + y + 1
    end
end


@testset "Operators under mixed barrier conditions" begin
    regular_grids = [0:4, -1:6]
    irregular_grids = [[-1.0; 0.0; 2.0; 5.0; 9.0], [0.2; 0.3; 0.5; 0.9; 1.6; 2.5]]
        for x̄ in [regular_grids; irregular_grids],
            (L_bc_generator, L_generator) in [(L₁₋bc, L₁₋), (L₁₊bc, L₁₊), (L₂bc, L₂)],
            (ξ_lb, ξ_ub) in [(-2, 1), (-3, -2), (3, -4), (4, 3)],
            directions in [(:forward, :backward), (:backward, :forward),
                            (:backward, :backward), (:forward, :forward)]
            # Setup
            # RHS function
            f(x) = x^2

            # grid setup
            # x̄ = 0:4
            x = interiornodes(x̄)
            M = length(x)
            Δ_0p = x̄[2] - x̄[1]
            Δ_Mp = x̄[end] - x̄[end-1]

            # boundary condition
            bc = (Mixed(ξ = ξ_lb, direction = directions[1]),
                Mixed(ξ = ξ_ub, direction = directions[2]))

            # corresponding boundary condition matrix
            B_lb = (directions[1] != :backward) ? [-1+ξ_lb*Δ_0p; 1; zeros(M)] : [-1; 1+ξ_lb*Δ_0p; zeros(M)]
            B_ub = (directions[2] != :forward) ? [zeros(M); -1; 1+ξ_ub*Δ_Mp] : [zeros(M); -1+ξ_ub*Δ_Mp; 1]

            B = transpose([B_lb B_ub])
            b = [0; 0]

            # operator on the interior, with bc applied
            L_bc = L_bc_generator(x̄, bc)
            # operator on the exterior
            L = L_generator(x̄)

            # solution for interior v from L_bc
            v_bc = L_bc \ f.(x)
            # solution for extended v from L by stacking up boundary condition matrices
            v̄ = [L; B] \ [f.(x); b]

            # test if interior solutions are identical
            @test v_bc ≈ v̄[2:end-1]
            # test if extrapolated solutions are identical
            @test extrapolatetoboundary(v_bc, x̄, bc) ≈ v̄
        end
end

@testset "Accuracy test for absorbing boundary conditions" begin
    using LinearAlgebra
    regular_grids = [1:20, -5:-1]
    irregular_grids = [[-1.0; 0.0; 2.0; 5.0; 9.0], [0.2; 0.3; 0.5; 0.9; 1.6; 2.5]]
    for x̄ in [regular_grids; irregular_grids],
        (L_bc_generator, L_generator, L_affine_generator) in [(L₁₋bc, L₁₋, L₁₋affine), (L₁₊bc, L₁₊, L₁₊affine), (L₂bc, L₂, L₂affine)],
        loc in 0:3
        # Setup
        # RHS function
        f(x) = x^2

        # grid setup
        # x̄ = 0:4
        x = interiornodes(x̄)
        M = length(x)
        Δ_0p = x̄[2] - x̄[1]
        Δ_Mp = x̄[end] - x̄[end-1]

        # boundary condition
        bc = (Absorbing(loc), Absorbing())

        # corresponding boundary condition matrix
        B = transpose([[1; zeros(M+1)] [zeros(M+1); 1]])
        b = [0; 0]

        # operator on the interior, with bc applied
        L_bc = L_bc_generator(x̄, bc)
        # operator on the exterior
        L = L_generator(x̄)
        # apply interior boundary conditions (if needed)
        RHS = f.(x)
        if (bc[1].loc > 0)
            L[1:bc[1].loc,:] = zeros(bc[1].loc,M+2)
            L[1:bc[1].loc,2:(1+bc[1].loc)] = Diagonal(ones(bc[1].loc))
            RHS[1:bc[1].loc] .= zero(eltype(L))
        end

        # solution for interior v from L_bc
        v_bc = L_bc \ (f.(x) + L_affine_generator(x̄, f.(x), bc))
        # solution for extended v from L by stacking up boundary condition matrices
        v̄ = [L; B] \ [RHS; b]

        # test if extrapolated solutions are identical
        @test extrapolatetoboundary(v_bc, x̄, bc) ≈ v̄

    end
end

@testset "Accuracy test for nonhomogeneous absorbing boundary conditions" begin
    regular_grids = [0:4, -1:6]
    irregular_grids = [[-1.0; 0.0; 2.0; 5.0; 9.0], [0.2; 0.3; 0.5; 0.9; 1.6; 2.5]]
    for x̄ in [regular_grids; irregular_grids],
        (L_bc_generator, L_generator, L_affine_generator) in [(L₁₋bc, L₁₋, L₁₋affine), (L₁₊bc, L₁₊, L₁₊affine), (L₂bc, L₂, L₂affine)],
        (S_lower, S_upper) in [(0.0, 1.0), (1.0, 0.0), (1.5, -1.5), (1.2, 1.3), (1.0, 1.0)]
        # Setup
        # RHS function
        f(x) = x^2

        # grid setup
        # x̄ = 0:4
        x = interiornodes(x̄)
        M = length(x)
        Δ_0p = x̄[2] - x̄[1]
        Δ_Mp = x̄[end] - x̄[end-1]

        # boundary condition
        bc = (NonhomogeneousAbsorbing(S_lower), NonhomogeneousAbsorbing(S_upper))

        # corresponding boundary condition matrix
        B = transpose([[1; zeros(M+1)] [zeros(M+1); 1]])
        b = [S_lower; S_upper]

        # operator on the interior, with bc applied
        L_bc = L_bc_generator(x̄, bc)
        # operator on the exterior
        L = L_generator(x̄)

        # solution for interior v from L_bc
        v_bc = L_bc \ (f.(x) + L_affine_generator(x̄, f.(x), bc))
        # solution for extended v from L by stacking up boundary condition matrices
        v̄ = [L; B] \ [f.(x); b]

        # test if extrapolated solutions are identical
        @test extrapolatetoboundary(v_bc, x̄, bc) ≈ v̄

    end
end


fdir = "J:\\ENG\\PAC_Group\\CurrentProjects\\Electric ServoMotor Brake\\Plan & Proof\\Plan\\2019-9-13 Coil Winding"
_savefig = (plt,fname)-> begin
    fdir = "J:\\ENG\\PAC_Group\\CurrentProjects\\Electric ServoMotor Brake\\Plan & Proof\\Plan\\2019-9-13 Coil Winding"
    savefig(plt,joinpath(fdir,fname*".pdf"))
    savefig(plt,joinpath(fdir,fname*".png"))
end

plt=let
    step=1u"mm"
    id=70u"mm":step:140u"mm"
    od=80u"mm":step:150u"mm"
    len=26u"mm"
    power=50.0u"W"
    global f = x -> begin
        coil=CoilGeometry(x.id,x.od,x.len)
        if CoilFunctions.isvalidcoil(coil)
            CoilFunctions.minimumturns(coil,power,1.0)
        else
            NaN
        end
    end
    @time y=tmap(f,((id=id,od=od,len=len) for id in id, od in od))
    contour(ustrip.(id),ustrip.(od),y';fill=true,
        title="SBE7 24V Minimum Equivalent Turns",xlabel="ID (mm)",ylabel="OD (mm)",dpi=300)
    plot!([0, 100,100],[140,140,0];color=:red,linestyle=:dash,
        ylims=ustrip.(extrema(od)),xlims=ustrip.(extrema(id)),
        legend=:bottomright,label="SBE7 Reference")
end
_savefig(plt,"SBE7 24V Minimum Equivalent Turns")

plt=let
    step=1u"mm"
    id=70u"mm":step:140u"mm"
    od=80u"mm":step:150u"mm"
    len=26u"mm"
    power=50.0u"W"
    global f = x -> begin
        coil=CoilGeometry(x.id,x.od,x.len)
        if CoilFunctions.isvalidcoil(coil)
            CoilFunctions.minimumturns(coil,power,1.0;op=extrema)
        else
            (NaN, NaN)
        end
    end
    @time y=tmap(f,((id=id,od=od,len=len) for id in id, od in od))
    contour(ustrip.(id),ustrip.(od),map(x->x[2]-x[1],permutedims(y,(2,1)));fill=true,
        title="SBE7 24V Equivalent Range of Turn Count",xlabel="ID (mm)",ylabel="OD (mm)",dpi=300)
    plot!([0, 100,100],[140,140,0];color=:red,linestyle=:dash,
        ylims=ustrip.(extrema(od)),xlims=ustrip.(extrema(id)),
        legend=:bottomright,label="SBE7 Reference")
end
_savefig(plt,"SBE7 24V Equivalent Range of Turn Count")

plt=let
    step=0.1u"mm"
    id=70u"mm":step:140u"mm"
    od=80u"mm":step:150u"mm"
    len=26u"mm"
    power=50.0u"W"
    v = 24.0u"V"
    Ω = v^2/power |> u"Ω"
    global f = x -> begin
        coil=CoilGeometry(x.id,x.od,x.len)
        if CoilFunctions.isvalidcoil(coil)
            optimalcoil(coil,Ω,1.0).turns
        else
            NaN
        end
    end
    @time y=tmap(f,((id=id,od=od,len=len) for id in id, od in od))
    contour(ustrip.(id),ustrip.(od),y';fill=true,
        title="SBE7 24V Coil Minimum Turns",xlabel="ID (mm)",ylabel="OD (mm)",dpi=300)
    plot!([0, 100,100],[140,140,0];color=:red,linestyle=:dash,
        ylims=ustrip.(extrema(od)),xlims=ustrip.(extrema(id)),
        legend=:bottomright,label="SBE7 Reference")
end
_savefig(plt,"SBE7 24V Coil Minimum Turns")

plt=let
    step=0.1u"mm"
    id=80u"mm":step:120u"mm"
    od=130u"mm":step:150u"mm"
    len=26u"mm"
    power=50.0u"W"
    v = 24.0u"V"
    Ω = v^2/power |> u"Ω"
    global f = x -> begin
        coil=CoilGeometry(x.id,x.od,x.len)
        if CoilFunctions.isvalidcoil(coil)
            optimalcoil(coil,Ω,1.0).gauge
        else
            NaN
        end
    end
    @time y=tmap(f,((id=id,od=od,len=len) for id in id, od in od))
    contour(ustrip.(id),ustrip.(od),y';fill=true,
        title="SBE7 24V Coil Wire Gauge",xlabel="ID (mm)",ylabel="OD (mm)",dpi=300,
        levels=length(unique(y)))
end
_savefig(plt,"SBE7 24V Coil Wire Gauge")


using
    Test,
    AutocorrelationShell,
    Wavelets,
    LinearAlgebra,
    Plots

@testset "Transforms" begin include("testsets/transforms.jl") end
@testset "Visualizations" begin include("testsets/visualizations.jl") end


# tested for Julia v1.2
##### данная версия работает с арифметикой Float64
##### !!!!! и с данными с (слегка) НЕРАВНОМЕРНЫМ шагом "меток времени" (дискретного аргумента)

# © Сибирский федеральный университет, 2017
# Авторы: С.П.Царев, А.С.Пустошилов
# Теоретические детали и пример применения: S.P.Tsarev, A.A.Kytmanov, Discrete orthogonal polynomials as a tool for detection of small anomalies of time series: a case study of GPS final orbits: https://arxiv.org/abs/2004.00414

# Данный скрипт позволяет аппроксимировать полиномами больших степеней (степенью до нескольких сотен)
# одномерный набор скалярных и векторнозначных данных (который формально можно считать функцией дискретного аргумента, или временным рядом)
# в смысле наилучшего среднеквадратичного приближения с очень большим количеством точек (тысячи)
# и находить невязку аппроксимации.
# При этом можно приближать как весь отрезок данных полиномами на всем имеющемся в файле отрезке данных (или его части),
# так и проходя по нему скользящим окном (возможно, с перекрытием)

# Требует наличия вспомогательного скрипта service_functions-ru.jl со служебными функциями

############################################################
# Параметры запуска скрипта:
############################################################

# julia.exe <script-name.jl> <file> <read_columns> <output_degrees> <windowsparams>
# где:

# <script-name.jl> - имя данного файла

# <file> - имя обрабатываемого файла с данными.
# В качестве обрабатываемого файла должен выступать текстовый файл, содержащий данные для аппроксимации в отдельных колонках, в любом из числовых форматов (целые, с плавающей точкой, либо в экспоненциальном формате).
# Файл может содержать пустые и закомментированные строки (для комментирования используется символ #), такие строки в обработке будут пропущены.

# <read_columns> - номера считываемых из файла колонок
# номера указываются в квадратных скобках
# первым указывается номер колонки, содержащей дискретный аргумент ("метка времени").
# Если в качестве дискретного аргумента должен выступать номер строки,
# то необходимо указать 0 как столбец аргумента (см. примеры).
# Вторым и последующими указываются номера колонок, содержащие (вектор)-функцию этого аргумента.
# Примеры указания номеров колонок:
# [1,2] - первый столбец файла будет считан как дискретный аргумент, второй - как скалярная функция от этого аргумента;
# [0,1] - в качестве дискретного аргумента будет выступать номер строки, в качестве функции - первый столбец файла;
# [2,5:9] - в качестве дискретного аргумента будет выступать второй столбец, в качестве вектор-функции - столбцы 5,6,7,8,9

# <output_degrees> - какие степени полиномов будут использованы для вывода результатов.
# Фактически аппроксимация выполняется от 0 до максимальной степени, указанной в данном параметре, но выводится будут результаты только для указанных степеней.
# Примеры указания выводимых степеней:
# [1,2,4,5] - выполняется аппроксимация полиномами до 5-ой степени, и выводится результат после аппроксимации полиномами степени 1,2,4,5;
# [5] - выполняется аппроксимация полиномами до 5-ой степени, и выводится результат аппроксимации полиномами 5-ой степени;
# [1:4,5:5:20] - выполняется аппроксимация полиномами до 20-ой степени, и выводится результат после аппроксимации полиномами степени 1,2,3,4,5,10,15,20. Обозначения 1:4 и 5:5:20 имеют тот же смысл, что и для циклов в языке Julia.

# <windowsparams> - параметры окна обработки данных.
# В скрипте заложена возможность обработки данных тремя способами:
# M:N - одним окном;
# M:W:N - окнами с перекрытием в одну точку;
# M:W:S:N - окнами с заданным перекрытием.
# При этом всюду выше:
# M - начальная точка первого окна,
# N - последняя точка последнего окна (в качестве последней точки
# можно указать конец файла, для этого используется ключевое слово end),
# W - размер окна,
# S - перекрытие соседних окон.
# !!!ВАЖНОЕ ЗАМЕЧАНИЕ - 1!!!
# При формировании окон последнее окно формируется так, чтобы
# последняя точка окна совпадала с последней заданной точкой N,
# при этом перекрытие с предпоследним окном может быть больше, чем указано в исходных параметрах
# !!!ВАЖНОЕ ЗАМЕЧАНИЕ - 2!!!
# Если размер окна задан больше, чем количество доступных данных, то скрипт выдает ошибку в лог-файл и завершает работу
# **********************************************************
# Примеры указания параметров окна:
# 1:end - формируется одно окно размером в весь входной файл;
# 1:1000 - формируется одно окно размером в 1000 точек, начальная точка
# совпадает с первой точкой данных, конечная совпадает с 1000-й точкой данных;
# 1:100:1000 - формируются окна размером 100 точек по следующим отрезкам: (1, 100), (100, 199), (199, 298), ..., (901, 1000);
# 1:100:10:1000 - формируются окна размером 100 точек c перекрытием 10 точек по следующим отрезкам: (1, 100), (91, 190), (181, 280), ..., (901, 1000).

############################################################
# РЕЗУЛЬТАТЫ РАБОТЫ СКРИПТА
############################################################

# В результате работы скрипта в директории запуска формируется поддиректория APPROX,
# в которой создается еще одна поддиректория с именем <out_dir_name>,
# в <out_dir_name> по отдельности для каждого аппроксимируемого столбца из <read_columns>
# и каждой степени из <output_degrees> и каждого окна обработки
# создается файл из 4 столбцов:
# <дискретный аргумент>
# <исходные аппроксимируемые значения>
# <значения аппроксимирующего полинома>
# <невязка аппроксимации = разность второго и третьего стобца>
# В имени файла указываются аппроксимируемый столбец, начало окна аппроксимации (номер строки файла,
# не считая комментариев и пропущенных строк) и степень аппроксимирующего полинома.
# Все числа внутри файла выводятся @sprintf("%1.15e",...) - можно увеличить точность при необходимости.
# Для удобства дальнейшего построения графиков с помощью gnuplot расширение всех файлов: *.gplot_4
# **********************************************************
# Дополнительно в директории APPROX формируется файл result_max_resid.txt  (путем дописывания новых строк в конец, если этот файл уже есть),
# в котором содержится краткая информация о результате аппроксимации:
# максимум невязки при максимальной степени аппроксимации окна для каждого из входных столбцов
# В строку записываются следующие данные:
# - <out_dir_name> имя выходной директории аппроксимации
# - <current_window> обрабатываемое окно
# - максимум невязки для каждого аппроксимируемого столбца на текущем окне при максимальной степени аппроксимации

# **********************************************************
# Для логирования ошибок, если они возникли в ходе работы скрипта, в директории запуска скрипта создается файл errorlog_approx.log,
# в который выводятся следующие виды ошибок с указанием названия вычислительного эксперимента и времени запуска:
# "Некорректные данные, считано 0 строк" - происходит в случае, если скрипт не смог
# прочитать исходный файл или были указаны неверные номера столбцов;
# "Некорректные данные, длина окна больше, чем размер данных" -
# информирует о том, что при задании параметров окна был неверно выбран размер окна
# и данных недостаточно, чтобы покрыть хотя бы 1 окно.

############################################################
# Пример запуска:
############################################################
# julia <script.jl> F-testdata.txt [1:4] [0,1,3,5:5:20] 1:end
# 5:5:20 в <output_degrees> означает список степеней с началом 5, шагом 5 и концом 20
# 1:end означает обработку всего интервала "меток времени", с начала файла до конца как одного окна
############################################################

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

if length(ARGS) != 4
    println(length(ARGS))
    println("warning: Неверное количество входных аргументов")
    exit()
end

using DelimitedFiles
using LinearAlgebra
using Printf
using Dates

# Подключение модуля вспомогательных функций

include("service_functions-ru.jl")

# Начало основного скрипта аппроксимации

out_dir_name = "nonequ_$(ARGS[1])_col$(ARGS[2])_degs$(ARGS[3])_w[$(ARGS[4])]_Float64"
out_dir_name = replace(out_dir_name, ":"=>"-")

errorlog = "errorlog_approx.log"
max_resid_file = "APPROX/result_max_resid.txt"

filename = ARGS[1]
read_columns = make_numberlist(ARGS[2])

DATA = readbigfile(filename, read_columns, Float64)
length_DATA = length(DATA[:,1])

if length_DATA == 0
    add_log_messange(errorlog, "$(out_dir_name) Некорректные данные, считано 0 строк")
    exit()
end

output_degrees = make_numberlist(ARGS[3])
windows_list = make_windows(ARGS[4], length_DATA)

if length(windows_list) == 0
    add_log_messange(errorlog, "$(out_dir_name) Некорректные данные, длина окна больше, чем размер данных")
    exit()
end

mkpath("APPROX/$out_dir_name")

out_approx_max_resid = open(max_resid_file, "a")

cd("APPROX/$out_dir_name")

# maxdeg = output_degrees[end]
maxdeg = maximum(output_degrees)
sym_num_max_deg = length("$maxdeg")

# orthogonality tolerance:
prec10 = 15
# number of binary digits
prec = round(Int, prec10*log2(10))
orttol = 2.0^(-prec*90/100)
println("orttol=", orttol)

# Обработка окон в файле

for current_window in windows_list
    println("Processing window: $current_window")
    flush(stdout)
    # Подготовка таблицы значений дискретных ортогональных полиномов необходимых степеней для ТЕКУЩЕГО окна.
    # В массиве pnts - значения дискретного аргумента  (задаваемого первой прочитанной колонкой) одномерной дискретной (вектор)-функции (задаваемой остальными прочитанными колонками)
    pnts = DATA[current_window[1]:current_window[2],1]
    NN = length(pnts)
    p1 = pnts[1]
    pNN = pnts[end]
    # нормируем дискретный аргумент на интервал [-1,1]:
    pnts11 = zeros(Float64, NN)
    for k=1:NN
        pnts11[k] = 2*(pnts[k] - p1) / (pNN - p1) - 1
    end
    pols = zeros(Float64, NN, maxdeg + 1)
    # в массиве pols[j,k] будет формироваться значение ортог. полинома степени (k-1) в j-й точке дискретного аргумента (задаваемого первой прочитанной колонкой)
    # Вначале в этот массив будут внесены значения ортогональных полиномов Лежандра (они НЕ ортогональны на нашей лискретной решетке аргумента!), а потом специальным способом ортогонализованы и нормированы (вариант алгоритма Грама-Шмидта).
    # первый полином - константа
    pols[:,1] = ones(Float64, NN)
    # второй - линейная функция дискретного аргумента (задаваемого первой прочитанной колонкой)
    pols[:,2] = pnts11
    # формируем сначала (не дискретно-ортогональные!) ряды значений полиномов Лежандра (не нормированных) в точках pnts11
    for d = 3:(maxdeg + 1)
        n = d - 2
        # pols[:,d] =   (((2 * n + 1) .* pnts11 .* pols[:,d - 1]) - (n .* pols[:,d - 2])) ./ (n + 1);
        pols[:,d] =   (((2 * n + 1) * (pnts11 .* pols[:,d - 1])) - (n * pols[:,d - 2])) / (n + 1);
        println("degree = ", d - 1)
        flush(stdout)
    end
    println("Legendre polynomials done")
    norm0 = sqrt(sum(abs2, pols[:,1]))
    pols[:,1] = pols[:,1] / norm0
    println("Orthogonalization started")
    flush(stdout)
    max_resid_in_col = zeros(Float64, length(read_columns)-1)
    # полученные значения полиномов Лежандра теперь надо ортогонализовать!
    # pols[:,2] SHOULD be orthogonalized for non-equidistant points  !!!!
    for n = 2:(maxdeg + 1)
        u = pols[:,n]
        repN = 0
        nev = 1000000.0
        while ( nev > 2 * NN * orttol )
            repN = repN + 1
            for i = 1:(n - 1)
                cf = sum(u .* pols[:,i])
                u = u - cf * pols[:,i]  # Wikipedia version
                #  - именно такой вариант оказался устойчив при ортогонализации, даже для Float64 !!!!
            end
            nev = 0.0
            norm0 = sqrt(sum(abs2, u))
            for i = 1:(n - 1)
                # nev = max(nev, abs(sum(u .* pols[:,i])))
                nev = max(nev, abs(sum(u .* pols[:,i]))/norm0)
            end
        end
        norm0 = sqrt(sum(abs2, u))
        pols[:,n] = u / norm0
        println("degree = ", n - 1, "   max nev after orthogonalization = ", @sprintf("%1.15e",nev),
        "   repN = ", repN, "   norm was: ", @sprintf("%1.15e",norm0))
        flush(stdout)
    end
    # Конец ортогонализации - в массиве pols[:,:] имеем набор значений дискретных ортогональных полиномов,
    #   ортогональных и нормированных на множестве точек pnts в ДАННОМ окне
    println("Orthogonalization done")
    flush(stdout)
    for ind_cn = 2:length(read_columns)
        data_col = DATA[current_window[1]:current_window[2],ind_cn]
        data_arg = DATA[current_window[1]:current_window[2], 1]
        data_cn = copy(data_col)
        pol_approx = zeros(Float64, NN)
        # аппроксимация данных (по отдельности каждого столбца) полиномами - поскольку они ортогональны, достаточно простого скалярного произведения!
        for dn in 0:maxdeg
            p = pols[:,dn + 1]
            cf = sum(data_cn .* p)
            data_cn = data_cn .- (cf * p)
            pol_approx = pol_approx .+ (cf * p)
            print("col: ", read_columns[ind_cn], "   win: ", current_window, "   deg: ", dn)
            flush(stdout)
            if (dn in output_degrees)
                output_filename = "col$(read_columns[ind_cn])_win$(current_window)_deg$(print_with_zero(dn, sym_num_max_deg)).gplot_4"
                output_filename = replace(output_filename, " "=>"_")
                output_filename = replace(output_filename, ","=>"")
                out_file = open(output_filename, "w")
                for k = 1:length(pnts)
                    @printf(out_file, "%1.15e\t%1.15e\t%1.15e\t%1.15e\n", data_arg[k], data_col[k], pol_approx[k], data_cn[k])
                end
                println(" written!")
                close(out_file)
            else
                println()
                flush(stdout)
            end
        end # dn in 0:maxdeg
        max_resid_in_col[ind_cn-1] = maximum(abs.(data_cn))
    end # ind_cn
    println("$out_dir_name $(current_window) $(join(map(x->@sprintf("%1.15e",x),max_resid_in_col), "\t"))")
    println(out_approx_max_resid, "$out_dir_name $(current_window) $(join(map(x->@sprintf("%1.15e",x),max_resid_in_col), "\t"))")
    flush(out_approx_max_resid)
end # current_window

close(out_approx_max_resid)
exit()


@inline signbit(a::DoubleFloat{T}) where {T} = signbit(HI(a))
@inline sign(a::DoubleFloat{T}) where {T} = sign(HI(a))

@inline function (-)(a::DoubleFloat{T}) where {T}
    if iszero(LO(a))
        DoubleFloat(-HI(a), LO(a))
    else
        DoubleFloat(-HI(a), -LO(a))
    end
end

@inline function abs(a::DoubleFloat{T}) where {T}
    if HI(a) >= 0.0
        a
    else # HI(a) < 0.0
        -a
    end
end

@inline function flipsign(x::DoubleFloat{T}, y::T) where {T<:AbstractFloat}
    signbit(y) ? -x : x
end
@inline function flipsign(x::DoubleFloat{T}, y::DoubleFloat{T}) where {T<:AbstractFloat}
    signbit(y) ? -x : x
end

@inline function copysign(x::DoubleFloat{T}, y::T) where {T<:AbstractFloat}
    signbit(y) ? -(abs(x)) : abs(x)
end
@inline function copysign(x::DoubleFloat{T}, y::DoubleFloat{T}) where {T<:AbstractFloat}
    signbit(y) ? -(abs(x)) : abs(x)
end

function frexp(x::DoubleFloat{T}) where {T<:AbstractFloat}
    frhi, exphi = frexp(HI(x))
    frlo, explo = frexp(LO(x))
    return (frhi, exphi), (frlo, explo)
end

function ldexp(x::DoubleFloat{T}, exponent::I) where {T, I<:Integer}
    return DoubleFloat(ldexp(HI(x), exponent), ldexp(LO(x), exponent))
end

function ldexp(dhi::Tuple{T,T}, dlo::Tuple{T,T}) where {T<:AbstractFloat}
    return DoubleFloat(ldexp(dhi[1], dhi[2]), ldexp(dlo[1], dlo[2]))
end

function exponent(x::DoubleFloat{T}) where {T<:AbstractFloat}
    ehi = exponent(HI(x))
    elo = exponent(LO(x))
    return ehi, elo
end

function significand(x::DoubleFloat{T}) where {T<:AbstractFloat}
    shi = significand(HI(x))
    slo = significand(LO(x))
    return shi, slo
end

function signs(x::DoubleFloat{T}) where {T<:AbstractFloat}
    shi = sign(HI(x))
    slo = sign(LO(x))
    return shi, slo
end

function nextfloat(x::DoubleFloat{T}) where {T<:AbstractFloat}
    !isfinite(x) && return(x)
    signbit(x) && return -prevfloat(-x)
    iszero(LO(x)) && return DoubleFloat(HI(x), eps(HI(x)))
    return DoubleFloat{T}(HI(x), nextfloat(LO(x)))
end

function prevfloat(x::DoubleFloat{T}) where {T<:AbstractFloat}
    !isfinite(x) && return(x)
    signbit(x) && return -nextfloat(-x)
    iszero(LO(x)) && return DoubleFloat(HI(x), -eps(HI(x)))
    return DoubleFloat{T}(HI(x), prevfloat(LO(x)))
end

function prevfloat(x::DoubleFloat{T}, n::Int) where {T<:AbstractFloat}
     return nextfloat(x, -n)
end

function nextfloat(x::DoubleFloat{T}, n::Int) where {T<:AbstractFloat}
   !isfinite(x) && return(x)
   signbit(x) && return -prevfloat(-x, n)
   iszero(LO(x)) && return DoubleFloat(HI(x), sign(x)*T(n)*eps(HI(x)))
   signbit(LO(x)) && return DoubleFloat(HI(x), -prevfloat(-LO(x),n))
   return DoubleFloat(HI(x), nextfloat(LO(x),n))
end

eps(::Type{DoubleFloat{T}}) where {T<:AbstractFloat} = DoubleFloat{T}(eps(eps( one(T) ))/2)
eps(x::DoubleFloat{T}) where {T<:AbstractFloat} = abs(x) * eps(typeof(x))

ulp(::Type{DoubleFloat{T}}) where {T<:AbstractFloat} = DoubleFloat{T}(eps(eps( one(T) ))/4)
ulp(x::DoubleFloat{T}) where {T<:AbstractFloat} = abs(x) * ulp(typeof(x))

@inline function intpart(x::Tuple{T,T}) where {T<:AbstractFloat}
    hi, lo = x
    ihi = modf(hi)[2]
    ilo = modf(lo)[2]
    return add_2(ihi, ilo)
end

@inline function fracpart(x::Tuple{T,T}) where {T<:AbstractFloat}
    hi, lo = x
    fhi = modf(hi)[1]
    flo = modf(lo)[1]
    return add_2(fhi, flo)
end

function intpart(x::DoubleFloat{T}) where {T<:AbstractFloat}
    ihi, ilo = intpart(HILO(x))
    return DoubleFloat(ihi, ilo)
end

function fracpart(x::DoubleFloat{T}) where {T<:AbstractFloat}
    fhi, flo = fracpart(HILO(x))
    return DoubleFloat(fhi, flo)
end

function modf(x::DoubleFloat{T}) where {T<:AbstractFloat}
    hi, lo = HILO(x)
    fhi, ihi = modf(hi)
    flo, ilo = modf(lo)
    ihi, ilo = add_2(ihi, ilo)
    fhi, flo = add_2(fhi, flo)
    i = DoubleFloat(ihi, ilo)
    f = DoubleFloat(fhi, flo)
    return f, i
end

function fmod(fpart::DoubleFloat{T}, ipart::DoubleFloat{T}) where {T<:AbstractFloat}
   return ipart + fpart
end

function fmod(parts::Tuple{DoubleFloat{T}, DoubleFloat{T}}) where {T<:AbstractFloat}
   return parts[1] + parts[2]
end


module Logistic

using RecordedArrays
using Random
using Test
using EvolutionaryModelingTools

const c = ContinuousClock(100.0) # define a clock, the population will growth for 100 time unit

const r = 0.5
const d = 0.1
const K = 100
const n = recorded(DynamicEntry, c, 10)   # define a scalar to record population size

# simulate with this package

Random.seed!(1)

@cfunc growth_c(r, n) = r * n
@ufunc growth_u!(ind, n) = n[ind.I...] += 1 # ensure the indexing is correct

@cfunc death_c(d, n) = d * n
@ufunc death_u!(ind, n) = n[ind.I...] -= 1 # ensure the indexing is correct

@cfunc comp_c(r, K, n) = r * n * n / K
@ufunc comp_u!(ind::CartesianIndex{0}, n) = n[ind] -= 1

# the ind must be CartesianIndex{0}, this is just for test
@ufunc check_extinction(ind::CartesianIndex{0}, n) = n[ind] == 0 ? :extinct : :finnish # check

growth = Reaction(growth_c, growth_u!)
death = Reaction(death_c, death_u!)
competition = Reaction(comp_c, comp_u!)

ps = (; r, d, K, n)
rs = (growth, death, competition)

e1 = getentries(gillespie(check_extinction, c, ps, rs)[1].n)

# simulate manually

Random.seed!(1)

for _ in c
    global n
    # evaluate a_i
    growth_rate = r * n              # intrinsic growth
    death_rate = d * n               # intrinsic death
    competition_rate = r * n * n / K # resource competition

    summed = growth_rate + death_rate + competition_rate  # sum a_i

    τ = -log(rand()) / summed # compute time interval

    increase!(c, τ) # update current time

    # sample a reaction and adjust population size
    rn = rand() * summed
    if rn < growth_rate
        n[1] += 1
    else
        n[1] -= 1
    end

    state(n) <= 0 && break # break if population extinct
end

e2 = getentries(n)

@test getts(e1) == getts(e2)
@test getvs(e1) == getvs(e2)

end


"""Basic functions for computing models."""

"""
All foreground models inherit from this type.
"""
abstract type AbstractForegroundModel end

"""
    get_cosmology(::Type{T}; h=0.69, Neff=3.04, OmegaK=0.0,
        OmegaM=0.29, OmegaR=nothing, Tcmb=2.7255, w0=-1, wa=0)

Construct a background cosmology. This function duplicates the cosmology() function
in Cosmology.jl, but with the numeric type specified.

# Arguments:
- `::Type{T}`: numerical type to use for calculations

# Keywords
- `h` - Dimensionless Hubble constant
- `OmegaK` - Curvature density (Ω_k)
- `OmegaM` - Matter density (Ω_m)
- `OmegaR` - Radiation density (Ω_r)
- `Tcmb` - CMB temperature in Kelvin; used to compute Ω_γ
- `Neff` - Effective number of massless neutrino species; used to compute Ω_ν
- `w0` - CPL dark energy equation of state; `w = w0 + wa(1-a)`
- `wa` - CPL dark energy equation of state; `w = w0 + wa(1-a)`

# Example
```julia-repl
julia> get_cosmology(Float32; h=0.7)
Cosmology.FlatLCDM{Float32}(0.7f0, 0.7099147f0, 0.29f0, 8.5307016f-5)
```
"""
function get_cosmology(::Type{T}; h=0.69,
                   Neff=3.04,
                   OmegaK=0.0,
                   OmegaM=0.29,
                   OmegaR=nothing,
                   Tcmb=2.7255,
                   w0=-1,
                   wa=0) where T

    if OmegaR === nothing
        OmegaG = 4.48131e-7*Tcmb^4/h^2
        OmegaN = Neff*OmegaG*(7/8)*(4/11)^(4/3)
        OmegaR = OmegaG + OmegaN
    end

    OmegaL = 1 - OmegaK - OmegaM - OmegaR

    if !(w0 == -1 && wa == 0)
        return Cosmology.WCDM{T}(h, OmegaK, OmegaL, OmegaM, OmegaR, w0, wa)
    end

    if OmegaK < 0
        return Cosmology.ClosedLCDM{T}(h, OmegaK, OmegaL, OmegaM, OmegaR)
    elseif OmegaK > 0
        return Cosmology.OpenLCDM{T}(h, OmegaK, OmegaL, OmegaM, OmegaR)
    else
        return Cosmology.FlatLCDM{T}(h, OmegaL, OmegaM, OmegaR)
    end
end
get_cosmology(; h=0.69, Neff=3.04, OmegaK=0.0, OmegaM=0.29, OmegaR=nothing, Tcmb=2.7255,
    w0=-1, wa=0) where T = get_cosmology(Float32; h=h, Neff=Neff, OmegaK=OmegaK,
        OmegaM=OmegaM, OmegaR=OmegaR, Tcmb=Tcmb, w0=w0, wa=wa)

"""
    build_r2z_interpolator(min_z::T, max_z::T,
        cosmo::Cosmology.AbstractCosmology; n_bins=2000) where T

Construct a fast r2z linear interpolator.
"""
function build_r2z_interpolator(min_z::T, max_z::T,
    cosmo::Cosmology.AbstractCosmology; n_bins=2000) where T

    zrange = LinRange(min_z, max_z, n_bins)
    rrange = zero(zrange)
    for i in 1:n_bins
        rrange[i] = ustrip(T, u"Mpc",
            Cosmology.comoving_radial_dist(u"Mpc", cosmo, zrange[i]))
    end
    r2z = LinearInterpolation(rrange, zrange; extrapolation_bc=Line());
    return r2z
end

"""
    mz2c(m::T, z::T, cosmo::Cosmology.FlatLCDM{T}) where T

Compute concentration factor from Duffy et al. 2008.
"""
function mz2c(m::T, z::T, cosmo::Cosmology.FlatLCDM{T}) where T
    return T(7.85) * (m / (T(2.0e12)/cosmo.h))^(T(-0.081)) / (T(1.0)+z)^T(0.71)
end

"""
    m2r(m::T, cosmo::Cosmology.FlatLCDM{T}) where T

Convert virial mass to virial radius.
"""
function m2r(m::T, cosmo::Cosmology.FlatLCDM{T}) where T
    rho = T(2.78e11) * cosmo.Ω_m * cosmo.h^2
    return (T(3.0/(4π)) * m / rho)^T(1.0/3.0)
end

function f_nfw(x::T) where T
    return log1p(x) - x/(one(T)+x)
end
g(x, c, m) = m - f_nfw(x)/f_nfw(c)

function solve_r(c::T, lnm::T) where T
    m = exp(lnm)
    return find_zero( x -> g(x,c,m) , (T(0.0),T(100.0)), Bisection()) / c
end

"""
    build_c_lnm2r_interpolator(;cmin::T=1f-3, cmax::T=25.0f0,
        mmin::T=-7.1f0, mmax::T=0.0f0, nbin=100) where T

Generate a LinearInterpolation object that turns concentration
and ln(M_halo) into satellite radius.
"""
function build_c_lnm2r_interpolator(;cmin::T=1f-3, cmax::T=25.0f0,
        mmin::T=-7.1f0, mmax::T=0.0f0, nbin=100) where T
    # these defaults imply a minimum fractional mass of exp(-7)

    cs  = LinRange(cmin,cmax,nbin)
    ms  = LinRange(mmin,mmax,nbin)
    r = [solve_r(c_,m_) for c_ in cs, m_ in ms]

    return LinearInterpolation((T.(cs), T.(ms)), T.(r))
end

random_phi(::Type{Float32}) = rand(Float32) * Float32(2π)
random_theta(::Type{Float32}) = acos(2.0f0*rand(Float32)-1.0f0)
random_phi(t::DataType) = rand(t) * t(2π)
random_theta(t::DataType) = acos( t(2.0)*rand(t)-one(t))

"""
Inverse square law with redshift dependence.
"""
function l2f(luminosity::T, r_comoving::T, redshift::T) where T
    return luminosity / (T(4π) * r_comoving^2 * (one(T) + redshift) )
end

function flux2map!(result_map::Map{T_map,RingOrder}, fluxes, theta, phi) where {T_map, T}

    pixel_array = result_map.pixels
    fill!(pixel_array, zero(T))  # prepare the frequency map

    flux_to_Jy = ustrip(u"Jy", 1u"W/Hz*Mpc^-2")

    source_offset_I = generate_subhalo_offsets(sources.nsources_I)
    source_offset_II = generate_subhalo_offsets(sources.nsources_II)
    N_halo = size(sources.halo_mass, 1)
    total_n_sat_I = size(sources.L_I_151, 1)
    total_n_sat_II = size(sources.L_II_151, 1)

    flux_I = Array{T, 1}(undef, total_n_sat_I)
    flux_II = Array{T, 1}(undef, total_n_sat_II)
    redshift_I = Array{T, 1}(undef, total_n_sat_I)
    redshift_II = Array{T, 1}(undef, total_n_sat_II)
    θ_I = Array{T, 1}(undef, total_n_sat_I)
    θ_II = Array{T, 1}(undef, total_n_sat_II)
    ϕ_I = Array{T, 1}(undef, total_n_sat_I)
    ϕ_II = Array{T, 1}(undef, total_n_sat_II)

    Threads.@threads for i_halo in 1:N_halo
        nu = (1 + sources.redshift[i_halo]) * nu_obs
        hp_ind = sources.halo_hp_ind[i_halo]

        # Generate FR I fluxes
        for source_index in 1:sources.nsources_I[i_halo]
            # index of satellite in satellite arrays
            i_sat = source_offset_I[i_halo] + source_index
            L_c, L_l = get_core_lobe_lum(
                sources.L_I_151[i_sat], nu, model.I_R_int, model.I_γ,
                model.a_0,
                sources.a_coeff_I[1,i_sat], sources.a_coeff_I[2,i_sat],
                sources.a_coeff_I[3,i_sat], sources.cosθ_I[i_sat])

            flux_I[i_sat] = l2f(L_c+L_l, sources.dist[i_halo],
                sources.redshift[i_halo]) * flux_to_Jy
            redshift_I[i_sat] = sources.redshift[i_halo]
            θ_I[i_sat] = sources.θ[i_halo]
            ϕ_I[i_sat] = sources.ϕ[i_halo]
            pixel_array[hp_ind] += flux_I[i_sat]
        end

        # Generate FR II fluxes
        for source_index in 1:sources.nsources_II[i_halo]
            # index of satellite in satellite arrays
            i_sat = source_offset_II[i_halo] + source_index
            L_c, L_l = get_core_lobe_lum(
                sources.L_II_151[i_sat], nu, model.II_R_int, model.II_γ,
                model.a_0,
                sources.a_coeff_II[1,i_sat], sources.a_coeff_II[2,i_sat],
                sources.a_coeff_II[3,i_sat], sources.cosθ_II[i_sat])

            flux_II[i_sat] = l2f(L_c+L_l, sources.dist[i_halo],
                sources.redshift[i_halo]) * flux_to_Jy
            redshift_II[i_sat] = sources.redshift[i_halo]
            θ_II[i_sat] = sources.θ[i_halo]
            ϕ_II[i_sat] = sources.ϕ[i_halo]
            pixel_array[hp_ind] += flux_II[i_sat]
        end
    end

    # divide by healpix pixel size
    per_pixel_steradian = 1 / nside2pixarea(result_map.resolution.nside)
    pixel_array .*= per_pixel_steradian

    # maps are in Jansky per steradian, fluxes are in Jansky
    if return_fluxes
        return flux_I, redshift_I, θ_I, ϕ_I, flux_II, redshift_II, θ_II, ϕ_II
    end

end


# # Basic Regression
# This tutorial is a port of [hello_world.py](https://github.com/aamini/evidential-deep-learning/blob/d1d8e395fb083308d14fa92c5ce766e97b2a066a/hello_world.py) to julia.
# We create a synthetic dataset, train a simple evidential regression model and plot the results
using Flux, Plots
import EvidentialDeepLearning as EDL
using Distributions
# Lets create some data
function my_data(x_min, x_max, n; train=true)
    x = range(Float32(x_min), Float32(x_max), length=n)
    x = reshape(x, (1,n))
    σ = train ? 3f0 : 0f0
    y = x .^3 .+ rand(Normal(0f0, σ), size(x))
    return x, y
end
x_train, y_train = my_data(-4, 4, 1000)
x_test, y_test = my_data(-7, 7, 1000, train=false)
plot(title="Dataset", ylims=(-150,150), xlims=(-7,7))
scatter!(vec(x_train), vec(y_train), label="train", markersize=1)
plot!(vec(x_test), vec(y_test), label="truth")

# Our model is a basic single hidden layer dense network, where the output is lifted to the evidential world:
model = Flux.Chain(
    Dense(1, 64, Flux.relu),
    Dense(64, 4, identity),
    EDL.NIGRegressor(),
)
loss(x, y) = EDL.regression_loss(model(x), y, λ=1f-2)
# Lets train the model
data = Flux.Data.DataLoader((x_train, y_train), batchsize=100, shuffle=true)
opt = ADAM(5e-4) # training is quite sensitive to learning rate
for epoch in 1:500 # takes ~ 30s
    Flux.train!(loss, Flux.params(model), data, opt)
end
# Lets plot the results.
plot(legend=:bottom)
h = model(x_test)
preds = vec(EDL.predict.(h))
plot!(vec(x_test), preds, label="Prediction", color=:blue, xlims=(-7,7), ylims=(-150,150))
σ_pp = vec(EDL.std_predict.(h)) # std of the predictive posterior
lo = preds - σ_pp
hi = preds + σ_pp
plot!(vec(x_test), hi, fillrange=lo, label="Predicted error", color=:green, alpha=0.5)
scatter!(vec(x_train), vec(y_train), label="training data", markersize=1, color=:red)
plot!(vec(x_test), vec(y_test), label="truth", color=:black)


module Null_models_I_and_II
using LinearAlgebra
using DelimitedFiles

export sample_from_Null_model_I, sample_from_Null_model_II, PairwiseHammingDist,translate_fasta_to_num_matrix, transform_MSA_fasta, export_fasta_file
include("utils.jl")

##Dirac delta
function deltak(i::Int64,j::Int64)
  i==j ? 1.0 : 0.0
    end

###compute hamming distance matrix D
 function PairwiseHammingDist(sample::Array{Int64,2})
    Y=sample[1:end,1:end]
    Y = Y'
    (N,M) = size(Y)
    hav = zeros(M,M)

  @inbounds  for m in 1:M
               for n in (m+1):M
                hd = 0.
                for i = 1:N
                 hd += Y[i,m]!=Y[i,n]
                 end
               hd = hd / N

              hav[m,n] = hav[n,m]=hd
               end
              end
    return hav
end

function PairwiseHammingDist(sample::Any)
   if typeof(sample)==Array{Char,2}
     (M,N)=size(sample)
	 Y=Array{Int}(undef,M,N);
	 for i in 1:M
		for j in 1:N
         Y[i,j]=Di21[sample[i,j]]
        end
     end
    elseif typeof(sample)==Array{Int64,2}
          Y=sample[1:end,1:end]
    elseif typeof(sample)==String
        sample=translate_fasta_to_num_matrix(sample)
        Y=sample[1:end,1:end]
    else
     error("Null_models_I_and_II.jl - PairwiseHammingDist: input parameter format is wrong.\n")
   end
    Y = Y'
    (N,M) = size(Y)
    hav = zeros(M,M)

  @inbounds  for m in 1:M
               for n in (m+1):M
                hd = 0.
                for i = 1:N
                 hd += Y[i,m]!=Y[i,n]
                 end
               hd = hd / N

              hav[m,n] = hav[n,m]=hd
               end
              end
    return hav
end

#given column k and two rows m,n an exchange of the align entries a_km and a_kn is attempted.
function swap_align!(align::Array{Int64,2},m1::Int64,m2::Int64,k::Int64)
    alpha=align[m1,k]
    bet=align[m2,k]
    align[m1,k]=bet
    align[m2,k]=alpha
end

### cost function
function Energy(dtarget::Array{Float64,2},dnew::Array{Float64,2})
 return sum((dtarget .- dnew) .^2)
end

## frobenious distance between matrices
function compute_l2_error(d1::Array{Float64,2},d2::Array{Float64,2})

     return sqrt(sum((d1 .- d2).^2))/size(d1)[1]
end

# return delta matrix which store the increment in the hamming distance entries
function delta_d(align::Array{Int64,2},delta::Array{Float64,2},m1::Int64,m2::Int64,k::Int64)
    (M,L)=size(align)
    #delta .=0
 @inbounds @simd for j in 1:M
        if j!=m1&&j!=m2
        delta[j,m1]=-(deltak(align[m2,k],align[j,k])-deltak(align[m1,k],align[j,k]))/L
        delta[j,m2]=-delta[j,m1]
        delta[m1,j]=delta[j,m1]
        delta[m2,j]=delta[j,m2]
            end

        end

   return delta
end

function delta_d!(align::Array{Int64,2},delta::Array{Float64,2},m1::Int64,m2::Int64,k::Int64)
    (M,L)=size(align)
 @fastmath @inbounds @simd for j in 1:M
        if j!=m1&&j!=m2
        delta[j,m1]=-1.0*(deltak(align[m2,k],align[j,k])-deltak(align[m1,k],align[j,k]))/L
        delta[j,m2]=-1.0*delta[j,m1]
        delta[m1,j]=delta[j,m1]
        delta[m2,j]=delta[j,m2]
            end

        end

end

## change produced at d_current as consequence of swap entries m1 and m2
 function change_d!(d_current::Array{Float64,2},delta::Array{Float64,2},m1::Int64,m2::Int64)
        M=size(d_current)[1]

    @fastmath @inbounds @simd for j in 1:M
                     if j!=m1&&j!=m2
                     d_current[j,m1]=d_current[j,m1]+delta[j,m1]
                     d_current[j,m2]=d_current[j,m2]+delta[j,m2]
                     d_current[m1,j]=d_current[j,m1]
                     d_current[m2,j]=d_current[j,m2]
                     end
                    end

        end

##Frobenious norm betwenn hamming distances matrices at times t+1 (dinic) respect to dtarget
## given an alignment permutation
 function dE(dtarget::Array{Float64,2},dinic::Array{Float64,2},delta::Array{Float64,2},m1::Int64,m2::Int64)
  output=0.0
  s=size(dtarget)[1]
  @fastmath @inbounds for j in 1:s
  if j!=m1&&j!=m2
  output+=2*(dinic[j,m1]-dtarget[j,m1])*delta[j,m1]+delta[j,m1]^2+2*(dinic[j,m2]-dtarget[j,m2])*delta[j,m2]+delta[j,m2]^2
              end
       end
    return 2.0*output
 end

   function do_swap(align::Array{Int64,2},delta::Array{Float64,2},d_target::Array{Float64,2},d_current::Array{Float64,2},temp::Float64,energ::Float64)

    (M,L)=size(align)
    m1=rand(1:M)
    m2=rand(1:M)
    k=rand(1:L)
    de=0.0
    while align[m1,k]==align[m2,k]
           m1=rand(1:M)
           m2=rand(1:M)
           k=rand(1:L)
    end

     delta_d!(align,delta,m1,m2,k)###
     de=dE(d_target,d_current,delta,m1,m2)####
     if de<0.0 || rand()< exp(-de/temp)
            swap_align!(align,m1,m2,k)
            energ=energ+de
            change_d!(d_current,delta,m1,m2)###
               return 1,energ,d_current
           else
            return 0,energ,d_current
           end

    end

function update_param_SA(alignment::Array{Int64,2},alignment_best::Array{Int64,2},n_it_tot::Int64,n_it_current::Int64,flag::Int64,acceptance_rate::Float64, energy_min ::Float64,energy_min_prev ::Float64,energy::Float64,T_init::Float64,T_min::Float64,n_it_max::Int64,T_factor_slow::Float64,T_factor_fast::Float64)
 (M,L)=size(alignment)
 stop_condition = 0
 n_it_max =n_it_max#N_IT_MAX;
 flag_max = 100;
 n_swap_min = convert(Int64,round(M*L/5))
 acceptance_threshold_low = 0.0001
 acceptance_threshold_high = 0.95
 T=T_init
 T_min = T_min#2/(10*M)
 #T_factor_slow =T_factor_slow#T_FACTOR_SLOW,
 #T_factor_fast =T_factor_slow#T_FACTOR_FAST;

 #Update number of iterations for next MCMC
    if acceptance_rate!=0
      n_it_current = convert(Int64,round(n_swap_min / acceptance_rate))
    else
        n_it_current = 2*n_it_current
    end

    if n_it_current > M*L
        n_it_current = M*L
    end
##If a better solution has been found and acceptance rate is high enough, decrease temperature
    if energy_min_prev > energy_min && acceptance_rate > acceptance_threshold_low

#linear cooling scheme with two slopes
        if acceptance_rate > acceptance_threshold_high
        T = T*T_factor_fast;
        else
        T = T*T_factor_slow
        end
 #
        flag -=1;
        if flag<0
        flag=0
        end
    ## Else, give it another chance at this temperature, starting from the best solution
    else
        alignment .= alignment_best
        energy = energy_min;
        flag+=1
    end
    ## Test various stop conditions
    if T<T_min
    stop_condition=1
     println("SA terminated -- Temperature reached its minimum")
    elseif n_it_tot>n_it_max
        stop_condition = 1
     println("SA terminated -- Maximum number of iterations")

    elseif flag>flag_max
        stop_condition=1
    println("SA terminated - Could not find better solution OR acceptance rate too low $(flag).")
        end
    return (stop_condition,alignment,n_it_current,flag,energy,T);
end

function shuffle_alignment(alignment::Array{Int64,2},T::Float64,d_curr::Array{Float64,2}, d_target::Array{Float64,2})

 (M,L)=size(alignment)
 n_it_tot=0
 n_it_curr = 10000
 n_it_max=2*M*L
 stop_condition = 0
 stop_condition_max = 100
 err_l2 = compute_l2_error(d_curr, d_target)
 d_pos=zeros(M,M)

    while stop_condition<stop_condition_max
        n_swap_done = 0
        for i in 1:n_it_curr
         (add,energy,d_curr) = do_swap(alignment,d_pos,d_target,d_curr,T,err_l2)
             n_swap_done = n_swap_done + add;
        end
        acceptance_rate = n_swap_done/(n_it_curr-1)
        n_it_tot = n_it_tot + n_it_curr;
        err_l2 = compute_l2_error(d_curr,d_target);
        if acceptance_rate < 0.99
            T = 2*T
        else
            T = T / 1.1
        end
        println("n_it_tot:$(n_it_tot)")
        if n_it_tot>n_it_max
            stop_condition=stop_condition_max
        end
    end
    println("$(n_it_tot) iterations for shuffling\n")
    return T,d_curr
end

### run simmulating annealing with a linear cooling scheme with two slopes
function run_SA(align::Array{Int64,2},T_init::Float64,T_min::Float64,T_factor_slow::Float64,T_factor_fast::Float64,shuffle::Int64,num_iter_max::Int64,d_curr::Array{Float64,2},d_target::Array{Float64,2})

    (M,L)=size(align)
    T = T_init
    energy=0.0
    energy_min=0.0
    energy_min_prev=100.0
    acceptance_rate = 0.0;
    n_it_max = num_iter_max
    n_it_current = 1
    n_it_tot = 0
    stop_condition = 0
    flag=0
    ## Initial state
    println("runMC\n")
    alignment_best=align[1:end,1:end]
    err_l2 = compute_l2_error(d_curr,d_target)
    err_l2_min = err_l2
    energy = Energy(d_curr, d_target)

    energy_min = energy
    #println("Initial energy : $(energy) -- Initial l2 error: $(err_l2)")

    d_i=d_curr[1:end,1:end]

    if shuffle!=0

        println("Shuffling system - T = $(T) ...")
        (T,d_curr)=shuffle_alignment(align,T,d_i,d_target)
        err_l2 = compute_l2_error(d_curr, d_target)
        err_l2_min = err_l2
        energy = Energy(d_curr, d_target)
        energy_min = energy;
        println("Done.");
        println("After shuffling : energy = $(energy) -- l2error=$(err_l2) -- T =$(T)");
    end

    ## Sampling
    println("\nSampling ... \n")
        d_pos=zeros(M,M)

    while stop_condition==0
     #Calculation
    println("n_it_current: $(n_it_current)")
    println(" T : $(T)");
    println("energy : $(energy)");
    #println("energy 2 : $(Energy(d_target,d_curr))")
    println("acceptance_rate : $(acceptance_rate)");
      n_swap_done = 0;
           for i in 1:n_it_current

         (add,energy,d_curr) = do_swap(align,d_pos,d_target,d_curr,T,energy)

        n_swap_done = n_swap_done + add;
             if energy < energy_min
             energy_min_prev = energy_min
             energy_min = energy
             acceptance_rate = n_swap_done/i;
                if acceptance_rate < 0.001
             alignment_best .= align
                end
             end
          end

        acceptance_rate =  n_swap_done/n_it_current
        n_it_tot = n_it_tot + n_it_current
        err_l2 = compute_l2_error(d_curr,d_target)

        if err_l2<err_l2_min
          err_l2_min = err_l2
        end
        ## Just to have alignment_best initialized
        if acceptance_rate > 0.001
            alignment_best .= align
         end
  println("n_it_tot:$(n_it_tot)")

 ## Updating parameters
 (stop_condition,align,n_it_current,flag,energy,T)=update_param_SA(align,alignment_best,n_it_tot,n_it_current,flag,acceptance_rate, energy_min,energy_min_prev,energy,T,T_min,n_it_max,T_factor_slow,T_factor_fast)

    end

    println(" \ndone\n");
    println("n_it_tot:$(n_it_tot)")
    println("Final l2 error = $(err_l2_min)")

  return align

end

####### given a MSA in numeric format this function provide a randomized MSA according to Null-model I
function sample_from_Null_model_I(infile::String;outfile="",shuffle_temp=10.0)
  msa=translate_fasta_to_num_matrix(infile)
  println("Shuffling alignment")

  dtarget=PairwiseHammingDist(msa);
  shuffle_alignment(msa,shuffle_temp,dtarget,dtarget)
  msa_fasta=transform_MSA_fasta(msa)
  if !isempty(outfile)
  export_fasta_file(outfile,msa_fasta)
  println("randomized MSA has been exported with fasta format!!")
  end

 return msa_fasta
end

####### given a MSA in numeric format this function provide a randomized MSA according to Null-model II
function sample_from_Null_model_II(infile::String;outfile="",target_dist="",shuffle=1,shuffle_temp=10.0,T_factor_slow=0.8,T_factor_fast=0.1,min_temp="default",num_iter_max=20000000)
  msa=translate_fasta_to_num_matrix(infile)
   if !isempty(target_dist)
     dtarget=readdlm(target_dist)
    else
    dtarget=PairwiseHammingDist(msa);
   end

  (M,L)=size(msa)
  if min_temp=="default"
  	t_min=2/(10*M)
  else
  	t_min=min_temp
  end
  println("Parameters:")
  if shuffle==1
     println("Simulating annealing start from shuffled alignment")
  end
  println("minimal temperature  T_min=$(t_min)")
  println("slow cooling rate = $(T_factor_slow), fast cooling rate = $(T_factor_fast)")
  println("maximum number of iterations $(num_iter_max)")

  result=run_SA(msa,shuffle_temp,t_min,T_factor_slow,T_factor_fast,shuffle,num_iter_max,dtarget,dtarget)
  msa_fasta=transform_MSA_fasta(result)
  if !isempty(outfile)
  export_fasta_file(outfile,msa_fasta)
  println("randomized MSA has been exported with fasta format!!")
  end
 return msa_fasta
end

end


using Devectorize
include("incompressible_basis.jl");

  function ns_gradgrad(
    u::Array{Complex128},
    p::Array{Int64},
    order::Int64,
    quadx::Array{Float64},
    quady::Array{Float64},
    quadz::Array{Float64},
    quadw::Array{Float64})

    n=length(u);
    gradgrad=Array(Complex128,(n,));

      function outergrad(i::Int64,u,p,order,quadx,quady,quadz,quadw)
        dim=3;
        n=length(u);
        nq=length(quadw);
        gradgradq=Array(Complex128,(nq,));
       #Loop through quadrature nodes.
        for iq=1:nq
            (i1,i2,i3)=ind2sub(size(quadw),iq);
            gradv=evaluate_curl_basis_gradient(order,p[i],quadx[i1],quady[i2],quadz[i3]);
            gradu=zeros(Complex128,(dim,dim));
            for j=1:n
                tgradu=evaluate_curl_basis_gradient(order,p[j],quadx[i1],quady[i2],quadz[i3]);
                @devec gradu=gradu+u[j].*tgradu;
            end
            grad_dot_grad::Complex128 = 0.0;
            for d1 = 1 : dim
                for d2 = 1 : dim
                    grad_dot_grad=grad_dot_grad+gradu[d1,d2]*conj(gradv[d1,d2]);
                end
            end
            gradgradq[iq]=grad_dot_grad;
        end
        return sum(gradgradq.*quadw[:]);
    end

   return Array{Complex128}(pmap(i->outergrad(i,u,p,order,quadx,quady,quadz,quadw),1:n));
end

  function ns_convection(
    u::Array{Complex128},
    p::Array{Int64},
    order::Int64,
    quadx::Array{Float64},
    quady::Array{Float64},
    quadz::Array{Float64},
    quadw::Array{Float64})

    dim=3;
    n=length(u);
    nq=length(quadw);

    conv=Array(Complex128,(n,));

    function outerconv(i::Int64,u,p,order,quadx,quady,quadz,quadw)
        n=length(u);
        nq=length(quadw);
        dim=3;
        convq=Array(Complex128,(nq,));
        #Loop through "test" degrees of freedom.
            #Loop through quadrature nodes.
            for iq=1:nq
                #Compute test   function.
                (i1,i2,i3)=ind2sub(size(quadw),iq);
                (vx,vy,vz) =evaluate_curl_basis(order,p[i],quadx[i1],quady[i2],quadz[i3]);
                gradu=zeros(Complex128,(dim,dim));
                ux=0.0*im;
                uy=0.0*im;
                uz=0.0*im;
                for j =1:n
                    #Compute gradient of solution.
                    tgradu=evaluate_curl_basis_gradient(order,p[j],quadx[i1],quady[i2],quadz[i3]);
                    @devec gradu=gradu+u[j].*tgradu;
                    (tux,tuy,tuz) =evaluate_curl_basis(order,p[j],quadx[i1],quady[i2],quadz[i3]);
                    ux=ux+u[j]*tux;
                    uy=uy+u[j]*tuy;
                    uz=uz+u[j]*tuz;
                end
                udivu=ux*gradu[:,1]+uy*gradu[:,2]+uz*gradu[:,3];
                convq[iq]=udivu[1]*conj(vx)+udivu[2]*conj(vy)+udivu[3]*conj(vz);
            end
            return sum(convq.*quadw[:]);
        end
    return Array{Complex128}(pmap(i->outerconv(i,u,p,order,quadx,quady,quadz,quadw),1:n));
end

  function ns_eval(
    RTR, #Cholesky factor of mass matrix
    u::Array{Complex128},
    p::Array{Int64},
    order::Int64,
    quadx::Array{Float64},
    quady::Array{Float64},
    quadz::Array{Float64},
    quadw::Array{Float64})

    conv=ns_convection(u,p,order,quadx,quady,quadz,quadw);
    gradgrad=ns_gradgrad(u,p,order,quadx,quady,quadz,quadw);
    return RTR\(gradgrad-conv);
end

function ns_eval(RTR,u::Array{Complex128},V,GV,W)

    dim=3;
    n=length(u);
    gg=zeros(u);
    co=zeros(u);
    #Compute grad-grad term.
    for i=1:dim
      for j=1:dim
          gg[:]=gg[:]+(GV[i,j]')*(W*(GV[i,j]*u));
      end
    end

    #Compute convection term.
    for i=1:dim
      for j = 1:dim
        co[:]=co[:]+V[i]'*(W*((V[j]*u).*(GV[i,j]*u)))
      end
    end

    return RTR\(-gg-co);
end


export SpaceOperation
export apply_operation
export dimension
export isidentity, istranslation, ispoint

import LinearAlgebra
import LinearAlgebraX

"""
    SpaceOperation{Tp<:Real, Tt<:Real}

Represent a spatial symmetry operation of the following form:
```math
S_{[M, \\mathbf{R}]}: \\mathbf{r} \\mapsto M \\cdot ( \\mathbf{r} + \\mathbf{R} )
```

# Fields
* `matrix`: Rotation matrix
* `displacement`: Displacement vector
"""
struct SpaceOperation{Tp<:Real, Tt<:Real} <:AbstractSpaceSymmetryOperation{Tt}
    matrix::Matrix{Tp}
    displacement::Vector{Tt}

    function SpaceOperation{Tp, Tt}(dim::Integer) where {Tp<:Real, Tt<:Real}
        return new{Tp, Tt}(Matrix{Tp}(LinearAlgebra.I, dim, dim), zeros(Tt, dim))
    end

    function SpaceOperation(::Type{Tp}, ::Type{Tt}, dim::Integer) where {Tp<:Real, Tt<:Real}
        return new{Tp, Tt}(Matrix{Tp}(LinearAlgebra.I, dim, dim), zeros(Tt, dim))
    end

    function SpaceOperation{Tp, Tt}(
        matrix::AbstractMatrix,
        displacement::AbstractVector,
    ) where {Tp<:Real, Tt<:Real}
        dim = size(matrix, 1)
        if size(matrix, 2) != dim
            throw(DimensionMismatch("matrix is not square: dimensions are $(size(matrix))"))
        elseif length(displacement) != dim
            throw(DimensionMismatch(
                "dimensions of matrix and displacement do not match:"*
                " matrix dimensions are $(size(matrix)),"*
                " and vector dimension is $(length(displacement))"
            ))
        end
        return new{Tp, Tt}(matrix, displacement)
    end

    @doc """
        SpaceOperation(matrix::AbstractMatrix{Tp}, displacement::AbstractVector{Tt}) where {Tp<:Real, Tt<:Real}

    Construct a space operation by `matrix` and `displacement`.
    """
    function SpaceOperation(
        matrix::AbstractMatrix{Tp},
        displacement::AbstractVector{Tt},
    ) where {Tp<:Real, Tt<:Real}
        return SpaceOperation{Tp, Tt}(matrix, displacement)
    end

    @doc """
        SpaceOperation([point], [translation])

    Construct a space operation by `point` and `translation`.

    # Arguments
    * `point::PointOperation{Tp}`
    * `translation::TranslationOperation{Tt}`
    """
    function SpaceOperation(
        point::PointOperation{Tp},
        translation::TranslationOperation{Tt},
    ) where {Tp<:Real, Tt<:Real}
        return SpaceOperation{Tp, Tt}(point.matrix, translation.displacement)
    end

    function SpaceOperation(point::PointOperation{Tp}) where {Tp}
        dim = dimension(point)
        return SpaceOperation{Tp, Tp}(point.matrix, zeros(Tp, dim))
    end

    function SpaceOperation(translation::TranslationOperation{Tt}) where {Tt}
        dim = dimension(translation)
        return SpaceOperation{Int, Tt}(
            Matrix{Int}(LinearAlgebra.I, dim, dim),
            translation.displacement,
        )
    end
end

## Conversion

function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, arg::SpaceOperation{Tp2, Tt2}) where {Tp1, Tt1, Tp2, Tt2}
    return SpaceOperation(convert(Matrix{Tp1}, arg.matrix), convert(Vector{Tt1}, arg.displacement))
end

# function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, op::IdentityOperation{T}) where {Tp1, Tt1, T}
#     dim = dimension(op)
#     mat = Matrix{Tp1}(LinearAlgebra.I, (dim, dim))
#     dis = zeros(Tt1, size(mat, 1))
#     return SpaceOperation(mat, dis)
# end

function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, op::PointOperation{Tp2}) where {Tp1, Tt1, Tp2}
    dim = dimension(op)
    mat = convert(Matrix{Tp1}, op.matrix)
    dis = zeros(Tt1, dim)
    return SpaceOperation(mat, dis)
end

function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, op::TranslationOperation{Tt2}) where {Tp1, Tt1, Tt2}
    dim = dimension(op)
    mat = Matrix{Tp1}(LinearAlgebra.I, (dim, dim))
    dis = convert(Vector{Tt1}, op.displacement)
    return SpaceOperation(mat, dis)
end

function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, arg::AbstractVector{Tt2}) where {Tp1, Tt1, Tt2}
    dim = length(arg)
    mat = Matrix{Tp1}(LinearAlgebra.I, (dim, dim))
    dis = convert(Vector{Tt1}, arg)
    return SpaceOperation(mat, dis)
end

function Base.convert(::Type{SpaceOperation{Tp1, Tt1}}, arg::AbstractMatrix{Tp2}) where {Tp1, Tt1, Tp2}
    dim = size(arg, 1)
    mat = convert(Matrix{Tp1}, arg)
    dis = zeros(Tt1, dim)
    return SpaceOperation(mat, dis)
end

function Base.promote_rule(::Type{SpaceOperation{Tp1, Tt1}}, ::Type{SpaceOperation{Tp2, Tt2}}) where {Tp1, Tt1, Tp2, Tt2}
    Tp = promote_type(Tp1, Tp2)
    Tt = promote_type(Tt1, Tt2)
    return SpaceOperation{Tp, Tt}
end

# function Base.promote_rule(::Type{SpaceOperation{Tp, Tt}}, ::Type{IdentityOperation{T}}) where {Tp, Tt, T}
#     return SpaceOperation{Tp, Tt}
# end

function Base.promote_rule(::Type{SpaceOperation{Tp1, Tt1}}, ::Type{TranslationOperation{Tt2}}) where {Tp1, Tt1, Tt2}
    Tt = promote_type(Tt1, Tt2)
    return SpaceOperation{Tp1, Tt}
end

function Base.promote_rule(::Type{SpaceOperation{Tp1, Tt1}}, ::Type{PointOperation{Tp2}}) where {Tp1, Tt1, Tp2}
    Tp = promote_type(Tp1, Tp2)
    return SpaceOperation{Tp, Tt1}
end

function Base.promote_rule(::Type{TranslationOperation{Tt}}, ::Type{PointOperation{Tp}}) where {Tp, Tt}
    return SpaceOperation{Tp, Tt}
end

## Properties

"""
    isidentity(arg::SpaceOperation)

Check whether `arg` is identity (i.e. identity point and identity translation)
"""
isidentity(arg::SpaceOperation) = isone(arg.matrix) && iszero(arg.displacement)

"""
    istranslation(arg::SpaceOperation)

Check whether `arg` is a translation operation (i.e., point operation is identity)
"""
istranslation(arg::SpaceOperation) = isone(arg.matrix)

"""
    ispoint(arg::SpaceOperation)

Check whether `arg` is a point operation (i.e. translation is identity)
"""
ispoint(arg::SpaceOperation) = iszero(arg.displacement)

"""
    dimension(arg::SpaceOperation)

Return the spatial dimension of `arg`.
"""
dimension(arg::SpaceOperation) = length(arg.displacement)

function Base.hash(op::SpaceOperation{Tp, Tt}, h::UInt) where {Tp, Tt}
    h = hash(op.matrix, h)
    h = hash(op.displacement, h)
    h = hash(SpaceOperation{Tp, Tt}, h)
    return h
end

## operators
function Base.:(==)(lhs::SpaceOperation{Tp, Tt}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    return lhs.matrix == rhs.matrix && lhs.displacement == rhs.displacement
end

function Base.:(==)(lhs::SpaceOperation{Tp, Tt}, rhs::TranslationOperation{Tt}) where {Tp, Tt}
    return isone(lhs.matrix) && lhs.displacement == rhs.displacement
end

function Base.:(==)(lhs::SpaceOperation{Tp, Tt}, rhs::PointOperation{Tp}) where {Tp, Tt}
    return iszero(lhs.displacement) && lhs.matrix == rhs.matrix
end

function Base.:(==)(lhs::TranslationOperation{Tt}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    return isone(rhs.matrix) && lhs.displacement == rhs.displacement
end

function Base.:(==)(lhs::PointOperation{Tp}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    return iszero(rhs.displacement) && lhs.matrix == rhs.matrix
end

# function Base.:(==)(sop::SpaceOperation{Tp, Tt}, iden::IdentityOperation{<:Union{Tp, Tt}}) where {Tp, Tt}
#     return dimension(sop) == dimension(iden) && iszero(sop.displacement) && isone(sop.matrix)
# end

# function Base.:(==)(iden::IdentityOperation{<:Union{Tp, Tt}}, sop::SpaceOperation{Tp, Tt}) where {Tp, Tt}
#     return dimension(sop) == dimension(iden) && iszero(sop.displacement) && isone(sop.matrix)
# end

function Base.:(*)(lhs::PointOperation{Tp}, rhs::TranslationOperation{Tt}) where {Tp, Tt}
    return SpaceOperation{Tp, Tt}(lhs.matrix, rhs.displacement)
end

function Base.:(*)(lhs::TranslationOperation{Tt}, rhs::PointOperation{Tp}) where {Tp, Tt}
    rhs_matrix_inv = Matrix{Tp}(LinearAlgebraX.invx(rhs.matrix))
    return SpaceOperation{Tp, Tt}(rhs.matrix, rhs_matrix_inv * lhs.displacement)
end

#   ML ⋅ ( MR ⋅ ( x + ρR ) + ρL )
# = ML MR x + ML MR ρR + ML ρL
# = ML MR (x + ρR + inv(MR) ⋅ ρL )
function Base.:(*)(lhs::SpaceOperation{Tp, Tt}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    matrix = lhs.matrix * rhs.matrix
    rhs_matrix_inv = Matrix{Tp}(LinearAlgebraX.invx(rhs.matrix))
    displacement = rhs.displacement + rhs_matrix_inv * lhs.displacement
    return SpaceOperation{Tp, Tt}(matrix, displacement)
end

function Base.:(*)(lhs::SpaceOperation{Tp, Tt}, rhs::TranslationOperation{Tt}) where {Tp, Tt}
    matrix = lhs.matrix
    displacement = rhs.displacement + lhs.displacement
    return SpaceOperation{Tp, Tt}(matrix, displacement)
end

function Base.:(*)(lhs::TranslationOperation{Tt}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    matrix = rhs.matrix
    rhs_matrix_inv = Matrix{Tp}(LinearAlgebraX.invx(rhs.matrix))
    displacement = rhs.displacement + rhs_matrix_inv * lhs.displacement
    return SpaceOperation{Tp, Tt}(matrix, displacement)
end

function Base.:(*)(lhs::SpaceOperation{Tp, Tt}, rhs::PointOperation{Tp}) where {Tp, Tt}
    matrix = lhs.matrix * rhs.matrix
    rhs_matrix_inv = Matrix{Tp}(LinearAlgebraX.invx(rhs.matrix))
    displacement = rhs_matrix_inv * lhs.displacement
    return SpaceOperation{Tp, Tt}(matrix, displacement)
end

function Base.:(*)(lhs::PointOperation{Tp}, rhs::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    matrix = lhs.matrix * rhs.matrix
    displacement = rhs.displacement
    return SpaceOperation{Tp, Tt}(matrix, displacement)
end

function Base.inv(arg::SpaceOperation{Tp, Tt}) where {Tp, Tt}
    matrix_inv = Matrix{Tp}(LinearAlgebraX.invx(arg.matrix))
    return SpaceOperation{Tp, Tt}(matrix_inv, -arg.matrix * arg.displacement)
end

function Base.:(^)(op::SpaceOperation{Tp, Tt}, power::Integer) where {Tp, Tt}
    if power == 0
        return SpaceOperation(Tp, Tt, dimension(op))
    elseif power < 0
        op_inv = inv(op)
        return op_inv^(-power)
    else
        return op * op^(power-1)
    end
end

"""
    apply_operation(op::SpaceOperation{Tp, Tt}, coord::AbstractArray{<:Union{Tp, Tt}}) where {Tp, Tt}
"""
function apply_operation(op::SpaceOperation{Tp, Tt}, coord::AbstractArray{<:Union{Tp, Tt}}) where {Tp, Tt}
    return op.matrix * (coord .+ op.displacement)
end

function (op::SpaceOperation{Tp, Tt})(coord::AbstractArray{<:Union{Tp, Tt}}) where {Tp, Tt}
    return op.matrix * (coord .+ op.displacement)
end


export rule

@rule NormalMeanPrecision(:τ, Marginalisation) (q_out::Any, q_μ::Any) = begin
    θ = 2 / (var(q_out) + var(q_μ) + abs2(mean(q_out) - mean(q_μ)))
    α = convert(typeof(θ), 1.5)
    return Gamma(α, θ)
end

@rule NormalMeanPrecision(:τ, Marginalisation) (q_out_μ::Any, ) = begin
    m, V = mean_cov(q_out_μ)
    θ = 2 / (V[1,1] - V[1,2] - V[2,1] + V[2,2] + abs2(m[1] - m[2]))
    α = convert(typeof(θ), 1.5)
    return Gamma(α, θ)
end


# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/ModelReduction.jl/blob/master/LICENSE

"""
    guyan_reduction(K, m, s)

Reduce the stiffness matrix by Guyan Reduction. K = original stiffness matrix, m = master nodes, s= slave nodes.

"""
function guyan_reduction(K, m, s)
    Kred = K[m,m] - K[m,s]*inv(K[s,s])*K[s,m]
    return Kred
end


function upair(r, data :: Data)

	@unpack sig6, eps4 = data

	r6 = (r^2)^3

	return eps4*(sig6/r6 - 1.0)/r6
end


#unique!(push!(LOAD_PATH, "~/Documents/UGent/PhD/Code_cMPS/julia1.0/CMPSKit.jl/src/"))
unique!(push!(LOAD_PATH, joinpath(pwd(), "src")))

using Revise
using CMPSKit
using KrylovKit
using OptimKit
using LinearAlgebra
using JLD2

D = 16

Q = Constant(randn(D,D))
R = Constant(randn(D,D))
Ψ = InfiniteCMPS(Q, R)

alg = ConjugateGradient(; verbosity = 2, maxiter = 10^6, gradtol = 1e-4);

#H = ∫(∂ψ[1]'*∂ψ[1] + ∂ψ[2]'*∂ψ[2] - 10 * (ψ[1]'*ψ[1] + ψ[2]'*ψ[2]) + 100 * (ψ[1]*ψ[2] + ψ[2]*ψ[1]), (-Inf,+Inf))

#H = ∫(∂ψ'*∂ψ - 1 * ψ'*ψ + 1 * (ψ')^2*ψ^2, (-Inf,+Inf))

# function finalize!(x, f, g, numiter)
#     if mod(numiter, 1000) == 0
#         @save "tempoptresults2.jld" x
#     end
#     return x, f, g
# end
# groundstate(H, Ψ; optalg = alg, linalg = GMRES(; tol = 1e-4), finalize! = finalize!)
groundstate(H, Ψ; optalg = alg, linalg = GMRES(; tol = 1e-6))


using Distributions
using SpinGlassNetworks

function ramp(t::T, τ::T, α::T, pi::T, pf::T) where T <: Real
    p = (pf + pi) + (pf - pi) * tanh(α * (2.0 * t / τ - 1.0))
    p / 2.0
end

@testset "Coherent Ising Machine simulator for chimera instances." begin
    L = 128

    ig = ising_graph("$(@__DIR__)/instances/chimera_droplets/$(L)power/001.txt")

    scale = 0.3
    noise = Normal(0.1, 0.3)

    x0 = zeros(L)
    sat = 1.0
    time = 1000.
    pi, pf, α = -15.0, 0.0, 2.0
    momentum = 0.4

    pump = [ramp(t, time, α, pi, pf) for t ∈ 0:time]

    opo = OpticalOscillators{Float64}(ig, scale, noise)
    dyn = OPODynamics{Float64}(x0, sat, pump, momentum)

    pump_nmfa = [ramp(t, time, α, 10., 1.) for t ∈ 0:time]

    opo_nmfa = OpticalOscillators{Float64}(ig, scale, noise)
    dyn_nmfa = OPODynamics{Float64}(x0, sat, pump_nmfa, momentum)

    N = 500
    states = Vector{Vector{Int}}(undef, N)
    states_nmfa = copy(states)
    Threads.@threads for i ∈ 1:N
        states[i] = evolve_optical_oscillators(opo, dyn)
        states_nmfa[i] = noisy_mean_field_annealing(opo_nmfa, dyn_nmfa)
    end

    en = minimum(energy(states, ig))
    en_nmfa = minimum(energy(states_nmfa, ig))

    en_exact = -210.933334
    @testset "Energy found is at least negative and within the bounds" begin
        @test  en_exact <= en < 0.0
        @test en_exact <= en_nmfa < 0.0
    end
end