using DataStructures

mutable struct Node
    id::Int
    neighbours::Array{Node}
    color::Union{String, Nothing}
    distance::Union{Int, Nothing}
    predecessor::Union{Node, Nothing}
end
Node(id) = Node(id, [], nothing, nothing, nothing)

function makenodelist(adjlist::Array)::Array{Node, 1}
	ret::Array{Node, 1} = Array{Node, 1}(undef, length(adjlist))

	for i in 1 : length(adjlist)
		ret[i] = Node(i)
	end

	for i in 1 : length(adjlist)
		for j in adjlist[i]
			push!(ret[i].neighbours, ret[j])
		end
	end

	return ret
end

function isgoalnode(node::Node)::Bool
	return node.id == 8
end

function bfs!(nodes::Array{Node, 1}, start::Node)::Union{Node, Nothing}
	que::Queue{Node} = Queue{Node}()

	for i in nodes
		i.color = "white"
	end

	enqueue!(que, start)
	start.color = "gray"
	start.distance = 0

	while length(que) > 0
		cur_node = dequeue!(que)
		if cur_node.predecessor != nothing
			cur_node.distance = cur_node.predecessor.distance + 1
		end

		if isgoalnode(cur_node)
			return cur_node
		end

		for i in cur_node.neighbours
			if i.color == "white"
				enqueue!(que, i)
				i.predecessor = cur_node
				i.color = "gray"
			end
		end

		cur_node.color = "black"
	end

	return nothing
end

function makepathto(goalnode::Node)::Array{Int, 1}
	ret::Array{Int, 1} = [goalnode.id]

	while goalnode.predecessor != nothing
		goalnode = goalnode.predecessor
		push!(ret, goalnode.id)
	end
	reverse!(ret)
	return ret
end


#struct for saving the state of the system in some half
struct HydroStatus
	rho::Float64 #density
	u::Float64 #velocity
	p::Float64 #pressure
	gamma::Float64 #adiabatic index
	c::Float64 #speed of cound
	A::Float64 #Toro constant (4.8)
	B::Float64 #Toro constant (4.8)

	function HydroStatus(rho, u, p, gamma)
		c = sqrt(gamma * p / rho)
		A = 2. / (gamma + 1.) / rho
		B = (gamma - 1.) / (gamma + 1.) * p
		new(rho, u, p, gamma, c, A, B)
	end
end

#like the HydroStatus but saves only the important parameters - density, pressure and velocity
struct HydroStatusSimple
	rho::Float64 #density
	u::Float64 #velocity
	p::Float64 #pressure
end
HydroStatusSimple(status::HydroStatus) = HydroStatusSimple(status.rho, status.u, status.p)

#different types to dispatch the first guess of p_star calculation
#see "PressureGuess.jl"
abstract type FirstGuessScheme end
struct TwoRarefaction <: FirstGuessScheme end
struct PrimitiveValue <: FirstGuessScheme end
struct TwoShock <: FirstGuessScheme end
struct MeanPressure <: FirstGuessScheme end

#the type of the wave that will be created on some side of the contact discontinuity
abstract type WaveType end
struct Rarefaction <: WaveType end
struct Shock <: WaveType end
struct Vaccum <: WaveType end #for vaccum generated in the middle

#the side in which the calculation is happening
abstract type Side end
struct LeftSide <: Side end
struct RightSide <: Side end

#in some equations there is factor difference between the left and right side calculations
side_factor(::LeftSide) = -1
side_factor(::RightSide) = 1

#choose the some variable based on the side given
choose_side(left, right, ::LeftSide) = left
choose_side(left, right, ::RightSide) = right


# # Gaussian profile

struct GaussLaserConstantCache{IC,W,K,T,Z,E}
    inv_c::IC
    ω::W
    k::K
    T₀::T
    z_R::Z
    E₀::E
end

function GaussLaserConstantCache(;c, λ, w₀, a₀, mₑ, q)
    ω = 2π * c / λ
    k = 2π / λ
    T = 2π / ω
    z_R = w₀^2 * k / 2
    E₀ = a₀ * mₑ * c * ω / abs(q)

    return GaussLaserConstantCache(inv(c), ω, k, T, z_R, E₀)
end

@auto_hash_equals mutable struct GaussLaserCache{X,CE}
    x::X
    y::X
    wz::X
    Ex::CE
    Ey::CE
    Ez::CE
end

function GaussLaserCache(λ, E)
    ThreadLocal(GaussLaserCache(
        zero(λ),        # x
        zero(λ),        # y
        zero(λ),        # wz
        zero(E*im),     # Ex
        zero(E*im),     # Ey
        zero(E*im)      # Ez
    ))
end

function Base.fill!(cache::GaussLaserCache, x::AbstractVector)
    cache.x = x[1]
    cache.y = x[2]

    return nothing
end

@doc """
    struct GaussLaser <: AbstractLaser

The `GaussLaser` is defined by the `units` of the laser
(a positional argument that can be `:SI`, `:atomic` or `:SI_unitful` and `:atomic_unitful`)
and the following parameters
- `λ` is the laser wavelangth
- `a₀` is the normalized vector potential (defined as ``a_0=\\frac{eA}{m_e c^2}``)
- `ϕ₀` is the initial phase with the default value 0.0
- `w₀` is the beam radius at the Rayleigh range or [beam waist](https://en.wikipedia.org/wiki/Gaussian_beam#Beam_waist)
- `ξx` and `ξy` give the polarization and have the default value `1.0 + 0im` and `0.0 + 0im`
- `orientation` specifies how the laser is oriented with respect to the
default coordinate system (default `(:x, :z)`). See [`LaserGeometry`](@ref) for more details.
- `propagation_dir` is the propagation direction of the wave in the
intrinsic coordinate system (default `:z`). See [`LaserGeometry`](@ref) for more details.
- `profile` is the temporal profile of the pulse and the default one is constant (infinite pules duration)

During the initialization of the `GaussLaser` type, some useful derived values are
also computed
- `ω` is the angular frequency and is given by ``2π c / λ``
- `k` is the wavenumber and is given by ``2π / λ``
- `z_R` is the [Rayleigh range](https://en.wikipedia.org/wiki/Rayleigh_length) and is given by ``k w_0^2 / 2``
- `T₀` is the laser period and is given by ``2π / ω``
- `E₀` is the amplitude of the electric field and is given by ``a_0\\frac{m_q c \\omega}{q}``
"""
GaussLaser

struct GaussLaser{PhysicsConst,
                  Derived,
                  Cache,
                  Geometry,
                  Polarization,
                  Profile,
                  L,
                  F} <: AbstractLaser
    constants::PhysicsConst
    derived::Derived
    cache::Cache
    geometry::Geometry
    polarization::Polarization
    profile::Profile
    # laser parameters
    λ::L
    a₀::F
    ϕ₀::F
    w₀::L
end

function GaussLaser(units;
        λ,
        a₀,
        ϕ₀ = 0.0,
        w₀,
        ξx = 1.0+0im,
        ξy = 0,
        orientation = (:x, :z),
        profile = ConstantProfile()
    )

    ξx, ξy = promote(ξx, ξy)
    a₀, ϕ₀ = promote(a₀, ϕ₀)
    λ, w₀ = promote(λ, w₀)
    @assert hypot(ξx, ξy) ≈ 1 "Invalid ξx and ξy"
    constants = FundamentalConstants(units)
    @unpack mₑ, c, q = constants

    derived = GaussLaserConstantCache(; c, λ, w₀, a₀, mₑ, q)
    E₀ = derived.E₀

    cache = GaussLaserCache(λ, E₀)

    geometry = LaserGeometry(orientation)

    polarization = LaserPolarization(ξx, ξy)

    return GaussLaser(
        constants,
        derived,
        cache,
        geometry,
        polarization,
        profile,
        λ,
        a₀,
        ϕ₀,
        w₀
    )
end

update_cache!(::GaussLaser{P,D,Cache}, ::Any, ::Any) where {P,D,Cache<:Nothing} = nothing

function required_coords(::GaussLaser, r)
    CylindricalFromCartesian()(r)
end

function Ex(laser::GaussLaser, coords)
    @unpack E₀, k, z_R = immutable_cache(laser)
    ξx = polarization(laser, :ξx)
    @unpack w₀, ϕ₀ = laser
    @unpack r, z = coords

    wz = w(z, laser)
    update_cache!(laser, :wz, wz)

    ξx * E₀ * w₀/wz * exp(-(r/wz)^2 + im*(-(r^2*z)/(z_R*wz^2) + atan(z, z_R) - k*z + ϕ₀))
end

function Ez(laser::GaussLaser, coords)
    @unpack k, z_R = immutable_cache(laser)
    @unpack Ex, Ey, x, y, wz = mutable_cache(laser)
    z = coords.z

    2im/(k*wz^2)*(1 + im*(z/z_R))*(x*Ex + y*Ey)
end

function Bz(laser::GaussLaser, coords)
    @unpack k, z_R = immutable_cache(laser)
    @unpack Ex, Ey, x, y, wz = mutable_cache(laser)
    c = fundamental_constants(laser, :c)
    z = coords.z

    2im/(k*c*wz^2)*(1 + im*(z/z_R))*(y*Ex - x*Ey)
end


using Test
using Example2018

# Polynomial form a ring
# 1 addition is associative****
# 2 additive identity
# 3 additive inverse
# 4 additive commutes****
# 5 multiplication is...

@testset "Polynomials form a ring" begin

	z = Polynomial([0])

	for n in 1:100

		deg = rand(0:100)

		p = Polynomial(rand(-1000000:1000000, deg+1))

		@test p + z == p
		@test z + p == p
		@test p + (-p) == z
	end
