using SymPy

# National Income Model

eq1 =  SymFunction("eq1")
eq2 =  SymFunction("eq2")

Y, C, b, I₀, G₀, a = symbols("Y C b I₀ G₀, a")

eq1 = Eq(Y - C, I₀ + G₀)
eq2 = Eq(-b*Y + C, a)

display(eq1)
display(eq2)

row1 = [1 -1 I₀ + G₀]
row2 = [-b 1 a]

system  = sympy.Matrix((row1, row2))

display(system)

solution = linsolve(system, (Y, C))
display(solution)

# Demand and Supply Model

eq1 = SymFunction("eq1")
eq2 = SymFunction("eq2")
eq3 = SymFunction("eq3")

Qᵈ, Qˢ, a, b, P, f, Y, c, d, g, Pᵢ = symbols("Qᵈ Qˢ a b P f Y c d g Pᵢ")

eq1 = Eq(Qᵈ + b*P, a + f*Y)
eq2 = Eq(Qˢ + d*s, c - g*Pᵢ)
eq3 = Eq(Qᵈ - Qˢ)

row1 = [1 0 b a+f*Y]
row2 = [0 1 -d c-g*Pᵢ]
row3 = [1 -1 0 0]

system = sympy.Matrix(((1, 0, b, a+f*Y), (0, 1, -d, c-g*Pᵢ), (1, -1, 0, 0)))

solution = linsolve(system, (Qᵈ, Qˢ, P))

display(solution)

sympy.init_printing(use_latex = "mathjax")


## Exercise 5-7
## https://benlauwens.github.io/ThinkJulia.jl/latest/images/fig52.svg
## Figure 8. A Koch curve

## The Koch curve is a fractal that looks something like A Koch curve. To draw a Koch curve with length x, all you have to do is

## 1. Draw a Koch curve with length x/3.
## 2. Turn left 60 degrees.
## 3. Draw a Koch curve with length x/3.
## 4. Turn right 120 degrees.
## 5. Draw a Koch curve with length x/3.
## 6. Turn left 60 degrees.
## 7. Draw a Koch curve with length x/3.

## The exception is if x is less than 3: in that case, you can just draw a straight line with length x.

using ThinkJulia

## 1. Write a function called koch that takes a turtle and a length as parameters, and that uses the turtle to draw a Koch curve with the given length.
println("Ans 1: ")

function koch(turtle::Turtle, length)
    if length < 3
        forward(turtle, length)
        return
    end

    koch(turtle, length/3)
    turn(turtle, 60)
    koch(turtle, length/3)
    turn(turtle, -120)
    koch(turtle, length/3)
    turn(turtle, 60)
    koch(turtle, length/3)
end

turtle = Turtle()
# @svg begin
#     koch(turtle, 100)
# end

## 2. Write a function called snowflake that draws three Koch curves to make the outline of a snowflake.
println("Ans 2: ")

function snowflake(turtle, length)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
    koch(turtle, length)
    turn(turtle, -60)
end

# @svg begin
#     snowflake(turtle, 100)
# end

## 3. The Koch curve can be generalized in several ways. See https://en.wikipedia.org/wiki/Koch_snowflake for examples and implement your favorite.
println("Ans 3: ")

function koch_square(turtle::Turtle, length)
    if length < 3
        forward(turtle, length)
        return
    end

    koch_square(turtle, length/3)
    turn(turtle, 90)
    koch_square(turtle, length/3)
    turn(turtle, -90)
    koch_square(turtle, length/3)
    turn(turtle, -90)
    koch_square(turtle, length/3)
    turn(turtle, 90)
    koch_square(turtle, length/3)
    turn(turtle, 90)
end

@svg begin
    koch_square(turtle, 60)
end

println("End.")


#=
The sequence of triangle numbers is generated by adding the natural numbers. So the 7th triangle number would be 1 + 2 + 3 + 4 + 5 + 6 + 7 = 28. The first ten terms would be:

1, 3, 6, 10, 15, 21, 28, 36, 45, 55, ...

Let us list the factors of the first seven triangle numbers:

 1: 1
 3: 1,3
 6: 1,2,3,6
10: 1,2,5,10
15: 1,3,5,15
21: 1,3,7,21
28: 1,2,4,7,14,28
We can see that 28 is the first triangle number to have over five divisors.

What is the value of the first triangle number to have over five hundred divisors?
=#

function num_divisors(n)
  count,i = 2,2 # accounts for 'n' and '1'
  while (i^2 <= n)
    if n%i==0
      count += 2
    end
    i += 1
  end
  count
end

function triangle_number(n)
  return convert(Int64,(1/2)*n*(n+1))
end

function calc()
  i = 2
  while num_divisors(triangle_number(i)) < 500
    i += 1
  end
  triangle_number(i)
end
@time println(calc())


#=
The area of a circle is defined as πr^2. Estimate π to 3 decimal places using a Monte Carlo method.

Hint: The basic equation of a circle is x2 + y2 = r2
=#
function estimate_pi(;number_of_decimal::Int64 = 3)
    in = 0
    out = 0
    pi_estimated = 0.0
    n_points = 0
    while round(pi_estimated; digits = number_of_decimal) != round(pi; digits = number_of_decimal)
        x, y = rand(2)
        if (x^2 + y^2) <= 1
            in = in + 1
        end
        n_points = n_points + 1
        pi_estimated = in/n_points * 4
    end
    return pi_estimated
end


# This script solves a Fredholm equation of the first type

println('\n', " "^4, "> Loading the packages...")

using LinearAlgebra # Norm
using Optim # Optimization
using Plots # Plotting
using Printf # Formatted printing
using QuadGK # Gauss–Kronrod integration

# Use the GR backend for plots
gr()

# Change the default font for plots
default(fontfamily="Computer Modern", dpi=300, legend=:outerright)

println(" "^4, "> Computing the solution (Test)...")

# Define the kernel (Test)
K(x, s) = exp(s * x)

# Define the right part of the equation (Test)
u(x) = (exp(x + 1) - 1) / (x + 1)

# Set the integration intervals
a = 0
b = 1
c = 0
d = 1

# Set the number of nodes
n = 100

# Calculate the step
h = (b - a) / n

# Calculate the nodes for the s argument
s = [ a + 0.5 * h + (i - 1) * h for i in 1:n ]

# Set the initial value of the regularization parameter
α = 0.001

# Prepare a range of nodes for the residual calculation
xr = range(c, d; length=1000)

# Compute the residual of the solution with the specified number of nodes and the regularization parameter
function calculate(n::Int, α::Float64)::Tuple{Vector{Float64},Float64}
    # Calculate the step
    h = (b - a) / n

    # Calculate the nodes for the s argument
    s = [ a + 0.5 * h + (i - 1) * h for i in 1:n ]

    # Calculate the nodes for the t argument
    t = copy(s)

    # Compute the g vector
    g = [ quadgk(x -> K(x, s[i]) * u(x), c, d; rtol=1e-8)[1] for i in 1:n ]

    # Prepare a matrix for the computation
    Bα = Matrix{Float64}(undef, n, n)

    # Compute the Bα matrix
    for i in 1:n, j in 1:n
        Bα[i, j] = quadgk(x -> K(x, s[i]) * K(x, t[j]), c, d; rtol=1e-8)[1] * h + (i == j ? α : 0)
    end

    # Compute the solution
    z = Symmetric(Bα) \ g

    # Calculate the residual
    r = norm([ sum(K.(x, s) .* z .* h) for x in xr ] .- u.(xr))

    return z, r
end

# Compute the residual of the solution with the specified regularization parameter (optimization only)
function residual(θ::Vector{Float64})::Float64
    return calculate(100, θ[1])[2]
end

# Optimize over the regularization parameter
res = Optim.optimize(
    residual,
    [α,],
    LBFGS(),
    Optim.Options(
        show_trace=false,
        extended_trace=true,
        store_trace=true,
    );
    inplace=false,
)

# Save the trace and the results
open("test_optimization.trace", "w") do io
    println(io, res.trace)
    println(
        io,
        " * Parameters:\n",
        " "^4, "α = $(res.minimum)\n",
    )
    show(io, res)
end

# Print the residual
println(
    '\n',
    " "^6, "Regularization parameter: ", res.minimizer[1], '\n',
    " "^6, "Residual: ", res.minimum, '\n',
)

println(" "^4, "> Plotting the solution (Test)...")

# Recalculate the solution
z = calculate(n, res.minimizer[1])

# Plot the solution
p = plot(s, z; label="Приближенное решение", xlabel="s", ylabel="z(s)");

# Add the solution to the plot
plot!(p, x -> exp(x), a, b; label="Точное решение");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/test.pdf")

println('\n', " "^6, "* The figure `test.pdf` is saved. *", '\n')

println(" "^4, "> Plotting the residuals against the number of nodes (Test)...")

# Define the `n`s to plot against
ns = [ i for i in 10:300 ]

# Calculate the residuals for these `n`s
rs = @. getindex(calculate(ns, res.minimizer[1]), 2)

# Make a plot of the residual against `n`
p = plot(ns, rs; label="", xlabel="n", ylabel="r");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/test_residual.pdf")

println('\n', " "^6, "* The figure `test_residual.pdf` is saved. *", '\n')

println(" "^4, "> Computing the solution (Assignment)...")

# Define the kernel
K(x, s) = 1 / (1 + x + s)

# Define the right part of the equation
u(x) = 1 / sqrt(2 - x) * (log((2 + x) / (1 + x)) +
       2 * log((sqrt(3) + sqrt(2 - x)) / (2 + sqrt(2 - x))))

# Compute the g vector
g = [ quadgk(x -> K(x, s[i]) * u(x), c, d; rtol=1e-8)[1] for i in 1:n ]

# Optimize over the regularization parameter
res = Optim.optimize(
    residual,
    [α,],
    LBFGS(),
    Optim.Options(
        show_trace=false,
        extended_trace=true,
        store_trace=true,
    );
    inplace=false,
)

# Save the trace and the results
open("assignment_optimization.trace", "w") do io
    println(io, res.trace)
    println(
        io,
        " * Parameters:\n",
        " "^4, "α = $(res.minimum)\n",
    )
    show(io, res)
end

# Print the residual
println(
    '\n',
    " "^6, "Regularization parameter: ", res.minimizer[1], '\n',
    " "^6, "Residual: ", res.minimum, '\n',
)

println(" "^4, "> Plotting the solution (Assignment)...")

# Recalculate the solution
z = calculate(n, res.minimizer[1])

# Plot the solution
p = plot(s, z; label="", xlabel="s", ylabel="z(s)");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/assignment.pdf")

println('\n', " "^6, "* The figure `assignment.pdf` is saved. *", '\n')

println(" "^4, "> Plotting the residuals against the number of nodes (Assignment)...")

# Define the `n`s to plot against
ns = [ i for i in 10:300 ]

# Calculate the residuals for these `n`s
rs = @. getindex(calculate(ns, res.minimizer[1]), 2)

# Make a plot of the residual against `n`
p = plot(ns, rs; label="", xlabel="n", ylabel="r");

# Save the figure
savefig(p, "$(@__DIR__)/../plots/assignment_residual.pdf")

println('\n', " "^6, "* The figure `assignment_residual.pdf` is saved. *", '\n')

println(" "^4, "> Calculating the solutions for tables (Assignment)...")

# Define the header
header = "α = 1e-4" * " "^7 * "1e-5" * " "^11 * "1e-6" * " "^11 * "1e-7" *
         " "^11 * "1e-8" * " "^11 * "1e-9" * " "^11 * "1e-10" * " "^10 * "1e-11" *
         " "^10 * "1e-12" * " "^10 * "1e-13" * " "^10 * "1e-14" * " "^10 * "1e-15"

# Calculate the solutions for different numbers of nodes and regularization parameters
for n in [100, 200, 300]
    step = Int(n * 0.05)
    data = Matrix{Float64}(undef, 20, 12)
    format = Printf.Format("%.8e "^11 * "%.8e\n")
    open("$(@__DIR__)/../tables/$(n).dat", "w") do io
        println(io, header)
        for (i, α) in enumerate([1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, 1e-13, 1e-14, 1e-15])
            sol, _ = calculate(n, α)
            data[:, i] = sol[1:step:end]
        end
        for i in 1:20
            Printf.format(io, format, data[i, :]...)
        end
    end
end

println('\n', " "^6, "* Tables `100.dat`, `200.dat`, and `300.dat` are saved. *", '\n')


# This file is a part of AstroLib.jl. License is MIT "Expat".
# Copyright (C) 2016 Mosè Giordano.

_trueanom{T<:AbstractFloat}(E::T, e::T) =
    if e < 0
        # We don't need to check e > 1, because a DomainError is raised anyway.
        error("eccentricity must be in the range [0, 1]")
    else
        2.0*atan(sqrt((1.0 + e)/(1.0 - e))*tan(E/2.0))
    end

