md2 = fill("metadata", 2)
md3 = fill("metadata", 3)
md4 = fill("metadata", 4)
a = BagNode(ArrayNode(rand(3, 4)), [1:4], md4)
b = BagNode(ArrayNode(rand(3, 4)), [1:2, 3:4], md4)
c = BagNode(ArrayNode(rand(3, 4)), [1:1, 2:2, 3:4], md4)
d = BagNode(ArrayNode(rand(3, 4)), [1:4, 0:-1], md4)
wa = WeightedBagNode(ArrayNode(rand(3, 4)), [1:4], rand(1:4, 4), md4)
wb = WeightedBagNode(ArrayNode(rand(3, 4)), [1:2, 3:4], rand(1:4, 4), md4)
wc = WeightedBagNode(ArrayNode(rand(3, 4)), [1:1, 2:2, 3:4], rand(1:4, 4), md4)
wd = WeightedBagNode(ArrayNode(rand(3, 4)), [1:4, 0:-1], rand(1:4, 4), md4)
e = ArrayNode(rand(2, 2), md2)

f = ProductNode((wb,b), md2)
g = ProductNode([c, wc], md3)
h = ProductNode((wc,c), md3)
i = ProductNode((
              b,
              ProductNode((
                        b,
                        BagNode(
                                BagNode(
                                        ArrayNode(rand(2, 4)),
                                        [1:1, 2:2, 3:3, 4:4]
                                       ),
                                [1:3, 4:4]
                               )
                       ))
             ), md2)
k = ProductNode((a = wb, b = b), md2)
l = ProductNode((a = wc, b = c), md3)
m = ProductNode((a = wc, c = c), md3)
n = ProductNode((a = c, c = wc), md3)

@testset "testing nobs" begin
    @test nobs(a) == nobs(wa) == 1
    @test nobs(b) == nobs(wb) == 2
    @test nobs(c) == nobs(wc) == 3
    @test nobs(d) == nobs(wd) == 2
    @test nobs(e) == 2
    @test nobs(f) == nobs(wb) == nobs(b) == nobs(k)
    @test nobs(g) == nobs(c) == nobs(wc) == nobs(l)
    @test nobs(h) == nobs(wc) == nobs(c)
    @test nobs(i) == nobs(b)
end

@testset "testing ArrayNode hcat and vcat" begin
    @test catobs(e, e).data == hcat(e.data, e.data) == reduce(catobs, [e,e]).data
    @test hcat(e, e).data == hcat(e.data, e.data)
    @test vcat(e, e).data == vcat(e.data, e.data)
    x = ArrayNode(randn(2,3), rand(2, 3))
    @test catobs(x, x[0:-1]) isa ArrayNode{Matrix{Float64}, Matrix{Float64}}
    @inferred catobs(x, x[0:-1])
    @test reduce(catobs, [x, x[0:-1]]) isa ArrayNode{Matrix{Float64}, Matrix{Float64}}
    @inferred reduce(catobs, [x, x[0:-1]])
    @test cat(e, e, dims = ndims(e)).data == hcat(e.data, e.data)
end

@testset "testing BagNode catobs" begin
    @test catobs(a, b, c).data.data == hcat(a.data.data, b.data.data, c.data.data)
    @test reduce(catobs, [a, b, c]).data.data == hcat(a.data.data, b.data.data, c.data.data)
    @test catobs(a, b, c).bags.bags == vcat(a.bags, b.bags, c.bags).bags
    @test reduce(catobs, [a, b, c]).bags.bags == vcat(a.bags, b.bags, c.bags).bags
    @test catobs(c, a).data.data == hcat(c.data.data, a.data.data)
    @test reduce(catobs, [c, a]).data.data == hcat(c.data.data, a.data.data)
    @test catobs(c, a).bags.bags == vcat(c.bags, a.bags).bags
    @test reduce(catobs, [c, a]).bags.bags == vcat(c.bags, a.bags).bags
    @test catobs(a, c).data.data == hcat(a.data.data, c.data.data)
    @test reduce(catobs, [a, c]).data.data == hcat(a.data.data, c.data.data)
    @test catobs(a, c).bags.bags == vcat(a.bags, c.bags).bags
    @test reduce(catobs, [a, c]).bags.bags == vcat(a.bags, c.bags).bags
    @test catobs(a, d).data.data == hcat(a.data.data, d.data.data)
    @test reduce(catobs, [a, d]).data.data == hcat(a.data.data, d.data.data)
    @test catobs(a, d).bags.bags == vcat(a.bags, d.bags).bags
    @test reduce(catobs, [a, d]).bags.bags == vcat(a.bags, d.bags).bags
    @test catobs(d, a).data.data == hcat(d.data.data, a.data.data)
    @test reduce(catobs, [d, a]).data.data == hcat(d.data.data, a.data.data)
    @test catobs(d, a).bags.bags == vcat(d.bags, a.bags).bags
    @test reduce(catobs, [d, a]).bags.bags == vcat(d.bags, a.bags).bags
    @test catobs(d).data.data == hcat(d.data.data)
    @test reduce(catobs, [d]).data.data == hcat(d.data.data)
    @test catobs(d).bags.bags == vcat(d.bags).bags
    @test reduce(catobs, [d]).bags.bags == vcat(d.bags).bags
    @test cat(a, b, dims = ndims(a)).data.data == hcat(a.data.data, b.data.data)
end

@testset "testing WeightedBagNode catobs" begin
    @test catobs(wa, wb, wc).data.data == hcat(wa.data.data, wb.data.data, wc.data.data)
    @test reduce(catobs, [wa, wb, wc]).data.data == hcat(wa.data.data, wb.data.data, wc.data.data)
    @test catobs(wa, wb, wc).bags.bags == vcat(wa.bags, wb.bags, wc.bags).bags
    @test reduce(catobs, [wa, wb, wc]).bags.bags == vcat(wa.bags, wb.bags, wc.bags).bags
    @test catobs(wc, wa).data.data == hcat(wc.data.data, wa.data.data)
    @test reduce(catobs, [wc, wa]).data.data == hcat(wc.data.data, wa.data.data)
    @test catobs(wc, wa).bags.bags == vcat(wc.bags, wa.bags).bags
    @test reduce(catobs, [wc, wa]).bags.bags == vcat(wc.bags, wa.bags).bags
    @test catobs(wa, wc).data.data == hcat(wa.data.data, wc.data.data)
    @test reduce(catobs, [wa, wc]).data.data == hcat(wa.data.data, wc.data.data)
    @test catobs(wa, wc).bags.bags == vcat(wa.bags, wc.bags).bags
    @test reduce(catobs, [wa, wc]).bags.bags == vcat(wa.bags, wc.bags).bags
    @test catobs(wa, wd).data.data == hcat(wa.data.data, wd.data.data)
    @test reduce(catobs, [wa, wd]).data.data == hcat(wa.data.data, wd.data.data)
    @test catobs(wa, wd).bags.bags == vcat(wa.bags, wd.bags).bags
    @test reduce(catobs, [wa, wd]).bags.bags == vcat(wa.bags, wd.bags).bags
    @test catobs(wd, wa).data.data == hcat(wd.data.data, wa.data.data)
    @test reduce(catobs, [wd, wa]).data.data == hcat(wd.data.data, wa.data.data)
    @test catobs(wd, wa).bags.bags == vcat(wd.bags, wa.bags).bags
    @test reduce(catobs, [wd, wa]).bags.bags == vcat(wd.bags, wa.bags).bags
    @test catobs(wd).data.data == hcat(wd.data.data)
    @test reduce(catobs, [wd]).data.data == hcat(wd.data.data)
    @test catobs(wd).bags.bags == vcat(wd.bags).bags
    @test reduce(catobs, [wd]).bags.bags == vcat(wd.bags).bags
end

@testset "testing hierarchical catobs on ProductNodes" begin
    @test catobs(f, h).data[1].data.data == reduce(catobs, [f, h]).data[1].data.data ==
        hcat(wb.data.data, wc.data.data)
    @test catobs(f, h).data[2].data.data == reduce(catobs, [f, h]).data[2].data.data ==
        hcat(b.data.data, c.data.data)
    @test catobs(f, h, f).data[1].data.data == reduce(catobs, [f, h, f]).data[1].data.data ==
        hcat(wb.data.data, wc.data.data, wb.data.data)
    @test nobs(catobs(f,h)) == nobs(f) + nobs(h)

    @test catobs(g, g).data[1].data.data == reduce(catobs, [g, g]).data[1].data.data ==
        hcat(c.data.data, c.data.data)
    @test catobs(g, g).data[2].data.data == reduce(catobs, [g, g]).data[2].data.data ==
        hcat(wc.data.data, wc.data.data)
    @test catobs(g, g, g).data[1].data.data == reduce(catobs, [g, g, g]).data[1].data.data ==
        hcat(c.data.data, c.data.data, c.data.data)
    @test nobs(catobs(g, g)) == 2nobs(g)

    @test catobs(k, l).data[1].data.data == hcat(wb.data.data, wc.data.data)
    @test catobs(k, l).data[2].data.data == hcat(b.data.data, c.data.data)
    @test nobs(catobs(k,l)) == nobs(k) + nobs(l)

    # correct length/keyset but different subtrees
    @test_throws MethodError catobs(f, i)
    @test_throws MethodError reduce(catobs, [f, i])
    @test_throws MethodError catobs(m, n)
    @test_throws MethodError reduce(catobs, [m, n])
    # different tuple length or keyset
    @test_throws ArgumentError catobs(g, i)
    @test_throws ArgumentError reduce(catobs, [g, i])
    @test_throws ArgumentError catobs(f, g)
    @test_throws ArgumentError reduce(catobs, [f, g])
    @test_throws ArgumentError catobs(k, m)
    @test_throws ArgumentError reduce(catobs, [k, m])
    @test_throws ArgumentError catobs(l, m)
    @test_throws ArgumentError reduce(catobs, [l, m])
end

@testset "testing catobs with missing values" begin
    @test catobs(a, b, c).data.data == catobs(a, b, missing, c).data.data
    @test catobs(a, b, c).bags.bags == catobs(a, b, missing, c).bags.bags
    @test catobs(wa, wb, wc).data.data == catobs(wa, wb, missing, wc).data.data
    @test catobs(wa, wb, wc).bags.bags == catobs(wa, wb, missing, wc).bags.bags
    @test catobs(wa, wb, wc).weights == catobs(wa, wb, missing, wc).weights
end

@testset "testing catobs stability" begin
    for n in [a, b, c, d, e, f, h, k, l, wa, wb, wc, wd]
        @inferred catobs(n, n)
        @inferred reduce(catobs, [n, n])
    end
end

@testset "testing BagNode indexing" begin
    @test a[1].data.data == a.data.data
    @test a[1].bags.bags == [1:4]
    @test b[1:2].data.data == b.data.data
    @test b[1:2].bags.bags == [1:2,3:4]
    @test b[2].data.data == b.data.data[:,3:4]
    @test b[2].bags.bags == [1:2]
    @test b[1].data.data == b.data.data[:,1:2]
    @test b[1].bags.bags == [1:2]
    @test c[1].data.data == c.data.data[:,1:1]
    @test c[1].bags.bags == [1:1]
    @test c[[1,2]].data.data == c.data.data[:,1:2]
    @test c[[1,2]].bags.bags == [1:1,2:2]
    @test c[[2,1]].data.data == c.data.data[:,[2,1]]
    @test c[[2,1]].bags.bags == [1:1,2:2]
    @test d[[2,1]].data.data == d.data.data
    @test d[[2,1]].bags.bags == [0:-1,1:4]
    @test d[1:2].data.data == d.data.data
    @test d[1:2].bags.bags == [1:4,0:-1]
    @test nobs(d[2].data) == 0
    @test d[2].bags.bags == [0:-1]
    @test isempty(a[2:1].bags.bags)
    @test nobs(a[2:1].data) == 0
end

