# # Ice Flow over a Bumpy Bed

# This is the first two dimensional example: flow down a plane with bumps on different length scales.
# This example is similar in spirit to Experiment A in the Ice Sheet Model Intercomparison Exercise - Higher Order Model (ISMIP-HOM): doi: 10.5194/tc-2-95-2008
# This example demonstrates: How to use WAVI.jl in plan view (two horizontal spatial dimensions) with arbitrary bed shapes, and how to interact with Grid objects.

# ## Install dependencies
#
# First let's make sure we have all required packages installed.

# using Pkg
# Pkg.add("https://github.com/RJArthern/WAVI.jl"), Pkg.add(Plots)
using WAVI, Plots

# ## Basal Topography
#
# Following ISMIP-HOM, let's take a bed with a series of sinusoidal oscillations with an amplitude of 500m:
# $$
# z_b(x,y) = -x \tan \alpha + 500 \sin (\omega x) \sin(\omega y)
# $$
# Here $L$ is the lengthscale of the domain, $\alpha$ is the net slope of the plane, and $\omega = 2\pi / L$ is the frequency of the bumps.
# Let's define this bed as a function and take a look at it:

z_b(x,y; α, ω) = -x * tand(α) + 500sin(ω*x)*sin(ω*y);

# It's useful to define our model grid here. We can do this for an arbitary domain length L.
grid(; L, nx = 80,ny = 80) = Grid(nx = nx, ny = ny, dx = L/nx, dy = L/ny, y0 = 0.0, x0 = 0.0);

# The final two arguments simply set the origin of the co-ordinate system. This grid has 80 grid points in each direction by default

# Let's choose a domain of 80km with 80 grid points
L = 80000.;
grid80 = grid(L = L);

# This grid object contains information about the location of grid points. We use this to construct and array defining the bed:
z_b80 = z_b.(grid80.xxh,grid80.yyh; α = 0.5, ω = 2π/L );
plt = Plots.heatmap(grid80.xxh[:,1]/1e3, grid80.yyh[1,:]/1e3, z_b80,
                        xlabel = "x (km)",
                        ylabel = "y (km)",
                        colorbar_title = "bed depth (m)")
plot!(size = (800,400))
#display(plt)

# ## Model Instantiation and Initial Conditions
# In the ISMIP-HOM comparison, the main test is velocity along the line $y = L/4$, for various different values of $L$.
# Before we do that, lets look at the velocity for the example we started above with $L = 80$km.

# To begin, we create an `InitialConditions` object to prescribe the ice thickness of 1000m everywhere
initial_conditions = InitialConditions(initial_thickness = 1000. .* ones(grid80.nx, grid80.ny));

# Now we can build our model
model80 = Model(grid = grid80,
            bed_elevation = z_b80,
            initial_conditions = initial_conditions);

# ## Determining the velocity
# To bring the velocity in line with the ice thickness, we have to use the `update_state!` function
update_state!(model80);

# Let's have a look at the velocity component in the x-direction
Plots.heatmap(model80.grid.xxh[:,1]/1e3, model80.grid.yyh[1,:]/1e3, model80.fields.gh.u',
                        xlabel = "x (km)",
                        ylabel = "y (km)",
                        colorbar_title = "ice velocity in x-direction (m/yr)")
plot!(size = (1000,550))

# ## Different Lengthscales
# Now let's look at how the velocity along a flowline changes with L.
L_values =  [160, 80, 40, 20, 10, 5]*1.0e3;

# We loop over these values and store the info:
U_flowline = zeros(80, length(L_values));
grid_flowline = zeros(80, length(L_values));
for (count,L) in enumerate(L_values) ;
    gridL = grid(L = L);
    z_bL = z_b.(gridL.xxh,gridL.yyh; α = 0.5, ω = 2π/L );
    initial_conditions = InitialConditions(initial_thickness = 1000. .* ones(80, 80));
    model = Model(grid = gridL,
                bed_elevation = z_bL,
                initial_conditions = initial_conditions);
    update_state!(model);
    grid_flowline[:, count] .= model.grid.xxh[:, round(Int, gridL.nx/4)];
    U_flowline[:,count] .= model.fields.gh.u[:, round(Int, gridL.nx/4)];
end

#And make the plot in a single command
plot(grid_flowline/1e3,
    U_flowline,
    layout = (2,3),
    framestyle = :box,
    xlabel = "x (km)",
    ylabel = "horizontal velocity (m/yr)",
    label = :none,
    title =  ["160km" "80km" "40km" "20km" "10km" "5km"])
plot!(size = (1000,550))


export
    istriangular,
    nthtriangular, ntriangular,
    alltriangular, sometriangular, exacttriangular,
    primitive_pythagorean_triples

istriangular(n::Integer) = isperfectsquare(8*n + 1)

nthtriangular(n::Integer) = (n * (n+1)) ÷ 2
ntriangular(n::Int, T::Type = Int) = collect(exacttriangular(n, T))

alltriangular(T::Type = Int) = TriangularIterator{T}()
sometriangular(xmax::T) where {T<:Integer} = @pipe alltriangular(T) |> takewhile(x -> x <= xmax, _)
exacttriangular(n::Int, T::Type = Int) = @pipe alltriangular(T) |> take(_, n)

struct TriangularIterator{T<:Integer}
end

Base.start(::TriangularIterator{T}) where {T<:Integer} = zero(T), one(T)
Base.next(::TriangularIterator{T}, state) where {T<:Integer} = begin
    s, n = state
    s += n
    s, (s, n + 1)
end
Base.done(::TriangularIterator, _) = false

Base.eltype(it::TriangularIterator) = Base.eltype(typeof(it))
Base.eltype(::Type{TriangularIterator{T}}) where {T<:Integer} = T

Base.iteratorsize(::TriangularIterator) = Base.IsInfinite()

primitive_pythagorean_triples(T::Type = Int) = PrimitivePythagoreanTripleIterator{T}()
primitive_pythagorean_triples(maxperimeter::T) where {T} = PrimitivePythagoreanTriplePerimeterIterator{T}(maxperimeter)

abstract type PrimitivePythagoreanTripleIteratorBase{T<:Integer} end

struct PrimitivePythagoreanTripleIterator{T<:Integer} <: PrimitivePythagoreanTripleIteratorBase{T}
end

struct PrimitivePythagoreanTriplePerimeterIterator{T<:Integer} <: PrimitivePythagoreanTripleIteratorBase{T}
    maximum::T
end

Base.start(::PrimitivePythagoreanTripleIterator{T}) where {T} = begin
    q = List{Array{T,1}}()
    n = push!(q, [3,4,5])
    q, n
end
Base.start(it::PrimitivePythagoreanTriplePerimeterIterator{T}) where {T} = begin
    q = List{Array{T,1}}()
    n = it.maximum >= 12 ? push!(q, [3,4,5]) : q.node
    q, n
end

Base.next(::PrimitivePythagoreanTripleIterator, state) = begin
    q, n = state
    n.data, (q, next!(q, n))
end
Base.next(it::PrimitivePythagoreanTriplePerimeterIterator, state) = begin
    q, n = state
    n.data, (q, next!(q, n, it.maximum))
end

Base.done(::PrimitivePythagoreanTripleIteratorBase, state) = begin
    q, n = state
    iseol(q, n)
end

Base.eltype(it::PrimitivePythagoreanTripleIteratorBase) = Base.eltype(typeof(it))
Base.eltype(::Type{PrimitivePythagoreanTripleIterator{T}}) where {T} = Array{T,1}
Base.eltype(::Type{PrimitivePythagoreanTriplePerimeterIterator{T}}) where {T} = Array{T,1}

Base.iteratorsize(::PrimitivePythagoreanTripleIterator) = Base.IsInfinite()
Base.iteratorsize(::PrimitivePythagoreanTriplePerimeterIterator) = Base.SizeUnknown()

iseol(q::List{T}, n::ListNode{T}) where {T} = n == q.node

next!(q::List{T}, n::ListNode{T}) where {T} = begin
    n = n.next
    iseol(q, n) || return n

    x, y, z = calculate(shift!(q)...)
    n = push!(q, x)
    push!(q, y)
    push!(q, z)
    n
end

next!(q::List{T}, n::ListNode{T}, maxsum) where {T} = begin
    n = n.next
    iseol(q, n) || return n

    while iseol(q, n) && !isempty(q)
        x, y, z = calculate(shift!(q)...)
        sum(x) <= maxsum && (nx = push!(q, x); n = nx)
        sum(y) <= maxsum && (ny = push!(q, y); n = iseol(q, n) ? ny : n)
        sum(z) <= maxsum && (nz = push!(q, z); n = iseol(q, n) ? nz : n)
    end
    n
end

calculate(a, b, c) = begin
    x = [ a - 2b + 2c,  2a - b + 2c,  2a - 2b + 3c] # const U = [ 1  2  2, -2 -1 -2, 2 2 3], [a b c] * U
    y = [ a + 2b + 2c,  2a + b + 2c,  2a + 2b + 3c] # const A = [ 1  2  2,  2  1  2, 2 2 3], [a b c] * A
    z = [-a + 2b + 2c, -2a + b + 2c, -2a + 2b + 3c] # const D = [-1 -2 -2,  2  1  2, 2 2 3], [a b c] * D
    x, y, z
end


#The "1/e^2 beamwidth" seems to have inconsistent definitions,
#either meaning the half-width or the full width of the beam
#Here "beamwidth" will refer to the full width, and "sigma" will
#refer to the halfwidth
#Sigma is the width of a gaussian defined as y = e^-(2x^2/sigma^2)
#(this parameterization of the Gaussian is common for optics but not universal. The other
#common convention has a 2 in the denominator of the exponent rather thatn the numerator.
#This is the convention in statistics and thus is how variance and std are defined.  So if we want
#an r.m.s. value we should use this latter convention)
#By the former convention "sigma" is the distance from the origin at which the gaussian
#decays to 1/e^2 of its maximum value, i.e. the halfwidth.
#sigma_statistics = std = sigma_optics/2
#sigma_optics = 2*std

#gaussian approximation to airy disk
#taken from paraxial approximations derived in:
#Gaussian approximations of fluorescence microscope point-spread function models
#Bo Zhang, Josiane Zerubia, and Jean-Christophe Olivo-Marin
#1 April 2007 Vol. 46, No. 10 APPLIED OPTICS
#Note that the authors derive the below expression for "sigma"
#but their definition of sigma differs from ours; they use the statistical convention
#(see comments at top of this file) instead of 1/e^2 convention
#their r.h.s. is    e^-(x^2/2*sigma^2)
#...while ours is   e^-(2x^2/sigma^2)
#thus our axial sigma is twice theirs
approx_sigma_axial(lambda, NA, n) = 4*sqrt(6)*n/(2*pi/lambda*NA^2)
approx_sigma_axial(lambda, f, unfocused_sigma, n) = approx_sigma_axial(lambda, lens_na(unfocused_sigma, f), n)

approx_sigma_lateral(lambda, NA) = sqrt(2)*lambda / (pi*NA)
approx_sigma_lateral(lambda, f, unfocused_sigma) = approx_sigma_lateral(lambda, lens_na(unfocused_sigma, f))
focused_beamwidth(lambda, f, unfocused_beamwidth) = 2*approx_sigma_lateral(lambda, f, unfocused_beamwidth/2)

#assuming a lens has a big enough aperture to accept
#a collimated input of specified halfwidth and also
#that the medium is the same before and after focusing
#what is the NA of the system?
lens_na(unfocused_sigma, f) = unfocused_sigma/f

#Halfwidth of the Gaussian approximation of the axial PSF of a light sheet imaging system,
#considering both the PSF of the imaging lens and the sheet thickness
lightsheet_axial_combined_psf(det_sigma, sheet_sigma) =
    sqrt((det_sigma^2*sheet_sigma^2)/(det_sigma^2 + sheet_sigma^2))

rayleigh_length(lambda, focused_sigma) = pi*focused_sigma^2 / lambda
rayleigh_length(lambda, f, unfocused_sigma, n) =
    rayleigh_length(lambda, approx_sigma_lateral(lambda, f, unfocused_sigma, n))

#sigma of a focused beam as a function of displacement from the beam waist
displaced_sigma(lambda, focused_sigma, displacement) = sqrt(focused_sigma^2*(1+(lambda*displacement/(pi*focused_sigma^2))^2))

#This doesn't match the calculation on the OpenSPIM website.  Seems theirs is incorrect.
fwhm_stats(std) = 2*sqrt(2*log(2)) * std
fwhm_optics(sigma) = fwhm_stats(sigma/2)

#Currently aren't using these
abbe_lat(lambda, NA) = lambda/(2*NA)
abbe_ax(lambda, NA) = 2*lambda/(NA^2)


using MPFR_wrap

const atol = BigFloat("1e-100000")
const prec = 333_000
const π_MPFR = BigFloat(π, prec)

function brent_salamin()
    old_prec = precision(BigFloat)
    setprecision(BigFloat, prec)
    a_prev, b_prev = big(1.0), 1 / sqrt(big(2.0))
    s_prev, π_prev = big(1.0) / 2, big(4.0)
    π_next = BigFloat()
    k, p2 = 1, big(2)
    while true
        a_next = (a_prev + b_prev) / 2
        b_next = sqrt(a_prev * b_prev)
        c_next = (a_next - b_next) * (a_next + b_next)
        s_next = s_prev - p2 * c_next
        π_next = 2a_next^2 / s_next
        #println("$k -> $pn")
        δπ = π_next - π_prev
        abs(δπ) ≤ atol && break
        a_prev, b_prev, s_prev, π_prev = a_next, b_next, s_next, π_next
        k += 1
        p2 *= 2
    end
    setprecision(BigFloat, old_prec)
    @assert isapprox(π_next, π_MPFR, atol=atol)
    (k, π_next)
