# # Heat equation
#
# In this example, we numerically solve the 1D [heat
# equation](https://en.wikipedia.org/wiki/Heat_equation)
#
# ```math
# \frac{∂θ}{∂t} = ν \frac{∂^2 θ}{∂x^2},
# ```
#
# in a bounded domain ``x ∈ [-1, 1]`` with homogeneous Neumann boundary
# conditions, ``∂_x θ(±1, t) = 0``.

# ## Defining a B-spline basis

# The general idea is to approximate the unknown solution by a spline of order
# ``k``.
# For this, we first define a B-spline basis ``\{ b_i(x), \, i = 1, …, N \}``, such
# that the solution at a given time $t$ is approximated by
#
# ```math
# θ(x, t) = ∑_{i = 1}^N v_i(t) b_i(x),
# ```
#
# where the $v_i$ are the B-spline coefficients.
#
# A B-spline basis is uniquely defined by its order ``k`` and by a choice of
# *knot* locations within the spatial domain, which form the spatial grid.

# For this example, we take a uniform repartition of knots in ``[-1, 1]``.
knots_in = range(-1, 1; length = 11)

# We then create a B-spline basis of order ``k = 4`` using these knots.
using BSplineKit
B = BSplineBasis(BSplineOrder(4), knots_in)

# Note that the generated basis includes an *augmented* set of knots, in which
# each boundary is repeated ``k`` times:

knots(B)

# In other words, the boundary knots have multiplicity ``k``, while interior
# knots have multiplicity 1.
# This is common practice in bounded domains, and translates the fact that the
# solution does not need to be continuous at the boundaries.
# This provides additional degrees of freedom notably for the boundary
# conditions.
# This behaviour can be disabled via the `augment` argument of
# [`BSplineBasis`](@ref).

# We can now plot the knot locations (crosses) and the generated B-spline basis:

using CairoMakie
using LaTeXStrings
CairoMakie.activate!(type = "svg")

function plot_knots!(ax, ts; knot_offset = 0.03, kws...)
    ys = zero(ts)
    ## Add offset to distinguish knots with multiplicity > 1
    for i in eachindex(ts)[(begin + 1):end]
        if ts[i] == ts[i - 1]
            ys[i] = ys[i - 1] + knot_offset
        end
    end
    scatter!(ax, ts, ys; marker = :x, markersize = 20, color = :gray, kws...)
    ax
end

function plot_basis!(ax, B; eval_args = (), kws...)
    cmap = cgrad(:tab20)
    N = length(B)
    ts = knots(B)
    hlines!(ax, 0; color = :gray)
    for (n, bi) in enumerate(B)
        color = cmap[(n - 1) / (N - 1)]
        i, j = extrema(support(bi))
        lines!(ax, ts[i]..ts[j], x -> bi(x, eval_args...); color, linewidth = 2.5)
    end
    plot_knots!(ax, ts; kws...)
    ax
end

fig = Figure()
ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = L"b_i(x)")
plot_basis!(ax, B)
fig

# ## Imposing boundary conditions

# In BSplineKit, the recommended approach for solving boundary value problems is
# to use the **basis recombination** method.
# That is, to expand the solution onto a new basis consisting on linear
# combinations of B-splines ``b_i(x)``, such that each recombined basis function
# ``ϕ_j(x)`` individually satisfies the required homogeneous boundary conditions
# (BCs).
# Thanks to the local support of B-splines, basis recombination only involves a
# small number of B-splines near the boundaries.
#
# Using the [`RecombinedBSplineBasis`](@ref) type, we can easily define such
# recombined bases for many different BCs.
# In this example we generate a basis satisfying homogeneous Neumann BCs:

R = RecombinedBSplineBasis(Derivative(1), B)

fig = Figure()
ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = L"ϕ_i(x)")
plot_basis!(ax, R)
fig

# We notice that, on each of the two boundaries, the two initial (or final)
# B-splines of the original basis have been combined to produce a single basis
# function that has zero derivative at each respective boundary.
# To verify this, we can plot the basis function derivatives:

fig = Figure()
ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = L"ϕ_i^′(x)")
plot_basis!(ax, R; eval_args = (Derivative(1), ), knot_offset = 0.4)
fig

# Note that the new basis has two less functions than the original one,
# reflecting a loss of two degrees of freedom corresponding to the new
# constraints on each boundary:

length(B), length(R)

# ### Recombination matrix

# As stated above, the basis recombination approach consists in performing
# linear combinations of B-splines ``b_i`` to obtain a derived basis of
# functions ``ϕ_j`` satisfying certain boundary conditions.
# This can be conveniently expressed using a transformation matrix
# ``\mathbf{T}`` relating the two bases:
#
# ```math
# ϕ_j(x) = ∑_{i = 1}^N T_{ij} b_i(x)
# \quad \text{for } j = 1, 2, …, M,
# ```
#
# where ``N`` is the number of B-splines ``b_i``, and ``M < N`` is the number of
# ``ϕ_j`` functions (in this example, ``M = N - 2``).
#
# The recombination matrix associated to the generated basis can be obtained
# using [`recombination_matrix`](@ref):

T = recombination_matrix(R)

# Note that the matrix is almost an identity matrix, since most B-splines are
# kept intact in the new basis.
# This simple structure allows for very efficient computations using this
# matrix.
# The first and last columns indicate that Neumann BCs are imposed by adding the
# two first (and two last) B-splines, i.e.
#
# ```math
# ϕ_1(x) = b_1(x) + b_2(x),
# \qquad
# ϕ_M(x) = b_{N - 1}(x) + b_N(x).
# ```

# ### Representation of the solution
#
# Note that the solution ``θ(x, t)`` can be represented in the original and in
# the recombined B-spline bases as
#
# ```math
# θ(x, t) = ∑_{i = 1}^N v_i(t) b_i(x) = ∑_{j = 1}^M u_j(t) ϕ_j(x),
# ```
#
# where the ``u_j`` are the coefficients in the recombined basis.
#
# The recombination matrix introduced above can be used to transform between the
# coefficients ``u_j`` and ``v_i`` in both bases, via the linear relation
# ``\bm{v} = \mathbf{T} \bm{u}``.

# ## Initial condition

# We come back now to our problem.
# We want to impose the following initial condition:
#
# ```math
# θ(x, 0) = θ_0(x) = 1 + \cos(π x).
# ```

# First, we approximate this initial condition in the recombined
# B-spline basis that we have just constructed.
# This may be easily done using [`approximate`](@ref):

θ₀(x) = 1 + cos(π * x)
θ₀_spline = approximate(θ₀, R, MinimiseL2Error())

# To see that everything went well, we can plot the exact initial condition and
# its spline approximation, which show no important differences.

fig = Figure(resolution = (800, 400))
let ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = L"\theta")
    lines!(ax, -1..1, θ₀; label = L"θ_0(x)", color = :blue)
    lines!(ax, -1..1, x -> θ₀_spline(x); label = "Approximation", color = :orange, linestyle = :dash)
    axislegend(ax; position = :cb)
end
let ax = Axis(fig[1, 2]; xlabel = L"x", ylabel = "Difference")
    lines!(ax, -1..1, x -> θ₀(x) - θ₀_spline(x))
    plot_knots!(ax, knots(R); knot_offset = 0)
end
fig

# Note that we have access to the recombined B-spline coefficients ``u_j``
# associated to the initial condition, which we will use further below:

u_init = coefficients(θ₀_spline)

# ## Solving the heat equation
#
# To solve the governing equation, the strategy is to project the unknown
# solution onto the chosen recombined basis.
# That is, we approximate the solution as
#
# ```math
# θ(x, t) = \sum_{j = 1}^M u_j(t) \, ϕ_j(x).
# ```
#
# Plugging this representation into the heat equation, we find
#
# ```math
# \newcommand{\dd}{\mathrm{d}}
# ∑_j \frac{\dd u_j}{\dd t} \, ϕ_j(x) = ν ∑_j u_j \, ϕ_j''(x),
# ```
#
# where primes denote spatial derivatives.
#
# We can now use the [method of mean weighted
# residuals](https://en.wikipedia.org/wiki/Method_of_mean_weighted_residuals) to
# find the coefficients ``u_j``, by projecting the above equation onto a chosen
# set of *test* functions ``φ_i``:
#
# ```math
# ∑_j \frac{\mathrm{d} u_j}{\mathrm{d} t} \, ⟨φ_i, ϕ_j⟩ = ν ∑_j u_j \, ⟨φ_i, ϕ_j''⟩,
# ```
#
# where ``⟨ f, g ⟩ = ∫_{-1}^1 f(x) \, g(x) \, \mathrm{d} x`` is the inner product
# between functions.
#
# By choosing ``M`` different test functions ``φ_i``, the above problem can be
# written as the linear system
#
# ```math
# \mathbf{A} \frac{\mathrm{d} \bm{u}(t)}{\mathrm{d} t} =
# ν \mathbf{L} \bm{u}(t),
# ```
#
# where the matrices are defined by ``A_{ij} = ⟨ φ_i, ϕ_j ⟩`` and
# ``L_{ij} = ⟨ φ_i, ϕ_j'' ⟩``.
#
# Two of the most common choices of test functions ``φ_i`` are:
#
# - ``φ_i(x) = δ(x - x_i)``, where ``δ`` is Dirac's delta, and ``x_i`` are a set of
#   *collocation* points where the equation will be satisfied.
#   This approach is known as the **collocation** method.
#
# - ``φ_i(x) = ϕ_i(x)``, in which case this is the **Galerkin** method.
#
# We describe the solution using both methods in the following.

# ## Collocation method

# For the collocation method, we need to choose a set of ``M`` grid points
# ``x_j``.
# Since the basis functions implicitly satisfy the boundary conditions, these
# points must be chosen *inside* of the domain.
#
# The collocation points may be automatically generated by calling
# [`collocation_points`](@ref).
# Note that, since we pass the recombined basis `R`, the boundaries are not
# included in the chosen points:

xcol = collocation_points(R)

# We can now construct the matrices ``\mathbf{A}`` and ``\mathbf{L}`` associated
# to the collocation method.
# By definition, these matrices simply contain the evaluations of all basis
# functions ``ϕ_j`` and their derivatives at the collocation points:
# ``A_{ij} = ϕ_j(x_i)`` and ``L_{ij} = ϕ_j''(x_i)``.
# Both these matrices can be constructed in BSplineKit using
# [`collocation_matrix`](@ref).
# Note that both matrices are of type [`CollocationMatrix`](@ref), which wrap
# matrices defined in
# [BandedMatrices.jl](https://github.com/JuliaMatrices/BandedMatrices.jl).

Acol = collocation_matrix(R, xcol)
Lcol = collocation_matrix(R, xcol, Derivative(2))

# For convenience and performance, we can incorporate the heat diffusivity ``ν``
# in the ``\mathbf{L}`` matrix:

ν = 0.01
Lcol *= ν

# Finally, for the time integration, we use OrdinaryDiffEq.jl from the
# [DifferentialEquations.jl suite](https://diffeq.sciml.ai/stable/).

using LinearAlgebra
using OrdinaryDiffEq

function heat_rhs!(du, u, params, t)
    mul!(du, params.L, u)    # du = ν * L * u
    ldiv!(du, params.A, du)  # du = A \ (ν * L * u)
    du
end

## Solver parameters
params_col = (
    A = lu(Acol),  # we pass the factorised matrix A for performance
    L = Lcol,
)

tspan = (0.0, 10.0)
prob = ODEProblem(heat_rhs!, u_init, tspan, params_col)
sol_collocation = solve(prob, Tsit5(); saveat = 0.5)

function plot_heat_solution(sol, R)
    fig = Figure()
    ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = L"θ(x, t)")
    colormap = cgrad(:viridis)
    tspan = sol.prob.tspan
    Δt = tspan[2] - tspan[1]
    for (u, t) in tuples(sol)
        S = Spline(R, u)
        color = colormap[(t - tspan[1]) / Δt]
        lines!(ax, -1..1, x -> S(x); label = string(t), color, linewidth = 2)
    end
    Colorbar(fig[1, 2]; colormap, limits = tspan, label = "Time")
    fig
end

plot_heat_solution(sol_collocation, R)

# ## Galerkin method

# We start by constructing the Galerkin matrices ``\mathbf{A}`` and
# ``\mathbf{L}``.
# The first of these matrices, ``A_{ij} = ⟨ ϕ_i, ϕ_j ⟩``, is usually
# known as the *mass matrix* of the system.
# It is a positive definite symmetric matrix, which enables the use of Cholesky
# factorisation to solve the resulting linear system.
# Moreover, here it is banded thanks to the local support of the B-splines.
# The mass matrix can be constructed by calling [`galerkin_matrix`](@ref):

Agal = galerkin_matrix(R)

# Note that, unlike the collocation method, in the Galerkin method we don't need
# to specify a set of grid points, as functions are not evaluated at collocation
# points (they are instead integrated over the whole domain).
# The integration is performed using Gauss--Legendre quadrature, which can be
# made exact up to numerical precision, taking advantage of the fact that the
# product of two B-splines is a piecewise polynomial.

