#########
# THIS SECTION JUST STEALS FROM STATSBASE BUT REMOVES TYPE RESTRICTIONS AND ADDS SIMD

# Skewness
# This is Type 1 definition according to Joanes and Gill (1998)
"""
    skew(v, m=mean(v))

Compute the standardized skewness of iterable `v`.
"""
function skew(v, m::T) where {T<:Union{Missing,Real}}
    n = length(v)
    cm2 = 0.0   # empirical 2nd centered moment (variance)
    cm3 = 0.0   # empirical 3rd centered moment
    @simd for v_i in v
        z = v_i - m
        z2 = z * z

        cm2 += z2
        cm3 += z2 * z
    end
    cm3 /= n
    cm2 /= n
    return cm3 / sqrt(cm2 * cm2 * cm2)  # this is much faster than cm2^1.5
end

skew(v) = skew(v,mean(v))

# (excess) Kurtosis
# This is Type 1 definition according to Joanes and Gill (1998)
"""
    kurt(v, m=mean(v))

Compute the excess kurtosis of iterable `v`.
"""
function kurt(v, m::T) where {T<:Union{Missing,Real}}
    n = length(v)
    cm2 = 0.0  # empirical 2nd centered moment (variance)
    cm4 = 0.0  # empirical 4th centered moment
    @simd for v_i in v
        z = v_i - m
        z2 = z * z
        cm2 += z2
        cm4 += z2 * z2
    end
    cm4 /= n
    cm2 /= n
    return (cm4 / (cm2 * cm2)) - 3.0
end

kurt(v) = kurt(v,mean(v))

"""
    autocorrelate(x,n=1;demean=true)

Compute the autocorrelation of lag `n` of `x`. `demean` defaults to true.
"""
function autocorrelate(x,n::Int=1; demean::Bool=true)
    T = typeof(zero(eltype(x)) / 1)
    z::Vector{T} = demean ? x .- mean(x) : x
    zz = dot(z, z)
    ac = @views dot(z[1+n:end],z[1:end-n]) / zz
    return ac
end

"""
    autocorrelate(f,x,n=1;demean=true)

Compute the autocorrelation of lag `n` of `f(x)`. `demean` defaults to true and takes place after `f` is applied.
"""
function autocorrelate(f::Function,x,n::Int=1;demean::Bool=true)
    y = f.(x)
    T = typeof(zero(eltype(y)) / 1)
    z::Vector{T} = demean ? y .- mean(y) : y
    zz = dot(z, z)
    ac = @views dot(z[1+n:end],z[1:end-n]) / zz
    return ac
end

## static kernels are terrible here for long windows, need some branching on n to happen
function varianceratio(x::AbstractVector{T},n::Int) where {T}
    n == 1 && return one(T)
    k = makekernel(sum,-(n-1):0)
    vr = varianceratio(x,k,n)
    return vr
end

function varianceratio(x::AbstractVector{T},k::Kernel,n::Int) where {T}
    v1 = var(x)
    summed = applyrolling(k,x)
    vn = @views var(summed[n:end])
    vr = vn / (n*v1)
    return vr
end

# the below are optimized versions for frequently used horizons
# uses beyond the first will not incur the cost of kernel construction (~250ns)
vr2(x) = varianceratio(x,makekernel(sum,-1:0),2)
vr4(x) = varianceratio(x,makekernel(sum,-3:0),4)
vr6(x) = varianceratio(x,makekernel(sum,-5:0),6)
vr8(x) = varianceratio(x,makekernel(sum,-7:0),8)
vr12(x) = varianceratio(x,makekernel(sum,-11:0),12)

"""
    conditionalcor(f,x,y)

Compute the correlation between `x` and `y` conditional on some event defined by the function `f(x,y)`.
The function `f` must return an object that can index into `x` and `y`.

__**Example**__
```
x = randn(1000)
y = randn(1000)
conditionalcor((xx,yy) -> xx .<= 0,x,y)
conditionalcor((xx,yy) -> (xx .<= 0) .& (yy .<= 0),x,y)
```
"""
function conditionalcor(f,x,y)
    loc = f(x,y)
    out = @views cor(x[loc],y[loc])
    return out
end

downsidecor(x,y;q=0.5) = conditionalcor((xx,yy) -> xx .<= quantile(x,q),x,y)

upsidecor(x,y;q=0.5) = conditionalcor((xx,yy) -> xx .>= quantile(x,q),x,y)

loggrowth(X,n=1; scal=100.0) = scal.*(log.(X) .- lag(log.(X),n))

"""
    correlogram(x,y;leadlags::Int=10)

Create a correlogram from `x` and `y`. By default, uses up to lag 10. Total output is `2*leadlags+1` in size.
If the plot peaks to the left of 0, `x` leads `y`.
"""
function correlogram(x,y;leadlags::Int=10)
    leadlagrange = -leadlags:leadlags
    N = length(leadlagrange)
    cors = zeros(N)
    k = 1
    for i in -leadlags:0
        cors[k] = @views cor(x[1:end+i],y[(-i+1):end])
        k += 1
    end

    for i in 1:leadlags
        cors[k] = @views cor(x[i+1:end],y[1:end-i])
        k += 1
    end
    return cors,leadlagrange
end


# Functions that implement formulae for the information measures

# Information measures are reviewed in:
# Timme, Nicholas; Alford, Wesley; Flecker, Benjamin; Beggs, John M. (2013-07-03).
# "Synergy, redundancy, and multivariate information measures: an experimentalist's perspective".
# Journal of Computational Neuroscience. 36 (2): 119–140.
# http://link.springer.com/article/10.1007%2Fs10827-013-0458-4

# TODO: Add further measures (dual total correlation, delta i)

export apply_entropy_formula, apply_conditional_entropy_formula, apply_mutual_information_formula,
	apply_conditional_mutual_information_formula, apply_interaction_information_formula,
	apply_total_correlation_formula, apply_specific_information_formula, apply_redundancy_formula,
	apply_unique_information_formula, apply_synergy_formula, apply_cross_entropy_formula

function remove_non_finite(x)
	return isfinite(x) ? x : zero(x)
end

function remove_negative(x)
	return x < 0 ? zero(x) : x
end

# Parameters:
# 	- probabilities, array of floats
# 	- base, number
function apply_entropy_formula(p::AbstractArray{T}, base::R) where {T <: AbstractFloat, R <: Real}
	return -sum(remove_non_finite.(p .* log.(base, p)))
end

# Parameters:
# 	- joint entropy of all variables, number
#	- entropy of conditioned variables, number
function apply_conditional_entropy_formula(entropy_xy, entropy_y)
	return entropy_xy - entropy_y
end

# Parameters:
# 	- joint probabilities, array of floats
# 	- probabilities (first variable), array of floats
# 	- probabilities (second variable), array of floats
# 	- base, number
function apply_mutual_information_formula(p_xy::AbstractArray{T}, p_x::AbstractArray{T}, p_y::AbstractArray{T}, base::R) where {T <: AbstractFloat, R <: Real}
	return sum(remove_non_finite.(p_xy .* log.(base, p_xy ./ (p_x .* p_y))))
end
# Parameters:
# 	- entropy of first variable, number
# 	- entropy of second variable, number
#	- joint entropy of both variables, number
function apply_mutual_information_formula(entropy_x, entropy_y, entropy_xy)
	return entropy_x + entropy_y - entropy_xy
end

# Parameters:
# 	- joint entropy of first and conditioned variables, number
# 	- joint entropy of second and conditioned variables, number
#	- joint entropy of all three variables, number
function apply_conditional_mutual_information_formula(entropy_xz, entropy_yz, entropy_xyz, entropy_z)
	return entropy_xz + entropy_yz - entropy_xyz - entropy_z
end

# Parameters:
# 	- mutual information of first two variables, number
# 	- conditional mutual information of first two variables on the third, number
function apply_interaction_information_formula(conditional_mutual_information, mutual_information)
	return conditional_mutual_information - mutual_information
end

# Parameters:
# 	- entropy of first variable, number
# 	- entropy of second variable, number
# 	- entropy of third variable, number
#	- joint entropy of all three variables, number
function apply_total_correlation_formula(entropy_x, entropy_y, entropy_z, entropy_xyz)
	return entropy_x + entropy_y + entropy_z - entropy_xyz
end

# Parameters:
# 	- joint probabilities, array of floats
# 	- probabilities (source), array of floats
# 	- probabilities (target), array of floats
# 	- dimension along which to sum, integer
# 	- base, number
function apply_specific_information_formula(p_xz, p_x, p_z, dim_sum, base)
	return vec(sum(remove_non_finite.((p_xz ./ p_z) .* log.(base, p_xz ./ (p_x .* p_z))), dims = dim_sum))
end

# Parameters:
# 	- joint probabilities (source 1 and target), array of floats
# 	- joint probabilities (source 2 and target), array of floats
# 	- probabilities (source 1), array of floats
# 	- probabilities (source 2), array of floats
# 	- probabilities (target), array of floats
# 	- dimensions along which to sum, tuple of integers
# 	- base, number
function apply_redundancy_formula(p_xz::AbstractArray{T}, p_yz::AbstractArray{T}, p_x::AbstractArray{T}, p_y::AbstractArray{T}, p_z::AbstractArray{T}, dim_sum::Tuple{I, I}, base::R) where {T <: AbstractFloat, R <: Real, I <: Integer}
	minimum_specific_information = min.(
		apply_specific_information_formula(p_xz, p_x, p_z, dim_sum[1], base),
		apply_specific_information_formula(p_yz, p_y, p_z, dim_sum[2], base)
	)
	return sum(vec(p_z) .* vec(minimum_specific_information))
end
# Parameters:
# 	- probabilities (target), array of floats
# 	- specific information of source 1 and target, array of floats
# 	- specific information of source 2 and target, array of floats
# 	- base, number
function apply_redundancy_formula(p_z::AbstractArray{T}, specific_information_1::AbstractArray{T}, specific_information_2::AbstractArray{T}, base::R) where {T <: AbstractFloat, R <: Real}
	minimum_specific_information = min.(specific_information_1, specific_information_2)
	return sum(vec(p_z) .* vec(minimum_specific_information))
end

# Parameters:
# 	- mutual information of source 1 and target, number
# 	- redundancy of both sources and target, number
function apply_unique_information_formula(mutual_information, redundancy)
	# Rounding errors may lead to slightly negative results
	return remove_negative.(mutual_information - redundancy)
end

# Parameters:
# 	- interaction information of both sources and target, number
# 	- redundancy of both sources and target, number
function apply_synergy_formula(interaction_information, redundancy)
	return interaction_information + redundancy
end

# Parameters:
#	- probabilities of first variable
#	- probabilities of second variable
#	- base of exponent
function apply_cross_entropy_formula(p_x::AbstractArray{T}, p_y::AbstractArray{T}, base::R) where {T <: AbstractFloat, R <: Real}
    return -sum(remove_non_finite.(p_x .* log.(base, p_y)))
end


function makeenv(;H = 0.8, dims=(100,100))
	sdm = rand(MidpointDisplacement(H), dims...)
end

function makesdm(;H = 0.8, dims=(100,100), cutoff= 0.75)
	sdm = rand(MidpointDisplacement(H), dims...)
	sdm = sdm ./ sum(sdm)
	ind = findall(x->x==1, sdm .<= quantile(vec(sdm), cutoff))
	sdm[ind] .= 0
	sdm = sdm ./ sum(sdm)
	sdm
end

function makeoccurrence(sdm; npts=100)
    occmat = zeros(size(sdm))
    for _ in 1:npts
        ind = rand(Categorical(vec(sdm)))
        occmat[ind] += 1
    end
    occmat
    return findall(i->i>0, occmat)
end

function makebon(sdm, n=50, padding=0.15)
	x,y = size(sdm)
	pts = rand((n,2)...)

	pts[:,1] = floor.((pts[:,1] .* (1-padding)*x) .+ (0.5padding*x))
	pts[:,2] = floor.((pts[:,2] .* (1-padding)*y) .+ (0.5padding*y))
	convert.(Int32,pts)
end


export General
export get_info, print_info, get_info_deep
# General Matching
# ----------------

# * All matching distances find optimal COMPLETE matching by default
# * To instead find general optimal matching one can construct the generalised distance

struct General{T<:CompleteMatchingDistance}
    d::T
end

function get_cost_matrix_fixed(
    d_gen::General{S},
    X::Vector{T}, Y::Vector{T}
    ) where {S<:MatchDist, T}

    d_g = d_gen.d.ground_dist
    N, M = (length(X), length(Y))
    K = N + M
    C = zeros(K, K)
    for j in 1:K
        for i in 1:K
            if (i ≤ N) & (j ≤ M)
                C[i,j] = d_g(X[i],Y[j])
            elseif (i ≤ N) & (j > M)
                C[i,j] = d_g(X[i],nothing) # Penalty for non-inclusion of X entry
            elseif (i > N) & (j ≤ M)
                C[i,j] = d_g(Y[j],nothing) # Penalty for non-inclusion of Y entry
            end
        end
    end
    return C
end

function get_cost_matrix_fixed(
    d_gen::General{S},
    X::Vector{T}, Y::Vector{T}
    ) where {S<:FixPenMatchDist, T}

    d_g = d_gen.d.ground_dist
    penalty = d_gen.d.penalty
    N, M = (length(X), length(Y))
    K = N + M
    C = fill(penalty, K, K)
    for j in 1:K
        for i in 1:K
            if (i ≤ N) & (j ≤ M)
                C[i,j] = d_g(X[i],Y[j])
            elseif (i > N) & (j > M)
                C[i,j] = 0.0
            end
        end
    end
    return C
end

function get_cost_matrix_fixed(
    d_gen::General{S},
    X::Vector{T}, Y::Vector{T}
    ) where {S<:AvgSizeMatchDist, T}

    d_g = d_gen.d.ground_dist
    penalty = d_gen.d.penalty
    N, M = (length(X), length(Y))
    K = N + M
    C = zeros(K, K)
    x_mean_size, y_mean_size = (
        mean(x->d_g(x,nothing), X),
        mean(y->d_g(y,nothing), Y)
    )
    for j in 1:K
        for i in 1:K
            if (i ≤ N) & (j ≤ M)
                C[i,j] = d_g(X[i],Y[j])
            elseif (i ≤ N) & (j > M)
                C[i,j] = abs(d_g(X[i],nothing)-y_mean_size) + penalty # Penalty for non-inclusion of X entry
            elseif (i > N) & (j ≤ M)
                C[i,j] = abs(d_g(Y[j],nothing)-x_mean_size) + penalty # Penalty for non-inclusion of Y entry
            end
        end
    end
    return C

