using JuMP, BARON, Base.Test

m = Model(solver=BaronSolver())
ub = [9.422, 5.9023, 267.417085245]
@variable(m, 0 ≤ x[i=1:3] ≤ ub[i])

@NLconstraints(m, begin
    250 + 30x[1] -  6x[1]^2 == x[3]
    300 + 20x[2] - 12x[2]^2 == x[3]
    150 + 0.5*(x[1]+x[2])^2 == x[3]
end)

@NLobjective(m, Min, -x[3])

solve(m)

@test isapprox(getvalue(x[1]), 6.2934300, rtol=1e-6)
@test isapprox(getvalue(x[2]), 3.8218391, rtol=1e-6)
@test isapprox(getvalue(x[3]), 201.1593341, rtol=1e-5)
@test isapprox(getobjectivevalue(m), -201.1593341, rtol=1e-6)


using FastTransforms, LinearAlgebra

function oprec!(n::Integer, v::AbstractVector, alpha::Real, delta2::Real)
    if n > 0
        v[1] = 1
    end
    if n > 1
        v[2] = (4*alpha+8-(alpha+4)*delta2)/4
    end
    for i = 1:n-2
        v[i+2] = (((2*i+alpha+2)*(2*i+alpha+4)+alpha*(alpha+2))/(2*(i+1)*(2*i+alpha+2))*(2*i+alpha+3)/(i+alpha+3) - delta2/4*(2*i+alpha+3)/(i+1)*(2*i+alpha+4)/(i+alpha+3))*v[i+1] - (i+alpha+1)/(i+alpha+3)*(2*i+alpha+4)/(2*i+alpha+2)*v[i]
    end
    return v
end

"""
This example calculates the spectrum of the nonlocal diffusion operator:

```math
ℒ_δ u = ∫_𝕊² ρ_δ(|𝐱-𝐲|)[u(𝐱) - u(𝐲)] dΩ(𝐲),
```

defined in Eq. (2) of

    R. M. Slevinsky, H. Montanelli, and Q. Du, A spectral method for nonlocal diffusion operators on the sphere, J. Comp. Phys., 372:893--911, 2018.

available at https://doi.org/10.1016/j.jcp.2018.06.024
"""
function evaluate_lambda(n::Integer, alpha::T, delta::T) where T
    delta2 = delta*delta
    scl = (1+alpha)*(2-delta2/2)

    lambda = Vector{T}(undef, n)

    if n > 0
        lambda[1] = 0
    end
    if n > 1
        lambda[2] = -2
    end

    oprec!(n-2, view(lambda, 3:n), alpha, delta2)

    for i = 2:n-1
        lambda[i+1] *= -scl/(i-1)
    end

    p = plan_jac2jac(T, n-1, zero(T), zero(T), alpha, zero(T))

    lambda[2:end] .= p'lambda[2:end]

    for i = 2:n-1
        lambda[i+1] = ((2i-1)*lambda[i+1] + (i-1)*lambda[i])/i
    end

    for i = 2:n-1
        lambda[i+1] += lambda[i]
    end

    return lambda
end

lambda = evaluate_lambda(1024, -0.5, 1.0)
lambdabf = evaluate_lambda(1024, parse(BigFloat, "-0.5"), parse(BigFloat, "1.0"))

norm(lambda-lambdabf, Inf)/norm(lambda, Inf)


import RowEchelon
import LinearAlgebra

#=
Homework 6
Section 2.5
Problems 14
=#

# Create the matrix
A = [ 1 3 1 5 ; 5 20 6 31 ; -2 -1 -1 -4 ; -1 7 1 7]
LU = LinearAlgebra.lu(A)
print("L = \t")
println(LU.L)
print("U = \t")
println(LU.U)


using DifferentialEquations, Flux, DiffEqFlux
using Plots

u0 = [1.0,1.0]
tspan = (0.0,10.0)
tsteps = 0.0:0.1:10.0

my_nn = Chain(
  Dense(2, 32, relu),
  Dense(32, 2)) |> f64

p, re = Flux.destructure(my_nn)

function right_hand_side(du,u,p,t)
  m = re(p)
  nn_output = m(u)
  du[1] = nn_output[1]
  du[2] = nn_output[2]
end

prob = ODEProblem(right_hand_side,u0,tspan,p)
sol = solve(prob, Tsit5())

# Plot the solution
plot(sol)
savefig("Initial_NN_ODE.png")

function predict_ode(p)
  tmp_prob = remake(prob, p = p)
  Array(solve(tmp_prob, Tsit5(), saveat = tsteps))
end

function loss_ode(p)
    pred = predict_ode(p)
    loss = sum(abs2, 1.0 .- pred)
    return loss, pred
end

callback = function (p, l, pred)
  display(l)
  plt = plot(pred, ylim = (0, 6))
  display(plt)
  # Tell sciml_train to not halt the optimization. If return true, then
  # optimization stops.
  return false
end

result_ode = DiffEqFlux.sciml_train(loss_ode, p,
                                    cb = callback,
                                    maxiters = 100)

prob_final = ODEProblem(right_hand_side, u0, tspan, result_ode)
sol_final = solve(prob_final,Tsit5())
plot(sol_final)
savefig("NN_ode_optimized.png") # Both state components close to 1


using Test
using MultivariateBases
using DynamicPolynomials
@polyvar x y

@testset "Linear" begin
    basis = MonomialBasis([x, y])
    @test polynomialtype(basis, Int) == polynomialtype(x, Int)
    @test polynomial(i -> i^2, basis) == x + 4y
    @test coefficients(x + 4y, MonomialBasis) == [1, 4]
end
@testset "Affine" begin
    # It will be sorted and 1 will be moved at the end
    basis = MonomialBasis([1, x, y])
    @test polynomialtype(basis, Int) == polynomialtype(x, Int)
    @test polynomial(i -> i^2, basis) == x + 4y + 9
    @test coefficients(9 + x + 4y, MonomialBasis) == [1, 4, 9]
end
@testset "Quadratic" begin
    basis = MonomialBasis([x^2, x*y, y^2])
    @test polynomialtype(basis, Int) == polynomialtype(x, Int)
    @test polynomial(i -> i^2, basis) == x^2 + 4x*y + 9y^2
    @test coefficients(x^2 + 4x*y + 9y^2, MonomialBasis) == [1, 4, 9]
end
@testset "API degree = $degree" for degree in 0:3
    api_test(MonomialBasis, degree)
end


"""
# Algorithms with numbers

- Basic arithmetic
    - Addition
    - Bases and logs
    - Multiplication and division
- Modular arithmetic
    - Substitution rule
    - Modular addition and multiplication
    - Two's complement
    - Modular exponentiation
    - Euclid's algorithm for greatest common divisor
    - Euclid's rule
    - An extension of Euclid's algorithm
    - Modular division
    - Is your social security number a prime?
- Primality testing
    - Fermat's little theorem
    - Hey, that was group theory!
    - Generating random primes
    - Lagrange's prime number theorem
    - Carmichael numbers
    - Randomized algorithms: a virtual chapter
- Cryptography
    - An application of number theory?
    - Private-key schemes: one-time pad and AES
    - RSA
- Universal Hashing
    - Hash tables
    - Families of hash functions
"""
module Chapter1

export
    multiply,
    divide,
    modexp,
    euclid,
    extendedeuclid,
    primality,
    primality2

"""
    multiply(x, y)

Compute the product of two integers `x` and `y`.

# Examples
```jldoctest
julia> multiply(7, 8)
56
```
"""
function multiply(x::Integer, y::Integer)
    if y == 0
        return 0
    end

    z = multiply(x, y ÷ 2)
    if iseven(y)
        return 2z
    else
        return x + 2z
    end
end

"""
    divide(x, y)

Compute the quotient and remainder of `x ÷ y`.

# Examples
```jldoctest
julia> divide(5, 3)
(1, 2)
```
"""
function divide(x::Integer, y::Integer)
    if x == 0
        return (q,r) = (0,0)
    end

    (q,r) = divide(x ÷ 2, y)
    q = 2q
    r = 2r
    if isodd(x)
        r = r + 1
    end
    if r >= y
        r = r - y
        q = q + 1
    end
    return (q,r)
end

"""
    modexp(x, y, N)

Compute the modular exponentation `x^y mod N`.

# Examples
```jldoctest
julia> modexp(2, 3, 5)
3
```
"""
function modexp(x::Integer, y::Integer, N::Integer)
    if y == 0
        return 1
    end

    z = modexp(x, y ÷ 2, N)
    if iseven(y)
        return mod(z^2, N)
    else
        return mod(x * z^2, N)
    end
end

"""
    euclid(a, b)

Using Euclid's algorithm, compute the greatest common divisor
between two integers `a` and `b` where `a >= b >= 0`.

# Examples
```jldoctest
julia> euclid(25, 20)
5
```
"""
function euclid(a::Integer, b::Integer)
    @assert a >= b >= 0
    if b == 0
        return a
    end

    return euclid(b, mod(a, b))
end

"""
    extendedeuclid(a, b)

A simple extension of Euclid's algorithm.

- Input: Two positive integers `a` and `b` with `a >= b >= 0`.
- Ouput: Integers `x`, `y`, `d` such that `d = gcd(a,b)` and `ax + by = d`.

# Examples
```jldoctest
julia> extendedeuclid(9, 6)
(1, -1, 3)
```
"""
function extendedeuclid(a::Integer, b::Integer)
    @assert a >= b && b >= 0
    if b == 0
        return (1, 0, a)
    end

    (x′, y′, d) = extendedeuclid(b, mod(a, b))
    return (y′, x′ - a ÷ b * y′, d)
end

"""
    isprime(N)

Determine whether an integer `N` is prime using Fermat's little theorem.

# Examples
```jldoctest
julia> primality(5)
true
```
"""
function primality(N::Integer)
    if N < 2
        return false
    end

    a = rand(1:(N - 1))
    return powermod(a, N - 1, N) == 1
end

"""
    primality2(N)

Determine whether an integer `N` is prime with low error probability.
"""
function primality2(N::Integer)
    if N < 2
        return false
    end

    a = rand(1:(N - 1), N - 1)
    if all([powermod(a[i], N - 1, N) == 1 for i ∈ 1:(N - 1)])
        return true
    else
        return false
    end
end

end # module


using Distributions, StatsBase, Random
Random.seed!(1)

dist1 = TriangularDist(0,10,5)
dist2 = DiscreteUniform(1,5)
theorMean1, theorMean2 = mean(dist1), mean(dist2)

N = 10^6
data1 = rand(dist1,N)
data2 = rand(dist2,N)
estMean1, estMean2 = mean(data1), mean(data2)