end


__precompile__()

module AutoDiff
using DataStructures
export Constant,Variable, Call, CoTangent, Lift, val
export rect_lin,sigmoid,convolve
export minimize

import Base: (+), (-), (*), (/), (^),
(.+), (.-), (.*), (./), (.^), exp, log, sum

begin
	#my_zero(x) returns an array of zeros of the same shape as x
	#x can be a number, an array, an array of arrays, etc.
	function my_zero(A::Array)
		[my_zero(x) for x in A]
	end
	function my_zero{T<:Number}(A::Array{T})
		zeros(A)
	end
	function my_zero(x)
		zero(x)
	end
end

#Type CoTangent{T} stores a value of type T and a derivative
abstract CoTangent{T}
type Variable{T} <: CoTangent{T}
	val::T
	delta::T
end
type Constant{T} <: CoTangent{T}
	val::T
	delta::T
end
Variable{T}(val::T)=Variable{T}(val,my_zero(val))
Constant{T}(val::T)=Constant{T}(val,my_zero(val))

is_CoTangent(x::CoTangent)=true
is_CoTangent(x)=false

type Call{F,T} <: CoTangent{T}
	f::Function
	args::Tuple
	val::T
	delta::T
	function Call(f,args,v,delta)
		new(f,args,v,delta)
	end
end

type Lift{T}<:CoTangent{T}
	args
	val::T
	delta::T
end
function Lift(args)
	tmp=map(val,args)
	Lift(args,tmp,my_zero(tmp))
end

val(x)=x
val(x::CoTangent)=x.val

Call(f,args)=Call(f,args,f(map(val,args)...))
Call{T}(f::Function,args,val::T)=Call{Base.function_name(f),T}(f,args,val,my_zero(val))

begin
	function execute(x::Call)
		x.val=x.f(map(val,x.args)...)
	end
	function execute(x::Lift)
		x.val=map(val,x.args)
	end
	function execute(x) end
end

begin
	function clear_array{T<:Real}(A::Array{T})
		fill!(A,zero(T))
	end
	function clear_array{T<:Array}(A::Array{T})
		map(clear_array,A)
	end
	function reset_delta{T<:Real}(t::CoTangent{T})
		t.delta=zero(T)
	end
	function reset_delta{T<:Array}(t::CoTangent{T})
		clear_array(t.delta)
	end
end

function backprop end

#returns a function which computes f'
#expects a function f which takes an array of vars
#and an array of constants
function derivative(f, vars, constants)
	wrapped_vars=map(x->Variable(x),vars)
	wrapped_constants=map(x->Constant(x),constants)
	output=f(wrapped_vars,wrapped_constants)
	l=topological_order(output)
	function update_params(new_params)
		for (x,y) in zip(wrapped_vars,new_params)
			x.val=y
		end
	end
	function gradient(constants)
		map(reset_delta,l)
		for (x,y) in zip(wrapped_constants,constants)
			x.val=y
		end
		map(execute,l)
		output.delta=1
		map(backprop,reverse(l))
		return map(x->x.delta,wrapped_vars)
	end

	return (update_params,gradient)
end

function compute_grad(expr,l)
	map(reset_delta,l)
	expr.delta=1
	map(backprop,reverse(l))
end
begin
	function descend(d::Variable,delta)
		d.val -= delta.*d.delta
	end
	function descend(d::CoTangent,delta) end
end
function minimize(c::CoTangent;delta=0.01,its=1000,f=(it->nothing))
	l=topological_order(c)
	for i in 1:its
		map(execute,l)
		f(i)
		compute_grad(c,l)
		map(x->descend(x,delta), l)
	end
end

function depth_first_map(f,t::CoTangent)
	visited=Set()
	function traverse(c::Union{Call,Lift})
		if !(c in visited)
			for child in c.args
				traverse(child)
			end
			f(c)
			push!(visited,c)
		end
	end
	function traverse(c::Variable)
		if !(c in visited)
			f(c)
			push!(visited,c)
		end
	end
	function traverse(c) end
	traverse(t)
end

function topological_order(c::CoTangent)
	q=Any[]
	depth_first_map(x->push!(q,x),c)
	q
end

include("AutoDiff/printing.jl")
include("AutoDiff/backprop.jl")
include("AutoDiff/call_defs.jl")

function sigmoid(x)
	1 ./(1 .+exp(-x))
end
function rect_lin(x)
	max(x,0)
end
end


using DataDeps

register(DataDep(
    "synthetic",
    """
    Dataset: synthetic

    A simulated multivariate normal.
    Classes are determined by the first two dimensions, which correspond to signs of X*beta with beta[1] = 10, beta[2] = -10.
    Covariance structure is as follows:

    Σ[1,1] = 1, Σ[2,2] = 3, Σ[i,i] = 2
    Σ[1,2] = 0.9
    Σ[i,j] = 1e-3*randn()

    Observations: 1000
    Features:     500
    Classes:      2
    """,
    "script", # nothing to download
    "514ef72019b85634859baf459b38f37295d5fcc84d06be7d8e4a71a99a74cbdf";
    fetch_method=function(unused, localdir)
        #
        rng = StableRNG(2000)
        (m, n) = (10^3, 500)

        # covariance matrix
        Σ = Matrix{Float64}(2*I, n, n)
        for j in 1:n, i in j+1:n
            Σ[j,i] = 1e-3*randn(rng)
        end
        Σ[1,1] = 1.0
        Σ[2,2] = 3.0
        Σ[1,2] = 0.9

        L, _ = cholesky(Symmetric(Σ))

        # simulate data
        X = Matrix{Float64}(undef, m, n)
        for i in axes(X, 1)
            @views X[i, :] .= L*randn(rng, n)
        end

        # coefficients
        beta = zeros(n)
        beta[1] = 10.0
        beta[2] = -10.0

        # targets
        y = sign.(X*beta)
        target = map(yi -> yi > 0 ? 'A' : 'B', y)

        local_file = joinpath(localdir, "data.csv")
        df = hcat(DataFrame(target=target), DataFrame(X, :auto))
        CSV.write(local_file, df)
        return local_file
    end
))


using Flux, Flux.Data.MNIST, Statistics
using Flux: throttle, params
using Flux: @epochs, mse, onehotbatch, argmax, crossentropy
using Base.Iterators: repeated, partition
using Plots
using Colors
using Images

function generatedata_circle(r1, r2, N, σ=0.1)
	φ1 = LinRange(0, 2 * π, N)
	φ2 = LinRange(0, 2 * π, N)
	rx1 = r1 .+ σ * randn(N)
	rx2 = r2 .+ σ * randn(N)
	X1 = [rx1 .* cos.(φ1) rx1 .* sin.(φ1)]
	X2 = [rx2 .* cos.(φ2) rx2 .* sin.(φ2)]
	return X1', X2'
end

X1c, X2c = generatedata_circle(2, 0.5, 100)

X = [X1c X2c]
Y = [zeros(1, size(X1c, 2)) ones(1, size(X2c, 2))]

nHidden = 3

activeFunction = relu

m = Chain(
		Dense(size(X,1), nHidden, activeFunction),
		Dense(nHidden, nHidden, activeFunction),
		Dense(nHidden, 1, activeFunction)
		)

loss(x, y) = mse(m(x), y)
iters = 10000

dataset = repeated((X, Y), iters)

opt = ADAM()
evalcb = () -> @show([loss(X, Y)])

Flux.train!(loss, params(m), dataset, opt; cb = throttle(evalcb, 0.5))

function display_decision_boundaries(X1c, X2c, m, x1range, x2range, τ=0.0)
    D = [m([x1; x2])[1] for x2 in x2range, x1 in x1range]
    heatmap(
			x1range,
			x2range,
			sign.(D .- τ);
			color=:grays,
			xlim = [minimum(x1range),maximum(x1range)],
			ylim = [minimum(x2range),maximum(x2range)],
			)
    scatter!(
			X1c[1, :],
			X1c[2, :],
			color="red",
			label="Class 1",
			aspectratio=:1.0,
			)
    scatter!(
			X2c[1, :],
			X2c[2, :],
			color="blue",
			label="Class 2",
			)
end

x1range = range(-3; stop=3, length=200)
x2range = range(-3; stop=3, length=200)

display_decision_boundaries(X1c, X2c, m, x1range, x2range)
plot!(; title="Loss = $(round(loss(X,Y), digits=4))")

@show m.layers[1].W
@show m.layers[1].b
@show m.layers[1].σ;


## conversion between spectrum envelop and mel-cepstrum

# TODO(ryuichi) import from MelGeneralizedCepstrums or SPTK
# copied from r9y9/MelGeneralizedCepstrums.jl
function freqt!(wc::AbstractVector, c::AbstractVector, α;
                prev=Array(eltype(wc), length(wc)))
    fill!(wc, zero(eltype(wc)))
    dst_order = length(wc) - 1

    m1 = length(c)-1
    for i in -m1:0
        copy!(prev, wc)
        if dst_order >= 0
            @inbounds wc[1] = c[-i+1] + α*prev[1]
        end
        if dst_order >= 1
            wc[2] = (1.0-α*α)*prev[1] + α*prev[2]
        end
        for m=3:dst_order+1
            @inbounds wc[m] = prev[m-1] + α*(prev[m] - wc[m-1])
        end
    end

    wc
end

function freqt(c::AbstractVector, order=25, α=0.35)
    wc = Array(eltype(c), order+1)
    freqt!(wc, c, α)
end

