# This file is a part of Julia. License is MIT: https://julialang.org/license

# Methods operating on different special matrix types

# Interconversion between special matrix types

# conversions from Diagonal to other special matrix types
Bidiagonal(A::Diagonal) = Bidiagonal(A.diag, fill!(similar(A.diag, length(A.diag)-1), 0), :U)
SymTridiagonal(A::Diagonal) = SymTridiagonal(A.diag, fill!(similar(A.diag, length(A.diag)-1), 0))
Tridiagonal(A::Diagonal) = Tridiagonal(fill!(similar(A.diag, length(A.diag)-1), 0), A.diag,
                                       fill!(similar(A.diag, length(A.diag)-1), 0))

# conversions from Bidiagonal to other special matrix types
Diagonal(A::Bidiagonal) =
    iszero(A.ev) ? Diagonal(A.dv) :
        throw(ArgumentError("matrix cannot be represented as Diagonal"))
SymTridiagonal(A::Bidiagonal) =
    iszero(A.ev) ? SymTridiagonal(A.dv, A.ev) :
        throw(ArgumentError("matrix cannot be represented as SymTridiagonal"))
Tridiagonal(A::Bidiagonal) =
    Tridiagonal(A.uplo == 'U' ? fill!(similar(A.ev), 0) : A.ev, A.dv,
                A.uplo == 'U' ? A.ev : fill!(similar(A.ev), 0))

# conversions from SymTridiagonal to other special matrix types
Diagonal(A::SymTridiagonal) =
    iszero(A.ev) ? Diagonal(A.dv) :
        throw(ArgumentError("matrix cannot be represented as Diagonal"))
Bidiagonal(A::SymTridiagonal) =
    iszero(A.ev) ? Bidiagonal(A.dv, A.ev, :U) :
        throw(ArgumentError("matrix cannot be represented as Bidiagonal"))
Tridiagonal(A::SymTridiagonal) =
    Tridiagonal(copy(A.ev), A.dv, A.ev)

# conversions from Tridiagonal to other special matrix types
Diagonal(A::Tridiagonal) =
    iszero(A.dl) && iszero(A.du) ? Diagonal(A.d) :
        throw(ArgumentError("matrix cannot be represented as Diagonal"))
Bidiagonal(A::Tridiagonal) =
    iszero(A.dl) ? Bidiagonal(A.d, A.du, :U) :
    iszero(A.du) ? Bidiagonal(A.d, A.dl, :L) :
        throw(ArgumentError("matrix cannot be represented as Bidiagonal"))
SymTridiagonal(A::Tridiagonal) =
    A.dl == A.du ? SymTridiagonal(A.d, A.dl) :
        throw(ArgumentError("matrix cannot be represented as SymTridiagonal"))

# conversions from AbstractTriangular to special matrix types
Diagonal(A::AbstractTriangular) =
    isdiag(A) ? Diagonal(diag(A)) :
        throw(ArgumentError("matrix cannot be represented as Diagonal"))
Bidiagonal(A::AbstractTriangular) =
    isbanded(A, 0, 1) ? Bidiagonal(diag(A, 0), diag(A,  1), :U) : # is upper bidiagonal
    isbanded(A, -1, 0) ? Bidiagonal(diag(A, 0), diag(A, -1), :L) : # is lower bidiagonal
        throw(ArgumentError("matrix cannot be represented as Bidiagonal"))
SymTridiagonal(A::AbstractTriangular) = SymTridiagonal(Tridiagonal(A))
Tridiagonal(A::AbstractTriangular) =
    isbanded(A, -1, 1) ? Tridiagonal(diag(A, -1), diag(A, 0), diag(A, 1)) : # is tridiagonal
        throw(ArgumentError("matrix cannot be represented as Tridiagonal"))

const ConvertibleSpecialMatrix = Union{Diagonal,Bidiagonal,SymTridiagonal,Tridiagonal,AbstractTriangular}

convert(T::Type{<:Diagonal},       m::ConvertibleSpecialMatrix) = m isa T ? m : T(m)
convert(T::Type{<:SymTridiagonal}, m::ConvertibleSpecialMatrix) = m isa T ? m : T(m)
convert(T::Type{<:Tridiagonal},    m::ConvertibleSpecialMatrix) = m isa T ? m : T(m)

convert(T::Type{<:LowerTriangular}, m::Union{LowerTriangular,UnitLowerTriangular}) = m isa T ? m : T(m)
convert(T::Type{<:UpperTriangular}, m::Union{UpperTriangular,UnitUpperTriangular}) = m isa T ? m : T(m)

# Constructs two method definitions taking into account (assumed) commutativity
# e.g. @commutative f(x::S, y::T) where {S,T} = x+y is the same is defining
#     f(x::S, y::T) where {S,T} = x+y
#     f(y::T, x::S) where {S,T} = f(x, y)
macro commutative(myexpr)
    @assert myexpr.head===:(=) || myexpr.head===:function # Make sure it is a function definition
    y = copy(myexpr.args[1].args[2:end])
    reverse!(y)
    reversed_call = Expr(:(=), Expr(:call,myexpr.args[1].args[1],y...), myexpr.args[1])
    esc(Expr(:block, myexpr, reversed_call))
end

for op in (:+, :-)
    SpecialMatrices = [:Diagonal, :Bidiagonal, :Tridiagonal, :Matrix]
    for (idx, matrixtype1) in enumerate(SpecialMatrices) # matrixtype1 is the sparser matrix type
        for matrixtype2 in SpecialMatrices[idx+1:end] # matrixtype2 is the denser matrix type
            @eval begin # TODO quite a few of these conversions are NOT defined
                ($op)(A::($matrixtype1), B::($matrixtype2)) = ($op)(convert(($matrixtype2), A), B)
                ($op)(A::($matrixtype2), B::($matrixtype1)) = ($op)(A, convert(($matrixtype2), B))
            end
        end
    end

    for  matrixtype1 in (:SymTridiagonal,)                      # matrixtype1 is the sparser matrix type
        for matrixtype2 in (:Tridiagonal, :Matrix) # matrixtype2 is the denser matrix type
            @eval begin
                ($op)(A::($matrixtype1), B::($matrixtype2)) = ($op)(convert(($matrixtype2), A), B)
                ($op)(A::($matrixtype2), B::($matrixtype1)) = ($op)(A, convert(($matrixtype2), B))
            end
        end
    end

    for matrixtype1 in (:Diagonal, :Bidiagonal) # matrixtype1 is the sparser matrix type
        for matrixtype2 in (:SymTridiagonal,)   # matrixtype2 is the denser matrix type
            @eval begin
                ($op)(A::($matrixtype1), B::($matrixtype2)) = ($op)(convert(($matrixtype2), A), B)
                ($op)(A::($matrixtype2), B::($matrixtype1)) = ($op)(A, convert(($matrixtype2), B))
            end
        end
    end

    for matrixtype1 in (:Diagonal,)
        for (matrixtype2,matrixtype3) in ((:UpperTriangular,:UpperTriangular),
                                          (:UnitUpperTriangular,:UpperTriangular),
                                          (:LowerTriangular,:LowerTriangular),
                                          (:UnitLowerTriangular,:LowerTriangular))
            @eval begin
                ($op)(A::($matrixtype1), B::($matrixtype2)) = ($op)(($matrixtype3)(A), B)
                ($op)(A::($matrixtype2), B::($matrixtype1)) = ($op)(A, ($matrixtype3)(B))
            end
        end
    end
    for matrixtype in (:SymTridiagonal,:Tridiagonal,:Bidiagonal,:Matrix)
        @eval begin
            ($op)(A::AbstractTriangular, B::($matrixtype)) = ($op)(copyto!(similar(parent(A)), A), B)
            ($op)(A::($matrixtype), B::AbstractTriangular) = ($op)(A, copyto!(similar(parent(B)), B))
        end
    end
end

rmul!(A::AbstractTriangular, adjB::Adjoint{<:Any,<:Union{QRCompactWYQ,QRPackedQ}}) =
    (B = adjB.parent; rmul!(full!(A), adjoint(B)))
*(A::AbstractTriangular, adjB::Adjoint{<:Any,<:Union{QRCompactWYQ,QRPackedQ}}) =
    (B = adjB.parent; *(copyto!(similar(parent(A)), A), adjoint(B)))

# fill[stored]! methods
fillstored!(A::Diagonal, x) = (fill!(A.diag, x); A)
fillstored!(A::Bidiagonal, x) = (fill!(A.dv, x); fill!(A.ev, x); A)
fillstored!(A::Tridiagonal, x) = (fill!(A.dl, x); fill!(A.d, x); fill!(A.du, x); A)
fillstored!(A::SymTridiagonal, x) = (fill!(A.dv, x); fill!(A.ev, x); A)

_small_enough(A::Union{Diagonal, Bidiagonal}) = size(A, 1) <= 1
_small_enough(A::Tridiagonal) = size(A, 1) <= 2
_small_enough(A::SymTridiagonal) = size(A, 1) <= 2

function fill!(A::Union{Diagonal,Bidiagonal,Tridiagonal,SymTridiagonal}, x)
    xT = convert(eltype(A), x)
    (iszero(xT) || _small_enough(A)) && return fillstored!(A, xT)
    throw(ArgumentError("array of type $(typeof(A)) and size $(size(A)) can
    not be filled with $x, since some of its entries are constrained."))
end

# equals and approx equals methods for structured matrices
# SymTridiagonal == Tridiagonal is already defined in tridiag.jl

# SymTridiagonal and Bidiagonal have the same field names
==(A::Diagonal, B::Union{SymTridiagonal, Bidiagonal}) = iszero(B.ev) && A.diag == B.dv
==(B::Bidiagonal, A::Diagonal) = A == B

==(A::Diagonal, B::Tridiagonal) = iszero(B.dl) && iszero(B.du) && A.diag == B.d
==(B::Tridiagonal, A::Diagonal) = A == B

function ==(A::Bidiagonal, B::Tridiagonal)
    if A.uplo == 'U'
        return iszero(B.dl) && A.dv == B.d && A.ev == B.du
    else
        return iszero(B.du) && A.dv == B.d && A.ev == B.dl
    end
end
==(B::Tridiagonal, A::Bidiagonal) = A == B

==(A::Bidiagonal, B::SymTridiagonal) = iszero(B.ev) && iszero(A.ev) && A.dv == B.dv
==(B::SymTridiagonal, A::Bidiagonal) = A == B


import Yota: ungetindex, ungetindex!

@testset "helpers: getindex" begin
    x = rand(4)
    dx = zero(x)
    dy = 1.0
    @test ungetindex!(dx, x, dy, 2) == [0.0, 1, 0, 0]
    @test ungetindex(x, dy, 2) == [0.0, 1, 0, 0]

    x = rand(4)
    dx = zero(x)
    dy = [1, 1, 1]
    @test ungetindex!(dx, x, dy, [1, 3, 1]) == [2.0, 0, 1, 0]
    @test ungetindex(x, dy, [1, 3, 1]) == [2.0, 0, 1, 0]

    x = rand(4, 5)
    dx = zero(x)
    dy = ones(4, 3)
    expected = [2 0 1 0 0;
                2 0 1 0 0;
                2 0 1 0 0;
                2 0 1 0 0.0]
    @test ungetindex!(dx, x, dy, :, [1, 3, 1]) == expected
    @test ungetindex(x, dy, :, [1, 3, 1]) == expected

    ## additional smoke tests

    x = rand(3, 4)
    I = (1:2, 2:3)
    y = x[I...]
    dy = ones(size(y))
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    x = rand(3, 4)
    I = (1, 2:3)
    y = x[I...]
    dy = ones(size(y))
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    x = rand(3, 4)
    I = (1, :)
    y = x[I...]
    dy = ones(size(y))
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    x = rand(3, 4)
    I = (1, [1, 3])
    y = x[I...]
    dy = ones(size(y))
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    # single-element Cartesian index
    x = rand(3, 4)
    I = (1, 2)
    y = x[I...]
    dy = 1.0
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    # single-element linear index
    x = rand(3, 4)
    I = (5,)
    y = x[I...]
    dy = 1.0
    dx = zero(x)
    ungetindex!(dx, x, dy, I...)

    # CUDA
    if CUDA.functional()
        x = rand(4) |> cu
        dx = zero(x) |> cu
        dy = 1.0f0
        @test ungetindex!(dx, x, dy, 2) == cu([0.0, 1, 0, 0])
        @test ungetindex(x, dy, 2) == cu([0.0, 1, 0, 0])

        x = rand(4, 5) |> cu
        dx = zero(x) |> cu
        dy = ones(4, 3) |> cu
        @test ungetindex!(dx, x, dy, :, [1, 3, 1]) == cu(expected)
        @test ungetindex(x, dy, :, [1, 3, 1]) == cu(expected)
    end

end


"""
    basic_osm_graph_stub(weight_type=:distance, graph_type=:static)

The graph for the network below is returned, with all ways being two way except for 2004.
The diagram is approximately geospatially correct.

```ascii
                   1001─┐
                     │  └─┐100km/h, way=2002, dual carriageway
    50km/h, way=2001 │    └─┐
                     │      │
                   1002    1006
                     │      │
    50km/h, way=2001 │      │ 100km/h, way=2002, dual carriageway
                     │      │
                   1003    1007─────────1008   (1008 to 1007 is way 2004, 50km/h and one way)
                     │      │
    50km/h, way=2001 │    ┌─┘
                     │  ┌─┘100km/h, way=2002, dual carriageway
                   1004─┘
                     │
    50km/h, way=2003 │
                     │
                   1005
```
"""
function basic_osm_graph_stub(weight_type=:distance, graph_type=:static)
    # Nodes
    lats = [-38.0751637, -38.0752637, -38.0753637, -38.0754637, -38.0755637, -38.0752637, -38.0753637, -38.0753637]
    lons = [145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3326838, 145.3327838, 145.3327838, 145.3328838]
    node_ids = [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]
    nodes = Dict(id => Node(id, GeoLocation(lat, lon), Dict{String, Any}()) for (lat, lon, id) in zip(lats, lons, node_ids))

    # Ways
    way_ids = [2001, 2002, 2003, 2004]
    way_nodes = [
        [1001, 1002, 1003, 1004],
        [1001, 1006, 1007, 1004],
        [1004, 1005],
        [1008, 1007],
    ]
    tag_dicts = [
        Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50),  "lanes" => Int8(2)),
        Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(100), "lanes" => Int8(4)),
        Dict{String, Any}("oneway" => false, "reverseway" => false, "maxspeed" => Int16(50),  "lanes" => Int8(2)),
        Dict{String, Any}("oneway" => true,  "reverseway" => false, "maxspeed" => Int16(50),  "lanes" => Int8(1)),
    ]
    ways = Dict(way_id => Way(way_id, nodes, tag_dict) for (way_id, nodes, tag_dict) in zip(way_ids, way_nodes, tag_dicts))

    restriction1 = Restriction(
        3001,
        "via_node",
        Dict{String, Any}("restriction"=>"no_right_turn","type"=>"restriction"),
        2002,
        2001,
        1004,
        nothing,
        true,
        false
    )
    restrictions = Dict(restriction1.id => restriction1)
    U = LightOSM.DEFAULT_OSM_INDEX_TYPE
    T = LightOSM.DEFAULT_OSM_ID_TYPE
    W = LightOSM.DEFAULT_OSM_EDGE_WEIGHT_TYPE
    g = OSMGraph{U,T,W}(nodes=nodes, highways=ways, restrictions=restrictions)
    LightOSM.add_node_and_edge_mappings!(g)
    LightOSM.add_weights!(g, weight_type)
    LightOSM.add_graph!(g, graph_type)
    LightOSM.add_node_tags!(g)
    LightOSM.add_indexed_restrictions!(g)
    g.dijkstra_states = Vector{Vector{U}}(undef, length(g.nodes))
    LightOSM.add_kdtree!(g)
    return g