end

function get_cost_matrix_fixed(
    d_gen::General{S},
    X::Vector{T}, Y::Vector{T}
    ) where {S<:MinDistMatchDist, T}

    d_g = d_gen.d.ground_dist
    penalty = d_gen.d.penalty
    N, M = (length(X), length(Y))
    # C_v = view(C, 1:N, 1:M)
    C = pairwise_inbounds(d_g, X, Y)
    x_min_vec, y_min_vec = (
        map(minimum, eachrow(C)),
        map(minimum, eachcol(C))
    )
    C = [
        C [x + penalty for x ∈ x_min_vec, i in 1:N] ;
        [y + penalty for i in 1:M, y ∈ y_min_vec] zeros(M, N)
    ]
    return C

end


using YaoBase, YaoArrayRegister
import StaticArrays: SMatrix
export RotationGate, Rx, Ry, Rz, rot

"""
    RotationGate{N, T, GT <: AbstractBlock{N, Complex{T}}} <: PrimitiveBlock{N, Complex{T}}

RotationGate, with GT both hermitian and isreflexive.

# Definition

```math
\\mathbf{I} cos(θ / 2) - im sin(θ / 2) * mat(U)
```
"""
mutable struct RotationGate{N, T <: Real, GT <: AbstractBlock{N}} <: PrimitiveBlock{N}
    block::GT
    theta::T
    function RotationGate{N, T, GT}(block::GT, theta) where {N, T, GT <: AbstractBlock{N}}
        #ishermitian(block) && isreflexive(block) ||:1
        #    throw(ArgumentError("Gate type $GT is not hermitian or not isreflexive." * " " * string(ishermitian(block)) * " " * string(isreflexive(block)) ))
        new{N, T, GT}(block, T(theta))
    end
end

RotationGate(block::GT, theta::T) where {N, T <: Real, GT<:AbstractBlock{N}} = RotationGate{N, T, GT}(block, theta)

# convert to float if theta is not a floating point
#RotationGate(block::AbstractBlock, theta) = RotationGate(block, Float64(theta))

# bindings
"""
    Rx(theta)

Return a [`RotationGate`](@ref) on X axis.

# Example

```jldoctest
julia> Rx(0.1)
rot(X gate, 0.1)
```
"""
Rx(theta) = RotationGate(X, theta)

"""
    Ry(theta)

Return a [`RotationGate`](@ref) on Y axis.

# Example

```jldoctest
julia> Ry(0.1)
rot(Y gate, 0.1)
```
"""
Ry(theta) = RotationGate(Y, theta)

"""
    Rz(theta)

Return a [`RotationGate`](@ref) on Z axis.

# Example

```jldoctest
julia> Rz(0.1)
rot(Z gate, 0.1)
```
"""
Rz(theta) = RotationGate(Z, theta)

"""
    rot(U, theta)

Return a [`RotationGate`](@ref) on U axis.
"""
rot(axis::AbstractBlock, theta) = RotationGate(axis, theta)

content(x::RotationGate) = x.block
# General definition
function mat(::Type{T}, R::RotationGate{N}) where {N, T}
    I = IMatrix{1<<N, T}()
    return I * cos(R.theta / 2) - im * sin(R.theta / 2) * mat(T, R.block)
    #return I * cos(T(R.theta) / 2) - im * sin(T(R.theta) / 2) * mat(T, R.block)
end

# Specialized
mat(::Type{T}, R::RotationGate{1, <:Any, <:XGate}) where T =
    T[cos(R.theta/2) -im * sin(R.theta/2); -im * sin(R.theta/2) cos(R.theta/2)]
mat(::Type{T}, R::RotationGate{1, <:Any, <:YGate}) where T =
     T[cos(R.theta/2) -sin(R.theta/2); sin(R.theta/2) cos(R.theta/2)]
# mat(R::RotationGate{1, T, ZGate{Complex{T}}}) where T =
#     SMatrix{2, 2, Complex{T}}(cos(R.theta/2)-im*sin(R.theta/2), 0, 0, cos(R.theta/2)+im*sin(R.theta/2))

function apply!(r::ArrayReg, rb::RotationGate)
    v0 = copy(r.state)
    apply!(r, rb.block)
    # NOTE: we should not change register's memory address,
    # or batch operations may fail
    r.state .= -im*Complex{typeof(rb.theta)}(sin(rb.theta/2))*r.state + Complex{typeof(rb.theta)}(cos(rb.theta/2))*v0
    #r.state .= -im*(sin(rb.theta/2))*r.state + (cos(rb.theta/2))*v0
    return r
end

# parametric interface
niparams(::Type{<:RotationGate}) = 1
getiparams(x::RotationGate) = x.theta
setiparams!(r::RotationGate, param::Real) = (r.theta = param; r)

YaoBase.isunitary(r::RotationGate) = true

Base.adjoint(blk::RotationGate) = RotationGate(blk.block, -blk.theta)
Base.copy(R::RotationGate) = RotationGate(R.block, R.theta)
Base.:(==)(lhs::RotationGate{TA, GTA}, rhs::RotationGate{TB, GTB}) where {TA, TB, GTA, GTB} = false
Base.:(==)(lhs::RotationGate{TA, GT}, rhs::RotationGate{TB, GT}) where {TA, TB, GT} = lhs.theta == rhs.theta

cache_key(R::RotationGate) = R.theta


using Correlate
using Test
using Statistics

# We have run correlate and want to see if the result is within tolerance.

function check_correlation(zdata::Array{Float64,2}, corm::Array{Float64,2}, tol::Float64)
  (nrows,ncols) = size(zdata)

  cor_after = cor(zdata)

  for c in 1:ncols
    for r in (c+1):ncols
      if abs(corm[r,c] - cor_after[r,c]) > tol
        return false
      end
    end
  end

  return true
end

function test1()
  data = rand(1000,3)
  corm = [1 0.2 -0.3; 0.2 1 0.85; -0.3 .85 1]
  tol = 0.01
  rc = correlate!(data, corm, tol)
  if ! rc
    return false
  end

  if ! check_correlation(data, corm, tol)
    return false
  end

  (nrows,ncols) = size(data)

# now reverse it

  for c in 1:ncols
    for r in 1:ncols
      if r != c
        corm[r,c] = - corm[r,c]
      end
    end
  end

# write(STDERR, "CORM $(corm)\n")

  if ! correlate!(data, corm, tol)
    return false
  end

  if ! check_correlation(data, corm, tol)
    return false
  end

# now send back to uncorrelated

  for c in 1:ncols
    for r in 1:ncols
      if r != c
        corm[r,c] = 0.0
      end
    end
  end

  if ! correlate!(data, corm, tol)
    return false
  end

  return check_correlation(data, corm, tol)
end

# write your own tests here
@test test1()


## Monomial
"""
    Monomial(exponents)

A `Monomial` function is a product of powers of variables with nonnegative
integer exponents.
```math
x_1^{p_1} * x_2^{p_2} *\\cdots * x_n^{p_n}
```
The `Monomial` is defined using a vector of integers `exponents`.
This vector contains the correspondent exponents ``p``
for all the inputs from `1` to `n`.
"""
struct Monomial <: Basis
    exponents::Vector{Int}
    n::Int
end

# Constructor
function Monomial(exponents::Vector{Int})
    n = length(exponents)
    Monomial(exponents, n)
end

# evaluate: Vector -> scalar
function evaluate(X::Vector{Float64}, monomial::Monomial)
    y = 1.0
    @inbounds @fastmath @simd for i = 1:monomial.n
        y *= X[i]^monomial.exponents[i]
    end
    return y
end

# evaluate!: Vector of Vectors -> Vector
function evaluate!(
        Y::Vector{Float64}, X::Vector{Vector{Float64}}, monomial::Monomial,
        N::Int, p::Int, update!::Function=identity!, args::Tuple=())

    @inbounds for i = 1:N
        Y[i] = 1.0
    end
    @inbounds for i = 1:p
        if monomial.exponents[i] == 1
            update!(X, i, N, args...)
            x = X[i]
            @simd for j = 1:N
                Y[j] *= x[j]
            end
        elseif monomial.exponents[i] > 0
            update!(X, i, N, args...)
            x = X[i]
            @simd for j = 1:N
                Y[j] *= x[j]^monomial.exponents[i]
            end
        end
    end
end

## Generate Monomials
function generate_all(::Type{Monomial}, n::Int, order::Int;)
    n_monomials = binomial(n+order-1, n-1)
    monomials = Vector{Monomial}(n_monomials)
    for (i, c) in enumerate(combinations(1:(n+order-1), n-1))
        combin = [0; c-collect(1:(n-1)); order]
        monomials[i] = Monomial(diff(combin))
    end
    return monomials
end

function generate_all(::Type{Monomial}, n::Int, order_list; kwargs...)
    vcat([generate_all(Monomial, n, order; kwargs...) for order in order_list]...)
end

# Print Monomial
function function_display(monomial::Monomial, names::Vector{String})
    str =""
    first_term = true
    for i = 1:monomial.n
        exp = monomial.exponents[i]
        if exp == 0
            continue
        end
        if ~first_term
            str = string(str, " * ")
        end
        first_term = false
        if exp == 1
            str = string(str, names[i])
        else
            str = string(str, names[i], "^", exp)
        end
    end
    return str
end

# Function Compare Monomials
Base.isequal(x::Monomial, y::Monomial) = x.exponents == y.exponents

# Check if contains input
function contains_inputs(m::Monomial, list_of_inputs::BitVector)
    return all(m.exponents[list_of_inputs] .== 0)
end


include("./Distribution.jl")
using LinearAlgebra

mutable struct Optimisation_problem
    #### This small structure is only to make Newton's method mor readable ####
    δ::Array{Float64, 1} # describes the step
    cost::Float64 # value of the cost function
end

function make_a_step!(dist::Distribution, prb::Optimisation_problem)
    #### modifies β, γ, M -- assumes the step δ is correct ####
    # getting the constants we will need
    n_pop = size(dist.J)[1]
    lmax_half = size(dist.J)[3]

    # let's perform the step
    dist.β -= prb.δ[1]
    dist.γ -= prb.δ[2]
    for l_half = 1:lmax_half
        for a = 1:n_pop
            line = (l_half - 1) * n_pop + a + 2
            dist.M[line - 2] -= prb.δ[line]
        end
    end
end

function update_Newton!(dist::Distribution, prb::Optimisation_problem)
    #### Compute the step of Newton's method, then makes it                                    ####
    #### Assumes that cost and jacobian have already been computed when we enter this function ####

    ldiv!(prb.δ, lu(dist.jacobian), dist.cost) # this computes the step to performs

    make_a_step!(dist, prb) # this makes the step and modifies β, γ, M

    Cost_and_Jacobian!(dist) # this is makes sure the cost and jacobian have been computed when we next call update_Newton!
    prb.cost = norm(dist.cost) # computes how close to the root we are AFTER the step
end

function Root_finder_Newton!(dist::Distribution)
    #### Performs the entire Newton method                                                                    ####
    #### However, it doesn't choose the initial point itself, this should be done by a wrapper to this script ####
    # getting the constants we will need
    n_pop = size(dist.J)[1]
    lmax_half = size(dist.J)[3]

    # preallocating an array we will need
    δ = zeros(lmax_half * n_pop + 2)

    # Let's choose a precision goal and an maximal number of iterations
    ϵ = 10^(-6) # maximum acceptable cost
    n_max = 100 # maximumm acceptable number of iterations

    # Computing the initial cost because the Newton step assumes that when the step begins the cost has already been computed
    Cost_and_Jacobian!(dist)
    cost = norm(dist.cost)

    # Initializing our small Optimization problem with this cost
    prb = Optimisation_problem(δ, cost)

    # Let's perform the Newton method now
    n = 0
    while ((n < n_max) && (prb.cost > ϵ))
        n += 1
        update_Newton!(dist, prb)
    end
end


type GrenanderEstimator <: LocalFdrEstimator
  pi0estimator:: Pi0Estimator
end

# saves everything in sorted way for now, change that later...
type GrenanderLocalFdrFit <: LocalFdrFit
  pvalues   :: Array{Float64,1}
  f         :: Array{Float64,1} #density
  F         :: Array{Float64,1} #distribution
  pi0       :: Float64
end

pvalues(fdrfit::GrenanderLocalFdrFit) = fdrfit.pvalues
pi0(fdrfit::GrenanderLocalFdrFit)  = fdrfit.pi0
distribution(fdrfit::GrenanderLocalFdrFit)    = fdrfit.F
density(fdrfit::GrenanderLocalFdrFit)    =    fdrfit.f
localfdr(fdrfit::GrenanderLocalFdrFit)   =    fdrfit.pi0./fdrfit.f #TODO make sure between [0,1]
tailfdr(fdrfit::GrenanderLocalFdrFit)    =    fdrfit.pi0.*fdrfit.pvalues./fdrfit.F #TODO make sure between [0,1]

function fit(pv:: Array{Float64,1}, fdrestimator::GrenanderEstimator)
  m = length(pv)
  pi0 = estimatepi0(pv, fdrestimator.pi0estimator)

  pv_ecdf_fun = ecdf(pv)          #going about it in quite inefficient way but this should not be the bottleneck anyway
  pv_sorted = sort(pv)
  ecdf_value = Array(Float64,m)

  # apply modification to make grenander estimate consistent with two-groups model
  for i=1:m
    ecdf_value[i] = pv_ecdf_fun(pv_sorted[i])
    if ecdf_value[i] < pi0*pv_sorted[i]
      ecdf_value[i] = pi0*pv_sorted[i]
    elseif ecdf_value[i] > 1-pi0*(1-pv_sorted[i])
      ecdf_value[i] = 1-pi0*(1-pv_sorted[i])
    end
  end

  #denote by (x,y) the coords of the modified ecdf
  Δx = diff(pv_sorted)
  Δy = float64(diff(ecdf_value))

  f = Δy./Δx #rough density estimate

  f = -isotonicregression(-f, Δx) # make previous density estimate obey monotonicity
  F  = ecdf_value[1] + vcat(0,cumsum(f .* Δx))
  f = push!(f, f[m-1])

  GrenanderLocalFdrFit(pv_sorted,f,F,pi0)
