using HDF5
using MLDatasets
using MultivariateStats
# include("DPMMSubClusters.jl")
using MAT
using Random
using CSV
using NPZ
# using DPMMSubCluste~rs

DS_PATH = "/vildata/vlad/splitnet/experiments_data/"
mkpath(DS_PATH)

##Mnist
function create_mnist(PATH, max_dim=128)
    mnist_x, mnist_y = MNIST.traindata()
    reshaped_x = copy(Float32.(reshape(mnist_x,:,size(mnist_x)[end])))
    M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=max_dim) #64 provides 0.86 var presereved
    # M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=72) #64 provides 0.86 var presereved
	tranformed_x = transform(M, reshaped_x)

    MNIST_PATH = PATH * "mnist_dim_"*string(max_dim)*".h5"

    ispath(MNIST_PATH) && rm(MNIST_PATH)
    h5write(MNIST_PATH, "data", tranformed_x)
    h5write(MNIST_PATH, "labels", mnist_y)

    # MAT_PATH = PATH*"mnist.mat"
    # matwrite(MAT_PATH, Dict(
	# "data" => tranformed_x,
	# "labels" => mnist_y))

	npzwrite(PATH*"mnist_x.npy", tranformed_x')
	npzwrite(PATH*"mnist_y.npy", mnist_y)
end

##Mnist
function create_fashion_mnist(PATH, max_dim=128)
    fmnist_x, fmnist_y = FashionMNIST.traindata()
    reshaped_x = copy(Float32.(reshape(fmnist_x,:,size(fmnist_x)[end])))
    M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=max_dim) #64 provides 0.86 var presereved
    tranformed_x = transform(M, reshaped_x)

    MNIST_PATH = PATH * "fmnist_dim_"*string(max_dim)*".h5"

    ispath(MNIST_PATH) && rm(MNIST_PATH)
    h5write(MNIST_PATH, "data", tranformed_x)
    h5write(MNIST_PATH, "labels", fmnist_y)

    MAT_PATH = PATH*"fmnist.mat"
    matwrite(MAT_PATH, Dict(
	"data" => tranformed_x,
	"labels" => fmnist_y))

	npzwrite(PATH*"fmnist_x.npy", tranformed_x')
	npzwrite(PATH*"fmnist_y.npy", fmnist_y)
end

##cifar 10
function create_cifar10(PATH, max_dim=88)
    cifar_x, cifar_y = CIFAR10.traindata()
    reshaped_x = copy(Float32.(reshape(cifar_x,:,size(cifar_x)[end])))
    M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=max_dim) #128 preserves 0.918 of the variance
	# M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=16) #128 preserves 0.918 of the variance
    tranformed_x = transform(M, reshaped_x)

    CIFAR_PATH = PATH * "cifar10_dim_"*string(max_dim)*".h5"

    ispath(CIFAR_PATH) && rm(CIFAR_PATH)
    h5write(CIFAR_PATH, "data", tranformed_x)
    h5write(CIFAR_PATH, "labels", cifar_y)

	MAT_PATH = PATH*"cifar10.mat"
	matwrite(MAT_PATH, Dict(
	"data" => tranformed_x,
	"labels" => cifar_y))

	npzwrite(PATH*"cifar_x.npy", tranformed_x')
	npzwrite(PATH*"cifar_y.npy", cifar_y)
end

##cifar 100
function create_cifar100(PATH, max_dim=96)
    cifar_x, _, cifar_y = CIFAR100.traindata()
    reshaped_x = copy(Float32.(reshape(cifar_x,:,size(cifar_x)[end])))
    M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=max_dim) #128 preserves 0.918 of the variance
    tranformed_x = transform(M, reshaped_x)

    CIFAR_PATH = PATH * "cifar100_dim_"*string(max_dim)*".h5"

    ispath(CIFAR_PATH) && rm(CIFAR_PATH)
    h5write(CIFAR_PATH, "data", tranformed_x)
    h5write(CIFAR_PATH, "labels", cifar_y)

	MAT_PATH = PATH*"cifar100.mat"
	matwrite(MAT_PATH, Dict(
	"data" => tranformed_x,
	"labels" => cifar_y))

	npzwrite(PATH*"cifar100_x.npy", tranformed_x')
	npzwrite(PATH*"cifar100_y.npy", cifar_y)
end

function create_svhn(PATH, max_dim=64)
    train_x, train_y = SVHN2.traindata()
    reshaped_x = copy(Float32.(reshape(train_x,:,size(train_x)[end])))
    M = MultivariateStats.fit(PCA, reshaped_x; maxoutdim=max_dim) #128 preserves 0.918 of the variance
    tranformed_x = transform(M, reshaped_x)

    CIFAR_PATH = PATH * "svhn_dim_"*string(max_dim)*".h5"

    ispath(CIFAR_PATH) && rm(CIFAR_PATH)
    h5write(CIFAR_PATH, "data", tranformed_x)
    h5write(CIFAR_PATH, "labels", train_y)

	MAT_PATH = PATH*"svhn.mat"
	matwrite(MAT_PATH, Dict(
	"data" => tranformed_x,
	"labels" => train_y))

	npzwrite(PATH*"svhn_x.npy", tranformed_x')
	npzwrite(PATH*"svhn_y.npy", train_y)
end

function create_2d_gaussian(PATH)
	Random.seed!(1) #generate same data
    x,y,clusters =  DPMMSubClusters.generate_gaussian_data(100000,2,10,Float32(20.0))

    SAVE_PATH = PATH * "gaussian2d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian2d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian2d_x.npy", x')
	npzwrite(PATH*"gaussian2d_y.npy", y)

end

function create_3d_gaussian(PATH)
	Random.seed!(1234) #generate same data
    x,y,clusters =  DPMMSubClusters.generate_gaussian_data(1000000,3,6,Float32(3.0))

    SAVE_PATH = PATH * "gaussian3d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian3d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian3d_x.npy", x')
	npzwrite(PATH*"gaussian3d_y.npy", y)

end

function create_5d_gaussian(PATH)
	Random.seed!(1234) #generate same data
    x,y,clusters = DPMMSubClusters.generate_gaussian_data(1000000,5,40,Float32(2.0))

    SAVE_PATH = PATH * "gaussian5d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian5d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian5d_x.npy", x')
	npzwrite(PATH*"gaussian5d_y.npy", y)
end

function create_10d_gaussian(PATH)
	Random.seed!(1234) #generate same data
    x,y,clusters = DPMMSubClusters.generate_gaussian_data(400000,10,100,Float32(0.1))

    SAVE_PATH = PATH * "gaussian10d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian10d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian10d_x.npy", x')
	npzwrite(PATH*"gaussian10d_y.npy", y)
end

function create_30d_gaussian(PATH)
	Random.seed!(1234) #generate same data
    x,y,clusters = DPMMSubClusters.generate_gaussian_data(1000000,30,80,Float32(0.00000000001))

    SAVE_PATH = PATH * "gaussian30d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian30d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian30d_x.npy", x')
	npzwrite(PATH*"gaussian30d_y.npy", y)
end

function create_100d_gaussian(PATH)
	Random.seed!(1234) #generate same data
    x,y,clusters = DPMMSubClusters.generate_gaussian_data(100000,100,50,Float32(0.001))

    SAVE_PATH = PATH * "gaussian100d.h5"

    ispath(SAVE_PATH) && rm(SAVE_PATH)
    h5write(SAVE_PATH, "data", x)
    h5write(SAVE_PATH, "labels", y)

	MAT_PATH = PATH*"gaussian100d.mat"
	matwrite(MAT_PATH, Dict(
	"data" => x,
	"labels" => y))

	npzwrite(PATH*"gaussian100d_x.npy", x')
	npzwrite(PATH*"gaussian100d_y.npy", y)
end

function create_otto(PATH, PCA = false)
	ds = CSV.read("/home/dinari/smart_splits_experiments/dpgmm_smartsplits/otto-group-product-classification-challenge/train.csv")
	gt = ds[:,end]
	factorize_dict = Dict([v=>k for (k,v) in enumerate(unique(gt))])
	gt = (x -> factorize_dict[x]).(gt)

	data = Float32.(convert(Array,ds[:,2:end-1])')

	M = MultivariateStats.fit(MultivariateStats.PCA, data; maxoutdim=128)
	x_reshaped = transform(M, data)
	if PCA
		data = x_reshaped
	end
	SAVE_PATH = PATH * "otto.h5"

	ispath(SAVE_PATH) && rm(SAVE_PATH)
	h5write(SAVE_PATH, "data", data)
	h5write(SAVE_PATH, "labels", gt)

	MAT_PATH = PATH*"otto.mat"
	matwrite(MAT_PATH, Dict(
	"data" => data,
	"labels" => gt))

	npzwrite(PATH*"otto_x.npy", data')
	npzwrite(PATH*"otto_y.npy", gt)
end

function create_frogs(PATH)
	ds = CSV.read("/home/dinari/smart_splits_experiments/raw_datasets/Anuran Calls (MFCCs)/Frogs_MFCCs.csv")
	gt = ds[:,end-1]
	factorize_dict = Dict([v=>k for (k,v) in enumerate(unique(gt))])
	gt = (x -> factorize_dict[x]).(gt)

	data = Float32.(convert(Array,ds[:,1:end-4])')

	M = MultivariateStats.fit(PCA, data; maxoutdim=4) #64 provides 0.86 var presereved
	tranformed_x = transform(M, data)

	# data = tranformed_x

	SAVE_PATH = PATH * "frogs.h5"

	ispath(SAVE_PATH) && rm(SAVE_PATH)
	h5write(SAVE_PATH, "data", data)
	h5write(SAVE_PATH, "labels", gt)

	MAT_PATH = PATH*"frogs.mat"
	matwrite(MAT_PATH, Dict(
	"data" => data,
	"labels" => gt))

	npzwrite(PATH*"frogs_x.npy", data')
	npzwrite(PATH*"frogs_y.npy", gt)
end

function create_wine(PATH)
	ds = CSV.read("/home/dinari/smart_splits_experiments/raw_datasets/winequality-white.csv")
	gt = ds[:,end]
	factorize_dict = Dict([v=>k for (k,v) in enumerate(unique(gt))])
	gt = (x -> factorize_dict[x]).(gt)

	data = Float32.(convert(Array,ds[:,1:end-1])')

	M = MultivariateStats.fit(PCA, data; maxoutdim=10) #64 provides 0.86 var presereved
	tranformed_x = transform(M, data)

	data = tranformed_x

	SAVE_PATH = PATH * "wine.h5"

	ispath(SAVE_PATH) && rm(SAVE_PATH)
	h5write(SAVE_PATH, "data", data)
	h5write(SAVE_PATH, "labels", gt)

	MAT_PATH = PATH*"wine.mat"
	matwrite(MAT_PATH, Dict(
	"data" => data,
	"labels" => gt))

	npzwrite(PATH*"wine_x.npy", data')
	npzwrite(PATH*"wine_y.npy", gt)
end

function create_yeast(PATH)
	ds = CSV.read("/home/dinari/smart_splits_experiments/raw_datasets/yeast.data"; delim=',')
	gt = ds[:,end]
	factorize_dict = Dict([v=>k for (k,v) in enumerate(unique(gt))])
	gt = (x -> factorize_dict[x]).(gt)

	data = Float32.(convert(Array,ds[:,3:end-1])')

	M = MultivariateStats.fit(PCA, data; maxoutdim=3) #64 provides 0.86 var presereved
	tranformed_x = transform(M, data)

	data = tranformed_x

	SAVE_PATH = PATH * "yeast.h5"

	ispath(SAVE_PATH) && rm(SAVE_PATH)
	h5write(SAVE_PATH, "data", data)
	h5write(SAVE_PATH, "labels", gt)

	MAT_PATH = PATH*"yeast.mat"
	matwrite(MAT_PATH, Dict(
	"data" => data,
	"labels" => gt))

	npzwrite(PATH*"yeast_x.npy", data')
	npzwrite(PATH*"yeast_y.npy", gt)
end

function create_bees(PATH)
	ds = CSV.read("/home/dinari/smart_splits_experiments/raw_datasets/Tetragonula.csv")
	gt = ds[:,end]
	factorize_dict = Dict([v=>k for (k,v) in enumerate(unique(gt))])
	gt = (x -> factorize_dict[x]).(gt)

	data = Float32.(convert(Array,ds[:,1:end-3])') ./ 10000

	# M = MultivariateStats.fit(PCA, data; maxoutdim=3) #64 provides 0.86 var presereved
	# tranformed_x = transform(M, data)
	#
	# data = tranformed_x

	SAVE_PATH = PATH * "bees.h5"

	ispath(SAVE_PATH) && rm(SAVE_PATH)
	h5write(SAVE_PATH, "data", data)
	h5write(SAVE_PATH, "labels", gt)

	MAT_PATH = PATH*"bees.mat"
	matwrite(MAT_PATH, Dict(
	"data" => data,
	"labels" => gt))

	npzwrite(PATH*"bees_x.npy", data')
	npzwrite(PATH*"bees_y.npy", gt)
end

# Synthetic Data:
# create_2d_gaussian(DS_PATH)
# create_3d_gaussian(DS_PATH)
# create_5d_gaussian(DS_PATH)
# create_10d_gaussian(DS_PATH)
# create_30d_gaussian(DS_PATH)
# create_100d_gaussian(DS_PATH)

# Real Data:
# create_mnist(DS_PATH)
# create_fashion_mnist(DS_PATH, 128)
# create_svhn(DS_PATH)
create_cifar10(DS_PATH, 128)
create_cifar100(DS_PATH, 128)
# create_otto(DS_PATH)
# create_frogs(DS_PATH)
# create_wine(DS_PATH)
# create_yeast(DS_PATH)
# create_bees(DS_PATH)


using SparseArrays
using ModelingToolkit
import ModelingToolkit: bareiss!, find_pivot_col, bareiss_update!, swaprows!
import Base: swapcols!

function det_bareiss!(M)
    parity = 1
    _swaprows!(M, i, j) = (i != j && (parity = -parity); swaprows!(M, i, j))
    _swapcols!(M, i, j) = (i != j && (parity = -parity); swapcols!(M, i, j))
    # We only look at the last entry, so we don't care that the sub-diagonals are
    # garbage.
    zero!(M, i, j) = nothing
    rank = bareiss!(M, (_swapcols!, _swaprows!, bareiss_update!, zero!);
                    find_pivot = find_pivot_col)
    return parity * M[end, end]
end

@testset "bareiss tests" begin
# copy gives a dense matrix
@testset "bareiss tests: $T" for T in (copy, sparse)
    # matrix determinent pairs
    for (M, d) in ((BigInt[9 1 8 0; 0 0 8 7; 7 6 8 3; 2 9 7 7], -1),
                   (BigInt[1 big(2)^65+1; 3 4], 4 - 3 * (big(2)^65 + 1)))
        # test that the determinent was correctly computed
        @test det_bareiss!(T(M)) == d
    end
end end


__precompile__(true)
module InterestRates

# 0.4 compat
if !isdefined(Core, :String)
    typealias String UTF8String
end

using BusinessDays
using Base.Dates

export
	DayCountConvention,
	CompoundingType,
	CurveMethod,
	AbstractIRCurve,
	ERF, ER, discountfactor, zero_rate, forward_rate,
	ERF_to_rate, discountfactor_to_rate,
	isnullcurve

include("types.jl")
include("nullcurve.jl")

############# DAYCOUNT #################

daycount(conv::BDays252, date_start::Date, date_end::Date) = Int(bdays(conv.hc, date_start, date_end))
daycount(::Actual360, date_start::Date, date_end::Date) = Int(date_end - date_start)
daycount(::Actual365, date_start::Date, date_end::Date) = Int(date_end - date_start)

advancedays(conv::BDays252, date_start::Date, daycount::Int) = advancebdays(conv.hc, date_start, daycount)
advancedays(::Actual360, date_start::Date, daycount::Int) = date_start + Day(daycount)
advancedays(::Actual365, date_start::Date, daycount::Int) = date_start + Day(daycount)

function advancedays(conv::DayCountConvention, date_start::Date, daycount_vec::Vector{Int})
	l = length(daycount_vec)
	result = Array(Date, l)
	for i in 1:l
		result[i] = advancedays(conv, date_start, daycount_vec[i])
	end
	return result
end

advancedays(curve::AbstractIRCurve, daycount) = advancedays(curve_get_daycount(curve), curve_get_date(curve), daycount)

daysperyear(::BDays252) = 252
daysperyear(::Actual360) = 360
daysperyear(::Actual365) = 365

yearfraction(conv::DayCountConvention, date_start::Date, date_end::Date) = daycount(conv, date_start, date_end) / daysperyear(conv)
yearfraction(curve::AbstractIRCurve, maturity::Date) = yearfraction(curve_get_daycount(curve), curve_get_date(curve), maturity)

######### COMPOUNDING TYPE ############

# Effective Rate Factor
_ERF(::ContinuousCompounding, r::Float64, t::Float64) = t == 0.0 ? 1.0 : exp(r*t)
_ERF(::SimpleCompounding, r::Float64, t::Float64) = t == 0.0 ? 1.0 : 1.0 + r*t
_ERF(::ExponentialCompounding, r::Float64, t::Float64) = t == 0.0 ? 1.0 : (1.0+r)^t
_ERF(ct::CompoundingType, dcc::DayCountConvention, r::Float64, date_start::Date, date_end::Date) = _ERF(ct, r, yearfraction(dcc, date_start, date_end))
ERF(curve::AbstractIRCurve, maturity::Date) = _ERF(curve_get_compounding(curve), curve_get_daycount(curve), zero_rate(curve, maturity), curve_get_date(curve), maturity)
ERF(curve::AbstractIRCurve, forward_date::Date, maturity::Date) = ERF(curve, maturity) / ERF(curve, forward_date)

_ERF_to_rate(::ContinuousCompounding, ERF::Float64, t::Float64) = log(ERF) / t
_ERF_to_rate(::SimpleCompounding, ERF::Float64, t::Float64) = (ERF-1.0) / t
_ERF_to_rate(::ExponentialCompounding, ERF::Float64, t::Float64) = ERF^(1.0/t) - 1.0
ERF_to_rate(curve::AbstractIRCurve, ERF::Float64, t::Float64) = _ERF_to_rate(curve_get_compounding(curve), ERF, t)

discountfactor_to_rate(c::CompoundingType, _discountfactor_::Float64, t::Float64) = _ERF_to_rate(c, 1.0 / _discountfactor_, t)

function discountfactor_to_rate(c::CompoundingType, _discountfactor_vec_::Vector{Float64}, t_vec::Vector{Float64})
	l = length(_discountfactor_vec_)

	if l != length(t_vec)
		error("_discountfactor_vec_ and t_vec must have the same length. ($l != $(length(t_vec)))")
	end

	result = Array(Float64, l)
	for i in 1:l
		result[i] = discountfactor_to_rate(c, _discountfactor_vec_[i], t_vec[i])
	end

	return result
end

# Effective Rate = [Effective Rate Factor] - 1
_ER(c::CompoundingType, r::Float64, t::Float64) = _ERF(c, r, t) - 1.0
_ER(ct::CompoundingType, dcc::DayCountConvention, r::Float64, date_start::Date, date_end::Date) = _ER(ct, r, yearfraction(dcc, date_start, date_end))
ER(curve::AbstractIRCurve, maturity::Date) = _ER(curve_get_compounding(curve), curve_get_daycount(curve), zero_rate(curve, maturity), curve_get_date(curve), maturity)
ER(curve::AbstractIRCurve, forward_date::Date, maturity::Date) =  ERF(curve, forward_date, maturity) - 1.0

# [Discount Factor] = 1 / [Effective Rate Factor]
_discountfactor(ct::CompoundingType, r::Float64, t::Float64) = 1.0 / _ERF(ct, r, t)
_discountfactor(ct::CompoundingType, dcc::DayCountConvention, r::Float64, date_start::Date, date_end::Date) = _discountfactor(ct, r, yearfraction(dcc, date_start, date_end))
discountfactor(curve::AbstractIRCurve, maturity::Date) = _discountfactor(curve_get_compounding(curve), curve_get_daycount(curve), zero_rate(curve, maturity), curve_get_date(curve), maturity)

# Vector function for ERF and discountfactor functions
for fun in (:ERF, :discountfactor)
	@eval begin
		function ($fun)(curve::AbstractIRCurve, maturity_vec::Vector{Date})
			l = length(maturity_vec)
			_zero_rate_vec_ = zero_rate(curve, maturity_vec)
			result = Array(Float64, l)
			for i = 1:l
				result[i] = $( Symbol("_" * string(fun)) )(curve_get_compounding(curve), curve_get_daycount(curve), _zero_rate_vec_[i], curve_get_date(curve), maturity_vec[i])
			end
			return result
		end
	end
end

forward_rate(curve::AbstractIRCurve, forward_date::Date, maturity::Date) = ERF_to_rate(curve, ERF(curve, forward_date, maturity), yearfraction(curve_get_daycount(curve), forward_date, maturity))

function days_to_maturity(curve::AbstractIRCurve, maturity::Date)
	const d = daycount(curve_get_daycount(curve), curve_get_date(curve), maturity)
	if d < 0
		error("Maturity date $(maturity) should be greater than curve observation date $(curve_get_date(curve))")
	end
	return d
end

# Let's use the curve's method to multiple-dispatch. Ugly methods _zero_rate are not exported.
zero_rate(curve::AbstractIRCurve, maturity::Date) = _zero_rate(curve_get_method(curve), curve, maturity)
zero_rate(curve::AbstractIRCurve, maturity_vec::Vector{Date}) = _zero_rate(curve_get_method(curve), curve, maturity_vec)

include("methods.jl") # implements various _zero_rate() methods for each CurveMethod

end # module InterestRates


using StatsBase
using RDatasets
using Distributions
a = rand(4)
ordinalrank(a)
a = rand([1:5],30)
counts(a)
proportions(a,1:3)
countmap(a)
proportionmap(a)


# # Implementing log/exponent transformations
# The CoordinateTransformations library provides a flexible and extensible framework
# for constructing coordinate transformations.

using CoordinateTransformations

struct LogTransformation{T <: Real} <: CoordinateTransformations.Transformation
    base
end

struct ExponentialTransformation{T <: Real} <: CoordinateTransformations.Transformation
    base::T
end

const ExpTransformation = ExponentialTransformation

function LogTransformation(; base::T = ℯ) where T
    return LogTransformation(base)
end

function ExponentialTransformation(; base::T = ℯ) where T
    return ExponentialTransformation(base)
end

(lt::LogTransformation)(x) = log.(lt.base, x)
(pt::ExponentialTransformation)(x) = lt.base .^ x

Base.inv(lt::LogTransformation) = ExponentialTransformation(lt.base)
Base.inv(pt::ExponentialTransformation) = LogTransformation(pt.base)

compose(lt::LogTransformation, pt::ExponentialTransformation) = lt.base == pt.base ? IdentityTransformation() : ComposedTransformation(lt, pt)
compose(pt::ExponentialTransformationlt::LogTransformation) = lt.base == pt.base ? IdentityTransformation() : ComposedTransformation(pt, lt)


#####
# Binning error analysis
#####

"""
    binning_error(X[; binsize=0, warnings=false])

Calculates statistical error (eff. standard deviation) for correlated data.
How: Binning of data and assuming statistical independence of bins
(i.e. R plateau has been reached). (Eq. 3.18 basically)

The default `binsize=0` indicates automatic binning.
"""
function binning_error(X::AbstractVector{T}; binsize=0, warnings=false) where T<:Real
    # Data: real numbers
    if binsize == 0
        binsize = floor(Int, length(X)/32)
        binsize = binsize==0?1:binsize
    end

    isinteger(length(X) / binsize) || !warnings ||
        warn("Non-integer number of bins $(length(X) / binsize). " *
             "Last bin will be smaller than all others.")

    bin_means = map(mean, Iterators.partition(X, binsize))
    return sqrt(1/length(bin_means) * var(bin_means))
end
function binning_error(X::AbstractVector{T}; binsize=0, warnings=false) where T<:Complex
    # Data: complex numbers
    sqrt(binning_error(real(X), binsize=binsize, warnings=warnings)^2 +
        binning_error(imag(X), binsize=binsize, warnings=warnings)^2)
end

# Data: arrays
function binning_error(X::AbstractArray{T}; binsize=0, warnings=false) where T<:Number
    ndimsX = ndims(X)
    mapslices(y->binning_error(y; binsize=binsize, warnings=warnings), X, ndimsX)[(Colon() for _ in 1:ndimsX-1)...,1]
end
function binning_error(X::AbstractVector{T}; binsize=0, warnings=false) where T<:(AbstractArray{S} where S)
    binning_error(cat(ndims(X[1])+1, X...); binsize=binsize, warnings=warnings)
end

#####
# Useful plots
#####
"""
Plots the binning error coefficient function `R(binsize)` for multiple bin sizes.
Ideally, this plot shows a plateau (Fig. 3.3), indicating that the bin averages have become independent.
For correlated data `R>≈1` and `sqrt(R)` quantifies how much one would have underestimated
the one-sigma errorbar.

Returns bss and R from `R_function(X)`.
"""
function plot_binning_R(X::Vector{T}; min_nbins=32, figsize=(4,3)) where T<:Real

    try PyPlot
    catch
        warn("PyPlot not loaded. Loading it now...")
        eval(Expr(:toplevel, Expr(:using, Symbol("PyPlot"))))
    end

    bss, R = R_function(X, min_nbins=min_nbins)
    figure(figsize=figsize)
    plot(bss, R, "m.-")
    xlabel("bin size")
    ylabel("R");

    return bss, R
end

"""
Plot histogram of X with mean and statistical errorbars (including correlation effects)
and autocorrelation function C(t).

Optional keyword `error` can be "tau_binning", "tau_integrated", or "tau_fitting", "binning",
and sets method for error estimation.
"""
function plot_error(X::Vector{T}; binsize=0, histbins=50, error="binning", figsize=(10,4), digits=3) where T<:Real

    try PyPlot
    catch
        warn("PyPlot not loaded. Loading it now...")
        eval(Expr(:toplevel, Expr(:using, Symbol("PyPlot"))))
    end

    fig, ax = subplots(1, 2, figsize=figsize)

    Xmean = mean(X)
    Xstd = std(X)

    if error == "binning"
        err = binning_error(X, binsize=binsize, warnings=false)
    elseif error == "tau_binning"
        err = tau_binning_error(X, binsize=binsize)
    elseif error == "tau_integrated"
        err = tau_integrated_error(X)
    elseif error == "tau_fitting"
        err = tau_fitting_error(X)
    end

    ax[1][:hist](X, histbins, color="gray", alpha=.5, normed=1)
    ax[1][:set_ylabel]("\$ P \$")
    ax[1][:set_xlabel]("\$ X \$")
    ax[1][:set_yticks]([])
    ax[1][:axvline](Xmean, color="black", label="\$ $(round.(Xmean, digits)) \$", linewidth=2.0)

    ax[1][:axvline](Xmean+err, color="r", label="\$ \\pm $(round.(err, digits)) \$ (binning σ)", linewidth=2.0)
    ax[1][:axvline](Xmean-err, color="r", linewidth=2.0)

    ax[1][:axvline](Xmean+2*err, color="r", alpha=.3, label="\$ \\pm $(round.(2*err, digits)) \$ (binning 2σ)", linewidth=2.0)
    ax[1][:axvline](Xmean-2*err, color="r", alpha=.3, linewidth=2.0)

    ax[1][:axvline](Xmean+Xstd, color="g", label="\$ \\pm $(round.(Xstd, digits)) \$ (std)", linewidth=2.0)
    ax[1][:axvline](Xmean-Xstd, color="g", linewidth=2.0)

    ax[1][:legend](frameon=false, loc="best")

    ax[2][:plot](autocor(X), "-", color="k", linewidth=2.0)
    ax[2][:set_xlabel]("\$ t \$")
    ax[2][:set_ylabel]("\$ C(t) \$")

    tight_layout()
    return ax
end

#####
# Calculation of error coefficient (function) R. (Ch. 3.4 in QMC book)
#####
"""
Groups datapoints in bins of varying size `bs`.
Returns the used binsizes `bss` and the error coefficient function `R(bss)` (Eq. 3.20) which should feature
a plateau, i.e. `R(bs_p) ~ R(bs)` for `bs >= bs_p`. (Fig. 3.3)

Optional keyword `min_nbins`. Only bin sizes used that lead to at least `min_nbins` bins.
"""
function R_function(X::Vector{T}; min_nbins=10) where T<:Real
    bss = Int[]
    N = length(X)
    Xmean = mean(X)
    Xvar = var(X)

    for bs in 1:N
        N%bs == 0 ? push!(bss, bs) : nothing
    end

    n_bins = Int.(N./bss)
    bss = bss[n_bins .>= min_nbins] # at least 500 bins for every binsize

    R = zeros(length(bss))
    for (i, bs) in enumerate(bss)

        blockmeans = vec(mean(reshape(X, (bs,n_bins[i])), 1))

        blocksigma2 = 1/(n_bins[i]-1)*sum((blockmeans - Xmean).^2)

        R[i] = bs * blocksigma2 / Xvar
    end

    return bss, R
end

"""
Groups datapoints in bins of fixed binsize and returns error coefficient R. (Eq. 3.20)
"""
function R_value(X::Vector{T}, binsize::Int) where T<:Real
    N = length(X)
    n_bins = div(N,binsize)
    lastbs = rem(N,binsize)
    lastbs == 0 || (lastbs >= binsize/2 || warn("Binsize leads to last bin having less than binsize/2 elements."))

    blockmeans = vec(mean(reshape(X[1:n_bins*binsize], (binsize,n_bins)), 1))
    if lastbs != 0
        vcat(blockmeans, mean(X[n_bins*binsize+1:end]))
        n_bins += 1
    end

    blocksigma2 = 1/(n_bins-1)*sum((blockmeans - mean(X)).^2)
    return binsize * blocksigma2 / var(X)
end


function pixelate(s::Simulation, dim::Int64, sparsity_level::T, cushion::T) where T<:Real
	nsteps = s.steptotal[1]
	# either add percent occupation, make new function for it
	im = zeros(dim,dim)
	# ascertain limits of distribution - 4 sigma away!
	localmin = -4*sparsity_level
	localmax = 4*sparsity_level
	# create new bounds for scaling purposes
	globmin = localmin*(1+cushion)
	globmax = localmax*(1+cushion)
	# normalize and scale cell origins to pixel locations
	livingcellIDs = []
	for i in 1:length(s.cells)
		if s.cells[i].d < nsteps
			nothing
		else
			push!(livingcellIDs,i)
		end
	end
	o = zeros(T,length(livingcellIDs),2)
	for i in 1:length(livingcellIDs)
		o[i,1] = s.cells[livingcellIDs[i]].p[1] - globmin
		o[i,2] = s.cells[livingcellIDs[i]].p[2] - globmin
	end
	o .*= (dim / (globmax - globmin))
	# normalize radius to image
	r_im = (1/2.33) * (dim / (localmax-localmin))
	# check in terminal that the radius is reasonable
	#println(r_im)

	for x in 1:length(im[1,:])
		for y in 1:length(im[:,1])
			for i in 1:length(o[:,1])
				h = o[i,1]
				k = o[i,2]
				if ((x-h)^2 + (y-k)^2) < (r_im^2)
					im[y,x] = 1
				else
					nothing
				end
			end
		end
	end
	return im
end


export prox_grad!, ProxGradParams

# solves minimize(f(x) + g(x))
# where f is differentiable and the prox of g is easy to evaluate
# prox_g(x, alpha) solves minimize(g(z) + 1/(2*alpha)||x-z||^2
function prox_grad!(x, # the initial value of the variable
	f::Function,
	grad_f::Function,
	g::Function,
	prox_g::Function,
	params::OptParams = SimpleParams(ConstantStepSize(1), 100);
	verbose::Bool = false)

	# initialize
	objective = x -> f(x) + g(x)
	objval = objective(x)
	take_step(x, stepsize) = prox_g(x - stepsize*grad_f(x), stepsize)

	if verbose
		@printf("%10s%12s\n", "iter", "obj")
		@printf("%10d%12.4e\n", 0, objval)
	end

	for iter = 1:params.maxiters
		oldobjval = copy(objval)
		x, objval = step(params.stepsizerule,
			             objective,
			             take_step,
			             x,
			             objval0 = objval)

		verbose && iter%1==0 && @printf("%10d%12.4e\n", iter, objval)

		if stop(params, objval, oldobjval)
			verbose && println("stopping criterion satisfied")
			break
		end
	end
	return x
end

type ProxGradParams<:OptParams
	stepsizerule::StepSizeRule
	maxiters::Int
end
ProxGradParams() = ProxGradParams(ConstantStepSize(.5), 100)

function step(s::HopefulStepSize, objective::Function, take_step::Function, x0;
	objval0 = objective(x0))
	stepsize = s.initial_stepsize
	x = take_step(x0, stepsize)
	objval = objective(x)
	while objval >= objval0
		@show stepsize *= s.decrease_by
		x = take_step(x0, stepsize)
		objval = objective(x)
	end
	# hope and change!
	if stepsize < s.initial_stepsize
		s.initial_stepsize = stepsize
	else
		s.initial_stepsize *= s.increase_by
	end
	return x, objval
end


using MPI, Knet, ParticleSwarmOptimization
include("mnist.jl")
include("Args.jl")

function minibatch(x, y, batchsize)
    data = Any[]
    for i=1:batchsize:size(x,2)-batchsize+1
        j=i+batchsize-1
        push!(data, (x[:,i:j], y[:,i:j]))
    end
    return data
end

@knet function mnist2layer(x; winit=Gaussian(0,.1))
    h = wbf(x; out=64, f=:relu, winit=winit)
    return wbf(h; out=10, f=:soft, winit=winit)
end

function test(f, data, loss)
    sumloss = numloss = 0
    for (x,ygold) in data
        ypred = forw(f, x)
        sumloss += loss(ypred, ygold)
        numloss += 1
    end
    sumloss / numloss
end

function main()
    MPI.Init()
    args = parse_commandline()
    gpu(args["gpu"])
    comm = MPI.COMM_WORLD
    rank = MPI.Comm_rank(comm)
    world = MPI.Comm_size(comm)

    if rank == 0
        println("Parsed args:")

        for (arg,val) in args
            println("  $arg  =>  $val")
        end
    end

    dtrn = minibatch(identity(MNIST.xtrn), identity(MNIST.ytrn), args["batchsize"])
    dtst = minibatch(identity(MNIST.xtst), identity(MNIST.ytst), args["batchsize"])

    problem = NNProblem(:mnist2layer, args["l"], args["h"], MiniBatch(dtrn), softloss)

    #last parameter for the parallelization
    gbest = pso(problem, args["num"], args["epoch"], args["phi1"], args["phi2"], args["w"], args["vmin"], args["vmax"], true)

    if rank == 0
        println("\nBest: $(gbest.pbest_fitness)\n")
        println("Acc: $(1 - test(gbest.x, dtst, zeroone))")
    end

    MPI.Finalize()
end

main()


@testset "Utilities" begin

    @testset "`solve!` does not return anything" begin
        x = Variable()
        p = satisfy(x >= 0)
        output = solve!(p, SCS.Optimizer(verbose=0, eps=1e-6))
        @test output === nothing
    end

    @testset "Show" begin
        x = Variable()
        @test sprint(show, x) == """
        Variable
        size: (1, 1)
        sign: real
        vexity: affine
        $(Convex.show_id(x))"""
        fix!(x, 1.0)
        @test sprint(show, x) == """
        Variable
        size: (1, 1)
        sign: real
        vexity: constant
        $(Convex.show_id(x))
        value: 1.0"""

        @test sprint(show, 2*x) == """
        * (constant; real)
        ├─ 2
        └─ real variable (fixed) ($(Convex.show_id(x)))"""

        free!(x)
        p = maximize( log(x), x >= 1, x <= 3 )

        @test sprint(show, p) == """
        maximize
        └─ log (concave; real)
           └─ real variable ($(Convex.show_id(x)))
        subject to
        ├─ >= constraint (affine)
        │  ├─ real variable ($(Convex.show_id(x)))
        │  └─ 1
        └─ <= constraint (affine)
           ├─ real variable ($(Convex.show_id(x)))
           └─ 3

        status: `solve!` not called yet"""

        x = ComplexVariable(2,3)
        @test sprint(show, x) == """
        Variable
        size: (2, 3)
        sign: complex
        vexity: affine
        $(Convex.show_id(x))"""

        # test `MAXDEPTH`
        # We construct a binary tree of depth >= 3
        # to make sure it gets truncated appropriately.
        x = Variable(2)
        y = Variable(2)
        level3 = hcat(x,y)
        level2 = hcat(level3, level3)
        root = hcat(level2, level2)
        p = minimize(sum(x), root == root)
        @test sprint(show, p) == """
        minimize
        └─ sum (affine; real)
           └─ 2-element real variable ($(Convex.show_id(x)))
        subject to
        └─ == constraint (affine)
           ├─ hcat (affine; real)
           │  ├─ hcat (affine; real)
           │  │  ├─ …
           │  │  └─ …
           │  └─ hcat (affine; real)
           │     ├─ …
           │     └─ …
           └─ hcat (affine; real)
              ├─ hcat (affine; real)
              │  ├─ …
              │  └─ …
              └─ hcat (affine; real)
                 ├─ …
                 └─ …

        status: `solve!` not called yet"""

        # test `MAXWIDTH`
        x = Variable()
        p = satisfy([ x == i for i = 1:100])
        old_maxwidth = Convex.MAXWIDTH[]
        Convex.MAXWIDTH[] = 2
        @test sprint(show, p) == """
            minimize
            └─ 0
            subject to
            ├─ == constraint (affine)
            │  ├─ real variable ($(Convex.show_id(x)))
            │  └─ 1
            ├─ == constraint (affine)
            │  ├─ real variable ($(Convex.show_id(x)))
            │  └─ 2
            ⋮

            status: `solve!` not called yet"""
        Convex.MAXWIDTH[] = old_maxwidth

        # solved problem
        x = Variable()
        p = satisfy(x >= 0)
        output = solve!(p, SCS.Optimizer(verbose=0, eps=1e-6))
        @test sprint(show, p) == """
                minimize
                └─ 0
                subject to
                └─ >= constraint (affine)
                   ├─ real variable ($(Convex.show_id(x)))
                   └─ 0

                termination status: OPTIMAL
                primal status: FEASIBLE_POINT
                dual status: FEASIBLE_POINT"""
    end

    @testset "clearmemory" begin
        @test_deprecated Convex.clearmemory()
    end

    @testset "ConicObj with type $T" for T = [UInt32, UInt64]
        c = ConicObj()
        z = zero(T)
        @test !haskey(c, z)
        c[z] = (1, 1)
        @test c[z] == (1, 1)
        x = T[]
        for (k, v) in c
            push!(x, k)
        end
        @test x == collect(keys(c))
        d = copy(c)
        @test d !== c
    end

    @testset "length and size" begin
        x = Variable(2,3)
        @test length(x) == 6
        @test size(x) == (2, 3)
        @test size(x, 1) == 2
        @test size(x, 2) == 3

        x = Variable(3)
        @test length(x) == 3
        @test size(x) == (3, 1)

        x = Variable()
        @test length(x) == 1
        @test size(x) == (1, 1)
    end

    @testset "lastindex and axes" begin
        x = Variable(2, 3)
        @test axes(x) == (Base.OneTo(2), Base.OneTo(3))
        @test axes(x, 1) == Base.OneTo(2)
        @test lastindex(x) == 6
        @test lastindex(x, 2) == 3
        y = x[:,end]
        @test y isa AbstractExpr
        @test size(y) == (2, 1)
    end

    @testset "Parametric constants" begin
        z = Constant([1.0 0.0im; 0.0 1.0])
        @test z isa Constant{Matrix{Complex{Float64}}}

        # Helper functions
        @test Convex.ispos(1)
        @test Convex.ispos(0)
        @test !Convex.ispos(-1)
        @test Convex.ispos([0,1,0])
        @test !Convex.ispos([0,-1,0])
        @test Convex.isneg(-1)
        @test Convex.isneg(0)
        @test !Convex.isneg(1)
        @test Convex.isneg([0,-1,0])
        @test !Convex.isneg([0,1,0])
        @test Convex._size(3) == (1, 1)
        @test Convex._sign(3) == Positive()
        @test Convex._size([-1,1,1]) == (3, 1)
        @test Convex._sign([-1,1,1]) == NoSign()
        @test Convex._sign([-1,-1,-1]) == Negative()
        @test Convex._size([0 0; 0 0]) == (2, 2)
        @test Convex._sign([0 0; 0 0]) == Positive()
        @test Convex._size(0+1im) == (1, 1)
        @test Convex._sign(0+1im) == ComplexSign()

        @test Convex.imag_conic_form(Constant(1.0)) == [0.0]
        @test Convex.imag_conic_form(Constant([1.0, 2.0])) == [0.0, 0.0]
    end

    @testset "Base.vect" begin
    # Issue #223: ensure we can make vectors of variables
    @test size([Variable(2), Variable(3,4)]) == (2,)
    end

    @testset "Iteration" begin
        x = Variable(2,3)
        s = sum([xi for xi in x])
        x.value = [1 2 3; 4 5 6]
        # evaluate(s) == [21] (which might be wrong? expected 21)
        # but [21][1] === 21[1] === 21
        # so this should pass even after "fixing" that
        @test evaluate(s)[1] == 21

        x = Variable(4)
        @test [xi.inds for xi in x] == [1:1, 2:2, 3:3, 4:4]

        x = Variable(0)
        @test [xi for xi in x] == []
        @test iterate(x) == nothing
    end
    # returns [21]; not sure why
    # context("iteration") do
    #     x = Variable(2,3)
    #     s = sum([xi for xi in x])
    #     x.value = [1 2 3; 4 5 6]
    #     @fact evaluate(s) --> 21
    # end
end


module TravelingSalesmanHeuristics

using Random, LinearAlgebra

include("helpers.jl")
include("simulated_annealing.jl")
include("lowerbounds.jl")

export solve_tsp, lowerbound, repetitive_heuristic, two_opt,
       nearest_neighbor, cheapest_insertion, simulated_annealing, farthest_insertion
"""
    solve_tsp(distmat; quality_factor = 40)

Approximately solve a TSP by specifying a distance matrix. Return a tuple `(path, cost)`.

The optional keyword `quality_factor` (real number in [0,100]; defaults to 40) specifies the
tradeoff between computation time and quality of solution returned. Higher values
tend to lead to better solutions found at the cost of more computation time.

!!! note
    It is not guaranteed that a call with a high `quality_factor` will always run slower or
    return a better solution than a call with a lower `quality_factor`, though this is
    typically the case. A `quality_factor` of 100 neither guarantees an optimal solution
    nor the best solution  that can be found via extensive use of the methods in this package.

See also: individual heuristic methods such as [`farthest_insertion`](@ref)
"""
function solve_tsp(distmat::AbstractMatrix{T}; quality_factor::Real = 40.0) where {T<:Real}
    if quality_factor < 0 || quality_factor > 100
        @warn "quality_factor keyword passed to solve_tsp must be in [0,100]"
        quality_factor = clamp(quality_factor, 0, 100)
    end

    lowest_threshold = 5

    # begin adding heuristics as dictated by the quality_factor,
    # starting with the very fastest
    answers = Vector{Tuple{Vector{Int}, T}}()
    push!(answers, farthest_insertion(distmat; do2opt = false))
    if quality_factor < lowest_threshold # fastest heuristic and out
        return answers[1]
    end
    # otherwise, we'll try several heuristics and return the best

    # add any nearest neighbor heuristics as dictated by quality_factor
    if quality_factor >= 60 # repetitive-NN, 2-opt on each iter
        push!(answers, repetitive_heuristic(distmat, nearest_neighbor; do2opt = true))
    elseif quality_factor >= 25 # repetitive-NN, 2-opt on final
        rnnpath, _ = repetitive_heuristic(distmat, nearest_neighbor; do2opt = false)
        push!(answers, two_opt(distmat, rnnpath))
    elseif quality_factor >= 15 # NN w/ 2-opt
        push!(answers, nearest_neighbor(distmat; do2opt = true))
    end

    # farthest insertions as needed
    if quality_factor >= 70 # repetitive-FI, 2-opt on each
        push!(answers, repetitive_heuristic(distmat, farthest_insertion; do2opt = true))
    elseif quality_factor >= 5 # FI w/ 2-opt
        push!(answers, farthest_insertion(distmat; do2opt = true))
    end

    # cheapest insertions
    if quality_factor >= 90 # repetitive-CI w/ 2-opt on each
        push!(answers, repetitive_heuristic(distmat, cheapest_insertion; do2opt = true))
    elseif quality_factor >= 35
        push!(answers, cheapest_insertion(distmat; do2opt = true))
    end

    # simulated annealing refinement, seeded with best so far
    if quality_factor >= 80
        _, bestind = findmin([pc[2] for pc in answers])
        bestpath, bestcost = answers[bestind]
        nstart = ceil(Int, (quality_factor - 79)/5)
        push!(answers,
              simulated_annealing(distmat; num_starts = nstart, init_path = bestpath)
        )
    end

    # pick best
    _, bestind = findmin([pc[2] for pc in answers])
    return answers[bestind]
end

###
# path generation heuristics
###

"""
    nearest_neighbor(distmat)

Approximately solve a TSP using the nearest neighbor heuristic. `distmat` is a square real matrix
where distmat[i,j] represents the distance from city `i` to city `j`. The matrix needn't be
symmetric and can contain negative values. Return a tuple `(path, pathcost)`.

# Optional keyword arguments:
- `firstcity::Union{Int, Nothing}`: specifies the city to begin the path on. Passing `nothing` or
    not specifying a value corresponds to random selection.
- `closepath::Bool = true`: whether to include the arc from the last city visited back to the
    first city in cost calculations. If true, the first city appears first and last in the path.
- `do2opt::Bool = true`: whether to refine the path found by 2-opt switches (corresponds to
    removing path crossings in the planar Euclidean case).
"""
function nearest_neighbor(distmat::AbstractMatrix{T} where {T<:Real};
                          firstcity::Union{Int, Nothing} = rand(1:size(distmat, 1)),
                          repetitive::Bool = false,
                          closepath::Bool = true,
                          do2opt::Bool = true)
    # must have a square matrix
    num_cities = check_square(distmat, "Must pass a square distance matrix to nearest_neighbor")

    # extract a safe int value for firstcity
    if firstcity == nothing # random first city
        firstcityint = rand(1:num_cities)
    else # an int was passed
        firstcityint = firstcity
        if !(1 <= firstcityint <= num_cities)
            error("firstcity of $(firstcity) passed to `nearest_neighbor`" *
                  " out of range [1, $(num_cities)]")
        end
    end

    # calling with KW repetitive is deprecated; pass the call to repetitive_heuristic
    if repetitive
        @warn ("Calling `nearest_neighbor` with keyword `repetitive` is deprecated;'" *
             " instead call `repetitive_heuristic(distmat, nearest_neighbor; kwargs...)`")
        return repetitive_heuristic(distmat, nearest_neighbor;
                                    closepath = closepath, do2opt = do2opt)
    end

    # put first city on path
    path = Vector{Int}()
    push!(path, firstcityint)

    # cities to visit
    citiesToVisit = collect(1:(firstcityint - 1))
    append!(citiesToVisit, collect((firstcityint + 1):num_cities))

    # nearest neighbor loop
    while !isempty(citiesToVisit)
        curCity = path[end]
        dists = distmat[curCity, citiesToVisit]
        _, nextInd = findmin(dists)
        nextCity = citiesToVisit[nextInd]
        push!(path, nextCity)
        deleteat!(citiesToVisit, nextInd)
    end

    # complete cycle? (duplicates first city)
    if closepath
        push!(path, firstcityint)
    end

    # do swaps?
    if do2opt
        path, _ = two_opt(distmat, path)
    end

    return (path, pathcost(distmat, path))
end

"""
    cheapest_insertion(distmat::Matrix, initpath::AbstractArray{Int})

Given a distance matrix and an initial path, complete the tour by repeatedly doing the cheapest
insertion. Return a tuple `(path, cost)`. The initial path must have length at least 2, but can
be simply `[i, i]` for some city index `i` which corresponds to starting with a loop at city `i`.

!!! note
    Insertions are always in the interior of the current path so this heuristic can also be used for
    non-closed TSP paths.

Currently the implementation is a naive ``n^3`` algorithm.
"""
function cheapest_insertion(distmat::AbstractMatrix{T}, initpath::AbstractVector{S}) where {T<:Real, S<:Integer}
    check_square(distmat, "Distance matrix passed to cheapest_insertion must be square.")

    n = size(distmat, 1)
    path = Vector{Int}(initpath)

    # collect cities to visited
    visitus = setdiff(collect(1:n), initpath)

    while !isempty(visitus)
        bestCost = Inf
        bestInsertion = (-1, -1)
        for k in visitus
            for after in 1:(length(path) - 1) # can't insert after end of path
                c = inscost(k, after, path, distmat)
                if c < bestCost
                    bestCost = c
                    bestInsertion = (k, after)
                end
            end
        end
        # bestInsertion now holds (k, after)
        # insert into path, remove from to-do list
        k, after = bestInsertion
        insert!(path, after + 1, k)
        visitus = setdiff(visitus, k)
    end

    return (path, pathcost(distmat, path))
end
"""
    cheapest_insertion(distmat; ...)

Complete a tour using cheapest insertion with a single-city loop as the initial path. Return a
tuple `(path, cost)`.

### Optional keyword arguments:
- `firstcity::Union{Int, Nothing}`: specifies the city to begin the path on. Passing `nothing` or
    not specifying a value corresponds to random selection.
- `do2opt::Bool = true`: whether to improve the path found by 2-opt swaps.
"""
function cheapest_insertion(distmat::AbstractMatrix{T} where{T<:Real};
                            firstcity::Union{Int, Nothing} = rand(1:size(distmat, 1)),
                            repetitive::Bool = false,
                            do2opt::Bool = true)
    #infer size
    num_cities = size(distmat, 1)

    # calling with KW repetitive is deprecated; pass the call to repetitive_heuristic
    if repetitive
        @warn "Calling `cheapest_insertionr` with keyword `repetitive` is deprecated;'" *
             " instead call `repetitive_heuristic(distmat, cheapest_insertion; kwargs...)`"
        return repetitive_heuristic(distmat, cheapest_insertion; do2opt = do2opt)
    end

    # extract a safe int value for firstcity
    if firstcity == nothing # random first city
        firstcityint = rand(1:num_cities)
    else # an int was passed
        firstcityint = firstcity
        if !(1 <= firstcityint <= num_cities)
            error("firstcity of $(firstcity) passed to `cheapest_insertion`" *
                  " out of range [1, $(num_cities)]")
        end
    end

    # okay, we're not repetitive, put a loop on the first city
    path, cost = cheapest_insertion(distmat, [firstcityint, firstcityint])

    # user may have asked for 2-opt refinement
    if do2opt
        path, cost = two_opt(distmat, path)
    end

    return path, cost
end

"""
    farthest_insertion(distmat; ...)

Generate a TSP path using the farthest insertion strategy. `distmat` must be a square real matrix.
Return a tuple `(path, cost)`.

### Optional arguments:
- `firstCity::Int`: specifies the city to begin the path on. Not specifying a value corresponds
    to random selection.
- `do2opt::Bool = true`: whether to improve the path by 2-opt swaps.
"""
function farthest_insertion(distmat::AbstractMatrix{T};
                            firstcity::Int = rand(1:size(distmat, 1)),
                            do2opt::Bool = true) where {T<:Real}
    n = check_square(distmat, "Must pass square distance matrix to farthest_insertion.")
    if firstcity < 1 || firstcity > n
        error("First city for farthest_insertion must be in [1,...,n]")
    end
    smallval = minimum(distmat) - one(T) # will never be the max

    path = Int[firstcity, firstcity]
    sizehint!(path, n + 1)
    dists_to_tour = (distmat[firstcity, :] + distmat[:, firstcity]) / 2
    dists_to_tour[firstcity] = smallval

    while length(path) < n + 1
        # city farthest from tour
        _, nextcity = findmax(dists_to_tour)

        # find where to add it
        bestcost = inscost(nextcity, 1, path, distmat)
        bestind = 1
        for ind in 2:(length(path) - 1)
            altcost = inscost(nextcity, ind, path, distmat)
            if altcost < bestcost
                bestcost = altcost
                bestind = ind
            end
        end
        # and add the next city at the best spot found
        insert!(path, bestind + 1, nextcity) # +1 since arg to insert! is where nextcity ends up

        # update distances to tour
        dists_to_tour[nextcity] = smallval
        for i in 1:n
            c = dists_to_tour[i]
            if c == zero(T) # i already in tour
                continue
            end
            altc = (distmat[i, nextcity] + distmat[nextcity, i]) / 2 # cost i--nextcity
            if altc < c
                @inbounds dists_to_tour[i] = altc
            end
        end
    end

    if do2opt
        path, _ = two_opt(distmat, path)
    end

    return path, pathcost(distmat, path)
end

###
# path improvement heuristics
###

"""
    two_opt(distmat, path)

Improve `path` by doing 2-opt switches (i.e. reversing part of the path) until doing so no
longer reduces the cost. Return a tuple `(improved_path, improved_cost)`.

On large problem instances this heuristic can be slow, but it is highly recommended on small and
medium problem instances.

See also [`simulated_annealing`](@ref) for another path generation heuristic.
"""
function two_opt(distmat::AbstractMatrix{T}, path::AbstractVector{S}) where {T<:Real, S<:Integer}
    # size checks
    n = length(path)
    if size(distmat, 1) != size(distmat, 2)
        error("Distance matrix passed to two_opt must be square.")
    end

    # don't modify input
    path = copy(path) # Int is a bitstype

    # main loop
    # check every possible switch until no 2-swaps reduce objective
    # if the path passed in is a loop (first/last nodes are the same)
    # then we must keep these the endpoints of the path the same
    # ie just keep it a loop, and therefore it doesn't matter which node is at the end
    # if the path is not a cycle, we should respect the endpoints
    switchLow = 2
    switchHigh = n - 1
    prevCost = Inf
    curCost = pathcost(distmat, path)
    while prevCost > pathcost(distmat, path)
        prevCost = curCost
        # we can't change the first
        for i in switchLow:(switchHigh-1)
            for j in switchHigh:-1:(i+1)
                altCost = pathcost_rev(distmat, path, i, j)
                if altCost < curCost
                    curCost = altCost
                    reverse!(path, i, j)
                end
            end
        end
    end
    return path, pathcost(distmat, path)
end

end # module


"A non-allocating versin of norm(x::SVector, Inf)"
@inline infnorm(x::StaticVector) = maximum(abs.(x))


#=
1. Create set of N triangles T such that
	(a) they all meet in a common vertex v.
	(b) the edges E opposite to v are parallel to each other with uniform distance between
	pairs of triangles.
	(c) the length of each edge e in E equals the largest distance between to pairs of edges 		in E.
2. Create a copy T′ of T.
3. Rotate T′ around the axis through v and the center of the edges E by 90 degrees.
4. Measure time for T ∪ T′.
=#

using ViewerGL; GL = ViewerGL
using LinearAlgebraicRepresentation
Lar = LinearAlgebraicRepresentation;


struct BoxmapTile
    x::Float64
    y::Float64
    w::Float64
    h::Float64
end

"""
    buildrow(A, x, y, w, h)

Make a row of tiles from A.
"""
function buildrow(A, x, y, w, h)
    covered = sum(A)
    width = covered/h
    tiles = BoxmapTile[]
    for value in A
        push!(tiles, BoxmapTile(x, y, width, value/width))
        y += value/width
    end
    return tiles
end

"""
    buildcolumn(A, x, y, w, h)

Make a column of tiles from A.
"""
function buildcolumn(A, x, y, w, h)
    covered = sum(A)
    height = covered/w
    tiles = BoxmapTile[]
    for value in A
        push!(tiles, BoxmapTile(x, y, value/height, height))
        x += value/height
    end
    return tiles
end

"""
    layout(A, x, y, w, h)

From A, make a row of tiles (if wider than tall) or a column of tiles (if taller than wide).
"""
function layout(A, x, y, w, h)
    if w >= h
        return buildrow(A, x, y, w, h)
    else
        return buildcolumn(A, x, y, w, h)
    end
end

function rowlayoutleftover(A, x, y, w, h)
    covered = sum(A)
    width = covered/h
    layoutleftoverx = x + width
    layoutleftoverw = w - width
    return layoutleftoverx, y, layoutleftoverw, h
end

function columnlayoutleftover(A, x, y, w, h)
    covered = sum(A)
    height = covered/w
    layoutleftovery = y + height
    layoutleftoverh = h - height
    return x, layoutleftovery, w, layoutleftoverh
end

function layoutleftover(A, x, y, w, h)
    if w >= h
        return rowlayoutleftover(A, x, y, w, h)
    else
        return columnlayoutleftover(A, x, y, w, h)
    end
end

"""
    highestaspectratio()

Find the highest aspect ratio of a list of rectangles, given the length
of the side along which they are to be laid out.
"""
function highestaspectratio(A, x, y, w, h)
    return maximum([max(tile.w/tile.h, tile.h/tile.w) for tile in layout(A, x, y, w, h)])
end

function buildboxmap(A, x, y, w, h)
    length(A) == 0 && return []
    length(A) == 1 && return layout(A, x, y, w, h)
    i = 1
    while i < length(A) &&
        (highestaspectratio(A[1:i], x, y, w, h) >= highestaspectratio(A[1:i+1], x, y, w, h))
        i += 1
    end
    layoutleftoverx, layoutleftovery, layoutleftoverw, layoutleftoverh = layoutleftover(A[1:i], x, y, w, h)
    return append!(layout(A[1:i], x, y, w, h), buildboxmap(A[i+1:end], layoutleftoverx, layoutleftovery, layoutleftoverw, layoutleftoverh))
end

"""
    boxmap(A::Array, pt, w, h)

Build a box map of the values in `A` with one corner at `pt` and width `w` and
height `h`. There are `length(A)` boxes. The areas of the boxes are proportional
to the original values, scaled as necessary.

The return value is an array of BoxmapTiles. For example:

```
[BoxmapTile(0.0, 0.0, 10.0, 20.0)
 BoxmapTile(10.0, 0.0, 10.0, 13.3333)
 BoxmapTile(10.0, 13.3333, 10.0, 6.66667)]
```

with each tile containing `(x, y, w, h)`. `box()` and `BoundingBox()` can work with
BoxmapTiles as well.

# Example

```
using Luxor
@svg begin
    fontsize(16)
    fontface("HelveticaBold")
    pt = Point(-200, -200)
    a = rand(10:200, 15)
    tiles = boxmap(a, Point(-200, -200), 400, 400)
    for (n, t) in enumerate(tiles)
        randomhue()
        bb = BoundingBox(t)
        box(bb - 2, :stroke)
        box(bb - 5, :fill)
        sethue("white")
        text(string(n), midpoint(bb[1], bb[2]), halign=:center)
    end
end 400 400 "boxmap.svg"
```
"""
function boxmap(A::Array, pt::Point, w, h)
    # algorithm basically from Bruls, Huizing, van Wijk, "Squarified Treemaps"
    # without the silly name
    !all(n -> n > 0, A) && error("all values must be positive")
    sort!(A, rev=true)
    totalvalue = sum(A)
    totalarea = w * h
    normalizedA = A .* (totalarea/totalvalue)
    return buildboxmap(normalizedA, pt.x, pt.y, w, h)
end

"""
    box(tile::BoxmapTile, action::Symbol=:none; vertices=false)

Use a Boxmaptile to make or draw a rectangular box. Use `vertices=true` to obtain
the coordinates.

Create boxmaps using `boxmap()`.
"""
function box(tile::BoxmapTile, action::Symbol=:none; vertices=false)
    if vertices
        return [Point(tile.x,           tile.y + tile.h),
                Point(tile.x,           tile.y),
                Point(tile.x + tile.w,  tile.y),
                Point(tile.x + tile.w,  tile.y + tile.h)]
    end
    rect(tile.x, tile.y, tile.w, tile.h, action)
end

"""
    BoundingBox(tile::BoxmapTile)

Return a BoundingBox of a BoxmapTile (as created with `boxmap()`).
"""
function BoundingBox(tile::BoxmapTile)
    lcorner = Point(tile.x, tile.y)
    ocorner = Point(lcorner.x + tile.w, lcorner.y + tile.h)
    return BoundingBox(lcorner, ocorner)
end


@testset "Primitives" begin
    @test Pyramid(Point(0,0,0),1,2) == Pyramid{Int}(Point{3,Int}((0,0,0)),1,2)
    @test Particle(Point(0,0,0), Vec(1,1,1)) == Particle{3,Int}(Point{3,Int}((0,0,0)),Vec{3,Int}((1,1,1)))
    @test HyperCube(Vec(0,0,0), 1) == HyperCube{3,Int}(Vec{3,Int}(0,0,0), 1)

    centered_rect = centered(HyperCube)
    @test centered_rect == HyperCube{3,Float32}(Vec3f0(-0.5), 1f0)
    centered_rect = centered(HyperCube{2})
    @test centered_rect == HyperCube{2,Float32}(Vec2f0(-0.5f0), 1f0)

    centered_rect = centered(HyperCube{2, Float64})
    @test centered_rect == HyperCube{2,Float64}(Vec(-0.5,-0.5), 1.)

    centered_rect = centered(HyperCube{3, Float32})
    @test centered_rect == HyperCube{3,Float32}(Vec3f0(-0.5), 1f0)

end

@testset "Simplex" begin
    s = Simplex(Point(1,2,3))
    @test s == Simplex{1,Point{3,Int}}((Point{3,Int}((1,2,3)),))
    s = Simplex(Point(1,2), Point(2,3))
    @test s == Simplex{2,Point{2,Int}}((Point{2,Int}((1,2)),Point{2,Int}((2,3))))
end


cd(@__DIR__); include("setups/path128.jl")
gr(dpi = 200)

l = 64
𝛍 = nat_spec_filter(l, distDCT; σ = 0.05 * maximum(distDCT))
plot(0:N-1, 𝛍, legend = true, c = :blue,
     grid = false, frame = :box, marker = :circle, ms = 3, lw = 1, mswidth = 0,
     lab = latexstring("\\sigma=0.05 \\cdot d_{\\max}"))

𝛍 = nat_spec_filter(l, distDCT; σ = 0.1 * maximum(distDCT))
plot!(0:N-1, 𝛍, legend = true, c = :red,
     grid = false, frame = :box, marker = :circle, ms = 3, lw = 1, mswidth = 0,
     lab = latexstring("\\sigma=0.1 \\cdot d_{\\max}"))

𝛍 = nat_spec_filter(l, distDCT; σ = 0.2 * maximum(distDCT))
plot!(0:N-1, 𝛍, ylim = [0, 0.1], legend = true, c = :green,
     grid = false, frame = :box, marker = :circle, ms = 3, lw = 1, mswidth = 0,
     lab = latexstring("\\sigma=0.2 \\cdot d_{\\max}"), tickfontsize=10, legendfontsize = 12)
xticks!([0; 15:16:127], vcat(string("DC"), [string(k) for k in 15:16:127]))
plot!(xlab = latexstring("i"), ylab = latexstring("\\mu_{63}"), guidefontsize = 16)
plt = current()
savefig(plt, "../figs/Path128_NSGfilters_mu$(l-1).png")
display(plt)


### A Pluto.jl notebook ###
# v0.14.3

using Markdown
using InteractiveUtils

# ╔═╡ cac25d95-5750-4060-8d4b-f61466a997c7
using Pkg, DrWatson

# ╔═╡ 81902a32-f906-450e-ad49-5f5258c82f81
begin
	@quickactivate "NumericalMethodsforEngineers"
	using NumericalMethodsforEngineers, Test
end

# ╔═╡ 9bc9f18d-5fdf-48e5-a04b-c921a74e5983
md" ### `Ex_02_04.jl`"

# ╔═╡ 5bcab1ab-a649-4c3f-b821-05dea48fb924
a1 = [1. 2. -2.; 2. 5. -4.; -2. -4. 5.]

# ╔═╡ 2797ae90-10e1-4ab0-b6f9-b90c14c0f894
a1[1, 1]

# ╔═╡ a2722d3f-3fb9-4cb1-8831-451a1b00f563
a1[1:2, 1:2]

# ╔═╡ ce79e364-dd29-4f3e-afe3-a3752179d942
det(a1)

# ╔═╡ a1e3f46b-a330-4805-b18b-87e24a851ff8
cholesky(a1)

# ╔═╡ 190c25bf-83d1-4dd7-a969-ff54a1fabee2
cholesky(a1).L

# ╔═╡ dfb294be-22a7-4e98-a482-f5e195d87ccc
a2 = [16. 4. 8.; 4. 5. -4.; 8. -4. 22.]

# ╔═╡ c314cc17-e071-4cd4-83ec-da277c18adc8
det(a2[1, 1])

# ╔═╡ 69d90c44-5653-4c54-a935-0e29aa055816
det(a2[1:2, 1:2])

# ╔═╡ c5ee8e48-6523-4b21-82f1-511bacb36847
det(a2)

# ╔═╡ 4a3400e8-1277-45ce-9d1c-8c91004b6098
cholesky(a2)

# ╔═╡ dd26f467-2042-450f-b625-024c2d9ae406
upper = cholesky(a2).U

# ╔═╡ cf259396-c564-4f1b-95d5-e57eb2e45071
lower = cholesky(a2).L

# ╔═╡ 20b2309c-83e5-495f-a7e5-5352f62faf4d
lower * upper

# ╔═╡ 0ea29bc3-539c-407f-8294-4340ba6c7d8d
@test a2 == lower * upper

# ╔═╡ e068b790-c3b3-454c-ad26-d717cb3c6265
md" ### End of ex-02-04.jl"

# ╔═╡ Cell order:
# ╟─9bc9f18d-5fdf-48e5-a04b-c921a74e5983
# ╠═cac25d95-5750-4060-8d4b-f61466a997c7
# ╠═81902a32-f906-450e-ad49-5f5258c82f81
# ╠═5bcab1ab-a649-4c3f-b821-05dea48fb924
# ╠═2797ae90-10e1-4ab0-b6f9-b90c14c0f894
# ╠═a2722d3f-3fb9-4cb1-8831-451a1b00f563
# ╠═ce79e364-dd29-4f3e-afe3-a3752179d942
# ╠═a1e3f46b-a330-4805-b18b-87e24a851ff8
# ╠═190c25bf-83d1-4dd7-a969-ff54a1fabee2
# ╠═dfb294be-22a7-4e98-a482-f5e195d87ccc
# ╠═c314cc17-e071-4cd4-83ec-da277c18adc8
# ╠═69d90c44-5653-4c54-a935-0e29aa055816
# ╠═c5ee8e48-6523-4b21-82f1-511bacb36847
# ╠═4a3400e8-1277-45ce-9d1c-8c91004b6098
# ╠═dd26f467-2042-450f-b625-024c2d9ae406
# ╠═cf259396-c564-4f1b-95d5-e57eb2e45071
# ╠═20b2309c-83e5-495f-a7e5-5352f62faf4d
# ╠═0ea29bc3-539c-407f-8294-4340ba6c7d8d
# ╟─e068b790-c3b3-454c-ad26-d717cb3c6265


### A Pluto.jl notebook ###
# v0.12.17

using Markdown
using InteractiveUtils

# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
    quote
        local el = $(esc(element))
        global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing
        el
    end
end

# ╔═╡ 6d5ac8a4-47dc-11eb-3098-abac2e02369b
begin
using Plots
using PlutoUI
end

# ╔═╡ 7525b260-47dc-11eb-172e-a721065970f4
begin
	r= @bind range html"<input type=range min=5 max=15>"
	p= @bind power Slider(-2:5)

	md"""
	$(r) $(p)

	power= $(range)
	range= $(power)
	"""
end

# ╔═╡ 72e5bc98-47dc-11eb-1473-675b6bb0958c
begin
	f(x)=x.^power
	x=-range:range
	plot(x,f(x))
end

# ╔═╡ 13611b20-47e4-11eb-05c0-bb4c4d7be8d3
@bind dims html"""
<canvas width="200" height="200" style="position: relative"></canvas>

<script>
// 🐸 `currentScript` is the current script tag - we use it to select elements 🐸 //
const canvas = currentScript.closest('pluto-output').querySelector("canvas")
const ctx = canvas.getContext("2d")

var startX = 80
var startY = 40

function onmove(e){
	// 🐸 We send the value back to Julia 🐸 //
	canvas.value = [e.layerX - startX, e.layerY - startY]
	canvas.dispatchEvent(new CustomEvent("input"))

	ctx.fillStyle = '#ffecec'
	ctx.fillRect(0, 0, 200, 200)
	ctx.fillStyle = '#3f3d6d'
	ctx.fillRect(startX, startY, ...canvas.value)
}

canvas.onmousedown = e => {
	startX = e.layerX
	startY = e.layerY
	canvas.onmousemove = onmove
}

canvas.onmouseup = e => {
	canvas.onmousemove = null
}

// Fire a fake mousemoveevent to show something
onmove({layerX: 130, layerY: 160})

</script>
"""

# ╔═╡ Cell order:
# ╠═6d5ac8a4-47dc-11eb-3098-abac2e02369b
# ╠═72e5bc98-47dc-11eb-1473-675b6bb0958c
# ╠═7525b260-47dc-11eb-172e-a721065970f4
# ╠═13611b20-47e4-11eb-05c0-bb4c4d7be8d3


module PercolationModel

using Judycon
import Judycon: connect!

struct Percolation
    grid :: Matrix{Bool}
    li :: LinearIndices
    ny :: Int
    nx :: Int
    wuf1 :: QuickUnion
    wuf2 :: QuickUnion
    node1 :: Int
    node2 :: Int
end

function Percolation(ny, nx)
    grid = zeros(Bool, ny, nx)
    li = LinearIndices(grid)
    wuf1 = QuickUnion(nx*ny + 2)
    wuf2 = QuickUnion(nx*ny + 1)
    return Percolation(grid, li, ny, nx, wuf1, wuf2, nx*ny + 1, nx*ny + 2)
end

function connect!(p::Percolation, i, j)
    connect!(p.wuf1, i, j)
    connect!(p.wuf2, i, j)
end

function isopen(p::Percolation, i, j)
    return p.grid[i, j]
end

function number_of_open(p::Percolation)
    return sum(p.grid)
end

function isfull(p::Percolation, i, j)
    !isopen(p, i, j) && return false
    return isconnected(p.wuf2, p.node1, p.li[i, j])
end

function percolates(p::Percolation)
    return isconnected(p.wuf1, p.node1, p.node2)
end

function open!(p::Percolation, i, j)
    isopen(p, i, j) && return
    p.grid[i, j] = true

    i == 1       && connect!(p, p.node1, p.li[i, j])
    i == p.ny    && connect!(p.wuf1, p.node2, p.li[i, j])

    i > 1        && isopen(p, i-1, j) && connect!(p, p.li[i, j], p.li[i-1, j])
    i < p.ny - 1 && isopen(p, i+1, j) && connect!(p, p.li[i, j], p.li[i+1, j])
    j > 1        && isopen(p, i, j-1) && connect!(p, p.li[i, j], p.li[i, j-1])
    j < p.nx - 1 && isopen(p, i, j+1) && connect!(p, p.li[i, j], p.li[i, j+1])
end

using Statistics

struct Result{T}
    samples :: Vector{T}
    mean :: Float64
    stddev :: Float64
    confidence_lo :: Float64
    confidence_hi :: Float64
end

const CONFIDENCE_95 = 1.96

function Result(samples)
    n = length(samples)
    m = mean(samples)
    s = std(samples)
    cl = m - CONFIDENCE_95 * s / sqrt(n)
    ch = m + CONFIDENCE_95 * s / sqrt(n)
    return Result(samples, m, s, cl, ch)
end

using Printf

function Base.show(io::IO, r::Result)
    @printf("% 25s =  %d\n", "samples", length(r.samples))
    @printf("% 25s =  %0.3f\n", "mean", r.mean)
    @printf("% 25s =  %0.3f\n", "stddev", r.stddev)
    @printf("% 25s = [%0.3f, %0.3f]\n", "95% confidence interval", r.confidence_lo, r.confidence_hi)
end

function run_one(ny, nx)
    p = Percolation(ny, nx)
    while !percolates(p)
        open!(p, rand(1:ny), rand(1:nx))
    end
    return p
end

function run(n, trials)
    thresholds = zeros(trials)
    for i=1:trials
        p = run_one(n, n)
        thresholds[i] = number_of_open(p) / n^2
    end
    return Result(thresholds)
end

end


"""
Module UniformQuadMesh

Includes mesh related utility functions
"""

module UniformQuadMesh

using SparseArrays
using CommonUtils # for meshgrid

export uniform_quad_mesh
export quad_face_vertices

"""
uniform_quad_mesh(Kx::Int,Ky::Int)

Matlab uniform triangular mesh.

# Examples
```jldoctest
```
"""

function uniform_quad_mesh(Nx,Ny)

        Nxp = Nx+1;
        Nyp = Ny+1;
        Nv = convert(Int,Nxp*Nyp);
        K = convert(Int,Nx*Ny);

        x1D = LinRange(-1,1,Nxp);
        y1D = LinRange(-1,1,Nyp);
        x, y = meshgrid(x1D,y1D);
        I, J = meshgrid(collect(1:Nxp),collect(1:Nyp));
        inds = @. (I-1)*Ny + (J+I-1);
        EToV = zeros(Int,K,4);
        k = 1;
        for i = 1:Ny
                for j = 1:Nx
                        EToV[k,:] = [inds[i,j] inds[i,j+1] inds[i+1,j] inds[i+1,j+1]];
                        k += 1;
                end
        end

        VX = x[:];
        VY = y[:];

        return VX[:],VY[:],EToV
end

function uniform_quad_mesh(Kx)
        return uniform_quad_mesh(Kx,Ky)
end

"""
connect_2D(EToV)

Initialize quad elements connectivity matrices, element to element and
element to face connectivity.

# Examples
```jldoctest
"""

# ordering matters...
function quad_face_vertices()
        return [1,2],[2,4],[3,4],[1,3]
end

end


# Type definition
struct FourierSeries{T,S<:Real} <: FunctionSpace{T}
    coeffs::Vector{T}
    period::S
    function FourierSeries(coeffs::Vector{T}, period=1) where {T}
        isodd(length(coeffs)) ||
            throw(ArgumentError("expect odd length for coefficient vector"))
        S = typeof(period)
        new{T,S}(coeffs, period)
    end
end

# Basic properties
period(f::FourierSeries) = f.period
domain(f::FourierSeries) = (zero(period(f)), period(f))

nummodes(F::FourierSeries) = (length(F.coeffs)-1) >> 1
coefficients(F::FourierSeries) = F.coeffs

function Base.:(==)(F1::FourierSeries, F2::FourierSeries)
    F1.period == F2.period || return false
    K = max(nummodes(F1), nummodes(F2))
    for k = -K:K
        F1[k] == F2[k] || return false
    end
    return true
end

# Indexing, getting and setting coefficients
Base.eachindex(F::FourierSeries) = UnitRange(-nummodes(F), nummodes(F))
function Base.getindex(F::FourierSeries, k)
    if abs(k) > nummodes(F)
        return zero(F.coeffs[1])
    else
        j = ifelse(k == 0, 1, ifelse(k < 0, -2*k+1, 2*k))
        @inbounds getindex(F.coeffs, j)
    end
end
function Base.setindex!(F::FourierSeries, v, k)
    if abs(k) > nummodes(F)
        setnummodes!(F, abs(k))
    end
    j = ifelse(k == 0, 1, ifelse(k < 0, -2*k+1, 2*k))
    @inbounds setindex!(F.coeffs, v, j)
end

# Use as function
function (F::FourierSeries)(x)
    xred = x/period(F)
    x = mod1(xred, one(xred))
    K = nummodes(F)
    factor = 2pi
    f = zero(F[0]) + F[0]*(cos(0*factor)+im*sin(0*factor)) # for type stability
    for k = 1:K
        f += (F[k]+F[-k])*cos(factor*k*x) + im*(F[k]-F[-k])*sin(factor*k*x)
    end
    return f
end

# Change number of coefficients
function truncate!(F::FourierSeries; Kmax::Integer = nummodes(F), tol::Real = 0)
    Ktol = findlast(x->norm(x)>=tol, F.coeffs)
    Kmax = min(Kmax, Ktol === nothing ? 0 : Ktol>>1)
    if Kmax < nummodes(F)
        resize!(F.coeffs, 2*Kmax+1)
    end
    return F
end

function setnummodes!(F::FourierSeries, K::Int)
    while nummodes(F) < K
        push!(F.coeffs, zero(F.coeffs[1]), zero(F.coeffs[1]))
    end
    if nummodes(F) > K
        resize!(F.coeffs, 2*K+1)
    end
    return F
end

# special purpose constructor
Base.similar(F::FourierSeries, ::Type{T} = scalartype(F)) where {T} =
    FourierSeries([zero(T)*F[0]], period(F))

Base.zero(F::FourierSeries) = FourierSeries([zero(F[0])], period(F))
Base.one(F::FourierSeries) = FourierSeries([one(F[0])], period(F))

# Arithmetic (out of place)
Base.copy(F::FourierSeries) = FourierSeries(copy.(coefficients(F)), period(F))

Base.:-(F::FourierSeries) = FourierSeries(.-coefficients(F), period(F))
Base.:+(F::FourierSeries) = FourierSeries(.+coefficients(F), period(F))

const Const = Union{Number,AbstractArray}
Base.:*(F::FourierSeries, a::Const) =
    FourierSeries([f*a for f in coefficients(F)], period(F))
Base.:*(a::Const, F::FourierSeries) =
    FourierSeries([a*f for f in coefficients(F)], period(F))
Base.:/(F::FourierSeries, a::Const) =
    FourierSeries([f/a for f in coefficients(F)], period(F))
Base.:\(a::Const, F::FourierSeries) =
    FourierSeries([a\f for f in coefficients(F)], period(F))

function Base.:+(F1::FourierSeries, F2::FourierSeries)
    domain(F1) == domain(F2) || throw(DomainMismatch())
    K1 = nummodes(F1)
    K2 = nummodes(F2)
    K = max(K1, K2)
    F = FourierSeries(sizehint!([F1[0]+F2[0]], 2*K+1), period(F1))
    for k = 1:K
        if k <= K1 && k <= K2
            F[k] = F1[k] + F2[k]
            F[-k] = F1[-k] + F2[-k]
        elseif k <= K1
            F[k] = copy(F1[k])
            F[-k] = copy(F1[-k])
        else
            F[k] = copy(F2[k])
            F[-k] = copy(F2[-k])
        end
    end
    return F
end

function Base.:-(F1::FourierSeries, F2::FourierSeries)
    domain(F1) == domain(F2) || throw(DomainMismatch())
    K1 = nummodes(F1)
    K2 = nummodes(F2)
    K = max(K1, K2)
    F = FourierSeries(sizehint!([F1[0] - F2[0]], 2*K+1), period(F1))
    for k = 1:K
        if k <= K1 && k <= K2
            F[k] = F1[k] - F2[k]
            F[-k] = F1[-k] - F2[-k]
        elseif k <= K1
            F[k] = copy(F1[k])
            F[-k] = copy(F1[-k])
        else
            F[k] = -(F2[k])
            F[-k] = -(F2[-k])
        end
    end
    return F
end

Base.:*(F1::FourierSeries, F2::FourierSeries) = truncmul(F1, F2)

function truncmul(F1::FourierSeries, F2::FourierSeries;
                    Kmax::Integer = nummodes(F1) + nummodes(F2), tol::Real = 0)
    domain(F1) == domain(F2) || throw(DomainMismatch())
    F = FourierSeries(sizehint!([zero(F1[0])*zero(F2[0])], 2*Kmax+1), period(F1))
    truncmul!(F, F1, F2, true, false; Kmax = Kmax, tol = tol)
    return F
end

function Base.conj(F::FourierSeries)
    Fc = FourierSeries(map(conj, coefficients(F)), period(F))
    K = nummodes(F)
    for k = 1:K
        Fc[k], Fc[-k] = Fc[-k], Fc[k]
    end
    return Fc
end
function Base.adjoint(F::FourierSeries)
    Fc = FourierSeries(map(adjoint, coefficients(F)), period(F))
    K = nummodes(F)
    for k = 1:K
        Fc[k], Fc[-k] = Fc[-k], Fc[k]
    end
    return Fc
end
Base.transpose(F::FourierSeries) = FourierSeries(map(transpose, coefficients(F)), period(F))

LinearAlgebra.tr(F::FourierSeries) = FourierSeries(map(tr, coefficients(F)), period(F))

Base.real(F::FourierSeries) = rmul!(F+conj(F), 1//2)
Base.imag(F::FourierSeries) = rmul!(F-conj(F), 1//(2*im))

# Arithmetic (in place / mutating methods)
function Base.copy!(Fdst::FourierSeries, Fsrc::FourierSeries)
    domain(Fdst) == domain(Fsrc) || throw(DomainMismatch())
    setnummodes!(Fdst, nummodes(Fsrc))
    if eltype(Fdst) <: Number
        copy!(coefficients(Fdst), coefficients(Fsrc))
    else
        for k in eachindex(Fdst)
            copy!(Fdst[k], Fsrc[k])
        end
    end
    return Fdst
end
function LinearAlgebra.rmul!(F::FourierSeries, α::Number)
    if eltype(F) <: Number
        rmul!(coefficients(F), α)
    else
        for k in eachindex(F)
            rmul!(F[k], α)
        end
    end
    return F
end
function LinearAlgebra.lmul!(α::Number, F::FourierSeries)
    if eltype(F) <: Number
        lmul!(α, coefficients(F))
    else
        for k in eachindex(F)
            lmul!(α, F[k])
        end
    end
    return F
end

function LinearAlgebra.axpy!(α::Number, Fx::FourierSeries, Fy::FourierSeries)
    domain(Fx) == domain(Fy) || throw(DomainMismatch())
    Kx = nummodes(Fx)
    setnummodes!(Fy, max(Kx, nummodes(Fy)))
    Ky = nummodes(Fy)
    if eltype(Fy) <: Number
        if Ky > Kx
            LinearAlgebra.axpy!(α, coefficients(Fx), view(coefficients(Fy), 1:2Kx+1))
        else
            LinearAlgebra.axpy!(α, coefficients(Fx), coefficients(Fy))
        end
    else
        K = nummodes(Fx)
        Threads.@threads for k in -K:K
            axpy!(α, Fx[k], Fy[k])
        end
    end
    return Fy
end
function LinearAlgebra.axpby!(α::Number, Fx::FourierSeries, β::Number, Fy::FourierSeries)
    domain(Fx) == domain(Fy) || throw(DomainMismatch())
    Kx = nummodes(Fx)
    setnummodes!(Fy, max(Kx, nummodes(Fy)))
    Ky = nummodes(Fy)
    if eltype(Fy) <: Number
        if Ky > Kx
            LinearAlgebra.axpby!(α, coefficients(Fx), β, view(coefficients(Fy), 1:2Kx+1))
            lmul!(β, view(coefficients(Fy), (2Kx+2):(2Ky+1)))
        else
            LinearAlgebra.axpby!(α, coefficients(Fx), β, coefficients(Fy))
        end
    else
        K = nummodes(Fy)
        Threads.@threads for k in -K:K
            if abs(k) <= Kx
                axpby!(α, Fx[k], β, Fy[k])
            else
                lmul!(β, Fy[k])
            end
        end
    end
    return Fy
end

LinearAlgebra.mul!(Fdst::FourierSeries, α::Number, Fsrc::FourierSeries) =
    truncmul!(Fdst, α, Fsrc)
LinearAlgebra.mul!(Fdst::FourierSeries, Fsrc::FourierSeries, α::Number) =
    truncmul!(Fdst, Fsrc, α)
LinearAlgebra.mul!(F::FourierSeries, F1::FourierSeries, F2::FourierSeries,
                    α = true, β = false) = truncmul!(F, F1, F2, α, β)

function truncmul!(Fdst::FourierSeries, α₁::Number, Fsrc::FourierSeries,
                    α₂ = true, β = false;
                    Kmax::Integer = nummodes(Fsrc), tol::Real = 0)
    α = α₁ * α₂
    domain(Fdst) == domain(Fsrc) || throw(DomainMismatch())
    setnummodes!(Fdst, Kmax)
    if eltype(Fdst) <: Number
        mul!(coefficients(Fdst), α, coefficients(Fsrc))
    else
        for k in eachindex(Fdst)
            mul!(Fdst[k], α, Fsrc[k])
        end
    end
    return Fdst
end
function truncmul!(Fdst::FourierSeries, Fsrc::FourierSeries, α₁::Number,
                    α₂ = true, β = false;
                    Kmax::Integer = nummodes(Fsrc), tol::Real = 0)
    α = α₁ * α₂
    domain(Fdst) == domain(Fsrc) || throw(DomainMismatch())
    setnummodes!(Fdst, Kmax)
    if eltype(Fdst) <: Number
        mul!(coefficients(Fdst), coefficients(Fsrc), α)
    else
        for k in eachindex(Fdst)
            mul!(Fdst[k], Fsrc[k], α)
        end
    end
    return Fdst
end
function truncmul!(F::FourierSeries, F1::FourierSeries, F2::FourierSeries,
                    α = true, β = false;
                    Kmax::Integer = max(nummodes(F), nummodes(F1)+nummodes(F2)), tol::Real = 0)
    domain(F) == domain(F1) == domain(F2) || throw(DomainMismatch())
    K1 = nummodes(F1)
    K2 = nummodes(F2)
    K = min(Kmax, max(iszero(β) ? 0 : nummodes(F), K1+K2))
    setnummodes!(F, K)
    Threads.@threads for k = -K:K
        fk = F[k]
        if fk isa AbstractArray
            T = eltype(fk)
            if β == 0
                fill!(fk, T(β))
            elseif β != 1
                rmul!(fk, T(β))
            end
            for k1 = max(-K1, k-K2):min(K1, k+K2)
                k2 = k - k1
                fk1 = F1[k1]
                fk2 = F2[k2]
                if fk1 isa AbstractArray && fk2 isa AbstractArray
                    mul!(fk, fk1, fk2, T(α), T(true))
                elseif fk1 isa AbstractArray
                    axpy!(T(fk2*α), fk1, fk)
                elseif fk2 isa AbstractArray
                    axpy!(T(fk1*α), fk2, fk)
                else
                    @warn "unexpected branch"
                    fk .= fk + (fk1*fk2*α)
                end
            end
        else
            fk *= β
            for k1 = max(-K1, k-K2):min(K1, k+K2)
                k2 = k - k1
                fk1 = F1[k1]
                fk2 = F2[k2]
                fk += fk1*fk2*α
            end
            F[k] = fk
        end
    end
    if tol != 0
        truncate!(F; tol = tol)
    end
    return F
end

# Inner product and norm
function localdot(F1::FourierSeries, F2::FourierSeries)
    domain(F1) == domain(F2) || throw(DomainMismatch())
    K1 = nummodes(F1)
    K2 = nummodes(F2)
    K = K1+K2
    F = FourierSeries(sizehint!([zero(dot(F1[0], F2[0]))], 2*K+1), period(F1))
    for k = -K:K
        fk = F[k]
        for k1 = max(-K1, -K2-k):min(K1, K2-k)
            k2 = k + k1
            fk += dot(F1[k1], F2[k2])
        end
        F[k] = fk
    end
    return F
end

function LinearAlgebra.dot(F1::FourierSeries, F2::FourierSeries)
    domain(F1) == domain(F2) || throw(DomainMismatch())
    K = min(nummodes(F1), nummodes(F2))
    return sum(k->dot(F1[k], F2[k]), -K:K)
end
LinearAlgebra.norm(F::FourierSeries) = norm(norm(Fk) for Fk in coefficients(F))

function LinearAlgebra.isapprox(x::FourierSeries, y::FourierSeries;
                                atol::Real=0,
                                rtol::Real=defaulttol(x[0]))
    return norm(x-y) <= max(atol, rtol*max(norm(x), norm(y)))
end

function differentiate(F::FourierSeries)
    dF = 0im * F
    ω = (2*pi)/period(F)
    K = nummodes(F)
    for k = 1:K
        f = im*ω*k
        dF[-k] = -f*F[-k]
        dF[+k] = +f*F[+k]
    end
    return dF
end
function integrate(F::FourierSeries, (a,b) = domain(F))
    p = period(F)
    if b-a == p
        return F[0]*p
    else
        error("not yet implemented")
    end
end

# Simple Fourier transform: typically not needed for large number of points, but should
# work with matrix valued functions etc
fit(f, ::Type{FourierSeries}, (a,b)::Tuple{Real,Real}; kwargs...) =
    fit(f, FourierSeries, b-a; kwargs...)

function fit(f, ::Type{FourierSeries}, period = 1; Kmax = 10, tol = 1e-12)
    K = Kmax
    x = (0:2*K) * (period / (2K+1))
    fx = map(f, x)
    ω = 2*pi*0/period
    integrand = exp.((-im*ω) .* x) .* fx
    F = FourierSeries([sum(integrand)/(2K+1)], period)
    isrealf = all(isreal, fx)
    for k = 1:Kmax
        ω = 2*pi*k/period
        integrand .= exp.((-im*ω) .* x) .* fx
        F[k] = sum(integrand)/(2K+1)
        if isrealf
            F[-k] = conj(F[k])
        else
            integrand .= exp.((+im*ω) .* x) .* fx
            F[-k] = sum(integrand)/(2K+1)
        end
    end
    return F
end

# function Base.inv(F::FourierSeries; tol=1e-12, Kmax = 2*nummodes(F))
#     K = nummodes(F)
#     while true
#         Fx = map(F, (0:2*K)/(2*K+1))
#         Gx = map(inv, Fx)
#         krange = vcat([0],[(-1)^iseven(l)*((l+1)>>1) for l=1:2*K])
#         factor = -im*2*pi/(2*K+1)
#         coeffG = let factor=factor, Gx=Gx, K = K
#             map(krange) do k
#                 sum(exp(factor*k*(l-1))*Gx[l] for l = 1:(2*K+1))/(2*K+1)
#             end
#         end
#         G = FourierSeries(coeffG)
#         if norm(G*F-one(G))<tol || K == Kmax
#             return G
#         else
#             K = min(2*K, Kmax)
#         end
#     end
# end
#
# function inv2(F::FourierSeries; tol = 1e-12, Kmax = 2*nummodes(F))
#     Fi = inv(F; tol = tol, Kmax = Kmax)
#     Fi2 = linsolve(x->setnummodes!(x*F, nummodes(x)), one(F), Fi; tol = tol)
#
#     K = nummodes(F)
#     while true
#         Fx = map(F, (0:2*K)/(2*K+1))
#         Gx = map(inv, Fx)
#         krange = vcat([0],[(-1)^iseven(l)*((l+1)>>1) for l=1:2*K])
#         factor = -im*2*pi/(2*K+1)
#         coeffG = let factor=factor, Gx=Gx, K = K
#             map(krange) do k
#                 sum(exp(factor*k*(l-1))*Gx[l] for l = 1:(2*K+1))/(2*K+1)
#             end
#         end
#         G = FourierSeries(coeffG)
#         if norm(G*F-one(G))<tol || K == Kmax
#             return G
#         else
#             K = min(2*K, Kmax)
#         end
#     end
#     return Fi2
# end
#
# function Base.sqrt(F::FourierSeries; tol=1e-12, Kmax = 200)
#     K = nummodes(F)
#     while true
#         Fx = map(F, (0:2*K)/(2*K+1))
#         Gx = map(sqrt, Fx)
#         krange = vcat([0],[(-1)^iseven(l)*((l+1)>>1) for l=1:2*K])
#         factor = -im*2*pi/(2*K+1)
#         coeffG = let factor=factor, Gx=Gx
#             map(krange) do k
#                 sum(exp(factor*k*(l-1))*Gx[l] for l = 1:(2*K+1))/(2*K+1)
#             end
#         end
#         G = FourierSeries(coeffG)
#         if norm(G*G-F)<tol || K == Kmax
#             return G
#         else
#             K = min(2*K, Kmax)
#         end
#     end
# end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

using BAT
using Test

using ValueShapes, Distributions, ForwardDiff

@testset "test_generic_transforms" begin
    @testset "uniform_cdf" begin
        a = -1.0
        b = 3.0
        x = 2
        u = cdf(Uniform(a, b), x)

        @test @inferred(BAT.uniform_cdf(x, a, b)) ≈ u
        @test @inferred(BAT.uniform_invcdf(u, a, b)) ≈ x
        @test BAT.uniform_invcdf_ladj(u, a, b) ≈ log(abs(ForwardDiff.derivative(u -> BAT.uniform_invcdf(u, a, b), u)))

        @test @inferred(BAT.UniformCDFTrafo(a, b)) isa BAT.VariateTransform{Univariate,BAT.UnitSpace,BAT.MixedSpace}
        trafo = BAT.UniformCDFTrafo(a, b)
        @test @inferred(varshape(trafo)) == ScalarShape{Real}()
        @test BAT.target_space(trafo) == BAT.UnitSpace()
        @test BAT.source_space(trafo) == BAT.MixedSpace()

        @test @inferred(BAT.apply_vartrafo(trafo, x, 0)).v ≈ u
        @test BAT.apply_vartrafo(trafo, x, 0).ladj ≈ log(abs(ForwardDiff.derivative(x -> cdf(Uniform(a, b), x), x)))
        @test isnan(BAT.apply_vartrafo(trafo, x, NaN).ladj)
        @test @inferred(BAT.apply_vartrafo(inv(trafo), u, 0)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo), u, 0).ladj ≈ log(abs(ForwardDiff.derivative(u -> quantile(Uniform(a, b), u), u)))
        @test isnan(BAT.apply_vartrafo(inv(trafo), u, NaN).ladj)

        @test @inferred(inv(trafo)) isa BAT.VariateTransform{Univariate,BAT.MixedSpace,BAT.UnitSpace}
        @test @inferred(BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0.79).ladj ≈ 0.79

        @test @inferred(BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0)).v ≈ u
        @test BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0.79).ladj ≈ 0.79
    end

    @testset "logistic_cdf" begin
        mu = 2.0
        theta = 3.0
        x = 4
        u = cdf(Logistic(mu, theta), x)

        @test @inferred(BAT.logistic_cdf(x, mu, theta)) ≈ u
        @test @inferred(BAT.logistic_invcdf(u, mu, theta)) ≈ x
        @test BAT.logistic_invcdf_ladj(u, mu, theta) ≈ log(abs(ForwardDiff.derivative(u -> BAT.logistic_invcdf(u, mu, theta), u)))

        @test @inferred(BAT.LogisticCDFTrafo(mu, theta)) isa BAT.VariateTransform{Univariate,BAT.UnitSpace,BAT.InfiniteSpace}
        trafo = BAT.LogisticCDFTrafo(mu, theta)
        @test @inferred(varshape(trafo)) == ScalarShape{Real}()
        @test BAT.target_space(trafo) == BAT.UnitSpace()
        @test BAT.source_space(trafo) == BAT.InfiniteSpace()

        @test @inferred(BAT.apply_vartrafo(trafo, x, 0)).v ≈ u
        @test BAT.apply_vartrafo(trafo, x, 0).ladj ≈ log(abs(ForwardDiff.derivative(x -> cdf(Logistic(mu, theta), x), x)))
        @test isnan(BAT.apply_vartrafo(trafo, x, NaN).ladj)
        @test @inferred(BAT.apply_vartrafo(inv(trafo), u, 0)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo), u, 0).ladj ≈ log(abs(ForwardDiff.derivative(u -> quantile(Logistic(mu, theta), u), u)))
        @test isnan(BAT.apply_vartrafo(inv(trafo), u, NaN).ladj)

        @test @inferred(inv(trafo)) isa BAT.VariateTransform{Univariate,BAT.InfiniteSpace,BAT.UnitSpace}
        @test @inferred(BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0.79)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0.79).ladj ≈ 0.79

        @test @inferred(BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0.79)).v ≈ u
        @test BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0.79).ladj ≈ 0.79
    end

    @testset "exponential_cdf" begin
        theta = 3.0
        x = 5
        u = cdf(Exponential(theta), x)

        @test @inferred(BAT.exponential_cdf(x, theta)) ≈ u
        @test @inferred(BAT.exponential_invcdf(u, theta)) ≈ x
        @test BAT.exponential_cdf_ladj(x, theta) ≈ log(abs(ForwardDiff.derivative(x -> BAT.exponential_cdf(x, theta), x)))

        @test @inferred(BAT.ExponentialCDFTrafo(theta)) isa BAT.VariateTransform{Univariate,BAT.UnitSpace,BAT.MixedSpace}
        trafo = BAT.ExponentialCDFTrafo(theta)
        @test @inferred(varshape(trafo)) == ScalarShape{Real}()
        @test BAT.target_space(trafo) == BAT.UnitSpace()
        @test BAT.source_space(trafo) == BAT.MixedSpace()

        @test @inferred(BAT.apply_vartrafo(trafo, x, 0)).v ≈ u
        @test BAT.apply_vartrafo(trafo, x, 0).ladj ≈ log(abs(ForwardDiff.derivative(x -> cdf(Exponential(theta), x), x)))
        @test isnan(BAT.apply_vartrafo(trafo, x, NaN).ladj)
        @test @inferred(BAT.apply_vartrafo(inv(trafo), u, 0)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo), u, 0).ladj ≈ log(abs(ForwardDiff.derivative(u -> quantile(Exponential(theta), u), u)))
        @test isnan(BAT.apply_vartrafo(inv(trafo), u, NaN).ladj)

        @test @inferred(inv(trafo)) isa BAT.VariateTransform{Univariate,BAT.MixedSpace,BAT.UnitSpace}
        @test @inferred(BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0.79)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo) ∘ trafo, x, 0.79).ladj ≈ 0.79

        @test @inferred(BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0.79)).v ≈ u
        @test BAT.apply_vartrafo(inv(trafo ∘ inv(trafo)), u, 0.79).ladj ≈ 0.79
    end

    @testset "scaled_log" begin
        theta = 3.0
        x = 4
        y = log(x / theta)

        @test @inferred(BAT.ScaledLogTrafo(theta)) isa BAT.VariateTransform{Univariate,BAT.InfiniteSpace,BAT.MixedSpace}
        trafo = BAT.ScaledLogTrafo(theta)
        @test @inferred(varshape(trafo)) == ScalarShape{Real}()
        @test BAT.target_space(trafo) == BAT.InfiniteSpace()
        @test BAT.source_space(trafo) == BAT.MixedSpace()

        @test @inferred(BAT.apply_vartrafo(trafo, x, 0)).v ≈ y
        @test BAT.apply_vartrafo(trafo, x, 0).ladj ≈ log(abs(ForwardDiff.derivative(trafo, x)))
        @test isnan(BAT.apply_vartrafo(trafo, x, NaN).ladj)
        @test @inferred(BAT.apply_vartrafo(inv(trafo), y, 0)).v ≈ x
        @test BAT.apply_vartrafo(inv(trafo), y, 0).ladj ≈ log(abs(ForwardDiff.derivative(inv(trafo), y)))
        @test isnan(BAT.apply_vartrafo(inv(trafo), y, NaN).ladj)
    end
end


## Hermite
@register0 Hermite AbstractCCOP0
export Hermite

"""
    Hermite

The [Hermite](https://en.wikipedia.org/wiki/Hermite_polynomials) polynomials have two versions the physicists (`Hermite`  or  `H`) and the probablalists (`ChebyshevHermite` or  `Hₑ`). They are  related through  `Hᵢ(x) =  2^(i/2) Hₑᵢ(√2 x)`.

The Hermite   polynomials have weight  function `w(x)=exp(-x^2/2)` and domain the real line.

## Examples
```jldoctest
julia> using Polynomials,  SpecialPolynomials

julia> x = variable(Polynomial{Rational{Int}})
Polynomials.Polynomial(x)

julia> [basis(Hermite, i)(x) for i in 0:5]
6-element Vector{Polynomial{Float64, :x}}:
 Polynomials.Polynomial(1.0)
 Polynomials.Polynomial(2.0*x)
 Polynomials.Polynomial(-2.0 + 4.0*x^2)
 Polynomials.Polynomial(-12.0*x + 8.0*x^3)
 Polynomials.Polynomial(12.0 - 48.0*x^2 + 16.0*x^4)
 Polynomials.Polynomial(120.0*x - 160.0*x^3 + 32.0*x^5)

julia> [basis(ChebyshevHermite, i)(x) for i in 0:5]
6-element Vector{Polynomial{Float64, :x}}:
 Polynomials.Polynomial(1.0)
 Polynomials.Polynomial(1.0*x)
 Polynomials.Polynomial(-1.0 + 1.0*x^2)
 Polynomials.Polynomial(-3.0*x + 1.0*x^3)
 Polynomials.Polynomial(3.0 - 6.0*x^2 + 1.0*x^4)
 Polynomials.Polynomial(15.0*x - 10.0*x^3 + 1.0*x^5)
```

!!! note
    The Hermite family needs help, as the computed values for `Bn`,and,`Cn` are  both 0.
"""
Hermite

basis_symbol(::Type{<:Hermite}) = "H"
Polynomials.domain(::Type{<:Hermite}) = Polynomials.Interval(-Inf, Inf)

abcde(::Type{<:Hermite}) = NamedTuple{(:a, :b, :c, :d, :e)}((1, 0, 0, -2, 0))

k0(P::Type{<:Hermite}) = one(eltype(P))
function k1k0(P::Type{<:Hermite}, k::Int)
    val = 2 * one(eltype(P))
    val
end

norm2(::Type{<:Hermite}, n) = sqrt(pi) * 2^n * gamma(n + 1)
weight_function(::Type{<:Hermite}) = x -> exp(-x^2)
generating_function(::Type{<:Hermite}) = (t, x) -> exp(2 * t * x - t^2)
function classical_hypergeometric(::Type{<:Hermite}, n, x)
    as = iseven(n) ? (-n ÷ 2, -(n - 1) / 2) : (-n / 2, -(n - 1) ÷ 2)
    bs = ()
    (2x)^n * pFq(as, bs, -inv(x)^2)
end

gauss_nodes_weights(p::Type{P}, n) where {P<:Hermite} = FastGaussQuadrature.gausshermite(n)

## Overrides
# Use override here, as we get  An,Bn,Cn =  1, 0,0, otherwise
An(P::Type{<:Hermite}, n::Int) = 2 * one(eltype(P))
Bn(P::Type{<:Hermite}, n::Int) = zero(eltype(P))
Cn(P::Type{<:Hermite}, n::Int) = 2 * n * one(eltype(P))

β̃n(P::Type{<:Hermite}, n::Int) = zero(eltype(P))
γ̃n(P::Type{<:Hermite}, n::Int) = zero(eltype(P))
b̂̃n(P::Type{<:Hermite}, ::Val{N}) where {N} = zero(eltype(P)) #where {M} = error("Don't call me")#zero(S)
ĉ̃n(P::Type{<:Hermite}, ::Val{N}) where {N} = zero(eltype(P)) #where {M} = error("Don't call me")#zero(S)

## https://arxiv.org/pdf/1901.01648.pdf. Connection formula (14)
##  x^n  = n! sum(H_{n-2j}/ (2^j(n-2j)!j!) j = 0:floor(n/2))

Base.convert(P::Type{<:Hermite}, q::Polynomial) = connection(P, q)
function Base.iterate(
    o::Connection{P,Q},
    state=nothing,
) where {P<:Hermite,Q<:Polynomials.StandardBasisPolynomial}
    k, n = o.k, o.n

    if state == nothing
        i = k
        j = 0
        i > n && return nothing
        val = __hermite_lambda(i, k)
    elseif state[1] + 2 > n # terminate
        return nothing
    else
        j, val1 = state
        #val1 *= (i+1)*(i+2)/4/(j+1/4)
        j += 1
        i = k + 2j
        val = __hermite_lambda(i, k)
    end

    return (i, val), (j, val)
end

function __hermite_lambda(n, k)
    val = gamma(1 + n) / 2^n
    val /= gamma(1 + k)
    val /= gamma(1 + (n - k) / 2)
    val
end

# 2x more performant
function Polynomials.integrate(p::P, C::Number=0) where {T,X,P<:Hermite{T,X}}
    # int H_n = 1/(n+1) H_{n+1}
    R = eltype(one(T) / 1)
    d = degree(p)
    qs = zeros(R, d + 2)

    for i in 0:d
        qs[i + 2] = p[i] / (2(i + 1))
    end

    q = ⟒(P){R,X}(qs)
    q = q - q(0) + R(C)

    return q
end

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

@register0 MonicHermite AbstractCCOP0
export MonicHermite
ϟ(::Type{<:MonicHermite}) = Hermite
ϟ(::Type{<:MonicHermite{T}}) where {T} = Hermite{T}
@register_monic(MonicHermite)

pqr_start(::Type{<:MonicHermite}) = 0
pqr_symmetry(::Type{<:MonicHermite}) = true
pqr_weight(P::Type{<:MonicHermite}, n, x, dπx) = (one(P) * 2) * exp(-x^2) / (dπx * dπx)

##
## --------------------------------------------------
##
@register0 ChebyshevHermite AbstractCCOP0
export ChebyshevHermite
"""
    ChebyshevHermite

Type for the Probabalist's  [Hermite](https://en.wikipedia.org/wiki/Hermite_polynomials) polynomials.

"""
ChebyshevHermite

basis_symbol(::Type{<:ChebyshevHermite}) = "Hₑ"
Polynomials.domain(::Type{<:ChebyshevHermite}) = Polynomials.Interval(-Inf, Inf)

# https://arxiv.org/pdf/1901.01648.pdf eqn 17
abcde(::Type{<:ChebyshevHermite}) = NamedTuple{(:a, :b, :c, :d, :e)}((0, 0, 1, -1, 0))

kn(P::Type{<:ChebyshevHermite}, n::Int)    = one(eltype(P))
k1k0(P::Type{<:ChebyshevHermite}, k::Int)  = one(eltype(P))
k1k_1(P::Type{<:ChebyshevHermite}, k::Int) = one(eltype(P))
ismonic(::Type{<:ChebyshevHermite})        = true

weight_function(P::Type{ChebyshevHermite}) = x -> exp(-(one(eltype(P)) * x^2) / 2)
generating_function(::Type{<:ChebyshevHermite}) = (t, x) -> exp(t * x - t^2 / 2)
function classical_hypergeometric(::Type{<:ChebyshevHermite}, n, x)
    2^(-n / 2) * classical_hypergeometric(Hermite, n, x / sqrt(2))
end

function gauss_nodes_weights(P::Type{<:ChebyshevHermite}, n) where {α,β}
    xs, ws = glaser_liu_rokhlin_gauss_nodes(basis(ChebyshevHermite, n))
    xs, ws
end

pqr_start(::Type{<:ChebyshevHermite}) = 0
pqr_symmetry(::Type{<:ChebyshevHermite}) = true
pqr_weight(P::Type{<:ChebyshevHermite}, n, x, dπx) =
    sqrt(2) * gamma(1 + n) * sqrt(pi) / (dπx)^2

## Overrides
#An(P::Type{<:ChebyshevHermite}, n::Int) = one(eltype(P))
#Bn(P::Type{<:ChebyshevHermite}, n::Int) = zero(eltype(P))
#Cn(P::Type{<:ChebyshevHermite}, n::Int) = -one(eltype(P))*n

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

##  Multiplication
## TODO: work  on  types
AbstractHermite{T,X} = Union{Hermite{T,X},ChebyshevHermite{T,X}}

# Default is slow. Instead
# directly use a linearization formula from
# https://arxiv.org/pdf/1901.01648.pdf
# and  for Hermite, Hn(x) =2^(n/2)Hₑ_n(sqrt(2) x)
# so Hm⋅Hn = ∑ C_{m,n,j} 2^j H_{m+n-2j}
function ⊗(
    ::Type{<:AbstractHermite},
    p::P,
    q::Q,
) where {T,X,S,Y,P<:AbstractHermite{T,X},Q<:AbstractHermite{S,Y}}
    R = eltype(one(promote_type(T, S)) / 1)
    N, M = degree(p), degree(q)
    N == -1 && return zero(⟒(P){R,Y})
    M == -1 && return zero(⟒(P){R,X})
    N == 0 && return q * p[0]
    M == 0 && return p * q[0]
    X != Y && throw(ArgumentError("Variable names must match"))

    as = zeros(R, 1 + N + M)
    for i in eachindex(p)
        pᵢ = p[i]
        iszero(pᵢ) && continue
        for j in eachindex(q)
            qⱼ = q[j]
            pᵢqⱼ = pᵢ * qⱼ
            iszero(qⱼ) && continue
            λᵤ = one(R)
            for k in 0:min(i, j)
                as[1 + i + j - 2k] += pᵢqⱼ * λᵤ
                λᵤ *= (i - k) * (j - k) / (k + 1) * hermite_α(P)
            end
        end
    end
    ⟒(P){R,X}(as)
end

hermite_α(P::Type{<:ChebyshevHermite}) = 1
hermite_α(P::Type{<:Hermite}) = 2

#⊗(p::Hermite, q::Hermite) = linearization_product(p, q)
#⊗(p::ChebyshevHermite, q::ChebyshevHermite) = linearization_product(p, q)

# function Base.iterate(o::Linearization{P,R}, state =  nothing) where {P <: AbstractHermite,R}

#     l, m, n = o.l, o.m, o.n
#     l  > m + n && return nothing

#     if state == nothing

#         #k =  0
#         j = 0
#         p = min(l, n)
#         q = l - p
#         val = linearization_α(P, R, j, l, p, q)

#     else
#         # we work with l + 2j as there is a parity consideration
#         j, p, q, val  = state
#         s = l + j # l +2j + p + q = l +2j + l = 2l + 2j, so s=l+j
#         if p == 0 ||  q == m || q  > s
#             j += 1
#             l + 2j > n+m && return nothing #  l + 2j  is too  big now
#             p = min(l + j, n)  # p <= s
#             q = l + 2j - p
#             q > s + 1 && return nothing
#             val = linearization_α(P, R, j, l, p, q)
#         else
#             p -= 1
#             q += 1

#             λ = q/(p+1)*(s-(q-1))/(s-p)
#             val *= λ
#         end

#     end

#     return (p,q,val), (j, p, q, val)
# end

# #  https://arxiv.org/pdf/1901.01648.pdf equation (74)
# function linearization_α(P::Type{<:AbstractHermite}, R, j, l, n, m)
#     s = l + j # pass in j to avoid s = divrem(l + m + n,  2) call
#     val = one(R)
#     val *= gamma(1+m)*gamma(1+n)/gamma(1+s-m)/gamma(1+s-n)/gamma(1+s-l)
#     val *= linearization_λ(P, l, m,n)
# end
# linearization_λ(::Type{<:Hermite}, l, m, n) = 2.0^((m+n-l)/2)
# linearization_λ(::Type{<:ChebyshevHermite}, l, m, n) = 1


reduce_kvecs(vec::Vector,tol) = vec

function reduce_kvecs(vecs::Vector{Vector{T}},tol::T) where T<:AbstractFloat
    all_inds = collect(eachindex(vecs))
    vecs = map(vecs) do w
        ind_ins = findall([norm(v - w) < tol for v in vecs[all_inds]])
        inds = all_inds[ind_ins]
        deleteat!(all_inds,ind_ins)
        isempty(inds) ? [zero(T),-one(T)] :  mean(vecs[inds])
    end
    vecs = deleteat!(vecs, findall([w[2] < -sqrt(tol) for w in vecs]))

    return vecs
end