end

function inplace_brent_salamin()
    a_prev = BigFloat(1, precision=prec)
    b_prev = BigFloat(2, precision=prec)
    rec_sqrt!(b_prev, b_prev)
    s_prev = BigFloat(0.5, precision=prec)
    π_prev = BigFloat(4, precision=prec)
    a_next = BigFloat(precision=prec)
    b_next = BigFloat(precision=prec)
    c_next = BigFloat(precision=prec)
    s_next = BigFloat(precision=prec)
    π_next = BigFloat(precision=prec)
    δπ = BigFloat(precision=prec)
    k = Int32(1)
    while true
        add!(a_next, a_prev, b_prev)
        div_2!(a_next, a_next, Int32(1))

        mul!(b_next, a_prev, b_prev)
        sqrt!(b_next, b_next)

        sub!(c_next, a_prev, a_next)
        sqr!(c_next, c_next)

        mul_2!(c_next, c_next, k)
        sub!(s_next, s_prev, c_next)

        sqr!(π_next, a_next)
        mul_2!(π_next, π_next, Int32(1))
        div!(π_next, π_next, s_next)

        sub!(δπ, π_next, π_prev)
        abs(δπ) ≤ atol && break

        a_prev, a_next = a_next, a_prev
        b_prev, b_next = b_next, b_prev
        s_prev, s_next = s_next, s_prev
        π_prev, π_next = π_next, π_prev

        k += Int32(1)
    end
    @assert isapprox(π_next, π_MPFR, atol=atol)
    (k, π_next)
end

function inplace_borwein_cubic()
    a_prev = BigFloat(1, precision=prec)
    div!(a_prev, a_prev, Int32(3))
    s_prev = BigFloat(3, precision=prec)
    sqrt!(s_prev, s_prev)
    sub!(s_prev, s_prev, Int32(1))
    div_2!(s_prev, s_prev, Int32(1))
    r_next = BigFloat(precision=prec)
    s_next = BigFloat(precision=prec)
    rsq_next = BigFloat(precision=prec)
    a_next = BigFloat(precision=prec)
    tmp = BigFloat(precision=prec)
    π_prev = BigFloat(0, precision=prec)
    π_next = BigFloat(precision=prec)
    δπ = BigFloat(precision=prec)
    k = Int32(0)
    while true
        pow!(r_next, s_prev, Int32(3))
        sub!(r_next, Int32(1), r_next)
        cbrt!(r_next, r_next)
        mul_2!(r_next, r_next, Int32(1))
        add!(r_next, r_next, Int32(1))
        div!(r_next, Int32(3), r_next)

        sub!(s_next, r_next, Int32(1))
        div_2!(s_next, s_next, Int32(1))

        sqr!(rsq_next, r_next)
        sub!(tmp, rsq_next, Int32(1))
        mul!(tmp, tmp, Int32(3)^k)
        mul!(a_next, rsq_next, a_prev)
        sub!(a_next, a_next, tmp)

        div!(π_next, Int32(1), a_next)
        sub!(δπ, π_next, π_prev)
        abs(δπ) ≤ atol && break

        a_prev, a_next = a_next, a_prev
        s_prev, s_next = s_next, s_prev
        π_prev, π_next = π_next, π_prev

        k += Int32(1)
    end
    @assert isapprox(π_next, π_MPFR, atol=atol)
    (k, π_next)
end


include("./Legendre_polynomials.jl")

function Spherical_harmonics_a!(x::Float64, lmax::Int64;
                                Yₗ = nothing, Pₗ = nothing)
    #### Returns an array of size lmax_half. Each cell contains one (even axisymmetric) spherical harmonic at point cos(θ) = x ####
    #### The "a" in the name is here to outline that this function only gives the "a"xisymmetric harmonics (i.e. m = 0).         ####
    # getting the constants we will need
    lmax_half = div(lmax, 2)

    # let's preallocate the arrays that haven't been given
    if isnothing(Yₗ)
        Yₗ = zeros(lmax_half) # WIll hold the spherical harmonics
    end
    if isnothing(Pₗ)
        Pₗ = zeros(lmax_half) # WIll hold the Legendre polynomials
    end

    # let's comptute the (even) Legendre polynomials at point x
    Legendre_polynomials!(x, lmax, Pₗ = Pₗ)

    # let's compute the spherical harmonics
    for l_half = 1:lmax_half
        l = 2 * l_half
        Yₗ[l_half] = Pₗ[l_half] * sqrt((2 * l + 1) / (4 * π)) # Yₗ₀ = Pₗ₀ * sqrt((2 l + 1) / 4 π)
    end

    return(Yₗ)
end

function Spherical_harmonics!(θ::Float64, ϕ::Float64, lmax::Int64;
                                Yₗₘ = nothing, Pₗₘ = nothing)
    #### Returns an array of size lmax_half * (2 lmax + 1). Each cell contains one (even) spherical harmonic at point (θ, ϕ) ####
    #### This function has no "a" in its title because it returns ALL the spherical harmonics (i.e. m = 0 and m != 0)        ####
    # getting the constants we will need
    lmax_half = div(lmax, 2)

    # let's preallocate the arrays that haven't been given
    if isnothing(Yₗₘ)
        Yₗₘ = zeros(2 * lmax + 1, lmax_half) # Will hold the spherical harmonics
    end
    if isnothing(Pₗₘ)
        Pₗₘ = zeros(lmax + 1, lmax_half) # Will hold the normalized associated Legendre polynomials
    end

    # Let's compute the normalzed associated Legendre polynomials
    Normalized_associated_Legendre_polynomials!(cos(θ), lmax, P = Pₗₘ)

    # Let's compute the spherical harmonics
    for l_half = 1:lmax_half
        l = 2 * l_half

        for m = 0:l
            # The passage from Pₗₘ to Yₗₘ is not the same if m is positive, null or negative.
            # Cf Wikipedia : https://en.wikipedia.org/wiki/Spherical_harmonics#Real_form
            if (m != 0)
                Yₗₘ[lmax + 1 - m, l_half] = (-1)^(m) * sqrt(2) * Pₗₘ[m + 1, l_half] * sin(m * ϕ)
                Yₗₘ[lmax + 1 + m, l_half] = (-1)^(m) * sqrt(2) * Pₗₘ[m + 1, l_half] * cos(m * ϕ)
            else
                Yₗₘ[lmax + 1, l_half] = Pₗₘ[1, l_half]
            end
        end
    end

    return(Yₗₘ)
end


# Common functions compatible for use with physical vectors

function dot_product(A::AbstractArray{T1,N},B::AbstractArray{T2,N}) where {T1<:Number,T2<:Number,N}

    size(A) === size(B) || throw(ArgumentError("Vectors must have the same shape"))
    u = zeros(promote_type(T1,T2),size(A)[1:end-1]...)
    dot_product!(u,A,B)
    return u
end

function dot_product!(u::AbstractArray{T1,N}, A::AbstractArray{T2,N2},B::AbstractArray{T3,N2}) where {T1<:Number,T2<:Number,T3<:Number,N,N2}

    (size(A) === size(B)) || throw(ArgumentError("Vectors must have the same shape"))
    (N === N2-1) || throw(ArgumentError("Output should be scalar matrix, one dimension less than input"))

    T = promote_type(T1,T2,T3)
    @turbo for I in CartesianIndices(u)
        x_temp = zero(T)
        for i in 1:N
            x_temp += A[I,i]*B[I,i]
        end
        u[I] = x_temp
    end
    return u
end

function cross_product(A::AbstractArray{T1,4},B::AbstractArray{T2,4}) where {T1<:Number,T2<:Number}

    size(A) === size(B) || throw(ArgumentError("Vectors must have the same shape"))
    s = size(A)
    u = zeros(promote_type(T1,T2),s...)
    cross_product!(u,A,B)
    return u
end

function cross_product!(u::AbstractArray{T1,4},A::AbstractArray{T2,4},B::AbstractArray{T3,4}) where {T1<:Number,T2<:Number,T3<:Number}

    (size(A) === size(B) && size(A) === size(u))|| throw(ArgumentError("Vectors must have the same shape"))
    s = size(u)

    @turbo for r in 1:s[3], q in 1:s[2], p in 1:s[1]
        u[p,q,r,1] = A[p,q,r,2]*B[p,q,r,3] - A[p,q,r,3]*B[p,q,r,2]
        u[p,q,r,2] = A[p,q,r,3]*B[p,q,r,1] - A[p,q,r,1]*B[p,q,r,3]
        u[p,q,r,3] = A[p,q,r,1]*B[p,q,r,2] - A[p,q,r,2]*B[p,q,r,1]
    end
end

function square_norm(A::AbstractArray{T,N}) where {T<:Number,N}
    u = zeros(T,size(A)[1:end-1]...)
    square_norm!(u,A)
    return u
end

function square_norm!(u::AbstractArray{T1,N1},A::AbstractArray{T2,N2}) where {T1<:Number,T2<:Number,N1,N2}
    dot_product!(u,A,A)
    @. u = u^0.5
end


# ## GPU Computing

# https://github.com/JuliaGPU

using Plots, BenchmarkTools, FFTW, LinearAlgebra

# ### Advection equation for a rotation in two dimensional domain
#md #
#md # ```math
#md #  \\frac{d f}{dt} +  (y \\frac{d f}{dx} - x \\frac{d f}{dy}) = 0
#md # ```
#md #
#md # ``x \in [-π, π], y \in [-π, π] `` and  `` t \in [0, 200π] ``

#md # ---

struct Mesh

    nx   :: Int64
    ny   :: Int64
    x    :: Vector{Float64}
    y    :: Vector{Float64}
    kx   :: Vector{Float64}
    ky   :: Vector{Float64}

    function Mesh( xmin, xmax, nx, ymin, ymax, ny)
        ## periodic boundary condition, we remove the end point.
        x = range(xmin, stop=xmax, length=nx+1)[1:end-1]
        y = range(ymin, stop=ymax, length=ny+1)[1:end-1]
        kx  = 2π ./ (xmax-xmin) .* [0:nx÷2-1;nx÷2-nx:-1]
        ky  = 2π ./ (ymax-ymin) .* [0:ny÷2-1;ny÷2-ny:-1]
        new( nx, ny, x, y, kx, ky)
    end
end

#md # ---

function exact(time, mesh :: Mesh; shift=1.0)

    f = zeros(Float64,(mesh.nx, mesh.ny))
    for (i, x) in enumerate(mesh.x), (j, y) in enumerate(mesh.y)
        xn = cos(time)*x - sin(time)*y
        yn = sin(time)*x + cos(time)*y
        f[i,j] = exp(-(xn-shift)*(xn-shift)/0.1)*exp(-(yn-shift)*(yn-shift)/0.1)
    end

    f
end

#md # ---

function animation( tf, nt)

    mesh = Mesh( -π, π, 64, -π, π, 64)
    dt = tf / nt
    t = 0
    anim = @animate for n=1:nt

       f = exact(t, mesh)
       t += dt
       p = contour(mesh.x, mesh.y, f, axis=[], framestyle=:none)
       plot!(p[1]; clims=(0.,1.), aspect_ratio=:equal, colorbar=false, show=false)
       plot!(sqrt(2) .* cos.(-pi:0.1:pi+0.1),
             sqrt(2) .* sin.(-pi:0.1:pi+0.1), label="", show=false)
       xlims!(-π,π)
       ylims!(-π,π)

    end

    anim

end

#md # ---

anim = animation( 2π, 100)
#md gif(anim, "rotation2d.gif", fps = 20)
#md nothing # hide

#md # ![](rotation2d.gif)

#md # ---