"""
$(SIGNATURES)

sp2mc converts power spectrum envelope to mel-cepstrum

``
|X(\\omega)|^{2} -> c\_{\\alpha}(m)
``
"""
function sp2mc(powerspec::AbstractVector,
               order,
               α; # all-pass constant
               fftlen::Int=(length(powerspec)-1)*2
    )
    # |X(ω)|² -> log(|X(ω)²|)
    logperiodogram = log(powerspec)

    # transform log-periodogram to real cepstrum
    # log(|X(ω)|²) -> c(m)
    c = real(irfft(logperiodogram, fftlen))
    c[1] /= 2.0

    # c(m) -> cₐ(m)
    freqt(c, order, α)
end

"""
$(SIGNATURES)

mc2sp converts mel-cepstrum to power spectrum envelope.

``
c\_{\\alpha}(m) -> |X(\\omega)|^{2}
``

equivalent: `exp(2real(MelGeneralizedCepstrums.mgc2sp(mc, α, 0.0, fftlen)))`
Note that `MelGeneralizedCepstrums.mgc2sp` returns log magnitude spectrum.
"""
function mc2sp{T}(mc::AbstractVector{T}, α, fftlen)
    # back to cepstrum from mel-cesptrum
    # cₐ(m) -> c(m)
    c = freqt(mc, fftlen>>1, -α)
    c[1] *= 2.0

    symc = zeros(T, fftlen)
    copy!(symc, c)
    for i=1:length(c)-1
        symc[end-i+1] = symc[i+1]
    end

    # back to power spectrum
    # c(m) -> log(|X(ω)|²) -> |X(ω)|²
    exp(real(rfft(symc)))
end

# extend vector to vector transformation for matrix input
for f in [:sp2mc,
          :mc2sp,
          ]
    @eval begin
        function $f(x::AbstractMatrix, args...; kargs...)
            outbuf = $f(view(x, :, 1), args...; kargs...)
            ret = Array(eltype(outbuf), length(outbuf), size(x, 2))
            copy!(ret, 1, outbuf, 1, length(outbuf))
            for i = 2:size(x, 2)
                @inbounds ret[:, i] = $f(view(x, :, i), args...; kargs...)
            end
            ret
        end
    end
end


using IntervalArithmetic
using Base.Test

@testset "Operations on boxes" begin
    A = IntervalBox(1..2, 3..4)
    B = IntervalBox(0..2, 3..6)

    @test 2*A == IntervalBox(2..4, 6..8)
    @test A + B == IntervalBox(1..4, 6..10)
    @test dot(A, B) == @interval(9, 28)

    @test A ⊆ B
    @test A ∩ B == A
    @test A ∪ B == B

    X = IntervalBox(1..2, 3..4)
    Y = IntervalBox(3..4, 3..4)

    @test isempty(X ∩ Y)
    @test X ∪ Y == IntervalBox(1..4, 3..4)

    X = IntervalBox(2..4, 3..5)
    Y = IntervalBox(3..5, 4..17)
    @test X ∩ Y == IntervalBox(3..4, 4..5)

    v = [@interval(i, i+1) for i in 1:10]
    V = IntervalBox(v...)
    @test length(V) == 10

    Y = IntervalBox(1..2)  # single interval
    @test isa(Y, IntervalBox)
    @test length(Y) == 1
    @test Y == IntervalBox( (Interval(1., 2.),) )

end

# @testset "@intervalbox tests" begin
#     @intervalbox f(x, y) = (x + y, x - y)
#
#     X = IntervalBox(1..1, 2..2)
#     @test f(X) == IntervalBox(3..3, -1 .. -1)
#
#     @intervalbox g(x, y) = x - y
#     @test isa(g(X), IntervalBox)
#
#     @test emptyinterval(X) == IntervalBox(∅, ∅)
#
# end

@testset "setdiff for IntervalBox" begin
    X = IntervalBox(2..4, 3..5)
    Y = IntervalBox(3..5, 4..6)
    @test setdiff(X, Y) == [ IntervalBox(3..4, 3..4),
                              IntervalBox(2..3, 3..5) ]

    X = IntervalBox(2..5, 3..6)
    Y = IntervalBox(-10..10, 4..5)
    @test setdiff(X, Y) == [ IntervalBox(2..5, 3..4),
                              IntervalBox(2..5, 5..6) ]

    X = IntervalBox(2..5, 3..6)
    Y = IntervalBox(4..6, 4..5)
    @test setdiff(X, Y) == [ IntervalBox(4..5, 3..4),
                              IntervalBox(4..5, 5..6),
                              IntervalBox(2..4, 3..6) ]

    X = IntervalBox(2..5, 3..6)
    Y = IntervalBox(3..4, 4..5)
    @test setdiff(X, Y) == [ IntervalBox(3..4, 3..4),
                              IntervalBox(3..4, 5..6),
                              IntervalBox(2..3, 3..6),
                              IntervalBox(4..5, 3..6) ]

    X = IntervalBox(2..5, 3..6)
    Y = IntervalBox(2..4, 10..20)
    @test setdiff(X, Y) == typeof(X)[X]

    X = IntervalBox(2..5, 3..6)
    Y = IntervalBox(-10..10, -10..10)
    @test setdiff(X, Y) == typeof(X)[]

    X = IntervalBox(1..4, 3..6, 7..10)
    Y = IntervalBox(2..3, 4..5, 8..9)
    @test setdiff(X, Y) == [ IntervalBox(2..3, 4..5, 7..8),
                              IntervalBox(2..3, 4..5, 9..10),
                              IntervalBox(2..3, 3..4, 7..10),
                              IntervalBox(2..3, 5..6, 7..10),
                              IntervalBox(1..2, 3..6, 7..10),
                              IntervalBox(3..4, 3..6, 7..10) ]

end

@testset "mid, diam, × for IntervalBox" begin
    X = (0..2) × (3..5)
    @test length(X) == 2
    @test X == IntervalBox(Interval(0, 2), Interval(3, 5))

    @test diam(X) == 2
    @test mid(X) == [1, 4]

    Y = X × (4..8)
    @test isa(Y, IntervalBox)
    @test length(Y) == 3
    @test Y == IntervalBox(Interval(0, 2), Interval(3, 5), Interval(4, 8))
    @test diam(Y) == 4
end

@testset "Constructing multidimensional IntervalBoxes" begin
    @test IntervalBox(1..2, Val{1}) == IntervalBox(1..2)
    @test IntervalBox(1..2, Val{2}) == (1..2) × (1..2)
    @test IntervalBox(1..2, Val{5}) == (1..2) × (1..2) × (1..2) × (1..2) × (1..2)

    @test IntervalBox(1..2, 3) == IntervalBox(1..2, Val{3})

end


# module mesh_rcm_1
# using FinEtools
# using FinEtools.MeshExportModule
# using FinEtools.MeshModificationModule: adjgraph, nodedegrees, revcm
# using Test
# using SparseArrays, UnicodePlots

# function test()
# 	conn = [9 1 8 4;
# 	1 3 2 8;
# 	8 2 7 5;
# 	2 6 7 7];
# 	nfens = 9;
# 	ag = adjgraph(conn, nfens)
# 	nd = nodedegrees(ag)
# 	@test ag == Array{Int64,1}[[9, 8, 4, 3, 2], [1, 3, 8,
# 	7, 5, 6], [1, 2, 8], [9, 1, 8], [8, 2, 7], [2, 7], [8, 2, 5, 6], [9, 1, 4, 3, 2, 7, 5], [1, 8, 4]]
# 	@test nd == [5, 6, 3, 3, 3, 2, 4, 7, 3]
# 	numbering = revcm(ag, nd)
# 	@test numbering == [4, 9, 5, 8, 3, 1, 7, 2, 6]

# 	A = sprand(nfens, nfens, 1/nfens)
# 	A = A+A'
# 	# display(spy(A))
# 	# display(spy(A[numbering, numbering]))
# end

# end
# using .mesh_rcm_1
# mesh_rcm_1.test()

# module mesh_rcm_2
# using FinEtools
# using FinEtools.MeshExportModule
# using FinEtools.MeshModificationModule: adjgraph, nodedegrees, revcm
# using Test
# using SparseArrays, UnicodePlots

# function test()
# 	nfens = 29;

# 	# A = sprand(nfens, nfens, 1/nfens)
# 	# A = A+A'
# 	A = spzeros(nfens, nfens)
# 	A[5 ,  1]  =  0.559559
# 	A[24,  1]  =  0.079212
# 	A[19,  2]  =  0.459102
# 	A[8 ,  3]  =  0.844709
# 	A[16,  3]  =  0.206808
# 	A[24,  4]  =  0.82036
# 	A[1 ,  5]  =  0.559559
# 	A[7 ,  5]  =  0.595562
# 	A[28,  6]  =  0.151036
# 	A[5 ,  7]  =  0.595562
# 	A[3 ,  8]  =  0.844709
# 	A[24,  8]  =  0.47093
# 	A[21,  9]  =  0.673707
# 	A[22,  9]  =  0.492159
# 	A[24,  9]  =  0.10736
# 	A[19, 10]  =  0.99992
# 	A[18, 11]  =  0.573561
# 	A[22, 11]  =  0.803174
# 	A[17, 12]  =  0.127183
# 	A[28, 12]  =  0.644722
# 	A[29, 14]  =  0.839357
# 	A[3 , 16]  =  0.206808
# 	A[20, 16]  =  0.0470789
# 	A[12, 17]  =  0.127183
# 	A[11, 18]  =  0.573561
# 	A[2 , 19]  =  0.459102
# 	A[10, 19]  =  0.99992
# 	A[16, 20]  =  0.0470789
# 	A[26, 20]  =  0.661536
# 	A[9 , 21]  =  0.673707
# 	A[9 , 22]  =  0.492159
# 	A[11, 22]  =  0.803174
# 	A[24, 23]  =  0.373656
# 	A[1 , 24]  =  0.079212
# 	A[4 , 24]  =  0.82036
# 	A[8 , 24]  =  0.47093
# 	A[9 , 24]  =  0.10736
# 	A[23, 24]  =  0.373656
# 	A[20, 26]  =  0.661536
# 	A[6 , 28]  =  0.151036
# 	A[12, 28]  =  0.644722
# 	A[14, 29]  =  0.839357

