module TestCSets
using Test

using Catlab, Catlab.Theories, Catlab.Graphs, Catlab.CategoricalAlgebra,
  Catlab.CategoricalAlgebra.FinSets
using Catlab.Graphs.BasicGraphs: TheoryGraph
using Catlab.Present

@present TheoryDDS(FreeSchema) begin
  X::Ob
  Φ::Hom(X,X)
end
const DDS = CSetType(TheoryDDS, index=[:Φ])

# FinSets interop
#################

g = Graph(6)
add_edges!(g, 2:4, 3:5)
f = FinFunction(g, :V)
@test collect(f) == 1:6
@test is_indexed(f)
f = FinFunction(g, :src)
@test codom(f) == FinSet(6)
@test collect(f) == 2:4
@test is_indexed(f)

f = FinDomFunction(g, :E)
@test collect(f) == 1:3
@test is_indexed(f)
f = FinDomFunction(g, :tgt)
@test codom(f) == TypeSet(Int)
@test collect(f) == 3:5
@test is_indexed(f)

g = path_graph(WeightedGraph{Float64}, 3, E=(weight=[0.5, 1.5],))
f = FinDomFunction(g, :weight)
@test codom(f) == TypeSet(Float64)
@test collect(f) == [0.5, 1.5]

# C-set morphisms
#################

# Constructors and accessors.
g, h = path_graph(Graph, 4), cycle_graph(Graph, 2)
α = CSetTransformation((V=[1,2,1,2], E=[1,2,1]), g, h)
@test components(α) == (V=α[:V], E=α[:E])
@test α[:V] isa FinFunction{Int} && α[:E] isa FinFunction{Int}
@test α[:V](3) == 1
@test α[:E](2) == 2

α′ = CSetTransformation(g, h, V=[1,2,1,2], E=[1,2,1])
@test components(α′) == components(α)
α′′ = CSetTransformation(g, h, V=FinFunction([1,2,1,2]), E=FinFunction([1,2,1]))
@test components(α′′) == components(α)

# Naturality.
@test is_natural(α)
β = CSetTransformation((V=[1,2,1,2], E=[1,1,1]), g, h)
@test !is_natural(β)
β = CSetTransformation((V=[2,1], E=[2,1]), h, h)
@test is_natural(β)

# Category of C-sets.
@test dom(α) === g
@test codom(α) === h
@test compose(α,β) == CSetTransformation((V=α[:V]⋅β[:V], E=α[:E]⋅β[:E]), g, h)
@test force(compose(id(g), α)) == α
@test force(compose(α, id(h))) == α

# Limits
#-------

# Terminal object in Graph: the self-loop.
term = Graph(1)
add_edge!(term, 1, 1)
lim = terminal(Graph)
@test ob(lim) == term
@test force(delete(lim, g)) ==
  CSetTransformation((V=fill(1,4), E=fill(1,3)), g, term)

# Products in Graph: unitality.
lim = product(g, term)
@test ob(lim) == g
@test force(proj1(lim)) == force(id(g))
@test force(proj2(lim)) ==
  CSetTransformation((V=fill(1,4), E=fill(1,3)), g, term)

# Product in Graph: two directed intervals (Reyes et al 2004, p. 48).
I = Graph(2)
add_edge!(I, 1, 2)
prod = ob(product(I, I))
@test nv(prod) == 4
@test ne(prod) == 1
@test src(prod) != tgt(prod)

# Product in Graph: deleting edges by multiplying by an isolated vertex.
g = path_graph(Graph, 4)
g0 = ob(product(g, Graph(1)))
@test nv(g0) == nv(g)
@test ne(g0) == 0

# Product in Graph: copying edges by multiplying by the double self-loop.
loop2 = Graph(1)
add_edges!(loop2, [1,1], [1,1])
lim = product(g, loop2)
g2 = ob(lim)
@test nv(g2) == nv(g)
@test ne(g2) == 2*ne(g)
@test src(g2) == repeat(src(g), 2)
@test tgt(g2) == repeat(tgt(g), 2)
α = CSetTransformation((V=[2,3], E=[2]), I, g)
β = CSetTransformation((V=[1,1], E=[2]), I, loop2)
γ = pair(lim, α, β)
@test force(γ⋅proj1(lim)) == α
@test force(γ⋅proj2(lim)) == β

# Equalizer in Graph from (Reyes et al 2004, p. 50).
g, h = cycle_graph(Graph, 2), Graph(2)
add_edges!(h, [1,2,2], [2,1,1])
ϕ = CSetTransformation((V=[1,2], E=[1,2]), g, h)
ψ = CSetTransformation((V=[1,2], E=[1,3]), g, h)
@test is_natural(ϕ) && is_natural(ψ)
eq = equalizer(ϕ, ψ)
@test ob(eq) == I
@test force(incl(eq)[:V]) == FinFunction([1,2])
@test force(incl(eq)[:E]) == FinFunction([1], 2)

# Pullback in Graph from (Reyes et al 2004, p. 53).
# This test exposes an error in the text: using their notation, there should be
# a second edge between the vertices (1,3) and (1,4).
g0, g1, g2 = Graph(2), Graph(3), Graph(2)
add_edges!(g0, [1,1,2], [1,2,2])
add_edges!(g1, [1,2,3], [2,3,3])
add_edges!(g2, [1,2,2], [1,2,2])
ϕ = CSetTransformation((V=[1,2,2], E=[2,3,3]), g1, g0)
ψ = CSetTransformation((V=[1,2], E=[1,3,3]), g2, g0)
@test is_natural(ϕ) && is_natural(ψ)
lim = pullback(ϕ, ψ)
@test nv(ob(lim)) == 3
@test sort!(collect(zip(src(ob(lim)), tgt(ob(lim))))) ==
  [(2,3), (2,3), (3,3), (3,3)]
@test is_natural(proj1(lim)) && is_natural(proj2(lim))

# Same pullback using generic limit interface.
diagram = FreeDiagram([g1, g2, g0], [(ϕ,1,3), (ψ,2,3)])
π1, π2, _ = lim′ = limit(diagram)
@test ob(lim′) == ob(lim)
@test force(π1) == force(proj1(lim))
@test force(π2) == force(proj2(lim))

# Colimits
#---------

# Initial object in graph: the empty graph.
colim = initial(Graph)
@test ob(colim) == Graph()
@test create(colim, g) == CSetTransformation((V=Int[], E=Int[]), Graph(), g)

# Coproducts in Graph: unitality.
g = path_graph(Graph, 4)
colim = coproduct(g, Graph())
@test ob(colim) == g
@test force(coproj1(colim)) == force(id(g))
@test force(coproj2(colim)) ==
  CSetTransformation((V=Int[], E=Int[]), Graph(), g)

# Coproduct in Graph.
h = cycle_graph(Graph, 2)
colim = coproduct(g, h)
coprod = ob(colim)
@test nv(coprod) == 6
@test src(coprod) == [1,2,3,5,6]
@test tgt(coprod) == [2,3,4,6,5]
α = CSetTransformation((V=[1,2,1,2], E=[1,2,1]), g, h)
β = id(h)
γ = copair(colim, α, β)
@test force(coproj1(colim)⋅γ) == α
@test force(coproj2(colim)⋅γ) == force(β)

# Coequalizer in Graph: collapsing a segment to a loop.
g = Graph(2)
add_edge!(g, 1, 2)
α = CSetTransformation((V=[1], E=Int[]), Graph(1), g)
β = CSetTransformation((V=[2], E=Int[]), Graph(1), g)
@test is_natural(α) && is_natural(β)
coeq = coequalizer(α, β)
@test ob(coeq) == ob(terminal(Graph))
@test force(proj(coeq)[:V]) == FinFunction([1,1])
@test force(proj(coeq)[:E]) == FinFunction([1])

# Pushout in Graph from (Reyes et al 2004, p. 59).
α = CSetTransformation((V=[2], E=Int[]), Graph(1), g)
β = CSetTransformation((V=[1], E=Int[]), Graph(1), ob(terminal(Graph)))
@test is_natural(α) && is_natural(β)
colim = pushout(α, β)
@test nv(ob(colim)) == 2
@test src(ob(colim)) == [1,2]
@test tgt(ob(colim)) == [2,2]
@test is_natural(coproj1(colim)) && is_natural(coproj2(colim))

# Same pushout using generic colimit interface.
#diagram = FreeDiagram([Graph(1), g, ob(terminal(Graph))], [(α,1,2), (β,1,3)])
# ^ This diagram gives colimit that is isomorphic but not equal!
diagram = FreeDiagram([g, ob(terminal(Graph)), Graph(1)], [(α,3,1), (β,3,2)])
ι1, ι2, _ = colim′ = colimit(diagram)
@test ob(colim′) == ob(colim)
@test force(ι1) == force(coproj1(colim))
@test force(ι2) == force(coproj2(colim))

# Attributed C-set morphisms
############################

# Constructors and accessors.
g = path_graph(WeightedGraph{Float64}, 2, E=(weight=2.0,))
h = path_graph(WeightedGraph{Float64}, 4, E=(weight=[1.,2.,3.],))
α = ACSetTransformation((V=[2,3], E=[2]), g, h)
@test length(components(α)) == 2

# Naturality.
@test is_natural(α)
β = ACSetTransformation((V=[2,1], E=[2]), g, h)
@test !is_natural(β) # Preserves weight but not graph homomorphism
β = ACSetTransformation((V=[1,2], E=[1]), g, h)
@test !is_natural(β) # Graph homomorphism but does not preserve weight

# Colimits
#---------

@present TheoryVELabeledGraph <: TheoryGraph begin
  Label::Data
  vlabel::Attr(V,Label)
  elabel::Attr(E,Label)
end
const VELabeledGraph = ACSetType(TheoryVELabeledGraph, index=[:src,:tgt])

# Initial labeled graph.
@test ob(initial(VELabeledGraph{Symbol})) == VELabeledGraph{Symbol}()

# Coproduct of labeled graphs.
g = VELabeledGraph{Symbol}()
add_vertices!(g, 2, vlabel=[:u,:v])
add_edge!(g, 1, 2, elabel=:e)
h = VELabeledGraph{Symbol}()
add_vertex!(h, vlabel=:u)
add_edge!(h, 1, 1, elabel=:f)
coprod = ob(coproduct(g, h))
@test subpart(coprod, :vlabel) == [:u, :v, :u]
@test subpart(coprod, :elabel) == [:e, :f]

# Pushout of labeled graph.
g0 = VELabeledGraph{Symbol}()
add_vertex!(g0, vlabel=:u)
α = ACSetTransformation((V=[1], E=Int[]), g0, g)
β = ACSetTransformation((V=[1], E=Int[]), g0, h)
@test is_natural(α) && is_natural(β)
colim = pushout(α, β)
@test src(ob(colim)) == [1,1]
@test tgt(ob(colim)) == [2,1]
@test subpart(ob(colim), :vlabel) == [:u, :v]
@test subpart(ob(colim), :elabel) == [:e, :f]

α′ = ACSetTransformation(V=[2], E=Int[], g0, g)
@test !is_natural(α′) # Vertex labels don't match.
@test_throws ErrorException pushout(α′, β)

# Finding C-set morphisms
#########################

# Graphs
#-------

g, h = path_graph(Graph, 3), path_graph(Graph, 4)
@test homomorphisms(g, h) == [CSetTransformation((V=[1,2,3], E=[1,2]), g, h),
                              CSetTransformation((V=[2,3,4], E=[2,3]), g, h)]
@test !is_isomorphic(g, h)

I = ob(terminal(Graph))
@test homomorphism(g, I) == CSetTransformation((V=[1,1,1], E=[1,1]), g, I)
@test !is_homomorphic(g, I, monic=true)
@test !is_homomorphic(I, h)

# Graph homomorphism starting from partial assignment, e.g. vertex assignment.
α = CSetTransformation((V=[2,3,4], E=[2,3]), g, h)
@test homomorphisms(g, h, initial=(V=[2,3,4],)) == [α]
@test homomorphisms(g, h, initial=(V=Dict(1 => 2, 3 => 4),)) == [α]
@test homomorphisms(g, h, initial=(E=Dict(1 => 2),)) == [α]
# Inconsistent initial assignment.
@test !is_homomorphic(g, h, initial=(V=Dict(1 => 1), E=Dict(1 => 3)))
# Consistent initial assignment but no extension to complete assignment.
@test !is_homomorphic(g, h, initial=(V=Dict(1 => 2, 3 => 3),))

