using LinearAlgebra, DiffEqOperators, Random, Test, BandedMatrices, SparseArrays

@show "2D"

@testset "2D Multiplication with no boundary points and dx = 1.0" begin

    # Test (Lxx + Lyy)*M, dx = 1.0, no coefficient
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(0.1i)+sin(0.1j)
        end
    end

    Lxx = CenteredDifference{1}(2,2,1.0,N)
    Lyy = CenteredDifference{2}(2,2,1.0,N)
    A = Lxx + Lyy

    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lxx*M)[1:N,2:N+1] +(Lyy*M)[2:N+1,1:N])

    # Test a single axis, multiple operators: (Lx + Lxx)*M, dx = 1.0
    Lx = CenteredDifference{1}(1,2,1.0,N)
    A = Lx + Lxx

    M_temp = zeros(100,102)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)+(Lxx*M))

    # Test a single axis, multiple operators: (Ly + Lyy)*M, dx = 1.0, no coefficient
    Ly = CenteredDifference{2}(1,2,1.0,N)
    A = Ly + Lyy

    M_temp = zeros(102,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly*M)+(Lyy*M))

    # Test multiple operators on both axis: (Lx + Ly + Lxx + Lyy)*M, no coefficient
    A = Lx + Ly + Lxx + Lyy
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)[1:N,2:N+1] +(Ly*M)[2:N+1,1:N] + (Lxx*M)[1:N,2:N+1] +(Lyy*M)[2:N+1,1:N])
end

@testset "2D Multiplication with identical bpc and dx = 1.0" begin

    # Test (Lxxxx + Lyyyy)*M, dx = 1.0, no coefficient, two boundary points on each axis
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(0.1i)+sin(0.1j)
        end
    end

    Lx4 = CenteredDifference{1}(4,4,1.0,N)
    Ly4 = CenteredDifference{2}(4,4,1.0,N)
    A = Lx4 + Ly4

    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test a single axis, multiple operators: (Lxxx + Lxxxx)*M, dx = 1.0
    Lx3 = CenteredDifference{1}(3,4,1.0,N)
    A = Lx3 + Lx4

    M_temp = zeros(100,102)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)+(Lx4*M))

    # Test a single axis, multiple operators: (Lyyy + Lyyyy)*M, dx = 1.0, no coefficient
    Ly3 = CenteredDifference{2}(3,4,1.0,N)
    A = Ly3 + Ly4

    M_temp = zeros(102,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)+(Ly4*M))

    # Test multiple operators on both axis: (Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test (Lxxx + Lyyy)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Ly3
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N])

end

@testset "2D Multiplication with differing bpc and dx = 1.0" begin

    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N+2)
    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(0.1i)+sin(0.1j)
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,1.0,N)
    # Lx3 has 1 boundary point
    Lx3 = CenteredDifference{1}(3,3,1.0,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,1.0,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M, dx = 1.0
    A = Lx2+Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M, dx = 1.0
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,1.0,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,1.0,N)
    # Ly4 has 2 boundary points
    Ly4 = CenteredDifference{2}(4,4,1.0,N)
    M_temp = zeros(N+2,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M, dx = 1.0
    A = Ly2+Ly4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M, dx = 1.0
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Test multiple operators on both axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

end

@testset "2D Multiplication with identical bpc and non-trivial dx = dy = 0.1" begin

    # Test (Lxxxx + Lyyyy)*M, dx = 0.1, dy = 0.01, no coefficient, two boundary points on each axis
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(0.1i)+sin(0.1j)
        end
    end

    Lx4 = CenteredDifference{1}(4,4,0.1,N)
    Ly4 = CenteredDifference{2}(4,4,0.1,N)
    A = Lx4 + Ly4

    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test a single axis, multiple operators: (Lxxx + Lxxxx)*M, dx = 0.1
    Lx3 = CenteredDifference{1}(3,4,0.1,N)
    A = Lx3 + Lx4

    M_temp = zeros(100,102)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)+(Lx4*M))

    # Test a single axis, multiple operators: (Lyyy + Lyyyy)*M, dx = 0.01, no coefficient
    Ly3 = CenteredDifference{2}(3,4,0.1,N)
    A = Ly3 + Ly4

    M_temp = zeros(102,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)+(Ly4*M))

    # Test multiple operators on both axis: (Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient dx =
    A = Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test (Lxxx + Lyyy)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Ly3
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N])

end

@testset "2D Multiplication with identical bpc and non-trivial dx = 0.1, dy = 0.25" begin

    # Test (Lxxxx + Lyyyy)*M, dx = 0.1, dy = 0.01, no coefficient, two boundary points on each axis
    dx = 0.1
    dy = 0.25
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx*i)+sin(dy*j)
        end
    end

    Lx4 = CenteredDifference{1}(4,4,dx,N)
    Ly4 = CenteredDifference{2}(4,4,dy,N)
    A = Lx4 + Ly4

    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test a single axis, multiple operators: (Lxxx + Lxxxx)*M, dx = 0.1
    Lx3 = CenteredDifference{1}(3,4,dx,N)
    A = Lx3 + Lx4

    M_temp = zeros(100,102)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)+(Lx4*M))

    # Test a single axis, multiple operators: (Lyyy + Lyyyy)*M, dx = 0.01, no coefficient
    Ly3 = CenteredDifference{2}(3,4,dy,N)
    A = Ly3 + Ly4

    M_temp = zeros(102,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)+(Ly4*M))

    # Test multiple operators on both axis: (Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient dx =
    A = Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

    # Test (Lxxx + Lyyy)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Ly3
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N])

end

@testset "2D Multiplication with differing bpc and non-trivial dx = dy = 0.1" begin

    dx = 0.1
    dy = 0.1
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N+2)
    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx*i)+sin(dy*j)
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M, dx = 1.0
    A = Lx2+Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M, dx = 1.0
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = CenteredDifference{2}(4,4,dy,N)
    M_temp = zeros(N+2,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M, dx = 1.0
    A = Ly2+Ly4
    mul!(M_temp, A, M)
    @test isapprox(M_temp, ((Ly2*M) + (Ly4*M)), atol = 1e-7)

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M, dx = 1.0
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Test multiple operators on both axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

end

@testset "2D Multiplication with differing bpc and non-trivial dx = 0.1, dy = 0.25" begin

    dx = 0.1
    dy = 0.25
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N+2)
    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx*i)+sin(dy*j)
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M, dx = 1.0
    A = Lx2+Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M)) || maximum(abs.(M_temp - ((Lx2*M) + (Lx4*M)))) < 1e-10

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M, dx = 1.0
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = CenteredDifference{2}(4,4,dy,N)
    M_temp = zeros(N+2,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M, dx = 1.0
    A = Ly2+Ly4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M, dx = 1.0
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Test multiple operators on both axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])

end

# THis testset uses the last testset which has a several non-trivial cases,
# and additionally tests coefficient handling. All operators are handled by the
# fast 2D/3D dispatch.
@testset "2D coefficient handling" begin

    dx = 0.1
    dy = 0.25
    N = 100
    M = zeros(N+2,N+2)
    M_temp = zeros(N,N+2)
    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx*i)+sin(dy*j)
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = 5.5*CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = 0.5*CenteredDifference{1}(4,4,dx,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M, dx = 1.0
    A = Lx2+Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M, dx = 1.0
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = 2.0*CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)
    M_temp = zeros(N+2,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M, dx = 1.0
    A = Ly2+Ly4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M, dx = 1.0
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Test multiple operators on both axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(100,100)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Lx3*M)[1:N,2:N+1] +(Ly3*M)[2:N+1,1:N] + (Lx4*M)[1:N,2:N+1] +(Ly4*M)[2:N+1,1:N])
end

@show "irregular"

@testset "x and y are both irregular grids" begin

    N = 100
    dx = cumsum(rand(N+2))
    dy = cumsum(rand(N+2))
    M = zeros(N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx[i])+sin(dy[j])
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Test composition of all first-dimension operators
    A = Lx2+Lx3+Lx4
    M_temp = zeros(N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Lx2*M + Lx3*M + Lx4*M)

    # Test composition of all second-dimension operators
    A = Ly2+Ly3+Ly4
    M_temp = zeros(N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Ly2*M + Ly3*M + Ly4*M)

    # Test composition of all operators
    A = Lx2+Lx3+Lx4+Ly2+Ly3+Ly4
    M_temp = zeros(N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Lx3*M)[1:N,2:N+1]+(Lx4*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Ly3*M)[2:N+1,1:N]+(Ly4*M)[2:N+1,1:N])

end

