using SphericalHarmonics
using SphericalHarmonicModes
using Test
using HCubature
using Aqua
using LegendrePolynomials
using WignerD
using OffsetArrays
using MultiplesOfPi
using ForwardDiff

using SphericalHarmonics: NorthPole,
SouthPole,
allocate_y,
allocate_p,
RealHarmonics,
ComplexHarmonics,
_lmax,
associatedLegendre,
associatedLegendrex,
Unnormalized,
LMnorm,
Orthonormal

@testset "project quality" begin
    Aqua.test_all(SphericalHarmonics, ambiguities = false)
end

@testset "allocate" begin
    lmax = 4
    @test size(allocate_y(lmax)) == size(allocate_y(ComplexF64, lmax))
    @test eltype(allocate_y(Complex{BigFloat}, lmax)) == Complex{BigFloat}
    @test eltype(allocate_y(BigFloat, lmax)) == BigFloat

    @test size(allocate_p(lmax)) == size(allocate_p(Float64, lmax))
    @test eltype(allocate_p(BigFloat, lmax)) == BigFloat
end

@testset "Ylm and Plm explicit" begin

    function explicit_shs_complex_fullrange(θ, φ)
        Y00 = 0.5 * sqrt(1/π)
        Y1m1 = 0.5 * sqrt(3/(2*π))*sin(θ)*exp(-im*φ)
        Y10 = 0.5 * sqrt(3/π)*cos(θ)
        Y11 = -0.5 * sqrt(3/(2*π))*sin(θ)*exp(im*φ)
        Y2m2 = 0.25 * sqrt(15/(2*π))*sin(θ)^2*exp(-2*im*φ)
        Y2m1 = 0.5 * sqrt(15/(2*π))*sin(θ)*cos(θ)*exp(-im*φ)
        Y20 = 0.25 * sqrt(5/π)*(3*cos(θ)^2 - 1)
        Y21 = -0.5 * sqrt(15/(2*π))*sin(θ)*cos(θ)*exp(im*φ)
        Y22 = 0.25 * sqrt(15/(2*π))*sin(θ)^2*exp(2*im*φ)
        Y3m3 = 1/8 * exp(-3 * im * φ) * sqrt(35/π) * sin(θ)^3
        Y3m2 = 1/4 * exp(-2 * im * φ) * sqrt(105/(2*π)) * cos(θ) * sin(θ)^2
        Y3m1 = 1/8 * exp(-im * φ) * sqrt(21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
        Y30 = 1/4 * sqrt(7/π) * (-3 * cos(θ) + 5 * cos(θ)^3)
        Y31 = -(1/8) * exp(im * φ) * sqrt(21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
        Y32 = 1/4 * exp(2 * im * φ) * sqrt(105/(2*π)) * cos(θ) * sin(θ)^2
        Y33 = -(1/8) * exp(3 * im * φ) * sqrt(35/π) * sin(θ)^3
        return [Y00, Y1m1, Y10, Y11, Y2m2, Y2m1, Y20, Y21, Y22,
               Y3m3, Y3m2, Y3m1, Y30, Y31, Y32, Y33]
    end

    function explicit_shs_complex_zeroto(θ, φ)
        Y00 = 0.5 * sqrt(1/π)
        Y10 = 0.5 * sqrt(3/π)*cos(θ)
        Y11 = -0.5 * sqrt(3/(2*π))*sin(θ)*exp(im*φ)
        Y20 = 0.25 * sqrt(5/π)*(3*cos(θ)^2 - 1)
        Y21 = -0.5 * sqrt(15/(2*π))*sin(θ)*cos(θ)*exp(im*φ)
        Y22 = 0.25 * sqrt(15/(2*π))*sin(θ)^2*exp(2*im*φ)
        Y30 = 1/4 * sqrt(7/π) * (-3 * cos(θ) + 5 * cos(θ)^3)
        Y31 = -(1/8) * exp(im * φ) * sqrt(21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
        Y32 = 1/4 * exp(2 * im * φ) * sqrt(105/(2*π)) * cos(θ) * sin(θ)^2
        Y33 = -(1/8) * exp(3 * im * φ) * sqrt(35/π) * sin(θ)^3
        return [Y00, Y10, Y11, Y20, Y21, Y22, Y30, Y31, Y32, Y33]
    end

    function explicit_associated_legendre(θ)
        # test only the unnormalized polynomials
        x = cos(θ)
        P00 = 1
        P10 = x
        P11 = -sin(θ)
        P20 = (3x^2 - 1)/2
        P21 = -3x*sin(θ)
        P22 = 3(1-x^2)

        [P00, P10, P11, P20, P21, P22]
    end

    @testset "associated legendre polynomials" begin
        for θ in LinRange(0, π, 100)
            P = computePlmcostheta(θ, 2, norm = Unnormalized())
            @test P ≈ explicit_associated_legendre(θ)
        end

        @testset "condon-shortley phase" begin
            θ = pi/3
            for normtype in [Unnormalized, LMnorm, Orthonormal]
                P = computePlmcostheta(θ, 2, norm = normtype())
                PnoCS = computePlmcostheta(θ, 2, norm = normtype(csphase = false))
                for (ind, (l,m)) in enumerate(ML(0:2, ZeroTo))
                    @test P[ind] ≈ (-1)^m * PnoCS[ind] atol=1e-14 rtol=1e-14
                end
            end
        end
    end

    @testset "complex harmonics" begin

        for θ in LinRange(0, π, 100), ϕ in LinRange(0, 2π, 200)
            Y = computeYlm(θ, ϕ, 3)
            Yex = explicit_shs_complex_fullrange(θ, ϕ)
            for (ind,mode) in enumerate(ML(0:3))
                @test Y[mode] ≈ Yex[ind]
            end

            Y = computeYlm(θ, ϕ, 3, ZeroTo)
            Yex = explicit_shs_complex_zeroto(θ, ϕ)
            for (ind,mode) in enumerate(ML(0:3, ZeroTo))
                @test Y[mode] ≈ Yex[ind]
            end
        end
    end

    @testset "real harmonics" begin
        function explicit_shs_real_fullrange(θ, φ)
            Y00 = 0.5 * sqrt(1/π)
            Y1m1 = -0.5 * sqrt(3/π)*sin(θ)*sin(φ)
            Y10 = 0.5 * sqrt(3/π)*cos(θ)
            Y11 = -0.5 * sqrt(3/π)*sin(θ)*cos(φ)
            Y2m2 = 0.25 * sqrt(15/π)*sin(θ)^2*sin(2*φ)
            Y2m1 = -0.5 * sqrt(15/π)*sin(θ)*cos(θ)*sin(φ)
            Y20 = 0.25 * sqrt(5/π)*(3*cos(θ)^2 - 1)
            Y21 = -0.5 * sqrt(15/π)*sin(θ)*cos(θ)*cos(φ)
            Y22 = 0.25 * sqrt(15/π)*sin(θ)^2*cos(2*φ)
            Y3m3 = -1/8 * sin(3 * φ) * sqrt(2*35/π) * sin(θ)^3
            Y3m2 = 1/4 * sin(2 * φ) * sqrt(105/π) * cos(θ) * sin(θ)^2
            Y3m1 = -1/8 * sin(φ) * sqrt(2*21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
            Y30 = 1/4 * sqrt(7/π) * (-3 * cos(θ) + 5 * cos(θ)^3)
            Y31 = -(1/8) * cos(φ) * sqrt(2*21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
            Y32 = 1/4 * cos(2 * φ) * sqrt(105/π) * cos(θ) * sin(θ)^2
            Y33 = -(1/8) * cos(3 * φ) * sqrt(2*35/π) * sin(θ)^3
            return [Y00, Y1m1, Y10, Y11, Y2m2, Y2m1, Y20, Y21, Y22,
                   Y3m3, Y3m2, Y3m1, Y30, Y31, Y32, Y33]
        end

        function explicit_shs_real_zeroto(θ, φ)
            Y00 = 0.5 * sqrt(1/π)
            Y10 = 0.5 * sqrt(3/π)*cos(θ)
            Y11 = -0.5 * sqrt(3/π)*sin(θ)*cos(φ)
            Y20 = 0.25 * sqrt(5/π)*(3*cos(θ)^2 - 1)
            Y21 = -0.5 * sqrt(15/π)*sin(θ)*cos(θ)*cos(φ)
            Y22 = 0.25 * sqrt(15/π)*sin(θ)^2*cos(2*φ)
            Y30 = 1/4 * sqrt(7/π) * (-3 * cos(θ) + 5 * cos(θ)^3)
            Y31 = -(1/8) * cos(φ) * sqrt(2*21/π) * (-1 + 5 * cos(θ)^2) * sin(θ)
            Y32 = 1/4 * cos(2 * φ) * sqrt(105/π) * cos(θ) * sin(θ)^2
            Y33 = -(1/8) * cos(3 * φ) * sqrt(2*35/π) * sin(θ)^3
            return [Y00, Y10, Y11, Y20, Y21, Y22, Y30, Y31, Y32, Y33]
        end

        for θ in LinRange(0, π, 100), ϕ in LinRange(0, 2π, 100)
            Y = computeYlm(θ, ϕ, 3, FullRange, SphericalHarmonics.RealHarmonics())
            Yex = explicit_shs_real_fullrange(θ, ϕ)
            for (ind,mode) in enumerate(ML(0:3))
                @test Y[mode] ≈ Yex[ind]
            end

            Y = computeYlm(θ, ϕ, 3, ZeroTo, SphericalHarmonics.RealHarmonics())
            Yex = explicit_shs_real_zeroto(θ, ϕ)
            for (ind,mode) in enumerate(ML(0:3, ZeroTo))
                @test Y[mode] ≈ Yex[ind]
            end
        end
    end

    @testset "Pole" begin
        @testset "NorthPole" begin
            Y = computeYlm(NorthPole(), 3)
            Yex = explicit_shs_complex_fullrange(0, 0)
            @test Y ≈ Yex

            Y = computeYlm(NorthPole(), 3, ZeroTo)
            Yex = explicit_shs_complex_zeroto(0, 0)
            @test Y ≈ Yex
        end
        @testset "SouthPole" begin
            Y = computeYlm(SouthPole(), 3)
            Yex = explicit_shs_complex_fullrange(π, 0)
            @test Y ≈ Yex
        end
        @testset "RealHarmonics == ComplexHarmonics" begin
            for x in (NorthPole(), SouthPole())
                YC = computeYlm(x, 4, FullRange, SphericalHarmonics.ComplexHarmonics())
                YR = computeYlm(x, 4, FullRange, SphericalHarmonics.RealHarmonics())
                @test YC == YR

                YC = computeYlm(x, 4, ZeroTo, SphericalHarmonics.ComplexHarmonics())
                YR = computeYlm(x, 4, ZeroTo, SphericalHarmonics.RealHarmonics())
                @test YC == YR
            end
        end
    end
end

@testset "computePlm!" begin
    lmax = 10
    θ = pi/3
    coeff = SphericalHarmonics.compute_coefficients(lmax);
    P = SphericalHarmonics.allocate_p(Float64, lmax);
    computePlmcostheta!(P, θ, lmax, coeff);

    @test P == computePlmcostheta(θ, lmax)

    computePlmcostheta!(P, NorthPole(), lmax, coeff);
    @test P == computePlmcostheta(NorthPole(), lmax)

    @testset "single m" begin
        computePlmcostheta!(P, θ, lmax, coeff);
        P2 = SphericalHarmonics.allocate_p(Float64, lmax);
        P3 = SphericalHarmonics.allocate_p(Float64, lmax);
        computePlmcostheta!(P3, θ, lmax, nothing, coeff);
        @test P3 ≈ P

        for l in 0:lmax, m in 0:l
            computePlmcostheta!(P2, θ, l, m, coeff);
            for l2 in m:l
                @test P2[(l2,m)] == P[(l2,m)]
            end
        end

        @test_throws ArgumentError computePlmcostheta!(P2, θ, 2, 3, coeff)
        @test_throws ArgumentError computePlmcostheta!(P2, θ, 2, -1, coeff)
    end
end

@testset "computePlmx and computePlmcostheta" begin
    θ = pi/3
    @test SphericalHarmonics.computePlmx(cos(θ), 4) ≈ computePlmcostheta(θ, 4)
    @test SphericalHarmonics.computePlmx(cos(θ), lmax = 4) ≈ computePlmcostheta(θ, 4)
    @test SphericalHarmonics.computePlmx(cos(θ), 4) ≈ computePlmcostheta(θ, lmax = 4)
    @test SphericalHarmonics.computePlmx(cos(θ), lmax = 4) ≈ computePlmcostheta(θ, lmax = 4)

    @test SphericalHarmonics.computePlmx(cos(θ), 4, 2) ≈ computePlmcostheta(θ, 4, 2)
    @test SphericalHarmonics.computePlmx(cos(θ), lmax = 4, m = 2) ≈ computePlmcostheta(θ, 4, 2)
    @test SphericalHarmonics.computePlmx(cos(θ), 4, 2) ≈ computePlmcostheta(θ, lmax = 4, m = 2)
    @test SphericalHarmonics.computePlmx(cos(θ), lmax = 4, m = 2) ≈ computePlmcostheta(θ, lmax = 4, m = 2)
end

@testset "computeYlm kwargs" begin
    θ, ϕ = pi/3, pi/3
    lmax = 10

    Y1 = SphericalHarmonics.computeYlm(θ, ϕ, lmax)
    Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax)
    Y3 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m_range = FullRange)
    Y4 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m_range = FullRange, SHType = SphericalHarmonics.ComplexHarmonics())
    @test Y1 == Y2 == Y3 == Y4

    for m = -lmax:lmax
        Y1 = SphericalHarmonics.computeYlm(θ, ϕ, lmax, m)
        Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m = m)
        Y3 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m = m, m_range = FullRange)
        Y4 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m = m, m_range = FullRange, SHType = SphericalHarmonics.ComplexHarmonics())
        @test Y1 == Y2 == Y3 == Y4
    end

    Y1 = SphericalHarmonics.computeYlm(θ, ϕ, lmax, ZeroTo)
    Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m_range = ZeroTo)
    Y3 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m_range = ZeroTo, SHType = SphericalHarmonics.ComplexHarmonics())
    @test Y1 == Y2 == Y3

    for m in 0:lmax
        Y1 = SphericalHarmonics.computeYlm(θ, ϕ, lmax, m, ZeroTo)
        Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m = m, m_range = ZeroTo)
        Y3 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m = m, m_range = ZeroTo, SHType = SphericalHarmonics.ComplexHarmonics())
        @test Y1 == Y2 == Y3
    end

    Y1 = SphericalHarmonics.computeYlm(θ, ϕ, lmax, ZeroTo, SphericalHarmonics.RealHarmonics())
    Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax = lmax, m_range = ZeroTo, SHType = SphericalHarmonics.RealHarmonics())
    @test Y1 == Y2