# As for the matrix ``\mathbf{L}`` representing the second derivative operator,
# we can write it using integration by parts as
#
# ```math
# L_{ij} = ⟨ ϕ_i, ϕ_j'' ⟩
# = -⟨ ϕ_i', ϕ_j' ⟩ + \left[ ϕ_i ϕ_j' \right]_{-1}^1  = -R_{ij},
# ```
#
# where ``R_{ij} = ⟨ ϕ_i', ϕ_j' ⟩`` is a positive definite symmetric matrix.
# Note that the boundary terms all vanish since all basis functions individually
# satisfy homogeneous Neumann boundary conditions, ``ϕ_i'(±1) = 0``.
# (The same result would be obtained with homogeneous Dirichlet boundary
# conditions.)
#
# As can be seen above, one well-known advantage of the Galerkin method is that
# the basis functions can satisfy weaker continuity conditions than in the
# collocation method, as high-order derivatives can be reduced using integration
# by parts.
#
# The matrix ``\mathbf{R}`` can be constructed using [`galerkin_matrix`](@ref):

Rgal = galerkin_matrix(R, (Derivative(1), Derivative(1)))

# Note that, instead, we could have constructed the original matrix
# ``\mathbf{L}``, which, as expected, is equal to ``\mathbf{R}`` up to a sign:

Lgal = galerkin_matrix(R, (Derivative(0), Derivative(2)))

# As in the collocation example, we include the heat diffusivity ``ν`` in the
# ``\mathbf{R}`` matrix:

parent(Rgal) .*= -ν  # we can't directly multiply Rgal, as it's a Hermitian wrapper

# We finally solve using DifferentialEquations.jl.
# Note that not much is changed compared to the collocation example.
# The only difference is that we use a Cholesky factorisation for the mass
# matrix ``\mathbf{A}``.

params_gal = (
    A = cholesky(Agal),
    L = Rgal,
)

prob = ODEProblem(heat_rhs!, u_init, tspan, params_gal)
sol_galerkin = solve(prob, Tsit5(); saveat = 0.5)

plot_heat_solution(sol_galerkin, R)

# ## Result comparison

# The solution of the Galerkin method looks very similar to the one obtained
# with the collocation method.
# However, as seen below, there are non-negligible differences between the two.

fig = Figure(resolution = (800, 400))
let ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = latexstring("θ(x, t = $(tspan[end]))"))
    for pair in (
            "Collocation" => sol_collocation,
            "Galerkin" => sol_galerkin,
        )
        label, sol = pair
        u = last(sol.u)
        S = Spline(R, u)
        lines!(ax, -1..1, x -> S(x); label, linewidth = 2)
    end
    axislegend(ax; position = :cb)
end
let ax = Axis(fig[1, 2]; xlabel = L"x", ylabel = "Difference")
    Sc = Spline(R, last(sol_collocation.u))
    Sg = Spline(R, last(sol_galerkin.u))
    lines!(ax, -1..1, x -> Sc(x) - Sg(x); linewidth = 2)
end
fig

# Compared to the Galerkin method, there seems to be some additional dissipation
# in the domain interior when using the collocation method.
# This hints at the presence of numerical dissipation introduced by this method.

# To finish, we compare the two solutions to a solution at a higher resolution, using
# a higher number of B-spline knots and a higher B-spline order.
# This last solution is obtained using the collocation method to allow for
# better comparisons between both methods.

hi_res = let
    knots_in = range(-1, 1; length = 101)
    B = BSplineBasis(BSplineOrder(6), knots_in)
    R = RecombinedBSplineBasis(Derivative(1), B)
    θ₀_spline = approximate(θ₀, R)
    u_init = coefficients(θ₀_spline)
    xcol = collocation_points(R)
    Acol = collocation_matrix(R, xcol)
    Lcol = ν .* collocation_matrix(R, xcol, Derivative(2))
    params_col = (A = lu(Acol), L = Lcol)
    prob = ODEProblem(heat_rhs!, u_init, tspan, params_col)
    sol = solve(prob, Tsit5(); saveat = 0.5)
    (; R, sol)
end

fig = Figure(resolution = (800, 400))
let ax = Axis(fig[1, 1]; xlabel = L"x", ylabel = latexstring("θ(x, t = $(tspan[end]))"))
    for pair in (
            "Collocation" => sol_collocation,
            "Galerkin" => sol_galerkin,
        )
        label, sol = pair
        u = last(sol.u)
        S = Spline(R, u)
        lines!(ax, -1..1, x -> S(x); label, linewidth = 2)
    end
    let u = last(hi_res.sol.u)
        S = Spline(hi_res.R, u)
        lines!(ax, -1..1, x -> S(x); label = "Hi-res", linewidth = 2, linestyle = :dash, color = :gray)
    end
    axislegend(ax; position = :cb)
end
let ax = Axis(fig[1, 2]; xlabel = L"x", ylabel = "Difference with hi-res solution")
    Sc = Spline(R, last(sol_collocation.u))
    Sg = Spline(R, last(sol_galerkin.u))
    S_hi = Spline(hi_res.R, last(hi_res.sol.u))
    lines!(ax, -1..1, x -> Sc(x) - S_hi(x); label = "Colloc.", linewidth = 2)
    lines!(ax, -1..1, x -> Sg(x) - S_hi(x); label = "Galerkin", linewidth = 2)
    axislegend(ax; position = :rb)
end
fig

# We see that the low-resolution solution with the Galerkin method matches the
# high-resolution solution.
# This confirms that the Galerkin method provides higher accuracy than the
# collocation method when both are used at the same resolution.


#
#
#In the 20×20 grid below, four numbers along a diagonal line have been marked in red.

A = [
08 02 22 97 38 15 00 40 00 75 04 05 07 78 52 12 50 77 91 08
49 49 99 40 17 81 18 57 60 87 17 40 98 43 69 48 04 56 62 00
81 49 31 73 55 79 14 29 93 71 40 67 53 88 30 03 49 13 36 65
52 70 95 23 04 60 11 42 69 24 68 56 01 32 56 71 37 02 36 91
22 31 16 71 51 67 63 89 41 92 36 54 22 40 40 28 66 33 13 80
24 47 32 60 99 03 45 02 44 75 33 53 78 36 84 20 35 17 12 50
32 98 81 28 64 23 67 10 26 38 40 67 59 54 70 66 18 38 64 70
67 26 20 68 02 62 12 20 95 63 94 39 63 08 40 91 66 49 94 21
24 55 58 05 66 73 99 26 97 17 78 78 96 83 14 88 34 89 63 72
21 36 23 09 75 00 76 44 20 45 35 14 00 61 33 97 34 31 33 95
78 17 53 28 22 75 31 67 15 94 03 80 04 62 16 14 09 53 56 92
16 39 05 42 96 35 31 47 55 58 88 24 00 17 54 24 36 29 85 57
86 56 00 48 35 71 89 07 05 44 44 37 44 60 21 58 51 54 17 58
19 80 81 68 05 94 47 69 28 73 92 13 86 52 17 77 04 89 55 40
04 52 08 83 97 35 99 16 07 97 57 32 16 26 26 79 33 27 98 66
88 36 68 87 57 62 20 72 03 46 33 67 46 55 12 32 63 93 53 69
04 42 16 73 38 25 39 11 24 94 72 18 08 46 29 32 40 62 76 36
20 69 36 41 72 30 23 88 34 62 99 69 82 67 59 85 74 04 36 16
20 73 35 29 78 31 90 01 74 31 49 71 48 86 81 16 23 57 05 54
01 70 54 71 83 51 54 69 16 92 33 48 61 43 52 01 89 19 67 48
]

#The product of these numbers is 26 × 63 × 78 × 14 = 1788696.
#
#What is the greatest product of four adjacent numbers in the same direction (up, down, left, right, or diagonally) in the 20×20 grid?

println(size(A))

maxval = typemin(Int64)
xi, xj, di = 0, 0, '.'
# horizontal
for i in 1:20
	for j in 1:20
		# horizontal
		if j <= 17
			x = prod(A[i, j:j+3])
			if x > maxval
				println(A[i, j:j+3])
				maxval, xi, xj, di = x, i, j, '>'
			end
		end
		# vertical
		if i <= 17
			x = prod(A[i:i+3, j])
			if x > maxval
				println(A[i:i+3, j])
				maxval, xi, xj, di = x, i, j, 'v'
			end
		end
		# top left -> bottom right
		if i <= 17 && j <= 17
			x = prod(diag(A[i:i+3, j:j+3]))
			if x > maxval
				println(diag(A[i:i+3, j:j+3]))
				maxval, xi, xj, di = x, i, j, '\\'
			end
		end
		# top right -> bottom left
		if i <= 17 && j <= 17
			x = prod(diag(rotr90(A[i:i+3, j:j+3])))
			if x > maxval
				println(diag(rotr90(A[i:i+3, j:j+3])))
				maxval, xi, xj, di = x, i, j, '/'
			end
		end
	end
end
print(maxval, ' ', xi, ' ', xj, ' ', di)
print(A[xi:xi+3, xj:xj+3])

# 89*94*97*87
# 70600674


"""
    polyinterp(t,y)

Construct a callable polynomial interpolant through the points in
vectors `t`,`y` using the barycentric interpolation formula.
"""
function polyinterp(t,y)
    n = length(t)-1
    C = (t[n+1]-t[1]) / 4           # scaling factor to ensure stability
    tc = t/C

    # Adding one node at a time, compute inverses of the weights.
    ω = ones(n+1)
    for m in 0:n-1
        d = tc[1:m+1] .- tc[m+2]    # vector of node differences
        @. ω[1:m+1] *= d            # update previous
        ω[m+2] = prod( -d )         # compute the new one
    end
    w = 1 ./ ω                      # go from inverses to weights

    # This function evaluates the interpolant at given x.
    p = function (x)
        terms = @. w / (x - t)
        if any(isinf.(terms))     # there was division by zero
            # return the node's data value
            idx = findfirst(x.==t)
            f = y[idx]
        else
            f = sum(y.*terms) / sum(terms)
        end
    end
    return p
end

"""
    triginterp(t,y)

Construct the trigonometric interpolant for the points defined by
vectors `t` and `y`.
"""
function triginterp(t,y)
    N = length(t)

    function τ(x)
        if x==0
            return 1.0
        else
            denom = isodd(N) ? N*sin(π*x/2) : N*tan(π*x/2)
            return sin(N*π*x/2)/denom
        end
    end

    return function (x)
        sum( y[k]*τ(x-t[k]) for k in eachindex(y) )
    end
end

"""
    ccint(f,n)

Perform Clenshaw-Curtis integration for the function `f` on `n`+1
nodes in [-1,1]. Returns the integral estimate and a vector of the
nodes used. Note: `n` must be even.
"""
function ccint(f,n)
    @assert iseven(n) "Value of `n` must be an even integer."
    # Find Chebyshev extreme nodes.
    θ = [ i*π/n for i in 0:n ]
    x = -cos.(θ)

    # Compute the C-C weights.
    c = similar(θ)
    c[[1,n+1]] .= 1/(n^2-1)
    s = sum( cos.(2k*θ[2:n])/(4k^2-1) for k in 1:n/2-1 )
    v = @. 1 - 2s - cos(n*θ[2:n])/(n^2-1)
    c[2:n] = 2v/n

    # Evaluate integrand and integral.
    I = dot(c,f.(x))   # vector inner product
    return I,x
end

"""
    glint(f,n)

Perform Gauss-Legendre integration for the function `f` on `n` nodes
in (-1,1). Returns the integral estimate and a vector of the nodes used.
"""
function glint(f,n)
    # Nodes and weights are found via a tridiagonal eigenvalue problem.
    β = @. 0.5/sqrt(1-(2*(1:n-1))^(-2))
    T = diagm(-1=>β,1=>β)
    λ,V = eigen(T)
    p = sortperm(λ)
    x = λ[p]               # nodes
    c = @. 2V[1,p]^2       # weights

    # Evaluate the integrand and compute the integral.
    I = dot(c,f.(x))      # vector inner product
    return I,x
end

"""
    intinf(f,tol)

Perform adaptive doubly-exponential integration of function `f`
over (-Inf,Inf), with error tolerance `tol`. Returns the integral
estimate and a vector of the nodes used.
"""
function intinf(f,tol)
    x = t -> sinh(sinh(t))
    dx_dt = t -> cosh(t)*cosh(sinh(t))
    g = t -> f(x(t))*dx_dt(t)

    # Find where to truncate the integration interval.
    M = 3
    while (abs(g(-M)) > tol/100) || (abs(g(M)) > tol/100)
        M += 0.5
        if isinf(x(M))
            @warn "Function may not decay fast enough."
            M -= 0.5
            break
        end
    end

    I,t = intadapt(g,-M,M,tol)
	return I,x.(t)