"""
    trueanom(E, e) -> true anomaly

### Purpose ###

Calculate true anomaly for a particle in elliptic orbit with eccentric anomaly
\$E\$ and eccentricity \$e\$.

### Explanation ###

In the two-body problem, once that the [Kepler's
equation](https://en.wikipedia.org/wiki/Kepler%27s_equation) is solved and
\$E(t)\$ is determined, the polar coordinates \$(r(t), \\theta(t))\$ of the body
at time \$t\$ in the elliptic orbit are given by

\$\$ \\theta(t) = 2\\arctan \\left(\\sqrt{\\frac{1 + e}{1 - e}} \\tan\\frac{E(t)}{2} \\right)\$\$
\$\$ r(t) = \\frac{a(1 - e^{2})}{1 + e\\cos(\\theta(t) - \\theta_{0})}\$\$

in which \$a\$ is the semi-major axis of the orbit, and \$\\theta_0\$ the value
of angular coordinate at time \$t = t_{0}\$.

### Arguments ###

* `E`: eccentric anomaly.  This can be either a scalar or an array
* `e`: eccentricity, in the elliptic motion regime (\$0 \\leq e \\leq 1\$)

### Output ###

The true anomaly.  If an array of eccentric anomalies is provided in input, an
array of the same length as `E` is returned.

### Example ###

Plot the true anomaly as a function of mean anomaly for eccentricity \$e = 0\$,
\$0.5\$, \$0.9\$.  Use [PyPlot.jl](https://github.com/stevengj/PyPlot.jl) for
plotting.

``` julia
using PyPlot
M=linspace(0, 2pi, 1001)[1:end-1];
for ecc in (0, 0.5, 0.9)
    E = kepler_solver(M, ecc);
    plot(M, cirrange(trueanom(E, ecc), 2pi))
end
```

### Notes ###

The eccentric anomaly can be calculated with `kepler_solver` function.
"""
trueanom(E::Real, e::Real) = _trueanom(promote(float(E), float(e))...)

function trueanom{R1<:Real,R2<:Real}(E::AbstractArray{R1}, e::R2)
    typeν = promote_type(float(R1), float(R2))
    ν = similar(E, typeν)
    for i in eachindex(E)
        ν[i] = trueanom(E[i], e)
    end
    return ν
end


# Imports
using Random, Statistics, Plots, Printf
Random.seed!(430);

# Part (a)
N = 10000;
Xₙ = randn(2, 1);
μₓ = [0.5; 0.3];
Cₓ = [1 0.6; 0.6 sqrt(2)];

# 2-dimensional Gaussian vector
X = Cₓ * Xₙ + μₓ;
println("Answer of part (a)")
println("2-dimensional Gaussian vector:")
println(X)

# Part (b)
# Generate 10000 vector with related parameters
function generate(matrix, μₓ, n)
    samples = zeros(N, 2)
    for i in 1:n
        X = randn(2, 1);
        X = sqrt(matrix) * X + μₓ;
        samples[i, :] = X;
    end
    return samples
end

# Plot generated samples
samples = generate(Cₓ, μₓ, N)
X₁, X₂ = samples[:, 1], samples[:, 2];
display(plot(X₁, X₂, seriestype = :scatter, title = "Scatter Plot"))

# Part (c)
#∑ = zeros(2,2)
∑ = []
for i in 1:N
    #∑ += samples[i, :]*samples[i, :]'
    push!(∑, samples[i, :]*samples[i, :]')
end
#Sₓ = ∑/N
Sₓ = sum(∑) / N;
println("\nAnswer of part (c) for $N samples: ")
println(Sₓ)

# When we increase "N", convergence will be [1.25 0.75; 0.75 1.504] matrix
println("Convergence limit matrix:")
convergence = Cₓ + μₓ*μₓ'


# # [Ecosystem Models](@id ecosystem_example)
#
#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/examples/Ecosystem.ipynb)

using AlgebraicDynamics
using AlgebraicDynamics.DWDDynam
using AlgebraicDynamics.UWDDynam

using Catlab.CategoricalAlgebra
using Catlab.WiringDiagrams
using Catlab.Graphics
using Catlab.Programs

using OrdinaryDiffEq
using Plots, Plots.PlotMeasures

# ## Land Ecosystem

# ### Rabbits and foxes

# A standard Lotka Volterra predator-prey model is the composition of three primitive resource sharers:

# 1. a model of rabbit growth: this resource sharer has dynamics $\dot r(t) = \alpha r(t)$ and one port which exposes the rabbit population.
# 2. a model of rabbit/fox predation: this resource sharer has dynamics $$\dot r(t) = -\beta r(t) f(t), \dot f(t) = \gamma r(t)f(t)$$ and two ports which expose the rabbit and fox populations respectively.
# 3. a model of fox population decline: this resource sharer has dynamics $\dot f(t) = -\delta f(t)$ and one port which exposes the fox population.

# However, there are not two independent rabbit populations -- one that grows and one that gets eaten by foxes. Likewise, there are not two independent fox populations -- one that declines and one that feasts on rabbits. To capture these interactions between the trio of resource sharers, we compose them by identifying the exposed rabbit populations and identifying the exposed fox populations.
# The syntax for this undirected composition is defined by an undirected wiring diagram.

## Define the primitive systems
dotr(u,p,t) = p[1]*u
dotrf(u,p,t) = [-p[2]*u[1]*u[2], p[3]*u[1]*u[2]]
dotf(u,p,t) = -p[4]*u

rabbit_growth = ContinuousResourceSharer{Float64}(1, dotr)
rabbitfox_predation = ContinuousResourceSharer{Float64}(2, dotrf)
fox_decline = ContinuousResourceSharer{Float64}(1, dotf)

## Define the composition pattern
rabbitfox_pattern = @relation (rabbits, foxes) begin
    rabbit_growth(rabbits)
    rabbitfox_predation(rabbits,foxes)
    fox_decline(foxes)
end

## Compose
rabbitfox_system = oapply(rabbitfox_pattern, [rabbit_growth, rabbitfox_predation, fox_decline])

# Previously, when we derived the Lotka-Volterra model via [undirected composition](https://algebraicjulia.github.io/AlgebraicDynamics.jl/dev/examples/Lotka-Volterra/#Undirected-composition), we by-hand defined the undirected wiring diagram that implements the composition pattern. In contrast, here we implement the same composition pattern as before but this time using the [`@relation` macro](https://algebraicjulia.github.io/Catlab.jl/stable/apis/programs/#Catlab.Programs.RelationalPrograms.@relation-Tuple). This strategy simplifies the definition and explicitly names the boxes and variables. We  visualize the composition pattern below.

to_graphviz(rabbitfox_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))

# We can now construct an `ODEProblem` from the resource sharer `rabbitfox_system` and plot the solution.

α, β, γ, δ = 0.3, 0.015, 0.015, 0.7
params = [α, β, γ, δ]

u0 = [10.0, 100.0]
tspan = (0.0, 100.0)

prob = ODEProblem(rabbitfox_system, u0, tspan, params)
sol = solve(prob, Tsit5())

plot(sol, lw=2, title = "Lotka-Volterra Predator-Prey Model", bottom_margin=10mm, left_margin=10mm, label=["rabbits" "foxes"])
xlabel!("Time")
ylabel!("Population size")

# ### Rabbits, foxes, and hawks
# Suppose we now have a three species ecosystem containing rabbits, foxes, and hawks. Foxes and hawks both prey upon rabbits but do not interact with each other. This ecosystem consists of five primitive systems which share variables.
# 1. rabbit growth:  $\dot r(t) = \alpha r(t)$
# 2. rabbit/fox predation:  $\dot r(t) = -\beta r(t) f(t), \dot f(t) = \delta r(t)f(t)$
# 3. fox decline:  $\dot f(t) = -\gamma f(t)$
# 4. rabbit/hawk predation: $\dot r(t) = -\beta' r(t)h(t), \dot h(t) = \delta' r(t)h(t)$
# 5. hawk decline:  $\dot h(t) = -\gamma' h(t)$

# This means the desired composition pattern has five boxes and many ports and wires to keep track of. Instead of implementing this composition pattern by hand, we construct it as a pushout.

## Define the composition pattern for rabbit growth
rabbit_pattern = @relation (rabbits,) -> rabbit_growth(rabbits)

## Define the composition pattern for the rabbit/hawk Lotka Volterra model
rabbithawk_pattern = @relation (rabbits, hawks) begin
    rabbit_growth(rabbits)
    rabbit_hawk_predation(rabbits,hawks)
    hawk_decline(hawks)
end

## Define transformations between the composition patterns
rabbitfox_transform  = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbitfox_pattern)
rabbithawk_transform = ACSetTransformation((Box=[1], Junction=[1], Port=[1], OuterPort=[1]), rabbit_pattern, rabbithawk_pattern)

## Take the pushout to define the composition pattern for the rabbit, fox, hawk system
rabbitfoxhawk_pattern = ob(pushout(rabbitfox_transform, rabbithawk_transform))

## Visualize the compsition pattern
to_graphviz(rabbitfoxhawk_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".9"))

#-
## Define the additional primitive systems
dotrh(x, p, t) = [-p[5]*x[1]*x[2], p[6]*x[1]*x[2]]
doth(x, p, t)  = -p[7]*x

rabbithawk_predation = ContinuousResourceSharer{Float64}(2, dotrh)
hawk_decline         = ContinuousResourceSharer{Float64}(1, doth)

## Compose
land_system = oapply(rabbitfoxhawk_pattern,
                        [rabbit_growth, rabbitfox_predation, fox_decline,
                         rabbithawk_predation, hawk_decline])

## Solve and plot
β′, γ′, δ′ = .01, .01, .5
params = vcat(params, [β′, γ′, δ′])

u0 = [10.0, 100.0, 50.0]
tspan = (0.0, 100.0)

prob = ODEProblem(land_system, u0, tspan, params)
sol = solve(prob, Tsit5())

plot(sol, lw=2, title = "Land Ecosystem", bottom_margin=10mm, left_margin=10mm, label=["rabbits" "foxes" "hawks"])
xlabel!("Time")
ylabel!("Population size")

# Unfortunately, the hawks are going extinct in this model. We'll have to give hawks something else to eat!
#-
# ## Ocean Ecosystem

# Consider a ocean ecosystem containing three species —- little fish, big fish, and sharks -— with two predation interactions —- sharks eat big fish and big fish eat little fish.

# This ecosystem can be modeled as the composition of 3 machines:
# 1. Evolution of the little fish population:  this machine has one exogenous variable which represents a population of predators $h(t)$ that hunt little fish. This machine has one output which emits the little fish population. The dynamics of this machine is the driven ODE $$\dot f(t) = \alpha f(t) - \beta f(t)h(t)$$
# 2. Evolution of the big fish population:  this machine has two exogenous variables which represent a population of prey $e(t)$ that are eaten by big fish and a population of predators $h(t)$ which hunt big fish. This machine has one output which emits the big fish population. The dynamics of this machine is the drive ODE $$\dot F(t) = \gamma F(t)e(t) - \delta F(t) - \beta'F(t)h(t)$$
# 3. Evolution of the shark population:  this machine has one exogenous variable which represents a population of prey $e(t)$ that are eaten by sharks. This machine has one output which emits the shark population. The dynamics of this machine is the driven ODE $$\dot s(t) = \gamma's(t)e(t) - \delta's(t)$$

## Define the primitive systems
dotfish(f, x, p, t) = [p[1]*f[1] - p[2]*x[1]*f[1]]
dotFISH(F, x, p, t) = [p[3]*x[1]*F[1] - p[4]*F[1] - p[5]*x[2]*F[1]]
dotsharks(s, x, p, t) = [p[6]*s[1]*x[1]-p[7]*s[1]]

fish   = ContinuousMachine{Float64}(1,1,1, dotfish,   f->f)
FISH   = ContinuousMachine{Float64}(2,1,1, dotFISH,   F->F)
sharks = ContinuousMachine{Float64}(1,1,1, dotsharks, s->s)

# We compose these machines by (1) sending the output of the big fish machine as the input to both the little fish and shark machines and (2) sending the output of the little fish and shark machines as the inputs to the big fish machine.
# The syntax for this directed composition is given by a directed wiring diagram.

## Define the composition pattern
ocean_pattern = WiringDiagram([], [])
fish_box = add_box!(ocean_pattern, Box(:fish, [:pop], [:pop]))
Fish_box = add_box!(ocean_pattern, Box(:Fish, [:pop, :pop], [:pop]))
shark_box = add_box!(ocean_pattern, Box(:shark, [:pop], [:pop]))

add_wires!(ocean_pattern, Pair[
    (fish_box, 1)  => (Fish_box, 1),
    (shark_box, 1) => (Fish_box, 2),
    (Fish_box, 1)  => (fish_box, 1),
    (Fish_box, 1)  => (shark_box, 1)
])

## Visualize the composition pattern
to_graphviz(ocean_pattern, orientation=TopToBottom)

#-

## Compose
ocean_system = oapply(ocean_pattern, [fish, FISH, sharks])