@testset "testing WeightedBagNode indexing" begin
    @test wa[1].data.data == wa.data.data
    @test wa[1].bags.bags == [1:4]
    @test wb[1:2].data.data == wb.data.data
    @test wb[1:2].bags.bags == [1:2,3:4]
    @test wb[2].data.data == wb.data.data[:,3:4]
    @test wb[2].bags.bags == [1:2]
    @test wb[1].data.data == wb.data.data[:,1:2]
    @test wb[1].bags.bags == [1:2]
    @test wc[1].data.data == wc.data.data[:,1:1]
    @test wc[1].bags.bags == [1:1]
    @test wc[[1,2]].data.data == wc.data.data[:,1:2]
    @test wc[[1,2]].bags.bags == [1:1,2:2]
    @test wc[[2,1]].data.data == wc.data.data[:,[2,1]]
    @test wc[[2,1]].bags.bags == [1:1,2:2]
    @test wd[[2,1]].data.data == wd.data.data
    @test wd[[2,1]].bags.bags == [0:-1,1:4]
    @test wd[1:2].data.data == wd.data.data
    @test wd[1:2].bags.bags == [1:4,0:-1]
    @test nobs(wd[2].data) == 0
    @test wd[2].bags.bags == [0:-1]
end

@testset "testing ProductNode indexing" begin
    @test h[1].data[1].data.data ==  wc[1].data.data
    @test h[[3,2]].data[1].data.data ==  wc[[3,2]].data.data
    @test h[1:2].data[2].data.data ==  c[1:2].data.data
    @test h[end].data[2].data.data ==  c[end].data.data
    @test g[1].data[1].data.data ==  c[1].data.data
    @test g[[3,2]].data[1].data.data ==  c[[3,2]].data.data
    @test g[1:2].data[2].data.data ==  wc[1:2].data.data
    @test g[end].data[2].data.data ==  wc[end].data.data
    @test l[1].data[:a].data.data ==  wc[1].data.data
    @test l[[3,2]].data[:a].data.data ==  wc[[3,2]].data.data
    @test l[1:2].data[:b].data.data ==  c[1:2].data.data
    @test l[end].data[:b].data.data ==  c[end].data.data
end

@testset "testing nested ragged array" begin
    x = BagNode(ArrayNode(rand(3,10)),[1:2,3:3,0:-1,4:5,6:6,7:10])
    y = BagNode(x,[1:2,3:3,4:5,6:6])
    @test y[1].data.data.data == x.data.data[:,1:3]
    @test y[1].data.bags.bags == [1:2,3:3]
    @test y[1:2].data.data.data == x.data.data[:,1:3]
    @test y[1:2].data.bags.bags == [1:2,3:3,0:-1]
    @test y[2:3].data.data.data == x.data.data[:,4:6]
    @test y[2:3].data.bags.bags == [0:-1,1:2,3:3]
end

@testset "testing sparsify" begin
    @test sparsify(zeros(10, 10), 0.05) isa SparseMatrixCSC
    @test sparsify(randn(10, 10), 0.05) isa Matrix
    @test sparsify(randn(10), 0.05) isa Vector
end

@testset "testing sparsify and mapdata" begin
    x = ProductNode((ProductNode((ArrayNode(randn(5,5)), ArrayNode(zeros(5,5)))), ArrayNode(zeros(5,5))))
    xs = mapdata(i -> sparsify(i, 0.05), x)
    @test xs.data[2].data isa SparseMatrixCSC
    @test xs.data[1].data[2].data isa SparseMatrixCSC
    @test xs.data[1].data[1].data == x.data[1].data[1].data
end

@testset "testing missing mapdata" begin
    x = ProductNode((ProductNode((ArrayNode(randn(5,5)), ArrayNode(zeros(5,5)))), ArrayNode(zeros(5,5))), BagNode(missing, AlignedBags([0:-1]), nothing))
    xs = mapdata(i -> sparsify(i, 0.05), x)
    @test xs.data[2].data isa SparseMatrixCSC
    @test xs.data[1].data[2].data isa SparseMatrixCSC
    @test xs.data[1].data[1].data == x.data[1].data[1].data
end

@testset "testing equals and hash" begin
    a2 = deepcopy(a)
    i2 = deepcopy(i)
    k2 = ProductNode((a = wb, b = b), md2)
    metadata1 = fill("metadata", 4)
    metadata2 = "Oh, Hi Mark"
    r = rand(3,4)
    x1 = BagNode(ArrayNode(r),[1:4], metadata1)
    x2 = BagNode(ArrayNode(r),[1:4], metadata1)
    x3 = BagNode(ArrayNode(r),[1:4], metadata2)

    @test a != b
    @test a != i
    @test a == a2
    @test i == i2
    @test i != k
    @test k == k2
    @test x1 == x2
    @test x1 != x3
    @test hash(a) !== hash(b)
    @test hash(a) === hash(a2)
    @test hash(a) !== hash(i)
    @test hash(i) === hash(i2)
    @test hash(i) !== hash(k)
    @test hash(k) === hash(k2)
    @test hash(x1) === hash(x2)
    @test hash(x1) !== hash(x3)
end

@testset "testing equals with missings" begin
    a = ArrayNode([0.0 missing 0.0 0.0 1.0])
    b = ArrayNode([0.0 missing 0.0 0.0 2.0])
    c = ArrayNode([0.0 missing 0.0 missing 2.0])
    @test a != a
    @test isequal(a, a)
    @test a != b
    @test !isequal(a, b)
    @test b != b
    @test isequal(b, b)
    @test a != c
    @test !isequal(a, c)
    @test b != c
    @test !isequal(b, c)
    @test c != c
    @test isequal(c, c)

    d = ProductNode((; a=a, b=b))
    e = ProductNode((; a=a, b=b), [])
    @test isequal(d, d)
    @test !isequal(d, e)
    @test d != d
    @test d != e

    f = BagNode(a,[1:4])
    g = BagNode(b,[1:4])
    @test f != f
    @test f != g
    @test isequal(f, f)
    @test !isequal(f, g)

    wf = WeightedBagNode(a, [1:4], rand(1:4, 4))
    wg = WeightedBagNode(b, [1:4], rand(1:4, 4))
    @test wf != wf
    @test wf != wg
    @test isequal(wf, wf)
    @test !isequal(wf, wg)

    h = LazyNode(:Test, a.data)
    i = LazyNode(:Test, b.data)
    @test isequal(h == h, missing)
    @test h != i
    @test isequal(h, h)
    @test !isequal(h, i)
end

@testset "dropmeta" begin
    for n in [a, b, c, d, e, f, h, k, l, wa, wb, wc, wd]
        @test dropmeta(n) |> Mill.metadata |> isnothing
    end
end


function _packdensematrix!(A::GBVecOrMat{T}, M::DenseVecOrMat; desc = nothing) where {T}
    desc = _handledescriptor(desc)
    Csize = length(A) * sizeof(T)
    values = Ref{Ptr{Cvoid}}(pointer(M))
    isuniform = false
    libgb.GxB_Matrix_pack_FullC(
        A.p,
        values,
        Csize,
        isuniform,
        desc
    )
    return A
end

function _packcscmatrix!(
    A::GBVecOrMat{T},
    colptr::Vector{libgb.GrB_Index},
    rowidx::Vector{libgb.GrB_Index},
    values::Vector{T};
    desc = nothing
    ) where {T}
    colptr .-= 1
    rowidx .-= 1
    colptrsize = length(colptr) * sizeof(libgb.GrB_Index)
    rowidxsize = length(rowidx) * sizeof(libgb.GrB_Index)
    valsize = length(values) * sizeof(T)
    colptr = Ref{Ptr{libgb.GrB_Index}}(pointer(colptr))
    rowidx = Ref{Ptr{libgb.GrB_Index}}(pointer(rowidx))
    values = Ref{Ptr{Cvoid}}(pointer(values))
    desc = _handledescriptor(desc)

    x = libgb.GxB_Matrix_pack_CSC(
        A,
        colptr,
        rowidx,
        values,
        colptrsize,
        rowidxsize,
        valsize,
        false,
        false,
        desc
    )
    return A
end

function _packcsrmatrix!(
    A::GBVecOrMat{T},
    rowptr::Vector{libgb.GrB_Index},
    colidx::Vector{libgb.GrB_Index},
    values::Vector{T};
    desc = nothing
    ) where {T}
    rowptr .-= 1
    colidx .-= 1
    rowptrsize = length(rowptr) * sizeof(libgb.GrB_Index)
    colidxsize = length(colidx) * sizeof(libgb.GrB_Index)
    valsize = length(values) * sizeof(T)
    rowptr = Ref{Ptr{libgb.GrB_Index}}(pointer(rowptr))
    colidx = Ref{Ptr{libgb.GrB_Index}}(pointer(colidx))
    values = Ref{Ptr{Cvoid}}(pointer(values))
    desc = _handledescriptor(desc)

    libgb.GxB_Matrix_pack_CSC(
        A,
        rowptr,
        colidx,
        values,
        rowptrsize,
        colidxsize,
        valsize,
        false,
        false,
        desc
    )
    return A
end


using DataStructures
using Test

#  Setup for pycall tests - done by travis, uncomment for manual run
# using Conda; ENV["PYTHON"] = Conda.PYTHONDIR
# using Pkg
# Pkg.build("PyCall")

# Conda.add_channel("conda-forge")
# Conda.add("cfgrib")

using PyCall