end


# A test case for wide lines and mitering at joints

@block Contributors ["GLMakie backend tests"] begin
    @cell "Miter Joints for line rendering" [lines] begin
        scene = Scene()

        r = 4
        sep = 4*r
        scatter!(scene, (sep+2*r)*[-1,-1,1,1], (sep+2*r)*[-1,1,-1,1])

        for i=-1:1
            for j=-1:1
                angle = pi/2 + pi/4*i
                x = r*[-cos(angle/2),0,-cos(angle/2)]
                y = r*[-sin(angle/2),0,sin(angle/2)]

                linewidth = 40 * 2.0^j
                lines!(scene, x .+ sep*i, y .+ sep*j, color=RGBAf0(0,0,0,0.5), linewidth=linewidth)
                lines!(scene, x .+ sep*i, y .+ sep*j, color=:red)
            end
        end
        scene
    end
end


using Test
using NQCDynamics
using NQCDynamics: Calculators

@testset "fill_density!" begin
    @testset "SingleState" for state ∈ (1, 2)
        electronics = SingleState(state)
        density = zeros(2,2)
        NonadiabaticDistributions.fill_density!(density, electronics)
        @test density[state, state] == 1
    end

    @testset "ElectronicPopulation" begin
        population = [0.5, 0.5]
        electronics = ElectronicPopulation(population)
        density = zeros(2, 2)
        NonadiabaticDistributions.fill_density!(density, electronics)
        @test density[1, 1] == density[2, 2] == 0.5
    end
end

@testset "initialise_density_matrix" begin
    calc = Calculators.Calculator(DoubleWell(), 1, Float64)
    electronics = SingleState(1)
    density = NonadiabaticDistributions.initialise_density_matrix(electronics, calc)
    @test density == [1 0; 0 0]
end

rs = (fill(0.0, 1, 1), fill(0.0, 1, 1, 1))
calculators = (
    Calculators.Calculator(DoubleWell(), 1, Float64),
    Calculators.Calculator(DoubleWell(), 1, 1, Float64),
    )
@testset "transform_density!" for (r, calc) ∈ zip(rs, calculators)
    density = [1.0 0; 0 0]
    NonadiabaticDistributions.transform_density!(density, calc, r, :to_adiabatic)
    @test density ≈ [0.5 0.5; 0.5 0.5]
    NonadiabaticDistributions.transform_density!(density, calc, r, :to_diabatic)
    @test density ≈ [1.0 0; 0 0]
    @test_throws ArgumentError NonadiabaticDistributions.transform_density!(density, calc, r, :blah)
end

@testset "initialise_adiabatic_density_matrix" for (r, calc) ∈ zip(rs, calculators)
    @testset "Adiabatic" begin
        electronics = SingleState(1, Adiabatic())
        density = NonadiabaticDistributions.initialise_adiabatic_density_matrix(electronics, calc, r)
        @test density[1,1] == 1
    end

    @testset "Diabatic" begin
        electronics = SingleState(1, Diabatic())
        density = NonadiabaticDistributions.initialise_adiabatic_density_matrix(electronics, calc, r)
        @test all(density .≈ 0.5)
    end
end


abstract type AbstractObjectDetection{T <: Real} end

@enum DetectorType HUMAN MACHINE

struct ObjectDetectionCore
    class_name::String
    confidence::Union{Float32, Nothing}
    image_width::Union{Int, Nothing}
    image_height::Union{Int, Nothing}
    detector_type::DetectorType
    detector_name::String
end

function ObjectDetectionCore(class_name::String, detector_type::DetectorType, detector_name::String)
    ObjectDetectionCore(class_name, nothing, nothing, nothing, detector_type, detector_name)
end

function ObjectDetectionCore(class_name::String, confidence::Float32, detector_type::DetectorType, detector_name::String)
    ObjectDetectionCore(class_name, confidence, nothing, nothing, detector_type, detector_name)
end

class_name(detection::AbstractObjectDetection) = detection.core.class_name
confidence(detection::AbstractObjectDetection) = detection.core.confidence
image_width(detection::AbstractObjectDetection) = detection.core.image_width
image_height(detection::AbstractObjectDetection) = detection.core.image_height
detector_type(detection::AbstractObjectDetection) = detection.core.detector_type
detector_name(detection::AbstractObjectDetection) = detection.core.detector_name

function centroid(detection::AbstractObjectDetection{T}) :: Point2{T} where T
    error("No implementation for $(typeof(detection))")
end

function bounding_box_detection(detection::AbstractObjectDetection{T}) :: BoundingBoxDetection{T} where T
    error("No implementation for $(typeof(detection))")
end

function bounding_box(detection::AbstractObjectDetection{T}) :: Rect2{T} where T
    return bounding_box_detection(detection).rect
end

function iou(d1::AbstractObjectDetection{T}, d2::AbstractObjectDetection{T}) where T
    bb1 = bounding_box(d1)
    bb2 = bounding_box(d2)
    left_1, top_1 = bb1.origin; right_1, bottom_1 = bb1.origin + bb1.widths
    left_2, top_2 = bb2.origin; right_2, bottom_2 = bb2.origin + bb2.widths

    if bottom_1 <= top_2 || bottom_2 <= top_1 || right_1 <= left_2 || right_2 <= left_1
        return 0.0
    end

    area_1 = (bottom_1 - top_1) * (right_1 - left_1)
    area_2 = (bottom_2 - top_2) * (right_2 - left_2)

    a = min(right_1, right_2)
    b = max(top_1, top_2)
    c = max(left_1, left_2)
    d = min(bottom_1, bottom_2)
    intersection = (d - b) * (a - c)
    union = area_1 + area_2 - intersection

    return intersection / union
end


@testset "Basic tests" begin
    optimizer = POI.Optimizer(GLPK.Optimizer())

    MOI.set(optimizer, MOI.Silent(), true)

    x = MOI.add_variables(optimizer, 2)
    y, cy = MOI.add_constrained_variable(optimizer, POI.Parameter(0))
    z = MOI.VariableIndex(4)
    cz = MOI.ConstraintIndex{MOI.SingleVariable,POI.Parameter}(4)

    for x_i in x
        MOI.add_constraint(
            optimizer,
            MOI.SingleVariable(x_i),
            MOI.GreaterThan(0.0),
        )
    end

    @test_throws ErrorException("Cannot constrain a parameter") MOI.add_constraint(
        optimizer,
        MOI.SingleVariable(y),
        MOI.EqualTo(0.0),
    )

    @test_throws ErrorException("Variable not in the model") MOI.add_constraint(
        optimizer,
        MOI.SingleVariable(z),
        MOI.GreaterThan(0.0),
    )

    cons1 = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([1.0, 1.0], [x[1], y]),
        0.0,
    )

    c1 = MOI.add_constraint(optimizer, cons1, MOI.EqualTo(2.0))

    obj_func = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([1.0, 1.0], [x[1], y]),
        0.0,
    )
    MOI.set(
        optimizer,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        obj_func,
    )
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)

    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.ObjectiveValue()) == 2

    @test MOI.get(optimizer, MOI.VariablePrimal(), x[1]) == 2

    @test_throws ErrorException("Variable not in the model") MOI.get(
        optimizer,
        MOI.VariablePrimal(),
        z,
    )

    MOI.set(optimizer, MOI.ConstraintSet(), cy, POI.Parameter(1.0))

    @test_throws ErrorException("Parameter not in the model") MOI.set(
        optimizer,
        MOI.ConstraintSet(),
        cz,
        POI.Parameter(1.0),
    )

    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.ObjectiveValue()) == 2

    @test MOI.get(optimizer, MOI.VariablePrimal(), x[1]) == 1

    new_obj_func = MOI.ScalarAffineFunction(
        MOI.ScalarAffineTerm.([1.0, 1.0], [x[1], x[2]]),
        0.0,
    )
    MOI.set(
        optimizer,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        new_obj_func,
    )
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MIN_SENSE)

    MOI.optimize!(optimizer)

    @test MOI.get(optimizer, MOI.ObjectiveValue()) == 1

    @test MOI.supports(optimizer, MOI.VariableName(), MOI.VariableIndex)
    @test MOI.supports(optimizer, MOI.ConstraintName(), MOI.ConstraintIndex)
    @test MOI.get(optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE
    @test MOI.get(optimizer, MOI.VariableName(), x[1]) == ""
    @test MOI.get(optimizer, MOI.ConstraintName(), c1) == ""
end

@testset "Special cases of getters" begin
    ipopt = Ipopt.Optimizer()
    MOI.set(ipopt, MOI.RawParameter("print_level"), 0)
    opt_in = MOIU.CachingOptimizer(MOIU.Model{Float64}(), ipopt)
    optimizer = POI.Optimizer(opt_in)

    A = [2.0 1.0; 1.0 2.0]
    a = [1.0, 1.0]
    c = [2.0, 1.0]

    x = MOI.add_variables(optimizer, 2)

    for x_i in x
        MOI.add_constraint(
            optimizer,
            MOI.SingleVariable(x_i),
            MOI.GreaterThan(0.0),
        )
    end

    y, cy = MOI.add_constrained_variable(optimizer, POI.Parameter(0))

    quad_terms = MOI.ScalarQuadraticTerm{Float64}[]

    push!(quad_terms, MOI.ScalarQuadraticTerm(A[1, 1], x[1], y))
    push!(quad_terms, MOI.ScalarQuadraticTerm(A[1, 2], x[1], y))
    push!(quad_terms, MOI.ScalarQuadraticTerm(A[2, 2], x[2], y))

    constraint_function = MOI.ScalarQuadraticFunction(
        MOI.ScalarAffineTerm.(a, [x[1], y]),
        quad_terms,
        0.0,
    )

    cons_index =
        MOI.add_constraint(optimizer, constraint_function, MOI.LessThan(25.0))

    obj_func = MOI.ScalarQuadraticFunction(
        MOI.ScalarAffineTerm.(c, [x[1], x[2]]),
        [MOI.ScalarQuadraticTerm(A[2, 2], x[2], y)],
        0.0,
    )
    MOI.set(
        optimizer,
        MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
        obj_func,
    )
    MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)

    @test MOI.get(optimizer, MOI.ObjectiveFunctionType()) ==
          MOI.ScalarQuadraticFunction{Float64}
    @test MOI.get(optimizer, MOI.NumberOfVariables()) == 3
end


