# # Defining custom `LinearMap` types

# In this section, we want to demonstrate on a simple, actually built-in, linear map type
# how to define custom `LinearMap` subtypes. First of all, `LinearMap{T}` is an extendable
# abstract type, where `T` denotes the `eltype`.

# ## Basics

# As an example, we want to define a map type whose objects correspond to lazy analogues
# of `fill`ed matrices. Naturally, we need to store the filled value `λ` and the `size`
# of the linear map.

using LinearMaps, LinearAlgebra

struct MyFillMap{T} <: LinearMaps.LinearMap{T}
    λ::T
    size::Dims{2}
    function MyFillMap(λ::T, dims::Dims{2}) where {T}
        all(≥(0), dims) || throw(ArgumentError("dims of MyFillMap must be non-negative"))
        promote_type(T, typeof(λ)) == T || throw(InexactError())
        return new{T}(λ, dims)
    end
end

# By default, for any `A::MyFillMap{T}`, `eltype(A)` returns `T`. Upon application to a
# vector `x` and/or interaction with other `LinearMap` objects, we need to check consistent
# sizes.

Base.size(A::MyFillMap) = A.size

# By a couple of defaults provided for all subtypes of `LinearMap`, we only need to define
# a `LinearMaps._unsafe_mul!` method to have a minimal, operational type. The (internal)
# function `_unsafe_mul!` is called by `LinearAlgebra.mul!`, constructors, and conversions
# and only needs to be concerned with the bare computing kernel. Dimension checking is done
# on the level of `mul!` etc. Factoring out dimension checking is done to minimise overhead
# caused by repetitive checking.

function LinearMaps._unsafe_mul!(y::AbstractVecOrMat, A::MyFillMap, x::AbstractVector)
    return fill!(y, iszero(A.λ) ? zero(eltype(y)) : A.λ*sum(x))
end

# Again, due to generic fallbacks the following now "just work":

# * out-of-place multiplication `A*x`,
# * in-place multiplication with vectors `mul!(y, A, x)`,
# * in-place multiply-and-add with vectors `mul!(y, A, x, α, β)`,
# * in-place multiplication and multiply-and-add with matrices `mul!(Y, A, X, α, β)`,
# * conversion to a (sparse) matrix `Matrix(A)` and `sparse(A)`.

A = MyFillMap(5.0, (3, 3)); x = ones(3); sum(x)

#-

A * x

#-

mul!(zeros(3), A, x)

#-

mul!(ones(3), A, x, 2, 2)

#-

mul!(ones(3,3), A, reshape(collect(1:9), 3, 3), 2, 2)

# ## Multiply-and-add and the `MulStyle` trait

# While the above function calls work out of the box due to generic fallbacks, the latter
# may be suboptimally implemented for your custom map type. Let's see some benchmarks.

using BenchmarkTools

@benchmark mul!($(zeros(3)), $A, $x)

#-

@benchmark mul!($(zeros(3)), $A, $x, $(rand()), $(rand()))

# The second benchmark indicates the allocation of an intermediate vector `z`
# which stores the result of `A*x` before it gets scaled and added to (the scaled)
# `y = zeros(3)`. For that reason, it is beneficial to provide a custom "5-arg `mul!`"
# if you can avoid the allocation of an intermediate vector. To indicate that there
# exists an allocation-free implementation, you should set the `MulStyle` trait,
# whose default is `ThreeArg()`.

LinearMaps.MulStyle(A::MyFillMap) = FiveArg()

function LinearMaps._unsafe_mul!(
    y::AbstractVecOrMat,
    A::MyFillMap,
    x::AbstractVector,
    α::Number,
    β::Number
)
    if iszero(α)
        !isone(β) && rmul!(y, β)
        return y
    else
        temp = A.λ * sum(x) * α
        if iszero(β)
            y .= temp
        elseif isone(β)
            y .+= temp
        else
            y .= y .* β .+ temp
        end
    end
    return y
end

# With this function at hand, let's redo the benchmark.

@benchmark mul!($(zeros(3)), $A, $x, $(rand()), $(rand()))

# There you go, the allocation is gone and the computation time is significantly reduced.

# ## Adjoints and transposes

# Generically, taking the transpose (or the adjoint) of a (real, resp.) map wraps the
# linear map by a `TransposeMap`, taking the adjoint of a complex map wraps it by an
# `AdjointMap`.