end

@testset "computeYlm!" begin
    θ, ϕ = pi/3, pi/3
    lmax = 10
    P = SphericalHarmonics.computePlmcostheta(θ, lmax)
    Y1 = SphericalHarmonics.allocate_y(ComplexF64, lmax)
    Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax)

    SphericalHarmonics.computeYlm!(Y1, θ, ϕ, lmax)
    @test Y1 ≈ Y2

    SphericalHarmonics.computeYlm!(Y1, θ, ϕ; lmax = lmax)
    @test Y1 ≈ Y2

    SphericalHarmonics.computeYlm!(Y1, P, θ, ϕ; lmax = lmax)
    @test Y1 ≈ Y2

    SphericalHarmonics.computeYlm!(Y1, θ, ϕ, lmax, ZeroTo)
    Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax, ZeroTo)
    @test Y1[1:length(ML(ZeroTo(lmax),ZeroTo))] ≈ Y2

    @testset "single m" begin
        Y2 = SphericalHarmonics.computeYlm(θ, ϕ, lmax)
        Ym = SphericalHarmonics.allocate_y(ComplexF64, lmax)

        for l in 0:lmax, m in -l:l
            computeYlm!(Ym, P, θ, ϕ, l, m)
            for l2 in abs(m):l
                @test Ym[(l2,m)] == Y2[(l2,m)]
            end
        end

        Y2 = SphericalHarmonics.computeYlm(θ, ϕ; lmax = lmax, SHType = SphericalHarmonics.RealHarmonics())
        Ym = SphericalHarmonics.allocate_y(Float64, lmax)

        for l in 0:lmax, m in -l:l
            computeYlm!(Ym, P, θ, ϕ, l, m, SphericalHarmonicModes.FullRange, SphericalHarmonics.RealHarmonics())
            for l2 in abs(m):l
                @test Ym[(l2,m)] == Y2[(l2,m)]
            end
        end

        @test_throws ArgumentError computeYlm!(Ym, P, θ, ϕ, 2, 3)
        @test_throws ArgumentError computeYlm!(Ym, P, θ, ϕ, 2, -3)
    end
end

@testset "Pole" begin
    @testset "utils" begin
        @test one(NorthPole()) == 1
        @test one(SouthPole()) == 1
        @test zero(NorthPole()) == 0
        @test zero(SouthPole()) == 0

        @test float(NorthPole()) == 0
        @test float(SouthPole()) == float(pi)

        @test Float64(NorthPole()) == 0
        @test Float64(SouthPole()) == float(pi)

        @test promote_rule(SouthPole, Float64) == Float64
        @test promote_rule(SouthPole, BigFloat) == BigFloat
    end

    @testset "North pole" begin
        @testset "Plm" begin
            for norm in [LMnorm(), Unnormalized(), Orthonormal()]
                @test computePlmcostheta(0, 10, norm = norm) ≈ computePlmcostheta(NorthPole(),10, norm = norm)
            end
            for (l,m) in ML(0:4, ZeroTo)
                @test associatedLegendre(0, l, m, norm = SphericalHarmonics.Unnormalized()) ≈ (m == 0) atol=1e-14 rtol=1e-8
            end
            P1 = computePlmcostheta(0, 10, norm = SphericalHarmonics.Unnormalized())
            P2 = computePlmx(1, 10, norm = SphericalHarmonics.Unnormalized())
            for (l,m) in ML(0:10, ZeroTo)
                @test P1[(l,m)] ≈ (m == 0) atol=1e-14 rtol=1e-8
                @test P2[(l,m)] ≈ (m == 0) atol=1e-14 rtol=1e-8
            end
        end
        @testset "computePlmcostheta!" begin
            lmax = 3
            P = SphericalHarmonics.allocate_p(lmax)
            for m = 0:lmax
                computePlmcostheta!(P, NorthPole(), lmax, m)

                P2 = computePlmcostheta(NorthPole(), lmax, m)
                P3 = computePlmcostheta(0, lmax, m)

                for l = m:lmax
                    @test P[(l,m)] == P2[(l,m)]
                    @test isapprox(P[(l,m)], P3[(l,m)], atol=1e-10, rtol=1e-10)
                end
            end
        end
        @testset "computeYlm!" begin
            lmax = 3
            Y = SphericalHarmonics.allocate_y(lmax)
            P = computePlmcostheta(NorthPole(), lmax)
            computeYlm!(Y, P, NorthPole(), 0, lmax)
            Y2 = computeYlm(NorthPole(), 0, lmax)
            @test all(Y ≈ Y2)

            for m in -lmax:lmax
                computeYlm!(Y, P, NorthPole(), 0, lmax, m)
                Y2 = computeYlm(NorthPole(), 0, lmax, m)
                Y3 = computeYlm(0, 0, lmax, m)
                for l in abs(m):lmax
                    @test Y[(l,m)] == Y2[(l,m)]
                    @test isapprox(Y[(l,m)], Y3[(l,m)], atol=1e-10, rtol=1e-10)
                end
            end
        end
        @testset "Ylm" begin
            Y0 = computeYlm(0,0,10,ZeroTo)
            Y0R = computeYlm(0,0,10,ZeroTo,RealHarmonics())
            Y1 = computeYlm(0,0,10)
            @test Y1 ≈ computeYlm(NorthPole(),10)
            @test Y1 ≈ computeYlm(NorthPole(),lmax = 10)
            @test Y1 ≈ computeYlm(NorthPole(),π/2,10)
            @test Y0 ≈ computeYlm(NorthPole(),10,ZeroTo)
            @test Y0 ≈ computeYlm(NorthPole(),π/2,10,ZeroTo)
            @test Y0R ≈ computeYlm(NorthPole(),10,ZeroTo,RealHarmonics())
            @test Y0R ≈ computeYlm(NorthPole(),π/2,10,ZeroTo,RealHarmonics())
        end
        @testset "trignometric functions" begin
            @test cos(NorthPole()) == 1
            @test sec(NorthPole()) == 1
            @test sin(NorthPole()) == 0
        end
    end
    @testset "south pole" begin
        @testset "Plm" begin
            @test computePlmcostheta(π,10) ≈ computePlmcostheta(SouthPole(),10)
        end
        @testset "computePlmcostheta!" begin
            lmax = 3
            P = SphericalHarmonics.allocate_p(lmax)

            for m = 0:lmax
                computePlmcostheta!(P, SouthPole(), lmax, m)

                P2 = computePlmcostheta(SouthPole(), lmax, m)
                P3 = computePlmcostheta(π, lmax, m)
                for l = m:lmax
                    @test P[(l,m)] == P2[(l,m)]
                    @test isapprox(P[(l,m)], P3[(l,m)], atol=1e-10, rtol=1e-10)
                end
            end
        end
        @testset "computeYlm!" begin
            lmax = 3
            Y = SphericalHarmonics.allocate_y(lmax)
            P = computePlmcostheta(SouthPole(), lmax)
            computeYlm!(Y, P, SouthPole(), 0, lmax)
            Y2 = computeYlm(SouthPole(), 0, lmax)
            @test all(Y ≈ Y2)

            for m = -lmax:lmax
                computeYlm!(Y, P, SouthPole(), 0, lmax, m)
                Y2 = computeYlm(SouthPole(), 0, lmax, m)
                Y3 = computeYlm(π, 0, lmax, m)
                for l in abs(m):lmax
                    @test Y[(l,m)] == Y2[(l,m)]
                    @test isapprox(Y[(l,m)], Y2[(l,m)], atol=1e-10, rtol=1e-10)
                end
            end
        end
        @testset "Ylm" begin
            @test computeYlm(π,0,10) ≈ computeYlm(SouthPole(),10)
            @test computeYlm(π,0,10) ≈ computeYlm(SouthPole(),π/2,10)
            @test computeYlm(π,0,10,ZeroTo) ≈ computeYlm(SouthPole(),10,ZeroTo)
            @test computeYlm(π,0,10,ZeroTo) ≈ computeYlm(SouthPole(),π/2,10,ZeroTo)
            @test computeYlm(π,0,10,ZeroTo,RealHarmonics()) ≈ computeYlm(SouthPole(),10,ZeroTo,RealHarmonics())
            @test computeYlm(π,0,10,ZeroTo,RealHarmonics()) ≈ computeYlm(SouthPole(),π/2,10,ZeroTo,RealHarmonics())
        end
        @testset "trignometric functions" begin
            @test cos(SouthPole()) == -1
            @test sec(SouthPole()) == -1
            @test sin(SouthPole()) == 0
        end
    end
end

@testset "precision" begin

    isapproxdefault((x,y); kw...) = isapprox(ComplexF64(x), y; kw...)

    @testset "θ = pi/2" begin
        P = computePlmcostheta(big(pi)/2, lmax = big(10))
        @test P[(1,1)] ≈ -(√(3/big(pi)))/2
        @test P[(2,2)] ≈ (√(15/big(pi)))/4
        @test P[(3,3)] ≈ -(√(35/(2big(pi))))/4
        @test P[(4,4)] ≈ (√(35/big(pi)))*3/16
        @test P[(5,5)] ≈ -(√(77/(2big(pi))))*3/16
        @test P[(6,6)] ≈ (√(3003/(2big(pi))))/32
        @test P[(7,7)] ≈ -(√(715/big(pi)))*3/64
        @test P[(8,8)] ≈ (√(12155/big(pi)))*3/256
        @test P[(9,9)] ≈ -(√(230945/(2big(pi))))/256
        @test P[(10,10)] ≈ (√(969969/(2big(pi))))/512
    end

    θ_range_big = LinRange(0, big(pi), 20)
    ϕ_range_big = LinRange(0, 2big(pi), 5)

    @testset "lmax 1000" begin
        lmax = 1000

        coeff_big = SphericalHarmonics.compute_coefficients(big(lmax))
        coeff_Float64 = SphericalHarmonics.compute_coefficients(lmax)

        Y1 = SphericalHarmonics.allocate_y(Complex{BigFloat}, lmax)
        Y2 = SphericalHarmonics.allocate_y(Complex{Float64}, lmax)

        P1 = SphericalHarmonics.allocate_p(BigFloat, lmax)
        P2 = SphericalHarmonics.allocate_p(Float64, lmax)

        for θ in θ_range_big
            computePlmcostheta!(P1, θ, lmax, coeff_big)
            computePlmcostheta!(P2, Float64(θ), lmax, coeff_Float64)
            for ϕ in ϕ_range_big
                computeYlm!(Y1, P1, θ, ϕ, lmax)
                computeYlm!(Y2, P2, Float64(θ), Float64(ϕ), lmax)
                @test all(x -> isapproxdefault(x, atol=1e-11,  rtol=1e-11), zip(Y1, Y2))
            end
        end
    end
    @testset "lmax 100" begin
        lmax = 100

        coeff_big = SphericalHarmonics.compute_coefficients(big(lmax))
        coeff_Float64 = SphericalHarmonics.compute_coefficients(lmax)

        Y1 = SphericalHarmonics.allocate_y(Complex{BigFloat}, lmax)
        Y2 = SphericalHarmonics.allocate_y(Complex{Float64}, lmax)

        P1 = SphericalHarmonics.allocate_p(BigFloat, lmax)
        P2 = SphericalHarmonics.allocate_p(Float64, lmax)

        for θ in θ_range_big
            computePlmcostheta!(P1, θ, lmax, coeff_big)
            computePlmcostheta!(P2, Float64(θ), lmax, coeff_Float64)
            for ϕ in ϕ_range_big
                computeYlm!(Y1, P1, θ, ϕ, lmax)
                computeYlm!(Y2, P2, Float64(θ), Float64(ϕ), lmax)
                @test all(x -> isapproxdefault(x, atol=1e-13,  rtol=1e-13), zip(Y1, Y2))
            end
        end
    end

    function doublefactorial(l)
        prod(float(i) for i in big(l):-2:1)
    end

    # check against the explicit value for θ = pi/2
    for l in 499:1000
        P = SphericalHarmonics.associatedLegendrex(big(0), l = l, m = l, norm = Unnormalized())
        @test P ≈ (-1)^l * doublefactorial(2l-1)
    end