using BERL
using YAML
using Test
using Cambrian
using CartesianGeneticProgramming
using NeuroEvolution
using UUIDs
cd("..")

"Tests an environment with CGP"
function test_env(env_name::String; env_params...)
    if env_name == "gym"
        name = string("Gym_", env_params[:gym_env])
    elseif env_name == "atari_ram"
        name = string("Atari_", env_params[:atari_game])
    else
        name = env_name
    end
    @testset "$name" begin
        @test env_name in keys(environments)
        cfg_path = "./src/algorithms/CGP/$env_name.yaml"
        cfg = CartesianGeneticProgramming.get_config(cfg_path)
        cfg["n_population"]=10
        cfg["n_gen"]=10
        for (k, v) in pairs(env_params)
            cfg[string(k)] = v
        end

        # Create and setup the environment
        env::BERLenv = environments[env_name](cfg)
        @test env.name != ""

        ind = CGPInd(cfg)
        f = fitness(ind, env)

        if env_name in ["iris"]
            @test 0 <= f[1] <= 1
        end
        @test typeof(f)==Array{Float64, 1}

        new_gen!(env, cfg)
    end
end

"Tests an algo on XOR"
function test_algo(algo_name::String)
    @testset "$algo_name" begin
        cfg_path = "./src/algorithms/$algo_name/xor.yaml"

        if algo_name in ["NEAT", "HyperNEAT", "CPPN"]
            cfg = NeuroEvolution.get_config(cfg_path)
        else
            cfg = CartesianGeneticProgramming.get_config(cfg_path)
        end
        cfg["n_gen"]=10
        cfg["n_population"]=10

        # Create and setup the environment
        env::BERLenv = environments["xor"](cfg)

        # Fitness function
        fit::Function = i::Cambrian.Individual -> fitness(i, env)
        cfg["id"] = "TestAlgo_$(algo_name)_$(env.name)_$(string(UUIDs.uuid4()))"
        evo = algorithms[algo_name](cfg, fit)
        @test typeof(evo)==Cambrian.Evolution
        @test length(evo.population)>0
    end
end

function test_berl()
    # Algorithms
    algs = []
    for (a, v) in pairs(YAML.load_file("./run_config/algorithms.yaml"))
        if a != "runs"
            test_algo(a)
        end
    end

    # Environments
    envs=[]
    for (e, v) in pairs(YAML.load_file("./run_config/environments.yaml"))
        if e in gym_envs
            d = test_env("gym"; gym_env=e)
        elseif e in atari_envs
            d = test_env("atari_ram"; atari_game=e)
        else
            d = test_env(e)
        end
    end

    # Start BERL
    @testset "Start BERL" begin
        m = start_berl("NEAT", "iris")
        @test typeof(m)==Dict{Any,Any}
        @test m["Best fitness"]>0
        @test m["Run time"]>0
        @test m["Memory allocated"]>0
        print_metrics(m)
    end
end

println("Starting tests")

# test_berl()

run_berl()

@testset "plot" begin
    BERLplot("CGP_XOR_f7739035-ba96-4400-b9bf-0492f6ec1da0")
    BERLplot(["CGP_XOR_f7739035-ba96-4400-b9bf-0492f6ec1da0",
    "CGP_XOR_ea455af9-9423-452c-8946-f67bf39a7da4"]; name="test.svg")
end


type IIDMInput <: AbstractInput end

## Read inputs
function read_input(intput_type::T, instance_path::String) where T<:Type{IIDMInput}
  data = parse_file(instance_path)

  ## Cllecting bus information
  bus = Dict{String, Dict{String, Any}}()
  ind_bus = 1
  for substation in root(data)["substation"]
    substationname = attribute(substation, "id")
    busind = ind_bus
    ind_bus += 1

    voltagelvl = substation["voltageLevel"][1]
    length(substation["voltageLevel"])==1 || warn("Substation $substationname has several voltageLevel, accounting for $(attribute(voltagelvl, "id")) only")

    busname = attribute(voltagelvl, "id")
    length(voltagelvl["busBreakerTopology"][1]["bus"]) == 1 || warn("Substation has several buses, accounting for $busname only..")
    basekV = parse(attribute(voltagelvl, "nominalV"))
    minV = parse(attribute(voltagelvl, "lowVoltageLimit"))
    maxV = parse(attribute(voltagelvl, "highVoltageLimit"))

    busBreakerTopology = voltagelvl["busBreakerTopology"][1]["bus"][1]
    V, θ = parse(attribute(busBreakerTopology, "v")), parse(attribute(busBreakerTopology, "angle"))*π/180

    bus[busname] = Dict{String, Any}()
    bus[busname]["Volt"] = IIDMVolt(busname, busind, substationname, basekV, minV, maxV)
    bus[busname]["MeasureVolt"] = IIDMVoltMeasure(V*exp(im*θ), NaN)

    for cur_load in voltagelvl["load"]
      load_id = attribute(cur_load, "id")
      load0 = parse(attribute(cur_load, "p0")) + im*parse(attribute(cur_load, "q0"))
      load = parse(attribute(cur_load, "p")) + im*parse(attribute(cur_load, "q"))

      abs(load-load0) < 1e-5 || warn("$busname $load_id : load - load0 = $(load-load0)")

      bus[busname]["LOAD_$load_id"] = IIDMLoad(load_id, load)
    end

    for cur_gen in voltagelvl["generator"]
      gen_id = attribute(cur_gen, "id")
      minP = parse(attribute(cur_gen, "minP"))
      maxP = parse(attribute(cur_gen, "maxP"))
      targetV = parse(attribute(cur_gen, "targetV"))
      volt_regulator_on = parse(attribute(cur_gen, "voltageRegulatorOn"))

      attribute(cur_gen["property"][1], "value") == "FUEL" || warn("generator $gen_id of $busname not of expected type FUEL.")
      capabilityCurve = cur_gen["reactiveCapabilityCurve"][1]
      minP = parse(attribute(pt, "p"))
      minQminP, maxQminP = parse(attribute(pt, "minQ")), parse(attribute(pt, "maxQ"))
      maxP = parse(attribute(pt, "p"))
      minQmaxP, maxQmaxP = parse(attribute(pt, "minQ")), parse(attribute(pt, "maxQ"))
      S_bounds = [(minP+im*minQminP, minP+im*maxQminP), (maxP+im*minQmaxP, maxP+im*maxQmaxP)]

      bus[busname]["GEN_$gen_id"] = IIDMGenerator(gen_id, minP, maxP, targetV, volt_regulator_on, S_bounds)
    end
  end

  ## Collecting link information
  link = Dict{Link, Dict{String, Any}}()
  for line in root(data)["line"]
    orig = attribute(line, "voltageLevelId1")
    dest = attribute(line, "voltageLevelId2")
    linkname = Link(orig, dest)
    lineid = attribute(line, "id")
    r = parse(Float64, attribute(line, "r"))
    x = parse(Float64, attribute(line, "x"))
    G1 = parse(Float64, attribute(line, "g1")) + im*parse(Float64, attribute(line, "b1"))
    G2 = parse(Float64, attribute(line, "g2")) + im*parse(Float64, attribute(line, "b2"))
    S1 = parse(Float64, attribute(line, "p1")) + im*parse(Float64, attribute(line, "q1"))
    S2 = parse(Float64, attribute(line, "p2")) + im*parse(Float64, attribute(line, "q2"))
    basekV_orig = bus["$orig"]["Volt"].nomV
    basekV_dest = bus["$dest"]["Volt"].nomV
    lim = parse(Float64, attribute(line["currentLimits1"][1], "permanentLimit"))

    if !haskey(link, linkname)
      link[linkname] = Dict{String, Any}()
    end
    link[linkname][lineid] = IIDMLine_π(r, x, G1, G2, basekV_orig, basekV_dest, lim)
    link[linkname]["Measure_$lineid"] = IIDMLine_πMeasure(S1, S2)
  end

  ds = DataSource(bus, link)

  ## Building GridStructure
  node_linksin = node_linksout = Dict{String, Set{Link}}()
  node_vars = Dict{String, Dict{String, Variable}}()
  link_vars = Dict{Link, Dict{String, Variable}}()
  gs = GridStructure("BaseCase", node_linksin, node_linksout)

  node_formulations = Dict{String, Dict{Tuple{Type, String}, Symbol}}()
  link_formulations = Dict{Link, Dict{Tuple{Type, String}, Symbol}}()
  mp = MathematicalProgramming(node_formulations, link_formulations, node_vars, link_vars)
  return OPFProblems("BaseCase"=>Scenario(ds, gs, mp))
end


function create_param_dual_cum_sum(model::ParametricOptimizer)
    return zeros(model.number_of_parameters_in_model)
end

function calculate_dual_of_parameters(model::ParametricOptimizer)
    param_dual_cum_sum = create_param_dual_cum_sum(model)
    update_duals_with_affine_constraint_cache!(param_dual_cum_sum, model)
    update_duals_with_quadratic_constraint_cache!(param_dual_cum_sum, model)
    update_duals_in_affine_objective!(param_dual_cum_sum, model.affine_objective_cache)
    update_duals_in_quadratic_objective!(param_dual_cum_sum, model.quadratic_objective_cache_pc)
    empty_and_feed_duals_to_model(model, param_dual_cum_sum)
    return model
end

function update_duals_with_affine_constraint_cache!(param_dual_cum_sum::Vector{Float64}, model::POI.ParametricOptimizer)
    for S in SUPPORTED_SETS
        affine_constraint_cache_inner = model.affine_constraint_cache[MOI.ScalarAffineFunction{Float64}, S]
        if !isempty(affine_constraint_cache_inner)
            update_duals_with_affine_constraint_cache!(param_dual_cum_sum, model.optimizer, affine_constraint_cache_inner)
        end
    end
    return
end

function update_duals_with_affine_constraint_cache!(param_dual_cum_sum::Vector{Float64}, optimizer::OT, affine_constraint_cache_inner::DD.WithType{F, S}) where {OT,F,S}
    for (ci, param_array) in affine_constraint_cache_inner
        calculate_parameters_in_ci!(param_dual_cum_sum, optimizer, param_array, ci)
    end
    return
end

function update_duals_with_quadratic_constraint_cache!(param_dual_cum_sum::Vector{Float64}, model::POI.ParametricOptimizer)
    for S in SUPPORTED_SETS
        quadratic_constraint_cache_pc_inner = model.quadratic_constraint_cache_pc[MOI.ScalarQuadraticFunction{Float64}, S]
        if !isempty(quadratic_constraint_cache_pc_inner)
            update_duals_with_quadratic_constraint_cache!(param_dual_cum_sum, model, quadratic_constraint_cache_pc_inner)
        end
    end
    return
end

function update_duals_with_quadratic_constraint_cache!(param_dual_cum_sum::Vector{Float64}, model::ParametricOptimizer, quadratic_constraint_cache_pc_inner::DD.WithType{F, S}) where {F,S}
    for (poi_ci, param_array) in quadratic_constraint_cache_pc_inner
        moi_ci = model.quadratic_added_cache[poi_ci]
        calculate_parameters_in_ci!(param_dual_cum_sum, model.optimizer, param_array, moi_ci)
    end
    return
end

function calculate_parameters_in_ci!(param_dual_cum_sum::Vector{Float64}, optimizer::OT, param_array::Vector{MOI.ScalarAffineTerm{T}}, ci::CI) where {OT, CI, T}
    cons_dual = MOI.get(optimizer, MOI.ConstraintDual(), ci)

    for param in param_array
        param_dual_cum_sum[param.variable_index.value - PARAMETER_INDEX_THRESHOLD] += cons_dual*param.coefficient
    end
    return
end

function update_duals_in_affine_objective!(param_dual_cum_sum::Vector{Float64}, affine_objective_cache::Vector{MOI.ScalarAffineTerm{T}}) where T
    for param in affine_objective_cache
        param_dual_cum_sum[param.variable_index.value - PARAMETER_INDEX_THRESHOLD] += param.coefficient
    end
    return
end

function update_duals_in_quadratic_objective!(param_dual_cum_sum::Vector{Float64}, quadratic_objective_cache_pc::Vector{MOI.ScalarAffineTerm{T}}) where T
    for param in quadratic_objective_cache_pc
        param_dual_cum_sum[param.variable_index.value - PARAMETER_INDEX_THRESHOLD] += param.coefficient
    end
    return
end

function empty_and_feed_duals_to_model(model::ParametricOptimizer, param_dual_cum_sum::Vector{Float64})
    empty!(model.dual_value_of_parameters)
    model.dual_value_of_parameters = zeros(model.number_of_parameters_in_model)
    for (vi_val_minus_threshold, param_dual) in enumerate(param_dual_cum_sum)
        model.dual_value_of_parameters[vi_val_minus_threshold] = param_dual
    end
    return
end

struct ParameterDual <: MOI.AbstractVariableAttribute end

function MOI.get(model::ParametricOptimizer, ::ParameterDual, vi_val::Int)
    if !is_additive(model, MOI.ConstraintIndex{MOI.SingleVariable, Parameter}(vi_val))
        error("Cannot calculate the dual of a multiplicative parameter")
    end
    return model.dual_value_of_parameters[vi_val - PARAMETER_INDEX_THRESHOLD]
