using NeuralPDE, Flux, ModelingToolkit, GalacticOptim, Optim, DiffEqFlux

cb = function (p,l)
    println("Current loss is: $l")
    return false
end

hill(s, km, n) = (s^n + km^n) /  (s^n + km^n)
norm2(X, a, b) = (X-a)^2.0 / b^2.0

@parameters H, S, θ
#@derivatives Dt'~t
@derivatives Dss''~S
@derivatives Dhh''~H
@derivatives Ds'~S
@derivatives Dh'~H
@variables p(..)

# Parameters
k₁ = 40.
k₂ = 1.
k₃ = 0.004
k₄ = 0.02
k₅ = 0.04
k₆ = 0.004
KM₁ = 280.
KM₂ = 4250.
KM₃ = 2200.
KM₄ = 90.
n₁ = 2.
n₂ = 4.
n₃ = 2.
n₄ = 4.
α = 0.35
β = 0.95
S_tot = 225.
D_H=500.
D_S=0.45

σ_H, σ_S = 5000.0, 150.0
H0, S0 = 3275.0, 112.0

# ODE
F_H = k₁ * (1. -α)*hill(S, KM₁, n₁) * hill(KM₁, S, n₁) * hill(H, KM₂, n₂) + k₂ - k₃*H
F_S = k₄ * (1. -β)*hill(H, KM₃,n₃)  * hill(KM₃, H, n₃)  * hill(S, KM₄, n₄)*(S_tot - S) + k₅ - k₆*S

##
#using SymPy

#S, H = symbols("S H")

#F_H = k₁ * (1. -α)*hill(S, KM₁, n₁) * hill(KM₁, S, n₁) * hill(H, KM₂, n₂) + k₂ - k₃*H
#F_S = k₄ * (1. -β)*hill(H, KM₃,n₃)  * hill(KM₃, H, n₃)  * hill(S, KM₄, n₄)*(S_tot - S) + k₅ - k₆*S # get the derivatives

## derivate here

# Discretization
dS = 1.0; dH=10.0;

# PDE
#eq =   -Dh(F_H)*p(S,H,θ)-F_H*Dh(p(S,H,θ)) - Ds(F_S)*p(S,H,θ) - F_S*Ds(p(S,H,θ)) + 500.0 * Dhh(p(S,H,θ)) + 0.45 *Dss(p(S,H,θ)) ~ 0
eq =   -(-0.004)*p(S,H,θ)-F_H*Dh(p(S,H,θ)) - (-0.005)*p(S,H,θ) - F_S*Ds(p(S,H,θ)) + 500.0 * Dhh(p(S,H,θ)) + 0.45 *Dss(p(S,H,θ)) ~ 0
      #1. ~ dS*dH*p(S,H,θ)

# Domains
domains = [ S ∈ IntervalDomain(0.0,112.0),
            H ∈ IntervalDomain(0.0,3275.0)]

S_max = domains[1].domain.upper
H_max = domains[2].domain.upper

# Boundary
bcs = [
    #p(S,H, θ) ~  (2*pi*σ_H*σ_S)^-1 * exp( -norm2(H,H0, σ_H) - norm2(S,S0, σ_S)  ),
    p(0.0,H,θ) ~ 0.,
    p(S,0.0,θ) ~ 0.,
    p(S_max,H,θ) ~ 0.,
    p(S,H_max,θ) ~ 0.,
    Dh(p(0.0,H,θ)) ~ 0.,
    Dh(p(S,0.0,θ)) ~ 0.,
    Dh(p(S_max,H,θ)) ~ 0.,
    Dh(p(S,H_max,θ)) ~ 0.,
    Ds(p(0.0,H,θ)) ~ 0.,
    Ds(p(S,0.0,θ)) ~ 0.,
    Ds(p(S_max,H,θ)) ~ 0.,
    Ds(p(S,H_max,θ)) ~ 0.,
    #p(0,H,θ) ~ p(S,0,θ),
    #p(S,0,θ) ~ p(S_max,H,θ),
    #p(S_max,H,θ) ~ p(S,H_max,θ),
    #p(S,H_max,θ) ~ p(0,H,θ)
]

##

# Neural network
chain = FastChain(FastDense(2,16,Flux.σ),FastDense(16,16,Flux.σ),FastDense(16,1))

strategy = GridTraining()
discretization = PhysicsInformedNN([dS,dH],chain,strategy=strategy)

indvars = [S,H]
depvars = [p]
dim = length(domains)

expr_pde_loss_function = build_loss_function(eq,indvars,depvars)
expr_bc_loss_functions = [build_loss_function(bc,indvars,depvars) for bc in bcs]

train_sets = generate_training_sets(domains,[dS,dH],bcs,indvars,depvars)
train_domain_set, train_bound_set, train_set= train_sets

phi = discretization.phi
autodiff = discretization.autodiff
derivative = discretization.derivative
initθ = discretization.initθ

pde_loss_function = get_loss_function(eval(expr_pde_loss_function),
                           train_domain_set,
                           phi,
                           derivative,
                           strategy)
bc_loss_function = get_loss_function(eval.(expr_bc_loss_functions),
                          train_bound_set,
                          phi,
                          derivative,
                          strategy)

function loss_function(θ,p)
    return pde_loss_function(θ) + bc_loss_function(θ)
end
f = OptimizationFunction(loss_function, GalacticOptim.AutoZygote())
prob = GalacticOptim.OptimizationProblem(f, initθ)

# optimizer
opt = Optim.BFGS()
res = GalacticOptim.solve(prob, opt; cb = cb, maxiters=1500)
phi = discretization.phi

# Analysis
ss,hs = [domain.domain.lower:di:domain.domain.upper for domain in domains for di in [dS, dH]]

u_predict  = [phi([t,x],res.minimizer)[i] for t in ss for x in hs]

plot(ss,hs, u_predict[i], st=:surface,title = "predict");


function test_diophantine_solutions()
    a = diophantine_solution(1, 2)
    b = diophantine_solution(7, 9)
    c = diophantine_solution(-2, 42)
    s = [a, b, c]

    @test collect(diophantine_solutions(s)) == s
    @test eltype(diophantine_solutions(s)) == DiophantineSolutionXY{Int}

    x = diophantine_solution(big(0), big(0))
    @test eltype(diophantine_solutions([x])) == DiophantineSolutionXY{BigInt}
end

function test_diophantine_solutions_nonex_noney()
    @test collect(diophantine_nonex_noney()) == []
    @test eltype(diophantine_nonex_noney()) == DiophantineSolutionXY{Int}
    @test eltype(diophantine_nonex_noney(BigInt)) == DiophantineSolutionXY{BigInt}
end

function test_diophantine_solutions_anyx_anyy()
end

function test_diophantine_solutions_onex_anyy()
    @test collect(take(diophantine_onex_anyy(42), 5)) == [
        diophantine_solution(42, 0),
        diophantine_solution(42, 1),
        diophantine_solution(42, -1),
        diophantine_solution(42, 2),
        diophantine_solution(42, -2)
    ]
    @test eltype(diophantine_onex_anyy(42)) == DiophantineSolutionXY{Int}

    @test collect(take(diophantine_onex_anyy(big(42)), 5)) == [
        diophantine_solution(big(42), big(0)),
        diophantine_solution(big(42), big(1)),
        diophantine_solution(big(42), big(-1)),
        diophantine_solution(big(42), big(2)),
        diophantine_solution(big(42), big(-2))
    ]
    @test eltype(diophantine_onex_anyy(big(42))) == DiophantineSolutionXY{BigInt}
end

function test_diophantine_solutions_anyx_oney()
    @test collect(take(diophantine_anyx_oney(42), 5)) == [
        diophantine_solution(0, 42),
        diophantine_solution(1, 42),
        diophantine_solution(-1, 42),
        diophantine_solution(2, 42),
        diophantine_solution(-2, 42)
    ]
    @test eltype(diophantine_anyx_oney(42)) == DiophantineSolutionXY{Int}

    @test collect(take(diophantine_anyx_oney(big(42)), 5)) == [
        diophantine_solution(big(0), big(42)),
        diophantine_solution(big(1), big(42)),
        diophantine_solution(big(-1), big(42)),
        diophantine_solution(big(2), big(42)),
        diophantine_solution(big(-2), big(42))
    ]
    @test eltype(diophantine_anyx_oney(big(42))) == DiophantineSolutionXY{BigInt}
end

function test_diophantine_solutions_linearx_lineary()
    @test collect(take(diophantine_linearx_lineary(2, 0, -3, 0), 5)) == [
        diophantine_solution(0, 0),
        diophantine_solution(2, -3),
        diophantine_solution(-2, 3),
        diophantine_solution(4, -6),
        diophantine_solution(-4, 6)
    ]
    @test eltype(diophantine_linearx_lineary(2, 0, -3, 0)) == DiophantineSolutionXY{Int}

    @test collect(take(diophantine_linearx_lineary(big(2), big(0), big(-3), big(0)), 5)) == [
        diophantine_solution(big(0), big(0)),
        diophantine_solution(big(2), big(-3)),
        diophantine_solution(big(-2), big(3)),
        diophantine_solution(big(4), big(-6)),
        diophantine_solution(big(-4), big(6))
    ]
    @test eltype(diophantine_linearx_lineary(big(2), big(0), big(-3), big(0))) == DiophantineSolutionXY{BigInt}
end

function test_diophantine_solutions_quadraticx_quadraticy()
    @test collect(take(diophantine_quadraticx_quadraticy(1, 2, 3, -1, 3, 2), 5)) == [
        diophantine_solution(3, 2),
        diophantine_solution(6, 4),
        diophantine_solution(2, -2),
        diophantine_solution(11, 4),
        diophantine_solution(3, -8)
    ]
    @test eltype(diophantine_quadraticx_quadraticy(1, 2, 3, -1, 3, 2)) == DiophantineSolutionXY{Int}

    @test collect(take(diophantine_quadraticx_quadraticy(big(1), big(2), big(3), big(-1), big(3), big(2)), 5)) == [
        diophantine_solution(big(3), big(2)),
        diophantine_solution(big(6), big(4)),
        diophantine_solution(big(2), big(-2)),
        diophantine_solution(big(11), big(4)),
        diophantine_solution(big(3), big(-8))
    ]
    @test eltype(diophantine_quadraticx_quadraticy(big(1), big(2), big(3), big(-1), big(3), big(2))) == DiophantineSolutionXY{BigInt}
end


export TotalDegreeSolutionIterator, totaldegree_startsystem, totaldegree

"""
    TotalDegreeSolutionIterator(degrees, b)

Given the `Vector`s `degrees` and `b` `TotalDegreeSolutionIterator` enumerates all solutions
of the system
```math
\\begin{align*}
    z_1^{d_1} - b_1 &= 0 \\\\
    z_1^{d_2} - b_2 &= 0 \\\\
    &\\vdots \\\\
    z_n^{d_n} - b_n &= 0 \\\\
\\end{align*}
```
where ``d_i`` is `degrees[i]` and ``b_i`` is `b[i]`.
"""
struct TotalDegreeSolutionIterator{T<:Complex, Iter}
    degrees::Vector{Int}
    b::Vector{T}

    iterator::Iter
end
function TotalDegreeSolutionIterator(degrees::Vector{Int}, b::Vector{T}) where {T<:Complex}
    iterator = Base.Iterators.product(map(d -> 0:d-1, degrees)...)
    TotalDegreeSolutionIterator(degrees, b, iterator)
end

Base.start(iter::TotalDegreeSolutionIterator) = start(iter.iterator)
function Base.next(iter::TotalDegreeSolutionIterator{T}, state) where {T<:Complex}
    indices, nextstate = next(iter.iterator, state)

    value = map(1:length(indices), indices) do i, k
        d = iter.degrees[i]
        convert(T, iter.b[i]^(1/d) * exp(2π*im/d)^k)
    end
    value, nextstate
end
Base.done(iter::TotalDegreeSolutionIterator, state) = done(iter.iterator, state)
Base.length(iter::TotalDegreeSolutionIterator) = length(iter.iterator)
Base.eltype(iter::TotalDegreeSolutionIterator{T}) where {T} = Vector{T}

"""
    totaldegree(H::Type{AbstractPolynomialHomotopy}, F, [unitroots=false])

Construct a  total degree homotopy of type `H` with `F` and an iterator of its solutions.
This is the homotopy with start system
```math
\\begin{align*}
    z_1^{d_1} &- b_1\\\\
    z_1^{d_2} &- b_2\\\\
    &\\vdots \\\\
    z_n^{d_n} &- b_n\\\\
\\end{align*}
```
and target system `F`, where ``d_i`` is the degree of the ``i``-th polynomial of `F`.
If `unitroots=true` then ``b_i=1`` otherwise ``b_i`` is a random
complex number (with real and imaginary part in the unit interval).

## Example
```julia
H, startsolutions = totaldegree(StraightLineHomotopy{Complex128}, [x^2+y+1, x^3*y-2])
```
"""
function totaldegree(H::Type{<:AbstractPolynomialHomotopy{T}}, fs::Vector{<:MP.AbstractPolynomial{U}}; kwargs...) where {T<:Complex, U<:Number}
    F = convert(Vector{FP.Polynomial{T}}, fs)
    totaldegree(H, F; kwargs...)
end
function totaldegree(H::Type{<:AbstractPolynomialHomotopy}, fs::Vector{<:MP.AbstractPolynomial{U}}; kwargs...) where {U<:Number}
    F = convert(Vector{FP.Polynomial{promote_type(Complex128, U)}}, fs)
    totaldegree(H, F; kwargs...)
end

function totaldegree(H::Type{<:AbstractPolynomialHomotopy}, F::Vector{FP.Polynomial{U}}; kwargs...) where {U<:Number}
    G, solutions = totaldegree_startsystem(F; kwargs...)
    H(G, F), solutions
end