@testset "era5-levels-members DataSet parity" begin
    test_file = joinpath(dir_testfiles, "era5-levels-members.grib")
    res = CfGRIB.DataSet(test_file)

    @testset "Variables" begin
        @test res.variables["number"].dimensions == ("number",)
        @test res.variables["number"].data == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        @test res.variables["number"].attributes == Dict(
            "units" => "1",
            "long_name" => "ensemble member numerical id",
            "standard_name" => "realization"
        )

        @test res.variables["time"].dimensions == ("time",)
        @test res.variables["time"].data == [
            1483228800, 1483272000, 1483315200, 1483358400
        ]
        @test res.variables["time"].attributes == Dict(
            "units" => "seconds since 1970-01-01T00:00:00",
            "calendar" => "proleptic_gregorian",
            "long_name" => "initial time of forecast",
            "standard_name" => "forecast_reference_time"
        )

        @test res.variables["step"].dimensions == ()
        @test res.variables["step"].data == 0 #  In python this is a float
        @test res.variables["step"].attributes == Dict(
            "units" => "hours",
            "long_name" => "time since forecast_reference_time",
            "standard_name" => "forecast_period"
        )

        @test res.variables["isobaricInhPa"].dimensions == ("isobaricInhPa",)
        @test res.variables["isobaricInhPa"].data == [850, 500]
        @test res.variables["isobaricInhPa"].attributes == Dict(
            "units" => "hPa",
            "stored_direction" => "decreasing",
            "long_name" => "pressure",
            "standard_name" => "air_pressure",
            "positive" => "down"
        )

        @test res.variables["latitude"].dimensions == ("latitude",)
        @test res.variables["latitude"].data == [
            90.,   87.,  84.,  81.,  78.,  75.,  72.,  69.,  66.,  63.,  60.,
            57.,   54.,  51.,  48.,  45.,  42.,  39.,  36.,  33.,  30.,  27.,
            24.,   21.,  18.,  15.,  12.,   9.,   6.,   3.,   0.,  -3.,  -6.,
            -9.,  -12., -15., -18., -21., -24., -27., -30., -33., -36., -39.,
            -42., -45., -48., -51., -54., -57., -60., -63., -66., -69., -72.,
            -75., -78., -81., -84., -87., -90.
        ]
        @test res.variables["latitude"].attributes == Dict(
            "units" => "degrees_north",
            "stored_direction" => "decreasing",
            "long_name" => "latitude",
            "standard_name" => "latitude"
        )

        @test res.variables["longitude"].dimensions == ("longitude",)
        @test res.variables["longitude"].data == [
            0.,   3.,   6.,   9.,  12.,  15.,  18.,  21.,  24.,  27.,  30.,
            33.,  36.,  39.,  42.,  45.,  48.,  51.,  54.,  57.,  60.,  63.,
            66.,  69.,  72.,  75.,  78.,  81.,  84.,  87.,  90.,  93.,  96.,
            99., 102., 105., 108., 111., 114., 117., 120., 123., 126., 129.,
            132., 135., 138., 141., 144., 147., 150., 153., 156., 159., 162.,
            165., 168., 171., 174., 177., 180., 183., 186., 189., 192., 195.,
            198., 201., 204., 207., 210., 213., 216., 219., 222., 225., 228.,
            231., 234., 237., 240., 243., 246., 249., 252., 255., 258., 261.,
            264., 267., 270., 273., 276., 279., 282., 285., 288., 291., 294.,
            297., 300., 303., 306., 309., 312., 315., 318., 321., 324., 327.,
            330., 333., 336., 339., 342., 345., 348., 351., 354., 357.
        ]
        @test res.variables["longitude"].attributes == Dict(
            "units" => "degrees_east",
            "long_name" => "longitude",
            "standard_name" => "longitude"
        )

        @test res.variables["valid_time"].dimensions == ("time",)
        @test res.variables["valid_time"].data == [
            1.4832288e+09, 1.4832720e+09, 1.4833152e+09, 1.4833584e+09
        ]
        @test res.variables["valid_time"].attributes == Dict(
            "units" => "seconds since 1970-01-01T00:00:00",
            "calendar" => "proleptic_gregorian",
            "long_name" => "time",
            "standard_name" => "time"
    )
    end

    @testset "OnDiskArray" begin
        @test res.variables["z"].dimensions == (
            "number", "time", "isobaricInhPa", "longitude", "latitude"
        )
        @test typeof(res.variables["z"].data) == CfGRIB.OnDiskArray
        @test size(res.variables["z"].data) == (10, 4, 2, 120, 61)
        @test_broken res.variables["z"].missing_value == 9999
        #  Currently these are not `OrderedDict`, and in the wrong order
        #  TODO: Make these into 'OrderedDict', and order correctly
        @test_broken typeof(res.variables["z"].attributes) == OrderedDict
        @test res.variables["z"].attributes == OrderedDict(
            "GRIB_paramId"                            => 129,
            "GRIB_shortName"                          => "z",
            "GRIB_units"                              => "m**2 s**-2",
            "GRIB_name"                               => "Geopotential",
            "GRIB_cfName"                             => "geopotential",
            "GRIB_cfVarName"                          => "z",
            "GRIB_dataType"                           => "an",
            "GRIB_missingValue"                       => 9999,
            "GRIB_numberOfPoints"                     => 7320,
            "GRIB_totalNumber"                        => 10,
            "GRIB_typeOfLevel"                        => "isobaricInhPa",
            "GRIB_NV"                                 => 0,
            "GRIB_stepUnits"                          => 1,
            "GRIB_stepType"                           => "instant",
            "GRIB_gridType"                           => "regular_ll",
            "GRIB_gridDefinitionDescription"          => "Latitude/Longitude Grid",
            "GRIB_Nx"                                 => 120,
            "GRIB_iDirectionIncrementInDegrees"       => 3.0,
            "GRIB_iScansNegatively"                   => 0,
            "GRIB_longitudeOfFirstGridPointInDegrees" => 0.0,
            "GRIB_longitudeOfLastGridPointInDegrees"  => 357.0,
            "GRIB_Ny"                                 => 61,
            "GRIB_jDirectionIncrementInDegrees"       => 3.0,
            "GRIB_jPointsAreConsecutive"              => 0,
            "GRIB_jScansPositively"                   => 0,
            "GRIB_latitudeOfFirstGridPointInDegrees"  => 90.0,
            "GRIB_latitudeOfLastGridPointInDegrees"   => -90.0,
            "long_name"                               => "Geopotential",
            "units"                                   => "m**2 s**-2",
            "standard_name"                           => "geopotential",
            "coordinates"                             => "number time step isobaricInhPa latitude longitude valid_time"
        )
        #  Should be approx. equal to python:
        #    `res.variables["z"].data[:, :, 1, 1, 1]`
        @test res.variables["z"].data[:, :, 2, 2, 2] ≈ [
            51213.703 51188.277 50836.094 50185.605
            51216.605 51180.03  50833.027 50210.992
            51232.766 51160.65  50823.082 50199.85
            51203.06  51182.15  50816.246 50177.18
            51235.203 51181.234 50823.41  50182.023
            51207.727 51179.25  50813.094 50190.875
            51210.867 51179.35  50818.527 50183.945
            51193.94  51153.36  50794.383 50159.473
            51203.395 51183.875 50835.9   50207.61
            51197.383 51183.77  50819.78  50192.44
        ]
        @test size(res.variables["z"].data[1, 1, 1, :, :]) == (120, 61) # ' vs python
        #  Should be approx. equal to python:
        #    `res.variables["z"].data[0, 0, 0, :, :][:4, :4]`
        #  Used `adjoint` explicitly for clarity, the ' operator works here too
        @test adjoint(res.variables["z"].data[1, 1, 1, :, :])[1:4, 1:4] ≈ [
            14201.754 14201.754 14201.754 14201.754
            14323.254 14333.129 14342.879 14353.504
            14235.629 14252.129 14266.254 14275.879
            14104.254 14085.879 14069.504 14048.379
        ]
        #  Should be approx. equal to python:
        #    `res.variables["z"].data[0, 0, 0, :, :][-4:, -4:]`
        @test adjoint(res.variables["z"].data[1, 1, 1, :, :])[end-3:end, end-3:end] ≈ [
            12140.004 12226.504 12312.629 12406.004
            12417.629 12470.379 12520.129 12568.629
            12633.879 12658.754 12682.879 12705.879
            12660.379 12660.379 12660.379 12660.379
        ]

        @test res.variables["t"].dimensions == (
            "number", "time", "isobaricInhPa", "longitude", "latitude"
        )
        @test typeof(res.variables["t"].data) == CfGRIB.OnDiskArray
        @test size(res.variables["t"].data) == (10, 4, 2, 120, 61)
        @test_broken res.variables["t"].missing_value == 9999
        #  TODO: Make these into 'OrderedDict', and order correctly
        @test_broken typeof(res.variables["t"].attributes) == OrderedDict
        @test res.variables["t"].attributes == OrderedDict(
            "GRIB_paramId"                            => 130,
            "GRIB_shortName"                          => "t",
            "GRIB_units"                              => "K",
            "GRIB_name"                               => "Temperature",
            "GRIB_cfName"                             => "air_temperature",
            "GRIB_cfVarName"                          => "t",
            "GRIB_dataType"                           => "an",
            "GRIB_missingValue"                       => 9999,
            "GRIB_numberOfPoints"                     => 7320,
            "GRIB_totalNumber"                        => 10,
            "GRIB_typeOfLevel"                        => "isobaricInhPa",
            "GRIB_NV"                                 => 0,
            "GRIB_stepUnits"                          => 1,
            "GRIB_stepType"                           => "instant",
            "GRIB_gridType"                           => "regular_ll",
            "GRIB_gridDefinitionDescription"          => "Latitude/Longitude Grid",
            "GRIB_Nx"                                 => 120,
            "GRIB_iDirectionIncrementInDegrees"       => 3.0,
            "GRIB_iScansNegatively"                   => 0,
            "GRIB_longitudeOfFirstGridPointInDegrees" => 0.0,
            "GRIB_longitudeOfLastGridPointInDegrees"  => 357.0,
            "GRIB_Ny"                                 => 61,
            "GRIB_jDirectionIncrementInDegrees"       => 3.0,
            "GRIB_jPointsAreConsecutive"              => 0,
            "GRIB_jScansPositively"                   => 0,
            "GRIB_latitudeOfFirstGridPointInDegrees"  => 90.0,
            "GRIB_latitudeOfLastGridPointInDegrees"   => -90.0,
            "long_name"                               => "Temperature",
            "units"                                   => "K",
            "standard_name"                           => "air_temperature",
            "coordinates"                             => "number time step isobaricInhPa latitude longitude valid_time",
        )
        #  Should be approx. equal to python:
        #    `res.variables["t"].data[:, :, 0, 1, 2]`
        @test res.variables["t"].data[:, :, 1, 3, 2] ≈ [
            252.73932 253.1473  252.33952 253.19273
            252.73364 253.0484  252.51244 253.55228
            252.90796 253.02562 252.53241 253.17216
            252.62859 252.69759 252.48486 253.37857
            252.89291 253.22601 252.22084 254.01117
            252.91525 253.18896 252.48277 254.20941
            252.78569 253.13112 252.4354  253.75746
            252.38048 252.64601 252.12773 253.89052
            252.68658 252.9497  252.17213 254.07466
            252.59758 252.99641 252.4409  253.73424
        ]
        @test size(res.variables["t"].data[1, 1, 1, :, :]) == (120, 61) # ' vs python
        #  Should be approx. equal to python:
        #    `res.variables["t"].data[0, 0, 0, :, :][:4, :4]`
        @test adjoint(res.variables["t"].data[1, 1, 1, :, :])[1:4, 1:4] ≈ [
            252.66315 252.66315 252.66315 252.66315
            252.61041 252.68658 252.73932 252.78229
            251.11041 251.36627 251.6026  251.8487
            253.93658 254.41315 254.57526 254.62994
        ]
        #  Should be approx. equal to python:
        #    `res.variables["t"].data[0, 0, 0, :, :][-4:, -4:]`
        @test adjoint(res.variables["t"].data[1, 1, 1, :, :])[end-3:end, end-3:end] ≈ [
            260.59283 260.75104 260.6983  260.6983
            259.23737 259.14752 259.04987 258.8897
            258.56158 258.40143 258.27643 258.1983
            258.5401  258.5401  258.5401  258.5401
        ]
    end
end

cfgrib_dataset_py = pyimport("cfgrib.dataset")

test_files = [
    # "era5-levels-corrupted.grib",# - skip corrupted file tests
    "era5-levels-members.grib",
    "fields_with_missing_values.grib",
    "forecast_monthly_ukmo.grib",
    # "hpa_and_pa.grib",# - DatasetBuildError("multiple values for unique key, try re-open the file with one of
    "lambert_grid.grib",
    "multi_param_on_multi_dims.grib",
    # "reduced_gg.grib", - TODO: enable test with next release of Julia
    "regular_gg_ml.grib",
    "regular_gg_ml_g2.grib",
    "regular_gg_pl.grib",
    "regular_gg_sfc.grib",
    # "regular_gg_wrong_increment.grib", # Not clear how to handle data that does not fit into given array size
    "regular_ll_msl.grib",
    "regular_ll_sfc.grib",
    "regular_ll_wrong_increment.grib",
    "scanning_mode_64.grib",
    # "spherical_harmonics.grib", - ecCodes provides no latitudes/longitudes for gridType='sh'
    "t_analysis_and_fc_0.grib",
    # "t_on_different_level_types.grib", - DatasetBuildError("multiple values for unique key, try re-open the file with one of
    # "tp_on_different_grid_resolutions.grib", - DatasetBuildError("multiple values for unique key, try re-open the file with one of
    # "uv_on_different_levels.grib" - cfgrib.dataset.DatasetBuildError: key present and new value is different
]

attributes_key_blacklist = [
    "history"
]

python_type_mapping = Dict(
    "<class 'cfgrib.dataset.Variable'>" => CfGRIB.Variable,
    "<class 'cfgrib.dataset.OnDiskArray'>" => CfGRIB.OnDiskArray
)