end

function MOI.get(model::ParametricOptimizer, ::MOI.ConstraintDual, cp::MOI.ConstraintIndex{MOI.SingleVariable,POI.Parameter})
    if !is_additive(model, cp)
        error("Cannot calculate the dual of a multiplicative parameter")
    end
    return model.dual_value_of_parameters[cp.value - PARAMETER_INDEX_THRESHOLD]
end

function MOI.get(model::ParametricOptimizer, attr::MOI.ConstraintDual, ci::MOI.ConstraintIndex)
    return MOI.get(model.optimizer, attr, ci)
end

function is_additive(model::ParametricOptimizer, cp::MOI.ConstraintIndex)
    if cp.value in model.multiplicative_parameters
        return false
    end
    return true
end


using Test
using Printf: @printf, @sprintf
using StaticArrays: SVector
using QuantumLattices.Essentials.DegreesOfFreedom
using QuantumLattices.Essentials.QuantumOperators: ID, Operator, Operators, sequence, id, LaTeX, latexformat
using QuantumLattices.Essentials.Spatials: AbstractPID, PID, CPID, Point, Bond, Bonds, Lattice, pidtype, rcoord, icoord
using QuantumLattices.Essentials: kind, update!, reset!
using QuantumLattices.Interfaces: rank, expand, ⊗, ⊕
using QuantumLattices.Prerequisites: Float, decimaltostr
using QuantumLattices.Prerequisites.Traits: parameternames, isparameterbound, contentnames, getcontent

import QuantumLattices.Essentials.QuantumOperators: latexname, script
import QuantumLattices.Essentials.DegreesOfFreedom: ishermitian, statistics, couplingcenters, abbr
import QuantumLattices.Prerequisites.VectorSpaces: shape

struct DID{N<:Union{Int, Symbol}} <: SimpleIID
    nambu::N
end
@inline Base.show(io::IO, did::DID) = @printf io "DID(%s)" did.nambu
@inline Base.adjoint(sl::DID{Int}) = DID(3-sl.nambu)
@inline statistics(::Type{<:DID}) = :f
@inline DID(did::DID, ::CompositeInternal) = did

struct DFock <: SimpleInternal{DID{Int}}
    nnambu::Int
end
@inline shape(f::DFock) = (1:f.nnambu,)
@inline DID(i::CartesianIndex, vs::DFock) = DID(i.I...)
@inline CartesianIndex(did::DID{Int}, vs::DFock) = CartesianIndex(did.nambu)
@inline shape(iidspace::IIDSpace{DID{Symbol}, DFock}) = (1:iidspace.internal.nnambu,)
@inline shape(iidspace::IIDSpace{DID{Int}, DFock}) = (iidspace.iid.nambu:iidspace.iid.nambu,)
@inline shape(iidspace::IIDSpace{DID{Symbol}, DFock}) = (1:iidspace.internal.nnambu,)
@inline shape(iidspace::IIDSpace{DID{Int}, DFock}) = (iidspace.iid.nambu:iidspace.iid.nambu,)

@inline script(::Val{:site}, index::Index{<:AbstractPID, <:DID}; kwargs...) = index.pid.site
@inline script(::Val{:nambu}, index::Index{<:AbstractPID, <:DID}; kwargs...) = index.iid.nambu==2 ? "\\dagger" : ""

@inline latexname(::Type{<:OID{<:Index{<:AbstractPID, <:DID}}}) = Symbol("OID{Index{AbstractPID, DID}}")
latexformat(OID{<:Index{<:AbstractPID, <:DID}}, LaTeX{(:nambu,), (:site,)}('d'))

const DCoupling{V, I<:ID{DID}, C<:Subscripts} = Coupling{V, I, C}
@inline Base.repr(tc::(Coupling{V, <:ID{DID}} where V)) = @sprintf "%s ph(%s)" decimaltostr(tc.value) join(tc.cid.nambus, ", ")
@inline couplingcenters(::(Coupling{V, <:ID{DID}} where V), ::Bond, ::Val) = (1, 2)
@inline DCoupling(value, nambus::Tuple{Vararg{Int}}) = Coupling(value, ID(DID, nambus), Subscripts((nambu=Subscript(nambus),)))
@inline DCoupling(value, nambus::Subscript) = Coupling(value, ID(DID, Tuple(nambus)), Subscripts((nambu=nambus,)))

@inline abbr(::Type{<:Term{:Mu}}) = :mu
@inline abbr(::Type{<:Term{:Hp}}) = :hp
@inline ishermitian(::Type{<:Term{:Mu}}) = true
@inline ishermitian(::Type{<:Term{:Hp}}) = false

@testset "IID" begin
    did = DID(1)
    @test statistics(did) == statistics(typeof(did)) == :f

    did₁, did₂ = DID(1), DID(2)
    ciid = CompositeIID(did₁, did₂)
    @test length(ciid) == length(typeof(ciid)) == 2
    @test rank(ciid) == rank(typeof(ciid)) == 2
    @test iidtype(ciid, 1) == iidtype(typeof(ciid), 1) == DID{Int}
    @test iidtype(ciid, 2) == iidtype(typeof(ciid), 2) == DID{Int}
    @test ciid[1]==did₁ && ciid[2]==did₂
    @test ciid.nambus == (1, 2)
    @test string(ciid) == "DID(1) ⊗ DID(2)"
    @test did₁⊗did₂ == ciid
    @test did₁⊗ciid == CompositeIID(did₁, did₁, did₂)
    @test ciid⊗did₁ == CompositeIID(did₁, did₂, did₁)
    @test ciid⊗ciid == CompositeIID(did₁, did₂, did₁, did₂)
end

@testset "SimpleInternal" begin
    it = DFock(2)
    @test it|>eltype == DID{Int}
    @test it|>typeof|>eltype == DID{Int}
    @test it == deepcopy(it)
    @test isequal(it, deepcopy(it))
    @test it|>string == "DFock(nnambu=2)"
    @test it|>collect == [DID(1), DID(2)]
    @test statistics(it) == statistics(typeof(it)) == :f
    @test match(DID(1), it) && match(DID, DFock)
    @test filter(DID(1), it) == filter(DID, it) == it
    @test filter(DID(1), DFock) == filter(DID, DFock) == DFock
end

@testset "CompositeInternal" begin
    it₁, it₂ = DFock(2), DFock(3)

    ci = CompositeInternal{:⊕}(it₁, it₂)
    @test eltype(ci) == eltype(typeof(ci)) == DID{Int}
    @test rank(ci) == rank(typeof(ci)) == 2
    @test string(ci) == "DFock(nnambu=2) ⊕ DFock(nnambu=3)"
    for i = 1:2
        @test ci[i] == it₁[i]
    end
    for i = 1:3
        @test ci[2+i] == it₂[i]
    end
    @test it₁⊕it₂ == ci
    @test it₁⊕ci == CompositeInternal{:⊕}(it₁, it₁, it₂)
    @test ci⊕it₁ == CompositeInternal{:⊕}(it₁, it₂, it₁)
    @test ci⊕ci == CompositeInternal{:⊕}(it₁, it₂, it₁, it₂)
    @test filter(DID(1), ci) == filter(DID, ci) == ci
    @test filter(DID(1), typeof(ci)) == filter(DID, typeof(ci)) == typeof(ci)

    ci = CompositeInternal{:⊗}(it₁, it₂)
    @test eltype(ci) == eltype(typeof(ci)) == CompositeIID{Tuple{DID{Int}, DID{Int}}}
    @test rank(ci) == rank(typeof(ci)) == 2
    @test string(ci) == "DFock(nnambu=2) ⊗ DFock(nnambu=3)"
    count = 1
    for i = 1:3
        for j = 1:2
            @test ci[count] == CompositeIID(it₁[j], it₂[i])
            count += 1
        end
    end
    @test it₁⊗it₂ == ci
    @test it₁⊗ci == CompositeInternal{:⊗}(it₁, it₁, it₂)
    @test ci⊗it₁ == CompositeInternal{:⊗}(it₁, it₂, it₁)
    @test ci⊗ci == CompositeInternal{:⊗}(it₁, it₂, it₁, it₂)
    @test filter(DID(1), ci) == filter(DID, ci) == ci
    @test filter(DID(1), typeof(ci)) == filter(DID, typeof(ci)) == typeof(ci)
end

