export rational_cubic_interpolation,convex_rational_cubic_control_parameter_to_fit_second_derivative_at_left_side,convex_rational_cubic_control_parameter_to_fit_second_derivative_at_right_side
minimum_rational_cubic_control_parameter_value = -(1 - sqrt(eps()))
maximum_rational_cubic_control_parameter_value = 2 / (eps()^2)

function is_zero(x::Float64)::Bool
	return abs(x) < floatmin()
end

function rational_cubic_interpolation(x::Float64, x_l::Float64, x_r::Float64, y_l::Float64, y_r::Float64, d_l::Float64, d_r::Float64, r::Float64)::Float64
	h = (x_r - x_l)
	if abs(h) <= 0
		return 0.5 * (y_l + y_r)
	end
	#r should be greater than -1. We do not use  assert(r > -1)  here in order to allow values such as NaN to be propagated as they should.
	t = (x - x_l) / h
	if !(r >= maximum_rational_cubic_control_parameter_value)
		t = (x - x_l) / h
		omt = 1 - t
		t2 = t^2
		omt2 = omt^2
		# Formula (2.4) divided by formula (2.5)
		return (y_r*t2*t + (r*y_r-h*d_r)*t2*omt + (r*y_l+h*d_l)*t*omt2 + y_l*omt2*omt) / (1 + (r-3)*t*omt)
	end
	# Linear interpolation without over-or underflow.
	return y_r*t + y_l*(1-t)
end

function rational_cubic_control_parameter_to_fit_second_derivative_at_left_side(x_l::Float64, x_r::Float64, y_l::Float64, y_r::Float64, d_l::Float64, d_r::Float64, second_derivative_l::Float64)::Float64
	h = (x_r - x_l)
	numerator = 0.5*h*second_derivative_l + (d_r - d_l)
	if is_zero(numerator)
		return 0
	end
	denominator = (y_r-y_l)/h - d_l
	if is_zero(denominator)
		if numerator > 0
			return maximum_rational_cubic_control_parameter_value
		 else
			return minimum_rational_cubic_control_parameter_value
		end
	end
	return numerator / denominator
end

function rational_cubic_control_parameter_to_fit_second_derivative_at_right_side(x_l::Float64, x_r::Float64, y_l::Float64, y_r::Float64, d_l::Float64, d_r::Float64, second_derivative_r::Float64)::Float64
	h = (x_r - x_l)
	numerator = 0.5*h*second_derivative_r + (d_r - d_l)
	if is_zero(numerator)
		return 0.0
	end
	denominator = d_r - (y_r-y_l)/h
	if is_zero(denominator)
		if numerator > 0
			return maximum_rational_cubic_control_parameter_value
		 else
			return minimum_rational_cubic_control_parameter_value
		end
	end
	return numerator / denominator
end

function minimum_rational_cubic_control_parameter(d_l::Float64, d_r::Float64, s::Float64, preferShapePreservationOverSmoothness::Bool)::Float64
	monotonic = d_l*s >= 0 && d_r*s >= 0
	convex = d_l <= s && s <= d_r
	concave = d_l >= s && s >= d_r
	if !monotonic && !convex && !concave  # If 3==r_non_shape_preserving_target, this means revert to standard cubic.
		return minimum_rational_cubic_control_parameter_value
	end
	d_r_m_d_l = d_r - d_l
	d_r_m_s = d_r - s
	s_m_d_l = s - d_l
	r1 = -floatmax()
	r2 = r1
	# If monotonicity on this interval is possible, set r1 to satisfy the monotonicity condition (3.8).
	if monotonic
		if !is_zero(s)  # (3.8), avoiding division by zero.
			r1 = (d_r + d_l) / s # (3.8)
		elseif preferShapePreservationOverSmoothness  # If division by zero would occur, and shape preservation is preferred, set value to enforce linear interpolation.
			 r1 = maximum_rational_cubic_control_parameter_value # This value enforces linear interpolation.
		end
	end
	if convex || concave
		if !(is_zero(s_m_d_l) || is_zero(d_r_m_s))  # (3.18), avoiding division by zero.
			r2 = max(abs(d_r_m_d_l/d_r_m_s), abs(d_r_m_d_l/s_m_d_l))
		elseif preferShapePreservationOverSmoothness
			r2 = maximum_rational_cubic_control_parameter_value
		end # This value enforces linear interpolation.
	 elseif monotonic && preferShapePreservationOverSmoothness
		r2 = maximum_rational_cubic_control_parameter_value
	end # This enforces linear interpolation along segments that are inconsistent with the slopes on the boundaries, e.g., a perfectly horizontal segment that has negative slopes on either edge.
	return max(minimum_rational_cubic_control_parameter_value, max(r1, r2))
end

function convex_rational_cubic_control_parameter_to_fit_second_derivative_at_left_side(x_l::Float64, x_r::Float64, y_l::Float64, y_r::Float64, d_l::Float64, d_r::Float64, second_derivative_l::Float64, preferShapePreservationOverSmoothness::Bool)::Float64
	r = rational_cubic_control_parameter_to_fit_second_derivative_at_left_side(x_l, x_r, y_l, y_r, d_l, d_r, second_derivative_l)
	r_min = minimum_rational_cubic_control_parameter(d_l, d_r, (y_r-y_l)/(x_r-x_l), preferShapePreservationOverSmoothness)
	return max(r, r_min)
end

function convex_rational_cubic_control_parameter_to_fit_second_derivative_at_right_side(x_l::Float64, x_r::Float64, y_l::Float64, y_r::Float64, d_l::Float64, d_r::Float64, second_derivative_r::Float64, preferShapePreservationOverSmoothness::Bool)::Float64
	r = rational_cubic_control_parameter_to_fit_second_derivative_at_right_side(x_l, x_r, y_l, y_r, d_l, d_r, second_derivative_r)
	r_min = minimum_rational_cubic_control_parameter(d_l, d_r, (y_r-y_l)/(x_r-x_l), preferShapePreservationOverSmoothness)
	return max(r, r_min)
end


@testset "grids" begin
    bbox1 = [-96.6357421875,31.12819929911196,-84.9462890625,40.58058466412764]
    bbox2 = [-220.78125,-80.64703474739618,-29.53125,78.34941069014629]
    bbox3 = [-5.1,-5.1,5.1,5.1]

    feat2 = point_grid(bbox2, 500., nothing, "miles")
    @test feat2.features[1].geometry.coordinates ≈ [-199.504452, -77.132893]

    feat3 = rectangle_grid(bbox3, 1., 1., nothing, "degrees")
    @test feat3.features[1].geometry.coordinates ≈ [
					[
						[
							-5.025791,
							-5.005842
						],
						[
							-5.025791,
							-4.004674
						],
						[
							-4.020633,
							-4.004674
						],
						[
							-4.020633,
							-5.005842
						],
						[
							-5.025791,
							-5.005842
						]
					]] atol=0.0001

    feat1 = hexgrid(bbox1, 25., nothing, false, "miles")
"""
	@test feat1.features[1].geometry.coordinates ≈
    [
                [
                    [
                        -95.704593,
                        31.467449
                    ],
                    [
                        -95.927937,
                        31.780802
                    ],
                    [
                        -96.374626,
                        31.780802
                    ],
                    [
                        -96.59797,
                        31.467449
                    ],
                    [
                        -96.374626,
                        31.154096
                    ],
                    [
                        -95.927937,
                        31.154096
                    ],
                    [
                        -95.704593,
                        31.467449
                    ]
                ]]
				"""
	bbox4 = [-95.877685546875,74.46849062193377,-94.031982421875,74.90226611990785]
    fc = triangle_grid(bbox4, 12.5, nothing, "miles")
	@test fc.features[1].geometry.coordinates ≈ [
					[
						[
							-95.877686,
							74.468491
						],
						[
							-95.877686,
							74.649405
						],
						[
							-95.20202,
							74.468491
						],
						[
							-95.877686,
							74.468491
						]
					]]
	fc2 = square_grid(bbox4, 12.5, nothing, "miles")
	@test fc2.features[1].geometry.coordinates ≈ [
					[
						[
							-95.630499,
							74.504464
						],
						[
							-95.630499,
							74.685378
						],
						[
							-94.954834,
							74.685378
						],
						[
							-94.954834,
							74.504464
						],
						[
							-95.630499,
							74.504464
						]
					]]
end


using StatsBase
using Distributions
using CategoricalArrays
using DataFrames
using CSV
using RatPack

file = "../examples/test_ratings.csv"
(a,b) = read_ratings(file)

file = "../examples/test_competitions.csv"
(c,d) = read_results( file )

R = RatingsList( d )

file = "../data/test_competitions_1.csv"
(e,f) = read_results( file )
summarize(e, f)

input_ratings = RatingsList( f )
input_competitions = e

ratings = update_ratings(UpdateColley(),
                         input_ratings,
                         input_competitions)

ratings = update_ratings(UpdateKeenerScores(; skew = false),
                         input_ratings,
                         input_competitions)

ratings = update_ratings(UpdateElo(;r0 = 0.0),
                         input_ratings,
                         input_competitions)

# try out the NFL data
file = "../data/nfl_2009_regular.csv"
(e,f) = read_results( file )
summarize(e, f)

input_ratings = RatingsList( f )
input_competitions = e

ratings = update_ratings(UpdateKeenerScores(; skew=true, norm=false),
                         input_ratings,
                         input_competitions)

S = sort(collect(ratings.ratings), by = tuple -> last(tuple), rev=true)

# iterate Elo on the data, one by one
file = "../data/nfl_2009.csv"
(nfl_ext_competitions,  nfl_ext_player_list) = read_results( file )
nfl_ext_ratings = RatingsList( nfl_ext_player_list )

rule = UpdateElo(;r0 = 0.0, K = 32.0, θ=1000.0/log(10.0) )
r = nfl_ext_ratings
for i=1:size(nfl_ext_competitions,1)
    global r = update_ratings(rule, r, nfl_ext_competitions[ [i], :])
end
S = sort(collect(r.ratings), by = tuple -> last(tuple), rev=true)

mean(last.(S)) # should be close to r0 = 0.0


using Test
import ClimateMachine
using ClimateMachine.VariableTemplates: @vars, Vars
using ClimateMachine.Mesh.Grids:
    EveryDirection, HorizontalDirection, VerticalDirection
using ClimateMachine.MPIStateArrays: weightedsum
using ClimateMachine.BalanceLaws

import GaussQuadrature
using MPI
using LinearAlgebra

ClimateMachine.init()

@testset "Exponential and Cutoff filter matrix" begin
    let
        # Values computed with:
        #   https://github.com/tcew/nodal-dg/blob/master/Codes1.1/Codes1D/Filter1D.m
        #! format: off
        W = [0x3fe98f3cd0d725e8  0x3fddfd863c6c9a44  0xbfe111110d0fd334  0x3fddbe357bce0b5c  0xbfc970267f929618
             0x3fb608a150f6f927  0x3fe99528b1a1cd8d  0x3fcd41d41f8bae45  0xbfc987d5fabab8d5  0x3fb5da1cd858af87
             0xbfb333332eb1cd92  0x3fc666666826f178  0x3fe999999798faaa  0x3fc666666826f176  0xbfb333332eb1cd94
             0x3fb5da1cd858af84  0xbfc987d5fabab8d4  0x3fcd41d41f8bae46  0x3fe99528b1a1cd8e  0x3fb608a150f6f924
             0xbfc970267f929618  0x3fddbe357bce0b5c  0xbfe111110d0fd333  0x3fddfd863c6c9a44  0x3fe98f3cd0d725e8]
        #! format: on
        W = reinterpret.(Float64, W)

        N = size(W, 1) - 1

        topology = ClimateMachine.Mesh.Topologies.BrickTopology(
            MPI.COMM_SELF,
            -1.0:2.0:1.0,
        )

        grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
            topology;
            polynomialorder = N,
            FloatType = Float64,
            DeviceArray = Array,
        )

        filter = ClimateMachine.Mesh.Filters.ExponentialFilter(grid, 0, 32)
        nf = length(filter.filter_matrices)
        @test all(ntuple(i -> filter.filter_matrices[i] ≈ W, nf))
    end

    let
        # Values computed with:
        #   https://github.com/tcew/nodal-dg/blob/master/Codes1.1/Codes1D/Filter1D.m
        #! format: off
        W = [0x3fd822e5f54ecb62   0x3fedd204a0f08ef8   0xbfc7d3aa58fd6968   0xbfbf74682ac4d276
             0x3fc7db36e726d8c1   0x3fe59d16feee478b   0x3fc6745bfbb91e20   0xbfa30fbb7a645448
             0xbfa30fbb7a645455   0x3fc6745bfbb91e26   0x3fe59d16feee478a   0x3fc7db36e726d8c4
             0xbfbf74682ac4d280   0xbfc7d3aa58fd6962   0x3fedd204a0f08ef7   0x3fd822e5f54ecb62]
        #! format: on
        W = reinterpret.(Float64, W)

        N = size(W, 1) - 1

        topology = ClimateMachine.Mesh.Topologies.BrickTopology(
            MPI.COMM_SELF,
            -1.0:2.0:1.0,
        )
        grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
            topology;
            polynomialorder = N,
            FloatType = Float64,
            DeviceArray = Array,
        )

        filter = ClimateMachine.Mesh.Filters.ExponentialFilter(grid, 1, 4)
        nf = length(filter.filter_matrices)
        @test all(ntuple(i -> filter.filter_matrices[i] ≈ W, nf))
    end

    let
        T = Float64
        N = (5, 3)
        Nc = (4, 2)

        topology = ClimateMachine.Mesh.Topologies.BrickTopology(
            MPI.COMM_SELF,
            -1.0:2.0:1.0,
        )
        grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
            topology;
            polynomialorder = N,
            FloatType = T,
            DeviceArray = Array,
        )

        ξ = ClimateMachine.Mesh.Grids.referencepoints(grid)
        ξ1 = ξ[1]
        ξ2 = ξ[2]
        a1, b1 = GaussQuadrature.legendre_coefs(T, N[1])
        a2, b2 = GaussQuadrature.legendre_coefs(T, N[2])
        V1 = GaussQuadrature.orthonormal_poly(ξ1, a1, b1)
        V2 = GaussQuadrature.orthonormal_poly(ξ2, a2, b2)

        Σ1 = ones(T, N[1] + 1)
        Σ2 = ones(T, N[2] + 1)
        Σ1[(Nc[1]:N[1]) .+ 1] .= 0
        Σ2[(Nc[2]:N[2]) .+ 1] .= 0

        W1 = V1 * Diagonal(Σ1) / V1
        W2 = V2 * Diagonal(Σ2) / V2

        filter = ClimateMachine.Mesh.Filters.CutoffFilter(grid, Nc)
        @test filter.filter_matrices[1] ≈ W1
        @test filter.filter_matrices[2] ≈ W2
    end

    let
        T = Float64
        N = (5, 3)
        Nc = (4, 2)

        topology = ClimateMachine.Mesh.Topologies.BrickTopology(
            MPI.COMM_SELF,
            -1.0:2.0:1.0,
        )
        grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
            topology;
            polynomialorder = N,
            FloatType = T,
            DeviceArray = Array,
        )

        ξ = ClimateMachine.Mesh.Grids.referencepoints(grid)
        ξ1 = ξ[1]
        ξ2 = ξ[2]
        a1, b1 = GaussQuadrature.legendre_coefs(T, N[1])
        a2, b2 = GaussQuadrature.legendre_coefs(T, N[2])
        V1 = GaussQuadrature.orthonormal_poly(ξ1, a1, b1)
        V2 = GaussQuadrature.orthonormal_poly(ξ2, a2, b2)

        Σ1 = ones(T, N[1] + 1)
        Σ2 = ones(T, N[2] + 1)
        Σ1[(Nc[1]:N[1]) .+ 1] .= 0
        Σ2[(Nc[2]:N[2]) .+ 1] .= 0

        W1 = V1 * Diagonal(Σ1) / V1
        W2 = V2 * Diagonal(Σ2) / V2

        filter =
            ClimateMachine.Mesh.Filters.MassPreservingCutoffFilter(grid, Nc)
        @test filter.filter_matrices[1] ≈ W1
        @test filter.filter_matrices[2] ≈ W2
    end