@testset "pycall tests for $test_file" for test_file in test_files
    test_file_path = joinpath(dir_testfiles, test_file)
    res_py = cfgrib_dataset_py.open_file(test_file_path)
    res_jl = CfGRIB.DataSet(test_file_path)

    res_py_attributes = copy(res_py.attributes)
    [delete!(res_py_attributes, key) for key in attributes_key_blacklist]
    [delete!(res_jl.attributes, key) for key in attributes_key_blacklist]
    @test res_jl.attributes == res_py_attributes

    @test res_jl.dimensions == res_py.dimensions

    @test Set(keys(res_py.variables)) == Set(keys(res_jl.variables))

    @testset "variable $var" for var in keys(res_jl.variables)
        var_jl = res_jl.variables[var]
        var_py = res_py.variables[var]

        var_d_type_py = typeof(var_py.data)
        var_d_type_py_native = pystr(py"type($(var_py.data))")

        #  If the returned value is not a PyObject, it has been automatically
        #  converted to a native julia type, e.g. numpyarray/list -> Julia Array
        if var_d_type_py != PyObject
            if var_d_type_py <: Number
                @test var_jl.data ≈ var_py.data
            elseif var_d_type_py <: Array{T,1} where T <: Number
                #  One-dimensional arrays should be identical
                @test var_jl.data ≈ var_py.data
            elseif var_d_type_py <: Array
                #  Too much effort to try and figure out which dimension(s) have
                #  been transposed due to the row/col differences between julia
                #  and python, easier to just test if any case works
                if size(var_jl.data) == size(var_py.data)
                    if var_jl.data ≈ var_py.data
                        @test true
                    else
                        @test adjoint(var_jl.data) ≈ var_py.data
                    end
                elseif size(adjoint(var_jl.data)) == size(var_py.data)
                    @test adjoint(var_jl.data) ≈ var_py.data
                else
                    #  Values not equal for adjoint or direct comparison
                    @test false
                end
            else
                @warn "Julia Native var $var of type $var_d_type_py not tested"
            end
        #  If it is still a PyObject, it's most likely a CfGRIB 'OnDiskArray'
        elseif var_d_type_py == PyObject
            var_d_type_py_jl = python_type_mapping[var_d_type_py_native]
            if var_d_type_py_jl == CfGRIB.OnDiskArray
                @test var_jl.data.geo_ndim == var_py.data.geo_ndim

                jl_geo_dims = var_jl.dimensions[end-var_jl.data.geo_ndim+1:end]
                py_geo_dims = var_py.dimensions[end-var_jl.data.geo_ndim+1:end]
                transpose_flag = !(jl_geo_dims == py_geo_dims)

                multidim_idx = tuple(
                    ones(Int, length(size(var_jl.data)) - var_jl.data.geo_ndim)...,
                    repeat([Colon()], var_jl.data.geo_ndim)...
                )
                if transpose_flag
                    var_jl_dims = (
                        var_jl.dimensions[1:end-var_jl.data.geo_ndim]...,
                        var_jl.dimensions[end-var_jl.data.geo_ndim+1:end][end:-1:1]...
                    )
                    var_jl_data = adjoint(var_jl.data[multidim_idx...])
                elseif var_jl.data.geo_dims[end:-1:1] == var_py.data.geo_dims
                    var_jl_dims = var_jl.dimensions
                    var_jl_data = var_jl.data[multidim_idx...]
                end

                var_py_data = var_py.data.build_array()[multidim_idx...]
                var_py_data = replace(var_py_data, NaN=>missing)

                @test var_jl_dims == var_py.dimensions
                @test ismissing.(var_jl_data) ≈ ismissing.(var_py_data)
                @test collect(skipmissing(var_jl_data)) ≈ collect(skipmissing(var_py_data))
            else
                @warn "PyObject var $var of type $var_d_type_py not tested"
            end
        else
            @warn "var $var of type $var_d_type_py not tested"
        end

        @test var_jl.attributes == var_py.attributes
    end
end


using Images: data, Image, load
ENV["PYTHON"] = "/usr/bin/python3"
using PyCall
using PartsBasedDetector: make_estimator, estimate

filename = "dress.jpg"
url = "http://images0.chictopia.com/photos/districtofchic/9466442894/" *
      "black-skinny-jeans-dl1961-jeans-sky-blue-pixie-market-shirt_400.jpg"

filename_png = replace(filename, ".jpg", ".png")
run(`wget --continue $url -O $filename`)
run(`convert $filename $filename_png`)

estimator = make_estimator()
candidates = estimate(estimator, filename, n_candidates = 3)
parts = candidates[1].parts
x, y = parts[:, 1], parts[:, 2]

@pyimport matplotlib.pyplot as plt
image = plt.imread(filename_png)
plt.imshow(image)
plt.plot(x, y, color="r", marker="o", ls="")
plt.show()


export SSP

"""
    Atmostphere
All objects describing atmospheric condtions should be a subtype of this.
This includes objects: SSP
"""
abstract type Atmosphere end

# --------------- #
# - SSP - #
# --------------- #

"""
    SSP
Sound speed profile.
### Fields:
* `sound_speed_profile`  - x-depth, y-sound speed.
### Available Constructors:
* `SSP(;sound_speed_profile= Vec2(10.0f0, 10.0f0))`
* `SSP(sound_speed_profile)`
"""
struct SSP{T<:AbstractArray} <: Atmosphere
    sound_speed_profile::Vec2{T}
end

SSP(;sound_speed_profile= Vec2(10.0f0, 10.0f0)) =
    SSP(sound_speed_profile)


@component struct EditorMoveGuides
    es::NTuple{4,Entity} #for now only x,y,z axis and sphere
end
@component struct EditorRotateGuides
    es::NTuple{3,Entity}
end

@component @with_kw struct Movable
    axis_thickness::Float32 = 0.2f0
    axis_length::Float32    = 5.0f0
    font_size::Float32      = 0.3f0
end

@component @with_kw struct Rotatable
    ring_radii::Float32 = 5.0f0
    rotation_speed::Float32 = 0.02f0 / 200
    font_size::Float32 = 0.3f0
end

struct Editor <: System end

function Overseer.requested_components(::Editor)
    return (Movable, Rotatable, InstancedDefaultVao, EditorMoveGuides, EditorRotateGuides)
end

function Overseer.update(::Editor, m::AbstractLedger)
    poly_vao      = m[InstancedDefaultVao]
    movable       = m[Movable]
    rotatable     = m[Rotatable]
    spatial       = m[Spatial]
    rotation_comp = m[Rotation]
    move_guides   = m[EditorMoveGuides]
    rotate_guides = m[EditorRotateGuides]
    text          = m[Text]
    text_vao      = m[TextVao]
    line_vao      = m[LineVao]

    poly = m[PolygonGeometry] #arrows are here
    selectable = m[Selectable]
    camera = singleton(m, Camera3D) #this we need to know how much a dragged mouse correspond to a movement in the world
    camera_entity = Entity(m[Camera3D].indices.packed[1])
    eye = spatial[camera_entity].position
    keyboard = singleton(m, Keyboard)
    mouse = singleton(m, Mouse)
    rotated = false
    moved = false

    set_visibility = b -> begin
        for g in (move_guides, rotate_guides)
            for e in @entities_in(g)
                for guide_entity in g[e].es
                    if guide_entity in poly_vao
                        poly_vao[guide_entity].visible = b
                    end
                    if guide_entity in text_vao
                        text_vao[guide_entity].visible = b
                    end
                    if guide_entity in line_vao
                        line_vao[guide_entity].visible = b
                    end
                end
            end
        end
    end

    if pressed(keyboard) && keyboard.button ∈ CTRL_KEYS
        set_visibility(true)
        #setup move guides
        for e in @entities_in(movable && spatial && !move_guides)
            mov            = movable[e]
            editor_guides  = assemble_axis_arrows(spatial[e].position; axis_length = mov.axis_length, thickness = mov.axis_thickness)
            guide_entities = map(x -> Entity(m, x..., Selectable()), editor_guides)
            move_guides[e] = EditorMoveGuides(guide_entities)
        end

        #setup rotation_comp guides
        for e in @entities_in(rotatable && spatial && !rotate_guides)
            if !in(e, rotation_comp)
                rotation_comp[e] = Rotation(Z_AXIS, 0.0f0)
            end

            editor_guides = assemble_orientation_sphere(spatial[e].position;
                                                        radius = rotatable[e].ring_radii)
            guide_entities = map(x -> Entity(m, x..., Selectable(; color_modifier = 1.5f0)),
                                 editor_guides)
            rotate_guides[e] = EditorRotateGuides(guide_entities)
        end

        #sanitize positions of move_guides
        for e in @entities_in(move_guides)
            guide_entities = move_guides[e].es
            t_spat = spatial[e]
            for g in guide_entities
                spatial[g] = t_spat
            end
            pos = t_spat.position
            text[guide_entities[1]] = Text(;
                                           str = "Position: (x=$(pos[1]), y=$(pos[2]), z=$(pos[3]))",
                                           font_size = movable[e].font_size,
                                           offset = Vec3f0(0, 0, -2.0f0))
        end

        #sanitize positions and orientiations of rotation_comp guides
        for e in @entities_in(rotate_guides)
            rot = rotation_comp[e]
            guide_entities = rotate_guides[e].es
            for guide in guide_entities
                spatial[guide] = spatial[e]
            end
            ax = direction(rot)
            rotation_comp[guide_entities[1]] = rot #this is the in plane one, so should rotate along the direction of the object
            # rotation_comp[guide_entities[2]] = Rotation(rot.q * rotation(Y_AXIS, X_AXIS))#this is the out of plane one, so should rotate along the perpendicular direction of the object
            rotation_comp[guide_entities[2]] = Rotation(rot.q * rotation(X_AXIS, Z_AXIS))
            rotation_comp[guide_entities[3]] = Rotation(rot.q * rotation(Y_AXIS, Z_AXIS))
            #this is the out of plane one, so should rotate along the perpendicular direction of the object
            text[guide_entities[1]] = Text(;
                                           str = "Orientation: (x=$(ax[1]), y=$(ax[2]), z=$(ax[3]))",
                                           font_size = rotatable[e].font_size,
                                           offset = Vec3f0(0, 0, -2.0f0))
        end

        #handle moving
        for parent_entity in @entities_in(move_guides)
            for e in move_guides[parent_entity].es[2:end]
                if selectable[e].selected
                    if pressed(mouse)
                        mouse_drag = (mouse.dx * camera.right - mouse.dy * camera.up) *
                                     camera.translation_speed / 2
                        guide_direction = direction(rotation_comp[e])
                        move = guide_direction * dot(guide_direction, mouse_drag)

                        t_spat = spatial[parent_entity]
                        spatial[parent_entity] = Spatial(t_spat;
                                                         position = t_spat.position + move)
                        moved = true
                    end
                end
            end
        end

        #handle rotating
        for parent_entity in @entities_in(rotate_guides)
            rotate_entities = rotate_guides[parent_entity].es
            for e in rotate_entities
                if selectable[e].selected
                    if pressed(mouse)
                        rotation_axis = direction(rotation_comp[e])
                        ray = Ray(mouse, singleton(m, Canvas), camera, eye)
                        plane = Plane(spatial[e].position, rotation_axis)
                        closest_point = intersect(ray, plane)
                        tangent = cross(rotation_axis, closest_point - spatial[e].position)
                        mouse_drag = (mouse.dx * camera.right - mouse.dy * camera.up) *
                                     rotatable[parent_entity].rotation_speed

                        guide_rotation = Quaternions.qrotation(rotation_axis,
                                                               dot(tangent, mouse_drag))

                        t_rot = rotation_comp[parent_entity]
                        rotation_comp[parent_entity] = Rotation(guide_rotation * t_rot.q)
                        rotated = true
                    end
                end
            end
        end
    else
        set_visibility(false)
    end
    rotated && push!(singleton(m, UpdatedComponents), Rotation)
    return moved && push!(singleton(m, UpdatedComponents), Spatial)
end


# Author: AJ Tyler
# Date: 24/01/22
module ClusterStructureAnalysis
include("io.jl") # This file contains the IO module.
include("comparison.jl") # This file contains the Comparison module
include("visualisation.jl") # This file contains the Visualisation module.
include("classify.jl") # This file contains the Classification module.
using .Comparison # This module the contains functions which compare configurations.
using .IO # This module contains functions that read in and write out files.
using .Visualisation # This module contains functions which create a .vesta file with helpful colourisations.
using .Classify # This module contains functions which classify configurations based on their symmetries.
using Graphs # This module contains graph theory functions which allow for a graphical representation of configurations.
export classify # This function uses the atomic CNA profile of a configuration to identify symmetries.
export compare # This function computed the CNA profiles of configurations oevr a range of rCut values.