## Solve and plot
α, β, γ, δ, β′, γ′, δ′ = 0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35
params = [α, β, γ, δ, β′, γ′, δ′]

u0 = [100.0, 10, 2.0]
tspan = (0.0, 100.0)

prob = ODEProblem(ocean_system, u0, tspan, params)
sol = solve(prob, FRK65(0))

plot(sol, lw=2, title = "Ocean Ecosystem", bottom_margin=10mm, left_margin=10mm, label=["little fish" "big fish" "sharks"])
xlabel!("Time")
ylabel!("Population size")

# ## Total ecosystem
# ### Another layer of composition

# We will introduce a final predation interaction -- hawks eat little fish --  which will combine the land and ocean ecosystems.

# There will be 16 parameters in to the total ecosystem.
# - parameters 1-7 will determine the land ecosystem
# - parameters 8 and 9 will determine the hawk/little fish predation. Parameter 8 gives the rate of hawk growth and parameter 8 gives the rate of little fish decline.
# - parameter 10-16 will determine the ocean ecosystem.

# The composition will be as resource shareres so the first thing we will do is use the dynamics of the machine `ocean_system` to define the dynamics of a resource sharer. We will also define a resource sharer that models hawk/little fish predation.

## Define the additional primitive systems
ocean_system_rs = ContinuousResourceSharer{Float64}(3, (u,p,t)->eval_dynamics(ocean_system, u, [], p[10:16]))

dothf(u,p,t) = [p[8]*u[1]*u[2], -p[9]*u[1]*u[2]]
fishhawk_predation = ContinuousResourceSharer{Float64}(2, dothf)

## Define the composition pattern
eco_pattern = @relation () where (rabbits, foxes, hawks, littlefish, BigFish, sharks)  begin
    turf(rabbits,foxes,hawks)
    air(hawks, littlefish)
    surf(littlefish, BigFish, sharks)
end

## Visualize the composition pattern
to_graphviz(eco_pattern, box_labels = :name, junction_labels = :variable, edge_attrs=Dict(:len => ".75"))

#-
## Compose
ecosystem=oapply(eco_pattern, [land_system, fishhawk_predation, ocean_system_rs])

# We can now plot the evolution of the total ecosystem.

## Solve and plot
u0 = [100.0, 50.0, 20.0, 100, 10, 2.0]
tspan = (0.0, 100.0)

params = [0.3, 0.015, 0.015, 0.7, .01, .01, .5,
          0.001, 0.003,
          0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35]

prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])

# Let's zoom in on a narrower time-window.
tspan = (0.0, 30.0)

prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])

# As a sanity check we can define the rates for the hawk/little fish predation to be 0. This decouples the land and ocean ecosystems. As expected, the plot shows the original evolution of the land ecosystem overlayed with the original evolution of the ocean ecosystem. This shows that they two ecosystems now evolve independently.

tspan = (0.0, 100.0)
params = [0.3, 0.015, 0.015, 0.7, .01, .01, .5,
          0, 0,
          0.35, 0.015, 0.015, 0.7, 0.017, 0.017, 0.35]
prob = ODEProblem(ecosystem, u0, tspan, params)
sol = solve(prob, Tsit5())
plot(sol, lw=2, bottom_margin=10mm, left_margin=10mm, label = ["rabbits" "foxes" "hawks" "little fish" "big fish" "sharks"])


##############   EXAMPLE-3     ##############
# Correlated errors in X and Y

using LinearFitXYerrors
using Plots; gr()

# INPUT DATA:
# Mahon, K. [1996] The New “York” Regression: Application of an Improved Statistical Method to Geochemistry. International Geology Review, 38(4), pp.293–303

#  Table 3
#      X        Y
M = [0.037   0.0080
     0.035   0.0084
     0.032   0.0100
     0.040   0.0085
     0.013   0.0270
     0.038   0.0071
     0.042   0.0043
     0.030   0.0160]

X = M[:,1]
Y = M[:,2]
σX = 0.03*X     # standard deviation of errors in X
σY = 0.10*Y     # standard deviation of errors in Y
r1 = 0  # correlation coefficient between errors
r2 = 0.7071

# COMPUTE and PLOT:

# st = linearfitxy(X, Y; isplot=true, ratio=:auto);  # if assuming no errors
# stxy1 = linearfitxy(X, Y; σX=σX, σY=σY, r=r1, isplot=true, ratio=:auto);

stxy2 = linearfitxy(X, Y; σX=σX, σY=σY, r=r2, isplot=true, ratio=:auto);

savefig("Example3_LinearFitXYerrors.png")
########################################


using LinearAlgebra
using SparseArrays
using Plots

"This routine solves the periodic advection 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

a = 1 # advection speed
Δt = .9*Δx / a # timestep
T = 2.0 # final time

# initial condition and forcing
u0(x) = exp(-25*x^2)
u = u0.(x)
f(uL,uR) = uL

# indexing for periodic boundaries
index_left = [m+1; 1:m+1]
index_right = [1:m+1; 1]
fv_flux = zeros(m+2) # storage at interfaces

Nsteps = ceil(Int,T/Δt)
Δt = T / Nsteps
interval = 10
plot()
unorm = zeros(Nsteps)
@gif for k = 1:Nsteps
    for i = 1:m+2 # loop over cell interfaces
        left = index_left[i]
        right = index_left[i]
        fv_flux[i] = f(u[left],u[right])
    end
    for i = 1:m+1
        u[i] = u[i] - a*Δt/Δx * (fv_flux[i+1]-fv_flux[i])
    end

    if k % interval==0
        plot(x,u,linewidth=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))


"""
    SEq1

Solutions of Equations in One Variable
here are functions to find root:
1. Bisection
2. Fixed-Point Iteration
3. Newton Method
4. The Secant Method
5. The False Position Method
6.  Müller’s Method
"""
module SEq1
using ForwardDiff

"""
    Bisection(f::Function, a::Real, b::Real; TOL::Float64=0.0000001, N::Int=40, output_seq::Bool=false)

Use Bisection To find a solution to.

``f(x) = 0``

You should use the function, and a interval ``[a, b]``, tolerance TOL, maximum number of iterations N.

Then you will get approximate solution ``p`` or can't find in maximum N and return -1.

you can set output_seq=true to output sequence.
"""
@inline function Bisection(f::Function, a::Real, b::Real; TOL::Float64=0.0000001, N::Int=40, output_seq::Bool=false)
    i = 1
    fa = f(a)
    seq = []
    if f(a)*f(b) > 0
        return -1
    else
        while i <= N
            p = a + (b - a)/2
            fp = f(p)
            push!(seq, [p, f(p)])
            if fp == 0 || (b - a)/2 < TOL
                if output_seq
                    return seq
                else
                    return p
                end
                break
            end
            i += 1
            if fa*fp > 0
                a = p
                fa = fp
            else
                b = p
            end
        end
        return -1
    end
end

"""
    fixed_point(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)

Use fixed point Iteration to find root.

The number p is a **fixed point** for a given function g if ``g(p) = p``.

input a function, initial p₀, optional Args: tolerancs TOL, maximum number of iterations N, output_seq.

Output a root p or sequence.
"""
@inline function fixed_point(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)
    i = 1
    seq = []
    while i <= N
        p = f(p₀)
        push!(seq, p₀)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p
    end
    return -1
end

"""
    Newton(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=20, output_seq=false)

Use Newton method to find root.

you should use a function and initial p₀, then you can set Args or use default.

"""
@inline function Newton(f::Function, p₀::Real; TOL::Float64=0.0000001, N::Int=20, output_seq::Bool=false)
    i = 1
    seq = []
    f1(x) = ForwardDiff.derivative(x -> f(x), x)
    while i <= N
        push!(seq, p₀)
        p = p₀ - f(p₀)/f1(p₀)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        p₀ = p
        i += 1
    end
    return -1
end

"""
    Secant(f::Function, p₀::Real, )

Use the Secant Method to find root
A function and p₀, p₁(p₀<p₁), then you can set Args or use default.
"""
@inline function Secant(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.0000001, N::Int=30, output_seq::Bool=false)
    i = 2
    seq = []
    a₀ = f(p₀)
    a₁ = f(p₁)
    push!(seq, p₀)
    push!(seq, p₁)
    while i <= N
        p = p₁ - a₁*(p₁ - p₀)/(a₁ - a₀)
        push!(seq, p)
        if abs(p - p₁) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p₁
        a₀ = a₁
        p₁ = p
        a₁ = f(p)
    end
end

"""
    FalsePos(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.00000001, N::Int=40, output_seq::Bool=false)

Use the false position method to find root.
a function and interval ``[a, b]``, then you can set Args or use default.
"""
@inline function FalsePos(f::Function, p₀::Real, p₁::Real; TOL::Float64=0.00000001, N::Int=40, output_seq::Bool=false)
    i = 2
    seq = []
    q₀ = f(p₀)
    q₁ = f(p₁)
    push!(seq, p₀)
    push!(seq, p₁)
    while i <= N
        p = p₁ - q₁*(p₁ - p₀)/(q₁ - q₀)
        push!(seq, p)
        if abs(p - p₁) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        q = f(p)
        if q*q₁ < 0
            p₀ = p₁
            q₀ = q₁
        end
        p₁ = p
        q₁ = q
    end
    return -1
end

