function bisection(f, xmin::T, xmax::T; config = Options()) where {T <: Number}
    local x₀ = xmin
    local x₁ = xmax
    local x  = zero(T)

    x₀ < x₁ || begin x₀, x₁ = x₁, x₀ end

    local y₀ = f(x₀)
    local y₁ = f(x₁)
    local y  = zero(y₀)

    # y₀ * y₁ ≤ 0 || error("Either no or multiple real roots in [xmin,xmax]")

    for j in 1:config.max_iterations
        x = (x₀ + x₁) / 2
        y = f(x)

        # println("j = ", j, " , x₀ = ", x₀, " , x₁ = ", x₁)

        !isapprox(y, zero(y), atol=config.f_abstol) || break

        if y₀ * y > 0
            x₀ = x  # Root is in the right half of [x₀,x₁].
            y₀ = y
        else
            x₁ = x  # Root is in the left half of [x₀,x₁].
            y₁ = y
        end

        !isapprox(x₁ - x₀, zero(x), atol=config.x_abstol) || break
    end

    # println("α=", x, ", f(α)=", y, ", ftol=", config.f_abstol, ", abs(x₁-x₀)=", abs(x₁-x₀), ", xtol=", config.x_abstol)

    # i != ls.nmax || error("Max iteration number exceeded")

    return x
end

bisection(f, x::Number; kwargs...) = bisection(f, bracket_minimum(f, x)...; kwargs...)

"""
simple bisection line search
"""
mutable struct BisectionState{OBJ,OPT} <: LinesearchState where {OBJ <: UnivariateObjective, OPT <: Options}
    objective::OBJ
    config::OPT

    function BisectionState(objective, config)
        new{typeof(objective), typeof(config)}(objective, config)
    end
end

function BisectionState(objective::UnivariateObjective; config = Options())
    BisectionState(objective, config)
end

function BisectionState(objective::MultivariateObjective; config = Options())
    cache = LinesearchCache(objective.x_f)
    ls_objective = linesearch_objective(objective, cache)
    BisectionState(ls_objective, config)
end

Base.show(io::IO, ls::BisectionState) = print(io, "Bisection")

LinesearchState(algorithm::Bisection, objective; kwargs...) = BisectionState(objective; kwargs...)

function (ls::BisectionState)()
    bisection(ls.objective, 0., 1.; config = ls.config)
end


using Distributions
using Expectations
using StatsBase
using InformationGeometry
dist = LogNormal()
E = expectation(dist)
@show E(x->x^2)
dist1 = Gamma()
E1 = expectation(dist1)
@show E1(x->x^2)

crossentropy([0.2, 0.3, 0.5], [0.3, 0.4, 0.3])

lognormalpdf = x -> pdf(dist,x)

loggammapdf = x -> pdf(dist1,x)

E(x -> lognormalpdf(x))

A = rand(dist,1000)

B = rand(dist1,1000)

dist2 = Normal(1,3)

dist3 = Normal(5,2)

C = kldivergence(dist2,dist3)
KullbackLeibler(dist2,dist3)

D = kldivergence(dist1,dist)
KullbackLeibler(dist,dist3)

E1(x -> log(lognormalpdf(x)))- E1(x -> log(loggammapdf(x)))

KullbackLeibler(x->loggammapdf(x),y->lognormalpdf(y),HyperCube([-20,20]); Carlo=true, N=Int(3e6))

KullbackLeibler(x->pdf(Normal(1,3),x),y->pdf(Normal(5,2),y),HyperCube([-20,20]); Carlo=true, N=Int(3e6))
KullbackLeibler(Normal(1,3),Normal(5,2))