end

@testset "single mode" begin
    @testset "Associated Legendre Polynomials" begin
        lmax = 20
        coeff = SphericalHarmonics.compute_coefficients(lmax)

        @testset "explicit coeff" begin
            P = SphericalHarmonics.allocate_p(lmax)
            for θ in LinRange(0, pi, 10)
                computePlmcostheta!(P, θ, lmax, coeff)
                for l = 0:lmax, m = 0:l
                    Plm = SphericalHarmonics.associatedLegendre(θ, l, m, coeff)
                    @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
                end
            end

            P = computePlmcostheta(SphericalHarmonics.NorthPole(), lmax = lmax)
            for l = 0:lmax, m = 0:l
                Plm = SphericalHarmonics.associatedLegendre(SphericalHarmonics.NorthPole(), l, m, coeff)
                @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
            end

            P = computePlmcostheta(SphericalHarmonics.SouthPole(), lmax = lmax)
            for l = 0:lmax, m = 0:l
                Plm = SphericalHarmonics.associatedLegendre(SphericalHarmonics.SouthPole(), l, m, coeff)
                @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
            end
        end
        @testset "implicit coeff" begin
            P = SphericalHarmonics.allocate_p(lmax)
            for θ in LinRange(0, pi, 10)
                computePlmcostheta!(P, θ, lmax, coeff)
                for l = 0:lmax, m = 0:l
                    Plm = SphericalHarmonics.associatedLegendre(θ, l, m)
                    Plm2 = SphericalHarmonics.associatedLegendre(θ, l = l, m = m)
                    @test Plm2 == Plm
                    @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
                end
            end

            P = computePlmcostheta(SphericalHarmonics.NorthPole(), lmax = lmax)
            for l = 0:lmax, m = 0:l
                Plm = SphericalHarmonics.associatedLegendre(SphericalHarmonics.NorthPole(), l, m)
                @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
            end

            P = computePlmcostheta(SphericalHarmonics.SouthPole(), lmax = lmax)
            for l = 0:lmax, m = 0:l
                Plm = SphericalHarmonics.associatedLegendre(SphericalHarmonics.SouthPole(), l, m)
                @test isapprox(P[(l,m)], Plm, atol = 1e-14, rtol = 1e-14)
            end
        end
    end
    @testset "SphericalHarmonics" begin
        lmax = 20
        coeff = SphericalHarmonics.compute_coefficients(lmax);

        P = SphericalHarmonics.allocate_p(lmax)
        Y = SphericalHarmonics.allocate_y(lmax)
        Yreal = SphericalHarmonics.allocate_y(Float64, lmax)
        @testset "ComplexHarmonics" begin
            @testset "explicit coeff" begin
                for θ in LinRange(0, pi, 10)
                    computePlmcostheta!(P, θ, lmax, coeff)
                    for ϕ in LinRange(0, 2pi, 10)[1:end-1]
                        computeYlm!(Y, P, θ, ϕ, lmax = lmax)
                        for l = 0:lmax, m = -l:l
                            Ylm = SphericalHarmonics.sphericalharmonic(θ, ϕ, l, m,
                                SphericalHarmonics.ComplexHarmonics(), coeff)

                            Ylm2 = SphericalHarmonics.sphericalharmonic(θ, ϕ; l = l, m = m,
                                SHType = SphericalHarmonics.ComplexHarmonics(), coeff = coeff)

                            @test Ylm == Ylm2

                            @test isapprox(Y[(l,m)], Ylm, atol = 1e-14, rtol = 1e-14)
                        end
                    end
                end
            end
            @testset "implicit coeff" begin
                for θ in LinRange(0, pi, 10)
                    computePlmcostheta!(P, θ, lmax, coeff)
                    for ϕ in LinRange(0, 2pi, 10)[1:end-1]
                        computeYlm!(Y, P, θ, ϕ, lmax = lmax)
                        for l = 0:lmax, m = -l:l
                            Ylm = SphericalHarmonics.sphericalharmonic(θ, ϕ, l, m,
                                SphericalHarmonics.ComplexHarmonics())

                            Ylm2 = SphericalHarmonics.sphericalharmonic(θ, ϕ; l = l, m = m,
                                SHType = SphericalHarmonics.ComplexHarmonics())

                            @test Ylm == Ylm2

                            @test isapprox(Y[(l,m)], Ylm, atol = 1e-14, rtol = 1e-14)
                        end
                    end
                end
            end
        end
        @testset "RealHarmonics" begin
            @testset "explicit coeff" begin
                for θ in LinRange(0, pi, 10)
                    computePlmcostheta!(P, θ, lmax, coeff)
                    for ϕ in LinRange(0, 2pi, 10)[1:end-1]
                        computeYlm!(Yreal, P, θ, ϕ, lmax = lmax, SHType = SphericalHarmonics.RealHarmonics())
                        for l = 0:lmax, m = -l:l
                            Ylm = SphericalHarmonics.sphericalharmonic(θ, ϕ, l, m,
                                SphericalHarmonics.RealHarmonics(), coeff)

                            Ylm2 = SphericalHarmonics.sphericalharmonic(θ, ϕ; l = l, m = m,
                                SHType = SphericalHarmonics.RealHarmonics(), coeff = coeff)

                            @test Ylm == Ylm2

                            @test isapprox(Yreal[(l,m)], Ylm, atol = 1e-14, rtol = 1e-8)
                        end
                    end
                end
            end
            @testset "implicit coeff" begin
                for θ in LinRange(0, pi, 10)
                    computePlmcostheta!(P, θ, lmax, coeff)
                    for ϕ in LinRange(0, 2pi, 10)[1:end-1]
                        computeYlm!(Yreal, P, θ, ϕ, lmax = lmax, SHType = SphericalHarmonics.RealHarmonics())
                        for l = 0:lmax, m = -l:l
                            Ylm = SphericalHarmonics.sphericalharmonic(θ, ϕ, l, m,
                                SphericalHarmonics.RealHarmonics())

                            Ylm2 = SphericalHarmonics.sphericalharmonic(θ, ϕ; l = l, m = m,
                                SHType = SphericalHarmonics.RealHarmonics())

                            @test Ylm == Ylm2

                            @test isapprox(Yreal[(l,m)], Ylm, atol = 1e-14, rtol = 1e-8)
                        end
                    end
                end
            end
        end
    end
end

@testset "Orthonormality" begin

    @testset "Associated Legendre Polynomials" begin
        shnorm(::SphericalHarmonics.LMnorm) = 1/pi
        shnorm(::SphericalHarmonics.Orthonormal) = 1
        function testnorm(f, norm = SphericalHarmonics.LMnorm())
            I, E = hcubature(f, [0], [π]);
            @test I ≈ shnorm(norm)
        end
        function testorthogonal(f)
            I, E = hcubature(f, [0], [π], atol=1e-10);
            @test isapprox(abs(I), 0, atol = max(abs(E), 1e-10))
        end
        lmax = 10
        coeff = SphericalHarmonics.compute_coefficients(lmax);
        P = SphericalHarmonics.allocate_p(lmax);

        @testset "Normalization" begin
            function f!(x, P, l, m, norm = SphericalHarmonics.LMnorm())
                θ = x[1]
                computePlmcostheta!(P, θ, l, m, coeff, norm = norm)
                sin(θ) * P[(l,m)]^2
            end

            for norm in [SphericalHarmonics.LMnorm(), SphericalHarmonics.Orthonormal()]
                for m = 0:lmax, l = m:lmax
                    testnorm(x -> f!(x, P, l, m, norm), norm)
                end
            end
        end

        @testset "Orthogonality" begin
            @testset "same m, different l" begin
                function f!(x, P, k, l, m, norm = SphericalHarmonics.LMnorm())
                    θ = x[1]
                    computePlmcostheta!(P, θ, max(k, l), m, coeff, norm = norm)
                    sin(θ) * P[(k,m)] * P[(l,m)]
                end

                for norm in [SphericalHarmonics.LMnorm(), SphericalHarmonics.Orthonormal()]
                    for m = 0:lmax, l = m:lmax, k = m:lmax
                        k == l && continue
                        testorthogonal(x -> f!(x, P, k, l, m, norm))
                    end
                end
            end

            @testset "same l, different m" begin
                function f!(x, P, l, m1, m2)
                    θ = x[1]
                    computePlmcostheta!(P, θ, l, coeff)
                    P[(l,m1)] * P[(l,m2)] / sin(θ)
                end

                for l = 0:lmax, m1 in 0:l, m2 in m1+1:l
                    testorthogonal(x -> f!(x, P, l, m1, m2))
                end
            end
        end
    end

    @testset "SphericalHarmonics" begin
        function testnorm(f)
            I, E = hcubature(f, [0, 0], [π, 2π]);
            @test all(x->isapprox(x, 1), I)
        end
        function testortho(f)
            I, E = hcubature(f, [0, 0], [π, 2π], atol=1e-10);
            @test isapprox(abs(I), 0, atol = max(abs(E), 1e-10))
        end

        @testset "Normalization" begin
            lmax = 10
            @testset "Complex harmonics" begin
                f(θϕ) = sin(θϕ[1]) .* abs2.(computeYlm(θϕ[1], θϕ[2], lmax = lmax))
                testnorm(f)
            end
            @testset "Real harmonics" begin
                f(θϕ) = sin(θϕ[1]) .* abs2.(computeYlm(θϕ[1], θϕ[2], lmax = lmax, SHType = SphericalHarmonics.RealHarmonics()))
                testnorm(f)
            end
        end
        @testset "Orthogonality" begin
            lmax = 3
            coeff = SphericalHarmonics.compute_coefficients(lmax)

            @testset "Complex harmonics" begin
                Y = SphericalHarmonics.allocate_y(lmax)
                P = SphericalHarmonics.allocate_p(lmax)

                function f!(θϕ, Y, P, l, m1, m2)
                    θ, ϕ = θϕ
                    computePlmcostheta!(P, θ, l, coeff)
                    computeYlm!(Y, P, θ, ϕ, l)
                    sin(θ) * Y[(l,m1)] * conj(Y[(l,m2)])
                end

                for l in 0:lmax, m1 in -l:0, m2 in m1+1:l
                    testortho(x->f!(x, Y, P, l, m1, m2))
                end
            end

            @testset "Real harmonics" begin
                Y = SphericalHarmonics.allocate_y(Float64, lmax)
                P = SphericalHarmonics.allocate_p(lmax)

                function f!(θϕ, Y, P, l, m1, m2)
                    θ, ϕ = θϕ
                    computePlmcostheta!(P, θ, l, coeff)
                    computeYlm!(Y, P, θ, ϕ, l, SphericalHarmonicModes.FullRange, SphericalHarmonics.RealHarmonics())
                    sin(θ) * Y[(l,m1)] * Y[(l,m2)]
                end

                for l in 0:lmax, m1 in -l:0, m2 in m1+1:l
                    testortho(x->f!(x, Y, P, l, m1, m2))
                end
            end
        end
    end
end