"""
    ModifiedNewton(f::Function, p₀::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)

Newton and ModifiedNewton, Both methods are rapidly convergent to the actual zero.
"""
@inline function ModifiedNewton(f::Function, p₀::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
    i = 1
    seq = []
    push!(seq, p₀)
    f1(x) = ForwardDiff.derivative(x -> f(x), x)
    f11(x) = ForwardDiff.derivative(x -> f1(x), x)
    while i <= N
        p = p₀ - f(p₀)*f1(p₀)/((f1(p₀))^2 - f(p₀)*f11(p₀))
        push!(seq, p)
        if abs(p - p₀) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        i += 1
        p₀ = p
    end
    return -1
end

"""
    Muller(f::Function, p₀::Real, p₁::Real, p₂::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
"""
@inline function Muller(f::Function, p₀::Real, p₁::Real, p₂::Real; TOL::Float64=0.00000001, N::Int=20, output_seq::Bool=false)
    h₁ = p₁ - p₀
    h₂ = p₂ - p₁
    δ₁ = (f(p₁) - f(p₀))/h₁
    δ₂ = (f(p₂) - f(p₁))/h₂
    d = (δ₂ - δ₁)/(h₁ + h₂)
    i = 3
    seq = []
    while i <= N
        b = δ₂ + h₂*d
        D = sqrt(complex(b^2 - 4*f(p₂)*d))
        if abs(b - D) < abs(b + D)
            E = b + D
        else
            E = b - D
        end
        h = -2 * f(p₂)/E
        p = p₂ + h
        push!(seq, p)
        if abs(h) < TOL
            if output_seq
                return seq
            else
                return p
            end
            break
        end
        p₀ = p₁
        p₁ = p₂
        p₂ = p
        h₁ = p₁ - p₀
        h₂ = p₂ - p₁
        δ₁ = (f(p₁) - f(p₀))/h₁
        δ₂ = (f(p₂) - f(p₁))/h₂
        d = (δ₂ - δ₁)/(h₁ + h₂)
        i += 1
    end
    return -1
end

end


# Helpful packages for working with images and factorizations
# using Pkg; Pkg.add("Images")
# using Pkg; Pkg.add("ImageMagick") # And this allows us to load JPEG-encoded images
using Images, LinearAlgebra, Interact

# ------------------------------------------------------------------------------------------
# ### Using a SVD to compress an image
#
# In this exercise, we'll use a singular value decomposition (SVD) to compress an image --
# so that we can store an image without keeping around "unnecessary" information.
#
# To start, let's define a singular value decomposition. In a SVD, we take a matrix $A$ and
# factorize it so that
#
# $$A = USV^T$$
#
# where matrices $U$ and $V$ are unitary and hold our singular vectors. Matrix $S$ is
# diagonal and stores our singular values in decreasing order from top/left to bottom/right.
#
# In Julia, our images are stored as arrays, so we can think of `yellowbanana` as a matrix
# ------------------------------------------------------------------------------------------

file = download("https://uploads6.wikiart.org/images/salvador-dali/the-persistence-of-memory-1931.jpg!Large.jpg")

img = load(file)

size(img)

img[24,24] # Each element in the array is a color

dump(img[24,24])

# ------------------------------------------------------------------------------------------
# We can extract each "channel" of red, green, and blue and view each independently:
# ------------------------------------------------------------------------------------------

channels = Float64.(channelview(img))
Gray.(channels[1, :, :])

# ------------------------------------------------------------------------------------------
# That means we can take the SVD of this image. So, we can store this picture of a banana as
# sets of singular vectors and singular values.
#
# **The reason this is important** is that we'll find that we do **not** need to keep track
# of *all* the singular vectors and *all* the singular values to store an image that still
# looks like a banana! This means we can choose to keep only the important information,
# throw away the rest, and thereby "compress" the image.
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# If we don't throw away any data, we get back what we started with:
# ------------------------------------------------------------------------------------------

U, S, V = svd(channels[1,:,:])
Gray.(U * Diagonal(S) * V')

# ------------------------------------------------------------------------------------------
# But of course we're not doing any compression here — the sizes of U, S, and V are bigger
# than our original matrix! This is like the opposite of compression.  The key is that the
# values are stored in decreasing order so we can start throwing things away.
# ------------------------------------------------------------------------------------------

sum(length.((U, S, V)))

length(img)

Gray.(U[:, 1:25] * Diagonal(S[1:25]) * V[:, 1:25]')

sum(length.((U[:, 1:25], S[1:25], V[:, 1:25])))/length(img)

# ------------------------------------------------------------------------------------------
# Of course this is just one channel of the image. Let's put it all back together and see
# how we can compress the different channels to find an acceptable compression level.
# ------------------------------------------------------------------------------------------

function rank_approx(M, k)
    U, S, V = svd(M)

    M = U[:, 1:k] * Diagonal(S[1:k]) * V[:, 1:k]'

    M = min.(max.(M, 0.0), 1.)
end

n = 100
@manipulate for k1 in 1:n, k2 in 1:n, k3 in 1:n
    colorview(  RGB,
                rank_approx(channels[1,:,:], k1),
                rank_approx(channels[2,:,:], k2),
                rank_approx(channels[3,:,:], k3)
    )
end

# ------------------------------------------------------------------------------------------
# **So how can we use a SVD to determine what information in an image is really important?**
#
# The singular values tell us!
#
# If we have matrices $U$, $S$, and $V$ from our image, we can rebuild that image with the
# matrix product $USV^T$.
#
# Taking this matrix product is the same as adding together the outer products of each
# corresponding pair of vectors from $U$ and $V$, scaled by a singular value ($\sigma$) from
# $S$. In other words, for a (100 x 100) pixel image,
#
# $$A_{image} = USV^T = \sum_{i = 1}^{100} \sigma_i \mathbf{u_i}\mathbf{v_i'} $$
#
# Every outer product $u_i * v_i'$ creates a (100 x 100) matrix. Here we're summing together
# one hundred (100 x 100) matrices in order to create the original matrix $A_{image}$. The
# matrices at the beginning of the series -- those that are scaled by **large** singular
# values -- will be **much** more important in recreating the original matrix $A_{image}$.
#
# This means we can approximate $A_{image}$ as
#
# $$A_{image} \approx \sum_{i = 1}^{n} \sigma_i \mathbf{u_i}\mathbf{v_i'}$$
#
# where $n < 100$.
#
#
# #### Exercise
#
# Write a function called `compress_image`. Its input arguments should be an image and the
# factor by which you want to compress the image. A compressed grayscale image should
# display when `compress_image` is called.
#
# For example,
#
# ```julia
# compress_image("images/104_100.jpg", 33)
# ```
#
# will return a compressed image of a grayscale banana built using 3 singular values. (This
# image has 100 singular values, so use `fld(100, 33)` to determine how many singular values
# to keep. `fld` performs "floor" division.)
#
# *Hints*:
#
# * Perform the SVD on the `channelview` of a grayscale image.
# * In an empty input cell, execute `?svd` to find a function that wil perform an SVD for
# you.
# ------------------------------------------------------------------------------------------

.nbexports/introductory-tutorials/intro-to-julia/long-version/050 Compressing an Image.jl


#function to calculate volume
function sphere_vol(r)

    return 4/3*pi*r^3
end

quadratic(a, sqr_term, b) = (-b + sqr_term) / 2a

#quadratic funtion calculation

function quadratic2(a::Float64, b::Float64, c::Float64)

    sqr_term = sqrt(b^2-4a*c)
    r1 = quadratic(a, sqr_term, b)
    r2 = quadratic(a, -sqr_term, b)

    r1, r2
end

vol = sphere_vol(3)

@printf "volume = %0.3f\n" vol
#prints volume = 113.097

quad1, quad2 = quadratic2(2.0, -2.0, -12.0)
println("result 1: ", quad1)
#> result 1: 3.0
println("result 2: ", quad2)
#> result 2: -2.0


function isPrime(n::Int)
	for i in 2:ceil(sqrt(n))
		if n%i==0 && n!=i return false end
	end
	return true
end

function nextPrime!(l)
	x = l[end]+2
	while !isPrime(x)
		x += 2
	end
	push!(l, x)
end

l = [2,3,5,7,11,13]
while length(l) < 10001
	nextPrime!(l)
end
println(length(l))
println(l[end])


# Julia program to find the sum of divisors of a number.

function sum_of_divisors(num)
    i = 1
    sum = 0
    while (i <= sqrt(num))
        if num % i == 0 && i == sqrt(num)
            sum = sum + i
        elseif num % i == 0
            sum = sum + i
            sum = sum + (num ÷ i)
        end
        i = i + 1
    end
    return sum
end

print("Enter the number: ")
num = readline()
num = parse(Int, num)
sum = sum_of_divisors(num)
print("The Sum of the Divisors of the given number is $sum.")

"""
Time Complexity - O(n^(0.5)), where 'n' given is the number
Space Complexity - O(1)

SAMPLE INPUT AND OUTPUT

SAMPLE I

Enter the number: 256
The Sum of the Divisors of the given number is 511.

SAMPLE II

Enter the number: 5687
The Sum of the Divisors of the given number is 6384.
"""


# Formulas used for Chapter 2 exercises
yearly_compounded_interest(i, r, y) = (i * (1 + (r / 100)))^y

find_effective_rate(i, n) = (1 + i / n)^n - 1

"internal rate of return"
function irr(func1, func2, init, stop)
    sol = Float64[]
    λₖ = func1(init)
    λₖ₊₁ = λₖ - func1(λₖ) / func2(λₖ)
    push!(sol, float(λₖ))
    if stop == 0
        return [λₖ]
    end
    push!(sol, float(λₖ₊₁))
    if stop == 1
        return [λₖ, λₖ₊₁]
    end
    for k in range(2; stop=stop)
        λₖ = λₖ₊₁
        λₖ₊₁ = λₖ - func1(λₖ) / func2(λₖ)
        push!(sol, float(λₖ₊₁))
    end
    return sol
end

"net present value"
npv(inv, val, rate, year) = -inv + val / (1 + rate)^year

"annual equivalent rate"
aer(r, n) = (1 + r / n)^n - 1

"continuously compounded interest"
cci(r) = exp(r) - 1

"present value"
function pv(p, r, n)
    interest = 0.0
    for i in range(0, n - 1; step=1)
        interest += (1 + r)^-i
    end
    return p * interest
end

# From Lecture Notes

"present value"
function present_value(s, r)
    return mapreduce((k, x) -> x / (1 + r)^k, +, 0:(length(s) - 1), s)
end


"""If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23.
Find the sum of all the multiples of 3 or 5 below 1000."""

function sum_multiples_3_5(n::Int)
    multiples3or5 = union(3:3:n-1 , 5:5:n-1)
    return sum(multiples3or5)
end

@assert sum_multiples_3_5(10) ==23

@show sum_multiples_3_5(1000)

# Oneliner and faster
@show sum([i for i in 1:999 if i%3==0 || i%5 ==0]);


function getGrid(minongrid, maxongrid, GridPoints, method)

#NB: This code borrows some code from Chris Carroll of John's Hopkins University
#His website is here http://www.econ2.jhu.edu/people/ccarroll/ and his
#notes on solving dynamic models are very good - they can be found
#here: http://www.econ2.jhu.edu/people/ccarroll/SolvingMicroDSOPs.pdf

#% ------------------------------------------------------------------------
# Explanation

# We need to a grid from a to b. A basic approach could involve spacing out
# the point equally. The following line of code would achieve this:
#     grid= linspace(a, b, GridPoints);

# If we want to space them out so that the growth rate of the distance
# between spaces is equal we achieve this by spacing out the logs of grid
# equally. We could achieve this by the following line of code:
#     grid= exp(linspace(log(a), log(b), GridPoints));

# This is problematic if a<=0. The approach taken here is to get a grid
# from log(1) to log(b - a + 1), exponentiate and then subrtact 1
# so that we have a grid from 0 to b-a. If we have add a to each point we
# get the desired result - a log-spaced grid from a to b

span = maxongrid - minongrid;     # b - a

if method=="equalsteps"
    grid= linspace(min, span, GridPoints);
elseif method== "logsteps"
  loggrid = linspace(log(1), log(1+span), GridPoints);
  grid = exp(loggrid)-1;
elseif method=="3logsteps"
  loggrid = linspace(log(1+log(1+log(1))), log(1+log(1+log(1+span))), GridPoints);
  grid = exp(exp(exp(loggrid)-1)-1)-1;
elseif method=="5logsteps"
  loggrid = linspace(log(1+log(1+log(1+log(1+log(1))))), log(1+log(1+log(1+log(1+log(1+span))))), GridPoints);
  grid = exp(exp(exp(exp(exp(loggrid)-1)-1)-1)-1)-1;
elseif method=="10logsteps"
  loggrid = linspace(log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1)))))))))), log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+log(1+span)))))))))), GridPoints);
  grid = exp(exp(exp(exp(exp(exp(exp(exp(exp(exp(loggrid)-1)-1)-1)-1)-1)-1)-1)-1)-1)-1;
else
    error("Error in getgrid. You have entered an invalid method for choosing the distance between grid points. Method must be one of equalsteps, logsteps, 3logsteps, 5logsteps or 10logsteps.");
end

grid = grid + minongrid*ones(Float64,GridPoints,1);

return grid;

end


import LinearAlgebra
import SparseArrays
#using Images
import Images
import MAT

LA = LinearAlgebra
SA = SparseArrays
Img = Images

# Matrix
dim = 4
A = rand(dim,dim)
AT = A'
A2 = A*AT

@show A[11] == A[1,2]
b = rand(dim)
# Solve for x where Ax = b
x = A\b
@show LA.norm(A*x - b)

@show typeof(A)
@show typeof(b)
@show typeof(rand(1,dim))
@show typeof(AT)

#A = [1 2;3 4]
sizeof(A) # => byte count
sizeof(AT)
B = copy(AT)
sizeof(B)

# \(x, y) => x^(-1) * y
#x = \(A,b) # => A^(-1)*b
@show \(3,6) # => 1/3 * 6
@show 3\6    # => 1/3 * 6

1//3 + 1//2

#A = sparse([1,2,4], [1,1,1], [1.0,1.0,1.0], 4, 2)
#F = qr(A)

luA = LA.lu(A)
# LU = PA
LA.norm(luA.L*luA.U - luA.P*A)

# QR = A
qrA = LA.qr(A)
Q = qrA.Q
R = qrA.R
LA.norm(Q*R - A)

# Skip Cholesky factorization

# Skip Sparse Linear Algebra

# Images as matrices
X1 = Images.load("data/khiam-small.jpg")
# Gray scale
Xgray = Images.Gray.(X1)
# Extract RGB values
R = map(i -> X1[i].r, 1:length(X1))    # => Vector
R2 = Float64.(reshape(R, size(X1)...)) # (80089,) => (283,283)
G = map(i -> X1[i].g, 1:length(X1))
G2 = Float64.(reshape(G, size(X1)...)) # (80089,) => (283,283)
B = map(i -> X1[i].b, 1:length(X1))
B2 = Float64.(reshape(B, size(X1)...))

Z = zeros(size(G2)...)
Images.RGB.(Z,G2,Z)

Xgrayvalues = Float64.(Xgray)
# SVD: A = U*Sigma*V'
usv =LA.svd(Xgrayvalues)
# usv.U, S, V, Vt
U = usv.U
S = usv.S
V = usv.V
Vt = usv.Vt

LA.norm(U*LA.diagm(S)*Vt - Xgrayvalues)

# Use the top 4 singular vectors/values to form a new image
i = 1
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 = usv.S[i]*u1*v1'

i = 2
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

i = 3
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

i = 4
u1 = usv.U[:,i]
v1 = usv.V[:,i]
img1 += usv.S[i]*u1*v1'

Images.Gray.(img1)

i = 1:100
u1 = usv.U[:,i]
s1 = SA.spdiagm(0=>usv.S[i])
v1 = usv.V[:,i]
img1 = u1 * s1 * v1'
Img.Gray.(img1)
# Close but not the same
LA.norm(Xgrayvalues - img1)

# Face recognition
M = MAT.matread("data/face_recog_qr.mat")
# V2 contains 490 faces
# each column is a face and need to reshape into 192,168
V2 = M["V2"]
q = reshape(V2[:,200], 192, 168)
Img.Gray.(q)
b = q[:]

A = V2[:,2:end]
x = A\b
Img.Gray.(reshape(A*x, 192,168))
LA.norm(A*x - b)

# Skip


begin
# Jump Search

# Julia code to jump search x in arr[].
# If x is present then return its location,
# otherwise return -1
import Base:length,show,AbstractArray
arr = [ 0, 1, 1, 2, 3, 5, 8, 13, 21,
    34, 55, 89, 144, 233, 377, 610 ]