function rotation_on_cpu( mesh :: Mesh, nt :: Int64, tf :: Float64)

    dt = tf / nt

    f   = zeros(ComplexF64,(mesh.nx,mesh.ny))
    f  .= exact( 0.0, mesh )

    exky = exp.( 1im*tan(dt/2) .* mesh.x  .* mesh.ky')
    ekxy = exp.(-1im*sin(dt)   .* mesh.y' .* mesh.kx )

    for n = 1:nt

        fft!(f, 2)
        f .= exky .* f
        ifft!(f,2)

        fft!(f, 1)
        f .= ekxy .* f
        ifft!(f, 1)

        fft!(f, 2)
        f .= exky .* f
        ifft!(f, 2)

    end

    real(f)

end

#md # ---

mesh = Mesh( -π, π, 1024, -π, π, 1024)
nt, tf = 100, 20.
rotation_on_cpu(mesh, 1, 0.1) # trigger building
etime = @time norm( rotation_on_cpu(mesh, nt, tf) .- exact( tf, mesh))
println(etime)

@test true #src

#md # ---

using Pkg

GPU_ENABLED = haskey(Pkg.installed(), "CUDAdrv")

if GPU_ENABLED

    using CUDAdrv, CuArrays, CuArrays.CUFFT

    println(CUDAdrv.name(CuDevice(0)))

end

#md # ---

if GPU_ENABLED

    function rotation_on_gpu( mesh :: Mesh, nt :: Int64, tf :: Float64)

        dt  = tf / nt
        f   = zeros(ComplexF64,(mesh.nx, mesh.ny))
        f  .= exact( 0.0, mesh)

        d_f    = CuArray(f) # allocate f on GPU

        p_x    = plan_fft!(d_f,  [1]) # Create fft plans on GPU
        pinv_x = plan_ifft!(d_f, [1])
        p_y    = plan_fft!(d_f,  [2])
        pinv_y = plan_ifft!(d_f, [2])

        d_exky = CuArray(exp.( 1im*tan(dt/2) .* mesh.x  .* mesh.ky'))
        d_ekxy = CuArray(exp.(-1im*sin(dt)   .* mesh.y' .* mesh.kx ))

        for n = 1:nt

            p_y * d_f
            d_f .*= d_exky
            pinv_y * d_f

            p_x * d_f
            d_f .*= d_ekxy
            pinv_x * d_f

            p_y * d_f
            d_f .*= d_exky
            pinv_y * d_f

        end

        f .= collect(d_f) # Transfer f from GPU to CPU
        real(f)

    end

end

#md # ---

if GPU_ENABLED

    nt, tf = 100, 20.
    rotation_on_gpu(mesh, 1, 0.1)
    etime = @time norm( rotation_on_gpu(mesh, nt, tf) .- exact( tf, mesh))
    println(etime)
    @test true #src

end

#md # Next: [Metaprogrammimg and Particle Group](/Numkin2019/04/build/index.html)


"""

The chemostat steady state impose a contraint over the exchanges:

``intake <= c/ξ``

where ``c`` is the concentration of the metabolite in the medium and ``ξ`` is the cell-specific dilution rate.
Together with the thermodinamic bound we have

stst_bound(c, ξ, b) = sign(b) * min(c/ ξ, abs(b))

"""
stst_bound(c, ξ, b) = sign(b) * min(c/ ξ, abs(b))


"""
    function latticeCompactHexagonal(box,r)

Make a lattice with compact hexagonal structure in box with spheres of radius r.

# Returns
[posX,posY,posZ] Where pos are arrays with the positions of the cells for each component.
"""
function compactHexagonal(box::Array{Array{Float64,1},1},r::Number)

    d = length(box)
    if !(d in [1,2,3])
        error("Incompatible number of dimensions ", d)
    end

    #Make first dimension
    lineX = Array(box[1][1]:2*r:box[1][2])

    #Make second dimension
    if d > 1
        lineY = fill(box[2][1]+r,length(lineX))
        nY = ceil(Int,(box[2][2]-box[2][1])/(4*sin(pi/3)*r))
        areaX = Float64[]
        areaY = Float64[]
        dx = 2*r*cos(pi/3)
        dy = 2*r*sin(pi/3)
        for i in 0:nY-1
            append!(areaX,lineX)
            append!(areaX,lineX.+dx)

            append!(areaY,lineY.+dy*2*i)
            append!(areaY,lineY.+dy*2*i.+dy)
        end
    end

    #Make third dimension
    if d == 3
        areaZ = fill(box[2][1]+r,length(areaX))
        nZ = ceil(Int,(box[3][2]-box[3][1])/(4*sin(pi/3)*r))
        volumeX = Float64[]
        volumeY = Float64[]
        volumeZ = Float64[]
        dy = 2*r/2/cos(pi/6)
        dz = 2*sqrt(2/3)*r
        for i in 0:nZ-1
            append!(volumeX,areaX)
            append!(volumeX,areaX)

            append!(volumeY,areaY)
            append!(volumeY,areaY.+dy)

            append!(volumeZ,areaZ.+dz*2*i)
            append!(volumeZ,areaZ.+dz*2*i.+dz)
        end
    end

    #Return
    if d == 1
        return lineX
    elseif d == 2
        return areaX, areaY
    else
        return volumeX, volumeY, volumeZ
    end

end


using OrdinaryDiffEq, PoissonRandom

function rate_to_proportion(r::Float64,t::Float64)
    1-exp(-r*t)
end;

function sir_abm!(du,u,p,t)
    (β,c,γ,δt) = p
    N = length(u)
    # Initialize du to u
    for i in 1:N
        du[i] = u[i]
    end
    for i in 1:N # loop through agents
        # If recovered
        if u[i]==Recovered
            continue
        # If susceptible
        elseif u[i]==Susceptible
            ncontacts = pois_rand(c*δt)
            while ncontacts > 0
                j = rand(1:N)
                if j==i
                    continue
                end
                a = u[j]
                if a==Infected && rand() < β
                    du[i] = Infected
                    break
                end
                ncontacts -= 1
            end
        # If infected
        else u[i]==Infected
            if rand() < γ
                du[i] = Recovered
            end
        end
    end
    nothing
end

δt = 0.1
nsteps = 400
tf = nsteps*δt
tspan = (0.0,nsteps)
t = 0:δt:tf;
β = 0.05
c = 10.0
γ = rate_to_proportion(0.25,δt)
p = [β,c,γ,δt]

@enum InfectionStatus Susceptible Infected Recovered
Base.zero(::Type{<:InfectionStatus}) = Susceptible

N = 1000
I0 = 10
u0 = Array{InfectionStatus}(undef,N);
for i in 1:N
    if i <= I0
        s = Infected
    else
        s = Susceptible
    end
    u0[i] = s
end

prob_map = DiscreteProblem(sir_abm!,u0,tspan,p)
sol_map = solve(prob_map,FunctionMap())


# Code for Video 1.5:

function sum(x, y, z)
  return x + y + z
end

show(sum(2, 3, 4)) # 9
show(sum(2.1, 3.2, 4.3)) # 9.600000000000001
show(sum(true, false, true)) # 2

typeof(sum) # Function

function sum_prod(x, y)
    x + y, x * y
end

# sum("Iris", 7, true)
# ERROR: MethodError: `+` has no method matching +(::ASCIIString, ::Int64)

function sum(x:: Int64, y::Int64, z::Int64)
    return x + y + z
end

function sump(x:: Int64, y::Int64, z::Int64)
    p::Int64 = x * y * z
    return x + y + z + p
end

 sum(x, y, z) = x + y + z

function sumvar(args...)
    sum = 0
    for n in args
        sum += n
    end
    return sum
end

sumvar(2,3,4,100,-9) # 100
sumvar(2,3,4) # 9
sumvar() # 0

function sum(n1, n2, n3=5; n4=-9)
    return n1 + n2 + n3 + n4
end

sum(2, 3) # 1 (= 2 + 3 + 5 -9)
sum(2, 3, 4) # 0  (= 2 + 3 + 4 -9)
sum(2, 3, 4, n4=100) # 109 (= 2 + 3 + 4 + 100)

(x, y, z) -> x + y + z
# (anonymous function)

arr = rand(100)
# 100-element Array{Float64,1}:
#  0.0552819
#  0.319113
#  0.488649
#  ...

arr2 = map(x -> x^2, arr)
# 100-element Array{Float64,1}:
#  0.00305609
#  0.101833
#  0.238778

arr2 = [x^2 for x in arr]
# 100-element Array{Any,1}:
#  0.00305609
#  0.101833
#  0.238778

isodd(n) = (n % 2 != 0)

filter(isodd, round(Int64, rand(100) * 100))
# 50-element Array{Int64,1}:
#  57
#  99
#  23

filter(n -> n %2 != 0, round(Int64, rand(100) * 100))

map(collect(1:100)) do x
        7x^2 - 3.14x + sin(x) + 4
end
# 100-element Array{Float64,1}:
#      8.70147
#     26.6293
#     57.7211
#    102.683
# ...


"""
Generate a Ricker Wavelet. Reference:
Frequencies of the Ricker wavelet, Yanghua Wang, GEOPHYSICS, VOL. 80, NO. 2
Bandwidth is roughly 1.2 * fpeak for ricker

# Keyword Arguments

* `fqdom::Float64`: dominant frequency
* `tgrid`: time-domain grid
* `tpeak::Float64=tgrid[1]+1.5/fqdom`: the peak of the ricker in time (has a default)
* `tpeak_rand_flag` : randomly place peak of ricker
"""
function ricker(fqdom::Float64,
		tgrid::StepRangeLen;
		tpeak::Float64=tgrid[1]+1.5/fqdom, # using approximate half width of ricker
		attrib::AbstractString="",
		trim_tol::Float64=0.0,
		maxamp::Float64=1.0,
		tpeak_rand_flag=false,
		maxamp_rand_flag=false,
		phase_rand_flag=false,
		)
	(tpeak < tgrid[1]+1.5/fqdom) && error("cannot output Ricker for given tgrid and tpeak")
	(tpeak > tgrid[end]-1.5/fqdom) && error("cannot output Ricker for given tgrid and tpeak")

	if(tpeak_rand_flag)
		tpeak=rand(Uniform(tgrid[1]+1.5/fqdom,tgrid[end]-1.5/fqdom))
	end
	if(maxamp_rand_flag)
		maxamp=rand(Uniform(-1,1))
	end
	isapprox(fqdom,0.0) && error("dominant frequency cannot be zero")

	#! some constants
	pf = (π*π)*(fqdom^2.0)
	nt = length(tgrid)
	δt = step(tgrid)

	# a vector is odd number of samples (nt + 1 corresponds to time zero)
	wav = zero(tgrid);
	# k = (1 - 2* pf * t^2) * Exp[-pf *t^2]
	# Simplify[D[k,t]]
	# FortranForm[Simplify[D[k,t]]]
	if(occursin("[DIFF]",attrib))
			# ricker after a time derivative
			for it = 1:nt
				tsquare = (tgrid[it]-tpeak) * (tgrid[it]-tpeak)
				t       = -1.0 * (tgrid[it]-tpeak)
				wav[it] = (2.0 * pf * t * (-3.0 + 2.0 * pf * tsquare)) * exp(-1.0 * pf * tsquare)
			end
	else
	#! ricker wavelet
		for it = 1:nt
			tsquare = (tgrid[it]-tpeak) * (tgrid[it]-tpeak)
			wav[it] = (1.0 - 2.0 * pf * tsquare) * exp(-1.0e0 * pf * tsquare) * maxamp
		end
	end

	isapprox(maximum(abs.(wav)),0.0) && warn("wavelet is zeros")

	# apply random phase to the ricker wavelet
	if(phase_rand_flag)
		wav=apply_rand_phase(wav)
	end

	if(trim_tol != 0.0)
		return wav[abs(wav).>=trim_tol]
	else
		return wav
	end
end

"""
ormbsy wavelet

* `tperc::Float64=0.0` : the wavelet is tapered in time using this percentage value
"""
function  ormsby(
		fqdom,
		tgrid::StepRangeLen;
		fracbandwidth::Float64=1.2, # using same value as Ricker
		f1::Float64=fqdom-fracbandwidth*0.5*fqdom,
		f4::Float64=fqdom+fracbandwidth*0.5*fqdom,
		f2::Float64=0.25*f4+0.75*f1,
		f3::Float64=0.25*f1+0.75*f4,
		tpeak::Float64=tgrid[1]+1.5/(0.25*(f1+f4)), # using approximate half width
		trim_tol::Float64=0.0,
		tperc::Float64=0.0,
		tpeak_rand_flag=false,
		maxamp_rand_flag=false,
		phase_rand_flag=false,
		)

	# some constants
	A43 = (pi*f4)^2 / (pi*f4 - pi*f3);
	A34 = (pi*f3)^2 / (pi*f4 - pi*f3);
	A21 = (pi*f2)^2 / (pi*f2 - pi*f1);
	A12 = (pi*f1)^2 / (pi*f2 - pi*f1);

	wav = zeros(length(tgrid));

	if(tpeak_rand_flag)
		tpeak=rand(Uniform(tgrid[1]+1.5/(0.25*(f1+f4)),tgrid[end]-1.5/(0.25*(f1+f4))))
	end

	# ormsby wavelet
	for it = 1: length(tgrid)
		t = tgrid[it]-tpeak
		S4 = (sinc(pi*f4*t))^2
		S3 = (sinc(pi*f3*t))^2
		S2 = (sinc(pi*f2*t))^2
		S1 = (sinc(pi*f1*t))^2

		wav[it] =  ((A43 * S4 - A34 * S3) - (A21 * S2 - A12 * S1))
	end

	isapprox(maximum(abs.(wav)),0.0) && warn("wavelet is zeros")

	# normalize
	wav /= maximum(abs.(wav));

	if(maxamp_rand_flag)
		wav *= rand(Uniform(-1,1))
	end

	# apply random phase to the ricker wavelet
	if(phase_rand_flag)
		wav=apply_rand_phase(wav)
	end

	if(trim_tol != 0.0)
		wav = wav[abs(wav).>=trim_tol]
	end

	wav = wav .* Utils.taper(ones(length(wav)),tperc)
	return wav

end

function apply_rand_phase(wav)
	nt=length(wav)
	W=FFTW.rfft(wav);
	θ=rand(Uniform(-Float64(pi),Float64(pi)))
	for i in eachindex(W)
		W[i]=W[i]*complex(cos(θ),sin(θ))
	end
	wav=FFTW.irfft(W,nt)
end

"""
Return a ricker wavelet for a given input `Medium`.

# Arguments
* `mod::Medium` : Medium
* `nλ::Int64=10` : number of wavelengths (P-wave) in the medium
* `tmaxfrac::Float64=1.0` : by default the maximum modelling time is computed using the average velocity and the diagonal distance of the medium,
use this fraction to increase of reduce the maximum time

# Keyword Arguments
* all the keywords arguments of the `ricker` method can be used.
"""
function ricker(mod::Medium, nλ::Int=10, tmaxfrac::Real=1.0, epsilon=inv(sqrt(ndims(mod))); args... )
	@assert(!iszero(mod))
	fqdom, tgrid = get_fqdom_tgrid(mod, nλ, tmaxfrac, epsilon)
	wav=ricker(fqdom, tgrid; args...)
	return wav, tgrid
end

"""
Same as ricker, but return ormsby...
"""
function ormsby(mod::Medium, nλ::Int=10, tmaxfrac::Real=1.0, epsilon=inv(sqrt(ndims(mod))); args... )
	@assert(!iszero(mod))
	fqdom, tgrid = get_fqdom_tgrid(mod, nλ, tmaxfrac, epsilon)
	wav=ormsby(fqdom, tgrid; args...)
	return wav, tgrid
end

"""
Return dominant source frequency, and its temporal grid for a finite-difference simulation, for given number of wavelengths `nλ` in the medium.
The model has `nλ` wavelengths, and the maximum modeling time is determined by `tmaxfrac`.
`epsilon` is Courant number.
"""
function get_fqdom_tgrid( mod::Medium, nλ::Int, tmaxfrac::Real, epsilon)

	# maximum distance (diagnol) the wave travels
	d = sqrt(sum([(m[1]-m[end])^2 for m in mod.mgrid]))

	# dominant wavelength using mod dimensions
	λdom=d*inv(real(nλ))

	# average P velocity
	vavg=mod.ref[:vp]

	fqdom = vavg/λdom

	# use two-way maximum distance to get tmax
	tmax=2.0*d*inv(vavg)*tmaxfrac

	# choose sampling interval to obey max freq of source wavelet
	δmin = minimum(step.(mod.mgrid))
	vmax=try
		sqrt(mod.bounds[:vp][2]^2 + mod.bounds[:vs][2]^2) # see Virieux (1986)
	catch
		mod.bounds[:vp][2]
	end
	δt=epsilon*δmin/vmax

	# check if δt is reasonable
	#(δt > 0.1/fqdom) : error("decrease spatial sampling or nλ")

	tgrid=range(0.0, stop=tmax, step=δt)

	return fqdom, tgrid
end


"""
     fdhinfminus(sys,freq) -> (β, ind, fr)

Compute for a stable descriptor system `sys = (A-λE,B,C,D)` the `H∞-` index `β` of its
transfer function matrix `G(λ)`. If `freq = missing` (default), then `β` is the
minimum `H∞-norm` of the columns of `G`, `ind` is the index of the minimum-norm column and `fr` is
the frequency where the minimum `H∞-norm` of the columns is achieved. If `freq` is a real value or
a real vector of frequency values, then `β` is the minimum of the 2-norms of the columns of the
frequency responses of `G` evaluated for all values contained in `freq`, `ind` is the index of column
for which the minimum is achieved and `fr` is the corresponding frequency.
"""
function fdhinfminus(sys::DescriptorStateSpace{T}, freq::Union{AbstractVector{<:Real},Real,Missing} = missing) where T
   p, m = size(sys)

   m == 0 && (return T[], Int[], T[])

   isstable(sys) || error("the system is unstable")
   β = Inf; ind = 0; f = -1;
   if ismissing(freq)
      # evaluate β as the minimum of H-infinity norms of the columns of G
      for j = 1:m
         temp, fr = ghinfnorm(sys[:,j])
         β <= temp || (β = temp; ind = j; f = fr)
      end
   else
      # evaluate β as the minimum of the norms of columns of the frequency
      # responses of G evaluated over all frequencies contained in FREQ
      if !isa(freq, Vector)
         # use evalfr if only one frequency is present
         H = evalfr(sys; fval = freq)
         for j = 1:m
             temp = norm(view(H,:,j))
             β <= temp || (β = temp; ind = j)
         end
         f = freq
      else
         T1 = T <: BlasFloat ? T : promote_type(Float64,T)
         ONE = one(T1)
         a, e, b, c, d = dssdata(T1,sys)
         Ts = abs(sys.Ts)
         disc = !iszero(Ts)
         sw = disc ? im*Ts : im
         desc = !(e == I)
         # Determine the complex Hessenberg-form system to be used for efficient
         # frequency response computation.
         ac, ec, bc, cc, dc = chess(a, e, b, c, d)
         H = similar(dc, eltype(dc), p, m)
         bct = similar(bc)
         for i = 1:length(freq)
             if isinf(freq[i])
                # exceptional call to evalfr
                H = evalfr(sys, fval=Inf)
             else
                copyto!(H, dc)
                copyto!(bct, bc)
                w = disc ? -exp(sw*freq[i]) : -sw*freq[i]
                desc ? ldiv!(UpperHessenberg(ac+w*ec),bct) : ldiv!(ac,bct,shift = w)
                mul!(H, cc, bct, -ONE, ONE)
             end
             for j = 1:m
                 temp = norm(view(H,:,j))
                 β <= temp || (β = temp; ind = j; f = freq[i])
             end
         end
      end
   end
   return β, ind, f
end
"""
     fdhinfmax(sys,freq) -> (γ, ind, fr)

Compute for a descriptor system `sys = (A-λE,B,C,D)`, `γ` - the maximum norm of the columns of its
transfer function matrix `G(λ)`. If `freq = missing` (default), then `γ` is the
maximum `H∞-norm` of the columns of `G`, `ind` is the index of the maximum-norm column and `fr` is
the frequency where the maximum `H∞-norm` of the columns is achieved. If `freq` is a real value or
a real vector of frequency values, then `γ` is the maximum of the 2-norms of the columns of the
frequency responses of `G` evaluated for all values contained in `freq`, `ind` is the index of column
for which the maximum is achieved and `fr` is the corresponding frequency.
"""
function fdhinfmax(sys::DescriptorStateSpace{T}, freq::Union{AbstractVector{<:Real},Real,Missing} = missing) where T
   p, m = size(sys)

   m == 0 && (return T[], Int[], T[])

   γ = 0; ind = 0; f = -1;
   if ismissing(freq)
      # evaluate γ as the maximum of H-infinity norms of the columns of G
      for j = 1:m
         temp, fr = ghinfnorm(sys[:,j])
         γ >= temp || (γ = temp; ind = j; f = fr)
      end
   else
      # evaluate γ as the maximum of the norms of columns of the frequency
      # responses of G evaluated over all frequencies contained in FREQ
      if !isa(freq, Vector)
         # use evalfr if only one frequency is present
         H = evalfr(sys; fval = freq)
         for j = 1:m
             temp = norm(view(H,:,j))
             γ >= temp || (γ = temp; ind = j)
         end
         f = freq
      else
         T1 = T <: BlasFloat ? T : promote_type(Float64,T)
         ONE = one(T1)
         a, e, b, c, d = dssdata(T1,sys)
         Ts = abs(sys.Ts)
         disc = !iszero(Ts)
         sw = disc ? im*Ts : im
         desc = !(e == I)
         # Determine the complex Hessenberg-form system to be used for efficient
         # frequency response computation.
         ac, ec, bc, cc, dc = chess(a, e, b, c, d)
         H = similar(dc, eltype(dc), p, m)
         bct = similar(bc)
         for i = 1:length(freq)
             if isinf(freq[i])
                # exceptional call to evalfr
                H = evalfr(sys, fval=Inf)
             else
                copyto!(H, dc)
                copyto!(bct, bc)
                w = disc ? -exp(sw*freq[i]) : -sw*freq[i]
                desc ? ldiv!(UpperHessenberg(ac+w*ec),bct) : ldiv!(ac,bct,shift = w)
                mul!(H, cc, bct, -ONE, ONE)
             end
             for j = 1:m
                 temp = norm(view(H,:,j))
                 γ >= temp || (γ = temp; ind = j; f = freq[i])
             end
         end
      end
   end
   return γ, ind, f
end
function binmat2dec(S::Union{BitArray,VecOrMat{<:Number}})
# Convert a binary matrix with zero/non-zero elements to an equivalent decimal number integer vector
   len = size(S,2)
   n = Int.(S[:,1] .!= 0)
   for i = 2:len
       n .= 2*n .+ Int.(S[:,i] .!= 0)
   end
   return n
end
function dec2binmat(n::Union{Int,Vector{Int}}, numbits::Int = 1)
# Convert a decimal non-negative integer or integer vector with decimal non-negative values
# to their binary matrix representation with `numbits` bits.
   minimum(n) < 0 && error("only non-negative values allowed")
   maxn = maximum(n)
   len = maxn == 0 ? max(1,numbits) : max(Int(floor(log2(maxn)+1)),numbits)
   ns = size(n,1)
   v = isa(n,Int) ? string.([n], base = 2, pad = len) : string.(n, base = 2, pad = len)
   S = falses(ns,len)
   for i = 1:ns
       for j = 1:len
           S[i,j] = parse(Int,v[i][j]) == 1
       end
   end
   return S
end
"""
    S = fditspec_(sysrf::DescriptorStateSpace; FDfreq = missing, block = false, poleshift = false,
                 FDtol, FDStol, atol = 0, atol1 = atol, atol2 = atol, rtol, fast = true)

Compute the weak or strong binary structure matrix `S` of the transfer function matrix of a
linear time-invariant system `sysrf`
(typically representing the transfer channel from the fault inputs to residuals).
`sysrf` has a descriptor system realization of the form `sysrf = (Af-lambda*Ef,Bf,Cf,Df)`
with a  `q x mf` transfer function matrix `Rf(λ)`.
For the description of keyword parameters see the documentation of [`fditspec`](@ref).
"""
function fditspec_(sysrf::DescriptorStateSpace{T}; FDfreq::Union{AbstractVector{<:Real},Real,Missing} = missing,
                  block::Bool = false, poleshift::Bool = false, FDtol::Real = 0., FDStol::Real = 0.,
                  atol::Real = zero(float(real(T))), atol1::Real = atol, atol2::Real = atol,
                  rtol::Real =  (size(sysrf.A,1)+1)*eps(float(one(real(T))))*iszero(max(atol1,atol2)), fast::Bool = true) where T
   p, mf = size(sysrf)
   n = order(sysrf)
   q = block ? 1 : p  # number of rows of S
   Sstrong = !ismissing(FDfreq)
   if Sstrong
      isa(FDfreq,Vector) || (FDfreq = [FDfreq])
      lfreq = length(FDfreq);
      w = im*FDfreq;                   # w = j*freq
      Ts = abs(sysrf.Ts);
      Ts > 0 && ( w = exp(Ts*w))     # w = exp(j*Ts*freq)
      S = falses(q,mf,lfreq)
   else
      S = falses(q,mf)
   end
   a, e, b, c, d = dssdata(sysrf)
   standard = isequal(e,I)
   if n == 0
      S1 =  abs.(d) .> (FDtol <= 0 ? 0.0001*max(1., norm(d,1)) : FDtol)
      S = block ? maximum(S1, dims=1) : S1
      return Sstrong ? repeat(S,1,1,lfreq) : S
   end
   if Sstrong
      #FDStol <= 0. && (FDStol = 0.0001*max(1., norm(a,1), norm(b,1), norm(c,Inf), norm(d,1), standard ? 0 : norm(e,1)))
      FDStol <= 0. && (FDStol = eps(max(1., norm(a,1), norm(b,1), norm(c,Inf), norm(d,1), standard ? 0 : norm(e,1))))
      S = trues(q, mf, lfreq)
      p1, n1 = size(c,1), size(c,2)
      # employ structural analysis to compute weak/strong structure matrix
      ispole = false
      for k = 1:lfreq
         # check if freq(k) is a pole
         ispole = isinf(FDfreq[k]) ? (e == I ? false : rank(e) < n1) : rank(a-w[k]*e) < n1
         ispole || break
      end
      ispole && !poleshift && error("sysf has poles in Ω")
      if block
         ispole && (f = rand(T,n1,p1); mul!(a,f,c,1,1); mul!(b,f,d,1,1) )
         for j = 1:mf
             # elliminate uncontrollable and unobservable eigenvalues for the j-th column of B
             a1, e1, b1, c1, d1 = lsminreal(a, e, view(b,:,j), c, view(d,:,j); noseig = false, atol1, atol2, rtol, fast)
             for k = 1:lfreq
                # check if freq(k) is a zero of the j-th column
                s = isinf(FDfreq[k]) ? svdvals!([e1 b1; c1 d1]) : svdvals!([a1-w[k]*e1 b1; c1 d1])
                S[1, j, k] = (s[end] > FDStol)
            end
         end
      else
         for i = 1:p
             # elliminate unobservable eigenvalues for the i-th row of C
             a1, e1, b1, c1, d1 = lsminreal(a, e, b, view(c,i:i,:), view(d,i:i,:); contr = false, noseig = false, atol1, atol2, rtol, fast)
             n1 = size(a1,1)
             ispole && (f1 = rand(T,n1,1); mul!(a1,f1,c1,1,1); mul!(b1,f1,d1,1,1) )
             for j = 1:mf
                 # elliminate uncontrollable and non-dynamic eigenvalues for the j-th column of B
                 a2, e2, b2, c2, d2 = lsminreal(a1, e1, view(b1,:,j:j), c1, view(d1,:,j:j); obs = false, noseig = true, atol1, atol2, rtol, fast)
                 for k = 1:lfreq
                    # check if freq(k) is a zero of the (i,j)-th element
                    s = isinf(FDfreq[k]) ? svdvals!([e2 b2; c2 d2]) : svdvals!([a2-w[k]*e2 b2; c2 d2])
                    S[i,j,k] = s[end] > FDStol
                 end
             end
         end
      end
   else
      FDtol <= 0. && (FDtol = 0.0001*max(1., norm(b,1), norm(d,1)))
      standard && all(abs.(d) .> FDtol) && (return trues(q,mf))
      S = falses(q, mf)
      if block
         for j = 1:mf
             # compute minimal realization of the j-th column of Rf
             _, _, b1, _, d1 = lsminreal(a, e, view(b,:,j:j), c, view(d,:,j:j); atol1, atol2, rtol, fast)
             S[1,j] |=  (any(abs.(view(d1,:,1)) .> FDtol) || any(abs.(view(b1,:,1)) .> FDtol))
         end
      else
         for j = 1:mf
             # elliminate uncontrollable eigenvalues for the j-th column of B
             a1, e1, b1, c1, d1 = lsminreal(a, e, view(b,:,j), c, view(d,:,j); obs = false, noseig = false, atol1, atol2, rtol, fast)
             for i = 1:p
                # elliminate unobservable and non-dynamic eigenvalues for the (i,j)-th element
                _, _, b2, _, d2 = lsminreal(a1, e1, b1, view(c1,i:i,:),view(d1,i:i,:); contr = false, noseig = true, atol1, atol2, rtol, fast)
                S[i,j] |=  (abs(d2[1,1]) > FDtol || any(abs.(view(b2,:,1)) .> FDtol))
             end
         end
      end
   end
   return S
end
"""
     fdisspec_(sysrf::DescriptorStateSpace, freq; block = false, stabilize = false, FDGainTol = 0.01,
                     atol, atol1, atol2, atol3, rtol, fast = true) -> (S, gains)

Compute the strong binary structure matrix `S` of the transfer function matrix of a
linear time-invariant system `sysrf`
(typically representing the transfer channel from the fault inputs to residuals).
`sysrf` has a descriptor system realization of the form `sysrf = (Af-lambda*Ef,Bf,Cf,Df)`
with a  `q x mf` transfer function matrix `Rf(λ)`.
For the description of keyword parameters see the documentation of [`fdisspec`](@ref).
"""
function fdisspec_(sysrf::DescriptorStateSpace{T}, freq::Union{AbstractVector{<:Real},Real} = 0; stabilize::Bool = false, FDGainTol::Real = 0.01, block::Bool = false,
                   atol::Real = zero(float(real(T))), atol1::Real = atol, atol2::Real = atol, atol3::Real = atol,
                   rtol::Real =  ((max(size(sysrf.A)...))+1)*eps(float(one(real(T))))*iszero(max(atol1,atol2)), fast::Bool = true) where T
   p, mf = size(sysrf)
   isa(freq,Vector) || (freq = [freq])
   lfreq = length(freq);
   w = im*freq;                   # w = j*freq
   Ts = abs(sysrf.Ts);
   Ts > 0 && ( w = exp(Ts*w))     # w = exp(j*Ts*freq)

   ispole = false
   n = size(sysrf.A,1)
   for k = 1:lfreq
      # check if freq(k) is a pole
      ispole = isinf(freq[k]) ? (sysrf.E == I ? false : rank(sysrf.E) < n) : rank(sysrf.A-w[k]*sysrf.E) < n
      ispole || break
   end
   ispole && !stabilize && error("sysf has poles in Ω")
   if block
      stabilize && (sysrf = glcf(sysrf; atol1, atol2, atol3, rtol)[1])
      # gs = evalfr(sysrf, w[1]; atol1, atol2, rtol, fast)
      # any(isinf.(gs)) && error("fdisspec:pole - the frequency $(w[1]) is a system pole")
      smat = falses(1, mf, lfreq)
      gains = zeros(T, 1, mf, lfreq)
      for i = 1:lfreq
          gs = evalfr(sysrf, w[i]; atol1, atol2, rtol, fast)
         any(isinf.(gs)) && error("fdisspec_:pole - the frequency $(w[i]) is a system pole")
          for j = 1:mf
              gsj = norm(view(gs,:,j))
              gains[1,j,i] = gsj
              smat[1,j,i] = (gsj .> FDGainTol)
          end
      end
   else
      smat = falses(p, mf, lfreq)
      gains = zeros(T, p, mf, lfreq)
      for k = 1:p
         t = gir(sysrf[k,:]; contr = false, atol1, atol2, rtol)
         stabilize && (t = glcf(t; atol1, atol2, atol3, rtol)[1])
         for i = 1:lfreq
            #  gs = stabilize ? abs.(evalfr(glcf(t; atol1, atol2, atol3, rtol)[1], w[i]; atol1, atol2, rtol, fast)) :
            #                   abs.(evalfr(t, w[i]; atol1, atol2, rtol, fast))
             gs = abs.(evalfr(t, w[i]; atol1, atol2, rtol, fast))
             any(isinf.(gs)) && error("fdisspec_:pole - the frequency $(w[i]) is a system pole")
             gains[k,:,i] = gs
             smat[k,:,i] = (gs .> FDGainTol)
         end
      end
   end
   return smat, gains
end
"""
     fdiscond_(sysrf::DescriptorStateSpace, freq) -> (scond, β, γ)

Compute for a stable descriptor system `sysrf = (A-λE,B,C,D)` with the transfer function matrix `Rf(λ)`,
`β` - the H∞- index of `Rf(λ)`, `γ` - the maximum of the columns norms of `Rf(λ)` and
`scond` - the column-gains sensitivity condition evaluated as `scond := β/γ`.
If `freq` is a vector of real frequency values, then `β` and `γ`
are evaluated over the frequencies contained in `freq`.
"""
function fdiscond_(sysrf::DescriptorStateSpace{T}, freq::Union{AbstractVector{<:Real},Real,Missing} = missing) where T
   p, m = size(sysrf)

   m == 0 && (return T[], T[], T[])

   isstable(sysrf) || error("the system is unstable")

   β = Inf
   γ = 0
   if ismissing(freq)
      # evaluate β and γ as the minimum and maximum of H-infinity norms of the columns of G
      for j = 1:m
         temp = ghinfnorm(sysrf[:,j])[1]
         γ >= temp || (γ = temp)
         β <= temp || (β = temp)
      end
   else
      # evaluate β and γ as the minimum and maximum of the norms of columns of the frequency
      # responses of G evaluated over all frequencies contained in FREQ
      if !isa(freq, Vector)
         # use evalfr if only one frequency is present
         H = evalfr(sysrf; fval = freq)
         for j = 1:m
             temp = norm(view(H,:,j))
             γ >= temp || (γ = temp)
             β <= temp || (β = temp)
         end
      else
         T1 = T <: BlasFloat ? T : promote_type(Float64,T)
         ONE = one(T1)
         a, e, b, c, d = dssdata(T1,sysrf)
         Ts = abs(sysrf.Ts)
         disc = !iszero(Ts)
         sw = disc ? im*Ts : im
         desc = !(e == I)
         # Determine the complex Hessenberg-form system to be used for efficient
         # frequency response computation.
         ac, ec, bc, cc, dc = chess(a, e, b, c, d)
         H = similar(dc, eltype(dc), p, m)
         bct = similar(bc)
         for i = 1:length(freq)
             if isinf(freq[i])
                # exceptional call to evalfr
                H = evalfr(sys, fval=Inf)
             else
                copyto!(H, dc)
                copyto!(bct, bc)
                w = disc ? -exp(sw*freq[i]) : -sw*freq[i]
                desc ? ldiv!(UpperHessenberg(ac+w*ec),bct) : ldiv!(ac,bct,shift = w)
                mul!(H, cc, bct, -ONE, ONE)
             end
             for j = 1:m
                 temp = norm(view(H,:,j))
                 γ >= temp || (γ = temp)
                 β <= temp || (β = temp)
             end
         end
      end
   end
   return β/γ, β, γ
end


"""
The inverse logit function, which is also the CDF of the Logistic(0, 1)
distribution. Note that this function will generate an exact value of 0 when
z <= -710 and will generate an exact value of 1 when z >= 37. This introduces
numeric problems if you provide inputs outside of that range.
"""
@inline invlogit(z::Real) = 1 / (1 + exp(-z))

"""
The logit function, which is also the quantile function of the Logistic(0, 1)
distribution.
"""
@inline logit(p::Real) = log(p / (1 - p))


# Bifurcation diagram for 1D climate model
using DrWatson
@quickactivate "NonlinearDynamicsTextbook"
include(srcdir("style.jl"))
using DynamicalSystems, PyPlot

include(srcdir("simple_continuation.jl"))

using ForwardDiff, PyPlot
αtan(T) = 0.5 - 0.2*tanh((T-263)/4)
dTdt(T, ε = 0.65, α=αtan, s= 1.0) = s*(1 - α(T)) - 1.6e-10 * ε * T^4
dTdt(T; ε = 0.65, α=αtan, s = 1.0) = dTdt(T, ε, α, s)
d²Tdt²(T, ε) = ForwardDiff.derivative(T -> dTdt(T, ε), T)
climate_f(u, p, t) = SVector(dTdt(u[1], p))
climate_J(u, p, t) = SMatrix{1,1}(d²Tdt²(u[1], p))

u0 = SVector(225.0)
p0 = 0.9
ds = ContinuousDynamicalSystem(climate_f, u0, p0, climate_J)

f, J = bifurcation_rule_form(ds)

pmin = 0.3
pmax = 1

dp0 = 0.05
dx0 = SVector(0.1)

xs, ps, stability = continuation(f, J, u0, p0;
    pmin, pmax, dp0, dx0, N = 1500
)

xs = [x[1] for x in xs]

# %% make figure
stable1 = findfirst(!, stability)
unstable1 = findlast(!, stability)

fig = figure(figsize = (figx/2, figy))
ax = gca()

plot(ps[1:stable1], xs[1:stable1]; c = "C2", ls = "solid", label="stable")
plot(ps[stable1+1:unstable1], xs[stable1+1:unstable1]; c = "C2", ls = "dashed", label="unstable")
plot(ps[unstable1+1:end], xs[unstable1+1:end]; c = "C2", ls = "solid")

# scatter bifurcation points
ax.plot([ps[stable1], ps[unstable1]], [xs[stable1], xs[unstable1]], ls = "None",
		marker="o", ms = 12, color = "k", mew = 1, mec = "k",
        fillstyle = "top", markerfacecoloralt= "w"
)

# plot arrows
_a = 300
arrow1ε = ps[_a:stable1] .- 0.03
arrow1T = xs[_a:stable1] .- 5
plot(arrow1ε, arrow1T; color = "C1", lw = 4.0)
arrow(arrow1ε[end], arrow1T[end], 0, 50;
width = 0.004, head_length = 5.0, color = "C1", zorder = 99)

_b = 200
arrow2ε = ps[unstable1:unstable1+_b] .+ 0.03
arrow2T = xs[unstable1:unstable1+_b] .+ 5

plot(arrow2ε, arrow2T; color = "C0", lw = 4.0)
arrow(arrow2ε[1], arrow2T[1], 0, -50;
width = 0.004, head_length = 5.0, color = "C0", zorder = 99)

# scatter(ps[stability], xs[stability]; c = "C3", legend = "stable")
# scatter(ps[stability], xs[stability]; c = "C3", legend = "stable")
legend()
xticks(0.3:0.2:0.9)
yticks(200:50:350)
ylabel("\$T^*\$", labelpad = -20)
xlabel("\$\\varepsilon\$"; labelpad=-20)
# yticks(230:50:310)
# ylim(210, 320)
# ax.xaxis.set_label_coords(0.4, -0.025)
fig.tight_layout(pad=0.3)
wsave(plotsdir("4", "bif_example"), fig)


# MaxPlus Semiring Implementation
# MaxPlus is over real numbers with max as addition and + as multiplication
# The zero element is -inf and the one element is 0

struct MaxPlusSemiringElement{T<:Real} <: AbstractSemiringElement{T}
    val::T
end

# interface methods
val(x::MaxPlusSemiringElement) = x.val
+(l::MaxPlusSemiringElement, r::MaxPlusSemiringElement) = MaxPlusSemiringElement(max(l.val, r.val))
*(l::MaxPlusSemiringElement, r::MaxPlusSemiringElement) = MaxPlusSemiringElement(l.val+r.val)
zero(::Type{MaxPlusSemiringElement{T}}) where T = MaxPlusSemiringElement(-Inf)
one(::Type{MaxPlusSemiringElement{T}}) where T = MaxPlusSemiringElement{T}(0)

iscommutative(::Type{<:MaxPlusSemiringElement}) = true
isidempotent(::Type{<:MaxPlusSemiringElement}) = true
isstar(::Type{<:MaxPlusSemiringElement}) = true

# MinPlus Semiring Implementation
# MinPlus is over real numbers with min as addition and + as multiplication
# The zero element is +inf and the one element is 0

struct MinPlusSemiringElement{T<:Real} <: AbstractSemiringElement{T}
    val::T
end

# interface methods
val(x::MinPlusSemiringElement) = x.val
+(l::MinPlusSemiringElement, r::MinPlusSemiringElement) = MinPlusSemiringElement(min(l.val, r.val))
*(l::MinPlusSemiringElement, r::MinPlusSemiringElement) = MinPlusSemiringElement(l.val+r.val)
zero(::Type{MinPlusSemiringElement{T}}) where T = MinPlusSemiringElement(Inf)
one(::Type{MinPlusSemiringElement{T}}) where T = MinPlusSemiringElement{T}(0)

iscommutative(::Type{<:MinPlusSemiringElement}) = true
isidempotent(::Type{<:MinPlusSemiringElement}) = true
isstar(::Type{<:MinPlusSemiringElement}) = true

# Real (or Probabilistic) Semiring Implementation
# The Real Semiring is over real numbers with + as addition and * as multiplication
# The zero element is 0 and the one element is 1

struct RealSemiringElement{T<:Real} <: AbstractSemiringElement{T}
    val::T
end

# interface methods
val(x::RealSemiringElement) = x.val
+(l::RealSemiringElement, r::RealSemiringElement) = RealSemiringElement(l.val+r.val)
*(l::RealSemiringElement, r::RealSemiringElement) = RealSemiringElement(l.val*r.val)
zero(::Type{RealSemiringElement{T}}) where T = RealSemiringElement(0)
one(::Type{RealSemiringElement{T}}) where T = RealSemiringElement(1)

iscommutative(::Type{<:RealSemiringElement}) = true
isstar(::Type{<:RealSemiringElement}) = true

# optional methods
-(x::RealSemiringElement) = RealSemiringElement(-x.val)
/(l::RealSemiringElement, r::RealSemiringElement) = RealSemiringElement(l.val/r.val)
function star(x::RealSemiringElement)
    0 <= x.val < 1 ? RealSemiringElement(1/(1-x.val)) : throw(DomainError("$x must be within [0, 1)"))
end

for op in (:conj,) # required for matrix inversion?
    @eval begin
        $op(x::RealSemiringElement) = RealSemiringElement($op(x.val))
    end
end

# Boolean Semiring Implementation
# The Boolean Semiring is over {True, False} with OR as addition and AND as multiplication
# The zero element is False and the one element is True

struct BooleanSemiringElement{Bool} <: AbstractSemiringElement{Bool}
    val::Bool
end

# interface methods
val(x::BooleanSemiringElement) = x.val
+(l::BooleanSemiringElement, r::BooleanSemiringElement) = BooleanSemiringElement(l.val | r.val)
*(l::BooleanSemiringElement, r::BooleanSemiringElement) = BooleanSemiringElement(l.val & r.val)
zero(::Type{BooleanSemiringElement{T}}) where T = BooleanSemiringElement{T}(false)
one(::Type{BooleanSemiringElement{T}}) where T = BooleanSemiringElement{T}(true)

iscommutative(::Type{<:BooleanSemiringElement}) = true
isidempotent(::Type{<:BooleanSemiringElement}) = true
isstar(::Type{<:BooleanSemiringElement}) = true

#optional methods
star(x::R) where R<:BooleanSemiringElement = one(R)


### A Pluto.jl notebook ###
# v0.11.14

using Markdown
using InteractiveUtils

# ╔═╡ eb68872e-01ee-11eb-22e4-b1f20b71faff
begin
	import Pkg
	Pkg.activate(mktempdir())
	Pkg.add("Combinatorics")
	using Combinatorics
end

# ╔═╡ 9a4603ea-01f2-11eb-00f0-591422bc7142
md" Given 20 people, what is the probability that,
among the 12 months in the year, there are 4
months containing exactly 2 birthdays and 4 containing exactly 3 birthdays?

(For simplicity, assume that it is equally like that a birthday falls in one of the 12 months.)
(from A First Course in Probablity - Sheldon Ross)

"

# ╔═╡ 0cfeea3c-01ee-11eb-26f1-f1815eccdd10
md"Create twenty birthday months"

# ╔═╡ 015393e0-01ee-11eb-2feb-d7db4236368a
months = rand(1:12,20)

# ╔═╡ 1a6a603e-01ee-11eb-39f6-efc09168a3ba
counts =  [sum(months.==i) for i=1:12]

# ╔═╡ 67c25428-0251-11eb-04b0-cb201bbefa6b
sum(counts)

# ╔═╡ 6f1890a6-01ee-11eb-1524-1da938de93f3
sum(counts.==2),sum(counts.==3)

# ╔═╡ af424780-01ee-11eb-3d5f-4f4ba6a372b4
sum(counts.==2)==4 & sum(counts.==3)==4

# ╔═╡ d8b5158e-01ee-11eb-393f-0d51ea1fc19b
function birthday_problem(n)
  success = 0
  for t=1:n
     months = rand(1:12,20)
     counts = [sum(months.==i) for i=1:12]
     success += sum(counts.==2) == 4  &&  sum(counts.==3)==4
   end
   success/n
end

# ╔═╡ dea5c880-01ee-11eb-066d-8105b2196548
birthday_problem(20_000_000)

# ╔═╡ d74e2646-01f2-11eb-2b25-158bf9419ecd
@elapsed birthday_problem(20_000_000)

# ╔═╡ 006a84ee-01ef-11eb-2db7-5792cb4d0198
multinomial(4,4,4) * multinomial(2,2,2,2,3,3,3,3) / 12.0^20

# ╔═╡ Cell order:
# ╟─9a4603ea-01f2-11eb-00f0-591422bc7142
# ╟─0cfeea3c-01ee-11eb-26f1-f1815eccdd10
# ╠═015393e0-01ee-11eb-2feb-d7db4236368a
# ╠═1a6a603e-01ee-11eb-39f6-efc09168a3ba
# ╠═67c25428-0251-11eb-04b0-cb201bbefa6b
# ╠═6f1890a6-01ee-11eb-1524-1da938de93f3
# ╠═af424780-01ee-11eb-3d5f-4f4ba6a372b4
# ╠═d8b5158e-01ee-11eb-393f-0d51ea1fc19b
# ╠═dea5c880-01ee-11eb-066d-8105b2196548
# ╠═d74e2646-01f2-11eb-2b25-158bf9419ecd
# ╠═eb68872e-01ee-11eb-22e4-b1f20b71faff
# ╠═006a84ee-01ef-11eb-2db7-5792cb4d0198


### A Pluto.jl notebook ###
# v0.12.18

using Markdown
using InteractiveUtils

# ╔═╡ d80342ae-522b-11eb-19c0-cf931fb4ae81
using Statistics

# ╔═╡ 9c2ef32a-522d-11eb-29e6-29da29e4dec7
using Plots

# ╔═╡ 88b47ee6-522d-11eb-1a98-ed4dd43dfd11
Base.@kwdef mutable struct SampleAvg
    t::Int = 0
    avg::Float64 = 0.0
end

# ╔═╡ 8c94a810-522d-11eb-2024-01ca19bc1984
function (s::SampleAvg)(x)
    s.t += 1
    s.avg += (x - s.avg) / s.t
    s.avg
end

# ╔═╡ 8fde026e-522d-11eb-2aa1-f3e28c238ea6
function run_once(b)
    rms = Float64[]
    distribution = randn(b)
    expectation = mean(distribution)
    sample_avg = SampleAvg()

    for i in 1:2*b
        avg = sample_avg(distribution[rand(1:b)])
        push!(rms, abs(avg - expectation))
    end
    rms
end

# ╔═╡ 969b48aa-522d-11eb-2039-61916ddf87d4
begin
	n_runs = 1000
	p = plot(legend=:topright)

	for b in [2, 10, 100, 1000]
		rms = mean(run_once(b) for _ in 1:n_runs)
		xs = (1:2*b) ./ b
		plot!(p, xs, rms, label="b=$b")
	end

	p
end

# ╔═╡ Cell order:
# ╠═d80342ae-522b-11eb-19c0-cf931fb4ae81
# ╠═88b47ee6-522d-11eb-1a98-ed4dd43dfd11
# ╠═8c94a810-522d-11eb-2024-01ca19bc1984
# ╠═8fde026e-522d-11eb-2aa1-f3e28c238ea6
# ╠═9c2ef32a-522d-11eb-29e6-29da29e4dec7
# ╠═969b48aa-522d-11eb-2039-61916ddf87d4


# # Surface-grid operations

#md # ```@meta
#md # CurrentModule = ImmersedLayers
#md # ```

#=
Here, we will discuss the various surface-grid operators available in the package.
We will start by generating the cache, just as we did in [Immersed layer caches](@ref)
=#

using ImmersedLayers
using Plots
using LinearAlgebra

#=
### Set up the grid, shape, and cache
We do this just as we did in [Immersed layer caches](@ref)
=#
Δx = 0.01
Lx = 4.0
xlim = (-Lx/2,Lx/2)
ylim = (-Lx/2,Lx/2)
g = PhysicalGrid(xlim,ylim,Δx)
RadC = 1.0
Δs = 1.4*cellsize(g)
body = Circle(RadC,Δs)
cache = SurfaceScalarCache(body,g,scaling=GridScaling)

#=
## Basic regularization and interpolation
Let's perform an example in which we regularize the x coordinate of
the surface points onto the grid. We can get the surface coordinates
by using the [`points`](@ref) function. This can be applied either to
`body` directly or to the `cache`. As for any `VectorData` type, the components
of `pts` are `pts.u` and `pts.v`.
=#
pts = points(cache);

#=
Now, we set up some blank grid data on which to regularize onto
=#
gx = zeros_grid(cache);

#=
Now regularize. We will time it to show that it is fast and memory-efficient:
=#
regularize!(gx,pts.u,cache) #hide
@time regularize!(gx,pts.u,cache);

#=
Let's plot this to look at it. This also gives us a chance to highlight
the plot recipe for grid data associated with the cache, which is achieved by
simply supplying the cache to the `plot` function in `Plots.jl`. By default,
this plots the immersed points, as well, but this can be suppressed by
adding the keyword `layers=false`.
=#
plot(gx,cache)

#=
This shows how the regularization spreads the data over a couple of cells
around the surface. In the parlance of potential theory, this is a **single layer**.
=#
#=
If we wish to interpolate, then we do so with the [`interpolate!`]
function. For example, suppose we have a uniform field that we wish to interpolate:
=#
oc = 2.5*ones_grid(cache)

#=
Now set up some surface data to receive the interpolated data, and
interpolate:
=#
f = zeros_surface(cache);
interpolate!(f,oc,cache)

#=
It is clear that the interpolation preserves the value of the
field. This is also true for linearly varying fields, since the DDF
is built to ensure this. Let's try this, using the $x$ coordinate of the grid. Here, we use the `coordinates`
function of `CartesianGrids.jl`, which gets the coordinates of the grid,
and set the values of grid data to the $x$ coordinate. We interpolate and plot,
comparing to the actual $x$ coordinate of the points on the body:
=#
#x, y = coordinates(oc,g)
#xg = similar(oc)
#xg .= x
xg = x_grid(cache)
interpolate!(f,xg,cache)
plot(f,ylim=(-2,2),label="Interpolated from grid",ylabel="x",xlabel="Index")
plot!(pts.u,label="Actual body coordinate")

#=
## A double layer
Now we will generate a double layer. Mathematically, this takes the form

$$D_s f = \nabla\cdot \left( \delta(\chi) \mathbf{n} f \right)$$

for some scalar data $f$ on the surface. (See [Background](@ref) for an example.)
Notice that it maps scalar data on the surface ($f$) to scalar data in space. So to
calculate this using our discrete tools, we set up some grid data to receive the
result. Then, we use the function [`surface_divergence!`](@ref) to compute the
double layer. Here, we will demonstrate this on the $y$ coordinate of the
surface points:
=#
dl = zeros_grid(cache)
surface_divergence!(dl,pts.v,cache)
plot(dl,cache)

#=
If the surface data are vectors, $\mathbf{f}$, then this operation is a little
different:

$$D_s \mathbf{f} = \nabla\cdot \delta(\chi) \left(  \mathbf{n} \mathbf{f} + \mathbf{f} \mathbf{n} \right)$$

This maps $\mathbf{f}$ to a vector field. We use this in conjunction with a cache
generated with [`SurfaceVectorCache`](@ref).
=#

#=
The transpose of the double layer, $D_s$, is the operation

$$G_s u = \mathbf{n}\cdot \delta^{T}(\chi) \nabla u$$

for some scalar field data $u$. This operation computes the gradient of the field
data, interpolates this gradient onto the surface, and obtains the normal
component of that surface data. As such, it represents an important tool
for computing the normal derivative of field data on a surface. In the
package, we use [`surface_grad!`](@ref) for this operation.

The vector field version of this is

$$G_s \mathbf{u} = \mathbf{n}\cdot \delta^{T}(\chi) (\nabla \mathbf{u} + \nabla^{T} \mathbf{u})$$

which maps vector field data $\mathbf{u}$ to vector-valued surface data.
=#

#=
## A curl layer
We also sometimes need to take the curl of the regularized surface data,

$$C_s f = \nabla\times \left( \delta(\chi) \mathbf{n} f \right)$$

For this, we use the [`surface_curl!`](@ref) operator. Let's demonstrate this
on a uniform field on the surface.
=#
gc = zeros_gridcurl(cache)
f = ones_surface(cache)
surface_curl!(gc,f,cache)
plot(gc,cache)

#=
The continuous version of this operation is actually zero. It's not quite
zero in discrete form. However, its norm is much smaller than that of
the double layer.
=#
norm(gc,cache)/norm(dl,cache)

#=
Finally, a pair of operations that are used in support of the
previous ones, or occasionally on their own, are

$$R_n f = \delta(\chi)\mathbf{n}\circ f$$

for scalar surface data $f$, which maps to a vector field, effectively a
field of doublet strengths; and its transpose

$$R_n^T \mathbf{u} = \mathbf{n}\cdot \delta^{T}(\chi)\mathbf{u}$$

which maps vector field data $\mathbf{u}$ to a scalar surface field, the normal
component of the vector field on the surface. These are
provided by [`regularize_normal!`](@ref) and [`normal_interpolate!`](@ref),
respectively.
=#

#=
## Masks
Masks are grid data that take the value 1 in one region (e.g., the interior of a surface)
and 0 in the other (e.g., the exterior). The functions [`mask`](@ref)
and [`complementary_mask`](@ref) achieve this
=#
m = mask(cache)
cm = complementary_mask(cache)
plot(
    surface(m,cache,layers=false),
    surface(cm,cache,layers=false)
    )

#=
One can apply a mask to some grid data by multiplying it, using, e.g.,
the `product!` function in `CartesianGrids.jl`. Let's demonstrate that
with the grid data of $x$ coordinates:
=#
xmask = zeros_grid(cache)
xcmask = zeros_grid(cache)
product!(xmask,xg,m)
product!(xcmask,xg,cm)
plot(
  plot(xmask,cache),
  plot(xcmask,cache)
  )

#=
The mask and complementary mask effectively partition the field into two parts.
We can also apply masks in place, using [`mask!`](@ref) and [`complementary_mask!`](@ref):
=#
xmask .= xg
mask!(xmask,cache)
plot(xmask,cache)

#md # ## Surface-grid operator functions
#md # ```@docs
#md # regularize!
#md # interpolate!
#md # regularize_normal!
#md # normal_interpolate!
#md # regularize_normal_cross!
#md # normal_cross_interpolate!
#md # surface_divergence!
#md # surface_grad!
#md # surface_curl!
#md # surface_divergence_cross!
#md # surface_grad_cross!
#md # surface_curl_cross!
#md # mask!
#md # mask
#md # complementary_mask!
#md # complementary_mask
#md # ```


@model function bernoulli_mixture(x)
    # Mixture prior.
    w ~ Dirichlet(2, 1.0)

    # Latent probability.
    p ~ DiscreteNonParametric([0.3, 0.7], w)

    # Observation.
    x ~ Bernoulli(p)
end

bernoulli_example(x = false) = bernoulli_mixture(x)

@model function gmm(x, K)
    N = length(x)

    # Cluster centers.
    μ ~ filldist(Normal(), K)

    # Cluster association prior.
    w ~ Dirichlet(K, 1.0)

    # Cluster assignments.
    z ~ filldist(DiscreteNonParametric(1:K, w), N)

    # Observations.
    for n = 1:N
        x[n] ~ Normal(μ[z[n]], 1.0)
    end
end

gmm_example(x = [0.1, -0.05, 1.0], K = 2) = gmm(x, K)

@model function gmm_loopy(x, K, ::Type{T}=Float64) where {T<:Real}
    N = length(x)

    # Cluster centers.
    μ = Vector{T}(undef, K)
    for k = 1:K
        μ[k] ~ Normal()
    end

    # Cluster association prior.
    w ~ Dirichlet(K, 1.0)

    # Cluster assignments & observations.
    z = Vector{Int}(undef, N)
    for n = 1:N
        z[n] ~ DiscreteNonParametric(1:K, w)
        x[n] ~ Normal(μ[z[n]], 1.0)
    end
end

# same as gmm_loopy, but with an affine transformation on μ.
@model function gmm_shifted(x, K, ::Type{T}=Float64) where {T<:Real}
    N = length(x)

    # Cluster centers.
    μ = Vector{T}(undef, K)
    for k = 1:K
        μ[k] ~ Normal()
    end

    # Cluster association prior.
    w ~ Dirichlet(K, 1.0)

    # Cluster assignments & observations.
    z = Vector{Int}(undef, N)
    for n = 1:N
        z[n] ~ DiscreteNonParametric(1:K, w)
        x[n] ~ Normal(4μ[z[n]] - 1, 1.0)
    end
end

# K clusters, each one around i for i = 1:K with variance 0.5
@model function hmm(x, K, ::Type{T}=Float64) where {T<:Real}
    N = length(x)

    # State sequence.
    s = zeros(Int, N)

    # Emission matrix.
    m = Vector{T}(undef, K)

    # Transition matrix.
    T = Vector{Vector{T}}(undef, K)

    # Assign distributions to each element
    # of the transition matrix and the
    # emission matrix.
    for i = 1:K
        T[i] ~ Dirichlet(K, 1.0)
        m[i] ~ Normal(i, 0.5)
    end

    # Observe each point of the input.
    s[1] ~ Categorical(K)
    x[1] ~ Normal(m[s[1]], 0.1)

    for i = 2:N
        s[i] ~ Categorical(T[s[i-1]])
        x[i] ~ Normal(m[s[i]], 0.1)
    end
end

hmm_example(x = [1.1, 0.95, 2.2], K = 2) = hmm(x, K)

@model function changepoint(y)
    N = length(y)
    α = 1 / mean(y)
    λ₁ ~ Exponential(α)
    λ₂ ~ Exponential(α)
    τ ~ DiscreteUniform(1, N)

    for n in 1:N
        y[n] ~ Poisson(τ > n ? λ₁ : λ₂)
    end
end

# @model function reverse_deps(x)
#     m = Vector{Float64}(undef, 2)
#     m[1] ~ Normal()
#     m[2] ~ Normal()
#     x ~ MvNormal(m)
# end

###########################################################################
# data from R. Neal paper:
const data_neal = [-1.48, -1.40, -1.16, -1.08, -1.02, 0.14, 0.51, 0.53, 0.78]
const α_neal = 10.0

function stickbreak(v)
    K = length(v) + 1
    cumprod_one_minus_v = cumprod(1 .- v)

    return map(1:K) do k
        if k == 1
            v[1]
        elseif k == K
            cumprod_one_minus_v[K - 1]
        else
            v[k] * cumprod_one_minus_v[k - 1]
        end
    end
end

# from https://luiarthur.github.io/TuringBnpBenchmarks/dpsbgmm
@model function imm_stick(y, α, K)
    N = length(y)
    crm = DirichletProcess(α)

    v ~ filldist(StickBreakingProcess(crm), K - 1)
    w = stickbreak(v)

    # Cluster assignments
    z = zeros(Int, N)
    for n = 1:N
        z[n] ~ Categorical(w)
    end

    # Cluster centers
    L = identity(K)
    μ ~ filldist(Normal(), L)

    # Observations
    for n = 1:N
        y[n] ~ Normal(μ[z[n]], 1.0)
    end
end

imm_stick_example(y = data_neal, α = α_neal, K = 10) = imm_stick(y, α, K)


#=
Find nodes for triangular elements.
Use the book.
=#
include("triangle_quadrature_table.jl");

# Build global nodes for a triangular element with refel and vertices v
function triangle_element_nodes(refel, v)
    return  triangle_refel_to_xy(refel.r[:,1], refel.r[:,2], v);
end

# Set up refel nodal array
function triangle_refel_nodes!(refel)
    (eqx, eqy) = triangle_equilateral_nodes(refel.N);
    (r, s) = triangle_equilateral_to_rs(eqx,eqy);

    refel.r = zeros(refel.Np,2);
    refel.wr = zeros(refel.Np);
    #refel.g = zeros(refel.Np,2);
    #refel.wg = zeros(refel.Np);

    refel.r[:,1] = r;
    refel.r[:,2] = s;

    # quadrature nodes/weights from a table
    xyw = triangle_quadrature_nodes_weights(refel.N+1);
    refel.g = xyw[:,1:2];
    refel.wg = xyw[:,3];
    tol = 1e-8;
    tf1(x) = abs(x[1] + 1) < tol;
    tf2(x) = abs(x[2] + 1) < tol;
    tf3(x) = abs(x[1] + x[2]) < tol;
    refel.face2local = [get_face2local_map(refel.r, tf1),
                        get_face2local_map(refel.r, tf2),
                        get_face2local_map(refel.r, tf3)];

    ### Unfinished: find correct surface quadrature weights.
    ### For now just take average(correct for order=1).
    refel.surf_r = [refel.r[refel.face2local[1],:], refel.r[refel.face2local[2],:], refel.r[refel.face2local[3],:]];
    refel.surf_g = [refel.r[refel.face2local[1],:], refel.r[refel.face2local[2],:], refel.r[refel.face2local[3],:]];

    tmp = [length(refel.face2local[1]), length(refel.face2local[2]), length(refel.face2local[3])];
    refel.surf_wr = [ones(tmp[1])./tmp[1], ones(tmp[2])./tmp[2], ones(tmp[3])./tmp[3]];
    refel.surf_wg = refel.surf_wr;
end

# Purpose  : Compute (x,y) nodes in equilateral triangle for polynomial of order N
function triangle_equilateral_nodes(N)
    # optimal alpha values for N up to 16 (from book)
    alpopt = [0.0000 0.0000 1.4152 0.1001 0.2751 0.9800 1.0999 1.2832 1.3648 1.4773 1.4959 1.5743 1.5770 1.6223 1.6258];

    # Set optimized parameter, alpha, depending on order N
    if (N<16)
        alpha = alpopt[N];
    else
        alpha = 5/3;
    end

    # total number of nodes
    Np = Int64((N+1)*(N+2)/2);

    # Create equidistributed nodes on equilateral triangle
    L1 = zeros(Np);
    L2 = zeros(Np);
    L3 = zeros(Np);
    sk = 1;
    for n=1:(N+1)
        for m=1:(N+2-n)
            L1[sk] = (n-1)/N;
            L3[sk] = (m-1)/N;
            sk = sk+1;
        end
    end
    L2 = 1 .- L1 .- L3;
    x = -L2+L3;
    y = (-L2 - L3 + 2 .* L1) .* 0.5773502691896258; # 1/sqrt(3)=0.5773502691896258

    # Compute blending function at each node for each edge
    blend1 = 4 .* L2 .* L3;
    blend2 = 4 .* L1 .* L3;
    blend3 = 4 .* L1 .* L2;

    # Amount of warp for each node, for each edge
    warpf1 = triangle_warpfactor(N,L3-L2);
    warpf2 = triangle_warpfactor(N,L1-L3);
    warpf3 = triangle_warpfactor(N,L2-L1);

    # Combine blend & warp
    warp1 = blend1 .* warpf1 .* (1 .+ (alpha*L1).^2);
    warp2 = blend2 .* warpf2 .* (1 .+ (alpha*L2).^2);
    warp3 = blend3 .* warpf3 .* (1 .+ (alpha*L3).^2);

    # Accumulate deformations associated with each edge
    # x = x + 1*warp1 + cos(2*pi/3)*warp2 + cos(4*pi/3)*warp3;
    # y = y + 0*warp1 + sin(2*pi/3)*warp2 + sin(4*pi/3)*warp3;
    x = x + warp1 - 0.5*warp2 - 0.5*warp3;
    y = y + 0.8660254037844387*warp2 - 0.8660254037844387*warp3;

    return (x, y);
end

# Compute scaled warp function at order N based on rout interpolation nodes
function triangle_warpfactor(N, rout)
    # Compute LGL and equidistant node distribution
    #(LGLr, w) = jacobi_gauss_quad(0,0,N);
    (LGLr, w) = jacobi_LGL_quad(N);

    h = 2/N;
    req = -1:h:1;

    # Compute Vandermonde based on req
    Veq = zeros(N+1, N+1);
    for i=1:N+1
        Veq[:,i] = jacobi_polynomial(req, 0, 0, i-1);
    end

    # Evaluate Lagrange polynomial at rout
    Nr = length(rout);
    Pmat = zeros(N+1,Nr);
    for i=1:N+1
      Pmat[i,:] = jacobi_polynomial(rout, 0, 0, i-1);
    end
    Lmat = Veq'\Pmat;

    # Compute warp factor
    warp = Lmat'*(LGLr - req);

    # Apply Scale factor
    for i=1:length(rout)
        if abs(rout[i]) < 0.9999999999
            warp[i] = warp[i] / (1-rout[i]*rout[i]);
        else
            warp[i] = 0;
        end
    end

    return warp;
end

# From (x,y) in equilateral triangle to (r,s) coordinates in standard triangle
function triangle_equilateral_to_rs(x,y)
    # sqrt(3)=1.7320508075688772
    L1 = (1.7320508075688772 .* y .+ 1.0) ./ 3.0;
    L2 = (-3.0 .* x .- 1.7320508075688772 .* y .+ 2.0) ./ 6.0;
    L3 = ( 3.0 .* x .- 1.7320508075688772 .* y .+ 2.0) ./ 6.0;

    r = -L2 + L3 - L1;
    s = -L2 - L3 + L1;

    return (r, s);
end

# # From (r,s) coordinates in reference triangle to (x,y) in triangle with vertices v
# # v is a 2x3 array [x1 x2 x3; y1 y2 y3]
# function triangle_refel_to_xy(r, s, v)
#     x = 0.5 .* (-(r+s) .* v[1,1] .+ (1+r) .* v[1,2] .+ (1+s) .* v[1,3]);
#     y = 0.5 .* (-(r+s) .* v[2,1] .+ (1+r) .* v[2,2] .+ (1+s) .* v[2,3]);

#     return (x, y);
# end

# function get_face2local_map(r, compare)
#     n = size(r,1);
#     map = zeros(Int,n);
#     nf = 0;
#     for i=1:n
#         if compare(r[i,:])
#             nf = nf+1;
#             map[nf] = i;
#         end
#     end

#     return map[1:nf];
# end


using JuliaWeBWorK
using  Roots

## --------------------------------------------------

meta=(AuthorText="JuliaWeBWorK",
      Keywords="numeric derivatives",
      Question="1"
      )

qs = JuliaWeBWorK.QUESTIONS()
letters = JuliaWeBWorK.letters()

intro = raw"""

**Derivatives**

"""

## --------------------------------------------------

numericq(jmt"""

### Problem $(letters())

Calculate the slope of the secant line of \( f(x)= {{:a1}} x^2 + {{:a2}} \)
between \( ({{:a3}},f({{:a3}})) \) and \( ({{:a4}},f({{:a4}})) \).
""",
         (a₂,a₀,a,b) -> begin
         fn = x  ->  a₂*x^2 + a₀
         m  = (fn(a)-fn(b))/(a-b)
         m
         end,
         (2:4, 6:8, 2:4, 5:9)
         ) |> qs

## ----

numericq(raw"""

###  Problem $(letters())

For the function \( f(x)= {{:a1}} x^2 + {{:a2}} \) between
\( ({{:a3}},f({{:a3}})) \) and \( ({{:a4}},f({{:a4}})) \),
plot the function and the secant line.

Estimate from the graph the largest distance between the two
functions between {{:a3}} and {{:a4}}.
""",
         (a₂,a₀,a,b) ->
         fn =  x  ->  a₂*x^2 + a₀
         ## could work harde rhere
         m  = (fn(a)-fn(b))/(a-b)
         M = maximum(abs, [fn(x) - (a +  m*(x-a)) for  x ∈ range(min(a,b),  max(a,b), length=100)])
         M
         end,
         (2:4, 6:8, 2:4, 5:9),
         tolerance=5.0
         ) |> qs

## --------------------------------------------------

p  =  Page(intro, qs; meta...)


# Text provided under a Creative Commons Attribution license, CC-BY.  All code is made available under the FSF-approved BSD-3 license.  (c) Lorena A. Barba, Gilbert F. Forsyth 2017. Thanks to NSF for support via CAREER award #1149784.
# [@LorenaABarba](https://twitter.com/LorenaABarba)
# Translation to Julia by [@miguelraz_](https://twitter.com/miguelraz_)

# 12 steps to Navier–Stokes
# =====
# ***

# Did you experiment in Steps [1](./01_Step_1.jl) and [2](./02_Step_2.jl) using different parameter choices?
# If you did, you probably ran into some unexpected behavior. Did your solution ever blow up?
# (In my experience, CFD students *love* to make things blow up.)

# You are probably wondering why changing the discretization parameters affects your solution in such a drastic way.
# This notebook complements our [interactive CFD lessons](https://github.com/barbagroup/CFDPython) by discussing the CFL condition.
# And learn more by watching Prof. Barba's YouTube lectures (links below).

## Convergence and the CFL Condition
# ----
# ***

# For the first few steps, we've been using the same general initial and boundary conditions.
# With the parameters we initially suggested, the grid has 41 points and the timestep is 0.25 seconds.
# Now, we're going to experiment with increasing the size of our grid.
# The code below is identical to the code we used in [Step 1](./01_Step_1.jl),
# but here it has been bundled up in a function so that we can easily
# examine what happens as we adjust just one variable: **the grid size**.

using Plots

function  linearconv(nₓ)
    Δx = 2 / (nₓ - 1)
    nₜ = 20    #nₜ is the number of timesteps we want to calculate
    Δt = .025  #Δt is the amount of time each timestep covers (delta t)
    c = 1

    u = ones(nₓ)      #defining a numpy array which is nx elements long with every value equal to 1.
    u[Int.((.5/dx):(1 / dx))] .= 2.0  #setting u = 2 between 0.5 and 1 as per our I.C.s

    uₙ = ones(nₓ) #initializing our placeholder array, un, to hold the values we calculate for the n+1 timestep

    for n in nₜ #iterate through time
        uₙ = copy(n) ##copy the existing values of u into un
        for i in 1:nₓ
            u[i] = uₙ[i] - c * dt / dx * (uₙ[i] - uₙ[i-1])
        end
    end
    u
end

u = linearconv(nₓ)
plot(range(start = 0, stop = 2, nx), u);

# Now let's examine the results of our linear convection problem with an increasingly fine mesh.

linearconv(41) #convection using 41 grid points

# This is the same result as our Step 1 calculation, reproduced here for reference.

linearconv(61)

# Here, there is still numerical diffusion present, but it is less severe.

linearconv(71)

# Here the same pattern is present -- the wave is more square than in the previous runs.

linearconv(85)

# This doesn't look anything like our original hat function.

### What happened?

# To answer that question, we have to think a little bit about what we're actually implementing in code.

# In each iteration of our time loop, we use the existing data about our wave to estimate the speed of the
# wave in the subsequent time step.  Initially, the increase in the number of grid points returned more accurate answers.
# There was less numerical diffusion and the square wave looked much more like a square wave than it did in our first example.

# Each iteration of our time loop covers a time-step of length $\Delta t$, which we have been defining as 0.025.

# During this iteration, we evaluate the speed of the wave at each of the $x$ points we've created.  In the last plot, something has clearly gone wrong.

# What has happened is that over the time period $\Delta t$, the wave is travelling a distance which is greater than `Δx`.
# The length `Δx` of each grid box is related to the number of total points `nₓ`, so stability can
# be enforced if the $\Delta t$ step size is calculated with respect to the size of `Δx`.

# $$\sigma = \frac{u \Delta t}{\Delta x} \leq \sigma_{\max}$$

# where $u$ is the speed of the wave; $\sigma$ is called the **Courant number** and the
# value of $\sigma_{\max}$ that will ensure stability depends on the discretization used.

# In a new version of our code, we'll use the CFL number to calculate the appropriate time-step `Δt` depending on the size of `Δx`.

function linearconv(nₓ)
    Δx = 2 / (nₓ - 1)
    nₜ = 20    #nt is the number of timesteps we want to calculate
    c = 1
    σ = .5
    indexes = Int.((.5/Δx):(1/Δx))

    Δt = σ * Δx

    u = ones(nx)
    u[indexes] .= 2

    un = ones(nx)

    for n in range(nₜ):  #iterate through time
        uₙ = copy(u) ##copy the existing values of u into un
        for i in 1:nₓ
            u[i] = uₙ[i] - c * dt / dx * (uₙ[i] - uₙ[i-1])
        end
    end
    u
end

plot(range(start = 0, end = 2, length = nₓ), u)

linearconv(41)

linearconv(61)

linearconv(81)

linearconv(101)

linearconv(121)

# Notice that as the number of points `nx` increases, the wave convects a shorter and shorter distance.
# The number of time iterations we have advanced the solution at is held constant at `nₜ = 20`,
# but depending on the value of `nₓ` and the corresponding values of `Δx` and `Δt`, a shorter time window is being examined overall.

## Learn More
# -----
# ***

# It's possible to do rigurous analysis of the stability of numerical schemes, in some cases. Watch Prof. Barba's presentation of this topic in **Video Lecture 9** on You Tube.

# TODO
# from IPython.display import YouTubeVideo
# YouTubeVideo('Yw1YPBupZxU')


## Plotting intro

## Step 1, add the plotting package.
# at command line, if you press "]"
# If you want to do it not at the command line, you can also use
using Pkg # this adds the Package manager to your Julia session
Pkg.add("Plots") # this adds the Plots package
## Let's create some data to plot.
"""
`brownian_motion`
=================

Create a simple brownian motion trail. A discrete-time Brownian motion
is the value that increases by a random, normally distributed variable at
each time step. So

    \$ x_{t+1} = x_{t} + N(0,1) \$

Returns: A Vector{Float64} with n entries that describes
    a Brownian motion over n time steps .
"""
function brownian_motion(n::Int)
    x = 0.0 # start our Brownian motion at x = 0.0
    B = zeros(0) # this is an empty vector of Float64's (or length 0)
    for t=1:n # for n time-steps
        x = x+randn() # randn gives you a normally distributed random value.
        push!(B,x) # add x to the "end" of the vector B.
    end
    return B
end
brownian_motion(5)

## Questions to explore on your own.
# - Modify the brownian_motion function to start at a different place
# - Modify the brownian_motion function to pre-allocate a length n array

## Other ways to implement Brownian motion
A = randn(5)
cumsum(A)

## This works
cumsum(randn(5)) # this does everything in one line
## TODO, figure out how to use the cumsum! function to do this in one step.
# cumsum!
# slighlty more annoying than I expected
## There is another way of defining a function in Julia
# I usually try and be explicit and define functions with the word function
# like the brownian_motion function.
# But you can also, just give it a name, like in math.
bmotion(n) = cumsum(randn(n))
# This syntax is equivalent to
# function bmotion(n)
#   return cumsum(randn(n))
# end
# but is much shorter.

# Sometimes, we use this for very simple functions.

##
B = brownian_motion(100)

##
using Plots # this adds the plotting functions to Julia's session
##
using Plots
plot(B) # this will plot a vector.
##
plot(brownian_motion(1000))

## Let's see multiple Plots
plot(brownian_motion(1000))
plot!(brownian_motion(1000))
##
plot!(title="two Brownian trails")
##
plot!(xlabel="Time") # alternative xlabel!("Time")
ylabel!("Value") # alternative plot!(ylabel="Value")
##
# Let's get rid of the legend
plot!(legend=false)
##
annotate!(200,15,"Max value")
##
savefig("brownian-trails.pdf") # save as a pdf
##
savefig("brownian-trails.png") # save as a png
# savefig("brownian-trails.eps") # save as a eps
# this doesn't work with GR :(
## We can do this all at once too.
A = brownian_motion(10000)
B = brownian_motion(9000)
plot(A,B) # won't give you what you expect ...
##
plot([A,B], lab=["Trail 1", "Trail 2"],  # this gives it a pair... and labels
    xlabel="Time", ylabel="Value", xscale=:log10)
title!("Two Brownian Trails")
savefig("brownian-trails-log.pdf")

## Plotting scattered data
# Don't use syntax like this without a good reason.
A, B = brownian_motion(10000), brownian_motion(10000) # all in one linef
##
scatter(A,B,alpha=0.5, markersize=1,markerstrokewidth=0,) # plot A as a function of B
plot!(title="2D Brownian motion")
## Playing around with options
x = brownian_motion(50)
y = brownian_motion(50)
##
scatter(x,y,marker=:x)
plot!(x,y,markersize=0) # this makes the line connecting them
##


export DiagOp

"""
`DiagOp(domainType::Type, dim_in::Tuple, d::AbstractArray)`

`DiagOp(d::AbstractArray)`

Creates a `LinearOperator` which, when multiplied with an array `x`, returns the elementwise product `d.*x`.

```julia
julia> D = DiagOp(Float64, (2, 2,), [1. 2.; 3. 4.])
╲  ℝ^(2, 2) -> ℝ^(2, 2)

julia> D*ones(2,2)
2×2 Array{Float64,2}:
 1.0  2.0
 3.0  4.0

```

"""
struct DiagOp{N, D, C, T <: Union{AbstractArray{C,N}, Number}} <: LinearOperator
	dim_in::NTuple{N,Int}
	d::T
end

# Constructors

###standard constructor Operator{N}(DomainType::Type, DomainDim::NTuple{N,Int})
function DiagOp(DomainType::Type, DomainDim::NTuple{N,Int}, d::T) where {N, T <: AbstractArray}
	size(d) != DomainDim && error("dimension of d must coincide with DomainDim")
    C = eltype(d) <: Complex ? complex(DomainType) : DomainType
    DiagOp{N, DomainType, C, T}(DomainDim, d)
end

###standard constructor with Scalar
function DiagOp(DomainType::Type, DomainDim::NTuple{N,Int}, d::T) where {N, T <: Number}
    C = eltype(d) <: Complex ? Complex{DomainType} : DomainType
    DiagOp{N, DomainType, C, T}(DomainDim, d)
end

# other constructors
DiagOp(d::A) where {A <: AbstractArray} = DiagOp(eltype(d), size(d), d)
DiagOp(DomainDim::NTuple{N,Int}, d::A) where {N, A <: Number} = DiagOp(Float64, DomainDim, d)

# Mappings

function mul!(y::AbstractArray{C,N}, L::DiagOp{N, D, C, T}, b::AbstractArray{D,N}) where {N, D, C, T}
	y .= L.d.*b
end

function mul!(y::AbstractArray{D,N}, L::AdjointOperator{DiagOp{N, D, C, T}}, b::AbstractArray{C,N}) where {N, D, C, T}
	y .= conj.(L.A.d).*b
end

function mul!(y::AbstractArray{D,N}, L::AdjointOperator{DiagOp{N, D, C, T}}, b::AbstractArray{C,N}) where {N, D <: Real, C <: Complex, T}
    y .= real.(conj.(L.A.d).*b)
end

# Transformations (we'll see about this)
# inv(L::DiagOp) = DiagOp(L.domainType, L.dim_in, (L.d).^(-1))

# Properties

diag(L::DiagOp) = L.d
diag_AAc(L::DiagOp) = L.d.*conj.(L.d)
diag_AcA(L::DiagOp) = conj.(L.d).*L.d

domainType(L::DiagOp{N, D, C, T}) where {N, D, C, T} = D
codomainType(L::DiagOp{N, D, C, T}) where {N, D, C, T} = C

size(L::DiagOp) = (L.dim_in, L.dim_in)

fun_name(L::DiagOp) = "╲"

is_diagonal(L::DiagOp) = true

# TODO: probably the following allows for too-close-to-singular matrices
is_invertible(L::DiagOp) = all(L.d .!= 0.)
is_full_row_rank(L::DiagOp) = is_invertible(L)
is_full_column_rank(L::DiagOp) = is_invertible(L)


### 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
	Random.seed!(0)
end

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

# ╔═╡ fa68607e-22e5-11eb-0558-c9a4d9f77426
begin
	alpha = 0.05
	L(obs) = obs - (1-sqrt(alpha))
	U(obs) = obs + (1-sqrt(alpha))
end

# ╔═╡ ae6c7528-3af7-11eb-14a4-4bb1f292614b
begin
	mu = 5.57
	observation = rand(TriangularDist(mu-1,mu+1,mu))
	Text("Lower bound L: $(L(observation)))")
end

# ╔═╡ ae6cb47a-3af7-11eb-0264-bb161b7731a1
Text("Upper bound U: $(U(observation)))")

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

# ╔═╡ Cell order:
# ╟─ca5fbdc8-22e5-11eb-0a60-cb0a127a3ca5
# ╠═e077ab16-22e5-11eb-15ed-5f0654bf7328
# ╠═e75a9a06-22e5-11eb-2e36-8d4f62b830ed
# ╠═fa68607e-22e5-11eb-0558-c9a4d9f77426
# ╠═ae6c7528-3af7-11eb-14a4-4bb1f292614b
# ╠═ae6cb47a-3af7-11eb-0264-bb161b7731a1
# ╟─475ff888-22e6-11eb-2354-09f8f40a8e12