@testset "cache" begin
    @testset "constructors" begin
        S1 = @inferred SphericalHarmonics.cache(3);
        S2 = @inferred SphericalHarmonics.cache(3, FullRange);
        S3 = @inferred SphericalHarmonics.cache(3, FullRange, ComplexHarmonics());
        S4 = @inferred SphericalHarmonics.cache(Float64, 3, FullRange, ComplexHarmonics());
        S5 = @inferred SphericalHarmonics.cache(3, ComplexHarmonics());
        S6 = @inferred SphericalHarmonics.cache(Float64, 3, ComplexHarmonics());
        @test typeof(S1) == typeof(S2) == typeof(S3) == typeof(S4) == typeof(S5) == typeof(S6)
    end
    @testset "FullRange, ComplexHarmonics" begin
        S = SphericalHarmonics.cache(3);
        @test _lmax(S) == 3
        θ, ϕ = pi/3, pi/4
        SphericalHarmonics.computePlmx!(S, cos(θ), 1)
        @test S.P.lmax == 1
        P2 = SphericalHarmonics.computePlmx(cos(θ), lmax = 1)
        @test S.P[1:length(P2)] == P2
        SphericalHarmonics.computePlmx!(S, cos(2θ))
        @test S.P.lmax == _lmax(S)
        @test S.P == SphericalHarmonics.computePlmx(cos(2θ), lmax = 3)
        computePlmcostheta!(S, θ, 3)
        @test _lmax(S) == 3
        @test S.P == computePlmcostheta(θ, lmax = 3)
        # This should be a no-op
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        computePlmcostheta!(S, 2θ)
        @test S.P ≈ computePlmcostheta(2θ, lmax = 3)

        computePlmcostheta!(S, θ)
        computeYlm!(S, θ, ϕ, 3)
        @test S.Y == computeYlm(θ, ϕ, 3)
        computePlmcostheta!(S, θ, 4)
        @test _lmax(S) == 4
        @test S.P == computePlmcostheta(θ, lmax = 4)
        computeYlm!(S, θ, ϕ, 4)
        @test S.Y == computeYlm(θ, ϕ, 4)
        computeYlm!(S, θ, ϕ)
        @test S.Y == computeYlm(θ, ϕ, 4)
        computeYlm!(S, θ, ϕ, 2)
        Y2 = computeYlm(θ, ϕ, 2)
        @test S.Y[1:length(Y2)] == Y2

        computePlmcostheta!(S, NorthPole(), 4)
        @test S.P.cosθ == cos(NorthPole())
        @test S.P == computePlmcostheta(NorthPole(), 4)
    end
    @testset "ZeroTo, ComplexHarmonics" begin
        S = SphericalHarmonics.cache(3, m_range = ZeroTo);
        @test _lmax(S) == 3
        θ, ϕ = pi/3, pi/4
        SphericalHarmonics.computePlmx!(S, cos(θ), 3)
        @test S.P == SphericalHarmonics.computePlmx(cos(θ), lmax = 3)
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        # This should be a no-op
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        computeYlm!(S, θ, ϕ, 3)
        @test S.Y == computeYlm(θ, ϕ, lmax = 3, m_range = ZeroTo)
        computePlmcostheta!(S, θ, 4)
        @test _lmax(S) == 4
        @test S.P == computePlmcostheta(θ, lmax = 4)
        computeYlm!(S, θ, ϕ, 4)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, m_range = ZeroTo)
        computeYlm!(S, θ, ϕ)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, m_range = ZeroTo)
        computeYlm!(S, θ, ϕ, 2)
        Y2 = computeYlm(θ, ϕ, lmax = 2, m_range = ZeroTo)
        @test S.Y[1:length(Y2)] == Y2

        computePlmcostheta!(S, NorthPole(), 4)
        @test S.P.cosθ == cos(NorthPole())
        @test S.P == computePlmcostheta(NorthPole(), 4)
    end
    @testset "FullRange, RealHarmonics" begin
        S = SphericalHarmonics.cache(3, SHType = RealHarmonics());
        @test _lmax(S) == 3
        θ, ϕ = pi/3, pi/4
        SphericalHarmonics.computePlmx!(S, cos(θ), 3)
        @test S.P == SphericalHarmonics.computePlmx(cos(θ), lmax = 3)
        # This should be a no-op
        SphericalHarmonics.computePlmx!(S, cos(θ), 3)
        @test S.P == SphericalHarmonics.computePlmx(cos(θ), lmax = 3)
        computePlmx!(S, cos(θ), 4)
        @test _lmax(S) == 4
        @test S.P == computePlmx(cos(θ), lmax = 4)

        S = SphericalHarmonics.cache(3, SHType = RealHarmonics());
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        # This should be a no-op
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        computeYlm!(S, θ, ϕ, 3)
        @test S.Y == computeYlm(θ, ϕ, lmax = 3, SHType = RealHarmonics())

        computePlmcostheta!(S, θ, 4)
        @test _lmax(S) == 4
        @test S.P == computePlmcostheta(θ, lmax = 4)
        computeYlm!(S, θ, ϕ, 4)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, SHType = RealHarmonics())
        computeYlm!(S, θ, ϕ)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, SHType = RealHarmonics())
        computeYlm!(S, θ, ϕ, 2)
        Y2 = computeYlm(θ, ϕ, lmax = 2, SHType = RealHarmonics())
        @test S.Y[1:length(Y2)] == Y2

        computePlmcostheta!(S, NorthPole(), 4)
        @test S.P.cosθ == cos(NorthPole())
        @test S.P == computePlmcostheta(NorthPole(), 4)
    end
    @testset "ZeroTo, RealHarmonics" begin
        S = SphericalHarmonics.cache(3, m_range = ZeroTo, SHType = RealHarmonics());
        @test _lmax(S) == 3
        θ, ϕ = pi/3, pi/4
        SphericalHarmonics.computePlmx!(S, cos(θ), 3)
        @test S.P == SphericalHarmonics.computePlmx(cos(θ), lmax = 3)
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        # This should be a no-op
        computePlmcostheta!(S, θ, 3)
        @test S.P == computePlmcostheta(θ, lmax = 3)
        computeYlm!(S, θ, ϕ, 3)
        @test S.Y == computeYlm(θ, ϕ, lmax = 3, m_range = ZeroTo, SHType = RealHarmonics())
        computePlmcostheta!(S, θ, 4)
        @test _lmax(S) == 4
        @test S.P == computePlmcostheta(θ, lmax = 4)
        computeYlm!(S, θ, ϕ, 4)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, m_range = ZeroTo, SHType = RealHarmonics())
        computeYlm!(S, θ, ϕ)
        @test S.Y == computeYlm(θ, ϕ, lmax = 4, m_range = ZeroTo, SHType = RealHarmonics())
        computeYlm!(S, θ, ϕ, 2)
        Y2 = computeYlm(θ, ϕ, lmax = 2, m_range = ZeroTo, SHType = RealHarmonics())
        @test S.Y[1:length(Y2)] == Y2

        computePlmcostheta!(S, NorthPole(), 4)
        @test S.P.cosθ == cos(NorthPole())
        @test S.P == computePlmcostheta(NorthPole(), 4)
    end
    @testset "show" begin
        S = SphericalHarmonics.cache(3);
        io = IOBuffer()
        show(io, S)
        s = String(take!(io))
        s_exp = "$(SphericalHarmonics.SphericalHarmonicsCache)(Float64, 3, $(SphericalHarmonicModes.FullRange), $(SphericalHarmonics.ComplexHarmonics)())"
        @test s == s_exp
        summary(io, S.P)
        s = String(take!(io))
        s_exp = "10-element normalized AssociatedLegendrePolynomials{Float64} for lmax = 3 (uninitialized)"
        @test s == s_exp
        θ = 0
        computePlmcostheta!(S, θ, 3)
        summary(io, S.P)
        s = String(take!(io))
        s_exp = "10-element normalized AssociatedLegendrePolynomials{Float64} for lmax = 3 and cosθ = 1"
        @test s == s_exp

        S = SphericalHarmonics.cache(BigFloat, 3);
        summary(io, S.P)
        s = String(take!(io))
        s_exp = "10-element normalized AssociatedLegendrePolynomials{BigFloat} for lmax = 3 (uninitialized)"
    end
    @testset "accessor methods" begin
        S = SphericalHarmonics.cache(Float64, 3);
        @test @inferred SphericalHarmonics.eltypeP(S) == Float64
        @test @inferred SphericalHarmonics.eltypeY(S) == ComplexF64
        @test SphericalHarmonics.getP(S) === S.P
        @inferred SphericalHarmonics.getP(S)
        @test SphericalHarmonics.getY(S) === S.Y
        @inferred SphericalHarmonics.getY(S)
    end
    @testset "deepcopy" begin
        S = SphericalHarmonics.cache(3);
        θ, ϕ = pi/3, pi/4
        SphericalHarmonics.computePlmcostheta!(S, θ)
        SphericalHarmonics.computeYlm!(S, θ, ϕ)
        Sd = deepcopy(S)
        @test Sd.C == S.C
        @test Sd.P == S.P
        @test Sd.Y == S.Y
        @test Sd.SHType == S.SHType
    end
end

@testset "Parity" begin
    lmax = 10
    S1 = SphericalHarmonics.cache(lmax)
    S2 = SphericalHarmonics.cache(lmax)
    for θ in LinRange(0, pi, 10)
        P1 = computePlmcostheta!(S1, θ)
        P2 = computePlmcostheta!(S2, pi - θ)
        for l in 0:lmax, m in 0:l
            @test isapprox(P2[(l,m)], (-1)^(l + m) * P1[(l,m)], atol = 1e-13, rtol = 1e-13)
        end
        for ϕ in LinRange(0, 2pi, 10)
            Y1 = computeYlm!(S1, θ, ϕ)
            Y2 = computeYlm!(S2, pi - θ, pi + ϕ)
            for l in 0:lmax, m in -l:l
                @test isapprox(Y2[(l,m)], (-1)^l * Y1[(l,m)], atol = 1e-13, rtol = 1e-13)
            end
        end
    end
end

@testset "m = 0 Legendre" begin
    # for m = 0, the spherical harmonics are normalized Legendre Polynomials and are purely real
    lmax = 100
    @testset "ComplexHarmonics" begin
        S = SphericalHarmonics.cache(lmax)
        LP = zeros(0:lmax)
        for θ in LinRange(0, pi, 10)
            LegendrePolynomials.collectPl!(LP, cos(θ), lmax = lmax)
            computePlmcostheta!(S, θ)
            for ϕ in LinRange(0, 2pi, 10)
                computeYlm!(S, θ, ϕ)
                for l in 0:lmax
                    @test isapprox(imag(S.Y[(l,0)]), 0, atol = 1e-13)
                    @test isapprox(S.Y[(l,0)], √((2l+1)/4pi) * LP[l], atol = 1e-13, rtol = 1e-13)
                end
            end
        end
    end
    @testset "RealHarmonics" begin
        S = SphericalHarmonics.cache(lmax, SHType = RealHarmonics())
        LP = zeros(0:lmax)
        for θ in LinRange(0, pi, 10)
            LegendrePolynomials.collectPl!(LP, cos(θ), lmax = lmax)
            computePlmcostheta!(S, θ)
            for ϕ in LinRange(0, 2pi, 10)
                computeYlm!(S, θ, ϕ)
                for l in 0:lmax
                    @test isapprox(imag(S.Y[(l,0)]), 0, atol = 1e-13)
                    @test isapprox(S.Y[(l,0)], √((2l+1)/4pi) * LP[l], atol = 1e-13, rtol = 1e-13)
                end
            end
        end
    end
end