end


@testset "NormRel" begin
  Qx, x = PolynomialRing(FlintQQ, "x")
  f = x^8 - x^4 + 1
  K, a = NumberField(f, "a", cached = false)
  S = prime_ideals_up_to(maximal_order(K), 1000)
  class_group(maximal_order(K))
  C, mC = Hecke.sunit_group_fac_elem(S)
  Q, mQ = quo(C, 3)
  CC, mCC = Hecke.NormRel._sunit_group_fac_elem_quo_via_brauer(K, S, 3)
  elts = GrpAbFinGenElem[]
  for i in 1:ngens(CC)
    u = mCC(CC[i])
    push!(elts, mQ(mC\u))
  end
  V = sub(Q, elts)[1]
  @test order(V) == order(Q)
  S = prime_ideals_up_to(maximal_order(K), Hecke.factor_base_bound_grh(maximal_order(K)))
  c, U = Hecke.NormRel._sunit_group_fac_elem_quo_via_brauer(K, S, 2)

  # Test a non-normal group

  Qx, x = PolynomialRing(FlintQQ, "x");
  K, a = NumberField(x^4-2*x^2+9);
  OK = maximal_order(K);
  lP = NfOrdIdl[]
  push!(lP, ideal(OK, 43, OK(a^2 - 12)));
  push!(lP, ideal(OK, 47, OK(a^2 - 14*a + 3)));
  push!(lP, ideal(OK, 53, OK(a^2 - 7*a - 3)));
  push!(lP, ideal(OK, 5, OK(a^2 - 1*a + 2)));
  push!(lP, ideal(OK, 2, OK(1//12*a^3 + 1//4*a^2 + 7//12*a + 7//4)));
  push!(lP, ideal(OK, 3, OK(a^2 + 1)));
  push!(lP, ideal(OK, 3, OK(8*a^3 + 4*a^2 + 2*a + 6)));
  push!(lP, ideal(OK, 37, OK(a^2 + 12*a - 3)));
  push!(lP, ideal(OK, 41, OK(a-15)));
  push!(lP, ideal(OK, 5, OK(a^2 + 1*a + 2)))
  U, mU = Hecke.NormRel._sunit_group_fac_elem_quo_via_brauer(K, lP, 8)
  S, mS = Hecke.sunit_group_fac_elem(lP)
  Q, mQ = quo(S, 8)
  V = quo(Q, [mQ(mS\(mU(U[i]))) for i in 1:ngens(U)])
  @test order(V[1]) == 1

  # Test a non-normal number field (with a C2 x C2 subgroup of automorphisms)
  # We do not test the non-quotient S-unit group, since the saturation is
  # killing us (because of assertions to full level)
  f = x^8 - 8*x^6 + 80*x^4 + 512*x^2 + 1024
  K, a = NumberField(f)
  OK = lll(maximal_order(K))
  # S invariant
  lP = prime_ideals_up_to(OK, 50)
  U, mU = Hecke.NormRel._sunit_group_fac_elem_via_brauer(K, lP)
  S, mS = Hecke.sunit_group_fac_elem(lP)
  V = quo(S, [(mS\(mU(U[i]))) for i in 1:ngens(U)])
  @test order(V[1]) == 1
end


using Statistics
using Random

struct ReactionSystem
    N::Integer
    M::Integer
    c::Vector{T} where T <: Real
    h::Vector{Function}
    update_X::Vector{Vector{Tuple{Integer,Integer}}}
    update_a::Vector{Vector{Integer}}
end

function gillespie_algorithm(rs::ReactionSystem, Xi; tmax = 100.0, nmax = 500)
    t = 0.0
    n = 1
    ts = Vector{typeof(t)}(undef, nmax)
    ts[1] = t
    X = copy(Xi)
    Xs = Array{typeof(Xi[1]),2}(undef, nmax, rs.N)
    Xs[1,:] = X
    a = [rs.c[ν]*(rs.h[ν](X)) for ν in 1:rs.M]
    a0 = sum(a)
    while t < tmax && n < nmax && a0 > 0
        τ = log(1/rand())/a0
        r = rand()
        μ = 1
        s = a[μ]
        while r*a0 > s
            μ += 1
            s += a[μ]
        end
        # update X
        for (i,j) in rs.update_X[μ]
            X[i] += j
        end
        # update a
        for ν in rs.update_a[μ]
            a[ν] = rs.c[ν]*(rs.h[ν](X))
        end
        a0 = sum(a) # recompute the entire sum to avoid rounding errors
        ts[n+1] = t
        Xs[n+1,:] = X
        t += τ
        n += 1
    end
    return ts[1:n], Xs[1:n,:]
end

function SI(α::Real)
    rs = ReactionSystem(2, 1, [α],
    [X -> X[1]*X[2]],
    [[(1,-1),(2,1)]],
    [[1]]
    )
end

function LotkaVolterra(c)
    rs = ReactionSystem(3,3,c,
    [X -> X[1]*X[3],
     X -> X[1]*X[2],
     X -> X[2]],
    [[(1,1)],[(1,-1),(2,1)],[(2,-1)]],
    [[1],[1,2,3],[2,3]]
    )
end

function ClosedLotkaVolterra(c)
    rs = ReactionSystem(3,3,c,
    [X -> X[1]*X[3],
     X -> X[1]*X[2],
     X -> X[2]],
    [[(1,1),(3,-1)],[(1,-1),(2,1)],[(2,-1)]],
    [[1],[1,2,3],[2,3]]
    )
end

function Brusselator(c)
    rs = ReactionSystem(4,4,c,
    [X -> X[3],
     X -> X[4]*X[1],
     X -> X[2]*X[1]*(X[1]-1)/2,
     X -> X[1]],
     [[(1,1)], [(1,-1),(2,1)], [(1,1),(2,-1)], [(1,-1)]],
     [[2,3,4], [2,3,4], [2,3,4], [2,3,4]]
    )
end

using Plots

si = SI(0.5)
ts, Xs = gillespie_algorithm(si, [100,1])
plot(ts, Xs[:,1], line=:steppre)
scatter!(ts, Xs[:,1], markersize=1)
plot!(ts, 100*exp.(-0.5*ts))

X0 = 100000
lv = LotkaVolterra([10/X0,0.01,10])

ts, Xs = gillespie_algorithm(lv, [1000,1000,X0], nmax = 10^6)

plot(ts, Xs[:,1], line=:steppre)
plot!(ts, Xs[:,2], line=:steppre)

plot(Xs[:,1],Xs[:,2], linewidth=0.1, label="")

X1 = 100000
X2 = 100000
br = Brusselator([5000/X0, 50/X2, 0.00005, 5])

ts, Xs = gillespie_algorithm(br, [1000,2000,X1,X2], tmax=10000.0, nmax=2*10^6)

plot(ts, Xs[:,1], line=:steppre)
plot!(ts, Xs[:,2], line=:steppre)

plot(Xs[:,1],Xs[:,2], linewidth=0.1, label="")


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
#
#   Compute the sun position.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# References
#
#   [1] The Astronomical Almanac for the year 2000 (p. C24).
#
#   [2] http://aa.usno.navy.mil/faq/docs/SunApprox.php
#
#   [3] Vallado, D. A (2013). Fundamentals of Astrodynamics and Applications.
#       Microcosm Press, Hawthorne, CA.
#
#   [4] The Astronomical Almanac for the year 2006.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export sun_position_i

"""
    sun_position_i(JD::Number)

Compute the Sun position represented in the Mean Equinox of Date (MOD) at the
Julian Day `JD`. The algorithm was adapted from [3, p. 277-279].

"""
function sun_position_i(JD::Number)
    # Constants.
    deg2rad = π/180

    # Number of Julian centuries from J2000 epoch.
    T_UT1 = (JD-JD_J2000)/36525.0

    # Here, we will assume that T_TBD ≈ T_UT1. This can be done because we are
    # using a low-precision algorithm [3].
    T_TBD = T_UT1

    # Mean anomaly of the Sun [deg].
    Ms = 357.529_109_2 + 35_999.050_34T_TBD

    # Convert Ms to [rad] and limit to the interval [0,2π].
    Ms = mod2pi(Ms*deg2rad)

    # Compute auxiliary variables.
    sinMs,  cosMs  = sincos(Ms)
    sin2Ms, cos2Ms = sincos(2Ms)

    # Mean longitude of the Sun [deg].
    λ_m = 280.460 + 36_000.771T_UT1

    # Ecliptic latitude of the Sun [deg].
    λ_e = λ_m + 1.914_666_471sinMs + 0.019_994_643sin2Ms

    # Obliquity of the ecliptic [deg].
    ϵ = 23.439_291 - 0.013_004_2T_TBD

    # Convert λ_e and ϵ to [rad] and limit to the interval [0,2π].
    λ_e = mod2pi(λ_e*deg2rad)
    ϵ   = mod2pi(  ϵ*deg2rad)

    # Auxiliary variables.
    sinϵ   , cosϵ   = sincos(ϵ)
    sinλ_e , cosλ_e = sincos(λ_e)

    # Distance of the Sun from Earth [m].
    r = ( 1.000_140_612 - 0.016_708_617cosMs - 0.000_139_589cos2Ms )*au2m

    # Compute the Sun vector represented in the Mean Equinox of Date (MOD).
    S_MOD = SVector{3,Float64}(r*cosλ_e, r*cosϵ*sinλ_e, r*sinϵ*sinλ_e)

    # TODO: This vector must be transformed to the inertial reference frame that
    # will be used in this toolbox (J2000, True of Date, etc.).
end


# EXAMPLE: Robertson's reaction from pages 3-4 of Solving Ordinary Differential Equations II
#     (Stiff and Differential-Algebraic Problems)

struct RobertsonSystem
    unused_var::Float64  # to make generalization more obvious to readers
    de::Function
    function RobertsonSystem(de::Function)
        return new(1.0, de)
    end
end

function stiff_de!(xx::Vector{T}, x::Vector{T}, s::RobertsonSystem, t::Float64=0.0) where {T}
    c1 = 0.04
    c2 = 1.0e4
    c3 = 3.0e7
    y1 = x[1]
    y2 = x[2]
    y3 = x[3]
    xx[1] = -c1 * y1 + c2 * y2 * y3
    xx[2] =  c1 * y1 - c2 * y2 * y3 - c3 * y2^2
    xx[3] =                           c3 * y2^2
    return nothing
end

my_stiff_struct = RobertsonSystem(stiff_de!)
x0 = [1.0, 0.0, 0.0]  # see Eq. 1.4
initial_h = 1.0e-4

rr = makeRadauIntegrator(my_stiff_struct, x0, 1.0e-16, 2, 1)
update_h!(rr, initial_h)  # force it to start with h = 1.0e-4
rr.rule.s = 2  # force it to start with Radau5
rr.step.h_max = Inf  # don't want timesteps truncated
n_steps = 10

xx = [x0]  # to avoid putting the loop in a function
t = [0.0]  # to avoid putting the loop in a function
for k = 1:n_steps
    x0 = xx[1]
    h, x_final = solveRadau(rr, x0)
    xx[1] = x_final
    t[1] += h
end

println("time: ", t[1])
println("second term: ", xx[1][2])
println("compare to Fig 1.3")

@testset "Robertson's reaction" begin
    @test 3.45e-5 < xx[1][2] < 3.7e-5
    @test t[1] == initial_h * (2^(n_steps) - 1)  # it can at most double h every step
end


#*******************************************************************************
#
#
#                Aplicação do split-operator em um sistema 1D
#
#
#*******************************************************************************

include("save_the_data.jl")

# constantes físicas
Ry = 13.6056917253e3; # meV
a0 = 0.0529177208319; # nm
h2m = (Ry*a0^2)/0.067; # massa efetiva GaAs
hbar = 0.65821188926; # meV.ps

# definição da rede espacial
L = 1000;
Nx = 2*L;
x_grid = linspace(-L/2, L/2, Nx+1)
x_grid = x_grid[1:(end-1)];
Δx = x_grid[2] - x_grid[1];

# definição da rede do "espaço-recíproco"
k = fftshift(linspace(-pi/Δx, pi/Δx, Nx+1)[1:end-1]);
Δk = k[2]-k[1];

# Condição inicial
σ = 15 ;
k₀_max = pi/(2*Δx);
frac_max = 0.01; # fração do valor máximo de "k" que será usado na cond. inicial
k₀ = frac_max * k₀_max;  # o valor de "k₀" não pode ultrapassar π/(2*Δx)
x₀ = -200;
Ψ₀ = exp(-(x_grid-x₀).^2/(2*σ^2) + 1im*k₀*x_grid); # Gaussian packet

# Hamiltoniano separado em "T + V"
T_k = h2m * k.^2;

α, β, γ = 10, 0.07, 0;
V_x = α./(e.^(-β*(x_grid-γ))+1);

function psi_dt(ψ,Δt)
  # espaço de posições 'x'
  psi = ψ;
  psi = [exp(-1im*V_x[i]*Δt/(2*hbar))*psi[i] for i=1:Nx ];
  # espaço de momentos 'k'
  fft!(psi);
  psi = [ exp(-1im*T_k[i]*Δt/hbar)*psi[i] for i=1:Nx ];
  # espaço de posições 'x'
  ifft!(psi);
  psi = [exp(-1im*V_x[i]*Δt/(2*hbar))*psi[i] for i=1:Nx ];
  return psi;
end

function psit(Ψ; t_final = 10.0, N_steps = 100)
  tspan = linspace(0, t_final, N_steps);
  delta_t = tspan[2]-tspan[1];

  for i = 1:N_steps
    psi_t = psi_dt(Ψ,delta_t);
    Ψ = psi_t;
  end
  Ψ
end

t_f = 2.5;
ψ_1 = psit(Ψ_0, t_final = t_f, N_steps = 10);
nome_dados = string("psi_t_",round(t_f,2))

nome_arquivo = "dados_split_operator.h5";
nome_grupo = "split_1D";
nome_subg = "potencial_degrau";

atr_cond_ini = Dict("largura_pacote" => round(σ,1),
                        "x_0" => round(x₀,1),
                        "frac_k_max" => round(frac_max,3));

atr_pot = Dict("altura" => round(α,1),
                        "beta" => round(β,2),
                        "posição" => round(γ,1));

save_them_all( nome_arquivo,
               nome_grupo,
               nome_subg,
               ψ_1,
               nome_dados,
               atr_cond_ini,
               atr_pot);


using Dierckx

# Structure Refinement
function Refine(data::Matrix{T}, tmatch::Matrix{T}, ensize::Int64, qstart::T; population::Int64=10, maxiter::Int64=100000, reports::Int64=100) where T<:Real

    # Initialize
    local NGENE = size(tmatch, 2) - 1;
    local GENES = collect(1:NGENE);
    chromosomes = reshape(sample(GENES, ensize*population, replace=true), ensize, population);

    # Pre-process the data according to qstart
    id = findall(data[:,1] .> qstart)[1];
    expdata = data[id:end, :];

    # filter out Problematic data
    expdata = expdata[(expdata[:, 3] .!= 0), :]; # error = 0.0
    expdata[:, 2] = abs.(expdata[:, 2]);

    t = zeros(size(expdata,1), size(tmatch,2));
    [t[:,i] = Spline1D(tmatch[:, 1], tmatch[:, i]; k=1)(expdata[:,1]) for i = 1:size(tmatch, 2)];
    t[:,1] = expdata[:,1];

    # Fitness Value
    function fitness(data::Matrix{T}, t::Matrix{T}, chromosomes::Matrix{Int64}, population::Int64) where T<:Real
        ft = zeros(population);

        function logfit(iexp::Matrix{T}, ifit::Vector{T}) where T<:Real

            #σh = iexp[:,2] + iexp[:,3];
            #σl = iexp[:,2] - iexp[:,3];

            σ = iexp[:,3]./(iexp[:,2] * log(10));
            #σ[index_pos] = 0.5 * (log10.(σh[index_pos]) - log10.(σl[index_pos]));
            return mean(((log10.(iexp[:,2]) - log10.(ifit))./σ).^2);
        end

        [ft[i] = logfit(expdata, mean(t[:, chromosomes[:,i] .+ 1], dims=2)[:,1]) for i = 1:population];
        return ft;
    end

    # Selection
    function selection(chromosomes::Matrix{Int64}, ft::Vector{T}, population::Int64) where T<:Real
        m = Int64(floor(population/3));
        sele = sample(collect(1:population), ProbabilityWeights(exp.(-3.0*ft)), m, replace=false);
        parents = chromosomes[:, sele];
        return parents;
    end

    # Crossover
    function crossover(parents::Matrix{Int64}, ensize::Int64, population::Int64)
        child = Matrix{Int64}(undef, ensize, population);
        nparents = size(parents, 2);
        for i = 1:population
            p = sample(collect(1:nparents), 2);
            father, mother = parents[:, p[1]], parents[:, p[2]];
            sameindex = findall(indexin(father, mother) .!== nothing);
            m = length(sameindex);
            if m == 0
                child[:, i] = sample([father; mother], ensize, replace=false);
            else
                child[1:m, i] = father[sameindex];
                child[m+1:end, i] = sample(unique([father; mother]), ensize-m);
            end
        end
        return child;
    end

    # Mutation
    function mutation(child::Matrix{Int64}, ensize::Int64, population::Int64; rate::Float64=0.3)
        mutated_child = copy(child);
        for i = 1:population
            if rand() <= rate
                nmutation = sample(collect(1:ensize), ProbabilityWeights(1.0 ./collect(1:ensize)));
                mutated_child[sample(collect(1:ensize), nmutation, replace=false), i] = sample(GENES, nmutation, replace=false);
            end
        end
        return mutated_child;
    end

    # Main process
    best_fitness = Inf;
    best_chromosome = Vector{Int64}(undef, size(chromosomes, 1));

    tick = Int64(round(maxiter/reports));

    for i = 1:maxiter
        ft = fitness(expdata, t, chromosomes, population);
        parents = selection(chromosomes, ft, population);
        child = crossover(parents, ensize, population);
        mutated_child = mutation(child, ensize, population);

        m = minimum(ft);

        if m <= best_fitness
            best_fitness = m;
            best_chromosome = chromosomes[:, findall(ft.==m)];
        end

        chromosomes = mutated_child;
        i % tick == 0 ? println("Generation #$i: fitness = $(round(m, digits=5)), best fit so far = $(round(best_fitness, digits=5)).") : nothing;
    end
    println("Finished Genetic Algorithm, with best fitness = $(round(best_fitness, digits=5)).");
    return best_fitness, best_chromosome;
end


# an artificial example illustrating the necessity of an extra projection in the multi-output case
using Logging

using StructuralIdentifiability

logger = Logging.SimpleLogger(stdout, Logging.Info)
global_logger(logger)

ode = @ODEmodel(
    x1'(t) = (1 + x1(t)^2) // 2,
    x2'(t) = (1 - x1(t)^2) // (1 + x1(t)^2),
    y1(t) = 2 * x1(t) // (b * (1 + x1(t)^2)),
    y2(t) = x2(t)
)

@time println(assess_global_identifiability(ode))


# solve the problem
a = 0;
b = 1;
n = 9;
f = 2*ones(n)
problem = SparseFiniteDifferenceBVPProblem(a, b, n, f);
assemble_system!(problem);
u = solve_bvp(problem);

# computer error
err = @. problem.x * (1-problem.x) - u;
# check error
norm(err, Inf)< 1e-14


mutable struct Priors
  delta0
  delta1
  sig2
  W
  eta0
  eta1
  v
  H
  alpha
  eps
end

# NOTE: Uses Gamma(shape, scale) parameterization.

function Priors(;K::Int, L::Dict{Bool, Int}, use_stickbreak::Bool=false)
  if use_stickbreak
    v_prior = a -> Beta(a, oftype(a, 1))
  else
    v_prior = a -> Beta(a / K, oftype(a, 1))
  end

  return Priors(Gamma(1, 1),# delta0
                Gamma(1, 1), # delta1
                LogNormal(-1, .1), # sig2
                # Gamma(2, .1), # sig2
                Dirichlet(ones(K) / K), # W
                Dirichlet(ones(L[0]) / L[0]), # eta0
                Dirichlet(ones(L[1]) / L[1]), # eta1
                v_prior, # v
                Uniform(0, 1), # H
                Gamma(0.1, 10.0), # alpha
                Beta(1, 99)) # eps
end


using FractionalSystems, ControlSystems
using Test

@testset "Test Lyapunov exponents" begin
    function RF(du, u, t)
        du[1] = u[2]*(u[3]-1+u[1]*u[1])+0.1*u[1]
        du[2] = u[1]*(3*u[3]+1-u[1]*u[1])+0.1*u[2]
        du[3] = -2*u[3]*(0.98+u[1]*u[2])
    end

    (LE, tspan)=FOLyapunov(RF, 0.999, 0, 0.02, 300, [0.1; 0.1; 0.1], 0.005, 1000)

    # Test the latest computed(final) Lyapunov exponents
    @test isapprox(LE[end-2:end], [0.06111650166568285
    0.0038981396237095034
   -1.8324646820425692]; atol=1e-3)
end


function test_mapped_dicts()
    d = UnitCircle()
    m = parameterization(d)
    b = Fourier(20)
    c = mapped_dict(b, m)
    f1(x,y) = exp(x+im*y)
    u = approximate(c, f1)
    @test in_support(c, 1, SVector(1.0, 0.0))
    @test abs(u(SVector(cos(10),sin(10)))-f1(cos(10),sin(10))) < 1e-6
end


function logprior(state::StateAM, prior::PriorAM)
  lp = logpdf(prior.gamma, state.gammaC)
  lp += logpdf(prior.gamma, state.gammaT)
  lp += logpdf(prior.eta, state.etaC)
  lp += logpdf(prior.eta, state.etaT)
  lp += sum(logpdf.(prior.mu, state.mu))
  lp += sum(logpdf.(prior.sigma, state.sigma))
  lp += sum(logpdf.(prior.phi, state.phi))
  lp += sum(logpdf.(prior.nu, state.nu))
  return lp
end


@doc doc"""
bbands{Float64}(x::Vector{Float64}, n::Int64=10, sigma::Float64=2.0)

Bollinger Bands (moving average with standard deviation bands)
""" ->
function bbands{Float64}(x::Vector{Float64}, n::Int64=10, sigma::Float64=2.0; ma::Function=sma, args...)
    @assert n<size(x,1) && n>0 "Argument n is out of bounds."
    out = zeros(size(x,1), 3)  # cols := lower bound, ma, upper bound
    out[:,2] = ma(x, n; args...)
    sd = runsd(x, n, false)
    out[:,1] = out[:,2] - sigma*sd
    out[:,3] = out[:,2] + sigma*sd
    return out
end

@doc doc"""
tr{Float64}(hlc::Array{Float64,2})

True Range
""" ->
function tr{Float64}(hlc::Array{Float64,2})
    if size(hlc,2) != 3
        error("HLC array must have 3 columns.")
    end
    n = size(hlc,1)
    out = zeros(n)
    out[1] = NaN
    @inbounds for i=2:n
        out[i] = max(hlc[i,1]-hlc[i,2], hlc[i,1]-hlc[i-1,3], hlc[i-1,3]-hlc[i,2])
    end
    return out[:,1]
end

@doc doc"""
atr{Float64}(hlc::Array{Float64,2}, n::Int64=14)

Average true range (uses exponential moving average)
""" ->
function atr{Float64}(hlc::Array{Float64,2}, n::Int64=14; ma::Function=ema)
    @assert n<size(hlc,1) && n>0 "Argument n out of bounds."
    return [NaN; ma(tr(hlc)[2:end], n)]
end

@doc doc"""
keltner{Float64}(hlc::Array{Float64,2}, nema::Int64=20, natr::Int64=10, mult::Int64=2)

Keltner Bands
""" ->
function keltner{Float64}(hlc::Array{Float64,2}, nema::Int64=20, natr::Int64=10, mult::Int64=2)
    out = zeros(size(hlc,1), 3)
    out[:,2] = ema(hlc[:,3], nema)
    out[:,1] = out[:,2] - mult*atr(hlc, natr)
    out[:,3] = out[:,2] + mult*atr(hlc, natr)
    return out
end


using Random, Distributions, Plots, Measures, LaTeXStrings
Random.seed!(1)

n = 10^3
beta0, beta1, sigma = 2.0, 1.5, 2.5
eta = 10^-3

xVals = rand(0:0.01:5,n)
yVals = beta0 .+ beta1*xVals + rand(Normal(0,sigma),n)

pts, b = [], [0, 0]
push!(pts,b)
for k in 1:10^4
    i = rand(1:n)
    g = [   2(b[1] + b[2]*xVals[i]-yVals[i]),
            2*xVals[i]*(b[1] + b[2]*xVals[i]-yVals[i])  ]
    global b -= eta*g
    push!(pts,b)
end

p1 = plot(first.(pts),last.(pts), c=:black,lw=0.5,label="SGD path")
     scatter!([b[1]],[b[2]],c=:blue,ms=5,label="SGD")
     scatter!([beta0],[beta1],
        c=:red,ms=5,label="Actual",
        xlabel=L"\beta_0", ylabel=L"\beta_1",
        ratio=:equal, xlims=(0,2.5), ylims=(0,2.5))

p2 = scatter(xVals,yVals, c=:black, ms=1, label="Data points")
     plot!([0,5],[b[1],b[1]+5b[2]], c=:blue,label="SGD")
     plot!([0,5],[beta0,beta0+5*beta1],	c=:red, label="Actual",
            xlims=(0,5), ylims=(-5,15), xlabel = "x", ylabel = "y")

plot(p1, p2, legend=:topleft, size=(800, 400), margin = 5mm)


# Type and constructors
"""
	(type) Circle{T<:AnyComplex} in the complex plane

Each `Circle` type is parameterized according to the common type of its complex input arguments.
"""
struct Circle{T<:AnyComplex} <: AbstractClosedCurve
	center::T
	radius::Float64
	ccw::Bool
end
"""
	Circle(zc,r,ccw=true)

Construct the circle with given center `zc`, radius `r`, and orientation (defaults to counterclockwise).

	Circle(a,b,c)

Construct the circle passing through the given three numbers. Orientation is determined so that the values are visited in the given order. If the three points are collinear (including when one of the given values is infinite), a `Line` is returned instead.
"""
Circle(z::AnyComplex,r::Real,ccw::Bool=true) = Circle{typeof(z)}(z,r,ccw)
Circle(z::Number,r::Real,ccw::Bool=true) = Circle(complex(float(z)),r,ccw)

# Construction by three points
function Circle(a::Number,b::Number,c::Number)
	a,b,c = promote( complex.(float.([a,b,c]))... )
	Circle(a,b,c)
end
function Circle(a::T,b::T,c::T) where {T<:AnyComplex}
	isinf(a) && return Line(b,c)
	isinf(b) && return Line(c,a)
	isinf(c) && return Line(a,b)
	# Use intersection of chord bisectors to find the center of the circle.
	w = (a-c)/2
	d1,d2 = a-b,c-b
	M = SMatrix{2,2}(real(d1),imag(d1),real(d2),imag(d2))
	if cond(M) > 0.1/eps(typeof(float(real(a))))
		# Collinear points
		return Line(a,b)
	else
		p =  M \ SVector(imag(w),-real(w))
		cen = (a+b)/2 - 1im*p[1]*d1
		ccw = isccw(a-cen,b-cen,c-cen)
		return Circle{T}(cen,abs(a-cen),ccw)
	end
end

# Complex type converters
for ctype in [:Spherical,:Polar,:Complex]
	@eval begin
		function $ctype(C::Circle{T}) where T<:AnyComplex
			Circle($ctype(C.center),C.radius,C.ccw)
		end
	end
end

# Required methods
function point(C::Circle,t::Real)
	z = C.ccw ? exp(2im*pi*t) : exp(-2im*pi*t)
	C.center + typeof(C.center)(z*C.radius)
end
(C::Circle)(t::Real) = point(C,t)

ispositive(C::Circle) = C.ccw

arclength(C::Circle) = 2π*C.radius

"""
	arg(C::Circle,z)

Find the parameter argument `t` such that `C(t)==z` is true.

This gives undefined results if `z` is not actually on the circle.
"""
function arg(C::Circle,z::Number)
	α = angle(z-C.center)/(2π)
	C.ccw ? mod(α,1) : mod(-α,1)
end

function unittangent(C::Circle{T},t::Real) where T <: AnyComplex
	C.ccw ? T( 1im*exp(2im*pi*t) ) : T( -1im*exp(-2im*pi*t) )
end
function tangent(C::Circle{T},t::Real) where T <: AnyComplex
	T( 2π*C.radius*unittangent(C,t) )
end

+(C::Circle,z::Number) = Circle(C.center+z,C.radius,C.ccw)
-(C::Circle) = Circle(-C.center,C.radius,C.ccw)
*(C::Circle,z::Number) = Circle(C.center*z,C.radius*abs(z),C.ccw)

"""
	inv(C)
Invert the circle `C` through the origin. In general the inverse is a `Circle`, though the result is a `Line` if `C` passes through the origin.
"""
function inv(C::Circle)
	w = 1 ./ point(C,[0,0.25,0.5])
	Circle(w...)
end

# Other methods
isfinite(::Circle) = true
conj(C::Circle) = Circle(conj(C.center),C.radius,!C.ccw)
reverse(C::Circle) = Circle(C.center,C.radius,!C.ccw)

"""
	isapprox(C1::Circle,C2::Circle; tol=<default>)
	C1 ≈ C2
Determine if `C1` and `C2` represent the same circle, irrespective of the type or values of its parameters. Identity is determined by agreement within `tol`, which is interpreted as the weaker of absolute and relative differences.
"""
function isapprox(C1::Circle,C2::Circle;tol=DEFAULT[:tol])
	return isapprox(C1.center,C2.center,rtol=tol,atol=tol) &&
	isapprox(C1.radius,C2.radius,rtol=tol,atol=tol)
end

function winding(C::Circle,z::Number)
	w =  abs(z-C.center) < C.radius ? 1 : 0
	C.ccw ? w : -w
end

"""
	dist(z,C::Circle)
Compute the distance from number `z` to the circle `C`.
"""
dist(z::Number,C::Circle) = abs( abs(z-C.center) - C.radius )

"""
	closest(z,C::Circle)
Find the point on circle `C` that lies closest to `z`.
"""
closest(z::Number,C::Circle) =	C.center + C.radius*sign(z - C.center)

"""
	reflect(z,C::Circle)
Reflect the value `z` across the circle `C`. (For reflection of a circle through a point, use translation and negation.)
"""
function reflect(z::Number,C::Circle)
	ζ = z-C.center
	ζ==0 ? convert(typeof(float(z)),Inf) : C.center + C.radius^2/conj(ζ)
end

function show(io::IO,C::Circle)
	orient = C.ccw ? "ccw" : "cw"
	print(IOContext(io,:compact=>true),"Circle(",C.center,",",C.radius,",",orient,")")
end
function show(io::IO,::MIME"text/plain",C::Circle{T}) where {T}
	orient = C.ccw ? "positively" : "negatively"
	print(io,"Circle{$T} in the complex plane:\n   centered at (",C.center,") with radius $(C.radius), $(orient) oriented")
end


"""
    hydrolysis_cos(temp[, pH=7., seawater::Bool=false])

COS abiotic hydrolysis rate in natural waters. Applicable range: temperature
5--30 C and pH 4--10.

# Parameters

* `temp`: Temperature in Kelvin.
* `pH`: pH value. Default is 7.
* `seawater` (*optional*): If `true`, calculate COS hydrolysis rate in the
  seawater [RC93]. If `false` (default), caculate COS hydrolysis rate in the
  fresh water [E89].

# Returns

The abiotic hydrolysis rate constant of COS [s^-1].

# References

[E89] Elliott, S., Lu, E., and Rowland, F. S. (1989). Rates and
    mechanisms for the hydrolysis of carbonyl sulfide in natural waters.
    *Environ. Sci. Tech.*, 23(4), 458--461.
    <https://doi.org/10.1021/es00181a011>

[RC93] Radford-Knoery, J., and Cutter, G. A. (1993). Determination of
    carbonyl sulfide and hydrogen sulfide species in natural waters using
    specialized collection procedures and gas chromatography with flame
    photometric detection. *Anal. Chem.*, 65(8), 976--982.
    <https://doi.org/10.1021/ac00056a005>

# Examples

```jldoctest
julia> @printf "%g" hydrolysis_cos(288.15)
6.60184e-06

julia> @printf "%g" hydrolysis_cos(288.15, 9.)
3.64899e-05

julia> @printf "%g" hydrolysis_cos(288.15, 8.2, true)
1.40695e-05
```
"""
function hydrolysis_cos(temp, pH=7., seawater::Bool=false)
    T_ref = 298.15
    c_OH = 10. ^ (pH - water_dissoc(temp))
    t_diff = 1. / temp - 1. / T_ref

    # note: the parameter values used here differ from those in the original
    # publications due to a small correction for water pK_w
    if seawater
        # RC93 equation
        return 1.63838819502e-5 * exp(-6444.02904777 * t_diff) +
            11.7115101829 * exp(-2427.27401921 * t_diff) * c_OH
    else
        # E89 equation
        return 2.11834513803e-5 * exp(-10418.3722377 * t_diff) +
            14.16881179 * exp(-6469.11889197 * t_diff) * c_OH
    end
end


"""
Three-stage Runge-Kutta method of Zhao and Wei with parameter C=4
[Math. Meth. Appl. Sci. 2014, 37 1042–1071, figure 1b]
"""
@generated function timestep!(op::CollisionOperator{M,N,ℳ,𝒩,RT,CT}, u₀::Matrix{RT}, u₁::Matrix{RT}, Δt::RT) where {M,N,ℳ,𝒩,RT,CT}
    local u::Vector{Matrix{RT}} = [zeros(RT,M,N), zeros(RT,M,N), zeros(RT,M,N)]
    local f::Vector{Matrix{RT}} = [zeros(RT,M,N), zeros(RT,M,N), zeros(RT,M,N)]

    local a::Vector{RT} = [0.0, 0.5, 1.0]
    local b::Vector{RT} = [1/6, 2/3, 1/6]
    local c::Vector{RT} = [0.0, 0.5, 1.0]

    quote
        @assert size(u₀) == size(u₁) == (M,N)

        $u[1] .= u₀
        collision_operator!(op, $u[1], $f[1])

        update_field!($f[1], u₀, $u[2], Δt * $a[2])
        collision_operator!(op, $u[2], $f[2])

        update_field!($f[2], u₀, $u[3], Δt * $a[3])
        collision_operator!(op, $u[3], $f[3])

        update_field!($f[1], u₀, u₁, Δt * $b[1])
        update_field!($f[2], u₁, u₁, Δt * $b[2])
        update_field!($f[3], u₁, u₁, Δt * $b[3])

        u₁
    end
end

function update_field!(f::Matrix{RT}, u₀::Matrix{RT}, u₁::Matrix{RT}, fac::RT) where {RT}
    @assert size(f) == size(u₀) == size(u₁)
    @inbounds for j in 1:size(u₁,2)
        for i in 1:size(u₁,1)
            u₁[i,j] = u₀[i,j] + fac * f[i,j]
        end
    end
end


module Day17

export day17

function parseinput(::Val{3}, filename)
    n = length(readline(filename))
    space = zeros(Int,n+16,n+16,n+16)
    for (i,line) in enumerate(readlines(filename))
        for (j,ch) in enumerate(line)
            if ch == '.'
                space[7+i,7+j,8] = 0
            else
                space[7+i,7+j,8] = 1
            end
        end
    end
    space
end

function parseinput(::Val{4}, filename)
    n = length(readline(filename))
    space = zeros(Int,n+16,n+16,n+16, n+16)
    for (i,line) in enumerate(readlines(filename))
        for (j,ch) in enumerate(line)
            if ch == '.'
                space[7+i,7+j,8,8] = 0
            else
                space[7+i,7+j,8,8] = 1
            end
        end
    end
    space
end

function count_neighbours(::Val{3}, space, x,y,z)
    count = - space[x,y,z]
    for i in -1:1, j in -1:1, k in -1:1
        count += space[x+i, y+j, z+k]
    end
    count
end

function count_neighbours(::Val{4}, space, x,y,z,w)
    count = - space[x,y,z,w]
    for i in -1:1, j in -1:1, k in -1:1, s in -1:1
        count += space[x+i, y+j, z+k, w+s]
    end
    count
end

function simulate(::Val{3}, space)
    new_space = copy(space)
    for x in 2:size(space,1)-1, y in 2:size(space,2)-1, z in 2:size(space,3)-1
        n = count_neighbours(Val(3),space,x,y,z)
        if space[x,y,z] == 1 && n != 2 && n != 3
            new_space[x,y,z] = 0
        elseif space[x,y,z] == 0 && n == 3
            new_space[x,y,z] = 1
        end
    end
    new_space
end

function simulate(::Val{4}, space)
    new_space = copy(space)
    for x in 2:size(space,1)-1, y in 2:size(space,2)-1, z in 2:size(space,3)-1, w in 2:size(space,4)-1
        n = count_neighbours(Val(4),space,x,y,z,w)
        if space[x,y,z,w] == 1 && n != 2 && n != 3
            new_space[x,y,z,w] = 0
        elseif space[x,y,z,w] == 0 && n == 3
            new_space[x,y,z,w] = 1
        end
    end
    new_space
end

function day17(filename, dim)
    space = parseinput(Val(dim), filename)
    for cycle in 1:6
        space = simulate(Val(dim), space)
    end
    sum(space)
end

end


module PointGroups

using GenericSVD: svd
using NearestNeighbors: KDTree, nn
using StaticArrays: SArray, SVector, cross, det, norm, qr, svd
using Suppressor: @suppress

export cyclic_group, rotoreflection_group, pinwheel_group, pyramid_group,
    dihedral_group, prismatic_group, antiprismatic_group,
    chiral_tetrahedral_group, full_tetrahedral_group, pyritohedral_group,
    chiral_octahedral_group, full_octahedral_group,
    chiral_icosahedral_group, full_icosahedral_group,
    tetrahedron_vertices, tetrahedron_edge_centers, tetrahedron_face_centers,
    tetrahedron_rotoinversion_centers, pyritohedron_vertices,
    octahedron_vertices, octahedron_edge_centers, octahedron_face_centers,
    icosahedron_vertices, icosahedron_edge_centers, icosahedron_face_centers,
    CHIRAL_POLYHEDRAL_POINT_GROUPS, POLYHEDRAL_POINT_GROUPS,
    multiplication_table, count_central_elements, degenerate_orbits,
    isometric, isometries, identify_point_group

######################################################### 3D SYMMETRY OPERATIONS

function rotation_matrix_z(::Type{T}, i::Int, n::Int) where {T}
    _zero = zero(T)
    _one = one(T)
    if i % n == 0
        return SArray{Tuple{3,3},T,2,9}(
            +_one, _zero, _zero,
            _zero, +_one, _zero,
            _zero, _zero, +_one)
    elseif iseven(n) && (i % n) == (n >> 1)
        return SArray{Tuple{3,3},T,2,9}(
            -_one, _zero, _zero,
            _zero, -_one, _zero,
            _zero, _zero, +_one)
    end
    c = cospi(T(2*i) / T(n))
    s = sinpi(T(2*i) / T(n))
    return SArray{Tuple{3,3},T,2,9}(
        c, s, _zero, -s, c, _zero, _zero, _zero, _one)
end

function rotoreflection_matrix_z(::Type{T}, i::Int, n::Int) where {T}
    _zero = zero(T)
    _one = one(T)
    if i % n == 0
        return SArray{Tuple{3,3},T,2,9}(
            +_one, _zero, _zero,
            _zero, +_one, _zero,
            _zero, _zero, -_one)
    elseif iseven(n) && (i % n) == (n >> 1)
        return SArray{Tuple{3,3},T,2,9}(
            -_one, _zero, _zero,
            _zero, -_one, _zero,
            _zero, _zero, -_one)
    end
    c = cospi(T(2*i) / T(n))
    s = sinpi(T(2*i) / T(n))
    return SArray{Tuple{3,3},T,2,9}(
        c, s, _zero, -s, c, _zero, _zero, _zero, -_one)
end

function rotation_matrix_to_x(v::SVector{3,T}) where {T}
    _one = one(T)
    x, y, z = v
    if signbit(x)
        x, y, z = -x, -y, -z
    end
    xp1 = x + _one
    y2 = y * y
    yz = y * z
    z2 = z * z
    return SArray{Tuple{3,3},T,2,9}(
        x, -y, -z,
        y, _one - y2 / xp1, -yz / xp1,
        z, -yz / xp1, _one - z2 / xp1)
end

function rotation_matrix_to_y(v::SVector{3,T}) where {T}
    _one = one(T)
    x, y, z = v
    if signbit(y)
        x, y, z = -x, -y, -z
    end
    yp1 = y + _one
    x2 = x * x
    xz = x * z
    z2 = z * z
    return SArray{Tuple{3,3},T,2,9}(
        _one - x2 / yp1, x, -xz / yp1,
        -x, y, -z,
        -xz / yp1, z, _one - z2 / yp1)
end

function rotation_matrix_to_z(v::SVector{3,T}) where {T}
    _one = one(T)
    x, y, z = v
    if signbit(z)
        x, y, z = -x, -y, -z
    end
    zp1 = z + _one
    x2 = x * x
    xy = x * y
    y2 = y * y
    return SArray{Tuple{3,3},T,2,9}(
        _one - x2 / zp1, -xy / zp1, x,
        -xy / zp1, _one - y2 / zp1, y,
        -x, -y, z)
end

function rotation_matrix_x_pi(::Type{T}) where {T}
    _zero = zero(T)
    _one = one(T)
    return SArray{Tuple{3,3},T,2,9}(
        +_one, _zero, _zero,
        _zero, -_one, _zero,
        _zero, _zero, -_one)
end

function reflection_matrix_y(::Type{T}) where {T}
    _zero = zero(T)
    _one = one(T)
    return SArray{Tuple{3,3},T,2,9}(
        +_one, _zero, _zero,
        _zero, -_one, _zero,
        _zero, _zero, +_one)
end

############################################################# AXIAL POINT GROUPS

cyclic_group(::Type{T}, n::Int) where {T} =
    [rotation_matrix_z(T, i, n) for i = 0 : n-1]

function rotoreflection_group(::Type{T}, n::Int) where {T}
    @assert iseven(n)
    return [(iseven(i)
        ? rotation_matrix_z(T, i, n)
        : rotoreflection_matrix_z(T, i, n))
        for i = 0 : n-1]
end

pinwheel_group(::Type{T}, n::Int) where {T} = vcat(
    [rotation_matrix_z(T, i, n) for i = 0 : n-1],
    [rotoreflection_matrix_z(T, i, n) for i = 0 : n-1])

pyramid_group(::Type{T}, n::Int) where {T} = vcat(
    [rotation_matrix_z(T, i, n) for i = 0 : n-1],
    [rotation_matrix_z(T, i, n) * reflection_matrix_y(T) for i = 0 : n-1])

dihedral_group(::Type{T}, n::Int) where {T} = vcat(
    [rotation_matrix_z(T, i, n) for i = 0 : n-1],
    [rotation_matrix_z(T, i, n) * rotation_matrix_x_pi(T) for i = 0 : n-1])

prismatic_group(::Type{T}, n::Int) where {T} = vcat(
    [rotation_matrix_z(T, i, n) for i = 0 : n-1],
    [rotation_matrix_z(T, i, n) * rotation_matrix_x_pi(T) for i = 0 : n-1],
    [rotoreflection_matrix_z(T, i, n) for i = 0 : n-1],
    [rotation_matrix_z(T, i, n) * reflection_matrix_y(T) for i = 0 : n-1])

antiprismatic_group(::Type{T}, n::Int) where {T} = vcat(
    [rotation_matrix_z(T, i, n) for i = 0 : n-1],
    [rotoreflection_matrix_z(T, i, 2*n) for i = 1 : 2 : 2*n],
    [rotation_matrix_z(T, i, n) * reflection_matrix_y(T) for i = 0 : n-1],
    [rotoreflection_matrix_z(T, i, 2*n) * reflection_matrix_y(T)
     for i = 1 : 2 : 2*n])

######################################################## POLYHEDRAL POINT GROUPS

function chiral_tetrahedral_group(::Type{T}) where {T}
    M = SArray{Tuple{3,3},T,2,9}
    _zero = zero(T)
    _one = one(T)

    return [
        M(+_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, +_one),
        M(+_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, +_one),

        M(_zero, _zero, +_one, +_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, -_one, +_one, _zero, _zero, _zero, -_one, _zero),
        M(_zero, _zero, -_one, -_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, +_one, -_one, _zero, _zero, _zero, -_one, _zero),

        M(_zero, +_one, _zero, _zero, _zero, +_one, +_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, -_one, +_one, _zero, _zero),
        M(_zero, +_one, _zero, _zero, _zero, -_one, -_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, +_one, -_one, _zero, _zero),
    ]
end

function full_tetrahedral_group(::Type{T}) where {T}
    M = SArray{Tuple{3,3},T,2,9}
    _zero = zero(T)
    _one = one(T)

    return [
        M(+_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, +_one),
        M(+_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, +_one),

        M(+_one, _zero, _zero, _zero, _zero, +_one, _zero, +_one, _zero),
        M(+_one, _zero, _zero, _zero, _zero, -_one, _zero, -_one, _zero),
        M(-_one, _zero, _zero, _zero, _zero, +_one, _zero, -_one, _zero),
        M(-_one, _zero, _zero, _zero, _zero, -_one, _zero, +_one, _zero),

        M(_zero, _zero, +_one, +_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, -_one, +_one, _zero, _zero, _zero, -_one, _zero),
        M(_zero, _zero, -_one, -_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, +_one, -_one, _zero, _zero, _zero, -_one, _zero),

        M(_zero, +_one, _zero, +_one, _zero, _zero, _zero, _zero, +_one),
        M(_zero, -_one, _zero, +_one, _zero, _zero, _zero, _zero, -_one),
        M(_zero, -_one, _zero, -_one, _zero, _zero, _zero, _zero, +_one),
        M(_zero, +_one, _zero, -_one, _zero, _zero, _zero, _zero, -_one),

        M(_zero, +_one, _zero, _zero, _zero, +_one, +_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, -_one, +_one, _zero, _zero),
        M(_zero, +_one, _zero, _zero, _zero, -_one, -_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, +_one, -_one, _zero, _zero),

        M(_zero, _zero, +_one, _zero, +_one, _zero, +_one, _zero, _zero),
        M(_zero, _zero, -_one, _zero, -_one, _zero, +_one, _zero, _zero),
        M(_zero, _zero, +_one, _zero, -_one, _zero, -_one, _zero, _zero),
        M(_zero, _zero, -_one, _zero, +_one, _zero, -_one, _zero, _zero),
    ]
end

pyritohedral_group(::Type{T}) where {T} =
    vcat(+chiral_tetrahedral_group(T),
         -chiral_tetrahedral_group(T))

function chiral_octahedral_group(::Type{T}) where {T}
    M = SArray{Tuple{3,3},T,2,9}
    _zero = zero(T)
    _one = one(T)

    return [
        M(+_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, +_one),
        M(+_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, +_one),

        M(_zero, _zero, +_one, +_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, -_one, +_one, _zero, _zero, _zero, -_one, _zero),
        M(_zero, _zero, -_one, -_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, +_one, -_one, _zero, _zero, _zero, -_one, _zero),

        M(_zero, +_one, _zero, _zero, _zero, +_one, +_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, -_one, +_one, _zero, _zero),
        M(_zero, +_one, _zero, _zero, _zero, -_one, -_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, +_one, -_one, _zero, _zero),

        M(+_one, _zero, _zero, _zero, _zero, +_one, _zero, -_one, _zero),
        M(+_one, _zero, _zero, _zero, _zero, -_one, _zero, +_one, _zero),
        M(-_one, _zero, _zero, _zero, _zero, +_one, _zero, +_one, _zero),
        M(-_one, _zero, _zero, _zero, _zero, -_one, _zero, -_one, _zero),

        M(_zero, -_one, _zero, +_one, _zero, _zero, _zero, _zero, +_one),
        M(_zero, +_one, _zero, +_one, _zero, _zero, _zero, _zero, -_one),
        M(_zero, +_one, _zero, -_one, _zero, _zero, _zero, _zero, +_one),
        M(_zero, -_one, _zero, -_one, _zero, _zero, _zero, _zero, -_one),

        M(_zero, _zero, +_one, _zero, -_one, _zero, +_one, _zero, _zero),
        M(_zero, _zero, -_one, _zero, +_one, _zero, +_one, _zero, _zero),
        M(_zero, _zero, +_one, _zero, +_one, _zero, -_one, _zero, _zero),
        M(_zero, _zero, -_one, _zero, -_one, _zero, -_one, _zero, _zero),
    ]
end

full_octahedral_group(::Type{T}) where {T} =
    vcat(+chiral_octahedral_group(T),
         -chiral_octahedral_group(T))

function chiral_icosahedral_group(::Type{T}) where {T}
    M = SArray{Tuple{3,3},T,2,9}
    _zero = zero(T)
    _one = one(T)
    two = _one + _one
    four = two + two
    five = four + _one
    half = inv(two)
    quarter = inv(four)
    hphi = quarter * (sqrt(five) + _one)
    hpsi = quarter * (sqrt(five) - _one)

    return [
        # Identity matrix
        M(+_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, +_one),

        # 180-degree rotations about coordinate axes
        M(+_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, +_one, _zero, _zero, _zero, -_one),
        M(-_one, _zero, _zero, _zero, -_one, _zero, _zero, _zero, +_one),

        # 120-degree rotations about [±1, ±1, ±1]
        M(_zero, _zero, +_one, +_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, -_one, +_one, _zero, _zero, _zero, -_one, _zero),
        M(_zero, _zero, -_one, -_one, _zero, _zero, _zero, +_one, _zero),
        M(_zero, _zero, +_one, -_one, _zero, _zero, _zero, -_one, _zero),
        M(_zero, +_one, _zero, _zero, _zero, +_one, +_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, -_one, +_one, _zero, _zero),
        M(_zero, +_one, _zero, _zero, _zero, -_one, -_one, _zero, _zero),
        M(_zero, -_one, _zero, _zero, _zero, +_one, -_one, _zero, _zero),

        M(+half, +hphi, +hpsi, +hphi, -hpsi, -half, -hpsi, +half, -hphi),
        M(+half, +hphi, +hpsi, -hphi, +hpsi, +half, +hpsi, -half, +hphi),
        M(+half, +hphi, -hpsi, +hphi, -hpsi, +half, +hpsi, -half, -hphi),
        M(+half, +hphi, -hpsi, -hphi, +hpsi, -half, -hpsi, +half, +hphi),
        M(+half, -hphi, +hpsi, +hphi, +hpsi, -half, +hpsi, +half, +hphi),
        M(+half, -hphi, +hpsi, -hphi, -hpsi, +half, -hpsi, -half, -hphi),
        M(+half, -hphi, -hpsi, +hphi, +hpsi, +half, -hpsi, -half, +hphi),
        M(+half, -hphi, -hpsi, -hphi, -hpsi, -half, +hpsi, +half, -hphi),

        M(-half, +hphi, +hpsi, +hphi, +hpsi, +half, +hpsi, +half, -hphi),
        M(-half, +hphi, +hpsi, -hphi, -hpsi, -half, -hpsi, -half, +hphi),
        M(-half, +hphi, -hpsi, +hphi, +hpsi, -half, -hpsi, -half, -hphi),
        M(-half, +hphi, -hpsi, -hphi, -hpsi, +half, +hpsi, +half, +hphi),
        M(-half, -hphi, +hpsi, +hphi, -hpsi, +half, -hpsi, +half, +hphi),
        M(-half, -hphi, +hpsi, -hphi, +hpsi, -half, +hpsi, -half, -hphi),
        M(-half, -hphi, -hpsi, +hphi, -hpsi, -half, +hpsi, -half, +hphi),
        M(-half, -hphi, -hpsi, -hphi, +hpsi, +half, -hpsi, +half, -hphi),

        M(+hphi, +hpsi, +half, +hpsi, +half, -hphi, -half, +hphi, +hpsi),
        M(+hphi, +hpsi, +half, -hpsi, -half, +hphi, +half, -hphi, -hpsi),
        M(+hphi, +hpsi, -half, +hpsi, +half, +hphi, +half, -hphi, +hpsi),
        M(+hphi, +hpsi, -half, -hpsi, -half, -hphi, -half, +hphi, -hpsi),
        M(+hphi, -hpsi, +half, +hpsi, -half, -hphi, +half, +hphi, -hpsi),
        M(+hphi, -hpsi, +half, -hpsi, +half, +hphi, -half, -hphi, +hpsi),
        M(+hphi, -hpsi, -half, +hpsi, -half, +hphi, -half, -hphi, -hpsi),
        M(+hphi, -hpsi, -half, -hpsi, +half, -hphi, +half, +hphi, +hpsi),

        M(-hphi, +hpsi, +half, +hpsi, -half, +hphi, +half, +hphi, +hpsi),
        M(-hphi, +hpsi, +half, -hpsi, +half, -hphi, -half, -hphi, -hpsi),
        M(-hphi, +hpsi, -half, +hpsi, -half, -hphi, -half, -hphi, +hpsi),
        M(-hphi, +hpsi, -half, -hpsi, +half, +hphi, +half, +hphi, -hpsi),
        M(-hphi, -hpsi, +half, +hpsi, +half, +hphi, -half, +hphi, -hpsi),
        M(-hphi, -hpsi, +half, -hpsi, -half, -hphi, +half, -hphi, +hpsi),
        M(-hphi, -hpsi, -half, +hpsi, +half, -hphi, +half, -hphi, -hpsi),
        M(-hphi, -hpsi, -half, -hpsi, -half, +hphi, -half, +hphi, +hpsi),

        M(+hpsi, +half, +hphi, +half, -hphi, +hpsi, +hphi, +hpsi, -half),
        M(+hpsi, +half, +hphi, -half, +hphi, -hpsi, -hphi, -hpsi, +half),
        M(+hpsi, +half, -hphi, +half, -hphi, -hpsi, -hphi, -hpsi, -half),
        M(+hpsi, +half, -hphi, -half, +hphi, +hpsi, +hphi, +hpsi, +half),
        M(+hpsi, -half, +hphi, +half, +hphi, +hpsi, -hphi, +hpsi, +half),
        M(+hpsi, -half, +hphi, -half, -hphi, -hpsi, +hphi, -hpsi, -half),
        M(+hpsi, -half, -hphi, +half, +hphi, -hpsi, +hphi, -hpsi, +half),
        M(+hpsi, -half, -hphi, -half, -hphi, +hpsi, -hphi, +hpsi, -half),

        M(-hpsi, +half, +hphi, +half, +hphi, -hpsi, -hphi, +hpsi, -half),
        M(-hpsi, +half, +hphi, -half, -hphi, +hpsi, +hphi, -hpsi, +half),
        M(-hpsi, +half, -hphi, +half, +hphi, +hpsi, +hphi, -hpsi, -half),
        M(-hpsi, +half, -hphi, -half, -hphi, -hpsi, -hphi, +hpsi, +half),
        M(-hpsi, -half, +hphi, +half, -hphi, -hpsi, +hphi, +hpsi, +half),
        M(-hpsi, -half, +hphi, -half, +hphi, +hpsi, -hphi, -hpsi, -half),
        M(-hpsi, -half, -hphi, +half, -hphi, +hpsi, -hphi, -hpsi, +half),
        M(-hpsi, -half, -hphi, -half, +hphi, -hpsi, +hphi, +hpsi, -half),
    ]
end

full_icosahedral_group(::Type{T}) where {T} =
    vcat(+chiral_icosahedral_group(T),
         -chiral_icosahedral_group(T))

################################################### DEGENERATE POLYHEDRAL ORBITS

function tetrahedron_vertices(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _one = one(T)
    irt3 = inv(sqrt(_one + _one + _one))
    return [
        V(+irt3, +irt3, +irt3),
        V(+irt3, -irt3, -irt3),
        V(-irt3, +irt3, -irt3),
        V(-irt3, -irt3, +irt3),
    ]
end

function tetrahedron_edge_centers(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _one = one(T)
    _zero = zero(T)
    return [
        V(+_one, _zero, _zero),
        V(-_one, _zero, _zero),
        V(_zero, +_one, _zero),
        V(_zero, -_one, _zero),
        V(_zero, _zero, +_one),
        V(_zero, _zero, -_one),
    ]
end

function tetrahedron_face_centers(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _one = one(T)
    irt3 = inv(sqrt(_one + _one + _one))
    return [
        V(-irt3, -irt3, -irt3),
        V(-irt3, +irt3, +irt3),
        V(+irt3, -irt3, +irt3),
        V(+irt3, +irt3, -irt3),
    ]
end

function tetrahedron_rotoinversion_centers(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _zero = zero(T)
    _one = one(T)
    irt2 = inv(sqrt(_one + _one))
    return [
        V(_zero, +irt2, +irt2),
        V(_zero, +irt2, -irt2),
        V(_zero, -irt2, +irt2),
        V(_zero, -irt2, -irt2),
        V(+irt2, _zero, +irt2),
        V(-irt2, _zero, +irt2),
        V(+irt2, _zero, -irt2),
        V(-irt2, _zero, -irt2),
        V(+irt2, +irt2, _zero),
        V(+irt2, -irt2, _zero),
        V(-irt2, +irt2, _zero),
        V(-irt2, -irt2, _zero),
    ]
end

function pyritohedron_vertices(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _one = one(T)
    irt3 = inv(sqrt(_one + _one + _one))
    return [
        V(+irt3, +irt3, +irt3),
        V(+irt3, +irt3, -irt3),
        V(+irt3, -irt3, +irt3),
        V(+irt3, -irt3, -irt3),
        V(-irt3, +irt3, +irt3),
        V(-irt3, +irt3, -irt3),
        V(-irt3, -irt3, +irt3),
        V(-irt3, -irt3, -irt3),
    ]
end

octahedron_vertices(::Type{T}) where {T} =
    tetrahedron_edge_centers(T)

octahedron_edge_centers(::Type{T}) where {T} =
    tetrahedron_rotoinversion_centers(T)

octahedron_face_centers(::Type{T}) where {T} =
    pyritohedron_vertices(T)

function icosahedron_vertices(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _zero = zero(T)
    _one = one(T)
    two = _one + _one
    five = two + two + _one
    half = inv(two)
    sqrt_five = sqrt(five)
    phi = half * (sqrt_five + _one)
    psi = half * (sqrt_five - _one)
    qdrt_five = sqrt(sqrt_five)
    a = inv(qdrt_five * sqrt(phi))
    b = inv(qdrt_five * sqrt(psi))
    return [
        V(_zero, +b, +a),
        V(_zero, -b, +a),
        V(_zero, +b, -a),
        V(_zero, -b, -a),
        V(+a, _zero, +b),
        V(+a, _zero, -b),
        V(-a, _zero, +b),
        V(-a, _zero, -b),
        V(+b, +a, _zero),
        V(-b, +a, _zero),
        V(+b, -a, _zero),
        V(-b, -a, _zero),
    ]
end

function icosahedron_edge_centers(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _zero = zero(T)
    _one = one(T)
    two = _one + _one
    four = two + two
    five = four + _one
    half = inv(two)
    quarter = inv(four)
    hphi = quarter * (sqrt(five) + _one)
    hpsi = quarter * (sqrt(five) - _one)
    return [
        V(+_one, _zero, _zero),
        V(-_one, _zero, _zero),
        V(_zero, +_one, _zero),
        V(_zero, -_one, _zero),
        V(_zero, _zero, +_one),
        V(_zero, _zero, -_one),
        V(+half, +hphi, +hpsi),
        V(+half, +hphi, -hpsi),
        V(+half, -hphi, +hpsi),
        V(+half, -hphi, -hpsi),
        V(-half, +hphi, +hpsi),
        V(-half, +hphi, -hpsi),
        V(-half, -hphi, +hpsi),
        V(-half, -hphi, -hpsi),
        V(+hpsi, +half, +hphi),
        V(-hpsi, +half, +hphi),
        V(+hpsi, +half, -hphi),
        V(-hpsi, +half, -hphi),
        V(+hpsi, -half, +hphi),
        V(-hpsi, -half, +hphi),
        V(+hpsi, -half, -hphi),
        V(-hpsi, -half, -hphi),
        V(+hphi, +hpsi, +half),
        V(+hphi, -hpsi, +half),
        V(-hphi, +hpsi, +half),
        V(-hphi, -hpsi, +half),
        V(+hphi, +hpsi, -half),
        V(+hphi, -hpsi, -half),
        V(-hphi, +hpsi, -half),
        V(-hphi, -hpsi, -half),
    ]
end

function icosahedron_face_centers(::Type{T}) where {T}
    V = SArray{Tuple{3},T,1,3}
    _zero = zero(T)
    _one = one(T)
    three = _one + _one + _one
    five = three + _one + _one
    six = three + three
    irt3 = inv(sqrt(three))
    a = sqrt((three + sqrt(five)) / six)
    b = sqrt((three - sqrt(five)) / six)
    return [
        V(_zero, +b, +a),
        V(_zero, -b, +a),
        V(_zero, +b, -a),
        V(_zero, -b, -a),
        V(+a, _zero, +b),
        V(+a, _zero, -b),
        V(-a, _zero, +b),
        V(-a, _zero, -b),
        V(+b, +a, _zero),
        V(-b, +a, _zero),
        V(+b, -a, _zero),
        V(-b, -a, _zero),
        V(+irt3, +irt3, +irt3),
        V(+irt3, +irt3, -irt3),
        V(+irt3, -irt3, +irt3),
        V(+irt3, -irt3, -irt3),
        V(-irt3, +irt3, +irt3),
        V(-irt3, +irt3, -irt3),
        V(-irt3, -irt3, +irt3),
        V(-irt3, -irt3, -irt3),
    ]
end

const CHIRAL_POLYHEDRAL_POINT_GROUPS = [
    (chiral_tetrahedral_group, [tetrahedron_vertices,
        tetrahedron_edge_centers, tetrahedron_face_centers]),
    (chiral_octahedral_group, [octahedron_vertices,
        octahedron_edge_centers, octahedron_face_centers]),
    (chiral_icosahedral_group, [icosahedron_vertices,
        icosahedron_edge_centers, icosahedron_face_centers]),
]

const POLYHEDRAL_POINT_GROUPS = [
    (chiral_tetrahedral_group, [tetrahedron_vertices,
        tetrahedron_edge_centers, tetrahedron_face_centers]),
    (full_tetrahedral_group, [
        tetrahedron_vertices, tetrahedron_edge_centers,
        tetrahedron_face_centers, tetrahedron_rotoinversion_centers]),
    (pyritohedral_group, [
        pyritohedron_vertices, tetrahedron_edge_centers]),
    (chiral_octahedral_group, [octahedron_vertices,
        octahedron_edge_centers, octahedron_face_centers]),
    (full_octahedral_group, [octahedron_vertices,
        octahedron_edge_centers, octahedron_face_centers]),
    (chiral_icosahedral_group, [icosahedron_vertices,
        icosahedron_edge_centers, icosahedron_face_centers]),
    (full_icosahedral_group, [icosahedron_vertices,
        icosahedron_edge_centers, icosahedron_face_centers]),
]

####################################################### ABSTRACT GROUP STRUCTURE

function inf_norm(xs::AbstractArray{T}) where {T}
    result = zero(T)
    @simd ivdep for x in xs
        result = max(result, abs(x))
    end
    return result
end

function multiplication_table(
        group::Vector{SArray{Tuple{N,N},T,2,M}}) where {N,T,M}
    n = length(group)
    indices, dists = nn(
        KDTree(SVector{M,T}.(group)),
        [SVector{M,T}(g * h) for h in group for g in group])
    return (reshape(indices, n, n), inf_norm(dists))
end

function count_central_elements(mul_table::Matrix{Int})
    m, n = size(mul_table)
    @assert m == n
    return count(all(
        @inbounds mul_table[i,j] == mul_table[j,i]
        for j = 1 : n) for i = 1 : n)
end

################################################################ ORBIT STRUCTURE

function rotation_axis(mat::SArray{Tuple{3,3},T,2,9}, epsilon) where {T}
    @assert inf_norm(mat' * mat - one(mat)) <= epsilon
    u, s, v = @suppress svd(mat - sign(det(mat)) * one(mat))
    @assert s[1] > epsilon
    @assert s[2] > epsilon
    @assert zero(T) <= s[3] <= epsilon
    return v[:,3]
end

function connected_components(adjacency_lists::Dict{V,Vector{V}}) where {V}
    visited = Dict{V,Bool}()
    for (v, l) in adjacency_lists
        visited[v] = false
    end
    components = Vector{V}[]
    for (v, l) in adjacency_lists
        if !visited[v]
            visited[v] = true
            current_component = [v]
            to_visit = copy(l)
            while !isempty(to_visit)
                w = pop!(to_visit)
                if !visited[w]
                    visited[w] = true
                    push!(current_component, w)
                    append!(to_visit, adjacency_lists[w])
                end
            end
            push!(components, current_component)
        end
    end
    @assert allunique(vcat(components...))
    return components
end

function degenerate_orbits(group::Vector{SArray{Tuple{3,3},T,2,9}},
                           epsilon) where {T}
    Point = SArray{Tuple{3},T,1,3}
    points = Vector{Point}()
    for (i, g) in enumerate(group)
        try
            axis = rotation_axis(g, epsilon)
            push!(points, +axis)
            push!(points, -axis)
        catch e
            if !(e isa AssertionError)
                rethrow(e)
            end
        end
    end
    clusters = Vector{Tuple{Point,Vector{Point}}}()
    for point in points
        found = false
        for (center, cluster) in clusters
            if norm(point - center) <= epsilon
                found = true
                push!(cluster, point)
                break
            end
        end
        if !found
            push!(clusters, (point, [point]))
        end
    end
    @assert all(
        norm(p - q) <= epsilon
        for (_, cluster) in clusters
        for p in cluster for q in cluster)
    n = length(clusters)
    @assert all(
        @inbounds norm(clusters[i][1] - clusters[j][1]) > epsilon
        for i = 1 : n-1 for j = i+1 : n)
    adjacency_lists = Dict(i => Int[] for i = 1 : n)
    for (i, (center, _)) in enumerate(clusters)
        for g in group
            p = g * center
            dist, j = minimum(
                (norm(p - q), k)
                for (k, (q, _)) in enumerate(clusters))
            @assert dist <= epsilon
            if i != j
                @inbounds push!(adjacency_lists[i], j)
            end
        end
    end
    return [[@inbounds clusters[i][1] for i in comp]
            for comp in connected_components(adjacency_lists)]
end

############################################################# FINDING ISOMETRIES

function labeled_distances(points::Vector{SVector{N,T}}) where {T,N}
    return [(sqrt(sum((points[i] - points[j]).^2)), i, j)
            for j = 2 : length(points)
            for i = 1 : j-1]
end

function bucket_by_first(items::Vector{T}, epsilon) where {T}
    result = Vector{T}[]
    if length(items) == 0
        return result
    end
    push!(result, [items[1]])
    for i = 2 : length(items)
        if abs(items[i][1] - result[end][end][1]) <= epsilon
            push!(result[end], items[i])
        else
            push!(result, [items[i]])
        end
    end
    for bucket in result
        @assert abs(bucket[end][1] - bucket[1][1]) <=epsilon
    end
    return result
end

middle(x::AbstractVector) = x[(length(x) + 1) >> 1]

function candidate_isometries(
        a_points::Vector{SVector{3,T}},
        b_points::Vector{SVector{3,T}}, epsilon) where {T}
    _one = one(T)
    two = _one + _one
    four = two + two
    one_fourth = inv(four)
    seven_fourths = (four + two + _one) * one_fourth
    result = SArray{Tuple{3,3},T,2,9}[]
    a_buckets = bucket_by_first(sort!(labeled_distances(a_points)), epsilon)
    b_buckets = bucket_by_first(sort!(labeled_distances(b_points)), epsilon)
    if length.(a_buckets) != length.(b_buckets)
        return result
    end
    a_midpoints = [middle(bucket)[1] for bucket in a_buckets]
    b_midpoints = [middle(bucket)[1] for bucket in b_buckets]
    for (a, b) in zip(a_midpoints, b_midpoints)
        if !(abs(a - b) <= epsilon)
            return result
        end
    end
    selected_bucket = minimum(
        (length(bucket), abs(one(T) - middle(bucket)[1]), i)
        for (i, bucket) in enumerate(a_buckets)
        if one_fourth <= middle(bucket)[1] <= seven_fourths)[3]
    _, i, j = middle(a_buckets[selected_bucket])
    u1, v1 = a_points[i], a_points[j]
    pos_mat = inv(hcat(u1, v1, cross(u1, v1)))
    neg_mat = inv(hcat(u1, v1, cross(v1, u1)))
    for (_, k, l) in b_buckets[selected_bucket]
        u2, v2 = b_points[k], b_points[l]
        fwd_mat = hcat(u2, v2, cross(u2, v2))
        rev_mat = hcat(v2, u2, cross(v2, u2))
        push!(result, fwd_mat * pos_mat)
        push!(result, fwd_mat * neg_mat)
        push!(result, rev_mat * pos_mat)
        push!(result, rev_mat * neg_mat)
    end
    for mat in result
        @assert inf_norm(mat' * mat - one(mat)) <= epsilon
    end
    return result
end

function isometric(
        a_points::Vector{SVector{3,T}},
        b_points::Vector{SVector{3,T}}, epsilon) where {T}
    b_tree = KDTree(b_points)
    for mat in candidate_isometries(a_points, b_points, epsilon)
        indices, distances = nn(b_tree, [mat * p for p in a_points])
        if allunique(indices) && (inf_norm(distances) <= epsilon)
            return true
        end
    end
    return false
end

function isometries(
        a_points::Vector{SVector{3,T}},
        b_points::Vector{SVector{3,T}}, epsilon) where {T}
    result = SArray{Tuple{3,3},T,2,9}[]
    b_tree = KDTree(b_points)
    for mat in candidate_isometries(a_points, b_points, epsilon)
        indices, distances = nn(b_tree, [mat * p for p in a_points])
        if allunique(indices) && (inf_norm(distances) <= epsilon)
            push!(result, mat)
        end
    end
    return result
end

isometries(points::Vector{SVector{3,T}}, epsilon) where {T} =
    isometries(points, points, epsilon)

##################################################### POINT GROUP IDENTIFICATION

@enum MatrixType begin
    IdentityMatrix
    PureInversion
    PureRotation # 180-degree rotation
    PureReflection
    GeneralRotation
    GeneralRotoinversion
end

function identify_point_group(group::Vector{SArray{Tuple{3,3},T,2,9}},
                              epsilon) where {T}
    n = length(group)
    mul_table, dist = multiplication_table(group)
    @assert dist <= epsilon
    @assert (n, n) == size(mul_table)

    identity_matrix = one(SArray{Tuple{3,3},T,2,9})
    if n == 1
        return ("C_1", identity_matrix)
    end

    id_dist, id = minimum(
        (inf_norm(mat - identity_matrix), i)
        for (i, mat) in enumerate(group))
    @assert id_dist <= epsilon
    @assert issorted(view(mul_table, id, :))
    @assert issorted(view(mul_table, :, id))

    is_abelian = (mul_table == mul_table')

    order_table = zeros(Int, n)
    for g = 1 : n
        acc = g
        ord = 1
        while acc != id
            acc = mul_table[acc,g]
            ord += 1
        end
        order_table[g] = ord
    end
    @assert minimum(order_table) == 1
    max_order = maximum(order_table)
    is_cyclic = (max_order == n)

    _zero = zero(T)
    _one = one(T)
    two = _one + _one
    matrix_types = Vector{MatrixType}(undef, n)
    axes = zeros(SVector{3,T}, n)

    for (i, mat) in enumerate(group)
        u, s, v = @suppress svd(mat - one(mat); full=true)
        if inf_norm(s - [_zero, _zero, _zero]) <= epsilon
            @assert id == i
            @assert !signbit(det(mat))
            matrix_types[i] = IdentityMatrix
        elseif inf_norm(s - [two, two, two]) <= epsilon
            @assert signbit(det(mat))
            matrix_types[i] = PureInversion
        elseif inf_norm(s - [two, two, _zero]) <= epsilon
            @assert !signbit(det(mat))
            matrix_types[i] = PureRotation
            axes[i] = v[:,3]
        elseif inf_norm(s - [two, _zero, _zero]) <= epsilon
            @assert signbit(det(mat))
            matrix_types[i] = PureReflection
            axes[i] = v[:,1]
        elseif abs(s[1] - s[2]) <= epsilon
            @assert !signbit(det(mat))
            @assert s[3] <= epsilon
            matrix_types[i] = GeneralRotation
            axes[i] = v[:,3]
        else
            @assert signbit(det(mat))
            @assert abs(s[1] - two) <= epsilon
            @assert abs(s[2] - s[3]) <= epsilon
            matrix_types[i] = GeneralRotoinversion
            axes[i] = v[:,1]
        end
    end

    has_inversion = (PureInversion in matrix_types)
    has_pure_reflection = (PureReflection in matrix_types)
    is_chiral = (!has_inversion && !has_pure_reflection
                                && !(GeneralRotoinversion in matrix_types))

    principal_axes = [axes[i]
        for i = 1 : n
        if order_table[i] == max_order && (
            matrix_types[i] == PureRotation ||
            matrix_types[i] == GeneralRotation ||
            matrix_types[i] == GeneralRotoinversion)]
    if isempty(principal_axes)
        principal_axis = zero(SVector{3,T})
        axis_rotation_matrix = identity_matrix
    else
        principal_axis = first(principal_axes)
        axis_rotation_matrix = rotation_matrix_to_z(principal_axis)
    end

    if n == 2
        if is_chiral
            return ("C_2", axis_rotation_matrix)
        elseif PureInversion in matrix_types
            return ("C_i", identity_matrix)
        else
            return ("C_s", rotation_matrix_to_z(axes[3 - id]))
        end
    end

    orthogonal_rotation_indices = [i
        for i = 1 : n
        if (matrix_types[i] == PureRotation &&
            abs(principal_axis' * axes[i]) <= epsilon)]
    if !isempty(orthogonal_rotation_indices)
        orthogonal_axis =
            axis_rotation_matrix * axes[first(orthogonal_rotation_indices)]
        @assert abs(orthogonal_axis[3]) <= epsilon
        axis_rotation_matrix =
            rotation_matrix_to_x(orthogonal_axis) * axis_rotation_matrix
    end

    orthogonal_reflection_indices = [i
        for i = 1 : n
        if (matrix_types[i] == PureReflection &&
            abs(principal_axis' * axes[i]) <= epsilon)]
    if !isempty(orthogonal_reflection_indices)
        secondary_axis =
            axis_rotation_matrix * axes[first(orthogonal_reflection_indices)]
        @assert abs(secondary_axis[3]) <= epsilon
        secondary_rotation_matrix =
            rotation_matrix_to_y(secondary_axis) * axis_rotation_matrix
    end

    if n == 4
        if is_cyclic
            return (is_chiral ? "C_4" : "S_4", axis_rotation_matrix)
        elseif is_chiral
            return ("D_2", axis_rotation_matrix)
        elseif has_inversion
            return ("C_2h", axis_rotation_matrix)
        else
            return ("C_2v", secondary_rotation_matrix)
        end
    end

    has_max_order_negative_element = any(
        order_table[i] == max_order && (
            matrix_types[i] == PureInversion ||
            matrix_types[i] == PureReflection ||
            matrix_types[i] == GeneralRotoinversion)
        for i = 1 : n)

    if n == 8
        if is_cyclic
            return (is_chiral ? "C_8" : "S_8", axis_rotation_matrix)
        elseif is_chiral
            return ("D_4", axis_rotation_matrix)
        elseif is_abelian
            if max_order == 2
                return ("D_2h", axis_rotation_matrix)
            else
                @assert max_order == 4
                return ("C_4h", axis_rotation_matrix)
            end
        elseif has_max_order_negative_element
            return ("D_2d", secondary_rotation_matrix)
        else
            return ("C_4v", secondary_rotation_matrix)
        end
    end

    if is_cyclic
        @assert is_abelian
        if is_chiral
            return ("C_$n", axis_rotation_matrix)
        elseif PureReflection in matrix_types
            @assert iseven(n)
            @assert isodd(n >> 1)
            return ("C_$(n >> 1)h", axis_rotation_matrix)
        else
            @assert iseven(n)
            return ("S_$n", axis_rotation_matrix)
        end
    end

    if is_abelian
        @assert iseven(n)
        @assert iseven(n >> 1)
        return ("C_$(n >> 1)h", axis_rotation_matrix)
    end

    is_axial = (max_order >= 3) && all(
        abs(sqrt(abs(axes[i]' * axes[j])) - _one) <= epsilon
        for i = 1 : n, j = 1 : n
        if (order_table[i] == max_order &&
            order_table[j] == max_order))

    if is_axial
        @assert iseven(n)
        if is_chiral
            return ("D_$(n >> 1)", axis_rotation_matrix)
        elseif !has_max_order_negative_element
            refl_index = first(i for i = 1 : n
                               if matrix_types[i] == PureReflection)
            refl_axis = axis_rotation_matrix * axes[refl_index]
            return ("C_$(n >> 1)v",
                    rotation_matrix_to_y(refl_axis) * axis_rotation_matrix)
        else
            @assert iseven(n >> 1)
            if xor(iseven(n >> 2), PureInversion in matrix_types)
                return ("D_$(n >> 2)d", secondary_rotation_matrix)
            else
                return ("D_$(n >> 2)h", axis_rotation_matrix)
            end
        end
    else
        if n == 12
            return ("T", axis_rotation_matrix)
        elseif n == 48
            return ("O_h", axis_rotation_matrix)
        elseif n == 60
            return ("I", axis_rotation_matrix)
        elseif n == 120
            return ("I_h", axis_rotation_matrix)
        else
            @assert n == 24
            if is_chiral
                return ("O", axis_rotation_matrix)
            else
                return (PureInversion in matrix_types ? "T_h" : "T_d",
                        axis_rotation_matrix)
            end
        end
    end
end

###################################################### TEST POINT CONFIGURATIONS

function equatorial_points(height::T, offset::T, n::Int) where {T}
    _one = one(T)
    two = _one + _one
    a = sqrt(_one + height * height)
    return [SVector{3,T}(
            cospi(two * (T(i) + offset) / n),
            sinpi(two * (T(i) + offset) / n),
            height) / a
        for i = 0 : n-1]
end

function alternating_equatorial_points(height::T, offset::T, n::Int) where {T}
    @assert iseven(n)
    _one = one(T)
    two = _one + _one
    a = sqrt(_one + height * height)
    return [SVector{3,T}(
            cospi(two * (T(i) + offset) / n),
            sinpi(two * (T(i) + offset) / n),
            iseven(i) ? height : -height) / a
        for i = 0 : n-1]
end

cyclic_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(-inv(T(5)), inv(T(20)), n),
    equatorial_points(inv(T(10)), T(3)/T(20), n))

rotoreflection_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(zero(T), inv(T(5)), n),
    alternating_equatorial_points(inv(T(10)), inv(T(5)), n))

pinwheel_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(+inv(T(5)), inv(T(10)), n),
    equatorial_points(-inv(T(5)), inv(T(10)), n))

pyramid_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(inv(T(5)), zero(T), n),
    equatorial_points(zero(T), +inv(T(10)), n),
    equatorial_points(zero(T), -inv(T(10)), n))

dihedral_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(zero(T), +inv(T(10)), n),
    equatorial_points(zero(T), -inv(T(10)), n),
    equatorial_points(+inv(T(5)), +inv(T(10)), n),
    equatorial_points(-inv(T(5)), -inv(T(10)), n))

prismatic_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(zero(T), +inv(T(10)), n),
    equatorial_points(zero(T), -inv(T(10)), n),
    equatorial_points(+inv(T(5)), zero(T), n),
    equatorial_points(-inv(T(5)), zero(T), n))

antiprismatic_point_configuration(::Type{T}, n::Int) where {T} = vcat(
    equatorial_points(zero(T), zero(T), n),
    equatorial_points(zero(T), +inv(T(10)), n),
    equatorial_points(zero(T), -inv(T(10)), n),
    equatorial_points(+inv(T(5)), zero(T), n),
    equatorial_points(zero(T), inv(T(2)), n),
    equatorial_points(zero(T), T(2)/T(5), n),
    equatorial_points(zero(T), T(3)/T(5), n),
    equatorial_points(-inv(T(5)), inv(T(2)), n))

################################################ TEST POINT GROUP IDENTIFICATION

function matching_distance(a::Vector{SArray{X,T,R,N}},
                           b::Vector{SArray{X,T,R,N}}) where {X,T,R,N}
    if length(a) != length(b)
        return typemax(T)
    end
    a_points = SVector{N,T}.(a)
    b_points = SVector{N,T}.(b)
    b_tree = KDTree(b_points)
    idxs, dists = nn(b_tree, a_points)
    if !allunique(idxs)
        return typemax(T)
    else
        return maximum(dists)
    end
end

random_rotation(::Type{T}, ::Val{N}) where {T,N} =
    qr(SArray{Tuple{N,N},T,2,N*N}(randn(N, N))).Q

function test_alignment(
        points::Vector{SVector{3,T}},
        expected_group::Vector{SArray{Tuple{3,3},T,2,9}},
        epsilon) where {T}
    rotation = random_rotation(T, Val(3))
    rotated_points = [rotation * p for p in points]
    name, align = identify_point_group(
        isometries(rotated_points, epsilon),
        epsilon)
    @assert abs(det(align) - one(T)) <= epsilon
    dist = matching_distance(
        isometries([align * p for p in rotated_points], epsilon),
        expected_group)
    @assert dist <= epsilon
    return name
end

function test_axial_group_alignment(::Type{T}, n_max::Int,
                                    epsilon) where {T}
    cyclic_groups = [
        test_alignment(cyclic_point_configuration(T, n),
                       cyclic_group(T, n), epsilon)
        for n = 1 : n_max]
    @assert cyclic_groups == ["C_$n" for n = 1 : n_max]

    rotoreflection_groups = [
        test_alignment(rotoreflection_point_configuration(T, n),
                       rotoreflection_group(T, n), epsilon)
        for n = 2 : 2 : n_max]
    @assert rotoreflection_groups == [n == 2 ? "C_i" : "S_$n"
                                      for n = 2 : 2 : n_max]

    pinwheel_groups = [
        test_alignment(pinwheel_point_configuration(T, n),
                       pinwheel_group(T, n), epsilon)
        for n = 1 : n_max]
    @assert pinwheel_groups == [n == 1 ? "C_s" : "C_$(n)h"
                                for n = 1 : n_max]

    pyramid_groups = [
        test_alignment(pyramid_point_configuration(T, n),
                       pyramid_group(T, n), epsilon)
        for n = 2 : n_max]
    @assert pyramid_groups == ["C_$(n)v" for n = 2 : n_max]

    dihedral_groups = [
        test_alignment(dihedral_point_configuration(T, n),
                       dihedral_group(T, n), epsilon)
        for n = 2 : n_max]
    @assert dihedral_groups == ["D_$n" for n = 2 : n_max]

    prismatic_groups = [
        test_alignment(prismatic_point_configuration(T, n),
                       prismatic_group(T, n), epsilon)
        for n = 2 : n_max]
    @assert prismatic_groups == ["D_$(n)h" for n = 2 : n_max]

    antiprismatic_groups = [
        test_alignment(antiprismatic_point_configuration(T, n),
                       antiprismatic_group(T, n), epsilon)
        for n = 2 : n_max]
    @assert antiprismatic_groups == ["D_$(n)d" for n = 2 : n_max]
end

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

end # module PointGroups