x = 610
n = length(arr)
function jumpsearch(arr::AbstractArray{},n,x)

    # Finding block size to be jumped
    step = sqrt(n)

    # Finding the block where element is
    # present (if it is present)
    prev = 1
    while arr[round(Int,floor(min(step, n)))] < x
        prev = step
        step += sqrt(n)
        if prev >= n
            return -1
		end
	end
    # Doing a linear search for x in
    # block beginning with prev.
    while arr[round(Int,floor(prev))] < x
        prev += 1

        # If we reached next block or end
        # of array, element is not present.
        if prev == min(step, n)
            return -1
		end
	end
    # If element is found
    if arr[round(Int,floor(prev))] == x
        return prev
	end
    return -1
end
global result=-1

result=jumpsearch(arr,n, x)

println("Result after function execution:",result)
if result == -1
    println("Element:",x," is not present in array",arr)
else
    println("Element:",x," is present at index:", result," in array",arr)
end
end


using DifferentialEquations
using Plots
using CPUTime

function main()
     a = 3.0  # パラメータ

     """
     微分方程式
     """
     dy(y, t, a) = a * y
     y0 = 1.0  # 初期値を設定
     tspan = (0.0, 1.0)  # 解く範囲を設定

     prob = ODEProblem(dy , y0 , tspan)
     sol = solve(prob)  # ソルバで解く

     # plot
     plot(
          sol,
          linewidth=5,
          xaxis="x",
          yaxis="y(x)",
          label="solution"
     )

     # savefig("solution_of_example_2.png")  #  保存
end

"""
オイラー法

・参考にしました -> [url](https://twitter.com/genkuroki/status/1301832571131633665/photo/1)
"""
function jisaku_solve_euler(dx, x₀, t_span, Δt)
     t = range(t_span..., step = Δt)  # 時間軸
     x = Vector{typeof(x₀)}(undef, length(t))  # 解を格納する1次元配列

     x[1] = x₀  # 初期値
     for i in 1:length(x)-1
          x[i+1] = x[i] + dx(t[i], x[i])Δt
     end

     t, x
end

"""ルンゲクッタ法（4次）"""
function jisaku_solve_RungeKutta(dx, x₀, t_span, Δt)
     t = range(t_span..., step = Δt)  # 時間軸
     x = Vector{typeof(x₀)}(undef, length(t))  # 解を格納する1次元配列

     x[1] = x₀  # 初期値
     for i in 1:length(x)-1
          k₁ = dx(t[i], x[i])
          k₂ = dx(t[i]+Δt/2, x[i]+k₁*Δt/2)
          k₃ = dx(t[i]+Δt/2, x[i]+k₂*Δt/2)
          k₄ = dx(t[i]+Δt, x[i]+k₃*Δt)
          x[i+1] = x[i] + (k₁ + 2k₂ + 2k₃ +k₄)Δt/6
     end

     t, x
end

function main()
     a = 3.0  # パラメータ
     dx(t, x) = a * x  # 微分方程式

     x₀ = 1.0
     t_span = (0.0, 5.0)
     Δt = 0.01
     t, x = jisaku_solve_RungeKutta(dx, x₀, t_span, Δt)
     plot(t, x, label="RungeKutta")

     t, x = jisaku_solve_euler(dx, x₀, t_span, Δt)
     plot!(t, x, label="euler")

     x = exp.(a*t)
     plot!(t, x, label="exact sol exp(at)")
end


"""Julia program to implement Subset Sum problem.
Given a set, we have to find if there exist a subset with the given sum.
The given problem can be solved using Dynamic Programming.
"""

function subset_sum(set, n, S)

    # Initialize the entire 2D matrix with 0 as value, signifying false.
    dp = zeros(Int, n + 1, S + 1)

    # When the sum is zero, the answer is always true, by forming an empty subset.
    for i in 1:n + 1
        dp[i,1] = 1
    end

    # When the given set is empty and the required sum is not zero, then such a subset cannot e formed.
    for i in 1:S + 1
        dp[1,i] = 0
    end

    """Tabulating the dp array in Bottom-Up manner. In such a way that each cell of the dp[i, j]
       array contains whether a subset of sum j can be formed with elements in the given set till i"""
    for i in 2:n + 1
        for j in 2:S + 1
            if j < (set[i - 1] + 1)
                dp[i,j] = dp[(i - 1),j]
            else
                if (dp[(i - 1),j] == 1)
                    dp[i, j] = 1
                elseif (dp[(i - 1),(j - set[i - 1])] == 1)
                    dp[i, j] = 1
                else
                    dp[i, j] = 0
                end
            end
        end
    end
    return dp[n + 1,S + 1]
end

print("What is the size of the set? ")
n = readline()
n = parse(Int, n)
arr = Int[]
if (n > 0)
    print("Enter the numbers: ")
    arr = [parse(Int, num) for num in split(readline())]
else
    print("The set is Empty!!!")
    exit()
end
print("Enter the sum? ")
S = readline()
S = parse(Int, S)
res = subset_sum(arr, n, S)
if (res == 1)
    print("A subset with the given sum is present in the array.")
else
    print("A subset with the given sum is not present in the array.")
end

"""
Time Complexity - O(n * S), where 'n' is the size of the array and 'S is the Sum
Space Complexity - O(n * S)

SAMPLE INPUT AND OUTPUT

SAMPLE I

What is the size of the set? 3
Enter the numbers: 3 2 1
Enter the sum? 55
A subset with the given sum is not present in the array.

SAMPLE II

What is the size of the set? 4
Enter the numbers: 10 20 30 40
Enter the sum? 60
A subset with the given sum is present in the array.
"""


export Linear

"""
**Linear function**

    Linear(c)

Returns the function
```math
f(x) = \\langle c, x \\rangle.
```
"""
struct Linear{R <: Real, A <: AbstractArray{R}} <: ProximableFunction
    c::A
end

is_separable(f::Linear) = true
is_convex(f::Linear) = true
is_smooth(f::Linear) = true

function (f::Linear{R})(x::AbstractArray{R}) where R
    return dot(f.c, x)
end

function prox!(y::AbstractArray{R}, f::Linear{R}, x::AbstractArray{R}, gamma::Union{R, AbstractArray{R}}=1.0) where R
    y .= x .- gamma.*(f.c)
    fy = dot(f.c, y)
    return fy
end

function gradient!(y::AbstractArray{R}, f::Linear{R}, x::AbstractArray{R}) where R
    y .= f.c
    return dot(f.c, x)
end

fun_name(f::Linear) = "Linear function"
fun_expr(f::Linear) = "x ↦ c'x"

function prox_naive(f::Linear, x, gamma)
    y = x - gamma.*(f.c)
    fy = dot(f.c, y)
    return y, fy
end


# Algorithm 2.5
# False Position method for finding roots
# Same as the secant method, but it checks
# if the roots are bracketed between successive
# iterations.

function false_approx(f, p0, p1, tolerance, maxIter)
	for i ∈ 2:maxIter
		q0 = f(p0)
		q1 = f(p1)
		p = p1 - q1*(p1 - p0)/(q1 - q0)
		if abs(p - p1) < tolerance
			return p
		end
		q = f(p)
		if q ⋅ q1 < 0
			p0 = p1
			q0 = q1
		end
		p1 = p
		q1 = q
	end
	throw(ArgumentError("Method failed"))
end

println(false_approx(x->(cos(x) - x), 0.5, π/4, 0.1, 100))


"""
    median(nums)

Finds median of a vector of numbers

## Example

```julia
median([2,1,3,4])                   # returns 2.5
median([2, 70, 6, 50, 20, 8, 4])    # returns 8
median([0])                         # returns 0
```

Contributed By:- [Ashwani Rathee](https://github.com/ashwani-rathee) and [Rratic](https://github.com/Rratic)
"""
function median(nums::Vector{T}) where {T<:Number}
    sorted = sort(nums)
    len = length(sorted)
    mid_index = div(len, 2)
    return if len % 2 == 0
        (sorted[mid_index+1] + sorted[mid_index]) / 2
    else
        sorted[mid_index+1]
    end
end


"""
rw_test(p; method = "VR", m = 1)

TEST OF RANDOM WALK

Random walk with uncorrelated increments (RW3) is the weakest form version of the random walk.
The other versions are random walk with iid increments (RW1) and random walk with independent increments (RW2).

Box and Pierce (1970) test of RW(3): the autocorrelation of increments at all leads and lags should be zero under random walk.
By summing the squared autocorrelation, the Box-Pierce Q-statistics is designed to detect departures from zero autocorrelations in either directions and at all lags

Ljung and Box (1978) test of RW(3): the Ljung-Box Q-statistics provides the finite-sample correction which yields a better fit to the chi-squared distribution for small sample size.

Variance ratio test of RW(1): the variance of random walk increments must be a linear function of the time interval under random walk. The variance ratios statistics tests such property.

INPUT
`p': a T x 1 vector of asset prices
`method': method used to test random walk hypthesis with uncorrelated increments:
        - BP: the Box and Pierce (1970) statistics
        - LB: the Ljung and Box (1978) statistics
        - VR: variance ratio test
`m': number of autocorrelation lags used for test

OUTPUT
`test_statistics': test statistics of the random walk test
`pvalue': p-value of the test statistics

"""

function rw_test(p, method = "VR", m = 1)
    r = diff(log.(p), dims = 1)
    T = length(r); # length of the time series

    if method == "BP" # Box and Pierce (1970) test of RW(3)
        Qm = 0.0;
        for k = 1:m
            rho_k = autocor(r, [k]; demean = false);
            Qm = Qm .+ rho_k.^2;
        end
        test_statistics = T * Qm;
        pvalue = 1.0 .- cdf.(Chisq(m), test_statistics);
    elseif method == "LB" # Ljung and Box (1978) test of RW(3)
        Qm = 0.0;
        for k = 1:m
            rho_k = autocor(r, [k]; demean = false);
            Qm = Qm .+ rho_k.^2;
        end
        test_statistics = T * (T + 2) / (T - m) * Qm;
        pvalue = 1.0 .- cdf.(Chisq(m), test_statistics);
    elseif method == "VR" # Variance ratio test of RW(1)
        VR = 1.0;
        for k = 1:m-1
            rho_k = autocor(r, [k]; demean = false);
            VR = VR .+ 2 .* (1 .- k / m) .* rho_k;
        end
        test_statistics = sqrt(T * m) * (VR .- 1.0) ./ sqrt(2.0 .* (m - 1));
        pvalue = 1.0 .- cdf.(Normal(0.0, 1.0), test_statistics);
    end

    return (test_statistics, pvalue)
end


"""
Julia program to check if a number is a Strong number or not.
Strong Number is a number whose sum of the factorial of digits
is equal to the number itself.
"""

# Helper function to return the factorial of a number
function factorial(num)
    if ( num == 1 || num == 0 )
        return 1
    end
    return num * factorial( num - 1)
end

""" Extract each digit of the given number and calculate the sum of the
    factorial of each digit present in the given number"""
function strong_number(num)
    temp = num
    sum = 0
    while(temp > 0)
        rem = temp % 10
        sum = sum + factorial(rem)
        temp = temp ÷ 10
    end
    return sum
end

print("Enter the number: ")
num = readline()
num = parse(Int, num)
sum = strong_number(num)
if sum == num
    println("The given number $num is a Strong Number.")
else
    println("The given number $num is not a Strong Number.")
end

"""
Time Complexity: O(n*log(n)), where 'n' is the given number
Space Complexity: O(1)

SAMPLE INPUT AND OUTPUT

SAMPLE 1
Enter the number: 3124
The given number 3124 is not a Strong Number.

SAMPLE 2
Enter the number: 145
The given number 145 is a Strong Number.
"""


### A Pluto.jl notebook ###
# v0.16.0

using Markdown
using InteractiveUtils

# ╔═╡ cc106912-0b14-4981-a67b-580dda8a56ed
begin
	using Pkg
	cd(joinpath(dirname(@__FILE__),".."))
    Pkg.activate(pwd())
    using Distributions, LinearAlgebra, Plots, InteractiveUtils
end