@testset "regular x grid (dx=0.25) and irregular y grid" begin

    N = 100
    dx = 0.25
    dx_vec = 0.25:0.25:(N+2)*0.25
    dy = cumsum(rand(N+2))
    M = zeros(N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx*i)+sin(dy[j])
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Test that composition of all x-operators works
    A = Lx2 + Lx3 + Lx4
    M_temp = zeros(N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Lx2*M + Lx3*M + Lx4*M)

    # Test that composition of all y-operators works
    A = Ly2 + Ly3 + Ly4
    M_temp = zeros(N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Ly2*M + Ly3*M + Ly4*M)

    # Test that composition of both x and y operators works
    A = Lx2 + Ly2 + Lx3 + Ly3 + Ly4 + Lx4
    M_temp = zeros(N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Lx3*M)[1:N,2:N+1]+(Lx4*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Ly3*M)[2:N+1,1:N]+(Ly4*M)[2:N+1,1:N])

    # Last case where we now have some `irregular-grid` operators operating on the
    # regular-spaced axis x

    # These operators are operating on the regular grid x, but are constructed as though
    # they were irregular grid operators. Hence we test if we can seperate irregular and
    # regular gird operators on the same axis
    # Lx2 has 0 boundary points
    _Lx2 = 4.532*CenteredDifference{1}(2,2,dx_vec,N)
    # Lx3 has 1 boundary point
    _Lx3 = 0.235*CenteredDifference{1}(3,3,dx_vec,N)
    # Lx4 has 2 boundary points
    _Lx4 = CenteredDifference{1}(4,4,dx_vec,N)

    A += _Lx2 + _Lx3 + _Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((_Lx2*M)[1:N,2:N+1]+(_Lx3*M)[1:N,2:N+1]+(_Lx4*M)[1:N,2:N+1]+(Lx2*M)[1:N,2:N+1]+(Lx3*M)[1:N,2:N+1]+(Lx4*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Ly3*M)[2:N+1,1:N]+(Ly4*M)[2:N+1,1:N])

end

@testset "irregular x grid and regular y grid (dy = 0.25)" begin

    N = 100
    dy = 0.25
    dy_vec = 0.25:0.25:(N+2)*0.25
    dx = cumsum(rand(N+2))
    M = zeros(N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            M[i,j] = cos(dx[i])+sin(dy*j)
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Test that composition of all x-operators works
    A = Lx2 + Lx3 + Lx4
    M_temp = zeros(N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Lx2*M + Lx3*M + Lx4*M)

    # Test that composition of all y-operators works
    A = Ly2 + Ly3 + Ly4
    M_temp = zeros(N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Ly2*M + Ly3*M + Ly4*M)

    # Test that composition of both x and y operators works
    A = Lx2 + Ly2 + Lx3 + Ly3 + Ly4 + Lx4
    M_temp = zeros(N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Lx3*M)[1:N,2:N+1]+(Lx4*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Ly3*M)[2:N+1,1:N]+(Ly4*M)[2:N+1,1:N])

    # Last case where we now have some `irregular-grid` operators operating on the
    # regular-spaced axis y

    # These operators are operating on the regular grid y, but are constructed as though
    # they were irregular grid operators. Hence we test if we can seperate irregular and
    # regular gird operators on the same axis
    # Ly2 has 0 boundary points
    _Ly2 = CenteredDifference{2}(2,2,dy_vec,N)
    # Ly3 has 1 boundary point
    _Ly3 = CenteredDifference{2}(3,3,dy_vec,N)
    # Ly4 has 2 boundary points
    _Ly4 = 12.1*CenteredDifference{2}(4,4,dy_vec,N)

    A += _Ly2 + _Ly3 + _Ly4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1]+(Lx3*M)[1:N,2:N+1]+(Lx4*M)[1:N,2:N+1]+(Ly2*M)[2:N+1,1:N]+(Ly3*M)[2:N+1,1:N]+(Ly4*M)[2:N+1,1:N]+(_Ly2*M)[2:N+1,1:N]+(_Ly3*M)[2:N+1,1:N]+(_Ly4*M)[2:N+1,1:N])

end

################################################################################
# 3D Multiplication Tests
################################################################################

@show "3D"

@testset "3D Multiplication with no boundary points and dx = dy = dz = 1.0" begin

    # Test (Lxx + Lyy + Lzz)*M, dx = dy = dz = 1.0, no coefficient
    N = 100
    M = zeros(N+2,N+2,N+2)
    M_temp = zeros(N,N,N)

    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(0.1i)+sin(0.1j) + exp(0.01k)
            end
        end
    end

    Lxx = CenteredDifference{1}(2,2,1.0,N)
    Lyy = CenteredDifference{2}(2,2,1.0,N)
    Lzz = CenteredDifference{3}(2,2,1.0,N)
    A = Lxx + Lyy + Lzz

    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lxx*M)[1:N,2:N+1,2:N+1] + (Lyy*M)[2:N+1,1:N,2:N+1] + (Lzz*M)[2:N+1,2:N+1,1:N])

    # Test a single axis, multiple operators: (Lx + Lxx)*M, dx = 1.0
    Lx = CenteredDifference{1}(1,2,1.0,N)
    A = Lx + Lxx

    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)+(Lxx*M))

    # Test a single axis, multiple operators: (Ly + Lyy)*M, dy = 1.0, no coefficient
    Ly = CenteredDifference{2}(1,2,1.0,N)
    A = Ly + Lyy

    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly*M)+(Lyy*M))

    # Test a single axis, multiple operators: (Lz + Lzz)*M, dz = 1.0, no coefficient
    Lz = CenteredDifference{3}(1,2,1.0,N)
    A = Lz + Lzz

    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lz*M)+(Lzz*M))

    # Test multiple operators on both axis: (Lx + Ly + Lxx + Lyy)*M, no coefficient
    A = Lx + Ly + Lxx + Lyy
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)[1:N,2:N+1,:] +(Ly*M)[2:N+1,1:N,:] + (Lxx*M)[1:N,2:N+1,:] +(Lyy*M)[2:N+1,1:N,:])

    # Test multiple operators on both axis: (Lx + Lxx + Lz + Lzz)*M, no coefficient
    A = Lx + Lxx + Lz + Lzz
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)[1:N,:,2:N+1] + (Lxx*M)[1:N,:,2:N+1]  + (Lz*M)[2:N+1,:,1:N] +(Lzz*M)[2:N+1,:,1:N])

    # Test multiple operators on both axis: (Ly + Lyy + Lz + Lzz)*M, no coefficient
    A = Ly + Lyy + Lz + Lzz
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly*M)[:,1:N,2:N+1] + (Lyy*M)[:,1:N,2:N+1] + (Lz*M)[:,2:N+1,1:N] +(Lzz*M)[:,2:N+1,1:N])

    # Test multiple operators on both axis: (Lx + Ly + Lxx + Lyy + Lz + Lzz)*M, no coefficient
    A = Lx + Ly + Lxx + Lyy + Lz + Lzz
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx*M)[1:N,2:N+1,2:N+1] +(Ly*M)[2:N+1,1:N,2:N+1] + (Lxx*M)[1:N,2:N+1,2:N+1] +(Lyy*M)[2:N+1,1:N,2:N+1] + (Lz*M)[2:N+1,2:N+1,1:N] +(Lzz*M)[2:N+1,2:N+1,1:N])

end