"""
This function computed the CNA profiles of configurations over a range of rCut values. These configurations are then compared between
eachother using a similarity score. If these similarity score is high enough over enough rCut values, then the configurations are
considered to be the same. For each input .xyz file, a .txt file is generated inside a rCut_XXX directory for each rCut value (XXX)
containing the total CNA profiles of each configuration. A comparison_NAME.txt file is also generated inside the comparison directory
for each input file, which lists which configurations were most similar to each configuration and for which rCuts. Finally
a unique_NAME.txt file is also created inside the comparison directory which lists each set of distinguishable configurations and their
associated energy statistics.

Inputs:
configDir: (String) The path to (or name of, if in the local directory) the directory containing the .xyz files.
					The .xyz files must have the following format NAME_N_L.xyz, where NAME is unique for all the files in the
					directory, N is the number of atoms in each configuration and L is the number of configurations in the file.
Keyword Arguments:
rCutRange: (Vector{Float64}) A range of cut-off radii to test, in units of EBL. Default is 10 values between 4/3 and 2.
similarityMeasure: (String) The method of comparing configurations, either "atomic" or "total" CNA. Default is total.
similarityThreshold: (Float64) Similarity threshold above which two configurations are considered identical.
EBL: (Float64) Equilibrium Bond Length. Default is 3.1227 Angstroms (from MP2 data for Neon2).
"""
function compare(configDir::String; rCutRange::Vector{Float64} = round.(LinRange(4/3,2,10);digits = 3), similarityMeasure = "total", similarityThreshold = 0.95,EBL=3.1227)
	rm("$configDir/comparison";force = true,recursive = true) # Delete comparision directory if exists from previous run
	mkdir("$configDir/comparison") # Make comparision directory
	M = length(rCutRange) # Number of rCut values to test
	files = readdir(configDir) # Create list of all files/directories in the configuration directory
	for file in files # For each file
		isXYZ,fileName,fp,L,N,B = processFileName(configDir,file) # Extract information from filename
		if isXYZ # If file is a .xyz file
			# Compute CNA profiles of all configurations in file
			energies,totalProfiles, atomicProfiles,_,_,_ = processFile(fp,L,N,B,true,EBL,rCutRange;M)
			# Find most similar configurations for each configuration based on CNA profiles over all rCut values
			similarConfigs, maxSims = compareConfigs(rCutRange,energies,totalProfiles, atomicProfiles, configDir,fileName, similarityMeasure,M,L,N)
			if L == 1 # If only have one configuration in the file
				continue # Don't need to do any comparisons
			end
			sortingArray = sortperm(maxSims[1,:],rev=true) # Sort configurations by maximum similarity values in decreasing order
			# Sort arrays
			maxSims, sortedEnergies, similarConfigs = maxSims[sortingArray], energies[sortingArray], similarConfigs[sortingArray]
			# Write out configuration comparison results
			writeComparison(configDir,fileName, sortingArray, sortedEnergies, maxSims, similarConfigs,L)
			# Group configurations into distinguishable sets
			uniqueConfigs,uniqueEnergies,uniqueSimilarities = groupConfigs(energies,sortingArray,maxSims, similarConfigs, similarityThreshold,M)
			numUnique = length(uniqueSimilarities) # Compute how many distinguishable sets are in the file
			uniqueStats = configStats(uniqueEnergies,numUnique) # Compute statistics on energies of each distinguishable set
			writeUnique(configDir,fileName,numUnique,uniqueConfigs,uniqueSimilarities, uniqueStats) # Write out distinguishable sets file
		end
	end
end

"""
This function uses the atomic CNA profile of a configuration to identify symmetries. These symmetries include icosahedral (ICO),
body centred cubic (BCC), face centered cubic (FCC), hexagonal close packed (HCP) and OTHER. Futhermore, if a core symmetry can be
identified, then cap and capping atoms are also identified. For each input .xyz file, a NAME_classification.txt file is generated inside
the classification directory, with which atoms have which symmertry and if they are part of the cluster's core, a cap or a capping atom.
A NAME_i.vesta file is also generated in the visulisation directory, where i is the configuration number in the .xyz file, which uses the
classification information to which configurations were most similar to each configuration and for which rCuts. Finally a unique_NAME.txt
file is created which colour the atoms so aid visualisation of the cluster's structure.

Inputs:
configDir: (String) The path to (or name of, if in the local directory) the directory containing the .xyz files.
					The .xyz files must have the following format NAME_N_L.xyz, where NAME is unique for all the files in the
					directory, N is the number of atoms in each configuration and L is the number of configurations in the file.
Keyword Arguments:
rCut: (Float64) The cut-off radii used in the CNA analysis in units of EBL. Default is 4/3.
EBL: (Float64) Equilibrium Bond Length. Default is 3.1227 Angstroms (from MP2 data for Neon2).
"""
function classify(configDir::String; rCut = 4/3,EBL=3.1227)
	rm("$configDir/classification";force = true,recursive = true) # Delete classification directory if exists from previous run
	mkdir("$configDir/classification") # Create the classification directory
	rm("$configDir/visualisation";force = true,recursive = true) # Delete visualisation directory if exists from previous run
	mkdir("$configDir/visualisation") # Create the visualisation directory
	files = readdir(configDir) # Create list of all files/directories in the configuration directory
	for file in files # For each file
		isXYZ,fileName,fp,L,N,B = processFileName(configDir,file) # Extract information from filename
		if isXYZ # If file is a .xyz file
			# Compute CNA profiles and atom shell numbers of all configurations in file
			_,_, atomicProfiles,configurations, shells, bondGraphs = processFile(fp,L,N,B,false,EBL,[rCut])
			# Identify symmetries of the clusters using CNA profiles
			classifications, capSymmetries = findSymmetries(shells,atomicProfiles,bondGraphs,L)
			writeClassification(configDir,fileName,classifications,capSymmetries,L) # Output found symmetries
			vestaFile(configDir,fileName,configurations,classifications,capSymmetries,rCut,EBL,L,N) # Create colourised .vesta file
		end
	end
end

end


export FisherBRCLearner

mutable struct EntropyBC{A<:NeuralNetworkApproximator}
    policy::A
    α::Float32
    lr_alpha::Float32
    target_entropy::Float32
    # Logging
    policy_loss::Float32
end

mutable struct FisherBRCLearner{
    BA<:NeuralNetworkApproximator,
    BC1<:NeuralNetworkApproximator,
    BC2<:NeuralNetworkApproximator,
    R<:AbstractRNG,
} <: AbstractLearner
    policy::BA
    behavior_policy::EntropyBC
    qnetwork1::BC1
    qnetwork2::BC2
    target_qnetwork1::BC1
    target_qnetwork2::BC2
    γ::Float32
    τ::Float32
    α::Float32
    f_reg::Float32
    reward_bonus::Float32
    batch_size::Int
    pretrain_step::Int
    update_freq::Int
    update_step::Int
    lr_alpha::Float32
    target_entropy::Float32
    rng::R
    # Logging
    qnetwork_loss::Float32
    policy_loss::Float32
end

"""
    FisherBRCLearner(;kwargs...)

See paper: [Offline reinforcement learning with fisher divergence critic regularization](https://arxiv.org/abs/2103.08050).

# Keyword arguments

- `policy`, used to get action.
- `behavior_policy::EntropyBC`, used to estimate log μ(a|s).
- `qnetwork1`, used to get Q-values.
- `qnetwork2`, used to get Q-values.
- `target_qnetwork1`, used to estimate the target Q-values.
- `target_qnetwork2`, used to estimate the target Q-values.
- `γ::Float32 = 0.99f0`, reward discount rate.
- `τ::Float32 = 0.005f0`, the speed at which the target network is updated.
- `α::Float32 = 0.0f0`, entropy term.
- `f_reg::Float32 = 1.0f0`, the weight of gradient penalty regularizer.
- `reward_bonus::Float32 = 5.0f0`, add extra value to the reward.
- `batch_size::Int = 32`
- `pretrain_step::Int = 1000`, the number of pre-training rounds.
- `update_freq::Int = 50`, the frequency of updating the `approximator`.
- `lr_alpha::Float32 = 0.003f0`, learning rate of tuning entropy.
- `action_dims::Int = 0`, the dimensionality of the action.
- `update_step::Int = 0`
- `rng = Random.GLOBAL_RNG`

`policy` is expected to output a tuple `(μ, logσ)` of mean and
log standard deviations for the desired action distributions, this
can be implemented using a `GaussianNetwork` in a `NeuralNetworkApproximator`.
"""
function FisherBRCLearner(;
    policy,
    behavior_policy,
    qnetwork1,
    qnetwork2,
    target_qnetwork1,
    target_qnetwork2,
    γ = 0.99f0,
    τ = 0.005f0,
    α = 0.0f0,
    f_reg = 1.0f0,
    reward_bonus = 5.0f0,
    batch_size = 32,
    pretrain_step = 1000,
    update_freq = 50,
    lr_alpha = 0.003f0,
    behavior_lr_alpha = 0.001f0,
    action_dims = 0,
    update_step = 0,
    rng = Random.GLOBAL_RNG,
)
    copyto!(qnetwork1, target_qnetwork1)  # force sync
    copyto!(qnetwork2, target_qnetwork2)  # force sync
    entropy_behavior_policy = EntropyBC(behavior_policy, 0.0f0, behavior_lr_alpha, Float32(-action_dims), 0.0f0)
    FisherBRCLearner(
        policy,
        entropy_behavior_policy,
        qnetwork1,
        qnetwork2,
        target_qnetwork1,
        target_qnetwork2,
        γ,
        τ,
        α,
        f_reg,
        reward_bonus,
        batch_size,
        pretrain_step,
        update_freq,
        update_step,
        lr_alpha,
        Float32(-action_dims),
        rng,
        0f0,
        0f0,
    )
end

function (l::FisherBRCLearner)(env)
    D = device(l.policy)
    s = send_to_device(D, state(env))
    s = Flux.unsqueeze(s, ndims(s) + 1)
    action = dropdims(l.policy(l.rng, s; is_sampling=true), dims=2)
end

function RLBase.update!(l::FisherBRCLearner, batch::NamedTuple{SARTS})
    if l.update_step == 0
        update_behavior_policy!(l.behavior_policy, batch)
    else
        update_learner!(l, batch)
    end
end

function update_behavior_policy!(l::EntropyBC, batch::NamedTuple{SARTS})
    D = device(l.policy)
    s, a, r, t, s′ = (send_to_device(D, batch[x]) for x in SARTS)
    # Update behavior policy with entropy
    gs = gradient(Flux.params(l.policy)) do
        log_π = l.policy.model(s, a)
        _, entropy = l.policy.model(s; is_sampling=true, is_return_log_prob=true)
        loss = mean(l.α .* entropy .- log_π)
        # Update entropy
        ignore() do
            l.policy_loss = loss
            l.α -= l.lr_alpha * mean(-entropy .- l.target_entropy)
        end
        loss
    end
    update!(l.policy, gs)
end