end

struct FilterTestModel{N} <: ClimateMachine.BalanceLaws.BalanceLaw end
ClimateMachine.BalanceLaws.vars_state(::FilterTestModel, ::Auxiliary, FT) =
    @vars()
ClimateMachine.BalanceLaws.init_state_auxiliary!(::FilterTestModel, _...) =
    nothing

# Legendre Polynomials
l0(r) = 1
l1(r) = r
l2(r) = (3 * r^2 - 1) / 2
l3(r) = (5 * r^3 - 3r) / 2

low(x, y, z) = l0(x) * l0(y) + 4 * l1(x) * l1(y) + 5 * l1(z) + 6 * l1(z) * l1(x)

high(x, y, z) = l2(x) * l3(y) + l3(x) + l2(y) + l3(z) * l1(y)

filtered(::EveryDirection, dim, x, y, z) = high(x, y, z)
filtered(::VerticalDirection, dim, x, y, z) =
    (dim == 2) ? l2(x) * l3(y) + l2(y) : l3(z) * l1(y)
filtered(::HorizontalDirection, dim, x, y, z) =
    (dim == 2) ? l2(x) * l3(y) + l3(x) : l2(x) * l3(y) + l3(x) + l2(y)

ClimateMachine.BalanceLaws.vars_state(::FilterTestModel{4}, ::Prognostic, FT) =
    @vars(q1::FT, q2::FT, q3::FT, q4::FT)
function ClimateMachine.BalanceLaws.init_state_prognostic!(
    ::FilterTestModel{4},
    state::Vars,
    aux::Vars,
    localgeo,
    filter_direction,
    dim,
)
    (x, y, z) = localgeo.coord
    state.q1 = low(x, y, z) + high(x, y, z)
    state.q2 = low(x, y, z) + high(x, y, z)
    state.q3 = low(x, y, z) + high(x, y, z)
    state.q4 = low(x, y, z) + high(x, y, z)

    if !isnothing(filter_direction)
        state.q1 -= filtered(filter_direction, dim, x, y, z)
        state.q3 -= filtered(filter_direction, dim, x, y, z)
    end
end

@testset "Exponential and Cutoff filter application" begin
    N = 3
    Ne = (1, 1, 1)

    @testset for FT in (Float64, Float32)
        @testset for dim in 2:3
            @testset for direction in (
                EveryDirection,
                HorizontalDirection,
                VerticalDirection,
            )
                brickrange = ntuple(
                    j -> range(FT(-1); length = Ne[j] + 1, stop = 1),
                    dim,
                )
                topl = ClimateMachine.Mesh.Topologies.BrickTopology(
                    MPI.COMM_WORLD,
                    brickrange,
                    periodicity = ntuple(j -> true, dim),
                )

                grid =
                    ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
                        topl,
                        FloatType = FT,
                        DeviceArray = ClimateMachine.array_type(),
                        polynomialorder = N,
                    )

                filter = ClimateMachine.Mesh.Filters.CutoffFilter(grid, 2)

                model = FilterTestModel{4}()
                dg = ClimateMachine.DGMethods.DGModel(
                    model,
                    grid,
                    nothing,
                    nothing,
                    nothing;
                    state_gradient_flux = nothing,
                )

                @testset for target in ((1, 3), (:q1, :q3))
                    Q = ClimateMachine.DGMethods.init_ode_state(
                        dg,
                        nothing,
                        dim,
                    )
                    ClimateMachine.Mesh.Filters.apply!(
                        Q,
                        target,
                        grid,
                        filter,
                        direction = direction(),
                    )
                    P = ClimateMachine.DGMethods.init_ode_state(
                        dg,
                        direction(),
                        dim,
                    )
                    @test Array(Q.data) ≈ Array(P.data)
                end
            end
        end
    end
end

@testset "Mass Preserving Cutoff filter application" begin
    N = 3
    Ne = (1, 1, 1)

    @testset for FT in (Float64, Float32)
        @testset for dim in 2:3
            @testset for direction in (
                EveryDirection,
                HorizontalDirection,
                VerticalDirection,
            )
                brickrange = ntuple(
                    j -> range(FT(-1); length = Ne[j] + 1, stop = 1),
                    dim,
                )
                topl = ClimateMachine.Mesh.Topologies.BrickTopology(
                    MPI.COMM_WORLD,
                    brickrange,
                    periodicity = ntuple(j -> true, dim),
                )

                grid =
                    ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
                        topl,
                        FloatType = FT,
                        DeviceArray = ClimateMachine.array_type(),
                        polynomialorder = N,
                    )

                filter = ClimateMachine.Mesh.Filters.MassPreservingCutoffFilter(
                    grid,
                    2,
                )

                model = FilterTestModel{4}()
                dg = ClimateMachine.DGMethods.DGModel(
                    model,
                    grid,
                    nothing,
                    nothing,
                    nothing;
                    state_gradient_flux = nothing,
                )

                @testset for target in ((1, 3), (:q1, :q3))
                    Q = ClimateMachine.DGMethods.init_ode_state(
                        dg,
                        nothing,
                        dim,
                    )
                    ClimateMachine.Mesh.Filters.apply!(
                        Q,
                        target,
                        grid,
                        filter,
                        direction = direction(),
                    )
                    P = ClimateMachine.DGMethods.init_ode_state(
                        dg,
                        direction(),
                        dim,
                    )
                    @test Array(Q.data) ≈ Array(P.data)
                end
            end
        end
    end
end

ClimateMachine.BalanceLaws.vars_state(
    ::FilterTestModel{1},
    ::Prognostic,
    FT,
) where {N} = @vars(q::FT)
function ClimateMachine.BalanceLaws.init_state_prognostic!(
    ::FilterTestModel{1},
    state::Vars,
    aux::Vars,
    localgeo,
)
    (x, y, z) = localgeo.coord
    state.q = abs(x) - 0.1
end

@testset "TMAR filter application" begin

    N = 4
    Ne = (2, 2, 2)

    @testset for FT in (Float64, Float32)
        @testset for dim in 2:3
            brickrange =
                ntuple(j -> range(FT(-1); length = Ne[j] + 1, stop = 1), dim)
            topl = ClimateMachine.Mesh.Topologies.BrickTopology(
                MPI.COMM_WORLD,
                brickrange,
                periodicity = ntuple(j -> true, dim),
            )

            grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
                topl,
                FloatType = FT,
                DeviceArray = ClimateMachine.array_type(),
                polynomialorder = N,
            )

            model = FilterTestModel{1}()
            dg = ClimateMachine.DGMethods.DGModel(
                model,
                grid,
                nothing,
                nothing,
                nothing;
                state_gradient_flux = nothing,
            )

            @testset for target in ((1,), (:q,), :)
                Q = ClimateMachine.DGMethods.init_ode_state(dg)

                initialsumQ = weightedsum(Q)
                @test minimum(Q.realdata) < 0

                ClimateMachine.Mesh.Filters.apply!(
                    Q,
                    target,
                    grid,
                    ClimateMachine.Mesh.Filters.TMARFilter(),
                )

                sumQ = weightedsum(Q)

                @test minimum(Q.realdata) >= 0
                @test isapprox(initialsumQ, sumQ; rtol = 10 * eps(FT))
            end
        end
    end
end

function cubedshellwarp(a, b, c, R = max(abs(a), abs(b), abs(c)))

    function f(sR, ξ, η)
        X, Y = tan(π * ξ / 4), tan(π * η / 4)
        x1 = sR / sqrt(X^2 + Y^2 + 1)
        x2, x3 = X * x1, Y * x1
        x1, x2, x3
    end

    fdim = argmax(abs.((a, b, c)))
    if fdim == 1 && a < 0
        # (-R, *, *) : Face I from Ronchi, Iacono, Paolucci (1996)
        x1, x2, x3 = f(-R, b / a, c / a)
    elseif fdim == 2 && b < 0
        # ( *,-R, *) : Face II from Ronchi, Iacono, Paolucci (1996)
        x2, x1, x3 = f(-R, a / b, c / b)
    elseif fdim == 1 && a > 0
        # ( R, *, *) : Face III from Ronchi, Iacono, Paolucci (1996)
        x1, x2, x3 = f(R, b / a, c / a)
    elseif fdim == 2 && b > 0
        # ( *, R, *) : Face IV from Ronchi, Iacono, Paolucci (1996)
        x2, x1, x3 = f(R, a / b, c / b)
    elseif fdim == 3 && c > 0
        # ( *, *, R) : Face V from Ronchi, Iacono, Paolucci (1996)
        x3, x2, x1 = f(R, b / c, a / c)
    elseif fdim == 3 && c < 0
        # ( *, *,-R) : Face VI from Ronchi, Iacono, Paolucci (1996)
        x3, x2, x1 = f(-R, b / c, a / c)
    else
        error("invalid case for cubedshellwarp: $a, $b, $c")
    end

    return x1, x2, x3
end

@testset "Mass Preserving Cutoff Filter Conservation Test" begin
    N = 3
    Ne = (1, 1, 1)
    dim = 3
    # dim, direction
    @testset for FT in (Float64,)
        Rrange = [FT(1.0), FT(1.2)]

        topl = ClimateMachine.Mesh.Grids.StackedCubedSphereTopology(
            MPI.COMM_WORLD,
            1,
            Rrange,
            boundary = (5, 6),
        )

        grid = ClimateMachine.Mesh.Grids.DiscontinuousSpectralElementGrid(
            topl,
            FloatType = FT,
            DeviceArray = ClimateMachine.array_type(),
            polynomialorder = (N, N),
            meshwarp = cubedshellwarp,
        )

        mp_filter =
            ClimateMachine.Mesh.Filters.MassPreservingCutoffFilter(grid, 2)

        reg_filter = ClimateMachine.Mesh.Filters.CutoffFilter(grid, 2)

        model = FilterTestModel{4}()
        dg = ClimateMachine.DGMethods.DGModel(
            model,
            grid,
            nothing,
            nothing,
            nothing;
            state_gradient_flux = nothing,
        )

        # test mp filter
        filter = mp_filter
        Q = ClimateMachine.DGMethods.init_ode_state(dg, nothing, dim)
        sum_before_1 = weightedsum(Q, 1)
        sum_before_2 = weightedsum(Q, 2)
        sum_before_3 = weightedsum(Q, 3)
        target = 1:3
        ClimateMachine.Mesh.Filters.apply!(Q, target, grid, filter)
        sum_after_1 = weightedsum(Q, 1)
        sum_after_2 = weightedsum(Q, 2)
        sum_after_3 = weightedsum(Q, 3)

        @test sum_before_1 ≈ sum_after_1
        @test sum_before_2 ≈ sum_after_2
        @test sum_before_3 ≈ sum_after_3

        # test regular filter
        filter = reg_filter
        Q = ClimateMachine.DGMethods.init_ode_state(dg, nothing, dim)
        sum_before_1 = weightedsum(Q, 1)
        sum_before_2 = weightedsum(Q, 2)
        sum_before_3 = weightedsum(Q, 3)
        target = 1:3
        ClimateMachine.Mesh.Filters.apply!(Q, target, grid, filter)
        sum_after_1 = weightedsum(Q, 1)
        sum_after_2 = weightedsum(Q, 2)
        sum_after_3 = weightedsum(Q, 3)

        @test !(sum_before_1 ≈ sum_after_1)
        @test !(sum_before_2 ≈ sum_after_2)
        @test !(sum_before_3 ≈ sum_after_3)

    end
end


# Generic Cholesky decomposition for fixed-size matrices, mostly unrolled

# Currently all sanity checks are disabled!
@generated function Base.chol(A::StaticMatrix)
    if size(A) === (1, 1)
        return :(_chol1(A))
    elseif size(A) === (2, 2)
        #ishermitian(A) || Base.LinAlg.non_hermitian_error("chol")
        return :(_chol2(A))
    elseif size(A) === (3, 3)
        #ishermitian(A) || Base.LinAlg.non_hermitian_error("chol")
        return :(_chol3(A))
    else
        return :(chol(Array(A)))
    end
end

@generated function _chol1(A::StaticMatrix)
    @assert size(A) == (1,1)
    T = promote_type(typeof(sqrt(one(eltype(A)))), Float32)
    newtype = similar_type(A,T)

    quote
        $(Expr(:meta, :inline))
        ($newtype)((sqrt(A[1]), ))
    end