@testset "3D Multiplication with identical bpc and dx = dy = dz = 1.0" begin

    N = 100
    M = zeros(N+2,N+2,N+2)
    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(0.1i)+sin(0.1j) + exp(0.01k)
            end
        end
    end

    # Test a single axis, multiple operators: (Lxxx + Lxxxx)*M, dx = dy = dz = 1.0
    # Lx3 and Lx4 have the same number of boundary points
    Lx3 = CenteredDifference{1}(3,4,1.0,N)
    Lx4 = CenteredDifference{1}(4,4,1.0,N)
    A = Lx3 + Lx4

    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)+(Lx4*M))

    # Test a single axis, multiple operators: (Lyyy + Lyyyy)*M, dx = dy = dz = 1.0, no coefficient
    # Ly3 and Ly4 have the same number of boundary points
    Ly3 = CenteredDifference{2}(3,4,1.0,N)
    Ly4 = CenteredDifference{2}(4,4,1.0,N)
    A = Ly3 + Ly4

    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)+(Ly4*M))

    # Test a single axis, multiple operators: (Lzzz + Lzzzz)*M, dx = dy = dz = 1.0, no coefficient
    # Lz3 and Lz4 have the same number of boundary points
    Lz3 = CenteredDifference{3}(3,4,1.0,N)
    Lz4 = CenteredDifference{3}(4,4,1.0,N)
    A = Lz3 + Lz4

    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lz3*M)+(Lz4*M))

    # Test (Lxxxx + Lyyyy)*M, dx = 1.0, no coefficient, two boundary points on each axis
    M_temp = zeros(N,N,N+2)
    A = Lx4 + Ly4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test (Lxxx + Lyyy)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Ly3
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:])

    # Test multiple operators on both axis: (Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test (Lxxxx + Lzzzz)*M, dx = 1.0, no coefficient, two boundary points on each axis
    M_temp = zeros(N,N+2,N)
    A = Lx4 + Lz4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test (Lxxx + Lzzz)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Lz3
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N])

    # Test multiple operators on both axis: (Lxxx + Lzzz + Lxxxx + Lzzzz)*M, no coefficient
    A = Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test (Lyyyy + Lzzzz)*M, dx = 1.0, no coefficient, two boundary points on each axis
    M_temp = zeros(N+2,N,N)
    A = Ly4 + Lz4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test (Lyyy + Lzzz)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Ly3 + Lz3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N])

    # Test multiple operators on both axis: (Lyyy + Lzzz + Lyyyy + Lzzzz)*M, no coefficient
    A = Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test a single operator on each axis: (Lx3 + Ly3 + Lz3)*M, no coefficient
    A = Lx3 + Ly3 + Lz3
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,2:N+1] + (Ly3*M)[2:N+1,1:N,2:N+1] +(Lz3*M)[2:N+1,2:N+1,1:N])

    # Test a single operator on each axis: (Lx4 + Ly4 + Lz4)*M, no coefficient
    A = Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1,2:N+1] + (Ly4*M)[2:N+1,1:N,2:N+1] +(Lz4*M)[2:N+1,2:N+1,1:N])

    # Test multiple operators on each axis: (Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4)*M, no coefficient
    A = Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,2:N+1] + (Ly3*M)[2:N+1,1:N,2:N+1] +(Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] + (Ly4*M)[2:N+1,1:N,2:N+1] +(Lz4*M)[2:N+1,2:N+1,1:N])

end

@show "3D BPC"

@testset "3D Multiplication with differing bpc and dx = dy = dz = 1.0" begin

    N = 100
    M = zeros(N+2,N+2,N+2)
    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(0.1i)+sin(0.1j) + exp(0.01k)
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,1.0,N)
    # Lx3 has 1 boundary point
    Lx3 = CenteredDifference{1}(3,3,1.0,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,1.0,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M, dx = 1.0
    A = Lx2+Lx4
    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M, dx = 1.0
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,1.0,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,1.0,N)
    # Ly4 has 2 boundary points
    Ly4 = CenteredDifference{2}(4,4,1.0,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M, dx = 1.0
    A = Ly2+Ly4
    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M, dx = 1.0
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Lz2 has 0 boundary points
    Lz2 = CenteredDifference{3}(2,2,1.0,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,1.0,N)
    # Lz4 has 2 boundary points
    Lz4 = CenteredDifference{3}(4,4,1.0,N)

    # Test a single axis, multiple operators: (Lzy+Lzzzz)*M, dz = 1.0
    A = Lz2+Lz4
    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz4*M))

    # Test a single axis, multiple operators: (Lzz++Lzzz+Lzzzz)*M, dz = 1.0
    A += Lz3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz3*M) + (Lz4*M))

    # Test multiple operators on two axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M, no coefficient
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,:,2:N+1]+(Lz2*M)[2:N+1,:,1:N]+(Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M, no coefficient
    A = Ly2 + Lz2 + Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test operators on all three axis (Lxx + Lyy + Lzz + Lxxx + Lyyy + Lzzz + Lxxxx + Lyyyy + Lzzzz)*M, no coefficient
    A = Lx2 + Ly2 + Lz2 + Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1])+(Lz4*M)[2:N+1,2:N+1,1:N]

end

@testset "3D Multiplication with identical bpc and dx = 0.5, dy = 0.25, dz = 0.05, no coefficients" begin

    N = 100
    M = zeros(N+2,N+2,N+2)
    dx = 0.5
    dy = 0.25
    dz = 0.05
    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx*i)+sin(dy*j) + exp(dz*k)
            end
        end
    end

    # Test a single axis, multiple operators: (Lxxx + Lxxxx)*M
    # Lx3 and Lx4 have the same number of boundary points
    Lx3 = CenteredDifference{1}(3,4,dx,N)
    Lx4 = CenteredDifference{1}(4,4,dx,N)
    A = Lx3 + Lx4

    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)+(Lx4*M))

    # Test a single axis, multiple operators: (Lyyy + Lyyyy)*M
    # Ly3 and Ly4 have the same number of boundary points
    Ly3 = CenteredDifference{2}(3,4,dy,N)
    Ly4 = CenteredDifference{2}(4,4,dy,N)
    A = Ly3 + Ly4

    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)+(Ly4*M))

    # Test a single axis, multiple operators: (Lzzz + Lzzzz)*M
    # Lz3 and Lz4 have the same number of boundary points
    Lz3 = CenteredDifference{3}(3,4,dz,N)
    Lz4 = CenteredDifference{3}(4,4,dz,N)
    A = Lz3 + Lz4

    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lz3*M)+(Lz4*M))

    # Test (Lxxxx + Lyyyy)*M, two boundary points on each axis
    M_temp = zeros(N,N,N+2)
    A = Lx4 + Ly4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test (Lxxx + Lyyy)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Ly3
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:])

    # Test multiple operators on both axis: (Lxxx + Lyyy + Lxxxx + Lyyyy)*M
    A = Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test (Lxxxx + Lzzzz)*M, two boundary points on each axis
    M_temp = zeros(N,N+2,N)
    A = Lx4 + Lz4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test (Lxxx + Lzzz)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Lx3 + Lz3
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N])

    # Test multiple operators on both axis: (Lxxx + Lzzz + Lxxxx + Lzzzz)*M
    A = Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test (Lyyyy + Lzzzz)*M, dx = 1.0, no coefficient, two boundary points on each axis
    M_temp = zeros(N+2,N,N)
    A = Ly4 + Lz4
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test (Lyyy + Lzzz)*M, no coefficient. These operators have non-symmetric interior stencils
    A = Ly3 + Lz3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N])

    # Test multiple operators on both axis: (Lyyy + Lzzz + Lyyyy + Lzzzz)*M
    A = Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test a single operator on each axis: (Lx3 + Ly3 + Lz3)*M
    A = Lx3 + Ly3 + Lz3
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,2:N+1] + (Ly3*M)[2:N+1,1:N,2:N+1] +(Lz3*M)[2:N+1,2:N+1,1:N])

    # Test a single operator on each axis: (Lx4 + Ly4 + Lz4)*M
    A = Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx4*M)[1:N,2:N+1,2:N+1] + (Ly4*M)[2:N+1,1:N,2:N+1] +(Lz4*M)[2:N+1,2:N+1,1:N])

    # Test multiple operators on each axis: (Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4)*M
    A = Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx3*M)[1:N,2:N+1,2:N+1] + (Ly3*M)[2:N+1,1:N,2:N+1] +(Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] + (Ly4*M)[2:N+1,1:N,2:N+1] +(Lz4*M)[2:N+1,2:N+1,1:N])

end

@testset "3D Multiplication with differing bpc and dx = 0.5, dy = 0.25, dz = 0.05, no coefficients" begin

    N = 100
    M = zeros(N+2,N+2,N+2)
    dx = 0.5
    dy = 0.25
    dz = 0.05
    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx*i)+sin(dy*j) + exp(dz*k)
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M
    A = Lx2+Lx4
    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = CenteredDifference{2}(4,4,dy,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M
    A = Ly2+Ly4
    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Lz2 has 0 boundary points
    Lz2 = CenteredDifference{3}(2,2,dz,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,dz,N)
    # Lz4 has 2 boundary points
    Lz4 = CenteredDifference{3}(4,4,dz,N)

    # Test a single axis, multiple operators: (Lzy+Lzzzz)*M
    A = Lz2+Lz4
    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz4*M))

    # Test a single axis, multiple operators: (Lzz++Lzzz+Lzzzz)*M
    A += Lz3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz3*M) + (Lz4*M))

    # Test multiple operators on two axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,:,2:N+1]+(Lz2*M)[2:N+1,:,1:N]+(Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M
    A = Ly2 + Lz2 + Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test operators on all three axis (Lxx + Lyy + Lzz + Lxxx + Lyyy + Lzzz + Lxxxx + Lyyyy + Lzzzz)*M, no coefficient
    A = Lx2 + Ly2 + Lz2 + Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1])+(Lz4*M)[2:N+1,2:N+1,1:N]