function update_learner!(l::FisherBRCLearner, batch::NamedTuple{SARTS})
    D = device(l.policy)
    s, a, r, t, s′ = (send_to_device(D, batch[x]) for x in SARTS)
    r .+= l.reward_bonus
    γ, τ, α = l.γ, l.τ, l.α

    a′ = l.policy(l.rng, s′; is_sampling=true)
    q′_input = vcat(s′, a′)
    target_q′ = min.(l.target_qnetwork1(q′_input), l.target_qnetwork2(q′_input))

    y = r .+ γ .* (1 .- t) .* vec(target_q′)

    # Train Q Networks
    a = reshape(a, :, l.batch_size)
    q_input = vcat(s, a)
    log_μ = l.behavior_policy.policy.model(s, a) |> vec
    a_policy = l.policy(l.rng, s; is_sampling=true)

    q_grad_1 = gradient(Flux.params(l.qnetwork1)) do
        q1 = l.qnetwork1(q_input) |> vec
        q1_grad_norm = gradient(Flux.params([a_policy])) do
            q1_reg = mean(l.qnetwork1(vcat(s, a_policy)))
        end
        reg = mean(q1_grad_norm[a_policy] .^ 2)
        loss = mse(q1 .+ log_μ, y) + l.f_reg * reg
        ignore() do
            l.qnetwork_loss = loss
        end
        loss
    end
    update!(l.qnetwork1, q_grad_1)

    q_grad_2 = gradient(Flux.params(l.qnetwork2)) do
        q2 = l.qnetwork2(q_input) |> vec
        q2_grad_norm = gradient(Flux.params([a_policy])) do
            q2_reg = mean(l.qnetwork2(vcat(s, a_policy)))
        end
        reg = mean(q2_grad_norm[a_policy] .^ 2)
        loss = mse(q2 .+ log_μ, y) + l.f_reg * reg
        ignore() do
            l.qnetwork_loss += loss
        end
        loss
    end
    update!(l.qnetwork2, q_grad_2)

    # Train Policy
    p_grad = gradient(Flux.params(l.policy)) do
        a, log_π = l.policy(l.rng, s; is_sampling=true, is_return_log_prob=true)
        q_input = vcat(s, a)
        q = min.(l.qnetwork1(q_input), l.qnetwork2(q_input)) .+ log_μ
        policy_loss = mean(α .* log_π .- q)
        ignore() do
            l.policy_loss = policy_loss
            # Tune entropy automatically
            l.α -= l.lr_alpha * mean(-log_π .- l.target_entropy)
        end
        policy_loss
    end
    update!(l.policy, p_grad)

    # polyak averaging
    for (dest, src) in zip(
        Flux.params([l.target_qnetwork1, l.target_qnetwork2]),
        Flux.params([l.qnetwork1, l.qnetwork2]),
    )
        dest .= (1 - τ) .* dest .+ τ .* src
    end
end


# file for functions related to interpolating from solution points to
# faces

"""
  Holds all the temporary arrays needed to interpolate the metrics
"""
mutable struct Interpolation{Tmsh, Tdim}
  dxdxi_el::Array{Tmsh, 3}
  dxdxi_el_bar::Array{Tmsh, 3}

  dxdxi_elface::Array{Tmsh, 3}
  dxdxi_elface_bar::Array{Tmsh, 3}

  dxidx_node::Array{Tmsh, 2}
  dxidx_node_bar::Array{Tmsh, 2}

  dxdxi_node::Array{Tmsh, 2}
  dxdxi_node_bar::Array{Tmsh, 2}

  bndry_arr::Array{Boundary, 1}
end

function Interpolation(mesh::PumiMesh2D{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 4, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 4, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 2, 2)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 2, 2)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 2}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh2D, ::Type{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 4, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 4, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 2, 2)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 2, 2)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 2}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh3D{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 9, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 9, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 3, 3)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 3, 3)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 3}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

function Interpolation(mesh::PumiMesh3D, ::Type{Tmsh}) where Tmsh
  dxdxi_el = zeros(Tmsh, 9, mesh.numNodesPerElement, 1)
  dxdxi_el_bar = zeros(dxdxi_el)

  dxdxi_elface = zeros(Tmsh, 9, mesh.sbpface.numnodes, 1)
  dxdxi_elface_bar = zeros(dxdxi_elface)

  dxidx_node = zeros(Tmsh, 3, 3)
  dxidx_node_bar = zeros(dxidx_node)

  dxdxi_node = zeros(Tmsh, 3, 3)
  dxdxi_node_bar = zeros(dxdxi_node)

  bndry_arr = Array{Boundary}(1)
  return Interpolation{Tmsh, 3}(dxdxi_el, dxdxi_el_bar,
                                dxdxi_elface, dxdxi_elface_bar,
                                dxidx_node, dxidx_node_bar,
                                dxdxi_node, dxdxi_node_bar,
                                bndry_arr)
end

"""
  Interpolates dxidx and jac to the face nodes, faces shared in parallel and
  boundary faces, as well as gets the coordinates of the nodes for
  the shared faces and boundaryfaces.  All the functions use smart allocators,
  so the fields of the mesh object will be allocated if needed, but not if
  they were previously allocated.

  This is the main entry point for everything in this file.
"""
function interpolateCoordinatesAndMetrics(mesh::PumiMeshDG)

  # interpolate metric terms
  interpolateMapping(mesh)

  #  allocate coordinate arrays if needed
  allocateFaceCoordinates(mesh)

  # get the coordinates
  getBndryCoordinates(mesh, mesh.bndryfaces, mesh.coords_bndry)
  getInterfaceCoordinates(mesh, mesh.interfaces, mesh.coords_interface)
  for i=1:mesh.npeers
    getBndryCoordinates(mesh, mesh.bndries_local[i],
                        mesh.coords_sharedface[i])
  end

  return nothing
end

"""
  Allocates the dxidx and jac arrays for the face nodes/ shared face
  data.  Non-curvilinear meshes only.  Specifically:
    mesh.dxidx_face, mesh.jac_face
    mesh.dxidx_bndry, mesh.jac_bndry
    mesh.dxidx_sharedface, mesh.jac_shareface (and their internal arrays)

    Also the corresponding bar variables
      mesh.dxidx_face_bar, mesh.jac_face_bar
      mesh.dxidx_bndry_bar, mesh.jac_bndry_bar
      mesh.dxidx_sharedface_bar, mesh.jac_sharedface_bar
"""
function allocateInterpolatedMetrics(mesh::PumiMeshDG{Tmsh}) where Tmsh

  dim = mesh.dim
  sbpface = mesh.sbpface
   if !isFieldDefined(mesh, :dxidx_face, :jac_face, :dxidx_sharedface,
                           :jac_sharedface, :dxidx_bndry, :jac_bndry)
    # interior faces
    mesh.dxidx_face = zeros(Tmsh, dim, dim, sbpface.numnodes, mesh.numInterfaces)
    mesh.jac_face = zeros(Tmsh, sbpface.numnodes, mesh.numInterfaces)

    mesh.dxidx_face_bar = zeros(mesh.dxidx_face)
    mesh.jac_face_bar = zeros(mesh.jac_face)

    # boundary faces
    mesh.dxidx_bndry = zeros(Tmsh, dim, dim, sbpface.numnodes,
                                   mesh.numBoundaryFaces)
    mesh.jac_bndry = zeros(Tmsh, sbpface.numnodes, mesh.numBoundaryFaces)
    mesh.dxidx_bndry_bar = zeros(mesh.dxidx_bndry)
    mesh.jac_bndry_bar = zeros(mesh.jac_bndry)

    # parallel shared faces
    mesh.dxidx_sharedface = Array{Array{Tmsh, 4}}(mesh.npeers)
    mesh.jac_sharedface = Array{Array{Tmsh, 2}}(mesh.npeers)
    mesh.dxidx_sharedface_bar = Array{Array{Tmsh, 4}}(mesh.npeers)
    mesh.jac_sharedface_bar = Array{Array{Tmsh, 2}}(mesh.npeers)
    for i=1:mesh.npeers
      mesh.dxidx_sharedface[i] = zeros(Tmsh, dim, dim, sbpface.numnodes,
                                       mesh.peer_face_counts[i])

      mesh.jac_sharedface[i] = Array{Tmsh}(sbpface.numnodes, mesh.peer_face_counts[i])
      mesh.dxidx_sharedface_bar[i] = zeros(mesh.dxidx_sharedface[i])
      mesh.jac_sharedface_bar[i] = zeros(mesh.jac_sharedface[i])

    end
  else
    # zero out metrics?
    fill!(mesh.dxidx_face, 0.0)
    fill!(mesh.jac_face, 0.0)
    fill!(mesh.dxidx_bndry, 0.0)
    fill!(mesh.jac_bndry, 0.0)
    for i=1:mesh.npeers
      fill!(mesh.dxidx_sharedface[i], 0.0)
      fill!(mesh.jac_sharedface[i], 0.0)
    end
  end

  return nothing
end

"""
  Allocates coordinate arays for the boundary and shared face nodes.
  Specifically:
    mesh.coords_bndry
    mesh.coords_sharedface (and its internal arrays)
"""
function allocateFaceCoordinates(mesh::PumiMeshDG{Tmsh}) where Tmsh

  sbpface = mesh.sbpface

  if !isFieldDefined(mesh, :coords_bndry, :coords_sharedface)
    mesh.coords_bndry = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                  mesh.numBoundaryFaces)
    mesh.coords_bndry_bar = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                  mesh.numBoundaryFaces)
    mesh.coords_interface = zeros(Tmsh, mesh.dim, sbpface.numnodes, mesh.numInterfaces)
    mesh.coords_sharedface = Array{Array{Tmsh, 3}}(mesh.npeers)
    for i=1:mesh.npeers
      mesh.coords_sharedface[i] = zeros(Tmsh, mesh.dim, sbpface.numnodes,
                                             mesh.peer_face_counts[i])
    end
  else
    fill!(mesh.coords_bndry, 0.0)
    fill!(mesh.coords_interface, 0.0)
    for i=1:mesh.npeers
      fill!(mesh.coords_sharedface[i], 0.0)
    end

  end

  return nothing
end

"""
  Interpolates dxidx, jac to the face nodes of local interfaces, shared
  faces, and boundary faces.  This function uses a smart allocator to
  allocate the relevent fields of the mesh object if they have been been
  allocated previously and to not allocate them if they have been previously.

  By convention, the metrics of elementL of an interface are interpolated
  (recall elementL is always the local elements for shared faces).

  See allocateInterpolatedMetrics for the fields that are populated by this
  function.
"""
function interpolateMapping(mesh::PumiMeshDG{Tmsh}) where Tmsh

  allocateInterpolatedMetrics(mesh)

  sbpface = mesh.sbpface
  dim = mesh.dim

  # pull out the needed arrays
  dxidx_face = mesh.dxidx_face
  dxidx_sharedface = mesh.dxidx_sharedface
  jac_face = mesh.jac_face

  dxidx_bndry = mesh.dxidx_bndry
  jac_bndry = mesh.jac_bndry
  jac_sharedface = mesh.jac_sharedface

  # temporary storage
  interp_data = Interpolation(mesh)

  for i=1:mesh.numInterfaces
    dxidx_i = sview(dxidx_face, :, :, :, i)
    jac_i = sview(jac_face, :, i)

    interface_i = mesh.interfaces[i]
    el = interface_i.elementL
    face = interface_i.faceL
    interp_data.bndry_arr[1] =  Boundary(1, face)

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)

    interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)

  end
  # now do shared edges
  for peer = 1:mesh.npeers
    dxidx_p = dxidx_sharedface[peer]
    jac_p = jac_sharedface[peer]
    interfaces_p = mesh.shared_interfaces[peer]
    for i=1:mesh.peer_face_counts[peer]
      dxidx_i = sview(dxidx_p, :, :, :, i)
      jac_i = sview(jac_p, :, i)

      interface_i = interfaces_p[i]
      el = interface_i.elementL
      face = interface_i.faceL

      interp_data.bndry_arr[1] = Boundary(1, face)

      dxidx_in = sview(mesh.dxidx, :, :, :, el)
      jac_in = sview(mesh.jac, :, el)

      interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)
    end
  end

  # now do boundary
  for i=1:mesh.numBoundaryFaces
    bndry = mesh.bndryfaces[i]

    dxidx_i = sview(dxidx_bndry, :, :, :, i)
    jac_i = sview(jac_bndry, :, i)

    el = bndry.element
    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    interp_data.bndry_arr[1] = bndry
    interpolateFace(interp_data, mesh.sbpface, dxidx_in, jac_in, dxidx_i, jac_i)
  end

  return nothing

end  # end function