# 	ag = adjgraph(A)
# 	nd = nodedegrees(ag)
# 	numbering = revcm(ag, nd)
# 	display(spy(A))
# 	display(spy(A[numbering, numbering]))
# end

# end
# using .mesh_rcm_2
# mesh_rcm_2.test()

# module mesh_rcm_3
# using FinEtools
# using FinEtools.MeshExportModule
# using FinEtools.MeshModificationModule: adjgraph, nodedegrees, revcm
# using Test
# using SparseArrays, UnicodePlots

# function test()
# 	nfens = 19;

# 	# A1 = sprand(nfens, nfens, 1/nfens)
# 	# @show A1 = A1+A1'
# 	A1 = spzeros(nfens, nfens)
# 	A1[7 ,  1]  =  0.783374
# 	A1[18,  1]  =  0.6411
# 	A1[8 ,  2]  =  0.66032
# 	A1[13,  2]  =  0.552169
# 	A1[5 ,  3]  =  0.522678
# 	A1[11,  4]  =  0.244274
# 	A1[3 ,  5]  =  0.522678
# 	A1[19,  5]  =  0.870687
# 	A1[15,  6]  =  0.254443
# 	A1[17,  6]  =  0.138423
# 	A1[1 ,  7]  =  0.783374
# 	A1[8 ,  7]  =  0.274651
# 	A1[11,  7]  =  0.255421
# 	A1[15,  7]  =  0.961861
# 	A1[2 ,  8]  =  0.66032
# 	A1[7 ,  8]  =  0.274651
# 	A1[11,  8]  =  0.0421145
# 	A1[4 , 11]  =  0.244274
# 	A1[7 , 11]  =  0.255421
# 	A1[8 , 11]  =  0.0421145
# 	A1[12, 11]  =  0.610131
# 	A1[16, 11]  =  0.678996
# 	A1[11, 12]  =  0.610131
# 	A1[19, 12]  =  0.510702
# 	A1[2 , 13]  =  0.552169
# 	A1[18, 13]  =  0.0696182
# 	A1[14, 14]  =  0.213021
# 	A1[15, 14]  =  0.516788
# 	A1[6 , 15]  =  0.254443
# 	A1[7 , 15]  =  0.961861
# 	A1[14, 15]  =  0.516788
# 	A1[17, 15]  =  0.34131
# 	A1[11, 16]  =  0.678996
# 	A1[6 , 17]  =  0.138423
# 	A1[15, 17]  =  0.34131
# 	A1[1 , 18]  =  0.6411
# 	A1[13, 18]  =  0.0696182
# 	A1[5 , 19]  =  0.870687
# 	A1[12, 19]  =  0.510702

# 	A = vcat(hcat(A1, 0*A1, 0*A1), hcat(0*A1, 0*A1, 0*A1), hcat(0*A1, 0*A1, A1))
# 	ag = adjgraph(A)
# 	nd = nodedegrees(ag)
# 	numbering = revcm(ag, nd)
# 	display(spy(A))
# 	display(spy(A[numbering, numbering]))
# 	@test numbering == [55, 52, 44, 56, 51, 53, 39, 40, 45, 46, 54, 42, 49, 50, 57, 43, 41, 17, 14, 6, 18, 13, 15, 1, 2, 7, 8, 16, 4, 11, 12, 19, 5, 3, 48, 47, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27,
# 26, 25, 24, 23, 22, 21, 20, 10, 9]
# end

# end
# using .mesh_rcm_3
# mesh_rcm_3.test()

# module mesh_rcm_5
# using FinEtools
# using FinEtools.MeshModificationModule: adjgraph, nodedegrees, revcm
# using Test
# using SparseArrays, UnicodePlots
# using LinearAlgebra
# using LinearAlgebra: norm

# function test()
# 	nfens = 511117;

# 	A = sprand(nfens, nfens, 1/nfens)
# 	A = A+A' + 1I
# 	@time ag = adjgraph(A)
# 	@time nd = nodedegrees(ag)
# 	@time numbering = revcm(ag, nd)

# 	# display(spy(A))
# 	# display(spy(A[numbering, numbering]))
# 	# b = rand(nfens)
# 	# inumbering = deepcopy(numbering)
# 	# inumbering[numbering] = 1:nfens
# 	# x = A * b
# 	# xp = A[numbering, numbering] * b[numbering]
# 	# xp[numbering]
# 	# @test norm(x - xp[inumbering]) < 1.0e-5*nfens
# end

# end
# using .mesh_rcm_5
# mesh_rcm_5.test()


using QuadGK
using StableRNGs
using Transits
using Test

const PLOT = get(ENV, "TEST_PLOTS", "false") == "true"
PLOT && include("plots.jl")

rng = StableRNG(2752)

@testset "Transits" begin
    include("elliptic.jl")
    include("Mn_integral.jl")
    include("poly.jl")
end


abstract type AbstractLinregSolver end

"""
    SolveQR()

Pass as `method` to [`linregress`](@ref) to use QR factorization to solve the
linear regression system. Slower but more accurate for ill-conditioned systems
than [`SolveCholesky`](@ref).
"""
struct SolveQR <: AbstractLinregSolver end

"""
    SolveCholesky()

Pass as `method` to [`linregress`](@ref) to use Cholesky factorization to solve
the linear regression system. Faster but less accurate for ill-conditioned
systems than [`SolveQR`](@ref).
"""
struct SolveCholesky <: AbstractLinregSolver end

struct LinearRegressor{T<:Union{Matrix{<:Real},Vector{<:Real}}}
    intercept::Bool
    coeffs::T
end

"""
    coef(lr::LinearRegressor)

Returns the linear regression coefficients of a [`linregress`](@ref) result. If
`lr.intercept` is `true`, this includes the bias (intercept) in the last
position.
"""
coef(lr::LinearRegressor) = lr.coeffs
islinear(lr::LinearRegressor) = true

slope(lr::LinearRegressor) = lr.intercept ? _slope(lr.coeffs) : lr.coeffs
bias(lr::LinearRegressor) = lr.intercept ? _bias(lr.coeffs) : _zero_bias(lr.coeffs)

_slope(coef::AbstractMatrix) = @view coef[1:end-1, :]
_slope(coef::AbstractVector) = @view coef[1:end-1]
_bias(coef::AbstractMatrix) = @view coef[end:end, :]
_bias(coef::AbstractVector) = coef[end]
_zero_bias(coef::AbstractMatrix) = zero(eltype(lr.coeffs), 1, size(lr.coeffs, 2))
_zero_bias(coef::AbstractVector) = zero(eltype(lr.coeffs))

function (lr::LinearRegressor)(X::AbstractMatrix)
    if lr.intercept
        return X * _slope(lr.coeffs) .+ _bias(lr.coeffs)
    else
        return X * lr.coeffs
    end
end

function (lr::LinearRegressor{<:Vector})(x::AbstractVector)
    if lr.intercept
        return x'_slope(lr.coeffs) + _bias(lr.coeffs)
    else
        return dot(x, lr.coeffs)
    end
end

if VERSION < v"1.5"
    _vec(x) = collect(vec(x))
else
    _vec(x) = vec(x)
end