end

@show "3D with coefficients"

# This is the same test set as the above test set with the addition of coefficients
@testset "3D Multiplication with coefficients" begin

    N = 100
    M = zeros(N+2,N+2,N+2)
    dx = 0.5
    dy = 0.25
    dz = 0.05
    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx*i)+sin(dy*j) + exp(dz*k)
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = 1.234*CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 2.456*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Test a single axis, multiple operators: (Lxx+Lxxxx)*M
    A = Lx2+Lx4
    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx4*M))

    # Test a single axis, multiple operators: (Lxx++Lxxx+Lxxxx)*M
    A += Lx3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M) + (Lx3*M) + (Lx4*M))

    # Ly2 has 0 boundary points
    Ly2 = CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = 4.014*CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 1.49*CenteredDifference{2}(4,4,dy,N)

    # Test a single axis, multiple operators: (Lyy+Lyyyy)*M
    A = Ly2+Ly4
    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly4*M))

    # Test a single axis, multiple operators: (Lyy++Lyyy+Lyyyy)*M
    A += Ly3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M) + (Ly3*M) + (Ly4*M))

    # Lz2 has 0 boundary points
    Lz2 = 1.546*CenteredDifference{3}(2,2,dz,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,dz,N)
    # Lz4 has 2 boundary points
    Lz4 = 0.55*CenteredDifference{3}(4,4,dz,N)

    # Test a single axis, multiple operators: (Lzy+Lzzzz)*M
    A = Lz2+Lz4
    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz4*M))

    # Test a single axis, multiple operators: (Lzz++Lzzz+Lzzzz)*M
    A += Lz3
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lz2*M) + (Lz3*M) + (Lz4*M))

    # Test multiple operators on two axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,:,2:N+1]+(Lz2*M)[2:N+1,:,1:N]+(Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M
    A = Ly2 + Lz2 + Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test operators on all three axis (Lxx + Lyy + Lzz + Lxxx + Lyyy + Lzzz + Lxxxx + Lyyyy + Lzzzz)*M, no coefficient
    A = Lx2 + Ly2 + Lz2 + Lx3 + Ly3 + Lz3 + Lx4 + Ly4 + Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1])+(Lz4*M)[2:N+1,2:N+1,1:N]

end

@show "3D irregular"

@testset "x, y, and z are all irregular grids" begin

    N = 100
    dx = cumsum(rand(N+2))
    dy = cumsum(rand(N+2))
    dz = cumsum(rand(N+2))
    M = zeros(N+2,N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx[i])+sin(dy[j]) + exp(dz[k])
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Lz2 has 0 boundary points
    Lz2 = CenteredDifference{3}(2,2,dz,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,dz,N)
    # Lz4 has 2 boundary points
    Lz4 = CenteredDifference{3}(4,4,dz,N)

    # Test composition of all first-dimension operators
    A = Lx2+Lx3+Lx4
    M_temp = zeros(N,N+2,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Lx2*M + Lx3*M + Lx4*M)

    # Test composition of all second-dimension operators
    A = Ly2+Ly3+Ly4
    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Ly2*M + Ly3*M + Ly4*M)

    # Test composition of all third-dimension operators
    A = Lz2+Lz3+Lz4
    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ (Lz2*M + Lz3*M + Lz4*M)

    # Test multiple operators on two axis: (Lxx + Lyy + Lxxx + Lyyy + Lxxxx + Lyyyy)*M, no coefficient
    A = Lx2 + Ly2 + Lx3 + Ly3 + Lx4 + Ly4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)

    # Numerical errors accumulating for this test case, more so than the other tests
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Lx3*M)[1:N,2:N+1,:] +(Ly3*M)[2:N+1,1:N,:] + (Lx4*M)[1:N,2:N+1,:] +(Ly4*M)[2:N+1,1:N,:]) rtol=1e-3

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M, no coefficient
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lx4 + Lz4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Lx2*M)[1:N,:,2:N+1]+(Lz2*M)[2:N+1,:,1:N]+(Lx3*M)[1:N,:,2:N+1] +(Lz3*M)[2:N+1,:,1:N] + (Lx4*M)[1:N,:,2:N+1] +(Lz4*M)[2:N+1,:,1:N])

    # Test multiple operators on two axis: (Lxx + Lzz + Lxxx + Lzzz + Lxxxx + Lzzzz)*M, no coefficient
    A = Ly2 + Lz2 + Ly3 + Lz3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)

    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Ly3*M)[:,1:N,2:N+1] +(Lz3*M)[:,2:N+1,1:N] + (Ly4*M)[:,1:N,2:N+1] +(Lz4*M)[:,2:N+1,1:N])

    # Test composition of all operators
    A = Lx2+Lx3+Lx4+Ly2+Ly3+Ly4+Lz2+Lz3+Lz4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1])+(Lz4*M)[2:N+1,2:N+1,1:N]

end

@testset "irregular x grid and regular y and z grids (dy = 0.25, dz = 0.05)" begin

    N = 100
    dy = 0.25
    dz = 0.05
    dy_vec = 0.25:0.25:(N+2)*0.25
    dz_vec = 0.05:0.05:(N+2)*0.05
    dx = cumsum(rand(N+2))
    M = zeros(N+2,N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx[i])+sin(dy*j)+exp(dz*k)
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Lz2 has 0 boundary points
    Lz2 = 1.4*CenteredDifference{3}(2,2,dz,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,dz,N)
    # Lz4 has 2 boundary points
    Lz4 = CenteredDifference{3}(4,4,dz,N)

    # Test that composition of both x and y operators works
    A = Lx2 + Ly2 + Lx3 + Ly3 + Ly4 + Lx4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Lx3*M)[1:N,2:N+1,:]+(Lx4*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Ly3*M)[2:N+1,1:N,:]+(Ly4*M)[2:N+1,1:N,:])

    # Test that composition of both x and z operators works
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lz4 + Lx4
    M_temp = zeros(N,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,:,2:N+1]+(Lx3*M)[1:N,:,2:N+1]+(Lx4*M)[1:N,:,2:N+1]+(Lz2*M)[2:N+1,:,1:N]+(Lz3*M)[2:N+1,:,1:N]+(Lz4*M)[2:N+1,:,1:N])

    # Test that composition of x, y, and z operators works
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lz4 + Lx4 + Ly2 + Ly3 + Ly4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1]+(Lz4*M)[2:N+1,2:N+1,1:N])

    # Last case where we now have some `irregular-grid` operators operating on the
    # regular-spaced axis y

    # These operators are operating on the regular grid y, but are constructed as though
    # they were irregular grid operators. Hence we test if we can seperate irregular and
    # regular gird operators on the same axis
    # Ly2 has 0 boundary points
    _Ly2 = CenteredDifference{2}(2,2,dy_vec,N)
    # Lz3 has 1 boundary point
    _Lz3 = CenteredDifference{3}(3,3,dz_vec,N)
    # Ly4 has 2 boundary points
    _Ly4 = 12.1*CenteredDifference{2}(4,4,dy_vec,N)

    A += _Ly2 + _Lz3 + _Ly4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1]+(Lz4*M)[2:N+1,2:N+1,1:N] + (_Ly2*M)[2:N+1,1:N,2:N+1]+(_Lz3*M)[2:N+1,2:N+1,1:N]+(_Ly4*M)[2:N+1,1:N,2:N+1])

end