# ╔═╡ 235e2200-fce9-11ea-0696-d36cddaa843e
md"""
# Phase transition in networks
## Generalities - Networks

A network is a set of objects (called nodes or vertices) that are connected together. The connections between the nodes are called edges or links. In mathematics, networks are often referred to as [graphs](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)).

In the following, we will only consider undirected networks i.e. if node $i$ is connected to node $j$, then node $j$ is automatically connected to node $i$ (as is the case with Facebook friendships). For unweighted networks of $N$ nodes, the network structure can be represented by an $N \times N$ adjacency matrix $A$:

```math
a_{i,j} = \left\{
                \begin{array}{ll}
                  1 & \text{if there is an edge from node $i$ to node $j$}\\
                  0 & \text{otherwise}\\
                \end{array}
                \right.
```
The degree of a node $i$ is the number of connections it has. In terms of the adjacency matrix
$A$, the degree of node $i$ is the sum of the i$^{\text{th}}$ row of $A$:
$$k_i = \sum_{j=1}^{N}a_{i,j}$$

The average node degree $\langle k \rangle$ is then given by: $$\langle k \rangle = \sum_{i=1}^{N}k_{i}$$

## Erdös-Rényi random graph model
One of the simplest graph models is the Erdös-Rényi random graph model, denoted by $\mathcal{G}(N,p)$ with $N$ being the amount of nodes and $p$ being the probability that a link exists between two nodes. Self-loops are excluded. The value of $p$ is typically small (this is to avoid that the average degree $\langle k \rangle$ depends on $N$, cf. specialised literature for more details). When studying random graph model models, a major aim is to predict the average behaviour of certain network metrics and, if possible, their variance.

The Erdös-Rényi random graph  exhibits a phase transition. Let us consider the size (i.e., number of nodes) of the largest connected component in the network as a function of the mean degree ⟨k⟩. When ⟨k⟩ = 0, the network is trivially composed of N disconnected nodes. In the other extreme of ⟨k⟩ = N − 1, each node pair is adjacent such that the network is trivially connected. Between the two extremes, the network does not change smoothly in terms of the largest component size. Instead, a giant component, i.e., a component whose size is the largest and proportional to N, suddenly appears as ⟨k⟩ increases, marking a phase transition. The goal of this application is to determine this value by simulation.

## Problem solution
We split the problem in a series of subproblems:
* generating a random graph
* determine the average degree
* identifying the size of the largest connected component
* visualising the result
* determine the critical value

"""

# ╔═╡ 976bb1e0-07c9-11eb-2a4f-fb7aac69f8ec
begin
	function gengraph(N::Int, p::Float64)
		d = Bernoulli(p) # define the distribution
		A = rand(d, N, N)
		# set main diagonal to 0
		A[diagind(A)] = zeros(Int,N)
		return LinearAlgebra.Symmetric(A)
	end

	function avgdegree(A)
		return mean(sum(A, dims=1))
	end

	"""
		neighbors(A, i)

	Détermine les voisins du noeud i dans le graph représenté par A
	"""
	function neighbors(A, i)
		findall(x -> isequal(x, 1), @view A[i,:])
	end

	function components(A)
		N = size(A,1) # network size
		visited = Dict(i => false for i in 1:N)
		comps = []
		for n in 1:N
			if !visited[n]
				push!(comps,dig(A, n, visited))
				end
		end

		return maximum(length,comps) / N
	end

	function dig(A, n, visited, comp=[])
		visited[n] = true
		push!(comp, n)
		for neigbor in neighbors(A, n)
			if !visited[neigbor]
				dig(A, neigbor, visited, comp)
			end
		end
		return comp
	end

	function sim(N::Int, p::Float64)
		A = gengraph(N,p)
		return components(A), avgdegree(A)
	end
end

# ╔═╡ 42f5cca6-07d1-11eb-3169-cd3650c42081
N = 1000

# ╔═╡ a060dc62-07cb-11eb-2aaa-7190bde5dc36
# simulation part (range of probabilities, 10 simulations for each probability)
let N = N
	P = range(5e-5, 3e-3, length=20)
	rat_comp = Array{Float64, 1}()
	k_moy = Array{Float64, 1}()
	for p in P
		for _ in 1:10
			res = sim(N,p)
			push!(rat_comp, res[1])
			push!(k_moy, res[2])
		end
	end
	scatter(k_moy, rat_comp, ylims=(0,1), label="", xlabel="<k>", ylabel="N_largest_comp / N")
end

# ╔═╡ 1f0d3e38-402b-4d82-a041-c623c5d531b3
md"""
# Test from previous years
* ants foraging (cf. "./Exercises/data/test2019.pdf")
* housing evolution (cf. "./Exercises/data/test2020.pdf")

"""

# ╔═╡ Cell order:
# ╠═cc106912-0b14-4981-a67b-580dda8a56ed
# ╟─235e2200-fce9-11ea-0696-d36cddaa843e
# ╠═976bb1e0-07c9-11eb-2a4f-fb7aac69f8ec
# ╠═42f5cca6-07d1-11eb-3169-cd3650c42081
# ╠═a060dc62-07cb-11eb-2aaa-7190bde5dc36
# ╟─1f0d3e38-402b-4d82-a041-c623c5d531b3


# # Poisson Equation
#
# We want to solve the [poisson equation](https://en.wikipedia.org/wiki/Poisson_equation) on the unit interval. It is given by
# `Δu = f` with boundary conditions `u(0) = a` and `u(1) = b`
# First of all let us choose some values for the parameters and remark, that there is an exact solution:
f = 1.0
a = -1.0
b = 2.0

u_analytic(x) = f/2*x^2 + (b-a-f/2) * x + a

# We would like to recompute this solution numerically
using DiffEqOperators

nknots = 10
h = 1.0/(nknots+1)
ord_deriv = 2
ord_approx = 2

Δ = CenteredDifference(ord_deriv, ord_approx, h, nknots)
bc = DirichletBC(a, b)

# Before solving the equation, lets take a look at Δ and bc:
# display(Array(Δ))
# display(bc*zeros(nknots))
# We see that `Δ` is a (lazy) matrix with the laplace stencil extended over the boundaries.
# And `bc` acts by padding the values just outside the boundaries.

u = (Δ*bc) \ fill(f, nknots)
knots = range(h, step=h, length=nknots)

# Since we used a second order approximation and the analytic solution itself was a second order
# polynomial, we expect that they are equal up to rounding errors:
using Test
@test u ≈ u_analytic.(knots)


# ------------------------------------------------------------------------------------------
# # Entropies & Dimensions
#
#
# Topics:
# * Entropies from **DynamicalSystems.jl**
# * Generalized Dimension
# * Automated dimension estimation!
# * Other related concepts
# * Docstrings
#
# ---
#
# # Generalized Entropy
#
# * In the study of dynamical systems there are many quantities that identify as "entropy".
# * These quantities are not the more commonly known [thermodynamic
# ones](https://en.wikipedia.org/wiki/Entropy), used in Statistical Physics.
# * Rather, they are more like the entropies of [information
# theory](https://en.wikipedia.org/wiki/Entropy_(information_theory), which represent
# information contained within a dataset.
# * In general, the more "uncertain" or "random" the dataset is, the larger its entropy will
# be. On the other hand, the lower the entropy, the more "predictable" the dataset becomes.
#
#
# Let $p$ be an array of probabilities (such that it sums to 1). Then the generalized
# entropy is defined as
#
# $$
# R_\alpha(p) = \frac{1}{1-\alpha}\log\left(\sum_i p[i]^\alpha\right)
# $$
#
# and is also called [Rényi entropy](https://en.wikipedia.org/wiki/R%C3%A9nyi_entropy).
# Other entropies, like e.g. the [Shannon
# entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory) are generalized by it,
# since for $\alpha = 1$, the Rényi entropy becomes the Shannon entropy,
#
# $$
# R_1(p) = -\left(\sum_i p[i] \log (p[i]) \right)
# $$
#
# The Rényi entropy can be computed for a specific dataset, given $p$. But how does one get
# $p$?
# 1. $p$ represents the probability that a point of a dataset falls into a specific "bin".
# 2. It is nothing more than the (normalized) histogram of the dataset!
# ------------------------------------------------------------------------------------------

using DynamicalSystems

# ------------------------------------------------------------------------------------------
# Let's generate a dataset so that we can practice calculating entropies.
# ------------------------------------------------------------------------------------------

N = 100000
randomdata = Dataset(rand(N,3))

# ------------------------------------------------------------------------------------------
# ---
#
# ```julia
# genentropy(α, ε, dataset::AbstractDataset; base = e)
# ```
# * This function calculates the generalized entropy of order `α`.
# * It first calculates the probability array $p$.
# * The "histogram" is created by partitioning the `dataset` into boxes of size `ε`.
#
# ------------------------------------------------------------------------------------------

genentropy(2, 0.1, randomdata)

genentropy(2, 0.01, randomdata)

genentropy(2, 0.001, randomdata)

genentropy(2, 0.0001, randomdata)

# ------------------------------------------------------------------------------------------
# Note that the output of `genentropy` changed with changing $\varepsilon$ until we hit
# $\varepsilon = 0.001$.
#
# At this point the value for the entropy has already saturated. There's no use in
# partitioning the dataset in smaller boxes.
#
# ---
#
# Now let's calculate the entropy of a coin toss!
#
# First, let's create an array, `y`, that stores the results of coin tosses as `0`s or `1`s
# for `1000000` tosses.
# ------------------------------------------------------------------------------------------

y = Float64.(rand(Bool, 1000000))

sh = genentropy(1, 0.1, y) # Renyi entropy with α = 1 is the Shannon entropy

# ------------------------------------------------------------------------------------------
# The above number should be log(2) [by
# definition](https://en.wikipedia.org/wiki/Shannon_(unit)
# ------------------------------------------------------------------------------------------

isapprox(sh, log(2), rtol = 1e-4)

# ------------------------------------------------------------------------------------------
# `genentropy` is conveniently used with `trajectory` outputs.
#
# Here we create a trajectory for a towel map,
# ------------------------------------------------------------------------------------------

towel = Systems.towel()
tr = trajectory(towel, N-1);
summary(tr)

# ------------------------------------------------------------------------------------------
# and calculate its entropy:
# ------------------------------------------------------------------------------------------

genentropy(1, 0.01, tr) # The result is with log base-e !

# ------------------------------------------------------------------------------------------
# Let's also compare the entropy of the above dataset (a trajectory of the towel map) with
# that of a random dataset:
# ------------------------------------------------------------------------------------------

genentropy(1, 0.01, randomdata)

# ------------------------------------------------------------------------------------------
# * As expected, the entropy of the random dataset is higher.
#
# ---
#
# How much time does the computation take?
# ------------------------------------------------------------------------------------------

using BenchmarkTools
@btime genentropy(1, 0.01, $tr);

# ------------------------------------------------------------------------------------------
# ## Specialized histogram
# * Partitioning the dataset (i.e. generating a "histogram") is in general a costly
# operation that depends exponentially on the number of dimensions.
# * In this specific application however, we can tremendously reduce the memory allocation
# and time spent!
#
# Let's get the array of probabilities $p$ for size ε where `tr` is the trajectory of the
# towel map
# ------------------------------------------------------------------------------------------

ε = 0.01
p = non0hist(ε, tr)

# ------------------------------------------------------------------------------------------
# Here's a sanity check, showing our probabilities should sum to roughly `1`.
# ------------------------------------------------------------------------------------------

sum(p)

# ------------------------------------------------------------------------------------------
# How long does generating the histogram take?
# ------------------------------------------------------------------------------------------

@btime non0hist($ε, $tr);

# ------------------------------------------------------------------------------------------
# How long does this take if we create 9-dimensional data and compare again?
# ------------------------------------------------------------------------------------------

nine = Dataset(rand(N, 9))
@btime non0hist($ε, $nine);

# ------------------------------------------------------------------------------------------
# * We went from dimension 3 to dimension 9 but the time roughly only tripled
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# # Attractor Dimension
# 1. There are numerous methods that one can use to calculate a so-called "dimension" of a
# dataset, like for example the [Fractal
# dimension](https://en.wikipedia.org/wiki/Fractal_dimension).
#
# 2. Most of the time these dimensions indicate some kind of scaling behavior.
#
# 3. For example, the scaling of `genentropy` with decreasing `ε` gives the so-called
# "generalized dimension".
#
#
# $ E \approx -D\log(\varepsilon)$ with $E$ the entropy and $D$ the "dimension".
#
# ---
# I want to know dimension of attractor of the Towel Map!
#
#
# ------------------------------------------------------------------------------------------

towel = Systems.towel()
towel_tr = trajectory(towel, 1000000);
summary(towel_tr)

# ------------------------------------------------------------------------------------------
# Note that more points = more precision = more computations = more time!
#
# Now I want to compute `genentropy` for different ε.
#
# Which ε should we use...?
#
# Let's do a "random" logspace based guess...
# ------------------------------------------------------------------------------------------

ες = logspace(-4, 1, 12)

Es = zeros(ες)
for (i, ε) ∈ enumerate(ες)
    Es[i] = genentropy(1, ε, towel_tr)
end
Es

# ------------------------------------------------------------------------------------------
# **Shorter version:**
# ------------------------------------------------------------------------------------------

Es = genentropy.(1, ες, towel_tr)

# ------------------------------------------------------------------------------------------
# Alright. Remember that it should be that $E \approx -D\log(\varepsilon)$
#  with $E$ the entropy and $D$ the "dimension".
#
# Let's plot and see:
# ------------------------------------------------------------------------------------------

using PyPlot; figure(figsize = (6,4))
x = -log.(ες)
plot(x, Es); xlabel("-log(ε)"); ylabel("Entropy");
plot([x[4], x[4]], [0, 15], color = "C1", alpha = 0.5)
plot([x[end-3], x[end-3]], [0, 15], color = "C1", alpha = 0.5);