"""
    totaldegree_startsystem(F::Vector{FP.Polynomial{<:Complex}}, [unit_roots=false])

Return the system
```math
\\begin{align*}
    z_1^{d_1} &- b_1\\\\
    z_1^{d_2} &- b_2\\\\
    &\\vdots \\\\
    z_n^{d_n} &- b_n\\\\
\\end{align*}
```
where ``d_i`` is the degree of the ``i``-th polynomial of `F` and an iterator of its
solutions.
If `unitroots=true` then ``b_i=1`` otherwise ``b_i`` is a random
complex number (with real and imaginary part in the unit interval).
"""
function totaldegree_startsystem(F::Vector{FP.Polynomial{Complex{T}}}; unitroots=false) where {T<:AbstractFloat}
    vars = FP.variables(F[1])
    degrees = FP.degree.(F)
    if unitroots
        b = ones(Complex{T}, length(degrees))
    else
        b = convert.(Complex{T}, rand(Complex128, length(degrees)))
    end

    n = length(degrees)
    N = length(vars)
    make_homogenous = !all(FP.ishomogenous, F) && length(vars) == length(F) + 1
    if n + 1 == N
        if !all(FP.ishomogenous, F)
            return error("In order to create a total degree start system your input system needs " *
                "to have the same number of variables as number of equations or to have N variables
                and N - 1 homogenous polynomials. Currently your system has " *
                "$(length(vars)) variables and $(length(degrees)) equations and is not homogenous.")
        end

        G = map(2:N, degrees) do i, d
            exponents = zeros(Int, N, 2)
            exponents[i, 1] = d
            exponents[1, 2] = d
            coeffs = zeros(Complex{T}, 2)
            coeffs[1] = one(Complex{T})
            coeffs[2] = -b[i - 1]
            FP.Polynomial(exponents, coeffs, vars)
        end

        startsolutions::Vector{Vector{Complex{T}}} =
            map(TotalDegreeSolutionIterator(degrees, b)) do sol
                unshift!(sol, 1)
            end
        G, startsolutions
    elseif n != N
        return error("In order to create a total degree start system your input system needs " *
            "to have the same number of variables as number of equations. Currently your system has " *
            "$(length(vars)) variables and $(length(degrees)) equations.")
    else
        G = map(1:n, degrees) do i, d
            exponents = zeros(Int, n, 2)
            exponents[i, 1] = d
            coeffs = zeros(Complex{T}, 2)
            coeffs[1] = one(Complex{T})
            coeffs[2] = -b[i]
            FP.Polynomial(exponents, coeffs, vars)
        end
        G, collect(TotalDegreeSolutionIterator(degrees, b))::Vector{Vector{Complex{T}}}
    end
end
function totaldegree_startsystem(F::Vector{FP.Polynomial{T}}; kwargs...) where {T<:Number}
    totaldegree_startsystem(convert.(FP.Polynomial{complex(float(T))}, F); kwargs...)
end


#=
  Who killed agatha? (The Dreadsbury Mansion Murder Mystery) in Julia + ConstraintSolver.jl.

  This is a standard benchmark for theorem proving.

  http://www.lsv.ens-cachan.fr/~goubault/H1.dist/H1.1/Doc/h1003.html
  """
  Someone in Dreadsbury Mansion killed Aunt Agatha.
  Agatha, the butler, and Charles live in Dreadsbury Mansion, and
  are the only ones to live there. A killer always hates, and is no
  richer than his victim. Charles hates noone that Agatha hates. Agatha
  hates everybody except the butler. The butler hates everyone not richer
  than Aunt Agatha. The butler hates everyone whom Agatha hates.
  Noone hates everyone. Who killed Agatha?
  """

  Originally from F. J. Pelletier:
  Seventy-five problems for testing automatic theorem provers.
  Journal of Automated Reasoning, 2: 191 216, 1986.
  http://www.sfu.ca/~jeffpell/papers/75ATPproblems86.pdf

  I have blogged about the problem here:
  * "Learning constraint programming - part II: Modeling with the Element constraint"
    http://www.hakank.org/constraint_programming_blog/2009/05/learning_constraint_programmin.html
  * "Learning Constraint Programming IV: Logical constraints: Who killed Agatha? revisited"
  http://www.hakank.org/constraint_programming_blog/2009/05/learning_constraint_programmin_3.html

  Model created by Hakan Kjellerstrand, hakank@gmail.com
  See also my Julia page: http://www.hakank.org/julia/

=#
using ConstraintSolver, JuMP
using Cbc, GLPK
const CS = ConstraintSolver
include("constraints_utils.jl")

function print_matrix(x)
    print_grid(x)
end

function who_killed_agatha()

    cbc_optimizer = optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0)
    glpk_optimizer = optimizer_with_attributes(GLPK.Optimizer)

    model = Model(optimizer_with_attributes(CS.Optimizer,   "all_solutions"=> true,
                                                            # "all_optimal_solutions"=>true,
                                                            "logging"=>[],

                                                            # "traverse_strategy"=>:BFS,
                                                            # "traverse_strategy"=>:DFS, # <-
                                                            # "traverse_strategy"=>:DBFS,

                                                            # "branch_split"=>:Smallest,
                                                            # "branch_split"=>:Biggest,
                                                            # "branch_split"=>:InHalf, # <-

                                                            # "simplify"=>false,
                                                            # "simplify"=>true, # default

                                                            "time_limit"=>16,

                                                            # "lp_optimizer" => cbc_optimizer, # 45.8s
                                                            # "lp_optimizer" => glpk_optimizer, # 21.5s
                                        ))

    n = 3
    agatha = 1
    butler = 2
    charles = 3
    @variable(model, 1 <= killer <= 3, Int)
    @variable(model, hates[1:n,1:n], Bin)
    @variable(model, richer[1:n,1:n], Bin)
    @variable(model, is_killer[1:3], Bin)

    # Connect killer to is_killer
    for i in 1:n
        @constraint(model, is_killer[i] := {killer == i})
    end
    @constraint(model, sum(is_killer) == 1)

    #
    # Constraints
    #
    # Agatha, the butler, and Charles live in Dreadsbury Mansion, and
    # are the only ones to live there.
    #

    # * A killer always hates, and is no richer than his victim.
    for i in 1:n
        @constraint(model, is_killer[i] => {hates[i, agatha] == 1})
        @constraint(model, is_killer[i] => {richer[i, agatha] == 0})
    end

    # * Define the concept of richer: no one is richer than him-/herself
    for i in 1:n
        @constraint(model,richer[i,i]== 0)
    end

    # (contd...) if i is richer than j then j is not richer than i
    for i in 1:n, j in 1:n
        if i != j
            @constraint(model, richer[i,j] => {richer[j,i] == 0})
            @constraint(model, !richer[j,i] => {richer[i,j] == 1})
        end
    end

    # * Charles hates no one that Agatha hates.
    for i in 1:n
        @constraint(model, hates[agatha, i] => {hates[charles, i] == 0})
    end

    # * Agatha hates everybody except the butler.
    @constraint(model,hates[agatha, butler] == 0)
    @constraint(model,hates[agatha, charles] == 1)
    @constraint(model,hates[agatha, agatha] == 1)

    # * The butler hates everyone not richer than Aunt Agatha.
    for i in 1:n
      @constraint(model, !richer[i, agatha] => {hates[butler, i] == 1})
    end

    # * The butler hates everyone whom Agatha hates.
    for i in 1:n
        @constraint(model, hates[agatha, i] => {hates[butler, i] == 1})
    end

    # * No one hates every one.
    for i in 1:n
        @constraint(model, sum([hates[i,j] for j in 1:n]) <= 2)
    end

    # * A killer always hates, and is no richer than his victim.
    for i in 1:n
       @constraint(model, is_killer[i] => {hates[i,agatha] == 1})
       @constraint(model, is_killer[i] => {richer[i,agatha] == 0})
    end

    # * Who killed Agatha?

    # Solve the problem
    println("solve")
    optimize!(model)

    status = JuMP.termination_status(model)
    println("status:$status")
    solutions = []
    if status == MOI.OPTIMAL
        num_sols = MOI.get(model, MOI.ResultCount())
        println("\nnum_sols:$num_sols\n")

        for sol in 1:num_sols
            # println("solution #$sol")
            xkiller = convert.(Integer,JuMP.value.(killer,result=sol))
            # println("killer: $xkiller\n")
            push!(solutions,xkiller)
        end
    end
    people = ["agatha","butler","charles"]
    println("solutions: $([people[sol] for sol in solutions])")
end

who_killed_agatha()


@symbol_func function r_le(cur_reactor::AbstractReactor, cur_rho::AbstractSymbol)
  cur_T = T_rho(cur_reactor, cur_rho)

  cur_r = 1.07e-4

  cur_r *= sqrt(cur_T)

  cur_r /= cur_reactor.B_0

  cur_r
end


function PutCallParity(CallPutFlag, OptionPrice, F, X, T, r)

  # returns for a known options price and the corresponding underlying and Strike
  # the parity price of the other option gender

  if CallPutFlag == "c"
    p = OptionPrice - (F - X) * exp(-r * T)
  elseif CallPutFlag == "p"
    c = OptionPrice + (F - X) * exp(-r * T)
  end

end


## Initialization Filters

using LinearAlgebra, FFTW

generatefilters(type,dims;form=:list) = _generatefilters(Val(type),dims,Val(form))

# DCT
function _generatefilters(::Val{:DCT},dims,::Val{:list})
    H = _generatefilters(Val(:DCT),dims,Val(:matrix))
    return Array.(_filterlist(H,dims))
end
function _generatefilters(::Val{:DCT},dims,::Val{:matrix})
    @assert length(dims) == 2           "Only 2D DCT is implemented"      # todo
    @assert all(i->i==first(dims),dims) "Only square DCT is implemented"  # todo

    temp = dct(Matrix(I,dims),1)
    return kron(temp,temp)' / sqrt(prod(dims))
end


@testset "loglikelihoods.jl" begin
    @model function demo(xs, y)
        s ~ InverseGamma(2, 3)
        m ~ Normal(0, √s)
        for i in eachindex(xs)
            xs[i] ~ Normal(m, √s)
        end

        y ~ Normal(m, √s)
    end

    xs = randn(3)
    y = randn()
    model = demo(xs, y)
    chain = sample(model, MH(), MCMCThreads(), 100, 2)
    var_to_likelihoods = pointwise_loglikelihoods(model, chain)
    @test haskey(var_to_likelihoods, "xs[1]")
    @test haskey(var_to_likelihoods, "xs[2]")
    @test haskey(var_to_likelihoods, "xs[3]")
    @test haskey(var_to_likelihoods, "y")

    for chain_idx in MCMCChains.chains(chain)
        for (i, (s, m)) in enumerate(zip(chain[:, :s, chain_idx], chain[:, :m, chain_idx]))
            @test logpdf(Normal(m, √s), xs[1]) == var_to_likelihoods["xs[1]"][i, chain_idx]
            @test logpdf(Normal(m, √s), xs[2]) == var_to_likelihoods["xs[2]"][i, chain_idx]
            @test logpdf(Normal(m, √s), xs[3]) == var_to_likelihoods["xs[3]"][i, chain_idx]
            @test logpdf(Normal(m, √s), y) == var_to_likelihoods["y"][i, chain_idx]
        end
    end

    var_info = VarInfo(model)
    results = pointwise_loglikelihoods(model, var_info)
    var_to_likelihoods = Dict(string(vn) => ℓ for (vn, ℓ) in results)
    s, m = var_info[SampleFromPrior()]
    @test logpdf(Normal(m, √s), xs[1]) == var_to_likelihoods["xs[1]"]
    @test logpdf(Normal(m, √s), xs[2]) == var_to_likelihoods["xs[2]"]
    @test logpdf(Normal(m, √s), xs[3]) == var_to_likelihoods["xs[3]"]
    @test logpdf(Normal(m, √s), y) == var_to_likelihoods["y"]
end


# create pde structure and mesh structure
pde[1], mesh[1] = Preprocessing.initializeexasim();

# Define PDE model: governing equations, initial solutions, and boundary conditions
pde[1].model = "ModelD";            # ModelC, ModelD, ModelW
pde[1].modelfile = "pdemodel1";    # name of a file defining the PDE model
include(pde[1].modelfile * ".jl"); # include the PDE model file
pde[1].modelnumber = 1;

# Set discretization parameters, physical parameters, and solver parameters
pde[1].porder = 3;                  # polynomial degree
pde[1].physicsparam = [1.0 0.0];    # thermal conductivity and boundary value
pde[1].tau = [1.0];                 # DG stabilization parameter

# Choose computing platform and set number of processors
#pde[1].platform = "gpu";           # choose this option if NVIDIA GPUs are available
pde[1].mpiprocs = 1;                # number of MPI processors

# create a linear mesh for a square domain
mesh[1].p, mesh[1].t = Mesh.SquareMesh(8,8,1); # a mesh of 8 by 8 quadrilaterals
# expressions for disjoint boundaries
mesh[1].boundaryexpr = [p -> (p[2,:] .< 1e-3), p -> (p[1,:] .> 1-1e-3), p -> (p[2,:] .> 1-1e-3), p -> (p[1,:] .< 1e-3)];
mesh[1].boundarycondition = [1 1 1 1]; # Set boundary condition for each disjoint boundary


using BlockBandedMatrices, BlockArrays, LinearAlgebra, MatrixFactorizations, Test
import BlockBandedMatrices: blockcolsupport