end

"""
    intsing(f,tol)

Adaptively integrate function `f` over (0,1), where `f` may be
singular at zero, with error tolerance `tol`. Returns the
integral estimate and a vector of the nodes used.
"""
function intsing(f,tol)
    x = t -> 2/(1+exp(2sinh(t)))
	dx_dt = t -> cosh(t)/cosh(sinh(t))^2
	g = t -> f(x(t))*dx_dt(t)

    # Find where to truncate the integration interval.
    M = 3
    while abs(g(M)) > tol/100
        M += 0.5
        if iszero(x(M))
            @warn "Function may grow too rapidly."
            M -= 0.5
            break
        end
    end

    I,t = intadapt(g,0,M,tol)
	return I,x.(t)
end


using IntervalWavelets
using OffsetArrays
using Test

@testset "Interior scaling functions" begin
    @testset "Dyadic dilation matrix has the expected form" begin
        h1 = interior_filter(3, :min)
        H1 = IntervalWavelets.dyadic_dilation_matrix(h1)

        @test size(H1) == (length(h1) - 2, length(h1) - 2)

        @test H1 == sqrt(2) *
        [ h1[1] h1[0]    0     0  ;
          h1[3] h1[2] h1[1] h1[0] ;
          h1[5] h1[4] h1[3] h1[2] ;
             0     0  h1[5] h1[4] ]

        h2 = interior_filter(3, :symmlet)
        H2 = IntervalWavelets.dyadic_dilation_matrix(h2)

        @test H2 == sqrt(2) *
        [ h2[-1] h2[-2]      0      0  ;
          h2[1]  h2[0]   h2[-1] h2[-2] ;
          h2[3]  h2[2]   h2[1]  h2[0] ;
             0      0    h2[3]  h2[2] ]
    end

    @testset "Construct interior scaling function" begin
        h = interior_filter(2)
        phi = interior_scaling_function(h)

        @test collect(support(h)) == Integer.(support(phi))
        @test vanishing_moments(phi) == 2
        @test resolution(phi) == 0

        phi = interior_scaling_function(h, 1)

        @test vanishing_moments(phi) == 2
        @test resolution(phi) == 1

        @test_throws DomainError interior_scaling_function(h, -1)
    end

    @testset "Specific interior scaling functions" begin
        h = interior_filter(2)
        phi = interior_scaling_function(h, 1)

        @test phi.(support(phi)) ≈ [0.0 ; 0.93301 ; 1.36602 ; 0.0 ; -0.36602 ; 0.06698 ; 0.0] atol = 10.0^-4
    end

    @testset "Haar scaling functions" begin
        haar_filter = interior_filter(1, :min)

        haar = interior_scaling_function(haar_filter)
        @test haar.(support(haar)) == [1.0 ; 0.0]

        haar = interior_scaling_function(haar_filter, 1)
        @test haar.(support(haar)) ≈ [1.0 ; 1.0 ; 0.0]
    end

    @testset "Increase resolution of interior scaling function" begin
        h = interior_filter(2)
        phi0 = interior_scaling_function(h)

        phi0_support = support(phi0)
        @test phi0_support == IntervalWavelets.all_dyadic_rationals(-1, 2, 0)

        phi1 = IntervalWavelets.increase_resolution(phi0)
        @test support(phi1) == IntervalWavelets.all_dyadic_rationals(-1, 2, 1)

        @test phi1.(phi0_support) == phi0.(phi0_support)
    end
end


# # Multiple dispatch

#-

# **Multiple dispatch** is a key feature of Julia, that we will explore in this notebook.
#
# It helps make software fast. It also makes software extensible, programmable, and downright fun to play with.
#
# It may just herald a breakthrough for parallel computation.

#-

# 1. Roman numerals
# 2. Functions
# 3. Parallel computing

#-

# ## 1. Roman numerals (for fun)

#-

# Let's define a **new struct** that represents a Roman numeral. For coding simplicity, we'll just deal with numbers between 0 and 9.
#
# **Exercise**: Extend this to larger numbers. (Recall that Roman numbers are a base-10 system!)

struct Roman
    n::Int
end

Base.show(io::IO, r::Roman) = print(io, 'ⅰ' + (r.n - 1) % 10 )  # nice display; 'ⅰ' is a Unicode Roman numeral

# We can create an object of this type as follows:

Roman(4)

#-