println("Symmetric Triangular Distiribution on [0,10] has mean $theorMean1
	(estimated: $estMean1)")
println("Discrete Uniform Distiribution on {1,2,3,4,5} has mean $theorMean2
	(estimated: $estMean2)")


"""
    MeanSquareError(NeuralNetwork)

Sum of square error loss function.

    Usage:
```julia-repl
MeanSquareError(NeuralNetwork)
```
Arguments:
- `NeuralNetwork` : The NeuralNetwork structure.
"""
function MeanSquareError(NeuralNetwork)
	ΣSE = 0.0

	for i in 1:size(NeuralNetwork.y, 1)
		ΣSE += (NeuralNetwork.y[i] - NeuralNetwork.output[i]) ^ 2.0
    end

    loss = 1.0 / size(NeuralNetwork.y) * ΣSE
    return loss
end


# Generates an AR(p) process with coefficients `ϕ`.
# `ϕ` should be provided as a vector and it represents the coefficients of the AR model.
# Hence the order of the generated process is equal to the length of `ϕ`.
# `s` indicates the total length of the series to be generated.
function generate_process(ϕ::AbstractVector{Float32}, s::Int)
    @assert s > 0 "s must be positive"
    # Generate white noise
    ϵ = randn(Float32, s)
    # Initialize time series
    X = zeros(Float32, s)
    p = length(ϕ)
    X[1] = ϵ[1]
    # Reverse the order of the coefficients for multiplication later on
    ϕ = reverse(ϕ)
    # Fill first p observations
    for t ∈ 1:p-1
        X[t+1] = X[1:t]'ϕ[1:t] + ϵ[t+1]
    end
    # Compute values iteratively
    for t ∈ p+1:s
        X[t] = X[t-p:t-1]'ϕ + ϵ[t]
    end
    X
end

# Create batches of a time series `X` by splitting the series into
# sequences of length `s`. Each new sequence is shifted by `r` steps.
# When s == r,  the series is split into non-overlapping batches.
function batch_timeseries(X, s::Int, r::Int)
    @assert r > 0 "r must be positive"
    # If X is passed in format T×1, reshape it
    if isa(X, AbstractVector)
        X = permutedims(X)
    end
    T = size(X, 2)
    @assert s ≤ T "s cannot be longer than the total series"
    # Ensure uniform sequence lengths by dropping the first observations until
    # the total sequence length matches a multiple of the batchsize
    X = X[:, ((T - s) % r)+1:end]
    [X[:, t:r:end-s+t] for t ∈ 1:s] # Output
end


using Catlab, Catlab.Theories
using Catlab.CategoricalAlgebra
using Catlab.Graphs
using Catlab.Graphics
using Catlab.Graphics.Graphviz
#import Catlab.Graphics.Graphviz: to_graphviz, to_graphviz_property_graph

using Colors
draw(g) = to_graphviz(g, node_labels=true, edge_labels=true)

GraphvizGraphs.to_graphviz(f::ACSetTransformation; kw...) =
  to_graphviz(GraphvizGraphs.to_graphviz_property_graph(f; kw...))

function GraphvizGraphs.to_graphviz_property_graph(f::ACSetTransformation; kw...)
  pg = GraphvizGraphs.to_graphviz_property_graph(dom(f); kw...)
  vcolors = hex.(range(colorant"#0021A5", stop=colorant"#FA4616", length=nparts(codom(f), :V)))
  ecolors = hex.(range(colorant"#6C9AC3", stop=colorant"#E28F41", length=nparts(codom(f), :E)))
  hex.(colormap("Oranges", nparts(codom(f), :V)))
  for v in vertices(dom(f))
    fv = f[:V](v)
    set_vprops!(pg, v, Dict(:color => "#$(vcolors[fv])"))
  end
  for e in edges(dom(f))
    fe = f[:E](e)
    set_eprops!(pg, e, Dict(:color => "#$(ecolors[fe])"))
  end
  pg
end

to_graphviz(Catlab.Graphs.BasicGraphs.TheoryGraph)

# # The Category of Graphs
#
# The Theory of Graphs is given by the following Schema:
# ```julia
# @present TheoryGraph(FreeSchema) begin
#   V::Ob
#   E::Ob
#   src::Hom(E,V)
#   tgt::Hom(E,V)
# end

# """ Abstract type for graphs, aka directed multigraphs.
# """
# @abstract_acset_type AbstractGraph <: HasGraph

# """ A graph, also known as a directed multigraph.
# """
# @acset_type Graph(TheoryGraph, index=[:src,:tgt]) <: AbstractGraph
# ```
#
# That is all we need to do to generate the functor category [TheoryGraph, FinSet].
# Catlab knows how to take a finitely presented category and generate all the data structures
# that you need to represent functors into FinSet and natural transformations between those functors.
# Note: the index=[:src, :tgt] keyword argument tells Catlab that you want to have an efficient index
# the preimages of those morphisms. in this example, we want to be able to find the incoming and
# outgoing edges of a vertex in O(1) time.

# ## Creating some Graphs

# Once you have fixed the schema (aka indexing category or theory), you can make some instances.
# Catlab has a DSL for specifying instances of any schema. It is called `@acset`.
# In order to specify a Functor F=(F₀, F₁) into FinSet, you need to provide some data.
#     1. For every A:Ob(C), you need F₀(A):FinSet
#     2. For every f:A→B, you need to specify a FinFunction F₁(f):F₀(A)→F₀(B)
# If the theory C has some equations, the data you provide would have to also satisfy those equations.
# The theory of graphs has no equations, so there are no constraints on the data you provide,
# except for those that come from functoriality.

e = @acset Graphs.Graph begin
    V = 2
    E = 1
    src = [1]
    tgt = [2]
end

draw(e)

# a wedge is two edges that share a target
w = @acset Graphs.Graph begin
    V = 3
    E = 2
    src=[1,3]
    tgt=[2,2]
end

draw(w)

# The CSet API generalizes the traditional Graph API

parts(w, :V)  # vertex set

parts(w,:E) # edge set

w[:src] # source map

w[:tgt] # target map

incident(w, 1, :src) # edges out of vertex 1

incident(w, 2, :tgt) # edges into vertex 2

w[incident(w, 2, :tgt), :src] # vertices that are the source of edges whose target is vertex 2

w[incident(w, 1, :src), :tgt] # vertices that are the target of edges whose src is vertex 1

# ### Exercise:
# 1. Use the @acset macro to make a graph with at least 5 vertices
# 2. Draw the graph
# 3. Compute in neighbors and out neighbors and make sure they match your expectations.
# 4. Write a function that computes the 2-hop out-neighbors of a vertex.

# # Graph Homomorphisms
# We can construct some graph homomorphisms between our two graphs.
# What data do we need to specify?

ϕ = ACSetTransformation(e,w,E=[1], V=[1,2])

is_natural(ϕ)

# The ACSetTransformation constructor does not automatically validate that the naturality squares commute!

ϕᵦ = ACSetTransformation(e,w,E=[1], V=[3,2])
is_natural(ϕᵦ)

# Our ϕᵦ in't natural because the edge map e₁ ↦ e₁ is not consistent with our vertex map, which sends v₁ ↦ v₃ and v₂ ↦ v₂. We can fix this by sending e₁ to e₂

ϕᵦ′ = ACSetTransformation(e,w,E=[2], V=[3,2])
is_natural(ϕᵦ′)

# So how does Catlab store the data of the natural transformation?
# the domain
ϕ.dom
# the codomain
ϕ.codom
# the components
ϕ.components
# you can see the components using standard indexing notation with the object names. Notice that while CSets are indexed by morphisms, natural transformations are indexed by objects.
ϕ[:V]
ϕ[:E]
# We can check the  naturality squares ourselves
# The sources are preserved: `src ⋅ ϕᵥ == ϕₑ ⋅ src`
ϕ[:V](dom(ϕ)[:,:src]) == codom(ϕ)[collect(ϕ[:E]), :src]
# The targets are preserved: `tgt ⋅ ϕᵥ == ϕₑ ⋅ tgt`
ϕ[:V](dom(ϕ)[:,:tgt]) == codom(ϕ)[collect(ϕ[:E]), :tgt]

# This approach generalizes to the following:
#
# ```julia
# function is_natural(α::ACSetTransformation{S}) where {S}
#    X, Y = dom(α), codom(α)
#    for (f, c, d) in zip(hom(S), dom(S), codom(S))
#      Xf, Yf, α_c, α_d = subpart(X,f), subpart(Y,f), α[c], α[d]
#      all(Yf[α_c(i)] == α_d(Xf[i]) for i in eachindex(Xf)) || return false
#    end
#    return true
# end
# ```
#
# Notice how we iterate over the homs in the schema category S `(f, c, d) in zip(hom(S), dom(S), codom(S))` We get one universally quantified equation `all(Yf[α_c(i)] == α_d(Xf[i]) for i in eachindex(Xf))` for each morphism in the indexing category
#
# ### Exercise:
# 1. Take your graph from the previous exercise and construct a graph homomorphism from the wedge (w) into it.
# 2. Check that the naturality equations are satisfied.
# 3. Explain why we don't need to specify any data for the source and target morphisms in TheoryGraph when definining a graph homomorphism

# ## Finding Homomorphisms Automatically
# As you saw in the previous exercise, constructing a natural transformation can be quite tedious. We want computers to automate tedious things for us. So we use an algorithm to enumerate all the homomorphisms between two CSets.

# CSet homomorphisms f:A→B are ways of finding a part of B that is shaped like A. You can view this as pattern matching. The graph A is the pattern and the graph B is the data. A morphism f:A→B is a collection of vertices and edges in B that is shaped like A. Note that you can ask Catlab to enforce constraints on the homomorphisms it will find including computing monic (injective) morphisms by passing the keyword `monic=true`. A monic morphism into B is a subobject of B.  You can pass `iso=true` to get isomorphisms.

t = @acset Graphs.Graph begin
    V = 3
    E = 3
    src = [1,2,1]
    tgt = [2,3,3]
end

draw(t)

T = @acset Graphs.Graph begin
    V = 6
    E = 9
    src = [1,2,1, 3, 1,5,2,2,4]
    tgt = [2,3,3, 4, 4,6,5,6,6]
end

draw(T)

# The simplest pattern in a graph is just a single edge and each homomorphism ϕ:e→G is a single edge in G.
length(homomorphisms(e, T, monic=true)) == nparts(T,:E) # number of edges

length(homomorphisms(t, T))

# We can define this helper function to print out all the homomorphisms between graphs. Because all our graphs are simple, we only need to look at the vertex components.
graphhoms(g,h) = begin
    map(homomorphisms(g,h)) do ϕ
        collect(ϕ[:V])
    end
end

graphhoms(t, T)

# Homs ϕ:t→T are little triangles in T, but homs ϕ:T→t are colorings of T with 3 colors. The fact that there are edges in t that are missing, means that it provides constraints on what graphs have morphisms into it. For example, there are no morphisms T→t.

graphhoms(T, t)

# The reason we don't have a morphism into t is vertices 2,3,4,5 aren't arranged into a triangle. We can relax those constraints by adding loops to the codomain. Loops in the codomain allow you to merge adjacent vertices when you construct the homomorphism.

add_loops!(g) = add_parts!(g, :E, nparts(g,:V), src=parts(g,:V), tgt=parts(g,:V))
add_loops(g) = begin
    h = copy(g)
    add_loops!(h)
    return h
end

draw(add_loops(t))

# Once we add loops, then we have morphisms.

length(homomorphisms(T,add_loops(t)))

# ## Bipartite Graphs
# Many computer science problems involve graphs that have two types of vertices. For example, when matching students to classes, you might represent the students as one type of vertex and the classes as another type of vertex. Then the edges (s,c) represent "student s is enrolled in class c". In this scenario there can be no edges from a class vertex to another class vertex, or from a student vertex to a student vertex. Graphs for which there exists such a classification are called bipartite graphs. In Category Theory, we shift from thinking about graphs with properties to graph homomorphisms that witness that property and think of bipartitioned graphs.

# First we construct a bipartite graph:
sq = apex(product(add_loops(e), add_loops(e)))
rem_parts!(sq, :E, [1,5,6,8,9])
draw(sq)

# We will use the symmetric edge graph to identify the bipartitions of this graph.
esym = @acset Graphs.Graph begin
    V = 2
    E = 2
    src = [1,2]
    tgt = [2,1]
end

draw(id(esym))
# There are two ways to bipartition sq.
graphhoms(sq, esym)

# This comes from the fact that esym has 2 automorphisms!
graphhoms(esym, esym)

# the first coloring
draw(homomorphisms(sq, esym)[1])

# but we can also swap the roles of the colors
draw(homomorphisms(sq, esym)[2])

# ### Exercise:
# 1. Construct a graph representation of a checkerboard
# 2. Draw the two bipartitions of the checkerboard

# We can generalize the notion of Bipartite graph to any number of parts. I like to call Kₖ the k-coloring classifier because homomorphims into α:G → Kₖ are k-colorings of G.

clique(k::Int) = begin
    Kₖ = Graphs.Graph(k)
    for i in 1:k
        for j in 1:k
            if j ≠ i
                add_parts!(Kₖ, :E, 1, src=i, tgt=j)
            end
        end
    end
    return Kₖ
end

K₃ = clique(3)
draw(id(K₃))

# Our graph T is not 2-colorable,
length(homomorphisms(T, esym))

# but we can use 3 colors to color T.
draw(homomorphism(T, K₃))

# ### Exercise:
# 1. Find a graph that is not 3-colorable
# 2. Find a graph that is not 4-colorable

# ## Homomorphisms in [C, Set] are like Types
# Any graph can play the role of the codomain. If you pick a graph that is incomplete, you get a more constrained notion of coloring where there are color combinations that are forbidden.
triloop = @acset Graphs.Graph begin
    V = 3
    E = 3
    src = [1,2,3]
    tgt = [2,3,1]
end

draw(id(triloop))

# With this graph, we can pick out directed 3-cycles in a graph like T2,
T2 = @acset Graphs.Graph begin
    V = 6
    E = 6
    src = [1,2,3,4,5,6]
    tgt = [2,3,1,5,6,4]
end
graphhoms(T2, triloop)

# and we can draw those cyclic roles with colors
draw(homomorphisms(T2, triloop)[1])

T3 = @acset Graphs.Graph begin
    V = 6
    E = 7
    src = [1,2,3,4,5,6, 2]
    tgt = [2,3,1,5,6,4, 4]
end
graphhoms(T3, triloop)

# Using the colors as shown:
draw(id(triloop))

# We can see our coloring of T3:
draw(homomorphisms(T3, triloop)[1])


"""Calculte the Continuous Ranked Probability Score (CRPS) of an ensemble array wrt
a true observation o. In-place version that will sort the array."""
function CRPS!(x::Array{T,1},o::T) where T
    sort!(x)
    n = length(x)
    dp = 1/n                                # probability increment per xi in x

    # find index m to split integration over x into heaviside=0 or 1
    m = findfirst(xi -> xi >= o,x)

    # integration from o to x if o is outside of the range of x
    s = m==1 ? x[1]-o : 0.0

    # integrate over the heaviside=0 part
    for i in 1:(m == nothing ? n-1 : m-1)
        s += (i*dp)^2*(x[i+1]-x[i])
    end

    # integrate over the heaviside=1 part
    for i in (m == nothing ? n : m):n-1
        s += (i*dp-1)^2*(x[i+1]-x[i])
    end

    # integration from x to o if o is outside of the range of x
    s += (m == nothing) ? o-x[end] : 0.0

    return s
end

"""Calculte the Continuous Ranked Probability Score (CRPS) of an ensemble array wrt
a true observation o."""
CRPS(x::Array{T,1},o::T) where T = CRPS!(copy(x),o)
CRPS(x::Array{T,1},o::Real) where T = CRPS!(copy(x),T(o))


using Memoize, IterTools, LinearAlgebra

@memoize function mem_det(A::Array{Int,2})
    return round(Int, det(A))
end

"""
`istum(A)` determines if the integer matrix `A`
is totally unimodular. This may be called with an
optional second argument `istum(A,true)` that,
in case the matrix is not totally unimodular, will
report the first submatrix found whose determinant
is not in `{-1,0,1}`.

**Warning**: This is not an efficient function.
"""
function istum(A::Array{Int,2}, verbose::Bool = false)
    r, c = size(A)
    n = min(r, c)

    for k = 1:n
        rows = subsets(collect(1:r), k)
        cols = subsets(collect(1:c), k)
        for x in rows
            for y in cols
                B = A[x, y]
                d = mem_det(B)
                if abs(d) > 1
                    if verbose
                        println("Rows: ", x)
                        println("Cols: ", y)
                        println("$B has determinant $d")
                    end
                    return false
                end
            end
        end
    end
    return true
end


#SIR-X Model

using DifferentialEquations

beta = 0.215; gamma = 0.07; N = 7000;
eta0 = 0.003; eta = 0.003

"""SIR-X Model
Extension of the basic SIR model

"""
function SIR_X!(du,u,p,t)
    du[1] = -beta* u[1]/N*u[2] - eta0*u[1]                          #S
    du[2] = beta*u[1]/N*u[2] - gamma*u[2] - eta0*u[2] - eta*u[2]    #I
    du[3] = gamma*u[2] + eta0*u[1]                                  #R
    du[4] = (eta0 + eta)*u[2]                                       #X
end

   u0 = [N-15.0;15.0;0.0;0.0]
   tspan = (0.0,100.0)
   prob = ODEProblem(SIR_X!,u0,tspan)
   sol = solve(prob)

   plot(sol)


# Functions related to the counting bound.

module CountingBound

export countingBound, approxNumMaximalCliques1

"""
Counting bound, based on Shannon's argument.
  m: number of edges
  w: number of 'wires' -- that is, log2(number of functions),
    which is the number of bits needed to specify a function
  Returns: average number of NAND gates (with unbounded fan-in)
    required to compute any of those functions.
    (This may not be an integer).
"""
function countingBound(m, w)
  m = BigFloat(m)
  w = BigFloat(w)
  b = m - 0.5
  # the "-1" here is because this is the average, not the max.
  sqrt(2*w + b*b) - b - 1
end

"""
  Approximate number of maximal hypercliques of some size.
  Note that k < r < n .
  Also, the precision of what's returned can be set by setprecision().
  k: number of vertices per hyperedge
  r: number of vertices in the clique
  n: vertices in the larger graph
  Returns: expected number of maximal hypercliques. (This is
		approximate, but presumably it's more accurate for larger
    numbers).
"""
function approxNumMaximalCliques1(k, r, n)
  k = BigInt(k)
  r = BigInt(r)
  n = BigInt(n)
  one = BigInt(1)
  two = BigInt(2)

  # probability that one of those is not covered by a larger clique
  a = one << binomial(r, k-one)
  # print("computed a\n")
  pNumerator = (a-one) ^ (n-r)
  # print("computed numerator\n")
  pDenominator = a ^ (n-r)
  # print("computed denominator\n")

  # expected number of r-cliques should be equivalent to:
  # numRCliques = Rational(binomial(n, r), two ^ binomial(r, k))
  # # result is number of cliques, * prob. they're maximal
  # numRCliques * (pNumerator / pDenominator)
  r = (pNumerator * binomial(n, r)) /
    (pDenominator * (one << binomial(r, k)))
  # ??? is this off by two? I don't think so.
  r
end

"""
  Approximate number of maximal hypercliques of some size
(alternate take).
  Note that k < r < n .
  Also, the precision of what's returned can be set by setprecision().
  k: number of vertices per hyperedge
  r: number of vertices in the clique
  n: vertices in the larger graph
  Returns: number of maximal hypercliques. This is approximate, because
    it's the expected number. (Presumably it's more accurate for larger
    numbers).
"""
function approxNumMaximalCliques2(k, r, n)
  k = BigInt(k)
  r = BigInt(r)
  n = BigInt(n)
  one = BigInt(1)
  two = BigInt(2)

  # probability that one of those is not covered by a larger clique
  a = one << binomial(r, k-one)
  print("computed a\n")
  pNumerator = (a-one) ^ (n-r)
	# ??? how is this implemented for BigInts?
	# also, if a = 1111111... in binary, is there a cheaper way to
	# compute this?
  print("computed numerator\n")
  pDenominator = a ^ (n-r)
	# FIXME is denominator all powers of two? If so, presumably could
	# replace the division with a bit shift
  print("computed denominator\n")

  # expected number of r-cliques should be equivalent to:
  # numRCliques = Rational(binomial(n, r), two ^ binomial(r, k))
  # # result is number of cliques, * prob. they're maximal
  # numRCliques * (pNumerator / pDenominator)
  r = (pNumerator * binomial(n, r)) /
    (pDenominator * (one << binomial(r, k)))

  # ??? is this off by two? (doesn't seem to be, for small k, r, n)
  r
end

end


### Edge and Gradient Related Image Operations ###

# Phase (angle of steepest gradient ascent), calculated from X and Y gradient images
"""
    phase(grad_x, grad_y) -> p

Calculate the rotation angle of the gradient given by `grad_x` and
`grad_y`. Equivalent to `atan(-grad_y, grad_x)`, except that when both `grad_x` and
`grad_y` are effectively zero, the corresponding angle is set to zero.
"""
function phase(grad_x::T, grad_y::T, tol=sqrt(eps(T))) where T<:Number
    atan(-grad_y, grad_x) * ((abs(grad_x) > tol) | (abs(grad_y) > tol))
end
phase(grad_x::Number,   grad_y::Number)   = phase(promote(grad_x, grad_y)...)
phase(grad_x::NumberLike, grad_y::NumberLike) = phase(gray(grad_x), gray(grad_y))

phase(grad_x::AbstractRGB, grad_y::AbstractRGB) = phase(vecsum(grad_x), vecsum(grad_y))

magnitude_phase(grad_x::NumberLike, grad_y::NumberLike) =
    hypot(grad_x, grad_y), phase(grad_x, grad_y)

function magnitude_phase(grad_x::AbstractRGB, grad_y::AbstractRGB)
    gx, gy = vecsum(grad_x), vecsum(grad_y)
    magnitude_phase(gx, gy)
end

vecsum(c::AbstractRGB) = float(red(c)) + float(green(c)) + float(blue(c))

## TODO? orientation seems nearly redundant with phase, deprecate?

"""
    orientation(grad_x, grad_y) -> orient

Calculate the orientation angle of the strongest edge from gradient images
given by `grad_x` and `grad_y`.  Equivalent to `atan(grad_x, grad_y)`.  When
both `grad_x` and `grad_y` are effectively zero, the corresponding angle is set to
zero.
"""
function orientation(grad_x::T, grad_y::T, tol=sqrt(eps(T))) where T<:Number
    atan(grad_x, grad_y) * ((abs(grad_x) > tol) | (abs(grad_y) > tol))
end
orientation(grad_x::Number,   grad_y::Number)   = orientation(promote(grad_x, grad_y)...)
orientation(grad_x::NumberLike, grad_y::NumberLike) = orientation(gray(grad_x), gray(grad_y))

orientation(grad_x::AbstractRGB, grad_y::AbstractRGB) = orientation(vecsum(grad_x), vecsum(grad_y))

# Magnitude of gradient, calculated from X and Y image gradients
"""
```
m = magnitude(grad_x, grad_y)
```

Calculates the magnitude of the gradient images given by `grad_x` and `grad_y`.
Equivalent to `sqrt(grad_x.^2 + grad_y.^2)`.

Returns a magnitude image the same size as `grad_x` and `grad_y`.
"""
magnitude(grad_x::AbstractArray, grad_y::AbstractArray) = hypot.(grad_x, grad_y)

Base.hypot(x::AbstractRGB, y::AbstractRGB) = hypot(vecsum(x), vecsum(y))

phase(grad_x::AbstractArray, grad_y::AbstractArray) = phase.(grad_x, grad_y)

# Orientation of the strongest edge at a point, calculated from X and Y gradient images
# Note that this is perpendicular to the phase at that point, except where
# both gradients are close to zero.

orientation(grad_x::AbstractArray{T}, grad_y::AbstractArray{T}) where {T} = orientation.(grad_x, grad_y)

# Return both the magnitude and phase in one call
"""
    magnitude_phase(grad_x, grad_y) -> m, p

Convenience function for calculating the magnitude and phase of the gradient
images given in `grad_x` and `grad_y`.  Returns a tuple containing the magnitude
and phase images.  See `magnitude` and `phase` for details.
"""
function magnitude_phase(grad_x::AbstractArray{T}, grad_y::AbstractArray{T}) where T
    m = similar(grad_x, eltype(T))
    p = similar(m)
    for I in eachindex(grad_x, grad_y)
        m[I], p[I] = magnitude_phase(grad_x[I], grad_y[I])
    end
    m, p
end

# Return the magnitude and phase of the gradients in an image
function magnitude_phase(img::AbstractArray, method::Function=KernelFactors.ando3, border::AbstractString="replicate")
    grad_x, grad_y = imgradients(img, method, border)
    return magnitude_phase(grad_x, grad_y)
end

# Return the x-y gradients and magnitude and phase of gradients in an image
"""
```
grad_y, grad_x, mag, orient = imedge(img, kernelfun=KernelFactors.ando3, border="replicate")
```

Edge-detection filtering. `kernelfun` is a valid kernel function for
[`imgradients`](@ref), defaulting to [`KernelFactors.ando3`](@ref).
`border` is any of the boundary conditions specified in `padarray`.

Returns a tuple `(grad_y, grad_x, mag, orient)`, which are the horizontal
gradient, vertical gradient, and the magnitude and orientation of the strongest
edge, respectively.
"""
function imedge(img::AbstractArray, kernelfun=KernelFactors.ando3, border::AbstractString="replicate")
    grad_y, grad_x = imgradients(img, kernelfun, border)
    mag = magnitude(grad_y, grad_x)
    orient = orientation(grad_y, grad_x)
    return (grad_y, grad_x, mag, orient)
end

# Thin edges
"""
```
thinned = thin_edges(img, gradientangle, [border])
thinned, subpix = thin_edges_subpix(img, gradientangle, [border])
thinned, subpix = thin_edges_nonmaxsup(img, gradientangle, [border]; [radius::Float64=1.35], [theta=pi/180])
thinned, subpix = thin_edges_nonmaxsup_subpix(img, gradientangle, [border]; [radius::Float64=1.35], [theta=pi/180])
```

Edge thinning for 2D edge images.  Currently the only algorithm available is
non-maximal suppression, which takes an edge image and its gradient angle, and
checks each edge point for local maximality in the direction of the gradient.
The returned image is non-zero only at maximal edge locations.

`border` is any of the boundary conditions specified in `padarray`.

In addition to the maximal edge image, the `_subpix` versions of these functions
also return an estimate of the subpixel location of each local maxima, as a 2D
array or image of `Graphics.Point` objects.  Additionally, each local maxima is
adjusted to the estimated value at the subpixel location.

Currently, the `_nonmaxsup` functions are identical to the first two function
calls, except that they also accept additional keyword arguments.  `radius`
indicates the step size to use when searching in the direction of the gradient;
values between 1.2 and 1.5 are suggested (default 1.35).  `theta` indicates the
step size to use when discretizing angles in the `gradientangle` image, in
radians (default: 1 degree in radians = pi/180).

Example:

```
g = rgb2gray(rgb_image)
gx, gy = imgradients(g)
mag, grad_angle = magnitude_phase(gx,gy)
mag[mag .< 0.5] = 0.0  # Threshold magnitude image
thinned, subpix =  thin_edges_subpix(mag, grad_angle)
```
"""
thin_edges(img::AbstractArray{T,2}, gradientangles::AbstractArray, border::AbstractString="replicate") where {T} =
    thin_edges_nonmaxsup(img, gradientangles, border)
thin_edges_subpix(img::AbstractArray{T,2}, gradientangles::AbstractArray, border::AbstractString="replicate") where {T} =
    thin_edges_nonmaxsup_subpix(img, gradientangles, border)

# Code below is related to non-maximal suppression, and was ported to Julia from
# http://www.csse.uwa.edu.au/~pk/research/matlabfns/Spatial/nonmaxsup.m
# (Please conserve the original copyright below.)

# NONMAXSUP - Non-maxima suppression
#
# Usage:
#          (im,location) = nonmaxsup(img, gradientangles, radius);
#
# Function for performing non-maxima suppression on an image using
# gradient angles.  Gradient angles are assumed to be in radians.
#
# Input:
#   img - image to be non-maxima suppressed.
#
#   gradientangles - image containing gradient angles around each pixel in radians
#                    (-pi,pi)
#
#   radius  - Distance in pixel units to be looked at on each side of each
#             pixel when determining whether it is a local maxima or not.
#             This value cannot be less than 1.
#             (Suggested value about 1.2 - 1.5)
#
# Returns:
#   im        - Non maximally suppressed image.
#   location  - `Graphics.Point` image holding subpixel locations of edge
#               points.
#
# Notes:
#
# This function uses bilinear interpolation to estimate
# intensity values at ideal, real-valued pixel locations on each side of
# pixels to determine if they are local maxima.

# Copyright (c) 1996-2013 Peter Kovesi
# Centre for Exploration Targeting
# The University of Western Australia
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# The Software is provided "as is", without warranty of any kind.

# December  1996 - Original version
# September 2004 - Subpixel localization added
# August    2005 - Made Octave compatible
# October   2013 - Final thinning applied to binary image for Octave
#                  compatbility (Thanks to Chris Pudney)
# June      2014 - Ported (and modified significantly) to Julia (Kevin Squire)

import .Point

if !applicable(zero, Point)
    import Base.zero
    zero(::Type{Point}) = Point(0.0,0.0)
end

# Used to encode the sign, integral, and fractional components of
# an offset from a coordinate
struct CoordOffset
    s::Int      # sign
    i::Int      # integer part
    f::Float64  # fractional part
end

CoordOffset(x::Float64) = ((frac,i) = modf(x); CoordOffset(sign(frac), round(Int, i), abs(frac)))
(-)(off::CoordOffset) = CoordOffset(-off.s,-off.i, off.f)
(*)(x::Number, off::CoordOffset) = x*(off.i + off.s*off.f)
(*)(off::CoordOffset, x::Number) = x*(off.i + off.s*off.f)
(+)(x::Number, off::CoordOffset) = x + off.i + off.s*off.f
(+)(off::CoordOffset, x::Number) = x + off.i + off.s*off.f

# Precalculate x and y offsets relative to centre pixel for each orientation angle
function _calc_discrete_offsets(θ, radius)

    θ_count = round(Int, 2π/θ)
    θ = 2π/θ_count
    angles = (0:θ_count)*θ

    # x and y offset of points at specified radius and angles
    # from each reference position.

    xoffs = [CoordOffset( x) for x in  radius * cos.(angles)]
    yoffs = [CoordOffset(-y) for y in  radius * sin.(angles)]

    return θ, xoffs, yoffs
end

_discretize_angle(angle::AbstractFloat, invθ) =
    angle < 0 ? round(Int, (angle + 2π)*invθ)+1 : round(Int, angle*invθ)+1

# Interpolate the value of an offset from a particular pixel
#
# Returns (interpolated value, min_value of adjacent pixels in direction of offset)
#
# The second value is made available to eliminate double edges; if the value at
# (x,y) is less than the value or values adjacent to it in the direction of the
# gradient (xoff,yoff), then it is not a local maximum

function _interp_offset(img::AbstractArray, x::Integer, y::Integer, xoff::CoordOffset, yoff::CoordOffset, Ix, Iy, pad)
    fx = Ix[x + xoff.i + pad]
    fy = Iy[y + yoff.i + pad]
    cx = Ix[x + xoff.i + xoff.s + pad]
    cy = Iy[y + yoff.i + yoff.s + pad]

    tl = img[fy,fx]    # Value at bottom left integer pixel location.
    tr = img[fy,cx]    # bottom right
    bl = img[cy,fx]    # top left
    br = img[cy,cx]    # top right

    upperavg = tl + xoff.f * (tr - tl)  # Now use bilinear interpolation to
    loweravg = bl + xoff.f * (br - bl)  # estimate value at x,y

    min_adjacent = (fx == x) & (fy == y) ? min(tr,bl) : tl

    return (upperavg + yoff.f * (loweravg - upperavg), min_adjacent)
end

# Core edge thinning algorithm using nonmaximal suppression
function thin_edges_nonmaxsup_core!(out::AbstractArray{T,2}, location::AbstractArray{Point,2},
                                    img::AbstractArray{T,2}, gradientangles::AbstractMatrix, radius, border, theta) where T
    calc_subpixel = !isempty(location)

    # Error checking
    size(img) == size(gradientangles) == size(out) || error("image, gradient angle, and output image must all be the same size")
    calc_subpixel && size(location) != size(img) && error("subpixel location has a different size than the input image")
    radius < 1.0 && error("radius must be >= 1")

    # Precalculate x and y offsets relative to centre pixel for each orientation angle
    θ, xoffs, yoffs = _calc_discrete_offsets(theta, radius)
    iθ = 1/θ

    # Indexes to use for border handling
    pad = ceil(Int, radius)
    Ix = Images.padindexes(img, 2, pad, pad, border)
    Iy = Images.padindexes(img, 1, pad, pad, border)

    # Now run through the image interpolating grey values on each side
    # of the centre pixel to be used for the non-maximal suppression.

    (height,width) = size(img)

    for x = 1:width, y = 1:height
        (c = img[y,x]) == 0 && continue  # For thresholded images

        or = _discretize_angle(gradientangles[y,x],iθ)   # Disretized orientation
        v1, n1 = _interp_offset(img, x, y, xoffs[or], yoffs[or], Ix, Iy, pad)

        if (c > v1) & (c >= n1) # We need to check the value on the other side...
            v2, n2 = _interp_offset(img, x, y, -xoffs[or], -yoffs[or], Ix, Iy, pad)

            if (c > v2) & (c >= n2)  # This is a local maximum.
                                     # Record value in the output image.
                if calc_subpixel
                    # Solve for coefficients of parabola that passes through
                    # [-1, v2]  [0, img] and [1, v1].
                    # v = a*r^2 + b*r + c

                    # c = img[y,x]
                    a = (v1 + v2)/2 - c
                    b = a + c - v2

                    # location where maxima of fitted parabola occurs
                    r = -b/2a
                    location[y,x] = Point(x + r*xoffs[or], y + r*yoffs[or])

                    if T<:AbstractFloat
                        # Store the interpolated value
                        out[y,x] = a*r^2 + b*r + c
                    else
                        out[y,x] = c
                    end
                else
                    out[y,x] = c
                end
            end
        end
    end

    out
end

# Main function call when subpixel location of edges is not needed
function thin_edges_nonmaxsup!(out, img, gradientangles, border::AbstractString="replicate";
                               radius::Float64=1.35, theta=pi/180)
    thin_edges_nonmaxsup_core!(out, Matrix{Point}(0,0), img, gradientangles, radius, border, theta)
end

function thin_edges_nonmaxsup(img, gradientangles, border::AbstractString="replicate";
                                 radius::Float64=1.35, theta=pi/180)
    (height,width) = size(img)
    out = zeros(eltype(img), height, width)
    thin_edges_nonmaxsup_core!(out, Matrix{Point}(undef,0,0), img, gradientangles, radius, border, theta)
end

# Main function call when subpixel location of edges is desired
function thin_edges_nonmaxsup_subpix!(out, location, img, gradientangles,
                                      border::AbstractString="replicate";
                                      radius::Float64=1.35, theta=pi/180)
    eltype(location) != Point && error("Preallocated subpixel location array/image must have element type Graphics.Point")

    thin_edges_nonmaxsup_core!(out, location, img, gradientangles, radius, border, theta)
    img, location
end

function thin_edges_nonmaxsup_subpix(img, gradientangles,
                                     border::AbstractString="replicate";
                                     radius::Float64=1.35, theta=pi/180)
    (height,width) = size(img)
    out = zeros(eltype(img), height, width)
    location = zeros(Point, height, width)
    thin_edges_nonmaxsup_core!(out, location, img, gradientangles, radius, border, theta)
    out, location
end

"""
```
canny_edges = canny(img, (upper, lower), sigma=1.4)
```

Performs Canny Edge Detection on the input image.

Parameters :

  (upper, lower) :  Bounds for hysteresis thresholding
  sigma :           Specifies the standard deviation of the gaussian filter

# Example

```julia
imgedg = canny(img, (Percentile(80), Percentile(20)))
```
"""
function canny(img_gray::AbstractMatrix{T}, threshold::Tuple{N,N}, sigma::Number = 1.4) where {T<:NumberLike, N<:Union{NumberLike,Percentile{NumberLike}}}
    img_grayf = imfilter(img_gray, KernelFactors.IIRGaussian((sigma,sigma)), NA())
    img_grad_y, img_grad_x = imgradients(img_grayf, KernelFactors.sobel)
    img_mag, img_phase = magnitude_phase(img_grad_x, img_grad_y)
    img_nonMaxSup = thin_edges_nonmaxsup(img_mag, img_phase)
    if N<:Percentile{}
        upperThreshold ,lowerThreshold = StatsBase.percentile(img_nonMaxSup[:], [threshold[i].p for i=1:2])
    else
        upperThreshold, lowerThreshold = threshold
    end
    img_thresholded = hysteresis_thresholding(img_nonMaxSup, upperThreshold, lowerThreshold)
    edges = map(i -> i >= 0.9, img_thresholded)
    edges
end

canny(img::AbstractMatrix, threshold::Tuple{N,N}, args...) where {N<:Union{NumberLike,Percentile{NumberLike}}} =
    canny(convert(Array{Gray}, img), args...)

function hysteresis_thresholding(img_nonMaxSup::AbstractArray{T, 2}, upperThreshold::Number, lowerThreshold::Number) where T
    img_thresholded = map(i -> i > lowerThreshold ? i > upperThreshold ? 1.0 : 0.5 : 0.0, img_nonMaxSup)
    queue = CartesianIndex{2}[]
    R = CartesianIndices(size(img_thresholded))

    I1, Iend = first(R), last(R)
    for I in R
      if img_thresholded[I] == 1.0
        img_thresholded[I] = 0.9
        push!(queue, I)
        while !isempty(queue)
          q_top = popfirst!(queue)
          for J in CartesianIndices(map((f,l)->f:l,(max(I1, q_top - I1)).I, (min(Iend, q_top + I1)).I))
            if img_thresholded[J] == 1.0 || img_thresholded[J] == 0.5
              img_thresholded[J] = 0.9
              push!(queue, J)
            end
          end
        end
      end
    end
    img_thresholded
end

function padindexes(img::AbstractArray{T,n}, dim, prepad, postpad, border::AbstractString) where {T,n}
    M = size(img, dim)
    I = Vector{Int}(undef, M + prepad + postpad)
    I = [(1 - prepad):(M + postpad);]
    if border == "replicate"
        I = min.(max.(I, 1), M)
    elseif border == "circular"
        I = 1 .+ mod.(I .- 1, M)
    elseif border == "symmetric"
        I = [1:M; M:-1:1][1 .+ mod.(I .- 1, 2 * M)]
    elseif border == "reflect"
        I = [1:M; M-1:-1:2][1 .+ mod.(I .- 1, 2 * M - 2)]
    else
        error("unknown border condition")
    end
    I
end


"""
    CalculusWithJulia

A package to accompany [notes](calculuswithjulia.github.io) on using Julia with calculus.

This package does two things: 1) it loads several other packages making it easier to use (and install) the functionality provided by them and 2) defines a handful of functions and plot recipes for convenience.

## Packages loaded by `CalculusWithJulia`

### Built in packages.

The `MathConstants` package is reexported, allowing `e` to be used instead of `\euler[tab]` for a value of Euler's constant.

The `SpecialFunctions` is loaded giving access to a few special functions used in these notes, e.g., `airyai`.

The `LinearAlgebra` package is loaded for access to several of its functions for working with vectors `norm`, `cdot` (`⋅`), `cross` (`×`), `det`.

### Plotting

The `Plots` package is loaded giving access to `plot`, `scatter`, `annotate`, etc. The backends  used in the notes are `plotly`, `gr`, and `pyplot`.

The `ImplicitEquations` package is loaded for plotting implicitly defined functions.

In addition, several plot recipes are provided to ease the creation of plots:
`plotif`, `trimplot`, and `signchart` are used for plotting univariate functions;
`plot_polar` and `plot_parametric_curve` are used to plot curves in 2 or 3 dimensions;
`plot_parametric_surface` makes the plotting os parameterically defined surfaces easier;
`vectorfieldplot` and `vectorfieldplot3d` can be used to plot vector fields; and
`arrow` is a simplified interface to `quiver` that also indicates 3D vectors.

### Symbolic math

The `Sympy` package is loaded for symbolic mathematics

### Zeros of functions

The `Roots` package is loaded for its `fzero` and `fzeros` functions.

### Derivatives

The `ForwardDiff` package is loaded giving access to its  `derivative`,  `gradient`, `jacobian`, and `hessian` functions for finding automatic derivatives of functions. In addition, this package defines `'` (for functions) to return a derivative (which commits [type piracy](https://docs.julialang.org/en/v1/manual/style-guide/index.html#Avoid-type-piracy-1)), `∇` to find the gradient (`∇(f)`), the divergence (`∇⋅F`). and the curl (`∇×F`), along with `divergence` and `curl`.

### Integration

This package reexports `QuadGK`, for one-dimensional integrals; `HCubature`, for multidimensional integrals; and provides `riemann`, for Riemann sums, and `fubini`, for iterated integrals over non-rectangular regions based on `QuadGK`.

"""
module CalculusWithJulia

using Reexport
@reexport using Plots
using RecipesBase
import ImplicitEquations # handle conflict with SymPy in plot-utils.jl
import ImplicitEquations: Pred
@reexport using LinearAlgebra
@reexport using Base.MathConstants
@reexport using SpecialFunctions
@reexport using Roots
@reexport using SymPy
import ForwardDiff
export ForwardDiff
@reexport using QuadGK
@reexport using HCubature

include("multidimensional.jl")
include("derivatives.jl")
include("integration.jl")
include("plot-utils.jl")

export unzip, divergence, gradient, curl, ∇
export tangent, secant, D
export riemann, fubini
export plotif, trimplot, signchart,
       plot_polar, plot_polar!,
       plot_parametric_curve,   plot_parametric_curve!,
       plot_parametric_surface, plot_parametric_surface!,
       vectorfieldplot,   vectorfieldplot!,
       vectorfieldplot3d, vectorfieldplot3d,
       arrow, arrow!

end # module


function recursive_factorial(n)
    if n == 0
        return 1
    else
        return n*recursive_factorial(n-1)
    end
end

recursive_factorial(7)

function recursive_factorial_info(n)
    if n == 0
        println("n == 0, returning 1")
        return 1
    else
        println("n == ", n, ", calling itself with parameter ", n-1)
        output = n*recursive_factorial_info(n-1)
        println("n == ", n, ", finished calling itself, multiplying and returning ", n, "! = ", output)
        return output
    end
end

recursive_factorial_info(7)

function ack(m,n)
    if m == 0
        return n + 1
    elseif m > 0 && n == 0
        return ack(m-1,1)
    else
        return ack(m-1, ack(m,n-1))
    end
end

ack(3,4)

function ack_info(m,n)
    function printme()
        print("ack(", m, ",", n, "): ")
    end
    if m == 0
        output = n + 1
        printme()
        println("Case 1: returning n + 1 = ", output)
        return output
    elseif m > 0 && n == 0
        printme()
        println("Case 2: calling itself with parameters m-1,1 == ", m-1, ",", 1)
        output = ack_info(m-1,1)
        printme()
        println("Case 2: finished calling itself, returning with output ", output)
        return output
    else
        printme()
        println("Case 3: calling itself for new n-value with parameters m,n-1 == ", m, ",", n-1)
        newn = ack_info(m,n-1)
        printme()
        println("Case 3: finished calling itself for new n-value == ", newn)
        printme()
        println("Case 3: calling itself with parameters m-1, A(m,n-1) == ", m-1, ",", newn)
        output = ack_info(m-1,newn)
        printme()
        println("Case 3: finished calling itself, returning ", output)
        return output
    end
end

ack_info(2,1)

function my_gcd(a,b)
    if a == 0
        return b
    elseif b == 0
        return a
    else
        return my_gcd(b, a % b)
    end
end

factor = 123_456_789
prime1 = 67_867_979
prime2 = 86_028_121
my_gcd(prime1*factor, prime2*factor)

using PyPlot

function drawTriangle(x, y, level)
    # Draw recursively colored triangles.
    # x,y are 3-vectors that define the vertices of a triangle.

    if level == 0
        # Recursion limit (depth) reached
        fill(x, y, "y") # Color whole triangle yellow
    else
        # Draw the triangle...
        plot(x[[1,2,3,1]], y[[1,2,3,1]], "k", linewidth=0.5)
        # Determine the midpoints...
        a = (x + x[[2,3,1]]) / 2
        b = (y + y[[2,3,1]]) / 2
        # Draw and color the interior triangle mauve
        fill(a, b, "m")
        # Apply the process to the three "corner" triangles...
        newx = [x a a[[3,1,2]]]
        newy = [y b b[[3,1,2]]]
        for i = 1:3
            drawTriangle(newx[i,:], newy[i,:], level - 1)
        end
    end
end

# Equilateral triangle
x = [0, 1, 0.5]
y = [0, 0, 1/sqrt(2)]

drawTriangle(x, y, 5)

function mergeLR!(L, R, x)
    # Merge the *already sorted arrays* L and R into a sorted array x
    i = j = k = 1

    # Merge L and R into x
    while i <= length(L) && j <= length(R)
        if L[i] < R[j]
            x[k] = L[i]
            i += 1
        else
            x[k] = R[j]
            j += 1
        end
        k += 1
    end

    # Copy remaining elements
    while i <= length(L)
        x[k] = L[i]
        i += 1
        k += 1
    end
    while j <= length(R)
        x[k] = R[j]
        j += 1
        k += 1
    end
end

function mergesort!(x)
    # Sort the elements of the array x using the Mergesort algorithm
    if length(x) <= 1
        return x
    else
        mid = length(x) ÷ 2   # Find the midpoint of the array
        L = x[1:mid]          # Divide array into 2 halves
        R = x[mid+1:end]

        mergesort!(L)         # Sort first half
        mergesort!(R)         # Sort second half

        mergeLR!(L, R, x)
    end
end

# Example: Sort random integers
x = rand(1:1000, 10)
println(x)
mergesort!(x)
println(x)

for array_size = Int64[1e3, 1e4, 1e5, 1e6, 1e7]
    x = rand(array_size)   # Random floating point numbers
    println("n = ", array_size)
    @time mergesort!(x)
end

function Mvalues(n)
    returned_values = Int64[]

    function M(n)
        if n > 100
            newval = n - 10
        else
            newval = M(M(n + 11))
        end
        push!(returned_values, newval)
        return newval
    end

    M(n)
    return returned_values
end

Mvalues(105)     # Easy - terminates immediately, M(105) = 95

Mvalues(97)      # More complex - finally returns M(97) = 91


function mk_score(x)
    s = 0
    n = length(x)

    for i = 1:n-1
        for j = i : n
            s = s + sign(x[j] - x[i])
        end
    end

    return s
end

function variance_s(x, n)
    # calculate the unique data
    unique_x = unique(x)
    g = length(unique_x)

    # calculate the var(s)
    if n == g            # there is no tie
        var_s = (n*(n-1)*(2*n+5))/18

    else                 # there are some ties in data
        tp = zeros(length(unique_x))
        demo = ones(n)

        for i in 1:g
            tp[i] = sum(demo[x == unique_x[i]])
        end
        var_s = (n*(n-1)*(2*n+5) - sum(tp*(tp-1)*(2*tp+5)))/18
    end

    return var_s
end

function z_score(s, var_s)
    if s > 0
        z = (s - 1)/sqrt(var_s)
    elseif s == 0
        z = 0
    elseif s < 0
        z = (s + 1)/sqrt(var_s)
    end

    return z
end

function p_value(z, α)
    # two tail test
    p = 2*(1-normcdf(abs(z)))
    h = abs(z) > norminvcdf(1-α/2)

    if (z < 0) && h
        trend = "decreasing"
    elseif (z > 0) && h
        trend = "increasing"
    else
        trend = "no trend"
    end

    return p, h, trend
end


using Plots
#using LsqFit
import Distributions as di
using Random
using Turing
using Statistics
using StatsPlots
using FillArrays

n = di.Normal(1,2)
sample_n = rand(n,20)

@model function normal_fit(data)
	μ ~ Uniform(-10,10)
	σ ~ Uniform(0,20)
    data ~ MvNormal(Fill(μ,length(data)),σ)
end

model1 = normal_fit(sample_n)
chain = Turing.sample(model1,NUTS(0.65),1000)
plot(chain)

rwd = Normal(0,1)
rw_steps = rand(rwd,999)

x = [0.0]
for step in rw_steps
    push!(x,x[end]+step)
end
plot(x)

steps = []
for i in 1:length(x)-1
    push!(steps,x[i+1]-x[i])
end

rw_steps
steps

@model function diffusion_fit(data)
	μ = 0
	σ ~ Uniform(0,20)
	for i in 1:length(data)
		data[i] ~ Normal(μ,σ)
	end
end

model2 = diffusion_fit(steps)
chain2 = Turing.sample(model2,NUTS(0.65),1000)


#### TriDiagSolverFuncs

# A module with functions to solve tridiagonal
# systems of equations using the Thomas algorithm.

export solve_tridiag!
export solve_tridiag_stored!
export init_β_γ!

"""
    solve_tridiag!(x, B, a, b, c, n, xtemp, γ, β)

Solves for `x` in the equation
          `Ax = B`
where `A` is a tridiagonal matrix:
```
           _                                           _ -1
          |  b[1] c[1]                                   |
          |  a[1] b[2]  c[2]                             |
          |       a[2]  b[3]  c[3]                       |
 x    =   |           *     *     *                      |   B
          |                 *     *     *                |
          |                    a[n-2] b[n-1]  c[n-1]     |
          |_                           a[n-1]  b[n]     _|

          |______________________________________________|
                                 A
```
and given arguments:
--------------------------------------------
| x[1:n]       | the result                |
| B[1:n]       | right hand side           |
| a[1:n-1]     | sub-diagonal              |
| b[1:n]       | main diagonal             |
| c[1:n-1]     | super-diagonal            |
| n            | system size               |
| xtemp[1:n]   | temporary                 |
| γ[1:n-1]     | temporary                 |
| β[1:n]       | temporary                 |
--------------------------------------------
"""
function solve_tridiag!(x, B, a, b, c, n, xtemp, γ, β)
  # Define coefficients:
  β[1] = b[1]
  γ[1] = c[1]/β[1]
  for i in 2:n-1
    β[i] = b[i]-a[i-1]*γ[i-1]
    γ[i] = c[i]/β[i]
  end
  β[n] = b[n]-a[n-1]*γ[n-1]

  # Forward substitution:
  xtemp[1] = B[1]/β[1]
  for i = 2:n
    m = B[i] - a[i-1]*xtemp[i-1]
    xtemp[i] = m/β[i]
  end

  # Backward substitution:
  x[n] = xtemp[n]
  for i in n-1:-1:1
    x[i] = xtemp[i]-γ[i]*x[i+1]
  end
end

"""
    solve_tridiag_stored!(x, B, a, β, γ, n, xtemp)

Solves for `x` in the equation
          `Ax = B`
where `A` is a tridiagonal matrix.

Coefficients in solve_tridiag! can be pre-computed,
by applying LU factorization to A (shown below).
The coefficients, β and γ, can be computed in init_β_γ!.
```
 _                                           _
|  b[1]  c[1]                                 |
|  a[1]  b[2]  c[2]                           |
|         a[2]  b[3]  c[3]                    |
|           *     *     *                     |
|                 *     *     *               |
|                    a[n-2]  b[n-1]  c[n-1]   |
|_                            a[n-1]  b[n]   _|

=
 _                                        _   _                                      _ -1
|  β[1]                                    | |  1  γ[1]                               |
|  α[1]  β[2]                              | |        1  γ[2]                         |
|        α[2]  β[3]                        | |              1  γ[3]                   |
|           *     *     *                  | |                 *     *                |
|                 *     *                  | |                       *     *          |
|                    α[n-2]  β[n-1]        | |                             1   γ[n-1] |
|_                           α[n-1]  β[n] _| |_                                1     _|
```

and given arguments:
--------------------------------------------
| x[1:n]       | the result                |
| B[1:n]       | right hand side           |
| a[1:n-1]     | sub-diagonal              |
| β[1:n]       | temporary                 |
| γ[1:n-1]     | temporary                 |
| n            | system size               |
| xtemp[1:n]   | temporary                 |
--------------------------------------------
"""
function solve_tridiag_stored!(x, B, a, β, γ, n, xtemp)
  # Forward substitution:
  xtemp[1] = B[1]/β[1]
  for i = 2:n
    m = B[i] - a[i-1]*xtemp[i-1]
    xtemp[i] = m/β[i]
  end

  # Backward substitution:
  x[n] = xtemp[n]
  for i = n-1:-1:1
    x[i] = xtemp[i]-γ[i]*x[i+1]
  end
end

"""
    init_β_γ!(β, γ, a, b, c, n)

Returns the pre-computed coefficients, from applying
LU factorization, for the tridiagonal system. These
coefficients can be passed as arguments to solve_tridiag_stored!.
--------------------------------------------
| β[1:n]       | temporary                 |
| γ[1:n-1]     | temporary                 |
| a[1:n-1]     | sub-diagonal              |
| b[1:n]       | main diagonal             |
| c[1:n-1]     | super-diagonal            |
| n            | system size               |
--------------------------------------------
"""
function init_β_γ!(β, γ, a, b, c, n)
  β[1] = b[1]
  γ[1] = c[1]/β[1]
  for i = 2:n-1
    β[i] = b[i]-a[i-1]*γ[i-1]
    γ[i] = c[i]/β[i]
  end
  β[n] = b[n]-a[n-1]*γ[n-1]
end


function EuropeanSpreadOptionMC(K, T, S1, S2, sig1, sig2, div1, div2, rho, r, N, M)

## setting parameters

dt = T/N
nu1dt = (r - div1 - 0.5 * sig1^2) * dt
nu2dt = (r - div2 - 0.5 * sig2^2) * dt
sig1sdt = sig1 * sqrt(dt)
sig2sdt = sig2 * sqrt(dt)
srho = sqrt(1 - rho^2)

## variables to store the sums of the option values

sum_CT = 0
sum_CT2 = 0

## outer loop is the number of simulations bigger is better

for j = 1:M

	St1 = S1
	St2 = S2

	## inner loop is simulating each price path of length N
	for i = 1:N

		error1 = randn()
		error2 = randn()

		z1 = error1
		z2 = rho * error1 + srho * error2

		St1 = St1 * exp(nu1dt + sig1sdt * z1)
		St2 = St2 * exp(nu2dt + sig2sdt * z2)

	end

    	## wraps up and stores all the values of each path
	CT = max(0, St1 - St2 - K)
	sum_CT = sum_CT + CT
	sum_CT2 = sum_CT2 + CT^2

end

## discounting the average call value
call_value = sum_CT / M * exp(-r*T)

SD = (sqrt(sum_CT2 - 1/M * (sum_CT^2)) * exp(-2*r*T)) / (M-1)
SE = SD / sqrt(M)

println("The Call Value is $call_value")
println("The SD is $SD")
println("The SE is $SE")

MC = [call_value, SD, SE]

end


# # Basics: Arithmetic, Assignment, and Strings
#
# Topics:
# 1. Infix arithmetic operators
# 2. Numeric literals
# 3. Comparisons
# 4. Assignments
# 5. Strings

#-

# ## Arithmetic should largely be familiar

3 + 7 # addition

#-

10 - 3 # subtraction

#-

20 * 5 # multiplication

#-

100 / 10 # division

#-

10 ^ 2 # exponentiation

#-

101 % 2 # remainder (modulus)

#-

sqrt(2) # square root

#-

√2 # Unicode to the rescue: \sqrt + TAB

# Note that dividing two integers yields a floating point number. There are two additional operators that may be helpful here:

10 / 6

#-

10 ÷ 6 # \div TAB or the `div` function

#-

div(10, 6)

#-

10 // 6

# ### Numbers: Many different ways to write the number forty-two

fortytwos = (42, 42.0, 4.20e1, 4.20f1, 84//2, 0x2a)

#-

for x in fortytwos
    show(x)
    println("\tisa $(typeof(x))")
end

# ### Bitwise arithmetic

0x2a & 0x70 # AND

#-

0x2a | 0x70 # OR

#-

42 & 112

#-

0b0010 << 2 # == 0b1000

# ### Logical operators

false && true # AND

#-

false || true # OR

# Note that they "short-circuit!"

x = -42
x > 0 || error("x must be positive")

# ### Comparisons

1 == 1.0 # Equality

#-

1 === 1.0 # Programmatically identical

#-

3 < π

#-

1 <= 1

#-

.1 + .2

#-

.1 + .2 ≈ .3 # \approx + TAB

# Comparisons "chain"
#
# Try inserting parentheses around one of these comparisons

2 == 2.0 == 0x02

#-

x = 42
0 < x < 100 || error("x must be between 0 and 100")

# ### Higher precision

big(2)^1000

big(pi)

big"0.1"

big(0.1)

# # Assignment
#
# Assignment in Julia is done with the single `=`. All it does is associates a name (on the left) to a value (on the right).

x = 1 # Use the name `x` for the value `1`

#-

y = x # Use the name `y` for the value that `x` refers to

#-

x = "hello!" # Decide you have a better use for the name `x`

#-

y # Is still the value 1

#-

# "Simultaneous" multiple assignment

x, y = y, x  # swap x and y
x, y = y, x  # swap back

#-

ϵ = eps(1.0) # You can make your own unicode names

#-

5ϵ # Juxtaposition is multiplication

#-

# We make use of juxtaposition for complex numbers

(1 + 2im)^2

# ## Updating operators
#
# All the infix arithmetic operators above can be used as "updating" operators in conjunction with an assignment:

y += 1

#-

## This is exactly the same as:
y = y + 1

# Note that it's just re-purposing the _same name_ for a new value. This means that the type might even change:

y /= 2

# # Strings

s1 = "I am a string."

#-

s2 = """I am also a string. """

#-

"Here, we get an "error" because it's ambiguous where this string ends "

#-

"""Look, Mom, no "errors"!!! """

#-

    println("""The other nice thing about triple-quoted
               string literals is that they ignore leading
               indentation, which is nice for long strings
               in real code. Try changing these quotes!""")

# Strings are not written with single `'`s — that's used for a single character:

first(s1)

#-

'⊂'

#-

'If you try writing a string in single-quotes, you will get an error'

# ## String interpolation

#-

# You can use the dollar sign inside a string to evaluate a Julia expression inside a string — either a single variable or a more complicated expression:

name = "Jane"
num_fingers = 10
num_toes = 10
println("Hello, my name is $name.")
println("I have $num_fingers fingers and $num_toes toes.")

#-

 println("That is $(num_fingers + num_toes) digits in all!!")

#-

@assert days == 365
@assert days_float == 365.0

Courses/Introduction to Julia/01. Arithmetic, Assignment, and Strings.jl


include("Lanczos_Ritz_Utilities.jl")
include("../../src/iterative_lanczos.jl")

# ------------------------------------------------------------------------------
# Demonstrating the loss of orthogonality of the lanczos vectors.
# And the linear dependence of eigenvalues of the exterior of the spectrum.
# ------------------------------------------------------------------------------
n = 64
A = Diagonal(LinRange(-1, 1, n).^3)
il = IterativeLanczos(A, rand(n))
for _ in 1: n - 1
    il()
end
Q = GetQMatrix(il)
T = GetTMatrix(il)

fig = heatmap(Q'*Q, size=(722, 512))
fig2 = heatmap(Q'*A*Q, size=(722, 512))
fig3 = heatmap(
    (A*Q[:, 1:end - 1] - Q*T[:, 1:end - 1]).|> abs,
    size=(1200, 768), dpi=250
)
savefig(fig, "$(@__DIR__)/plots/fig3.png")
savefig(fig2, "$(@__DIR__)/plots/fig4.png")

# ------------------------------------------------------------------------------
# All the ritz values during the computations process. and plotting it.
# ------------------------------------------------------------------------------

function RiztTrajectoryPlot(filename, n=64; itr_offset=20, full_ortho=false, title="")
    A = Diagonal(LinRange(-1, 1, n).^3)
    il = IterativeLanczos(A, ones(n))
    il.reorthogonalize = full_ortho
    FoundRitzValues = Vector{Vector{Float64}}()
    # push!(FoundRitzValues, [GetTMatrix(il)])
    TrueEigenValues = diag(A)

    for II in 1: n - 1
        il()
        T = GetTMatrix(il)
        λs, _ = eigen(T)
        if II >= itr_offset
            push!(FoundRitzValues, λs)
        end
    end

    for RitzValue in FoundRitzValues
        sort!(RitzValue, rev=true)
    end

    fig4 = scatter(title=title, legend=false)
    for Idx in 1: div(itr_offset,2)
        RitzTrajectory = Vector{Float64}()
        for RitzValues in FoundRitzValues
            if Idx <= length(RitzValues)
                push!(RitzTrajectory, RitzValues[Idx])
            end
        end
        plot!(
            fig4,
            # Idx: length(RitzTrajectory) + Idx - 1,
            itr_offset:(length(RitzTrajectory) + itr_offset - 1),
            RitzTrajectory, size=(750,750),
            dpi=250,
            markershape=:cross,
            linestyle=:solid
        )
    end
    for Idx in 1:div(itr_offset, 2)
        RitzTrajectory = Vector{Float64}()
        for RitzValues in FoundRitzValues
            if Idx <= length(RitzValues)
                push!(RitzTrajectory, RitzValues[end - Idx + 1])
            end
        end
        plot!(
            fig4,
            # Idx: length(RitzTrajectory) + Idx - 1,
            itr_offset:(length(RitzTrajectory) + itr_offset - 1),
            RitzTrajectory,
            dpi=250,
            markershape=:xcross,
            linestyle=:solid
        )
    end
    xlabel!(fig4, "iterations")
    ylabel!(fig4, "ritz values")
    display(fig4)
    savefig(fig4, "$(@__DIR__)/plots/$(filename).png")

return end

RiztTrajectoryPlot("ritz_trajectory_plot_floats", title="ritz trajectory no-reorthgonalization")
RiztTrajectoryPlot("ritz_trajectory_plot_exact", full_ortho=true, title="ritz trajectory with full-reorthogonalizatoin")


# ------------------------- #
# Testing normal form games #
# ------------------------- #

# NOTE: We include `@inferred` at least once for each function name. We do
#       that multiple times for the same function if we have particular reason
#       to believe there might be a type stability with that function.

using MathOptInterface
const MOI = MathOptInterface
using Clp
using CDDLib

@testset "Testing normal_form_game.jl" begin

    # Player #

    @testset "Player with 1 opponent" begin
        coordination_game_matrix = [4 0; 3 2]
        player = Player(coordination_game_matrix)

        @test @inferred(best_response(player, 2)) == 2
        @test @inferred(best_response(player, [1/2, 1/2])) == 2
        @test @inferred(best_response(player, [1/2, 1/2], BROptions())) == 2
        @test sort(@inferred(best_responses(player, [2/3, 1/3]))) ==
              sort([1, 2])
        @test best_response(
            player, [2/3, 1/3], tie_breaking=:random
            ) in [1, 2]
        @test best_response(
            player, [2/3, 1/3], BROptions(tol=1e-5, tie_breaking=:random)
            ) in [1, 2]
        @test_throws ArgumentError best_response(
            player, [2/3, 1/3], tie_breaking=:invalid_symbol
            )
        @test @inferred(is_best_response(player, 1, 1))
        @test @inferred(is_best_response(player, [1/2, 1/2], [2/3, 1/3]))

        # Perturbed best response
        @test best_response(player, [2/3, 1/3], [0., 0.1]) == 2
        @test best_response(player, [2, 1], [0., 0.1]) == 2

        # Dominated actions
        @test !(@inferred(is_dominated(player, 1)))
        @test @inferred(dominated_actions(player)) == Int[]
    end

    @testset "Player with 2 opponents" begin
        payoffs_2opponents = Array{Int}(undef, 2, 2, 2)
        payoffs_2opponents[:, 1, 1] = [3, 1]
        payoffs_2opponents[:, 1, 2] = [6, 0]
        payoffs_2opponents[:, 2, 1] = [4, 5]
        payoffs_2opponents[:, 2, 2] = [2, 7]
        player = @inferred Player(payoffs_2opponents)

        @test @inferred(payoff_vector(player, (1, 2))) == [6, 0]
        @test !(@inferred(is_best_response(player, 1, (2, 1))))
        @test @inferred(best_response(player, (2, 2))) == 2
        @test sort(@inferred(best_responses(player, ([3/7, 4/7], [1/2, 1/2])))) ==
              sort([1, 2])

        @test_throws MethodError best_response(player, (1, [1/2, 1/2]))

        @test !(@inferred(is_dominated(player, 1)))
        @test @inferred(dominated_actions(player)) == Int[]
    end

    @testset "convert for Player" begin
        T1 = Int
        T2 = Float64
        player = Player(T1[1 2; 3 4])
        N = ndims(player.payoff_array)

        for T in [T1, T2]
            player_new = Player{N,T}(player)
            @test eltype(player_new.payoff_array) == T
            @test player_new.payoff_array == player.payoff_array

            # Constructor always makes a copy
            @test player_new.payoff_array !== player.payoff_array
        end

        for T in [T1, T2]
            player_new = Player(T, player)
            @test eltype(player_new.payoff_array) == T
            @test player_new.payoff_array == player.payoff_array

            # Constructor always makes a copy
            @test player_new.payoff_array !== player.payoff_array
        end

        player_new = @inferred Player(player)
        @test eltype(player_new.payoff_array) == T1
        @test player_new.payoff_array == player.payoff_array

        # Constructor always makes a copy
        @test player_new.payoff_array !== player.payoff_array

        for T in [T1, T2]
            player_new = convert(Player{N,T}, player)
            @test eltype(player_new.payoff_array) == T
            @test player_new.payoff_array == player.payoff_array

            @test (player_new.payoff_array === player.payoff_array) ==
                  (T == T1)
        end
    end

    @testset "repr(Player)" begin
        A = [1 2; 3 4]
        player = Player(A)
        r = repr("text/plain", A)
        @test repr(player) ==
            replace(r, string(typeof(A)) =>
                       split(string(typeof(player)), ".")[end])
    end

    @testset "Tests on delete_action for Player" begin
        shapley_game = [0 1 0; 0 0 1; 1 0 0]
        player = Player(shapley_game)

        player_new_1 = @inferred delete_action(player, 1, 1)
        player_new_2 = @inferred delete_action(player, [1, 2], 1)
        @test player_new_1.payoff_array == Player([0 0 1; 1 0 0]).payoff_array
        @test player_new_2.payoff_array == Player([1 0 0]).payoff_array
    end

    # NormalFormGame #

    @testset "symmetric NormalFormGame with 2 players" begin
        coordination_game_matrix = [4 0; 3 2]
        g = @inferred(NormalFormGame(coordination_game_matrix))

        # NOTE: getindex(g, 1, 2) is equivalent to `g[1, 2]`. I use the former
        #       so we can test the type stability of the get index function
        @test @inferred(getindex(g, 1, 2)) == [0, 3]
        @test @inferred(getindex(g, CartesianIndex(1, 2))) == [0, 3]
        @test @inferred is_nash(g, (1, 1))
        @test @inferred is_nash(g, ([2/3, 1/3], [2/3, 1/3]))
    end

    @testset "asymmetric NormalFormGame with 2 players" begin
        matching_pennies_bimatrix = Array{Float64}(undef, 2, 2, 2)
        matching_pennies_bimatrix[:, 1, 1] = [1, -1]
        matching_pennies_bimatrix[:, 1, 2] = [-1, 1]
        matching_pennies_bimatrix[:, 2, 1] = [-1, 1]
        matching_pennies_bimatrix[:, 2, 2] = [1, -1]
        g = @inferred(NormalFormGame(matching_pennies_bimatrix))

        @test g[2, 1] == [-1, 1]
        @test !(is_nash(g, (1, 1)))
        @test is_nash(g, ([1/2, 1/2], [1/2, 1/2]))
    end

    @testset "asymmetric NormalFormGame with 3 players" begin
        payoffs_2opponents = Array{Int}(undef, 2, 2, 2)
        payoffs_2opponents[:, 1, 1] = [3, 1]
        payoffs_2opponents[:, 1, 2] = [6, 0]
        payoffs_2opponents[:, 2, 1] = [4, 5]
        payoffs_2opponents[:, 2, 2] = [2, 7]
        player = @inferred Player(payoffs_2opponents)
        g = @inferred NormalFormGame(tuple([player for i in 1:3]...))

        @test @inferred(getindex(g, 1, 1, 2)) == [6, 4, 1]
        @test @inferred(getindex(g, CartesianIndex(1, 1, 2))) == [6, 4, 1]
        @test @inferred is_nash(g, (1, 1, 1))
        @test @inferred !(is_nash(g, (1, 1, 2)))

        p = (1 + sqrt(65)) / 16
        @test is_nash(g, ([1-p, p], [1-p, p], [1-p, p]))
    end

    @testset "NormalFormGame input action sizes" begin
        g = @inferred NormalFormGame((2, 3, 4))

        @test @inferred(num_players(g)) == 3
        @test g.players[1].payoff_array == zeros((2, 3, 4))
        @test g.players[2].payoff_array == zeros((3, 4, 2))
        @test g.players[3].payoff_array == zeros((4, 2, 3))
    end

    @testset "NormalFormGame setindex" begin
        g = @inferred NormalFormGame((2, 2))
        g[1, 1] = [0, 10]
        g[1, 2] = [0, 10]
        g[2, 1] = [3, 5]
        g[CartesianIndex(2, 2)] = [-2, 0]

        @test g.players[1].payoff_array == [0 0; 3 -2]
        @test g.players[2].payoff_array == [10 5; 10 0]
    end

    @testset "NormalFormGame constant payoffs" begin
        g = NormalFormGame((2, 2))

        @test @inferred is_nash(g, (1, 1))
        @test is_nash(g, (1, 2))
        @test is_nash(g, (2, 1))
        @test is_nash(g, (2, 2))
    end

    @testset "convert for NormalFormGame" begin
        T1 = Int
        T2 = Float64
        players = (Player(T1[1 2 3; 4 5 6]), Player(T1[7 8; 9 10; 11 12]))
        g = NormalFormGame(players)
        N = num_players(g)

        for T in [T1, T2]
            g_new = @inferred NormalFormGame{N,T}(g)
            for (player_new, player) in zip(g_new.players, players)
                @test eltype(player_new.payoff_array) == T
                @test player_new.payoff_array == player.payoff_array

                # Constructor always makes a copy
                @test player_new.payoff_array !== player.payoff_array
            end
        end

        for T in [T1, T2]
            g_new = @inferred NormalFormGame(T, g)
            for (player_new, player) in zip(g_new.players, players)
                @test eltype(player_new.payoff_array) == T
                @test player_new.payoff_array == player.payoff_array

                # Constructor always makes a copy
                @test player_new.payoff_array !== player.payoff_array
            end
        end

        g_new = @inferred NormalFormGame(g)
        for (player_new, player) in zip(g_new.players, players)
            @test eltype(player_new.payoff_array) == T1
            @test player_new.payoff_array == player.payoff_array

            # Constructor always makes a copy
            @test player_new.payoff_array !== player.payoff_array
        end

        for T in [T1, T2]
            g_new = @inferred convert(NormalFormGame{N,T}, g)
            for (player_new, player) in zip(g_new.players, players)
                @test eltype(player_new.payoff_array) == T
                @test player_new.payoff_array == player.payoff_array

                @test (player_new.payoff_array === player.payoff_array) ===
                      (T == T1)
            end
        end
    end

    @testset "Tests on delete_action for NormalFormGame" begin
        shapley_game = Array{Int}(undef, 3, 3, 2)
        shapley_game[:, 1, 1] = [0, 0, 1]
        shapley_game[:, 2, 1] = [1, 0, 0]
        shapley_game[:, 3, 1] = [0, 1, 0]
        shapley_game[:, 1, 2] = [0, 1, 0]
        shapley_game[:, 2, 2] = [0, 0, 1]
        shapley_game[:, 3, 2] = [1, 0, 0]
        g = NormalFormGame(shapley_game)

        deleted_game_1 = Array{Int}(undef, 2, 3, 2)
        deleted_game_1[:, 1, 1] = [0, 1]
        deleted_game_1[:, 2, 1] = [0, 0]
        deleted_game_1[:, 3, 1] = [1, 0]
        deleted_game_1[:, 1, 2] = [1, 0]
        deleted_game_1[:, 2, 2] = [0, 1]
        deleted_game_1[:, 3, 2] = [0, 0]

        deleted_game_2 = Array{Int}(undef, 1, 3, 2)
        deleted_game_2[1, 1, 1] = 1
        deleted_game_2[1, 2, 1] = 0
        deleted_game_2[1, 3, 1] = 0
        deleted_game_2[1, 1, 2] = 0
        deleted_game_2[1, 2, 2] = 1
        deleted_game_2[1, 3, 2] = 0

        g_new_1 = @inferred delete_action(g, 1, 1)
        g_new_2 = @inferred delete_action(g, [1, 2], 1)
        @test g_new_1.players[1].payoff_array ==
            NormalFormGame(deleted_game_1).players[1].payoff_array
        @test g_new_1.players[2].payoff_array ==
            NormalFormGame(deleted_game_1).players[2].payoff_array
        @test g_new_2.players[1].payoff_array ==
            NormalFormGame(deleted_game_2).players[1].payoff_array
        @test g_new_2.players[2].payoff_array ==
            NormalFormGame(deleted_game_2).players[2].payoff_array
    end

    # Trivial cases with one player #

    @testset "Player with 0 opponents" begin
        payoffs = [0, 1]
        player = Player(payoffs)

        @test @inferred(payoff_vector(player, nothing)) == [0, 1]
        @test @inferred is_best_response(player, 2, nothing)
        @test @inferred(best_response(player, nothing)) == 2
        @test @inferred is_dominated(player, 1)
        @test !is_dominated(player, 2)

        payoffs = [0, 1, -1]
        player = Player(payoffs)
        dom_actions = [1, 3]
        @test @inferred(dominated_actions(player)) == dom_actions

    end

    @testset "NormalFormGame with 1 player" begin
        payoffs = [0, 1, 1]
        g = @inferred NormalFormGame(Player(payoffs))
        @test num_players(g) == 1
        @test g.players[1].payoff_array == [0, 1, 1]
        @test g[1] == 0
        @test is_nash(g, 2)
        @test !(is_nash(g, 1))
        @test is_nash(g, [0, 1/2, 1/2])

        g = NormalFormGame((2,))
        @test num_players(g) == 1
        @test g.players[1].payoff_array == zeros(2)
        g[1] = 10
        @test g.players[1].payoff_array == [10, 0]
    end

    # Invalid inputs #

    @testset "Player 0 actions" begin
        @test_throws ArgumentError p = Player(Float64[])
    end

    @testset "NormalFormGame invalid players shape inconsistent" begin
        p1 = Player(zeros((2, 3)))
        p2 = Player(zeros((2, 3)))
        @test_throws ArgumentError g = NormalFormGame((p1, p2))
    end

    @testset "NormalFormGame invalid players number inconsistent" begin
        p1 = Player(zeros((2, 2, 2)))
        p2 = Player(zeros((2, 2, 2)))
        @test_throws MethodError g = NormalFormGame((p1, p2))
    end

    @testset "NormalFormGame invalid nonsquare matrix" begin
        @test_throws ArgumentError g = NormalFormGame(zeros((2, 3)))

        A = [0, 1, 1]
        A = reshape(A, (size(A)..., 1))
        @test_throws ArgumentError g = NormalFormGame(A)
    end

    @testset "NormalFormGame invalid payoff profiles" begin
        @test_throws ArgumentError g = NormalFormGame(zeros((2, 2, 1)))
    end

    @testset "NormalFormGame empty tuple" begin
        @test_throws ArgumentError g = NormalFormGame(tuple())
    end

    @testset "NormalFormGame 0 actions" begin
        @test_throws ArgumentError g = NormalFormGame((0, 2))
    end

    @testset "payoff_vector empty tuple" begin
        p1 = Player(zeros((2, 2, 2)))
        @test_throws ArgumentError payoff_vector(p1, tuple())
    end

    @testset "is_dominated linprog error" begin
        player = Player([1. 1.; 0. -1.; -1. 0.])

        function clp_optimizer_silent_maxiter1()
            optimizer = Clp.Optimizer()
            MOI.set(optimizer, MOI.Silent(), true)
            MOI.set(optimizer, MOI.RawParameter("MaximumIterations"), 1)
            return optimizer
        end
        lp_solver = clp_optimizer_silent_maxiter1
        @test_throws ErrorException is_dominated(player, 1,
                                                 lp_solver=lp_solver)
    end

    # Utility functions #

    @testset "pure2mixed" begin
        num_actions = 3
        pure_action = 1
        mixed_action = [1., 0., 0.]
        @test @inferred(pure2mixed(num_actions, pure_action)) == mixed_action
    end

    # Pareto efficiency & Pareto dominance #

    @testset "Tests on Pareto efficiency and dominance" begin
        coordination_game_matrix = [4 0;
                                    3 2]

        equal_po_p1_bimatrix = Array{Float64}(undef, 2, 2, 2)
        equal_po_p1_bimatrix[1, 1, :] = [1, -1]
        equal_po_p1_bimatrix[1, 2, :] = [1, 1]
        equal_po_p1_bimatrix[2, 1, :] = [1, 1]
        equal_po_p1_bimatrix[2, 2, :] = [1, -1]

        three_p_equal_po_array = Array{Int}(undef, 2, 2, 2)
        three_p_equal_po_array[:, :, 1] = [2 0; 0 2]
        three_p_equal_po_array[:, :, 2] = [2 0; 0 2]

        p1 = p2 = p3 = Player(three_p_equal_po_array)

        games_dict = [NormalFormGame(coordination_game_matrix),
                      NormalFormGame(equal_po_p1_bimatrix),
                      NormalFormGame((p1, p2, p3))]

        act_prof_dict = [[(1, 1), (1, 2), (2, 1), (2, 2)],
                         [(1, 1), (1, 2), (2, 1), (2, 2)],
                         [(1, 1, 1), (2, 1, 1), (1, 2, 1), (2, 2, 1),
                          (1, 1, 2), (2, 1, 2), (1, 2, 2), (2, 2, 2)]]

        @testset "Testing is_pareto_efficient" begin
            output_dict = [[true, false, false, false],
                           [false, true, true, false],
                           [true, false, false, false, false, false, false,
                            true]]

            for i = 1:length(games_dict)
                for j in 1:length(act_prof_dict[i])
                    @test @inferred is_pareto_efficient(games_dict[i],
                                                       act_prof_dict[i][j]) ==
                                    output_dict[i][j]
                end
            end
        end

        @testset "Testing Pareto dominance" begin
            output_dict = [[true, false, false, false],
                           [false, false, false, false],
                           [false, false, false, false, false, false, false,
                            false]]

            for i = 1:length(games_dict)
                for j in 1:length(act_prof_dict[i])
                    @test @inferred is_pareto_dominant(games_dict[i],
                                                       act_prof_dict[i][j]) ==
                                    output_dict[i][j]
                end
            end
        end

        @testset "Test is_dominated" begin
            coordination_game_matrix = [4 0; 3 2]
            player = Player(coordination_game_matrix)
            for action = 1:num_actions(player)
                @test !is_dominated(player, action)
            end

            payoffs_2opponents = Array{Int}(undef, 2, 2, 2)
            payoffs_2opponents[:, 1, 1] = [3, 1]
            payoffs_2opponents[:, 1, 2] = [6, 0]
            payoffs_2opponents[:, 2, 1] = [4, 5]
            payoffs_2opponents[:, 2, 2] = [2, 7]
            player = Player(payoffs_2opponents)

            for i = 1:num_actions(player)
                @test !is_dominated(player, i)
            end

        end

        @testset "Test player corner cases" begin
            n, m = 3, 4
            player = Player(zeros((n, m)))
            for action = 1:n
                @test is_best_response(player, action, ones(m) * 1/m)
                @test !is_dominated(player, action)
            end

            e = 1e-8
            player = Player([-e -e;
                             1 -1;
                             -1 1])
            action = 1
            @test is_best_response(player, action, [1/2, 1/2], tol=e)
            @test !is_best_response(player, action, [1/2, 1/2], tol=e/2)
            @test !is_dominated(player, action, tol=e+1e-16)
            @test dominated_actions(player, tol=e+1e-16) == Int[]
            @test is_dominated(player, action, tol=e/2)
            @test dominated_actions(player, tol=e/2) == [action]
        end

        @testset "Test rational input game" begin
            T = Rational{BigInt}
            lp_solver = CDDLib.Optimizer{T}

            # Corner cases
            e = 1//(2^25)
            player = Player([-e -e;
                             1//1 -1//1;
                             -1//1 1//1])

            action = 1
            @test !is_dominated(T, player, action, tol=e, lp_solver=lp_solver)
            @test is_dominated(T, player, action, tol=e//2, lp_solver=lp_solver)

            player.payoff_array[1, 1:2] .= 0;

            @test !is_dominated(T, player, action, tol=0, lp_solver=lp_solver)

            # Simple game
            game_matrix = [2//3 1//3;
                           1//3 2//3]
            player = Player(game_matrix)

            @test dominated_actions(T, player, lp_solver=lp_solver) == Int[]
        end
    end

end


# This file was generated, do not modify it.

using MLJ, RDatasets, Random

data = dataset("datasets", "USArrests")
names(data)

describe(data, :mean, :std)

X = select(data, Not(:State))
X = coerce(X, :UrbanPop=>Continuous);

@load PCA pkg=MultivariateStats

pca_mdl = PCA(pratio=1)
pca = machine(pca_mdl, X)
fit!(pca)

W = transform(pca, X);

schema(W).names

r = report(pca)
cumsum(r.principalvars ./ r.tvar)

data = dataset("ISLR", "OJ")

X = select(data, [:PriceCH, :PriceMM, :DiscCH, :DiscMM, :SalePriceMM,
                  :SalePriceCH, :PriceDiff, :PctDiscMM, :PctDiscCH]);

Random.seed!(1515)

@pipeline SPCA(std = Standardizer(),
               pca = PCA())
spca_mdl = SPCA()
spca = machine(spca_mdl, X)
fit!(spca)
W = transform(spca, X)
names(W)

r = report(spca).reports[1]
cumsum(r.principalvars ./ r.tvar)

Random.seed!(1515)

@load KMeans
@pipeline SPCA2(std = Standardizer(),
                pca = PCA(),
                km = KMeans(k=3))

spca2_mdl = SPCA2()
spca2 = machine(spca2_mdl, X)
fit!(spca2)

assignments = report(spca2).reports[1].assignments
mask1 = assignments .== 1
mask2 = assignments .== 2
mask3 = assignments .== 3;

using PyPlot

figure(figsize=(8, 6))
for (m, c) in zip((mask1, mask2, mask3), ("red", "green", "blue"))
    plot(W[m, 1], W[m, 2], ls="none", marker=".", markersize=10, color=c)
end

xlabel("PCA-1", fontsize=13)
ylabel("PCA-2", fontsize=13)
legend(["Group 1", "Group 2", "Group 3"], fontsize=13)

savefig("assets/literate/ISL-lab-10-cluster.svg") # hide


############
## d1, d2 ##
############

function bsDs(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1 = log(S/(K*exp(-r*T)))/(sigma*sqrt(T)) + 0.5*sigma*sqrt(T)
    d2 = d1 - sigma*sqrt(T)

    return (d1, d2)
end

###############
## BS Prices ##
###############

function bsCall(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)

    return S*cdf(Normal(), d1) - K*exp(-r*T)*cdf(Normal(), d2)
end

function bsPut(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)

    return K*exp(-r*T)*cdf(Normal(), -d2) - S*cdf(Normal(), -d1)
end

###########
## Delta ##
###########

function bsDeltaCall(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return cdf(Normal(), d1)
end

function bsDeltaPut(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return -cdf(Normal(), -d1)
end

###########
## Gamma ##
###########

function bsGamma(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return pdf(Normal(), d1)/(S*sigma*sqrt(T))
end

##########
## Vega ##
##########

function bsVega(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return S*pdf(Normal(), d1)*sqrt(T)
end

###########
## Theta ##
###########

function bsThetaCall(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return -r*exp(-r*T)*K*cdf(Normal(), d2) - sigma*S*pdf(Normal(), d1)/(2*sqrt(T))
end

function bsThetaPut(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return r*exp(-r*T)*K*cdf(Normal(), -d2) - sigma*S*pdf(Normal(), d1)/(2*sqrt(T))
end

#########
## Rho ##
#########

function bsRhoCall(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return exp(-r*T)*K*T*cdf(Normal(), d2)
end

function bsRhoPut(sigma::Float64, S::Float64, K::Int, r::Float64, T::Float64)
    d1, d2 = bsDs(sigma, S, K, r, T)
    return -exp(-r*T)*K*T*cdf(Normal(), -d2)
end

########################
## Implied volatility ##
########################

function implVola(sigma0::Float64, P::Float64, S::Float64, K::Int,
                  r::Float64, T::Float64, prec::Float64, isCall::Bool)
    iv = []
    if isCall
        iv = implVolaCall(sigma0, P, S, K, r, T, prec)
    else
        iv = implVolaPut(sigma0, P, S, K, r, T, prec)
    end
    return iv
end

function implVolaCall(sigma0::Float64, P::Float64, S::Float64, K::Int, r::Float64, T::Float64, prec::Float64)
    # define maximum iteration size
    maxIter = 1000

    # calculate deviation
    stdNorm = Normal(0, 1)
    d1, d2 = bsDs(sigma0, S, K, r, T)
    currVega = S*pdf(stdNorm, d1)*sqrt(T)
    currPrice = S*cdf(stdNorm, d1) - K*exp(-r*T)*cdf(stdNorm, d2)
    priceDiff = P - currPrice

    iterCounter = 0
    while (abs(priceDiff) > prec) && (iterCounter < maxIter)
        # Newton Raphson
        sigma0 = sigma0 + priceDiff/currVega

        # new d1, d2, delta, price and deviation
        d1, d2 = bsDs(sigma0, S, K, r, T)
        currVega = S*pdf(stdNorm, d1)*sqrt(T)
        currPrice = S*cdf(stdNorm, d1) - K*exp(-r*T)*cdf(stdNorm, d2)
        priceDiff = P - currPrice
        iterCounter += 1
    end
    return (sigma0, priceDiff, iterCounter)
end

function implVolaPut(sigma0::Float64, P::Float64, S::Float64, K::Int, r::Float64, T::Float64, prec::Float64)
    # define maximum iteration size
    maxIter = 1000

    # calculate deviation
    stdNorm = Normal(0, 1)
    d1, d2 = bsDs(sigma0, S, K, r, T)
    currVega = S*pdf(stdNorm, d1)*sqrt(T)
    currPrice = K*exp(-r*T)*cdf(stdNorm, -d2) - S*cdf(stdNorm, -d1)
    priceDiff = P - currPrice

    iterCounter = 0
    while (abs(priceDiff) > prec) && (iterCounter < maxIter)
        # Newton Raphson
        sigma0 = sigma0 + priceDiff/currVega

        # new d1, d2, delta, price and deviation
        d1, d2 = bsDs(sigma0, S, K, r, T)
        currVega = S*pdf(stdNorm, d1)*sqrt(T)
        currPrice = K*exp(-r*T)*cdf(stdNorm, -d2) - S*cdf(stdNorm, -d1)
        priceDiff = P - currPrice
        iterCounter += 1
    end
    return (sigma0, priceDiff, iterCounter)
end


# Triangular decompostion with Partial Pivoting

p = collect (1:m)                                       # initialization of the permutation vector

for k=1:m
    j = indmax(abs(A[k:m,k])); j = k-1+j                # pivot search
    p[[k,j]] = p[[j,k]]; A[[k,j],:] = A[[j,k],:]        # row swap
    A[k+1:m,k] /= A[k,k]
    A[k+1:m,k+1:m]  -= A[k+1:m,k]*A[k:k,k+1:m]
end

Code/Numerical Analysis & Machine Learning/Numerical Linear Algebra/TriangularDecomposition.jl


## To get this as a string I pasted into vim and then did :%s/\n  //
str = "7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450"

max = 1
for i = 1:(length(str)-13)
    product = 1
    substr = str[i:i+13]
    for j = 1:13
        digit = parse(Int, substr[j])
        product = product * digit
        if product > max
            max = product
        end
    end
end

println(max)


using LinearAlgebra
using SparseArrays
using Plots

"This routine solves the periodic wave equation using finite volumes."

m = 200 # number of points

# define spatial grid
xv = LinRange(-1,1,m+2)
Δx = xv[2]-xv[1]
x = xv[1:end-1] .+ Δx/2 # m+1 cell centers

Δt = .9*Δx # timestep
T = 2.0 # final time

# initial condition and forcing
p0(x) = exp(-100*(x+.5)^2)
u0(x) = 0.

# finite volume flux function
function f(pL,uL,pR,uR)
    # transform to characteristic variables V = inv(R)*U
    # v1L = -pL+uL
    v2L = pL+uL
    v1R = pR-uR # ??
    # v2R = pR+uR
    v1 = v1R # right characteristic
    v2 = v2L # left characteristic
    pflux = .5*(-v1 + v2)
    uflux = .5*(v1 + v2)
    return pflux, uflux
end
function f(pL,uL,pR,uR)
    pflux = .5*(uL+uR) - .5*(pR-pL)
    uflux = .5*(pL+pR) - .5*(uR-uL)
    return pflux,uflux
end

p = p0.(x)
u = u0.(x)
Nsteps = ceil(Int,T/Δt)
Δt = T / Nsteps

index_left = [m+1; 1:m+1]
index_right = [1:m+1; 1]

interval = 10
plot()
unorm = zeros(Nsteps)
pflux = zeros(m+2)
uflux = zeros(m+2)
@gif for k = 1:Nsteps
    global u,f
    for i = 1:m+2 # loop over cell interfaces
        left = index_left[i]
        right = index_right[i]
        flux_i = f(p[left],u[left],p[right],u[right])
        pflux[i] = flux_i[1]
        uflux[i] = flux_i[2]
    end
    p .= p .- Δt/Δx .* diff(pflux)
    u .= u .- Δt/Δx .* diff(uflux)

    if k % interval==0
        plot(x,p,linewiΔth=2,legend=false,title="Solution at time $(k*Δt)",ylims=(-1.5,1.5))
        println("on timestep $k out of $Nsteps.")
    end
end every interval
# plot(x,u,linewiΔth=2,legend=false,title="Solution at final time $(T)",ylims=(-1.5,1.5))


"""
    SEIso <: Isotropic{SqEuclidean}

Isotropic Squared Exponential kernel (covariance)
```math
k(x,x') = σ²\\exp(- (x - x')ᵀ(x - x')/(2ℓ²))
```
with length scale ``ℓ`` and signal standard deviation ``σ``.
"""
mutable struct SEIso <: Isotropic{SqEuclidean}
    "Squared length scale"
    ℓ2::Float64
    "Signal variance"
    σ2::Float64
    "Priors for kernel parameters"
    priors::Array

    """
        SEIso(ll::Float64, lσ::Float64)

    Create `SEIso` with length scale `exp(ll)` and signal standard deviation `exp(lσ)`.
    """
    SEIso(ll::Float64, lσ::Float64) = new(exp(2 * ll), exp(2 * lσ), [])
end

function set_params!(se::SEIso, hyp::VecF64)
    length(hyp) == 2 || throw(ArgumentError("Squared exponential only has two parameters"))
    se.ℓ2, se.σ2 = exp(2 * hyp[1]), exp(2 * hyp[2])
end

get_params(se::SEIso) = Float64[log(se.ℓ2) / 2, log(se.σ2) / 2]
get_param_names(se::SEIso) = [:ll, :lσ]
num_params(se::SEIso) = 2

Statistics.cov(se::SEIso, r::Float64) = se.σ2*exp(-0.5*r/se.ℓ2)

@inline dk_dll(se::SEIso, r::Float64) = r/se.ℓ2*cov(se,r)
@inline function dk_dθp(se::SEIso, r::Float64, p::Int)
    if p==1
        return dk_dll(se, r)
    elseif p==2
        return dk_dlσ(se, r)
    else
        return NaN
    end
end