end

@generated function _chol2(A::StaticMatrix)
    @assert size(A) == (2,2)
    T = promote_type(typeof(sqrt(one(eltype(A)))), Float32)
    newtype = similar_type(A,T)

    quote
        $(Expr(:meta, :inline))
        @inbounds a = sqrt(A[1])
        @inbounds b = A[3] / a
        @inbounds c = sqrt(A[4] - b'*b)
        ($newtype)((a, $(zero(T)), b, c))
    end
end

@generated function _chol3(A::StaticMatrix)
    @assert size(A) == (3,3)
    T = promote_type(typeof(sqrt(one(eltype(A)))), Float32)
    newtype = similar_type(A,T)

    quote
        $(Expr(:meta, :inline))
        @inbounds a11 = sqrt(A[1])
        @inbounds a12 = A[4] / a11
        @inbounds a22 = sqrt(A[5] - a12'*a12)
        @inbounds a13 = A[7] / a11
        @inbounds a23 = (A[8] - a12'*a13) / a22
        @inbounds a33 = sqrt(A[9] - a13'*a13 - a23'*a23)
        ($newtype)((a11, $(zero(T)), $(zero(T)), a12, a22, $(zero(T)), a13, a23, a33))
    end
end


#=##############################################################################
# DESCRIPTION
    Post-processing acoustic methods.

# AUTHORSHIP
  * Author    : Eduardo J. Alvarez and Tyler Critchfield
  * Email     : Edo.AlvarezR@gmail.com
  * Created   : Nov 2019
  * License   : MIT
=###############################################################################

"Adds two sound pressure levels together."
function addSPL(x, y)

    #* For better understanding of what's going on, the following commented
    #* steps are equivalent to the returned line
    # pref = 2e-5

    # rms1 = pref^2 * 10 ^ (oaspl1 / 10) # convert to root mean square pressure
    # rms2 = pref^2 * 10 ^ (oaspl2 / 10)
    # rms_combined = rms1 + rms2 # add together

    # oaspl_combined = 10 * log10(rms_combined / (pref^2)) # convert back to decibels

    # return oaspl_combined

    return 10 * log10.(10 .^ (x/10) .+ 10 .^ (y/10))
end

"Adds a vector of sound pressure levels."
function addSPL(x::AbstractArray)

    return 10 * log10.(sum(10 .^ (x/10)))
end

"SPL spectrum to OASPL - works for A-weighted and non-A-weighted
    spectrums"
function SPL2OASPL(spls)                # vector of sound pressure levels over the spectrum

    # don't need a reference pressure when converting back and forth
    ps = 10 .^ (spls / 20)
    ps_sum = sum(ps.^2)

    oaspl = 10 * log10(ps_sum)

    return oaspl
end

"Takes a sound pressure level spectrum in the frequency domain and
    converts it to pressure in the time domain through an inverse
    fast fourier transform."
function spl2pressure(spls)             # vector of sound pressure levels over the spectrum

    pref = 20e-6 # reference pressure
    p2s = pref^2 * 10 .^ (spls / 10.0)

    # inverse fourier transform
    ps = FFTW.ifft(p2s)

    return ps
end

"Takes in an unweighted SPL in decibels and frequency and returns
    the A-weighted SPL in decibels"
function aWeight(freq,                    # frequency
                 spl)                     # unweighted sound pressure level at each frequency

    if size(freq, 1) != size(spl, 1) # ensure vectors have same dimensions
        spl = spl'
    end

    wa = aWeight(freq)

    pref = 2e-5 #? may not need?

    p = pref^2 * 10.0 .^ (spl / 10) # convert to Pa
    pa = wa .* p # a-weight

    spla = 10 * log10.(pa / pref^2) # back to dB

    return spla
end

function aWeight(freq)

    # unchanging constants defined in wopwop user manual
    K1 = 2.243e16 #s^-4
    K3 = 1.562
    f1 = 20.599 # Hz
    f2 = 107.653 # Hz
    f3 = 737.862 # Hz
    f4 = 12194.22 # Hz

    wc = K1 * freq .^ 4 ./ ((freq .^ 2 .+ f1^2) .^ 2 .* (freq .^ 2 .+ f4^2) .^ 2) # C-weighting function
    wa = wc .* K3 .* freq .^ 4 ./ ((freq .^ 2 .+ f2^2) .* (freq .^ 2 .+ f3^2)) # A-weighting function
end

"Takes pressure output from wopwop and converts it to sound pressure
    level spectrum or a scalar overall sound pressure level if
    oasplFlag == true (for both cases, it is A-weighted if
    AweightingFlag == true).

Note 2: Currently does not include any windowing or filtering methods.
"
function pressure2SPL(time,                     # time history
                        p;                      # pressure (Pa) at each time step
                        AweightingFlag=false,   # SPL values are A-weighted
                        oasplFlag=false)        # returns scalar OASPL en lieu of SPL spectrum

    pref = 2e-5

    deltat = time[2] - time[1]
    T = time[end]
    deltaf = 1/T
    N = length(time)
    M = convert(Int64, floor(N/2 + 1))

    x = 0:M-1
    f = x * deltaf # frequencies

    Y = FFTW.fft(p)
    Y = Y[1:M] # cutoff frequencies higher than Nyquist frequency

    Pc = Y * 2 * deltat / T
    Pc[1] *= 0.5 # complex pressure

    p2local = 0.5 * abs2.(Pc) #mean square pressure
    spls = 10 * log10.(p2local / pref^2) #sound pressure level

    if AweightingFlag == true
        spls = aWeight(f, spls)
        if oasplFlag
            p2local = pref^2 * 10 .^ (spls / 10)
        end
    end

    if oasplFlag
        p2 = 1.0 * sum(p2local)
        oaspl = 10 * log10(p2 / pref^2)

        return oaspl
    else
        return f, spls
    end
end

"Same as function above, but weights the SPLs based on the frequency bin width.

Note: Intuitively this seems like it should work better. It will not match OASPL output from PSU-WOPWOP."
function pressure2SPL_weighted(time,                     # time history
                                p;                      # pressure (Pa) at each time step
                                AweightingFlag=false,   # SPL values are A-weighted
                                oasplFlag=false)        # returns scalar OASPL en lieu of SPL spectrum

    pref = 2e-5

    deltat = time[2] - time[1]
    T = time[end]
    deltaf = 1/T
    N = length(time)
    M = convert(Int64, floor(N/2 + 1))

    x = 0:M-1
    f = x * deltaf # frequencies

    Y = FFTW.fft(p)
    Y = Y[1:M] # cutoff frequencies higher than Nyquist frequency

    Pc = Y * 2 * deltat / T
    Pc[1] *= 0.5 # complex pressure

    p2local = 0.5 * abs2.(Pc) * deltaf #mean square pressure
    spls = 10 * log10.(p2local / pref^2) #sound pressure level

    if AweightingFlag == true
        spls = aWeight(f, spls)
        if oasplFlag
            p2local = pref^2 * 10 .^ (spls / 10)
        end
    end

    if oasplFlag
        p2 = 1.0 * sum(p2local)
        oaspl = 10 * log10(p2 / pref^2)

        return oaspl
    else
        return f, spls
    end
end

# """
#     `pressure_time2frequency(time, pressure, windowtype, SPLtype)`

# Converts pressure from time domain to frequency domain, calculates overall sound pressure level (OASPL)

# # ARGUMENTS
# * `time::Array{Real, 1}`        : Vector of time segments associated with each pressure.
# * `pressure::Array{Real, 1}`    : Pressure as a function of time.

# # OPTIONAL ARGUMENTS
# * `windowtype::String`          : Data windowing options:
#                                     "None", "Hann"
# * `windowtype::String`          : Specifies wether non-weighted or A-weighted SPL should be calculated. Options:
#                                     "Normal", "A-weighted"

# """
# function pressure_time2frequency(time::Array{Float64, 1}, pressure::Array{Float64, 1},
#                                     windowtype::String, SPLtype::String)

#     #Define constants
#     K1 = 2.243*10^16
#     K3 = 1.562
#     f1 = 20.599
#     f2 = 107.653
#     f3 = 737.862
#     f4 = 12194.22

#     #Time & frequency parameters
#     deltat = time[2] - time[1] #time spacing
#     T = time[end] #total time
#     deltaf = 1/T #frequency spacing
#     N = length(time) # number of time points
#     M = convert(Int64,floor(N/2 + 1)) # number of frequency bins of type Int64 for indexing later

#     # println("Pressure before windowing:")
#     # println(pressure)
#     # Window data if specified - treats data as periodic for fourier transform
#     if windowtype != "None"
#         pressure = FLOWNoise.windowdata!(time, pressure, windowtype)
#     end

#     # println("Pressure after windowing:")
#     # println(pressure)

#     # Use FFTW package to perform fast fourier transform on pressure data
#     Y = FFTW.fft(pressure)

#     # Initialize variables
#     Pc = zeros(Complex{Float64},M,1)
#     p2local = zeros(Float64,M,1)
#     SPLlocal_dB = zeros(Float64,M,1)
#     p2Alocal = zeros(Float64,M,1)
#     SPLlocal_dBA = zeros(Float64,M,1)
#     wc = ones(Float64,M,1)
#     wa = ones(Float64,M,1)
#     p2 = 0.0

#     x = 1:M
#     f = x*deltaf

#     for m in 1:M

#         if m > 1
#             Pc[m] = Y[m] * 2 * deltat / T #Complex pressure (magnitude and phase) at each frequency bin
#         else
#             Pc[m] = Y[m] * deltat / T
#         end

#         if SPLtype == "A-weighted" #if "Normal" then wa[m] = 1.0 and does not affect output

#             #A-weighted prep, from PSU-WOPWOP manual
#             wc[m] = K1 * f[m]^4 / ((f[m]^2 + f1^2)^2*(f[m]^2 + f4^2)^2)
#             wa[m] = wc[m] * K3 * f[m]^4 / ((f[m]^2 + f2^2)*(f[m]^2 + f3^2))

#         elseif SPLtype != "Normal"
#             error("Invalid SPLtype specified.")
#         end

#         # global p2 += wa[m] * abs2(Pc[m]) #overall mean square pressure
#         p2 += wa[m] * abs2(Pc[m]) #overall mean square pressure
#         p2local[m] = 0.5 * wa[m] * abs2(Pc[m]) #local mean square pressure for specific frequency
#         SPLlocal_dB[m] = 10.0 * log(10,p2local[m]/((20*10.0^-6)^2)) #local SPL

#     end

#     p2 *= 0.5

#     OASPL_dB = 10.0 * log(10,p2/(20*10.0^-6)^2) # overall sound pressure level in dB

#     return SPLlocal_dB, OASPL_dB
# end

"""
    `windowdata!(time, data, windowtype)`

Windows data, or "tapers" the ends to zero. This effectively makes the data periodic to avoid discontinuities in a fourier transform.

# ARGUMENTS
* `time::Array{Real, 1}`        : Vector of time segments associated with each data point in data.
* `data::Array{Real, 1}`        : Data (later to be transformed) as a function of time.

# OPTIONAL ARGUMENTS
* `windowtype::String`          : Data windowing options:
                                    "Hann", "Flat Top", "Blackman"

NOTES:
* Windowing changes the amplitude of the data in the time history, so a scaling factor must also be used, depending on which type of windowing is being performed.
* Windowing adds certain frequencies to the spectrum. Do not trust windoewed time signals which have frequencies closer than five bins away from both ends of the spectrum's frequency limits.
"""
function windowdata!(time, data, windowtype::String="Hann")
    T = time[end]
    tindex = 1
    # Apply windowing
    for t in time
        if windowtype == "Hann"
            wh = 1/2 - 1/2 * cos(2*pi*t/T)
            scalingfactor = 1.633
        elseif windowtype == "Flat Top"
            wh = 1 - 1.93 * cos(2*pi*t/T) + 1.29 * cos(4*pi*t/T) - 0.388 * cos(6*pi*t/T) + 0.0322 * cos(8*pi*t/T)
            scalingfactor = 0.515
        elseif windowtype == "Blackman"
            wh = 0.42 - 0.50 * cos(2*pi*t/T) + 0.08 * cos(4*pi*t/T)
            scalingfactor = 1.8119
        else
            error("Invalid windowtype defined.")
        end

        data[tindex] *= wh * scalingfactor
        # global tindex += 1
        tindex += 1
    end # TODO: broadcast this in the future

    return data

#TODO: currently not solving problem in case 2 not matching up
end


test_gage_principal_axes(
    [0. 1. 1.;
     0. 0. 0.;],
    [0.  1.  0.;
     sqrt(2)/2. 0. -sqrt(2)/2.;
     sqrt(2)/2. 0. sqrt(2)/2. ], "vector in the Y-Z plane")


export IterationResult, IterationStatus, ITERATION_CONVERGED, ITERATION_NUMERICAL_ISSUES, ITERATION_EXCEEDED_MAX_ITERATIONS, fixed_point_iteration

function within_tolerance(x, x_next, tolerance)
    for (a, b) in zip(x, x_next)
        if abs(a - b) >= tolerance
            return false
        end
    end

    return true
end

function numerical_issues(x)
    for elem in x
        if !isfinite(elem)
            return true
        end
    end

    return false
end

@enum IterationStatus begin
    ITERATION_CONVERGED
    ITERATION_NUMERICAL_ISSUES
    ITERATION_EXCEEDED_MAX_ITERATIONS
end

struct IterationResult
    status::IterationStatus
    delta::Vector{Float64}
    iterations::Int32
    evaluations::Int32
end

export SimpleFixedPointIteration

Base.@kwdef struct SimpleFixedPointIteration
    tolerance::Float64 = 1e-14
    max_iterations::Integer = 5_000
end

function fixed_point_iteration(iteration::SimpleFixedPointIteration, x0::AbstractVector, contraction!::Function)
    x = copy(x0)
    x_next = Vector{eltype(x)}(undef, length(x))
    iterations = 0

    while iterations < iteration.max_iterations
        contraction!(x_next, x)
        iterations += 1

        if numerical_issues(x_next)
            return IterationResult(ITERATION_NUMERICAL_ISSUES, x, iterations, iterations)
        end

        if within_tolerance(x, x_next, iteration.tolerance)
            return IterationResult(ITERATION_CONVERGED, x_next, iterations, iterations)
        end

        x .= x_next
    end

    return IterationResult(ITERATION_EXCEEDED_MAX_ITERATIONS, x, iterations, iterations)
end

export SQUAREMIteration, SQUAREM1, SQUAREM2, SQUAREM3

@enum SQUAREMScheme SQUAREM1=1 SQUAREM2=2 SQUAREM3=3

Base.@kwdef struct SQUAREMIteration
    max_iterations::Int = 1_000
    tolerance::Float64 = 1e-14
    scheme::SQUAREMScheme = SQUAREM3
    step_min::Float64 = 1.0
    step_max::Float64 = 1.0
    step_factor::Float64 = 4.0
end

function fixed_point_iteration(iteration::SQUAREMIteration, x_initial::AbstractVector, contraction!::Function)
    x0 = copy(x_initial)
    x1 = Vector{eltype(x0)}(undef, length(x0))
    x2 = Vector{eltype(x0)}(undef, length(x0))
    x_accel = Vector{eltype(x0)}(undef, length(x0))
    r = Vector{eltype(x0)}(undef, length(x0))
    v = Vector{eltype(x0)}(undef, length(x0))

    step_min = iteration.step_min
    step_max = iteration.step_max

    evaluations = 0
    iterations = 0

    while iterations < iteration.max_iterations
        # First step
        contraction!(x1, x0)
        evaluations += 1

        if numerical_issues(x1)
            return IterationResult(ITERATION_NUMERICAL_ISSUES, x0, iterations, evaluations)
        end

        if within_tolerance(x1, x0, iteration.tolerance)
            return IterationResult(ITERATION_CONVERGED, x1, iterations, evaluations)
        end

        # Second step
        contraction!(x2, x1)
        evaluations += 1

        if numerical_issues(x2)
            return IterationResult(ITERATION_NUMERICAL_ISSUES, x1, iterations, evaluations)
        end

        if within_tolerance(x2, x1, iteration.tolerance)
            return IterationResult(ITERATION_CONVERGED, x2, iterations, evaluations)
        end

        @. r = x1 - x0
        @. v = (x2 - x1) - r

        # Compute the step length
        if iteration.scheme == SQUAREM1
            alpha = (r ⋅ v) / (v ⋅ v)
        elseif iteration.scheme == SQUAREM2
            alpha = (r ⋅ r) / (r ⋅ v)
        else
            alpha = -sqrt((r ⋅ r) / (v ⋅ v))
        end

        # Bound the step length and update its bounds
        alpha = -max(step_min, min(step_max, -alpha))
        if -alpha == step_max
            step_max *= iteration.step_factor
        end
        if -alpha == step_min && step_min < 0
            step_min *= iteration.step_factor
        end

        # Acceleration step using Horner's Method
        # Equivalent to @. x_accel = x0 - 2 * alpha * r + alpha^2 * v
        @. x_accel = muladd(alpha, muladd(alpha, v, -2 * r), x0)

        contraction!(x0, x_accel)
        evaluations += 1

        iterations += 1

        if numerical_issues(x0)
            return IterationResult(ITERATION_NUMERICAL_ISSUES, x1, iterations, evaluations)
        end

        if within_tolerance(x0, x_accel, iteration.tolerance)
            return IterationResult(ITERATION_CONVERGED, x2, iterations, evaluations)
        end
    end

    return IterationResult(ITERATION_EXCEEDED_MAX_ITERATIONS, x0, iterations, evaluations)
end


# represents s*log(ε) + c as ε -> 0
struct LogNumber <: Number
    s::ComplexF64
    c::ComplexF64
end

@inline logpart(z::Number) = zero(z)
@inline finitepart(z::Number) = z

@inline logpart(l::LogNumber) = l.s
@inline finitepart(l::LogNumber) = l.c

Base.promote_rule(::Type{LogNumber}, ::Type{<:Number}) = LogNumber
Base.convert(::Type{LogNumber}, z::LogNumber) = z
Base.convert(::Type{LogNumber}, z::Number) = LogNumber(0, z)

==(a::LogNumber, b::LogNumber) = logpart(a) == logpart(b) && finitepart(a) == finitepart(b)
Base.isapprox(a::LogNumber, b::LogNumber; opts...) = ≈(logpart(a), logpart(b); opts...) && ≈(finitepart(a), finitepart(b); opts...)
Base.isapprox(a::LogNumber, b::Number; opts...) = ≈(a(1),b; opts...)
Base.isapprox(a::Number, b::LogNumber; opts...) = ≈(a,b(1); opts...)

(l::LogNumber)(ε) = logpart(l)*log(ε) + finitepart(l)

for op in (:real, :imag, :conj)
    @eval $op(l::LogNumber) = LogNumber($op(logpart(l)), $op(finitepart(l)))
end

for f in (:+, :-)
    @eval begin
        $f(a::LogNumber, b::LogNumber) = LogNumber($f(a.s, b.s), $f(a.c, b.c))
        $f(l::LogNumber, b::Number) = LogNumber(l.s, $f(l.c, b))
        $f(a::Number, l::LogNumber) = LogNumber($f(l.s), $f(a, l.c))
    end
end

-(l::LogNumber) = LogNumber(-l.s, -l.c)

for Typ in (:Bool, :Number)
    @eval begin
        *(l::LogNumber, b::$Typ) = LogNumber(l.s*b, l.c*b)
        *(a::$Typ, l::LogNumber) = LogNumber(a*l.s, a*l.c)
    end
end
/(l::LogNumber, b::Number) = LogNumber(l.s/b, l.c/b)

function exp(l::LogNumber)::ComplexF64
    if real(l.s) > 0
        0.0+0.0im
    elseif real(l.s) < 0
        Inf+Inf*im
    elseif real(l.s) == 0 && imag(l.s) == 0
        exp(l.c)
    else
        NaN + NaN*im
    end
end

Base.show(io::IO, x::LogNumber) = print(io, "($(logpart(x)))log ε + $(finitepart(x))")


@testset "Splitting Test" begin

    net_file = "$(@__DIR__)/../examples/networks/R2_R2.nnet"

    net = read_nnet(net_file, last_layer_activation = Id())
    A = [1. 0.; 0. 1.; -1. 0.; 0. -1.;]
    b = [1.1, 1.1, 0.1, 0.1]
    X = Hyperrectangle(low=[-1,-1], high=[1,1])
    # The true output set is a union of two segments : (0,0)->(0,1) U (0,0)->(1,0)
    Y = HPolytope(A, b) # Output constraints. Hyperrectangle: -0.1, -0.1, 1.1, 1.1
    problem = Problem(net, X, Y)

    @testset "Neurify" begin
        solver = Neurify()
        result = solve(solver, problem)
        @test result.status == :holds
    end

    @testset "ReluVal" begin
        solver = ReluVal()
        result = solve(solver, problem)
        @test result.status == :holds
    end

end


using Simulacrum

# all to all
n = rand(collect(1:1000))
@test length(indices(AllToAll(n))) == n * (n-1) # n-1 hyperedges (excluding loops)
@test_throws ErrorException AllToAll(1)
@test_throws ErrorException AllToAll(-1)

# cycle
n = rand((1:1000))
@test length(indices(Cycle(n))) == n
@test_throws ErrorException Cycle(0)
@test isa(Cycle(4), Simulacrum.AbstractPattern)
@test indices(Cycle(1)) == [(1,1)] # a cycle of length 1 is a loop

# path
n = rand((1:1000))
@test length(indices(Path(n))) == n-1
@test_throws ErrorException Path(0)


using LogNormals
using Test, Distributions, LinearAlgebra, StatsBase, Missings, Random
using OffsetArrays, RecursiveArrayTools

function boot_sem_cor()
    #using FillArrays
    ##using BandedMatrices
    ##using Bootstrap
    nsum = 100
    probgap = 0.6
    acf0 = [1,0.4,0.1]
    Sigma = cormatrix_for_acf(nsum, acf0);
    dmn = MvNormal(ones(nsum), Symmetric(Sigma));
    #always use the same gaps
    igap = sample(1:nsum,trunc(Int,nsum*probgap), replace=false)
    b = allowmissing(rand(dmn));
    b[igap] .= missing;
    sem_cor(b, acf0, ExactMissing())
    nboot = 10_000
    resboot = map(1:nboot) do iboot
        b = allowmissing(rand(dmn));
        b[igap] .= missing
        sum(skipmissing(b)), mean(skipmissing(b)), sem_cor(b, acf0, ExactMissing())
    end;
    sums_boot, means_boot, semcor_boot = vectuptotupvec(resboot);
    mean(sums_boot), std(sums_boot)
    mean(means_boot), std(means_boot)
    dvn = SimpleDistributionVector(Fill(Normal(1,1), nsum)...);
    dvn[ismissing.(b)] .= missing;
    dsum = sum(dvn, AutoCorrelationFunction(acf0), SkipMissing())
    dmean = mean(dvn, AutoCorrelationFunction(acf0), SkipMissing())
    @test dmean.σ ≈ std(means_boot) atol=0.01
    # mean over Normals with missings works
    function sem_cor_benchmark(x, acfe, ms::MissingStrategy=ExactMissing())
        n = length(x)
        n <= 1 && return(std(x))
        nmiss = count(ismissing.(x))
        nfin = n - nmiss
        neff = effective_n_cor(x, acfe)
        σ2uncorr = var(skipmissing(x))
        # BLUE Var(x) for correlated: Zieba11 eq.(1)
        σ2 = σ2uncorr*(nfin-1)*neff/(nfin*(neff-1))
        if ms === ExactMissing() && (nmiss != 0)
            σ = Fill(√σ2, n)
            dv = ParamDistributionVector(Normal{nonmissingtype(eltype(x))}, x, σ)
            acfes = n < length(acfe) ? view(acfe,1:n) : acfe
            dm = mean(dv, AutoCorrelationFunction(acfes), SkipMissing())
            #@show acfes, dm, length(dv), count(ismissing,dv), count(ismissing,x)
            return(dm.σ)
        else
            return(√(σ2/neff))
        end
    end
    res_sem_cor = sem_cor(b, acf0)
    @test res_sem_cor ≈ std(means_boot) atol=0.02
    res_sem_cor2a = sem_cor_benchmark(b, acf0, ms=ExactMissing())
    @test res_sem_cor2a ≈ std(means_boot) atol=0.02
    res_sem_cor2b = sem_cor_benchmark(b, acf0, ms=SkipMissing())
    @test res_sem_cor2b ≈ std(means_boot) atol=0.02
    #using BenchmarkTools: @btime
    @btime sem_cor_benchmark($b, $acf0, ms=ExactMissing())
    @btime sem_cor_benchmark($b, $acf0, ms=SkipMissing())
    # neff based method much faster
    function tmpf()
        #using StatsPlots
        plot(density(means_boot))
        vline!([mean(means_boot), quantile(means_boot,[0.025,0.975])...])
        #
        # plot(density(means_boot), label = "density mean")
        # vline!([mean(means_boot), quantile(means_boot,[0.025,0.975])...], label = "stats mean")
        density(semcor_boot, label = "density sem_cor")
        vline!([mean(semcor_boot), quantile(semcor_boot,[0.025,0.975])...], label = "stats sem_cor")
        vline!([std(means_boot)], label = "std(means_bootstrap)") # true mu and sigma
        vline!([dmean.σ], label = "sum_normals - true distribs") # true mu and sigma
    end
end

function inspect_lags_autocorrelation()
    nsum = 1_000
    acf0 = [1,0.4,0.1]
    Sigma = cormatrix_for_acf(nsum, acf0);
    dmn = MvNormal(ones(nsum), Symmetric(Sigma));
    a = rand(dmn);
    probgap = 0.4
    clustersize = 2
    #clustersize = 50
    nboot = 1_000
    resboot = map(1:nboot) do iboot
        igap0 = sample(0:(nsum-clustersize),trunc(Int,nsum*probgap/clustersize), replace=true);
        b = allowmissing(a);
        # outer product see https://stackoverflow.com/a/44592419
        #igap0.+Base.OneTo(clustersize)'
        b[igap0.+Base.OneTo(clustersize)'] .= missing;
        ae = autocor(b, 1:2, ms=ExactMissing())
        af = autocor(b, 1:2, ms=SkipMissing())
        (ae, af)
    end;
    ae, af = VectorOfArray.(vectuptotupvec(resboot));
    mean(ae; dims=2)
    mean(af; dims=2)
    autocor(a, 1:2)

end

function benchmark_count_forlags()
    #using BenchmarkTools: @btime
    function count_forlags_slower(pred, x,lags)
        cnt = zeros(size(lags))
        ax = OffsetArrays.no_offset_view(axes(x,1))
        lx = length(x)
        for i in 1:lx
            for (ik,k) in enumerate(lags)
                i > lx-k && continue
                if (pred(x[ax[i]])::Bool || pred(x[ax[i+k]])::Bool); cnt[ik] += 1; end
            end
        end
        cnt
    end
    x = repeat([1,2,missing,missing,5], 500)
    lags = 0:4
    @btime count_forlags($ismissing, $x, $lags)
    @btime count_forlags_slower($ismissing, $x, $lags)
    # the version with calling subfunction is faster
    @code_warntype count_forlags(ismissing, x, lags)
    @code_warntype LogNormals.count_forlags2(ismissing, x, lags)
    @code_warntype LogNormals.count_forlag(ismissing, x, 2)
end


using Vec
using Test
import LinearAlgebra: norm, normalize, dot
import Random: seed!

@test invlerp(0.0,1.0,0.5) ≈ 0.5
@test invlerp(0.0,1.0,0.4) ≈ 0.4
@test invlerp(0.0,1.0,0.7) ≈ 0.7
@test invlerp(10.0,20.0,12.0) ≈ 0.2

include("test_vecE2.jl")
include("test_vecE3.jl")
include("test_vecSE2.jl")
include("test_quat.jl")

include("test_coordinate_transforms.jl")
include("test_geomE2.jl")

include("test_diff.jl")


using Combinatorics

@testset "Veronese Lift" begin
    @test_throws DimensionMismatch permanent([1 2], collect(Combinatorics.permutations(1:2)))
    @test_throws DimensionMismatch SwitchOnSafety.veroneselift_explicit([1 2], 3)
    @test_throws ArgumentError SwitchOnSafety.veroneselift_explicit([1 2; 3 4], 0)
    @test_throws ArgumentError veroneselift([1 2; 3 4], 0)

    @test [1.0 2 * √3 4 * √3 8.0] ≈ @inferred veroneselift([1 2], 3)

    s3 = sqrt(3)
    expected = [11^3 s3*11^2*12 s3*11*12^2 12^3;
    s3*11^2*21 11*(11*22+2*21*12) 12*(2*11*22+21*12) s3*12^2*22;
    s3*11*21^2 21*(2*11*22+21*12) 22*(11*22+2*21*12) sqrt(3)*12*22^2;
    21^3 s3*21^2*22 s3*21*22^2 22^3]
    @test expected ≈ @inferred veroneselift([11 12; 21 22], 3)
    @test expected ≈ @inferred SwitchOnSafety.veroneselift_explicit([11 12; 21 22], 3)
end


#=
This file is part of Pseudospectra.jl.

Julia translation
copyright 2017 Ralph Smith

Portions derived from EigTool
Copyright (c) 2002-2014, The Chancellor, Masters and Scholars
of the University of Oxford, and the EigTool Developers. All rights reserved.

SPDX-License-Identifier: BSD-3-Clause
License-Filename: LICENSES/BSD-3-Clause_Eigtool
=#

"""
    grcar(N)

construct a Grcar non-normal matrix of rank N

This is a popular example in the field of matrix iterations
of a matrix whose spectrum is in the right half-plane but
whose numerical range is not.  It's also a popular example
in the study of nonsymmetric Toeplitz matrices.  The matrix
was first described in [^Grcar1989] and its pseudospectra were first
plotted in [^Trefethen1991].

[^Grcar1989]: J. F. Grcar, "Operator coefficient methods for linear equations", tech. report SAND89-8691, Sandia National Labs, 1989
[^Trefethen1991]: L. N. Trefethen, "Psuedospectra of matrices", in "Numerical Analysis 1991" (Dundee 1991), Longman Sci. Tech., Harlow, 1992, 234-266.
"""
function grcar(N::Integer)
  G = (diagm(0 => ones(N)) - diagm(-1 => ones(N-1)) +
      diagm(1 => ones(N-1)) + diagm(2 => ones(N-2)) +
      diagm(3 => ones(N-3)))
end

"""
    cheb(N)

construct a matrix representing differentiation of rank N+1 Chebyshev
value-space basis, based on [Trefethen2000]

[^Trefethen2000] L.N.Trefethen, *Spectral Methods in MATLAB*, SIAM, Philadelphia, 2000.
"""
function cheb(N::Integer)
    if N==0
        return 0.0,1.0
    end
    x = cos.((π/N)*collect(0:N))
    c = vcat(2,ones(N-1),2) .* map(x -> (-1)^x,0:N)
    dX = x .- x'
    D = (c*(1 ./ c)') ./ (dX + I)
    D = D - diagm(0 => vec(sum(D, dims=2)))
    return D,x
end

"""
    advdiff(N,η=0.015)

construct the matrix representing an advection-diffusion operator via
Chebyshev collocation.
"""
function advdiff(N,η=0.015)
    D,x = cheb(N)
    # rescale to interval 0:1
    D = 2*D
    x = (x .+ 1)*(1/2)
    L = η*D^2 + D
    # impose boundary conditions
    L = L[2:N,2:N]
    # Gauss-Chebyshev quadratrure weights
    w = sqrt.(π*sqrt.(x-x.^2)*(1/(2N)))
    # convert to L2 norm on function space
    w = w[2:N]
    L = diagm(0 => w) * L * diagm(0 => 1.0 ./ w)
end

"""
    orrsommerfeld(N,R=5722,α=1.0)

construct a matrix representing the Orr-Sommerfeld operator in the
rank N+1 Chebyshev value-space basis.
Note: this imposes a norm of dubious physical meaning, since a clever
trick is used to suppress spurious eigenvalues.

[^HS1994] W.Z.Huang and D.M.Sloan, "The pseudospectral method for solving differential eigenvalue equations," J. Comput. Phys. 111, 399-409 (1994).
"""
function orrsommerfeld(N,R=5722,α=1.0)
    D,x = cheb(N)
    D2 = D^2
    D2 = D2[2:N,2:N]
    S = diagm(0 => [0; 1 ./(1 .- x[2:N].^2); 0])
    D4 = (diagm(0 => 1 .- x.^2)*D^4 - 8*diagm(0 => x)*D^3 - 12*D^2)*S
    D4 = D4[2:N,2:N]
    a2 = α^2
    A = (D4-2*a2*D2+a2^2*I)*(1/R) - 2α*im*I - α*im*diagm(0 => 1 .- x[2:N].^2)*(D2-a2*I)
    B = D2-a2*I
    C = B\A
end

"""
    trefethen_tutorial(N) -> B,A

compute the matrices used for tutorial purposes
in [^Trefethen1999]. `A` is the Chebyshev discretization
of a Schrödinger operator with complex potential.
`B` includes weight factors so that a basic L2 norm is appropriate.

[^Trefethen1999]: L.N.Trefethen, "Computation of Pseudospectra", Acta Numerica 1999.
"""
function trefethen_tutorial(N)
    D = zeros(N+2,N+2)
    iran = 0:(N+1)
    ci = vcat(2,ones(N),2)
    x = cos(π*iran*(1/(N+1)))
    for j=0:N+1
        cj = ((j==0) || (j==N+1)) ? 2 : 1
        denom = cj * (x[iran+1]-x[j+1])
        denom[j+1] = 1
        D[iran+1,j+1] = ci .* (-1.0).^(iran+j) ./ denom
        if (j > 0) && (j<N+1)
            D[j+1,j+1] = -(1/2)*x[j+1] / (1-x[j+1]^2)
        end
    end
    D[1,1] = (2*(N+1)^2+1)/6
    D[N+2,N+2] = -(2*(N+1)^2+1)/6
    L = 10.0
    D = D / L
    A = D^2
    # absorb boundary conditions
    x = x[2:N+1]
    x = L*x
    A = A[2:N+1,2:N+1]
    A = A + (3+3im) * diagm(0 => x.^2) - (1/16)*diagm(0=> x.^4)
    # absorb Chebyshev weights
    w = sqrt.(π*sqrt(L^2 - x.^2) * (1 / (2*(N+1))))
    B = zeros(eltype(A),N,N)
    for j=1:N
        B[:,j] = w .* A[:,j] * (1/w[j])
    end
    B,A
end

"""
    convdiff_fd(N)

construct matrix representation of 2-D NxN convection-diffusion operator

convdiff_fd(N) returns matrix C of dimension N^2 representing the
operator
         -ϵ ∇^2 u  + ∂u/∂y

on the unit square with Dirichlet boundary conditions. The
discretisation is done using finite differences (see, e.g., [^Hemmingsson1998]),
resulting in large, sparse matrices. The code is based on a
routine by Daniel Loghin.

[^Hemmingsson1998]: L. Hemmingsson, "A Semi-circulant Preconditioner for the Convection-Diffusion Equation", Numer. Math 81, 211-248 (1998).

"""
function convdiff_fd(N)

    # Diffusion parameter
    e = 1e-2

    # Wind blowing in y-direction.
    w1 = 0
    w2 = 1

    # Set up the grid size (more points in y direction due to wind)
    n1 = N
    n2 = 4*N
    h1 = 1/(n1+1)
    h2 = 1/(n2+1)

    # Generate 1D finite difference matrices
    alpha1 = w1/h1
    beta1 = 2*e/h1^2
    alpha2 = w2/h2
    beta2 = 2*e/h2^2

    A1 = spdiagm(-1 => (-alpha1-beta1)*ones(n1-1),
                  0 => 2*beta1*ones(n1),
                  1 => (alpha1-beta1)*ones(n1-1))
    A2 = spdiagm(-1 => (-alpha2-beta2)*ones(n2-1),
                  0 => 2*beta2*ones(n2),
                  1 => (alpha2-beta2)*ones(n2-1))

    # Now compute the 2D finite difference matrix
    C = kron(sparse(1.0I,n2,n2),A1) + kron(A2,sparse(1.0I,n1,n1))
end

function gallery5(T=Float64)
    @assert T <: Real
    A = hcat([-9,70,-575,3891,1024],
             [11,-69,575,-3891,-1024],
             [-21,141,-1149,7782,2048],
             [63,-421,3451,-23345,-6144],
             [-252,1684,-13801,93365,24572])
    Matrix{T}(A)
end

"""
    kahan(N)

construct Kahan's matrix of rank N

Kahan's matrix [^Kahan1966] was devised to illustrate that QR
factorisation with column pivoting is not a fail-safe
method for determining the rank of the matrix. Rank
determination is related to the "distance to singularity"
of a matrix [^Higham1988], or in other words, how large a perturbation
is needed to make the matrix singular. The pseudospectra are
shown in [^Trefethen1991].

[^Higham1988]: N. J. Higham, "Matrix nearness problems and applications", NA Rep. 161, Dept. of Maths., U. of Manchester, June 1988.
[^Kahan1966]: W. Kahan, "Numerical linear algebra", Canad. Math. Bull. 9, pp. 757-801, 1966.
[^Trefethen1991]: L. N. Trefethen, "Psuedospectra of matrices", in "Numerical Analysis 1991" (Dundee 1991), Longman Sci. Tech., Harlow, 1992, 234-266.
"""
function kahan(N)
    s = (1/10)^(1/(N-1))
    c = sqrt(1-s^2)

    col = -s.^(0:N-1)*c
    K = triu(repmat(col,1,N),1)+diagm(0 => s.^(0:N-1))
end

"""
    demmel(N,M)

construct Demmel's matrix of rank N with edge value M

Demmel's matrix was perhaps the first matrix
whose pseudospectra (with N=3) appeared in [^Demmel1987]
Demmel devised the matrix in order to disprove
a conjecture of Van Loan's [^VanLoan1985].

[^Demmel1987]: J. W. Demmel, "A counterexample for two conjectures about stability", IEEE Trans. Auto. Control, AC-32, pp. 340-343, 1987.
[^VanLoan1985]: C. Van Loan, "How near is a stable matrix to an unstable matrix?", in R. Brualdi, et al., eds, Contemporary Mathematics 47, Amer. Math. Soc., 1985
"""
function demmel(N,M=1e4)
    isdefined(Main, :ToeplitzMatrices) ||
        throw(ErrorException("this function requires the ToeplitzMatrices package"))
  B = M^(1/(N-1))
  c = vcat(ones(1), zeros(N-1))
  r = B.^(0:N-1)
  D = -full(Main.ToeplitzMatrices.Toeplitz(c,r))
end

"""
    landau_fox_li(N,F)

construct a Landau matrix of rank N for Fresnel number F

The discretization of a Fox-Li operator in the theory of laser cavities
by [^Landau1977] is one of the earliest applications of pseudospectra.

[^Landau1977]: H. J. Landau, "The notion of approximate eigenvalues applied to an integral equation of laser theory", Quart. Appl. Math. 35 (1977), pp. 165-172.
"""
function landau_fox_li(N,F=0)
    (F == 0) && (F = (N > 200) ? 32 : 12)
    # calculate Gaussian quadrature nodes and weights
    β = 0.5 * (1 .- (2.0 * collect(1:N-1)).^(-2)) .^(-1/2)
    T = diagm(1 => β) + diagm(-1 => β)
    ep = eigen(T)
    d,V = ep.values, ep.vectors
    idx = sortperm(d)
    nodes = d[idx]
    w = zeros(N)
    w[1:N] = 2*V[1,idx].^2
    B = zeros(N,N) .+ 0im
    for k in 1:N
        B[k,:] = w' * sqrt(F*im) .* exp.(-im*π*F*(nodes[k] .- nodes').^2)
    end
    w = sqrt.(w)
    for j in 1:N
        B[:,j] = w .* B[:,j] * (1/w[j])
    end
    B
end

"""
    supg(N)

construct a SUPG matrix for an NxN mesh

The matrix arises from SUPG discretisation of an advection-diffusion operator.

This demo is based on a function by Mark Embree, which
was essentially distilled from the IFISS software of
Elman and Silvester.  See [^Fischer1999] and [^DJS].

For this example, whilst the eigenvalues computed by eigs
are inaccurate due to the sensitivity of the problem, the
approximate pseudospectra are very good approximations to
the true ones.

[^Fischer1999]: B. Fischer, A. Ramage, D. Silvester and A. Wathen, "Towards Parameter-Free Streamline Upwinding for Advection-Diffusion Problems", Comput. Methods Appl. Mech. Eng., 179, 1999, 185-202.
[^DJS]: http://www.ma.umist.ac.uk/djs/software.html
"""
function supg(N)
    h = 1/(N+1)
    theta = 0
    nu = (N < 20) ? 0.01 : 0.0001

    # Compute the "optimal" upwinding parameter
    hbar = h/(max(sin(theta), cos(theta)))
    delta = max(0, 1/2 - nu/hbar)

    w = [-sin(theta),cos(theta)]
    wx2 = w[1]^2
    wy2 = w[2]^2

    if (N > 10)
        A = spzeros(N*N, N*N)
        v = ones(N)
        for j=1:N
            blk =  v*(nu*[-1/3 8/3 -1/3] +
                      h*[-w[1]/3  0 w[1]/3] +
                      delta*h*[(wy2-2*wx2)/3 4/3*(wx2+wy2) (wy2-2*wx2)/3])
            A[N*(j-1)+1:N*j, N*(j-1)+1:N*j] = spdiagm(-1 => blk[1:end-1,1],
                                                       0 => blk[:,2],
                                                       1 => blk[1:end-1,3])
            if (j>1)
                blk = v*(nu*[-1/3 -1/3 -1/3] +
                         h*[-(w[1]+w[2])/12 -w[2]/3 (w[1]-w[2])/12] +
                         delta*h*
                         [(-(wx2+wy2)/6-w[1]*w[2]/2) (wx2-2*wy2)/3 (-(wx2+wy2)/6+w[1]*w[2]/2)])
                A[N*(j-1)+1:N*j, N*(j-2)+1:N*(j-1)] = spdiagm(-1 => blk[1:end-1,1],
                                                               0 => blk[:,2],
                                                               1 => blk[1:end-1,3])
            end
            if (j<N)
                blk = v*(nu*[-1/3 -1/3 -1/3] +
                         h*[(w[2]-w[1])/12 w[2]/3 (w[1]+w[2])/12] +
                         delta*h*
                         [(-(wx2+wy2)/6+w[1]*w[2]/2) (wx2-2*wy2)/3 (-(wx2+wy2)/6-w[1]*w[2]/2)])
                A[N*(j-1)+1:N*j, N*j+1:N*(j+1)] = spdiagm(-1 => blk[1:end-1,1],
                                                           0 => blk[:,2],
                                                           1 => blk[1:end-1,3])
            end
        end

    else
        nn = N*N
        H = zeros(nn,nn)
        S = zeros(nn,nn)
        U = zeros(nn,nn)

        for j=1:N
            for k=1:N
                row = (j-1)*N+k
                H[row, row] = 8/3
                (k > 1) && (H[row, row-1] = -1/3)
                (k < N) && (H[row, row+1] = -1/3)
                (j > 1) && (H[row, row-N] = -1/3)
                (j < N) && (H[row, row+N] = -1/3)
                ((j > 1) & (k > 1)) && (H[row, row-N-1] = -1/3)
                ((j > 1) & (k < N)) && (H[row, row-N+1] = -1/3)
                ((j < N) & (k > 1)) && (H[row, row+N-1] = -1/3)
                ((j < N) & (k < N)) && (H[row, row+N+1] = -1/3)
            end
        end

        for j=1:N
            for k=1:N
                row = (j-1)*N+k
                S[row, row] = 0
                (k > 1) && (S[row, row-1] = -1/3*w[1])
                (k < N) && (S[row, row+1] =  1/3*w[1])
                (j > 1) && (S[row, row-N] = -1/3*w[2])
                (j < N) && (S[row, row+N] =  1/3*w[2])
                ((j > 1) & (k > 1)) && (S[row, row-N-1] = -(w[1]+w[2])/12)
                ((j > 1) & (k < N)) && (S[row, row-N+1] =  (w[1]-w[2])/12)
                ((j < N) & (k > 1)) && (S[row, row+N-1] =  (w[2]-w[1])/12)
                ((j < N) & (k < N)) && (S[row, row+N+1] =  (w[1]+w[2])/12)
            end
        end

        for j=1:N
            for k=1:N
                row = (j-1)*N+k
                U[row, row] = 4/3*(wx2+wy2)
                (k > 1) && (U[row, row-1] = (wy2-2*wx2)/3)
                (k < N) && (U[row, row+1] = (wy2-2*wx2)/3)
                (j > 1) && (U[row, row-N] = (wx2-2*wy2)/3)
                (j < N) && (U[row, row+N] = (wx2-2*wy2)/3)
                ((j > 1) & (k > 1)) && (U[row, row-N-1] = -(wx2+wy2)/6 - w[1]*w[2]/2)
                ((j > 1) & (k < N)) && (U[row, row-N+1] = -(wx2+wy2)/6 + w[1]*w[2]/2)
                ((j < N) & (k > 1)) && (U[row, row+N-1] = -(wx2+wy2)/6 + w[1]*w[2]/2)
                ((j < N) & (k < N)) && (U[row, row+N+1] = -(wx2+wy2)/6 - w[1]*w[2]/2)
            end
        end

        A = nu*H + h*S + delta*h*U
    end
    A
end # supg

"""
    olmstead(N)

load Olmstead matrix from file (`N ∈ {500,1000}`)

!!! note

    This function requires a Matrix Market file from [http://math.nist.gov/MatrixMarket/data/NEP/olmstead/olmstead.html]
"""
function olmstead(N)
    isdefined(Main, :MatrixMarket) ||
        throw(ErrorException("this function requires the MatrixMarket package"))
    if N==500
        A = Main.MatrixMarket.mmread(joinpath(dirname(@__FILE__),"data","olm500.mtx"))
    elseif N==1000
        A = Main.MatrixMarket.mmread(joinpath(dirname(@__FILE__),"data","olm1000.mtx"))
    else
        throw(ArgumentError("no data available for size $N"))
    end
    A
end
"""
    rdbrusselator(N)

load reaction-diffusion Brusselator matrix from file (`N ∈ {800,3200}`)

!!! note

    This function requires a Matrix Market file from [http://math.nist.gov/MatrixMarket/data/NEP/brussel/brussel.html]
"""
function rdbrusselator(N)
    isdefined(Main, :MatrixMarket) ||
        throw(ErrorException("this function requires the MatrixMarket package"))
    if N==800
        A = Main.MatrixMarket.mmread(joinpath(dirname(@__FILE__),"data","rdb800l.mtx"))
    elseif N==3200
        A = Main.MatrixMarket.mmread(joinpath(dirname(@__FILE__),"data","rdb3200l.mtx"))
    else
        throw(ArgumentError("no data available for size $N"))
    end
    A
end


using RandomLinearAlgebraSolvers
using LinearAlgebra, LLSModels, SparseArrays, Stopping
using Random, Test

using MatrixDepot

Random.seed!(1234)

# algo_list = [:RandomVectorSketch, :randomInv]
algo_list = [:RandomizedKaczmarz, :RandomizedBlockKaczmarz, :RandomizedCD]
algo_pd_list = [:RandomizedCD2, :RandomizedNewton]

@testset "RandomLinearAlgebraSolvers.jl" begin
  for T in [Float16, Float32, Float64], algo in union(algo_list, algo_pd_list)
    A = T[1 0; 0 1]
    b = T[1; 1]
    stp = eval(algo)(RLAStopping(A, b))
    sol = stp.current_state.x
    @test eltype(sol) == T
  end
end

function overdetermined_with_random_matrices(T = Float64, m = 10, n = 5)
  A = rand(T, m, n)
  sol = rand(T, n)
  b = A * sol
  return A, b, sol
end

function overdetermined_with_sparse_random_matrices(T = Float64, m = 10, n = 5)
  A = sprand(T, m, n, 1 / log(n * m))
  sol = rand(T, n)
  b = A * sol
  return A, b, sol
end

function overdetermined_with_random_pd_matrices(T = Float64, n = 10)
  A = rand(T, n, n)
  A = A' * A
  A = (A + A') / 2
  sol = rand(T, n)
  b = A * sol
  return A, b, sol
end

function hilbert(T = Float64, n = 4)
  A = matrixdepot("hilb", n)
  sol = rand(T, n)
  b = A * sol
  return A, b, sol
end

pb_generators =
  [:hilbert, :overdetermined_with_random_matrices, :overdetermined_with_sparse_random_matrices]

pb_pd_generators = [:overdetermined_with_random_pd_matrices]

for meth in algo_list, fpb in pb_generators
  @testset "$meth: Test $(fpb)" begin
    A, b, sol = eval(fpb)()
    stp = RLAStopping(A, b, max_iter = 1000000, max_eval = 100000, rtol = 1e-2, atol = 0.0)
    eval(meth)(stp)
    @test status(stp) == :Optimal
    @show @allocated eval(meth)(stp)
  end
end

for meth in union(algo_list, algo_pd_list), fpb in pb_pd_generators
  @testset "$meth: Test $(fpb)" begin
    A, b, sol = eval(fpb)()
    stp = RLAStopping(A, b, max_iter = 1000000, max_eval = 100000, rtol = 1e-2, atol = 0.0)
    eval(meth)(stp)
    @test status(stp) == :Optimal
    @show @allocated eval(meth)(stp)
  end
end


export AAFT

"""
    AAFT()

An amplitude-adjusted-fourier-transform (AAFT) surrogate[^Theiler1991].

AAFT surrogates have the same linear correlation, or periodogram, and also
preserves the amplitude distribution of the original data.

AAFT surrogates can be used to test the null hypothesis that the data come from a
monotonic nonlinear transformation of a linear Gaussian process
(also called integrated white noise)[^Theiler1991].

[^Theiler1991]: J. Theiler, S. Eubank, A. Longtin, B. Galdrikian, J. Farmer, Testing for nonlinearity in time series: The method of surrogate data, Physica D 58 (1–4) (1992) 77–94.
"""
struct AAFT <: Surrogate end

function surrogenerator(x, method::AAFT, rng = Random.default_rng())
    n = length(x)
    m = mean(x)
    forward = plan_rfft(x)
    inverse = plan_irfft(forward * x, n)
    𝓕 = forward * (x .- m)

    init = (
        x_sorted = sort(x),
        ix = zeros(Int, n),
        inverse = inverse,
        m = m,
        𝓕 = 𝓕,
        r = abs.(𝓕),
        ϕ = angle.(𝓕),
        shuffled𝓕 = similar(𝓕),
        coeffs = zero(𝓕),
        n = n,
    )

    return SurrogateGenerator(method, x, similar(x), init, rng)
end

function (sg::SurrogateGenerator{<:AAFT})()
    s, rng = sg.s, sg.rng

    init_fields = (:x_sorted, :ix, :inverse, :m, :r, :ϕ, :shuffled𝓕, :coeffs, :n)
        x_sorted, ix,  inverse, m, r, ϕ, shuffled𝓕, coeffs, n =
        getfield.(Ref(sg.init), init_fields)

    coeffs .= rand(rng, Uniform(0, 2π), length(shuffled𝓕))
    shuffled𝓕 .= r .* exp.(coeffs .* 1im)
    s .= (inverse * shuffled𝓕) .+ m

    # Rescale back to original values to obtain AAFT surrogate.
    sortperm!(ix, s)
    s[ix] .= x_sorted

    return s
end


using StringParserPEG

calc1 = Grammar("""
  start => (number & op & number) {"start"}

  op => (plus | minus) {"op"}
  number => (-(space) & r([0-9]+)r) {"number"}
  plus => (-(space) & '+') {"plus"}
  minus => (-(space) & '-') {"minus"}
  space => r([ \\t\\n\\r]*)r
""")
toresult(node,children,::MatchRule{:default}) = node.value
toresult(node,children,::MatchRule{:number}) = parse(Int,node.value)
toresult(node,children,::MatchRule{:plus}) = +
toresult(node,children,::MatchRule{:minus}) = -
toresult(node,children,::MatchRule{:start}) = children[2](children[1],children[3])

data = "4+5"

(ast, pos, err) = parse(calc1, data)
transformed = transform(toresult,ast)
transformed


using Test, YaoBlocks, YaoArrayRegister

@testset "test constructor" for T in [Float16, Float32, Float64]
    # NOTE: type should follow the axis
    @test RotationGate(X, 0.1) isa PrimitiveBlock{1}
    @test_throws TypeError RotationGate{1, Complex{T}, XGate} # will not accept non-real type

    @test Rx(T(0.1)) isa RotationGate{1, T, XGate}
    @test Ry(T(0.1)) isa RotationGate{1, T, YGate}
    @test Rz(T(0.1)) isa RotationGate{1, T, ZGate}
end

@testset "test matrix" begin
    theta = 2.0
    for (DIRECTION, MAT) in [
        (X, [cos(theta/2) -im*sin(theta/2); -im*sin(theta/2) cos(theta/2)]),
        (Y, [cos(theta/2) -sin(theta/2); sin(theta/2) cos(theta/2)]),
        (Z, [exp(-im*theta/2) 0;0 exp(im*theta/2)]),
        (CNOT, exp(-mat(CNOT)/2*theta*im |> Matrix)),
        (control(2, (1,), 2=>X), exp(-mat(CNOT)/2*theta*im |> Matrix))]

        @test mat(RotationGate(DIRECTION, theta)) ≈ MAT
    end
end

@testset "test apply" begin
    r = rand_state(1)
    @test state(apply!(copy(r), Rx(0.1))) ≈ mat(Rx(0.1)) * state(r)
end

@testset "test dispatch" begin
    @test dispatch!(Rx(0.1), 0.3) == Rx(0.3)
    @test nparameters(Rx(0.1)) == 1

    @testset "test $op" for op in [+, -, *, /]
        @test dispatch!(op, Rx(0.1), π) == Rx(op(0.1, π))
    end

    @test_throws AssertionError dispatch!(Rx(0.1), (0.2, 0.3))
end

@testset "adjoints" begin
    @test Rx(0.1)' == Rx(-0.1)
    @test Rx(0.2)' == Rx(-0.2)
    @test copy(Rx(0.1)) == Rx(0.1)

    g = Rx(0.1) # creates a new one
    @test copy(g) !== g
end


using LinearAlgebra: isapprox
using LazyLieManifolds
using Test
using StaticArrays
using LinearAlgebra

import LazyLieManifolds: ⊕

using Manifolds

@testset "LazyLieManifolds.jl" begin

    _p0 = 1.0I(3)
    _X1 = [0.0 -pi/2 1.0;  pi/2 0.0 0.0; 0.0 0.0 1.0]
    _X2 = [0.0  pi/2 1.0; -pi/2 0.0 0.0; 0.0 0.0 1.0]

    p0 = SE([1.0 0.0; 0 1.0], [0.0, 0.0])

    M = LLM.manifold(p0)
    @test all(isapprox.(_p0, affine_matrix(M, p0[])))

    X1 = se{2}([1., 0, pi/2])
    X2 = se{2}([1., 0, -pi/2])

    @test all(isapprox.(_X1, affine_matrix(M, X1[])))
    @test all(isapprox.(_X2, affine_matrix(M, X2[])))

    p1 = p0 ⊕ X1

    p2 = p1 ⊕ X2

    p3 = p1 ∘ p2

    @time LLM.PosePose(p1, p0, p2)
    @time LLM.PosePose_x(p1, p0, p2)

    SE{3}()

    p0 = LLM._identity(SE{3})
    X1 = se{3}([1., 0, 0, pi/2, 0, 0])
    X2 = se{3}([1., 0, 0, -pi/2, 0, 0])
    p1 = p0 ⊕ X1
    p2 = p1 ⊕ X2

    # @code_warntype exp(X1)
    log(p2)
    log(p1, p2)

    ##

    p0 = SO([1.0 0.0; 0 1.0])
    X1 = so{2}([pi/2])
    X2 = se{2}([1., 0, -pi/2])

    # p1 = p0 ⊕ X1
    # p2 = p1 ⊕ X2
    # p3 = p1 ∘ p2

    SO{5}()

    p0 = LLM._identity(SO{3})
    X1 = so{3}([pi/2, 0, 0])
    X2 = so{3}([-pi/2, 0, 0])

    log(p2)
    log(p1, p2)

end

##

# Compare SArray to MArray
# tested on small function as
# M: (5 allocations: 176 bytes)
# S: (3 allocations: 112 bytes)
##################
p0 = SE{2}(ProductRepr((@MVector([0.0, 0.0]), @MMatrix([1.0 0.0; 0.0 1.0]))))

X1 = se{2}(hat(M, p0[], @MVector([1., 0.0, pi/2])))
X1 = se{2}(hat(M, p0[], @MVector([1., 0.0, -pi/2])))

p1 = p0 ⊕ X1
p2 = p1 ⊕ X2
p3 = p1 ∘ p2

@time LLM.PosePose(p1, p0, p2)
@time LLM.PosePose_x(p1, p0, p2)
@time LLM.PosePose_d(p1, p0, p2)

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

p0 = SE{2}(ProductRepr((@SVector([0.0, 0.0]), @SMatrix([1.0 0.0; 0.0 1.0]))))

X1 = se{2}(hat(M, p0[], @SVector([1., 0.0, pi/2])))
X1 = se{2}(hat(M, p0[], @SVector([1., 0.0, -pi/2])))

p1 = p0 ⊕ X1
p2 = p1 ⊕ X2
p3 = p1 ∘ p2

@time LLM.PosePose(p1, p0, p2)
@time LLM.PosePose_x(p1, p0, p2)
@time LLM.PosePose_d(p1, p0, p2)

ProductRepr((@SVector([0.0, 0.0]), @SMatrix([1.0 0.0; 0.0 1.0])))
ProductRepr((@MVector([0.0, 0.0]), @MMatrix([1.0 0.0; 0.0 1.0])))


module Diffusion
export aug, bb, brown, brown1, cumsum0, dW, dW1, dWcond, euler, ito, quvar, ydx, bracket, (..)

# lines starting with "#%" contain the documentation in ReST
# the leading "#%" and up to 2 spaces are removed.
# careful, depending on your tab length, 8 spaces and one tab may look identically aligned, but may end up not so
# in the corresponding style file

#%  .. currentmodule:: Diffusion
#%
##%  .. |Ito| unicode:: It U+014D
#%  .. |Ito| unicode:: Ito
#%

#%  Module Diffusion
#%  ----------------
#%
#%  Introduction
#%  ~~~~~~~~~~~~
#%
#%  The functions in this module operate on three conceptual different objects, (although they
#%  are currently just represented as vectors and arrays.)
#%
#%  Stochastic processes, denoted ``x``, ``y``, ``w`` are arrays of values which
#%  are sampled at distance ``dt``, ``ds``, where ``dt``, ``ds`` are either scalar or
#%  Vectors ``length(dt)=size(W)[end]``.
#%  Stochastic differentials are denoted ``dx``, ``dw`` etc., and are first differences of
#%  stochastic processes. Finally, t can denote the total time or correspond to a vector of
#%  ``size(W)[end]`` sampling time poins.
#%
#%  Note the following convention: In analogy with the definition of the |Ito| integral,
#%
#%  	intxdw[i] = x[i]](w[i+1]-w[i]) (== x[i]dw[i])
#%
#%  and
#%
#%  	length(w) = length(dw) + 1
#%

#%  Reference
#%  ~~~~~~~~~
#%
#%  .. function:: brown1(u, t, n::Integer)
#%
#%  	Compute ``n`` equally spaced samples of 1d Brownian motion in
#%  	the interval ``[0,t]``, starting from point ``u``
#%

function brown1(u, t, n::Integer)
	x = randn(n)*sqrt(t/(n-1)) ; x[1] = u #(n-1) gaussians
	x = cumsum(x)
end

#%  .. function:: brown(u, t, d::Integer, n::Integer)
#%
#%  	Simulate ``n`` equally spaced samples of ``d``-dimensional Brownian motion in
#%  	the interval ``[0,t]``, starting from point ``u``
#%

function brown(u, t, d::Integer, n::Integer)
	x = randn(d, n)*sqrt(t/(n-1)) ; x[:,1] = u #(n-1) gaussians
	x = cumsum(x,2)
end

#%  .. function:: dW1(t, n::Integer)
#%                dW(t, d::Integer, n::Integer)
#%
#%  	Simulate a ``1``-dimensional (``d``-dimensional)
#%  	Wiener differential with ``n`` values in the
#%  	the interval ``[0,t]``, starting from point ``u``
#%

dW1(t, n::Integer) = randn(n)*sqrt(t/n)
dW(t, d::Integer, n::Integer) = randn(d, n)*sqrt(t/n)

#%  .. function:: dW(dt::Vector, d::Integer)
#%
#%  	Simulate a ``d``-dimensional Wiener differential sampled at
#%  	time points with distances given by the vector ``dt``
#%
mulinto(x,y) = (x[:] = [x.*y])
function dW(dt::Vector, d::Integer)
	#% scale each row of a randn by sqrt(dt)
	dw = mapslices(x -> mulinto(x, sqrt(dt)), randn(d, length(dt)), 2:2)
	return dw
end

### depreciated
dT(t, n::Integer) = ones(n)*(t/n)
function dT0(t, n)
	dt = ones(n)*(t/(n-1))
	dt[1] = 0
	return dt
end

function dW1(dt)
	dw = randn(length(dt)).* sqrt(dt)
	return dw
end

#%  .. function:: ito(y, dx)
#%                ito(dx)
#%                cumsum0(dx)
#%
#%  	Integrate a valued stochastic process with respect to a stochastic differential.
#%  	R, R^2 (d rows, n columns), R^3.
#%
#%	``ito(dx)`` is a shortcut for ``ito(ones(size(dx)[end], dx)``.
#%	So ``ito(dx)`` is just a ``cumsum0`` function which is a inverse to ``dx = diff([0, x1, x2, x3,...])``.
#%
function cumsum0(dx::Vector)
 	n = length(dx) + 1
	x = similar(dx, n)
	x[1] = 0.0
	for i in 2:n
		x[i] = x[i-1] + dx[i-1]
	end
	x
end

ito(dx::Vector) = cumsum0(dx)

function ito(dx)
	s = [size(dx)...]
	d = ndims(dx)
	n = s[end] += 1
	x = similar(dx, s...)
	if(d==2)
		x[:,1] = 0.0
		for i in 2:n;	x[:,i] = x[:,i-1] + dx[:,i-1]; end
	elseif (d==3)
		x[:,:,1] = 0.0
		for i in 2:n;	x[:,:,i] = x[:,:,i-1] + dx[:,:,i-1]; end
	else
		error("ndims(dx) > 3 not implemented")
	end
	x
end

function ito(x::Vector, dw::Vector)
	n = length(dw) + 1
	y = similar(dw, n)
	y[1] = 0
	for i in 2:n
		y[i] = y[i-1] + x[i-1]*dw[i-1]
	end
	y
end

#scalar version

function ito0(x, dy)
	n = length(dy) + 1
	y = 0.0
	for i in 2:n
		y = y + x[i-1]*dy[i-1]
	end
	y
end

#TODO  ito(x::Array, dw::Array)

#%  .. function:: ..(y, dx)
#%                ydx(y, dx)
#%
#%  	``y .. dx`` returns the stochastic differential ``ydx`` defined by the property
#%
#% 		ito(ydx) == ito(y, dx)
#%

function ydx(y::Vector, dx::Vector)
	n = length(dx) +1
	dy = zeros(n)
	for i in 2:n
		dy[i-1] = y[i-1]*dx[i-1]
	end
	return dy
end

function ydx(y::Matrix, dx::Matrix)
	n = size(dx)[end]
	dim = [size(y[:,1]'*dx[:,1])..., n]
	dy = zeros(dim...)
	for i in 2:n+1
		dy[:, i-1] = y[:,i-1]'*dx[:, i-1]
	end
	return dy
end

function (..)(y, dx)
	if (size(y) == ())
		return y*dx
	elseif (size(y)[end] == size(dx)[end])
		return ydx(y,dx)
	else
		return y * dx
	end
end

#%  .. function:: bb(u, v, t, n)
#%
#%  	Simulates ``n`` equidistant samples of a Brownian bridge from point ``u`` to ``v`` in time ``t``
#%

function bb(u, v, t, n)
	x = cumsum0(randn(n-1)*sqrt(t/(n-1)))
	u + x + linspace(0, v-u-x[end], n)
end

#%  .. function:: dWcond1(v,t,n)
#%
#%    	Simulates ``n`` equidistant samples of a "bridge noise": that is a Wiener differential ``dW``
#%    	conditioned on ``W(t) = v``
#%
#%

function dWcond(v,t,n)
	x = randn(n)*sqrt(t/n)
	x += -sum(x)/n + v/n
end
# sum(dWcond(2, 5, 10)) approx 2

#%  .. function:: aug(dw, dt, n)
#%                aug(dt, n)
#%
#%  	Take Wiener differential sampled at ``dt`` and return Wiener differential subsampled ``n`` times
#%  	between each observation with new length ``length(dw)*n``.
#%  	``aug(dt,n)`` computes the corresponding subsample of times.
#%
function aug(dw, dt::Vector, n)
	x = zeros(length(dw)*n)
	for i in 1:length(dw)
		x[(1:n)+(i-1)*n] = dWcond(dw[i], dt[i], n)
	end
	x
end
function aug(dw, dt, n)
	x = zeros(length(dw)*n)
	for i in 1:length(dw)
		x[(1:n)+(i-1)*n] = dWcond(dw[i], dt, n)
	end
	x
end

function aug(dt, n)
	x = zeros(length(dt)*n)
	for i in 1:length(dt)
		x[(1:n)+(i-1)*n] = ones(n)*dt[i]/n
	end
	x
end

#todo: aug(dw, dt, ds)

#%  .. function:: quvar(x)
#%
#%  	Computes quadratic variation of ``x``.
#%

function quvar(x::Vector)
	sum(diff(x).^2)
end

#%  .. function:: bracket(x)
#%                bracket(x,y)
#%
#%  	Computes quadratic variation process of ``x`` (of ``x`` and ``y``).
#%

function bracket(x)
	cumsum0(diff(x).^2)
end

function bracket(x,y)
	cumsum0(diff(x).*diff(y))
end

#%  .. function:: euler(t0, u, b, sigma, dt, dw)
#%                euler(t0, u, b, sigma, dt)
#%
#%  	Simulates a 1-dimensional diffusion process using the Euler-Maruyama approximation
#%  	with drift ``b(t,x)`` and diffusion coefficient ``sigma(t,x)``
#%  	starting in ``(t0, u)`` using ``dt`` and given Wiener differential ``dw``.
#%

function euler(t0, u, b, sigma, dt::Vector, dw::Vector)
	X = zeros(length(dw)+1)

	X[1] = u
	t = t0
	for i in 1:length(dw)
		t += dt[i]
		X[i+1] = X[i] + b(t,X[i])*dt[i] + sigma(t,dw[i])*dw[i]
	end
	X
end
function euler(t0, u, b, sigma, dt::Float64, dw::Vector)
	X = zeros(length(dw)+1)
	X[1] = u
	t = t0

	for i in 1:length(dw)
		t += dt
		X[i+1] = X[i] + b(t,X[i])*dt  + sigma(t,dw[i])*dw[i]
	end
	X
end
euler(t0, u, b, sigma, dt::Vector) = euler(t0, u, b, sigma, dt, dW1(dt))

end


#=

  Euler #14 in Julia.

  Problem 14
  """
  The following iterative sequence is defined for the set of positive integers:

  n n/2 (n is even)
  n 3n + 1 (n is odd)

  Using the rule above and starting with 13, we generate the following
  sequence:
  13 40 20 10 5 16 8 4 2 1

  It can be seen that this sequence (starting at 13 and finishing at 1)
  contains
  10 terms. Although it has not been proved yet (Collatz Problem), it is
  thought that all starting numbers finish at 1.

  Which starting number, under one million, produces the longest chain?

  NOTE: Once the chain starts the terms are allowed to go above one million.)
  """

  This Julia program was created by Hakan Kjellerstrand, hakank@gmail.com
  See also my Julia page: http://www.hakank.org/julia/

=#

using Memoization
include("Euler.jl")

# It's much slower with memoizing!
@memoize function hailstone_memo(n)
    if n % 2 == 0
        return round(Int,n / 2)
    else
        return 3*n+1;
    end
end

function hailstone(n)
    if n % 2 == 0
        return round(Int,n / 2)
    else
        return 3*n+1;
    end
end

#
# Using 1..1_000_000: 0.68331034s
# Using 3..2..1_000_000: 0.36079831s
function euler14a()
    maxN = 0
    maxLen = 0
    for n in 1:1_000_000
    # for n in 3:2:1_000_000
        m = n
        alen = 0
        while m > 1
            m = hailstone(m)
            alen += 1
        end
        alen += 1
        if alen > maxLen
            maxN = n
            maxLen = alen
        end
    end
    return maxN
end

# Cache the lengths.
# 1..999_999: 1.66648847s
# 3..2..999_999: 1.09878097s
function euler14b()
    hash = Dict()
    maxN = 0
    maxLen = 0
    for n in 2:1_000_000
    # for n in 3:2:1_000_000
        m = n
        mlen = 1
        while m > 1
            if haskey(hash,m)
                mlen = hash[m]+mlen-1
                m = 1
            else
                m = hailstone(m)
                mlen += 1
            end
        end
        if !haskey(hash,n)
            hash[n] = mlen
        end
        if mlen > maxLen
            maxN = n
            maxLen = mlen
        end
    end
    return maxN;
end

# Using an array instead:
# 0.04143852s
function euler14c()
    limit = 1_000_000
    hash = zeros(limit)
    maxN = 0
    maxLen = 0
    for n = 2:1_000_000
        m = n
        mlen = 1
        while m > 1
            if m <= limit && hash[m] > 0
                mlen = hash[m]+mlen-1
                m = 1
            else
                m = hailstone(m)
                mlen += 1
            end
        end
        if hash[n] == 0
            hash[n] = mlen
        end
        if mlen > maxLen
            maxN = n
            maxLen = mlen
        end
    end
    return maxN;
end

# run_euler(euler14a)
# run_euler(euler14b)
run_euler(euler14c)


@testset "Maximum size rectangle binary sub-matrix with all 1s" begin

    matrix = [
        0  1  1  0  1
        1  1  0  1  0
        0  1  1  1  0
        1  1  1  1  0
        1  1  1  1  1
        0  0  0  0  0
    ]

    @test maximum_rectagular_submatrix(matrix) == (9, [(2, 4)], [(3, 5)])
    @test maximum_rectagular_submatrix(BitArray(matrix)) == (9, [(2, 4)], [(3, 5)])

    matrix = [
        0  1  1  0
        1  1  1  1
        1  1  1  1
        1  1  0  0
    ]

    @test maximum_rectagular_submatrix(matrix) == (8, [(1, 4)], [(2, 3)])
    @test maximum_rectagular_submatrix(BitArray(matrix)) == (8, [(1, 4)], [(2, 3)])
end


t₀ = 0.
q₀ = [1.]
p₀ = [1.]
x₀ = [1., 1.]
λ₀ = [0.]

function f_ode(x, fx)
    fx[1] = x[1]
end

ode  = ODE{Float64, typeof(f_ode)}(1, 1, f_ode, t₀, q₀)
ode1 = ODE(f_ode, t₀, q₀)
ode2 = ODE(f_ode, q₀)

@test ode == ode1
@test ode == ode2

function f_pode(q, p, fp)
    fq[1] = q[1]
end

function g_pode(q, p, fp)
    fp[1] = 2p[1]
end

pode  = PODE{Float64, typeof(f_pode), typeof(g_pode)}(1, 1, f_pode, g_pode, t₀, q₀, p₀)
pode1 = PODE(f_pode, g_pode, t₀, q₀, p₀)
pode2 = PODE(f_pode, g_pode, q₀, p₀)

@test pode == pode1
@test pode == pode2

sode  = SODE{Float64}(1, 1, q₀, p₀, t₀, do_nothing, do_nothing, f_pode, g_pode)
sode1 = SODE(q₀, p₀, t₀; v=f_pode, f=g_pode)
sode2 = SODE(q₀, p₀; v=f_pode, f=g_pode)

@test sode == sode1
@test sode == sode2
# @test pode1 == pode2

function f_dae(x, fx)
    fx[1] = x[1]
    fx[2] = x[2]
end

function u_dae(x, λ, fu)
    fu[1] = +λ[1]
    fu[1] = -λ[1]
end

function ϕ_dae(x, λ, fϕ)
    fϕ[1] = x[2] - x[1]
end

dae  = DAE{Float64}(2, 1, f_dae, u_dae, ϕ_dae, t₀, x₀, λ₀)
dae1 = DAE(f_dae, u_dae, ϕ_dae, t₀, x₀, λ₀)
dae2 = DAE(f_dae, u_dae, ϕ_dae, x₀, λ₀)

@test dae == dae1
@test dae == dae2

function f_pdae(q, p, fp)
    fq[1] = q[1]
end

function g_pdae(q, p, fp)
    fp[1] = p[1]
end

function u_pdae(q, p, λ, fu)
    fu[1] = +λ[1]
end

function v_pdae(q, p, λ, fv)
    fv[1] = -λ[1]
end

function ϕ_pdae(q, p, λ, fϕ)
    fϕ[1] = p[1] - q[1]
end

pdae  = PDAE{Float64}(1, 1, f_pdae, g_pdae, u_pdae, v_pdae, ϕ_pdae, t₀, q₀, p₀, λ₀)
pdae1 = PDAE(f_pdae, g_pdae, u_pdae, v_pdae, ϕ_pdae, t₀, q₀, p₀, λ₀)
pdae2 = PDAE(f_pdae, g_pdae, u_pdae, v_pdae, ϕ_pdae, q₀, p₀, λ₀)

@test pdae == pdae1
@test pdae == pdae2


#=
ndgrid.jl
lazy and non-lazy versions of ndgrid
=#

using LazyArrays: Applied, ApplyArray

export ndgrid_array_r, ndgrid_lazy

"""
    grid = ndgrid_repeat(v::AbstractVector, dims::Dims{D}, d::Int)
Do the type of repeat needed for `ndgrid`, e.g., `repeat(x, 1, n)`,
for arbitrary dimensions, where `1 ≤ d ≤ D` and `dims[d] == length(v)`.
Output size is `dims`.
"""
function ndgrid_repeat(v::AbstractVector, dims::Dims{D}, d::Int) where D
    @boundscheck checkbounds([dims...], d)
    @boundscheck length(v) == dims[d] ||
        throw(DimensionMismatch("$d $(dims[d]) $(length(v))"))
    t1 = ntuple(i -> i == d ? length(v) : 1, Val(D)) # (1,…,1,n,1,…,1)
    t2 = ntuple(i -> i == d ? 1 : dims[i], Val(D)) # (?,…,?,1,?,…,?)
    repeat(reshape(v, t1), t2...)
end

"""
    ndgrid_array_r(args::AbstractVector...)
Returns tuple of `length(args)` arrays, each of size `tuple(length.(args)...)`.
This method is provided for convenience and testing,
but `ndgrid` is less efficient than broadcast so should be avoided.
The tuple returned here requires `prod(length.(args)) * length(args)` memory;
using `ndgrid_lazy` is an alternative that uses `O(length(args))` memory.
"""
function ndgrid_array_r(args::AbstractVector...)
    D = length(args)
    T = Tuple{ntuple(i -> Array{eltype(args[i]), D}, D)...} # return type
    fun = i -> ndgrid_repeat(args[i], length.(args), i)
    ntuple(fun, D)::T
end

# lazy array method for a single element of the ndgrid tuple:

const _Repeat = Applied{A, typeof(ndgrid_repeat), Tuple{V,Dim,Int}} where
    {A <: Any, V <: AbstractVector, Dim <: Dims{D} where D}
Base.ndims(r::_Repeat) = length(r.args[2])
Base.size(r::_Repeat) = r.args[2]
Base.eltype(r::_Repeat) = eltype(r.args[1])

const Repeat{T,N} = ApplyArray{T, N, typeof(ndgrid_repeat)}
# Base.IndexStyle(Repeat) = IndexCartesian() # default

@inline Base.@propagate_inbounds function Base.getindex(
    r::Repeat{T,N},
    i::Vararg{Int,N},
) where {T,N}
    @boundscheck checkbounds(r, i...)
    return @inbounds r.args[1][i[r.args[3]]]
end

"""
    ndgrid_lazy(args::AbstractVector...)
Returns tuple of lazy array `Repeat` items,
avoiding the memory issues of `ndgrid`.

# Examples
```jldoctest
julia> ndgrid_lazy(1:3, 1:2)
([1 1; 2 2; 3 3], [1 2; 1 2; 1 2])

julia> ndgrid_lazy(1:3, 1:2)[1]
ndgrid_repeat(3-element UnitRange{Int64}, Tuple{Int64, Int64}, Int64):
 1  1
 2  2
 3  3
```
"""
function ndgrid_lazy(args::AbstractVector...)
    fun = i -> ApplyArray(ndgrid_repeat, args[i], length.(args), i)
    return ntuple(fun, Val(length(args)))
end


module TestAsynchronous

#println("\nAsynchronousExamples: Demonstrating the ability to simulate models with asynchronous semantics")

using Modia
@usingModiaPlot

BooleanPulse1 = Model(
	startTime = parameter,
	width = parameter,
	equations = :[
        y = after(startTime) && ! after(startTime + width)
	]
)

TestBooleanPulse1 = Model(
	pulse = BooleanPulse1 | Map(startTime=2u"s", width=2u"s"),
)

model = @instantiateModel(TestBooleanPulse1, log=true, logCode=true)
simulate!(model, Tsit5(), stopTime=10, log=true, logEvents=true)
plot(model, [("pulse.y")], heading="TestBooleanPulse1", figure=1)

BooleanPulse = Model(
	width = parameter | Map(value=50, min=0, max=100) | info"Width of pulse in % of period",
	period = parameter | Map(min=0) | info"Time for one period",
	startTime = parameter | info"Time instant of first pulse",
	pulseStart = Var(start=:(startTime)) | info"Start time of pulse",
	y = output,
	equations = :[
		Twidth = period*width/100
		clock1 = Clock(startTime, period)
        pulseStart = sample(if initial(); startTime else time end, clock1)
        y = after(pulseStart) && ! after(pulseStart + Twidth)
	]
)

TestBooleanPulse = Model(
	pulse = BooleanPulse | Map(startTime=2u"s", period=2u"s"),
)

model = @instantiateModel(TestBooleanPulse, log=true, logCode=true)
simulate!(model, Tsit5(), stopTime=10, log=true, logEvents=true)
plot(model, [("pulse.y"), ("pulse.pulseStart")], heading="TestBooleanPulse", figure=2)

# References:
# http://osp.mans.edu.eg/cs212/Seq_circuits_Summary_FF-types.htm
# https://www.kth.se/social/files/56124acff27654028dfc0f80/F12asyFSM1_eng.pdf

SRFlipFlop = Model(
    Q = Var(init=false),
    equations = :[
		Q = S || ! R && pre(Q)
    ]
)

TestSRFlipFlop = Model(
	set = BooleanPulse | Map(startTime=2u"s", period=4u"s"),
	reset = BooleanPulse | Map(startTime=5u"s", period=4u"s"),
	sr = SRFlipFlop,
	equations = :[
		connect(set.y, sr.S)
		connect(reset.y, sr.R)
	]
)

model = @instantiateModel(TestSRFlipFlop, log=true, logCode=true)
simulate!(model, Tsit5(), stopTime=15, log=true, logEvents=true)
plot(model, [("sr.S"), ("sr.R"), ("sr.Q")], heading="TestSRFlipFlop", figure=3)

end


using Binning
using Flux
using Plots

# binning function stuff -------------------------------

x = range(-10, stop=10, length=200)
plot(x, SmoothStep.(x, 0, 4), label="binning function")

# demonstrate the action of the bin function
y = sin.(x)
b_out = [sin(xᵢ)*SmoothStep(xᵢ, 0, 4) for xᵢ ∈ x]

plot!(x, y, color=:red, label="signal")
plot!(x, b_out, color=:green, label="binned signal")

savefig("../figures/smoothstep.pdf")
savefig("../figures/smoothstep.svg")

#---- test binning layer action --------------------------
x = Array(0:.1:10)
f = sin.(x)
testBinner = DomainBinner(x, 20)
binit = testBinner.b

fnew = testBinner(f)
xnew = getBinCenters(testBinner)

p = plot()
b = sort(vcat(testBinner.b₁,testBinner.b, testBinner.bₑ))
println(size(fnew), size(b))

for i ∈ 2:length(b)
    println(i)
    plot!(p, b[i-1:i], [fnew[i-1], fnew[i-1]], color=:black, fill=(0, 0.5, :black), label="")
end
xlabel!("bin(s)")
ylabel!("count")

savefig("../figures/binPlot.pdf")

#---- test training via integration problem ----------

# set up loss function
function loss()
    bins = sort(vcat(testBinner.b₁,testBinner.b, testBinner.bₑ))
    fnew = testBinner(f)
    Δx = [bins[i]-bins[i-1] for i ∈ 2:length(bins)]
    Î = sum([fnew[i]*Δx[i] for i ∈ 1:length(fnew)])
    I = sum(0.1.*f)
    return (Î-I)^2 # square to make sure loss > 0
end

function binSpreadLoss()
    bins = sort(vcat(testBinner.b₁,testBinner.b, testBinner.bₑ))
    bmin = bins[1]
    bmax = bins[end]
    E = abs(bmax-bmin)-(testBinner.bₑ-testBinner.b₁)
end

λ=1
function composedLoss()
    return loss() + λ*binSpreadLoss()
end

println(loss())
println(binSpreadLoss())
println(composedLoss())

opt = Descent(0.0005)
data = Iterators.repeated((), 200)
ps = params(testBinner)
println(ps)

# train the model!
Flux.train!(composedLoss, ps, data, opt, cb=()->println(composedLoss()))

println(composedLoss())

#p1 = plot()
b = sort(vcat(testBinner.b₁,testBinner.b, testBinner.bₑ))

for i ∈ 2:length(b)
    plot!(p, b[i-1:i], [fnew[i-1], fnew[i-1]], color=:blue, fill=(0, 0.5, :blue), label="")
end
xlabel!("bin(s)")
ylabel!("count")
savefig("../figures/trained_bin_comparison.pdf")
savefig("../figures/trained_bin_comparison.png")


using NonlinearEigenproblems.RKHelper
using Plots

function nleigs_residual_plot(title, solution_info, Σ; plot_attributes...)
    Lam = solution_info.Lam
    Res = solution_info.Res

    n = size(Lam, 1)
    LamC = zero(Lam)
    ResC = zero(Res)

    # first iteration
    LamC[1,1] = Lam[1,1]
    ResC[1,1] = Res[1,1]

    # next iterations
    for i = 2:n
        sLam = Lam[1:i,i]
        sRes = Res[1:i,i]
        for j = 1:i-1
            _,ii = findmin(abs.(sLam .- LamC[j,i-1]))
            LamC[j,i] = sLam[ii]
            ResC[j,i] = sRes[ii]
            deleteat!(sLam, ii)
            deleteat!(sRes, ii)
        end
        LamC[i,i] = sLam[1]
        ResC[i,i] = sRes[1]
    end

    z = map(p -> inpolygon(real(p), imag(p), real(Σ), imag(Σ)), LamC[:,end])

    ResIn = ResC[z,:]
    ResOut = ResC[.!z,:]
    LamIn = LamC[z,:]
    LamOut = LamC[.!z,:]

    p = plot(title=title, xlabel="iteration", ylabel="residual", leg=false; plot_attributes...)

    for i = 1:n
        color, style = isempty(Σ) || z[i] ? (:red, :solid) : (:black, :dot)
        p = plot!(p, i:n, ResC[i,i:n], yscale=:log10, linecolor=color, linestyle=style)
    end

    display(p)
end