@testset "irregular y grid and regular x and z gris (dx = 0.25, dz = 0.05)" begin

    N = 100
    dx = 0.25
    dz = 0.05
    dx_vec = 0.25:0.25:(N+2)*0.25
    dz_vec = 0.05:0.05:(N+2)*0.05
    dy = cumsum(rand(N+2))
    M = zeros(N+2,N+2,N+2)

    for i in 1:N+2
        for j in 1:N+2
            for k in 1:N+2
                M[i,j,k] = cos(dx*i)+sin(dy[j])+exp(dz*k)
            end
        end
    end

    # Lx2 has 0 boundary points
    Lx2 = CenteredDifference{1}(2,2,dx,N)
    # Lx3 has 1 boundary point
    Lx3 = 1.45*CenteredDifference{1}(3,3,dx,N)
    # Lx4 has 2 boundary points
    Lx4 = CenteredDifference{1}(4,4,dx,N)

    # Ly2 has 0 boundary points
    Ly2 = 8.14*CenteredDifference{2}(2,2,dy,N)
    # Ly3 has 1 boundary point
    Ly3 = CenteredDifference{2}(3,3,dy,N)
    # Ly4 has 2 boundary points
    Ly4 = 4.567*CenteredDifference{2}(4,4,dy,N)

    # Lz2 has 0 boundary points
    Lz2 = 1.4*CenteredDifference{3}(2,2,dz,N)
    # Lz3 has 1 boundary point
    Lz3 = CenteredDifference{3}(3,3,dz,N)
    # Lz4 has 2 boundary points
    Lz4 = CenteredDifference{3}(4,4,dz,N)

    # Test that composition of both x and y operators works
    A = Lx2 + Ly2 + Lx3 + Ly3 + Ly4 + Lx4
    M_temp = zeros(N,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,:]+(Lx3*M)[1:N,2:N+1,:]+(Lx4*M)[1:N,2:N+1,:]+(Ly2*M)[2:N+1,1:N,:]+(Ly3*M)[2:N+1,1:N,:]+(Ly4*M)[2:N+1,1:N,:])

    # Test that composition of both y and z operators works
    A = Ly2 + Ly3 + Ly4 + Lz2 + Lz3 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    # Need to figure out why this test is exploding
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])

    ############################################################################
    # Tests to isolate the above problem
    ############################################################################

    A = Ly2 + Ly3 + Ly4
    M_temp = zeros(N+2,N,N+2)
    mul!(M_temp, A, M)
    @test M_temp ≈ Ly2*M + Ly3*M + Ly4*M

    ### Test the addition of Lz2, Lz3, Lz4 seperately

    A = Ly2 + Ly3 + Ly4 + Lz2
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N])

    A = Ly2 + Ly3 + Ly4 + Lz3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz3*M)[:,2:N+1,1:N])

    A = Ly2 + Ly3 + Ly4 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz4*M)[:,2:N+1,1:N])

    ###

    A = Ly2 + Ly3 + Ly4 + Lz2 + Lz3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N])

    A = Ly2 + Ly3 + Ly4 + Lz2 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])

    A = Ly2 + Ly3 + Ly4 + Lz3 + Lz4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])

    ###

    A = Ly2 + Ly3 + Ly4 + Lz2 + Lz2
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz2*M)[:,2:N+1,1:N])
    # It appears that multiple z operators with some y operators is causing the issues

    ###

    A = Lz2 + Lz3 + Lz4
    M_temp = zeros(N+2,N+2,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ Lz2*M + Lz3*M + Lz4*M

    A = Lz2 + Lz3 + Lz4 + Ly2
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])

    A = Lz2 + Lz3 + Lz4 + Ly3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])
    # It seems that the y paddign could be the issue

    A = Lz2 + Lz3 + Lz4 + Ly4
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])
    # It seems that the y paddign could be the issue

    A = Lz2 + Lz3 + Lz4 + Ly4 + Ly3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])
    # It seems that the y paddign could be the issue

    A = Lz2 + Lz3 + Lz4 + Ly4 + Ly2
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Ly4*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N]+(Lz4*M)[:,2:N+1,1:N])
    # It seems that the y paddign could be the issue

    ###

    A = Lz2 + Lz3 +Ly3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N])

    A = Lz3 +Ly3
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly3*M)[:,1:N,2:N+1]+(Lz3*M)[:,2:N+1,1:N])

    A = Lz2 + Lz3 +Ly2
    M_temp = zeros(N+2,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Ly2*M)[:,1:N,2:N+1]+(Lz2*M)[:,2:N+1,1:N]+(Lz3*M)[:,2:N+1,1:N])

    # It seems that the padding of y is forcing multiple z operators to fail

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

    # Test that composition of x, y, and z operators works
    A = Lx2 + Lz2 + Lx3 + Lz3 + Lz4 + Lx4 + Ly2 + Ly3 + Ly4
    M_temp = zeros(N,N,N)
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1]+(Lz4*M)[2:N+1,2:N+1,1:N])

    # Last case where we now have some `irregular-grid` operators operating on the
    # regular-spaced axis x and z

    # These operators are operating on the regular grid x and z, but are constructed as though
    # they were irregular grid operators. Hence we test if we can seperate irregular and
    # regular gird operators on the same axis
    # Lx2 has 0 boundary points
    _Lx2 = CenteredDifference{1}(2,2,dx_vec,N)
    # Lz3 has 1 boundary point
    _Lz3 = CenteredDifference{3}(3,3,dz_vec,N)
    # Lx4 has 2 boundary points
    _Lx4 = 12.1*CenteredDifference{1}(4,4,dx_vec,N)

    A += _Lx2 + _Lz3 + _Lx4
    mul!(M_temp, A, M)
    @test M_temp ≈ ((Lx2*M)[1:N,2:N+1,2:N+1]+(Ly2*M)[2:N+1,1:N,2:N+1]+(Lz2*M)[2:N+1,2:N+1,1:N]+(Lx3*M)[1:N,2:N+1,2:N+1] +(Ly3*M)[2:N+1,1:N,2:N+1]
     + (Lz3*M)[2:N+1,2:N+1,1:N] + (Lx4*M)[1:N,2:N+1,2:N+1] +(Ly4*M)[2:N+1,1:N,2:N+1]+(Lz4*M)[2:N+1,2:N+1,1:N] + (_Lx2*M)[1:N,2:N+1,2:N+1]+(_Lz3*M)[2:N+1,2:N+1,1:N]+(_Lx4*M)[1:N,2:N+1,2:N+1])

end


# 1-dimensional Total Variation (times a constant)

export TotalVariation1D

"""
**1-dimensional Total Variation**

    TotalVariation1D(λ=1)

With a nonnegative scalar parameter λ, returns the function
```math
f(x) = λ ∑_{i=2}^{n} |x_i - x_{i-1}|.
```
"""
struct TotalVariation1D{T <: Real} <: ProximableFunction
    lambda::T
    function TotalVariation1D{T}(lambda::T) where {T <: Real}
        if lambda < 0
            error("parameter λ must be nonnegative")
        else
            new(lambda)
        end
    end
end

is_separable(f::TotalVariation1D) = false
is_convex(f::TotalVariation1D) = true
is_positively_homogeneous(f::TotalVariation1D) = true

TotalVariation1D(lambda::R=1) where {R <: Real} = TotalVariation1D{R}(lambda)

function (f::TotalVariation1D)(x::AbstractArray)
    return f.lambda * norm(x[2:end] - x[1:end-1], 1)
end

# Condat algorithm
# https://lcondat.github.io/publis/Condat-fast_TV-SPL-2013.pdf
function tvnorm_prox_condat(y::AbstractArray, x::AbstractArray, lambda::Real)
    # solves y = arg min_z lambda*sum_k |z_{k+1}-z_k| + 1/2 * ||z-x||^2
    N = length(x)

    k = k0 = kmin = kplus = 1
    vmin = x[1] - lambda
    vmax = x[1] + lambda
    umin = lambda
    umax = -lambda

    while 0 < 1
        while k == N
            if umin < 0
                y[k0:kmin] .= vmin
                kmin += 1
                k = k0 = kmin
                vmin = x[k]
                umin = lambda
                umax = x[k] + lambda - vmax
            elseif umax > 0
                y[k0:kplus] .= vmax
                kplus +=1
                k = k0 = kplus
                vmax = x[k]
                umax = -lambda
                umin = x[k] - lambda - vmin
            else
                y[k0:N] .= vmin + umin/(k-k0+1)
                return
            end

            if k==N
                y[N] = vmin + umin
                return
            end
        end

        if x[k+1] + umin < vmin - lambda
            y[k0:kmin] .= vmin
            kmin += 1
            k = k0 = kplus = kmin
            vmin = x[k]
            vmax = x[k] + 2*lambda
            umin = lambda
            umax = -lambda
        elseif x[k+1] + umax > vmax + lambda
            y[k0:kplus] .= vmax
            kplus += 1
            k = k0 =kmin = kplus
            vmin = x[k] - 2*lambda
            vmax = x[k]
            umin = lambda
            umax = -lambda
        else
            k += 1
            umin = umin + x[k] - vmin
            umax = umax + x[k] - vmax
            if umin >= lambda
                vmin = vmin + (umin-lambda)/(k-k0+1)
                umin = lambda
                kmin = k
            end

            if umax <= -lambda
                vmax += (umax+lambda)/(k-k0+1)
                umax = -lambda
                kplus = k
            end
        end
    end
end

function prox!(y::AbstractArray{T}, f::TotalVariation1D, x::AbstractArray{T}, gamma::Real=1.0) where T <: Real
    a = gamma * f.lambda
    tvnorm_prox_condat(y, x, a)
    return f.lambda * norm(y[2:end] - y[1:end-1], 1)