# ------------------------------------------------------------------------------------------
# What typically happens is that there is some region where this scaling behavior holds, but
# then it stops holding due to the finite amount of data points.
#
# Above, the expected scaling behavior holds between the orange vertical lines.
#
# Let's choose the curve points that do fall in the linear regime of the above plot,
# ------------------------------------------------------------------------------------------

x, y = -log.(ες)[4:end-2], Es[4:end-2]

# ------------------------------------------------------------------------------------------
# and find the slope of the curve there, to calculate the dimension, D.
# ------------------------------------------------------------------------------------------

offset, slope = linreg(x, y)
D = slope

# ------------------------------------------------------------------------------------------
# This is actually a correct result, the information dimension of the attractor of the towel
# map is around 2.
#
# ---
#
# * Are the values of `ες` we used good?
# * For a general dataset, how can we determine them?
#
# the function `estimate_boxsizes(dataset; kwargs...)` can help with that!
# ------------------------------------------------------------------------------------------

ες = estimate_boxsizes(towel_tr)

# ------------------------------------------------------------------------------------------
# Let's plot $E$ vs. $-\log \epsilon$ again
# ------------------------------------------------------------------------------------------

Es = genentropy.(1, ες, towel_tr)
figure(figsize = (6,4))
plot(-log.(ες), Es); xlabel("-log(ε)"); ylabel("E");

# ------------------------------------------------------------------------------------------
# ---
# # Automated Dimension Estimation
#
# Given some arbitrary plot like the one above, is there any algorithm to deduce a scaling
# region???
#
# The function `linear_regions(x, y; kwargs...)` decomposes the function `y(x)` into regions
# where  the function is linear.
#
# It returns the indices of `x` that correspond to linear regions and the approximated
# tangents at each region!
# ------------------------------------------------------------------------------------------

x = -log.(ες)
lrs, slopes = linear_regions(x, Es)

for i in 1:length(slopes)
    println("linear region $(i) starts from index $(lrs[i]) and ends at index $(lrs[i+1])")
    println("with corresponding slope $(slopes[i])")
    println()
end

# ------------------------------------------------------------------------------------------
# The linear region which is biggest is "probably correct one".
# Here the last linear region is the largest; thus the slope is
# ------------------------------------------------------------------------------------------

slopes[end]

# ------------------------------------------------------------------------------------------
# This `linear_regions` function seems very shady... Is there any "easy" way to visualize
# what it does? *Say no more!*
#
# In this next example, we'll use `ChaosTools`, which belows to `DynamicalSystems`.
# ------------------------------------------------------------------------------------------

using PyPlot, ChaosTools

# ------------------------------------------------------------------------------------------
# `ChaosTools` provides a function, `plot_linear_regions` !
# ------------------------------------------------------------------------------------------

figure(figsize=(6,4))
plot_linear_regions(x, Es)
xlabel("-log(ε)"); ylabel("E");

# ------------------------------------------------------------------------------------------
# Adjust the tolerance of `linear_regions` using keyword argument `tol`
# ------------------------------------------------------------------------------------------

figure(figsize=(6,4))
plot_linear_regions(x, Es; tol = 0.8)
xlabel("-log(ε)"); ylabel("E");

# ------------------------------------------------------------------------------------------
# Notice how the color schemes allow us to visualize that adjusting the tolerance reduces
# our number of linear regimes!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# ## `generalized_dim` function
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Let's summarize what we just did to estimate the dimension of an attractor.
#
# 1. We decided on some partition sizes `ες` to use (the function `estimate_boxsizes` can
# give an estimate for that).
# 2. For each `ε` in `ες` we calculated the entropy via `genentropy`. We stored these
# entropies in an array `Es`.
# 3. We tried to find a "linear scaling region" of the curve `Es` vs. `-log.(ες)`.
# 4. The slope of this "linear scaling region" is the dimension we estimated.
#
# Wouldn't it be **cool** if all of this process could happen with one function call?
#
# This is *exactly* what the following function does:
# ```julia
# generalized_dim(α, dataset, ες = estimate_boxsizes(tr))
# ```
# which computes the `α`-order generalized dimension.
# ------------------------------------------------------------------------------------------

generalized_dim(2.0, tr)

generalized_dim(1.0, tr)

# ------------------------------------------------------------------------------------------
# The first input to `generalized_dim` is our value for $\alpha$!
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# Similarly, let's calculate the dimension of the Henon map that we have seen in previous
# tutorials,
# ------------------------------------------------------------------------------------------

hen = Systems.henon()
tr = trajectory(hen, 200000)
generalized_dim(0, tr)

# ------------------------------------------------------------------------------------------
# and of the Lorenz system:
# ------------------------------------------------------------------------------------------

lor = Systems.lorenz()
tr_lor = trajectory(lor, 1000.0; dt = 0.05);

generalized_dim(2.0, tr_lor)

# ------------------------------------------------------------------------------------------
# The correlation dimension of the Lorenz attractor (for default parameters) is reported
# *somewhere* around `2.0` (Grassberger and Procaccia, 1983).
#
# ## `generalized_dim` is but a crude estimate!
#
# **It is important to understand that `generalized_dim` is only a crude estimate! You must
# check and double-check and triple-check if you want more accuracy!**
# ------------------------------------------------------------------------------------------

# ------------------------------------------------------------------------------------------
# # Other related concepts
# ## Kaplan-Yorke dimension
# The Kaplan-Yorke dimension is defined as simply the (interpolated) number where the sum of
# Lyapunov exponents crosses zero.
#
# This simple interpolation is done by the function `kaplanyorke_dim(ls)`.
# ------------------------------------------------------------------------------------------

lor = Systems.lorenz()

ls = lyapunovs(lor, 4000.0; Ttr = 100.0)

kaplanyorke_dim(ls)

# ------------------------------------------------------------------------------------------
# ## Permutation Entropy
#
# Another entropy-like quantity that you can compute with **DynamicalSystems.jl** is the
# [permutation entropy](https://juliadynamics.github.io/DynamicalSystems.jl/latest/chaos/ent
# ropies/#permutation-entropy).
#
# This is done with the function `permentropy(s, order; ...)`. The permutation entropy is
# different because it requires a *timeseries* `s` as the input.
# ------------------------------------------------------------------------------------------

# create timeseries:
s = (ds = Systems.towel(); trajectory(towel, 10000)[:, 1])
# order `o` permutation entropy:
o = 6
permentropy(s, o)

# ------------------------------------------------------------------------------------------
# # Docstrings
# ------------------------------------------------------------------------------------------

?genentropy

?generalized_dim

?non0hist

?estimate_boxsizes

?linear_regions

?kaplanyorke_dim

?permentropy


### A Pluto.jl notebook ###
# v0.12.16

using Markdown
using InteractiveUtils

# ╔═╡ e077ab16-22e5-11eb-15ed-5f0654bf7328
using Pkg, DrWatson

# ╔═╡ e75a9a06-22e5-11eb-2e36-8d4f62b830ed
begin
	@quickactivate "StatisticsWithJuliaPlutoNotebooks"
	using Random, Distributions, Plots, LaTeXStrings
	Random.seed!(0)
end

# ╔═╡ ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
md"## Listing5.09"

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
begin
	actualAlpha, actualLambda = 2,3
	gammaDist = Gamma(actualAlpha,1/actualLambda)
	n = 10^2
	sample = rand(gammaDist, n)
end

# ╔═╡ 68b12c46-3a9c-11eb-03b3-bf9862c5371c
begin
	alphaGrid = 1:0.02:3
	lambdaGrid = 2:0.02:5
end

# ╔═╡ 68b16da0-3a9c-11eb-21e2-53c689f1599c
likelihood = [prod([pdf.(Gamma(a,1/l),v) for v in sample])
                        for l in lambdaGrid, a in alphaGrid]

# ╔═╡ 68b1f8ce-3a9c-11eb-2ad9-a5af4bd66c0e
surface(alphaGrid, lambdaGrid, likelihood, lw=0.1,
	c=cgrad([:blue, :red]), legend=:none, camera = (135,20),
	xlabel=L"\alpha", ylabel=L"\lambda", zlabel="Likelihood")

# ╔═╡ 475ff888-22e6-11eb-2354-09f8f40a8e12
md"## End of listing5.09"

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═68b12c46-3a9c-11eb-03b3-bf9862c5371c
# ╠═68b16da0-3a9c-11eb-21e2-53c689f1599c
# ╠═68b1f8ce-3a9c-11eb-2ad9-a5af4bd66c0e
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12


using DiffEqBiological
using DifferentialEquations
using Plots
using Colors

logistic_model = @reaction_network logistic begin
  r, cell + nutrient --> 2cell
end r

r = 1.5
cell0 = 1.0
nutrient0 = 2000.0

tspan = (0.0,0.008)
p =  (r)
u0 = [cell0, nutrient0]

oprob = ODEProblem(logistic_model, u0, tspan, p)
osol = solve(oprob, reltol=1e-8,abstol=1e-8)
Kbio = nutrient0+cell0
n = [((Kbio)*(cell0*exp(r*t)))/((Kbio)+(cell0*(exp(r*t)-1))) for t in osol.t]
#plot(osol)
plot(osol.t, [osol.u[i][1] for i in 1:length(osol.t)], labels="Cells", color= "blue", lw=2.0)
plot!(osol.t, [osol.u[i][2] for i in 1:length(osol.t)], labels="Nutrients", color="red", lw=2.0)

# https://nextjournal.com/sosiris-de/diffeqbiological
# What is the final value of cell(t)?  How does it relate to K?
# Can you overlay the analytical solution?

prob = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(prob,Direct(),logistic_model)
sol = solve(jump_prob,FunctionMap())
#plot(sol)
plot(sol.t, [sol.u[i][1] for i in 1:length(sol.t)], labels="", color="blue", lw=2)
plot!(sol.t, [sol.u[i][2] for i in 1:length(sol.t)], labels="", color="red", lw=2)
# Can you plot the stochastic solution (sol) on top of the deterministic solution (osol)?

plot(osol)
plot!(sol)

#or

p1=plot(osol)
p2=plot(sol)

plot(p1,p2, xaxis="Time", yaxis="Population Size")

# Can you see how step-like the results are?
# What happens if you increase nutrient0 to 2000.0?
# Do the stochastic results still look very different to the deterministic ones?
# If you run line 27 above several times, saving the results in a list comprehension,
# then drawing them all on the same plot, will they be the same?

nsins=100
prob = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(prob,Direct(),logistic_model)
solutions =[solve(jump_prob,FunctionMap()) for i in 1:nsins]
plot(solutions[1].t, [solutions[1].u[i][1] for i in 1:length(solutions[1].t)], labels="Cells", color= "blue", xaxis="Time", yaxis="Population Size", title="Discrete Stochastic Populations Dynamics", lw=0.3)
plot!(solutions[1].t,[solutions[1].u[i][2] for i in 1:length(solutions[1].t)], labels="Nutrients", color="red", lw=0.3)
for j in 2:nsins
    p=plot!(solutions[j].t, [solutions[j].u[i][1] for i in 1:length(solutions[j].t)], labels="", color= "blue", lw=0.3)
    q=plot!(solutions[j].t,[solutions[j].u[i][2] for i in 1:length(solutions[j].t)], labels="", color="red", lw=0.3)
    display(p)
    display(q)
end
oprob = ODEProblem(logistic_model, u0, tspan, p)
osol = solve(oprob, reltol=1e-8,abstol=1e-8)
Kbio = nutrient0+cell0
n = [((Kbio)*(cell0*exp(r*t)))/((Kbio)+(cell0*(exp(r*t)-1))) for t in osol.t]
plot!(osol.t, [osol.u[i][1] for i in 1:length(osol.t)], labels="Deterministic Continuous Solution", color="black", lw=2)
plot!(osol.t, [osol.u[i][2] for i in 1:length(osol.t)], labels="", color="black", legend=:right, lw=2)

# Can you simulate from the discrete stochastic exponential model?

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

using DiffEqBiological
using DifferentialEquations
using Plots
using Colors

exponential_model = @reaction_network exponential begin
  r, cell  --> 2cell
end r

r = 5.0
cell0 = 10.0

tspan = (0.0,1.0)
p = (r)
u0 = [cell0]

oprobe = ODEProblem(exponential_model, u0, tspan, p)
osole = solve(oprobe)
plot(osole, labels="Deterministic Solution", lw=2)

probe = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(probe,Direct(),exponential_model)
sole = solve(jump_prob,FunctionMap())
#plot(sole)
plot!(sole, title="Deterministic and Stochastic Exponential Model", xaxis="Time", yaxis="Popuplation Size", legend=:left, lw=2, labels="Stochastic Solution")