@testset "rotation of coordinates" begin
    # Ylm(θ, ϕ) = Yl0(0, 0) D^l_{0,-m}(0, θ, ϕ)
    lmax = 10
    S = SphericalHarmonics.cache(lmax);
    Dvec = OffsetArray([zeros(ComplexF64, 2l+1, 2l+1) for l in 0:lmax], 0:lmax);
    Jyvec = OffsetArray([zeros(ComplexF64, 2l+1, 2l+1) for l in 0:lmax], 0:lmax);
    @testset "NorthPole" begin
        Yl0NP = computeYlm(NorthPole(), 0, lmax = lmax)
        for θ in LinRange(0, pi, 10)
            computePlmcostheta!(S, θ)
            for ϕ in LinRange(0, 2pi, 10)
                Ylmθϕ = computeYlm!(S, θ, ϕ)
                for l in 0:lmax
                    Dp = wignerD!(Dvec[l], l, 0, θ, ϕ, Jyvec[l])
                    D = OffsetArray(Dp, -l:l, -l:l)
                    for m in -l:l
                        @test isapprox(Ylmθϕ[(l,m)], Yl0NP[(l,0)] * D[0,-m], atol = 1e-13, rtol = 1e-8)
                    end
                end
            end
        end
    end
    @testset "arbitrary point" begin
        # We represent an arbitrary point in two coordinate systems
        # We assume that it has coordinates (θ1,ϕ1) in the first frame and (θ2,ϕ2) in the second frame
        # The frames may be shown to be related through S2 = Rz(ϕ1)Ry(θ1-θ2)Rz(-ϕ2) S1,
        # where Ri(ω) represents a rotation about the axis i by an angle ω.
        # We note that the rotation operator may be represented as D = exp(-iαJz)exp(-iβJy)exp(-iγJz)
        # Comparing, we obtain α = ϕ1, β = θ1-θ2, γ = -ϕ2
        # The Spherical harmonics transform through Ylm(n2) = ∑_m′ conj(D^l_{m,m′}(-γ,-β,-α)) Ylm(n1)
        # Substituting, we obtain the matrix conj(D^l_{m,m′}(ϕ2,θ2-θ1,-ϕ1))
        # We note that choosing (θ1,ϕ1) = (0,0) in this gives us the matrix conj(D^l_{m,m′}(ϕ2,θ2,0)),
        # which, using symmetries, reduces to the previous case with the point at the north pole

        θ1, ϕ1 = pi/6, pi/3

        Y1lm = computeYlm(θ1, ϕ1, lmax = lmax)
        for θ2 in LinRange(0, pi, 10)
            computePlmcostheta!(S, θ2)
            for ϕ2 in LinRange(0, 2pi, 10)
                Ylmθϕ = computeYlm!(S, θ2, ϕ2)
                for l in 0:lmax
                    Dp = wignerD!(Dvec[l], l, ϕ2, θ2-θ1, -ϕ1, Jyvec[l])
                    D = OffsetArray(Dp, -l:l, -l:l)
                    for m in -l:l
                        rotY1lm = sum(conj(D[m, m′]) * Y1lm[(l,m′)] for m′ = -l:l)
                        @test isapprox(Ylmθϕ[(l,m)], rotY1lm, atol = 1e-13, rtol = 1e-8)
                    end
                end
            end
        end
    end
end

@testset "negative theta" begin
    lmax = 10
    S1 = SphericalHarmonics.cache(lmax);
    S2 = SphericalHarmonics.cache(lmax);
    for θ in LinRange(0, pi, 10)
        computePlmcostheta!(S1, θ)
        θ2 = -θ
        computePlmcostheta!(S2, θ2)
        for ϕ in LinRange(0, 2pi, 10)
            Ylmθϕ = computeYlm!(S1, θ, ϕ)
            Ylmθ2ϕ = computeYlm!(S2, θ2, ϕ)
            for l in 0:lmax, m in -l:l
                @test isapprox(Ylmθ2ϕ[(l,m)], (-1)^m * Ylmθϕ[(l,m)], atol = 1e-13, rtol = 1e-8)
            end
        end
    end
end

@testset "theta ± nπ" begin
    lmax = 10
    S1 = SphericalHarmonics.cache(lmax);
    S2 = SphericalHarmonics.cache(lmax);
    S3 = SphericalHarmonics.cache(lmax);
    for θ in LinRange(0, pi, 10)
        computePlmcostheta!(S1, θ)
        θ2 = pi + θ
        computePlmcostheta!(S2, θ2)
        θ3 = 2pi + θ
        computePlmcostheta!(S3, θ3)
        for ϕ in LinRange(0, 2pi, 10)
            Ylmθϕ = computeYlm!(S1, θ, ϕ)
            Ylmθ2ϕ = computeYlm!(S2, θ2, ϕ)
            Ylmθ3ϕ = computeYlm!(S3, θ3, ϕ)
            for l in 0:lmax, m in -l:l
                @test isapprox(Ylmθ2ϕ[(l,m)], (-1)^l * Ylmθϕ[(l,m)], atol = 1e-13, rtol = 1e-8)
                @test isapprox(Ylmθ3ϕ[(l,m)], Ylmθϕ[(l,m)], atol = 1e-13, rtol = 1e-8)
            end
        end
    end
end

@testset "derivatives of Plm" begin
    for l in 1:20, m in l:-2:0
        # the polynomials for even l+m are even, so their derivatives are odd
        # we check that these go to zero exactly at θ = pi/2
        dP = ForwardDiff.derivative(θ -> associatedLegendre(θ, l = l, m = m), Pi/2)
        @test dP == 0
        dP = ForwardDiff.derivative(x -> associatedLegendrex(x, l = l, m = m), 0)
        @test dP == 0
    end
end


# ---
# title: 1434. Number of Ways to Wear Different Hats to Each Other
# id: problem1434
# author: Tian Jun
# date: 2020-10-31
# difficulty: Hard
# categories: Dynamic Programming, Bit Manipulation
# link: <https://leetcode.com/problems/number-of-ways-to-wear-different-hats-to-each-other/description/>
# hidden: true
# ---
#
# There are `n` people and 40 types of hats labeled from 1 to 40.
#
# Given a list of list of integers `hats`, where `hats[i]` is a list of all hats
# preferred by the `i-th` person.
#
# Return the number of ways that the n people wear different hats to each other.
#
# Since the answer may be too large, return it modulo `10^9 + 7`.
#
#
#
# **Example 1:**
#
#
#
#     Input: hats = [[3,4],[4,5],[5]]
#     Output: 1
#     Explanation: There is only one way to choose hats given the conditions.
#     First person choose hat 3, Second person choose hat 4 and last one hat 5.
#
# **Example 2:**
#
#
#
#     Input: hats = [[3,5,1],[3,5]]
#     Output: 4
#     Explanation: There are 4 ways to choose hats
#     (3,5), (5,3), (1,3) and (1,5)
#
#
# **Example 3:**
#
#
#
#     Input: hats = [[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4]]
#     Output: 24
#     Explanation: Each person can choose hats labeled from 1 to 4.
#     Number of Permutations of (1,2,3,4) = 24.
#
#
# **Example 4:**
#
#
#
#     Input: hats = [[1,2,3],[2,3,5,6],[1,3,7,9],[1,8,9],[2,5,7]]
#     Output: 111
#
#
#
#
# **Constraints:**
#
#   * `n == hats.length`
#   * `1 <= n <= 10`
#   * `1 <= hats[i].length <= 40`
#   * `1 <= hats[i][j] <= 40`
#   * `hats[i]` contains a list of **unique** integers.
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


using ExactReals
using Test