# Monic and iso on a componentwise basis.
g1, g2 = path_graph(Graph, 3), path_graph(Graph, 2)
add_edges!(g1, [1,2,3,2], [1,2,3,3])  # loops on each node and one double arrow
add_edge!(g2, 1, 2)  # double arrow
@test length(homomorphisms(g2, g1)) == 8 # each vertex + 1->2, and four for 2->3
@test length(homomorphisms(g2, g1, monic=[:V])) == 5 # remove vertex solutions
@test length(homomorphisms(g2, g1, monic=[:E])) == 2 # two for 2->3
@test length(homomorphisms(g2, g1, iso=[:E])) == 0

# Symmetic graphs
#-----------------

g, h = path_graph(SymmetricGraph, 4), path_graph(SymmetricGraph, 4)
αs = homomorphisms(g, h)
@test all(is_natural(α) for α in αs)
@test length(αs) == 16
αs = isomorphisms(g, h)
@test length(αs) == 2
@test map(α -> collect(α[:V]), αs) == [[1,2,3,4], [4,3,2,1]]
g = path_graph(SymmetricGraph, 3)
@test length(homomorphisms(g, h, monic=true)) == 4

# Graph colorability via symmetric graph homomorphism.
# The 5-cycle has chromatic number 3 but the 6-cycle has chromatic number 2.
K₂, K₃ = complete_graph(SymmetricGraph, 2), complete_graph(SymmetricGraph, 3)
C₅, C₆ = cycle_graph(SymmetricGraph, 5), cycle_graph(SymmetricGraph, 6)
@test !is_homomorphic(C₅, K₂)
@test is_homomorphic(C₅, K₃)
@test is_natural(homomorphism(C₅, K₃))
@test is_homomorphic(C₆, K₂)
@test is_natural(homomorphism(C₆, K₂))

# Labeled graphs
#---------------

@present TheoryLabeledGraph <: TheoryGraph begin
  Label::Data
  label::Attr(V,Label)
end
const LabeledGraph = ACSetType(TheoryLabeledGraph, index=[:src,:tgt])

g = cycle_graph(LabeledGraph{Symbol}, 4, V=(label=[:a,:b,:c,:d],))
h = cycle_graph(LabeledGraph{Symbol}, 4, V=(label=[:c,:d,:a,:b],))
@test homomorphism(g, h) == ACSetTransformation((V=[3,4,1,2], E=[3,4,1,2]), g, h)
h = cycle_graph(LabeledGraph{Symbol}, 4, V=(label=[:a,:b,:d,:c],))
@test !is_homomorphic(g, h)

# Sub-C-sets
############

# Construct sub-C-sets.
X = path_graph(Graph, 4)
A = Subobject(X, V=[2,3,4], E=[2,3])
@test Subobject(X, V=[false,true,true,true], E=[false,true,true]) == A
α = hom(A)
@test is_natural(α)
@test dom(α) == path_graph(Graph, 3)
@test codom(α) == X

# Lattice of sub-C-sets.
X = Graph(6)
add_edges!(X, [1,2,3,4,4], [3,3,4,5,6])
A, B = Subobject(X, V=1:4, E=1:3), Subobject(X, V=3:6, E=3:5)
@test A ∧ B == Subobject(X, V=3:4, E=3:3)
@test A ∨ B == Subobject(X, V=1:6, E=1:5)
@test ⊤(X) |> force == Subobject(X, V=1:6, E=1:5)
@test ⊥(X) |> force == Subobject(X, V=1:0, E=1:0)

# Bi-Heyting algebra of sub-C-sets.
#
# Implication in Graph (Reyes et al 2004, Sec 9.1, p. 139).
I = Graph(1)
Y = path_graph(Graph, 3) ⊕ path_graph(Graph, 2) ⊕ path_graph(Graph, 2)
add_vertex!(Y)
add_edge!(Y, 2, 8)
Z = cycle_graph(Graph, 1) ⊕ cycle_graph(Graph, 1)
ιY, ιZ = colim = pushout(CSetTransformation(I, Y, V=[3]),
                         CSetTransformation(I, Z, V=[1]))
B_implies_C, B = Subobject(ιY), Subobject(ιZ)
C = Subobject(ob(colim), V=2:5, E=2:3)
@test (B ⟹ C) == B_implies_C

# Subtraction in Graph (Reyes et al 2004, Sec 9.1, p. 144).
X = ob(colim)
C = Subobject(X, V=2:5, E=[2,3,ne(X)-1])
@test (B \ C) == Subobject(X, V=[nv(X)], E=[ne(X)])

# Negation in Graph (Reyes et al 2004, Sec 9.1, p. 139-140).
X = cycle_graph(Graph, 1) ⊕ path_graph(Graph, 2) ⊕ cycle_graph(Graph, 4)
add_vertex!(X)
add_edge!(X, 4, 8)
A = Subobject(X, V=[2,3,4,5,8], E=[3,7])
neg_A = Subobject(X, V=[1,6,7], E=[1,5])
@test is_natural(hom(A)) && is_natural(hom(neg_A))
@test ¬A == neg_A
@test ¬neg_A == Subobject(X, V=[2,3,4,5,8], E=[2,3,7])

# Non in Graph (Reyes et al 2004, Sec 9.1, p. 144).
X = path_graph(Graph, 5) ⊕ path_graph(Graph, 2) ⊕ cycle_graph(Graph, 1)
A = Subobject(X, V=[1,4,5], E=[4])
non_A = Subobject(X, V=setdiff(vertices(X), 5), E=setdiff(edges(X), 4))
@test ~A == non_A
@test ~non_A == Subobject(X, V=[4,5], E=[4])

# Negation and non in DDS.
S₁ = DDS(); add_parts!(S₁, :X, 5, Φ=[3,3,4,5,5])
S₂ = DDS(); add_parts!(S₂, :X, 3, Φ=[2,3,3])
ι₁, ι₂ = colim = coproduct(S₁, S₂)
S = ob(colim)
A = Subobject(S, X=[3,4,5])
@test ¬A == Subobject(ι₂)
@test ¬Subobject(ι₂) == Subobject(ι₁)
@test ~A == ⊤(S) |> force

# Serialization
###############

function roundtrip_json_acset(x::T) where T <: AbstractACSet
  mktempdir() do dir
    path = joinpath(dir, "acset.json")
    write_json_acset(x, path)
    read_json_acset(T, path)
  end
end

g = star_graph(Graph, 5)
@test roundtrip_json_acset(g) == g

g = path_graph(WeightedGraph{Float64}, 3, E=(weight=[0.5, 1.5],))
@test roundtrip_json_acset(g) == g

g = VELabeledGraph{Symbol}()
add_vertices!(g, 2, vlabel=[:u,:v])
add_edge!(g, 1, 2, elabel=:e)
@test roundtrip_json_acset(g) == g

@present TheoryLabeledDDS <: TheoryDDS begin
  Label::Data
  label::Attr(X, Label)
end
const LabeledDDS = ACSetType(TheoryLabeledDDS, index=[:Φ, :label])

ldds = LabeledDDS{Int}()
add_parts!(ldds, :X, 4, Φ=[2,3,4,1], label=[100, 101, 102, 103])
@test roundtrip_json_acset(ldds) == ldds

end


using TensorNetworkCodes
using Test

@testset "pauli_are_commuting" begin
    # 5-qubit code stabilizers: XZZXI, IXZZX, XIXZZ, ZXIXZ
    stabilizers = [[1, 3, 3, 1, 0], [0, 1, 3, 3, 1], [1, 0, 1, 3, 3], [3, 1, 0, 1, 3]]
    logical_x = [1, 1, 1, 1, 1]  # XXXXX
    logical_z = [3, 3, 3, 3, 3]  # ZZZZZ
    @test pauli_are_commuting(stabilizers)
    @test pauli_are_commuting(vcat(stabilizers, [logical_x]))
    @test pauli_are_commuting(vcat(stabilizers, [logical_z]))
    @test !pauli_are_commuting([logical_x, logical_z])
    @test pauli_are_commuting([Int[]])  # empty operator
    @test pauli_are_commuting([])  # empty operator list
end

@testset "pauli_are_independent" begin
    # 5-qubit code stabilizers: XZZXI, IXZZX, XIXZZ, ZXIXZ
    stabilizers = [[1, 3, 3, 1, 0], [0, 1, 3, 3, 1], [1, 0, 1, 3, 3], [3, 1, 0, 1, 3]]
    logical_x = [1, 1, 1, 1, 1]  # XXXXX
    logical_z = [3, 3, 3, 3, 3]  # ZZZZZ
    @test pauli_are_independent(stabilizers)
    @test pauli_are_independent(vcat(stabilizers, [logical_x, logical_z]))
    extra_stabilizer = pauli_product.(stabilizers[1], stabilizers[2])
    @test !pauli_are_independent(vcat(stabilizers, [extra_stabilizer]))
    @test pauli_are_independent([Int[]])  # empty operator
    @test pauli_are_independent([])  # empty operator list
end

@testset "pauli_commutation" begin
    # single-qubit
    @test pauli_commutation.(0, [0, 1, 2, 3]) == [0, 0, 0, 0]  # I with IXYZ
    @test pauli_commutation.(1, [0, 1, 2, 3]) == [0, 0, 1, 1]  # X with IXYZ
    @test pauli_commutation.(2, [0, 1, 2, 3]) == [0, 1, 0, 1]  # Y with IXYZ
    @test pauli_commutation.(3, [0, 1, 2, 3]) == [0, 1, 1, 0]  # Z with IXYZ
    # multi-qubit
    # 5-qubit code stabilizers: XZZXI, IXZZX, XIXZZ, ZXIXZ
    stabilizers = [[1, 3, 3, 1, 0], [0, 1, 3, 3, 1], [1, 0, 1, 3, 3], [3, 1, 0, 1, 3]]
    @test pauli_commutation.(Ref([1, 1, 1, 1, 1]), stabilizers) == zeros(4) # XXXXX
    @test pauli_commutation.(Ref([3, 3, 3, 3, 3]), stabilizers) == zeros(4) # ZZZZZ
    @test pauli_commutation.(Ref([1, 1, 0, 0, 0]), stabilizers) == [1, 0, 0, 1] # XXIII
    @test pauli_commutation.(Ref([0, 0, 0, 3, 3]), stabilizers) == [1, 1, 0, 1] # IIIZZ
    @test pauli_commutation.(Ref([0, 0, 2, 0, 0]), stabilizers) == [1, 1, 1, 0] # IIYII
    @test pauli_commutation(Int[], Int[]) == 0  # empty operator
end

@testset "pauli_pow" begin
    @test pauli_pow.(0, [-1, 0, 1, 2, 3]) == [0, 0, 0, 0, 0]  # I to powers
    @test pauli_pow.(1, [-1, 0, 1, 2, 3]) == [1, 0, 1, 0, 1]  # X to powers
    @test pauli_pow.(2, [-1, 0, 1, 2, 3]) == [2, 0, 2, 0, 2]  # Y to powers
    @test pauli_pow.(3, [-1, 0, 1, 2, 3]) == [3, 0, 3, 0, 3]  # Z to powers
end

@testset "pauli_product" begin
    @test pauli_product.(0, [0, 1, 2, 3]) == [0, 1, 2, 3]  # I with IXYZ -> IXYZ
    @test pauli_product.(1, [0, 1, 2, 3]) == [1, 0, 3, 2]  # X with IXYZ -> XIZY
    @test pauli_product.(2, [0, 1, 2, 3]) == [2, 3, 0, 1]  # Y with IXYZ -> YZIX
    @test pauli_product.(3, [0, 1, 2, 3]) == [3, 2, 1, 0]  # Z with IXYZ -> ZYXI
end

@testset "pauli_product" begin
    operators = [[0, 1, 2, 3], [1, 0, 3, 2], [2, 3, 0, 1]]
    operators_copy = deepcopy(operators)
    @test pauli_product(operators) == [3, 2, 1, 0]
    @test operators == operators_copy  # operators not modified
    @test pauli_product([Int[]]) == []  # empty operator
end