"""
  Reverse mode of interpolateMapping.  This function propigates the adjoint
  parts of the metrics at the interfaces, boundary faces, and shared faces
  back to the volume nodes.

  This function back propigates both dxidx and jac, although for 2D it turns
  out jac has no effect on the computation.

"""
function interpolateMapping_rev(mesh::PumiMeshDG{Tmsh}) where Tmsh

  sbpface = mesh.sbpface
  dim = mesh.dim

  # pull out the needed arrays
  dxidx_face = mesh.dxidx_face
  dxidx_face_bar = mesh.dxidx_face_bar

  dxidx_sharedface = mesh.dxidx_sharedface
  dxidx_sharedface_bar = mesh.dxidx_sharedface_bar

  dxidx_bndry = mesh.dxidx_bndry
  dxidx_bndry_bar = mesh.dxidx_bndry_bar

  jac_face = mesh.jac_face
  jac_face_bar = mesh.jac_face_bar

  jac_bndry = mesh.jac_bndry
  jac_bndry_bar = mesh.jac_bndry_bar

  jac_sharedface = mesh.jac_sharedface
  jac_sharedface_bar = mesh.jac_sharedface_bar

  # temporary storage
  interp_data = Interpolation(mesh)

  for i=1:mesh.numInterfaces
    dxidx_i = sview(dxidx_face, :, :, :, i)
    dxidx_bar_i = sview(dxidx_face_bar, :, :, :, i)
    jac_i = sview(jac_face, :, i)
    jac_bar_i = sview(jac_face_bar, :, i)

    interface_i = mesh.interfaces[i]
    el = interface_i.elementL
    face = interface_i.faceL
    interp_data.bndry_arr[1] =  Boundary(1, face)

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    jac_bar_in = sview(mesh.jac_bar, :, el)

    interpolateFace_rev(interp_data, mesh.sbpface,
                         dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                         dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)

  end
  # now do shared edges
  for peer = 1:mesh.npeers
    dxidx_p = dxidx_sharedface[peer]
    dxidx_bar_p = dxidx_sharedface_bar[peer]
    jac_p = jac_sharedface[peer]
    jac_bar_p = jac_sharedface_bar[perr]

    interfaces_p = mesh.shared_interfaces[peer]
    for i=1:mesh.peer_face_counts[peer]
      dxidx_i = sview(dxidx_p, :, :, :, i)
      dxidx_bar_i = sview(dxidx_bar_p, :, :, :, i)
      jac_i = sview(jac_p, :, i)
      jac_bar_i = sview(jac_bar_p, :, i)

      interface_i = interfaces_p[i]
      el = interface_i.elementL
      face = interface_i.faceL

      interp_data.bndry_arr[1] = Boundary(1, face)

      dxidx_in = sview(mesh.dxidx, :, :, :, el)
      dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
      jac_in = sview(mesh.jac, :, el)
      jac_bar_in = sview(mesh.jac_bar, :, el)

      interpolateFace_rev(interp_data, mesh.sbpface,
                      dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                      dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)
    end
  end

  # now do boundary
  for i=1:mesh.numBoundaryFaces
    bndry = mesh.bndryfaces[i]

    dxidx_i = sview(dxidx_bndry, :, :, :, i)
    dxidx_bar_i = sview(dxidx_bndry_bar, :, :, :, i)
    jac_i = sview(jac_bndry, :, i)
    jac_bar_i = sview(jac_bndry_bar, :, i)

    el = bndry.element

    dxidx_in = sview(mesh.dxidx, :, :, :, el)
    dxidx_bar_in = sview(mesh.dxidx_bar, :, :, :, el)
    jac_in = sview(mesh.jac, :, el)
    jac_bar_in = sview(mesh.jac_bar, :, el)

    interp_data.bndry_arr[1] = bndry
    interpolateFace_rev(interp_data, mesh.sbpface,
                    dxidx_in, dxidx_bar_in, jac_in, jac_bar_in,
                    dxidx_i, dxidx_bar_i, jac_i, jac_bar_i)
  end

  return nothing
end

# 2D version
function interpolateFace(interp_data::Interpolation{Tmsh, 2}, sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh}) where Tmsh

  # unpack argumetns
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  dim = 2
  numNodesPerElement = size(dxidx_hat_in, 3)
  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate2(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # now store dxidx, |J| at the boundary nodesa
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    scale!(dxidx_node, 1./det_dxdxi)

    detJ = det2(dxidx_node)
    fac = 1./detJ
    for k=1:dim
      for p=1:dim
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  return nothing

end

"""
  Reverse mode of interpolateFace.

  Propigates the adjoint parts of the inputs to the adjoint parts of the
  outputs.  This function also verifies the primal parts have not been modified
  (ie. it computes values dxidx_hat_out and verifies they are the same as
  the ones already present in the array).

  Inputs (see interpolateFace):
    interp_data
    sbpface
    dxidx_hat_in
    jac_in
    dxidx_hat_out
    dxidx_hat_out_bar: the adjoint part of dxidx_hat_out
    jac_out
    jac_out_bar: the adjoint part of jac_out

  Outputs:
    dxidx_hat_in_bar: the adjoint part of dxidx_hat_in
    jac_in_bar: the adjoint part of jac_in

  Aliasing restrictions: please don't
"""
function interpolateFace_rev(interp_data::Interpolation{Tmsh, 2},
                         sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         dxidx_hat_in_bar::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         jac_in_bar::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         dxidx_hat_out_bar::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh},
                         jac_out_bar::AbstractVector{Tmsh}) where Tmsh

  #----------------------------------------------------------------------------
  # forward sweep
  # unpack arguments
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  fill!(dxdxi_el, 0.0)
  fill!(dxdxi_elface, 0.0)

  dim = 2
  numNodesPerElement = size(dxidx_hat_in, 3)

  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