@testset "ExactReals.jl" begin

    @testset "Constructors" begin
        a = ExactReal(3//5)
        @test value_range(a) == (0.6, nextfloat(0.6))

        b = ExactReal(3)
        @test value_range(b) == (prevfloat(3.0), nextfloat(3.0))
    end

    @testset "Arithmetic" begin

        a = ExactReal(3//5)

        b = a + a
        @test value_range(b) == (1.2, nextfloat(1.2))

        c = a * a
        @test value_range(c) == (0.36, nextfloat(0.36))

        d = -a
        @test value_range(d) == (prevfloat(-0.6), -0.6)

        e = sqrt(a)
        s = sqrt(0.6)
        @test value_range(e) == (prevfloat(s), s)
    end

    @testset "Comparisons" begin
        a = ExactReal(3//5)
        @test ExactReals.compare(1 - a, a) == -1

        @test a > ExactReal(1//2)
    end

end


"""
Problem 1:

Given an array of 12-digit binary numbers, find a) the most repeated digit in each position
and b) the least repeated digit in each position. Then convert both numbers to decimal and
multiply.

Problem 2:
Given an array of 12-digit binary numbers, repeatedly filter them out given a digit criteria.

"""

# Read input
function readInput() :: Array{Array{Bool,1},1}
  f = open("./inputs/day3.txt", "r")
  return map( y -> map(x -> parse(Bool,x), collect(y)), eachline(f))
end

# Converts from binary to decimal (binary given as array of bools).
function toInt(v :: Array{Bool,1})
  num = 0
  for i in 1:length(v)
    num += (v[i] ? 1 : 0) * 2^(length(v)-i)
  end
  return num
end

# Given a set of arrays of bools, returns an array of the most common digit in each position.
function rates(data :: Array{Array{Bool,1},1})
  counts = zeros(length(data[1]))
  for dd in data
    counts += map(x-> x ? 1 : -1, dd)
  end
  return map(x -> x >= 0 ? true : false, counts), map(x -> x >= 0 ? false : true, counts)
end

# Same as rates, but for only for position 'i'.
function nextBit(dd, i :: Int) :: Bool
  counts = sum(map(x -> x[i] ? 1 : -1, dd))
  return (counts >= 0 ? true : false)
end

# Solves problem 2
function rates2(data :: Array{Array{Bool,1},1})

  gamma = deepcopy(data)
  i = 1
  while size(gamma)[1] > 1 && i <= size(data[1])[1]
    bit = nextBit(gamma,i)
    println(i, "    ", bit)
    gamma = filter(x -> x !== nothing , map( x -> x[i] == bit ? x : nothing, gamma))
    i += 1
  end
  println(gamma)
  @assert size(gamma)[1] == 1 "Gamma is not well defined"

  epsilon = deepcopy(data)
  i = 1
  while size(epsilon)[1] > 1 && i <= size(data[1])[1]
    bit = !nextBit(epsilon,i)
    println(i, "    ", bit)
    epsilon = filter(x -> x !== nothing , map( x -> x[i] == bit ? x : nothing, epsilon))
    i += 1
  end
  println(epsilon)
  @assert size(epsilon)[1] == 1 "Epsilon is not well defined"

  return gamma[1], epsilon[1]
end

data = readInput()

display("Problem 1: ")
gamma, epsilon = rates(data)
display(toInt(gamma))
display(toInt(epsilon))
display(toInt(gamma) * toInt(epsilon))

display("Problem 2: ")
gamma, epsilon = rates2(data)
display(toInt(gamma))
display(toInt(epsilon))
display(toInt(gamma) * toInt(epsilon))


# This example predicts the temporal evolution of a field
# consisting of coupled discrete systems, U,
# which has to be represented as vectors of vectors.
# Where the field comes from does not matter, but to make the example
# runnable we load one of the test systems of TimeseriesPrediction.
#
# In this example we use many coupled henon maps, which can be thought of
# as a field, where each map is a point in space.
#
# Importantly, the results are compared with the "real" evolution of the
# system.
#
# In the plots, the x axis is space and y axis is time.

# ### Simulate a test system
using PyPlot
using TimeseriesPrediction

testdir = dirname(dirname(pathof(TimeseriesPrediction)))*"/test"
@assert isdir(testdir)
include(testdir*"/system_defs.jl")

M = 100 # how many maps to couple, i.e. how much "space" to cover
N = 1000
p = 20
U,V = coupled_henon1D(M, N+p)

# ### Temporal prediction of field U
pool = U[1:N]
test  = U[N:N+p]

γ = 2; τ = 1; B = 1; k = 1;
em = cubic_shell_embedding(pool,γ,τ,B,k,ConstantBoundary(10.))

pred = temporalprediction(pool,em, p; progress = false)

err = [abs.(test[i]-pred[i]) for i=1:p+1]

println("Maximum error: ", maximum(maximum(e) for e in err))

# ### Plot prediction

# Deduce field maximum values
vmax = max(maximum(maximum(s) for s in test),
           maximum(maximum(s) for s in pred))
vmin = min(minimum(minimum(s) for s in test),
           minimum(minimum(s) for s in pred))

# plot plot plot
fig = figure(figsize=(8,8))
ax1 = subplot2grid((3,1), (0,0))
ax2 = subplot2grid((3,1), (1,0))
ax3 = subplot2grid((3,1), (2,0))

ptest = reverse(Matrix(cat(test..., dims = 2)'), dims = 1)
ppred = reverse(Matrix(cat(pred..., dims = 2)'), dims = 1)
perr = reverse(Matrix(cat(err..., dims = 2)'), dims = 1)

im1 = ax1[:imshow](ppred, cmap="viridis", vmin = vmin, vmax = vmax)
im2 = ax2[:imshow](ptest, cmap="viridis", vmin = vmin, vmax = vmax)
im3 = ax3[:imshow](perr, cmap="inferno", vmin = 0, vmax = vmax-vmin)

for (im, ax) in zip([im1,im2,im3], [ax1,ax2,ax3])
    colorbar(im, ax = ax, fraction=0.02, pad=0.01)# format="%.1f")
end
ax1[:set_title]("Prediction")
ax2[:set_title]("Real evolution")
ax3[:set_title]("Absolute error")

ax2[:set_ylabel]("time")
ax3[:set_xlabel]("space")
tight_layout(w_pad=0.1, h_pad=0.00001)


export Reshape

"""
`Reshape(A::AbstractOperator, dim_out...)`

Shorthand constructor:

`reshape(A, idx...)`

Reshape the codomain dimensions of an `AbstractOperator`.

```julia
julia> A = Reshape(DFT(10),2,5)
¶ℱ  ℝ^10 -> ℂ^(2, 5)

julia> R = reshape(Conv((19,),randn(10)),7,2,2)
¶★  ℝ^19 -> ℝ^(7, 2, 2)

```

"""
struct Reshape{N,L<:AbstractOperator} <: AbstractOperator
	A::L
	dim_out::NTuple{N,Int}

	function Reshape(A::L,dim_out::NTuple{N,Int}) where {N, L<:AbstractOperator}
		if prod(size(A,1)) != prod(dim_out)
			throw(DimensionMismatch("new dimensions $(dim_out) must be consistent with AbstractOperator codomain size $(size(A,1))"))
		end
		new{N,L}(A,dim_out)
	end
end

# Constructors

Reshape(A::L, dim_out::Vararg{Int,N}) where {N,L<:AbstractOperator} =
Reshape(A, dim_out)

# Mappings

function mul!(y::C, R::Reshape{N,L}, b::D) where {N,L,C,D}
	y_res = reshape(y,size(R.A,1))
	mul!(y_res, R.A, b)
end

function mul!(y::D, A::AdjointOperator{Reshape{N,L}}, b::C) where {N,L,C,D}
    R = A.A
	b_res = reshape(b,size(R.A,1))
	mul!(y, R.A', b_res)
end

# Properties

size(R::Reshape) = (R.dim_out, size(R.A,2))

  domainType(  R::Reshape) =   domainType(R.A)
codomainType(  R::Reshape) = codomainType(R.A)

is_linear(      R::Reshape) = is_linear(R.A)
is_null(        R::Reshape) = is_null(R.A)
is_eye(        R::Reshape)  = is_eye(R.A)
is_diagonal(    R::Reshape) = is_diagonal(R.A)
is_AcA_diagonal(R::Reshape) = is_AcA_diagonal(R.A)
is_AAc_diagonal(R::Reshape) = is_AAc_diagonal(R.A)
is_orthogonal(  R::Reshape) = is_orthogonal(  R.A)
is_invertible(  R::Reshape) = is_invertible(R.A)
is_full_row_rank(  R::Reshape)    = is_full_row_rank(     R.A)
is_full_column_rank(  R::Reshape) = is_full_column_rank(  R.A)

fun_name(R::Reshape) = "¶"*fun_name(R.A)
remove_displacement(R::Reshape) = Reshape(remove_displacement(R.A), R.dim_out)

function permute(T::Reshape{N,L}, p::AbstractVector{Int}) where {N,L}
    A = AbstractOperators.permute(T.A,p)
    return Reshape(A,T.dim_out)
end


print("""

Test Kabsch algorithm
=====================
""")

let a = [ 1  1 0
         2  1 0
         1+cos(pi/4) 1-sin(pi/4) 0
         1  0 0 ],
    b = [  1 1 0
         1 2 0
         1+cos(pi/4) 1+sin(pi/4)  0 ], # mean(b,1) 1.2357  1.56904  0.0
    sa = a[1:3, :],
    ma = mean(sa, 1) # mean x, y, z for a[1:3,:]

    # Center b, sa, a
    center!(b)
    sa[:,:] = sa .- ma
    a[:,:]  = a  .- ma # center != 0

    @test_approx_eq_eps mean(b,1) [0.0, 0.0, 0.0] 1e-13
    @test_approx_eq_eps mean(sa,1) [0.0, 0.0, 0.0] 1e-13

    F = kabsch(b, sa) # Reference: b
    R = sa * F
    RR = a * F

    @test_approx_eq R b
    @test_approx_eq RR[1:3,:] b

    @test_approx_eq RR[4,:] .- RR[1,:] [1.0, 0.0, 0.0]
    @test_approx_eq sqrt( (RR[1,1] - RR[4,1])^2 + (RR[1,2] - RR[4,2])^2 ) 1.0

    @test_approx_eq_eps rmsd(R, b) 0.0 1e-13
    @test_approx_eq_eps rmsd(RR[1:3,:], b) 0.0 1e-13
end

let P = [51.65 -1.90 50.07
         50.40 -1.23 50.65
         50.68 -0.04 51.54
         50.22 -0.02 52.85],
    Q = [51.30 -2.99 46.54
         51.09 -1.88 47.58
         52.36 -1.20 48.03
         52.71 -1.18 49.38], # P and Q are from BiomolecularStructures.jl' kabsch tests
    Qdistances = Float64[ sqrt(sumabs2(Q[j,:] .- Q[i,:])) for i in 1:4, j in 1:4 ]

    center!(P)
    center!(Q)

    @test_approx_eq_eps mean(P, 1) zeros(3) 1e-13
    @test_approx_eq_eps mean(Q, 1) zeros(3) 1e-13

    rotationmatrix = kabsch(P, Q)
    rotated = Q * rotationmatrix

    @test_approx_eq rmsd(P, rotated) 0.0030426652601371583

    # Internal distances mustn't change
    @test_approx_eq Float64[ sqrt(sumabs2(Q[j,:] .- Q[i,:])) for i in 1:4, j in 1:4 ] Qdistances # Translated
    @test_approx_eq Float64[ sqrt(sumabs2(rotated[j,:] .- rotated[i,:])) for i in 1:4, j in 1:4 ] Qdistances # Translated  and rotated
end

let P = [-1. 0. 0.
         0. 2. 0.
         0. 1. 0.
         0. 1. 1],
    Q = [0. -1. -1.
         0. -1. 0
         0. 0. 0.
         -1. 0. 0.]

    center!(P)
    center!(Q)
    rotated = Q * kabsch(P, Q)
    @test_approx_eq_eps rmsd(P, rotated) 0.695 0.001
end

print("""

RMSF
----
""")

let A = [ -1. -1. -1.
          -1.  0.  1. ],
    B = [  1.  1.  1.
          -1.  0.  1. ],
    w = [ .25 , .75 ]

    @test_throws ArgumentError mean_coordinates(Matrix{Float64}[A[1,:], B])
    @test_throws ArgumentError mean_coordinates(Matrix{Float64}[A[:,1:2], B])
    @test_throws ArgumentError mean_coordinates(Matrix{Float64}[A])
    @test_throws ArgumentError mean_coordinates(Matrix{Float64}[A,  B'])
    @test_throws ArgumentError mean_coordinates(Matrix{Float64}[A', B'])

    @test mean_coordinates(Matrix{Float64}[A, B]) == [ 0. 0. 0.
                                                      -1. 0. 1. ]

    @test mean_coordinates(Matrix{Float64}[A, B], w) == [ 0.5 0.5 0.5
                                                         -1.  0.  1. ]

    @test mean_coordinates(Matrix{Float64}[A, B], reverse(w)) == [ -0.5 -0.5 -0.5
                                                                   -1.   0.   1. ]

    @test rmsf(Matrix{Float64}[A, B]) == [sqrt(3), 0.]
    @test rmsf(Matrix{Float64}[A, B], w) == [sqrt(0.25*6.75 + 0.75*0.75), 0.]

end

print("""

Superimpose PDBs and RMSD
-------------------------
""")

let hemoglobin = read(joinpath(pwd(), "data", "2hhb.pdb.gz"), PDBFile, group="ATOM", model="1")

    α1 = @residues hemoglobin model "1" chain "A" group "ATOM" residue "*"
    α2 = @residues hemoglobin model "1" chain "C" group "ATOM" residue "*"
    β1 = @residues hemoglobin model "1" chain "B" group "ATOM" residue "*"
    β2 = @residues hemoglobin model "1" chain "D" group "ATOM" residue "*"

    a1, a2, rα = superimpose(α1, α2)

    @test_approx_eq_eps rα 0.230 0.001 # Bio3D RMSD

    b1, b2, rβ = superimpose(β1, β2)

    @test_approx_eq_eps rβ 0.251 0.001 # Bio3D & Chimera's MatchMaker RMSD

    @test length(a1) == length(α1)
    @test length(a2) == length(α2)
    @test length(b1) == length(β1)
    @test length(b2) == length(β2)

    @test a1 != α1
    @test a2 != α2

    @test b1 != β1
    @test b2 != β2

    print("""
    rmsd with Vector{PDBResidue}
    """)

    @test_approx_eq_eps rmsd(α1, α2) 0.230 0.001
    @test_approx_eq_eps rmsd(β1, β2) 0.251 0.001

    @test_approx_eq_eps rmsd(a1, a2, superimposed=true) 0.230 0.001
    @test_approx_eq_eps rmsd(b1, b2, superimposed=true) 0.251 0.001

    ca_a1 = CAmatrix(a1)
    ca_a2 = CAmatrix(a2)
    ca_b1 = CAmatrix(b1)
    ca_b2 = CAmatrix(b2)

    print("""
    Centered
    """)

    @test_approx_eq_eps mean(ca_a1,1) zeros(3) 1e-13
    @test_approx_eq_eps mean(ca_a2,1) zeros(3) 1e-13
    @test_approx_eq_eps mean(ca_b1,1) zeros(3) 1e-13
    @test_approx_eq_eps mean(ca_b2,1) zeros(3) 1e-13

    @test PDB._iscentered(ca_a1)
    @test PDB._iscentered(ca_a2)
    @test PDB._iscentered(ca_b1)
    @test PDB._iscentered(ca_b2)

    print("""
    RMSD
    """)

    @test rα == rmsd(ca_a1, ca_a2)
    @test rβ == rmsd(ca_b1, ca_b2)

    print("""
    coordinatesmatrix, centeredresidues and centeredcoordinates
    """)

    @test_approx_eq coordinatesmatrix(centeredresidues(α1)) coordinatesmatrix(a1)
    @test_approx_eq coordinatesmatrix(centeredresidues(β1)) coordinatesmatrix(b1)

    @test_approx_eq centeredcoordinates(α1) coordinatesmatrix(a1)
    @test_approx_eq centeredcoordinates(β1) coordinatesmatrix(b1)

    print("""
    Superimpose with centered PDBs
    """)

    b12, b22, rβ2 = superimpose(b1, b2)
    @test_approx_eq rβ2 rβ

    print("""
    RMSF
    """)

    @test rmsf(Vector{PDBResidue}[α1, α2]) == rmsf(Vector{PDBResidue}[α1, α2], [.5, .5])
    @test rmsf(Vector{PDBResidue}[β1, β2]) == rmsf(Vector{PDBResidue}[β1, β2], [.5, .5])

end


@testset "Utilities" begin
    deg  = 1
    aux  = true
    red  = false
    case = "case5_spm.m"

    file  = joinpath("../test/data/matpower", case)

    # solve problem
    result = _SPM.run_sopf_iv(file, _PM.IVRPowerModel, ipopt_solver, aux=aux, deg=deg, red=red)
    @test result["termination_status"] == LOCALLY_SOLVED

    pg_coeff = pce_coeff(result, "gen", 1, "pg")
    @test isapprox(pg_coeff, [1.5000000149998045;    0.000459749295155349;   0.00010187050082378998])

    vr_coeff = pce_coeff(result, "bus", 2, "vr")
    @test isapprox(vr_coeff, [0.9902405059792688;  -0.0015349621281835933;  -0.0002292009456770499  ])

    pg_sample = sample(result, "gen", 1, "pg"; sample_size=10)
    @test length(pg_sample) == 10

    pg_density = density(result, "gen", 1, "pg"; sample_size=10)
    # @test TODO: what to test here? do we re-export KernelDensity.UnivariateKDE?

end


"""
    isna(x)

Indicate whether `x` is `missing` or `NaN`.
"""
isna(x::Any) = isnan(x) ? true : false
isna(::Missing) = true

# """
#     skipnas(itr)

# Return an iterator over the elements in `itr` skipping all non-applicable values (`missing` and `NaN`). The returned object can be indexed using indices of `itr`.
# """
# skipnas(itr) = SkipNAs(itr)

# struct SkipNAs{T}
#     x::T
# end
# IteratorSize(::Type{<:SkipNAs}) = SizeUnknown()
# IteratorEltype(::Type{SkipNAs{T}}) where {T} = IteratorEltype(T)
# eltype(::Type{SkipNAs{T}}) where {T} = nonmissingtype(eltype(T))

# function iterate(itr::SkipNAs, state...)
#     item, state = iterate(itr.x, state...)
#     while isna(item)
#         item, state = iterate(itr.x, state)
#     end
#     item, state
# end

# IndexStyle(::Type{<:SkipNAs{T}}) where {T} = IndexStyle(T)
# eachindex(itr::SkipNAs) =
#     Iterators.filter(i -> !isna(@inbounds(itr.x[i])), eachindex(itr.x))
# keys(itr::SkipNAs) =
#     Iterators.filter(i -> !isna(@inbounds(itr.x[i])), keys(itr.x))
# Base.@propagate_inbounds function getindex(itr::SkipNAs, I...)
#     v = itr.x[I...]
#     isna(v) && throw(MissingException("the value at index $I is missing or NaN"))
#     v
# end

"""
    lchoose(n, j)

    Return the log of the j-combination of a set that has n elements.
"""
function lchoose(n::Int, j::Int)
    return loggamma(n+1) - loggamma(j+1) - loggamma(n-j+1)
end


using BEAST
using CompScienceMeshes

using Test

Γ = meshrectangle(1.0,1.0,1.0)
γ = meshsegment(1.0,1.0,3)
X = raviartthomas(Γ, γ)

x = divergence(X)

@test numfunctions(X) == 2
@test numfunctions(x) == numfunctions(X)
for (_f,_g) in zip(x.fns, X.fns)
    @test length(_f) == length(_g)
    if length(_f) == 1
        c = _f[1].cellid
        _s = chart(Γ, Γ.faces[c])
        @test _f[1].coeff == 1 / volume(_s)
    else
        @test _f[1].coeff + _f[2].coeff == 0
    end
end

edges = skeleton(Γ, 1)
vps = cellpairs(Γ, edges)
rwg = raviartthomas(Γ, vps)
dvg = divergence(rwg)

D = spzeros(Int,numcells(edges), numcells(Γ))
for (_m,_f) in enumerate(dvg.fns)
    for _s in _f
        _e = _s.cellid
        D[_m,_e] = _s.coeff
    end
end

C = connectivity(edges, Γ)
@test size(C) == size(D')

## Test the curl of the linear Lagrange elements
using CompScienceMeshes
using BEAST
using Test

m = meshrectangle(1.0, 1.0, 0.25)
X = duallagrangec0d1(m)
@test numfunctions(X) == numcells(m)

Y = curl(X)
@test numfunctions(X) == numfunctions(Y)
@test isa(Y, BEAST.RTBasis)
@test isa(refspace(Y), BEAST.BEAST.RTRefSpace)

# test the chain property
Z = divergence(Y)
for fn ∈ Z.fns
    total = zero(scalartype(Z))
    for _sh ∈ fn
        total += _sh.coeff
    end
    @test total ≈ 0
end


@testset "centroid" begin
    poinsI2 = [[0,0], [0,1], [1,0], [1,1]]
    cI2 = centroid(poinsI2)
    @test cI2 == [0.5, 0.5]

    poinsF2 = [[0.,0], [0,1], [1,0], [1,1]]
    cF2 = centroid(poinsF2)
    @test cF2 == [0.5, 0.5]

    poinsI3 = [[0,0,0], [0,0,1], [0,1,0], [0,1,1]]
    cI3 = centroid(poinsI3)
    @test cI3 == [0, 0.5, 0.5]

    poinsF3 = [SVector(i, j, k) for i in 0:1 for j in 0:1 for k in 0:1]
    cF3 = centroid(poinsF3)
    @test cF3 == SVector(0.5, 0.5, 0.5)
end


points(S::Jacobi, n) = points(Chebyshev(domain(S)), n)

struct JacobiTransformPlan{CPLAN,CJT}
    chebplan::CPLAN
    cjtplan::CJT
end

plan_transform(S::Jacobi, v::AbstractVector) =
    JacobiTransformPlan(plan_transform(Chebyshev(), v), plan_icjt(v, S.a, S.b))
*(P::JacobiTransformPlan, vals) = P.cjtplan*(P.chebplan*vals)

struct JacobiITransformPlan{CPLAN,CJT}
    ichebplan::CPLAN
    icjtplan::CJT
end

plan_itransform(S::Jacobi,v::AbstractVector) =
    JacobiITransformPlan(plan_itransform(Chebyshev(), v), plan_cjt(v, S.a, S.b))
*(P::JacobiITransformPlan,cfs) = P.ichebplan*(P.icjtplan*cfs)

function coefficients(f::AbstractVector,a::Jacobi,b::Chebyshev)
    if domain(a) == domain(b) && (!isapproxinteger(a.a-0.5) || !isapproxinteger(a.b-0.5))
        cjt(f,a.a,a.b)
    else
        defaultcoefficients(f,a,b)
    end
end
function coefficients(f::AbstractVector,a::Chebyshev,b::Jacobi)
    isempty(f) && return f
    if domain(a) == domain(b) && (!isapproxinteger(b.a-0.5) || !isapproxinteger(b.b-0.5))
        icjt(f,b.a,b.b)
    else
        defaultcoefficients(f,a,b)
    end
end

function coefficients(f::AbstractVector,a::Jacobi,b::Jacobi)
    if domain(a) == domain(b) && (!isapproxinteger(a.a-b.a) || !isapproxinteger(a.b-b.b))
        jjt(f,a.a,a.b,b.a,b.b)
    else
        defaultcoefficients(f,a,b)
    end
end


@testset "307.range-sum-query-mutable.jl" begin
    @testset "307 case 1" begin
        ST = SegmentTree([1, 3, 5])
        @test sum_range(ST, 1, 3) == 9
        update!(ST, 2, 2)
        @test sum_range(ST, 1, 3) == 8
    end
end


using Winston

p = FramedPlot(
        aspect_ratio=1,
        xrange=(0,100),
        yrange=(0,100))

n = 21
x = linspace(0, 100, n)
yA = 40 + 10randn(n)
yB = x + 5randn(n)

a = Points(x, yA, kind="circle")
setattr(a, label="a points")

b = Points(x, yB)
setattr(b, label="b points")
style(b, kind="filled circle")

s = Slope(1, (0,0), kind="dotted")
setattr(s, label="slope")

l = Legend(.1, .9, {a,b,s})

add(p, s, a, b, l)

file(p, "example2.png")


using RandomMatrixDistributions
using Test

@testset "MarchenkoPastur" begin

    @test_throws ErrorException MarchenkoPastur(5)

    dist = MarchenkoPastur(1)

    @test minimum(dist) == 0
    @test maximum(dist) == 4

    @test pdf(dist, -1) == pdf(dist, 5) == 0
    @test pdf(dist, 2) ≈ 1/(2*pi)
end

@testset "Wachter" begin

    @test_throws ErrorException Wachter(10, 10)

    dist = Wachter(1, 0.5)

    @test minimum(dist) == 0
    @test maximum(dist) == 16

    @test pdf(dist, -1) == pdf(dist, 17) == 0
    @test pdf(Wachter(0.5, 0), 1) ≈ pdf(MarchenkoPastur(0.5), 1)
end


@defcomp impacttropicalstorms begin
    regions = Index()

    hurrdam = Variable(index=[time,regions])
    hurrdead = Variable(index=[time,regions])

    hurrbasedam = Parameter(index=[regions])
    hurrdamel = Parameter(default = -0.514)
    hurrnl = Parameter(default = 3)
    hurrpar = Parameter(default = 0.04)
    hurrbasedead = Parameter(index=[regions])
    hurrdeadel = Parameter(default = -0.501)

    gdp90 = Parameter(index=[regions])
    pop90 = Parameter(index=[regions])

    population = Parameter(index=[time,regions])
    income = Parameter(index=[time,regions])

    regstmp = Parameter(index=[time,regions])

    function run_timestep(p, v, d, t)

        for r in d.regions
            ypc = p.income[t, r] / p.population[t, r] * 1000.0
            ypc90 = p.gdp90[r] / p.pop90[r] * 1000.0

            # This is hurrican damage
            v.hurrdam[t, r] = 0.001 * p.hurrbasedam[r] * p.income[t, r] * (ypc / ypc90)^p.hurrdamel * ((1.0 + p.hurrpar * p.regstmp[t, r])^p.hurrnl - 1.0)

            v.hurrdead[t, r] = 1000.0 * p.hurrbasedead[r] * p.population[t, r] * (ypc / ypc90)^p.hurrdeadel * ((1.0 + p.hurrpar * p.regstmp[t, r])^p.hurrnl - 1.0)
        end
    end
end


function measure_feature(property::BasicTopology, t::IndexedTable, labels::AbstractArray, N::Int = 0)
    N = N == 0 ? maximum(labels) : N
    fill_properties(property, t)
end

function fill_properties(property::BasicTopology, t::IndexedTable)
    t = property.holes ? determine_holes(t) : t
    t = property.euler_number ? compute_euler_number(t) : t
end

function compute_euler_number(t)
    @transform t {euler₄ = (1/4)*(:Q₁ - :Q₃ + 2*:Qₓ), euler₈ = (1/4)*(:Q₁ - :Q₃ - 2*:Qₓ)}
end

function determine_holes(t)
    # Number of holes equals the number of connected components (i.e. 1) minus
    # the Euler number.
    @transform t {holes = 1 - (1/4)*(:Q₁ - :Q₃ - 2*:Qₓ)}
end


#
# Atomic Orbitals - based on Christina C. Lee's blog post
# (see https://albi3ro.github.io/M4/prerequisites/Atomic-Orbitals.html)
#

using GSL    # GSL holds the special functions
using GR

a0 = 5.291772109217e-11

# The unitless radial coordinate
ρ(r,n) = 2r/(n*a0)

# The θ and ϕ dependence
function Yml(m::Int,l::Int,θ::Real,ϕ::Real)
    (-1.0)^m*sf_legendre_Plm(l,abs(m),cos(θ))*e^(im*m*ϕ)
end

# The Radial dependence
function R(n::Int,l::Int,ρ::Real)
    sf_laguerre_n(n-l-1,2*l+1,ρ)*e^(-ρ/2)*ρ^l
end

# A normalization: This is dependent on the choice of polynomial representation
function norm(n::Int,l::Int)
    sqrt((2/n)^3 * factorial(n-l-1)/(2n*factorial(n+l)))
end

# Generates an Orbital Funtion of (r,θ,ϕ) for a specificied n,l,m.
function Orbital(n::Int,l::Int,m::Int)
    # we make sure l and m are within proper bounds
    if l > n || abs(m) > l
        throw(DomainError())
    end
    Ψ(ρ,θ,ϕ) = norm(n,l) * R(n,l,ρ) * abs(Yml(m,l,θ,ϕ))
    Ψ
end

function CarttoSph(x::Array,y::Array,z::Array)
    r = sqrt(x.^2+y.^2+z.^2)
    θ = acos(z./r)
    ϕ = atan(y./x)
    r,θ,ϕ
end

function calculate_electronic_density(n,l,m)
    N = 50
    r = 1e-9
    s = linspace(-r,r,N)
    x,y,z = meshgrid(s,s,s)

    r,θ,ϕ = CarttoSph(x,y,z)
    Ψ = Orbital(n,l,m)

    Ψv = zeros(Float32,N,N,N)
    for i in 1:N
        for j in 1:N
            for k in 1:N
                Ψv[i,j,k] = abs(Ψ(ρ(r[i,j,k],n),θ[i,j,k],ϕ[i,j,k]))
            end
        end
    end

    (Ψv-minimum(Ψv))/(maximum(Ψv)-minimum(Ψv))
end

Ψv = calculate_electronic_density(3,2,0)

for alpha in 0:360
    isosurface(Ψv,isovalue=0.25,rotation=alpha)
end


"""
Geometry struct containing parameters and arrays describing the Gaussian grid
and the vertical levels. NF is the number format used for the precomputed constants.
"""
struct Geometry{NF<:AbstractFloat}      # NF: Number format

    # GRID-POINT SPACE
    nlon::Int           # Number of longitudes
    nlat::Int           # Number of latitudes
    nlev::Int           # Number of vertical levels
    nlat_half::Int      # Number of latitudes in one hemisphere
    nlon_half::Int      # Half the number of longitudes

    dlon::NF            # grid spacing in longitude
    dlat::NF            # average grid spacing in latitude
    lon::Array{NF,1}    # array of longitudes
    lat::Array{NF,1}    # array of latitudes

    # VERTICAL SIGMA COORDINATE σ = p/p0 (fraction of surface pressure)
    n_stratosphere_levels::Int      # number of upper levels for stratosphere
    σ_levels_half::Array{NF,1}      # σ at half levels
    σ_levels_full::Array{NF,1}      # σ at full levels
    σ_levels_thick::Array{NF,1}     # σ level thicknesses
    σ_levels_half⁻¹_2::Array{NF,1}  # 1/(2σ_levels_full)       #TODO rename?
    σ_f::Array{NF,1}                # akap/(2σ_levels_thick)   #TODO rename?

    # SINES AND COSINES OF LATITUDE
    sinlat::Array{NF,1}         # sin of latitudes
    coslat::Array{NF,1}         # cos of latitudes
    sinlat_NH::Array{NF,1}      # only northern hemisphere
    coslat_NH::Array{NF,1}      # only northern hemisphere
    radang::Array{NF,1}         # radians of latitudes TODO rename to radlat?
    cosg::Array{NF,1}           # this should be sinlat?!
    cosg⁻¹::Array{NF,1}         # rename to sinlat⁻¹?
    cosg⁻²::Array{NF,1}         # rename to sinlat⁻²?

    # CORIOLIS FREQUENCY
    f::Array{NF,1}              # = 2Ω*sin(lat)

    # GEOPOTENTIAL CALCULATION WORK ARRAYS
    xgeop1::Array{NF,1}                  # ?
    xgeop2::Array{NF,1}                  # ?
    lapserate_correction::Array{NF,1}    # ?
end

"""
Defines the geometry.
"""
function Geometry(P::Parameters)

    @unpack nlon,nlat,nlev,trunc = P
    @unpack R,Ω,akap = P
    @unpack n_stratosphere_levels = P

    nlat_half = nlat ÷ 2
    nlon_half = nlon ÷ 2

    # GRID SPACE ARRAYS GAUSSIAN GRID - lon is equi-spaced, lat is not!
    dlon = 360 / nlon                       # grid spacing in longitude
    dlat = 180 / nlat                       # average grid spacing in latitude
    lon  = Array(0:dlon:360-dlon)           # array of longitudes
    # array of latitudes (North to South) corresponding to the zeros
    # of the (unassociated) legendre polynomial order nlat
    lat  = reverse(asind.(gausslegendre(nlat)[1]))

    # VERTICAL SIGMA COORDINATE
    # σ = p/p0 (fraction of surface pressure)
    # sorted such that σ_levels_half[end] is at the planetary boundary
    σ_levels_half = vertical_coordinates(P)
    σ_levels_full = 0.5*(σ_levels_half[2:end] + σ_levels_half[1:end-1])
    σ_levels_thick = σ_levels_half[2:end] - σ_levels_half[1:end-1]
    σ_levels_half⁻¹_2 = 1 ./ (2σ_levels_thick)
    σ_f = akap ./ (2σ_levels_full)

    # SINES AND COSINES OF LATITUDE
    sinlat = sind.(lat)
    coslat = cosd.(lat)
    sinlat_NH = sinlat[1:nlat_half] # sinlat only for northern hemisphere = NH
    coslat_NH = coslat[1:nlat_half]
    radang = asin.(sinlat)
    cosg   = sinlat                 # inconsistent here due to the sin/cos swap
    cosg⁻¹ = 1 ./ cosg
    cosg⁻² = 1 ./ cosg.^2

    # CORIOLIS FREQUENCY
    f = 2Ω*sinlat

    # GEOPOTENTIAL coefficients to calculate geopotential (TODO reference)
    xgeop1 = zeros(nlev)
    xgeop2 = zeros(nlev)
    for k in 1:nlev
        xgeop1[k] = R*log(σ_levels_half[k+1]/σ_levels_half[k])
        if k != nlev
            xgeop2[k+1] = R*log(σ_levels_full[k+1]/σ_levels_half[k+1])
        end
    end

    # LAPSE RATE correction (TODO reference)
    lapserate_correction = zeros(nlev-2)
    for k in 2:nlev-1
        lapserate_correction[k-1] = 0.5*xgeop1[k]*
                    log(σ_levels_half[k+1]/σ_levels_full[k]) / log(σ_levels_full[k+1]/σ_levels_full[k-1])
    end

    # conversion to number format NF happens here
    Geometry{P.NF}( nlon,nlat,nlev,nlat_half,nlon_half,
                    dlon,dlat,lon,lat,
                    n_stratosphere_levels,
                    σ_levels_half,σ_levels_full,σ_levels_thick,σ_levels_half⁻¹_2,σ_f,
                    sinlat,coslat,sinlat_NH,coslat_NH,radang,
                    cosg,cosg⁻¹,cosg⁻²,f,xgeop1,xgeop2,lapserate_correction)
end

"""Vertical sigma coordinates defined by their nlev+1 half levels `σ_levels_half`. Sigma coordinates are
fraction of surface pressure (p/p0) and are sorted from top (stratosphere) to bottom (surface).
The first half level is at 0 the last at 1. Evaluate a generalised logistic function with
coefficients in `P` for the distribution of values in between. Default coefficients follow
the L31 configuration historically used at ECMWF."""
function vertical_coordinates(P::Parameters)
    @unpack nlev,GLcoefs = P

    halflevels_normalised = range(0,1,nlev+1)   # normalised = level/nlev
    σ_levels_half = generalised_logistic(halflevels_normalised,GLcoefs)
    σ_levels_half[1] = 0           # topmost half-level is at 0 pressure
    σ_levels_half[end] = 1         # lowermost half-level is at p=p_surface

    @assert isincreasing(σ_levels_half) "Vertical coordinates are not increasing."
    return σ_levels_half
end

"""Generalised logistic function based on the coefficients in `coefs`."""
function generalised_logistic(x,coefs::GenLogisticCoefs)
    @unpack A,K,C,Q,B,M,ν = coefs
    return @. A + (K-A)/(C+Q*exp(-B*(x-M)))^(1/ν)
end


cp = CPEG.CPEGWorkspace()
g = cp.params.gravity
# check gravity at surface
r = g.R*normalize(SA[1, 2, 4.0])

# check gravity at surface is around 3.7 m/s^2
a = CPEG.gravity(g, r)
@test norm(a) < 3.8
@test norm(a) > 3.6

# check J2 calculation by doing both parts seperately

# position vector
r_1 = g.R*1.1*normalize(SA[1, -2, 1.8])

# accel from the gravity function
a1 = CPEG.gravity(g, r_1)

# gravity due to central body
a1_central = -r_1*g.μ/(norm(r_1)^3)

# gravity due to J2
nr = norm(r_1)
x,y,z = r_1
a1_j2 = -1.5*g.J2*(g.μ/nr^2)*(g.R/nr)^2*SA[(1-5*(z/nr)^2)*x/nr,
                                           (1-5*(z/nr)^2)*y/nr,
                                           (3-5*(z/nr)^2)*z/nr]

# test the sum of these is equal to the output of the gravity function
@test a1 ≈ (a1_central + a1_j2) rtol = 1e-12


__precompile__()

module Master

using Types
export masternodes, gaussnodes, mkshape, mkmaster;

include("masternodes.jl");
include("gaussnodes.jl");
include("legendrepolynomial.jl");
include("simplexmonomial.jl");
include("tensorproduct.jl");
include("mkshape.jl");

struct MASTERStruct
    nd::IntP; # physical dimension
    porder::IntP; # polynomial degree
    pgauss::IntP; # order of Gauss quadrature
    elemtype::IntP; # type of elements
    nodetype::IntP; # type of nodes
    npe::IntP; # number of polynomials per element
    npf::IntP; # number of polynomials per face
    nge::IntP; # number of Gauss points per element
    ngf::IntP; # number of Gauss points per face
    np1d::IntP; # number of polynomials per 1D element
    ng1d::IntP; # number of Gauss points per 1D element
    perm::Array{IntP,2}; # to get face nodes from element nodes
    shapegt::Array{FloatP,3}; # element shape functions at Gauss points (transpose)
    shapegw::Array{FloatP,3}; # element shape functions at Gauss points multiplied by Gauss weights
    shapfgt::Array{FloatP,3}; # face shape functions at Gauss points (transpose)
    shapfgw::Array{FloatP,3}; # face shape functions at Gauss points multiplied by Gauss weights
    shapent::Array{FloatP,3}; # element shape functions at nodes (transpose)
    shapen::Array{FloatP,3};  # element shape functions at nodes
    shapfnt::Array{FloatP,3}; # face shape functions at nodes (transpose)
    shapfn::Array{FloatP,3};  # face shape functions at nodes
    xpe::Array{FloatP,2}; # nodal points on master element
    gpe::Array{FloatP,2}; # gauss points on master element
    gwe::Array{FloatP,2}; # gauss weighs on master element
    xpf::Array{FloatP,2}; # nodal points on master face
    gpf::Array{FloatP,2}; # gauss points on master face
    gwf::Array{FloatP,2}; # gauss weighs on master face
    shap1dgt::Array{FloatP,3}; # 1D shape functions at Gauss points (transpose)
    shap1dgw::Array{FloatP,3}; # 1D shape functions at Gauss points multiplied by Gauss weights
    shap1dnt::Array{FloatP,3}; # 1D shape functions at nodes (transpose)
    shap1dn::Array{FloatP,3}; # 1D shape functions at nodes
    xp1d::Array{FloatP,2}; # node points on 1D element
    gp1d::Array{FloatP,2}; # gauss points on 1D element
    gw1d::Array{FloatP,2}; # gauss weights on 1D element
    telem::Array{IntP,2}; # element
    tface::Array{IntP,2}; # face
end

function mkmaster(dim::IntP,porder::IntP,pgauss::IntP,elemtype::IntP,nodetype::IntP)

# node positions on the master element and master face
xpe,telem,xpf,tface,perm = masternodes(porder,dim,elemtype);

# Gauss points and weights on the master volume element
gpe, gwe = gaussnodes(pgauss,dim,elemtype);

# shape functions and derivatives on the master volume element
shapeg = mkshape(porder,xpe,gpe,elemtype);
shapen = mkshape(porder,xpe,xpe,elemtype);

npe,nge,nd1 = size(shapeg);
shapegt = zeros(nge,npe,nd1);
shapent = zeros(npe,npe,nd1);
shapegw = zeros(npe,nge,nd1);
Mgwe = zeros(nge,nge);
for i = 1:nge
    Mgwe[i,i] = gwe[i];
end
for d=1:nd1
    shapegt[:,:,d] = shapeg[:,:,d]';
    shapent[:,:,d] = shapen[:,:,d]';
    shapegw[:,:,d] = shapeg[:,:,d]*Mgwe;
end

if dim>1
    # Gauss points and weights on the master face element
    gpf, gwf = gaussnodes(pgauss,dim-1,elemtype);
    # shape functions and derivatives on the master face element
    shapfg = mkshape(porder,xpf,gpf,elemtype);
    shapfn = mkshape(porder,xpf,xpf,elemtype);
else
    gpf   = [0.0];
    gwf   = [1.0];
    shapfg = reshape([1.0],1,1,1);
    shapfn = reshape([1.0],1,1,1);
end

npf = size(shapfg,1);
ngf = size(shapfg,2);
shapfgt = zeros(ngf,npf,dim);
shapfnt = zeros(npf,npf,dim);
shapfgw = zeros(npf,ngf,dim);
Mgwf = zeros(ngf,ngf);
for i = 1:ngf
    Mgwf[i,i] = gwf[i];
end
for d=1:dim
    shapfgt[:,:,d] = shapfg[:,:,d]';
    shapfnt[:,:,d] = shapfn[:,:,d]';
    shapfgw[:,:,d] = shapfg[:,:,d]*Mgwf;
end

xp1d,~,~,~,~ = masternodes(porder,1,elemtype);
gp1d,gw1d = gaussnodes(pgauss,1,elemtype);
shap1dg = mkshape(porder,xp1d,gp1d,elemtype);
shap1dn = mkshape(porder,xp1d,xp1d,elemtype);

np1d = size(shap1dg,1);
ng1d = size(shap1dg,2);
shap1dgt = zeros(ng1d,np1d,2);
shap1dnt = zeros(np1d,np1d,2);
shap1dgw = zeros(np1d,ng1d,2);
Mgw1d = zeros(ng1d,ng1d);
for i = 1:ng1d
    Mgw1d[i,i] = gw1d[i];
end
for d=1:2
    shap1dgt[:,:,d] = shap1dg[:,:,d]';
    shap1dgw[:,:,d] = shap1dg[:,:,d]*Mgw1d;
    shap1dnt[:,:,d] = shap1dn[:,:,d]';
end

master = MASTERStruct(dim, porder, pgauss, elemtype, nodetype, npe, npf,
    nge, ngf, np1d, ng1d, perm, shapegt, shapegw, shapfgt, shapfgw, shapent, shapen,
    shapfnt, shapfn, xpe, gpe, gwe, xpf, gpf, gwf, shap1dgt, shap1dgw, shap1dnt,
    shap1dn, xp1d, gp1d, gw1d, telem, tface);

return master;

end

function mkmaster(app)
    master = mkmaster(app.nd,app.porder,app.pgauss,app.elemtype,app.nodetype);
    return master;
end

end