@testset "BlockBandedMatrix QR/QL" begin
    @testset "Square QR" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N,1:N, (2,1))
        A.data .= randn.()

        F = qr(A)
        @test F isa MatrixFactorizations.QR{Float64,<:BlockSkylineMatrix}
        @test F.factors ≈ qr(Matrix(A)).factors
        @test F.τ ≈ LinearAlgebra.qrfactUnblocked!(Matrix(A)).τ

        Q,R = F
        Q̃,R̃ = qr(Matrix(A))
        @test R ≈ R̃
        @test Q ≈ Q̃

        b = randn(size(A,1))
        @test Q'b ≈ Q̃'b
        @test Q*b ≈ Q̃*b

        @test factorize(A) isa MatrixFactorizations.QR{Float64,<:BlockSkylineMatrix}
        @test F\b ≈ ldiv!(F, copy(b)) ≈ Matrix(A)\b ≈ A\b
    end

    @testset "Thin QR" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N+1,1:N, (2,1))
        A.data .= randn.()

        F = qr(A)
        @test F.factors ≈ qr(Matrix(A)).factors
        @test F.τ ≈ LinearAlgebra.qrfactUnblocked!(Matrix(A)).τ

        Q,R = F
        Q̃,R̃ = qr(Matrix(A))
        @test R ≈ R̃
        @test Q ≈ Q̃

        b = randn(size(A,1))
        @test Q'b ≈ Q̃'b
        @test Q*b ≈ Q̃*b

        @test Q'b ≈ lmul!(Q', copy(b)) ≈ Q̃'*b

        @test F\b ≈ ldiv!(F, copy(b))[1:15] ≈ Matrix(A)\b ≈ A\b
    end

    @testset "Wide QR" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N,1:N+1, (2,1))
        A.data .= randn.()

        F = qr(A)
        @test F.factors ≈ qr(Matrix(A)).factors
        @test F.τ ≈ LinearAlgebra.qrfactUnblocked!(Matrix(A)).τ

        Q,R = F
        Q̃,R̃ = qr(Matrix(A))
        @test R ≈ R̃
        @test Q ≈ Q̃

        b = randn(size(A,1))
        @test Q'b ≈ Q̃'b
        @test Q*b ≈ Q̃*b
        Q'b
        # @test_broken DimensionMismatch ldiv!(F, copy(b))

        @test_broken F\b ≈ ldiv!(F, copy(b)) ≈ Matrix(A)\b ≈ A\b
    end

    @testset "Square QL" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N,1:N, (2,1))
        A.data .= randn.()

        F = ql(A)
        @test F.factors ≈ ql(Matrix(A)).factors
        @test F.τ ≈ MatrixFactorizations.qlfactUnblocked!(Matrix(A)).τ

        Q,L = F
        Q̃,L̃ = ql(Matrix(A))
        @test L ≈ L̃
        @test Q ≈ Q̃

        b = randn(size(A,1))
        @test Q'b ≈ Q̃'b
        @test Q*b ≈ Q̃*b

        b = randn(size(A,1))
        @test F\b ≈ ldiv!(F, copy(b)) ≈ Matrix(A)\b ≈ A\b
    end

    @testset "Thin QL" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N+1,1:N, (2,1))
        A.data .= randn.()

        # F = ql(A)
        # @test F.factors ≈ ql(Matrix(A)).factors
        # @test F.τ ≈ MatrixFactorizations.qlfactUnblocked!(Matrix(A)).τ

        # Q,L = F
        # Q̃,L̃ = ql(Matrix(A))
        # @test L ≈ L̃
        # @test Q ≈ Q̃

        # b = randn(size(A,1))
        # @test Q'b ≈ Q̃'b
        # @test Q*b ≈ Q̃*b

        # b = randn(size(A,1))
        # @test F\b ≈ ldiv!(F, copy(b)) ≈ Matrix(A)\b ≈ A\b
    end

    @testset "Wide QL" begin
        N = 5
        A = BlockBandedMatrix{Float64}(undef, 1:N,1:N+1, (2,1))
        A.data .= randn.()

        @test_throws ArgumentError ql(A)
    end

    @testset "Complex QR/QL" begin
        @testset "Simple" begin
            A=BlockBandedMatrix{Float64}(I, [1,1],[1,1], (0,0))
            Af=qr(A)
            B=BlockBandedMatrix{ComplexF64}(I, [1,1],[1,1], (0,0))
            Bf=qr(B)
            @test Af.factors == Bf.factors
            @test Af.τ == Bf.τ
        end

        @testset "Off-diagonals" begin
            for qrl in [qr,ql]
                for T in [Float64, ComplexF64]
                    A = BlockBandedMatrix{T}(undef, [3,2,1,3],[3,2,1,3], (1,1))
                    A.data .= rand(T, size(A.data))
                    Af = qrl(A)
                    @test Af.factors isa BlockBandedMatrix

                    Am = Matrix(A)
                    Amf = qrl(Am)

                    @test Af.factors ≈ Amf.factors
                end
            end
        end
    end

    @testset "Fast QR \\" begin
        A = [1. 2; 3 4]; A = A + A'
        B = [5. 6; 7 8]
        N = 10_000;
        T = BlockBandedMatrix(mortar(Tridiagonal(fill(Matrix(B'),N-1), fill(zeros(2,2),N), fill(B,N-1))))
        z = 1+2im
        @time F = qr(T - z*I);
        @test blockbandwidths(UpperTriangular(F.factors)) == (0,2)
        @test blockcolsupport(UpperTriangular(F.factors),Block(4)) == Block.(2:4)
        V = view(F.factors,Block.(1:N), Block.(1:N))
        @test blocksize(V) == (N,N)
        @test @allocated(blocksize(V)) ≤ 40
        @test blockbandwidths(V) == (1,2)
        @test @allocated(blockbandwidths(V)) ≤ 40
        R = UpperTriangular(V)
        @test axes(R) == axes(V) == axes(F.factors)
        @test blocksize(R) == (N,N)
        @test @allocated(blocksize(R)) ≤ 40
        @test blockbandwidths(R) == (0,2)
        @test @allocated(blockbandwidths(R)) ≤ 40
        @test blockcolsupport(R,Block(4)) == Block.(2:4)
        @test @allocated(blockcolsupport(R,Block(4))) ≤ 40

        b = [1; zeros(size(T,1)-1)]
        B = [Matrix(I,2,2); zeros(size(T,1)-2,2)]
        @test ((T - z*I)\b)[1] ≈ (F\b)[1] ≈ (F \ B)[1,1] ≈ ((T - z*I)\B)[1,1] ≈ -0.1309123477325813 + 0.28471699370329884im
    end
end

# N = 500;
# A = BlockBandedMatrix{Float64}(undef, (1:N,1:N), (2,1));
# A.data .= randn.();
# @time F = qr(A); # 11s
# b = randn(size(A,1));
# @time F\b; # 0.6s


export consistent_tangent_matrix, isotropic_function, strain_voigt_to_tensor, bi_isotropic_function
@doc raw"""
    consistent_tangent_matrix(inputs::Union{Array{Float64, 2}, PyObject},Dc::Union{Array{Float64,2}, PyObject})

Returns the consistent tangent matrices. The size of the return is $N\times 3 \times 3$.

$$D_c^{ep} = D_c - \frac{D_c\frac{\partial g}{\partial \sigma}\left(\frac{\partial f}{\partial \sigma} \right)^T D_c }{E_p + \frac{\partial g}{\partial \sigma} Dc \left(\frac{\partial f}{\partial \sigma} \right)^T}$$

Here `inputs` is a $N\times 7$ matrix, where each row is

$$\left[\frac{\partial g}{\partial \sigma}, \frac{\partial f}{\partial \sigma}, E_p\right]$$

`Dc` is a $3\times 3$ **row-major** matrix; each row is a linear elasticity matrix.

!!! info
    The gradients with respect to both `inputs` and `H` are available.
"""
function consistent_tangent_matrix(inputs::Union{Array{Float64, 2}, PyObject},H::Union{Array{Float64,2}, PyObject})
    @assert size(inputs,2)==7
    @assert size(H,1)==size(H,2)==3
    plasticity_ = load_op_and_grad("$(@__DIR__)/../../deps/CustomOp/build/libDataLib","plasticity")
    inputs,H = convert_to_tensor([inputs,H], [Float64,Float64])
    out = plasticity_(inputs,H)
    set_shape(out, (size(inputs, 1), 3, 3))
end

@doc raw"""
    isotropic_function(coef::Union{Array{Float64,2}, PyObject},strain::Union{Array{Float64,2}, PyObject})

Isotropic function of a symmetric tensor.

$$T = s_0 I + s_1 A + s_2 A^2$$

Here

$$\texttt{coef(i,:)} = [s_0\ s_1\ s_2]$$

$$A = \left[\begin{matrix}
S_{i1} & S_{i3}/2\\
S_{i3}/2 & S_{i2}
\end{matrix}\right]$$

where $S_{ij}$ is the $i$-th row and $j$-th column of `strain`
"""
function isotropic_function(coef::Union{Array{Float64,2}, PyObject},strain::Union{Array{Float64,2}, PyObject})
    @assert size(strain,2)==size(coef,2)==3
    @assert size(coef,1)==size(strain,1)
    isotropic_ = load_op_and_grad("$(@__DIR__)/../../deps/CustomOp/build/libDataLib","isotropic")
    coef,strain = convert_to_tensor([coef,strain], [Float64,Float64])
    out = isotropic_(coef,strain)
    set_shape(out, (size(coef,1), 3))
end

@doc raw"""
    strain_voigt_to_tensor(inp::Union{Array{Float64,2}, PyObject})

Converts Voigt strain tensors to matrix form

$$\begin{bmatrix}
\epsilon_{11}\\
\epsilon_{22}\\
2\epsilon_{12}\\
\end{bmatrix} \rightarrow \begin{bmatrix}
\epsilon_{11} &  \epsilon_{12}\\
\epsilon_{12} & \epsilon_{22}
\end{bmatrix}$$

The input is $N\times 3$ and the output is $N\times 2 \times 2$
"""
function strain_voigt_to_tensor(inp::Union{Array{Float64,2}, PyObject})
    @assert size(inp,2)==3
    tensor_rep_ = load_op_and_grad("$(@__DIR__)/../../deps/CustomOp/build/libDataLib","tensor_rep")
    inp = convert_to_tensor([inp], [Float64]); inp = inp[1]
    set_shape(tensor_rep_(inp), (size(inp,1), 2,2))
end

@doc raw"""
    bi_isotropic_function(coef::Union{Array{Float64,2}, PyObject},strain::Union{Array{Float64,2}, PyObject},
    strainrate::Union{Array{Float64,2}, PyObject})

Isotropic function of two symmetric tensors $A$ and $B$

$$\begin{aligned}
T(A, B) &= \gamma_0 I + \gamma_1 A + \gamma_2 B + \gamma_3 A^2 + \gamma_4 (A*B+B*A)\\
& + \gamma_5 B^2 + \gamma_6 (A^2 B + BA^2) + \gamma_7 (AB^2 + B^2 A) + \gamma_8 (A^2B^2+B^2A^2)
\end{aligned}$$

This is useful for rate-dependent constitutive relation. For example,

$$\sigma^{n+1} = \mathcal{C}(\epsilon^{n+1}, \dot\epsilon^{n+1})$$

Here

- `coef`: $N\times 9$ coefficient array

- `strain`: $N\times 3$ strain tensor in Voigt notation

- `strainrate`: $N\times 3$ strain rate tensor in Voigt notation
"""
function bi_isotropic_function(coef::Union{Array{Float64,2}, PyObject},strain::Union{Array{Float64,2}, PyObject},
    strainrate::Union{Array{Float64,2}, PyObject})
    @assert size(coef, 2)==9
    isotropic_two_ = load_op_and_grad("$(@__DIR__)/../../deps/CustomOp/build/libDataLib","isotropic_two")
    coef,strain,strainrate = convert_to_tensor([coef,strain,strainrate], [Float64,Float64,Float64])
    T = isotropic_two_(coef,strain,strainrate)
    set_shape(T, (size(strain, 1), 3))
end


export FiniteTranslationSymmetry
export TranslationSymmetry
export dimension
export group, # group_order, group_multiplication_table,
       character_table,
       irrep, irreps, irrep_dimension,
       num_irreps, numirreps, irrepcount,
       element, elements,
       element_name, element_names,
       generator_elements, generator_indices,
       symmetry_product

export fractional_momentum
export isbragg

export symmetry_name

export translation_symmetry_embedding

using GroupTools

import LinearAlgebraX
import MathExpr

"""
    FiniteTranslationSymmetry

Represent lattice translation symmetry.

```
julia> FiniteTranslationSymmetry([3 0; 0 2])
```

# Fields
* `hypercube::Hypercube`
* `elements::Vector{TranslationOperation{Int}}`
* `group::FiniteGroup`

# Additional Fields
* `generators::Vector{Int}`
* `conjugacy_classes::Vector{Vector{Int}}`
* `character_table::Matrix{ComplexF64}`
* `irreps::Vector{Vector{Matrix{ComplexF64}}}`
* `element_names::Vector{String}`

# Cache Fields
* `generator_translations::Matrix{Int}`
* `coordinates::Vector{Vector{Int}}`
* `orthogonal_shape::Vector{Int}`
* `orthogonal_coordinates::Vector{Vector{Int}}`
* `orthogonal_to_coordinate_map::Dict{Vector{Int}, Vector{Int}}`
* `coordinate_to_orthogonal_map::Dict{Vector{Int}, Vector{Int}}`
* `orthogonal_shape_matrix::Matrix{Int}`
* `orthogonal_reduced_reciprocal_shape_matrix::Matrix{Rational{Int}}`
* `fractional_momenta::Vector{Vector{Rational{Int}}}`
"""
struct FiniteTranslationSymmetry <: AbstractSymmetry{TranslationOperation{Int}}

    hypercube::Hypercube

    elements::Vector{TranslationOperation{Int}}
    group::FiniteGroup

    generators::Vector{Int}
    conjugacy_classes::Vector{Vector{Int}}
    character_table::Matrix{ComplexF64}
    irreps::Vector{Vector{Matrix{ComplexF64}}}
    element_names::Vector{String}

    # for quick
    generator_translations::Matrix{Int}
    coordinates::Vector{Vector{Int}}
    orthogonal_shape::Vector{Int}
    orthogonal_coordinates::Vector{Vector{Int}}

    orthogonal_to_coordinate_map::Dict{Vector{Int}, Vector{Int}}
    coordinate_to_orthogonal_map::Dict{Vector{Int}, Vector{Int}}

    orthogonal_shape_matrix::Matrix{Int}
    orthogonal_reduced_reciprocal_shape_matrix::Matrix{Rational{Int}}
    fractional_momenta::Vector{Vector{Rational{Int}}}

    @doc """
        FiniteTranslationSymmetry(shape::Matrix{<:Integer}; tol=√ϵ)
    """
    function FiniteTranslationSymmetry(shape::Matrix{<:Integer}; tol::Real=Base.rtoldefault(Float64))
        return FiniteTranslationSymmetry(Hypercube(shape))
    end

    @doc """
        FiniteTranslationSymmetry(lattice::Lattice; tol=√ϵ)
    """
    function FiniteTranslationSymmetry(lattice::Lattice; tol::Real=Base.rtoldefault(Float64))
        return FiniteTranslationSymmetry(lattice.hypercube)
    end

    @doc """
        FiniteTranslationSymmetry(hypercube::Hypercube; tol=√ϵ)
    """
    function FiniteTranslationSymmetry(
        hypercube::Hypercube;
        tol::Real=Base.rtoldefault(Float64),
    )
        generator_translations = find_generators(hypercube)
        return FiniteTranslationSymmetry(hypercube, generator_translations; tol=tol)
    end

    @doc """
        FiniteTranslationSymmetry(hypercube::Hypercube, generators::AbstractMatrix{<:Integer}; tol::Real=Base.rtoldefault(Float64))
    """
    function FiniteTranslationSymmetry(
        hypercube::Hypercube,
        generator_translations::AbstractMatrix{<:Integer};
        tol::Real=Base.rtoldefault(Float64),
    )
        if !isone(LinearAlgebraX.detx(generator_translations))
            throw(ArgumentError("generator translation is not unimodular"))
        end

        # BEGIN Orthogonal
        coordinates = generate_coordinates(hypercube, generator_translations)
        coordinate_indices = Dict(r => i for (i, r) in enumerate(coordinates))

        pbcadd = (x::Vector{Int}, y::Vector{Int}) -> hypercube.wrap(x+y)[2]
        group = FiniteGroup(generate_multiplication_table(coordinates; product=pbcadd))
        @assert isabelian(group)

        ord_group = group_order(group)

        generators = Int[ coordinate_indices[ hypercube.wrap(v)[2] ]
                              for v in eachcol(generator_translations) ]

        orthogonal_shape = Int[group.period_lengths[g] for g in generators] # in "generator" coordinates
        orthogonal_coordinates = vec([[x...] for x in Iterators.product([0:(d-1) for d in orthogonal_shape]...)])

        @assert ord_group == prod(orthogonal_shape) == length(orthogonal_coordinates) == length(coordinates)

        orthogonal_to_coordinate_map = Dict{Vector{Int}, Vector{Int}}()
        coordinate_to_orthogonal_map = Dict{Vector{Int}, Vector{Int}}()

        let ortho_latvec = generator_translations
            for (r, r_ortho) in zip(coordinates, orthogonal_coordinates)
                @assert r == hypercube.wrap(ortho_latvec * r_ortho)[2]
                orthogonal_to_coordinate_map[r_ortho] = r
                coordinate_to_orthogonal_map[r] = r_ortho
            end
        end
        orthogonal_shape_matrix = hcat(
                    [group_order(group, g) * v
                        for (g, v) in zip(generators, eachcol(generator_translations))]...)
        orthogonal_reduced_reciprocal_shape_matrix = LinearAlgebraX.invx(transpose(orthogonal_shape_matrix))

        elements = [TranslationOperation(r) for r in coordinates]

        fractional_momenta = let modunit = x -> mod(x, 1)
            [modunit.( orthogonal_reduced_reciprocal_shape_matrix * orthogonal_integer_momentum )
                 for orthogonal_integer_momentum in orthogonal_coordinates]
        end

        # each element of an abelian group is a conjugacy class
        element_names = ["$(orthogonal_to_coordinate_map[t])" for t in orthogonal_coordinates]
        conjugacy_classes = [[i] for (i,x) in enumerate(element_names)]

        momentum(oc::AbstractVector{<:Integer}) = [2π * x / d for (x, d) in zip(oc, orthogonal_shape)]
        character_table = ComplexF64[cis(-LinearAlgebra.dot(momentum(kd), t))
                                     for kd in orthogonal_coordinates,
                                          t in orthogonal_coordinates]

        character_table = MathExpr.cleanupnumber(character_table, tol)

        let
            χ = ComplexF64[cis(-2π * LinearAlgebra.dot(k, t)) for k in fractional_momenta, t in coordinates]
            @assert isapprox(character_table, χ; atol=tol)
        end

        # each element forms a conjugacy class
        irreps = Vector{Matrix{ComplexF64}}[]
        for (idx_rep, momentum) in enumerate(orthogonal_coordinates)
            matrices = Matrix{ComplexF64}[]
            for (idx_elem, orthogonal_translation) in enumerate(orthogonal_coordinates)
                push!(matrices, character_table[idx_rep, idx_elem] * ones(ComplexF64, 1, 1))
            end
            push!(irreps, matrices)
        end

        return new(
            hypercube, elements, group, generators,
            conjugacy_classes, character_table, irreps, element_names,
            generator_translations,
            coordinates,
            orthogonal_shape, orthogonal_coordinates,
            orthogonal_to_coordinate_map, coordinate_to_orthogonal_map,
            orthogonal_shape_matrix, orthogonal_reduced_reciprocal_shape_matrix,
            fractional_momenta,
        )
    end
end

function TranslationSymmetry(args...; kwargs...)
    @warn "TranslationSymmetry has been renamed to FiniteTranslationSymmetry." maxlog=1
    return FiniteTranslationSymmetry(args...; kwargs...)
end

"""
    dimension(sym::FiniteTranslationSymmetry)

Spatial dimension of the translation symmetry
"""
dimension(sym::FiniteTranslationSymmetry) = dimension(sym.hypercube)

"""
    group(sym::FiniteTranslationSymmetry)

Group structure of the translation symmetry
"""
GroupTools.group(sym::FiniteTranslationSymmetry) = sym.group

"""
    group_order(sym::FiniteTranslationSymmetry, g...)

Group order of the translation symmetry. Calls `group_order(group(sym), g...)`
"""
GroupTools.group_order(sym::FiniteTranslationSymmetry, g...) = group_order(group(sym), g...)

"""
    group_multiplication_table(sym::FiniteTranslationSymmetry)

Group multiplication table of the translation symmetry.
Calls `group_multiplication_table(group(sym))`
"""
GroupTools.group_multiplication_table(sym::FiniteTranslationSymmetry) = group_multiplication_table(group(sym))

"""
    elements(sym::FiniteTranslationSymmetry)

Get the elements of the translation symmetry.
"""
GroupTools.elements(sym::FiniteTranslationSymmetry) = sym.elements

"""
    element(sym::FiniteTranslationSymmetry, i)

Get the `i`th element of the translation symmetry.
"""
GroupTools.element(sym::FiniteTranslationSymmetry, g) = sym.elements[g]

"""
    element_names(sym::FiniteTranslationSymmetry)

Get the names of the elements of the translation symmetry.
"""
GroupTools.element_names(sym::FiniteTranslationSymmetry) = sym.element_names

"""
    element_name(sym::FiniteTranslationSymmetry, i)

Get the name of the `i`th element of the translation symmetry.
"""
GroupTools.element_name(sym::FiniteTranslationSymmetry, g) = sym.element_names[g]

"""
    generator_indices(sym::FiniteTranslationSymmetry)

Return indices of the generating translations.
"""
generator_indices(sym::FiniteTranslationSymmetry) = sym.generators

"""
    generator_elements(sym::FiniteTranslationSymmetry)

Return the generating translation operations.
"""
generator_elements(sym::FiniteTranslationSymmetry) = element(sym, sym.generators)

"""
    symmetry_product(tsym::FiniteTranslationSymmetry)

Return a binary function which combines two translation operations, with the given periodic
boundary condition.

```jldoctest
julia> using LatticeTools

julia> tsym = FiniteTranslationSymmetry([3 0; 0 4]);

julia> p = symmetry_product(tsym);

julia> p(TranslationOperation([2, 1]), TranslationOperation([2, 3]))
TranslationOperation{Int64}([1, 0])
```
"""
function symmetry_product(sym::FiniteTranslationSymmetry)
    function product(lhs::TranslationOperation, rhs::TranslationOperation)
        return TranslationOperation(sym.hypercube.wrap(lhs.displacement + rhs.displacement)[2])
    end
    return product
end

function symmetry_canonize(sym::FiniteTranslationSymmetry)
    function canonize(arg::TranslationOperation{<:Integer})
        return TranslationOperation(sym.hypercube.wrap(arg.displacement)[2])
    end
    function canonize(arg::PointOperation{<:Integer})
        return arg
    end
    function canonize(arg::SpaceOperation{<:Integer, <:Integer})
        return SpaceOperation(canonize(arg.matrix), canonize(arg.displacement))
    end
    return canonize
end

Base.eltype(::FiniteTranslationSymmetry) = TranslationOperation{Int}
Base.valtype(::FiniteTranslationSymmetry) = TranslationOperation{Int}
Base.eltype(::Type{<:FiniteTranslationSymmetry}) = TranslationOperation{Int}
Base.valtype(::Type{<:FiniteTranslationSymmetry}) = TranslationOperation{Int}

Base.in(item::Any, sym::FiniteTranslationSymmetry) = false
# function Base.in(item::IdentityOperation, sym::FiniteTranslationSymmetry)
#     dimension(item) == dimension(sym)
# end
function Base.in(item::TranslationOperation{<:Integer}, sym::FiniteTranslationSymmetry)
    dimension(item) == dimension(sym)
end
function Base.in(item::PointOperation, sym::FiniteTranslationSymmetry)
    dimension(item) == dimension(sym) && istranslation(item)
end
function Base.in(item::SpaceOperation{Tp, <:Integer}, sym::FiniteTranslationSymmetry) where {Tp}
    dimension(item) == dimension(sym) && istranslation(item)
end

Base.iterate(sym::FiniteTranslationSymmetry) = iterate(elements(sym))
Base.iterate(sym::FiniteTranslationSymmetry, i) = iterate(elements(sym), i)

Base.length(sym::FiniteTranslationSymmetry) = length(elements(sym))

"""
    symmetry_name(sym::FiniteTranslationSymmetry)

Name of the translation symmetry. Return `FiniteTranslationSymmetry[(n11,n21)x(n12,n22)]`.
"""
function symmetry_name(sym::FiniteTranslationSymmetry)
    n11 = sym.hypercube.shape_matrix[1,1]
    n12 = sym.hypercube.shape_matrix[1,2]
    n21 = sym.hypercube.shape_matrix[2,1]
    n22 = sym.hypercube.shape_matrix[2,2]
    return "FiniteTranslationSymmetry[($n11,$n21)x($n12,$n22)]"
end

# Irreducible Representations

"""
    character_table(sym::FiniteTranslationSymmetry)

Return the character table of the translation symmetry.
"""
character_table(sym::FiniteTranslationSymmetry) = sym.character_table

"""
    irreps(sym::FiniteTranslationSymmetry)

Return the irreducible representations of the translation symmetry
"""
irreps(sym::FiniteTranslationSymmetry) = sym.irreps

"""
    irrep(sym::FiniteTranslationSymmetry, idx)

Return the `idx`th irreducible representation of the translation symmetry
"""
irrep(sym::FiniteTranslationSymmetry, idx) = sym.irreps[idx]

"""
    num_irreps(sym::FiniteTranslationSymmetry)

Return the number of irreducible representations of the translation symmetry,
i.e. number of allowed momentum points.
Aliases: [`num_irreps`](@ref), [`numirreps`](@ref), [`irrepcount`](@ref)
"""
num_irreps(sym::FiniteTranslationSymmetry) = length(sym.irreps)

"""
    numirreps(sym::FiniteTranslationSymmetry)

Return the number of irreducible representations of the translation symmetry,
i.e. number of allowed momentum points.
Aliases: [`num_irreps`](@ref), [`numirreps`](@ref), [`irrepcount`](@ref)
"""
numirreps(sym::FiniteTranslationSymmetry) = length(sym.irreps)

"""
    irrepcount(sym::FiniteTranslationSymmetry)

Return the number of irreducible representations of the translation symmetry,
i.e. number of allowed momentum points.
Aliases: [`num_irreps`](@ref), [`numirreps`](@ref), [`irrepcount`](@ref)
"""
irrepcount(sym::FiniteTranslationSymmetry) = length(sym.irreps)

"""
    irrep_dimension(sym::FiniteTranslationSymmetry, idx::Integer)

Dimension of the `idx`th irrep of the translation symmetry,
which is always 1 for translation symmetry which is Abelian.
"""
irrep_dimension(sym::FiniteTranslationSymmetry, idx::Integer) = 1 # size(first(sym.irreps[idx]), 1)

"""
    fractional_momentum(sym::FiniteTranslationSymmetry, g...)

Return the `g`th fractional momentum of the translation symmetry.
"""
fractional_momentum(sym::FiniteTranslationSymmetry, g...) = sym.fractional_momenta[g...]

"""
    isbragg(shape, integer_momentum, identity_translation)

Test whether an orthogonal momentum is compatible with the identity translation.
Since identity translation can only be in the trivial representation
(otherwise the phases cancel), this function tests whether the phase is unity.
The orthogonal momentum is given as an integer vector.

```math
  t(\\rho) \\vert \\psi(k) \\rangle
    = exp(i k \\cdot \\rho ) \\vert \\psi(k) \\rangle
    = \\vert \\psi(k) \\rangle
```

When the orthogonal shape is [n₁, n₂, ...], orthogonal momentum is chosen from
{0, 1, ..., n₁-1} × {0,1, ..., n₂-1} × ....
"""
function isbragg(
    shape::AbstractVector{<:Integer},
    integer_momentum::AbstractVector{<:Integer},
    identity_translation::AbstractVector{<:Integer},
)
    if length(integer_momentum) != length(shape)
        throw(DimensionMismatch("lengths of momentum and shape do not match"))
    elseif length(shape) != length(identity_translation)
        throw(DimensionMismatch("lengths of shape and translation do not match"))
    elseif any(shape .<= 0)
        throw(ArgumentError("shape needs to be all positive"))
    end
    value = Rational{Int}(0)
    for (i, j, k) in zip(integer_momentum, identity_translation, shape)
        value += i * j // k
    end
    return mod(value, 1) == 0
end

"""
    isbragg(shape, integer_momentum, identity_translations)

Check for Bragg condition at the `integer momentum` for all the `identity translations`.
"""
function isbragg(
    shape::AbstractVector{<:Integer},
    integer_momentum::AbstractVector{<:Integer},
    identity_translations::AbstractVector{<:AbstractVector{<:Integer}},
)
    return all(isbragg(shape, integer_momentum, t) for t in identity_translations)
end

"""
    isbragg(k, t)

Check for Bragg condition at momentum `k` and translation `t`.
Fractional momentum is normalized to 1, i.e. lies within [0, 1)ⁿ
"""
function isbragg(
    fractional_momentum::AbstractVector{<:Rational{<:Integer}},
    translation::AbstractVector{<:Integer},
)
    if length(fractional_momentum) != length(translation)
        throw(DimensionMismatch("lengths of momentum and translation do not match"))
    end
    value = LinearAlgebra.dot(fractional_momentum, translation)
    return mod(value, 1) == 0
end

"""
    isbragg(k, translations)

Check for Bragg condition at `k` for all the translations.
"""
function isbragg(
    fractional_momentum::AbstractVector{<:Rational{<:Integer}},
    translations::AbstractVector{<:AbstractVector{<:Integer}},
)
    return all(isbragg(fractional_momentum, t) for t in translations)
end

"""
    isbragg(tsym, tsym_irrep_index, translation)

Check for Bragg condition at momentum given by the `tsym_irrep_index`.
"""
function isbragg(
    tsym::FiniteTranslationSymmetry,
    tsym_irrep_index::Integer,
    translation::TranslationOperation{<:Integer},
)
    fractional_momentum = tsym.fractional_momenta[tsym_irrep_index]
    return isbragg(fractional_momentum, translation.displacement)
end

"""
    isbragg(tsym, tsym_irrep_index, translations)

Check for Bragg condition at momentum given by the `tsym_irrep_index` for all translations.
"""
function isbragg(
    tsym::FiniteTranslationSymmetry,
    tsym_irrep_index::Integer,
    translations::AbstractVector{<:TranslationOperation{<:Integer}},
)
    fractional_momentum = tsym.fractional_momenta[tsym_irrep_index]
    return all(isbragg(fractional_momentum, t.displacement) for t in translations)
end

# """
#     iscompatible(tsym, tsym_irrep_index, identity_translation)

# Test whether the identity translation is compatible with the irreducible representation
# of the translation symmetry. The identity translation is in units of the "generators" of the lattice,
# i.e. orthogonal integer shape.
# """
# function iscompatible(tsym::FiniteTranslationSymmetry,
#                       tsym_irrep_index::Integer,
#                       identity_translation::AbstractVector{<:Integer})
#                       #identity_translation::TranslationOperation{<:Integer})
#     orthogonal_momentum = tsym.orthogonal_coordinates[tsym_irrep_index]
#     orthogonal_shape = tsym.orthogonal_shape
#     return isbragg(orthogonal_shape, orthogonal_momentum, identity_translation.displacement)
# end

# function iscompatible(tsym::FiniteTranslationSymmetry,
#                       tsym_irrep_index::Integer,
#                       identity_translations::AbstractVector{<:AbstractVector{<:Integer}})
#                     #   identity_translations::AbstractVector{<:TranslationOperation{<:Integer}})
#     orthogonal_shape = tsym.orthogonal_shape
#     orthogonal_momentum = tsym.orthogonal_coordinates[tsym_irrep_index]
#     return all(isbragg(orthogonal_shape, orthogonal_momentum, t.displacement)
#                    for t in identity_translations)
# end

# function iscompatible(# lattice::Lattice,
#                       tsym::FiniteTranslationSymmetry,
#                       tsym_irrep_index::Integer,
#                       identity_translation::AbstractVector{<:Integer})
#     # !iscompatible(lattice, tsym) && return false
#     orthogonal_momentum = tsym.orthogonal_coordinates[tsym_irrep_index]
#     orthogonal_shape = tsym.orthogonal_shape
#     return iscompatible(orthogonal_momentum, orthogonal_shape, identity_translation)
# end

# function iscompatible(# lattice::Lattice,
#                       tsym::FiniteTranslationSymmetry,
#                       tsym_irrep_index::Integer,
#                       identity_translations::AbstractVector{<:AbstractVector{<:Integer}})
#     orthogonal_momentum = tsym.orthogonal_coordinates[tsym_irrep_index]
#     orthogonal_shape = tsym.orthogonal_shape
#     return all(iscompatible(orthogonal_momentum, orthogonal_shape, t) for t in identity_translations)
# end

# function iscompatible(orthogonal_shape::AbstractVector{<:Integer},
#                       orthogonal_momentum::AbstractVector{<:Integer},
#                       identity_translation::TranslationOperation{<:Integer})
#     value = Rational{Int}(0)
#     for (n, i, j) in zip(orthogonal_shape, orthogonal_momentum, identity_translation.displacement)
#         value += i * j // n
#     end
#     return mod(value, 1) == 0
# end

# """
#     iscompatible(orthogonal_momentum, orthogonal_shape, identity_translations)

# Test whether the given momentum is compatible with *all* the identity translations.
# """
# function iscompatible(orthogonal_shape::AbstractVector{<:Integer},
#                       orthogonal_momentum::AbstractVector{<:Integer},
#                       identity_translations::AbstractVector{<:TranslationOperation{<:Integer}})
#     return all(iscompatible(orthogonal_shape, orthogonal_momentum, t) for t in identity_translations)
# end

# function generators(lattice::Lattice, tsym::FiniteTranslationSymmetry)
#     if lattice.hypercube != tsym.hypercube
#         throw(ArgumentError("lattice and translation symmetry not consistent"))
#     end
#     n_uc = length(lattice.hypercube.coordinates)
#     n_orb = numsite(lattice.unitcell)
#     dim = dimension(lattice)
#     permutations = Permutation[]
#     trans_ortho = zeros(Int, dim)
#     for d in 1:dimension
#         trans_ortho[:] = 0
#         trans_ortho[d] = 1
#         trans_coord = tsym.orthogonal_to_coordinate_map[trans_ortho]
#         p = zeros(Int, n_uc * n_orb)
#         for (site_index1, ((site_name1, uc_coord1), _)) in enumerate(lattice.supercell.sites)
#             _, uc_coord2 = lattice.hypercube.wrap(uc_coord1 + trans_coord)
#             site_index1 = getsiteindex(lattice.supercell, (site_name1, uc_coord1))
#             site_index2 = getsiteindex(lattice.supercell, (site_name1, uc_coord2))
#             p[site_index1] = site_index2
#         end
#         push!(permutations, Permutation(p))
#     end
#     return permutations
# end

    # function FiniteTranslationSymmetry(shape::Matrix{<:Integer}; tol::Real=Base.rtoldefault(Float64))
    #     return FiniteTranslationSymmetry(orthogonalize(HypercubicLattice(shape)))
    # end

    # function FiniteTranslationSymmetry(lattice::Lattice; tol::Real=Base.rtoldefault(Float64))
    #     return FiniteTranslationSymmetry(lattice.hypercube)
    # end

    # function FiniteTranslationSymmetry(hypercube::HypercubicLattice; tol::Real=Base.rtoldefault(Float64))
    #     if dimension(hypercube) == 1
    #         generator_translations = ones(Int, (1,1))
    #         return FiniteTranslationSymmetry(hypercube, generator_translations; tol=tol)
    #     elseif dimension(hypercube) == 2
    #         generator_translations = decompose_lattice_2d(hypercube)
    #         return FiniteTranslationSymmetry(hypercube, generator_translations; tol=tol)
    #     else
    #         error("Currenly only supports 1D and 2D")
    #     end
    # end

    # function FiniteTranslationSymmetry(hypercube::HypercubicLattice,
    #                              generator_translations::AbstractMatrix{<:Integer};
    #                              tol::Real=Base.rtoldefault(Float64))

    #     if MathExpr.determinant(generator_translations) != 1
    #         throw(ArgumentError("generator translation is not unimodular"))
    #     end

    #     group = FiniteGroup(translation_group_multiplication_table(hypercube))
    #     ord_group = group_order(group)

    #     @assert isabelian(group)
    #     @assert ord_group == length(hypercube.coordinates)

    #     generators = Int[ hypercube.coordinate_indices[ hypercube.wrap(v)[2] ]
    #                          for v in eachcol(generator_translations) ]

    #     # BEGIN Orthogonal
    #     orthogonal_shape = [group.period_lengths[g] for g in generators] # in "generator" coordinates
    #     orthogonal_coordinates = vec([[x...] for x in Iterators.product([0:(d-1) for d in orthogonal_shape]...)])

    #     @assert prod(orthogonal_shape) == ord_group
    #     @assert length(orthogonal_coordinates) == ord_group

    #     orthogonal_to_coordinate_map = Dict{Vector{Int}, Vector{Int}}()
    #     coordinate_to_orthogonal_map = Dict{Vector{Int}, Vector{Int}}()

    #     let ortho_latvec = generator_translations #hcat(hypercube.coordinates[generators]...)
    #         for r_ortho in orthogonal_coordinates
    #             _, r = hypercube.wrap(ortho_latvec * r_ortho)
    #             orthogonal_to_coordinate_map[r_ortho] = r
    #             coordinate_to_orthogonal_map[r] = r_ortho
    #         end
    #     end
    #     orthogonal_shape_matrix = hcat(
    #                 [group_order(group, g) * v
    #                     for (g, v) in zip(generators, eachcol(generator_translations))]...
    #             )
    #     orthogonal_reduced_reciprocal_shape_matrix = MathExpr.inverse(transpose(orthogonal_shape_matrix))
    #     # END Orthogonal

    #     elements = [TranslationOperation(orthogonal_to_coordinate_map[r_ortho])
    #                     for r_ortho in orthogonal_coordinates]

    #     @assert hypercube.coordinates == [t.displacement for t in elements] "Remove this when hypercube coordinates convention is fixed"

    #     fractional_momenta = let modunit = x -> mod(x, 1)
    #         [modunit.( orthogonal_reduced_reciprocal_shape_matrix * orthogonal_integer_momentum )
    #              for orthogonal_integer_momentum in orthogonal_coordinates]
    #     end

    #     # each element of an abelian group is a conjugacy class
    #     element_names = ["$(orthogonal_to_coordinate_map[t])" for t in orthogonal_coordinates]
    #     conjugacy_classes = [[i] for (i,x) in enumerate(element_names)]

    #     momentum(oc::AbstractVector{<:Integer}) = [2π * x / d for (x, d) in zip(oc, orthogonal_shape)]
    #     character_table = ComplexF64[cis(-LinearAlgebra.dot(momentum(kd), t))
    #                                  for kd in orthogonal_coordinates,
    #                                       t in orthogonal_coordinates]

    #     character_table = cleanup_number(character_table, tol)

    #     # each element forms a conjugacy class
    #     irreps = Vector{Matrix{ComplexF64}}[]
    #     for (idx_rep, momentum) in enumerate(orthogonal_coordinates)
    #         matrices = Matrix{ComplexF64}[]
    #         for (idx_elem, orthogonal_translation) in enumerate(orthogonal_coordinates)
    #             push!(matrices, character_table[idx_rep, idx_elem] * ones(ComplexF64, 1, 1))
    #         end
    #         push!(irreps, matrices)
    #     end

    #     return new(hypercube, elements, group, generators,
    #                conjugacy_classes, character_table, irreps, element_names,
    #                generator_translations,
    #                orthogonal_shape, orthogonal_coordinates,
    #                orthogonal_to_coordinate_map, coordinate_to_orthogonal_map,
    #                orthogonal_shape_matrix, orthogonal_reduced_reciprocal_shape_matrix,
    #                fractional_momenta)
    # end


using ImageMethodReverb, Random
Fs = 8e3                        # sampling frequency
L = Lx, Ly, Lz =  4.,5.,3.      # room dimensions in meters
T60 = 0.7                       # reverberation time
# or alternatively
#β = (0.9,0.9,0.7,0.7,0.8,0.8)
#(  βx1     ,    βx2   ,    βy1   ,    βy2    ,  βz1 ,   βz2  )
#(front wall, rear wall, left wall, right wall, floor, ceiling)

xs = (0.5, 0.5, 0.5)          #src pos (in meters)
xr = (Lx-0.1, Ly-0.3, Lz-0.2) #mic pos
Nt = round(Int,Fs/2)          #time samples (1/5 sec)

#h, = rim(xs,xr,L,β,Nt,Fs)
h, = rim(xs,xr,L,T60,Nt,Fs)
t = range(0; length = Nt, step = 1/Fs )

# more mics
h, = rim(xs,[(1,1,1),(1,2,1)],L,T60,Nt,Fs)

#with source signal
s = randn(Nt)   #src signal
p, = rim(s,xs,[(1,1,1),(1,1.5,1)],L,T60,Nt,Fs)

using Plots
p = plot(t,h, xlabel="Time", ylabel="Sound Pressure")


include("./FieldFactory/rayclassgrp.jl")
include("./FieldFactory/ab_exts.jl")
include("./FieldFactory/rcf_brauer.jl")


using Plots, QuadGK, ColorSchemes, ForwardDiff

# Vector e.g. [1, 4, 5] or collected Range e.g. collect(1:5)
inputstates = [0, 2, 3]
sort!(inputstates)

function constant(n)
    1/sqrt(size(inputstates, 1))
    #2/(n*pi)*(1-cos(n*pi/2))
end

function calculate(states)
    dx = 0.25
    dt = 1/30
    elapse = 10
    m = 0.2
    w = 1

    h = 1.054571817
    A = sqrt(2h*(states[end]+0.5)/w/m)+4
    xran = (-A):dx:A
    yran = 0:dx:elapse
    clrs = ColorSchemes.GnBu_9
    grad1 = cgrad(ColorSchemes.rainbow)

    Cₙ = [constant(i) for i in states]

    function GenerateHermite(n)
        Hermite=Function[]

        push!(Hermite,x->1);
        push!(Hermite,x->2*x);

        for ni in 3:n
            push!(Hermite,x->2*x*Hermite[ni-1](x)-2*(ni-1)*Hermite[ni-2](x))
        end
        return Hermite
    end

    Hermite=GenerateHermite(states[end]+1)

    Ψₙ(x) = [(m*w/pi/h)^(1/4)*Hermite[i+1](sqrt(m*w/h)*x)*exp(-m*w*x^2/2/h)/sqrt(2^i*factorial(big(i))) for i in states]
    dΨₙ(x) = ForwardDiff.derivative(Ψₙ, x)

    φₙ(t) = [exp(-im*w*t*(i+0.5)) for i in states]

    radii = [[0,abs(i)] for i in Cₙ]

    E(n)=h*w*(n+0.5)

    Eₙ = [E(i) for i in states]

    anim = @animate for ti in 0:dt:elapse

            Ψ(x) = sum(Ψₙ(x).*φₙ(ti).*Cₙ)
            dΨ(x) = sum(dΨₙ(x).*φₙ(ti).*Cₙ)

            ref = real.(Ψ.(xran))
            imf = imag.(Ψ.(xran))
            arg = vcat(atan.(ref,imf), pi, -pi)
            ρ(x) = abs2(Ψ(x))
            V(x) = m*w^2*x^2/2
            J(x) = real(im*h/(2*m)*(Ψ(x)*conj(dΨ(x))-conj(Ψ(x))*dΨ(x)))
            xΨ(x) = x*ρ(x)
            x²Ψ(x) = x^2*ρ(x)
            #expcX = first(quadgk(xΨ, -Inf, Inf))
            #expcX² = first(quadgk(x²Ψ, -Inf, Inf))
            #σₓ = sqrt(expcX²-expcX^2)

            theta = [[0,-i] for i in angle.(φₙ(ti))]

            p1 = plot(xran, imf, ref, ylims = [-1,1], zlims = [-1,1], lc = grad1, labels = false, title = "Ψ(x, t)", zlabel = "Re(Ψ)", ylabel = "Im(Ψ)", line_z = arg, legend = false, camera = [20sin(ti)+55, 25])
            p2 = plot(xran, ρ, ylims = [0,1], color = clrs[6], labels = false, title = "∣Ψ(x, t)∣²", fill = true)
            p3 = plot(theta, radii, proj =:polar, lims = [0,1], showaxis = false, labels = false, palette = [clrs[4], clrs[5], clrs[6], clrs[7], clrs[8]], title = "φ(t)")
            #scatter!(p2, [expcX], [0.5], color = clrs[5], xerror = σₓ, markerstrokecolor=clrs[8], labels = false)
            plot!(p1, xran, zeros(size(xran,1),1), zeros(size(xran,1),1), label = false, alpha = 0.1, color = :black)
            plot!(p2, V, label = "V(x)", color = clrs[5], linestyle=:dash)

            p5 = plot(xran, J, ylims = [-A, A], labels = false, title = "J(x,t)", color = clrs[6])

            l1 = @layout[a b; c{0.4h} d]

            plot(p1, p2, p3, p5, fontfamily = :serif, layout = l1)

    end

    Eplot = vcat(E(states[begin]-2), E(states[begin]-1), Eₙ, E(states[end]+1), E(states[end]+2))
    Cplot = vcat(0, 0, abs2.(Cₙ), 0, 0)
    Psi(x, t) = abs2(sum(Ψₙ(x).*φₙ(t).*Cₙ))
    l2 = @layout[a{0.3w} [b; c]]

    p4 = plot(Cplot, Eplot, xlims = [0, 1], markershapes = :circle, ylabel = "E", xlabel = "∣C∣²", labels = false, line = clrs[4], markercolors = clrs[4])
    hline!(p4, [sum(abs2.(Cₙ).*Eₙ)], color = clrs[9], label = false)
    p8 = surface(xran, yran, Psi, camera = [15, 75], seriescolor = cgrad(clrs), ylabel = "t", zlabel = "∣Ψ(x, t)∣²")
    p6 = contourf(xran, yran, Psi, seriescolor = cgrad(clrs), ylabel = "t")

    stats = plot(p4, p8, p6, layout = l2, fontfamily = :serif)

    savefig(stats, "qho1_$inputstates _stats.png")

    gif(anim, "qho1_$inputstates.gif", fps = 1/dt)
end

@time calculate(inputstates)


using ConstrainedDynamics
using ConstrainedDynamics: vrotate, Vmat, GenericJoint, AbstractBody, orthogonalrows, deepcopy
using LinearAlgebra

function Fixed_abstract(body1::AbstractBody{T}, body2; p1 = szeros(T, 3), p2 = szeros(T, 3), qoffset = one(UnitQuaternion{T})) where T
    vertices = (p1, p2)
    function gfixed(joint::GenericJoint, xa::AbstractVector, qa::UnitQuaternion, xb::AbstractVector, qb::UnitQuaternion)
       G= [
           vrotate(xb + vrotate(vertices[2], qb) - (xa + vrotate(vertices[1], qa)), inv(qa));
           Vmat(qa \ qb / qoffset)
       ]

       return G
   end
   function gfixed(joint::GenericJoint, xb::AbstractVector, qb::UnitQuaternion)
       G= [
           xb + vrotate(vertices[2], qb) - vertices[1];
           Vmat(qb / qoffset)
       ]

       return G
   end

   return GenericJoint{6}(body1, body2, gfixed)
end
function Revolute_abstract(body1::AbstractBody{T}, body2, axis; p1 = szeros(T, 3), p2 = szeros(T, 3), qoffset = one(UnitQuaternion{T})) where T
    vertices = (p1, p2)
    V1, V2, V3 = orthogonalrows(axis)
    V12 = [V1;V2]

    function grevolute(joint::GenericJoint, xa::AbstractVector, qa::UnitQuaternion, xb::AbstractVector, qb::UnitQuaternion)
       G= [
           vrotate(xb + vrotate(vertices[2], qb) - (xa + vrotate(vertices[1], qa)), inv(qa));
           V12*Vmat(qa \ qb / qoffset)
       ]

       return G
   end
   function grevolute(joint::GenericJoint, xb::AbstractVector, qb::UnitQuaternion)
       G= [
           xb + vrotate(vertices[2], qb) - vertices[1];
           V12*Vmat(qb / qoffset)
       ]

       return G
   end

   return GenericJoint{5}(body1, body2,grevolute)
end

joint_axis = [1.0;0.0;0.0]
l = 1.0
w, d = .1, .1

vert11 = [0.;0.;l / 2]
vert12 = -vert11
vert21 = [0.;0.;l / 2]

origin = Origin{Float64}()
origin_abstract = deepcopy(origin)
link1 = Box(w, d, l, l)
link1_abstract = deepcopy(link1)
link2 = deepcopy(link1)
link2_abstract = deepcopy(link1)

joint0to1 = EqualityConstraint(Fixed(origin, link1; p2=vert11))
joint1to2 = EqualityConstraint(Revolute(link1, link2,joint_axis; p1=vert12, p2=vert21))
joint0to1_abstract = EqualityConstraint(Fixed_abstract(origin_abstract, link1_abstract; p2=vert11))
joint1to2_abstract = EqualityConstraint(Revolute_abstract(link1_abstract, link2_abstract,joint_axis; p1=vert12, p2=vert21))

links = [link1;link2]
links_abstract = [link1_abstract;link2_abstract]
constraints = [joint0to1;joint1to2]
constraints_abstract = [joint0to1_abstract;joint1to2_abstract]

mech = Mechanism(origin, links, constraints)
mech_abstract= Mechanism(origin_abstract, links_abstract, constraints_abstract)

setPosition!(origin,link1,p2 = vert11)
setPosition!(origin_abstract,link1_abstract,p2 = vert11)

function compare(s1,s2)
    @test all(isapprox.(norm.(s1.x[1]-s2.x[1]), 0.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.q[1]./s2.q[1]), 1.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.x[2]-s2.x[2]), 0.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.q[2]./s2.q[2]), 1.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.ω[1]-s2.ω[1]), 0.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.v[1]-s2.v[1]), 0.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.ω[2]-s2.ω[2]), 0.0; atol = 1e-8)) &&
        all(isapprox.(norm.(s1.v[2]-s2.v[2]), 0.0; atol = 1e-8))