nsims=100
probe = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(probe,Direct(),exponential_model)
solutions =[solve(jump_prob,FunctionMap()) for i in 1:nsims]
plot(solutions[1].t, [solutions[1].u[i][1] for i in 1:length(solutions[1].t)], labels="Cells", color="blue", lw=0.2)
for j in 2:nsims
    p=plot!(solutions[j].t, [solutions[j].u[i][1] for i in 1:length(solutions[j].t)], labels="", color="blue", legend=:left, title="Exponential Model w/ Stochastic and Deterministic Solutions", lw=0.2)
    display(p)
end
oprobe = ODEProblem(exponential_model, u0, tspan, p)
osole = solve(oprobe)
plot!(osole, labels="Deterministic Solution", xaxis="Time", yaxis="Population Size",color="black", lw=2)

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

# Can you simulate from the discrete stochastic Lotka-Volterra model?

lv_model = @reaction_network lv begin
  k1, prey --> 2prey
  k2, prey + predator --> 2predator
  k3, predator --> null
 # k4, prey --> null
end k1 k2 k3 #k4

k1 = 1.0
k2 = 0.1
k3 = 0.1
#k4 = 0.001

prey0 = 40.0
predator0 = 100.0
null0 = 0.0
tspan = (0.0,200.0)
p =  (k1,k2,k3)
u0 = [prey0, predator0]

oproblv = ODEProblem(lv_model, u0, tspan, p)
osollv = solve(oproblv, reltol=1e-5,abstol=1e-5)
#plot(osollv)
plot(osollv.t, [osollv.u[i][1] for i in 1:length(osollv.t)], labels="D-Prey", color= "blue", lw=2)
plot!(osollv.t, [osollv.u[i][2] for i in 1:length(osollv.t)], labels="D-Predators", color="orange", lw=2)

prob = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(prob,Direct(),lv_model)
sollv = solve(jump_prob,FunctionMap())
#plot(sollv)
plot!(sollv.t, [sollv.u[i][1] for i in 1:length(sollv.t)], labels="S-Prey", color= "green", lw=2.0)
plot!(sollv.t, [sollv.u[i][2] for i in 1:length(sollv.t)], labels="S-Predators", color="red", lw=2.0, title="Lotka-Volterra Deterministic and Stochastic Models", xaxis="Time", yaxis="Population Size")

# at larger pops the stochastic solution looks very similar to the deterministic solution but it doesnt have the advantage of being continuous

# Can you simulate from the discrete stochastic birth-death model?

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

bd_model = @reaction_network bd begin
  lambda, cell  --> 2cell
  mu, cell  --> null
end lambda mu

lambda = 2.0
#lambda = 0.8
mu= 1.5
#mu= 0.5
cell0 = 100.0
null0 = 0.0

tspan = (0.0,5.0)
p =  (lambda, mu)
u0 = [cell0, null0]

oprobbd = ODEProblem(bd_model, u0, tspan, p)
osolbd = solve(oprobbd, reltol=1e-8,abstol=1e-8)
#plot(osolbd)
plot(osolbd.t, [osolbd.u[i][1] for i in 1:length(osolbd.t)], labels="", color= "blue", ls=:dash)
plot!(osolbd.t, [osolbd.u[i][2] for i in 1:length(osolbd.t)], labels="", color="red", ls=:dash)

probbd = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(probbd,Direct(),bd_model)
solbd = solve(jump_prob,FunctionMap())
#plot(solbd)
plot!(solbd.t, [solbd.u[i][1] for i in 1:length(solbd.t)], labels="Cells", color= "blue")
plot!(solbd.t, [solbd.u[i][2] for i in 1:length(solbd.t)], labels="Dead Cells", color="red", legend=:left, title="Birth-Death Model w/ Deterministic and Stochastic Solutions", xaxis="Time", yaxis="Popuplation Size")

nsins=100
probbd = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(probbd,Direct(),bd_model)
solutions =[solve(jump_prob,FunctionMap()) for i in 1:nsins]
plot(solutions[1].t, [solutions[1].u[i][1] for i in 1:length(solutions[1].t)], labels="Cells", color= "blue", xaxis="Time", yaxis="Population Size", title="Discrete Stochastic Populations Dynamics", lw=0.3)
plot!(solutions[1].t,[solutions[1].u[i][2] for i in 1:length(solutions[1].t)], labels="Dead Cells", color="red", lw=0.2)
for j in 2:nsins
    p=plot!(solutions[j].t, [solutions[j].u[i][1] for i in 1:length(solutions[j].t)], labels="", color= "blue", lw=0.2)
    q=plot!(solutions[j].t,[solutions[j].u[i][2] for i in 1:length(solutions[j].t)], labels="", color="red", lw=0.2)
    display(p)
    display(q)
end
oprobbd = ODEProblem(bd_model, u0, tspan, p)
osolbd = solve(oprobbd, reltol=1e-10,abstol=1e-10)
plot!(osolbd.t, [osolbd.u[i][1] for i in 1:length(osolbd.t)], labels="Deterministic Solution", color="black", lw=3)
plot!(osolbd.t, [osolbd.u[i][2] for i in 1:length(osolbd.t)], labels="", color="black", legend=:left, lw=3, title="Birth-Death Model Showing Stochastic Behavior")

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

# What is the difference between the birth-death model and the exponential model?

using DiffEqBiological
using DifferentialEquations
using Plots
using Colors
#using ParameterizedFunctions

mtdna_model = @reaction_network mtdna begin
  b, wildtype --> 2wildtype
  d, wildtype  --> null
  bmut, mut --> 2mut
  dmut, mut --> null
  m, wildtype --> wildtype + mut
  # can we have wildtype --> 2mut
end b d bmut dmut m

# parameter values

b = 0.001875*365
d = 0.001875*365
bmut = 0.001875*365
dmut = 0.001875*365
m = (1e-5)*365

# initial conditions

wildtype0 = 100.0
mut0 = 0.0
null0 = 0.0
dummy0 = 1.0
tspan= (0.0, 100.0)
p = (b,d,bmut,dmut,m)
u0 = [wildtype0, mut0, null0]

sim_to_tot = function(res, totmax, b_small, b_big, replicative_advantage)
  u0 = res.u[end]
  tot_mtDNA0 = u0[1] + u0[3]
  pnew = collect(p)
  if tot_mtDNA0 >= totmax
    pnew[1] = b_small
    pnew[3] = b_small * replicative_advantage
  else
    pnew[1] = b_big
    pnew[3] = b_big * replicative_advantage
  end
  pnewer = (pnew...,)
  oprobmtdna = ODEProblem(mtdna_model, u0, tspan, pnewer)#, callback=cset)
  osolmtdna = solve(oprobmtdna)
  totmtDNA = [osolmtdna.u[i][1] + osolmtdna.u[i][3] for i in 1:length(osolmtdna.t)];
  triggered = [t>=totmax for t in totmtDNA];
  lastres = findfirst(triggered)-1;
  times = [t for t in osolmtdna.t[1:lastres]]
  species = [u for u in osolmtdna.u[1:lastres]]
  res = (t = times, u = species)
  return(res)
end

res = (t = [0.0], u = [u0])
current_time = res.t[1]
allres = []
while current_time <= tspan[2]
  global current_time, allres, b
  resnew = sim_to_tot(res, 105.0, 0.0, b, 1.0)
  ttotal = [t + current_time for t in resnew.t]
  current_time = ttotal[end]
  updatedres = (t=ttotal, u=resnew.u)
  push!(allres,updatedres)
end

times = collect(Iterators.flatten([res.t for res in allres]))
species = collect(Iterators.flatten([res.u for res in allres]))
osolmtdna = (t=times,u=species)

plot(osolmtdna.t, [osolmtdna.u[i][1] for i in 1:length(osolmtdna.t)], labels="Wildtype", color= "blue")
plot!(osolmtdna.t, [osolmtdna.u[i][3] for i in 1:length(osolmtdna.t)], labels="Mutant", color="red")
plot!(osolmtdna.t, [osolmtdna.u[i][1]+osolmtdna.u[i][3] for i in 1:length(osolmtdna.t)], labels="Total", color="black")

using DiffEqBiological
using DifferentialEquations
using Plots
using Colors

mtdna_model = @reaction_network mtdna begin
  b, wildtype --> 2wildtype
  d, wildtype  --> null
  bmut, mut --> 2mut
  dmut, mut --> null
  m, wildtype --> wildtype + mut
  # can we have wildtype --> 2mut
end b d bmut dmut m

# parameter values

b = 0.001875*365
d = 0.001875*365
bmut = 0.001875*365
dmut = 0.001875*365
m = (1e-5)*365

# initial conditions

wildtype0 = 100.0
mut0 = 10.0
null0 = 0.0
dummy0 = 1.0
tspan= (0.0, 100.0)
p = (b,d,bmut,dmut,m)
u0 = [wildtype0, mut0, null0]

sim_to_tot2 = function(res, totmax, b_small, b_big, replicative_advantage)
  u0 = res.u[end]
  tot_mtDNA0 = u0[1] + u0[3]
  pnew = collect(p)
  if tot_mtDNA0 >= totmax
    pnew[1] = b_small
    pnew[3] = b_small * replicative_advantage
  else
    pnew[1] = b_big
    pnew[3] = b_big * replicative_advantage
  end
  pnewer = (pnew...,)
  probmtdna = DiscreteProblem(u0, tspan ,pnewer)
  jump_prob = JumpProblem(probmtdna,Direct(),mtdna_model)
  solmtdna = solve(jump_prob,FunctionMap())
  totmtDNA = [solmtdna.u[i][1] + solmtdna.u[i][3] for i in 1:length(solmtdna.t)];
  triggered = [t>=totmax for t in totmtDNA];
  lastres = findfirst(triggered)-1;
  times = [t for t in solmtdna.t[1:lastres]]
  species = [u for u in solmtdna.u[1:lastres]]
  res = (t = times, u = species)
  return(res)
end

res = (t = [0.0], u = [u0])
current_time = res.t[1]
allres = []
while current_time <= tspan[2]
  global current_time, allres, b
  resnew = sim_to_tot2(res, 105.0, 0.0, b, 1.0)
  ttotal = [t + current_time for t in resnew.t]
  current_time = ttotal[end]
  updatedres = (t=ttotal, u=resnew.u)
  push!(allres,updatedres)
end

times = collect(Iterators.flatten([res.t for res in allres]))
species = collect(Iterators.flatten([res.u for res in allres]))
solmtdna = (t=times,u=species)

plot(solmtdna.t, [solmtdna.u[i][1] for i in 1:length(solmtdna.t)], labels="Wildtype", color= "blue")
plot!(solmtdna.t, [solmtdna.u[i][3] for i in 1:length(solmtdna.t)], labels="Mutant", color="red")
plot!(solmtdna.t, [solmtdna.u[i][1] + solmtdna.u[i][3] for i in 1:length(solmtdna.t)], labels="Total", color="black", legend=:outerright, title="mtDNA Population Dynamics Within a Cell", xaxis="Time (Years)", yaxis="Population Size")

nsins=10
probmtdna = DiscreteProblem(u0, tspan ,p)
jump_prob = JumpProblem(probmtdna,Direct(),mtdna_model)
solutions =[solve(jump_prob,FunctionMap()) for i in 1:nsins]
plot(solutions[1].t, [solutions[1].u[i][1] for i in 1:length(solutions[1].t)], labels="Wildtype", color= "blue", xaxis="Time", yaxis="Population Size", title="mtDNA Populations Dynamics", lw=0.3)
plot!(solutions[1].t,[solutions[1].u[i][2] for i in 1:length(solutions[2].t)], labels="Mutant", color="red", lw=0.3)
for j in 2:nsins
    p=plot!(solutions[j].t, [solutions[j].u[i][1] for i in 1:length(solutions[j].t)], labels="", color= "blue", lw=0.3)
    q=plot!(solutions[j].t,[solutions[j].u[i][3] for i in 1:length(solutions[j].t)], labels="", color="red", lw=0.3)
    display(p)
    display(q)
end
oprobmtdna = ODEProblem(mtdna_model, u0, tspan, p)
osolmtdna = solve(oprobmtdna)
plot!(osolmtdna.t, [osolmtdna.u[i][1] for i in 1:length(osolmtdna.t)], labels="Deterministic Continuous Solution", color="black", lw=2)
plot!(osolmtdna.t, [osolmtdna.u[i][3] for i in 1:length(osolmtdna.t)], labels="", color="black", legend=:right, lw=2)

# What happens if a cells is born with no mutations?

# What happens if a cell inherits a lot of mutations?  70%?

# What happens in both of the scenarios above if bmut > b?


"""
    combination(n, r)

returns the number of ways picking r unordered outcomes from
n possibilities, without repetition

# Arguments:
- `n`: Positive integers of items to choose from
- 'r': Positive integers of items to choose

Contributed By:- [Mayur Dahibhate](https://github.com/mayurdahibhate)
"""

function combination(n, r)

    function factorial(n)
        fact = 1

        if n == 0 || n == 1
            return fact
        end

        for i = 1:n
            fact = fact * i
        end

        return fact
    end

    comb = factorial(n) / (factorial(r) * factorial(n - r))

    return convert(Int64, comb)
end