end

fun_name(f::TotalVariation1D) = "1D Total Variation"
fun_dom(f::TotalVariation1D) = "AbstractArray{Real}"
fun_expr(f::TotalVariation1D) = "x ↦ λ ∑_{i=2}^{n} |x_i - x_{i-1}|"
fun_params(f::TotalVariation1D) = "λ = $(f.lambda)"


# KronigPenny

"""
    mutable struct KronigPennyPotential <: Potential

    KronigPennyPotential(v0, ρ) # Default constructor

holds parameters of Kronig-Penny potential.

Fields
* `v0` : potential height in units of ``E_{1}^{(0)}``
* `ρ` : barrier width in units of period ``a``,  ``\\rho = \\dfrac{b}{a}``, ``0 < \\rho < 1``
  * Note that a position ``x`` is expressed in units of ``a`` throughout this package.

The constructor `KronigPennyPotential(v0, ρ)` confirms that `0 ≤ ρ ≤ 1`, otherwise throws an error.
"""
mutable struct KronigPennyPotential <: Potential
   v0:: Real
   ρ:: Real # ρ = b/a

   KronigPennyPotential(v0, ρ) = 0 ≤ ρ ≤ 1 ? new(v0,ρ) : error("ρ < 0 or 1 < ρ")
end

"""
    get_potential(::KronigPennyPotential)

returns a function ``v`` to evaluate potential ``v(x)`` as a position ``x``,
such that:
```math
\\begin{aligned}
v(x) & = \\begin{cases}
v_{0} &
\\text{inside well, i.e.,}
 \\dfrac{1-\\rho}{2} \\le \\dfrac{x}{a} \\le \\dfrac{1+\\rho}{2},
 \\\\
0 & \\text{outside well}\\end{cases}
\\\\
v(x+a) &= v(x)
\\end{aligned}
```
* Note that a position ``x`` is expressed in units of ``a`` throughout this package.
"""
function get_potential(pot::KronigPennyPotential)
   return x -> (1-pot.ρ)/2 <= mod(x,1)< (1+pot.ρ)/2 ? 0 : pot.v0
end

"""
    mutable struct KronigPennyModel <: Model

holds parameters of Kronig-Penny model.

* Fields given to the constructor:
  * `potential`
  * `nmax`
  * `Ka` : wavenumber multiplied by `a`, period
* Fields calculated by constructor:
  * `mmax` : size of Hamiltonian matrix
  * `qnum` : iterator of quantum numbers
* Fields calculated by constructor and `update!` methods:
  * `hnm` : hamiltonian matrix
"""
mutable struct KronigPennyModel <: Model
   potential::KronigPennyPotential
   nmax::Int64  # maximum of quantum numbers
   mmax::Int64  # size of Hamiltonian
   Ka::Float64
   qnum::Alternates
   hnm::Matrix
end

"""
    KronigPennyModel(::KronigPennyPotential, nmax, Ka)

is a constructor of Kronig-Penny model, and defines other fields: `qnum`, `nmax`, and `hnm`
* Mandantory parameters:
  * `pot` : potential
  * `nmax` : maximum of quantum numbers
  * `Ka` : wavenumber multiplied by `a`, period
"""
function KronigPennyModel(pot::KronigPennyPotential, nmax, Ka)
   qnum=Alternates(nmax)
   mmax=length(qnum)
   hnm=zeros(Float64,(mmax,mmax))
   model_lem=KronigPennyModel(pot,nmax,mmax,Ka,qnum,hnm)
   constuctMatrix(model_lem)
   model_lem
end

"""
    update!(model_lem::KronigPennyModel; Ka=nothing)

updates Kronnig-Penny model.

* Optional parameters
  * `Ka` : wavenumber multiplied by `a`, period
"""
function update!(model_lem::KronigPennyModel; Ka=nothing)
   if !isnothing(Ka)
      model_lem.Ka=Ka
   end
   constuctMatrix(model_lem)
   model_lem
end

"""
    constuctMatrix(model_lem::KronigPennyModel)

computes and fills Hamiltonian matrix fields `hnm` in `model_lem`.

```math
h_{nm} = \\begin{cases}
\\left(2n + \\dfrac{Ka}{\\pi}\\right)^{2} + v_{0} (1-\\rho) &
\\text{for}\\; n = m\\;\\text{(diagonal elements)} \\\\
v_{0}
\\dfrac{(-1)^{m-n+1}}{\\pi} \\dfrac{\\sin \\pi(m-n)\\rho}{m-n}
 &
 \\text{for}\\; n \\neq m\\;\\text{(off-diagonal elements)}\\end{cases}
```
"""
function constuctMatrix(model_lem::KronigPennyModel)
   qnum=model_lem.qnum
   hnm=model_lem.hnm
   Ka=model_lem.Ka
   potential=model_lem.potential
   v0=potential.v0
   ρ=potential.ρ

   for (i,m) in enumerate(qnum)
      for (j,n) in Iterators.take(enumerate(qnum), i-1)
         hnm[i,j] = 0
      end
   end

   for (i,m) in enumerate(qnum)
      hnm[i,i] = (2m+Ka/π)^2 + v0*(1-ρ)
   end

   for (i,m) in enumerate(qnum)
      for (j,n) in Iterators.take(enumerate(qnum), i-1)
         hnm[i,j] = v0*(-1)^(m-n+1)/π * sinpi((m-n)ρ)/(m-n)
      end
      for (j,n) in Iterators.drop(enumerate(qnum), i)
         hnm[i,j] = v0*(-1)^(m-n+1)/π * sinpi((m-n)ρ)/(m-n)
      end
   end
end


using LinearAlgebra: checksquare

"""
    quadratic_expansion(A::IntervalMatrix, t)

Exactly compute the quadratic formula ``At + \\frac{1}{2}A^2t^2`` using interval
arithmetics.

### Input

- `A` -- interval matrix
- `t` -- non-negative time value

### Algorithm

See Lemma 1 in *Reachability Analysis of Linear Systems with Uncertain Parameters
and Inputs* by M. Althoff, O. Stursberg, M. Buss.
"""
function quadratic_expansion(A::IntervalMatrix, t)
    n = checksquare(A)

    t2d2 = t^2/2
    @inline function κ(aii, t)
        if -1/t ∈ aii
            return -1/2
        else
            a = inf(aii) * t + inf(aii)^2 * t2d2
            b = sup(aii) * t + sup(aii)^2 * t2d2
            return min(a, b)
        end
    end

    W = IntervalMatrix(similar(A.mat))

    @inbounds for j in 1:n
        for i in 1:n
            S = 0
            if i ≠ j
                for k in 1:n
                    if k ≠ i && k ≠ j
                        S = S + A[i, k] * A[k, j]
                    end
                end
                W[i, j] = A[i, j] * (t + (A[i, i] + A[j, j]) * t2d2) + S * t2d2
            else
                for k in 1:n
                    if k ≠ i
                        S = S + A[i, k] * A[k, j]
                    end
                end
                u = inf(A[i, i]) * t + inf(A[i, i])^2 * t2d2
                v = sup(A[i, i]) * t + sup(A[i, i])^2 * t2d2
                W[i, i] = Interval(κ(A[i, i], t), max(u, v)) + S * t2d2
            end
        end
    end
    return W
end

"""
    expm_overapproximation(M::IntervalMatrix{T, Interval{T}}, t, p) where {T}

Overapproximation of the exponential of an interval matrix.

### Input

- `A` -- interval matrix
- `t` -- non-negative time value
- `p` -- order of the approximation

### Algorithm

See Theorem 1 in *Reachability Analysis of Linear Systems with Uncertain
Parameters and Inputs* by M. Althoff, O. Stursberg, M. Buss.
"""
function expm_overapproximation(A::IntervalMatrix{T, Interval{T}}, t, p) where {T}
    n = checksquare(A)

    E = _expm_remainder(A, t, p; n=n)
    S = IntervalMatrix(zeros(Interval{T}, n, n))
    Ai = A * A
    fact_num = t^2
    fact_denom = 2
    for i in 3:p
        fact_num *= t
        fact_denom *= i
        Ai = Ai * A
        S = S + Ai * (fact_num / fact_denom)
    end
    W = quadratic_expansion(A, t)
    res = W + S + E

    # add identity matrix implicitly
    for i in 1:n
        res[i, i] += one(T)
    end
    return res
end