end

for i=1:10
    randang = pi/4*rand()
    setPosition!(link1,link2,p1 = vert12,p2 = vert21,Δq = UnitQuaternion(RotX(randang)))
    setPosition!(link1_abstract,link2_abstract,p1 = vert12,p2 = vert21,Δq = UnitQuaternion(RotX(randang)))
    storage = simulate!(mech, 1, record = true)
    storage_abstract = simulate!(mech_abstract, 1, record = true)

    compare(storage,storage_abstract)
end


@testset "Group wrapped in ArrayManifold" begin
    G = SpecialOrthogonal(3)
    M = Rotations(3)
    AG = ArrayManifold(G)
    @test base_group(AG) === G
    @test Manifolds.is_decorator_group(AG) === Val(true)

    eg = Matrix{Float64}(I, 3, 3)
    ω = [[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]
    x, y = [exp(M, eg, hat(M, eg, ωi)) for ωi in ω]
    v = hat(M, eg, [-1.0, 2.0, 0.5])

    e = Identity(AG)
    @test e === Identity(G)
    x2, y2 = ArrayMPoint(x), ArrayMPoint(y)
    v2 = ArrayTVector(v)

    @test identity(AG, x2) isa ArrayMPoint
    @test isapprox(G, identity(AG, x2).value, identity(G, x))
    @test identity(AG, e) === e
    @test_throws DomainError identity(AG, Identity(TranslationGroup(3)))

    eg = similar(x2)
    identity!(AG, eg, x2)
    @test isapprox(G, eg.value, identity(G, x))
    eg = similar(x2)
    identity!(AG, eg, e)
    @test isapprox(G, eg.value, identity(G, x))

    @test inv(AG, x2) isa ArrayMPoint
    @test isapprox(G, inv(AG, x2).value, inv(G, x))
    @test inv(AG, e) === e
    @test_throws DomainError inv(AG, Identity(TranslationGroup(3)))

    xinv = similar(x2)
    inv!(AG, xinv, x2)
    @test isapprox(G, xinv.value, inv(G, x))
    eg = similar(x2)
    inv!(AG, eg, e)
    @test isapprox(G, eg.value, e)

    @test compose(AG, x2, y2) isa ArrayMPoint
    @test isapprox(G, compose(AG, x2, y2).value, compose(G, x, y))
    @test compose(AG, x2, e) === x2
    @test compose(AG, e, x2) === x2

    xy = similar(x2)
    compose!(AG, xy, x2, y2)
    @test isapprox(G, xy.value, compose(G, x, y))
    xy = similar(x2)
    compose!(AG, xy, e, y2)
    @test isapprox(G, xy.value, compose(G, e, y))
    xy = similar(x2)
    compose!(AG, xy, x2, e)
    @test isapprox(G, xy.value, compose(G, x, e))

    for conv in (LeftAction(), RightAction())
        @test translate(AG, x2, y2, conv) isa ArrayMPoint
        @test isapprox(G, translate(AG, x2, y2, conv).value, translate(G, x, y, conv))

        xy = similar(x2)
        translate!(AG, xy, x2, y2, conv)
        @test isapprox(G, xy.value, translate(G, x, y, conv))

        @test inverse_translate(AG, x2, y2, conv) isa ArrayMPoint
        @test isapprox(G, inverse_translate(AG, x2, y2, conv).value, inverse_translate(G, x, y, conv))

        xinvy = similar(x2)
        inverse_translate!(AG, xinvy, x2, y2, conv)
        @test isapprox(G, xinvy.value, inverse_translate(G, x, y, conv))
    end

    for conv in (LeftAction(), RightAction())
        @test translate_diff(AG, y2, x2, v2, conv; atol = 1e-10) isa ArrayTVector
        @test isapprox(G, translate_diff(AG, y2, x2, v2, conv; atol = 1e-10).value, translate_diff(G, y, x, v, conv))

        vout = similar(v2)
        translate_diff!(AG, vout, y2, x2, v2, conv; atol = 1e-10)
        @test isapprox(vout.value, translate_diff(G, y, x, v, conv))

        @test inverse_translate_diff(AG, y2, x2, v2, conv; atol = 1e-10) isa ArrayTVector
        @test isapprox(G, inverse_translate_diff(AG, y2, x2, v2, conv; atol = 1e-10).value, inverse_translate_diff(G, y, x, v, conv))

        vout = similar(v2)
        inverse_translate_diff!(AG, vout, y2, x2, v2, conv; atol = 1e-10)
        @test isapprox(vout.value, inverse_translate_diff(G, y, x, v, conv))
    end
end


using DelimitedFiles

C = Vector{Matrix{Float64}}(undef, 6)
for j = 1:size(C,2)
    C[j] = readdlm("data/u$j.csv",',')
end

Fun(coefficients(C[1][:,1] + im*C[1][:,2], Chebyshev(), Legendre())


# Magic smoke starts leaking from the submarine's arithmetic logic unit (ALU).
# Without the ability to perform basic arithmetic and logic functions, the
# submarine can't produce cool patterns with its Christmas lights!
#
# It also can't navigate. Or run the oxygen system.
#
# Don't worry, though - you probably have enough oxygen left to give you enough
# time to build a new ALU.
#
# The ALU is a four-dimensional processing unit: it has integer variables w, x,
# y, and z. These variables all start with the value 0. The ALU also supports
# six instructions:
#
# - inp a - Read an input value and write it to variable a.
# - add a b - Add the value of a to the value of b, then store the result in
#   variable a.
# - mul a b - Multiply the value of a by the value of b, then store the result
#   in variable a.
# - div a b - Divide the value of a by the value of b, truncate the result to
#   an integer, then store the result in variable a. (Here, "truncate" means to
#   round the value toward zero.)
# - mod a b - Divide the value of a by the value of b, then store the remainder
#   in variable a. (This is also called the modulo operation.)
# - eql a b - If the value of a and b are equal, then store the value 1 in
#   variable a. Otherwise, store the value 0 in variable a.
#
# In all of these instructions, a and b are placeholders; a will always be the
# variable where the result of the operation is stored (one of w, x, y, or z),
# while b can be either a variable or a number. Numbers can be positive or
# negative, but will always be integers.
#
# The ALU has no jump instructions; in an ALU program, every instruction is run
# exactly once in order from top to bottom. The program halts after the last
# instruction has finished executing.
#
# (Program authors should be especially cautious; attempting to execute div
# with b=0 or attempting to execute mod with a<0 or b<=0 will cause the program
# to crash and might even damage the ALU. These operations are never intended
# in any serious ALU program.)
#
# For example, here is an ALU program which takes an input number, negates it,
# and stores it in x:
#
# inp x
# mul x -1
#
# Here is an ALU program which takes two input numbers, then sets z to 1 if the
# second input number is three times larger than the first input number, or
# sets z to 0 otherwise:
#
# inp z
# inp x
# mul z 3
# eql z x
#
# Here is an ALU program which takes a non-negative integer as input, converts
# it into binary, and stores the lowest (1's) bit in z, the second-lowest (2's)
# bit in y, the third-lowest (4's) bit in x, and the fourth-lowest (8's) bit in
# w:
#
# inp w
# add z w
# mod z 2
# div w 2
# add y w
# mod y 2
# div w 2
# add x w
# mod x 2
# div w 2
# mod w 2
#
# Once you have built a replacement ALU, you can install it in the submarine,
# which will immediately resume what it was doing when the ALU failed:
# validating the submarine's model number. To do this, the ALU will run the
# MOdel Number Automatic Detector program (MONAD, your puzzle input).
#
# Submarine model numbers are always fourteen-digit numbers consisting only of
# digits 1 through 9. The digit 0 cannot appear in a model number.
#
# When MONAD checks a hypothetical fourteen-digit model number, it uses
# fourteen separate inp instructions, each expecting a single digit of the
# model number in order of most to least significant. (So, to check the model
# number 13579246899999, you would give 1 to the first inp instruction, 3 to
# the second inp instruction, 5 to the third inp instruction, and so on.) This
# means that when operating MONAD, each input instruction should only ever be
# given an integer value of at least 1 and at most 9.
#
# Then, after MONAD has finished running all of its instructions, it will
# indicate that the model number was valid by leaving a 0 in variable z.
# However, if the model number was invalid, it will leave some other non-zero
# value in z.
#
# MONAD imposes additional, mysterious restrictions on model numbers, and
# legend says the last copy of the MONAD documentation was eaten by a tanuki.
# You'll need to figure out what MONAD does some other way.
#
# To enable as many submarine features as possible, find the largest valid
# fourteen-digit model number that contains no 0 digits. What is the largest
# model number accepted by MONAD?

# read input
program = map(eachline("input.txt")) do l
  (opcode, operands...) = split(l, ' ')
  op1 = operands[1][1]
  op2 = nothing
  if length(operands) > 1
    op2 = operands[2]
    if '0' <= op2[1] <= '9' || op2[1] == '-'
      op2 = parse(Int, op2)
    else
      op2 = op2[1]
    end
  end
  (opcode, op1, op2)
end

# location of all inp instructions
inpip = accumulate((i,_) -> findnext(((o,_,_),) -> o == "inp", program, i+1), 1:14)

# You could brute force today's puzzle by writing code to run the program with
# all possible inputs, but it will take forever to run. Instead, today's puzzle
# is mostly about figuring out what the program does. If you inspect it, you'll
# see it has 14 blocks of code that are basically identical:
#
# inp w
# mul x 0
# add x z
# mod x 26
# div z AAA
# add x BBB
# eql x w
# eql x 0
# mul y 0
# add y 25
# mul y x
# add y 1
# mul z y
# mul y 0
# add y w
# add y CCC
# mul y x
# add z y
#
# If you work that out, it becomes:
#
# w = input
# x = int((z % 26 + BBB) != w)               # always 0 or 1
# z = (z / AAA) * (25x + 1) + (w + CCC)x
#
# When BBB is positive, it's always >= 10 and AAA is always 1, so:
# * int(z % 26 + BBB != w) will always be 1 since z % 26 is always
#   non-negative, BBB >= 10, and w < 10;
# * z reduces to z = 26z + w + CCC
#
# You can think of this operation as "pushing" a digit (namely w + CCC) onto
# the end of z in base 26.
#
# When BBB is negative, AAA is always 26. AAA is 1 seven times, and 26 seven
# times. So if we're "pushing" a digit when AAA is 1, we must be popping it
# when AAA is 26. For that to be true, we want the z equation to equal exactly
# z / 26 which happens when x is zero. x is zero when (z % 26 + BBB) == w.
#
# So, basically, matching pushes with pops gives us constraints such that:
# * wpush + CCCpush == wpop - BBBpop
result = Vector{Char}(undef, 14)
stack = Vector{NTuple{2,Int}}()
sizehint!(stack, 7)
for i in 1:14
  ip = inpip[i]
  (bbb,ccc) = (program[ip+5][3], program[ip+15][3])
  if bbb > 0
    push!(stack, (i, ccc))
  else
    (j,delta) = pop!(stack)
    delta += bbb
    result[i] = min(9, 9 + delta) + '0'
    result[j] = min(9, 9 - delta) + '0'
  end
end

println("Model: ", String(result))


module MultinomialBayes
  using ...Statistics
  using StatsBase

  function getCounts(dataset)
    return Statistics.sumColumn(dataset)
  end

  function getDictionarySize(dataset)
    return size(dataset)[2]
  end

  function getClassifierCount(labels)
    return countmap(labels)
  end

  function getDictionary(data, labels)
    classifier_counts = getClassifierCount(labels)
    dictionarySize = getDictionarySize(data)
    dictionary = Dict{Integer, Array{Float64,2}}();
    dictionaryProbabilities = Dict{Integer, Array{Float64,2}}()
    for key in keys(classifier_counts)
      dictionary[key] = getCounts(data[find(x-> x==key,labels),:])
    #  println((sum(dictionary[key])))
      dictionaryProbabilities[key] = (dictionary[key].+1)./(sum(dictionary[key])+dictionarySize)
    end

    return dictionary, dictionaryProbabilities
  end

  function classifyDocuments(documents, dictionaryProb)
    predictedLabels = zeros(Float64,size(documents)[1],1)
    for i=1:size(documents)[1]
      probabilities = Dict()
      for key in keys(dictionaryProb)
        println(filter!(x->x!=0,dictionaryProb[key].^documents[i,:]))
        probabilities[key] = prod(dictionaryProb[key].^documents[i,:])
      end
      #println(probabilities)
      predictedLabels[i]=collect(keys(probabilities))[indmax(collect(values(probabilities)))]
    end

    return predictedLabels
  end

end


export NFFTOp
import Base.adjoint

mutable struct NFFTOp{T} <: AbstractLinearOperator{T}
  nrow :: Int
  ncol :: Int
  symmetric :: Bool
  hermitian :: Bool
  prod :: Function
  tprod :: Nothing
  ctprod :: Function
  nprod :: Int
  ntprod :: Int
  nctprod :: Int
  plan
  toeplitz :: Bool
end

"""
    NFFTOp(shape::Tuple, tr::Trajectory; nodes=nothing, kargs...)

generates a `NFFTOp` which evaluates the MRI Fourier signal encoding operator using the NFFT.

# Arguments:
* `shape::NTuple{D,Int64}`  - size of image to encode/reconstruct
* `tr::Trajectory`          - Trajectory with the kspace nodes to sample
* (`nodes=nothing`)         - Array containg the trajectory nodes (redundant)
* (`kargs`)                   - additional keyword arguments
"""
function NFFTOp(shape::Tuple, tr::Trajectory; nodes=nothing, toeplitz=false,
                oversamplingFactor=1.25, kernelSize=3, kargs...)
  nodes==nothing ? nodes=kspaceNodes(tr) : nothing
  # plan = NFFTPlan(nodes, shape, kernelSize, oversamplingFactor, precompute=NFFT.FULL)
  plan = plan_nfft(nodes, shape, kernelSize, oversamplingFactor, precompute=NFFT.FULL)

  return NFFTOp{ComplexF64}(size(nodes,2), prod(shape), false, false
            , x->produ(plan,x)
            , nothing
            , y->ctprodu(plan,y), 0, 0, 0, plan, toeplitz)
end

function produ(plan::NFFT.NFFTPlan, x::Vector{T}) where T<:Union{Real,Complex}
  y = nfft(plan,reshape(x[:],plan.N))
  return vec(y)
end

function ctprodu(plan::NFFT.NFFTPlan, y::Vector{T}) where T<:Union{Real,Complex}
  x = nfft_adjoint(plan, y[:])
  return vec(x)
end

function Base.copy(S::NFFTOp)
  plan = copy(S.plan)
  return NFFTOp{ComplexF64}(size(plan.x,2), prod(plan.N), false, false
         , x->produ(plan,x)
         , nothing
         , y->ctprodu(plan,y), 0, 0, 0, plan, S.toeplitz)
end

### Normal Matrix Code ###

struct NFFTNormalOp{S,D}
  shape::S
  weights::D
  fftplan
  ifftplan
  λ::Array{ComplexF64}
  xL::Matrix{ComplexF64}
end

function Base.copy(S::NFFTNormalOp)
  fftplan = plan_fft(zeros(ComplexF64, Tuple(2*collect(S.shape)));flags=FFTW.MEASURE)
  ifftplan = plan_ifft(zeros(ComplexF64, Tuple(2*collect(S.shape)));flags=FFTW.MEASURE)
  return NFFTNormalOp(S.shape, S.weights, fftplan, ifftplan, S.λ, S.xL)
end

function NFFTNormalOp(S::NFFTOp, W)
  shape = S.plan.N
  weights = W*ones(S.nrow)

  λ, ft, ift = diagonalizeOp(S.plan, weights)
  xL = zeros(ComplexF64,2*shape[1], 2*shape[2])

  return NFFTNormalOp(shape, W, ft, ift, λ, xL)
end

function SparsityOperators.normalOperator(S::NFFTOp, W=I)
  if S.toeplitz
    return NFFTNormalOp(S,W)
  else
    return NormalOp(S,W)
  end
end

function Base.:*(S::NFFTNormalOp, x::AbstractVector{T}) where T
  shape = S.shape

  # xL = zeros(T,Tuple(2*collect(shape)))
  # xL[1:shape[1],1:shape[2]] = x
  # λ = reshape(S.λ,Tuple(2*collect(shape)))

  # y = (S.ifftplan*( λ.*(S.fftplan*xL)))[1:shape[1],1:shape[2]]

  S.xL .= 0
  S.xL[1:shape[1],1:shape[2]] .= reshape(x,shape)
  λ = reshape(S.λ,Tuple(2*collect(shape)))

  y = (S.ifftplan*( λ.*(S.fftplan*S.xL)))[1:shape[1],1:shape[2]]

  return vec(y)
end

function diagonalizeOp(p::NFFT.NFFTPlan, weights=nothing)
  shape = p.N
  nodes = p.x

  # plan the FFTs
  fftplan = plan_fft(zeros(ComplexF64, Tuple(2*collect(shape)));flags=FFTW.MEASURE)
  ifftplan = plan_ifft(zeros(ComplexF64, Tuple(2*collect(shape)));flags=FFTW.MEASURE)

  # calculate first column of block Toeplitz matrix
   e1 = zeros(ComplexF64,shape)
   e1[1] = 1.
   firstCol = nfft_adjoint(p, nfft(p,e1)[:].*weights)
   firstCol = reshape(firstCol, shape)

  # calculate first rows of the leftmost Toeplitz blocks
  p2 = plan_nfft([-1,1].*nodes, shape, 3, 1.25)
  firstRow = nfft_adjoint(p2, nfft(p2,e1)[:].*weights)
  firstRow = reshape(firstRow, shape)

  # construct FT matrix of the eigentvalues
  # here a row and column in the center is filled with zeros.
  # this is done to ensure that the FFT can operate
  # on arrays with even size in each dimension
  eigMat = zeros(ComplexF64, Tuple(2*collect(shape)))
  eigMat[1:shape[1], 1:shape[2]] .= firstCol
  eigMat[shape[1]+2:end, 1:shape[2]] .= firstRow[end:-1:2,:]
  eigMat[1:shape[1], shape[2]+2:end] .= conj.(firstRow[:,end:-1:2])
  eigMat[shape[1]+2:end, shape[2]+2:end] .=  conj.(firstCol[end:-1:2,end:-1:2])

  return vec(fftplan*eigMat), fftplan, ifftplan
end

#
# calculate the matrix element A_{j,k} explicitely
#
#function getMatrixElement(j::Int, k::Int, shape::Tuple, nodes::Matrix; weights=nothing)
#  elem=0.
#  x = mod(k-1,shape[1])-mod(j-1,shape[1])
#  y = div(k-1,shape[1])-div(j-1,shape[1])
#  if weights != nothing
#    for i=1:size(nodes,2)
#      elem += exp( -2*pi*1im*(nodes[1,i]*x + nodes[2,i]*y) )*weights[i]
#    end
#  else
#    for i=1:size(nodes,2)
#      elem += exp( -2*pi*1im*(nodes[1,i]*x + nodes[2,i]*y) )
#    end
#  end
#
#  return elem
#end


### A Pluto.jl notebook ###
# v0.17.4

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
        el
    end
end

# ╔═╡ d55bc09f-90f8-4a68-8f66-49a0538c682d
begin
	using PlutoUI
	using Test
	bigbreak = HTML("
"^4)
	html""""""
end

# ╔═╡ 67910dd6-000d-4d10-9473-be3a5944cf5d
md"
# Crash-course: _Understanding_ Julia

## The origin story of Julia. 🥚 🐔

The _'two language problem'_

> Scientific computing has traditionally required the highest performance, yet domain   experts have largely moved to slower dynamic languages for daily work.

Our answer
> The Julia programming language is a **flexible dynamic language**, appropriate for scientific and numerical computing, **with performance comparable to traditional statically-typed** languages.
"

# ╔═╡ 4ef295dd-11e0-4fe2-9c11-9a0e8a27f2ab
html""""""

# ╔═╡ b54a2e64-0303-4ff6-82f4-fb3f91f9b771
html"""
In this course we will learn why Julia is fast and the basics of programming in Julia.
 """

# ╔═╡ c5a7ba98-0f68-414d-8923-2c35caac34cc
bigbreak

# ╔═╡ 9138271c-ddfe-4181-9506-ba3922593163
md"""
### We will use material from:
0. [JuliaAcademy: Introduction to Julia](https://github.com/JuliaAcademy/Introduction-to-Julia)
1. [The manual from the official Julia documentation](https://docs.julialang.org/en/v1/manual/getting-started/),
2. [The Fast Track to Julia (cheat-sheet)](https://steffenpl.github.io/Julia-Cheat-Sheet/).
4. [MATLAB–Python–Julia (cheat-sheet)](https://cheatsheets.quantecon.org/)
5. [Makie.jl Plotting function examples](https://makie.juliaplots.org/stable/examples/plotting_functions/)

!!! correct "Hint: Where to find details?"
	If you need more details on a specific topic, read the **the official documentation**! It is _actually_ **well-written** and an **excellent introduction**!

"""

# ╔═╡ e1ffc859-b8f7-4f74-aa64-c83041e5c808
bigbreak

# ╔═╡ 9a7536a5-5c0f-4514-a64a-02504b1bad46
md"""
## Outline

**Today:**
- Intro
- The Julia RELP and Juno IDE
- Essential data types
- Control flow
- Arrays and functions

**Tuesday:**
- Scope of variables
- The type system and multiple dispatch
- Custom datatypes
- The package manager
- Plotting _(with Makie.jl)_

**Wednesday:**
- Linear Algebra
- More plotting
- Interfaces
- Parametric methods and types

**Thursday:**
- Modules
- Testing
- Workflow
- ... (let's see were we end up)

"""

# ╔═╡ cf47e80d-a42d-4df0-81b0-9360b9decae9
bigbreak

# ╔═╡ 6592ddea-7126-458d-8a38-f058ab3cb366
html"""
Disclaimer: Julia is not  simply "MATLAB but faster"!
"""

# ╔═╡ bfd077aa-8c54-44b0-bafb-67cb1f8e7176
md"Even if similar on the surface, those languages are very different."

# ╔═╡ 3545e4f9-df86-4261-943b-e79a48274716
md"""
Imagine this MATLAB code (taken from the official documentation):
```julia
# MATLAB code
x = -2:0.25:2;
y = x;
[X,Y] = meshgrid(x, y);

F = X.*exp(-X.^2-Y.^2);
```
"""

# ╔═╡ 273be1ff-58b0-4dab-9726-80b1fa8aa0c1
md"Typical naive translation to Julia *could* look like this:"

# ╔═╡ 1af43c9b-151f-4b22-aa88-110ad018e0ad
meshgrid(x,y) = x * one.(y'), one.(x) * y'   # Julia does not have meshgrid function, let's just make one for this example

# ╔═╡ ae2fc32c-a471-4e64-9d33-e9e6bf677166
let
	x = -2:0.25:2
	y = x
	X, Y = meshgrid(x, y)

	F = X .* exp.( -X.^2 - Y.^2 )

	F[1:3,1:3]
end  # hide output

# ╔═╡ 3608f7cf-86e3-4cdb-ada6-46c6beb3310a
md"...but there is a small problem here:"

# ╔═╡ b6406021-00e6-419f-8e15-9f80196433eb
md"""
!!! danger "MATLAB ≠ Julia"
	Good MATLAB code is often poor Julia code. *(And vice versa!)*

However, good Julia code is often still pretty (and usually needs much less memory)!
"""

# ╔═╡ ff3539e9-296f-403d-a031-86c4df8566f7
[i+j for i in 1:10, j in 1:2]

# ╔═╡ be19363a-dfd3-430e-b1c9-f8b69309411d
let
	xs = -2:0.25:2
	ys = xs

	F = [x * exp( -x^2-y^2 ) for x in xs, y in ys]

	F[1:3,1:3]
end

# ╔═╡ 0c50f149-4cea-4793-acd2-91b58595dc3b
md"""
### 👉 Do not blindly apply MATLAB or python tips to Julia!

Check out the [Noteworthy Differences from other Languages](https://docs.julialang.org/en/v1/manual/noteworthy-differences/) if you want to see more! *(We will see some of these later.)*

Most prominent difference: There is often no reason to avoid `for` loops in Julia!
"""

# ╔═╡ a725c129-1f57-47d3-810c-bfee4f600bcf
md"""#### ☝ Important: In Julia, the first index is "1" *and not zero*.
*(For all non-MATLAB users: I feel you, but that's how it is.)*"""

# ╔═╡ 80986d60-d993-4293-bd22-72cdef15988b
bigbreak

# ╔═╡ 9bccf94a-19d1-4d31-82d5-d882b1fd1e29
md"## Task #0: How to work with Julia in the terminal (aka REPL) and the Juno IDE."

# ╔═╡ add8900c-f7a6-450e-8e04-0e7237cf80f3
@bind n PlutoUI.Slider(1:100)

# ╔═╡ 250726a2-4c56-46fe-a2b1-8f5da0dc761e
n^2

# ╔═╡ 56a560dc-dfac-43a9-afa2-a3cd6733d14f
n^5

# ╔═╡ 711dc218-0d78-4a39-a380-bc461e249ba5
bigbreak

# ╔═╡ 7bcf336b-c2dc-40ad-bb43-82fbca1c8565
md"""## Task #1: Compute 1+2 with Julia

...let's call it the mathematician's Hello World 😜
"""

# ╔═╡ e42073de-0d42-42c3-9f1a-53f2ff54b91c
bigbreak

# ╔═╡ 52fb0f4d-c1c0-4ca2-9bd1-913414fcf9c8
md"""
## Task #2: Some basic math
!!! correct "Warm-up: Fibonacci numbers"
    Compute the first 100.000 Fibonacci numbers (using a `for` loop).
"""

# ╔═╡ 20b697f4-0bf2-426b-ae3b-a3864386cc19
bigbreak

# ╔═╡ 6714a575-dc6a-4926-bcfa-349b91aca16e
md"# This looks just like MATLAB! Why is that faster?"

# ╔═╡ 5c66ea8a-ca3e-4d07-b16c-8f4015f87671
html"
The answer is not as simple as it might seem...
"

# ╔═╡ 5efe07fc-b7e4-4069-83c6-b1534c39ac99
md"""
Let's consider a simple function, like this generic `add(x,y) = x + y`.

The first time we call `add` with new input types,
the Julia **just-in-time** (JIT) compiler generates a code with *static types* and compiles that!
"""

# ╔═╡ 172cf452-791b-4d7a-934a-0dfa05374e14
add(x,y) = x+y

# ╔═╡ 8d9ca351-c8f6-49bd-8a91-2c34e883d0c4
md"For `(Int64, Int64)`"

# ╔═╡ 3675a743-1ddc-4cd4-b954-8ca354bea723
with_terminal() do
	@code_llvm debuginfo=:none add(1,2)
end

# ╔═╡ d533573e-10fd-4480-9b17-93ef3dacb91e
md"For `(Float64, Float64)`"

# ╔═╡ 2e34f342-f0a8-4e62-a4bc-6bda81c5b0c6
with_terminal() do
	@code_llvm debuginfo=:none add(1.0,2.0)
end

# ╔═╡ 6c9df6b6-746a-4721-b77b-52718abab2e5
md"Doing the same in a for loop is still fast!"

# ╔═╡ 549a0344-a442-4dfd-b01f-f1b06723df7e
function add_4_numbers(x,y)
	z = zero(eltype(x))
	@inbounds for i in 1:4
		z += x[i] + y[i]
	end
	return z
end

# ╔═╡ ab6b4c8b-0b62-4c50-bf2f-f9b969d23aff
with_terminal() do
	@code_llvm add_4_numbers([1,2,3,4],[1,2,3,5])
end

# ╔═╡ dd6dff51-8765-48da-9ccc-6fb6ca228bff
bigbreak

# ╔═╡ 299f6a19-a805-4ff2-8108-95dd94a5bb6d

# ╔═╡ 92da736b-2da2-43bb-9c8c-c661a327acc0
md"""
### Meeting Julia's JIT compiler and type stability

"""

# ╔═╡ 00000000-0000-0000-0000-000000000001
PLUTO_PROJECT_TOML_CONTENTS = """
[deps]
PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
PlutoUI = "~0.7.30"
"""

# ╔═╡ 00000000-0000-0000-0000-000000000002
PLUTO_MANIFEST_TOML_CONTENTS = """
# This file is machine-generated - editing it directly is not advised

julia_version = "1.7.1"
manifest_format = "2.0"

[[deps.AbstractPlutoDingetjes]]
deps = ["Pkg"]
git-tree-sha1 = "8eaf9f1b4921132a4cff3f36a1d9ba923b14a481"
uuid = "6e696c72-6542-2067-7265-42206c756150"
version = "1.1.4"

[[deps.ArgTools]]
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"

[[deps.Artifacts]]
uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"

[[deps.Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

[[deps.ColorTypes]]
deps = ["FixedPointNumbers", "Random"]
git-tree-sha1 = "024fe24d83e4a5bf5fc80501a314ce0d1aa35597"
uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
version = "0.11.0"

[[deps.CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"

[[deps.Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"

[[deps.Downloads]]
deps = ["ArgTools", "LibCURL", "NetworkOptions"]
uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"

[[deps.FixedPointNumbers]]
deps = ["Statistics"]
git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc"
uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
version = "0.8.4"

[[deps.Hyperscript]]
deps = ["Test"]
git-tree-sha1 = "8d511d5b81240fc8e6802386302675bdf47737b9"
uuid = "47d2ed2b-36de-50cf-bf87-49c2cf4b8b91"
version = "0.0.4"

[[deps.HypertextLiteral]]
git-tree-sha1 = "2b078b5a615c6c0396c77810d92ee8c6f470d238"
uuid = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2"
version = "0.9.3"

[[deps.IOCapture]]
deps = ["Logging", "Random"]
git-tree-sha1 = "f7be53659ab06ddc986428d3a9dcc95f6fa6705a"
uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
version = "0.2.2"

[[deps.InteractiveUtils]]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[deps.JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "8076680b162ada2a031f707ac7b4953e30667a37"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.2"

[[deps.LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21"

[[deps.LibCURL_jll]]
deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"]
uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0"

[[deps.LibGit2]]
deps = ["Base64", "NetworkOptions", "Printf", "SHA"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[deps.LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "MbedTLS_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"

[[deps.Libdl]]
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"

[[deps.LinearAlgebra]]
deps = ["Libdl", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"

[[deps.Logging]]
uuid = "56ddb016-857b-54e1-b83d-db4d58db5568"

[[deps.Markdown]]
deps = ["Base64"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"

[[deps.MbedTLS_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"

[[deps.Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[deps.MozillaCACerts_jll]]
uuid = "14a3606d-f60d-562e-9121-12d972cd8159"

[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"

[[deps.OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"

[[deps.Parsers]]
deps = ["Dates"]
git-tree-sha1 = "92f91ba9e5941fc781fecf5494ac1da87bdac775"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.2.0"

[[deps.Pkg]]
deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[deps.PlutoUI]]
deps = ["AbstractPlutoDingetjes", "Base64", "ColorTypes", "Dates", "Hyperscript", "HypertextLiteral", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "UUIDs"]
git-tree-sha1 = "5c0eb9099596090bb3215260ceca687b888a1575"
uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8"
version = "0.7.30"

[[deps.Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[[deps.REPL]]
deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"]
uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"

[[deps.Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"

[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"

[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"

[[deps.SparseArrays]]
deps = ["LinearAlgebra", "Random"]
uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[[deps.Statistics]]
deps = ["LinearAlgebra", "SparseArrays"]
uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[[deps.Tar]]
deps = ["ArgTools", "SHA"]
uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"

[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[[deps.UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[deps.Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"

[[deps.Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"

[[deps.libblastrampoline_jll]]
deps = ["Artifacts", "Libdl", "OpenBLAS_jll"]
uuid = "8e850b90-86db-534c-a0d3-1478176c7d93"

[[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

[[deps.p7zip_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
"""

# ╔═╡ Cell order:
# ╟─d55bc09f-90f8-4a68-8f66-49a0538c682d
# ╟─67910dd6-000d-4d10-9473-be3a5944cf5d
# ╟─4ef295dd-11e0-4fe2-9c11-9a0e8a27f2ab
# ╟─b54a2e64-0303-4ff6-82f4-fb3f91f9b771
# ╟─c5a7ba98-0f68-414d-8923-2c35caac34cc
# ╟─9138271c-ddfe-4181-9506-ba3922593163
# ╟─e1ffc859-b8f7-4f74-aa64-c83041e5c808
# ╟─9a7536a5-5c0f-4514-a64a-02504b1bad46
# ╟─cf47e80d-a42d-4df0-81b0-9360b9decae9
# ╟─6592ddea-7126-458d-8a38-f058ab3cb366
# ╟─bfd077aa-8c54-44b0-bafb-67cb1f8e7176
# ╟─3545e4f9-df86-4261-943b-e79a48274716
# ╟─273be1ff-58b0-4dab-9726-80b1fa8aa0c1
# ╠═1af43c9b-151f-4b22-aa88-110ad018e0ad
# ╠═ae2fc32c-a471-4e64-9d33-e9e6bf677166
# ╟─3608f7cf-86e3-4cdb-ada6-46c6beb3310a
# ╟─b6406021-00e6-419f-8e15-9f80196433eb
# ╠═ff3539e9-296f-403d-a031-86c4df8566f7
# ╠═be19363a-dfd3-430e-b1c9-f8b69309411d
# ╟─0c50f149-4cea-4793-acd2-91b58595dc3b
# ╟─a725c129-1f57-47d3-810c-bfee4f600bcf
# ╟─80986d60-d993-4293-bd22-72cdef15988b
# ╟─9bccf94a-19d1-4d31-82d5-d882b1fd1e29
# ╠═add8900c-f7a6-450e-8e04-0e7237cf80f3
# ╠═250726a2-4c56-46fe-a2b1-8f5da0dc761e
# ╠═56a560dc-dfac-43a9-afa2-a3cd6733d14f
# ╟─711dc218-0d78-4a39-a380-bc461e249ba5
# ╟─7bcf336b-c2dc-40ad-bb43-82fbca1c8565
# ╟─e42073de-0d42-42c3-9f1a-53f2ff54b91c
# ╟─52fb0f4d-c1c0-4ca2-9bd1-913414fcf9c8
# ╟─20b697f4-0bf2-426b-ae3b-a3864386cc19
# ╟─6714a575-dc6a-4926-bcfa-349b91aca16e
# ╟─5c66ea8a-ca3e-4d07-b16c-8f4015f87671
# ╟─5efe07fc-b7e4-4069-83c6-b1534c39ac99
# ╠═172cf452-791b-4d7a-934a-0dfa05374e14
# ╟─8d9ca351-c8f6-49bd-8a91-2c34e883d0c4
# ╠═3675a743-1ddc-4cd4-b954-8ca354bea723
# ╟─d533573e-10fd-4480-9b17-93ef3dacb91e
# ╠═2e34f342-f0a8-4e62-a4bc-6bda81c5b0c6
# ╠═6c9df6b6-746a-4721-b77b-52718abab2e5
# ╠═549a0344-a442-4dfd-b01f-f1b06723df7e
# ╠═ab6b4c8b-0b62-4c50-bf2f-f9b969d23aff
# ╟─dd6dff51-8765-48da-9ccc-6fb6ca228bff
# ╠═299f6a19-a805-4ff2-8108-95dd94a5bb6d
# ╟─92da736b-2da2-43bb-9c8c-c661a327acc0
# ╟─00000000-0000-0000-0000-000000000001
# ╟─00000000-0000-0000-0000-000000000002