function (lr::LinearRegressor{<:Matrix})(x::AbstractVector)
    if lr.intercept
        return _vec(x'_slope(lr.coeffs) + _bias(lr.coeffs))
    else
        return _vec(x'lr.coeffs)
    end
end

@doc raw"""
    linregress(X, y; intercept=true, method=SolveQR())
    linregress(X, y, weights; intercept=true, method=SolveQR())

Do (possibly weighted) linear regression to obtain coefficients β such that `ŷ
= X * β` minimizes `‖ŷ - y‖²`. In the default case, this corresponds to solving
`X \ y`.

Returns a `LinearRegressor`, which can be passed to [`coef`](@ref) to extract
the coefficients β or called with a vector or matrix `X` to predict at a single
point or set of points.

Currently assumes that, if `X` is a Matrix, that `size(X) == (length(y),
num_coefs)` (i.e., each row describes the features for one observation).

## Keyword Arguments:

- If `intercept` is `true`, implicitly adds a column of ones to `X` to model a
  bias term.

- `method` determines how to solve the linear regression system (see
  [`SolveQR`](@ref) and [`SolveCholesky`](@ref)).
"""
function linregress(X, y, weights=nothing; intercept=true, method=SolveQR())
    size(X, 1) == size(y, 1) || throw(DimensionMismatch("size of X and y does not match in first dimension"))
    if intercept
        X = _append_bias_column(method, X)
    else
        X = _maybe_convert(method, X)
    end
    coeffs = if weights === nothing
        _lin_solve(method, X, y)
    else
        length(weights) == size(y, 1) || throw(DimensionMismatch("size of y and weights does not match in first dimension"))
        W = Diagonal(weights)
        _lin_solve(method, X, y, W)
    end
    return LinearRegressor(intercept, coeffs)
end

function _lin_solve(solver::AbstractLinregSolver, X, y, W)
    # √W X \ √W y
    Wsqrt = sqrt(W)
    return _lin_solve(solver, Wsqrt * X, Wsqrt * y)
end

function _lin_solve(::SolveQR, X, y)
    return X \ y
end

function _lin_solve(::SolveCholesky, X, y, W)
    # X' W X \ (X' W y)
    return ldiv!(cholesky!(Hermitian(X' * W * X)), X' * (W * y))
end

function _lin_solve(::SolveCholesky, X, y)
    # X'X \ X'y
    return ldiv!(cholesky!(Hermitian(X'X)), X'y)
end

function _append_bias_column(::AbstractLinregSolver, X)
    # avoids any conversions at this point
    ones_column = ones(eltype(X), size(X, 1))
    return [X ones_column]
end

function _append_bias_column(::SolveCholesky, X)
    # already promotes to the correct type
    ones_column = ones(LinearAlgebra.choltype(X), size(X, 1))
    return [X ones_column]
end

_maybe_convert(::AbstractLinregSolver, X) = X
_maybe_convert(::SolveCholesky, X) = convert(Array{LinearAlgebra.choltype(X)}, X)


using Utils
using ProblemParser

using Lazy
using MLStyle

struct SNum
    str::AbstractString
end

@data SameOrChanged begin
    Same(SNum)
    Changed(SNum)
end
Changed(inner::String) = Changed(SNum(inner))

macro s_str(str)
    SNum(str)
end

const file = find_input(@__FILE__)
const input = parse(Lines(Map(SNum)), slurp(file))

function getinner(str::AbstractString)
    opened = 0
    comma = -1
    for i in eachindex(str)
        if str[i] == '['
            opened += 1
        elseif str[i] == ']'
            opened -= 1
            opened == 0 && return view(str, 2:i - 1), comma - 1, i - 1
        elseif str[i] == ',' && opened == 1
            comma = i
        end
    end
    error("closing bracket not found")
end

function replacematch1(str::AbstractString, (match, repl)::Pair; reversed=false)
    re = match === nothing ? str : string(
        str[begin:match.offset - 1],
        reversed ? reverse(string(repl)) : repl,
        str[match.offset + length(match.match):end],
    )
    if reversed
        re = reverse(re)
    end
    re
end

anumber = r"\d+"

function explode(num::SNum)::SameOrChanged
    str = num.str
    depth = 0
    for i in eachindex(str)
        c = str[i]
        if c == '['
            @views if depth == 4
                prefix = reverse(str[begin:i - 1])
                lhs, comma, bracket = getinner(str[i:end])
                rhs = lhs[comma + 1:end]
                lhs = lhs[begin:comma - 1]
                suffix = str[i + bracket + 1:end]

                pos = match(anumber, prefix)
                if pos !== nothing
                    number = parse.(Int, [lhs, reverse(pos.match)]) |> sum
                    prefix = replacematch1(prefix, pos => number; reversed=true)
                end

                pos = match(anumber, suffix)
                if pos !== nothing
                    number = parse.(Int, [rhs, pos.match]) |> sum
                    suffix = replacematch1(suffix, pos => number)
                end

                return string(prefix, 0, suffix) |> Changed
            else
                depth += 1
            end
        elseif c == ']'
            depth -= 1
        end
    end
    Same(num)
end

append(lhs::SNum, rhs::SNum) = string('[', lhs.str, ',', rhs.str, ']') |> SNum

append(lhs, rhs) = string('[', lhs, ',', rhs, ']')

function Base.split(num::SNum)::SameOrChanged
    for m in eachmatch(anumber, num.str)
        n = parse(Int, m.match)
        if n > 9
            lr = [n, n + 1] .÷ 2
            return replacematch1(num.str, m => append(lr...)) |> Changed
        end
    end
    Same(num)
end

function Base.reduce(num::SNum)::SNum
    while true
        num = @match explode(num) begin
            Changed(num) => num
            Same(num) => @match split(num) begin
                Changed(num) => num
                _ => return num
            end
        end
    end
end

Base.:+(lhs::SNum, rhs::SNum) = append(lhs, rhs) |> reduce

function magnitude(num::AbstractString)
    if isdigit(num[1])
        m = match(anumber, num)
        parse(Int, m.match)
    else
        lhs, comma = getinner(num)
        rhs = lhs[comma + 1:end]
        lhs = lhs[begin:comma - 1]
        3magnitude(lhs) + 2magnitude(rhs)
    end
end
magnitude(num::SNum) = magnitude(num.str)

part1(input) = @>> input foldl(+) magnitude

assertequal(part1(input), 3725)

part2(input) = maximum(magnitude(a + b) for a in input for b in input)

assertequal(part2(input), 4832)

using Test
@testset "18" begin
    @testset "addition" begin
        @test s"[[[[4,3],4],4],[7,[[8,4],9]]]" + s"[1,1]" == s"[[[[0,7],4],[[7,8],[6,0]]],[8,1]]"
    end
    @testset "replacematch1" begin
        target = "this is a foo string"
        @test replacematch1(target, match(r"fo+", target) => "bar") == "this is a bar string"
        target = reverse(target)
        @test replacematch1(target, match(r"o+f", target) => "bar"; reversed=true) == "this is a bar string"
    end
    @testset "explode" begin
        @test explode(s"[[[[[9,8],1],2],3],4]") == Changed(s"[[[[0,9],2],3],4]")
        @test explode(s"[7,[6,[5,[4,[3,2]]]]]") == Changed(s"[7,[6,[5,[7,0]]]]")
        @test explode(s"[[6,[5,[4,[3,2]]]],1]") == Changed(s"[[6,[5,[7,0]]],3]")
        @test explode(s"[[3,[2,[1,[7,3]]]],[6,[5,[4,[3,2]]]]]") == Changed(s"[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]")
        @test explode(s"[[3,[2,[8,0]]],[9,[5,[4,[3,2]]]]]") == Changed(s"[[3,[2,[8,0]]],[9,[5,[7,0]]]]")
        @test explode(s"[[3,[2,[8,0]]],[9,[5,[4,1]]]]") == Same(s"[[3,[2,[8,0]]],[9,[5,[4,1]]]]")
    end
    @testset "split" begin
        @test split(s"[[[[0,7],4],[9,[0,9]]],[1,1]]") == Same(s"[[[[0,7],4],[9,[0,9]]],[1,1]]")
        @test split(s"[[[[0,7],4],[15,[0,13]]],[1,1]]") == Changed(s"[[[[0,7],4],[[7,8],[0,13]]],[1,1]]")
        @test split(s"[[[[0,7],4],[[7,8],[0,13]]],[1,1]]") == Changed(s"[[[[0,7],4],[[7,8],[0,[6,7]]]],[1,1]]")
    end
    @testset "reduce" begin
        @test reduce(s"[[[[[4,3],4],4],[7,[[8,4],9]]],[1,1]]") == s"[[[[0,7],4],[[7,8],[6,0]]],[8,1]]"
    end
    @testset "magnitude" begin
        @test magnitude(s"[[1,2],[[3,4],5]]") == 143
        @test magnitude(s"[[[[0,7],4],[[7,8],[6,0]]],[8,1]]") == 1384
        @test magnitude(s"[[[[1,1],[2,2]],[3,3]],[4,4]]") == 445
        @test magnitude(s"[[[[3,0],[5,3]],[4,4]],[5,5]]") == 791
        @test magnitude(s"[[[[5,0],[7,4]],[5,5]],[6,6]]") == 1137
        @test magnitude(s"[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]") == 3488
    end
end


# This example was inspired by this great blog post:
# https://cre8math.com/2015/10/04/creating-fractals/

using GLVisualize, GLAbstraction, Reactive, GeometryTypes, Colors, GLWindow
import GLVisualize: slider, mm, button, labeled_slider

if !isdefined(:runtests)
    window = glscreen()
end

description = """
Demonstrating a UI for exploring the Koch snowflake.
"""

function spin(dir, α, len)
    x, y = dir
    Point(x*cos(α) - y*sin(α), x*sin(α) + y*cos(α)) * len
end
function fractal_step!(
        a, b, depth, angles,
        result = Point2f0[a], levels = Float32[depth] # tmp too not allocate
    )
    depth == 0 && return result, levels, b
    N = length(angles)
    diff = (b - a)
    len = norm(diff) / (N - 1)
    nth_segment = normalize(diff)
    for n = 1:N
        b = a + spin(nth_segment * len, angles[n]...) # rotate with current angle
        _, _, b = fractal_step!(a, b, depth-1, angles, result, levels) # recursion step
        if n < N
            push!(result, b)
            push!(levels, depth)
        end
        nth_segment = normalize(b - a)
        a = b
    end
    result, levels, b
end

const T = Float64
const P = Point{2, T}

function generate_fractal(angles, depth = 5)
    tmp = zeros(P, length(angles))
    angles = map(x-> (deg2rad(T(x[1])), T(x[2])), angles)
    result, levels, b = fractal_step!(P(0,0), P(300,0), round(Int, depth), angles)
    push!(result, b)
    push!(levels, depth)
    mini, maxi = extrema(result)
    w = 1 ./ maximum(maxi - mini)
    map!(result, result) do p
        1000 * (p - mini) .* w
    end
    # invert
    result, levels
end

iconsize = 5mm
textsize = 5mm

editarea, viewarea = x_partition_abs(window.area, round(Int, 12 * iconsize))
edit_screen = Screen(
    window, area = editarea,
    color = RGBA{Float32}(0.0f0, 0.0f0, 0.0f0, 1f0),
    stroke = (1f0, RGBA{Float32}(0.13f0, 0.13f0, 0.13f0, 1f0))
)
viewscreen = Screen(
    window, area = viewarea,
    color = RGBA(0.0f0, 0.0f0, 0.0f0, 1f0)
)

angles = ntuple(4) do i
    labeled_slider(0.0:1.0:360.0, edit_screen, text_scale = textsize, icon_size = iconsize, knob_scale = 3mm)
end

iterations_v, iterations_s = labeled_slider(1:11, edit_screen, text_scale = textsize, icon_size = iconsize, knob_scale = 3mm)

cmap_v, cmap_s = widget(
    map(RGBA{Float32}, colormap("Blues", 5)),
    edit_screen;
    area = (12 * iconsize, iconsize/3),
    knob_scale = 1.3mm,
)

thickness_v, thickness_s = widget(
    Signal(0.4f0), edit_screen,
    text_scale = textsize,
    range = 0f0:0.05f0:20f0
)

segments = Point2f0[
    (0.0, 0.0),
    (2 * iconsize, 0.0),
    (4 * iconsize, iconsize /  2),
    (6 * iconsize, iconsize / -2),
    (7 * iconsize, 0.0)
]

# we could restrict the movement of the points with the kw_arg clampto
# But I don't really feel like restricting the user here ;)
line_v, line_s = widget(segments, edit_screen, knob_scale = 1.5mm)

center_v, center_s = button("⛶", relative_scale = iconsize, edit_screen)

controls = Pair[
    "angle 1" => angles[1][1],
    "angle 2" => angles[2][1],
    "angle 3" => angles[3][1],
    "angle 4" => angles[4][1],
    "iterations" => iterations_v,
    "colormap" => cmap_v,
    "thickness" => thickness_v,
    "segment" => line_v,
    "center cam" => center_v
]

_view(visualize(
    controls,
    text_scale = textsize,
    width = 12iconsize
), edit_screen, camera = :fixed_pixel)

function to_anglelengths(angles, line)
    diff0 = Point2f0(1, 0)
    v1 = first(line)
    maxlen = 0
    for (i, v2) in enumerate(line[2:end])
        diff1 = v2 - v1
        len = norm(diff1)
        diff1 = normalize(diff1)
        maxlen = max(len, maxlen)

        d = dot(diff0, diff1)
        det = cross(diff0, diff1)

        angles[i] = (atan2(det, d), len)
        v1 = v2; diff0 = diff1
    end
    for (i, al) in enumerate(angles)
        angles[i] = (rad2deg(al[1]), al[2] / maxlen)
    end
    angles
end
v0 = to_anglelengths(Array{Tuple{Float32, Float32}}(4), value(line_s))

angle_vec1 = foldp(to_anglelengths, v0, line_s)

angle_s = map(last, angles)
anglevec2 = foldp(Array{Tuple{Float32, Float32}}(4), angle_s...) do angles, s...
    for i=1:4
        angles[i] = s[i], 1.0
    end
    angles
end
anglevec = merge(angle_vec1, anglevec2)

it1_points = map(anglevec) do angles
    generate_fractal(angles, 1)[1] ./ 100f0
end

line_level = map(anglevec, iterations_s) do angles, iter
    generate_fractal(angles, iter)
end
line_pos = map(first, line_level)
_view(visualize(
    line_pos, :lines,
    thickness = thickness_s,
    color_map = cmap_s,
    color_norm = map(i-> Vec2f0(0, i), iterations_s),
    intensity = map(last, line_level),
    boundingbox = nothing
), viewscreen, camera = :orthographic_pixel)

_view(visualize(
    it1_points, :lines, model = translationmatrix(Vec3f0(20, 20, 0)),
    thickness = 2f0, color = RGBA(1f0, 1f0, 1f0, 1f0)
), viewscreen, camera = :fixed_pixel)

const cam = viewscreen.cameras[:orthographic_pixel]

s = preserve(map(center_s) do clicked
    clicked && center!(cam, AABB(value(line_pos)))
    nothing
end)

# center won't get executed before the first time the center button is clicked.
# we still want to start centered ;)
center!(cam, AABB(value(line_pos)))

if !isdefined(:runtests)
    renderloop(window)
    # clean up signals
    for (v, s) in angles
        close(s, false)
    end
    close(center_s, false)
    close(thickness_s, false)
    close(iterations_s, false)
    close(iterations_s, false)
end

# you can change angles directly like this:
# you need to start the renderloop asynchronously then though: @async renderloop(window)
# And then of course, don't clean up the signals before you are done
#push!(angles[3][2], 220f0)


###############################################################################
## Least squares problem parametrized by regularizer reg
#    min_x  0.5 * ||Ax-y||² + g(reg, x)
struct LeastsquaresPb{Tr, Tm, Txman} <: CompositeProblem
    A::Matrix{Float64}
    y::Vector{Float64}
    regularizer::Tr
    n::Int64
    x0::Txman
    M_x0::Tm
end

problem_dimension(pb::LeastsquaresPb) = pb.n

## f
# 0th order
f(pb::LeastsquaresPb, x) = 0.5 * norm(pb.A * x - pb.y)^2
# f(pb::LeastsquaresPb, x::AbstractMatrix) = f(pb, view(x, :))

# 1st order
∇f!(pb::LeastsquaresPb, res, x) = (res .= transpose(pb.A) * (pb.A * x - pb.y))
∇f(pb::LeastsquaresPb, x) = transpose(pb.A) * (pb.A * x - pb.y)

# # ∇f!(pb::LeastsquaresPb, res, x::AbstractMatrix) = ∇f!(pb, view(res, :), view(x, :))
# ∇f(pb::LeastsquaresPb, x::AbstractMatrix) = reshape(transpose(pb.A) * (pb.A * view(x, :) - pb.y), size(x))

# 2nd order
∇²f_h!(pb::LeastsquaresPb, res, x, h) = (res .= transpose(pb.A) * (pb.A * h))
∇²f_h(pb::LeastsquaresPb, x, h) = transpose(pb.A) * (pb.A * h)

# conditioning
get_gradlips(pb::LeastsquaresPb) = opnorm(pb.A)^2
get_μ_cvx(pb::LeastsquaresPb) = (svdvals(pb.A)[end])^2

## instance generation:
function build_original_signal(::regularizer_l1, n, sparsity, seed)
    Random.seed!(seed)
    x0 = rand(Normal(), n)

    Random.seed!(seed)
    inds_nz = Set(randperm(n)[1:sparsity])

    nnz_coords = BitVector(zeros(n))
    for ind in inds_nz
        nnz_coords[ind] = 1
    end

    x0 .*= nnz_coords

    return x0, l1Manifold(nnz_coords)
end

function build_original_signal(reg::regularizer_distball, n, sparsity, seed)
    Random.seed!(seed)
    x0 = rand(Normal(), n)
    x0 *= reg.r / norm(x0, reg.p)

    return x0, PSphere(reg.p, reg.r, n)
end

function build_original_signal(reg::regularizer_lnuclear, mn, sparsity, seed)
    m, n = mn

    Random.seed!(seed)
    x0 = rand(Normal(), m, n)

    F = svd(x0)
    x0 = F.U[:, 1:sparsity] * Diagonal(F.S[1:sparsity]) * F.Vt[1:sparsity, :]

    return vec(x0), FixedRankMatrices(m, n, sparsity)
end

function build_original_signal(reg::regularizer_group{Tr}, n, sparsity, seed) where {Tr}
    x0 = zeros(n)
    Ms = []
    for (i, group) in enumerate(reg.groups)
        x0i, M0 = build_original_signal(reg.regs[i], length(group), sparsity, seed)
        x0[group] .= x0i
        push!(Ms, M0)
    end

    return x0, ProductManifold(Ms...)
end

function update_regularizationstrength!(regularizer, delta)
    regularizer.λ = delta
    return
end
function update_regularizationstrength!(
    regularizer::regularizer_group{Tr},
    delta,
) where {Tr}
    for i in 1:length(regularizer.regs)
        update_regularizationstrength!(regularizer.regs[i], delta)
    end
    return
end

"""
    get_random_qualifiedleastsquares(n, m, regularizer, sparsity; seed=1234)

Build a least-squares problem that fits the signal recovery framework. The optimization
problem solution should be close to the original signal `x0` and lie exactly on the same
optimal manifold `M_x0`.
"""
function get_random_qualifiedleastsquares(n, m, regularizer, sparsity; seed = 1234, A=nothing, delta = 0.01)
    # A is drawn from the standard normal distribution
    # b = Ax0 + e where e is taken from the normal distribution with standard deviation 0.001.
    # We set λ1 so that the original sparsity is ultimately recovered.
    n_vectorialpb = isa(n, Tuple) ? n[1]*n[2] : n

    Random.seed!(seed)
    if isnothing(A)
        A = rand(Normal(), m, n_vectorialpb)
    end

    x0, M0 = build_original_signal(regularizer, n, sparsity, seed)

    Random.seed!(seed + 3)
    e = rand(Normal(0, delta^2), m)

    y = A * x0 + e

    update_regularizationstrength!(regularizer, delta)
    return LeastsquaresPb(A, y, regularizer, n_vectorialpb, x0, M0)
end

function get_random_qualifiedlasso(n, m, sparsity; seed = 1234)
    return get_random_qualifiedleastsquares(
        n,
        m,
        regularizer_l1(1.0),
        sparsity;
        seed = seed,
    )
end

# function get_randomlasso(n, m, sparsity; reg=regularizer_l1, seed=1234)
#     # A is drawn from the standard normal distribution
#     # b = Ax0 + e where e is taken from the normal distribution with standard deviation 0.001.
#     # We set λ1 so that the original sparsity is ultimately recovered.

#     elseif reg <: regularizer_l12
#         T = reg.parameters[1]
#         ngroups = Int(ceil(n/T))

#         Random.seed!(seed)
#         inds_nz = Set(randperm(ngroups)[1:sparsity])

#         for i in 1:ngroups-1
#             !(i in inds_nz) && (x0[T*i-3:T*i] .= 0)
#         end
#         !(ngroups in inds_nz) && (x0[T*ngroups-3:end] .= 0)

function get_lasso_MLE(;n=20, m=15, sparsity=0.5, δ=0.1, seed=1234)
    @assert 0 ≤ sparsity ≤ 1

    Random.seed!(seed)
    A = rand(Normal(), m, n)

    Random.seed!(seed+1)
    x0 = rand(Normal(), n)
    Random.seed!(seed+2)
    optstructure = l1Manifold(rand(Bernoulli(1-sparsity), n))
    x0 = project(optstructure, x0)

    # measures are linear model + (unbiaised) noise
    y = A*x0 + rand(Normal(0, δ^2), m)

    return LeastsquaresPb(A, y, regularizer_l1(δ), n, x0, optstructure)
end


using Statistics: mean
using Distributions: logpdf
using EvidentialDeepLearning: evidence, predict, std_aleatoric
using LinearAlgebra: norm
using Statistics: var

export nll

"""

    nll(d,y;agg=mean)

Negative log likelihood loss. Given by `agg(-logpdf.(d,y))`.

# Arguments
* d (array of) distributions
* y (array of) points in the distributions domain.
"""
function nll(d,y;agg=mean)
    agg(-logpdf.(d,y))
end

"""

    evidence_regularizer(d,y;agg=mean)

Loss function for NormalInverseGamma regression.
Described in $(REF_ASSR2019).
"""
function evidence_regularizer(d,y;agg=mean)
    function evi_reg(d,y)
        err = y-predict(d)
        evidence(d) * norm(err)
    end
    agg(evi_reg.(d, y))
end

function regression_loss(d, y; λ=1f-2)
    pp = posterior_predictive.(d)
    nll(pp, y) + λ*evidence_regularizer(d,y)
end

"""

    dirichlet_sos(d, y)

Regularized sum of squares loss for dirichlet classification. Described in $(REF_SKK2018).
"""
function dirichlet_sos(ds, labels::AbstractVector; λ)
    @argcheck eltype(ds) <: Dirichlet
    @argcheck size(ds) == size(labels)
    T = sampletype(eltype(ds))
    ret = float(zero(T))
    for (d, label) in zip(ds, labels)
        d̃ = remove_non_misleading_evidence(d̃)
        ret += sos(d,label) + λ * kl_uniform(Dirichlet(d̃))
    end
    return ret/length(labels)
end

function remove_non_misleading_evidence(d::Dirichlet, label::Integer)
    T = sampletype(d)
    Dirichlet(setindex(d.α, one(T), label))
end

function sos(d::Dirichlet{N}, label::Integer) where {N}
    α = d.α
    S = sum(α)
    m = α./S
    mse = (m[label] - 1)^2 - m[label]^2 + sum(abs2, m)
    sum_var = sum(α .* (S .- α) ./ (S .^2 .* (S .+ 1)))
    mse + sum_var
end


### A Pluto.jl notebook ###
# v0.14.5

using Markdown
using InteractiveUtils

# ╔═╡ 8df38f30-b0e9-11eb-2f2a-1bb2f968d206
md"""
# [Day 20: Infinite Elves and Infinite Houses](https://adventofcode.com/2015/day/20)

To keep the Elves busy, Santa has them deliver some presents by hand, door-to-door. He sends them down a street with infinite houses numbered sequentially: `1`, `2`, `3`, `4`, `5`, and so on.

Each Elf is assigned a number, too, and delivers presents to houses based on that number:

-   The first Elf (number `1`) delivers presents to every house: `1`, `2`, `3`, `4`, `5`, ....
-   The second Elf (number `2`) delivers presents to every second house: `2`, `4`, `6`, `8`, `10`, ....
-   Elf number `3` delivers presents to every third house: `3`, `6`, `9`, `12`, `15`, ....

There are infinitely many Elves, numbered starting with `1`. Each Elf delivers presents equal to ten times his or her number at each house.

So, the first nine houses on the street end up like this:

```
House 1 got 10 presents.
House 2 got 30 presents.
House 3 got 40 presents.
House 4 got 70 presents.
House 5 got 60 presents.
House 6 got 120 presents.
House 7 got 80 presents.
House 8 got 150 presents.
House 9 got 130 presents.

```

The first house gets `10` presents: it is visited only by Elf `1`, which delivers `1 * 10 = 10` presents. The fourth house gets `70` presents, because it is visited by Elves `1`, `2`, and `4`, for a total of `10 + 20 + 40 = 70` presents.

**What is the lowest house number of the house to get at least as many presents as the number in your puzzle input?**
"""

# ╔═╡ 3508f641-2704-4bd5-9d44-987b29e87cf2
puzzle_input = "36000000"

# ╔═╡ 70139d68-0c2f-4d14-a753-fa6c5e573f35

# ╔═╡ Cell order:
# ╟─8df38f30-b0e9-11eb-2f2a-1bb2f968d206
# ╟─3508f641-2704-4bd5-9d44-987b29e87cf2
# ╠═70139d68-0c2f-4d14-a753-fa6c5e573f35


using MPI, MPIArrays

MPI.Init()
const comm = MPI.COMM_WORLD
const rank = MPI.Comm_rank(comm)
const nb_procs = MPI.Comm_size(comm)
@assert nb_procs == 4

N = 4 # size of the matrix

# Sequentially display the partitons
function display_partitions(a::MPIArray, message)
  if rank != 0
    return
  end

  println("---- $message ----")

  for r in 0:nb_procs-1
    println("local matrix for rank $r:")
    display(getblock(a[localindices(a,r)...]))
    println()
  end
end

# Initial tiled distribution:
A = MPIArray{Int}(comm, (2, 2), N, N)
forlocalpart!(lp -> fill!(lp, rank), A)
sync(A)
display_partitions(A, "Initial distribution")

# Rows only distro
redistribute!(A, 4, 1)
display_partitions(A, "Rows")

#  non-uniform
redistribute!(A, [N], [1,0,0,3])
display_partitions(A, "Non-uniform")

redistribute!(A)
display_partitions(A, "Restored default")

if rank == 0
  println("-------Complete array -------")
  display(A)
  println()
end

# Clean up
free(A)
MPI.Finalize()


export IAAFT
"""
    IAAFT(M = 100, tol = 1e-6, W = 75)

An iteratively adjusted amplitude-adjusted-fourier-transform surrogate[^SchreiberSchmitz1996].

IAAFT surrogate have the same linear correlation, or periodogram, and also
preserves the amplitude distribution of the original data, but are improved relative
to AAFT through iterative adjustment (which runs for a maximum of `M` steps).
During the iterative adjustment, the periodograms of the original signal and the
surrogate are coarse-grained and the powers are averaged over `W` equal-width
frequency bins. The iteration procedure ends when the relative deviation
between the periodograms is less than `tol` (or when `M` is reached).

IAAFT, just as AAFT, can be used to test the null hypothesis that the data
come from a monotonic nonlinear transformation of a linear Gaussian process.

[^SchreiberSchmitz1996]: T. Schreiber; A. Schmitz (1996). "Improved Surrogate Data for Nonlinearity Tests". [Phys. Rev. Lett. 77 (4)](https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.77.635)
"""
struct IAAFT <: Surrogate
    M::Int
    tol::Real
    W::Int

    function IAAFT(;M::Int = 100, tol::Real = 1e-6, W::Int = 75)
        new(M, tol, W)
    end
end

Base.show(io::IO, x::IAAFT) = print(io, "IAAFT(M=$(x.M), tol=$(x.tol), W=$(x.W))")

function surrogenerator(x, method::IAAFT)
    # Pre-plan Fourier transforms
    forward = plan_rfft(x)
    inverse = plan_irfft(forward*x, length(x))

    # Pre-compute stuff that can be used for different surrogate realizations
    m = mean(x)
    x_sorted = sort(x)
    𝓕 = forward*(x .- m)
    r_original = abs.(𝓕)

    # Coarse-grain the periodograms when comparing them between iterations.
    px = DSP.periodogram(x)
    range = LinRange(0.0, 0.5, method.W)
    px_binned = interp(px.freq, px.power, range)

    # These are updated during iteration procedure
    𝓕new = Vector{Complex{Float64}}(undef, length(𝓕))
    𝓕sorted = Vector{Complex{Float64}}(undef, length(𝓕))
    ϕsorted = Vector{Complex{Float64}}(undef, length(𝓕))

    init = (forward = forward, inverse = inverse, m = m, 𝓕 = 𝓕, r_original = r_original,
            px_binned = px_binned, range = range, x_sorted = x_sorted,
            𝓕new = 𝓕new, 𝓕sorted =  𝓕sorted, ϕsorted = ϕsorted)

    return SurrogateGenerator(method, x, init)
end

function (sg::SurrogateGenerator{<:IAAFT})()
    init_fields = (:forward, :inverse, :m, :𝓕, :r_original,
                    :px_binned, :range,
                    :x_sorted,
                    :𝓕new, :𝓕sorted, :ϕsorted)
    forward, inverse, m, 𝓕, r_original,
        px_binned, range,
        x_sorted,
        𝓕new, 𝓕sorted, ϕsorted = getfield.(Ref(sg.init), init_fields)

    x = sg.x
    M = sg.method.M
    tol = sg.method.tol

    # Keep track of difference between periodograms between iterations
    diffs = zeros(Float64, 2)

    # RANK ORDERING.
    # Create some Gaussian noise, and find the indices that sorts it with
    # increasing amplitude. Then sort the original time series according to
    # the indices rendering the Gaussian noise sorted.
    n = length(x)
    g = rand(Normal(), n)
    ts_sorted = x[sortperm(g)]

    # The surrogate
    s = Vector{Float64}(undef, n)

    iter = 1
    success = false
    while iter <= M
        # Take the Fourier transform of `ts_sorted` and get the phase angles of
        # the resulting complex numbers.
        𝓕sorted .= forward * ts_sorted
        ϕsorted .= angle.(𝓕sorted)

        # The new spectrum preserves the amplitudes of the Fourier transform of
        # the original time series, but randomises the phases (because the
        # phases are derived from the *randomly sorted* version of the original
        # time series).
        𝓕new .= r_original .* exp.(ϕsorted .* 1im)

        # Now, let the surrogate time series be the values of the original time
        # series, but sorted according to the new spectrum. The shuffled series
        # is generated by taking the inverse Fourier transform of the spectrum
        # consisting of the original amplitudes, but having randomised phases.
        s .= real.(inverse * 𝓕new) # ifft normalises by default

        # map original values onto shuffle
        s[sortperm(s)] = x_sorted
        ts_sorted .= s

        # Convergence check on periodogram
        ps = DSP.periodogram(s)
        ps_binned = interp(ps.freq, ps.power, range)

        if iter == 1
            diffs[1] = sum((px_binned[2] .- ps_binned[2]).^2) / sum(px_binned[2].^2)
        else
            diffs[2] = sum((px_binned[2] .- ps_binned[2]).^2) / sum(px_binned[2].^2)
            abs(diffs[1] - diffs[2]) < tol ? break : diffs[1] = copy(diffs[2])
        end

        iter += 1
    end

    return s
end


using StaticArrays
using Parameters
using OrdinaryDiffEq
using Test

@inbounds @inline function ż(z, p, t)
    @unpack A, B, D = p
    p₀, p₂ = z[1:2]
    q₀, q₂ = z[3:4]

    return SVector{4}(
        -A * q₀ - 3 * B / √2 * (q₂^2 - q₀^2) - D * q₀ * (q₀^2 + q₂^2),
        -q₂ * (A + 3 * √2 * B * q₀ + D * (q₀^2 + q₂^2)),
        A * p₀,
        A * p₂
  )
end

condition(u, t, integrator) = u
affect!(integrator) = nothing
cbf(idx) = ContinuousCallback(condition,
    affect!, nothing, save_positions=(false, true), idxs=idx)
z0 = SVector{4}(7.1989885061904335, -0.165912283356219, 0., -3.63534900748947)

tspan=(0.,300.)
prob=ODEProblem(ż, z0, tspan, (A=1, B=0.55, D=0.4), callback=cbf(3))
sol=solve(prob, Vern9(), abstol=1e-14, reltol=1e-14,
    save_everystep=false, save_start=false, save_end=false, maxiters=1e6)

@test length(sol) == 126

prob=ODEProblem(ż, z0, (0,400.), (A=1, B=0.55, D=0.4), callback=cbf(3))
sol=solve(prob, Vern9(), abstol=1e-14, reltol=1e-14, save_everystep=false, save_start=false, save_end=false, maxiters=2e4)

@test length(sol) == 148

prob=ODEProblem(ż, z0, (0,5000.), (A=1, B=0.55, D=0.4), callback=cbf(3))
sol=solve(prob, Vern9(), abstol=1e-14, reltol=1e-14, save_everystep=false, save_start=false, save_end=false, maxiters=1e6)

@test length(sol) == 1883

using ParameterizedFunctions
f = @ode_def BallBounce begin
  dy =  v
  dv = -g
end g
function condition(u,t,integrator) # Event when event_f(u,t) == 0
  u[1]
end
function affect!(integrator)
  integrator.u[2] = -integrator.u[2]
end
cb2 = ContinuousCallback(condition,affect!)
tspan = (0.0,100000.0)
u0 = [50.0,0.0]
p = 9.8
prob = ODEProblem(f,u0,tspan,p)
sol = solve(prob,Tsit5(),callback=cb2)
@test minimum(sol') > -40


Random.seed!(0)
@testset "fixed points" begin

#=
    Global objects.
=#

# Function converter for out of place --> in place.
function make_inplace(f::Function)
    function inplace_f(out, x)
        out .= f(x)
    end
    return inplace_f
end

# "Naive" simple iteration methods for comparison.
    # In place.
    function iterate!(f!, x0; residualnorm = (x -> norm(x,Inf)), tol = 1E-10, maxiter=100)
        residual = Inf
        iter = 1
        xold = x0
        xnew = copy(x0)
        while residual > tol && iter < maxiter
            f!(xnew, xold)
            residual = residualnorm(xold - xnew);
            xold = copy(xnew)
            iter += 1
        end
        return (xold,iter)
    end
    # Out of place.
    function iterate(f, x0; residualnorm = (x -> norm(x,Inf)), tol = 1E-10, maxiter=100)
        residual = Inf
        iter = 1
        xold = x0
        xnew = copy(x0)
        while residual > tol && iter < maxiter
            xnew = f(xold)
            residual = residualnorm(xold - xnew);
            xold = copy(xnew)
            iter += 1
        end
        return (xold,iter)
    end
    # Tests of these.
    A_1 = [0.7 0.0; 0.0 0.3];
    b_1 = [1.5; 3.2];
    f_1 = x -> A_1 * x + b_1;
    f_1! = make_inplace(f_1)
    init_x = [3.4, 4.3]
    @test iterate!(f_1!, init_x)[1] == iterate(f_1, init_x)[1] ≈ [5.0, 4.571428571428571]

#=
    Anderson tests.
=#

# Tests against the above example.
    @test fixedpoint(f_1!, [3.4, 4.3]).zero == fixedpoint(f_1, [3.4, 4.3]; inplace = false).zero ≈ [5.0, 4.571428571428571]
    @test fixedpoint(f_1!, [3.4, 4.3]; m = 2).zero == fixedpoint(f_1, [3.4, 4.3]; inplace = false, m = 2).zero ≈ [5.0, 4.571428571428571]

# Tests for some common functions.
    # x -> sin.(x)
    f_2 = x -> 0.5 * x
    f_2! = make_inplace(f_2)
    srand = 123 # For determinism in the random tests.
    init_x2 = rand(Float64, 4)
    @test fixedpoint(f_2!, init_x2; iterations = 10000, ftol = 1e-15).zero == fixedpoint(f_2, init_x2; inplace = false, iterations = 10000, ftol = 1e-15).zero
    @test isapprox(fixedpoint(f_2!, init_x2; iterations = 10000, ftol = 1e-15).zero, zeros(Float64, 4), atol = 1e-10)
    # x -> exp(-x)
    f_3 = x -> exp.(-x)
    f_3! = make_inplace(f_3)
    init_x3 = [rand(Float64)*100]
    @test fixedpoint(f_3!, init_x3).zero == fixedpoint(f_3, init_x3; inplace = false).zero ≈ [0.5671432953088511]
    @test fixedpoint(f_3!, init_x3; m = 4).zero == fixedpoint(f_3, init_x3; inplace = false, m = 4).zero ≈ [0.5671432953088511]

# Tests for...

#=
    Gradient method tests.
=#
    # Autodifferentiation tests.
    @test fixedpoint(f_3!, init_x3; autodiff = :forward).zero == fixedpoint(f_3, init_x3; inplace = false, autodiff = :forward).zero ≈ [0.5671432953088511]
    @test fixedpoint(f_3!, init_x3; autodiff = :central).zero == fixedpoint(f_3, init_x3; inplace = false, autodiff = :central).zero ≈ [0.5671432953088511]

    # Newton tests.
    @test fixedpoint(f_3!, init_x3; autodiff = :forward, method = :newton).zero == fixedpoint(f_3, init_x3; inplace = false, autodiff = :forward, method = :newton).zero ≈ [0.5671432953088511]
    @test fixedpoint(f_3!, init_x3; autodiff = :central, method = :newton).zero == fixedpoint(f_3, init_x3; inplace = false, autodiff = :central, method = :newton).zero ≈ [0.5671432953088511]

#=
    `inplace` flag tests.
=#
    # default behavior
    foo = x -> 0.5 * x
    @test norm(fixedpoint(foo, init_x2, xtol = 1e-10, ftol = 0.0).zero) ≈ 0.0 atol = 1e-10
    bar = make_inplace(foo)
    @test norm(fixedpoint(bar, init_x2, xtol = 1e-10, ftol = 0.0).zero) ≈ 0.0 atol = 1e-10
    # error handling
    @test_throws MethodError fixedpoint(foo, init_x2, xtol = 1e-10, ftol = 0.0, inplace = true).zero
    @test_throws MethodError fixedpoint(bar, init_x2, xtol = 1e-10, ftol = 0.0, inplace = false).zero
    # concurrency tests
    baz(x) = 0.5*x
    function baz(out, x)
        out .= 0.5*x
    end
    @test fixedpoint(baz, init_x2).zero == fixedpoint(foo, init_x2).zero # use the one-argument (out of place) method if it exists
    # edge case
    zak(x) = map!(n -> 1/2 * n, x, x) # one-argument in-place function
    @test (fixedpoint(zak, init_x2).zero == fixedpoint(baz, init_x2).zero) == false # should fail
end