@testset "Index" begin
    @test parameternames(Index) == (:pid, :iid)
    @test isparameterbound(Index, Val(:pid), PID) == false
    @test isparameterbound(Index, Val(:iid), DID) == true

    index = Index(CPID('S', 4), DID(1))
    @test index|>pidtype == CPID{Char}
    @test index|>typeof|>pidtype == CPID{Char}
    @test index|>iidtype == DID{Int}
    @test index|>typeof|>iidtype == DID{Int}
    @test index|>adjoint == Index(CPID('S', 4), DID(2))
    @test statistics(index) == statistics(typeof(index)) == :f
    @test ishermitian(ID(index', index)) == true
    @test ishermitian(ID(index, index)) == false
end

@testset "OID" begin
    @test contentnames(CompositeOID) == (:index,)
    @test parameternames(CompositeOID) == (:index,)
    @test isparameterbound(CompositeOID, Val(:index), Index) == true
    @test isparameterbound(CompositeOID, Val(:index), Index{PID, DID{Int}}) == false

    @test contentnames(OID) == (:index, :rcoord, :icoord)
    @test parameternames(OID) == (:index, :coord)
    @test isparameterbound(OID, Val(:index), Index) == true
    @test isparameterbound(OID, Val(:index), Index{PID, DID{Int}}) == false
    @test isparameterbound(OID, Val(:coord), SVector) == true
    @test isparameterbound(OID, Val(:coord), SVector{2, Float}) == false

    oid = OID(Index(PID(1), DID(1)), [0.0, -0.0], [0.0, 0.0])
    @test indextype(oid) == indextype(typeof(oid)) == Index{PID, DID{Int}}
    @test statistics(oid) == statistics(typeof(oid)) == :f
    @test oid' == OID(Index(PID(1), DID(2)), rcoord=SVector(0.0, 0.0), icoord=SVector(0.0, 0.0))
    @test hash(oid, UInt(1)) == hash(OID(Index(PID(1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 1.0)), UInt(1))
    @test propertynames(ID(oid)) == (:indexes, :rcoords, :icoords)
    @test fieldnames(OID) == (:index, :rcoord, :icoord)
    @test string(oid) == "OID(Index(PID(1), DID(1)), [0.0, 0.0], [0.0, 0.0])"
    @test ID(oid', oid)' == ID(oid', oid)
    @test ishermitian(ID(oid', oid)) == true
    @test ishermitian(ID(oid, oid)) == false
    @test oidtype(DFock, Point{2, PID, Float}, Val(:default)) == OID{Index{PID, DID{Int}}, SVector{2, Float}}
end

@testset "Operator" begin
    opt = Operator(1.0,
        OID(Index(PID(1), DID(2)), SVector(0.5, 0.5), SVector(1.0, 1.0)),
        OID(Index(PID(1), DID(1)), SVector(0.0, 0.5), SVector(0.0, 1.0))
        )
    @test rcoord(opt) == SVector(0.5, 0.0)
    @test icoord(opt) == SVector(1.0, 0.0)

    opt = Operator(1.0, OID(Index(PID(1), DID(2)), SVector(0.5, 0.0), SVector(1.0, 0.0)))
    @test rcoord(opt) == SVector(0.5, 0.0)
    @test icoord(opt) == SVector(1.0, 0.0)
end

@testset "Hilbert" begin
    map = pid->DFock((pid.site-1)%2+1)
    hilbert = Hilbert(map, [CPID(1, 1), CPID(1, 2)])
    @test hilbert == Hilbert{DFock}(map, [CPID(1, 1), CPID(1, 2)])
    reset!(hilbert, (CPID(2, 1), CPID(2, 2)))

    hilbert = Hilbert(pid=>DFock(2) for pid in [PID(1), PID(2)])
    @test hilbert[PID(1)] == hilbert[PID(2)] == DFock(2)

    hilbert = Hilbert(PID(1)=>DFock(2), PID(2)=>DFock(3))
    @test hilbert[PID(1)]==DFock(2) && hilbert[PID(2)]==DFock(3)
end

@testset "IIDSpace" begin
    DID₁, DID₂, it = DID(2), DID(:σ), DFock(2)
    iidspace = IIDSpace(DID₁⊗DID₂, it⊗it)
    @test eltype(iidspace) == eltype(typeof(iidspace)) == CompositeIID{Tuple{DID{Int}, DID{Int}}}
    @test kind(iidspace) == kind(typeof(iidspace)) == :info
    @test length(iidspace) == 2
    @test iidspace[1]==DID(2)⊗DID(1) && iidspace[2]==DID(2)⊗DID(2)
    @test expand((DID₁, DID₂), (it, it)) == iidspace
    @test collect(iidspace) == [DID(2)⊗DID(1), DID(2)⊗DID(2)]
end

@testset "Metric" begin
    valtype(Metric, AbstractOID) = AbstractOID

    index = Index(CPID('S', 4), DID(1))
    oid = OID(index, SVector(0.5, 0.0), SVector(1.0, 0.0))

    m = OIDToTuple((:scope, :site, :nambu))
    @test m == OIDToTuple(:scope, :site, :nambu)
    @test isequal(m, OIDToTuple(:scope, :site, :nambu))
    @test keys(m) == keys(typeof(m)) == (:scope, :site, :nambu)
    @test filter(≠(:nambu), m) == OIDToTuple(:scope, :site)
    @test OIDToTuple(Index{PID, DID{Int}}) == OIDToTuple(:site, :nambu)
    @test OIDToTuple(OID{Index{CPID{Int}, DID{Int}}}) == OIDToTuple(:scope, :site, :nambu)
    @test OIDToTuple(Hilbert{DFock, CPID{Int}}) == OIDToTuple(:scope, :site, :nambu)
    @test m(index) == ('S', 4, 1) == m(oid)
    @test filter(≠(:scope), m)(index) == (4, 1)
    @test filter(≠(:nambu), m)(index) == ('S', 4)
    @test filter(∉((:site, :nambu)), m)(index) == ('S',)
end

@testset "Table" begin
    @test contentnames(Table) == (:by, :contents)

    by = filter(≠(:nambu), OIDToTuple(Index{PID, DID{Int}}))

    table = Table([Index(PID(1), DID(1)), Index(PID(1), DID(2))], by)
    @test empty(table) == Table{Index{PID, DID{Int}}}(by)
    @test table[Index(PID(1), DID(1))]==1 && table[Index(PID(1), DID(2))]==1

    hilbert = Hilbert(pid=>DFock(2) for pid in [PID(1), PID(2)])
    inds1 = (Index(PID(1), iid) for iid in DFock(2))|>collect
    inds2 = (Index(PID(2), iid) for iid in DFock(2))|>collect
    @test Table(hilbert, by) == Table([inds1; inds2], by)
    @test Table(hilbert, by) == union(Table(inds1, by), Table(inds2, by))

    opt = Operator(1.0im,
        OID(Index(PID(1), DID(2)), SVector(0.0, 0.0), SVector(1.0, 0.0)),
        OID(Index(PID(1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 0.0))
        )
    @test sequence(opt, table) == (1, 1)
    @test haskey(table, opt.id) == (true, true)

    table = Table(hilbert)
    @test table == Table([inds1; inds2])
    @test reset!(empty(table), [inds1; inds2]) == table
    @test reset!(empty(table), hilbert) == table
end

@testset "Subscript" begin
    sub = Subscript(4)
    @test contentnames(typeof(sub)) == (:contents, :rep, :constraint)
    @test getcontent(sub, :contents) == sub.pattern
    @test sub==deepcopy(sub) && isequal(sub, deepcopy(sub))
    @test string(sub) == "[* * * *]"
    @test rank(sub) == rank(typeof(sub)) == 4
    @test match(sub, (2, 2, 2, 2))

    sub = Subscript((1, 2, 3, 4))
    @test string(sub) == "[1 2 3 4]"
    @test sub == subscript"[1 2 3 4]" == subscript"[1 2 3 4]; false"
    @test rank(sub) == 4
    @test match(sub, (1, 3, 3, 4))

    sub = Subscript((1, 2, 2, 1), true)
    @test string(sub) == "[1 2 2 1]"
    @test sub == subscript"[1 2 2 1]; true"
    @test rank(sub) == 4
    @test match(sub, (1, 2, 2, 1)) && !match(sub, (1, 1, 2, 1))

    sub = subscript"[x₁ x₂ x₁ x₂]"
    @test string(sub) == "[x₁ x₂ x₁ x₂]"
    @test rank(sub) == 4
    @test match(sub, (1, 2, 1, 2)) && !match(sub, (1, 2, 2, 1))

    sub = subscript"[x₁ 4 4 x₂](x₁ < x₂)"
    @test string(sub) == "[x₁ 4 4 x₂](x₁ < x₂)"
    @test rank(sub) == rank(typeof(sub)) == 4
    @test match(sub, (1, 4, 4, 2)) && !match(sub, (2, 4, 4, 1))
end

@testset "Subscripts" begin
    subscripts = Subscripts((nambu=subscript"[x y](x > y)",), (nambu=subscript"[x x y y](x < y)",))
    @test subscripts.rep == (1:2=>("[x y](x > y)",), 3:6=>("[x x y y](x < y)",))
    @test hash(subscripts) == hash(subscripts.rep)
    @test string(subscripts) == "nambu[x y](x > y) × nambu[x x y y](x < y)"
    @test repr(subscripts, 1:2, :nambu) == "[x y](x > y)×[x x y y](x < y)"
    @test repr(subscripts, 1, :nambu) == "[x y](x > y)"
    @test repr(subscripts, 2, :nambu) == "[x x y y](x < y)"

    @test rank(subscripts) == rank(typeof(subscripts)) == 6
    @test rank(subscripts, 1) == rank(typeof(subscripts), 1) == 2
    @test rank(subscripts, 2) == rank(typeof(subscripts), 2) == 4
    @test match(subscripts, (DID(2), DID(1), DID(2), DID(2), DID(3), DID(3)))
    @test match(subscripts, DID(2)⊗DID(1)⊗DID(2)⊗DID(2)⊗DID(3)⊗DID(3))
    @test !match(subscripts, DID(1)⊗DID(2)⊗DID(2)⊗DID(2)⊗DID(3)⊗DID(3))
    @test !match(subscripts, DID(2)⊗DID(1)⊗DID(3)⊗DID(3)⊗DID(2)⊗DID(2))
    @test subscripts == Subscripts((nambu=subscript"[x y](x > y)",))*Subscripts((nambu=subscript"[x x y y](x < y)",))
end

@testset "couplings" begin
    @test parameternames(Coupling) == (:value, :cid, :subscripts)
    @test isparameterbound(Coupling, :cid, Tuple{DID{Int}}) == false
    @test isparameterbound(Coupling, :cid, ID{DID{Int}}) == true
    @test isparameterbound(Coupling, :subscripts, Subscripts{Tuple{NamedTuple{(:nambu,), Tuple{Subscript{Tuple{Int}}}}}}) == true
    @test isparameterbound(Coupling, :subscripts, Subscripts) == true
    @test isparameterbound(Coupling, :subscripts, Subscripts{Tuple{}, Tuple{}}) == false

    tc = DCoupling(2.0, (2,))
    @test id(tc) == ID(CompositeIID(tc.cid), tc.subscripts)
    @test rank(tc) == rank(typeof(tc)) == 1
    @test tc == Coupling(2.0, id(tc))
    @test ID{SimpleIID}(tc) == tc.cid
    @test Subscripts(tc) == tc.subscripts

    point = Point(CPID(1, 1), (0.0, 0.0), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>DFock(2))
    @test couplingcenters(tc, point, Val(:Mu)) == (1,)
    @test couplingpoints(tc, point, Val(:Mu)) == (point,)
    @test couplinginternals(tc, point, hilbert, Val(:Mu)) == (DFock(2),)

    tc₁ = DCoupling(1.5, (1, 2))
    tc₂ = DCoupling(2.0, subscript"[a b](a < b)")
    @test tc₁*tc₂ == Coupling(3.0, ID(DID(1), DID(2), DID(:a), DID(:b)), Subscripts((nambu=Subscript((1, 2)),), (nambu=subscript"[a b](a < b)",)))

    ex = expand(tc₁, point, hilbert, Val(:info))
    @test eltype(ex) == eltype(typeof(ex)) == Operator{Float64, NTuple{2, OID{Index{CPID{Int}, DID{Int}}, SVector{2, Float64}}}}
    @test collect(ex) == [
        Operator(1.5, ID(
            OID(Index(CPID(1, 1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 0.0)),
            OID(Index(CPID(1, 1), DID(2)), SVector(0.0, 0.0), SVector(0.0, 0.0))
            ))
        ]

    ex = expand(tc₂, point, hilbert, Val(:info))
    @test eltype(ex) == eltype(typeof(ex)) == Operator{Float64, NTuple{2, OID{Index{CPID{Int}, DID{Int}}, SVector{2, Float64}}}}
    @test collect(ex) == [
        Operator(2.0, ID(
            OID(Index(CPID(1, 1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 0.0)),
            OID(Index(CPID(1, 1), DID(2)), SVector(0.0, 0.0), SVector(0.0, 0.0))
            ))
        ]

    @test @couplings(DCoupling(1.0, (1, 1))) == Couplings(DCoupling(1.0, (1, 1)))
    @test @couplings(DCoupling(1.0, (1,))+DCoupling(1.0, (2,))) == DCoupling(1.0, (1,))+DCoupling(1.0, (2,))
end

@testset "TermFunction" begin
    ta = TermAmplitude()
    @test ta() == 1

    ta = TermAmplitude(x->x+3.0)
    @test ta(1.0) == 4.0

    tcs = DCoupling(1.0, (1, 1)) + DCoupling(2.0, (2, 2))
    termcouplings = TermCouplings(tcs)
    @test termcouplings == deepcopy(TermCouplings(tcs))
    @test isequal(termcouplings, deepcopy(TermCouplings(tcs)))
    @test valtype(termcouplings) == valtype(typeof(termcouplings)) == typeof(tcs)
    @test termcouplings() == tcs

    fx = i -> i%2==1 ? (DCoupling(1.0, (1, 1))+DCoupling(1.0, (2, 2))) : (DCoupling(1.0, (2, 1))+DCoupling(1.0, (1, 2)))
    termcouplings = TermCouplings(fx)
    @test termcouplings == TermCouplings{typejoin(typeof(fx(1)), typeof(fx(2)))}(fx)
    @test valtype(termcouplings) == valtype(typeof(termcouplings)) == typejoin(typeof(fx(1)), typeof(fx(2)))
    @test termcouplings(1) == fx(1)
    @test termcouplings(2) == fx(2)

    @test ismodulatable(TermModulate{Val{true}}) == ismodulatable(TermModulate{<:Function}) == true
    @test ismodulatable(TermModulate{Val{false}}) == false
    termmodulate = TermModulate(:t)
    @test termmodulate(t=1) == 1
    @test isnothing(termmodulate(mu=1))
    @test isnothing(termmodulate())
    @test ismodulatable(termmodulate) == true

    termmodulate = TermModulate(:t, t->t*2.0)
    @test ismodulatable(termmodulate) == true
    @test termmodulate(2) == 4
end

@testset "Term" begin
    point = Point(CPID(1, 1), (0.0, 0.0), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>DFock(2))
    term = Term{:Mu}(:mu, 1.5, 0, couplings=@couplings(DCoupling(1.0, (2, 1))), amplitude=(bond->3), modulate=false)
    @test term|>kind == term|>typeof|>kind == :Mu
    @test term|>id == term|>typeof|>id == :mu
    @test term|>valtype == term|>typeof|>valtype == Float
    @test term|>rank == term|>typeof|>rank == 2
    @test term|>abbr == term|>typeof|>abbr == :mu
    @test term|>ismodulatable == term|>typeof|>ismodulatable == false
    @test term|>ishermitian == term|>typeof|>ishermitian == true
    @test term == deepcopy(term)
    @test isequal(term, deepcopy(term))
    @test string(term) == "Mu{2}(id=mu, value=1.5, bondkind=0, factor=1.0)"
    @test repr(term, point, hilbert) == "mu: 4.5 ph(2, 1)"
    @test +term == term
    @test -term == term*(-1) == replace(term, factor=-term.factor)
    @test 2*term == term*2 == replace(term, factor=term.factor*2)
    @test term/2 == term*(1/2) == replace(term, factor=term.factor/2)

    p1 = Point(PID(1), (0.0, 0.0), (0.0, 0.0))
    p2 = Point(PID(2), (0.0, 0.0), (0.0, 0.0))
    hilbert = Hilbert(pid=>DFock(2) for pid in [PID(1), PID(2)])
    term = Term{:Mu}(:mu, 1.5, 0,
        couplings=bond->bond.pid.site%2==0 ? Couplings(DCoupling(1.0, (2, 2))) : Couplings(DCoupling(1.0, (1, 1))),
        amplitude=bond->3,
        modulate=true
    )
    @test term|>ismodulatable == term|>typeof|>ismodulatable == true
    @test repr(term, p1, hilbert) == "mu: 4.5 ph(1, 1)"
    @test repr(term, p2, hilbert) == "mu: 4.5 ph(2, 2)"
    @test one(term) == replace(term, value=1.0)
    @test zero(term) == replace(term, value=0.0)
    @test term.modulate(mu=4.0) == 4.0
    @test isnothing(term.modulate(t=1.0))
    @test update!(term, mu=4.25) == replace(term, value=4.25)
    @test term.value == 4.25
end

@testset "expand" begin
    point = Point(PID(1), (0.0, 0.0), (0.0, 0.0))
    hilbert = Hilbert(point.pid=>DFock(2))
    term = Term{:Mu}(:mu, 1.5, 0, couplings=@couplings(DCoupling(1.0, (2, 1))), amplitude=bond->3.0, modulate=true)
    operators = Operators(
        Operator(+2.25,
            OID(Index(PID(1), DID(2)), SVector(0.0, 0.0), SVector(0.0, 0.0)),
            OID(Index(PID(1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 0.0))
            )
        )
    @test expand(term, point, hilbert, half=true) == operators
    @test expand(term, point, hilbert, half=false) == operators*2

    bond = Bond(1, Point(PID(2), (1.5, 1.5), (1.0, 1.0)), Point(PID(1), (0.5, 0.5), (0.0, 0.0)))
    hilbert = Hilbert(pid=>DFock(2) for pid in [PID(1), PID(2)])
    term = Term{:Hp}(:t, 1.5, 1, couplings=@couplings(DCoupling(1.0, (2, 1))), amplitude=bond->3.0, modulate=true)
    operators = Operators(
        Operator(4.5,
            OID(Index(PID(1), DID(2)), SVector(0.5, 0.5), SVector(0.0, 0.0)),
            OID(Index(PID(2), DID(1)), SVector(1.5, 1.5), SVector(1.0, 1.0))
            )
        )
    @test expand(term, bond, hilbert, half=true) == operators
    @test expand(term, bond, hilbert, half=false) == operators+operators'

    lattice = Lattice(:Tuanzi, [Point(CPID(1, 1), (0.0, 0.0), (0.0, 0.0))], vectors=[[1.0, 0.0]], neighbors=1)
    bonds = Bonds(lattice)
    hilbert = Hilbert(pid=>DFock(2) for pid in lattice.pids)
    term = Term{:Mu}(:mu, 1.5, 0, couplings=@couplings(DCoupling(1.0, (2, 1))), amplitude=bond->3.0, modulate=true)
    operators = Operators(
        Operator(+2.25,
            OID(Index(CPID(1, 1), DID(2)), SVector(0.0, 0.0), SVector(0.0, 0.0)),
            OID(Index(CPID(1, 1), DID(1)), SVector(0.0, 0.0), SVector(0.0, 0.0))
            )
        )
    @test expand(term, bonds, hilbert, half=true) == operators
    @test expand(term, bonds, hilbert, half=false) == operators*2
end

@testset "script" begin
    oid = OID(Index(CPID('d', 1), DID(2)), SVector(0.0, 0.0), SVector(1.0, 0.0))
    latex = LaTeX{(:nambu,), (:site,)}('c', vectors=(SVector(1.0, 0.0), SVector(0.0, 1.0)))
    @test script(Val(:rcoord), oid) == "[0.0, 0.0]"
    @test script(Val(:icoord), oid) == "[1.0, 0.0]"
    @test script(Val(:integralicoord), oid; vectors=get(latex.options, :vectors, nothing)) == "[1, 0]"
    @test script(Val(:site), oid) == 1
    @test script(Val(:nambu), oid) == "\\dagger"
end


# This file is part of Kpax3. License is MIT.

function processchain(x::AminoAcidData,
                      fileroot::String)
  # open and close output files to check writing permissions
  # fail early if we can't write, saving the computational time required to
  # process the chain
  # use "append" for a non destructive operation
  fp = open(string(fileroot, "_posterior_k.csv"), "a")
  close(fp)

  fp = open(string(fileroot, "_posterior_R.csv"), "a")
  close(fp)

  fp = open(string(fileroot, "_posterior_C.csv"), "a")
  close(fp)

  n = zeros(Int, 1)
  m = zeros(Int, 1)
  N = zeros(Int, 1)

  k = zeros(Int, 1)

  h = 0

  fpR = open(string(fileroot, "_row_partition.bin"), "r")

  read!(fpR, n)
  read!(fpR, m)
  read!(fpR, N)

  R = zeros(Int, n[1])

  pk = zeros(Float64, n[1])
  pR = zeros(Float64, div(n[1] * (n[1] - 1), 2))

  T = 0
  try
    while !eof(fpR)
      read!(fpR, k)
      read!(fpR, R)

      pk[k[1]] += 1

      h = 0
      for i in 1:(n[1] - 1), j in (i + 1):n[1]
        h += 1
        pR[h] += float(R[i] == R[j])
      end

      T += 1
    end
  finally
    close(fpR)
  end

  if T != N[1]
    warn(string("Expecting ", N[1], " simulations but instead found ", T, "."))
  end

  pk /= T
  pR /= T

  fp = open(string(fileroot, "_posterior_k.csv"), "w")
  write(fp, "\"k\",\"p(k | x)\"\n")
  for g in 1:n[1]
    if pk[g] > 0.0
      write(fp, string(g, ",", pk[g], "\n"))
    end
  end
  close(fp)

  fp = open(string(fileroot, "_posterior_R.csv"), "w")
  write(fp, "\"Unit i\",\"Unit j\",\"p(i ~ j | x)\"\n")
  h = 0
  for i in 1:(n[1] - 1), j in (i + 1):n[1]
    h += 1
    write(fp, string("\"", x.id[i], "\",", "\"", x.id[j], "\",", pR[h], "\n"))
  end
  close(fp)

  fpC = open(string(fileroot, "_col_partition.bin"), "r")

  read!(fpC, n)
  read!(fpC, m)
  read!(fpC, N)

  C = zeros(UInt8, m[1])

  pC = zeros(Float64, 3, m[1])

  T = 0
  try
    while !eof(fpC)
      readbytes!(fpC, C, m[1])

      for b in 1:m[1]
        pC[C[b], b] += 1
      end

      T += 1
    end
  finally
    close(fpC)
  end

  if T != N[1]
    warn(string("Expecting ", N[1], " simulations but instead found ", T, "."))
  end

  pC /= T

  # estimate the probabilities of amino acids at each site
  #
  # Use a Dirichlet(1/J, ..., 1/J) as the prior distibution for the vector of
  # probabilities, where J is the total number of unique amino acids observed
  # at the current site. If y[j] is the count of amino acid j, then
  # a[j] = y[j] + 1 / J
  # is the corresponding parameter of the posterior probability.
  #
  # Posterior expected value is p = a / sum(a) = (y + 1 / J) / (n + 1)
  p = sum(float(x.data), dims=2)

  key = 1
  idxstart = 1
  a = 0.0
  count = 0.0
  for idxend in 1:m[1]
    if x.key[idxend] != key
      idx = idxstart:(idxend - 1)
      a = 1 / length(idx)

      for i in idx
        p[i] = (p[i] + a) / (n[1] + 1)
      end

      key += 1
      idxstart = idxend
      count = 0.0
    end

    count += p[idxend]
  end

  idx = idxstart:m[1]
  a = 1 / length(idx)

  for i in idx
    p[i] = (p[i] + a) / (n[1] + 1)
  end

  fp = open(string(fileroot, "_posterior_C.csv"), "w")
  write(fp, string("\"Site\",\"AminoAcid\",\"Proportion\",\"p('noise' | x)\",",
                   "\"p('weak signal' | x)\",\"p('strong signal' | x)\"\n"))

  isuint8 = eltype(x.ref) == UInt8

  polval = if isuint8
              UInt8('.')
            else
              "."
            end

  key = 0
  b = 1
  for j in 1:length(x.ref)
    if x.ref[j] == polval
      key += 1

      while (b <= m[1]) && (x.key[b] == key)
        write(fp, string(j, ",\"",
                        uppercase(isuint8 ? string(Char(x.val[b])) : x.val[b]),
                        "\",", p[b], ",", pC[1, b], ",", pC[2, b], ",",
                        pC[3, b], "\n"))
        b += 1
      end
    else
      write(fp, string(j, ",\"",
                       uppercase(isuint8 ? string(Char(x.ref[j])) : x.ref[j]),
                       "\",", 1.0, ",", 1.0, ",", 0.0, ",", 0.0, "\n"))
    end
  end
  close(fp)

  nothing
end

function savestate!(fpR::IOStream,
                    fpC::IOStream,
                    state::AminoAcidState)
  write(fpR, state.k)
  write(fpR, state.R)

  for b in 1:size(state.C, 2)
    write(fpC, state.C[state.cl[1], b] < 0x03 ? state.C[state.cl[1], b] : 0x03)
  end

  nothing
end


using ONNX, Flux, ProtoBuf
include("ops_tests.jl")

# test basic conv with padding
ip = read_input("$ONNX_TEST_PATH/test_basic_conv_with_padding")
main_test("$ONNX_TEST_PATH/test_basic_conv_with_padding",
    read_output("$ONNX_TEST_PATH/test_basic_conv_with_padding"),
        read_input("$ONNX_TEST_PATH/test_basic_conv_with_padding")[1],
            read_input("$ONNX_TEST_PATH/test_basic_conv_with_padding")[2])

# test basic conv without padding
ip = read_input("$ONNX_TEST_PATH/test_basic_conv_without_padding")
main_test("$ONNX_TEST_PATH/test_basic_conv_without_padding",
    read_output("$ONNX_TEST_PATH/test_basic_conv_without_padding"),
        read_input("$ONNX_TEST_PATH/test_basic_conv_without_padding")[1],
            read_input("$ONNX_TEST_PATH/test_basic_conv_without_padding")[2])

# Conv with strides and no pads
ip = read_input("$ONNX_TEST_PATH/test_conv_with_strides_no_padding")
main_test("$ONNX_TEST_PATH/test_conv_with_strides_no_padding",
          read_output("$ONNX_TEST_PATH/test_conv_with_strides_no_padding"),
          read_input("$ONNX_TEST_PATH/test_conv_with_strides_no_padding")[1],
          read_input("$ONNX_TEST_PATH/test_conv_with_strides_no_padding")[2])

# Conv with strides and pads
ip = read_input("$ONNX_TEST_PATH/test_conv_with_strides_padding")
main_test("$ONNX_TEST_PATH/test_conv_with_strides_padding",
          read_output("$ONNX_TEST_PATH/test_conv_with_strides_padding"),
          read_input("$ONNX_TEST_PATH/test_conv_with_strides_padding")[1],
          read_input("$ONNX_TEST_PATH/test_conv_with_strides_padding")[2])

# Test Dropout default
main_test("$ONNX_TEST_PATH/test_dropout_default",
            read_output("$ONNX_TEST_PATH/test_dropout_default"),
            read_input("$ONNX_TEST_PATH/test_dropout_default")[1])

# Test Dropout random
main_test("$ONNX_TEST_PATH/test_dropout_random",
            read_output("$ONNX_TEST_PATH/test_dropout_random"),
            read_input("$ONNX_TEST_PATH/test_dropout_random")[1])


###############################################
#                                             #
# Main file for starting solving functions    #
# and generating figures,                     #
# parallelized with 'Distributed'             #
#                                             #
###############################################

# Load parallel computing package
using Distributed
using SharedArrays
using NLopt
using FiniteDifferences
using GridInterpolations
using JLD2
using Plots

# Include main functions
include("nopolicy.jl")
include("withpolicy_parallel.jl")
include("figures_paper.jl")

# Include helper functions
include("helperfunctions.jl")

# Tell Julia to use several processes for distributed computing
nprocs = 8;
addprocs(nprocs)

@everywhere begin
    # Load packages
    using Pkg; Pkg.activate(".") # Activate environment so that workers "see" packages
    using Distributed
    using SharedArrays
    using NLopt
    using FiniteDifferences
    using GridInterpolations

    # Include helper functions
    include("helperfunctions.jl")
end

# Solve model with standard parameters
@time noint    = nopolicy();                          # no intervention at all
@time notest   = withpolicy("notest", nprocs;);        # no testing
@time untarget = withpolicy("untargettest", nprocs;);  # untargeted testing
@time target   = withpolicy("targettest", nprocs;);    # targeted testing
@time isolate  = withpolicy("isolate", nprocs;);       # isolation

# Save important objects
#save("src/data/calib1_nointervention.jld2", noint);
#save("src/data/calib1_notest_small.jld2", notest);
#save("src/data/calib1_untarget_small.jld2", untarget);
#save("src/data/calib1_target_small.jld2", target);
#save("src/data/calib1_isolate_small.jld2", isolate);

# Load objects
#noint = load("src/data/calib1_nointervention.jld2");
#notest = load("src/data/calib1_notest_small.jld2");
#untarget = load("src/data/calib1_untarget_small.jld2");
#target = load("src/data/calib1_target_small.jld2");
#isolate = load("src/data/calib1_isolate_small.jld2");

# Figures (using freshly-generated data)
createFig5(noint, notest, untarget)
#savefig("src/figures/Fig5_dim20")
createFig6(notest, untarget, target)
#savefig("src/figures/Fig6_dim20")
createFig7(untarget, target, isolate)
#savefig("src/figures/Fig7_dim20")

# Figures (using stored data)
#createFig5()
#createFig6()
#createFig7()


using .Networks

using DataStructures
using Dates
using JuMP
using Printf
using Parameters

@with_kw mutable struct EODAssessmentModel <: AbstractModelContainer
    model::Model = Model()

    # Uncertainty model
    #bus,ts
    uncertain_load::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();
    #limitableUnit,ts
    uncertain_prod::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();

    # Market model
    #unit, ts
    p_injected::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();
    b_in::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();
    b_marg::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();
    b_out::SortedDict{Tuple{String, DateTime},VariableRef} =
        SortedDict{Tuple{String, DateTime},VariableRef}();

    #ts :
    #cut prod
    p_cut_prod::SortedDict{DateTime,VariableRef} =
        SortedDict{DateTime,VariableRef}();
    #LoL
    p_loss_of_load::SortedDict{DateTime,VariableRef} =
        SortedDict{DateTime,VariableRef}();

    #objective
    loss_of_load_obj =  AffExpr(0.)
    prod_obj =  AffExpr(0.)
    cut_prod_obj =  AffExpr(0.)
    full_obj = AffExpr(0.)
end

function add_load_uncertainties_vars!(model_container::EODAssessmentModel,
                                    network, TS, assessment_uncertainties)
    for bus in Networks.get_buses(network)
        bus_id = Networks.get_id(bus)
        lb_l = get_assessment_uncertainties_lb(assessment_uncertainties, bus_id)
        ub_l = get_assessment_uncertainties_ub(assessment_uncertainties, bus_id)
        for ts in TS
            name =  @sprintf("uncertain_load[%s,%s]", bus_id, ts);
            model_container.uncertain_load[bus_id,ts] = @variable(model_container.model, base_name = name,
                                                                lower_bound = lb_l, upper_bound = ub_l)
        end
    end

    return model_container.uncertain_load
end
function add_prod_uncertainties_vars!(model_container::EODAssessmentModel,
                                    network, TS, assessment_uncertainties, tso_actions)
    for limitable_gen in Networks.get_generators_of_type(network, Networks.LIMITABLE)
        gen_id = Networks.get_id(limitable_gen)
        lb_l = get_assessment_uncertainties_lb(assessment_uncertainties, gen_id)
        for ts in TS
            # this allows to set p_inj[gen_id] == uncertain_prod without needing to express min(uncertain, limit) in the model
            ub_l = min(get_assessment_uncertainties_ub(assessment_uncertainties, gen_id),
                    get_limitation(tso_actions, gen_id, ts),
                    Networks.get_p_max(limitable_gen))
            name =  @sprintf("uncertain_prod[%s,%s]", gen_id, ts);
            model_container.uncertain_prod[gen_id, ts] = @variable(model_container.model, base_name=name,
                                                                lower_bound = lb_l, upper_bound = ub_l)
        end
    end

    return model_container.uncertain_prod
end
function add_uncertainties_vars!(model_container::EODAssessmentModel,
                                network, TS, assessment_uncertainties, tso_actions)
    add_load_uncertainties_vars!(model_container,
                                network, TS, assessment_uncertainties)
    add_prod_uncertainties_vars!(model_container,
                                network, TS, assessment_uncertainties, tso_actions)
    return model_container
end

function add_limitable_prod_vars!(model_container::EODAssessmentModel, network, TS)
    for limitable_gen in Networks.get_generators_of_type(network, Networks.LIMITABLE)
        gen_id = Networks.get_id(limitable_gen)
        for ts in TS
            name =  @sprintf("p_injected[%s,%s]", gen_id, ts)
            model_container.p_injected[gen_id,ts] = @variable(model_container.model, base_name=name)
        end
    end

    return model_container.p_injected
end

function add_use_limitables_constraints!(model_container::EODAssessmentModel, network, TS, tso_actions)
    for limitable_gen in Networks.get_generators_of_type(network, Networks.LIMITABLE)
        gen_id = Networks.get_id(limitable_gen)
        for ts in TS
            limit_l = get_limitation(tso_actions, gen_id, ts)
            @assert(upper_bound(model_container.uncertain_prod[gen_id, ts]) <= limit_l)
            @constraint(model_container.model,
                        model_container.p_injected[gen_id,ts] == model_container.uncertain_prod[gen_id, ts])
        end
    end
    return model_container
end

function add_pilotable_prod_vars!(model_container_p::EODAssessmentModel, network, TS, tso_actions)
    p_injected_l = model_container_p.p_injected
    b_in_l = model_container_p.b_in
    b_marg_l = model_container_p.b_marg
    b_out_l = model_container_p.b_out

    for pilotable_gen in Networks.get_generators_of_type(network, Networks.PILOTABLE)
        gen_id = Networks.get_id(pilotable_gen)
        for ts in TS
            pmin_l, pmax_l = safeget_imposition(tso_actions, gen_id, ts)
            name_l =  @sprintf("b_in[%s,%s]", gen_id, ts);
            b_in_l[gen_id,ts] = @variable(model_container_p.model, base_name = name_l, binary = true)
            name_l =  @sprintf("b_marginal[%s,%s]", gen_id, ts);
            b_marg_l[gen_id,ts] = @variable(model_container_p.model, base_name = name_l, binary = true)
            name_l =  @sprintf("b_out[%s,%s]", gen_id, ts);
            b_out_l[gen_id,ts] = @variable(model_container_p.model, base_name = name_l, binary = true)
            name_l =  @sprintf("prod[%s,%s]", gen_id, ts);
            p_injected_l[gen_id,ts] = @variable(model_container_p.model, base_name = name_l,
                                                lower_bound=0., upper_bound = pmax_l);

            @constraint(model_container_p.model,
                        p_injected_l[gen_id,ts] >= ( pmax_l * b_in_l[gen_id,ts] +  pmin_l * b_marg_l[gen_id,ts] ));
            @constraint(model_container_p.model,
                        p_injected_l[gen_id,ts] <= (1 - b_out_l[gen_id,ts]) * pmax_l);
            @constraint(model_container_p.model,
                        b_out_l[gen_id,ts] + b_marg_l[gen_id,ts] + b_in_l[gen_id,ts] == 1.);
        end
    end

    return model_container_p
end

function add_market_vars!(model_container::EODAssessmentModel, network, TS, assessment_uncertainties, tso_actions)
    add_limitable_prod_vars!(model_container, network, TS)
    add_pilotable_prod_vars!(model_container, network, TS, tso_actions)
end

function add_cut_prod_vars!(model_container::EODAssessmentModel, network, TS)
    for ts in TS
        name_l =  @sprintf("cut_prod[%s]", ts);
        model_container.p_cut_prod[ts] = @variable(model_container.model,
                                                    base_name = name_l, lower_bound=0.)

        prod_expr = sum(model_container.p_injected[gen_id,ts]
                        for gen_id in Networks.get_id.(Networks.get_generators(network)))
        @constraint(model_container.model, model_container.p_cut_prod[ts] <= prod_expr)
    end
    return model_container
end

function add_loss_of_load_vars!(model_container::EODAssessmentModel, network, TS)
    for ts in TS
        name_l =  @sprintf("loss_of_load[%s]", ts);
        model_container.p_loss_of_load[ts] = @variable(model_container.model,
                                                    base_name = name_l, lower_bound=0.)

        conso_expr = sum(model_container.uncertain_load[bus_id,ts]
                        for bus_id in Networks.get_id.(Networks.get_buses(network)))
        @constraint(model_container.model, model_container.p_loss_of_load[ts] <= conso_expr)
    end
    return model_container
end

function add_cheapest_prod_constraints!(model_container::EODAssessmentModel, network, TS)
    pilotables = Networks.get_generators_of_type(network, Networks.PILOTABLE)
    for (index, gen_1) in enumerate(pilotables)
        cost_1 = Networks.get_prop_cost(gen_1)
        gen_id_1 = Networks.get_id(gen_1)
        for gen_2 in pilotables[index:end]
            cost_2 = Networks.get_prop_cost(gen_2)
            if cost_1 < cost_2
                gen_id_2 = Networks.get_id(gen_2)
                for ts in TS
                    @constraint(model_container.model, model_container.b_in[gen_id_1,ts] >= model_container.b_in[gen_id_2,ts] );
                end
            end
        end
    end

    return model_container
end

function add_eod_constraint!(model_container_p::EODAssessmentModel, network, TS)

    for ts in TS

        supply_l = AffExpr(0.)
        for gen in Networks.get_generators(network)
            gen_id = Networks.get_id(gen)
            supply_l += model_container_p.p_injected[gen_id, ts];
        end
        supply_l -= model_container_p.p_cut_prod[ts];

        demand_l = AffExpr(0.)
        for bus in Networks.get_buses(network)
            bus_id = Networks.get_id(bus)
            demand_l += model_container_p.uncertain_load[bus_id, ts];
        end
        demand_l -= model_container_p.p_loss_of_load[ts];

        @constraint(model_container_p.model, supply_l == demand_l);
    end

    return model_container_p
end

function create_objective!(model_container::EODAssessmentModel, network, TS, loss_of_load_coeff, inj_prod_coeff, cut_prod_coeff)
    @assert(loss_of_load_coeff >= 0.)
    @assert(inj_prod_coeff >= 0.)
    @assert(cut_prod_coeff >= 0.)

    model_container.loss_of_load_obj = AffExpr(0.)
    for ts in TS
        add_to_expression!(model_container.loss_of_load_obj, loss_of_load_coeff * model_container.p_loss_of_load[ts])
    end

    model_container.cut_prod_obj = AffExpr(0.)
    for ts in TS
        add_to_expression!(model_container.cut_prod_obj, -cut_prod_coeff * model_container.p_cut_prod[ts])
    end

    model_container.prod_obj = AffExpr(0.)
    #exclude limitables to allow setting their uncertainties at a low level
    # for gen in Networks.get_generators_of_type(network, Networks.PILOTABLE)
    #     gen_id = Networks.get_id(gen)
    #     for ts in TS
    #         add_to_expression!(model_container.prod_obj, inj_prod_coeff * model_container.p_injected[gen_id, ts])
    #     end
    # end

    model_container.full_obj = model_container.loss_of_load_obj + model_container.cut_prod_obj + model_container.prod_obj
    return model_container.full_obj
end

#Only cut conso when no possible extra production
function add_loss_of_load_constraint!(model_container::EODAssessmentModel, network, TS, assessment_uncertainties)
    big_m = 0.
    for bus_id in Networks.get_id.(Networks.get_buses(network))
        big_m += get_assessment_uncertainties_ub(assessment_uncertainties, bus_id)
    end

    for pilotable_gen_id in Networks.get_id.(Networks.get_generators_of_type(network, Networks.PILOTABLE))
        for ts in TS
            b_in_var = model_container.b_in[pilotable_gen_id, ts]
            @constraint(model_container.model, model_container.p_loss_of_load[ts] <= big_m*b_in_var)
        end
    end
end

function add_uncertainties_constraints!(model_container_l::EODAssessmentModel, network, TS, assessment_uncertainties, configs)
    #FIXME constraints on uncertainties go here !
    #e.g | u[bus1] - u[bus2] | < 5
    #e.g ( 1 - u[bus1] / ub[limitable_1] ) <  ( 1 - u[limitable_1] / ub[limitable_1] )
end

function formulate_eod_assessment(network, TS, assessment_uncertainties, tso_actions, configs)
    model_container_l = EODAssessmentModel()

    add_uncertainties_vars!(model_container_l, network, TS, assessment_uncertainties, tso_actions)
    add_market_vars!(model_container_l, network, TS, assessment_uncertainties, tso_actions)
    add_cut_prod_vars!(model_container_l, network, TS)
    add_loss_of_load_vars!(model_container_l, network, TS)

    add_use_limitables_constraints!(model_container_l, network, TS, tso_actions)
    add_cheapest_prod_constraints!(model_container_l, network, TS)
    add_eod_constraint!(model_container_l, network, TS)
    add_loss_of_load_constraint!(model_container_l, network, TS, assessment_uncertainties)
    add_uncertainties_constraints!(model_container_l, network, TS, assessment_uncertainties, configs)

    create_objective!(model_container_l, network, TS,
                    configs.loss_of_load_coeff, configs.cut_prod_coeff, configs.inj_prod_coeff)
    @objective(get_model(model_container_l), Max, model_container_l.full_obj)

    return model_container_l
end

function is_validated(model_container::EODAssessmentModel)
    return value(model_container.loss_of_load_obj) < 1e-09
end

function has_positive_slack(model_container::EODAssessmentModel)::Bool
    return false # ( has_positive_value(model_container.lower.lol_model.p_loss_of_load)
           #  || has_positive_value(model_container.lower.lol_model.p_cut_prod) )
end

function get_assessment_uncertainties_lb(assessment_uncertainties, bus_or_limitable::String)
    return assessment_uncertainties[bus_or_limitable][1]
end
function get_assessment_uncertainties_ub(assessment_uncertainties, bus_or_limitable::String)
    return assessment_uncertainties[bus_or_limitable][2]
end


const DATA_FILE = joinpath(@__DIR__, "dummy.csv")
const NUM_DIMENSIONS, NUM_OBSERVATIONS = size(load_data(DATA_FILE)[1])
const SOLVER = optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0)

experiment = Dict{Symbol, Any}(
    :hash => 1,
    :data_file => DATA_FILE,
    :data_set_name => "Dummy",
    :output_file => joinpath(@__DIR__, "example_pool_qs.json"),
    :log_dir => joinpath(@__DIR__, "example_pool_qs.log"),
    :split_strategy_name => "Sf",
    :initial_pool_strategy_name => "Pu",
    :model => Dict(:type => :SVDDneg,
                   :param => Dict{Symbol, Any}(),
                   :init_strategy => SimpleCombinedStrategy(RuleOfThumbScott(), BoundedTaxErrorEstimate(0.05, 0.02, 0.98))),
    :query_strategy => Dict(:type => :RandomOutlierPQs,
                            :param => Dict{Symbol, Any}()),
    :split_strategy => OneClassActiveLearning.DataSplits(trues(NUM_OBSERVATIONS)),
    :oracle => Dict{Symbol, Any}(
        :type => :PoolOracle,
        :param => Dict{Symbol, Any}()
    ),
    :param => Dict(:num_al_iterations => 10,
                   :solver => SOLVER,
                   :initial_pools => fill(:U, NUM_OBSERVATIONS),
                   :adjust_K => true,
                   :initial_pool_resample_version => 1))


function Transformation(transform_func=identity)
    flip = Node((false, false, false))
    scale = Node(Vec3f0(1))
    scale = lift(flip, scale) do f, s
        map((f, s)-> f ? -s : s, Vec(f), s)
    end
    translation, rotation, align = (
        Node(Vec3f0(0)),
        Node(Quaternionf0(0, 0, 0, 1)),
        Node(Vec2f0(0))
    )
    trans = nothing
    model = map_once(scale, translation, rotation, align, flip) do s, o, q, a, flip
        parent = if trans !== nothing && isassigned(trans.parent)
            boundingbox(trans.parent[])
        else
            nothing
        end
        transformationmatrix(o, s, q, a, flip, parent)
    end
    return Transformation(
        translation,
        scale,
        rotation,
        model,
        flip,
        align,
        Node{Any}(transform_func)
    )
end

function Transformation(scene::SceneLike)
    flip = Node((false, false, false))
    scale = Node(Vec3f0(1))
    translation, rotation, align = (
        Node(Vec3f0(0)),
        Node(Quaternionf0(0, 0, 0, 1)),
        Node(Vec2f0(0))
    )
    pmodel = transformationmatrix(scene)
    trans = nothing
    model = map_once(scale, translation, rotation, align, pmodel, flip) do s, o, q, a, p, f
        bb = if trans !== nothing && isassigned(trans.parent)
            boundingbox(trans.parent[])
        else
            nothing
        end
        return p * transformationmatrix(o, s, q, align, f, bb)
    end

    ptrans = transformation(scene)
    trans = Transformation(
        translation,
        scale,
        rotation,
        model,
        flip,
        align,
        copy(ptrans.transform_func)
    )
    return trans
end

function translated(scene::Scene, translation...)
    tscene = Scene(scene, transformation = Transformation())
    transform!(tscene, translation...)
    tscene
end

function translated(scene::Scene; kw_args...)
    tscene = Scene(scene, transformation = Transformation())
    transform!(tscene; kw_args...)
    tscene
end

function transform!(
        scene::SceneLike;
        translation = Vec3f0(0),
        scale = Vec3f0(1),
        rotation = 0.0,
    )
    translate!(scene, translation)
    scale!(scene, scale)
    rotate!(scene, rotation)
end

transformation(t::Scene) = t.transformation
transformation(t::AbstractPlot) = t.transformation
transformation(t::Transformation) = t

scale(t::Transformable) = transformation(t).scale

scale!(t::Transformable, s) = (scale(t)[] = to_ndim(Vec3f0, Float32.(s), 1))

"""
    scale!(t::Transformable, x, y)
    scale!(t::Transformable, x, y, z)
    scale!(t::Transformable, xyz)
    scale!(t::Transformable, xyz...)

Scale the given `Transformable` (a Scene or Plot) to the given arguments.
Can take `x, y` or `x, y, z`.
This is an absolute scaling, and there is no option to perform relative scaling.
"""
scale!(t::Transformable, xyz...) = scale!(t, xyz)

rotation(scene::Transformable) = transformation(scene).rotation

function rotate!(::Type{T}, scene::Transformable, q) where T
    rot = convert_attribute(q, key"rotation"())
    if T === Accum
        rot1 = rotation(scene)[]
        rotation(scene)[] = rot1 * rot
    elseif T == Absolute
        rotation(scene)[] = rot
    else
        error("Unknown transformation: $T")
    end
end

"""
    rotate!(Accum, scene::Transformable, axis_rot...)

Apply a relative rotation to the Scene, by multiplying by the current rotation.
"""
rotate!(::Type{T}, scene::Transformable, axis_rot...) where T = rotate!(T, scene, axis_rot)

"""
    rotate!(scene::Transformable, axis_rot::Quaternion)
    rotate!(scene::Transformable, axis_rot::AbstractFloat)
    rotate!(scene::Transformable, axis_rot...)

Apply an absolute rotation to the Scene. Rotations are all internally converted to `Quaternion`s.
"""
rotate!(scene::Transformable, axis_rot...) = rotate!(Absolute, scene, axis_rot)
rotate!(scene::Transformable, axis_rot::Quaternion) = rotate!(Absolute, scene, axis_rot)
rotate!(scene::Transformable, axis_rot::AbstractFloat) = rotate!(Absolute, scene, axis_rot)

translation(scene::Transformable) = transformation(scene).translation

"""
    Accum
Force transformation to be relative to the current state, not absolute.
"""
struct Accum end

"""
    Absolute
Force transformation to be absolute, not relative to the current state.
This is the default setting.
"""
struct Absolute end

function translate!(::Type{T}, scene::Transformable, t) where T
    offset = to_ndim(Vec3f0, Float32.(t), 0)
    if T === Accum
        translation(scene)[] = translation(scene)[] .+ offset
    elseif T === Absolute
        translation(scene)[] = offset
    else
        error("Unknown translation type: $T")
    end
end
"""
    translate!(scene::Transformable, xyz::VecTypes)
    translate!(scene::Transformable, xyz...)

Apply an absolute translation to the Scene, translating it to `x, y, z`.
"""
translate!(scene::Transformable, xyz::VecTypes) = translate!(Absolute, scene, xyz)
translate!(scene::Transformable, xyz...) = translate!(Absolute, scene, xyz)
"""
    translate!(Accum, scene::Transformable, xyz...)

Translate the scene relative to its current position.
"""
translate!(::Type{T}, scene::Transformable, xyz...) where T = translate!(T, scene, xyz)

function transform!(scene::Transformable, x::Tuple{Symbol, <: Number})
    plane, dimval = string(x[1]), Float32(x[2])
    if length(plane) != 2 || (!all(x-> x in ('x', 'y', 'z'), plane))
        error("plane needs to define a 2D plane in xyz. It should only contain 2 symbols out of (:x, :y, :z). Found: $plane")
    end
    if all(x-> x in ('x', 'y'), plane) # xy plane
        translate!(scene, 0, 0, dimval)
    elseif all(x-> x in ('x', 'z'), plane) # xz plane
        rotate!(scene, Vec3f0(1, 0, 0), 0.5pi)
        translate!(scene, 0, dimval, 0)
    else #yz plane
        r1 = qrotation(Vec3f0(0, 1, 0), 0.5pi)
        r2 = qrotation(Vec3f0(1, 0, 0), 0.5pi)
        rotate!(scene,  r2 * r1)
        translate!(scene, dimval, 0, 0)
    end
    scene
end

transformationmatrix(x) = transformation(x).model

transform_func(x) = transform_func_obs(x)[]
transform_func_obs(x) = transformation(x).transform_func

"""
    apply_transform(f, data)
Apply the data transform func to the data
"""
apply_transform(f::typeof(identity), x) = x
# these are all ambiguity fixes
apply_transform(f::typeof(identity), x::AbstractArray) = x
apply_transform(f::typeof(identity), x::VecTypes) = x
apply_transform(f::typeof(identity), x::Number) = x
apply_transform(f::typeof(identity), x::ClosedInterval) = x

apply_transform(f::NTuple{2, typeof(identity)}, x) = x
apply_transform(f::NTuple{2, typeof(identity)}, x::AbstractArray) = x
apply_transform(f::NTuple{2, typeof(identity)}, x::VecTypes) = x
apply_transform(f::NTuple{2, typeof(identity)}, x::Number) = x
apply_transform(f::NTuple{2, typeof(identity)}, x::ClosedInterval) = x

apply_transform(f::NTuple{3, typeof(identity)}, x) = x
apply_transform(f::NTuple{3, typeof(identity)}, x::AbstractArray) = x
apply_transform(f::NTuple{3, typeof(identity)}, x::VecTypes) = x
apply_transform(f::NTuple{3, typeof(identity)}, x::Number) = x
apply_transform(f::NTuple{3, typeof(identity)}, x::ClosedInterval) = x

struct PointTrans{N, F}
    f::F
    function PointTrans{N}(f::F) where {N, F}
        if !hasmethod(f, Tuple{Point{N}})
            error("PointTrans with parameter N = $N must be applicable to an argument of type Point{$N}.")
        end
        new{N, F}(f)
    end
end

# PointTrans{N}(func::F) where {N, F} = PointTrans{N, F}(func)
Base.broadcastable(x::PointTrans) = (x,)

function apply_transform(f::PointTrans{N}, point::Point{N}) where N
    return f.f(point)
end

function apply_transform(f::PointTrans{N1}, point::Point{N2}) where {N1, N2}
    p_dim = to_ndim(Point{N1, Float32}, point, 0.0)
    p_trans = f.f(p_dim)
    if N1 < N2
        p_large = ntuple(i-> i <= N1 ? p_trans[i] : point[i], N2)
        return Point{N2, Float32}(p_large)
    else
        return to_ndim(Point{N2, Float32}, p_trans, 0.0)
    end
end

function apply_transform(f, data::AbstractArray)
    map(point-> apply_transform(f, point), data)
end

function apply_transform(f::Tuple{Any, Any}, point::VecTypes{2})
    Point2{Float32}(
        f[1](point[1]),
        f[2](point[2]),
    )
end
# ambiguity fix
apply_transform(f::NTuple{2, typeof(identity)}, point::VecTypes{2}) = point

function apply_transform(f::Tuple{Any, Any}, point::VecTypes{3})
    apply_transform((f..., identity), point)
end
# ambiguity fix
apply_transform(f::NTuple{2, typeof(identity)}, point::VecTypes{3}) = point

function apply_transform(f::Tuple{Any, Any, Any}, point::VecTypes{3})
    Point3{Float32}(
        f[1](point[1]),
        f[2](point[2]),
        f[3](point[3]),
    )
end
# ambiguity fix
apply_transform(f::NTuple{3, typeof(identity)}, point::VecTypes{3}) = point

apply_transform(f, number::Number) = f(number)

function apply_transform(f::Observable, data::Observable)
    return lift((f, d)-> apply_transform(f, d), f, data)
end

apply_transform(f, itr::Pair) = apply_transform(f, itr[1]) => apply_transform(f, itr[2])
function apply_transform(f, itr::ClosedInterval)
    mini, maxi = extrema(itr)
    return apply_transform(f, mini) .. apply_transform(f, maxi)
end

function apply_transform(f, r::Rect)
    mi = minimum(r)
    ma = maximum(r)
    mi_t = apply_transform(f, mi)
    ma_t = apply_transform(f, ma)
    Rect(Vec(mi_t), Vec(ma_t .- mi_t))
end
# ambiguity fix
apply_transform(f::typeof(identity), r::Rect) = r
apply_transform(f::NTuple{2, typeof(identity)}, r::Rect) = r
apply_transform(f::NTuple{3, typeof(identity)}, r::Rect) = r

pseudolog10(x) = sign(x) * log10(abs(x) + 1)
inv_pseudolog10(x) = sign(x) * (exp10(abs(x)) - 1)

struct Symlog10
    low::Float64
    high::Float64
    function Symlog10(low, high)
        if !(low < 0 && high > 0)
            error("Low bound needs to be smaller than 0 and high bound larger than 0. You gave $low, $high.")
        end
        new(Float64(low), Float64(high))
    end
end
function Symlog10(x)
    Symlog10(-x, x)
end
function (s::Symlog10)(x)
    if x > 0
        x <= s.high ? x / s.high * log10(s.high) : log10(x)
    elseif x < 0
        x >= s.low ? x / abs(s.low) * log10(abs(s.low)) : sign(x) * log10(abs(x))
    else
        x
    end
end
function inv_symlog10(x, low, high)
    if x > 0
        l = log10(high)
        x <= l ? x / l * high : exp10(x)
    elseif x < 0
        l = sign(x) * log10(abs(low))
        x >= l ? x / l * abs(s.low) : sign(x) * exp10(abs(x))
    else
        x
    end
end

inverse_transform(::typeof(identity)) = identity
inverse_transform(::typeof(log10)) = exp10
inverse_transform(::typeof(log)) = exp
inverse_transform(::typeof(log2)) = exp2
inverse_transform(::typeof(sqrt)) = x -> x ^ 2
inverse_transform(::typeof(pseudolog10)) = inv_pseudolog10
inverse_transform(F::Tuple) = map(inverse_transform, F)
inverse_transform(::typeof(logit)) = logistic
inverse_transform(s::Symlog10) = x -> inv_symlog10(x, s.low, s.high)