function _expm_remainder(A::IntervalMatrix{T}, t, p; n=checksquare(A)) where {T}
    nA = opnorm(A, Inf)
    c = nA * t / (p + 2)
    @assert c < 1
    Γ = IntervalMatrix(fill(zero(T)±one(T), (n , n)))
    return Γ * ((nA*t)^(p+1) * (1/factorial(p + 1) * 1/(1-c)))
end

"""
    expm_underapproximation(M::IntervalMatrix{T, Interval{T}}, t, p) where {T}

Overapproximation of the exponential of an interval matrix.

### Input

- `A` -- interval matrix
- `t` -- non-negative time value
- `p` -- order of the approximation

### Algorithm

See Theorem 2 in *Reachability Analysis of Linear Systems with Uncertain
Parameters and Inputs* by M. Althoff, O. Stursberg, M. Buss.
"""
function expm_underapproximation(A::IntervalMatrix{T, Interval{T}}, t, p) where {T}
    n = checksquare(A)

    Y = zeros(n, n)
    LA = inf(A)
    Ail = LA * LA
    Z = zeros(n, n)
    RA = sup(A)
    Air = RA * RA
    fact_num = t^2
    fact_denom = 2
    for i in 3:p
        fact_num *= t
        fact_denom *= i
        fact = fact_num / fact_denom
        Ail = Ail * LA
        Y = Y + Ail * fact
        Air = Air * RA
        Z = Z + Air * fact
    end

    B = IntervalMatrix(Matrix{Interval{T}}(undef, n , n))
    @inbounds for j in 1:n
        for i in 1:n
            minYZ = min(Y[i, j], Z[i, j])
            maxYZ = max(Y[i, j], Z[i, j])
            B[i, j] = Interval(minYZ, maxYZ)
        end
    end

    W = quadratic_expansion(A, t)
    res = W + B

    # add identity matrix implicitly
    for i in 1:n
        @inbounds res[i, i] += one(T)
    end
    return res
end


# Gleb: the code taken from the ode elimination projects, tests are there

using Oscar

#---------Beginning of utils---------------------------------------------------

function eval_at_dict(poly::P, d::Dict{P, <: RingElem}) where P <: MPolyElem
    """
    Evaluates a polynomial on a dict var => val
    missing values are replaced with zeroes
    """
    point = [get(d, v, base_ring(parent(poly))(0)) for v in gens(parent(poly))]
    return evaluate(poly, point)
end

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

function unpack_fraction(f::MPolyElem)
    return (f, one(parent(f)))
end

function unpack_fraction(f::Generic.Frac{<: MPolyElem})
    return (numerator(f), denominator(f))
end

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

function str_to_var(s::String, ring::MPolyRing)
    ind = findfirst(v -> (string(v) == s), symbols(ring))
    if ind == nothing
        throw(Base.KeyError("Variable $s is not found in ring $ring"))
    end
    return gens(ring)[ind]
end

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

function var_to_str(v::MPolyElem)
    ind = findfirst(vv -> vv == v, gens(parent(v)))
    return string(symbols(parent(v))[ind])
end

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

function switch_ring(v::MPolyElem, ring::MPolyRing)
    """
    For a variable v, returns a variable in ring with the same name
    """
    ind = findfirst(vv -> vv == v, gens(parent(v)))
    return str_to_var(string(symbols(parent(v))[ind]), ring)
end

#---------End of utils---------------------------------------------------------
#------------------------------------------------------------------------------

function truncate_matrix(M::MatElem{<: Generic.AbsSeriesElem}, prec::Int)
    return map(e -> truncate(e, prec), M)
end

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

function matrix_set_prec!(M::MatElem{<: Generic.AbsSeriesElem}, prec::Int)
    map(e -> set_prec!(e, prec), M)
end

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

function ps_matrix_const_term(M::MatElem{<: Generic.AbsSeriesElem})
    return map(e -> coeff(e, 0), M)
end

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

function _matrix_inv_newton_iteration(M::MatElem{T}, Minv::MatElem{T}) where T <: Generic.AbsSeriesElem{<: Generic.FieldElem}
    """
    Performs a single step of Newton iteration for inverting M with
    Minv being a partial result
    """
    return 2 * Minv - Minv * M * Minv
end

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

function ps_matrix_inv(M::MatElem{<: Generic.AbsSeriesElem{<: Generic.FieldElem}}, prec::Int=-1)
    """
    Input:
        - M - a square matrix with entries in a univariate power series ring
          it is assumed that M(0) is invertible and all entries having the same precision
    Output: the inverse of M computed up to prec (the precision of the matrix if nothing)
    """
    const_term = ps_matrix_const_term(M)
    prec = (prec == -1) ? precision(M[1, 1]) : prec
    power_series_ring = base_ring(parent(M))
    result = map(a -> power_series_ring(a), Base.inv(const_term))
    cur_prec = 1
    while cur_prec < prec
        result = _matrix_inv_newton_iteration(M, result)
        cur_prec *= 2
    end
    return result
end

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

function ps_diff(ps::Generic.AbsSeriesElem{<: Generic.RingElem})
    """
    Input: ps - (absolute capped) unvariate power series
    Output: the derivative of ps
    """
    result = zero(parent(ps))
    set_prec!(result, precision(ps))
    for exp in 1:(precision(ps) - 1)
        setcoeff!(result, exp - 1, coeff(ps, exp) * exp)
    end
    return result
end

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