#    detJ = jac_in[j]

    adjugate2(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

# everything in here is local to the loop, no need to compute it here
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det2(dxidx_node)
    fac = 1./detJ

    for k=1:dim
      for p=1:dim
        # verify this forward sweep is consistent with the original function
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
#        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  #----------------------------------------------------------------------------
  # reverse sweep

  # unpack arguments
  dxdxi_el_bar = interp_data.dxdxi_el_bar
  dxdxi_elface_bar = interp_data.dxdxi_elface_bar
  dxdxi_node_bar = interp_data.dxdxi_node_bar
  dxidx_node_bar = interp_data.dxidx_node_bar

  # zero them out
  fill!(dxdxi_el_bar, 0.0)
  fill!(dxdxi_elface_bar, 0.0)

  # each interation of this loop is independent - combine forward and reverse
  for j=sbpface.numnodes:-1:1

    #--------------------------------------------------------------------------
    # forward sweep
    fill!(dxdxi_node_bar, 0.0)
    fill!(dxidx_node_bar, 0.0)

    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det2(dxdxi_node)
    adjugate2(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det2(dxidx_node)
    fac = 1./detJ
#=
    for k=1:dim
      for p=1:dim
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
=#
    #--------------------------------------------------------------------------
    # reverse sweep

    det_dxdxi_bar = zero(det_dxdxi)
    fac_bar = zero(eltype(dxidx_hat_out))

    detJ_bar = jac_out_bar[j]*1

    for k=dim:-1:1
      for p=dim:-1:1
        dxidx_node_bar[p, k] += dxidx_hat_out_bar[p, k, j]*fac

        fac_bar += dxidx_hat_out_bar[p, k, j]*dxidx_node[p, k]
      end
    end

    detJ_bar -= fac_bar*1/(detJ*detJ)
    det2_rev(dxidx_node, dxidx_node_bar, detJ_bar)

    # uncomment this to introdue the bug
    for i=1:length(dxidx_node)
       dxidx_node[i] *= det_dxdxi  # need to reverse the primal variables too
       det_dxdxi_bar += dxidx_node_bar[i]*dxidx_node[i]*(-1/(det_dxdxi*det_dxdxi))
       dxidx_node_bar[i] = dxidx_node_bar[i]/det_dxdxi
    end

    adjugate2_rev(dxdxi_node, dxdxi_node_bar, dxidx_node, dxidx_node_bar)
    det2_rev(dxdxi_node, dxdxi_node_bar, det_dxdxi_bar)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxdxi_elface_bar[pos, j, 1] += dxdxi_node_bar[k, p]
      end
    end

  end  # end loop j

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate_rev!(sbpface, interp_data.bndry_arr, dxdxi_el_bar, dxdxi_elface_bar)

  # propagate back to the inputs
  for j=numNodesPerElement:-1:1
    fill!(dxidx_node_bar, 0.0)
    fill!(dxdxi_node_bar, 0.0)

    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    dxidx_hat_bar = sview(dxidx_hat_in_bar, :, :, j)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxidx_node_bar[k, p] += dxdxi_el_bar[pos, j, 1]
      end
    end

    adjugate2_rev(dxidx_hat, dxidx_hat_bar, dxidx_node, dxidx_node_bar)

    # it turns out jac_in isn't used for anything
#    jac_in_bar[j] = detJ_bar
  end  # end loop j

  return nothing

end

# 3d version
"""
  Interpolates the metric terms from the volume nodes of an element to
  the nodes of a particular face.

  Inputs:
    interp_data:  Interpolation object containing the temporary arrays.
                  Which face is interpolated to depends
                  interp_data.bndry_arr.face.  The element field does
                  not matter.  This array is overwritten.

    sbpface: an SBPFace
    dxidx_hat_in: 3D array holding dxidx/|J| at the volume nodes,
                   2 x 2 x numNodesPerElement in 2D or
                   3 x 3 x numNodesPerElemente in 3D.
    jac_hat_in: determanent of the dxidx at the volume nodes, vector of
                length numNodesPerElement

  Outputs:
    dxidx_i: array to be populated with dxidx/|J| at the face nodes,
             2 x 2 x numNodesPerFace in 2 or
             3 x 3 x numNodesPerFace in 3D

    jac_i: array to be populated with |J|, vector of length numNodesPerFAce

  Aliasing restrictions: do not alias
"""
function interpolateFace(interp_data::Interpolation{Tmsh, 3}, sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh}) where Tmsh
# herein, dxidx_hat is dxidx/|J|, and dxidx is the true dxidx

  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  dim = 3
  numNodesPerElement = size(dxidx_hat_in, 3)
  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate3(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        dxidx_node[k, p] *= detJ
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
#  face = bndry.face
#  bndry_arr = [Boundary(1, face)]
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)

  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # now store dxidx, |J| at the boundary nodesa
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    scale!(dxidx_node, 1./det_dxdxi)

    detJ = det3(dxidx_node)
    fac = 1./detJ
    for k=1:dim
      for p=1:dim
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
  end

  return nothing

end

function interpolateFace_rev(interp_data::Interpolation{Tmsh, 3},
                         sbpface,
                         dxidx_hat_in::AbstractArray{Tmsh, 3},
                         dxidx_hat_in_bar::AbstractArray{Tmsh, 3},
                         jac_in::AbstractVector{Tmsh},
                         jac_in_bar::AbstractVector{Tmsh},
                         dxidx_hat_out::AbstractArray{Tmsh, 3},
                         dxidx_hat_out_bar::AbstractArray{Tmsh, 3},
                         jac_out::AbstractVector{Tmsh},
                         jac_out_bar::AbstractVector{Tmsh}) where Tmsh

  #----------------------------------------------------------------------------
  # forward sweep
  # unpack arguments
  dxdxi_el = interp_data.dxdxi_el
  dxdxi_elface = interp_data.dxdxi_elface
  dxdxi_node = interp_data.dxdxi_node
  dxidx_node = interp_data.dxidx_node

  fill!(dxdxi_el, 0.0)
  fill!(dxdxi_elface, 0.0)

  dim = 3
  numNodesPerElement = size(dxidx_hat_in, 3)

  # get the data
  for j=1:numNodesPerElement
    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    detJ = jac_in[j]

    adjugate3(dxidx_hat, dxidx_node)
    for k=1:dim
      for p=1:dim
        dxidx_node[k, p] *= detJ
        pos = p + dim*(k-1)
        dxdxi_el[pos,j,1] = dxidx_node[k, p]
      end
    end
  end

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate!(sbpface, interp_data.bndry_arr, dxdxi_el, dxdxi_elface)

  # everything in here is local to the loop, no need to compute it here
  for j=1:sbpface.numnodes
    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det3(dxidx_node)
    fac = 1./detJ

    for k=1:dim
      for p=1:dim
        # verify this forward sweep is consistent with the original function
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
#        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
#    jac_out[j] = detJ
  end

  #----------------------------------------------------------------------------
  # reverse sweep

  # unpack arguments
  dxdxi_el_bar = interp_data.dxdxi_el_bar
  dxdxi_elface_bar = interp_data.dxdxi_elface_bar
  dxdxi_node_bar = interp_data.dxdxi_node_bar
  dxidx_node_bar = interp_data.dxidx_node_bar

  # zero them out
  fill!(dxdxi_el_bar, 0.0)
  fill!(dxdxi_elface_bar, 0.0)

  # each interation of this loop is independent - combine forward and reverse
  for j=sbpface.numnodes:-1:1

    #--------------------------------------------------------------------------
    # forward sweep
    fill!(dxdxi_node_bar, 0.0)
    fill!(dxidx_node_bar, 0.0)

    for k=1:dim
      for p=1:dim
        pos = p + dim*(k-1)
        dxdxi_node[k,p] = dxdxi_elface[pos, j, 1]
      end
    end

    # inv(A) = adj(A)/|A|
    det_dxdxi = det3(dxdxi_node)
    adjugate3(dxdxi_node, dxidx_node)
    for i=1:length(dxidx_node)
       dxidx_node[i] = dxidx_node[i] / det_dxdxi
    end

    detJ = det3(dxidx_node)
    fac = 1./detJ
#=
    for k=1:dim
      for p=1:dim
        @assert (dxidx_hat_out[p, k, j] - dxidx_node[p, k]*fac) < 1e-13
        dxidx_hat_out[p, k, j] = dxidx_node[p, k]*fac
      end
    end
    jac_out[j] = detJ
=#
    #--------------------------------------------------------------------------
    # reverse sweep
    det_dxdxi_bar = zero(det_dxdxi)
    fac_bar = zero(eltype(dxidx_hat_out))

    detJ_bar = jac_out_bar[j]*1

    for k=dim:-1:1
      for p=dim:-1:1
        dxidx_node_bar[p, k] += dxidx_hat_out_bar[p, k, j]*fac

        fac_bar += dxidx_hat_out_bar[p, k, j]*dxidx_node[p, k]
      end
    end

    detJ_bar -= fac_bar*1/(detJ*detJ)
    det3_rev(dxidx_node, dxidx_node_bar, detJ_bar)

    # uncomment this to introdue the bug
    for i=1:length(dxidx_node)
       dxidx_node[i] *= det_dxdxi  # need to reverse the primal variables too
       det_dxdxi_bar += dxidx_node_bar[i]*dxidx_node[i]*(-1/(det_dxdxi*det_dxdxi))
       dxidx_node_bar[i] = dxidx_node_bar[i]/det_dxdxi
    end

    adjugate3_rev(dxdxi_node, dxdxi_node_bar, dxidx_node, dxidx_node_bar)
    det3_rev(dxdxi_node, dxdxi_node_bar, det_dxdxi_bar)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxdxi_elface_bar[pos, j, 1] += dxdxi_node_bar[k, p]
      end
    end

  end  # end loop j

  # interpolate to the face
  face = interp_data.bndry_arr[1].face
  interp_data.bndry_arr[1] = Boundary(1, face)
  boundaryinterpolate_rev!(sbpface, interp_data.bndry_arr, dxdxi_el_bar, dxdxi_elface_bar)

  # propagate back to the inputs
  for j=numNodesPerElement:-1:1
    fill!(dxidx_node_bar, 0.0)
    fill!(dxdxi_node_bar, 0.0)
    detJ = jac_in[j]
    detJ_bar = zero(detJ)

    dxidx_hat = sview(dxidx_hat_in, :, :, j)
    dxidx_hat_bar = sview(dxidx_hat_in_bar, :, :, j)

    for k=dim:-1:1
      for p=dim:-1:1
        pos = p + dim*(k-1)
        dxidx_node_bar[k, p] += dxdxi_el_bar[pos, j, 1]

        dxidx_node[k, p] /= detJ
        detJ_bar += dxidx_node_bar[k, p]*dxidx_node[k, p]
        dxidx_node_bar[k, p] = dxidx_node_bar[k, p]*detJ
      end
    end

    adjugate3_rev(dxidx_hat, dxidx_hat_bar, dxidx_node, dxidx_node_bar)

    jac_in_bar[j] = detJ_bar
  end  # end loop j

  return nothing

end

#TODO: eorder these operations for column major arrays
function det2(A::AbstractMatrix)
# determinet of 2x2 matrix

  return A[1,1]*A[2,2] - A[1,2]*A[2,1]
end

"""
  Computes the reverse mode of det2, returning only the derivative and not
  the value of the determinant.

  Inputs:
    A: The matrix
    y_bar: the adjoint part of the determinant (the primal part is not required
           for the reverse calculation)

  Inputs/Outputs:
    A_bar: the adjoint part of A, which is incremented, not overwritten

"""
function det2_rev(A::AbstractMatrix, A_bar::AbstractMatrix, y_bar::Number)

  A_bar[1,1] += y_bar*A[2,2]
  A_bar[1,2] += -y_bar*A[2,1]
  A_bar[2,1] += -y_bar*A[1,2]
  A_bar[2,2] += y_bar*A[1,1]

  return nothing
end

function det3(A::AbstractMatrix)
# determinent of 3x3 matrix
# cofactor expansion about first column
  val1 = A[1,1]*(A[2,2]*A[3,3] - A[2,3]*A[3,2])
  val2 = -A[2,1]*(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  val3 = A[3,1]*(A[1,2]*A[2,3] - A[1,3]*A[2,2])
  return val1 + val2 + val3
end

function det3_rev(A::AbstractMatrix, A_bar::AbstractMatrix, y_bar::Number)

  # initialize intermediate adjoints to zero
  val1_bar = y_bar
  val2_bar = y_bar
  val3_bar = y_bar

  # val3 expression
  # for true alias compatability, need to apply reverse mode to each line
  # right to left, rather than left to right
  A_bar[1,1] += val3_bar*(A[2,2]*A[3,3] - A[2,3]*A[3,2])
  A_bar[2,2] += val3_bar*(A[1,1]*A[3,3])
  A_bar[3,3] += val3_bar*(A[1,1]*A[2,2])

  A_bar[2,3] += -val3_bar*A[1,1]*A[3,2]
  A_bar[3,2] += -val3_bar*A[1,1]*A[2,3]

  # val2 expression
  A_bar[2,1] += -val2_bar*(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  A_bar[1,2] += -val2_bar*A[2,1]*A[3,3]
  A_bar[3,3] += -val2_bar*A[2,1]*A[1,2]

  A_bar[1,3] += val2_bar*A[2,1]*A[3,2]
  A_bar[3,2] += val2_bar*A[2,1]*A[1,3]

  # val1 expression
  A_bar[3,1] += val1_bar*(A[1,2]*A[2,3] - A[1,3]*A[2,2])
  A_bar[1,2] += val1_bar*A[3,1]*A[2,3]
  A_bar[2,3] += val1_bar*A[3,1]*A[1,2]

  A_bar[1,3] += -val1_bar*A[3,1]*A[2,2]
  A_bar[2,2] += -val1_bar*A[3,1]*A[1,3]

  return nothing
end

function adjugate2(A::AbstractMatrix, B::AbstractMatrix)
  B[1,1] = A[2,2]
  B[1,2] = -A[1,2]
  B[2,1] = -A[2,1]
  B[2,2] = A[1,1]
end

"""
  Computes the reverse mode of adjugate2, without modifying A or B

  Inputs:
    A: The original matrix
    B: The adjuage matrix (presumably computed by adjugate2)
    B_bar: the adjoint part of B

  Inputs/Outputs:
    A_bar: The adjoint part of A, which is incremented, not overwritten

  Aliasing restrictions: I'm not sure what happens if things alias, so don't
                         do it.
"""
function adjugate2_rev(A::AbstractMatrix, A_bar::AbstractMatrix,
                       B::AbstractMatrix, B_bar::AbstractMatrix)

  A_bar[2,2] += B_bar[1,1]
  A_bar[1,2] += -B_bar[1,2]
  A_bar[2,1] += -B_bar[2,1]
  A_bar[1,1] += B_bar[2,2]

end

function adjugate3(A::AbstractMatrix, B::AbstractMatrix)

  B[1,1] = A[2,2]*A[3,3] - A[2,3]*A[3,2]
  B[1,2] = -(A[1,2]*A[3,3] - A[1,3]*A[3,2])
  B[1,3] = A[1,2]*A[2,3] - A[1,3]*A[2,2]

  B[2,1] = -(A[2,1]*A[3,3] - A[2,3]*A[3,1])
  B[2,2] = A[1,1]*A[3,3] - A[1,3]*A[3,1]
  B[2,3] = -(A[1,1]*A[2,3] - A[1,3]*A[2,1])

  B[3,1] = A[2,1]*A[3,2] - A[2,2]*A[3,1]
  B[3,2] = -(A[1,1]*A[3,2] - A[1,2]*A[3,1])
  B[3,3] = A[1,1]*A[2,2] - A[1,2]*A[2,1]
end

"""
  Computes the reverse mode of adjugate2, without modifying A or B

  Inputs:
    A: the original matrix
    B: the adjugate matrix (presumably computed by adjugate3)
    B_bar: the adjoint part of B

  Inputs/Outputs
    A_bar: the adjoint part of A, which is incremented, not overwritten

  Aliasing restrictions: Please don't
"""
function adjugate3_rev(A::AbstractMatrix, A_bar::AbstractMatrix,
                       B::AbstractMatrix, B_bar::AbstractMatrix)

  # B11
  B11_bar = B_bar[1,1]
  A_bar[2,2] += B11_bar*A[3,3]
  A_bar[3,3] += B11_bar*A[2,2]
  A_bar[2,3] += -B11_bar*A[3,2]
  A_bar[3,2] += -B11_bar*A[2,3]

  bar = B_bar[1,2]
  A_bar[1,2] += -bar*A[3,3]
  A_bar[3,3] += -bar*A[1,2]
  A_bar[1,3] += bar*A[3,2]
  A_bar[3,2] += bar*A[1,3]

  bar = B_bar[1,3]
  A_bar[1,2] += bar*A[2,3]
  A_bar[2,3] += bar*A[1,2]
  A_bar[1,3] += -bar*A[2,2]
  A_bar[2,2] += -bar*A[1,3]

  bar = B_bar[2,1]
  A_bar[2,1] += -bar*A[3,3]
  A_bar[3,3] += -bar*A[2,1]
  A_bar[2,3] += bar*A[3,1]
  A_bar[3,1] += bar*A[2,3]

  bar = B_bar[2,2]
  A_bar[1,1] += bar*A[3,3]
  A_bar[3,3] += bar*A[1,1]
  A_bar[1,3] += -bar*A[3,1]
  A_bar[3,1] += -bar*A[1,3]

  bar = B_bar[2,3]
  A_bar[1,1] += -bar*A[2,3]
  A_bar[2,3] += -bar*A[1,1]
  A_bar[1,3] += bar*A[2,1]
  A_bar[2,1] += bar*A[1,3]

  bar = B_bar[3,1]
  A_bar[2,1] += bar*A[3,2]
  A_bar[3,2] += bar*A[2,1]
  A_bar[2,2] += -bar*A[3,1]
  A_bar[3,1] += -bar*A[2,2]

  bar = B_bar[3,2]
  A_bar[1,1] += -bar*A[3,2]
  A_bar[3,2] += -bar*A[1,1]
  A_bar[1,2] += bar*A[3,1]
  A_bar[3,1] += bar*A[1,2]

  bar = B_bar[3,3]
  A_bar[1,1] += bar*A[2,2]
  A_bar[2,2] += bar*A[1,1]
  A_bar[1,2] += -bar*A[2,1]
  A_bar[2,1] += -bar*A[1,2]

  return nothing
end