typeof.([5 5.0 Roman(5) "Five" '5'  5//1])

# We would like to display it nicely, in Roman numerals:

x = [7 1 2 5 8 9]
Roman.(x)   # equivalent to map(Roman, x)  or  [Roman(w) for w in x]

# It'd be nice to be able to add Roman numerals together like normal numbers:

Roman(4) + Roman(5)

# But Julia doesn't know how to do that. Let's teach it by `import`ing the `+` function, which then allows us to _extend_ its definition:

import Base: +, *

+(a::Roman, b::Roman) = Roman(a.n + b.n)

#-

Roman(4) + Roman(5)

# This **adds a new method** to the function `+`:

methods(+)

#-

import Base.*
*(i::Roman, j::Roman) = Roman(i.n * j.n)                     # Multiply like a Roman

#-

Roman(3) * Roman(2)

#-

Roman.(1:3) .* [Roman(1) Roman(2) Roman(3)]

# But

Roman(3) * 2

#-

## Complicated mytimes to decide what to do based on type
## not suggested, better way coming soon
function mytimes(i,j)
  if isa(i,Roman) & isa(j,Number)
        return  fill(1, i.n, j)   # i by j matrix with ones
    elseif    isa(i,Number) & isa(j,Roman)
        return "😄"^ (i*j.n)   #  i * j happy faces
    else
        return("I Don't know")
    end
end

#-

mytimes(4,Roman(3)) # Twelve happys

#-

mytimes(Roman(4),3) # 4x3 matrix with ones

# The simplest thing to do is to explicitly define multiplication of a `Roman` by a number. We can do it as we see fit:

*(i::Number, j::Roman) = "😄"^ (i*j.n)        #  i * j happy faces

*(i::Roman, j::Number) =   fill(1, i.n, j)       # i by j matrix

#-

3 * Roman(3) # Nine happys

#-

Roman(3) * 5  # Three by Five matrix of ones

#-

t(x::Roman,y::Roman) = x.n * y.n

#-

t(Roman(5),Roman(4))

#-

##  Notice how tight the assembler is!
@code_native t(Roman(2),Roman(4))

# ## Functions

import Base: *, +, ^

#-

*(α::Number,   g::Function) = x -> α * g(x)   # Scalar times function

*(f::Function, λ::Number)   = x -> f(λ * x)   # Scale the argument

*(f::Function, g::Function) = x -> f(g(x))    # Function composition  -- abuse of notation!  use \circ in Julia 0.6

^(f::Function, n::Integer) = n == 1 ? f : f*f^(n-1) # A naive exponentiation algorithm by recursive multiplication

#-

+(f::Function, g::Function) = x -> f(x) + g(x)

# For example, the exponential function is defined as
#
# $$\exp(x) = \sum_{n=0}^\infty \frac{1}{n!} x^n.$$
#
# We can think of this just in terms of functions:
#
# $$\exp = \sum_{n=0}^\infty \frac{1}{n!} \mathrm{pow}_n,$$
#
# where $\mathrm{pow}_n(x) = x^n$.
#
# (starts to blur the symbolic with the numerical!)

pow(n) = x -> x^n

myexp = sum(1/factorial(big(n)) * pow(n) for n in 0:100)   # taylor series not efficient!

#-

[myexp(1); exp(1); exp(big(1))]

#-

f = x -> x^2
f(10)

#-

g = 3f
g(10)

#-

(f^2)(10)  # since we defined multiplication of functions as composition

#-

using Plots;
gr()

#-

x = pi*(0:0.001:4)

plot(x, sin.(x),    c="black", label="Fun")
plot!(x, (12*sin).(x),    c="green", label="Num * Fun")
plot!(x, (sin*12).(x),    c="red", alpha=0.9, label="Fun * Num")
plot!(x, (5*sin*exp).(x), c="blue", alpha=0.2, label="Num * Fun * Fun")

#-

plot([12*sin, sin*12, 5*sin*exp], 0:.01:4π, α=[1 .9 .2], c=[:green :red :blue])

# <img src="https://lh4.googleusercontent.com/--z5eKJbB7sg/UffjL1iAd4I/AAAAAAAABOc/S_wDVyDOBfQ/gauss.jpg">

#-

# ###  "Sin^2 phi is odious to me, even though Laplace made use of it; should  it be feared that sin^2 phi might become ambiguous, which would perhaps  never occur, or at most very rarely when speaking of sin(phi^2), well  then, let us write (sin phi)^2, but not sin^2 phi, which by analogy  should signify sin(sin phi)." -- Gauss

x=(0:.01:2) * pi;

plot(x, (sin^2).(x), c="blue")     # Squaring just works, y=sin(sin(x)), Gauss would be pleased!
plot!(x, sin.(x).^2,  c="red")

# # Exercise

h(a, b::Any) = "fallback"
h(a::Number, b::Number) = "a and b are both numbers"
h(a::Number, b) = "a is a number"
h(a, b::Number) = "b is a number"
h(a::Integer, b::Integer) = "a and b are both integers"

#-

## Try playing with h


using TriangleMesh
using Plots
using Triplot
using LinearAlgebra
using SparseArrays
using ForwardDiff
using UnPack  # for convenience @unpack macro
include("./TriFEMUtils.jl") # plotting and misc routines

poly = polygon_unitSquare()
# poly = polygon_regular(5)
poly = polygon_Lshape()
num_refinements = 3
max_triangle_area = .05/(4^num_refinements)
mesh = create_mesh(poly, quality_meshing=true,
                   add_switches="penva"*string(max_triangle_area)*"q") # switches

# new problem
f(x,y) = exp(sin(1+pi*x)*sin(pi*y))
on_Neumann(x,y)   = x ≈ 1 && y > 0 && y < .5 # right face x = 1 (excluding corners)
on_Dirichlet(x,y) = !on_Neumann(x,y) # Dirichlet boundary = everywhere else
u_Dirichlet(x,y)  = 0.0
∇u_dot_n(x,y)     = 0.0 # note that n = [1,0] on the Neumann face
u0(x,y) = sin(pi*x)*sin(pi*y)

# ordering of reference element vertices
# 3
# |`.
# 1--2
reference_vertices = [[-1,1,-1],[-1,-1,1]] # stored as [r,s]
reference_face_indices = [[1,2],[2,3],[1,3]] # ordering of faces for the reference element
ref_elem_info = (;reference_face_indices,reference_vertices)

# define reference basis functions
λ1(r,s) = -(r+s)/2
λ2(r,s) = (1+r)/2
λ3(r,s) = (1+s)/2
λ(r,s) = [λ1.(r,s) λ2.(r,s) λ3.(r,s)]
dλr() = [-.5 .5 0.0]
dλs() = [-.5 0.0 .5]

# x,y = lists of vertices
function compute_geometric_terms(x,y)
    # dx(r,s)/dr = dλ1/dr*x1 + dλ2/dr*x2 + dλ3/dr*x3 = [dλ1/dr dλ2/dr dλ3/dr] * [x1; x2; x3]
    dxdr,dydr = dλr()*x, dλr()*y
    dxds,dyds = dλs()*x, dλs()*y
    G = [dxdr dxds; dydr dyds] # G*r = x, r = inv(G)*x
    J = det(G) # change of variables det(Jacobian) for integration
    drdx, dsdx, drdy, dsdy = inv(G)
    return J, drdx, dsdx, drdy, dsdy
end

# assemble stiffness matrix and RHS vector
function assemble_FE_matrix(mesh)
    VX,VY,EToV = unpack_mesh_info(mesh)
    num_vertices = length(VX)
    num_elements = size(EToV,2) # number of elements = of columns

    # rq,sq,wq = 1/3,1/3,2.0
    rq,sq,wq = [-2/3; 1/3; -2/3], [-2/3; -2/3; 1/3], 2/3 * ones(3) # 3 point rule - Gockenbach

    A = spzeros(num_vertices, num_vertices)
    M = spzeros(num_vertices, num_vertices)
    b = zeros(num_vertices)
    for e = 1:num_elements # loop through all elements
        ids = EToV[:,e] # vertex ids = local to global index maps
        xv,yv = VX[ids],VY[ids]

        # compute geometric mappings
        J,drdx,dsdx,drdy,dsdy = compute_geometric_terms(xv,yv)
        dλdx = drdx*dλr() + dsdx*dλs() # dλr() = [dλ1/dr dλ2/dr dλ3/dr]
        dλdy = drdy*dλr() + dsdy*dλs() # dλs() = [dλ1/ds dλ2/ds dλ3/ds]
        reference_elem_area = 2.0
        @. A[ids,ids] += J*reference_elem_area*(dλdx'*dλdx + dλdy'*dλdy) # (dλdx'*dλdx)_ij = dλj/dx * dλi/dx

        # assemble mass matrix using quadrature: ∫ϕ_j * ϕ_i = integrals of quadratic
        M[ids,ids] .+= J*reference_elem_area*(λ(rq,sq)'*Diagonal(wq)*λ(rq,sq)) # M = V^T * diag(weights) * V

        # quadrature rule
        xq,yq = map_triangle_pts(rq,sq,xv,yv)
        b[ids] .+= J*λ(rq,sq)'*(wq.*f.(xq,yq))
    end
    return M,A,b
end

function compute_Neumann_BCs(mesh,ref_elem_info,on_Neumann_boundary,∇u_dot_n)

    @unpack reference_face_indices, reference_vertices = ref_elem_info
    VX,VY,EToV = unpack_mesh_info(mesh)
    boundary_indices, boundary_faces = get_boundary_info(reference_face_indices, mesh)
    # contributions from Neumann BCs

    num_vertices = length(VX)
    b = zeros(num_vertices)
    for (f,e) in boundary_faces
        ref_fids = reference_face_indices[f]
        fids = EToV[ref_fids,e]
        xf,yf = VX[fids],VY[fids]
        x_mid,y_mid = sum(xf)/2,sum(yf)/2

        if on_Neumann_boundary(x_mid,y_mid)
            r,s = reference_vertices
            r_mid, s_mid = sum(r[ref_fids])/2, sum(s[ref_fids])/2
            w_mid = 2.0

            ids = EToV[:,e]
            face_length = sqrt((xf[1]-xf[2])^2 + (yf[1]-yf[2])^2)
            b[ids] .+= face_length/2 * vec(λ(r_mid,s_mid)) * w_mid * ∇u_dot_n(x_mid,y_mid) # ∫ ∇u⋅n ϕ_i on each Neumann face
        end
    end
    return b
end
function modify_matrix_Dirichlet_BCs!(A,mesh,ref_elem_info,on_Dirichlet_boundary)
    @unpack reference_face_indices, reference_vertices = ref_elem_info
    VX,VY,EToV = unpack_mesh_info(mesh)
    boundary_indices, boundary_faces = get_boundary_info(reference_face_indices, mesh)

    # impose Dirichlet BCs
    for i in boundary_indices
        xi,yi = VX[i],VY[i]
        if on_Dirichlet_boundary(xi,yi)
            A[:,i] .= 0
            A[i,:] .= 0
            A[i,i] = 1.0
        end
    end
end
function compute_Dirichlet_BCs(A,mesh,ref_elem_info,on_Dirichlet_boundary,u_Dirichlet)
    @unpack reference_face_indices, reference_vertices = ref_elem_info
    VX,VY,EToV = unpack_mesh_info(mesh)
    boundary_indices, boundary_faces = get_boundary_info(reference_face_indices, mesh)

    num_vertices = length(VX)
    b = zeros(num_vertices)
    # impose Dirichlet BCs
    for i in boundary_indices
        xi,yi = VX[i],VY[i]
        if on_Dirichlet_boundary(xi,yi)
            b -= Vector(A[:,i]*u_Dirichlet(xi,yi))
        end
    end
    return b
end
# modifies the arguments
function constrain_Dirichlet_nodes!(b,mesh,ref_elem_info,on_Dirichlet_boundary,u_Dirichlet)
    @unpack reference_face_indices, reference_vertices = ref_elem_info
    VX,VY,EToV = unpack_mesh_info(mesh)
    boundary_indices, boundary_faces = get_boundary_info(reference_face_indices, mesh)

    for i in boundary_indices
        xi,yi = VX[i],VY[i]
        if on_Dirichlet_boundary(xi,yi)
            b[i] = u_Dirichlet(xi,yi)
        end
    end
end

M,A,b = assemble_FE_matrix(mesh)

# set up timestepping parameters
Δt = .001
T = .5

# create/prefactor Crank-Nicolson matrix
C = (M + .5*Δt*A)
modify_matrix_Dirichlet_BCs!(C,mesh,ref_elem_info,on_Dirichlet)
C = cholesky(Symmetric(C)) # speed up solution of C\b

# precompute BC contributions
b_Neumann = compute_Neumann_BCs(mesh,ref_elem_info,on_Neumann,∇u_dot_n)
b_Dirichlet = compute_Dirichlet_BCs(M+.5*Δt*A,mesh,ref_elem_info,on_Dirichlet,u_Dirichlet)

Nsteps = ceil(T/Δt)
Δt = T/Nsteps
VX,VY,EToV = unpack_mesh_info(mesh)
u = u0.(VX,VY)
plot()
VX,VY,EToV = unpack_mesh_info(mesh) # for plotting
@gif for i = 1:Nsteps
    global u

    # compute RHS
    b = M*u - .5*Δt*A*u

    # compute BC contributions
    @. b += b_Neumann + b_Dirichlet # add BC contributions

    # modify b_i = u[i]
    constrain_Dirichlet_nodes!(b,mesh,ref_elem_info,on_Dirichlet,u_Dirichlet)

    u = C \ b

    if i%5==0
        println("on iter $i out of $Nsteps")
        triplot(VX,VY,u,EToV)
    end
end every 5

# triplot(VX,VY,uexact.(VX,VY),EToV)


"Test the null of normality using the Jarque-Bera test statistic."
function jbtest(x::Vector)
  n = length(x)
  m1 = sum(x)/n
  m2 = sum((x .- m1).^2)/n
  m3 = sum((x .- m1).^3)/n
  m4 = sum((x .- m1).^4)/n
  b1 = (m3/m2^(3/2))^2
  b2 = (m4/m2^2)
  statistic = n * b1/6 + n*(b2 - 3)^2/24
  d = Chisq(2.)
  pvalue = 1. - cdf(d, statistic)
  statistic, pvalue
end


using LinearAlgebra

# k_local(Elastic Modulus,thickness,height,length,number of nodes,row one, row two, column one, column two)

#creates local stiffness matrix and maps to global
function k_local(k,l,n,r_1,r_2,c_1,c_2)
    k_global = zeros(n,n)
    k_global[r_1,c_1] = k
    k_global[r_1,c_2] = -k
    k_global[r_2,c_1] = -k
    k_global[r_2,c_2] = k
    return k_global
end

# constants from problem statement
E = 29*10^6
l = 2
k1 = 50
k2 = 60
k3 = 55
# combining all globally mapped matrices
kG = k_local(k1,l,4,1,2,1,2)+
     k_local(k2,l,4,2,3,2,3)+
     k_local(k3,l,4,3,4,3,4)
kG[4,:] = [0 0 0 1] #adjustment for Reaction from fixed end

# Force Vector
f = Float64[60 0 0 0]
F = transpose(f)

# Displacement vector
u = kG\F
println("Nodal displacement vector (inches):")
display(u)

# Strain Function
ϵ(x2,x1,l) = (x2 - x1)/l

#Strain Vector
ϵM = [ϵ(u[2],u[1],l)
      ϵ(u[3],u[2],l)
      ϵ(u[4],u[3],l) ]
println("Element strain vector (inch/inch):")
display(ϵM)

# Stress Function
σ(E,ϵ) = E*ϵ

# Stress Vector
σM = [σ(E,ϵM[1])
      σ(E,ϵM[2])
      σ(E,ϵM[3])]
println("Element Stress vector (psi):")
display(σM)


using Test, SineWaves, FFTW

@testset "SineWave constructor tests" begin
    frequency = 440.0
    samplerate = 48000.0
    delta = 2*pi*frequency/samplerate
    sinewave = SineWave(frequency, samplerate)

    @test sinewave.current ≈ 0.0
    @test sinewave.previous ≈ -sin(delta)
    @test sinewave.cosine ≈ cos(delta)

    too_hi_frequency = 40000.0

    @test_throws ErrorException SineWave(too_hi_frequency, samplerate)
end

@testset "fill! function tests" begin
    frequency = 440.0
    samplerate = 48000.0
    delta = 2*pi*frequency/samplerate
    N = 512

    sinewave = SineWave(frequency, samplerate)
    buffer = zeros(Float64, N)
    fill!(buffer, sinewave)

    @test buffer ≈ sin.(range(delta; step=delta, length = N))
end

@testset "spectrum function tests" begin
    frequency = 440.0
    samplerate = 48000.0
    delta = 2*pi*frequency/samplerate
    N = 512

    sinewave = SineWave(frequency, samplerate)
    buffer = zeros(Float64, N)
    fill!(buffer, sinewave)

    buffer_ft = FFTW.rfft(buffer)
    expected = abs.(buffer_ft)

    @test spectrum(buffer) ≈ expected
end


import Distributions
using Plots
using Statistics
using Turing
using StatsPlots

function invlogit(x)
	return exp(x)/(1+exp(x))
end

dose = [-0.86,-0.3,-0.05,0.73]
n = ones(4)*5
nd = [0,1,3,5]

@model function ld50_fit(dose,deaths,n)
    α ~ Uniform(-4.0,6.0)
    β ~ Uniform(-5.0,50)
	for i in 1:length(dose)
		Θ = invlogit(α+β*dose[i])
		deaths[i] ~ Binomial(n[i],Θ)
	end
end

model1 = ld50_fit(dose,nd,n)
chain = Turing.sample(model1,NUTS(0.65),1000)

alpha = vec(chain[:α])
beta = vec(chain[:β])

histogram(alpha)
histogram(beta)

ld50 = -alpha ./ beta

histogram(ld50)

mean(ld50)
std(ld50)


#in this file, op_a and op_b are a,b operators in Barry's original paper.
#H,V means horizontal and vertical
#p means '(prime) for output modes

using SymPy
using Printf
using PyCall
struct QRelaySym{T<:Tuple}
    aH::T
    bH::T
    aV::T
    bV::T
    apH::T
    bpH::T
    apV::T
    bpV::T
end

function trans(op, a, b, ap, bp, B)
    p = B * [ap; bp]
    op = subs(op, a, p[1])
    op = subs(op, b, p[2])
    return op
end

#2D rotation matrix
function rotmat(theta)
    c = cos(theta)
    s = sin(theta)
    return [c s; -s c]
end

function qrelay_op(n, phi, alpha, delta)
    #operators before BS
    aH = symbols(@sprintf("a_H1:%d", n+1))
    bH = symbols(@sprintf("b_H1:%d", n+1))
    aV = symbols(@sprintf("a_V1:%d", n+1))
    bV = symbols(@sprintf("b_V1:%d", n+1))

    op = 0
    for i=1:n
        op += phi[i] * (aH[i]*bH[i] + aV[i]*bV[i])
    end

    #beam splitter transformation
    B = 1/sqrt(2)*[1 1;-1 1]

    #operators after beam splitter
    apH = symbols(@sprintf("a'_H1:%d", n+1))
    bpH = symbols(@sprintf("b'_H1:%d", n+1))
    apV = symbols(@sprintf("a'_V1:%d", n+1))
    bpV = symbols(@sprintf("b'_V1:%d", n+1))

    for i=1:n-1
        op = trans(op, bH[i], aH[i+1], bpH[i], apH[i+1], B)
        op = trans(op, bV[i], aV[i+1], bpV[i], apV[i+1], B)
    end

    op = trans(op, aH[1], aV[1], apH[1], apV[1], rotmat(alpha))
    op = trans(op, bH[n], bV[n], bpH[n], bpV[n], rotmat(delta))

    syms = QRelaySym(aH, bH, aV, bV, apH, bpH, apV, bpV)

    return syms, op
end

function op_mat(op)
    op = op[:as_poly](domain="C")
    op_a = op.x[:gens]
    nab = op[:length]()
    op_ab = ones(SymPy.Sym, nab)
    coef = zeros(Complex, nab)
    mat = zeros(Int64, length(op_a), nab)
    for (i, (ps, c)) in enumerate(op[:as_dict]())
        for (j, p) in enumerate(ps)
            mat[j, i] = p
            op_ab[i] = op_a[j]^p * op_ab[i]
        end
        coef[i] = c
    end

    return op_a, op_ab, mat, coef
end


using Gurobi
function sudoku_gurobi(X)
# SUDOKU_GUROBI Solve a sedoku puzzle using Gurobi for an Binary LP
#
# S=sudoku_gurobi(X)
#   X has a set of initial conditions where anything ~= 1 ... 9 is an
#   unknown
# S is the solved sudoku puzzle

    assert(all(size(X) == (9, 9)))
    # create variables
    nvars = 9*9*9

    # index the variables for easier access
    vars = zeros(Int64, 9, 9, 9)
    ind = 1
    for i = 1:9
        for j = 1:9
            for v = 1:9
                vars[i, j, v] = ind
                ind = ind + 1
            end
        end
    end

    #obj = ones(Int64, 1, nvars) # no objective, just constraints

    # add known cells as constraints

    A = spzeros(nvars, 0)
    b = ones(1, 0)

    for i = 1:9
        for j = 1:9
            if X[i, j] < 1 || X[i, j] > 9
                # this is a free variable, ignore
            else
                # this is a constraint, add it
                cons = zeros(nvars)
                cons[vars[i, j, X[i, j]]] = 1
                A = [A sparse(cons)] # append a row
                b = [b 1]
            end
        end
    end

    #
    # add sudoku constraints
    #

    # each cell only has one value
    for i = 1:9
        for j = 1:9
            cons = zeros(Int64, nvars)
            for v = 1:9
                cons[vars[i, j, v]] = 1
            end
            A = [A sparse(cons)]
            b = [b 1]
        end
    end

    # each row has 1-9
    for i = 1:9
        for v = 1:9
            cons = zeros(nvars)
            # the value v only appears in each row once
            for j = 1:9
                cons[vars[i, j, v]] = 1
            end
            A = [A sparse(cons)]
            b = [b 1]
        end
    end

    # each column has 1-9
    for j = 1:9
        for v = 1:9
            cons = zeros(nvars)
            # the value v only appears in each row once
            for i = 1:9
                cons[vars[i, j, v]] = 1
            end
            A = [A sparse(cons)]
            b = [b 1]
        end
    end

    # subgrid has 1-9
    for v = 1:9
        for i0 = 0:2
            for j0 = 0:2
                cons = zeros(nvars)
                for i = 1:3
                    for j = 1:3
                        cons[vars[i+3*i0, j+3*j0, v]] = 1
                    end
                end
                A = [A sparse(cons)]
                b = [b 1]
            end
        end
    end

    # setup Gurobi call
    env = Gurobi.Env()

    model = Gurobi.Model(env, "sudoku_blp", :minimize)

    # add binary variables
    add_bvars!(model, zeros(nvars))

    # have the variables incorporated into the model
    update_model!(model)

    add_constrs_t!(model, A, '=', vec(b))

    # run optimization
    optimize(model)

    # get results
    x = get_solution(model)

    # assign solution
    S = zeros(Int64, 9 ,9)
    for i = 1:9
        for j = 1:9
            for v = 1:9
                if x[vars[i, j, v]] > 0
                    S[i, j] = v
                end
            end
        end
    end
    return S
end

##
X = [0 0 8 0 0 0 6 0 0;
     0 2 0 0 0 0 0 1 0;
     5 0 0 6 0 0 0 0 9;
     0 0 7 0 4 0 0 0 3;
     0 4 0 2 0 9 0 6 0;
     6 0 0 0 1 0 8 0 0;
     9 0 0 0 0 7 0 0 4;
     0 5 0 0 0 0 0 7 0;
     0 0 1 0 0 0 5 0 0];
S = sudoku_gurobi(X)

##

X = [0 0 4 9 3 0 0 0 5
     2 8 0 0 0 0 0 6 0
     0 1 0 0 2 0 0 4 0
     0 0 5 0 4 0 0 0 7
     0 2 0 0 0 5 0 0 0
     0 0 0 0 0 6 0 0 0
     0 0 0 1 0 0 0 8 2
     8 0 0 0 9 0 0 0 1
     0 7 0 0 0 0 0 0 0];
S = sudoku_gurobi(X)

##
X = [0 6 0 0 0 0 9 0 0;
     1 7 0 0 0 0 0 0 0;
     0 0 5 0 1 0 0 8 6;
     0 0 0 0 0 0 7 0 0;
     0 0 0 0 8 6 0 9 0;
     0 4 0 0 9 0 0 0 2;
     0 3 8 0 0 0 0 1 0;
     6 0 0 0 0 5 0 2 0;
     0 0 0 0 0 0 3 0 0];
S = sudoku_gurobi(X)

##
# http://www.nytimes.com/crosswords/game/sudoku/hard


doc"""
    Cauchy(μ, σ)

The *Cauchy distribution* with location `μ` and scale `σ` has probability density function

$f(x; \mu, \sigma) = \frac{1}{\pi \sigma \left(1 + \left(\frac{x - \mu}{\sigma} \right)^2 \right)}$

```julia
Cauchy()         # Standard Cauchy distribution, i.e. Cauchy(0.0, 1.0)
Cauchy(u)        # Cauchy distribution with location u and unit scale, i.e. Cauchy(u, 1.0)
Cauchy(u, b)     # Cauchy distribution with location u and scale b

params(d)        # Get the parameters, i.e. (u, b)
location(d)      # Get the location parameter, i.e. u
scale(d)         # Get the scale parameter, i.e. b
```

External links

* [Cauchy distribution on Wikipedia](http://en.wikipedia.org/wiki/Cauchy_distribution)

"""
immutable Cauchy <: ContinuousUnivariateDistribution
    μ::Float64
    σ::Float64

    Cauchy(μ::Real, σ::Real) = (@check_args(Cauchy, σ > zero(σ)); new(μ, σ))
    Cauchy(μ::Real) = new(μ, 1.0)
    Cauchy() = new(0.0, 1.0)
end

@distr_support Cauchy -Inf Inf

#### Parameters

location(d::Cauchy) = d.μ
scale(d::Cauchy) = d.σ

params(d::Cauchy) = (d.μ, d.σ)

#### Statistics

mean(d::Cauchy) = NaN
median(d::Cauchy) = d.μ
mode(d::Cauchy) = d.μ

var(d::Cauchy) = NaN
skewness(d::Cauchy) = NaN
kurtosis(d::Cauchy) = NaN

entropy(d::Cauchy) = log4π + log(d.σ)

#### Functions

zval(d::Cauchy, x::Float64) = (x - d.μ) / d.σ
xval(d::Cauchy, z::Float64) = d.μ + z * d.σ

pdf(d::Cauchy, x::Float64) = 1.0 / (π * scale(d) * (1.0 + zval(d, x)^2))
logpdf(d::Cauchy, x::Float64) = - (log1psq(zval(d, x)) + logπ + log(d.σ))

function cdf(d::Cauchy, x::Float64)
    μ, σ = params(d)
    invπ * atan2(x - μ, σ) + 0.5
end

function ccdf(d::Cauchy, x::Float64)
    μ, σ = params(d)
    invπ * atan2(μ - x, σ) + 0.5
end

function quantile(d::Cauchy, p::Float64)
    μ, σ = params(d)
    μ + σ * tan(π * (p - 0.5))
end

function cquantile(d::Cauchy, p::Float64)
    μ, σ = params(d)
    μ + σ * tan(π * (0.5 - p))
end

mgf(d::Cauchy, t::Real) = t == zero(t) ? 1.0 : NaN
cf(d::Cauchy, t::Real) = exp(im * (t * d.μ) - d.σ * abs(t))

#### Fitting

# Note: this is not a Maximum Likelihood estimator
function fit{T<:Real}(::Type{Cauchy}, x::AbstractArray{T})
    l, m, u = quantile(x, [0.25, 0.5, 0.75])
    Cauchy(m, (u - l) / 2.0)
end


using StatsBase: sample
using UnicodePlots: spy
using Printf

abstract type SEM end

struct GaussianSEM <: SEM
    p       ::Int64
    B       ::Matrix{Float64}
    err_var ::Vector{Float64}
    function GaussianSEM(B, err_var)
        @assert size(B, 1) == size(B, 2) == length(err_var)
        @assert all(err_var .> 0)
        p = length(err_var)
        new(p, B, err_var)
    end
end

function Base.show(io::IO, sem::GaussianSEM)
    print(io, "Gaussian SEM with $(sem.p) variables:\n")
    print(io, "B = \n")
    print(io, spy(sem.B))
    print(io, "σ² = $(sem.err_var)")
end

"""
    simulate(sem, [n])
    simulate(sem, [do_variables, do_values], [n])

Simulate from a Gaussian SEM `sem`. `n` is the sample size.
do-interventions can be performed by specifying vectors of `do_variables` and `do_values`.
"""
function simulate(sem::GaussianSEM)
    p = sem.p
    ϵ = randn(p) .* sqrt.(sem.err_var)
    return (I - sem.B) \ ϵ
end

function simulate(sem::GaussianSEM, do_variables::Vector{Int64}, do_values::Vector{Float64})
    @assert length(do_variables) == length(do_values)
    p = sem.p
    ϵ = randn(p) .* sqrt.(sem.err_var)
    ϵ[do_variables] .= do_values
    B = copy(sem.B)
    B[do_variables, :] .= 0
    return (I - B) \ ϵ
end

function simulate(sem::GaussianSEM, n::Int64)
    return vcat(map(i -> simulate(sem), 1:n)'...)
end

function simulate(sem::GaussianSEM, do_variables::Vector{Int64}, do_values::Vector{Float64}, n::Int64)
    return vcat(map(i -> simulate(sem, do_variables, do_values), 1:n)'...)
end

function causes(sem::SEM, i::Int64)
    @assert 1 <= i <= sem.p
    return (1:sem.p)[sem.B[i, :].!=0]
end

function cov(sem::GaussianSEM)
    S = inv(I - sem.B)
    return S * diagm(sem.err_var) * S'
end

"""
    random_gaussian_SEM(p, k; [lb=-2, ub=2, var_min=0.5, var_max=2])

Generate a random-graph acyclic SEM with `p` variables and `k` average degree, and random coefficients.
* `lb`, `ub`: coeff  ~ unif[`lb`, `ub`] with random sign
* `var_min`, `var_max`: var of error ~ unif[`var.min`, `var.max`]
"""
function random_gaussian_SEM(p::Int64, k::Int64; lb=-2, ub=2, var_min=0.5, var_max=2)
    B = zeros(p, p)
    B[rand(p, p) .< 2k / (p-1)] .= 1
    B[UpperTriangular(B).!=0] .= 0
    m = sum(B.==1)
    B[B.==1] .= (rand(m) * (ub - lb) .+ lb) .* sign.(randn(m))
    err_var = rand(p) * (var_max - var_min) .+ var_min
    _order = sample(1:p, p, replace=false)
    B = B[_order, _order]
    return GaussianSEM(B, err_var)
end

"""
    random_noise_intervened_SEM(sem::GaussianSEM, [p_intervened=2, noise_multiplier_min=0.5, noise_multiplier_max=2., avoid=[],
                                                   prob_coeff_unchanged=2/3, lb=-2, ub=2])

Produce a new SEM based on original SEM by changing coefficients and noise variances.
* `p_intervened`: randomly choose `p_intervened` variables to intervene; will avoid those specified in `avoid`
* [`noise_multiplier_min`, `noise_multiplier_max`]: interval that noise multiplier is uniformly sampled from
* `prob.coeff.unchanged`: probability that coefficient is not changed
* `[lb, ub]`: if to change, coefficient is drawn uniformly from this interval with random sign

Return: `sem_new`, `intervened_variables`
"""
function random_noise_intervened_SEM(sem::GaussianSEM;
                                     p_intervened=2, noise_multiplier_min=0.5, noise_multiplier_max=2., avoid=[],
                                     prob_coeff_unchanged=2/3, lb=-2, ub=2)
    B = copy(sem.B)
    p = sem.p
    err_var = copy(sem.err_var)
    vars = sample(setdiff(collect(1:p), avoid), p_intervened, replace=false)
    for i in vars
        noise_multiplier = rand() * (noise_multiplier_max - noise_multiplier_min) + noise_multiplier_min
        err_var[i] = err_var[i] * noise_multiplier
        if rand() > prob_coeff_unchanged
            _J = (1:p)[B[i, :].!=0]
            B[i, _J] .= rand(length(_J)) * (ub - lb) .+ lb
        end
    end
    return GaussianSEM(B, err_var), vars
end


"""
    crosscorrelate(series, template, [element_type=Float64], normalize_template=true)

Return the normalized cross-correlation between `series` and `template`.

If `series` is a vector of ``n`` elements and template a vector of ``N`` elements,
cross-correlation will be a vector of ``n - N + 1`` elements of type `element_type`.
The element of cross-correlation ``χ_k`` is Pearson correlation coefficient between
``x^{(k)}_i`` denoting the elements of `series` ``x_{i + k}`` and
the elements of `template` ``y_i``, with ``i = 1, 2, \\dots, N``

``
χ_k = \\frac{\\sum{\\left(x^{(k)}_i - μ_{x^{(k)}}\\right)
\\left(y_i - μ_y\\right)}}{N σ_{x^{(k)}} σ_y} \\, ,
``

where ``μ`` and ``σ`` denotes the mean and variance respectively.

If `normalize_template` is set to false then `template` is assumed to have mean and std
respectively equal to 0.0 and 1.0.

See the [Wikipedia page](https://en.wikipedia.org/wiki/Cross-correlation#Normalization)
on cross-correlation for more details.

# Examples

```jldoctest
julia> crosscorrelate(sin.(0:0.25pi:2pi), [1, 1+√2, 1])
7-element Vector{Float64}:
  0.23258781949447394
  1.000000000000001
  0.23258781949447402
  7.401486830834377e-17
 -0.23258781949447394
 -1.0000000000000007
 -0.23258781949447394
```
"""
function crosscorrelate(series::AbstractVector{T1}, template::AbstractVector{T2}, element_type::Type{T3}=Float64;
    normalize_template=true) where {T1 <: Number, T2 <: Number, T3 <: AbstractFloat}

    if isempty(series)
        throw(ArgumentError("Series must be a non-empty vector."))
    elseif isempty(template)
        throw(ArgumentError("Template must be a non-empty vector."))
    elseif size(series) < size(template)
        throw(DimensionMismatch("Template is longer than series."))
    end

    if normalize_template
        template_mean, template_std = mean_and_std(template, corrected=false)
        y = @. (template - template_mean) / template_std
    else
        y = template
    end

    N = length(template)
    cc = Vector{element_type}(undef, length(series) - N + 1)
    x = similar(y)
    for n in eachindex(cc)
        series_view = view(series, n:n + N - 1)
        series_view_std = std(series_view, corrected=false)
        @. x = series_view / series_view_std
        cc[n] = dot(x, y) / N
    end
    cc
end

"""
    maxfilter(x, tolerance)

Return a vector of the same size of `x` whose element at index `n` is the maximum
of the elements of `x` which are at most `tolerance` apart from `x[n]`
(i.e. in a window of at most `2 * tolerance + 1` elements).

# Examples

```jldoctest
julia> maxfilter(sin.(0:0.25pi:2pi), 1)
9-element Vector{Float64}:
  0.7071067811865475
  1.0
  1.0
  1.0
  0.7071067811865476
  1.2246467991473532e-16
 -0.7071067811865475
 -2.4492935982947064e-16
 -2.4492935982947064e-16
```
"""
function maxfilter(x::AbstractVector, l)
    y = similar(x)
    for n in eachindex(x)
        lower = max(n - l, firstindex(x))
        upper = min(n + l, lastindex(x))
        y[n] = maximum(view(x, lower:upper))
    end
    y
end

"""
    stack(correlations, offsets)

Return the average cross-correlation after aligning `correlations`.
Each cross-correlation `correlations[n]` is shifted to the right by `offsets[n]`.

# Examples

```jldoctest
julia> stack([[0, 1.0, 0, 0], [0, 0, 1.0, 0]], [1, 2])
3-element OffsetArray(::Vector{Float64}, 0:2) with eltype Float64 with indices 0:2:
 0.0
 1.0
 0.0
```
"""
function stack(correlations::AbstractVector{T1}, offsets::AbstractVector{T2}) where {T1 <: AbstractVector, T2 <: Integer}
    if isempty(correlations)
        throw(ArgumentError("Cross-correlations vector must be non-empty."))
    elseif length(correlations) != length(offsets)
        throw(DimensionMismatch("Cross-correlations and offsets vectors must have the same length."))
    end
    stackedcorrelations = OffsetVector.(correlations, -offsets)
    start = maximum(series -> firstindex(series), stackedcorrelations)
    stop = minimum(series -> lastindex(series), stackedcorrelations)
    OffsetVector(mean(series -> view(series, start:stop), stackedcorrelations), start:stop)
end

"""
    correlatetemplate(data, template, offsets, tolerance, [element_type=Float64])

Return the cross-correlation between `data` and `template`. The cross-correlations for each series
are aligned using `offsets` and averaged. If `tolerance`` is not zero, then the average accounts for possible
misplacement of each series by `tolerance` sample, and return the average of the maximum cross-correlation
compatible with that misplacement.
"""
function correlatetemplate(data, template, offsets, tolerance, element_type=Float64)
    if isempty(data)
        throw(ArgumentError("Data must be non-empty."))
    elseif !(length(data) == length(template) == length(offsets))
        throw(DimensionMismatch("Data, template and shifts must have the same length."))
    end
    correlations = similar(data, Vector{element_type})
    Threads.@threads for n = eachindex(data)
        correlations[n] = maxfilter(crosscorrelate(data[n], template[n], element_type), tolerance)
    end
    stack(correlations, offsets)
end


using Statistics
function demere(num::Int)
    numrolls = 4
    numsixes = 0
    for run in 1:num
        roll = rand(1:6, numrolls, 1)
        if any( roll .== 6)
            numsixes += 1
        end
    end
    probsix = numsixes/num
    return probsix
end

function roulette(k, t, num::Int)
    p = 18/37
    numruin = 0
    numbet = 0
    for run in 1:num
        money = k
        while money > 0 && money < t
            if rand(1)[1] < p
                money += 1
            else
                money -= 1
            end
            numbet += 1
        end
        if money == 0
            numruin += 1
        end
    end
    probruin = numruin / num
    averagebet = numbet / num
    return probruin, averagebet
end

function hitmiss(f::Function, box = [0; 1], num=10000; fmin=0, fmax=1)
   x = (box[2]-box[1]).*rand(num,1) .+ box[1]
   y = (fmax-fmin).*rand(num,1) .+ fmin
   rectarea = (fmax-fmin)*(box[2]-box[1])
   prop = sum(y.<f.(x))/num
   return prop*rectarea
end

function crudemc(f::Function, box = [0;1], num=10000)
   x = (box[2]-box[1]).*rand(num,1) .+ box[1]
   fmean = mean(f.(x))
   return (box[2]-box[1])*fmean
end


#===============================================================================

Using a combination of black square tiles and oblong tiles chosen from: red
tiles measuring two units, green tiles measuring three units, and blue tiles
measuring four units, it is possible to tile a row measuring five units in
length in exactly fifteen different ways.

How many ways can a row measuring fifty units in length be tiled?

===============================================================================#

using Memoize
using Base.Test

@memoize function ncases_swb(rowlen)
    # Return the number of cases starting with a block

    ncases_swglb(2, rowlen) + ncases_swglb(3, rowlen) + ncases_swglb(4, rowlen)
end

@memoize function ncases_swglb(blocklen, rowlen)
    # Return the number of cases starting with a block of a given length
    if rowlen < blocklen
        return 0
    end

    result = 1
    # case for which there is only the starting block

    rowlen -= blocklen
    # account for the head block

    for nspace = 0:rowlen-2
        # nbr spaces allowed between first block and second one
        result += ncases_swb(rowlen-nspace)
    end

    result
end

function ncases(rowlen)
    result = 1
    # No block at all

    for nspace = 0:rowlen-2
        # nbr spaces before first block
        result += ncases_swb(rowlen-nspace)
    end

    result
end

@test ncases(5) == 15

@printf("Euler 0117: %d", ncases(50))


### Load in with
### include(download("https://raw.githubusercontent.com/mth229/MTH229.jl/master/src/229.jl"))

using Pkg
pkg"add SymPy SpecialFunctions Roots ForwardDiff Plots QuadGK"

using Plots
using SpecialFunctions

import ForwardDiff
import QuadGK: quadgk

" f'(x) will find the derivative of `f` using Automatic Differentation from the `ForwardDiff` package "
Base.adjoint(f::Function) = x -> ForwardDiff.derivative(f, float(x))
D(f, n=1) = n > 1 ? D(D(f), n-1) : x -> ForwardDiff.derivative(f, float(x))

"""
Returns a function describing the tangent line to the graph of f at x=c.

Example. Where does the tangent line intersect the y axis?
```
f(x) = sin(x)
tl(x) = tangent(f, pi/4)(x)  # or tl = tangent(f, pi/3) to use a non-generic function
tl(0)
```

Uses the automatic derivative of `f` to find the slope of the tangent line at `x=c`.

"""
tangent(f,c) = x -> f(c) + f'(c) * (x-c)

"""
Returns a function describing the secant line to the graph of f at x=a and x=b.

Example. Where does the secant line intersect the y axis?
```
f(x) = sin(x)
a, b = pi/4, pi/3
sl(x) = secant(f, a, b)(x)  # or sl = sl(f, a, b) to use a non-generic function
sl(0)
```

"""
secant(f, a, b) = x -> f(a) + (f(b) - f(a)) / (b-a) * (x - a)

"""

`lim(f, c, n, dir="+")`: means to generate numeric table of values of `f` as `h` gets close to `c`.

Example:
```
f(x) = sin(x) / x
lim(f, 0)
```
"""
function lim(f::Function, c::Real; n::Int=6, dir="+")
	 hs = [(1/10)^i for i in 1:n] # close to 0
	 if dir == "+"
	   xs = c .+ hs
	 else
	   xs = c .- hs
	 end
	 ys = map(f, xs)
	 [xs ys]
end

"""

Simple implementation of the bisection method.

Example:

```julia
bisection(sin, 3, 4)
f(x) = x^5 - x^4 - x^3 - x^2 - x - 1
a = bisection(f, 1, 2)
f(a)
```

The display shows a simple graphic illustrating the method's division for the first few steps.

An easier-to-understand alternative to `Roots.find_zero(f, (a,b), Bisection())`.

"""
function bisection(f::Function, a, b)
    a,b = sort([a,b])

    if f(a) * f(b) > 0
        error("[a,b] is not a bracket. A bracket means f(a) and f(b) have different signs!")
    end

    M = a + (b-a) / 2

    i, j = 0, 64
    ss = fill("#", 65)
    ss[i+1]="a"; ss[j+1]="b"
    println("")
    println(join(ss))
    flag = true

    while a < M < b
        if flag && j-i == 1
            ss = fill(" ", 65)
            ss[j:(j+1)] = "⋮"
            println(join(ss))
            println("")
            flag = false
        end

        if f(M) == 0.0
            println("... exact answer found ...")
	    break
        end
        ## update step
	if f(a) * f(M) < 0
	    a, b = a, M

            if flag
                j = div(i + j, 2)
            end

	else
	    a, b = M, b

            if flag
                i = div(i + j, 2)
            end

	end

        if flag
            ss = fill(".", 65)
            ss[i+1]="a"; ss[j+1]="b"; ss[(i+2):j]="#"
            println(join(ss))
        end

        M = a + (b-a) / 2
    end
    M
end

import Roots
import Roots: newton, find_zero, find_zeros
newton(f, fp, x0; kwargs...) = Roots.find_zero((f,fp), x0, Roots.Newton(); kwargs...)
newton(f, x0; kwargs...) = newton(f, D(f), x0; kwargs...)
fzero(f, x0; kwargs...) = Roots.find_zero(f, x0; kwargs...)
fzero(f, a, b; kwargs...) = Roots.find_zero(f, (a, b); kwargs...)
fzeros(f, a, b; kwargs...) = Roots.find_zeros(f, a, b; kwargs...)

# some plotting utilities

"""
   trimplot(f, a, b, c=20; kwargs...)

Plot f over [a,b] but break graph if it exceeds c in absolute value.
"""
function trimplot(f, a, b, c=20; kwargs...)
  xs = linspace(a, b, 251) #range(a, stop=b, length=251)
  ys = f.(xs)

  us, vs = Real[], Real[]
  p = plot(us, vs, xlim=(a, b), legend=false, kwargs...)
  for (x,y) in zip(xs, ys)
    if abs(y) <= c
       push!(us, x); push!(vs, y)
    else
      length(us) > 0 && plot!(p, us, vs, color=:blue)
      empty!(us); empty!(vs)
    end
 end
 length(us) > 0 && plot!(p, us, vs, color=:blue)
 p
end

"""
    plotif(f, g, a, b)

Plot f colored depending on g < 0 or not.
"""
function plotif(f, g, a, b)
  plot([f, x -> g(x) > 0.0 ? f(x) : NaN], a, b, linewidth=5)
end

"""
   signchart(f, a, b)

Plot f over a,b with different color when negative.
"""
function signchart(f, a, b)
    p = plotif(f, f, a, b)
    plot!(p, zero)
    p
end

# visualize newtons method
function newton_vis(f, x0, a=Inf,b=-Inf; steps=5, kwargs...)
    xs = Float64[x0]
    for i in 1:steps
        push!(xs, xs[end] - f(xs[end]) / f'(xs[end]))
    end

    m,M = extrema(xs)
    m = min(m, a)
    M = max(M, b)

    p = plot(f, m, M; linewidth=3, legend=false, kwargs...)
    plot!(p, zero, m, M)
    for i in 1:steps
        plot!(p, [xs[i],xs[i],xs[i+1]], [0,f(xs[i]), 0])
        scatter!(p, xs[i:i],[0])
    end
    scatter!(p, [xs[steps+1]], [0])
    p
end

"""
riemann: compute Riemann sum approximations to a definite integral. As well, implement trapezoid and Simpson's rule.

Example:
```
f(x) = exp(x^2)
riemann(f, 0, 1, 1000)   # default right-Riemann sums
riemann(f, 0, 1, 1000, method="left")       # left sums
riemann(f, 0, 1, x1000, method="trapezoid")  # use trapezoid rule
riemann(f, 0, 1, 1000, method="simpsons")   # use Simpson's rule
```

"""
function riemann(f::Function, a::Real, b::Real, n::Int; method="right")
  if method == "right"
     meth = (f,l,r) -> f(r) * (r-l)
  elseif method == "left"
     meth= (f,l,r) -> f(l) * (r-l)
  elseif method == "trapezoid"
     meth = (f,l,r) -> (1/2) * (f(l) + f(r)) * (r-l)
  elseif method == "simpsons"
     meth = (f,l,r) -> (1/6) * (f(l) + 4*(f((l+r)/2)) + f(r)) * (r-l)
  end

  xs = a + (0:n) * (b-a)/n
  as = [meth(f, l, r) for (l,r) in zip(xs[1:end-1], xs[2:end])]
  sum(as)
end


"""
    BoolDistribution(p_true)

Create a distribution over Boolean values (`true` or `false`).

`p_true` is the probability of the `true` outcome; the probability of `false` is 1-`p_true`.
"""
struct BoolDistribution
    p::Float64 # probability of true
end

pdf(d::BoolDistribution, s::Bool) = s ? d.p : 1.0-d.p

rand(rng::AbstractRNG, d::BoolDistribution) = rand(rng) <= d.p

Base.iterate(d::BoolDistribution) = ((d.p, true), true)
function Base.iterate(d::BoolDistribution, state::Bool)
    if state
        return  ((1.0 - d.p, false), false)
    else
        return nothing
    end
end

support(d::BoolDistribution) = [true, false]

==(d1::BoolDistribution, d2::BoolDistribution) = d1.p == d2.p

Base.hash(d::BoolDistribution) = hash(d.p)

Base.length(d::BoolDistribution) = 2


# Julia translation of http://nbviewer.jupyter.org/github/barbagroup/AeroPython/blob/master/lessons/01_Lesson01_sourceSink.ipynb
# Lession 1 Source and sink

using PyPlot
using Distributions

close("all")
meshgrid(x,y) = (repmat(x',length(y),1),repmat(y,1,length(x)))

N = 40                                # number of points in each direction
x_start, x_end = -2.0, 2.0            # boundaries in the x-direction
y_start, y_end = -1.0, 1.0            # boundaries in the y-direction
x = linspace(x_start, x_end, N)    # creates a 1D-array with the x-coordinates
y = linspace(y_start, y_end, N)    # creates a 1D-array with the y-coordinates

# print("x = ", x)
# print("y = ", y)

X,Y=meshgrid(x,y)
# X = repmat(x',N,1)
# Y= repmat(y,1,N)

# plots the grid of points
size = 10
PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel('x', fontsize=16)
PyPlot.ylabel('y', fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
PyPlot.scatter(X, Y)

strength_source = 5.0                      # source strength
x_source, y_source = -1.0, 0.0             # location of the source

# computes the velocity field on the mesh grid

u_source = strength_source/(2*pi) .* (X-x_source)./((X-x_source).^2 + (Y-y_source).^2)
v_source = strength_source/(2*pi) .* (Y-y_source)./((X-x_source).^2 + (Y-y_source).^2)

# plotting the streamlines
size = 10
PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel('x', fontsize=16)
PyPlot.ylabel('y', fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
PyPlot.streamplot(X, Y, u_source, v_source, density=2, linewidth=1, arrowsize=2, arrowstyle="->")
PyPlot.scatter(x_source, y_source, color="#CD2305", s=80, marker="o", linewidth=0);

strength_sink = -5.0                     # strength of the sink
x_sink, y_sink = 1.0, 0.0                # location of the sink

# computes the velocity on the mesh grid
u_sink = strength_sink/(2*pi) .* (X-x_sink)./((X-x_sink).^2 + (Y-y_sink).^2)
v_sink = strength_sink/(2*pi) .* (Y-y_sink)./((X-x_sink).^2 + (Y-y_sink).^2)

# plots the streamlines
size = 10
PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel('x', fontsize=16)
PyPlot.ylabel('y', fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
PyPlot.streamplot(X, Y, u_sink, v_sink, density=2, linewidth=1, arrowsize=2, arrowstyle="->")
PyPlot.scatter(x_sink, y_sink, color="#CD2305", s=80, marker="o", linewidth=0);

# computes the velocity of the pair source/sink by superposition
u_pair = u_source + u_sink
v_pair = v_source + v_sink

# plots the streamlines of the pair source/sink
size = 10
PyPlot.figure(figsize=(size, (y_end-y_start)/(x_end-x_start)*size))
PyPlot.xlabel('x', fontsize=16)
PyPlot.ylabel('y', fontsize=16)
PyPlot.xlim(x_start, x_end)
PyPlot.ylim(y_start, y_end)
PyPlot.streamplot(X, Y, u_pair, v_pair, density=2.0, linewidth=1, arrowsize=2, arrowstyle="->")
PyPlot.scatter([x_source, x_sink], [y_source, y_sink],
             color="#CD2305", s=80, marker="o", linewidth=0);


# Demonstrate/test functions in cornerfeatures

using ImageProjectiveGeometry
using FileIO
using PyPlot

rad = 6

#img = PyPlot.imread("../sampleimages/lena.tif");
img = load("../sampleimages/lena.tif");

# Convert image from Gray Fixed Point Numbers to a simple array of floats
img = float(img);
figure(10), PyPlot.imshow(img);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Input image")

println("Harris corner detector")
(cimg, r, c) = ImageProjectiveGeometry.harris(img, 1,k= 0.04, radius=rad, N=100, subpixel=false, img=img, fig=12)
figure(12); PyPlot.title("Harris corners")
cimg[cimg .< 0] .= 0;   # eliminate -ve values
figure(2); PyPlot.imshow(cimg.^0.25);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Harris corner strength image")
ImageProjectiveGeometry.keypause()

println("Noble corner detector")
(cimg, r, c) = ImageProjectiveGeometry.noble(img, 1, radius=rad, N=100, subpixel=false, img=img, fig=12);
figure(12); PyPlot.title("Noble corners")
figure(2); PyPlot.imshow(cimg.^0.25);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Noble corner strength image")
ImageProjectiveGeometry.keypause()

println("Shi-Tomasi corner detector")
(cimg, r, c) = ImageProjectiveGeometry.shi_tomasi(img, 1, radius=rad, N=100, subpixel=false, img=img, fig=12);
figure(12); PyPlot.title("Shi_Tomasi corners")
figure(2); PyPlot.imshow(cimg.^0.25);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Shi-Tomasi corner strength image")
ImageProjectiveGeometry.keypause()

println("-ve Hessian features")
hdet = ImageProjectiveGeometry.hessianfeatures(img, 1)
(r, c) = ImageProjectiveGeometry.nonmaxsuppts(-hdet, N=100, img=img, fig=12)
figure(12); PyPlot.title("-ve Hessian features")
figure(2); PyPlot.imshow(-hdet);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("-ve Hessian image")
ImageProjectiveGeometry.keypause()

println("+ve Hessian features")
hdet = ImageProjectiveGeometry.hessianfeatures(img, 1)
(r, c) = ImageProjectiveGeometry.nonmaxsuppts(+hdet, N=100, img=img, fig=12)
figure(12); PyPlot.title("+ve Hessian features")
figure(2); PyPlot.imshow(hdet);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("+ve Hessian image")
ImageProjectiveGeometry.keypause()

println("Coherence")
cimg = ImageProjectiveGeometry.coherence(img, 2)
(r, c) = ImageProjectiveGeometry.nonmaxsuppts(cimg, N=10, img=img, fig=12)
figure(2); PyPlot.imshow(cimg);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Coherence image")
ImageProjectiveGeometry.keypause()

println("Fast radial")
(S, So) = ImageProjectiveGeometry.fastradial(img, [1, 3, 5, 7])
(r, c) = ImageProjectiveGeometry.nonmaxsuppts(-S, N=10, img=img, fig=12)
figure(12); PyPlot.title("Fast radial features")
figure(2); PyPlot.imshow(-S);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Fast radial image: orientation and gradient magnitude image")

figure(3); PyPlot.imshow(-So);
PyPlot.set_cmap(PyPlot.ColorMap("gray"))
PyPlot.title("Fast radial image: orientation only image")

nothing


include("orthopoly_evaluate.jl")
function compute_hermite(n, x, all=true);
    # Evaluate Orthonormalized Hermite polynomials at x
    # Physicists Hermite:
    #       w(x)=exp(-x^2) with integral sqrt(pi)
    #       Equivalent Mathematica: HermiteH[n,x]/Sqrt[Sqrt[Pi]*n!*2^n]
    # Probabilists Hermite:
    #       w(x)=exp(-x^2/2) with integral sqrt(2*pi)
    #       Equivalent Mathematica: HermiteH[n,x/Sqrt[2]]/Sqrt[Sqrt[Pi]*n!*2^(1/2+n)]

    ## Physicists Hermite Tridiagonal matrix (w=exp(-x^2))
    T = diagm(sqrt((1:n)/2),1) # n+1 by n+1 matrix
    T = T+T'
    c = sqrt(pi) #Integral of weight function
    ## Probabilists Hermite Tridiagonal matrix (w=exp(-x^2/2))
    # T = diagm(sqrt((1:n)),1) # n+1 by n+1 matrix
    # T = T+T'
    # c = sqrt(2*pi)
    ## Default to 0 through n computation
    if all
        phi = orthopoly_evaluate_all(T, x)
    else
        phi = orthopoly_evaluate_n(T, x)
    end
    phi/sqrt(c)
end


#Special named numbers and symbols

export bellnum,
    catalannum,
    lobbnum,
    narayana,
    fibonaccinum,
    jacobisymbol,
    lassallenum,
    legendresymbol,
    lucasnum,
    stirlings1,
    stirlings2,
    dealnnoy

"""
    bellnum(n)

Compute the ``n``th Bell number.
"""
function bellnum(n::Integer)
    if n < 0
        throw(DomainError(n))
    elseif n < 2
        return 1
    end
    list = Vector{BigInt}(undef, n)
    list[1] = 1
    for i = 2:n
        for j = 1:i - 2
            list[i - j - 1] += list[i - j]
        end
        list[i] = list[1] + list[i - 1]
    end
    return list[n]
end

"""
    catalannum(n)

Compute the ``n``th Catalan number.
"""
function catalannum(bn::Integer)
    if bn < 0
        throw(DomainError(bn, "n must be nonnegative"))
    else
        n = BigInt(bn)
    end
    div(binomial(2*n, n), (n + 1))
end

"""
    lobbnum(m,n)

Compute the Lobb number `L(m,n)`, or the generalised Catalan number given by ``\\frac{2m+1}{m+n+1} \\binom{2n}{m+n}``.
Wikipedia : https://en.wikipedia.org/wiki/Lobb_number
"""
function lobbnum(bm::Integer,bn::Integer)
    if !(0 <= bm <= bn)
        throw(DomainError("m and n must be non-negative"))
    else
        m = BigInt(bm)
        n = BigInt(bn)
    end
    div(binomial(2*n, m + n)*(2*m + 1), (m + n + 1))
end

"""
    narayana(n,k)

Compute the Narayana number `N(n,k)`` given by ``\\frac{1}{n}\\binom{n}{k}\\binom{n}{k-1}``
Wikipedia : https://en.wikipedia.org/wiki/Narayana_number
"""
function narayana(bn::Integer,bk::Integer)
    if !(1 <= bk <= bn)
        throw(DomainError("Domain is 1 <= k <= n"))
    else
        n = BigInt(bn)
        k = BigInt(bk)
    end
    div(binomial(n, k)*binomial(n, k - 1) , n)
end

function fibonaccinum(n::Integer)
    if n < 0
        throw(DomainError(n, "n must be nonnegative"))
    end
    z = Ref{BigInt}(0)
    ccall((:__gmpz_fib_ui, :libgmp), Cvoid, (Ref{BigInt}, UInt), z, UInt(n))
    return z[]
end

function jacobisymbol(a::Integer, b::Integer)
    ba = Ref{BigInt}(a)
    bb = Ref{BigInt}(b)
    return ccall((:__gmpz_jacobi, :libgmp), Cint, (Ref{BigInt}, Ref{BigInt}), ba, bb)
end

"""
    lassallenum(n)

Compute the ``n``th entry in Lassalle's sequence, OEIS entry A180874.
"""
function lassallenum(m::Integer)
    A = ones(BigInt, m)
    for n = 2:m
        A[n] = (-1)^(n-1) * (catalannum(n) + sum(j->(-1)^j*binomial(2n-1, 2j-1)*A[j]*catalannum(n-j), 1:n-1))
    end
    A[m]
end

function legendresymbol(a::Integer, b::Integer)
    ba = Ref{BigInt}(a)
    bb = Ref{BigInt}(b)
    return ccall((:__gmpz_legendre, :libgmp), Cint, (Ref{BigInt}, Ref{BigInt}), ba, bb)
end

function lucasnum(n::Integer)
    if n < 0
        throw(DomainError(n, "n must be nonnegative"))
    end
    z = Ref{BigInt}(0)
    ccall((:__gmpz_lucnum_ui, :libgmp), Cvoid, (Ref{BigInt}, UInt), z, UInt(n))
    return z[]
end

function stirlings1(n::Int, k::Int, signed::Bool=false)
    if signed == true
        return (-1)^(n - k) * stirlings1(n, k)
    end

    if n < 0
        throw(DomainError(n, "n must be nonnegative"))
    elseif n == k == 0
        return 1
    elseif n == 0 || k == 0
        return 0
    elseif n == k
        return 1
    elseif k == 1
        return factorial(n-1)
    elseif k == n - 1
        return binomial(n, 2)
    elseif k == n - 2
        return div((3 * n - 1) * binomial(n, 3), 4)
    elseif k == n - 3
        return binomial(n, 2) * binomial(n, 4)
    end

    return (n - 1) * stirlings1(n - 1, k) + stirlings1(n - 1, k - 1)
end

function stirlings2(n::Int, k::Int)
    if n < 0
        throw(DomainError(n, "n must be nonnegative"))
    elseif n == k == 0
        return 1
    elseif n == 0 || k == 0
        return 0
    elseif k == n - 1
        return binomial(n, 2)
    elseif k == 2
        return 2^(n-1) - 1
    end

    return k * stirlings2(n - 1, k) + stirlings2(n - 1, k - 1)
end

"""
Delannoy number  describes the number of paths from the southwest corner (0, 0) of a rectangular grid
to the northeast corner (m, n), using only single steps north, northeast, or east
"""
function dealnnoy(m::Integer,n::Integer)
    if m<0
        throw(DomainError(m, "m must be nonnegative"))
    end
    if n<0
        throw(DomainError(n, "n must be nonnegative"))
    end
    A = ones(BigInt, m+1, n+1)
    for i = 1:m+1
        A[i,1] = 1
    end
    for i = 1:n+1
        A[1,i] = 1
    end
    for i = 2:m+1
        for j = 2:n+1
            A[i,j] = A[i-1,j] + A[i-1,j-1] + A[i,j-1]
        end
    end
    return(A[m+1,n+1])
end


using Distributed
using Distributions

nb_draws = 100000

function inside_circle(x::Float64, y::Float64)
    output = 0
    if x^2 + y^2 <= 1
	output = 1
    end
    return output
end

function pi_serial(nbPoints::Int64 = 128 * 1000; d=Uniform(-1.0,1.0))

   #draw NbPoints from within the square centered in 0
   #with side length equal to 2
   xDraws = rand(d, nbPoints)
   yDraws = rand(d, nbPoints)
   sumInCircle = 0

   for (xValue, yValue) in zip(xDraws, yDraws)
	sumInCircle+=inside_circle(xValue, yValue)
   end

   return 4*sumInCircle/nbPoints

end

pi_approx = pi_serial(nb_draws);

println("Approximation for Pi $(pi_approx) with $(nb_draws) random draws")
println("True value is $(pi)")


"""
    simplex_projection_and_support(z)

Compute the Euclidean projection onto the probability simplex and the set of indices where it is nonzero.

See <https://arxiv.org/abs/1602.02068> for details.
"""
function simplex_projection_and_support(z::AbstractVector{R}) where {R<:Real}
    d = length(z)
    z_sorted = sort(z; rev=true)
    z_sorted_cumsum = cumsum(z_sorted)
    k = maximum(j for j in 1:d if (1 + j * z_sorted[j]) > z_sorted_cumsum[j])
    τ = (z_sorted_cumsum[k] - 1) / k
    p = Vector{R}(undef, d)
    s = Vector{Int}(undef, d)
    for i in 1:d
        p[i] = max(z[i] - τ, zero(R))
        s[i] = Int(!iszero(p[i]))
    end
    return p, s
end;

"""
    simplex_projection(z)

Compute the Euclidean projection onto the probability simplex.
"""
function simplex_projection(z::AbstractVector{<:Real})
    p, _ = simplex_projection_and_support(z)
    return p
end;

"""
    rrule(::typeof(simplex_projection), z)

Custom reverse rule for [`simplex_projection`](@ref) which bypasses the sorting step.

See <https://arxiv.org/abs/1602.02068> for details.
"""
function ChainRulesCore.rrule(::typeof(simplex_projection), z::AbstractVector{<:Real})
    p, s = simplex_projection_and_support(z)
    S = sum(s)
    function simplex_projection_pullback(dp)
        vjp = s .* (dp .- (dp's) / S)
        return (NoTangent(), vjp)
    end
    return p, simplex_projection_pullback
end;


function nmex(a::Matrix{Float64}, x::Vector{Float64};
    tol::Float64=1.0e-10, limit::Int64=1000, show=0)

  iters = 0
  n = size(a, 1)
  converged = false
  local big
  while true
    iters += 1
    x1 = a * x
    big = 0.0
    for i in 1:n
      abs(x1[i]) > abs(big) && (big = x1[i])
    end
    x1 = x1 / big
    (checkit(x1, x, tol) || (iters == limit)) && break
    x = x1
    iters <= show && println("$iters $big $x")
  end
  l2 = norm(x1)
  x1 = x1 / l2
  (iters, big, x1)
end


M, ΔM = 0.0266, 0.0001
L, ΔL = 0.380, 0.001
L1,ΔL1= 0.025, 0.001
L2,ΔL2= 0.190, 0.001
d,Δd= 0.008,0.001

m,Δm = 0.0753, 0.0001
h,Δh = 0.02, 0.001
de,Δde = 0.025, 0.001
di,Δdi = 0.008, 0.001

# Barra

Ih = (1/12)*M*L^2 + (1/16)*M*de^2
ΔIh = (1/12)*(ΔM*L^2 + 2*M*L*ΔL) + (1/16)*(ΔM*d + 2*M*d*Δd)

Ih_eixo = Ih + M*(L2 - L1)^2
ΔIh_eixo = ΔIh + ΔM*(L2-L1)^2 + 2*M*(L2-L1)*(ΔL2+ΔL1)

# Cilindro

Icl = (1/16)*m*(de^2 + di^2) +  (1/12)*m*h^2
ΔIcl = (1/16)*(Δm*(de^2+di^2) + 2*m*(de*Δde+di*Δdi)) + (1/12)*(Δm*h^2 + 2*m*h*Δh)

Icl_cm = Icl + m*(L2 - h/2)^2
ΔIcl_cm = ΔIcl + Δm*(L2- h/2)^2 + 2*m*(L2-h/2)*(ΔL2+ Δh/2)

Icl_eixo = Icl + m*(L - L1 - h/2)^2
ΔIcl_eixo = ΔIcl + Δm*(L - L1 - h/2)^2 + 2*m*(L-L1-h/2)*(ΔL +ΔL1 + Δh/2)

println("\n Sistema no centro de massa\n")
println("======================")
println("Ih ± ΔIh =" , round(Ih,digits=8), " ± ", round(ΔIh,digits=8))
println("Icl_cm ± ΔIcl_cm =" , round(Icl_cm,digits=8), " ± ", round(ΔIcl_cm,digits=8))

println("\n Sistema no eixo \n ")
println("======================")
println("Ih_eixo ± ΔIh_eixo =" , round(Ih_eixo,digits=8), " ± ", round(ΔIh_eixo,digits=8))
println("Icl_eixo ± ΔIh_eixo =" , round(Icl_eixo,digits=8), " ± ",round(ΔIcl_eixo,digits=8))


using Graphics
using Base.Test

# Part 1. Geometry

## Point-vector identity (typealias)
@test Point(2, 1) == Vec2(2, 1)

## Vector operators
@test Vec2(1, 2) + Vec2(3, 4) == Vec2(4, 6)
@test Vec2(1, 2) - Vec2(3, 4) == Vec2(-2, -2)
@test Vec2(1, 2) * 1 == Vec2(1, 2) # identity
@test Vec2(1, 2) * 0 == Vec2(0, 0) # null
@test Vec2(1, 2) * 2 == Vec2(2, 4) # scalar multiplication
@test Vec2(1, 2) / 2 == Vec2(0.5, 1)

## rotation: rotate()
@test_approx_eq rotate(Vec2(1, 2), π, Vec2(1, 1)).x 1
@test_approx_eq rotate(Vec2(1, 2), π, Vec2(1, 1)).y 0
@test_approx_eq rotate(Vec2(1, 2), 2π).x 1
@test_approx_eq rotate(Vec2(1, 2), 2π).y 2

## Euclidean norm/magnitude: norm()
@test norm(Vec2(1, 2)) == sqrt(5)
@test norm(Vec2(0, 0)) == 0

## Bounding boxes: BoundingBox
BBT_point_1 = Vec2(1, 1)
BBT_point_2 = Vec2(2, 3)
BBT_point_3 = Vec2(4, 5)
BBT = BoundingBox(BBT_point_1, BBT_point_2, BBT_point_3)

### BoundingBox attributes
@test BBT == BoundingBox(1, 4, 1, 5)
@test height(BBT) == 4
@test width(BBT) == 3
@test diagonal(BBT) == 5
@test_approx_eq aspect_ratio(BBT) 1.333333333333
@test xmin(BBT) == 1
@test xmax(BBT) == 4
@test center(BBT) == Vec2(2.5, 3)
@test xrange(BBT) == (1, 4)
@test yrange(BBT) == (1, 5)

### BoundingBox operations
BBT_1 = BoundingBox(2, 3, 4, 5)
BBT_2 = BoundingBox(6, 7, 8, 9)

#### BoundingBox (+)
@test BBT_1 + BBT_2 == BoundingBox(2, 7, 4, 9)

#### BoundingBox (&)
@test BBT_1 & BBT_2 == BoundingBox(6, 3, 8, 5)

#### deform()
@test deform(BBT_1, -1, 2, -3, 4) == BoundingBox(1, 5, 1, 9)

#### shift()
@test shift(BBT_1, -1, 2) == BoundingBox(1, 2, 6, 7)

#### scale()
@test BBT_1 * 3 == BoundingBox(1, 4, 3, 6)

#### rotate()
BBT_3 = rotate(BBT_1, π, Point(0, 1))
@test_approx_eq BBT_3.xmin -3
@test_approx_eq BBT_3.ymin -3
@test_approx_eq BBT_3.xmax -2
@test_approx_eq BBT_3.ymax -2

#### with_aspect_ratio()
@test with_aspect_ratio(BBT_1, 2) == BoundingBox(2.25, 2.75, 4, 5)
@test with_aspect_ratio(BBT_1, 0.5) == BoundingBox(2, 3, 4.25, 4.75)

#### isinside()
@test isinside(BBT_1, Point(2.5, 4.5))
@test isinside(BBT_1, Point(2, 4))
@test isinside(BBT_1, Point(1, 3)) == false


# Compute C = A * B, using naive matrix-multiplication algorithm,
# with a pre-allocated output array C.  ("!" is a Julia convention
# for functions that modify their arguments.)

function matmul!(C, A, B)
    m,n = size(A)
    n,p = size(B)
    size(C) == (m,p) || error("incorrect dimensions ", size(C), " ≠ $m × $p")
    for i = 1:m
        for k = 1:p
            c = zero(eltype(C))
            for j = 1:n
                @inbounds c += A[i,j] * B[j,k]
            end
            @inbounds C[i,k] = c
        end
    end
    return C
end

# a wrapper that allocates C of an appropriate type
matmul(A, B) = matmul!(Array{promote_type(eltype(A), eltype(B))}(undef,size(A,1), size(B,2)),A, B)

# Example:
# A = rand(5,6)
# B = rand(6,7)
# matmul(A,B)

Code/Numerical Analysis & Machine Learning/Matrix Multiplication/NaiveMatrixMultiplication.jl


"""
`X = DataUtils.expand_poly(x; degree = 5)`

Performs a polynomial basis expansion of the given `degree`
for the vector `x`.
The return value `X` is a matrix of size `(degree, length(x))`.

Note: all the features of `X` are centered and rescaled.
"""
function expand_poly(x::AbstractVector{T}; degree::Int = 5) where T<:Number
    n = length(x)
    x_vec = convert(Vector{Float64}, copy(collect(x)))
    X = zeros(Float64, (degree, n))
    @inbounds for i = 1:n
        for d = 1:degree
            X[d, i] += x_vec[i]^(d)
        end
    end
    X
end


"""Sieve of Eratosthenes function docstring"""
function es(n::Int) # one integer argument
    isprime = trues(n) # n-elt vector of true
    isprime[1] = false # since 1 is not prime
    for i in 2:isqrt(n) # loop odd ints less or equal to sqrt(n)
        if isprime[i] # conditonal evaluation
            for j in i^2:i:n # sequence with step i
                isprime[j] = false
            end
        end
    end
    return filter(x -> isprime[x], 1:n) # filter using anonymous fn
end

println(es(100)) # print all primes less or equal to 100
@time length(es(10^6)) # check fn execution time and memory usage


function isPrime(n::Int)
	for i in 2:ceil(sqrt(n))
		if n%i==0 && n!=i return false end
	end
	return true
end
primes = [1,2,3]

ubound = 2000000
for i in 5:2:ubound
	if isPrime(i)
		push!(primes, i)
	end
end

println(sum(primes))