typeof(A')

# Not surprisingly, without further definitions, multiplying `A'` by `x` yields an error.

try A'x catch e println(e) end

# If the operator is symmetric or Hermitian, the transpose and the adjoint, respectively,
# of the linear map `A` is given by `A` itself. So let us define corresponding checks.

LinearAlgebra.issymmetric(A::MyFillMap) = A.size[1] == A.size[2]
LinearAlgebra.ishermitian(A::MyFillMap) = isreal(A.λ) && A.size[1] == A.size[2]
LinearAlgebra.isposdef(A::MyFillMap) = (size(A, 1) == size(A, 2) == 1 && isposdef(A.λ))
Base.:(==)(A::MyFillMap, B::MyFillMap) = A.λ == B.λ && A.size == B.size

# These are used, for instance, in checking symmetry or positive definiteness of
# higher-order `LinearMap`s, like products or linear combinations of linear maps, or signal
# to iterative eigenproblem solvers that real eigenvalues are to be computed.
# Without these definitions, the first three functions would return `false` (by default),
# and the last one would fall back to `===`.

# With this at hand, we note that `A` above is symmetric, and we can compute

transpose(A)*x

# This, however, does not work for nonsquare maps

try MyFillMap(5.0, (3, 4))' * ones(3) catch e println(e) end

# which require explicit adjoint/transpose handling, for which there exist two *distinct* paths.

# ### Path 1: Generic, non-invariant `LinearMap` subtypes

# The first option is to write `LinearAlgebra.mul!` methods for the corresponding wrapped
# map types; for instance,

function LinearMaps._unsafe_mul!(
    y::AbstractVecOrMat,
    transA::LinearMaps.TransposeMap{<:Any,<:MyFillMap},
    x::AbstractVector
)
    λ = transA.lmap.λ
    return fill!(y, iszero(λ) ? zero(eltype(y)) : transpose(λ)*sum(x))
end

# Now, the adjoint multiplication works.

MyFillMap(5.0, (3, 4))' * ones(3)

# If you have set the `MulStyle` trait to `FiveArg()`, you should provide a corresponding
# 5-arg `mul!` method for `LinearMaps.TransposeMap{<:Any,<:MyFillMap}` and
# `LinearMaps.AdjointMap{<:Any,<:MyFillMap}`.

# ### Path 2: Invariant `LinearMap` subtypes

# Before we start, let us delete the previously defined method to make sure we use the
# following definitions.

Base.delete_method(
    first(methods(
        LinearMaps._unsafe_mul!,
        (AbstractVecOrMat, LinearMaps.TransposeMap{<:Any,<:MyFillMap}, AbstractVector))
    )
)

# The seconnd option is when your class of linear maps that are modelled by your custom
# `LinearMap` subtype are invariant under taking adjoints and transposes.

LinearAlgebra.adjoint(A::MyFillMap) = MyFillMap(adjoint(A.λ), reverse(A.size))
LinearAlgebra.transpose(A::MyFillMap) = MyFillMap(transpose(A.λ), reverse(A.size))

# With such invariant definitions, i.e., the adjoint/transpose of a `MyFillMap` is again
# a `MyFillMap`, no further method definitions are required, and the entire functionality
# listed above just works for adjoints/transposes of your custom map type.

mul!(ones(3), A', x, 2, 2)

#-

MyFillMap(5.0, (3, 4))' * ones(3)

# Now that we have defined the action of adjoints/transposes, the
# following right action on vectors is automatically defined:

ones(3)' * MyFillMap(5.0, (3, 4))

# and `transpose(x) * A` correspondingly, as well as in-place multiplication

mul!(similar(x)', x', A)

# and `mul!(transpose(y), transpose(x), A)`.

# ## Application to matrices

# By default, applying a `LinearMap` `A` to a matrix `X` via `A*X` does
# *not* apply `A` to each column of `X` viewed as a vector, but interprets
# `X` as a linear map, wraps it as such and returns `(A*X)::CompositeMap`.
# Calling the in-place multiplication function `mul!(Y, A, X)` for matrices,
# however, does compute the columnwise action of `A` on `X` and stores the
# result in `Y`. In case there is a more efficient implementation for the
# matrix application, you can provide `mul!` methods with signature
# `mul!(Y::AbstractMatrix, A::MyFillMap, X::AbstractMatrix)`, and, depending
# on the chosen path to handle adjoints/transposes, corresponding methods
# for wrapped maps of type `AdjointMap` or `TransposeMap`, plus potentially
# corresponding 5-arg `mul!` methods. This may seem like a lot of methods to
# be implemented, but note that adding such methods is only necessary/recommended
# for performance.


function neighborhood(A::Array, I::CartesianIndex)
    xmax, ymax = size(A)
    view(
        A,
        max(1, I[1] - 1):min(ymax, I[1] + 1),
        max(1, I[2] - 1):min(ymax, I[2] + 1),
    )
end

function liferule(A, I::CartesianIndex)
    num_ones = sum(neighborhood(A, I)) - A[I]
    if num_ones == 3
        return 1
    elseif num_ones == 2
        return A[I]
    else
        return 0
    end
end

function next(A::Array)
    B = zeros(size(A))
    for I in CartesianIndices(A)
        B[I] = liferule(A, I)
    end
    return B
end


using SymbolicNumericIntegration
using Symbolics

using SymbolicUtils
using SymbolicUtils.Rewriters

using Test
using PyCall
import SymPy

include("axiom.jl")

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

@variables x β

"""
    a list of basic standard integral tests
    based on http://integral-table.com/ with modifications
"""
basic_integrals = [
    # Basic Forms
    1,
    x^2,
    4x^3,
    # Integrals of Rational Functions
    1 / x,
    1 / (2x + 5),
    1 / (x + 1)^2,
    (x + 3)^3,
    x * (x - 2)^4,
    1 / (1 + x^2),
    1 / (9 + x^2),
    x / (4 + x^2),
    x^2 / (16 + x^2),
    x^3 / (1 + x^2),
    1 / (x^2 - 5x + 6),
    1 / (x^2 + x + 1),
    x / (x + 4)^2,
    x / (x^2 + x + 1),
    # Integrals with Roots
    sqrt(x - 2),
    1 / sqrt(x - 1),
    1 / sqrt(x + 1),
    1 / sqrt(4 - x),
    x * sqrt(x - 3),
    sqrt(2x + 5),
    (3x - 1)^1.5,
    x / sqrt(x - 1),
    x / sqrt(x + 1),
    sqrt(x / (4 - x)),
    sqrt(x / (4 + x)),
    x * sqrt(2x + 3),
    sqrt(x * (x + 2)),
    sqrt(x^3 * (x + 3)),
    sqrt(x^2 + 4),
    sqrt(x^2 - 4),
    sqrt(4 - x^2),
    x * sqrt(x^2 + 9),
    x * sqrt(x^2 - 9),
    1 / sqrt(x^2 + 4),
    1 / sqrt(x^2 - 4),
    1 / sqrt(4 - x^2),
    x / sqrt(x^2 + 4),
    x / sqrt(x^2 - 4),
    x / sqrt(4 - x^2),
    x^2 / sqrt(x^2 + 4),
    x^2 / sqrt(x^2 - 4),
    sqrt(x^2 - 5x + 6),
    x * sqrt(x^2 - 5x + 6),
    1 / sqrt(x^2 - 5x + 6),
    1 / (4 + x^2)^1.5,
    # Integrals with Logarithms
    log(x),
    x * log(x),
    x^2 * log(x),
    log(2x) / x,
    log(x) / x^2,
    log(2x + 1),
    log(x^2 + 4),
    log(x^2 - 4),
    log(x^2 - 5x + 6),
    x * log(x + 2),
    x * log(9 - 4x^2),
    log(x)^2,
    log(x)^3,
    x * log(x)^2,
    x^2 * log(x)^2,
    # Integrals with Exponentials
    exp(x),
    sqrt(x) * exp(x),
    x * exp(x),
    x * exp(3x),
    x^2 * exp(x),
    x^2 * exp(5x),
    x^3 * exp(x),
    x^3 * exp(2x),
    exp(x^2),
    x * exp(x^2),
    # Integrals with Trigonometric Functions
    sin(4x),
    sin(x)^2,
    sin(x)^3,
    cos(3x),
    cos(x)^2,
    cos(2x)^3,
    sin(x) * cos(x),
    sin(3x) * cos(5x),
    sin(x)^2 * cos(x),
    sin(3x)^2 * cos(x),
    sin(x) * cos(x)^2,
    sin(x) * cos(5x)^2,
    sin(x)^2 * cos(x),
    sin(x)^2 * cos(x)^2,
    sin(4x)^2 * cos(4x^2),
    tan(x),
    tan(7x),
    tan(x)^2,
    tan(x)^3,
    sec(x),
    sec(x) * tan(x),
    sec(x)^2 * tan(x),
    csc(x),
    sec(x) * csc(x),
    # Products of Trigonometric Functions and Monomials
    x * cos(x),
    x * cos(3x),
    x^2 * cos(x),
    x^2 * cos(5x),
    x * sin(x),
    x * sin(3x),
    x^2 * sin(x),
    x^2 * sin(5x),
    x * cos(x)^2,
    x * sin(x)^2,
    x * tan(x)^2,
    x * sec(x)^2,
    x^3 * sin(x),
    x^4 * cos(2x),
    sin(x)^2 * cos(x)^3,
    # Products of Trigonometric Functions and Exponentials
    exp(x) * sin(x),
    exp(3x) * sin(2x),
    exp(x) * cos(x),
    exp(2x) * cos(7x),
    x * exp(x) * sin(x),
    x * exp(x) * cos(x),
    # Integrals of Hyperbolic Functions
    cosh(x),
    exp(x) * cosh(x),
    sinh(3x),
    exp(2x) * sinh(3x),
    tanh(x),
    exp(x) * tanh(x),
    cos(x) * cosh(x),
    cos(x) * sinh(x),
    sin(x) * cosh(x),
    sin(x) * sinh(x),
    sinh(x) * cosh(x),
    sinh(3x) * cosh(5x),
    # Misc
    exp(x) / (1 + exp(x)),
    cos(exp(x)) * sin(exp(x)) * exp(x),
    cos(exp(x))^2 * sin(exp(x)) * exp(x),
    1 / (x * log(x)),
    (log(x - 1) + (x - 1)^-1) * log(x),
    1 / (exp(x) - 1),
    1 / (exp(x) + 5),
    sqrt(x) * log(x),
    log(log(x)) / x,
    x^3 * exp(x^2),
    sin(log(x)),
    x * cos(x) * exp(x),
    log(x - 1)^2,
    1 / (exp(2x) - 1),
    exp(x) / (exp(2x) - 1),
    x / (exp(2x) - 1),
    # derivative-divide examples (Lamangna 7.10.2)
    exp(x) * exp(exp(x)),
    exp(sqrt(x)) / sqrt(x),
    log(log(x)) / (x * log(x)),
    log(cos(x)) * tan(x),
    # rothstein-Trager examples (Lamangna 7.10.9)
    1 / (x^3 - x),
    1 / (x^3 + 1),
    1 / (x^2 - 8),
    (x + 1) / (x^2 + 1),
    x / (x^4 - 4),
    x^3 / (x^4 + 1),
    1 / (x^4 + 1),
    # bypass = true
    β,      # turn of bypass = true
    exp(x) / x - exp(x) / x^2,
    cos(x) / x - sin(x) / x^2,
    1 / log(x) - 1 / log(x)^2,
]

function test_integrals(; kw...)
    args = Dict(kw)
    misses = []
    k = 1

    for eq in basic_integrals
        if isequal(eq, β)
            printstyled("**** bypass on ****\n"; color=:red)
            bypass = true
            args[:bypass] = true
        else
            printstyled(k, ": "; color=:blue)
            k += 1
            printstyled(eq, " =>\n"; color=:green)
            solved, unsolved = SymbolicNumericIntegration.integrate(eq; args...)
            printstyled('\t', solved; color=:cyan)
            if isequal(unsolved, 0)
                println()
            else
                printstyled(" + ∫ ", unsolved, '\n'; color=:red)
                push!(misses, eq)
            end
        end
    end

    n = length(misses)
    if n > 0
        println("**** missess (n=$n) *****")
    end
    for eq in misses
        printstyled(eq, '\n'; color=:red)
    end
    return n
end

@testset "integral" begin
    n = test_integrals(; symbolic=false, verbose=false, homotopy=true, num_steps=2, num_trials=10)
    @test n > 0
end


using TradeModels, MarketData, TradeBlotter

signal = (sma(AAPL["Close"],50) .> sma(AAPL["Close"],200));
#signal = (sma(Cl,50) .> sma(Cl,200));
#signal = Cl .> sma(Cl,200);
aapl    = FinancialTimeSeries(AAPL, "AAPL")
golden = Blotter(signal, aapl)

trades = tradearray(golden, aapl["Close"])

pnls = convert(Array{Float64}, ([(t.close-t.open) for t in trades].*100))
pos  = pnls[pnls.>0]
neg  = pnls[pnls.<0]
profitfactor = sum(pos)/abs(sum(neg))
mae = convert(Array{Float64}, ([t.open - minimum(t.timeseries["Low"].values) for t in trades]))
trades

# read from csv, parse into blotter and analyze

# qux = readcsv(Pkg.dir("FinancialBlotter/src/dev/account.csv"));
#
# fee = qux[:,[1,4,5,6,10]];
# fum = fee[2:11, :]
# eurtstamp = DateTime{ISOCalendar, UTC}[parsedatetime_from_TOS(t) for t in fum[:,1]]
# eurvals = convert(Array{Float64},fum[:,[2,5]])
# eurtrades = Blotter(eurtstamp, eurvals, blottercolnames, Ticker("EURUSD"))
#
# # get euro data
#
# fname = Pkg.dir("FinancialBlotter/src/dev/data.csv")
# blob    = readcsv(fname)
# tstamps  = Date{ISOCalendar}[date(i) for i in blob[:, 1]]
# vals  = convert(Array{Float64}, blob[:,2])
# eurta = TimeArray(tstamps, vals, ["Close"])


module Day5

TESTINPUT = """0,9 -> 5,9
8,0 -> 0,8
9,4 -> 3,4
2,2 -> 2,1
7,0 -> 7,4
6,4 -> 2,0
0,9 -> 2,9
3,4 -> 1,4
0,0 -> 8,8
5,5 -> 8,2"""

struct Coord
    x::Int64
    y::Int64
end

Base.:+(a::Coord, b::Coord) = Coord(a.x + b.x, a.y + b.y)
Base.:-(a::Coord, b::Coord) = Coord(a.x - b.x, a.y - b.y)

Coord(s) = split(s, ",") |>
    x -> parse.(Int64, x) |>
    x -> Coord(x...)

struct Line
    from::Coord
    to::Coord
end

is_vertical_or_horizontal(l::Line) = l.from.x == l.to.x || l.from.y == l.to.y
gradient(l::Line) = (l.to.y - l.from.y) // (l.to.x - l.from.x)
x_direction(l::Line) = sign(l.to.x - l.from.x)
is_coord_on_line(c::Coord, l::Line) = (l.to.x - l.from.x) * (c.y - l.from.y) == (l.to.y - l.from.y) * (c.x - l.from.x)
function step(l::Line)
    m = gradient(l)
    if m.num == 0
        Coord(sign(l.to.x - l.from.x), 0)
    elseif m.den == 0
        Coord(0, sign(l.to.y - l.from.y))
    else
        Coord(x_direction(l) * m.den, x_direction(l) * m.num)
    end
end

function num_steps(l::Line)
    s = step(l)
    if s.x == 0
        Int64((l.to.y - l.from.y) / s.y) + 1
    else
        Int64((l.to.x - l.from.x) / s.x) + 1
    end
end

Base.iterate(l::Line) = (l.from, l.from + step(l))
Base.iterate(l::Line, c::Coord) = c == (l.to + step(l)) ? nothing : (c, c + step(l))
Base.IteratorSize(::Type{Line}) = Base.HasLength()
Base.IteratorEltype(::Type{Line}) = Base.HasEllype()
Base.eltype(l::Line) = Coord
Base.length(l::Line) = num_steps(l)

min_x(l::Line) = min(l.from.x, l.to.x)
max_x(l::Line) = max(l.from.x, l.to.x)
min_y(l::Line) = min(l.from.y, l.to.y)
max_y(l::Line) = max(l.from.y, l.to.y)

BBox = Tuple{Int64, Int64, Int64, Int64}
bbox(l::Line) = (min_x(l), min_y(l), max_x(l), max_y(l))
bigger_bbox(left::BBox, right::BBox) = (min(left[1], right[1]), min(left[2], right[2]), max(left[3], right[3]), max(left[4], right[4]))

Line(s) = split(s, " -> ") |>
    x -> Coord.(x) |>
    x -> Line(x...)

mutable struct Diagram
    x::Array{Int64, 2}
    top_left::Coord
    width::Int64
    height::Int64
end

diagram_size(ls::Vector{Line}) = mapreduce(bbox, bigger_bbox, ls)

function Diagram(ls::Vector{Line})
    box = diagram_size(ls)
    top_left = Coord(box[1], box[2])
    width = box[3] - box[1] + 1
    height = box[4] - box[2] + 1
    x = zeros(Int64, height, width)
    Diagram(x, top_left, width, height)
end

function mark!(d::Diagram, c::Coord)
    col = c.x - d.top_left.x + 1
    row = c.y - d.top_left.y + 1
    d.x[row, col] += 1
    d
end

function mark!(d::Diagram, l::Line)
    for c in l
        mark!(d, c)
    end
    d
end

function mark!(d::Diagram, lines::Vector{Line})
    for line in lines
        mark!(d, line)
    end
    d
end

marked_diagram(lines::Vector{Line}) = Diagram(lines) |> d -> mark!(d, lines)

count_at_least(d::Diagram, x::Int64) = sum(d.x .>= x)

run_part1(data_dir::String) = joinpath(data_dir, "day5.txt") |>
    readlines |>
    ls -> Line.(ls) |>
    ls -> filter(is_vertical_or_horizontal, ls) |>
    marked_diagram |>
    d -> count_at_least(d, 2)

run_part2(data_dir::String) = joinpath(data_dir, "day5.txt") |>
    readlines |>
    ls -> Line.(ls) |>
    marked_diagram |>
    d -> count_at_least(d, 2)

end


## Notation
"""
    ket([T = Float64], i::Integer, d::Integer) -> SparseVector{Complex{T}}

Create a vector representing the `i`th computational basis vector in dimension `d`.

## Example

```jldoctest
julia> ket(1,2)
2-element SparseVector{Complex{Float64},Int64} with 1 stored entry:
  [1]  =  1.0+0.0im

julia> collect(ans)
2-element Array{Complex{Float64},1}:
 1.0 + 0.0im
 0.0 + 0.0im
```
"""
ket(::Type{T}, i::Integer, d::Integer) where {T} = sparsevec([i], [one(complex(T))], d)

"""
    bra([T = Float64], i::Integer, d::Integer) -> SparseVector{Complex{T}}'

Create a dual vector representing the bra associated to `i`th computational basis vector in dimension `d`.

## Example

```jldoctest
julia> bra(1,2)
1×2 LinearAlgebra.Adjoint{Complex{Float64},SparseVector{Complex{Float64},Int64}}:
 1.0-0.0im  0.0-0.0im

julia> collect(ans)
1×2 Array{Complex{Float64},2}:
 1.0-0.0im  0.0-0.0im
```
"""
bra(::Type{T}, i::Integer, d::Integer) where {T} = ket(T, i, d)'

ket(i::Integer, d::Integer) = ket(Float64, i, d)
bra(i::Integer, d::Integer) = bra(Float64, i, d)

"""
    dm(ψ::AbstractVector)

Creates the density matrix version of a pure state `ψ` via the outer product.
"""
dm(ψ::AbstractVector) = ψ' ⊗ ψ

if VERSION < v"1.2-pre"
    (I::UniformScaling)(n::Integer) = Diagonal(fill(I.λ, n))
end

const ⊗ = kron

"""
    BB84_states([T::Type = Float64])

Generates the BB84 states `|0⟩`, `|1⟩`, `|-⟩`, and `|+⟩`, for use in [`guesswork`](@ref) or other functions. The
numeric type can be optionally specified by the argument.
"""
function BB84_states end

BB84_states() = BB84_states(Float64)

function BB84_states(::Type{T}) where {T<:Number}
    ketplus = (ket(T, 1, 2) + ket(T, 2, 2)) / sqrt(T(2))
    ketminus = (ket(T, 1, 2) - ket(T, 2, 2)) / sqrt(T(2))
    ketzero = ket(T, 1, 2)
    ketone = ket(T, 2, 2)
    return dm.([ketzero, ketone, ketminus, ketplus])
end

"""
    iid_copies(ρBs::AbstractVector{<:AbstractMatrix}, n::Integer) -> Vector{Matrix}

Create a vector of all states of the form ``ρ_1 \\otimes \\dotsm \\otimes ρ_n``
where the ``ρ_i`` range over the set `ρBs`.
"""
function iid_copies(ρBs::AbstractVector{<:AbstractMatrix}, n::Integer)
    inds = cartesian_power(length(ρBs), n)
    [foldl(⊗, ρBs[collect(I)]) for I in inds]
end

## Random states

"""
    simplexpt(unif)

Takes a vector of length `d-1` of numbers between `0.0` and `1.0` and converts it a point on the standard `d` dimensional simplex.
"""
function simplexpt(unif)
    d = length(unif) + 1
    T = eltype(unif)
    w = zeros(T, d + 1)
    w[2:d] .= sort(unif)
    w[d+1] = one(T)
    diff(w)
end

"""
    randprobvec([rng, T=Float64], d)

Generates points of type `T`, uniformly at random on the standard `d-1` dimensional simplex using an algorithm by [Smith and Tromble](http://www.cs.cmu.edu/~nasmith/papers/smith+tromble.tr04.pdf).

## Example

```julia
julia> randprobvec(3)
3-element Array{Float64,1}:
 0.24815974900033688
 0.17199716455672287
 0.5798430864429402

```
"""
randprobvec(rng::AbstractRNG, ::Type{T}, d::Integer) where T = simplexpt(rand(rng, T, d - 1))
randprobvec(::Type{T}, d::Integer) where T = randprobvec(Random.GLOBAL_RNG, T, d)
randprobvec(d::Integer) = randprobvec(Float64, d)

"""
    randunitary([rng, T=Float64], d)

Generates a unitary matrix of dimension `d` at random according to the Haar measure, using an algorithm described by Maris Ozols in ["How to generate a random unitary matrix"](http://home.lu.lv/~sd20008/papers/essays/Random%20unitary%20%5Bpaper%5D.pdf).
"""
function randunitary(rng::AbstractRNG, ::Type{T}, d::Integer) where T
    # `randn` for `BigFloat`'s isn't supported yet
    # (https://github.com/JuliaLang/julia/issues/17629)
    # so we convert afterwards instead (losing some randomness).
    rg1 = T.(randn(rng, d, d))
    rg2 = T.(randn(rng, d, d))
    RG = rg1 + im * rg2
    Q, R = qr(RG)
    r = diag(R)
    L = diagm(0 => r ./ abs.(r))
    return Q * L
end

randunitary(::Type{T}, d::Integer) where {T} = randunitary(Random.GLOBAL_RNG, T, d)
randunitary(d::Integer) = randunitary(Float64, d)

"""
    randdm([rng, T = Float64], d)

Generates a density matrix with numeric type `Complex{T}`, of dimension `d` at
random.

## Example

```julia
julia> randdm(2)
2×2 Array{Complex{Float64},2}:
 0.477118+0.0im        0.119848-0.0371569im
 0.119848+0.0371569im  0.522882+0.0im
```
"""
function randdm(rng::AbstractRNG, ::Type{T}, d::Integer) where T
    eigs = diagm(0 => randprobvec(rng, T, d))
    U = randunitary(rng, T, d)
    ρ = U * eigs * (U')
    return Matrix((ρ + ρ') / 2)
end

randdm(::Type{T}, d::Integer) where T = randdm(Random.GLOBAL_RNG, T, d)
randdm(d) = randdm(Float64, d)


using SciPy
using Test

@testset "stats.gmean" begin
    @test stats.gmean([1, 2, 3, 4, 5, 6, 7]) == 3.3800151591412964
end

@testset "stats.describe" begin
    desc = stats.describe(collect(0:9))
    @test desc == (nobs=10, minmax=(0, 9), mean=4.5, variance=9.166666666666666, skewness=0.0, kurtosis=-1.2242424242424244)
    desc = pystats.describe(collect(0:9))
    @test desc == (10, (0, 9), 4.5, 9.166666666666666, 0.0, -1.2242424242424244)
end


seatcodes = readlines("./src/Day5/input.txt")
using Chain
seatbin = @chain seatcodes begin
    replace.(_,'R' => '1')
    replace.(_, 'L' => '0')
    replace.(_, 'F' => '0')
    replace.(_, 'B' => '1')
end

rowbin = first.(seatbin, 7)
colbin = last.(seatbin, 3)
rownum = parse.(Int, rowbin, base = 2)
colnum = parse.(Int, colbin, base = 2)

seatid = (8 .* rownum) .+ colnum
max_seat_id = maximum(seatid)

[x for x in 1:max_seat_id if ((x - 1) ∈ seatid) & ((x + 1) ∈ seatid) & (x ∉ seatid)]


include("RectangularRoom.jl")
using Plots

# Let's define the room size along x, y, and z, in meters
Lx = 5.0
Ly = 4.0
Lz = 3.0

# Here we define the step of the grid over which we want to calculate the mode
# shape. It will be one point every rx, ry, rz meters along x, y, and z
# respectively.
rx = 0.1
ry = 0.1
rz = 0.1

# We identify the mode we want to calculate with these three indices, the mode
# numbers.
nx = 1
ny = 0
nz = 0

# Here we assign an amplitude to the mode, as if we were plotting the pressure
# field in the room as driven by a sine wave source of appropriate power to
# sustain the target SPL, at steady state regime. dB re 20uPa
SPL = 80.0

# Finally, we set the height in the room at which we want to plot the result.
lh = 1.80

# Computations below.

x, y, z = roomAxes(rx, ry, rz, Lx, Ly, Lz)
X, Y, Z = meshGrid(x, y, z)
li      = argmin(abs.(z .- lh))

P       = 20e-6 * exp10(SPL / 20.0)

eigenV  = P * mode.(X, Y, Z, nx, ny, nz, Lx, Ly, Lz)
eigenF  = modeFrequency(nx, ny, nz, Lx, Ly, Lz)

clibrary(:colorbrewer)

levs    = range(-P, stop = P, length = 128)

# Let's make a little plot
modePlot = contourf(x, y, eigenV[:, :, li],
        levels = levs,
        xlabel = "x [m]",
        ylabel = "y [m]",
        colorbar_title = "Pressure Disturbance [Pa]",
        title = "Resonance mode at $eigenF Hz, z = $(z[li]) m"
        )

### Can make a little animation too - Comment this section in case of troubles
eigenT  = 1.0 / eigenF

Tsteps  = 64

nT      = 1

aL      = nT * eigenT

Fs      = Tsteps * eigenF

nF      = floor(Integer, aL * Fs)

anim = @animate for i = 1:nF
    contourf(x, y, eigenV[:, :, li] * cospi(2 * eigenF * (i - 1) / Fs),
        levels = levs,
        xlabel = "x [m]",
        ylabel = "y [m]",
        colorbar_title = "Pressure Disturbance [Pa]",
        title = "Resonance mode at $eigenF Hz, z = $(z[li]) m"
        )
end

fps     = 12

gif(anim, "/tmp/rmode.gif", fps = fps)

###

# Let's show the first plot:
display(modePlot)


using OpenQuantumBase, Test

A = (s) -> (1 - s)
B = (s) -> s
u = [1.0 + 0.0im, 1] / sqrt(2)
ρ = u * u'

H = DenseHamiltonian([A, B], [σx, σz])

@test_throws ArgumentError DenseHamiltonian([A, B], [σx, σz], unit=:hh)
@test size(H) == (2, 2)
@test H(0) == 2π * σx
@test evaluate(H, 0) == σx
@test H(0.5) == π * (σx + σz)
@test evaluate(H, 0.5) == (σx + σz) / 2
@test get_cache(H) ≈ π * (σx + σz)

# update_cache method
C = similar(σz)
update_cache!(C, H, 10, 0.5)
@test C == -1im * π * (σx + σz)

# update_vectorized_cache method
C = get_cache(H)
C = C⊗C
update_vectorized_cache!(C, H, 10, 0.5)
temp = -1im * π * (σx + σz)
@test C == σi ⊗ temp - transpose(temp) ⊗ σi

# in-place update for matrices
du = [1.0 + 0.0im 0; 0 0]
H(du, ρ, 2, 0.5)
@test du ≈ -1.0im * π * ((σx + σz) * ρ - ρ * (σx + σz))

# eigen-decomposition
w, v = eigen_decomp(H, 0.5)
@test w ≈ [-1, 1] / sqrt(2)
w, v = eigen_decomp(H, 0.0)
@test w ≈ [-1, 1]
@test v ≈ [-1 1; 1 1] / sqrt(2)


constraints = [TruncateStd(1 + rand()) for i in 1:length(example_uvals)]

UV = UncertainValueDataset(example_uvals)

# A single constraint applied to each element of the dataset
n = 5
@test resample(UV, NoConstraint()) isa Vector{Real}
@test resample(UV, TruncateLowerQuantile(0.2)) isa Vector{Real}
@test resample(UV, TruncateUpperQuantile(0.2)) isa Vector{Real}
@test resample(UV, TruncateQuantiles(0.2, 0.8)) isa Vector{Real}
@test resample(UV, TruncateMaximum(2)) isa Vector{Real}
@test resample(UV, TruncateMinimum(0.0)) isa Vector{Real}
@test resample(UV, TruncateRange(-40, 10)) isa Vector{Real}
@test resample(UV, TruncateStd(1)) isa Vector{Real}

@test resample(UV, NoConstraint(), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateLowerQuantile(0.2), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateUpperQuantile(0.2), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateQuantiles(0.2, 0.8), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateMaximum(2), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateMinimum(0), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateRange(-40, 10), n) isa Vector{Vector{Real}}
@test resample(UV, TruncateStd(1), n) isa Vector{Vector{Real}}

# Different constraints applied to each element of the dataset
n = 3
@test resample(UV, constraints) isa Vector{Real}
@test resample(UV, constraints, n) isa Vector{Vector{Real}}


mutable struct ACWRPowerModel <: PowerModels.AbstractWRModel
    PowerModels.@pm_fields
end

function PowerModels.variable_bus_voltage(pm::ACWRPowerModel; kwargs...)
    variable_bus_voltage_magnitude_sqr(pm; kwargs...)
    variable_buspair_voltage_product(pm; kwargs...)

    nw = pm.cnw
    PowerModels.var(pm, nw)[:vr] = JuMP.@variable(
        pm.model,
        [i in PowerModels.ids(pm, nw, :bus)],
        base_name="$(nw)_vr",
        start = PowerModels.comp_start_value(PowerModels.ref(pm, nw, :bus, i), "vr_start", 1.0))
    PowerModels.var(pm, nw)[:vi] = JuMP.@variable(
        pm.model,
        [i in PowerModels.ids(pm, nw, :bus)],
        base_name="$(nw)_vi",
        start = PowerModels.comp_start_value(PowerModels.ref(pm, nw, :bus, i), "vi_start"))
end

function PowerModels.constraint_model_voltage(pm::ACWRPowerModel, n::Int)
    w  = var(pm, n, :w)
    wr = var(pm, n, :wr)
    wi = var(pm, n, :wi)
    vr = var(pm, n, :vr)
    vi = var(pm, n, :vi)
    for i in ids(pm, n, :bus)
        JuMP.@constraint(pm.model, w[i] == vr[i]^2 + vi[i]^2)
    end
    for (i,j) in ids(pm, n, :buspairs)
        JuMP.@constraint(pm.model, wr[(i,j)] == vr[i] * vr[j] + vi[i] * vi[j])
        JuMP.@constraint(pm.model, wi[(i,j)] == vi[i] * vr[j] - vr[i] * vi[j])
    end
end

build_acwr(data_file::String) = instantiate_model(PowerModels.parse_file(data_file), ACWRPowerModel, PowerModels.build_opf)


using GeometricAlgebra:
	bits_to_indices, indices_to_bits,
	bits_to_linear_index, linear_index_to_bits,
	sign_from_swaps, factor_from_squares

@testset "bits <-> indices" begin
	for bits ∈ [0b000, 0b101, 0b110, 0b1101001, rand(UInt16), rand(UInt64)]
		# expect to fail for UInt128
		@test indices_to_bits(bits_to_indices(bits)) == bits
	end
end

@testset "bits <-> linear index" begin
	cases = [
		(1, 0) => 0b000,
		(1, 1) => 0b001,
		(2, 1) => 0b010,
		(1, 2) => 0b011,
		(3, 1) => 0b100,
		(2, 2) => 0b101,
		(3, 2) => 0b110,
		(1, 3) => 0b111,
		(1, 8) => 0b01111_1111,
		(2, 8) => 0b10111_1111,
	]
	@testset "no. $(i) of grade $k: $bits" for ((i, k), bits) ∈ cases
		@test linear_index_to_bits(i, k) == bits
		@test bits_to_linear_index(bits) == i
	end

	for _ ∈ 1:10, bits ∈ (rand(UInt8), rand(UInt16))
		# warning: bits_to_linear_index() is typically slow for UInt32 or higher
		@test linear_index_to_bits(bits_to_linear_index(bits), grade(bits)) == bits
	end
end

@testset "sign_from_swaps()" begin
	# note that typographically bits are written 'backwards'; v124 == 0b1011
	@test sign_from_swaps(0b01, 0b10) == +1
	@test sign_from_swaps(0b10, 0b01) == -1
	@test sign_from_swaps(0b11, 0b01) == -1
	@test sign_from_swaps(0b111, 0b01) == +1
end

@testset "factor_from_squares()" begin
	@test factor_from_squares((1, 1, 1), 0b000) == 1
	@test factor_from_squares((1, 1, 1), 0b111) == 1
	@test factor_from_squares((-1, 1, 1), 0b001) == -1
	@test factor_from_squares((-1, 1, 1), 0b010) == +1
	@test factor_from_squares((-1, 1, 1), 0b111) == -1
	@test factor_from_squares((-1, -1, -1), 0b101) == +1
	@test factor_from_squares((-1, -1, -1), 0b111) == -1
end


@doc raw"""
Axisymmetric tokamak equilibrium in (R,Z,ϕ) coordinates with covariant
components of the vector potential given by
```math
A (R, Z, \phi) = \frac{B_0}{2} \, \bigg( R_0 \, \frac{Z}{R} , \, - R_0 \, \ln \bigg( \frac{R}{R_0} \bigg) , \, - \frac{r^2}{q_0} \bigg)^T ,
```
resulting in the magnetic field with covariant components
```math
B (R, Z, \phi) = \frac{B_0}{q_0} \, \bigg( - \frac{Z}{R} , \, \frac{R - R_0}{R} , \, - q_0 R_0 \bigg)^T ,
```
where $r = \sqrt{ (R - R_0)^2 + Z^2 }$.

Parameters:
 * `R₀`: position of magnetic axis
 * `B₀`: B-field at magnetic axis
 * `q₀`: safety factor at magnetic axis
"""
struct AxisymmetricTokamakCylindrical{T <: Number} <: AnalyticEquilibrium
    name::String
    R₀::T
    B₀::T
    q₀::T

    function AxisymmetricTokamakCylindrical{T}(R₀::T, B₀::T, q₀::T) where T <: Number
        new("AxisymmetricTokamakCylindricalEquilibrium", R₀, B₀, q₀)
    end
end

AxisymmetricTokamakCylindrical(R₀::T=1.0, B₀::T=1.0, q₀::T=2.0) where T <: Number = AxisymmetricTokamakCylindrical{T}(R₀, B₀, q₀)

function Base.show(io::IO, equ::AxisymmetricTokamakCylindrical)
    print(io, "Axisymmetric Tokamak Equilibrium in (R,Z,ϕ) Coordinates with\n")
    print(io, "  R₀ = ", equ.R₀, "\n")
    print(io, "  B₀ = ", equ.B₀, "\n")
    print(io, "  q₀ = ", equ.q₀)
end

function X(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    R(x,equ) * cos(ϕ(x,equ))
end

function Y(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    R(x,equ) * sin(ϕ(x,equ))
end

function Z(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    x[2]
end

function R(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    x[1]
end

function r(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    sqrt(r²(x, equ))
end

function r²(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    (R(x,equ) - equ.R₀)^2 + Z(x,equ)^2
end

function θ(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    atan2(Z(x,equ), R(x,equ) - equ.R₀)
end

function ϕ(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    x[3]
end

function J(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    R(x,equ)
end

function periodicity(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    p = zero(x)
    p[3] = 2π
    return p
end

function A₁(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    + equ.B₀ * equ.R₀ * Z(x,equ) / R(x,equ) / 2
end

function A₂(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    - equ.B₀ * equ.R₀ * log(R(x,equ) / equ.R₀) / 2
end

function A₃(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    - equ.B₀ * r²(x,equ) / equ.q₀ / 2
end

function g₁₁(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    one(T)
end

function g₂₂(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    one(T)
end

function g₃₃(x::AbstractArray{T,1}, equ::AxisymmetricTokamakCylindrical) where {T <: Number}
    R(x, equ)^2
end

macro axisymmetric_tokamak_equilibrium_cylindrical(R₀, B₀, q₀)
    generate_equilibrium_code(AxisymmetricTokamakCylindrical(R₀, B₀, q₀); output=false)
end


using .Distributions
using .PyCall

"""Gaussian Bimodal"""
function gaussianBimodal(μ=0.0, σ=1.0, α=0.5)
    # μ is the mean of the satellite gaussian, σ is the width (SD) and α is the proportional probability (i.e. the mass of the satellite compared to the central gaussian)
    if rand() > α # Draw from the first distribution
        x = randn()
    else # Draw from the satellite
        x = σ*randn() + μ
    end
end

function gaussianBimodal(P::Process)
    # Sample from a distribution formed from a single unit gaussian at the origin, and a satellite gaussian with parameters (height, mean, width) you choose.
    seed(P.solver_rng)
    sol = [gaussianBimodal(parameter_function(P)(t)...) for t in P.transient_t0:P.savedt:P.tmax]
end

gaussianBimodalSim = Process(
    process = gaussianBimodal,
    parameter_profile = (constantParameter, constantParameter, constantParameter),
    parameter_profile_parameters = ((1.0,), (1.0,), (0.5)),
    transient_t0 = 0.0,
    t0 = 0.0,
    savedt = 1,
    tmax = 10000.0)
export gaussianBimodalSim

"""Bimodal Switching"""
function bimodalSwitching(t⃗, α::Function, δ::Function)
    v = zeros(length(t⃗))
    for t ∈ 2:length(t⃗)
        Y = rand(Binomial(1, α(t⃗[t])))
        v[t] = Y*v[t-1] + (1-Y)*(1-v[t-1])
    end
    x = randn(length(t⃗)) .+ v.*δ.(t⃗)
end

function bimodalSwitching(P::Process)
    # Draw from one Gaussian but at each time step have a probability 1-α of switching to another gaussian at distance δ away
    seed(P.solver_rng)
    p1, p2 = [P.parameter_profile[i](P.parameter_profile_parameters[i]...) for i ∈ 1:lastindex(P.parameter_profile)]
    sol = bimodalSwitching(P.transient_t0:P.savedt:P.tmax, p1, p2)
end

bimodalSwitchingSim = Process(
    process = bimodalSwitching,
    parameter_profile = (constantParameter, constantParameter),
    parameter_profile_parameters = ((0.5,), (3.0)),
    transient_t0 = 0.0,
    t0 = 0.0,
    savedt = 1,
    tmax = 10000.0)
export bimodalSwitchingSim

"""Shifty Noise"""
function shiftyNoise(P::Process)
    # Parameters (η, C)
    seed(P.solver_rng)
    (η, C) = parameter_functions(P)
    sol = [η(t)*randn() + C(t) for t in P.transient_t0:P.savedt:P.tmax]
end

shiftyNoiseSim = Process(
    process = shiftyNoise,
    parameter_profile = (constantParameter, stepNoise),
    parameter_profile_parameters = [(1.0,), ((0.0, 1000.0), 100.0, 2.0, 0.0)],
    transient_t0 = 0.0,
    t0 = 0.0,
    savedt = 1,
    tmax = 1000.0)
export shiftyNoiseSim

"""AR"""
function AR(P::Process)
    # Variable number of AR parameters (ϕ₁, ϕ₂, ϕ₃, ...)
    seed(P.solver_rng)
    ϕ⃗ = parameter_function(P)
    p = length(ϕ⃗(gett0(P)))
    ξ⃗ = randn(p+1)
    X = zeros(length(times(P)), p+1)
    X[1, 1:length(getX0(P))] = getX0(P)
    for t ∈ 2:length(times(P))
        X[t, :], ξ⃗ = AR(X[t-1, :], ξ⃗, forcevec(ϕ⃗(t)))
    end
    return X[Int.(times(P, transient=true) .- (gett0(P)-1)), 1:length(getX0(P))]
end

arSim = Process(
    process = AR,
    X0 = [0.0], # Number of initial conditions should really be 1 + num. of parameters, but if you do not specify they default to 0.0
    parameter_profile = (constant, constant, constant, constant),
    parameter_profile_parameters = ((0.1,), (0.1,), (0.1,), (0.1,)),
    t0 = 0,
    dt = 1,
    savedt = 1,
    tmax = 10000)
export arSim

"""
Wrap the main function of the python package colorednoise
"""
function colorednoise(β, N)
    cn.powerlaw_psd_gaussian(β, N)
end
export colorednoise

# """
# Colored noise with a power-law spectrum described by the exponent β
# """
# function coloredNoise(P::Process)
#     # Parameter β
#     seed(P.solver_rng) # Does this carry over to python? Not a huge deal if not
#     (β,) = parameter_functions(P)
#     sol = [η(t)*randn() + C(t) for t in P.transient_t0:P.savedt:P.tmax]
# end

shiftyNoiseSim = Process(
    process = shiftyNoise,
    parameter_profile = (constantParameter, stepNoise),
    parameter_profile_parameters = [(1.0,), ((0.0, 1000.0), 100.0, 2.0, 0.0)],
    transient_t0 = 0.0,
    t0 = 0.0,
    savedt = 1,
    tmax = 1000.0)
export shiftyNoiseSim


using OrdinaryDiffEq, ODE, ODEInterfaceDiffEq, LSODA, Sundials, DiffEqDevTools

f = (du,u,p,t) -> begin
  @inbounds begin
  x = view(u,1:7)   # x
  y = view(u,8:14)  # y
  v = view(u,15:21) # x′
  w = view(u,22:28) # y′
  du[1:7] .= v
  du[8:14].= w
  for i in 15:28
    du[i] = zero(u[1])
  end
  for i=1:7,j=1:7
    if i != j
      r = ((x[i]-x[j])^2 + (y[i] - y[j])^2)^(3/2)
      du[14+i] += j*(x[j] - x[i])/r
      du[21+i] += j*(y[j] - y[i])/r
    end
  end
  end
end

prob = ODEProblem(f,[3.0,3.0,-1.0,-3.0,2.0,-2.0,2.0,3.0,-3.0,2.0,0,0,-4.0,4.0,0,0,0,0,0,1.75,-1.5,0,0,0,-1.25,1,0,0],(0.0,3.0))

abstols = 1.0 ./ 10.0 .^ (6:9)
reltols = 1.0 ./ 10.0 .^ (3:6);

using Plots; gr()

sol = solve(prob,Vern8(),abstol=1/10^12,reltol=1/10^10,maxiters=1000000)
test_sol = TestSolution(sol);
plot(sol)

#setups = [Dict(:alg=>ode45())]
#wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,save_everystep=false,numruns=100,maxiters=10000)
#plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>dopri5())
          Dict(:alg=>Tsit5())
          Dict(:alg=>Vern6())
]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,save_everystep=false,numruns=100,maxiters=10000)
plot(wp)

setups = [Dict(:alg=>DP5())
          Dict(:alg=>Tsit5())
          Dict(:alg=>Vern6())
]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,numruns=100,maxiters=10000,error_estimate=:L2,dense_errors=true)
plot(wp)

#setups = [Dict(:alg=>ode78())]
#wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,save_everystep=false,numruns=100,maxiters=1000)
#plot(wp)

setups = [Dict(:alg=>DP8())
          Dict(:alg=>Vern7())
          Dict(:alg=>Vern8())
          Dict(:alg=>dop853())
          Dict(:alg=>Vern6())
]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,save_everystep=false,numruns=100,maxiters=1000)
plot(wp)

setups = [Dict(:alg=>odex())
          Dict(:alg=>Vern7())
          Dict(:alg=>CVODE_Adams())
          Dict(:alg=>lsoda())
          Dict(:alg=>Vern6())
          Dict(:alg=>Tsit5())
          Dict(:alg=>ddeabm())
          Dict(:alg=>ARKODE(Sundials.Explicit(),order=6))
]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,save_everystep=false,numruns=20)
plot(wp)

setups = [Dict(:alg=>DP8())
          Dict(:alg=>Vern7())
          Dict(:alg=>Vern8())
          Dict(:alg=>Vern6())
]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,numruns=100,maxiters=1000,error_estimate=:L2,dense_errors=true)
plot(wp)

setups = [Dict(:alg=>Tsit5())
          Dict(:alg=>Vern9())
          Dict(:alg=>VCABM())
          Dict(:alg=>AitkenNeville(min_order=1, max_order=9, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointDeuflhard(min_order=1, max_order=9, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))]
solnames = ["Tsit5","Vern9","VCABM","AitkenNeville","Midpoint Deuflhard","Midpoint Hairer Wanner"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>ExtrapolationMidpointDeuflhard(min_order=1, max_order=9, init_order=9, threading=false))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=false))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, sequence = :romberg, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, sequence = :bulirsch, threading=true))]
solnames = ["Deuflhard","No threads","standard","Romberg","Bulirsch"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

setups = [Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=11, init_order=4, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=5, max_order=11, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=2, max_order=15, init_order=10, threading=true))
          Dict(:alg=>ExtrapolationMidpointHairerWanner(min_order=5, max_order=7, init_order=6, threading=true))]
solnames = ["1","2","3","4","5"]
wp = WorkPrecisionSet(prob,abstols,reltols,setups;appxsol=test_sol,names=solnames,
                      save_everystep=false,verbose=false,numruns=100)
plot(wp)

using DiffEqBenchmarks
DiffEqBenchmarks.bench_footer(WEAVE_ARGS[:folder],WEAVE_ARGS[:file])


using MiscUtil
using Test

@testset "MiscUtil.jl" begin

    ###########
    # sigmoid #
    ###########

     for x in [1e-6;0.0;1.0;5.0]
       @test abs(invsigmoid(sigmoid(x)) - x) < 1e-8
     end

    ####################
    # transformbetween #
    ####################

    for x in [1e-6;0.0;1.0;5.0]
        @test abs(invtransformbetween(transformbetween(x, -1.3, 10.1), -1.3, 10.1) - x) < 1e-8
    end

    ################
    # makepositive #
    ################

    for x in [1e-3;0.0;1.0;5.0]
        @test abs(invmakepositive(makepositive(x)) - x) < 1e-8
    end

end


s = 0
for i = 1:999
    if i%3 == 0 || i%5 == 0
        global s+=i
    end
end
println(s)


@testset "Quaternion Midpoint" begin
	# Test quaternion sqrt
	q0 = rand(4)
	q0 ./= norm(q0)
	sq0 = sqrt_quat(q0)
	Q0 = Quaternion(q0...)
	sQ0 = sqrt(Q0)
	sq0_ = [sQ0.s, sQ0.v1, sQ0.v2, sQ0.v3]
	@test norm(sq0 - sq0_) < 1e-10

	q0 = [1,0,0,0]
	q0 ./= norm(q0)
	sq0 = sqrt_quat(q0)
	Q0 = Quaternion(q0...)
	sQ0 = sqrt(Q0)
	sq0_ = [sQ0.s, sQ0.v1, sQ0.v2, sQ0.v3]
	@test norm(sq0 - sq0_) < 1e-10

	# Test midpoint
	q0 = rand(4)
	q0 ./= norm(q0)
	ψ = 0.05 * rand(3)
	qψ = cayley_map(ψ)
	sqψ = sqrt_quat(qψ)
	q1 = L_multiply(qψ) * q0
	q2 = L_multiply(sqψ) * L_multiply(sqψ) * q0
	@test norm(q1 - q2) < 1e-10
	@test norm(R_multiply(q0)' * q1 - qψ) < 1e-10

	qmid = L_multiply(sqψ) * q0
	qmid_ = L_multiply(sqrt_quat(R_multiply(q0)' * q1)) * q0
	@test norm(qmid - qmid_) < 1e-10
end


"""
    Step{T<:Number, S<:Integer} <: DecaySchedule
    Step(λ, γ, step_size::Integer)
    Step(;λ, γ, step_sizes)

A step schedule decays exponentially by `γ` every step
in `step_sizes`.
The output conforms to
```
λ * γ^{i - 1}
```
where `sum(step_sizes[1:(i - 1)]) < t <= sum(step_sizes[1:i])`

# Arguments:
- `λ::Number`: the base value
- `γ::Number`: the decay rate
- `step_sizes::Union{<:Integer, <:Vector}`: the step sizes
"""
struct Step{T<:Number, S<:Integer} <: DecaySchedule
    λ::T
    γ::T
    step_sizes::Vector{S}
end
Step(λ, γ, step_size::Integer) = Step(λ, γ, [step_size])
Step(;λ, γ, step_sizes) = Step(λ, γ, step_sizes)

basevalue(schedule::Step) = schedule.λ
function decay(schedule::Step, t)
    i = findlast(x -> t > x, cumsum(schedule.step_sizes))
    i = isnothing(i) ? 0 :
            (i >= length(schedule.step_sizes)) ? length(schedule.step_sizes) - 1 : i

    return schedule.γ^i
end

Base.eltype(::Type{<:Step{T}}) where T = T
Base.IteratorSize(::Type{<:Step}) = Base.IsInfinite()

# override default behavior for decay schedules
function Base.iterate(schedule::Step, state = (1, 1, 1))
    t, i, t0 = state
    if (i <= length(schedule.step_sizes)) && (t >= t0 + schedule.step_sizes[i])
        # move onto next step range
        i += 1
        t0 = t
    end

    return basevalue(schedule) * schedule.γ^(i - 1), (t + 1, i, t0)
end

"""
    Exp{T<:Number} <: DecaySchedule
    Exp(;λ, γ)

A exponential decay schedule at rate `γ`.
The output conforms to
```
λ * γ^{t - 1}
```

# Arguments:
- `λ::Number`: the base value
- `γ::Number`: the decay rate
"""
struct Exp{T<:Number} <: DecaySchedule
    λ::T
    γ::T
end
Exp(;λ, γ) = Exp(λ, γ)

basevalue(schedule::Exp) = schedule.λ
decay(schedule::Exp, t) = schedule.γ^(t - 1)

Base.eltype(::Type{<:Exp{T}}) where T = T
Base.IteratorSize(::Type{<:Exp}) = Base.IsInfinite()

"""
    Poly{T<:Number, S<:Integer} <: DecaySchedule
    Poly(;λ, p, max_iter)

A polynomial schedule decays with degree `p`.
The output conforms to
```
λ / (1 - floor((t - 1) / max_iter))^p
```

# Arguments
- `λ::Number`: the base value
- `p::Integer`: the degree of the polynomial
- `max_iter::Integer`: the total number of iterations
"""
struct Poly{T<:Number, S<:Integer} <: DecaySchedule
    λ::T
    p::S
    max_iter::S
end
Poly(;λ, p, max_iter) = Poly(λ, p, max_iter)

basevalue(schedule::Poly) = schedule.λ
decay(schedule::Poly, t) = (1 - (t - 1) / schedule.max_iter)^schedule.p

Base.eltype(::Type{<:Poly{T}}) where T = T
Base.IteratorSize(::Type{<:Poly}) = Base.HasLength()
Base.length(schedule::Poly) = schedule.max_iter

"""
    Inv{T<:Number, S<:Integer} <: DecaySchedule
    Inv(;λ, γ, p)

A decay schedule that inversely decays with rate `γ`.
The output conforms to
```
λ / (1 + (t - 1) * γ)^p
```

# Arguments
- `λ::Number`: the base value
- `γ::Number`: the decay rate
- `p::Integer`: the degree of decay
"""
struct Inv{T<:Number, S<:Integer} <: DecaySchedule
    λ::T
    γ::T
    p::S
end
Inv(;λ, γ, p) = Inv(λ, γ, p)

basevalue(schedule::Inv) = schedule.λ
decay(schedule::Inv, t) = 1 / (1 + (t - 1) * schedule.γ)^schedule.p

Base.eltype(::Type{<:Inv{T}}) where T = T
Base.IteratorSize(::Type{<:Inv}) = Base.IsInfinite()


############################
#### Homework No 1 #########
#### Ahmed El Fatmaoui #####
############################

#cd("/Users/ahmedelfatmaoui/Documents/MetricsIII/HOW1")
#---------------
#-----Q1--------
#---------------

using JLD2, Random, LinearAlgebra, Statistics, CSV, DataFrames, FreqTables, Distributions, Kronecker

function q1()
    #a)
         Random.seed!(1234)

         A = rand(Uniform(-5, 10),10,7)
         B = rand(Normal(-2, 15),10,7)
         C = [A[1:5,1:5] B[1:5,6:7]]
         D = (A .<= 0) .* A
    #d-g)
         elm = size(A)
         unqElm = unique(D)
         E = reshape(B,70,1)
         E2 = vec(B)
         F =  reshape([A B], 10, 7, 2)
         F = permutedims(F, [3, 1, 2])
         G = kronecker(B,C)
        # G2 = kronecker(C,F)
    #g-k)
    save("matrixpractice.jld", "A", A, "B", B, "C", C,"D", D, "E", E, "F", F, "G", G )
    save("firstmatrix.jld", "A", A, "B", B, "C", C,"D", D )
    CSV.write("Cmatrix.csv",DataFrame(C,:auto))
    CSV.write("Dmatrix.dat",DataFrame(D,:auto), delim = ' ')

     return A, B, C, D

end

A, B, C, D = q1()

#---------------
#-----Q2--------
#---------------

function q2(A, B, C)
    #a)
  AB = [A[i,j]*B[i,j] for i in 1:10, j in 1:7]

  #b)
  Cprime =  [C[i,j] for i in 1:5, j in 1:7 if -5 <= C[i,j] <= 5]

  C[-5 .<= C .<= 5]

  #c)
  N, K, T = 15169, 6, 5

  X = zeros(N,K,T);

  x5 = rand(Binomial(20,0.6),N)
  x6 = rand(Binomial(20,0.5),N)
      for k in 1:T

          X[:,1,k] = ones(N)

          if rand(1)[1] <= 75*(6-k)/5
              X[:,2,k] = ones(N)
          end

          X[:,3,k] = rand(Normal(15 + k - 1, 5*(k-1)),N)

          X[:,4,k] = rand(Normal(pi*(6-k)/3, 1/exp(1)),N)

          # let columns 1, 5 and 6 remain stationary over time.
          X[:,5, k] = x5
          X[:,6, k] = x6

      end

  # d)
  β = zeros(K,T);
  β[1,:] = [1:0.25:2;];
  β[2,:] = [log(j) for j in 1:T];
  β[3,:] = [-sqrt(j) for j in 1:T];
  β[4,:] = [exp(j) - exp(j+1) for j in 1:T];
  β[5,:] = [j for j in 1:T];
  β[6,:] = [j/3 for j in 1:T];

  β

  # e)

 Y = [X[:,:,t] * β[:,t] + rand(Normal(0,0.36),N,1)  for t in 1:T]

end

q2(A,B,C)

#---------------
#-----Q3--------
#---------------
df = CSV.read("nlsw88.csv", DataFrame);
function q3()
    # a)
    df = CSV.read("nlsw88.csv", DataFrame);
    # ismissing(df)
    # [print(string(":", x, ", ")) for x in names(df)]

        # b)
        NotMarrPer = 1 - sum(df[:,"married"])/size(df)[1];
        GradPer = sum(df[:,"collgrad"])/size(df)[1];

        println("Percent not married = ", round(NotMarrPer, digits = 4) * 100,
        "\nCollege graduate percentage = ", round(GradPer, digits =4 ) * 100)

            # c)  note: tabulate command is undefined
            # source of freq. tables:
            # https://stackoverflow.com/questions/34654489/rs-table-function-in-julia-for-dataframes

            prop(freqtable(df.race))

                # d)

                intrq = (describe(df,:q75)[:,2] - describe(df,:q25)[:,2])
                summarystats = [describe(df,:nmissing, :mean, :median, :std, :min, :max, :nunique) intrq]
                rename(summarystats, :x1 => :itrq)

                    # e)
                    ## Cross tab between industry and occupation
                    freqtable(df.industry, df.occupation)

                        # f)
                        # source: https://dataframes.juliadata.org/stable/man/split_apply_combine/

                        q1 = df[:, [:race, :occupation, :wage]];
                        gdf = groupby(q1, [:race, :occupation]);
                        combine(gdf, :wage => mean)

end

q3()

#---------------
#-----Q4--------
#---------------
function q4()
        fmat = load("firstmatrix.jld")
        #keys(fmat)
        #fmat["B"]

        # a-e)
        function matrixops(A, B)
            # element by element product of inputs, A and b
            # input matrices product
            # input matrices sum
                    if(size(A) != size(B))
                    println("inputs must have the same size")
                    else
                        return println(
                            "A .* B = ",  A .* B,
                            "\n\nA' * B = ", A' * B,
                            "\n\nA + B = ",  A + B
                        )
                    end
        end

        matrixops(A, B)

        # f)
        # we get the error message back
        matrixops(C, D)

        # g)

        matrixops(
            convert(Array,df.ttl_exp ),
            convert(Array,df.wage )
        )
end
    q4()


function reconstruct(w::Vector{Float64},c::Vector{Float64},U::Vector{Float64},
    j::Int,k::Float64,σ::Vector{Float64})
    P = function(Pout::Vector{Float64},x::Float64,Pi::Vector{Float64})
        if j == 1 # left boundary polynomial
            σ[1] = U[2] .- U[1];
            σ[2] = U[3] .- U[2];
            Pi[1] = U[1];
            Pi[2] = U[2] .+ σ[1]./k.*(x .- 1.5.*k);
            Pi[3] = 1./c[3].*(-c[1].*U[1] .+ (1-c[2]).*U[2] .- 1./24.*(σ[2] - σ[1]) .+
                (σ[2] .+ σ[1] .- 2.*c[2].*σ[1])./(2.*k).*(x .- 1.5.*k) .+ (σ[2] - σ[1])./(2.*k.^2).*
                (x .- 1.5.*k).^2);
        elseif j == length(U) # right boundary polynomial
            σ[1] = U[end-2] .- U[end-1];
            σ[2] = U[end-1] .- U[end];
            Pi[1] = U[end];
            Pi[2] = U[end-1] .- σ[2]./k.*(x - k.*(length(U)-1.5));
            Pi[3] = 1./c[3].*(-c[1].*U[end] .+ (1-c[2]).*U[end-1] .- 1./24.*(σ[1] - σ[2]) .-
                ((σ[1] .+ σ[2]) - 2.*c[2].*σ[2])./(2.*k).*(x .- k.*(length(U)-1.5)) .+
                (σ[1] - σ[2])./(2.*k.^2).*(x .- k.*(length(U)-1.5)).^2);
        else # interior polynomial
            σ[1] = U[j] .- U[j-1];
            σ[2] = U[j+1] .- U[j];
            Pi[1] = U[j] .+ σ[1]./k.*(x .- k.*(j-0.5));
            Pi[2] = 1./c[2].*((1.-c[1].-c[3]).*U[j] .- 1./24.*(σ[2] .- σ[1]) .+
                ((1 - 2.*c[3]).*U[j+1] .+ 2.*(c[3] .- c[1]).*U[j] .+
                (2.*c[1] .- 1).*U[j-1])./(2.*k).*(x .- k.*(j-0.5)) .+
                (σ[2] .- σ[1])./(2.*k.^2).*(x .- k.*(j-0.5)).^2);
            Pi[3] = U[j] .+ σ[2]./k.*(x .- k.*(j-0.5));
        end
        Pout[1] = dot(w,Pi);
    end
    return P
end


using OrdinaryDiffEq, DifferentialEquations, Turing

tspan = (0.0f0, 50.0f0)

"""
Lorenz system with chaotic parameter values
"""
function lorenz_system(du, u, p, t;
                     U_func_time=U_func_time_trivial,
                     U_func_space=U_func_space_trivial)
    ρ, σ, β = p
    x, y, z = u

    Ft = U_func_time(t, u)
    Fs = U_func_space(u)

    du[1] = σ*(y-x)
    du[2] = x*(ρ-z) - y
    du[3] = x*y - β*z
    du .+= Ft .+ Fs
end

# Unforced version for use with Turing.jl
function lorenz_system(u, p, t)
    u = convert.(eltype(p),u)

    ρ, σ, β = p
    x, y, z = u
    du = [σ*(y-x); x*(ρ-z) - y; x*y - β*z]
    return du
end

# Generate data, with a time component
p = [28, 10, 8//3]
u0 = [10.0f0, 0.0f0, 0.0f0]
ts = range(tspan[1], tspan[2], length=5000)

function solve_lorenz_system(U_func_time=U_func_time_trivial,
                              U_func_space=U_func_space_trivial)
    prob = ODEProblem((du, u, p, t)->lorenz_system(du, u, p, t,
                                U_func_time=U_func_time,
                                U_func_space=U_func_space),
                    u0, tspan, p)
    sol = solve(prob, Tsit5(), saveat=ts);
    return sol
end

#####
##### Turing function for Bayesian parameter estimation
#####
function lorenz_system(u, p, t)
    u = convert.(eltype(p),u)

    ρ, σ, β = p
    x, y, z = u
    du = [σ*(y-x); x*(ρ-z) - y; x*y - β*z]
    return du
end

@model lorenz_grad_residual(y, dat) = begin
    # Lorenz parameters
    ρ ~ Normal(10, 10.0)
    σ ~ Normal(10, 1.0)
    β ~ Normal(5, 1.0)
    params = [ρ, σ, β]
    noise ~ Truncated(Normal(5, 5.0), 0, 20)

    t = [0] # Not time dependent
    for i in 1:size(y,2)
        u = dat[:,i]
        du_params = lorenz_system(u, params, t)
        y[:, i] ~ MvNormal(du_params, [noise, noise, noise])
    end
end;

#####
##### True model in SINDy syntax
#####
r, s, b = p
#      x  y z  c xx xy xz yy yz zz
 A = [[-s s 0  0  0  0  0  0  0  0];
      [r -1 0  0  0  0 -1  0  0  0];
      [0 0 -b  0  0  1  0  0  0  0]]
n = size(A, 1)
sindy_library = Dict(
    "cross_terms"=>2,
    "constant"=>nothing
);
core_dyn_true = sindyModel(ts, A,
                            convert_string2function(sindy_library),
                            ["x", "y", "z"])

# Actually export
export ts, solve_lorenz_system, lorenz_system, core_dyn_true,
    lorenz_grad_residual


using SimpleRoots, Test

using SimpleRoots: QuadraticError

@testset "test bracketing" begin
    @testset "sin 0.0 is 0.0" begin
        @test findzero(sin, (-1.0, 1.3); atol=1e-30)[1] ≈ 0.0 atol = 1e-30
        @test findzero(sin, (-1.0, 1.3); atol=1e-30)[2] == true
        @test findzero(sin, Secant(-1.0, 1.3); atol=1e-30)[1] ≈ 0.0 atol = 1e-30
        @test findzero(sin, Secant(-1.0, 1.3); atol=1e-30)[2] == true
        @test findzero(sin, Brent(-1.0, 1.3); atol=1e-30)[1] ≈ 0.0 atol = 1e-30
        @test findzero(sin, Brent(-1.0, 1.3); atol=1e-30)[2] == true
        @test findzero(sin, Bisection(-1.0, 1.3); atol=1e-100, max_iter=500)[1] ≈ 0.0 atol = 1e-30
        @test findzero(sin, Bisection(-1.0, 1.3); atol=1e-100, max_iter=500)[2] == true
    end

    @testset "sin with 1 iteration isn't found" begin
        @test findzero(sin, (-1.0, 1.3); atol=1e-30, max_iter=1)[2] == false
        @test findzero(sin, Secant(-1.0, 1.3); atol=1e-30, max_iter=1)[2] == false
        @test findzero(sin, Brent(-1.0, 1.3); atol=1e-30, max_iter=1)[2] == false
        @test findzero(sin, Bisection(-1.0, 1.3); atol=1e-100, max_iter=1)[2] == false
        # Except bisection when the bracket is symmetrical
        @test findzero(sin, Bisection(-1.0, -1.0); atol=1e-100, max_iter=1)[2] == true
    end

    @testset "sin(x) - x / 2" begin
        func1(x) = sin(x) - x / 2
        @test findzero(func1, [0.5pi, pi]; atol=1e-30)[1] == 1.895494267033981
        @test findzero(func1, Bisection([0.5pi, pi]); atol=1e-30)[1] == 1.895494267033981
        @test findzero(func1, Secant([0.5pi, pi]); atol=1e-30)[1] == 1.895494267033981
        @test findzero(func1, Brent([0.5pi, pi]); atol=1e-30)[1] == 1.895494267033981
    end

    @testset "inference" begin
        @inferred findzero(sin, (-1.0, 1.0); atol=1e-100, max_iter=100)
        @inferred findzero(sin, Secant(-1.0, 1.0); atol=1e-100, max_iter=100)
        @inferred findzero(sin, Brent(-1.0, 1.0); atol=1e-100, max_iter=100)
        @inferred findzero(sin, Bisection(-1.0, 1.0); atol=1e-100, max_iter=100)
    end
end

@testset "test quadratic polynomials" begin
    @test quad(1.0, 3.0, -4.0) == (-4.0, 1.0)
    @test quad(Both(), 1.0, 3.0, -4.0) == (-4.0, 1.0)
    @test quad(Lower(), 1.0, 3.0, -4.0) == -4.0
    @test quad(Upper(), 1.0, 3.0, -4.0) == 1.0

    @test quad(Both(), 0.0, 1.0, 1.0) == (-1.0, -1.0)
    @test quad(Lower(), 0.0, 1.0, 1.0) == -1.0
    @test quad(Upper(), 0.0, 1.0, 1.0) == -1.0

    @test quad(Both(), 1.0, 0.0, 0.0) == (0.0, 0.0)
    @test quad(Lower(), 1.0, 0.0, 0.0) == 0.0
    @test quad(Upper(), 1.0, 0.0, 0.0) == 0.0

    @test quad(Both(), 0.0, 0.0, 0.0) == (0.0, 0.0)
    @test quad(Lower(), 0.0, 0.0, 0.0) == 0.0
    @test quad(Upper(), 0.0, 0.0, 0.0) == 0.0

    @test_throws QuadraticError quad(1.0, 1.0, 1.0)
    @test_throws QuadraticError quad(0.0, 0.0, 1.0)

    @inferred quad(1.0, 3.0, -4.0)
end


function check_distance_matrix(D::Matrix{<:Real})
    size(D, 1) > 1 || throw(DomainError(D, "Too few nodes"))
    if any(D .< 0)
        throw(DomainError(D, "Negative entries in distance matrix"))
    end
    if any(diag(D) .!= 0.0)
        throw(DomainError(D, "Diagonal entry not 0"))
    end
end

"""
    nj!(D::Matrix{Float64}, names::AbstractVector{String}=String[];
        force_nonnegative_edges::Bool=false)

Construct a phylogenetic tree from the input distance matrix and
vector of names (as strings), using the Neighbour-Joinging algorithm
([Satou & Nei 1987](https://doi.org/10.1093/oxfordjournals.molbev.a040454)).
The order of the `names` argument should match that of the row (and
column) of the matrix `D`.
With `force_nonnegative_edges` being `true`, any negative edge length
is changed to 0.0 (with a message).

Warning: `D` is modified.
"""
function nj!(D::Matrix{Float64}, names::AbstractVector{String}=String[];
             force_nonnegative_edges::Bool=false)

    check_distance_matrix(D)
    n = size(D, 1)              # number of species

    # when no names arg is supplied
    if isempty(names)
        names = string.(1:n)
    end

    # create empty network with n unconnected leaf nodes
    nodes = map(function(i)
                node = Node(i, true)
                node.name = names[i]
                return node
                end,
                1:n)

    net = HybridNetwork(nodes, Edge[])
    # an array of Node s.t. active_nodes[i] would correspond to the
    # ith entry in distance matrix D at each iteration
    active_nodes = copy(nodes)

    neglenp = 0  # number of negative edge lengths

    while n > 2
        # compute Q matrix and find min
        # warning: here i and j are indeces for D, not actual id's
        sums = sum(D, dims = 1)
        cur = Inf
        min_index = (0, 0)
        for i = 1:n
            for j = i:n
                if j != i
                    qij = (n-2) * D[i,j] - sums[i] - sums[j]
                    if qij < cur
                        cur = qij
                        min_index = (i, j)
                    end
                end
            end
        end

        # connect the nodes, compute the length of the edge
        (i, j) = min_index
        dik = D[i,j] / 2 + (sums[i] - sums[j]) / (2 * (n - 2))
        djk = D[i,j] - dik
        # force negative lengths to 0, if any
        if dik < 0.0
            neglenp += 1
            if force_nonnegative_edges dik = 0.0; end
        end
        if djk < 0.0
            neglenp += 1
            if force_nonnegative_edges djk = 0.0; end
        end

        # create new edges and node, update tree
        edgenum = net.numEdges
        eik = Edge(edgenum + 1, dik) # edge length must be Float64 for Edge()
        ejk = Edge(edgenum + 2, djk)
        node_k = Node(net.numNodes+1, false, false, [eik, ejk]) # new node
        node_i = active_nodes[i]
        node_j = active_nodes[j]
        setNode!(eik, Node[node_i, node_k])
        setNode!(ejk, Node[node_j, node_k])
        setEdge!(node_i, eik)
        setEdge!(node_j, ejk)
        pushEdge!(net, eik)
        pushEdge!(net, ejk)
        pushNode!(net, node_k)

        # update map and D
        # replace D[l, i] with D[l, k], delete D[ , j]
        for l in 1:n
            if !(l in [i j])
                D[l, i] = (D[l,i] + D[j,l] - D[i,j]) / 2
                D[i, l] = D[l, i]
            end
        end
        newidx = filter(u->u!=j, 1:n) # index 1:n\{j}
        D = view(D, newidx, newidx)

        # update active_nodes
        active_nodes[i] = node_k
        active_nodes = view(active_nodes, newidx)

        n = n - 1
    end

    # base case
    if D[1,2] < 0.0
        neglenp += 1
        if force_nonnegative_edges D[1,2] = 0.0; end
    end
    node1 = active_nodes[1]
    node2 = active_nodes[2]
    newedge = Edge(net.numEdges+1, D[1,2])
    setNode!(newedge, [node1, node2]) # isChild1 = true by default: nodes are [child, parent]
    if node1.number == net.numNodes
        newedge.isChild1 = false # to direct the edge from the last created node to the other node
    end
    setEdge!(node1, newedge)
    setEdge!(node2, newedge)
    pushEdge!(net, newedge)

    net.root = net.numNodes # to place the root at the last created node, which is internal

    # report on number of negative branches
    if neglenp > 0
        infostr = (force_nonnegative_edges ?
                   "$neglenp branches had negative lengths, reset to 0" :
                   "$neglenp branches have negative lengths" )
        @info infostr
    end

    return net
end

"""
    nj(D::DataFrame; force_nonnegative_edges::Bool=false)

Construct a tree from a distance matrix by neighbor joining, where
`D` is a `DataFrame` of the distance matrix, with taxon names taken
from the header of the data frame.
The rows are assumed to correspond to tips in the tree in the same order
as they do in columns.
With `force_nonnegative_edges` being `true`, any negative edge length
is changed to 0.0 (with a message).

For the algorithm, see
[Satou & Nei 1987](https://doi.org/10.1093/oxfordjournals.molbev.a040454).

See [`nj!`](@ref) for using a matrix as input.
"""
function nj(D::DataFrame; force_nonnegative_edges::Bool=false)
    nj!(Matrix{Float64}(D), names(D); force_nonnegative_edges=force_nonnegative_edges)
end


using LinearAlgebra

function powermethod(A, x0::AbstractVector=randn(size(A, 2)) |> normalize!; niter::Int=1000)
    e,v = eigen(A)
    return e[1], v[:,1]
    for i = 1:niter
        x0 = A*x0
        x0 |> normalize!
    end
    x0'*A*x0, x0
end

function powermethod_back(A, e, v, de, dv)
    @show A
    de isa Nothing && (de = zero(e))
    dv isa Nothing && (dv = zero(v))
    nm = norm(A*v)
    dvAinv = dv'*inv(A)
    Δ = zero(A)
    de*v*v' + norm(A*v)*(dvAinv*v)*v*v' - dvAinv'*v'
end

@primitive powermethod(A),dy,y powermethod_back(A, y..., dy...)

xa = randn(4,4); xa+=xa'
x = Param(xa)
b = randn(4)
y = @diff powermethod(x)[2] |> sum
grad(y, x)
#e = @diff powermethod(x)[1]
#grad(e, x)

δ = 1e-5
xa_ = copy(xa)
xa_[1]+=δ/2
pos = powermethod(xa_)
xa_[1]-=δ
neg = powermethod(xa_)
(pos[1]-neg[1])/δ

using Test
@testset "power method" begin
    N = 4
    A = randn(N, N); A = A+A'
    eg, vg = powermethod(A)
    eigenres = eigen(A)
    i = argmax(eigenres.values .|> abs)
    @test eg ≈ eigenres.values[i]
    @test vg'*eigenres.vectors[:,i] |> abs ≈ 1
    @test AutoGrad.gradcheck(A->powermethod(A)[2].*b |> sum, A)
end

N = 4
A = randn(N, N); A = A+A'
eg, vg = powermethod(A)
eigenres = eigen(A)
i = argmax(eigenres.values .|> abs)
@test eg ≈ eigenres.values[i]
@test vg'*eigenres.vectors[:,i] |> abs ≈ 1
@test AutoGrad.gradcheck(A->powermethod(A)[2].*b |> sum, A)


@doc raw"""
θ-pinch equilibrium in (x,y,z) coordinates with covariant components of
the vector potential given by
```math
A (x,y,z) = \frac{B_0}{2} \big( - y , \, x , \, 0 \big)^T
```
resulting in the magnetic field with covariant components
```math
B (x,y,z) = \big( 0 , \, 0 , \, B_0 \big)^T .
```

Parameters:
    B₀: B-field at magnetic axis
"""
struct ThetaPinch{T <: Number} <: CartesianEquilibrium
    name::String
    B₀::T

    function ThetaPinch{T}(B₀::T) where T <: Number
        new("ThetaPinchEquilibrium", B₀)
    end
end

ThetaPinch(B₀::T=1.0) where T <: Number = ThetaPinch{T}(B₀)

function Base.show(io::IO, equ::ThetaPinch)
    print(io, "θ-Pinch Equilibrium in (x,y,z) Coordinates with\n")
    print(io, "  B₀ = ", equ.B₀)
end

function R(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    r(x,equ)
end

function r(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    sqrt(r²(x,equ))
end

function r²(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    X(x,equ)^2 + Y(x,equ)^2
end

function θ(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    atan2(Y(x,equ), X(x,equ))
end

function ϕ(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    θ(x,equ)
end

function A₁(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    - equ.B₀ * Y(x,equ) / 2
end

function A₂(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    + equ.B₀ * X(x,equ) / 2
end

function A₃(x::AbstractArray{T,1}, equ::ThetaPinch) where {T <: Number}
    zero(eltype(x))
end

macro zpinch_equilibrium(R₀, B₀)
    generate_equilibrium_code(ThetaPinch(R₀, B₀); output=false)
end


"""
    frac(startdate::Date, enddate::Date, dc::DayCount)

Compute the fractional number of years between `startdate` and `enddate`, according to the
[`DayCount` object](@ref daycount_types) `dc`.

- If `startdate == enddate`, then the result is zero.
- If `startdate > enddate`, then the result is `-frac(enddate, startdate, dc)`.
"""
function frac
end

abstract type DayCount end

"""
    Actual365Fixed()

**Actual/365 (Fixed)** day count convention.

The year fraction is computed as:
```math
\\frac{\\text{# of days}}{365}
```

# Reference
 - 2006 ISDA definitions, §4.16 (d)
"""
struct Actual365Fixed <: DayCount end
const Actual365F = Actual365Fixed
function frac(startdate::Date, enddate::Date, ::Actual365Fixed)
    return Dates.value(enddate-startdate)/365
end

"""
    Actual360()

**Actual/360** day count convention.

The year fraction is computed as:
```math
\\frac{\\text{# of days}}{360}
```

# Reference
 - 2006 ISDA definitions, §4.16 (e)

"""
struct Actual360 <: DayCount end
function frac(startdate::Date, enddate::Date, ::Actual360)
    return Dates.value(enddate-startdate)/360
end

"""
    ActualActualISDA()

**Actual/Actual (ISDA)** day count convention.

The year fraction is computed as:
```math
\\frac{\\text{# of days in standard year}}{365} +
\\frac{\\text{# of days in leap year}}{366}
```

For the purposes of above, the start date is included and the end date is excluded.

# Reference
 - 2006 ISDA definitions, §4.16 (b)
"""
struct ActualActualISDA <: DayCount end
function frac(startdate::Date, enddate::Date, dc::ActualActualISDA)
    if startdate > enddate
        return -frac(enddate, startdate, dc)
    end
    startyear = year(startdate)
    endyear = year(enddate)
    if startyear == endyear
        return Dates.value(enddate - startdate) / daysinyear(startdate)
    else
        d1 = daysinyear(startyear)
        n1 = d1 - dayofyear(startdate) + 1
        d2 = daysinyear(endyear)
        n2 = dayofyear(enddate) - 1
        return n1/d1 + n2/d2 + (endyear - startyear - 1)
    end
end

"""
    ActualActualICMA(schedule::StepRange{Date,Month})

**Actual/Actual (ICMA)**, **Actual/Actual (ISMA)** or **ISMA-99** day count convention.

Note that this is dependent on the coupon or payment schedule of the underlying
security. This is provided via the `schedule` argument, currently only date ranges with
steps of months are supported.

The year fraction is computed as:
```math
\\sum_{\\text{schedule period}} \\frac{\\text{# of days in period}}{\\text{length of period} \\times \\text{periods per year}}
```

This ensures that:
* all days in a period are of equal length, and
* all periods are of equal length.

# Reference
 - 2006 ISDA definitions, §4.16 (c).
 - [ICMA Rule Book](https://www.isda.org/a/NIJEE/ICMA-Rule-Book-Rule-251-reproduced-by-permission-of-ICMA.pdf), Rule 251.1 (iii).
 - [EMU and Market Conventions: Recent Developments, ISDA - BS:9951.1](https://www.isda.org/a/AIJEE/1998-ISDA-memo-EMU-and-Market-Conventions-Recent-Developments.pdf), §4. The Actual/Actual Day Count Convention.
"""
struct ActualActualICMA{T<:AbstractVector{Date}} <: DayCount
    frequency::Int
    schedule::T
end
ActualActualICMA(schedule::StepRange{Date,Month}) = ActualActualICMA(Int(Month(12)/step(schedule)),schedule)
const ISMA99 = ActualActualICMA
### const ActualActualISMA = ActualActualICMA

function frac(startdate::Date, enddate::Date, dc::ActualActualICMA)
    if startdate > enddate
        return -frac(enddate,startdate,dc)
    end
    startdate < first(dc.schedule) || enddate > last(dc.schedule) && error("The start and end dates must be contained within the scheduled dates.")

    schedule = dc.schedule

    periodfrac = 0.

    # Fraction of the last period
    istart = findlast(x -> x < enddate, schedule)
    periodfrac += schedule[istart+1] == enddate ? 1. : Dates.value(enddate-schedule[istart])/Dates.value(schedule[istart+1]-schedule[istart])

    # Fraction of the first period
    inext = findfirst(x -> x >= startdate, schedule)
    periodfrac += schedule[inext] == startdate ? 0. : Dates.value(schedule[inext]-startdate)/Dates.value(schedule[inext]-schedule[inext-1])

    # Number of full period between the first and last periods
    periodfrac += istart-inext

    return periodfrac/dc.frequency
end

# helper function
thirty360(dy,dm,dd) = (360*dy + 30*dm + dd)/360

"""
    Thirty360()

**30/360** or **Bond Basis** day count convention.

The year fraction is computed as:
```math
\\frac{360 \\times (y_2 - y_1) + 30 \\times (m_2 - m_1) + (d_2 - d_1)}{360}
```
where
- ``y_1`` and ``y_2`` are the years of the start and end date, respectively.
- ``m_1`` and ``m_2`` are the months of the start and end date, respectively.
- ``d_1`` is the day of the month at the start date, unless it is 31, in which case it is
  30.
- ``d_2`` is the day of the month at the end date, unless it is 31 and ``d_1 ≥ 30``, in
  which case it is 30.

# Reference
 - 2006 ISDA definitions, §4.16 (f)
"""
struct Thirty360 <: DayCount end
const BondBasis = Thirty360
function frac(startdate::Date, enddate::Date, dc::Thirty360)
    if startdate > enddate
        return -frac(enddate, startdate, dc)
    end

    dy = year(enddate)-year(startdate)
    dm = month(enddate)-month(startdate)

    d1 = day(startdate)
    d2 = day(enddate)
    if d1 >= 30
        d1 = 30
        if d2 >= 30
            d2 = 30
        end
    end
    return thirty360(dy,dm,d2-d1)
end

"""
    ThirtyE360()

**30E/360** or **Eurobond Basis** day count convention.

The year fraction is computed as:
```math
\\frac{360 \\times (y_2 - y_1) + 30 \\times (m_2 - m_1) + (d_2 - d_1)}{360}
```
where
- ``y_1`` and ``y_2`` are the years of the start and end date, respectively.
- ``m_1`` and ``m_2`` are the months of the start and end date, respectively.
- ``d_1`` is the day of the month at the start date, unless it is 31st day of the month,
  in which case it is 30.
- ``d_2`` is the day of the month at the end date,  unless it is 31st day of the month,
  in which case it is 30.

# Reference
 - 2006 ISDA definitions, §4.16 (g)
"""
struct ThirtyE360 <: DayCount end
const EurobondBasis = ThirtyE360

function frac(startdate::Date, enddate::Date, dc::ThirtyE360)
    if startdate > enddate
        return -frac(enddate, startdate, dc)
    end
    dy = year(enddate)-year(startdate)
    dm = month(enddate)-month(startdate)
    d1 = min(day(startdate),30)
    d2 = min(day(enddate),30)
    return thirty360(dy,dm,d2-d1)
end

"""
    ThirtyE360ISDA(maturity::Date)

**30E/360 (ISDA)** day count convention. Note that this is dependant of the maturity date
of the underlying security, provided as the `maturity` argument.

The year fraction is computed as:
```math
\\frac{360 \\times (y_2 - y_1) + 30 \\times (m_2 - m_1) + (d_2 - d_1)}{360}
```
where
- ``y_1`` and ``y_2`` are the years of the start and end date, respectively.
- ``m_1`` and ``m_2`` are the months of the start and end date, respectively.
- ``d_1`` is the day of the month at the start date, unless it is:
  * the last day of February, or
  * the 31st day of the month,
  in which case it is 30.
- ``d_2`` is the day of the month at the end date, unless it is:
  * the last day of February and not the maturity date, or
  * the 31st day of the month,
  in which case it is 30.

# Reference
 - 2006 ISDA definitions, §4.16 (h)
"""
struct ThirtyE360ISDA <: DayCount
    maturity::Date
end
function frac(startdate::Date, enddate::Date, dc::ThirtyE360ISDA)
    if startdate > enddate
        return -frac(enddate, startdate, dc)
    elseif startdate == enddate
        # in case startdate == enddate == maturitydate == end of Feb.
        return 0.0
    end
    y1 = year(startdate)
    y2 = year(enddate)
    m1 = month(startdate)
    m2 = month(enddate)
    d1 = startdate == lastdayofmonth(startdate) ? 30 : day(startdate)
    d2 = enddate == lastdayofmonth(enddate) && !(m2 == 2 && enddate == dc.maturity) ? 30 : day(enddate)
    return thirty360(y2-y1,m2-m1,d2-d1)
end


"""
    filter_non_term_leaves!(g, term_vert)

Remove edges of `g` so that all non-isolated leaves of `g` are in the set `term_vert`
"""
function filter_non_term_leaves!(
    g::AbstractGraph{T},
    term_vert::Vector{<:Integer}
    ) where T<:Integer

    is_term = falses(nv(g))
    is_term[term_vert] .= true
    leaves = [v for v in vertices(g) if degree(g, v) == 1 && !is_term[v]]

    while !isempty(leaves)
        leaf = pop!(leaves)
        for v in neighbors(g, leaf)
            rem_edge!(g, v, leaf)
            if degree(g, v) == 1 && !is_term[v]
                push!(leaves, v)
            end
        end
    end

    return g
end

"""
	steiner_tree(g, term_vert, distmx=weights(g))

Return an approximately minimum steiner tree of connected, undirected graph `g` with positive edge
weights represented by `distmx` using [Approximate Steiner Tree](https://en.wikipedia.org/wiki/Steiner_tree_problem#Approximating_the_Steiner_tree).
The minimum steiner tree problem involves finding a subset of edges in `g` of minimum weight such
that all the vertices in `term_vert` are connected.

`t = length(term_vert)`.

### Performance
Runtime: O(t*(t*log(t)+|E|*log(|V| ))
Memory: O(t*|V|)
Approximation Factor: 2-2/t
"""
function steiner_tree end

@traitfn function steiner_tree(
    g::AG::(!IsDirected),
    term_vert::Vector{<:Integer},
    distmx::AbstractMatrix{U} = weights(g)
    ) where {U<:Real, T, AG<:AbstractGraph{T}}

    nvg = nv(g)
    term_to_actual = T.(term_vert)
    unique!(term_to_actual)

    # Compute the graph formed by inducing the metric closure graph of g on term_vert
    nvg_mc = length(term_to_actual)
    distmx_mc = Matrix{U}(undef, nvg_mc, nvg_mc)
    parents = Matrix{T}(undef, nvg, nvg_mc)

    for (i, v) in enumerate(term_to_actual)
        d_s = dijkstra_shortest_paths(g, v, distmx)
        @inbounds parents[:, i] = d_s.parents
        @inbounds distmx_mc[:, i] = @view d_s.dists[term_to_actual]
    end

    # Compute MST of Metric Closure graph
    mst_mc = kruskal_mst(CompleteGraph(nvg_mc), distmx_mc)
    expanded_mst = Vector{Edge{T}}()
    sizehint!(expanded_mst, nvg)

    # Expand each edge in mst_mc into the path it represents
    for e in mst_mc
        i = src(e)
        s = term_to_actual[i]
        t = term_to_actual[dst(e)]
        while s != t
            t_next = parents[t, i]
            push!(expanded_mst, Edge(min(t_next, t), max(t_next, t)))
            t = t_next
        end
    end

    # Compute the MST of the expanded graph
    mst_mst_mc = kruskal_mst(SimpleGraph(expanded_mst), distmx)

    # Remove non-terminal leaves
    return filter_non_term_leaves!(SimpleGraph(mst_mst_mc), term_to_actual)
end


load(::Day{1}, path) = parse.(Int, (readlines(path)))

# Part 1 – count increases
solve1(::Day{1}, data) = sum(x -> x > 0, diff(data))

# Part 2: count windowed increases ignoring identical summands in window
function solve2(::Day{1}, data; window=3)
    n = length(data)
    return sum(view(data, (1 + window):n) .> view(data, 1:(n - window)))
end

testresult1(::Day{1}) = 7
testresult2(::Day{1}) = 5


# This file is part of RLinearAlgebra.jl

module ProceduralTestLSVCProjStd

using Test, RLinearAlgebra, LinearAlgebra, Random

@testset "LSVC Projection Standard -- Procedural" begin
    # Supertype and aliases
    @test supertype(LinSysVecColProjStd) == LinSysVecColProjection
    @test CoordinateDescent == LinSysVecColProjStd
    @test GaussSeidel == LinSysVecColProjStd

    # Verify that the residual projection is zero
    Random.seed!(1010)

    A = rand(5,10)
    x = rand(10)
    b = A * x

    rsub = GaussSeidel()

    for i = 1:10
        @test let
            # Initialization of iteration
            z = rand(10)

            # Search Direction
            e = zeros(10)
            e[i] = 1.0

            # Update
            RLinearAlgebra.rsubsolve!(rsub, z, (e, A, dot(A * e, A * z - b)), i)

            abs(dot(A[:,i], A * z - b)) < 1e-15
        end
    end
end
end # End module


s = open("input.txt") do file
    readlines(file)
end

function GetID(seatCode)
    charArr = seatCode |> collect;
    binString = String(map(x->ifelse(x=='F','0','1'),charArr[1:7]));

    row = parse(Int, binString, base=2);

    binString = String(map(x->ifelse(x=='L','0','1'),charArr[8:10]));
    col = parse(Int, binString, base=2);

    return row * 8 + col;
end

foundSeats = zeros(930);

for seat in s
    foundSeats[GetID(seat)] = 1;
end

for i = 2:929
    if foundSeats[i] == 0 && foundSeats[i-1] == 1 && foundSeats[i+1] == 1
        println(i)
    end
end


module Wpm

import GreyDecision.GreyNumbers: GreyNumber
import GreyDecision.Utility: unitize

function wpm(decisionMat::Array{GreyNumber{T}, 2}, weights::Array{Float64,1},
    fns::Array{Function,1}) where {T <: Real}

    row, col = size(decisionMat)
    normalizedDecisionMat = similar(decisionMat)
    w = unitize(weights)
    colminmax = zeros(GreyNumber{T}, col)

    @inbounds for i in 1:col
        colminmax[i] = decisionMat[:, i] |> fns[i]
        if fns[i] == maximum
            normalizedDecisionMat[:, i] = map(x -> x / colminmax[i], decisionMat[:, i])
        elseif fns[i] == minimum
            normalizedDecisionMat[:, i] = map(x -> colminmax[i] / x, decisionMat[:, i])
        end
    end

    scoreMat = similar(normalizedDecisionMat)
    for i in 1:col
        scoreMat[:, i] = normalizedDecisionMat[:, i].^w[i]
    end

     scores = zeros(GreyNumber{Float64}, row)
    for i in 1:row
        scores[i] = prod(scoreMat[i, :])
    end
    rankings = sortperm(scores)

    bestIndex = rankings |> last

    result = Dict(
        :decisionMat => decisionMat,
        :normalizedDecisionMat => normalizedDecisionMat,
        :w => w,
        :scores => scores,
        :ranking => rankings,
        :bestIndex => bestIndex
    )

    return result
end

end # End of module


using DiffEqFlux, OrdinaryDiffEq, Flux, CUDA
CUDA.allowscalar(false) # Makes sure no slow operations are occuring

# Generate Data
u0 = Float32[2.0; 0.0]
datasize = 30
tspan = (0.0f0, 1.5f0)
tsteps = range(tspan[1], tspan[2], length = datasize)
function trueODEfunc(du, u, p, t)
    true_A = [-0.1 2.0; -2.0 -0.1]
    du .= ((u.^3)'true_A)'
end
prob_trueode = ODEProblem(trueODEfunc, u0, tspan)
# Make the data into a GPU-based array if the user has a GPU
ode_data = gpu(solve(prob_trueode, Tsit5(), saveat = tsteps))

dudt2 = FastChain((x, p) -> x.^3,
                  FastDense(2, 50, tanh),
                  FastDense(50, 2))
u0 = Float32[2.0; 0.0] |> gpu
p = initial_params(dudt2) |> gpu
prob_neuralode = NeuralODE(dudt2, tspan, Tsit5(), saveat = tsteps)

function predict_neuralode(p)
  gpu(prob_neuralode(u0,p))
end
function loss_neuralode(p)
    pred = predict_neuralode(p)
    loss = sum(abs2, ode_data .- pred)
    return loss, pred
end
# Callback function to observe training
list_plots = []
iter = 0
callback = function (p, l, pred; doplot = false)
  global list_plots, iter
  if iter == 0
    list_plots = []
  end
  iter += 1
  display(l)
  # plot current prediction against data
  plt = scatter(tsteps, Array(ode_data[1,:]), label = "data")
  scatter!(plt, tsteps, Array(pred[1,:]), label = "prediction")
  push!(list_plots, plt)
  if doplot
    display(plot(plt))
  end
  return false
end
result_neuralode = DiffEqFlux.sciml_train(loss_neuralode, p,
                                          ADAM(0.05), cb = callback,
                                          maxiters = 300)


@testset "Akima" begin
    x = 0.0:10.0
    y = [0.0, 2.0, 1.0, 3.0, 2.0, 6.0, 5.5, 5.5, 2.7, 5.1, 3.0]
    ak = EarthOrientation.Akima(x, y)
    xi = [0.0, 0.5, 1., 1.5, 2.5, 3.5, 4.5, 5.1, 6.5, 7.2, 8.6, 9.9, 10.]
    yi = [0., 1.375, 2., 1.5, 1.953125, 2.484375,
        4.1363636363636366866103344, 5.9803623910336236590978842,
        5.5067291516462386624652936, 5.2031367459745245795943447,
        4.1796554159017080820603951, 3.4110386597938129327189927,
        3.0]
    @testset for i in eachindex(xi, yi)
        @test EarthOrientation.interpolate(ak, xi[i]) ≈ yi[i]
    end
end


###Define functions to be used in testing below

###Test functions for TargetModel
function targetsin!(r::Vector,t::AbstractVector,paras::Vector)
    for i=1:length(t)
        r[i] = sin(paras[1]*t[i])
    end
    r
end

targetsin(t::AbstractVector,paras::Vector) = targetsin!(zeros(eltype(t),length(t)),t,paras)

###Test function for ODEModel
function odesin(t,y,ydot,paras)
    ydot[1] = paras[1]*cos(paras[1]*t)
end

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

### Test model creation and evaluation functions

timepoints1 = linspace(0.0,10.0,100)
parameters1 = parameters([:a],[1.0],[5.0],[3.0])
measurements1 = data(:function,timepoints1,targetsin,timepoints1,[3.0])
measurements2 = data(:function,timepoints1,targetsin,timepoints1,[2.5])
measurements3 = data(:function,timepoints1,targetsin,timepoints1,[2.0])
noisemodel1 = noise(:gaussian,[0.01])
initial1 = [0.0]

model1 = model(:target,parameters1,measurements1,noisemodel1,targetsin;name="Test")
model2 = model(:target!,parameters1,measurements1,noisemodel1,targetsin!;name="Test!")
model3 = model(:ode,parameters1,measurements1,noisemodel1,odesin,initial1,1,[1];name="Test")

@test_approx_eq_eps evaluate!(model1,[3.0]) measurements(model1) 1e-4
@test_approx_eq_eps evaluate!(model2,[3.0]) measurements(model2) 1e-4
@test_approx_eq_eps evaluate!(model3,[3.0]) measurements(model3) 1e-4

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

### Test the common initialize! function
s0 = samples(:base,1,1,Float64,Float64)
@test initialize!(trait(:initialize,:default),model1,s0).values == [3.0]
@test (srand(345) ; initialize!(trait(:initialize,:prior),model1,s0).values) == (srand(345) ; [rand(parameters1[1].prior)])

### Test the common interface functions
type TestModel <: AbstractModel
    parameters::Vector
end

@test numparas(TestModel(parameters1)) == 1
@test parameters(TestModel(parameters1)) == parameters1

@test_throws MethodError evaluate!(TestModel(parameters1),[1.0])
@test_throws MethodError dataindex(TestModel(parameters1))
@test_throws MethodError measurements(TestModel(parameters1))
@test_throws MethodError noisemodel(TestModel(parameters1))

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

### Test the geometry calculations
r1 = copy(evaluate!(model1,[3.0]))
r2 = copy(evaluate!(model2,[2.5]))
r3 = copy(evaluate!(model3,[2.0]))

@test_approx_eq loglikelihood(model1,r1) loglikelihood(noisemodel1,datavalues(measurements1),r1)
@test_approx_eq loglikelihood(model2,r2) loglikelihood(noisemodel1,datavalues(measurements1),r2)
@test_approx_eq loglikelihood(model3,r3) loglikelihood(noisemodel1,datavalues(measurements1),r3)

### Test the geometry function for zero-order samples
for m in [model1,model2,model3]
    s = samples(:base,1,3,Float64,Float64)
    copy!(s.values,[3.0,2.0,0.0])
    geometry!(m,s)
    ll = [loglikelihood(m,evaluate!(m,[3.0])) loglikelihood(m,evaluate!(m,[2.0])) -Inf]
    @test_approx_eq s.logprior logprior(parameters1,s.values,Float64)
    @test_approx_eq s.loglikelihood ll
end

###test the show functions
println()
println("====================")
println("Test show() function")
println("====================")
show(model1)
show(model2)
show(model3)
println("====================")
println("End  show() function")
println("====================")
println()


# Recurrence parameters as defined by Marwan et al. (2007)

"""
    recurrencerate(x, theiler=0)

Calculate the recurrence rate (RR) of a recurrence matrix, ruling out
the points within the Theiler window.
"""
recurrencerate(x::AbstractMatrix) = countnz(x)/prod(size(x))

function recurrencerate(x::AbstractMatrix; theiler::Integer=0)
    theiler < 0 && error("Theiler window length must be greater than 0")
    diags_remove = -(theiler-1):(theiler-1)
    theiler_points = 0
    theiler_nz = 0
    for d in diags_remove
        theiler_points += length(diag(x,d))
        theiler_nz += countnz(diag(x,d))
    end
    (countnz(x)-theiler_nz)/(prod(size(x))-theiler_points)
end

function tau_recurrence(x::AbstractMatrix{Bool})
    n = minimum(size(x))
    [countnz(diag(x,d))/(n-d) for d in (1:n-1)]
end

# Based on diagonal lines

function diagonalhistogram(x::AbstractMatrix{Bool}; theiler::Integer=1, kwargs...)
    theiler < 0 && error("Theiler window length must be greater than 0")
    bins = [0]
    nbins = 1
    current_diag = 0
    m, n = size(x)
    # Iterate over diagonals - excluding LOI and Theiler window
    # If the matrix is symmetric, examine only the upper triangle
    diag_collection = collect(theiler:n-2)
    xsym = issym(x)
    !xsym && prepend!(diag_collection, collect(-(m-2):-max(theiler,1)))
    for d in diag_collection
        increment = (xsym && d > 0) ? 2 : 1
        previous_cell = false
        first_c = max(1, d+1)
        last_c = min(n, m+d)
        for c = first_c:last_c
            # Operate on existing diagonal line
            if previous_cell
                # Extend length with current cell
                (extend = x[c-d,c]) && (current_diag += 1)
                # If arrived to the end of a line
                # add the current length to the corresponding bin
                if (!extend || (c == last_c)) && (current_diag > 0)
                    # Append new positions to the bins if needed
                    if current_diag > nbins
                        append!(bins, zeros(current_diag - nbins))
                        nbins = current_diag
                    end
                    bins[current_diag] += increment
                    current_diag = 0
                end
            end
            previous_cell = x[c-d,c]
        end
    end
    # Add isolated points in first bin
    allpoints = (theiler == 0) ? countnz(x) : countnz(triu(x, theiler)) + countnz(tril(x,-theiler))
    [allpoints - collect(2:nbins+1)'*bins; bins]
end

"""
    determinism(x; lmin=2, theiler=1)

Calculate the determinism (DET) of a recurrence matrix, ruling out
the points within the Theiler window and diagonals shorter than a minimum value.
"""
function determinism(diag_hist::AbstractVector; lmin=2, kwargs...)
    if lmin < 2
        error("lmin must be 2 or higher")
    end
    nbins = length(diag_hist)
    diag_points = collect(1:nbins) .* diag_hist
    (lmin > nbins) ? 0.0 : sum(diag_points[lmin:nbins])/sum(diag_points)
end

function determinism(x::AbstractMatrix; kwargs...)
    determinism(diagonalhistogram(x; kwargs...); kwargs...)
end

"""
    avgdiag(x; lmin=2, theiler=1)

Calculate the average diagonal length (L) in a recurrence matrix, ruling out
the points within the Theiler window and diagonals shorter than a minimum value.
"""
function avgdiag(diag_hist::AbstractVector; lmin=2, kwargs...)
    if lmin < 2
        error("lmin must be 2 or higher")
    end
    nbins = length(diag_hist)
    diag_points = collect(1:nbins) .* diag_hist
    (lmin > nbins) ? 0.0 : sum(diag_points[lmin:nbins])/sum(diag_hist[lmin:nbins])
end

function avgdiag(x::AbstractMatrix; kwargs...)
    avgdiag(diagonalhistogram(x; kwargs...); kwargs...)
end

"""
    maxdiag(x; lmin=2, theiler=1)

Calculate the longest diagonal (Lmax) in a recurrence matrix, ruling out
the points within the Theiler window and diagonals shorter than a minimum value.
"""

maxdiag(diag_hist::AbstractVector) = length(diag_hist)
maxdiag(x::AbstractMatrix; kwargs...) = maxdiag(diagonalhistogram(x; kwargs...))

"""
    divergence(x; lmin=2, theiler=1)

Calculate the divergence of a recurrence matrix
(actually the inverse of `maxdiag`.
"""
divergence(x; kwargs...) = 1/maxdiag(x; kwargs...)

"""
    entropy(x; lmin=2, theiler=1)

Calculate the entropy of diagonal lengths (ENT) of a recurrence matrix, ruling out
the points within the Theiler window and diagonals shorter than a minimum value.
"""
function entropy(diag_hist::AbstractVector; lmin=2, kwargs...)
    if lmin < 2
        error("lmin must be 2 or higher")
    end
    nbins = length(diag_hist)
    if lmin <= nbins
        prob_bins = diag_hist[lmin:nbins] ./ sum(diag_hist[lmin:nbins])
        prob_bins = prob_bins[find(prob_bins)]
        -sum(prob_bins .* log(prob_bins))
    else
        0.0
    end
end

function entropy(x::AbstractMatrix; kwargs...)
    entropy(diagonalhistogram(x; kwargs...); kwargs...)
end

"""
    trend(x; theiler=1, border=10)

Calculate the trend of recurrences in recurrence matrix towards its edges, ruling out
the points within the Theiler window and in the outermost diagonals.
"""
function trend(npoints::AbstractVector; theiler=1, border=10, kwargs...)
    nmax = length(npoints)
    rrk = npoints./collect(nmax:-1:1)
    m = nmax-border
    w = collect(theiler:m)-m/2
    w'*(rrk[theiler:m]-mean(rrk[theiler:m])) / (w'*w)
end

function trend(x::AbstractMatrix; kwargs...)
    trend(tau_recurrence(x); kwargs...)
end

# Number of l-length sequences, based on diagonals
function countsequences(diag_hist::AbstractVector; lmin=2, kwargs...)
    overlap = (1:length(diag_hist))' - lmin + 1
    overlap[overlap .< 0] = 0
    overlap * diag_hist
end

function countsequences(x::AbstractMatrix; kwargs...)
    countsequences(diagonalhistogram(x; kwargs...); kwargs...)
end

# 2. Based on vertical lines

function verticalhistogram(x::AbstractMatrix{Bool})
    bins = [0]
    nbins = 1
    current_vert = 0
    m, n = size(x)
    # Iterate over columns
    for c = 1:n
        previous_cell = false
        for r = 1:m
            if previous_cell
                (extend = x[r,c]) && (current_vert += 1)
                if (!extend || (r == n)) && (current_vert > 0)
                    if current_vert > nbins
                        append!(bins, zeros(current_vert - nbins))
                        nbins = current_vert
                    end
                    bins[current_vert] += 1
                    current_vert = 0
                end
            end
            previous_cell = x[r,c]
        end
    end
    # Add isolated points in first bin
    [countnz(x) - collect(2:nbins+1)'*bins; bins]
end

"""
    laminarity(x; lmin=2)

Calculate the laminarity (LAM) of a recurrence matrix, ruling out
vertical lines shorter than a minimum value.
"""
laminarity(vert_hist::AbstractVector; kwargs...) = determinism(vert_hist; kwargs...)
laminarity(x::AbstractMatrix; kwargs...) = laminarity(verticalhistogram(x); kwargs...)

"""
    trappingtime(x; lmin=2)

Calculate the trapping time (TT) of a recurrence matrix, ruling out
vertical lines shorter than a minimum value.
"""
trappingtime(vert_hist::AbstractVector; kwargs...) = avgdiag(vert_hist; kwargs...)
trappingtime(x::AbstractMatrix; kwargs...) = trappingtime(verticalhistogram(x); kwargs...)

"""
    maxvert(x; lmin=2)

Calculate the longest vertical line (Vmax) of a recurrence matrix, ruling out
lines shorter than a minimum value.
"""
maxvert(vert_hist::AbstractVector) = length(vert_hist)
maxvert(x::AbstractMatrix; kwargs...) = maxvert(verticalhistogram(x; kwargs...))


using Test
using LinearAlgebra
using Printf
using Random

Base.show(io::IO, f::Float64) = @printf(io, "%20.13e", f)

import Juqbox

include("evalGrad.jl")
include("test-stormer-verlet.jl")

refDir = "reference_solutions"

# test Stormer-Verlet time stepping
refFile = refDir * "/" * "err-mat-ref.jld2"
success = timestep_convergence(refFile)
println("time stepping test: pass=", success)
@test success

# test traceobjgrad for various setups
caseNames =["rabi", "swap02", "flux", "cnot2", "cnot3"]
caseDir = "cases"
nCases = length(caseNames)
pass = BitArray(undef, nCases)
pass .= false

q = 0
for case in caseNames
    global q += 1

     # setup the testcase (assign pcof0, params and wa
    juliaFile = caseDir * "/" * case * "-setup.jl"
    include(juliaFile);

    # evaluate fidelity
    caseRefFile = refDir * "/" * case * "-ref.jld2"
    pass[q] = evalObjGrad(pcof0, params, wa, caseRefFile)

    println("case=", case, " pass=", pass[q])
    @test pass[q]

end


import Symbolics.scalarize

"""
    Gain(k; name)

Outputs `y = k*u`. `k` can be a scalar or an array.
"""
function Gain(k=1; name)
    @variables u(t)=0 [input=true] y(t) [output=true]
    @parameters k=k
    eqs = [
        y ~ k*u
    ]
    ODESystem(eqs, t, name=name)
end

function Gain(K::AbstractArray; name)
    ny,nu = size(K, 1), size(K, 2)
    @variables u[1:nu](t)=0 [input=true] y[1:ny](t)=0 [output=true]
    eqs = y .~ K*u
    ODESystem(eqs, t, name=name)
end

"""
    Sum(n::Int; name)
    Sum(k::AbstractVector; name)

Creates a summing block that sums `n` inputs, `y = sum(u[i] for i ∈ 1:n)`.
A vector of summing coefficients `k` can also be provided, i.e., `y = sum(k[i]u[i] for i ∈ 1:n)`.
A block that subtracts one signal from another can thus be created by `@named sub = Sum([1, -1])`.
"""
function Sum(n::Int; name)
    @variables u[1:n](t)=0 [input=true] y(t)=0 [output=true]
    eqs = [y ~ scalarize(sum(u))]
    ODESystem(eqs, t, name=name)
end

function Sum(k::AbstractVector; name)
    n = length(k)
    @variables u[1:n](t)=0 [input=true] y(t)=0 [output=true]
    eqs = [y ~ scalarize(sum(k[i]*u[i] for i ∈ 1:n))]
    ODESystem(eqs, t, name=name)
end

function Product(n::Int=2; name)
    @variables u[1:n](t)=0 [input=true] y(t)=0 [output=true]
    eqs = [y ~ scalarize(prod(u))]
    ODESystem(eqs, t, name=name)
end