function NewEst3Ps(g0,m)
#NewEst3Ps    Calculates covariance matrix of sqrt(T)*sample average.
#
#
#  Usage:     Shat = NewEst3Ps(hhat,m)
#
#  Input:     g0          TxK matrix of moment functions
#             m           order of autoregression
#
#  Output:    Shat        covariance matrix of sum( hhat/sqrt(T) ),
#                         that is the covariance matrix of sqrt(T)*mean(hhat)
#
#
#  Note: The CLT typically say that sqrt(T)*sample average ->d N(mu,Shat),
#        where Shat is what this code estimates. Clearly, Var(sample average) =
#        Shat/T.
#
#
#  Reference: Newey and West, 1987, Econometrica
#             Newey, 1985, Journal of Econometrics
#
#
#  Paul Soderlind (Paul.Soderlind@unisg.ch), to Julia Oct 2015
#-----------------------------------------------------------------------

  T = size(g0,1)                     #g is Txq
  m = min(m,T-1)                     #number of lags

  g = g0 .- mean(g0,1)               #Normalizing to Eg=0

  S = g'g/T                          #(qxT)*(Txq)
  for s = 1:m
    Omega_s = g[s+1:T,:]'g[1:T-s,:]/T   #same as Sum[g(t)*g(t-s)',t=s+1,T]
    S       = S + (1 - s/(m+1))*(Omega_s + Omega_s')
  end

  return S

end
#-----------------------------------------------------------------------


module DatasetsSynthetic

using Random
export make_blobs, make_circles, make_moons

uniform_sample_in_zero_maxval(p, maxval) = maxval .* (1 .- rand(p))
uniform_sample_in_minval_maxval(p, minval, maxval) = (maxval-minval) .* rand(p) .+ minval
uniform_sample_in_minval_maxval(n, p, minval, maxval) = (maxval-minval) .* rand(n, p) .+ minval
normal_sample(p, mu, var) = mu .+ sqrt(var) .* randn(p)

"""
Shuffles the rows of an Array `X` and the values of a vector `y` using a randomly
generated permutation. The same permutation is used to shuffle both `X` and `y`.
"""
function shuffle_Xy(X, y; random_seed=Random.GLOBAL_RNG)
    Random.seed!(random_seed)
    perm = randperm(length(y))
    return X[perm,:], y[perm]
end

"""
make_blobs(n::Int=100;
           p::Int=2,
           centers::Int=3,
           cluster_std=1.0,
           center_box=(-10.,10.),
           element_type=Float64,
           random_seed=Random.GLOBAL_RNG,
           return_centers=false)

Generates a dataset with `n` examples of dimension `p` and returns a vector containing
as integers the membership of the different points generated.

The data is roughly grouped around several `centers`  which are created using `cluster_std`.
The data lives inside `center_box` in the case it is randomly generated.

- If `centers` is an integer the centroids are created randomly.
- If `centers` is an Array containing points the centroids are picked from `centers`.
- If `return_centers=true` the centroids of the blobs are returned.
"""
function make_blobs(n::Int=100; p::Int=2,
                    shuffle::Bool=false, centers::Int=3, cluster_std::Real=1.0, center_box=(-10.,10.),
                    element_type=Float64, random_seed=Random.GLOBAL_RNG, return_centers=false, verbose=0)

    Random.seed!(random_seed)
    X = zeros(n, p)
    y = zeros(n)

    if typeof(centers) <: Int
        n_centers = centers
        center_sample = uniform_sample_in_minval_maxval(n, p, center_box[1], center_box[2])
    else
        n_centers = length(centers)
    end

    if typeof(cluster_std) <: AbstractFloat
        cluster_std = cluster_std * randn(n_centers)
    end

    # generates div(n, n_centers) examples assigned to each center
    n_per_center = [div(n, n_centers) for x  in 1:n_centers]

    # adds the reamainding examples to each center up to n
    n_per_center = fill(div(n,n_centers), n_centers)
    n_per_center[end] += rem(n,n_centers)

    # generates the actual vectors close to each center blob
    start_ind = 1
    for (i, (n_blob, std, center)) in enumerate(zip(n_per_center, cluster_std, centers))
        ind_center = start_ind:(start_ind + n_per_center[i]-1)
        X[ind_center,:] .= center' .+ std .* randn(element_type, (n_per_center[i], p));
        y[ind_center] .= i
        if verbose>0
            println("center $i with $(n_per_center[i]) points created")
        end
        start_ind += n_per_center[i]
    end

    if shuffle
       X, y = shuffle_Xy(X, y ; random_seed=random_seed)
    end

    if return_centers
        return X, y, centers
    else
        return X, y
    end
end

"""
make_circles(n::Int=100; shuffle::Bool=true, noise::Number=0., random_seed=Random.GLOBAL_RNG, factor::Number=0.8)

Generates a dataset with `n` bi-dimensional examples. Samples are created
from two circles. One of the circles inside the other. The `noise` scalar
can be used to add noise to the generation process. The scalar `factor`
corresponds to the radius of the smallest circle.
"""
function make_circles(n::Int=100; shuffle::Bool=true, noise::Number=0., random_seed=Random.GLOBAL_RNG, factor::Number=0.8)

    Random.seed!(random_seed)

    n_out = div(n, 2)
    n_in = n - n_out

    linrange_out = Array(LinRange(0, 2 * pi, n_out))
    linrange_in  = Array(LinRange(0, 2 * pi, n_in))

    outer_circ_x = cos.(linrange_out)
    outer_circ_y = sin.(linrange_out)
    inner_circ_x = cos.(linrange_in) .* factor
    inner_circ_y = sin.(linrange_in) .* factor

    X = hcat(vcat(outer_circ_x, inner_circ_x), vcat(outer_circ_y, inner_circ_y))
    y = vcat(ones(Int,n_out), 2*ones(Int,n_in))

    if shuffle
       X, y = shuffle_Xy(X, y ; random_seed=random_seed)
    end

    X .+= noise .* rand(n, 2)

    return X, y
end

"""
make_moons(n::Int=100; shuffle::Bool=true, noise::Number=0.,
           translation::Number=0.5, factor::Number=1.0, random_seed=Random.GLOBAL_RNG)

Generates `n` examples sampling from two moons. The `noise` can be changed to add
noise to the samples.
"""
function make_moons(n::Int=100; shuffle::Bool=true, noise::Number=0.,
                   translation::Number=0.5, factor::Number=1.0, random_seed=Random.GLOBAL_RNG)

    Random.seed!(random_seed)

    n_out = div(n, 2)
    n_in = n - n_out

    linrange_out = Array(LinRange(0, pi, n_out))
    linrange_in  = Array(LinRange(0, pi, n_in))

    outer_circ_x = cos.(linrange_out)
    outer_circ_y = sin.(linrange_out)
    inner_circ_x = 1 .- cos.(linrange_in) .* factor
    inner_circ_y = 1 .- sin.(linrange_in) .* factor .- translation

    X = hcat(vcat(outer_circ_x, inner_circ_x), vcat(outer_circ_y, inner_circ_y))
    y = vcat(ones(Int,n_out), 2*ones(Int,n_in))

    if shuffle
       X, y = shuffle_Xy(X, y ; random_seed=random_seed)
    end

    X .+= noise .* rand(n, 2)

    return X, y
end

end #module


abstract type AbstractDiscreteMarkovChain <: AbstractMarkovChain end

"""
    DiscreteMarkovChain(transition_matrix)
    DiscreteMarkovChain(state_space, transition_matrix)
    DiscreteMarkovChain(continuous_markov_chain)

Creates a new discrete Markov chain object.

# Arguments
- `state_space`: The names of the states that make up the Markov chain.
- `transition_matrix`: The single step transition probability matrix.
- `continuous_markov_chain`: An instance of `ContinuousMarkovChain`.

# Examples
The following shows a basic Sunny-Cloudy-Rainy weather model.
```jldoctest DiscreteMarkovChain
using DiscreteMarkovChains
T = [
    0.9 0.1 0;
    0.5 0.2 0.3;
    0.1 0.4 0.5
]
X = DiscreteMarkovChain(["Sunny", "Cloudy", "Rainy"], T)
println(state_space(X))

# output

["Sunny", "Cloudy", "Rainy"]
```

```jldoctest DiscreteMarkovChain
println(transition_matrix(X))

# output

[0.9 0.1 0.0; 0.5 0.2 0.3; 0.1 0.4 0.5]
```

# References
1. [Wikipedia](https://en.wikipedia.org/wiki/Markov_chain#Discrete-time_Markov_chain)
2. [Dartmouth College](https://www.dartmouth.edu/~chance/teaching_aids/books_articles/probability_book/Chapter11.pdf)
"""
struct DiscreteMarkovChain <: AbstractDiscreteMarkovChain
    state_space
    transition_matrix
    function DiscreteMarkovChain(state_space, transition_matrix)
        check(state_space, transition_matrix, DiscreteMarkovChain)
        new(state_space, transition_matrix)
    end
end
"""
    required_row_sum(type)

# Arguments
- `type`: The type of Markov chain. It can be
`DiscreteMarkovChain` or `ContinuousMarkovChain`.

# Returns
The number that each row in the transition matrix should sum up to.
"""
required_row_sum(::Type{<:AbstractDiscreteMarkovChain}) = 1
required_row_sum(::Type{<:AbstractDiscreteMarkovChain}, ::Type{T}) where T = one(T)
function DiscreteMarkovChain(transition_matrix)
    return DiscreteMarkovChain(1:(size(transition_matrix)[1]), transition_matrix)
end

"""
    characteristic_matrix(::AbstractDiscreteMarkovChain)
    characteristic_matrix(::AbstractContinuousMarkovChain)

# Definitions
Many derivations and interesting ideas about Markov chains
involve the identity matrix or zero matrix somewhere along
the line. Most of the time, the identity matrix appears
more often in discrete Markov chains. Instead of the identity
matrix, the zero matrix appears in its place for continuous
Markov chains.

# Returns
The identity matrix if its argument is an instance of
`AbstractDiscreteMarkovChain`. The zero matrix if its
argument is an instance of `AbstractContinuousMarkovChain`
"""
characteristic_matrix(::AbstractDiscreteMarkovChain) = LinearAlgebra.I

"""
    periodicities(x::AbstractDiscreteMarkovChain)

A more advanced version of `communication_classes`
designed for discrete Markov chains. It is the same as
`communication_classes` but it returns periodicities as well.

# Definitions
The period, ``d_i`` of a state ``i`` is the greatest common denominator
of all integers ``n ∈ \\mathbb{N}`` for which ``p^{(n)}_{i,i} > 0``.
Written more succinctly,
```math
d_i = \\text{gcd}\\{ n ∈ \\mathbb{N} | p^{(n)}_{i,i} > 0 \\}
```
If ``d_i=1`` then state ``i`` is said to be aperiodic.

# Arguments
- `x`: some kind of discrete Markov chain.

# Returns
A tuple containing 3 arrays.
- This first array contains C arrays which store the states that communicate.
- The second array is an array of Bool where the ith value is true if the
  ith communication class is recurrent.
- The third array is the periodicity of each communication class.

# Examples
```jldoctest periodicities
using DiscreteMarkovChains
T = [
    1 0;
    0 1;
]
X = DiscreteMarkovChain(T)

periodicities(X)

# output

([[1], [2]], Any[true, true], Any[1, 1])
```

So the Markov chain has two communication classes and both are recurrent and aperiodic.

```jldoctest periodicities
T = [
    0.0 1.0 0.0;
    1.0 0.0 0.0;
    0.1 0.2 0.7;
]
X = DiscreteMarkovChain(["Sunny", "Cloudy", "Rainy"], T)

periodicities(X)

# output

([["Sunny", "Cloudy"], ["Rainy"]], Any[true, false], Any[2, 1])
```

So the Sunny and Cloudy states communicate and are recurrent with period 2.
The Rainy state is transient and aperiodic.
Note that this is not a very good weather model since once it
stops raining, it will never rain again. Also, each day after that,
the process will oscillate between Sunny and Cloudy.
"""
function periodicities(x::AbstractDiscreteMarkovChain)
    SI = state_index(x)
    T = transition_matrix(x)

    classes, recurrence = communication_classes(x)
    index_classes = [[SI[state] for state in class] for class in classes]

    periods = []
    for class in index_classes
        submatrix = T[class, class]
        push!(periods, breadth_first_search(submatrix))
    end
    return classes, recurrence, periods
end

"""
    is_regular(x)

# Definitions
A Markov chain is called a regular chain if some power of the
transition matrix has only positive elements. This is equivalent
to being ergodic and aperiodic.

# Arguments
- `x`: some kind of discrete Markov chain.

# Returns
`true` if the Markov chain, `x`, is regular.

# Examples
We will set up a matrix with 2 communication classes and
show that it is not regular.

```jldoctest is_regular
using DiscreteMarkovChains
T = [
    0 1 0;
    1 0 0;
    0 0 1;
]
X = DiscreteMarkovChain(T)

is_regular(X)

# output

false
```

Repeat the above but now all states communicate.

```jldoctest is_regular
T = [
    0.0 0.5 0.5;
    0.0 0.0 1.0;
    1.0 0.0 0.0;
]
X = DiscreteMarkovChain(T)

is_regular(X)

# output

true
```

Notice how a periodic chain is not regular even though
there is only one communication class.

```jldoctest is_regular
T = [
    0 1 0;
    0 0 1;
    1 0 0;
]
X = DiscreteMarkovChain(T)

is_regular(X)

# output

false
```
"""
function is_regular(x::AbstractDiscreteMarkovChain)
    classes, _, periods = periodicities(x)
    if length(classes) == 0
        return false
    end
    return (length(classes) == 1) & (periods[1] == 1)
end

"""
    first_passage_probabilities(x, t, i=missing, j=missing)

# Definitions
This is the probability that the process enters state ``j``
for the first time at time ``t`` given that the process started
in state ``i`` at time 0. That is, ``f^{(t)}_{i,j}``. If no `i`
or `j` is given, then it will return a matrix instead with
entries ``f^{(t)}_{i,j}`` for `i` and `j` in the state space of `x`.

# Why Do We Use A Slow Algorithm?
So that `t` can be symbolic if nessesary. That is, if symbolic math
libraries want to use this library, it will pose no hassle.

# Arguments
- `x`: some kind of Markov chain.
- `t`: the time to calculate the first passage probability.
- `i`: the state that the prcess starts in.
- `j`: the state that the process must reach for the first time.

# Returns
A scalar value or a matrix depending on whether `i` and `j` are given.

# Examples
```jldoctest first_passage_probabilities
using DiscreteMarkovChains
T = [
    0.1 0.9;
    0.3 0.7;
]
X = DiscreteMarkovChain(T)

first_passage_probabilities(X, 2)

# output

2×2 Array{Float64,2}:
 0.27  0.09
 0.21  0.27
```

If `X` has a custom state space,
then `i` and `j` must be in that state space.

```jldoctest first_passage_probabilities
T = [
    0.1 0.9;
    0.3 0.7;
]
X = DiscreteMarkovChain(["Sunny", "Rainy"], T)

first_passage_probabilities(X, 2, "Sunny", "Rainy")

# output

0.09000000000000001
```

Notice how this is the (1, 2) entry in the first example.

# References
1. [University of Windsor](https://scholar.uwindsor.ca/cgi/viewcontent.cgi?article=1125&context=major-papers)
2. [Durham University](http://maths.dur.ac.uk/stats/courses/ProbMC2H/_files/handouts/1516MarkovChains2H.pdf)
"""
function first_passage_probabilities(
    x::AbstractDiscreteMarkovChain, t, i=missing, j=missing
)
    S = state_space(x)
    T = transition_matrix(x)
    n = length(S)

    if n == 0
        return transition_matrix(x)
    end

    js = 1:n  # The columns to loop through
    calc_i_ne_j = true  # Calculate the off-diagonals
    calc_i_eq_j = true  # Calculate the diagonals
    if (i !== missing) && (j !== missing)
        i = state_index(x)[i]
        j = state_index(x)[j]
        js = [j]
        if i == j
            calc_i_ne_j = false
        else
            calc_i_eq_j = false
        end
    end

    Ft = zeros(eltype(T), n, n)  # Empty matrix

    # If i != j
    if calc_i_ne_j
        for j in js

            P0 = copy(T)
            P0[1:n, j] = zeros(eltype(T), n, 1)
            F = P0^(t-1) * T
            Ft[1:n, j] = F[1:n, j]
        end
    end

    # If i == j
    if calc_i_eq_j
        for j in js

            P_ = copy(T)
            P_[j, 1:n] = zeros(eltype(T), 1, n)

            Pnew = zeros(eltype(T), 2*n, 2*n)
            Pnew[1:n, 1:n] = T
            Pnew[(n+1):(2*n), (n+1):(2*n)] = P_
            Pnew[n+j, 1:n] = T[j, 1:n]

            P0 = copy(Pnew)
            P0[1:(2*n), j] = zeros(eltype(T), 2*n, 1)

            F = P0^(t - 1) * Pnew

            Ft[j, j] = F[n+j, j]
        end
    end

    if (i !== missing) && (j !== missing)
        return Ft[i, j]
    end
    return Ft
end


#
# Function that computes a moving average to smooth plots
#

function movingaverage( x :: Vector, n :: Int )

  if ! isodd(n)
    n = n + 1
  end
  delta = round(Int64,(n-1)/2)

  nx = length(x)
  y = similar(x)

  for i in 1:nx
    y[i] = 0.
    jmin = max(i-delta,1)
    jmax = min(i+delta,nx)
    for j in jmin:jmax
      y[i] = y[i] + x[j]
    end
    y[i] = y[i] / (jmax-jmin+1)

  end

  return y

end


# Plots power spectrum components of approximate Gaussian PSD:
using PyPlot

#nc = 4
#amp = [2.953823582534889, -0.04961614737314587, -1.6900719772140012, -0.21413545794774125]
#b =[1.589510614489265, 3.4729345651560872, 1.8698492189074047, 6.363451205766626]
#omega =[0.3329119766576696, 4.5999414581901386,1.7115890219170917, 2.205939216106191]
#amp = [2.953823582534889, -0.04961614737314587, -0.21413545794774125, -1.690719772140012]
#b =[1.589510614489265, 3.4729345651560872, 6.363451205766626, 1.8698492189074047]
#omega =[0.3329119766576696, 4.5999414581901386, 2.205939216106191,1.1115890219170917]

nc = 3
amp = [2.670143,-0.458705,-1.211438]
b = [1.500258,4.125558,1.744602]
omega = [0.346731,1.390912,1.798436]

nf = 100
f = 2*pi*logspace(-2,2,nf)
psdtot =  zeros(nf)
for i=1:nc
  psd = amp[i]*b[i]*sqrt(2./pi)*(b[i]^2+omega[i]^2+f.^2)./(b[i]^4+(omega[i]^2-f.^2).^2+2.*b[i]^2.*(omega[i]^2+f[i]^2))
  loglog(f,abs(psd))
  psdtot += psd
  loglog(f,psdtot,linestyle="dashed")
  read(STDIN,Char)
end

#plot(f,psdtot)


#=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=#
# Problem set 2 solutions
# Written by Tyler Ransom
# Commented by Giuseppe Grasso
#=~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~=#

using Random
using LinearAlgebra
using Statistics
using Optim
using DataFrames
using CSV
using HTTP
using GLM
using FreqTables
using ForwardDiff # for bonus at the very end
cd("/Users/peppegrass/Documents/GitHub/fall-2020/ProblemSets/PS2-optimization-intro/") # GG: sets working directory
pwd() ## GG: prints working directory
readdir() # GG: equivalent to -ls- to see elements of working directory

function allwrap()
    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # Question 1
    # Basic [# GG: NON-LINEAR] optimization in Julia
    #:::::::::::::::::::::::::::::::::::::::::::::::::::

    #=
    GG: We will use Julia’s Optim package, which is a function MINIMIZER.
    Thus, if we want to ﬁnd the maximum of f(x), we need to minimize −f(x).
    GG: Run code below in REPL to see how Optim works
    =#

    f(x) = -x[1]^4-10x[1]^3-2x[1]^2-3x[1]-2 # GG: original objective function
    minusf(x) = x[1]^4+10x[1]^3+2x[1]^2+3x[1]+2 # GG: -(obj fct)
    startval = rand(1)   # random starting value
    #=
    GG: The Optim package provides a function called optimize(). This function requires three inputs:
    the objective function, a starting value, and an optimization algorithm.
    We will not get too deep into optimization algorithms in this course, but for now just use LBFGS().
    =#
    result = optimize(minusf, startval, BFGS()) # GG: BFGS is a gradient-based algorithm: objective function needs to be differentiable for this to work; otherwise, use different algorithm
    println(result)

    #=
    GG: Look at the measures of convergence in the result:
    3 different criteria when doing non-linear optimization. Metaphor: climing a mountain and looking at ALTITUDE & LATITUDE/LONGITUDE
    1. Look at g(x): Most intuitive and common one: is the gradient 0 [necessary condition: FOC]?
    2. Look at |f(x) - f(x')|: Since the last iteration of this procedure (with the first iteration being originally based on the starting value), how much has the FUNCTION VALUE changed (i.e. has the ALTITUDE changed) between the previous or current iteration? Is there evidence for the slope not being at zero?
    3. Look at |x - x'|: How far did I step from the last iteration to this iteration, i.e. how did the value x (current) change wrt to x' (previous)? How much did LATITUDE/LONGITUDE change
        # GG: gradient 1. is the most important and also hardest to satisfy
        Tolerance values/thresholds can be set for gradient (gtol/gval), function (ftol/fval) and value (xtol/xval); e.g. e-06 as opposed to e-08
    =#

    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # Question 2
    # Now that we’re familiar with how Optim’s optimize() function works, lets try it on some real-world data.
    # Speciﬁcally, let’s use Optim to compute OLS estimates of a simple LINEAR REGRESSION using actual data.
    # The process for passing data to Optim can be tricky, so it will be helpful to go through this example.
    # First, let’s import and set up the data. Note that you will need to put the URL all on one line when executing this code in Julia.
    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    url = "https://raw.githubusercontent.com/OU-PhD-Econometrics/fall-2020/master/ProblemSets/PS1-julia-intro/nlsw88.csv" # GG: setting URL
    df = CSV.read(HTTP.get(url).body) # GG: reading CSV from URL
    X = [ones(size(df,1),1) df.age df.race.==1 df.collgrad.==1] # GG: subset of variables that will be used in regression
    y = df.married.==1 # GG: copying variable married from DataFrame df into array y
    # GG: By applying OLS to a binary choice model we are estimating a Linear Probability Model (LPM)

    # GG. A tricky thing with using Optim is that it requires something called a closure to be able to pass data into the function.
    # GG: OLS is minimizer of Sum of Squared Errors (SSR) so we write down the objective function
    function ols(beta, X, y) # GG: argument you are optimizing over has to come first in Optim; others (data) can come later
        ssr = (y.-X*beta)'*(y.-X*beta) # GG: one way of writing it (.- is called loop fusion; should be the most efficient)
        #= ssr = dot(y.-X*beta,y.-X*beta) Julia function dot() that takes dot product (an alternative) =#
        #= ssr = [sum[y[i]-X[i,:]*beta]^2 for i in 1:length(y)] (yet another alternative by comprehension; might contain mistakes; pseudo-code) =#
        #=  ssr = 0
            for i=1:length(y)
                ssr += (y[i]-X[i,:]*beta)^2
            end
        (yet another alternative using a loop; += is an operator to do iterations)
        =#
        return ssr
    end

    beta_hat_ols = optimize(b -> ols(b, X, y), rand(size(X,2)), LBFGS(), Optim.Options(g_tol=1e-6, iterations=100_000, show_trace=true))
    #= GG:
    - In exercise 1 we simply used optimize(minusf, ., .) because we had a univariate function of x
    Now here we got 3 arguments and we need to tell Julia that we are optimizing over b by using the following synthax
    optimize(b -> ols(b, X, y), ...)
    - rand(size(X,2)) are starting value which must have the same dimension as the parameter vector (or dim covariate vector including the constant)
    - LBFGS() is the algorithm chosen
    - Setting a few options through Optim.Options(g_tol=1e-6, iterations=100_000, show_trace=true)
        - tolerance value for gradient; capping number of iterations at 100k; show_trace=true prints out in the REPL what's happening at every step
    =#

    println(beta_hat_ols.minimizer) # GG: prints the output

    # GG: Since OLS has a closed form solution, we can check that this worked in a few different ways:
    # 1. by evaluating the known OLS closed form
    bols = inv(X'*X)*X'*y
    println(bols)
    # 2. by using the GLS package; very similar to R
    df.white = df.race.==1 # GG: Creating a dummy white for race==1
    bols_lm = lm(@formula(married ~ age + white + collgrad), df) # GG: like in R, once you tell where data is coming from, you can just use varnames stata-style
    println(bols_lm)
    # GG: It can be easily verified that all three solutions are the same

    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # Question 3
    # Use Optim to estimate the LOGIT LIKELIHOOD. Some things to keep in mind:
    #   To maximize the likelihood, you will need to pass Optim the negative of the likelihood function
    #   The likelihood function is included in the Lecture 4 slides
    #:::::::::::::::::::::::::::::::::::::::::::::::::::

    #=
    GG: Same principle but this time we apply it to MAXIMUM LIKELIHOOD estimation of a BINARY LOGIT model
    In previous exercise we programmed OLS; here we program logit
    =#
    function logit(alpha, X, y)

        P = exp.(X*alpha)./(1 .+ exp.(X*alpha)) # GG: choice probability. Using dots to vectorize operation, i.e. exp() applied to all elements of X*alpha; same for other operations

        loglike = -sum( (y.==1).*log.(P) .+ (y.==0).*log.(1 .- P) ) # GG: this is the LOGIT log-likelihood (times -1 coz of Optim is min fct); (y.==1) and (y.==0) are a booleans

        return loglike
    end
    alpha_hat_optim = optimize(a -> logit(a, X, y), rand(size(X,2)), LBFGS(), Optim.Options(g_tol=1e-6, iterations=100_000, show_trace=true))
    println(alpha_hat_optim.minimizer)
    # GG: as we saw in OLS, here we're optimizing wrt a, so inside optimize we use a -> logit(a, X, y)
    # GG: rand(size(X,2)) gives a vector the dimension of X of uniform 0,1 random numbers

    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # Question 4
    # Use the glm() function from the GLM package to check your answer. (Example code for how to do this is in the Lecture 3 slides.)
    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    alpha_hat_glm = glm(@formula(married ~ age + white + collgrad), df, Binomial(), LogitLink())
    println(alpha_hat_glm)

    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # Question 5
    # Use Optim to estimate a MULTINOMIAL LOGIT MODEL where the dependent variable is occupation and the covariates are the same as above.
    # Before doing this, clean the data to remove rows where occupation is missing.
    # We also need to aggregate some of the occupation categories or else we won’t be able to estimate our multinomial logit model:
    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    freqtable(df, :occupation) # note small number of obs in some occupations
    df = dropmissing(df, :occupation) # GG: dropping observations with missing occupation and overwriting df
    df[df.occupation.==8 ,:occupation] .= 7 # GG: recoding all values >7 to 7
    df[df.occupation.==9 ,:occupation] .= 7
    df[df.occupation.==10,:occupation] .= 7
    df[df.occupation.==11,:occupation] .= 7
    df[df.occupation.==12,:occupation] .= 7
    df[df.occupation.==13,:occupation] .= 7
    freqtable(df, :occupation) # problem solved
    # GG: Since we changed the number of rows of df, we also need to re-deﬁne our X and y objects:
    X = [ones(size(df,1),1) df.age df.race.==1 df.collgrad.==1]
    y = df.occupation

    #=
    GG: Hints:

    # With 7 choice alternatives, you will have K · 6 coefﬁcients, where K is the number of covariates in X.
    It may help to transform the parameter vector into a K ×6 matrix (to more easily reference the α j ’s for each j)

    # You should reset the tolerance of the gradient (g tol) to be 10 to the −5.
    This will help the estimation converge more quickly, without losing too much precision. Otherwise, it would take forever.

    # You may need to try different sets of starting values. Some candidates to consider are:
    - a vector of 0s
    - a vector of U[0,1] random numbers
    - a vector of U[−1,1] random numbers
    - the estimated values from Stata or R (see Stata example below)
        webuse nlsw88
        drop if mi(occupation)
        recode occupation (8 9 10 11 12 13 = 7)
        gen white = race==1
        mlogit occupation age white collgrad, base(7)
    In general it is a good strategy to run your model(s) through a more user-friendly interface like Stata or R before trying to implement them in Julia.
    But you might ask, “Why don’t we just use Stata or R, then?”
    The reason is because the models we will get to later in the course are much more difﬁcult to implement in those languages, because they can’t just be taken off the shelf.

    We can readapt the code from the binary logit above. We gotta readapt to the fact that we have J choices and KxJ parameters
    Functional form of choice probabilities and log-likelihood function is similar.
    So I could take loglike = -sum( (y.==1).*log.(P) .+ (y.==0).*log.(1 .- P) ) and readapt it as follows
    loglike = -sum( (y.==1).*log.(P1) .+ (y.==2).*log.(P2) .+ ...)
    or I can do it in a LOOP (which is adviced in that it allows to reduce mistakes)
    =#

    function mlogit(alpha, X, y) # GG: mlogit can oly be fed alpha as a vector, not as a matrix

        # GG: Defining K, J, N (not always a good idea to make extra allocations but in this case it helps understand what's what)
        K = size(X,2)
        J = length(unique(y))
        N = length(y)
        bigY = zeros(N,J) # GG: Initializing an NxJ matrix for the Ys. Looping over the number of occupation choices columns and creating a bunch of 1-0 columns (dummies) in the bigY matrix
        for j=1:J
            bigY[:,j] = y.==j
        end
        bigAlpha = [reshape(alpha,K,J-1) zeros(K)] # GG: reshaping alpha vector into a matrix in a way that is conformable to bigY (hence the zeroes at the end, reflecting the normalization of the parameters for the last occupation)

        # GG: need to compute numerator and denominator of choice probabilities. For MNL num changes for each of them; den doesn't.
        num = zeros(N,J)
        dem = zeros(N)
        for j=1:J
            num[:,j] = exp.(X*bigAlpha[:,j]) # GG: changes for each numerator; first one normalized at 1 by exploiting exp(0)
            dem .+= num[:,j] # GG: iterated sum of all numerators; .+= is vectorized iterative sum
        end

        # GG: and there you go, this is the choice probability;
        P = num./repeat(dem,1,J) # GG: the denominator is the same for all alternatives, but we need to make it conformable, so we repeat the denominator J times in a way that is conformable to the numerator
        # GG: repeat(dem,1,J) is a matrix the same size as the numerator (so that ./ can worl properly), repeated J times

        loglike = -sum( bigY.*log.(P) ) # GG: here we need to do a double summation (over i and j); Julia understands to do sum over the rows and over the columns
        # GG: but we could have said sum( sum( bigY.*log.(P) ; dims=2); dims=1)

        return loglike
    end

    # GG: different options for starting values; stata answers multiplied by 0-1 uniform random numbers appear to be the best starting values
    alpha_zero = zeros(6*size(X,2))
    alpha_rand = rand(6*size(X,2))
    alpha_true = [.1910213,-.0335262,.5963968,.4165052,-.1698368,-.0359784,1.30684,-.430997,.6894727,-.0104578,.5231634,-1.492475,-2.26748,-.0053001,1.391402,-.9849661,-1.398468,-.0142969,-.0176531,-1.495123,.2454891,-.0067267,-.5382892,-3.78975]
    alpha_start = alpha_true.*rand(size(alpha_true))
    println(size(alpha_true))
    alpha_hat_optim = optimize(a -> mlogit(a, X, y), alpha_start, LBFGS(), Optim.Options(g_tol = 1e-5, iterations=100_000, show_trace=true, show_every=50))
    alpha_hat_mle = alpha_hat_optim.minimizer
    println(alpha_hat_mle)

    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # BONUS: how to get STANDARD ERRORS?
    # Need to adjust objective function sligthly, i.e. need to obtain the hessian of the obj fun
    #:::::::::::::::::::::::::::::::::::::::::::::::::::
    # TR: first, we need to slightly modify our objective function
    function mlogit_for_h(alpha, X, y)

        K = size(X,2)
        J = length(unique(y))
        N = length(y)
        bigY = zeros(N,J)
        for j=1:J
            bigY[:,j] = y.==j
        end
        bigAlpha = [reshape(alpha,K,J-1) zeros(K)]

        T = promote_type(eltype(X),eltype(alpha)) # TR: this line is new; GG: becuase types matter; gotta be the same
        num   = zeros(T,N,J)                      # TR: this line is new; GG: you specify type T before specifying dimensions
        dem   = zeros(T,N)                        # TR: this line is new; GG: you specify type T before specifying dimensions
        for j=1:J
            num[:,j] = exp.(X*bigAlpha[:,j])
            dem .+= num[:,j]
        end

        P = num./repeat(dem,1,J)

        loglike = -sum( bigY.*log.(P) )

        return loglike
    end

    # GG: auodifferentiation: will get you the EXACT derivative of objective function, speeding up the optimization time because optimizer has gradient coded in (doesn't have to numerically approximate it) and it'll allow to get the numerical HESSIAN of your obj fct, which is what you need to compute STANDARD ERRORS
    # GG: need to pass a twice differentiable object (td) before the closure

    # declare that the objective function is twice differentiable
    td = TwiceDifferentiable(b -> mlogit_for_h(b, X, y), alpha_start; autodiff = :forward) # GG: uses forward mode autodifferentiation
    # run the optimizer
    alpha_hat_optim_ad = optimize(td, alpha_zero, LBFGS(), Optim.Options(g_tol = 1e-5, iterations=100_000, show_trace=true, show_every=50))
    # GG: so instead of putting as closure simply b -> mlogit_for_h(b, X, y), we put td, which stands for TwiceDifferentiable(b -> mlogit_for_h(b, X, y), alpha_start; autodiff = :forward)
    # GG: everything else stays the same
    alpha_hat_mle_ad = alpha_hat_optim_ad.minimizer
    # evaluate the Hessian at the estimates
    H  = Optim.hessian!(td, alpha_hat_mle_ad) # GG: getting the Hessian of twice differentiable object evaluated at the solution
    # TR: standard errors = sqrt(diag(inv(H))) [usually it's -H but we've already multiplied the obj fun by -1]
    # GG: from Adv Metrics on efficiency of Maximum Likelihood: -inv(H) enters the variance formula (sandwich); not taking negative coz we already took the negative of the objective function
    alpha_hat_mle_ad_se = sqrt.(diag(inv(H))) # GG: Take the square root of the diagonal elements of the inverse of the Hessian at the optimum
    println([alpha_hat_mle_ad alpha_hat_mle_ad_se]) # TR: these standard errors match Stata

    return nothing
end

# GG: Rather then getting the Hessian, we could also use Bootstrap to get the standard errors (but it'll take a lot longer)

#:::::::::::::::::::::::::::::::::::::::::::::::::::
# Question 6
# Wrap all of your code above into a function and then call that function at the very bottom of your script.
# Make sure you add println() statements after obtaining each set of estimates so that you can read them.
#:::::::::::::::::::::::::::::::::::::::::::::::::::
allwrap()


# 26 in base 16 or 2
base(16, 26)
base(2, 26)

# Parse to integer
parse(Int, "1a", 16)
parse(Int, "101101", 2)


module UnivariateProblems

    ### Sources
    ###
    ### [1]  http://infinity77.net/global_optimization/test_functions_1d.html

    struct UnivariateProblem
        name::AbstractString
        f::Function
        bounds::Vector{Float64}
        minimizers::Vector{Float64}
        minima::Vector{Float64}
    end

    examples = Dict{AbstractString, UnivariateProblem}()

    f(x) = 2x^2+3x+1

    examples["Polynomial"] = UnivariateProblem("Polynomial",
                                                  f,
                                                  [-2.0, 1.0],
                                                  [-0.75,],
                                                  [f(-0.75),])

    # Problem 04 from [1]
    p04(x) = -(16x^2-24x+5)exp(-x)

    examples["Problem04"] = UnivariateProblem("Problem04",
                                                p04,
                                                [1.9, 3.9],
                                                [2.868034,],
                                                [p04(2.868034),])

    # Problem 13 from [1]
    p13(x) = -x^(2/3)-(1-x^2)^(1/3)

    examples["Problem13"] = UnivariateProblem("Problem13",
                                                p13,
                                                [0.001, 0.99],
                                                [1 ./ sqrt(2.0),],
                                                [p13(1 ./ sqrt(2.0)),])

    # Problem 18 from [1]
    p18(x) = x <= 3.0 ? (x-2.0)^2 : 2.0*log(x-2.0)+1.0

    examples["Problem18"] = UnivariateProblem("Problem18",
                                                p18,
                                                [0.0, 6.0],
                                                [2.0,],
                                                [p18(2.0),])
end


# D(u)_function.jl

"""
Implementation of FHIP's susceptibility formula D(u) for the Optical Absorption of Polarons.

See FHIP 1962, equation (35c):
https://link.aps.org/doi/10.1103/PhysRev.127.1004.
"""

"""
ℜD(x::Float64, y::Float64, v::Float64, w::Float64, β::Float64)

    Calculate the real part of D(u) (equation (35c) in FHIP) for a complex argument u = x + iy. v and w are the variational Polaron parameters that minimise the free energy, for the supplied α Frohlich coupling. β is the reduced thermodynamical beta = ħω/kT with k being Boltzman's constant.
"""
function ℜD(x, y, v, w, β)
    R = (v^2 - w^2) / (w^2 * v)
    P = 1 / (exp(v * β) - 1)
    w^2 / v^2 * (
        R * cos(v * x) * (exp(-v * y) + 2 * P * cosh(v * y)) - (x^2 - y^2) / β - y -
        R * (2 * P + 1)
    )
end

"""
ℑD(x::Float64, y::Float64, v::Float64, w::Float64, β::Float64)

    Calculate the imaginary part of D(u) (equation (35c) in FHIP) for a complex argument u = x + iy. v and w are the variational Polaron parameters that minimise the free energy, for the supplied α Frohlich coupling. β is the reduced thermodynamical beta = ħω/kT with k being Boltzman's constant.
"""
function ℑD(x, y, v, w, β)
    R = (v^2 - w^2) / (w^2 * v)
    P = 1 / (exp(v * β) - 1)
    w^2 / v^2 * (R * sin(v * x) * (exp(-v * y) - 2 * P * sinh(v * y)) - x * (2 * y / β - 1))
end

# Create a complex number from the real and imaginary parts of D(u).

D(x, y, v, w, β) = ℜD(x, y, v, w, β) + 1im * ℑD(x, y, v, w, β)


export charλ,
       charA,
       charB
"""
charλ(q,ν,k)

char value λ_(ν+k) for Mathieu's equation

y'' + (λ_(ν+k) - 2 q cos( 2z )) y = 0

where

q ∈ ℝ       - parameter
ν ∈ [-1,1]  - fractional part of the non-integer order
k ∈ ℤ⁺      - range of integer parts of the order

"""
function charλ(q::Real, nu_::Real; k::UnitRange=1:1) # reduced = true
    #nu = reduced ? rem(nu_+1,2)-1 : nu_;
    nu = rem(nu_+1,2)-1;

    # Set matrix size using formula from Shirts paper (1993), Eqs. (2.1)-(2.2).
    nu0 = nu + maximum(k)
    C = (8.46 + 0.444*nu0)/(1 + 0.085*nu0)
    D =  (0.24 + 0.0214*nu0)/(1 + 0.059*nu0)
    N = ceil(Int, (nu0 + 2 + C*abs(q)^D)/2) # matrix size is 2N+1

    (two, q2, nu2) = float.(promote(2, q, nu))
    d0 = (two .* (-N:N) .- nu2).^2
    d1 = q2 .* ones(eltype(q2), 2 * N)
    A = SymTridiagonal(d0, d1)
    a = eigvals(A, k)
    return a
end

"""
charA(q; k=0:4)

char value A_k for Mathieu's equation

y'' + (A_k - 2 q cos( 2z )) y = 0

where

q ∈ ℝ  - parameter
k ∈ ℤ⁺ - eigenvalue index

"""
function charA(q::Real; k::UnitRange=1:1)
    all(x -> x >= 0, k) || throw(DomainError(k, "Indices must be non-negative integers."))

    # Boolean indices of even and odd n values
    ie = map(iseven, k)
    io = map(!, ie)

    a = Array{Float64}(undef ,length(k))
    k1 = k .+ 1
    a[ie] = charλ(abs(q), 0.0; k = k1)[ie]
    if q>=0
        a[io] = charλ(q, one(q); k = k1)[io]
    else
        if 0 in k # maybe not the cleanest way to do it
            a[io] = charλ(abs(q), one(q); k = k[2]:last(k))[io[2:end]]
        else
            a[io] = charλ(abs(q), one(q); k=k)[io]
        end
    end
    return a
end

"""
charB(q,k)

char value B_k for Mathieu's equation

y'' + (B_k - 2 q cos( 2z )) y = 0

where

q ∈ ℝ  - parameter
k ∈ ℤ  - eigenvalueindex

"""
function charB(q::Real; k::UnitRange=1:1)
    all(x -> x > 0, k) || throw(DomainError(k, "Indices must be positive integers."))
    # Boolean indices of even and odd n values
    ie = map(iseven, k)
    io = map(!, ie)

    b = Array{Float64}(undef, length(k))
    b[ie] = charλ(q,0.0,k=k)[ie]
    if q>=0
        b[io] = charλ(q,1.0,k=k)[io]
    else
        b[io] = charλ(abs(q),1.0,k = (k .+ 1))[io]
    end
    return b
end


f(x, y, z) = exp.(x .* y .* z) .* sin.(π .* x) .* sin.(π .* y) .* sin.(π .* z)

function advection_dominated(;N = 50, β = 1000.0)
    # Problem: Δu + βuₓ = f
    # u = 0 on the boundaries
    # f(x, y, z) = exp(xyz) sin(πx) sin(πy) sin(πz)
    # 2nd order central differences (shows serious wiggles)

    # Total number of unknowns
    n = N^3

    # Mesh width
    h = 1.0 / (N + 1)

    # Interior points only
    xs = range(0, stop=1, length=N + 2)[2 : N + 1]

    # The Laplacian
    Δ = laplace_matrix(Float64, N, 3) ./ -h^2

    # And the dx bit.
    ∂x_1d = spdiagm(-1 => fill(-β / 2h, N - 1), 1 => fill(β / 2h, N - 1))
    ∂x = kron(speye(N^2), ∂x_1d)

    # Final matrix and rhs.
    A = Δ + ∂x
    b = reshape([f(x, y, z) for x ∈ xs, y ∈ xs, z ∈ xs], n)

    A, b
end

function laplace_matrix(::Type{T}, n, dims) where T
    D = second_order_central_diff(T, n)
    A = copy(D)

    for idx = 2 : dims
        A = kron(A, speye(n)) + kron(speye(size(A, 1)), D)
    end

    A
end

second_order_central_diff(::Type{T}, dim) where {T} = convert(
    SparseMatrixCSC{T, Int},
    SymTridiagonal(fill(2 * one(T), dim), fill(-one(T), dim - 1))
)


export cyclotomic
export jacobi, kronecker, moebius, necklace

using Primes

"""
    cyclotomic(P, n)

Calculate cylotomic polynomial of degree `n` in polynom ring `P`.
This polynom is defined to be the disisor of `x^n - 1`, which is coprime to
all `x^k - 1` with `k < n`.

If `n` is prime, `cyclotomic(P, n) = (x^n - 1) / (x - 1).

If `n` is squarefree (product of distinct primes `p1 * p2 * ... * pk`)
and `c_k(x) = cyclotomic(P, p1*...*pk)` we use the recursion formula
` c_(k+1)(x) = c_k(x^pk) / c_k(x)`.

If `v = p1*...*pk` with the distinct prime factors of `n`, we have
`cyclotomic(P, n)(x) = cyclotomic(P, v)(x^(n/v))`.
"""
function cyclotomic(::Type{P}, n::Integer) where P<:UnivariatePolynomial
    T = basetype(P)
    (n < 2 || isprime(n)) && return P(ones(T, n))
    f = factor(n)
    l = length(f)
    v = collect(keys(f))
    v1 = prod(v)
    q = P(ones(T, v[1]))
    for k = 2:l
        q = div(spread(q, v[k]), q)
    end
    n == v1 ? q : spread(q, n ÷ v1)
end

# Jacobi symbol
function jacobi(n::Integer, k::Integer)
    k > 0 && k & 1 == 1 || throw(DomainError(k, "k must be positive odd number"))
    n = mod(n, k)
    t = 1
    while n != 0
        while n & 1 == 0
            n >>= 1
            r = k & 7
            if r == 3 || r == 5
                t = -t
            end
        end
        n, k = k, n
        if n & 3 == k & 3 == 3
            t = -t
        end
        n = mod(n, k)
    end
    k == 1 ? t : 0
end

# Kronecker symbol
function kronecker(n::Integer, k::Integer)
    n&1 == 0 && k&1 == 0 && return 0
    k == 0 && return n == 1 || n == -1 ? 1 : 0
    ks = k < 0 && n < 0 ? -1 : 1
    if k < 0
        k = -k
    end
    t = trailing_zeros(k)
    k >>= t
    ks = (n&7 == 3 || n&7 == 5 ) && t&1 == 1 ? -ks : ks
    jacobi(n, k) * ks
end

# moebius function
function moebius(n::Integer)
    n > 0 || throw(ArgumentError("moebius defined for positive integers only"))
    n == 1 && return 1
    f = Primes.factor(n)
    if maximum(values(f)) == 1
        ifelse(isodd(length(f)), -1, 1)
    else
        0
    end
end

# necklace polynomial - Moreau's necklace-counting function
"""
    necklace(q, n)

Return the value of the `necklace polynomial`

Count of irreducible monic polynomials of degree n over Z/q.
"""
necklace(q::Integer, n::Integer) = _necklace(q, n) ÷ n
necklace(q::Ring, n::Integer) = _necklace(q, n) / n
function _necklace(q, n::Integer)
    f = Primes.factor(n)
    p = collect(keys(f))
    m = length(p)
    s = 0
    for x = 0:(2^m-1)
        d = n
        μ = 1
        k = 1
        while x != 0
            if x & 1 == 1
                d ÷= p[k]
                μ = -μ
            end
            x >>= 1
            k += 1
        end
        s += q^d * μ
    end
    s
end


function representable_ints(nbits)
    return 1:(2^(nbits-1)-1)
end

function representable_floats(nbits,ebits)
    #= returns an array of all representable positive floats exclusive 0 and ComplexInfinity.
    nbits is the total number of bits (sign,exponent,significand)
    ebits is the number of exponent bits
    =#

    # derived constants
    bias = 2^(ebits-1) - 1
    sbits = nbits-ebits-1     # number of significand bits (minus one for sign bit)

    repr_floats = []           # represented as 000...0000

    # subnormal numbers (exponent is 0)
    for f = 1:2^sbits-1
        append!(repr_floats,2.0^(1-bias)*(f/2^sbits))
    end

    # all other numbers
    for e = 1:2^ebits-2     # the exponent cannot be 11...111 this represents NaN
        for f = 0:2^sbits-1
            append!(repr_floats,2.0^(e-bias)*(1.0 + f/2^sbits))
        end
    end

    return Float64.(repr_floats)
end

function wcdp_posit(plist)
    p_am = (plist[1:end-1]+plist[2:end])/2.
    p_wda = -log10.(abs.(log10.(p_am./plist[1:end-1])))

    # extend first and last point, taking no overflow/underflow into account
    p0 = plist[1]/16    # something much smaller than minpos
    pinf = plist[end]*16    # something much bigger than maxpos

    p_wda_0 = -log10.(abs.(log10.(p0/plist[2]))) # worst-case decimal accuracy for these extreme values
    p_wda_inf = -log10.(abs.(log10.(pinf/plist[end])))

    # worst-case decimal accuracy of interpolated on minpos/maxpos
    p_wda_minpos = p_wda_0 + log10(plist[1]/p0)/log10(p0/p_am[1])*(p_wda_0-p_wda[1])
    p_wda_maxpos = p_wda_inf + log10(plist[end]/pinf)/log10(pinf/p_am[end])*(p_wda_inf-p_wda[end])

    p_wda = vcat(p_wda_0,p_wda_minpos,p_wda,p_wda_maxpos,p_wda_inf)
    p_am = vcat(p0,plist[1],p_am,plist[end],pinf)

    return p_am,p_wda,plist
end

function wcdp_float(flist)

    f_am = (flist[1:end-1]+flist[2:end])/2.
    f_wda = -log10.(abs.(log10.(f_am./flist[1:end-1])))

    # extend with zeros due to overflow
    f_wda = vcat(0.55,f_wda)        # extrapolate somehow
    f_am = vcat(flist[1],f_am)

    return f_am,f_wda
end

function wcdp_approx(flist)

    f_am = (flist[1:end-1]+flist[2:end])/2.
    f_wda = -log10.(abs.(log10.(f_am./flist[1:end-1])))

    return f_am,f_wda
end

function wc_dec_acc_int(nbits)

    # assume rounding mode down
    # somehow interpolate with 0.13 onto the smallest representable number 1...

    i_am = [1.,2.,2^(nbits-1)-1]
    i_wda = [0.13,-log10(abs(log10(2))),-log10(abs(log10((2^(nbits-1)-1)/(2^(nbits-1)-2))))]

    return i_am,i_wda
end

decprec(xe,xr) = -log10.(abs.(log10.(xe./xr)))


# given an 1D interval, return quadrature nodes and weights

using LinearAlgebra
using FastGaussQuadrature

function quadgrid(xmin, xmax, n)
    # get nodes and weights in reference coord [-1, 1]
    x, w = gausslegendre(n)
    # map nodes and weights to current interval
    dist = (xmax - xmin)/2
    center = (xmax + xmin)/2
    weights = dist .* w
    nodes = dist .* x .+ center
    return nodes, weights
end

function quadgrid_old(xmin, xmax, n)
    dist = (xmax - xmin)/2
    center = (xmax + xmin)/2
    if n == 1
        w = 2
        x = 0
    elseif n == 2
        w = [1, 1]
        x =  1/sqrt(3) * [1, -1]
    elseif n == 3
        w = [5/9, 8/9, 5/9]
        x = -sqrt(3/5) * [-1, 0, 1]
    elseif n == 4
        w = [(18+sqrt(30))/36, (18+sqrt(30))/36,
             (18-sqrt(30))/36, (18-sqrt(30))/36]
        x = [sqrt(3/7 - 2/7*sqrt(6/5)), -sqrt(3/7 - 2/7*sqrt(6/5)),
             sqrt(3/7 + 2/7*sqrt(6/5)), -sqrt(3/7 + 2/7*sqrt(6/5))]
    elseif n == 5
        w = [128/225, (322+13*sqrt(70))/900, (322+13*sqrt(70))/900,
                      (322-13*sqrt(70))/900, (322-13*sqrt(70))/900]
        x = [0, sqrt(5-2*sqrt(10/7))/3, -sqrt(5-2*sqrt(10/7))/3,
                sqrt(5+2*sqrt(10/7))/3, -sqrt(5+2*sqrt(10/7))/3]
    end

    w_new = dist .* w
    x_new = dist .* x .+ center
    # fx = f.(x_new)
    # int = dot(w_new, fx)

    return x_new, w_new

end


# Used "Conjugate Bayesian analysis of the Gaussian distribution" by Murphy as
# a reference.  Note that there were some typos in that document so the code
# here may not correspond exactly.

immutable NormalInverseWishart <: Distribution
    dim::Int
    zeromean::Bool
    mu::Vector{Float64}
    kappa::Float64              # This scales precision (inverse covariance)
    Lamchol::Cholesky{Float64}  # Covariance matrix (well, sqrt of one)
    nu::Float64

    function NormalInverseWishart(mu::Vector{Float64}, kappa::Real,
                                  Lamchol::Cholesky{Float64}, nu::Real)
        # Probably should put some error checking in here
        d = length(mu)
        zmean::Bool = true
        for i = 1:d
            if mu[i] != 0.
                zmean = false
                break
            end
        end
        new(d, zmean, mu, float64(kappa), Lamchol, float64(nu))
    end
end

function NormalInverseWishart(mu::Vector{Float64}, kappa::Real,
                              Lambda::Matrix{Float64}, nu::Real)
    NormalInverseWishart(mu, kappa, cholfact(Lambda), nu)

end

function insupport(::Type{NormalInverseWishart}, x::Vector{Float64}, Sig::Matrix{Float64})
    return (all(isfinite(x)) &&
           size(Sig, 1) == size(Sig, 2) &&
           isApproxSymmmetric(Sig) &&
           size(Sig, 1) == length(x) &&
           hasCholesky(Sig))
end

pdf(niw::NormalInverseWishart, x::Vector{Float64}, Sig::Matrix{Float64}) =
        exp(logpdf(niw, x, Sig))

function logpdf(niw::NormalInverseWishart, x::Vector{Float64}, Sig::Matrix{Float64})
    if !insupport(NormalInverseWishart, x, Sig)
        return -Inf
    else
        p = size(x, 1)

        nu = niw.nu
        kappa = niw.kappa
        mu = niw.mu
        Lamchol = niw.Lamchol
        hnu = 0.5 * nu
        hp = 0.5 * p

        # Normalization
        logp::Float64 = hnu * logdet(Lamchol)
        logp -= hnu * p * log(2.)
        logp -= lpgamma(p, hnu)
        logp -= hp * (log(2.*pi) - log(kappa))

        # Inverse-Wishart
        logp -= (hnu + hp + 1.) * logdet(Sig)
        logp -= 0.5 * trace(Sig \ (Lamchol[:U]' * Lamchol[:U]))

        # Normal
        z = niw.zeromean ? x : x - mu
        logp -= 0.5 * kappa * invquad(PDMat(Sig), z)

        return logp

    end
end

function rand(niw::NormalInverseWishart)
    Sig = rand(InverseWishart(niw.nu, niw.Lamchol))
    mu = rand(MvNormal(niw.mu, Sig ./ niw.kappa))
    return (mu, Sig)
end


#=
Surprisingly there are only three numbers that can be written as the sum of fourth powers of their digits:

1634 = 14 + 64 + 34 + 44
8208 = 84 + 24 + 04 + 84
9474 = 94 + 44 + 74 + 44
As 1 = 14 is not a sum it is not included.

The sum of these numbers is 1634 + 8208 + 9474 = 19316.

Find the sum of all the numbers that can be written as the sum of fifth powers of their digits.
=#
#=
Note: Calculating limit:
Checking the number 111111111.
That number has 9 digits, so the maximum sum would be 9*59049 = 531441, which doesn't even come close to 111111111.
So any 9 digit number or greater can't ever reach a big enough sum.
=#
function sumofdigitpowers(a)
  total, i = 0, 2
  while i < length(digits(i))*9^a
    current = sum(map((x)->x^a, digits(i)))
    if current == i
      total += i
    end
    i+=1
  end
  total
end
@time println(sumofdigitpowers(5))


test_str = "939
7,13,x,x,59,x,31,19"

test_list = split(test_str, '\n')

list = readlines("day13_input.txt")

function departure_time(list)
    earliest = parse(Int, list[1])
    buses = map(x-> parse(Int, x), split(replace(list[2], ",x" => ""), ','))
    next_departures = Dict(map(x->(div(earliest, x) + 1 ) * x => x, buses))
    departure = minimum(keys(next_departures))
    return (departure - earliest) * next_departures[departure]
end

@show departure_time(test_list)
@show departure_time(list)
@show departure_time(test_list) == 295
@show departure_time(list) == 246

function time_stamp(line)
    buses = map(x-> parse(Int, x), split(replace(line, "x" => "-1"), ','))
    imax = argmax(buses .+ Array(0:-1:1-length(buses)))
    bmax = buses[imax]
    n = 1
    t = n*bmax + 1 - imax
    len = length(buses)
    while !all(≤(0), map(i->mod(t+i-1, buses[i]), 1:len))
        n += 1
        t = n*bmax + 1 - imax
    end
    return t
end

@show time_stamp(test_list[2])

@show time_stamp("17,x,13,19") == 3417
@show time_stamp("67,7,59,61") == 754018
@show time_stamp("67,x,7,59,61") == 779210
@show time_stamp("67,7,x,59,61") == 1261476
@show time_stamp("1789,37,47,1889") == 1202161486
#@show time_stamp(list[2]) # Too slow

# Implementing algorithm for solving linear Diophantine equations as
# explained in https://www.math.uwaterloo.ca/~wgilbert/Research/GilbertPathria.pdf
# but there is some bug. It works on all test examples, but not on the problem list.

function time_stamp_diophantine(line)
    buses = map(x-> parse(Int, x), split(replace(line, "x" => "0"), ','))
    running = [b for b=buses if b > 0]
    delays = [1-i for i=1:length(buses) if buses[i] > 0]
    offset = (mod.(running+delays,running))[2:end]
    len=length(running)
    T = fill(0, len, len)
    for i=1:len
        T[i,i] = 1
    end
    A = fill(0, len-1, len)
    for i=1:len-1
        A[i,1] = running[1]
        A[i,i+1] = - running[i+1]
    end
    R = deepcopy(A')
    for i=2:len
        while R[i,i-1] ≠ 0
            if abs(R[i-1,i-1]) > abs(R[i, i-1])
                a = div(R[i-1,i-1], R[i, i-1])
                if mod(R[i-1,i-1], R[i, i-1]) == 0
                    a -= 1
                end
                R[i-1,:] .-= a * R[i, :]
                T[i-1,:] .-= a * T[i,:]
            else
                a = div(R[i, i-1], R[i-1,i-1])
                R[i,:] .-= a * R[i-1,:]
                T[i,:] .-= a * T[i-1,:]
            end
        end
    end
    k = BigInt.((R[1:end-1,:]')\offset)
    k = [k; BigInt(floor(-sum([T[i,1]*k[i]/T[len,1] for i=1:len-1])))]
    n1 = sum([T[i,1]*k[i] for i=1:len])
#    while !all(>(0), [sum([T[i,j]*k[i] for i=1:len]) for j=1:len])
    while n1 < 0
        k[len] += sign(T[len,1])
        n1 = sum([T[i,1]*k[i] for i=1:len])
    end

    t = n1*running[1]
    return running, offset, len, A, T, R, k, n1, t
end

@show time_stamp_diophantine(test_list[2])[end] == 1068781
@show time_stamp_diophantine("7,13,x,x,59,x,31,19")[end] == 1068781
@show time_stamp_diophantine("17,x,13,19")[end] == 3417
@show time_stamp_diophantine("67,7,59,61")[end]  == 754018
@show time_stamp_diophantine("67,x,7,59,61")[end]  == 779210
@show time_stamp_diophantine("67,7,x,59,61")[end]  == 1261476
@show time_stamp_diophantine("1789,37,47,1889")[end]  == 1202161486
@show time_stamp_diophantine(list[2])[end] == 939490236001473

function test_result(line)
    running, offset, len, A, T, R, k, n1, t = time_stamp_diophantine(line)

    check = all(>(0), [sum([T[i,j]*k[i] for i=1:len]) for j=1:len])
    timings = (T'k) .* running
    check_offset = timings[2:end] - fill(timings[1], len-1)
    difference = all(==(0), check_offset + offset)
    return check, difference, T*A'==R
end

# Using the algorithm for the Chinese Remainder Theorem

function time_stamp_CRG(line)
    buses = map(x-> parse(Int, x), split(replace(line, "x" => "0"), ','))
    running = [b for b=buses if b > 0]
    offset = [mod(b+1-i,b) for (i,b) in enumerate(buses) if b>0]
    N = prod(running)
    d = div.(N,running)
    result = sum(offset .* d .* invmod.(d,running))
    return mod(result, N)
end

# There is an option to use Mods.jl package, which already has an implemention
# of the Chinese Remainder Theorem, as I learned from other Humans of Julia on
# Discord, but it is more obscure and slower, I prefer the direct implementation
# above.

using Mods

function time_stamp_ModsCRT(line)
    buses = map(x-> parse(Int, x), split(replace(line, "x" => "0"), ','))
    running = [b for b=buses if b > 0]
    x_delays = [i-1 for i=1:length(buses) if buses[i] > 0]
    mod_prep = Mod[Mod(b,d) for (d,b) in zip(running, x_delays)]
    rem = Mods.CRT(mod_prep...)
    return modulus(rem) - rem.val
end

@show time_stamp_CRG(test_list[2]) == 1068781
@show time_stamp_CRG(list[2]) == 939490236001473
@show time_stamp_ModsCRT(test_list[2]) == 1068781
@show time_stamp_ModsCRT(list[2]) == 939490236001473

#=
```julia
julia> @btime time_stamp_ModsCRT(list[2])
  23.110 μs (45 allocations: 5.47 KiB)
939490236001473

julia> @btime time_stamp_CRG(list[2])
  15.109 μs (24 allocations: 5.28 KiB)
939490236001473
```
 =#

nothing

#=

If `x=buses` (with `x=-1` if bus = 'x'), time `t` is given by the condition
that `t+i-1` is an integer multiple of `x[i]`, i.e. `mod(t+i-1,x) == 0` or
`mod(t, x) = 1-i` or `mod(t,x) = x-i+1`

In case `t` is not so big, we can just search for this condition
by stepping up `t` by multiples of `x[1]`. That was my first approach,
which works fast for the test set. But for the real list, that is not
computationally feasible. So we do differently.

The conditions

    `t + i - 1 = nᵢ xᵢ,   ∀ i=1:length(x) s.th. xᵢ > 0`

are equivalent to
    `t = n₁x₁ = n₂x₂ - 1 = ⋯ = nᵢxᵢ - i + 1,     ∀ i, with xᵢ > 0`

And we know that `x₁ > 0`. Hence we just iterate over `i` to find

    `For i = 1:
        n₁¹ = 1
    For i = 2:
        n₂² such that n₂²x₂ - 1 = jn₁¹x₁
        n₁² = j₂²n₁¹
    For i = 3:
        n₃³ such that n₃³x₂ - 2 = jn₁²x₁
        n₁² = jn₁²
    `n₁ such that t = n₁ x₁`
    `n₂ such that n₂x₂ - 1 = `

```julia
n = 1
for i = 2:length(x)
    j = 1
    while mod(j*n*buses[i]-i+1, buses[1]) > 0
        j += 1
    end
    n *= j
end
```

But this is slow in some cases. So we notice the problem can also be written as
    `t = 0 (mod x₁); t = x₂-1 (mod x₂); … t = xᵢ+1-i (mod xᵢ); …`

which is in the context of the Chinese Remainder Problem (see e.g.
[Chinese Remainder Theorem](https://en.wikipedia.org/wiki/Chinese_remainder_theorem))
and [Chinese Remainder Theorem with algorithm](https://shainer.github.io/crypto/math/2017/10/22/chinese-remainder-theorem.html))

An algorithm, in python, from the latter homepage, is the following

def ChineseRemainderGauss(x, a):
    """
    Return t such that mod(t,xᵢ) = aᵢ, ∀i=1,…,n, if 0 ≤ aᵢ< xᵢ
    and the xᵢ are pairwise coprime.
    """
    from math import prod
    P = prod(x)
    result = 0

    for i in range(len(n)):
        ai = a[i]
        xi = x[i]
        bi = P // xi

        result += ai * bi * invmod(bi, xi)

    return result % P

See also https://shainer.github.io/crypto/math/2017/10/22/chinese-remainder-theorem.html

 =#


using Plots

func1(R) = R/2
func2(R) = R/2 + [1 ; 0]
func3(R) = R/2 + [0 ; 1]
P = 200
num = 500000
x = []
y = []
for i in 1:num
    point = [rand() ; rand()]
    for j in 1:P
        point = rand([func1(point), func2(point), func3(point)])
    end
    push!(x, point[1])
    push!(y, point[2])
end

scatter(x,y,markersize = 0.000001, legend = false, border=:none, dpi=500)
savefig("C:\\Users\\Yaghoub\\Documents\\GitHub\\A-few-fractals-in-Julia\\Fractals-Fig\\SRT.png")


using StructuresKit

E = 29000.0
σy = 50.0                   #steel yield stress
σy1 = 50.0                  #steel yield stress
σu = 65.3                   #steel ultimate stress
σf=σy * (1 + 0.10)      #steel fracture stress#
ϵy = σy / E              #steel yield strain
ϵy1 = σy / E * 10    #steel strain at end of yield plateau
ϵu = 0.18             #steel ultimate strain
ϵf = 0.21             #steel fracture strain
n = 11

σ, ϵ  = MaterialModels.steel(σy, σy1, σu, σf, ϵy, ϵy1, ϵu, ϵf, n)

using Plots
plot(σ, ϵ, markershape = :o)

shape_name = "W14X90"

shape_info = CrossSection.AISC(shape_name)

n_Wshape=(4, 2, 4, 4, 4)

xcoords, ycoords = CrossSection.wshape_nodes(shape_info, n_Wshape)

plot(xcoords, ycoords, markershape = :o)

# using TriangleMesh

mesh_size = 0.01
mesh = CrossSection.triangular_mesh(xcoords, ycoords, mesh_size)

Ai, cxi, cyi = CrossSection.triangular_mesh_properties(mesh)

cx, cy = CrossSection.centroid_from_cells(Ai, cxi, cyi)

Ix = CrossSection.moment_of_inertia_from_cells(Ai, cyi, cy)
Iy = CrossSection.moment_of_inertia_from_cells(Ai, cxi, cx)

# #discretize cross-section with a triangular mesh
# function triangular_mesh(xcoords, ycoords, mesh_size)

#     num_nodes = length(xcoords)
#     num_segments = num_nodes

#     # n_point, n_point_marker, n_point_attribute, n_segment, n_holes
#     poly = TriangleMesh.Polygon_pslg(num_nodes, 1, 0, num_segments, 0)

#     node = [xcoords ycoords]
#     set_polygon_point!(poly, node)

#     node_marker = ones(Int, num_nodes, 1)
#     set_polygon_point_marker!(poly, node_marker)

#     segments = zeros(Int, num_segments, 2)
#     for i=1:num_segments

#         if i == num_segments
#             segments[i, 1:2] = [i, 1]
#         else
#             segments[i, 1:2] = [i, i+1]
#         end

#     end

#     set_polygon_segment!(poly, segments)

#     segment_markers = ones(Int, num_segments)
#     set_polygon_segment_marker!(poly, segment_markers)

#     #switches from https://www.cs.cmu.edu/~quake/triangle.html
#     switches = "penvVa" * string(mesh_size) * "D"

#     mesh = create_mesh(poly, switches)

#     return mesh

# end

#calculate triangle areas

# # https://keisan.casio.com/has10/SpecExec.cgi?path=05000000.Mathematics%252F01000500.Plane%2520geometry%252F10010300.Area%2520of%2520a%2520triangle%2520with%2520three%2520points%252Fdefault.xml&charset=utf-8
# function triangle_area(x1, y1, x2, y2, x3, y3)

#     A = abs((x1*y2 + x2*y3 + x3*y1 - y1*x2 - y2*x3 - y3*x1)/2)

# end

# #https://www.mathopenref.com/coordcentroid.html
# function triangle_centroid(x1, y1, x2, y2, x3, y3)

#     cx = (x1 + x2 + x3)/3
#     cy = (y1 + y2 + y3)/3

#     return cx, cy

# end

# function triangulation_properties(mesh)

#     #calculate cell area and centroid
#     Ai = zeros(Float64, mesh.n_cell)
#     cxi = zeros(Float64, mesh.n_cell)
#     cyi = zeros(Float64, mesh.n_cell)

#     for i = 1:mesh.n_cell

#         p1 = mesh.cell[1, i]
#         p2 = mesh.cell[2, i]
#         p3 = mesh.cell[3, i]

#         x1 = mesh.point[1, p1]
#         y1 = mesh.point[2,p1]
#         x2 = mesh.point[1, p2]
#         y2 = mesh.point[2,p2]
#         x3 = mesh.point[1, p3]
#         y3 = mesh.point[2,p3]

#     Ai[i] = triangle_area(x1, y1, x2, y2, x3, y3)

#     cxi[i], cyi[i] = triangle_centroid(x1, y1, x2, y2, x3, y3)

#     end

#     return Ai, cxi, cyi

# end

# #calculate cross-sectional area from cells

# # function area_from_cells(Ai)

# #     A = sum(Ai)

# # end

# # #calculate cross-section centroid from cells

# # function centroid_from_cells(Ai, cxi, cyi)

# #     A = area_from_cells(Ai)

# #     cx = sum(cxi .* Ai) / A
# #     cy = sum(cyi .* Ai) / A

# #     return cx, cy

# # end

# cx, cy = CrossSection.centroid_from_cells(Ai, cxi, cyi)

# # function moment_of_inertia_from_cells(Ai, ci, c)

# #     I = sum(((c .- ci) .^2 .* Ai))

# #     return I

# # end

# Ix = moment_of_inertia_from_cells(Ai, cyi, cy)
# Iy = moment_of_inertia_from_cells(Ai, cxi, cx)

# #calculate triangle centroids

# # for i = 1:mesh.n_cell

# #     p1 = mesh.cell[1, i]
# #     p2 = mesh.cell[2, i]
# #     p3 = mesh.cell[3, i]

# #     x1 = mesh.point[1, p1]
# #     y1 = mesh.point[2,p1]
# #     x2 = mesh.point[1, p2]
# #     y2 = mesh.point[2,p2]
# #     x3 = mesh.point[1, p3]
# #     y3 = mesh.point[2,p3]

# #    cx[i] = triangle_area(x1, y1, x2, y2, x3, y3)

# # end

# zcoords = zeros(Float64, mesh.n_point)
# coordinates = [mesh.point[1,:] mesh.point[2,:] zcoords]

# connectivity = mesh.cell'

# using Makie
# scene = Makie.poly(coordinates, connectivity, color=:lightgray, shading=:true, show_axis=:true, overdraw=:false, strokecolor = (:black, 0.6), strokewidth = 1)

# poly = polygon_Lshape()
# mesh = create_mesh(poly, info_str="my mesh", voronoi=true, delaunay=true, set_area_max=true)

# node = [1.0 0.0 ; 0.0 1.0 ; -1.0 0.0 ; 0.0 -1.0 ;
#         0.25 0.25 ; -0.25 0.25 ; -0.25 -0.25 ; 0.25 -0.25]

#         # size is number_segments x 2
# seg = [1 2 ; 2 3 ; 3 4 ; 4 1 ; 5 6 ; 6 7 ; 7 8 ; 8 5]

# # all points get marker 1
# node_marker = [ones(Int,4,1) ; 2*ones(Int,4,1)]
# # last segment gets a different marker
# seg_marker = [ones(Int,4) ; 2*ones(Int,4)]

# # size is number_points x number_attr
# node_attr = rand(8,2)

# # size is number_holes x 2
# hole = [0.5 0.5]

# polygon_struct_from_points

# shape_mesh, cross_section_edges = Mesh.open_cross_section_tessellation(xcoords, ycoords)

# connectivity = zeros(Int, length(shape_mesh), 3)
# for i=1:length(shape_mesh)
#     connectivity[i, :] = shape_mesh[i]
# end


"""
Betina Robust Sampled based Formulation

Further information:
  - Fernandes, B., Street, A., ValladA˜ £o, D., e Fernandes, C. (2016). An adaptive robust portfolio optimization model with loss constraints based on data-driven polyhedral uncertainty sets. European Journal of Operational Research, 255(3):961 – 970. ISSN 0377-2217. URL.

"""
struct RobustBetina <: AbstractSampleBased
    sampled_returns::Array{Float64,2}
    number_of_assets::Int
    number_of_samples::Int
end

function RobustBetina(;
    sampled_returns::Array{Float64,2}
)
    number_of_samples, number_of_assets = size(sampled_returns)

    return RobustBetina(
        sampled_returns, number_of_assets, number_of_samples
    )
end

function _portfolio_return_latex_RobustBetina_dual()
    return """
        ```math
        \\max_{\\theta} \\quad  \\theta \\\\
        s.t.  \\quad \\theta \\leq r_s ' w \\quad \\forall s = 1:\\mathcal{S} \\\\
        ```
        """
end

"""
    portfolio_return!(model::JuMP.Model, w, formulation::RobustBetina)

Returns worst case return in Betina's uncertainty set, defined by the following dual problem:

$(_portfolio_return_latex_RobustBetina_dual())
"""
function portfolio_return!(model::JuMP.Model, w, formulation::RobustBetina)
    # auxilary variables
    θ = @variable(model, θ)
    # convex hull
    @constraint(model, sum(formulation.sampled_returns * w , dims=2) .>= θ)

    return θ
end


using GaussQuadrature, GLMakie

function cubed_sphere_warp(
    a,
    b,
    c,
    R = max(abs(a), abs(b), abs(c)),
)

    function f(sR, ξ, η)
        X, Y = tan(π * ξ / 4), tan(π * η / 4)
        ζ1 = sR / sqrt(X^2 + Y^2 + 1)
        ζ2, ζ3 = X * ζ1, Y * ζ1
        ζ1, ζ2, ζ3
    end

    fdim = argmax(abs.((a, b, c)))
    if fdim == 1 && a < 0
        # (-R, *, *) : formulas for Face I from Ronchi, Iacono, Paolucci (1996)
        #              but for us face II of the developed net of the cube
        x1, x2, x3 = f(-R, b / a, c / a)
    elseif fdim == 2 && b < 0
        # ( *,-R, *) : formulas for Face II from Ronchi, Iacono, Paolucci (1996)
        #              but for us face III of the developed net of the cube
        x2, x1, x3 = f(-R, a / b, c / b)
    elseif fdim == 1 && a > 0
        # ( R, *, *) : formulas for Face III from Ronchi, Iacono, Paolucci (1996)
        #              but for us face IV of the developed net of the cube
        x1, x2, x3 = f(R, b / a, c / a)
    elseif fdim == 2 && b > 0
        # ( *, R, *) : formulas for Face IV from Ronchi, Iacono, Paolucci (1996)
        #              but for us face I of the developed net of the cube
        x2, x1, x3 = f(R, a / b, c / b)
    elseif fdim == 3 && c > 0
        # ( *, *, R) : formulas for Face V from Ronchi, Iacono, Paolucci (1996)
        #              and the same for us on the developed net of the cube
        x3, x2, x1 = f(R, b / c, a / c)
    elseif fdim == 3 && c < 0
        # ( *, *,-R) : formulas for Face VI from Ronchi, Iacono, Paolucci (1996)
        #              and the same for us on the developed net of the cube
        x3, x2, x1 = f(-R, b / c, a / c)
    else
        error("invalid case for cubed_sphere_warp(::EquiangularCubedSphere): $a, $b, $c")
    end

    return x1, x2, x3
end

N = 5
ξ¹, ω¹ = GaussQuadrature.legendre(N, GaussQuadrature.both)
ξ² = copy(ξ¹)
ξ³ = copy(ξ¹)

Ne = 12
vertices = collect(range(-1, 1, length = Ne + 1))
x¹ = zeros(N, Ne)
x² = copy(x¹)
x³ = copy(x¹)

for e in 1:Ne
    @. x¹[:, e] = (ξ¹ + 1) / 2 * (vertices[e+1] - vertices[e]) + vertices[e]
    @. x²[:, e] = (ξ¹ + 1) / 2 * (vertices[e+1] - vertices[e]) + vertices[e]
    @. x³[:, e] = (ξ¹ + 1) / 2 * (vertices[e+1] - vertices[e]) + vertices[e]
end

Nex = Ney = Ne
face1 = [(x¹[1], x²[j, ex], x³[k, ey]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface1 = [cubed_sphere_warp(face1[i]...) for i in eachindex(face1)]

face2 = [(x¹[end], x²[j, ex], x³[k, ey]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface2 = [cubed_sphere_warp(face2[i]...) for i in eachindex(face2)]

face3 = [(x¹[j, ey], x²[1], x³[k, ex]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface3 = [cubed_sphere_warp(face3[i]...) for i in eachindex(face1)]

face4 = [(x¹[j, ey], x²[end], x³[k, ex]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface4 = [cubed_sphere_warp(face4[i]...) for i in eachindex(face1)]

face5 = [(x¹[j, ex], x²[k, ey], x³[1]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface5 = [cubed_sphere_warp(face5[i]...) for i in eachindex(face1)]

face6 = [(x¹[j, ex], x²[k, ey], x³[end]) for j in 1:N, k in 1:N, ex in 1:Nex, ey in 1:Ney]
warpedface6 = [cubed_sphere_warp(face6[i]...) for i in eachindex(face1)]

# Now Plot
lw = 1  # linewidth
ms = 25 # marker size

fig = Figure(resolution = (1400, 1200))
ax = LScene(fig[1, 1])
# fig, ax, sc = scatter(warpedface1, color = :red, markersize = 0.0, show_axis = false)
# scatter(warpedface1, color = :red, markersize = 0.0, show_axis = false)
r = range(-1, 1, length = Ne + 1)

xa = zeros(Ne + 1, Ne + 1)
xb = zeros(Ne + 1, Ne + 1)
xc = zeros(Ne + 1, Ne + 1)

xa .= r
xb .= r'
xc .= 1

a = [cubed_sphere_warp(a, b, c)[1] for (a, b, c) in zip(xa, xb, xc)] .* 1.00
b = [cubed_sphere_warp(a, b, c)[2] for (a, b, c) in zip(xa, xb, xc)] .* 1.00
c = [cubed_sphere_warp(a, b, c)[3] for (a, b, c) in zip(xa, xb, xc)] .* 1.00

wireframe!(ax,
    a, b, c,
    show_axis = false,
    linewidth = lw)

wireframe!(ax,
    -c, a, b,
    show_axis = false,
    linewidth = lw * 5)

scatter!(ax, warpedface1, color = :red, markersize = ms * 0.75,)
scatter!(ax, warpedface3, color = :blue, markersize = ms,)

rotate_cam!(fig.scene.children[1], (π/12, π, 0))


function OPF_dual(optimizer,set_generators,set_nodes,set_demands,generators,demands,capacity,varcost,load_node,utility_node,links,links_rev,F_max_dict,B_dict,MapG,MapD)

m=Model(optimizer)

@variable(m, μ_G[g in set_generators]>= 0)
@variable(m, μ_D[d in set_demands]>= 0)
@variable(m, η_lower[link in links]>= 0)
@variable(m, η_upper[link in links]>= 0)
@variable(m, λ[set_nodes])
@variable(m, γ)

@objective(m, Min,     sum(μ_D[d]*demands[d,load_node]  for d in set_demands) +
                       sum(μ_G[g]*generators[g,capacity]  for g in set_generators)+
                       sum(F_max_dict[j]*(η_lower[j]+η_upper[j])   for j in links)
                       )

@constraint(m, constraint1[d in set_demands], -demands[d,utility_node]+ μ_D[d]+λ[MapD[d][2]]>= 0)

@constraint(m, constraint2[g in set_generators], generators[g,varcost]+ μ_G[g]-λ[MapG[g][2]]>= 0)

#@constraint(m, constraint3[n in set_nodes_ref],     sum(B_dict[j]*(λ[j[1]]-λ[j[2]]+η_upper[j] -η_lower[j]) for j in links if n == j[1])
#                                                     +γ == 0)

@constraint(m, constraint3[n in set_nodes_ref],     sum(B_dict[j]*(λ[j[1]]-λ[j[2]]+η_upper[j] -η_lower[j]) for j in links if n == j[1])
                                                      +sum(B_dict[j]*(-η_upper[j] +η_lower[j]) for j in links_rev if n == j[2] )+γ == 0)

#@constraint(m, constraint4[n in set_nodes_noref],  sum(B_dict[j]*(λ[j[1]]-λ[j[2]]+η_upper[j] -η_lower[j]) for j in links if n == j[1])
#                                                      == 0)

@constraint(m, constraint4[n in set_nodes_noref],  sum(B_dict[j]*(λ[j[1]]-λ[j[2]]+η_upper[j] -η_lower[j]) for j in links if n == j[1])
                                                      +sum(B_dict[j]*(-η_upper[j] +η_lower[j]) for j in links_rev if n == j[2] )== 0)

@time optimize!(m)

status = termination_status(m)
println("The solution status is: $status")

syscost_det=objective_value(m)

println("System Cost:",syscost_det)

Dual_constraint2=zeros(length(set_generators))

for g in set_generators
   Dual_constraint2[g]= JuMP.dual(constraint2[g])
end
println("Production level of generator g:", Dual_constraint2)

Dual_constraint1=zeros(length(set_demands))

for d in set_demands
   Dual_constraint1[d]= JuMP.dual(constraint1[d])
end
println("Consumption level of demand d: ", Dual_constraint1)

λ_value=zeros(length(set_nodes))
for n in set_nodes
  λ_value[n]=JuMP.value.(λ[n])
end
println("Electricity Price:",λ_value)

#Review this flow lines
#Review all the constraints

f_value=zeros(n_link)
global i=1

for j in links
global jj=j
if set_nodes_ref==jj[1]
global f_value[i]= B_dict[j]*(JuMP.dual(constraint3[j[1]])-JuMP.dual(constraint4[j[2]]))
elseif set_nodes_ref==jj[2]
global f_value[i]= B_dict[j]*(JuMP.dual(constraint4[j[1]])-JuMP.dual(constraint3[j[2]]))
else
global f_value[i]= B_dict[j]*(JuMP.dual(constraint4[j[1]])-JuMP.dual(constraint4[j[2]]))
end
println("Power Flow lines $j:", f_value[i])
global i=1+i
end

return (syscost_det,Dual_constraint2,Dual_constraint1,λ_value,f_value)
end

#cd("C:\\Users\\braya\\.julia\\dev\\DTU_BrayamValqui_SP2021\\src")
#include("Singlefile_OPF_dual.jl")


# Simulate a 5-area (~20 Hz) beta network instantiated by an AR(3) model =============================
# cd("Homework_3_Lect_5&6")

xDim=5 # state dimensionå
order=3# p=3 order of the model
T=3# Trial duration in seconds
nTrials=200
Fs=200# sampling rate in per second
dt=1/Fs
burnin=1000 # for transient removal
df=0.001#frequency resolution to be used in the parametric AR estimation
nF=1/df+1#number of frequencies evaluated in the parametric AR estimation

N=T*Fs

# The AR(3) matrices
A = zeros(xDim,xDim,order)
a=sqrt(2)
A[1,1,1] =  0.95*a
A[1,1,2] = -0.9025
A[2,1,2] =  0.5
A[3,1,3] = -0.4
A[4,1,2] = -0.5
A[4,4,1] =  0.25*a
A[4,5,1] =  0.25*a
A[5,4,1] = -0.25*a
A[5,5,1] =  0.25*a

# EXERCISE (1a): Stability/Stationarity Build the companion matrix (augmented state AR(1)) and check stability
m,n,p = size(A)
pn = (p-1)*m
Ac = [reshape(A,m,p*n); eye(pn) zeros(pn,m)]# companion matrix

# Sampling from the VAR(3) model
# The noise covariance (Using DIAGONAL COVARIANCE MATRIX FOR SIMPLICITY ...)
SIGMA =diag([0.60.5 0.3 0.3 0.6])
mu = zeros(xDim,1)
for r=1:nTrials
    r
    E=mvnrnd(mu,SIGMA,N+burnin)" #Multivariate GWN sequence
    X(:,1:order,r)=E(:,1:order)
    for k=order+1:N+burnin
        x=0
        for j=1:order
            x=x+squeeze(A(:,:,j))*squeeze(X(:,k-j,r))
        end
        X(:,k,r)=x+E(:,k)
    end
end
X=X(:,burnin+1:end,:)

# Computing the spectral matrix for the sampled VAR(p) data via multitaper ===========
NFFT=N
bandWidth = ...
removeTemporalMean=true
RemoveEnsembleMean=true
nTapers=[]
clear S
xDim = 5
for j=1:xDim
    x=squeeze(X(j,:,:))
    for k=j+1:xDim
        y=squeeze(X(k,:,:))
        [Sxx, Syy, Sxy, ~, ~, F, nTapers]= multitaperSpectrum(x,y,Fs,bandWidth,NFFT,removeTemporalMean,RemoveEnsembleMean,nTapers)
        S(j,j,:)=Sxx
        S(j,k,:)=Sxy #Cross-spectrum
        S(k,j,:)=conj(Sxy)
    end
end
S(j,j,:)=Syy

# To compute the partial coherence =======================
# S is the spectral matrix S(channel j, channel k, frequency), j,k = 1, 2, ... xDim
for j=1:xDim
    for k=j+1:xDim
        i1=setdiff([1:xDim],j)
        i2=setdiff([1:xDim],k)
        for f=1:length(F)
            Mjk = det(S(i1,i2,f))
            Mjj = det(S(i1,i1,f))
            Mkk = det(S(i2,i2,f))
            Cp(j,k,f) = abs(Mjk)/real(sqrt(Mjj*Mkk))#to avoid numerical issues, force it to be real
        end
    end
end

# Estimating VAR(p) from data ===================
[Ah,SIGMAh,Eh] = var_maxent(X,order)
DSIG = det(SIGMA)# residuals covariance matrix determinant
if DSIG <= 0
    fprintf(2,"  WARNING: residuals covariance not positive definite\n")
end
M=N*nTrials
L= -(M/2)*log(DSIG) #max loglikelihood
aic = -2*L + 2*order*xDim^2*(M/(M-order-1)) # Note AIC without correction = -2*L + 2*order*xDim^2
bic = -2*L + order*log(M)

# Transfer function H(f) and spectral matrix S(f) from fitted model
# F=[0:nF-1]*df*Fs/2
delta=1
F=linspace(0,0.5,nF)# use normalized frequency (cycles/sample) first
j=0
for f=F
    j=j+1
    H = eye(xDim) # identity matrix
    for m=1:order
        H=H-squeeze(Ah(:,:,m))*exp(-1i*m*2*pi*f)
    end
    H = inv(H)
    #S(:,:,j) = H*SIGMAh*ctranspose(H)
    S(:,:,j) = H*SIGMAh*H"
    #Note: the transpose " " " will also recognize the complex data and perform the conjugate transpose
    #Note also, however, that ." implements only the transpose, not the conjugate transpose
end
S = 2 * delta^2 * 1/(N*delta) * S #Normalize the spectral matrix
for k = 1:xDim, S(k,k,:) = real(S(k,k,:))end #make sure the diagonal has real numbers (power spectrum)
F=F*Fs# Change frequency to Hz


import IterativeSolvers: gmres!, bicgstabl!
import LinearAlgebra: ldiv!

abstract type AbstractLinearSolver end
abstract type DirectSolver <: AbstractLinearSolver end
abstract type IterativeSolver <: AbstractLinearSolver end

struct LU  <:DirectSolver end
struct GMRES  <: IterativeSolver end
struct BICGSTAB  <: IterativeSolver end

linsolve(A,b ; args...) = linsolve(A,b,LU() ; args...)

function linsolve(A,b,solver::AbstractLinearSolver ; args...)
    x = similar(b)
    linsolve!(x,A,b,solver; args...)
    return x
end

linsolve!(x,A,b ; args...) = linsolve!(x,A,b,LU() ; args...)

function linsolve!(x,A,b , ::LU ; args...)
  ldiv!(x, lu(A), b; args...)
end

function linsolve!(x,A,b,::GMRES ; args...)
    gmres!(x,A,b; args...)
end

function linsolve!(x,A,b,::BICGSTAB ; args...)
    bicgstabl!(x,A,b; args...)
end

# methods for sensitivty analysis
function linsolve!(x,x_adj,A,b,b_adj,::LU ; args...)
    F = lu(A)
    ldiv!(x, F, b; args...)
    ldiv!(x_adj, transpose(F), b_adj; args...)
end

function linsolve(A,b,b_adj,::LU ;  args...)
    x = similar(b) ; x_adj = similar(b_adj)
    linsolve!(x,x_adj,A,b,b_adj,LU(); args...)
    return x , x_adj
end


# import Base.copy
import Base.-
import Base./

export
    Vector2D,
    set!

mutable struct Vector2D{T <: AbstractFloat}
    x::T
    y::T

    function Vector2D{T}() where {T <: AbstractFloat}
        new(0.0, 0.0)
    end

    function Vector2D{T}(x::T, y::T) where {T <: AbstractFloat}
        new(x, y)
    end
end

# copy specializations
copy(v::Vector2D) = Vector2D{Float64}(v.x, v.y)

# setters/getters set_point
function set!(v::Vector2D{T}, x::T, y::T) where {T <: AbstractFloat}
    v.x = x
    v.y = y;
end

function set!(v::Vector2D{T}, from::Vector2D{T}) where {T <: AbstractFloat}
    v.x = from.x
    v.y = from.y;
end

function length(x::T, y::T) where {T <: AbstractFloat}
    sqrt(x * x + y * y)
end

function length(v::Vector2D{T}) where {T <: AbstractFloat}
    sqrt(v.x * v.x + v.y * v.y)
end

function length_sq(x::T, y::T) where {T <: AbstractFloat}
    x * x + y * y
end

# Scratch vectors
scv = Vector2D{Float64}()
scv2 = Vector2D{Float64}()

function add!(v1::Vector2D{T}, v2::Vector2D{T}, out::Vector2D{T}) where {T <: AbstractFloat}
    set!(out, v1.x + v2.x, v1.y + v2.y)
end

function sub!(v1::Vector2D{T}, v2::Vector2D{T}, out::Vector2D{T}) where {T <: AbstractFloat}
    set!(out, v1.x - v2.x, v1.y - v2.y)
end

function scale!(v::Vector2D{T}, s::T) where {T <: AbstractFloat}
    set!(v, v.x * s, v.y * s)
end

function scale!(v::Vector2D{T}, s::T, out::Vector2D{T}) where {T <: AbstractFloat}
    set!(out, v.x * s, v.y * s)
end

function div!(v::Vector2D{T}, value::T) where {T <: AbstractFloat}
    set!(v, v.x / value, v.y / value)
end

# distance between two vectors
function distance(v1::Vector2D{T}, v2::Vector2D{T}) where {T <: AbstractFloat}
    sub!(v1, v2, scv)
    length(scv)
end

# returns the angle in radians between this vector and the x axis
function angle(v::Vector2D{T}) where {T <: AbstractFloat}
    atan(v.y, v.x)
end

# Returns  multiplied to a length of 1.
# If the point is 0, it returns (1, 0)
function normalize!(v::Vector2D{T}) where {T <: AbstractFloat}
    len = length(v)
    if length(v) == 0.0
        set!(scv, 1.0, 0.0)
        return scv
    end

    div!(v, len);
end

# Calculates dot product of two points.
function dot(v1::Vector2D{T}, v2::Vector2D{T}) where {T <: AbstractFloat}
    v1.x * v2.x + v1.y * v2.y
end

# Calculates cross product of two points.
function cross(v1::Vector2D{T}, v2::Vector2D{T}) where {T <: AbstractFloat}
    v1.x * v2.y - v1.y * v2.x
end

# returns the angle in radians between two vector directions
function angle_between(v1::Vector2D{T}, v2::Vector2D{T}) where {T <: AbstractFloat}
    set!(scv, v1)
    set!(scv2, v2)

    normalize!(scv) #a2
    normalize!(scv2) #b2

    angle = atan(cross(scv, scv2), dot(scv, scv2))

    if abs(angle) < EPSILON
        return 0.0
    else
        return angle
    end
end

# Calculates perpendicular of v, rotated 90 degrees counter-clockwise -- cross(v, perp(v)) >= 0
function perpindicular(x::T, y::T) where {T <: AbstractFloat}
    Vector2D(-y, x)
end

function ccw_perpindicular!(v::Vector2D{T}) where {T <: AbstractFloat}
    set!(v, -v.y, v.x);
end

# Calculates perpendicular of v, rotated 90 degrees clockwise -- cross(v, rperp(v)) <= 0
function cw_perpindicular!(v::Vector2D{T}) where {T <: AbstractFloat}
    set!(v, v.y, -v.x);
end

function set_direction!(v::Vector2D{T}, degrees::T) where {T <: AbstractFloat}
    set!(v, cos(deg2rad(degrees)), sin(deg2rad(degrees)));
end


# This code is a julia translation of the following project:
# https://bitbucket.org/happyalu/mcep_alpha_calc

# mcepalpha computes appropriate α for a given sampling frequency.
function mcepalpha(fs::Real;
                   start::Float64=0.0,
                   stop::Float64=1.0,
                   step::Float64=0.001,
                   numpoints::Integer=1000)
    α_candidates = start:step:stop
    mel = melscale_vector(fs, numpoints)
    distances = [rms_distance(mel, warping_vector(α, numpoints)) for
                 α in α_candidates]
    return α_candidates[indmin(distances)]
end

function melscale_vector(fs::Real, len::Integer)
    step = (fs / 2.0) / len
    melscalev = 1000.0/log(2)*log(1 + step.*(1:len)./1000.0)
    return melscalev / melscalev[end]
end

function warping_vector(α::Float64, len::Integer)
    step = π / len
    ω = step .* (1:len)
    num = (1-α*α) * sin(ω)
    den = (1+α*α) * cos(ω) - 2*α
    warpfreq = atan(num./den)
    warpfreq[warpfreq .< 0] += π
    return warpfreq / warpfreq[end]
end

rms_distance(v1, v2) = sumabs2(v1 - v2) / length(v1)


function mean(x::Vector{T}) where {T}
    m = sum(x) / length(x)
    return m
end

function std(x::Vector{T}) where {T}
    m = mean(x)
    z = x .- m
    m = sqrt( sum(z.^2) / (length(x) - 1) )
    return m
end

function center(x::Vector)
    m = mean(x)
    y = copy(x)
    y .-= m
    return y
end

function cov(x::Vector{T}) where {T}
    return std(x)
end

function cov(x::Vector{T}, y::Vector{T}) where {T}
    mx = mean(x)
    my = mean(y)
    L = length(x)

    return 1 / (L - 1) * dot(x .- mx, y .- my)
end

function cov(x::Matrix{T}) where {T}
    # covariance in column way
    L = size(x,1)
    return 1/(L-1) * (x' * x)
end


# Utility functions for common algebraic operations.

"""
    rotateTensorToVectorZ(tensor, vector)

Rotate `tensor` with a rotation matrix that aligns the 3rd direction with `vector`, which is
equivalent to change the basis from (i,j,k) to (i′,j′,k′) where k′ ∥ vector.
Reference: [Tensor rotation](https://math.stackexchange.com/questions/2303869/tensor-rotation)
"""
function rotateTensorToVectorZ(tensor::AbstractMatrix{T}, v::AbstractVector{T}) where T
   k = SVector{3, T}(0.0, 0.0, 1.0)
   axis = v × k::SVector{3, T}
   if axis[1] == axis[2] == 0
      return tensor
   else
      normalize!(axis)
      angle = acos(v ⋅ k / hypot(v[1], v[2], v[3]))
      R = getRotationMatrix(axis, angle)
      return R * tensor * R'
   end
end

"""
    getRotationMatrix(axis::AbstractVector, angle) --> SMatrix{3,3}

Create a rotation matrix for rotating a 3D vector around a unit `axis` by an `angle` in
radians.
Reference: [Rotation matrix from axis and angle](https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle)

# Example

```julia
using LinearAlgebra
v = [-0.5, 1.0, 1.0]
v̂ = normalize(v)
angle = deg2rad(-74)
R = getRotationMatrix(v̂, angle)
```
"""
function getRotationMatrix(v::AbstractVector{<:AbstractFloat}, θ)
   sinθ, cosθ = sincos(eltype(v)(θ))
   tmp = 1 - cosθ
   m =  @SMatrix [
        cosθ+v[1]^2*tmp         v[1]*v[2]*tmp-v[3]*sinθ v[1]*v[3]*tmp+v[2]*sinθ;
        v[1]*v[2]*tmp+v[3]*sinθ cosθ+v[2]^2*tmp         v[2]*v[3]*tmp-v[1]*sinθ;
        v[1]*v[3]*tmp-v[2]*sinθ v[3]*v[2]*tmp+v[1]*sinθ cosθ+v[3]^2*tmp]
end

"""
    getRotationMatrix(e1::Matrix, e2::Matrix) --> SMatrix{3,3}

Obtain the rotation matrix from orthgonal base vectors `e1` to `e2`, such that a vector
``\\mathbf{u}_1`` in `e1` can be expressed as ``\\mathbf{u}_1 = M\\cdot \\mathbf{u}_2``,
where ``M`` is the rotation matrix and ``\\mathbf{u}_2`` is the same vector in `e2`.

# Example

```julia
e1 = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0]
e2 = [0.0 1.0 0.0; 1.0 0.0 0.0; 0.0 0.0 1.0]
R = getRotationMatrix(e1, e2)
```
"""
function getRotationMatrix(e1::AbstractMatrix, e2::AbstractMatrix)
   @views begin
      r11 = e1[:,1] ⋅ e2[:,1]
      r12 = e1[:,1] ⋅ e2[:,2]
      r13 = e1[:,1] ⋅ e2[:,3]
      r21 = e1[:,2] ⋅ e2[:,1]
      r22 = e1[:,2] ⋅ e2[:,2]
      r23 = e1[:,2] ⋅ e2[:,3]
      r31 = e1[:,3] ⋅ e2[:,1]
      r32 = e1[:,3] ⋅ e2[:,2]
      r33 = e1[:,3] ⋅ e2[:,3]
   end
   R = @SMatrix [r11 r12 r13; r21 r22 r23; r31 r32 r33]
end