@testset "pauli_product_pow" begin
    operators = [[0, 1, 2, 3], [1, 0, 3, 2], [2, 3, 0, 1]]
    operators_copy = deepcopy(operators)
    @test pauli_product_pow(operators, [0, 0, 0]) == [0, 0, 0, 0]
    @test pauli_product_pow(operators, [0, 0, 1]) == [2, 3, 0, 1]
    @test pauli_product_pow(operators, [0, 1, 0]) == [1, 0, 3, 2]
    @test pauli_product_pow(operators, [0, 1, 1]) == [3, 3, 3, 3]
    @test pauli_product_pow(operators, [1, 0, 0]) == [0, 1, 2, 3]
    @test pauli_product_pow(operators, [1, 0, 1]) == [2, 2, 2, 2]
    @test pauli_product_pow(operators, [1, 1, 0]) == [1, 1, 1, 1]
    @test pauli_product_pow(operators, [1, 1, 1]) == [3, 2, 1, 0]
    @test pauli_product_pow(operators, [-1, 3, 1]) == [3, 2, 1, 0]  # non-binary powers
    @test operators == operators_copy  # operators not modified
    @test pauli_product_pow([Int[]], [1]) == []  # empty operator
end

@testset "pauli_rep_change" begin
    @test pauli_rep_change.([0, 1, 2, 3]) == ['I', 'X', 'Y', 'Z']
    @test pauli_rep_change.(['I', 'X', 'Y', 'Z']) == [0, 1, 2, 3]
end

@testset "pauli_weight" begin
    @test pauli_weight([0, 0, 0, 0, 0]) == 0
    @test pauli_weight([0, 0, 1, 2, 0]) == 2
    @test pauli_weight([3, 0, 2, 0, 1]) == 3
    @test pauli_weight([3, 2, 1, 2, 2]) == 5
    @test pauli_weight(Int[]) == 0  # empty operator
end


function assert_pd(d, α)
    @assert α == 0 && all(x -> x > 0, d) "The input matrix is not positive definite."
end

function update_L!(L, d)
    @inbounds for j in 1:size(L, 2)
        _d = sqrt(d[j])
        L[j,j] = _d
        for i in Base.Iterators.drop(nzrange(L, j), 1)
            L.nzval[i] *= _d
        end
    end
    return L
end

mutable struct CholeskyPreconditioner{T, S <: AbstractSparseMatrix{T}} <: AbstractPreconditioner
    L::LowerTriangular{T, S}
    memory::Int
end
function EmptyCholeskyPreconditioner(A, memory=1)
    T = eltype(A)
    _A = A isa Symmetric || A isa Hermitian ? A.data : A
    return CholeskyPreconditioner(LowerTriangular(sparse(one(T)*I, size(_A)...)), memory)
end

function CholeskyPreconditioner(A, memory=2)
    _A = get_data(A)
    L, d, α = lldl(_A, memory=memory)
    assert_pd(d, α)
    update_L!(L, d)
    return CholeskyPreconditioner(LowerTriangular(L), memory)
end

function UpdatePreconditioner!(C::CholeskyPreconditioner, A, memory=C.memory)
    _A = get_data(A)
    L, d, α = lldl(_A, memory=memory)
    assert_pd(d, α)
    update_L!(L, d)
    C.L = LowerTriangular(L)
    return C
end