function ps_integrate(ps::Generic.AbsSeriesElem{<: Generic.FieldElem})
    """
    Input: ps - (absolute capped) unvariate power series
    Output: the integral of ps without constant term
    """
    result = zero(parent(ps))
    set_prec!(result, precision(ps) + 1)
    for exp in 0:(precision(ps) - 1)
        setcoeff!(result, exp + 1, coeff(ps, exp) // (exp + 1))
    end
    return result
end

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

function _matrix_homlinear_de_newton_iteration(
        A::MatElem{<: Generic.AbsSeriesElem{T}},
        Y::MatElem{<: Generic.AbsSeriesElem{T}},
        Z::MatElem{<: Generic.AbsSeriesElem{T}},
        cur_prec::Int
    ) where T <: Generic.FieldElem
    Yprime = map(ps_diff, Y)
    Z = Z + truncate_matrix(Z * (one(parent(A)) - Y * Z), cur_prec)
    Y = Y - truncate_matrix(Y * map(ps_integrate, Z * (Yprime - truncate_matrix(A, 2 * cur_prec - 1) * Y)), 2 * cur_prec)
    return (Y, Z)
end

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

function ps_matrix_homlinear_de(
        A::MatElem{<: Generic.AbsSeriesElem{T}},
        Y0::MatElem{<: T},
        prec::Int=-1
    ) where T <: Generic.FieldElem
    """
    Input:
        - A - a square matrix with entries in a univariate power series ring
        - Y0 - a square invertible matrix over the base field
    Output: matrix Y such that Y' = AY up to precision of A - 1 and Y(0) = Y0
    """
    prec = (prec == -1) ? precision(A[1, 1]) : prec
    ps_ring = base_ring(parent(A))
    cur_prec = 1
    Y = (one(parent(A)) + gen(ps_ring) * truncate_matrix(A, cur_prec)) * map(e -> truncate(ps_ring(e), cur_prec), Y0)
    Z = map(e -> truncate(ps_ring(e), cur_prec), Base.inv(Y0))
    while cur_prec < prec
        matrix_set_prec!(Y, 2 * cur_prec)
        matrix_set_prec!(Z, cur_prec)
        Y, Z = _matrix_homlinear_de_newton_iteration(A, Y, Z, cur_prec)
        cur_prec *= 2
    end
    return Y, Z
end

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

function _variation_of_constants(
        A::MatElem{<: Generic.AbsSeriesElem{T}},
        B::MatElem{<: Generic.AbsSeriesElem{T}},
        Yh::MatElem{<: Generic.AbsSeriesElem{T}},
        Zh::MatElem{<: Generic.AbsSeriesElem{T}},
        Y0::MatElem{<: T},
        prec::Int
    ) where T <: Generic.FieldElem
    Zh += truncate_matrix(Zh * (one(parent(A)) - Yh * Zh), prec)
    Y_particular = truncate_matrix(Yh * map(ps_integrate, Zh * B), prec)
    return Y_particular + Yh * map(e -> parent(A[1, 1])(e), Y0)
end

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

function ps_matrix_linear_de(
        A::MatElem{<: Generic.AbsSeriesElem{T}},
        B::MatElem{<: Generic.AbsSeriesElem{T}},
        Y0::MatElem{<: T},
        prec::Int=-1
    ) where T <: Generic.FieldElem
    """
    Input:
        - A, B - square matrices with entries in a univariate power series ring
        - Y0 - a matrix over the base field with the rows number the same as A
    Output: matrix Y such that Y' = AY + B up to precision of A - 1 and Y(0) = Y0
    """
    prec = (prec == -1) ? precision(A[1, 1]) : prec
    n = nrows(A)
    identity = one(MatrixSpace(base_ring(parent(Y0)), n, n))
    Yh, Zh = ps_matrix_homlinear_de(A, identity, prec)
    matrix_set_prec!(Zh, prec)
    return _variation_of_constants(A, B, Yh, Zh, Y0, prec)
end

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

function ps_ode_solution(
        equations::Array{P, 1},
        ic::Dict{P, T},
        inputs::Dict{P, Array{T, 1}},
        prec::Int
    ) where {T <: Generic.FieldElem, P <: MPolyElem{T}}
    """
    Input
        - equations - a system of the form A(x, u, mu)x' - B(x, u, mu) = 0,
                      where A is a generically nonsingular square matrix
        - ic - initial conditions for x's (dictionary)
        - inputs - power series for inputs represented as arrays (dictionary)
        - prec - precision of the solution
        Assumption: A is nonzero at zero
    Output: power series solution of the system
    """
    n = length(equations)
    ring = parent(equations[1])
    S = MatrixSpace(ring, n, n)
    Sv = MatrixSpace(ring, n, 1)
    Svconst = MatrixSpace(base_ring(ring), n, 1)
    eqs = Sv(equations)

    x_vars = filter(v -> (var_to_str(v) * "_dot" in map(string, symbols(ring))), gens(ring))
    x_vars = [x for x in x_vars]
    x_dot_vars = [str_to_var(var_to_str(x) * "_dot", ring) for x in x_vars]

    Jac_dots = S([derivative(p, xd) for p in equations, xd in x_dot_vars])
    Jac_xs = S([derivative(p, x) for p in equations, x in x_vars])

    ps_ring, t = PowerSeriesRing(base_ring(ring), prec, "t"; model=:capped_absolute)
    solution = Dict()
    for (u, coeffs) in inputs
        solution[u] = sum([coeffs[i] * t^(i - 1) for i in 1:length(coeffs)])
    end
    for x in x_vars
        solution[x] = ps_ring(ic[x])
    end
    for xd in x_dot_vars
        solution[xd] = ps_ring(0)
        set_prec!(solution[xd], 1)
    end

    cur_prec = 1
    while cur_prec < prec
        for i in 1:length(x_vars)
            set_prec!(solution[x_vars[i]], 2 * cur_prec)
            set_prec!(solution[x_dot_vars[i]], 2 * cur_prec)
        end
        eval_point = [solution[v] for v in gens(ring)]
        map(ps -> set_prec!(ps, 2 * cur_prec), eval_point)
        eqs_eval = map(p -> evaluate(p, eval_point), eqs)
        J_eval = map(p -> evaluate(p, eval_point), Jac_xs)
        Jd_eval = map(p -> evaluate(p, eval_point), Jac_dots)
        Jd_inv = ps_matrix_inv(Jd_eval, cur_prec)

        X_err = ps_matrix_linear_de(-Jd_inv * J_eval, -Jd_inv * eqs_eval, zero(Svconst))
        for i in 1:length(x_vars)
            solution[x_vars[i]] = solution[x_vars[i]] + X_err[i]
            solution[x_dot_vars[i]] = ps_diff(solution[x_vars[i]])
        end
        cur_prec *= 2
    end

    return solution
end

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

function ps_ode_solution(
        equations::Array{P, 1},
        ic::Dict{P, Int},
        inputs::Dict{P, Array{Int, 1}},
        prec::Int
    ) where P <: MPolyElem{<: Generic.FieldElem}
    bring = base_ring(parent(equations[1]))
    ps_ode_solution(
        equations,
        Dict(x => bring(v) for (x, v) in ic),
        Dict(u => map(v -> bring(v), vv) for (u, vv) in inputs),
        prec
    )
end


using OrdinaryDiffEq

@testset "Gravitational Functional Test" begin
    G = 1
    @testset "The well-known figure Eight choreography" begin

        m1 = MassBody(SVector(-0.995492, 0.0, 0.0), SVector(-0.347902, -0.53393, 0.0), 1.0)
        m2 = MassBody(SVector(0.995492, 0.0, 0.0), SVector(-0.347902, -0.53393, 0.0), 1.0)
        m3 = MassBody(SVector(0.0, 0.0, 0.0), SVector(0.695804, 1.067860, 0.0), 1.0)
        t1 = 0.0
        t2 = 2pi
        tspan = (t1, t2);
        system = GravitationalSystem([m1, m2, m3], G)
        simulation = NBodySimulation(system, tspan)
        sim_result = run_simulation(simulation)
        solution_simo_3 = sim_result.solution;
        ε = 0.1
        for j = 1:3, i = 1:3
            @test solution_simo_3[1][i,j] ≈ solution_simo_3[end][i,j] atol = ε
        end

        @testset "Analyzing simulation result" begin
            r1 = get_position(sim_result, tspan[1], 1)
            r1_by_index = get_position(sim_result, tspan[1], 1)
            v1 = get_velocity(sim_result, tspan[1], 1)
            masses = get_masses(system)
            for i = 1:3
                @test r1[i] == (-0.995492, 0.0, 0.0)[i]
                @test r1_by_index[i] == (-0.995492, 0.0, 0.0)[i]
                @test v1[i] == (-0.347902, -0.53393, 0.0)[i]
                @test masses[i] == 1.0
            end

            ε = 0.001
            e_kin = 1.218
            @test e_kin ≈ kinetic_energy(sim_result, t1) atol = ε
        end

        @testset "Using convertion into SecondOrderODEProblem" begin
            sim_result = run_simulation(simulation, DPRKN6())
            solution_simo_3_2nd = sim_result.solution;
            ε = 0.001
            for i = 1:3, j = 1:3
                @test solution_simo_3_2nd[1][9 + 3(i - 1) + j] ≈ solution_simo_3_2nd[end][9 + 3(i - 1) + j] atol = ε
            end
        end

        @testset "Using symplectic integrators" begin
            sim_result = run_simulation(simulation, VelocityVerlet(), dt=pi / 130)
            solution_simo_3_2nd = sim_result.solution;
            ε = 0.001
            for i = 1:3, j = 1:3
                @test solution_simo_3_2nd[1][9 + 3(i - 1) + j] ≈ solution_simo_3_2nd[end][9 + 3(i - 1) + j] atol = ε
            end

            sim_result = run_simulation(simulation, Yoshida6(), dt=pi / 12)
            solution_simo_3_2nd = sim_result.solution;
            ε = 0.001
            for i = 1:3, j = 1:3
                @test solution_simo_3_2nd[1][9 + 3(i - 1) + j] ≈ solution_simo_3_2nd[end][9 + 3(i - 1) + j] atol = ε
            end
        end
    end

    @testset "5-body choreography" begin
        m1 = MassBody(SVector(1.657666, 0.0, 0.0), SVector(0.0, -0.593786, 0.0), 1.0)
        m2 = MassBody(SVector(0.439775, -0.169717, 0.0), SVector(1.822785, 0.128248, 0.0), 1.0)
        m3 = MassBody(SVector(-1.268608, -0.267651, 0.0), SVector(1.271564, 0.168645, 0.0), 1.0)
        m4 = MassBody(SVector(-1.268608, 0.267651, 0.0), SVector(-1.271564, 0.168645, 0.0), 1.0)
        m5 = MassBody(SVector(0.439775, 0.169717, 0.0), SVector(-1.822785, 0.128248, 0.0), 1.0)
        tspan = (0.0, 2pi);
        system = GravitationalSystem([m1, m2, m3, m4, m5], G)
        simulation = NBodySimulation(system, tspan)
        sim_result = run_simulation(simulation, Tsit5(), abstol=1e-10, reltol=1e-10)
        solution_simo_5 = sim_result.solution;

        ε = 0.01
        for j = 1:5, i = 1:3
            @test solution_simo_5[1][i,j] ≈ solution_simo_5[end][i,j] atol = ε
        end
    end

    @testset "Constructing electorstatic potential parameters entity" begin
        default_potential = GravitationalParameters()
        @test 6.67408e-11 == default_potential.G

        io = IOBuffer()
        potential1 = GravitationalParameters()
        potential2 = GravitationalParameters(35.67)
        @test sprint(io -> show(io, potential1)) == "Gravitational:\n\tG:6.67408e-11\n"
        @test sprint(io -> show(io, potential2)) == "Gravitational:\n\tG:35.67\n"
    end
end