@inline function ldiv!(C::CholeskyPreconditioner{T}, y::AbstractVector{T}) where {T}
    ldiv!(C.L, y)
    return ldiv!(C.L', y)
end
@inline function ldiv!(y::AbstractVector{T}, C::CholeskyPreconditioner{T}, b::AbstractVector{T}) where {T}
    y .= b
    return ldiv!(C, y)
end
@inline function (\)(C::CholeskyPreconditioner{T, <:AbstractSparseMatrix{T}}, b::AbstractVector{T}) where {T}
    y = copy(b)
    return ldiv!(C, y)
end


using Distributed
addprocs(8)
@everywhere include("reproducibility.jl")
include("reproducibility.jl")

@everywhere using LightGraphs
@everywhere using NPZ
@everywhere using LinearAlgebra
@everywhere using Expokit

##

@everywhere function normalized_laplacian(g::Graph, T)
    m = laplacian_matrix(g, T)
    d = diagm(1. ./ sqrt.(diag(m)))
    d*m*d
end

@everywhere function get_hamiltonian(g::Graph, mode::Symbol)
    m = eval(mode)(g, Float64)
    if mode != :adjacency_matrix
        m *= -1
    end
    Symmetric(Matrix{Float64}(m))
end

@everywhere function normalize_eigen(eigdec, h)
    n = length(eigdec.values)
    λn, λ1 = eigdec.values[[1,n]]

    eigdec.values .-= λ1
    eigdec.values ./= λ1 - λn
    eigdec.values .+= 1

    eigdec, (h - λ1*I)/(λ1 - λn) + I
end

@everywhere function true_probability(h, eigdec, node)
    s1 = Sk(eigdec, node, 1)
    s2 = Sk(eigdec, node, 2)

    the_hamiltonian = s1*h
    the_hamiltonian[node,node] += 1
    init_state = eigdec.vectors[:,end]

    time = 1/sqrt(ε(eigdec, node))*sqrt(s2)/s1
    f_state = expmv(time, 1im*the_hamiltonian, init_state)
    abs2(f_state[node])
end

@everywhere function Δ(eigdec)
    eigdec.values[end] - eigdec.values[end-1]
end

@everywhere function Sk(eigdec, node::Int, k::Int)
    n = length(eigdec.values)
    @assert 1 <= k <= n
    @assert 1 <= node <= n
    a = eigdec.vectors[node,1:end-1]
    sum(abs2.(a) ./ (1 .- eigdec.values[1:end-1]).^k)
end

@everywhere function ε(eigdec, node::Int)
    abs2(eigdec.vectors[node,end])
end

# h = get_hamiltonian(erdos_renyi(10, 0.5), :adjacency_matrix)
# println(eigvals(h))

@everywhere function condition(g::SimpleGraph, node::Int, mode::Symbol)
    h = get_hamiltonian(g, mode)
    eigdec, h = normalize_eigen(eigen(h), h)

    s1, s2, s3 = (k->Sk(eigdec, node, k)).([1,2,3])
    my_ε = ε(eigdec, node)
    my_Δ = Δ(eigdec)
    result = Float64[]
    push!(result, sqrt(my_ε)/min(s1*s2/s3, my_Δ*sqrt(s2)))
    push!(result, my_Δ)
    push!(result, abs2(s1)/s2) # probability
    push!(result, 1/sqrt(my_ε)*sqrt(s2)/s1) # time
    push!(result, true_probability(h, eigdec, node))

    result
end

##

data_n = 100:100:5000
repeating = 200
@everywhere linreg(x, y) = hcat(fill!(similar(x), 1), x) \ y

@sync @distributed for m=1:repeating
    println("######### m = $m #########")
    for mode = [:normalized_laplacian, :adjacency_matrix, :laplacian_matrix]
        for vertex = [:1, :n]
            for m0 = 3:3
                @time begin
                    filename = "complex_search/BA-$m0-$vertex-$(string(mode)[1:3])-$m.npz"
                    @show filename

                    g = complete_graph(m0)
                    f = g->condition(g, vertex == :1 ? 1 : nv(g) , mode)

                    data_y = Vector{Float64}[]
                    for n = data_n
                        barabasi_albert!(g, n, m0)
                        push!(data_y, f(g))
                    end

                    npzwrite(filename, hcat(data_n, transpose(hcat(data_y...))))
                end
            end
        end
    end
end


# best practice: Ctrl-D out of REPL first, then run test
using Test
using LightGraphs
using LinearAlgebra
include("FFGmain.jl")
include("lattices.jl")

ffgmathtest = @testset "test-FFGMATH/LATTICES" begin
    ############### testing lattice routines #################################
    dims = (5,5);
    lg = Lattices.get_SquareLattice(dims,pbc=true).graph
    @test sort(unique(neighborhood(lg, 3,2))) ==
    sort([3,4,5,2,1,9,8,7,13,23,18,24,22])

    dims = (4,4)
    lattice = Lattices.get_MixedBoundarySquareLattice(dims)
    F = Lattices.get_Tightbinding_Wavefunctions(lattice)
    @test F.values ≈ [-3.414213562373095,-3.414213562373095,
    -1.4142135623730951,-1.4142135623730951,-1.4142135623730951,-1.4142135623730951,
    -0.5857864376269051,-0.5857864376269051,0.5857864376269051,0.5857864376269051,
    1.4142135623730951,1.4142135623730951,1.4142135623730951,1.4142135623730951,
    3.414213562373095,3.414213562373095]

    dims = (2,3);
    lattice = Lattices.get_SquareLattice(dims,pbc=true);
    @test Matrix(adjacency_matrix(lattice.graph)) == [0 1 1 0 1 0;
                                                     1 0 0 1 0 1;
                                                     1 0 0 1 1 0;
                                                     0 1 1 0 0 1;
                                                     1 0 1 0 0 1;
                                                     0 1 0 1 1 0]
    F = Lattices.get_Tightbinding_Wavefunctions(lattice);
    evals = F.values;
    @test evals ≈ [-3, -1, 0, 0, 2, 2]
    states = F.vectors[:,1:3]
    r = [1,2,3]
    ############### testing math routines #####################################
    @test FFG.get_Determinant(r,states) ≈ 0.3333333333
    tmat = [0 -1.2247448713915894 -1.2247448713915894;
            -1.2247448713915894 1.2247448713915894 0;
            1 0 -1]
    @test FFG.get_Inverse_Matrix(r,states) ≈ tmat

    ############### set up small matrix to test det ratio factor & inverse #####
    m1 = [1 2 4;
          4 8 12;
          8 12 16];
    m1inv = inv(m1);
    m2 = [1 4 4;
           4 12 12;
           8 15 16];

    u = [2 ;4 ;3 ];
    v = [0 ; 1 ; 0 ];

    @test FFG.det_ratio_factor(m1inv,u,v) ≈ det(m2)/det(m1)

    @test FFG.sherman_morrison_inverse_update(m1inv,u,v) ≈ inv(m2)
end

@testset "test-FFGMAIN/HELPERS" begin
    ############# test chain initialization #######################
    model = Dict(
        "dims" => (2,3),
        "lattice" => Lattices.get_SquareLattice((2,3),pbc=true),
        "filling" => 1,
        "hamiltonian" => Lattices.get_Tightbinding_Wavefunctions,
        "fermi_energy" => 0,
        )
    policy = FFG.SwapNeighborsPolicy()
    mc_spec = Dict("mc_steps" => 1,
                        "mc_warmup_steps" => 1)

    # just tests that the function doesn't error out
    c1 = FFG.GutzwillerChain()
    @test FFG.init_Chain!(c1,model=model,observable=Nothing,policy=policy,
    mc_spec=mc_spec) == Nothing

    c2 = FFG.DoubleGutzwillerChain()
    @test FFG.init_Chain!(c2,model=model,observable=Nothing,policy=policy,
    mc_spec=mc_spec) == Nothing

    ############ make a test chain $$$$$$$$$$$$$$$$$$$$$$$$

    model = Dict(
        "dims" => (2,3),
        "lattice" => Lattices.get_SquareLattice((2,3),pbc=true),
        "filling" => 1,
        "hamiltonian" => Lattices.get_Tightbinding_Wavefunctions,
        "fermi_energy" => 0,
        )

    function observable(chain::FFG.GutzwillerChain)
        state = FFG.get_State(chain)
        Sz1 = state.spin_config.sc[1]
        return Dict("Sz1" => Sz1)
    end

    policy = FFG.SwapNeighborsPolicy()
    mc_spec = Dict("mc_steps" => 1,
                    "mc_warmup_steps" => 1)

    testchain = FFG.GutzwillerChain();
    testchain.basechain = FFG.MarkovChain()

    r_up = [3,1,5]
    r_down = [2,6,4]

    # STATE CREATION TESTS
    bd = [FFG.Electron(2,"up"), FFG.Electron(1,"down"), FFG.Electron(1,"up"),
        FFG.Electron(3,"down"), FFG.Electron(3,"up"), FFG.Electron(2,"down")]
    @test FFG.make_business_directory(r_up,r_down) == bd

    spinc = FFG.SpinConfiguration([1,-1,1,-1,1,-1])
    @test FFG.make_spin_config(r_up,r_down).sc == spinc.sc

    # testing construction of initial bond graph
    using LightGraphs: SimpleGraph, add_edge!
    ib = SimpleGraph(6);
    add_edge!(ib,1,2);
    add_edge!(ib,3,4);
    add_edge!(ib,5,6);

    @test FFG.get_init_bonds(model["lattice"].graph,spinc) ==
    ib

    ## STATE UPDATE TESTS
    teststate = FFG.GutzwillerState(
    r_up,r_down, bd, spinc,ib,
    [], 0, 0, [],[]
    )

    testchain.basechain.state = teststate
    testchain.basechain.model = model

    ## test a simple move
    move = FFG.ExchangeMove((1,2))

    @test FFG.get_proposal_factor_ratio(testchain,move) == 3/7
    FFG.update_Rs!(teststate,move)
    @test teststate.r_up == [3,2,5]
    @test teststate.r_down == [1,6,4]

    FFG.update_Bonds!(testchain,move)

    ub = SimpleGraph(6);
    add_edge!(ub,1,2);
    add_edge!(ub,1,3);
    add_edge!(ub,1,5);
    add_edge!(ub,2,4);
    add_edge!(ub,2,6);
    add_edge!(ub,3,4);
    add_edge!(ub,5,6);
    @test teststate.bonds == ub

    FFG.update_Spin_config!(teststate,move)
    @test teststate.spin_config.sc == [-1,1,1,-1,1,-1]

    bd = [FFG.Electron(1,"down"),FFG.Electron(2,"up"),FFG.Electron(1,"up"),
        FFG.Electron(3,"down"),FFG.Electron(3,"up"),FFG.Electron(2,"down")]
    FFG.update_Business_directory!(teststate,move)
    @test teststate.business_directory == bd

end

@testset "maintest" begin
    include("maintest.jl")
    @test size(maintest()) == (3,)
end

@testset "swaproutines" begin
    """ Test suite for swap routines. """
    dims = (6,) # dimension of the lattice
    lattice = Lattices.get_SquareLattice(dims,pbc=true) # make a square lattice
    filling = 1 # setting filling ≢ 1 means there are holes.
    model = Dict(
        "dims" => dims,
        "lattice" => lattice,
        "filling" => filling,
        "hamiltonian" => Lattices.get_Tightbinding_Wavefunctions,
        "fermi_energy" => 0,
        )
    function observable(chain::FFG.GutzwillerChain)
        state = FFG.get_State(chain)
        Sz1 = state.spin_config.sc[1]
        data = Dict("Sz1" => Sz1)
        return data
    end
    policy = FFG.SwapNeighborsPolicy()
    mc_spec = Dict("mc_steps" => 1E3,
                    "mc_warmup_steps" => 1E3,
                    "sample_interval" => 1E2)

    dg = FFG.DoubleGutzwillerChain()
    c1 = dg.replicas[1]
    c2 = dg.replicas[2]
    c1.basechain = FFG.MarkovChain()
    c2.basechain = FFG.MarkovChain()
    c1.basechain.model = model
    c2.basechain.model = model

    r_up1 = [5,6,4]
    r_down1 = [2,1,3]
    r_up2 = [2,5,1]
    r_down2 = [6,4,3]

    teststate1 = FFG.get_test_state(c1,r_up1,r_down1)
    teststate2 = FFG.get_test_state(c2,r_up2,r_down2)

    dg.replicas[1].basechain.state = teststate1
    dg.replicas[2].basechain.state = teststate2

    # first lets test a region that has a single exchange
    swapsites = [1,4,5]

    (spinc1,spinc2) = (teststate1.spin_config.sc,teststate2.spin_config.sc)

    Tinder1 = FFG.ElectronTinder([],[])
    Tinder2 = FFG.ElectronTinder([],[])
    for site = swapsites

        if spinc1[site] != spinc2[site]
            FFG.addUser!(Tinder1,site,spinc1[site])
            FFG.addUser!(Tinder2,site,spinc2[site])
        end
    end
    @test FFG.SwipeRight(Tinder1) == FFG.SwipeRight(FFG.ElectronTinder([4],[1]))
    @test FFG.SwipeRight(Tinder2) == FFG.SwipeRight(FFG.ElectronTinder([1],[4]))

    # now comes the difficult test...

    #
    r_up1prime = [5,6,1]
    r_down1prime = [2,4,3]
    r_up2prime = [2,5,4]
    r_down2prime = [6,1,3]
    #
    # ts1p = FFG.get_test_state(c1,r_up1prime,r_down1prime)
    # ts2p = FFG.get_test_state(c2,r_up2prime,r_down2prime)

    # print(ts1p.det_A_up * ts1p.det_A_down * ts2p.det_A_up * ts2p.det_A_down,"\n")
    # print(teststate1.det_A_up * teststate1.det_A_down * teststate2.det_A_up * teststate2.det_A_down,"\n")

    # manual_eval = (ts1p.det_A_up * ts1p.det_A_down * ts2p.det_A_up * ts2p.det_A_down)/(teststate1.det_A_up * teststate1.det_A_down * teststate2.det_A_up * teststate2.det_A_down)
    @test FFG.compute_swapregion(dg,swapsites) ≈ 1

end


# smoothing polygons

Using Luxor

@png begin
    background("grey15")
   tiles = Tiler(600, 250, 1, 5, margin=10)
    for (pos, n) in tiles
        p = star(pos, tiles.tilewidth/2 - 2, 5, 0.3, 0, vertices=true)
        setdash("dot")
        sethue("white")
        prettypoly(p, close=true, :stroke)
        setdash("solid")
        sethue("antiquewhite")
        polysmooth(p, n * 2, :fill)
    end
end


using JuMP
using Dates

const OPTIMAL = JuMP.MathOptInterface.OPTIMAL
const INFEASIBLE = JuMP.MathOptInterface.INFEASIBLE
const UNBOUNDED = JuMP.MathOptInterface.DUAL_INFEASIBLE

function resolvePlne(model, verbose=1, nameOfPL="")

	optimize!(model)

	if verbose == 3
		println(solution_summary(model, verbose=true))
		return model
	end

	status = termination_status(model)

	# if status == JuMP.MathOptInterface.OPTIMAL
	# 	println("Valeur optimale = ", objective_value(model))
	# end

	if verbose >= 2

		last = ""
		for var in all_variables(model)

			which = split(JuMP.name(var), "[")[1]
			if which != last
				last = which
				println("\nVariables $(last)")
			end
			println(var, ": ", value(var))

		end

	end

	if verbose >= 1
		println("\nObjetive value: ", objective_value(model))
	end

	if ! isempty(nameOfPL)
		write_to_file(model, nameOfPL*"_model_"*string(Dates.now())*".mps")
	end

	return model

end

function getModelVariables(model)

end


# test bearing

using RoME
using Test

@testset "Triangulation test in 2D, 3 beacons" begin

# noise models
lmp_noise = Matrix(Diagonal([0.01;0.01].^2))

# new factor graph
fg = initfg()

# landmarks
addVariable!(fg, :l1, Point2)
addVariable!(fg, :l2, Point2)
addVariable!(fg, :l3, Point2)

addFactor!(fg, [:l1], PriorPoint2(MvNormal([10.0;1.0],lmp_noise)), autoinit=false)
addFactor!(fg, [:l2], PriorPoint2(MvNormal([10.0+sqrt(3)/2;-0.5],lmp_noise)), autoinit=false)
addFactor!(fg, [:l3], PriorPoint2(MvNormal([10.0-sqrt(3)/2;-0.5],lmp_noise)), autoinit=false)

# pose
addVariable!(fg, :x1, Pose2)
addFactor!(fg, [:x1;:l1], Pose2Point2Bearing(Normal(pi/2,0.05)), autoinit=false)
addFactor!(fg, [:x1;:l2], Pose2Point2Bearing(Normal(-pi/6,0.05)), autoinit=false)
addFactor!(fg, [:x1;:l3], Pose2Point2Bearing(Normal(-pi+pi/6,0.05)), autoinit=false)

# manualinit!(fg, :x1, [30.0*randn(2,100);randn(1,100)])

# drawGraph(fg)

tree,smt,hist = solveTree!(fg)

pts = getPoints(getKDE(fg, :x1))
pts[1,:] .-= 10.0

N = size(pts,2)

@test 0.8*N < sum(sqrt.(sum(pts[1:2,:].^2,dims=1)) .< 0.3)

@test 0.8*N < sum(abs.(pts[3,:]) .< 0.1)

#
# using RoMEPlotting
# Gadfly.set_default_plot_size(35cm,25cm)
# drawPosesLandms(fg, spscale=0.2) # |> PNG("/home/dehann/Downloads/triangulation.png",20cm,15cm)
# plotPose(fg, :x1, scale=0.1) # |> PNG("/home/dehann/Downloads/triangulationPose.png",20cm,15cm)
#
# plotLocalProduct(fg, :x1)
#
# plotFactor(fg,  lsf(fg, Pose2Point2Bearing)[1])

end

@testset "Triangulation test in 2D (opposite), 3 beacons" begin

# noise models
lmp_noise = Matrix(Diagonal([0.01;0.01].^2))

# new factor graph
fg = initfg()

# landmarks
addVariable!(fg, :l1, Point2)
addVariable!(fg, :l2, Point2)
addVariable!(fg, :l3, Point2)

addFactor!(fg, [:l1], PriorPoint2(MvNormal([-10.0;1.0-10.0],lmp_noise)), autoinit=false)
addFactor!(fg, [:l2], PriorPoint2(MvNormal([-10.0+sqrt(3)/2;-0.5-10.0],lmp_noise)), autoinit=false)
addFactor!(fg, [:l3], PriorPoint2(MvNormal([-10.0-sqrt(3)/2;-0.5-10.0],lmp_noise)), autoinit=false)

# pose
addVariable!(fg, :x1, Pose2)
addFactor!(fg, [:x1;:l1], Pose2Point2Bearing(Normal(pi/2,0.05)), autoinit=false)
addFactor!(fg, [:x1;:l2], Pose2Point2Bearing(Normal(-pi/6,0.05)), autoinit=false)
addFactor!(fg, [:x1;:l3], Pose2Point2Bearing(Normal(-pi+pi/6,0.05)), autoinit=false)

manualinit!(fg, :x1, [0.01*randn(2,100);-randn(1,100)])
# manualinit!(fg, :x1, [30.0*randn(2,100);randn(1,100)])

# drawGraph(fg)

tree,smt,hist = solveTree!(fg)

pts = getPoints(getKDE(fg, :x1))
pts[1,:] .+= 10.0
pts[2,:] .+= 10.0

N = size(pts,2)

@test 0.8*N < sum(sqrt.(sum(pts[1:2,:].^2,dims=1)) .< 0.3)

@test 0.8*N < sum(abs.(pts[3,:]) .< 0.1)

#
# using RoMEPlotting
# Gadfly.set_default_plot_size(35cm,25cm)
# drawPosesLandms(fg, spscale=0.2) # |> PNG("/home/dehann/Downloads/triangulation.png",20cm,15cm)
# plotPose(fg, :x1, scale=0.1) # |> PNG("/home/dehann/Downloads/triangulationPose.png",20cm,15cm)
#
# plotLocalProduct(fg, :x1)
#
# plotFactor(fg,  lsf(fg, Pose2Point2Bearing)[1])

end

#


using LAR

# test coarse surface approximation (r=1, R=3)
W,lar = toroidalsurface(1,3,10,20);
larview(W,lar.FV)
larview(W,lar.EV)
larview(W,lar.VV)
viewexploded(W,lar.FV)
viewexploded(W,lar.EV)
viewexploded(W,lar.VV)

# test non-manifold surface  (r=3, R=3)
V,lar = toroidalsurface(1,1,40,80);
larview(V,lar.FV)
viewexploded(V,lar.FV)

# test coarsest surface approximation (r=1, R=3)
W,lar = toroidalsurface(1,1,3,3);
larview(W,lar.FV)
larview(W,lar.EV)
viewexploded(W,lar.FV)
viewexploded(W,lar.EV)


"""
    full_matrix(system)

    returns matrix from a simulation step's linear system (i.e., A from "Ax = b")

    system: System
"""
full_matrix(system::System) = full_matrix(system.matrix_entries, system.dimrow, system.dimcol)

function full_matrix(matrix_entries::SparseMatrixCSC, dimrow, dimcol)
    range_row = [1:dimrow[1]]
    for (i,dim) in enumerate(collect(Iterators.rest(dimrow, 2)))
        push!(range_row, sum(dimrow[1:i])+1:sum(dimrow[1:i])+dim)
    end
    range_col = [1:dimcol[1]]
    for (i,dim) in enumerate(collect(Iterators.rest(dimcol, 2)))
        push!(range_col, sum(dimcol[1:i])+1:sum(dimcol[1:i])+dim)
    end

    A = zeros(sum(dimrow),sum(dimcol))

    for (i,row) in enumerate(matrix_entries.rowval)
        col = findfirst(x->i<x,matrix_entries.colptr)-1
            A[range_row[row],range_col[col]] = matrix_entries[row,col].value
    end
    return A
end

# assemble gradient (i.e., b from "Ax = b")
"""
    full_vector(system)

    returns residual vector from a simulation step's linear system (i.e., b from "Ax = b")

    system: System
"""
full_vector(system) = vcat(getfield.(system.vector_entries,:value)...)


module FactorFF
using Hecke, Markdown

function Hecke.norm(f::PolyElem{<: Generic.FunctionFieldElem})
    K = base_ring(f)
    P = polynomial_to_power_sums(f, degree(f)*degree(K))
    PQ = elem_type(base_field(K))[tr(x) for x in P]
    return power_sums_to_polynomial(PQ)
end

function from_mpoly(f::MPolyElem, S::PolyRing{<:Generic.Rat})
  @assert ngens(parent(f)) == 2
  @assert base_ring(f) == base_ring(base_ring(S))
  R = parent(numerator(gen(base_ring(S))))
  @assert isa(R, PolyRing)
  F = [zero(R) for i=0:degree(f, 1)]
  for (c, e) = zip(coefficients(f), exponent_vectors(f))
    setcoeff!(F[e[1]+1], e[2], c)
  end
  o = one(parent(F[1]))
  return S(map(x->base_ring(S)(x//o), F))
end

function Hecke.factor(f::Generic.Poly{<:Generic.Rat})
  Pf = parent(f)
  R, r = PolynomialRing(base_ring(base_ring(f)), 2)
  d = lcm(map(denominator, coefficients(f)))
  Fc = MPolyBuildCtx(R)
  for i=0:degree(f)
    c = numerator(coeff(f, i)*d)
    for j=0:degree(c)
      push_term!(Fc, coeff(c, j), [i,j])
    end
  end
  lf = factor(finish(Fc))
  @assert is_constant(lf.unit)

  fa = Fac(Pf(constant_coefficient(lf.unit)), Dict((from_mpoly(k, Pf), e) for (k,e) = lf))
  @assert sum(degree(x)*y for (x,y) = fa) == degree(f)
  return fa
end

function Hecke.factor(f::Generic.Poly{<:Generic.Rat{T}}, F::Generic.FunctionField{T}) where {T}
  return factor(map_coefficients(F, f))
end
#plain vanilla Trager, possibly doomed in pos. small char.
function Hecke.factor(f::Generic.Poly{<:Generic.FunctionFieldElem})
  if !is_squarefree(f)
    sf = gcd(f, derivative(f))
    f = divexact(f, sf)
  else
    sf = one(parent(f))
  end
  lc = leading_coefficient(f)
  f = divexact(f, lc)
  i = 0
  local N
  g = f
  t = gen(parent(f))
  a = gen(base_ring(t))

  while true
    if !iszero(constant_coefficient(g))
      N = norm(g)
      if is_squarefree(N)
        break
      end
    end
    i += 1
    g = evaluate(g, t-a)
    if i > 10
      error("not plausible")
    end
  end

  fN = factor(N)
  @assert isone(fN.unit)
  D = Fac(parent(f)(lc), Dict((gcd(map_coefficients(base_ring(f), p, parent = parent(f)), g)(t+i*a), k) for (p,k) = fN.fac))
  if !isone(sf)
    for k = keys(D.fac)
      D.fac[k] += valuation(sf, k)
    end
  end
  return D
end

#TODO: don't think this startegy is optimal, but it works...
function Hecke.splitting_field(f::Generic.Poly{<:Generic.Rat})
  f = divexact(f, gcd(f, derivative(f)))

  lf = factor(f)
  lf = [x for x = keys(lf.fac) if degree(x) > 1]
  if length(lf) == 0
    return base_ring(f)
  end

  while true
    G, b = FunctionField(lf[1], "b", cached = false)
    if length(lf) == 1 && degree(G) < 3
      return G
    end

    f = prod(lf)

    GT, t = PolynomialRing(G, cached = false)
    g = divexact(map_coefficients(G, f, parent = GT), t-b)

    i = 0
    local N
    while true
      if !iszero(constant_coefficient(g))
        N = norm(g)
        if is_squarefree(N)
          break
        end
      end
      i += 1
      g = evaluate(g, t-b)
      if i > 10
        error("not plausible")
      end
    end

    fN = factor(N)
    lf = [x for x = keys(fN.fac) if degree(x) > degree(G)]
      #the gcd of x with deg(x) == deg(G) will yield a linear
      #factor, hence does not contribute further
    if length(lf) == 0
      return G
    end
  end
end

@doc Markdown.doc"""
    swinnerton_dyer(V::Vector, x::Generic.Poly{<:Generic.Rat})
    swinnerton_dyer(n::Int, x::Generic.Poly{<:Generic.Rat})

Compute the minimal polynomial of $\sum \pm \sqrt{t+v_i}$ evaluated at $x$.
$t$ is the generator of the base field of the parent of $x$.

In the second variant, the polynomial has roots $\sum\pm\sqrt{t+i}$ for
  $i=1,\ldots,n$.
"""
function Hecke.swinnerton_dyer(V::Vector, x::Generic.Poly{<:Generic.Rat})
  n = length(V)
  @assert characteristic(parent(x)) == 0 || characteristic(parent(x)) > length(V)
  S = base_ring(x)
  T = gen(S)
  X = gen(parent(x))
  l = [(X^2 + T + i) for i = V]
  l = [ vcat([2*one(S)], polynomial_to_power_sums(x, 2^n)) for x = l]
  while n > 1
    i = 1
    while 2*i <= n
      l[i] = [sum(binomial(fmpz(h), fmpz(j))*l[2*i-1][j+1]*l[2*i][h-j+1] for j=0:h) for h=0:length(l[1])-1]
      i += 1
    end
    if isodd(n)
      l[i] = l[n]
      n = i
    else
      n = i-1
    end
  end
  f = power_sums_to_polynomial(l[1][2:end], parent(x))
  if x == gen(parent(x))
    return f
  else
    return f(x)
  end
end

function Hecke.swinnerton_dyer(n::Int, x::Generic.Poly{<:Generic.Rat})
  return swinnerton_dyer(x, collect(1:n))
end

end


function run_intcode(program, x, y)::Int
    program[2] = x
    program[3] = y
    pointer = 1
    while pointer <= length(program)
        opcode = program[pointer]
        if opcode == 1
            pt1 = program[pointer+1]+1
            pt2 = program[pointer+2]+1
            pt3 = program[pointer+3]+1
            program[pt3] = program[pt1] + program[pt2]
        elseif opcode == 2
            pt1 = program[pointer+1]+1
            pt2 = program[pointer+2]+1
            pt3 = program[pointer+3]+1
            program[pt3] = program[pt1] * program[pt2]
        elseif opcode == 99
            break
        else
            throw(ErrorException("Invalid opcode"))
        end
        pointer = pointer+4
    end

    return program[1]
end

program = open("intcode.csv") do f
    parse.(Int, split(readlines(f)[1], ","))
end
println(run_intcode(deepcopy(program), 12, 2))
for i in 0:99
    for j in 0:99
        res = try
            run_intcode(deepcopy(program), i, j)
        catch e
            -1
        end
        if res == 19690720
            println(100*i+j)
            break
        end
    end
end


"""
    enum_vertices(bp::BilevelProblem, poly_lib)

Computes the extreme points of the dual polyhedron described
in `bp` using the `poly_lib` polyhedral solver (compliant with `Polyhedra`).
"""
enum_vertices(bp::BilevelProblem, poly_lib) = enum_vertices(bp.B, bp.H, bp.d, poly_lib)

"""
    enum_vertices(B, H, d, poly_lib)

Computes the extreme points of the dual polyhedron described
by `(B, H, d)` using the `poly_lib` polyhedral solver.
Returns a tuple `(V, F, k)` where:
- `V` is the vector of sub-problem results, each containing a vector of extremum points `(α, β)`
- `F` is a boolean indicating feasibility
- `k` is the first encountered infeasible sub-problem if relevant, 0 otherwise
"""
function enum_vertices(B, H, d, poly_lib)
    (ml, nl) = size(B)
    (mu, _) = size(H)
    m = JuMP.Model()
    # vertices contains for each sub-problem k a Vector of exremal points (α, β)
    vertices = Vector{Vector{Tuple{Vector{Float64}, Float64}}}(undef, mu)
    for k in 1:mu
        @variable(m, α[i = 1:ml] >= 0)
        @variable(m, β >= 0)
        @constraint(m, B' * α + β .* d .>= H[k,:])
        poly = Polyhedra.polyhedron(m, poly_lib)
        vk = Polyhedra.points(Polyhedra.vrep(poly))
        if isempty(vk) # no vertices == infeasible
            return (vertices, false, k)
        end
        vertices[k] = [(Float64.(v[1:ml]), Float64(v[end])) for v in vk]
    end
    return (vertices, true, 0)
end

function dual_polyhedron(B, d, H, k::Integer, xvar, vvar, A, G, q, b, δ::Real;
                         poly_lib::Polyhedra.Library, optimizer)
    x = JuMP.value.(xvar)
    v = JuMP.value.(vvar)
    nl = length(d)
    ml = size(B, 1)
    mu = size(H, 1)
    m = Model(optimizer)
    @variable(m, α[i=1:ml] >= 0)
    @variable(m, β >= 0)
    @constraint(m,
        cons[j=1:nl],
        sum(B[i,j] * α[i] for i in 1:ml) + d[j] * β ≥ H[k,j]
    )
    res = b - A * x
    @objective(m, Min, dot(α, res) + β * (dot(d, v) + δ))
    optimize!(m)
    # infeasible => no extreme point, the whole problem is infeasible
    if termination_status(m) == MOI.INFEASIBLE || termination_status(m) == MOI.INFEASIBLE_OR_UNBOUNDED
        return (:INFEASIBLE, nothing)
    end
    if JuMP.objective_value(m) <= q[k] - dot(G[k,:], x)
        return (:OPTIMAL, nothing)
    end
    points = Polyhedra.points(Polyhedra.vrep(Polyhedra.polyhedron(m, poly_lib)))
    vertices = map(points) do pvec
        alpha_vec = pvec[1:end-1]
        beta = pvec[end]
        (Float64.(alpha_vec), Float64(beta))
    end
    return (:VERTICES, vertices)
end


"""
    LNAInput

This is a structure which holds the inputs needed for computing the Linear Noise Approximation (LNA). This structure will hold the stochastic system as provided by the user; uniquely defined through kinetic parameters, the rates of the system and the stoichiometry matrix.

# Arguments
- `params::AbstractArray{Float64,1}`: The rate parameters of the stochastic model.
- `S::AbstractArray{Float64,2}`: the stochiometry matrix of the system. Size: number of reactions x number of species.
- `reaction_rate_function::Function,`: This is a function f(x, parameters) which should return an array of the reaction rates of the system, i.e. S*f would describe the ODE representation of the system.
- `volume::Float64`: The volume of the reactants of the system.
"""
struct LNAInput
    params::AbstractArray{Float64,1}
    S::AbstractArray{Float64,2}
    reaction_rate_function::Function
    volume::Float64
end

"""
    LNA

This is a structure which will hold the LNA: the mean of the trajectories and the covariance between the species.

# Arguments
- `traj_means`: A (number of species) x (number of time points) array which holds the mean trajectory for each species on each row of the array.
- `traj_covars`: An array which holds the covariance matrix of the species at each time point.
- `time_points`: The timepoints the system was solved for.
"""
struct LNA
    traj_means::AbstractArray{Float64,2}
    traj_covars::AbstractArray{AbstractArray{Float64,2},1}
    time_points::AbstractArray{Float64,1}
end

"""
    compute_LNA(input::LNAInput,
        x0::Tuple{AbstractArray{Float64,1},AbstractArray{Float64,2}},
        Tspan::Tuple{Float64,Float64},
        saveat::Float64,
        solver::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=RK4();
        kwargs...)

The function computes the linear noise approximation to system through construction of two ODEs: one describing the trajectories of the mean of the LNA and the other describing the change the covariance between the variables. These outputs are held in a LNA structure.

# Arguments
- `x0::Tuple{AbstractArray{Float64,2},AbstractArray{Float64,2}}`: The initial conditions of the system. In the form of (the initial conditions of the species, the initial covariance matrix of the system).
- `Tspan::Tuple{Float64,Float64}`: The start and end times of the simulation.
- `saveat::Float64`: The number of time points the use wishes to solve the system for.
- `solver::DEAlgorithm`: The ODE solver the user wishes to use, for example `DifferentialEquations.RK4()`.

#Returns
- LNA
"""
function compute_LNA(input::LNAInput,
    x0::Tuple{AbstractArray{Float64,1},AbstractArray{Float64,2}},
    Tspan::Tuple{Float64,Float64},
    saveat::Float64,
    solver::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=RK4();
    kwargs...)

    if input.volume <= 0.0
        throw(ArgumentError("To use LNA please provide a positive volume of the
            reactants"))
    end

    no_of_species, no_of_reactions = size(input.S)

    ###  the below ODE solves the ODE of the mean and the ODE of the covariance together.
    ## this function is the mean variance decomposition of the LNA. The derviation of which can be
    ## found in "Approximation  and  inference  methods  forstochastic  biochemical  kinetics—a  tutorial  review" (2017) by
    ## David  Schnoerr et al.
    ## x is two matrices concatenated on top of each other: a diagonal matrix with the means on the diagonal follwed by the covariance matrix
    function Mean_ODE(dx, x, pars, t)
        mean_vec = diag(x)
        covar_mx = x[no_of_species+1:no_of_species*2,:]
        reaction_rates = input.reaction_rate_function(mean_vec, pars)
        reaction_rates_jac = ForwardDiff.jacobian(y -> input.reaction_rate_function(y, pars), mean_vec)
        A = input.S * reaction_rates_jac
        dx[1:no_of_species, :] .= diagm(0 => input.S*reaction_rates)
        dx[no_of_species + 1:end, :] .= A*covar_mx + covar_mx*A' + input.S * diagm(0 => reaction_rates) * input.S' ./ sqrt(input.volume)
    end

    prob = ODEProblem(Mean_ODE, vcat(diagm(0=>x0[1]), x0[2]), Tspan, input.params)
    mean_and_var = solve(prob, solver, saveat=saveat; kwargs...)

    mean_traj = Array{Float64,2}(undef, no_of_species, length(mean_and_var.t))
    covar_traj = Array{Array{Float64,2},1}(undef, length(mean_and_var.t))

    for j in 1:length(mean_and_var.t)
        mean_traj[:,j] = diag(mean_and_var[j][1:no_of_species,1:no_of_species])
        covar_traj[j] = mean_and_var[j][no_of_species+1:end, 1:no_of_species]
    end

    return LNA(mean_traj, covar_traj, mean_and_var.t)
end

"""
    sample_LNA_trajectories(lna::LNA, n_samples::Int64)

A function which samples from the LNA to output sampled trajectories. The LNA gives the mean of the tracjectories and the covariance between them; hence a single trajectory can be sampled from a Multivariate Normal distribution. The user can also sample more than one trajectory; which are then averaged.

# Arguments
- `lna::LNA`: LNA stucture.
- `n_samples::Int64`: The number of sampled tracjectories to be sampled and then averaged.

#Returns
-  A (number of species) x (number of time points) array which holds the averaged trajectory for each species on each row of the array.
"""
function sample_LNA_trajectories(lna::LNA, n_samples::Int64)
    trajectories = [rand(MvNormal(lna.traj_means[:,j], lna.traj_covars[j]), n_samples) for j in 1:length(lna.time_points)]
    return hcat([mean(traj,dims=2) for traj in trajectories]...)
end

"""
    get_LNA_trajectories(input::LNAInput, n_samples::Int64,
        x0::Tuple{AbstractArray{Float64,1},AbstractArray{Float64,2}},
        Tspan::Tuple{Float64,Float64},
        saveat::Float64,
        solver::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=RK4();
        kwargs...)

A function which computes the LNA and then samples from the it to output sampled trajectories. The user can also sample more than one trajectory; which are then averaged.

# Arguments
- `input::LNAInput`: LNAInput stucture.
- `n_samples::Int64`: The number of sampled tracjectories to be sampled and then averaged.
- `x0::Tuple{AbstractArray{Float64,2},AbstractArray{Float64,2}}`: The initial conditions of the system. In the form of (the initial conditions of the species, the initial covariance matrix of the system).
- `Tspan::Tuple{Float64,Float64}`: The start and end times of the simulation.
- `saveat::Float64`: The number of time points the use wishes to solve the system for.
- `solver::DEAlgorithm`: The ODE solver the user wishes to use, for example DifferentialEquations.RK4() .

#Returns
-  A (number of species) x (number of time points) array which holds the averaged trajectory for each species on each row of the array.
"""
function get_LNA_trajectories(input::LNAInput, n_samples::Int64,
    x0::Tuple{AbstractArray{Float64,1},AbstractArray{Float64,2}},
    Tspan::Tuple{Float64,Float64},
    saveat::Float64,
    solver::OrdinaryDiffEq.OrdinaryDiffEqAlgorithm=RK4();
    kwargs...)

    lna = compute_LNA(input, x0, Tspan, saveat, solver; kwargs...)
    return sample_LNA_trajectories(lna, n_samples)
end

    #this function is the mean variance decomposition of the LNAS
    ##LNA_Mean_Var takes the inputs and then performs the simulating of the mean (ODE solution) and the covariance matrix which changes over time.
    ##x0 is the initial conditions of the DE and the covariance matrix
    ##S is the stochiometry matrix
    ##reaction_rate_function is a function of the rates - MUST be in the same order as the stochiometry matrix is laid out
    ##volume is the volume of reactants
    #
    #
    #
    #the maths for this can be found in UserManual_v28102013.pdf in the LNA folder in Gaussian Procceses Drop box, p2 equation (7) - the ODE of the covariance. A volume term has been added which can be seen in other sources - the particular source was linked as the layout is the similar to the one implemented.


include("../utils.jl")

@testset "Group wrapped in ValidationManifold" begin
    G = SpecialOrthogonal(3)
    M = Rotations(3)
    AG = ValidationManifold(G)
    @test is_group_manifold(AG)

    eg = Matrix{Float64}(I, 3, 3)
    ω = [[1.0, 2.0, 3.0], [3.0, 2.0, 1.0]]
    p, q = [exp(M, eg, hat(M, eg, ωi)) for ωi in ω]
    X = hat(M, eg, [-1.0, 2.0, 0.5])

    e = Identity(G)
    p2, q2 = ValidationMPoint(p), ValidationMPoint(q)
    @test q2 === Manifolds.array_point(q2) # test that double wraps are avoided.
    X2 = ValidationTVector(X)

    @test Identity(G) isa Identity

    eg = allocate(p)
    identity_element!(G, eg)
    eg2 = allocate(p2)
    identity_element!(AG, eg2)
    @test eg2.value == eg
    eg = allocate(p2)

    @test inv(AG, p2) isa ValidationMPoint
    @test isapprox(G, inv(AG, p2).value, inv(G, p))
    @test inv(AG, e) isa ValidationMPoint{<:Identity}
    @test inv(AG, Identity(AG)) == inv(AG, e)

    pinvq = allocate(p2)
    inv!(AG, pinvq, p2)
    @test isapprox(G, pinvq.value, inv(G, p))
    eg = allocate(p2)
    inv!(AG, eg, e)
    @test isapprox(G, eg.value, Manifolds.array_value(e))

    @test compose(AG, p2, q2) isa ValidationMPoint
    @test isapprox(G, compose(AG, p2, q2).value, compose(G, p, q))
    @test compose(AG, p2, e) === p2
    @test compose(AG, e, p2) === p2

    pq = allocate(p2)
    compose!(AG, pq, p2, q2)
    @test isapprox(G, pq.value, compose(G, p, q))
    pq = allocate(p2)
    compose!(AG, pq, e, q2)
    @test isapprox(G, pq.value, compose(G, e, q))
    pq = allocate(p2)
    compose!(AG, pq, p2, e)
    @test isapprox(G, pq.value, compose(G, p, e))

    @test exp_lie(AG, X2) isa ValidationMPoint
    @test isapprox(G, exp_lie(AG, X2).value, exp_lie(G, X))
    expX = allocate(p2)
    exp_lie!(AG, expX, X2)
    @test isapprox(G, expX.value, exp_lie(G, X))

    @test log_lie(AG, p2) isa ValidationTVector
    @test isapprox(G, e, log_lie(AG, p2).value, log_lie(G, p))
    logp = allocate(X2)
    log_lie!(AG, logp, p2)
    @test isapprox(G, e, logp.value, log_lie(G, p))

    @test lie_bracket(AG, X, X2) isa ValidationTVector
    Xlb = allocate(X2)
    lie_bracket!(AG, Xlb, X, X2)
    @test isapprox(G, e, Xlb.value, lie_bracket(G, X, X2.value))

    @test adjoint_action(AG, p, X) isa ValidationTVector
    Xaa = allocate(X2)
    adjoint_action!(AG, Xaa, p, X2)
    @test isapprox(G, e, Xaa.value, adjoint_action(G, p, X2.value))

    for conv in (LeftAction(), RightAction())
        @test translate(AG, p2, q2, conv) isa ValidationMPoint
        @test isapprox(G, translate(AG, p2, q2, conv).value, translate(G, p, q, conv))

        pq = allocate(p2)
        translate!(AG, pq, p2, q2, conv)
        @test isapprox(G, pq.value, translate(G, p, q, conv))

        @test inverse_translate(AG, p2, q2, conv) isa ValidationMPoint
        @test isapprox(
            G,
            inverse_translate(AG, p2, q2, conv).value,
            inverse_translate(G, p, q, conv),
        )

        pinvq = allocate(p2)
        inverse_translate!(AG, pinvq, p2, q2, conv)
        @test isapprox(G, pinvq.value, inverse_translate(G, p, q, conv))
    end

    for conv in (LeftAction(), RightAction())
        @test translate_diff(AG, q2, p2, X2, conv; atol=1e-10) isa ValidationTVector
        @test isapprox(
            G,
            translate_diff(AG, q2, p2, X2, conv; atol=1e-10).value,
            translate_diff(G, q, p, X, conv),
        )

        Y = allocate(X2)
        translate_diff!(AG, Y, q2, p2, X2, conv; atol=1e-10)
        @test isapprox(Y.value, translate_diff(G, q, p, X, conv))

        @test inverse_translate_diff(AG, q2, p2, X2, conv; atol=1e-10) isa ValidationTVector
        @test isapprox(
            G,
            inverse_translate_diff(AG, q2, p2, X2, conv; atol=1e-10).value,
            inverse_translate_diff(G, q, p, X, conv),
        )

        Y = allocate(X2)
        inverse_translate_diff!(AG, Y, q2, p2, X2, conv; atol=1e-10)
        @test isapprox(Y.value, inverse_translate_diff(G, q, p, X, conv))
    end
end


# using DataStructures
# using Graphs
# using Statistics
# using Combinatorics

# lines = readlines("inputs/11-15/11ex.txt")
lines = readlines("inputs/11-15/11.txt")
nums = [[parse(Int, x) for x in line] for line in lines]

# 2D list
getat = function(x,y, M, default=-1)
  h = length(M)
  w = length(M[1])
  if x < 1 || y < 1 || x > h || y > w
    return default
  end
  return M[x][y]
end

incv! = function(x,y, M)
  h = length(M)
  w = length(M[1])
  if x < 1 || y < 1 || x > h || y > w
    return M
  end
  M[x][y] += 1
  return M
end

setat! = function(x,y,M,b)
  h = length(M)
  w = length(M[1])
  if x < 1 || y < 1 || x > h || y > w
    return M
  end
  M[x][y] = b
  return M
end

function day11()
  t = 0
  p2 = 0
  for step in 1:500
    # inc all by 1
    for r in 1:10
      for c in 1:10
        nums[r][c] += 1
      end
    end
    flashed = [[false for _ in 1:10] for _ in 1:10]
    for _ in 1:100
      for r in 1:10
        for c in 1:10
          if getat(r,c,nums) > 9
            for dr in [-1,0,1], dc in [-1,0,1]
              incv!(r+dr,c+dc,nums)
            end
            t += 1
            setat!(r,c,flashed,true)
          end
        end
      end

      for r in 1:10
        for c in 1:10
          if flashed[r][c]
            nums[r][c] = 0
          end
        end
      end
    end
    allf = true
    for r in 1:10
      for c in 1:10
        if !flashed[r][c]
          allf = false
        end
      end
    end
    if allf
      p2 = step
      return step
      break
    end
  end
end

function octopus_step!(model)
  # increase agent energys
  for agent in allagents(model)
    agent.energy += 1
  end
  while true
    update = false
    for agent in allagents(model)
      if !agent.flashed && agent.energy > 9
        update = true
        agent.flashed = true
        for neighbor in nearby_agents(agent, model)
          neighbor.energy += 1
        end
      end
    end
    for agent in allagents(model)
      agent.flashed = false
    end
    if !update
      break
    end
  end
  for agent in allagents(model)
    if agent.flashed
      agent.flashed = false
      agent.energy = 0
    end
  end
end


function part1(input)
    depths = parse.(Int, readlines(input))
    return count(>(0), diff(depths))
end

function part2(input)
    depths = parse.(Int, readlines(input))
    filtered_depths = depths[1:(end - 2)] + depths[2:(end - 1)] + depths[3:end]
    return count(>(0), diff(filtered_depths))
end


using ReachabilityAnalysis
using ExponentialUtilities
using Plots
using SparseArrays
using ReachabilityAnalysis: dim

_homogeneize(p::SecondOrderAffineContinuousSystem) = _homogeneize(p.M, p.K, p.b)

function _homogeneize(M, K, F)
    m = size(K, 1)
    Zm = spzeros(m, m)
    Im = sparse(1.0I, m, m)
    M = Diagonal(M)
    invM = inv(M)

    u0 = invM * F
    Aext = [Zm           Im      spzeros(m) ;
           -invM*K       Zm         u0      ;
            spzeros(1, 2m+1)                ]

    S = @system(x' = Aext*x) # sistema homogeneizado
end

function _reach_homog_krylov_LGG09_modif!(out, Ω₀::LazySet, Aᵀδ::AbstractMatrix,
                                          ℓ::AbstractVector, NSTEPS;
                                          hermitian=false, m=min(30, size(Aᵀδ, 1)), tol=1e-7)

    @assert size(out, 1) == 2
    @assert size(out, 2) == NSTEPS

    # initialization of the krylov subspace
    # esta parte genera una base de
    # \{Aᵀδ * ℓ, (Aᵀδ)^2 * ℓ, (Aᵀδ)^3 * ℓ, ..., (Aᵀδ)^m * ℓ}
    TA, Tb = eltype(Aᵀδ), eltype(ℓ)
    T = promote_type(TA, Tb)
    Ks = KrylovSubspace{T, real(T)}(length(ℓ), m)
    arnoldi!(Ks, Aᵀδ, ℓ; m=m, ishermitian=hermitian, tol=tol)

    # rᵢ stores is the cache for each vector: (Φᵀ)^i ℓ
    rᵢ = deepcopy(ℓ)

    @inbounds for i in 1:NSTEPS
        out[1, i] = ρ(rᵢ, Ω₀)
        out[2, i] = ρ(-rᵢ, Ω₀)
        expv!(rᵢ, i*1.0, Ks) # rᵢ <- exp(A^T δ * i) * ℓ
    end
    return out
end

# ------------------------------------------------------------------------
# Auxiliary methods that are not yet available in ReachabilityAnalysis.jl
# ------------------------------------------------------------------------

# calcula: out <- exp(A * NSTEPS * dt) * b
function _expv(A, b, NSTEPS, dt; hermitian=false, m=min(30, size(A, 1)), tol=1e-7)

    # initialization of the krylov subspace
    TA, Tb = eltype(A), eltype(b)
    T = promote_type(TA, Tb)
    Ks = KrylovSubspace{T, real(T)}(length(b), m)
    arnoldi!(Ks, A, b; m=m, ishermitian=hermitian, tol=tol)

    out = similar(b)
    expv!(out, NSTEPS*dt, Ks)

    return out
end

# calcula: out <- exp(A * NSTEPS * dt) * b
function _phiv(A, b, NSTEPS, dt; hermitian=false, m=min(30, size(A, 1)), tol=1e-7)

    # initialization of the krylov subspace
    TA, Tb = eltype(A), eltype(b)
    T = promote_type(TA, Tb)
    Ks = KrylovSubspace{T, real(T)}(length(b), m)
    arnoldi!(Ks, A, b; m=m, ishermitian=hermitian, tol=tol)

    out = Matrix{Float64}(undef, size(A, 1), 3)
    phiv!(out, NSTEPS*dt, Ks, 2)

    return view(out, :, 3) .* dt^2
end

#
# E₊ = ⊡(Φ₂ ⊡(A^2 X₀))
#
function _build_Eplus(A, X0, δt; m=min(30, size(A, 1)), tol=1e-7, Φ₂can=nothing)
    if isa(X0, Vector)
        X0 = Singleton(X0)
    end
    n = dim(X0)

    # build box E+
    H = symmetric_interval_hull(A^2 * X0)
    E₊_high = Vector{Float64}(undef, n)

    if isnothing(Φ₂can)
        Φ₂can = Φ₂_canonical(A, δt, m=m, tol=tol)
    end

    for i in 1:n
        Φ₂col = view(Φ₂can, :, i)
        E₊_high[i] = max(ρ(Φ₂col, H), abs(ρ(-Φ₂col, H)))
    end

    E₊_low = -E₊_high # since it is a SIH
    E₊ = Hyperrectangle(low=E₊_low, high=E₊_high)
end

function Φ₂_canonical(A, δt; m=min(size(A, 1)), tol=1e-7)
    n = size(A, 1)
    P3n = ReachabilityAnalysis._P_3n(abs.(A), δt, n)
    P3n_tr = copy(transpose(P3n))

    Φ₂can = Matrix{Float64}(undef, n, n)
    b = zeros(3n)
    for i in 1:n
        b[i] = 1.0
        out = _expv(P3n_tr, b, 1, 1.0, m=m, tol=tol)
        Φ₂can[:, i] .= view(out, 2n+1:3n)
        b[i] = 0.0
    end
    return Φ₂can
end

function _support_function_Eplus_baseline(d, A, δ, V)
    P = ReachabilityAnalysis.Φ₂(abs.(A), δ)
    E = symmetric_interval_hull(P * V)
    return ρ(d, E)
end

function _support_function_Eplus_krylov(d, A::AbstractMatrix{N}, δ,
                           V::AbstractHyperrectangle;
                           cutoff=eps(N), mk=min(30, size(A, 1)), tol=1e-7) where {N}

    # use only the indices in which V is not flat
    idx = findall(ri -> abs(ri) > cutoff, radius_hyperrectangle(V))
    Aabsᵀ = copy(transpose(abs.(A))) # sparse

    out = zero(N)
    n = size(A, 1)
    ei = zeros(N, n)
    @inbounds for i in idx
        ei[i] = one(N)

        R1 = phiv(δ, Aabsᵀ, ei, 2, m=mk, tol=tol) .* δ^2
        α = ρ(view(R1, :, 3), V)

        R2 = phiv(δ, Aabsᵀ, -ei, 2, m=mk, tol=tol) .* δ^2
        β = ρ(view(R2, :, 3), V)

        out += abs(d[i]) * max(α, abs(β))
        ei[i] = zero(N)
    end
    return out
end

function _support_function_Eplus_krylov_optim(d::AbstractVector{N}, A::AbstractMatrix{N}, δ,
                                              V::AbstractHyperrectangle;
                                              cutoff=eps(N), mk=min(30, size(A, 1)), tol=1e-7) where {N}

    # use only the indices in which V is not flat
    idx = findall(ri -> abs(ri) > cutoff, radius_hyperrectangle(V))
    Aabs = copy(abs.(A)) # sparse

    v = V.radius[idx]
    Maux = phiv(δ, Aabs, v, 2, m=mk, tol=tol)
    Pv = view(Maux, :, 3) .* δ^2 # rescale

    return dot(abs.(d), Pv)
end

function _build_Eplus_optim(A::SparseMatrixCSC{N, D}, X0::AbstractVector{N}, δt; m=min(30, size(A, 1)), tol=1e-7, cutoff=eps(N)) where {N, D}
    _build_Eplus_optim(A, Singleton(X0), δt; m=m, tol=tol, cutoff=cutoff)
end

# optimized version of the constructor of the Eplus set using a single Krylov subspace evaluation
# uses the idea in _support_function_Eplus_krylov_optim
function _build_Eplus_optim(A::SparseMatrixCSC{N, D}, X0::AbstractSingleton{N}, δt; m=min(30, size(A, 1)), tol=1e-7, cutoff=eps(N)) where {N, D}
    n = dim(X0)

    # compute V
    AX0 = linear_map(A, X0)
    V = symmetric_interval_hull(A * AX0)
    v = V.radius

    # compute Pv using Krylov
    Aabs = copy(abs.(A))
    Pv = _phiv(Aabs, v, 1, δt; m=m, tol=tol)

    # return the symmetric interval hull whose radius is Pv
    E⁺ = Hyperrectangle(zeros(n), Pv)

    return E⁺
end

function _build_Eplus_optim(A::SparseMatrixCSC{N, D}, X0::Hyperrectangle{N}, δt; m=min(30, size(A, 1)), tol=1e-7, cutoff=eps(N)) where {N, D}
    n = dim(X0)
    A2 = A * A; # fast if A sparse
    V = symmetric_interval_hull(A2 * X0)
    v = V.radius
    Aabs = copy(abs.(A))
    Pv = _phiv(Aabs, v, 1, δt; m=m, tol=tol)
    E⁺ = Hyperrectangle(zeros(n), Pv)
end


# ------------------------------------------------------------------
# Licensed under the MIT License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    boundingbox(geometry)

Axis-aligned bounding box of the `geometry`.
"""
boundingbox(geometry) = boundingbox(vertices(geometry))

# fallback implementation treats geometry as a set of points
function boundingbox(points::AbstractVector{Point{Dim,T}}) where {Dim,T}
  xmin = MVector(ntuple(i->typemax(T), Dim))
  xmax = MVector(ntuple(i->typemin(T), Dim))
  for p in points
    x = coordinates(p)
    @. xmin = min(x, xmin)
    @. xmax = max(x, xmax)
  end
  Box(Point(xmin), Point(xmax))
end

# -----------
# PRIMITIVES
# -----------

boundingbox(p::Primitive) = boundingbox(boundary(p))

boundingbox(b::Box) = b

function boundingbox(s::Sphere{Dim,T}) where {Dim,T}
  c = center(s)
  r = radius(s)
  r⃗ = Vec(ntuple(i->r, Dim))
  Box(c - r⃗, c + r⃗)
end


@concrete struct SimpleOrbit <: AbstractOrbit
    period
    t0
    b
    duration
    speed
    half_period
    ref_time
end

"""
    SimpleOrbit(; period, duration, t0=0, b=0.0)

Circular orbit parameterized by the basic observables of a transiting system.

# Parameters
* `period` - The orbital period of the planets, nominally in days
* `duration` - The duration of the transit, similar units as `period`.
* `t0` - The midpoint time of the reference transit, similar units as `period`
* `b` - The impact parameter of the orbit, unitless
"""
function SimpleOrbit(;period, duration, t0=zero(period), b=0)
    half_period = 0.5 * period
    duration > half_period && error("duration cannot be longer than half the period")
    speed = 2.0 * sqrt(1.0 - b^2) / duration
    ref_time =  t0 - half_period
    SimpleOrbit(period, t0, b, duration, speed, half_period, ref_time)
end

period(orbit::SimpleOrbit) = orbit.period
duration(orbit::SimpleOrbit) = orbit.duration

relative_time(orbit::SimpleOrbit, t) = mod(t - orbit.ref_time, period(orbit)) - orbit.half_period

function relative_position(orbit::SimpleOrbit, t)
    Δt = relative_time(orbit, t)
    x = orbit.speed * Δt
    y = orbit.b
    z = abs(Δt) < 0.5 * duration(orbit) ? one(x) : -one(x)
    return SA[x, y, z]
end

# TODO: if texp, ϵ += 0.5 * texp
#function in_transit(orbit::SimpleOrbit, t)
#    Δt = relative_time.(orbit, t)
#    ϵ = 0.5 * duration(orbit)
#    return findall(x -> abs(x) < ϵ, Δt)
#end
#function in_transit(orbit::SimpleOrbit, t, r)
#    Δt = relative_time.(orbit, t)
#    ϵ = √((1.0 + r)^2.0 - orbit.b^2.0) / orbit.speed
#    return findall(x -> abs(x) < ϵ, Δt)
#end

function flip(orbit::SimpleOrbit, ror)
    t0 = orbit.t0 + orbit.half_period
    b = orbit.b / ror
    speed = orbit.speed / ror
    ref_time = orbit.t0
    return SimpleOrbit(period(orbit), t0, b, duration(orbit), speed, orbit.half_period, ref_time)
end

function Base.show(io::IO, orbit::SimpleOrbit)
    T = duration(orbit)
    P = period(orbit)
    b = orbit.b
    t0 = orbit.t0
    print(io, "SimpleOrbit(P=$P, T=$T, t0=$t0, b=$b)")
end

function Base.show(io::IO, ::MIME"text/plain", orbit::SimpleOrbit)
    T = duration(orbit)
    P = period(orbit)
    b = orbit.b
    t0 = orbit.t0
    print(io,
        "SimpleOrbit\n period: ", P,
        "\n duration: ", T,
        "\n t0: ", t0,
        "\n b: ", b
    )
end


"""
    daughter = mother(this::CWT{W, T, <:ContWaveClass, N},
                        s::Real, nInOctave::Int, ω::AbstractArray{<:Real,1}) where {W, T, N}

given a CWT object, return a rescaled version of the mother wavelet, in the
fourier domain. ω is the frequency, which is fftshift-ed. s is the scale
variable.
"""
function mother(this::CWT{W,T,Morlet,N}, s::Real, sWidth::Real,
                  ω::AbstractArray{<:Real,1}) where {W,T,N}
    constant = this.σ[3] * (π)^(1 / 4)
    gauss = exp.(-(this.σ[1] .- ω / s).^2 / (sWidth^2))
    shift = this.σ[2] * exp.(-1 / 2 * (ω / s).^2)
    daughter = constant .* (gauss .- shift)
    return normalize(daughter, s, this.p)
end

function mother(this::CWT{W,T,<:Paul,N}, s::Real, sWidth::Real,
                  ω::AbstractArray{<:Real,1}) where {W,T,N}
    daughter = zeros(length(ω))
    constant = (2^this.α) / sqrt((this.α) * gamma(2 * (this.α)))
    polynomial = (ω[ω .>= 0] / s).^(this.α)
    expDecay = exp.(-(ω[ω .>= 0] / s))
    daughter[ω .>= 0] = constant .* polynomial .* expDecay
    return normalize(daughter, s, this.p)
end

function mother(this::CWT{W,T,<:Dog,N}, s::Real, sWidth::Real,
                  ω::AbstractArray{<:Real,1}) where {W,T,N}
    constant = im^(this.α) / sqrt(gamma((this.α) + 1 / 2))
    polynomial = (ω / s).^(this.α)
    gauss = exp.(-(ω / s).^2 / 2)
    daughter =  constant .* polynomial .* gauss
    return normalize(daughter, s, this.p)
end

function mother(this::CWT{W,T,Morse,N}, s::Real, sWidth::Real,
                  ω::AbstractArray{<:Real,1}) where {W,T,N}

    ga = this.waveType.ga;
    be = this.waveType.be;
    cf = this.waveType.cf;
    p = this.p;

    fo = morsefreq(this) * 2 * pi;
    fact = cf/fo;

    om = (ω / s);

    if be == 0
        daughter = 2 * exp.(-om.^ga);
    else
        daughter = 2 * exp.(-be.*log.(fo) .+ fo.^ga .+ be.*log.(om) .- om.^ga);
    end

    daughter[1] = 1/2*daughter[1]; # Due to unit step function
    # Ensure nice lowpass filters for beta=0;
    # Otherwise, doesn't matter since wavelets vanishes at zero frequency

    if any(daughter .!= daughter)
        @warn "the given values of gamma and beta are numerically unstable"
        daughter[daughter .!= daughter] .= 0;
    end

    return ContinuousWavelets.normalize(daughter, s, p)
end

function mother(this::CWT{W,T,<:ContOrtho,N}, s::Real, itpψ,
                ω::AbstractArray{<:Real,1}, n, n1) where {W,T,N}
    daughter = itpψ(range(1, stop=length(itpψ), step=length(itpψ) / n1 * s))
    daughter = padTo(daughter, n)
    daughter = circshift(daughter, -round(Int, n1 / s / 2))
    return daughter
end
function normalize(daughter, s, p)
    if p == Inf
        normTerm = maximum(abs.(daughter))
    else
        normTerm = s^(1 / p)
    end
    return daughter ./ normTerm
end

"""
    father(c::CWT, ω, averagingType::aT) where {aT<:Averaging}

this creates the averaging function, which covers the low frequency
information, and is emphatically not analytic. aT determines whether it has the
same form as the wavelets (`Father`), or just a bandpass `Dirac`.
`c.averagingLength` gives the number of octaves (base 2) that are covered by
the averaging function. The width is then derived so that it matches the next
wavelet at 1σ.

For the Morlet wavelet, the distribution is just a Gaussian, so it has variance
1/s^2 and mean σ[1]*s set the variance so that the averaging function has σ/2 at
the central frequency of the last scale.

For the Paul wavelets, it's a easy calculation to see that the mean of a paul
wavelet of order m is (m+1)/s, while σ=sqrt(m+1)/s. So we set the variance so
that the averaging function has 1σ at the central frequency of the last scale.

the derivative of a Gaussian (Dog) has a pretty nasty form for the mean and
variance; eventually, if you set 1/2 * σ_{averaging}=⟨ω⟩_{highest scale wavelet}, you
will get the scale of the averaging function to be
`s*gamma((c.α+2)/2)/sqrt(gamma((c.α+1)/2)*(gamma((c.α+3)/2)-gamma((c.α+2)/2)))`

"""
function father(c::CWT, ω, averagingType::Father, sWidth)
    s = 2^(min(c.averagingLength + getMinScaling(c) - 1, 0))
    s0, ω_shift = locationShift(c, s, ω, sWidth)
    averaging = adjust(c) .* mother(c, s0, 1, ω_shift)
end

function father(c::CWT{B,T,W}, ω, averagingType::Father,
                       fullVersion, s, N, n1) where {W <: ContOrtho,B,T}
    itp = genInterp(fullVersion)

    φ = itp(range(1, stop=length(fullVersion),
                  step=length(fullVersion) / n1 * s))
    φ = circshift(padTo(φ, N), -round(Int, n1 / s / 2))
end

# dirac version (that is, just a window around zero)
function father(c::CWT{<:WaveletBoundary,T}, ω,
                       averagingType::Dirac, sWidth) where {T}
    s = 2^(c.averagingLength) * sWidth
    averaging = zeros(T, size(ω))
    upperBound = getUpperBound(c, s)
    averaging[abs.(ω) .<= upperBound] .= 1
    return averaging
end

function father(c::CWT{W, T, <:Morse}, ω, averagingType::ContinuousWavelets.Father, sWidth) where {W,T}
    s = 2^(min(c.averagingLength + getMinScaling(c) - 1, 0))
    s0, ω_shift = locationShift(c, s, ω, sWidth)
    averaging = adjust(c) .* mother(c, s0, sWidth, ω_shift)
    return averaging
end

@doc """
      (daughters, ω) = computeWavelets(n1::Integer, c::CWT{W}; T=Float64, J1::Int64=-1, dt::S=NaN, s0::V=NaN) where {S<:Real,
                                                                   W<:WaveletBoundary, V}
just precomputes the wavelets used by transform c::CWT{W}. For details, see cwt
"""
function computeWavelets(n1::Integer, c::CWT{B,CT,W}; T=Float64, J1::Int64=-1, dt::S=NaN, s0::V=NaN, space=false) where {S <: Real,B <: WaveletBoundary,V,W,CT}
    # don't alter scaling with sampling information if it doesn't exists
    fλ = (4 * π) / (c.σ[1] + sqrt(2 + c.σ[1]^2))
    if isnan(dt) || (dt < 0)
        dt = 1
    end
    # smallest resolvable scale
    if isnan(s0) || (s0 < 0)
        s0 = 2 * dt / fλ
    end

    nOctaves, totalWavelets, sRange, sWidth = getNWavelets(n1, c)
    # padding determines the actual number of elements
    n, nSpace = setn(n1, c)
    # indicates whether we should keep a spot for the father wavelet
    isAve = !(typeof(c.averagingType) <: NoAve)
    # I guess matlab did occasionally do something useful

    ω = computeOmega(n1, nSpace, n)
    daughters = analyticOrNot(c, n, totalWavelets)

    # if the nOctaves is small enough there are none not covered by the
    # averaging, just use that
    if totalWavelets == 1
        @warn "an averaging length of $(c.averagingLength) results in fewer octaves than the minimum frequency $(getMinScaling(c)) for this wavelet type. Either decreasing the averagingLength or increase extraOctaves"
        onlyFather = father(c, ω, c.averagingType, sWidth[1])
        return onlyFather, ω
    end

    for (curWave, s) in enumerate(sRange)
        daughters[:,curWave + isAve] = mother(c, s, sWidth[curWave], ω)
    end
    if isAve # should we include the father?
        @debug "c = $(c), $(c.averagingType), size(ω)= $(size(ω))"
        daughters[:, 1] = father(c, ω, c.averagingType, sWidth[1])# [1:(n1+1)]
    end

    # adjust by the frame bound
    if c.frameBound > 0
        daughters = daughters .* (c.frameBound / norm(daughters, 2))
    end
    testFourierDomainProperties(daughters, isAve)
    if space
        x = zeros(T, n1); x[ceil(Int, n1 / 2)] = 1
        return cwt(x, c, daughters), ω
    else
        return (daughters, ω)
    end
end

function computeWavelets(n1::Integer, c::CWT{B,CT,W}; T=Float64, space=false) where {S <: Real,B <: WaveletBoundary,V,W <: ContOrtho,CT}

    # padding determines the actual number of elements
    n, nSpace = setn(n1, c)
    nOctaves, totalWavelets, sRange, sWidth = getNWavelets(n1, c)
    # indicates whether we should keep a spot for the father wavelet
    isAve = !(typeof(c.averagingType) <: NoAve)
    # I guess matlab did occasionally do something useful

    ω = computeOmega(n1, nSpace, n)
    daughters = zeros(nSpace, totalWavelets)
    φ, ψ, ψLen = getContWaveFromOrtho(c, nSpace)
    itpψ = genInterp(ψ)
    # if the nOctaves is small enough there are none not covered by the
    # averaging, so just use that
    if totalWavelets == 1
        onlyFather = father(c, ω, c.averagingType, φ, c.averagingLength, nSpace, n1)
        return rfft(onlyFather, 1), ω
    end

    for (curWave, s) in enumerate(sRange)
        daughters[:,curWave + isAve] = mother(c, s, itpψ, ω, nSpace, n1)
    end
    if isAve
        daughters[:, 1] = father(c, ω, c.averagingType, φ, sRange[1],
                                        nSpace, n1)
    end
    # switch to fourier domain and normalize appropriately
    daughters = rfft(daughters, 1)
    repeatSRange = [sRange[1:isAve]...; sRange...]
    for i in 1:size(daughters, 2)
        daughters[:, i] = normalize(daughters[:, i],
                                            repeatSRange[i],
                                            c.p)
    end

    # adjust by the frame bound
    if c.frameBound > 0
        daughters = daughters .* (c.frameBound / norm(daughters, 2))
    end
    testFourierDomainProperties(daughters, isAve)
    if space
        x = zeros(T, n1); x[ceil(Int, n1 / 2)] = 1
        return cwt(x, c, daughters), ω
    else
        return (daughters, ω)
    end
end

# not
function analyticOrNot(c::CWT{W,T,<:Union{Dog,ContOrtho},N}, n, totalWavelets) where {W,T,N}
    # Odd derivatives (and any orthogonal wavelets) introduce an imaginary
    # term, so we need a complex representation
    if abs(c.α % 2) == 1
        daughters = zeros(Complex{T}, n, totalWavelets)
    else
        daughters = zeros(T, n, totalWavelets)
    end
    return daughters
end

function analyticOrNot(c::CWT{W,T,<:Union{Morlet,Paul,Morse},N}, n, totalWavelets) where {W,T,N}
    daughters = zeros(T, n, totalWavelets)
    return daughters
end

function computeOmega(nOriginal, nSpace, nFreq)
    range(0, nOriginal >> 1 + 1, length=nFreq) # max size is the last frequency in the rfft of the original data size
end
# convenience methods

# it's ok to just hand the total size, even if we're not transforming across
# all dimensions
function computeWavelets(Y::Tuple, c::CWT{W}; T=Float64) where {S <: Real,W <: WaveletBoundary}
    return computeWavelets(Y[1], c; T=T)
end
function computeWavelets(Y::AbstractArray{<:Integer}, c::CWT{W}; T=Float64) where {S <: Real,W <: WaveletBoundary}
    return computeWavelets(Y[1], c, T=T)
end

# also ok to just hand the whole thing being transformed
function computeWavelets(Y::AbstractArray{<:Number}, c::CWT{W}; T=Float64) where {S <: Real,W <: WaveletBoundary}
    return computeWavelets(size(Y)[1], c, T=T)
end