module LotMassing

using NPFinancial, JuMP, BlackBoxOptim, Polyhedra, Cbc #,GAMS

mutable struct RotInfo
    mat
    cr
    theta
end

mutable struct SubPoly
    points
    ladoComun
end

mutable struct PolyData
    V
    A
    b
    proyeccion
end

mutable struct ConstraintData
    V
    k
    A
    b
    norm
end

mutable struct PolyShape
    Vertices
    NumRegions
end

mutable struct FlagPlotEdif3D
    predio
    volTeor
    restSombra
    edif
    sombraVolTeor_p
    sombraVolTeor_o
    sombraVolTeor_s
    sombraEdif_p
    sombraEdif_o
    sombraEdif_s
end

mutable struct ResultadoCabida
    SalidaNormativa
    SalidaArquitectonica
    SalidaIndicadores
    SalidaTerreno
    SalidaMonetaria
    SalidaFlujoCaja
    Xopt
end

mutable struct datosCabidaPredio
    x
    y
    ladosConCalle
    ANCHOESPACIOPUBLICO
    FUSIONTERRENOS
    DISTANCIAMETRO
end

mutable struct datosCabidaNormativa
    SEPMIN
    ANTEJARDIN
    RASANTE
    ALTURAMAX
    MAXPISOS
    COEFOCUPACION
    SUBPREDIALMIN
    DENSIDADMAX
    FLAGDENSIDADBRUTA
    COEFCONSTRUCTIBILIDAD
    ESTACIONAMIENTOSPORVIV
    PORCADICESTACVISITAS
    SUPPORESTACIONAMIENTO
    ESTBICICLETAPOREST
    BICICLETASPOREST
    FLAGCAMBIOESTPORBICICLETA
    MAXSUBTE
    COEFOCUPACIONEST
    SEPESTMIN
    REDUCCIONESTPORDISTMETRO
end

mutable struct datosCabidaArquitectura
    ALTURAPISO
    PORCSUPCOMUN
    PORCTERRAZA
    ANCHOMAX
    MATCELDASCONF
    MATCONFHOR
end

mutable struct datosCabidaComercial
    SUPDEPTOUTIL
    MAXPORCTIPODEPTO
    PRECIOVENTA
    PRECIOVENTAEST
end

mutable struct datosCabidaUnit
    duracionProyecto
    CostoTerreno
    ComisionCorredor
    Demolicion
    LosaSNT
    LosaBNT
    EstacVisita
    ExtrasPostVenta
    Arquitectura
    GestionAdministracion
    ITO # por mes
    Contabilidad # por mes
    Legales
    Calculo
    MecanicaSuelo
    Topografia
    ProyectoElectrico
    ProyectoSanitario
    ProyectoSeguridad
    ProyectoPavimentacion
    ProyectoBasura
    ProyectoCalefaccion
    Marketing
    PilotSalaVentas
    NotarialesLegales
    CBR
    EmpalmesAportes
    DerechosPermisosMunicipales
    InspeccionCertificacionTecnica # por mes
    Contribuciones
    CopiaPlanosArquitctura
    CopiaPlanosObra
    HallAcceso
    JardinesPaisajismo
    HabilitacionSalaUsosMultiples
    HabilitacionPiscinaEnfermeria
    CuentasServiciosDeptos
    GastosComunes
    ContribucionesDeptos
    Seguros
    ResponsabilidadCivil
    PostVentaInmobiliaria
    SeguroVentaEnVerde
    Imprevistos
end

# flujos corresponden a meses: 0 6 12 18 24 30
mutable struct datosCabidaFlujo
    IngresosVentas
    CostoTerreno
    CostoConstruccion
    CostoHonorariosProyectos
    CostoVenta
    CostoInmobiliarioObra
    CostosHabilitacion
    CostosPuestaEnMarcha
    CostosAtencionCliente
    Imprevistos
    ingresoLineaCredito
    pagoLineaCredito
    TasaImpuestoRenta
    TasaInteresLineaCredito
    DuracionLineaCredito
    Tasaciones
end

# Rentabilidad exigida
mutable struct datosCabidaRentabilidad
    RetornoExigido
end

struct salidaNormativa
    maxNumDeptos
    maxOcupacion
    maxConstructibilidad
    maxPisos
    maxAltura
    minEstacionamientosVendibles
    minEstacionamientosVisita
    minEstacionamientosDiscapacitados
end

struct salidaArquitectonica
    numDeptosTipo
    numDeptos
    ocupacion
    constructibilidad
    numPisos
    altura
    superficieUtilSNT
    superficieComunSNT
    superficieEdificadaSNT
    superficiePorPiso
    estacionamientosVendibles
    estacionamientosVisita
    numEstacionamientos
    numBicicleteros
end

struct salidaTerreno
    superficieTerreno
    superficieBruta
    superficieEdificacion
    costoTerrenoAntesCorr
    costoUnitTerrenoAntesCorr
    CostoCorredor
    costoTerreno
    costoUnitTerreno
end

struct salidaIndicadores
    IngresosVentas
    CostoTotal
    MargenAntesImpuesto
    TirAntesImpuestos
    ImpuestoRenta
    UtilidadDespuesImpuesto
    RentabilidadTotalBruta
    RentabilidadTotalNeta
    IncidenciaTerreno
end

struct salidaMonetaria
    IngresosVentas
    CostoTotal
    CostoTerreno
    CostoUnitarioTerreno
    CostoConstruccion
    CostoHonorariosProyectos
    CostoVenta
    CostoInmobiliarioObra
    CostosHabilitacion
    CostosPuestaEnMarcha
    CostosAtencionCliente
    Imprevistos
    CostoPagoIVA
end

struct salidaFlujoCaja
    IngresosVentas
    CostoTerreno
    CostoConstruccion
    CostoHonorariosProyectos
    CostoVenta
    CostoInmobiliarioObra
    CostosHabilitacion
    CostosPuestaEnMarcha
    CostosAtencionCliente
    Imprevistos
    CostoPagoIVA
    flujoCajaNetoAntesImpuestos
    TirAntesImpuestos
    ImpuestoRenta
    NetoDespuesImpuestos
    TirDespuesImpuestos
    LineaCredito
    MontoUtilizadoLineaCredito
    InteresLineaCredito
    CostoOperacionalCredito
    ApalancadoNetoAntesImpuesto
    TirApalancadoAntesImpuestos
end

struct salidaGeometria
    config
    celdasConfig
    p1
    p2
    p3
    p4
    pos_x
    pos_y
    largo
end

export datosCabidaPredio, datosCabidaNormativa, datosCabidaArquitectura, datosCabidaComercial, datosCabidaUnit,
         datosCabidaFlujo, datosCabidaRentabilidad, RotInfo, SubPoly, PolyData, salidaArquitectonica, salidaIndicadores, salidaMonetaria,
         salidaTerreno, salidaNormativa, salidaFlujoCaja, salidaGeometria, PolyShape, FlagPlotEdif3D, ConstraintData,
         ResultadoCabida

struct particulaPso
    pos_x
    pos_y
    largo_x
    largo_y
    theta
    alt
end

struct polyPso
    poly
    Rmat
    cr
    p_1
    p_2
    p_3
    p_4
end

export particulaPso, polyPso

MATCELDASCONF_ = [[1 3 5];
                  [2 3 4]; #
                  [1 3 NaN];
                  [3 5 NaN]; #
                  [2 3 NaN]; #
                  [3 4 NaN]; #
                  [1 NaN NaN];
                  [2 NaN NaN]]; #
MATCONFHOR_ = [[1 0 1];
               [0 1 0];
               [1 0 NaN];
               [0 1 NaN];
               [0 1 NaN];
               [0 1 NaN];
               [1 NaN NaN];
               [0 NaN NaN]];

"""
MATCELDASCONF_ = [[1 3 5];
                  [1 3 NaN];
                  [1 NaN NaN]];
MATCONFHOR_ = [[1 0 1];
               [1 0 NaN];
               [1 NaN NaN]];
"""

export MATCELDASCONF_, MATCONFHOR_

include("generaMatCeldasConf.jl")
include("infoPredio.jl")
include("calculaAnguloRotacion.jl")
include("nonconv2sumofconv_v2.jl")
include("ismembern.jl")
include("pso.jl")
include("plotCabidaOptima.jl")
include("generaCalles.jl")
include("generaSombraEdificio.jl")
include("optiEdificio.jl")
include("displayResults.jl")
include("plotBaseEdificio3d_v2.jl")
include("poly2D.jl")
include("polyShape.jl")
include("evol.jl")
include("resultConverter_v2.jl")
include("executaCalculoCabidas.jl")
include("generaVol3d_v4.jl")
include("generaSombraTeor_v3.jl")
include("randomPointsNearVertices.jl")
include("ajusteArea.jl")
include("generaSupBruta.jl")
include("combinaSoluciones_v2.jl")

export generaMatCeldasConf, infoPredio, calculaAnguloRotacion, ismembern, plotCabidaOptima,
       pso, generaCalles, generaSombraEdificio,
       optiEdificio,  displayResults, evol, poly2D, polyShape,
       nonconv2sumofconv_v2, resultConverter_v2, plotBaseEdificio3d_v2,
       executaCalculoCabidas, generaVol3d_v4, generaSombraTeor_v3, randomPointsNearVertices, ajusteArea, generaSupBruta,
       combinaSoluciones_v2
end


using Flight

using Base.Iterators
using OrdinaryDiffEq
using SciMLBase
using LinearAlgebra
using BenchmarkTools
using GLFW

################################## AC2 ######################################

function demo_rt_xbox()

    println("Generalize this for non-rt")

    h_trn = AltOrth(609.5);

    trn = HorizontalTerrain(altitude = h_trn);
    atm = System(AtmosphereDescriptor());
    ac = System(C172Aircraft());
    ac_mdl = Model(ac, (trn, atm); dt = 0.02, t_end = 40, adaptive = false, solver = RK4(), y_saveat = 0.02);

    kin_init = KinInit(v_eOb_b = [10, 0, 0],
                        ω_lb_b = [0, 0, 0],
                        q_nb = REuler(ψ = π, θ = 0.0, φ = 0.00),
                        Ob = Geographic(LatLon(ϕ = deg2rad(40.531818), λ = deg2rad(-3.574862)),
                                        h_trn + 0.85 + 0.0));

    atm.u.wind.v_ew_n[1] = 0
    ac.u.throttle = 0

    init!(ac, kin_init)
    #since the model was instantiated in advance, we need this to change its internal state vector
    reinit!(ac_mdl, ac.x)

    xp = XPInterface()
    disable_physics(xp)
    set_position(xp, kin_init)

    output_div = 1

    #add any joysticks connected before GLFW was imported
    init_joysticks()

    #enable detection of further joystick connections or disconnections
    GLFW.SetJoystickCallback(joystick_callback)
    GLFW.PollEvents() #check for newly connected joysticks

    # window = GLFW.CreateWindow(640, 480, "GLFW Callback Test")
    # GLFW.MakeContextCurrent(window)

    t_wall = time()

    # for i in take(ac_mdl.integrator, 5)
    for i in ac_mdl.integrator

        #when using the integrator as an iterator, we don't need to step, it is done
        #automatically at the beginning of each iteration. therefore, the dt we need
        #for pacing the simulation is not the proposed dt for the next step, but the
        #one in the step the integrator has already taken.

        #retrieve the dt just taken by the integrator
        dt = ac_mdl.dt

        #compute the wall time corresponding to the newly updated simulation
        t_wall_next = t_wall + dt

        #busy wait until the wall time catches up
        while (time() < t_wall_next) end
        # println(time()-t_wall_next)

        t_wall = t_wall_next

        if ac_mdl.success_iter % output_div == 0
            set_position(xp, ac_mdl.sys.y.kin.pos)
        end

        for joystick in values(connected_joysticks)
            update_joystick(joystick)
            Aircraft.assign_joystick_inputs!(ac, joystick)
            println(ac.u.throttle)
        end

        # Swap front and back buffers
        # GLFW.SwapBuffers(window)

        #when GLFW is implemented, build the model with t_end = Inf and use this to break
        # if !GLFW.WindowShouldClose(window)
        #     break
        # end

    end

    plot_settings = (linewidth=2, margin = 10mm, guidefontsize = 12)
    plots(ac_mdl; save_path = joinpath("tmp", "plots_demo_rt"), plot_settings...)

end


#= #############################################################################
Mesh Types

The mesh is generated via preprocessing from the M1d1d struct just before the
simulation.
############################################################################# =#
abstract type AbstractM1d1dMesh end;

"""
M1dCell:
Represents the structure for a regular, equaly sized 1d mesh:

    vol: volume of cell
    len: length of cell
    dA: cross section area normal to direction of len
"""
struct M1dCell{T <: AbstractFloat}
    vol::T
    len::T
    dA::T
end

"""
SubMesh1D:
Mesh type for the additiona discretization of the scrap pieces
here a symmetry conditin will be used, so the real no of cells
is ceil(no_cells_scrap_piece/2)
"""
struct SubMesh1D
    no_cells::Integer
    sub_cells::M1dCell
end

"""
Bulk1dMesh:
Mesh consiting out of gas_cells and solid_cells which overlap each other and may
exchange energy terms during the modeling. Both cells types define 1d
discretized meshes with constant spacing and cell volume.

    no_cells: number cells over length of shaft (bulk)
    A_fs: Interface area between solid and fluid cell(s)
    gas_cells: Properties of fluid (gas) cells
    solid_cells: Properties of solid cells
    solid_1d_submesh: If existent (!= nothing) proerties of the solid 1d submesh for thermal heat transfer inside the scrap pieces in each solid bulk (shaft) cell
"""
struct M1d1dMesh <: AbstractM1d1dMesh
    no_cells::Integer
    A_fs::AbstractFloat
    gas_cells::M1dCell
    solid_cells::M1dCell
    solid_1d_submesh::Union{Nothing,SubMesh1D}
end

#= #############################################################################
Mesh Value Setting Types
############################################################################# =#
"""
M1d1dVals:
Structure for storing (property) values for each M1d1d cell
    f: values of fluid cells
    s: values of solid cells
    s_sub: values for solid sub cells
"""
struct M1d1dVals{T <: AbstractFloat}
    f::Vector{T}
    s::Vector{T}
    s_sub::Union{Nothing, Vector{Vector{T}}}
end

Base.copy(S::M1d1dVals) = M1d1dVals(S.f, S.s, S.s_sub)

#= #############################################################################
Results Types
############################################################################# =#

"""
ResultsTimeStepWithoutScrapDisc:
Results for temperature distribution in bulk and offgas
"""
struct Results1d1d
    t::Float64
    T::M1d1dVals
end

#= #############################################################################
Outer Constructor Functions
############################################################################# =#
function M1d1dMesh(m::M1d1d, pid::Integer)

    # number bulk cells
    no_c = m.mesh_settings[pid].no_cells_bulk

    # length of bulk cell
    len_c =  m.bulk[pid].length / no_c

    # bulk volume
    V_b = m.bulk[pid].length * m.bulk[pid].cross_sect_surf_area

    # bulk volume of single cell
    V_bc = V_b / no_c

    V_sc = (1.0 - m.bulk[pid].porosity) * V_bc

    V_fc = m.bulk[pid].porosity * V_bc

    dA_sc = (1-m.bulk[pid].porosity) *  m.bulk[pid].cross_sect_surf_area

    dA_fc = m.bulk[pid].porosity * m.bulk[pid].cross_sect_surf_area

    solid_cells = model1d1d.M1dCell(V_sc, len_c, dA_sc)

    gas_cells = model1d1d.M1dCell(V_fc, len_c, dA_fc)

    weight_scrap_piece =  (m.bulk[pid].scrap_props.vol * pX_bounded(
        m.bulk[pid].scrap_props.thermal_props.rho, m.bulk[pid].T_charged))

    weight_bulk_cell = V_bc * m.bulk[pid].density

    no_scrap_pieces_per_bulk_cell = weight_bulk_cell / weight_scrap_piece

    # surface area between solid and gas cells
    A_fs_c =  ( m.bulk[pid].scrap_pieces_contact_factor *
            no_scrap_pieces_per_bulk_cell * m.bulk[pid].scrap_props.surf_area )

    if !isnothing(m.mesh_settings[pid].no_cells_scrap_piece)
        # cross section surface in thickness direction for solid scrap cell(s)
        no_sub_c = m.mesh_settings[pid].no_cells_scrap_piece

        dA_s_sub = m.bulk[pid].scrap_props.char_cross_section_area *
                no_scrap_pieces_per_bulk_cell

        len_s_sub = m.bulk[pid].scrap_props.char_thickness / (2.0 * no_sub_c)

        vol_s_sub = V_sc / (2.0 * no_sub_c)

        solid_1d_subcell = M1dCell(vol_s_sub, len_s_sub, dA_s_sub)

        solid_1d_submesh = SubMesh1D(no_sub_c, solid_1d_subcell)

    else
        solid_1d_submesh = nothing
    end

    return M1d1dMesh(no_c, A_fs_c, gas_cells, solid_cells, solid_1d_submesh)
end

function M1d1dVals(mesh1d1d::M1d1dMesh;
    init_val = 0.0)::M1d1dVals

    init_vec = ones(Float64, mesh1d1d.no_cells) .* init_val

    if !isnothing(mesh1d1d.solid_1d_submesh)
        sub_mesh_init = Vector{Vector{Float64}}(undef, mesh1d1d.no_cells)

        for i = 1:length(sub_mesh_init)
            sub_mesh_init[i] = (ones(Float64, mesh1d1d.solid_1d_submesh.no_cells)
            .* init_val)
        end
    else
        sub_mesh_init = nothing
    end

    return M1d1dVals(copy(init_vec), copy(init_vec), sub_mesh_init)
end

#= #############################################################################
Other functions
############################################################################# =#

function Base.isequal(m1::M1d1dMesh, m2::M1d1dMesh)

    return
    (
        m1.no_cells ==  m2.no_cells &&
        isapprox(m1.A_fs, m2.A_fs) &&
        m1.gas_cells == m2.gas_cells &&
        m1.soid_cells == m2.soid_cells
    )

end

function Base.isequal(sm1::Union{Nothing, SubMesh1D}, sm2::Union{Nothing, SubMesh1D})
    return
    (
        (isnothing(sm1) && isnothing(sm2)) ||
        (
            sm1.no_cells == sm2.no_cells &&
            sm1.sub_cells == sm2.sub_cells
        )
    )
end

function Base.isequal(c1::M1dCell, c2::M1dCell)
    return
    (
        isapprox(c1.vol, c2.vol) &&
        isapprox(c1.len, c2.len) &&
        isapprox(c1.dA, c2.dA)
    )
end

"""
print1d1dvals:
Formated output of M1d1dVals struct.
"""
function print1d1dvals(v::M1d1dVals)

    println("T-off-gas: \t T-solid")
    for i = 1:1:length(v.f)
        Printf.@printf("% 6.2f \t % 6.2f",v.f[i],v.s[i])

        if !isnothing(v.s_sub)
            print("---->(" )
            for j=1:1:length(v.s_sub[i])
                Printf.@printf("% 6.2f,",v.s_sub[i][j])
            end
            print(")" )
        end
        print("\n")
    end
end

function printInfo(mesh::M1d1dMesh;tabs::Int=0)

    if tabs > 0
        tabs_str = "\t".^tabs
    else
        tabs_str = ""
    end

    print_log(m1d1d_logfile, tabs_str * "Mesh-Info:")

    print_log(m1d1d_logfile, tabs_str * "No bulk cells: " * string(mesh.no_cells))
    print_log(m1d1d_logfile, tabs_str * "Cell gas-solid surface area: " * string(mesh.A_fs))
    print_log(m1d1d_logfile, tabs_str * "Gas cells:")
    printInfo(mesh.gas_cells;tabs = (tabs + 1))
    print_log(m1d1d_logfile, tabs_str * "Solid cells:")
    printInfo(mesh.solid_cells;tabs = (tabs + 1))

    if !isnothing(mesh.solid_1d_submesh)
        print_log(m1d1d_logfile, tabs_str * "Submesh:")
        print_log(m1d1d_logfile, tabs_str * "\t no sub cells: " *
                string(mesh.solid_1d_submesh.no_cells))
            print_log(m1d1d_logfile, tabs_str * "\t Subcells:")
            printInfo(mesh.solid_1d_submesh.sub_cells;tabs = (tabs + 2))
    end
end

function printInfo(mesh_cell::M1dCell; tabs::Int=0)

    if tabs > 0
        tabs_str = "\t".^tabs
    else
        tabs_str = ""
    end
    print_log(m1d1d_logfile, tabs_str * "vol: " * string(mesh_cell.vol) )
    print_log(m1d1d_logfile, tabs_str * "len: " * string(mesh_cell.len) )
    print_log(m1d1d_logfile, tabs_str * "dA: " * string(mesh_cell.dA) )
end


mutable struct gpuCTPF <: TopicModel
	"GPU accelerated collaborative topic Poisson factorization."

	K::Int
	M::Int
	V::Int
	U::Int
	N::Vector{Int}
	C::Vector{Int}
	R::Vector{Int}
	corp::Corpus
	topics::VectorList{Int}
	scores::Matrix{Float32}
	libs::VectorList{Int}
	drecs::VectorList{Int}
	urecs::VectorList{Int}
	a::Float32
	b::Float32
	c::Float32
	d::Float32
	e::Float32
	f::Float32
	g::Float32
	h::Float32
	alef::Matrix{Float32}
	he::Matrix{Float32}
	bet::Vector{Float32}
	vav::Vector{Float32}
	gimel::VectorList{Float32}
	gimel_old::VectorList{Float32}
	zayin::VectorList{Float32}
	dalet::Vector{Float32}
	het::Vector{Float32}
	phi::MatrixList{Float32}
	xi::MatrixList{Float32}
	elbo::Float32
	device::cl.Device
	context::cl.Context
	queue::cl.CmdQueue
	alef_kernel::cl.Kernel
	he_kernel::cl.Kernel
	bet_kernel::cl.Kernel
	vav_kernel::cl.Kernel
	gimel_kernel::cl.Kernel
	zayin_kernel::cl.Kernel
	dalet_kernel::cl.Kernel
	het_kernel::cl.Kernel
	phi_kernel::cl.Kernel
	phi_norm_kernel::cl.Kernel
	xi_kernel::cl.Kernel
	xi_norm_kernel::cl.Kernel
	N_cumsum_buffer::cl.Buffer{Int}
	J_cumsum_buffer::cl.Buffer{Int}
	R_cumsum_buffer::cl.Buffer{Int}
	Y_cumsum_buffer::cl.Buffer{Int}
	terms_buffer::cl.Buffer{Int}
	terms_sortperm_buffer::cl.Buffer{Int}
	counts_buffer::cl.Buffer{Int}
	readers_buffer::cl.Buffer{Int}
	ratings_buffer::cl.Buffer{Int}
	readers_sortperm_buffer::cl.Buffer{Int}
	alef_buffer::cl.Buffer{Float32}
	he_buffer::cl.Buffer{Float32}
	bet_buffer::cl.Buffer{Float32}
	vav_buffer::cl.Buffer{Float32}
	gimel_buffer::cl.Buffer{Float32}
	zayin_buffer::cl.Buffer{Float32}
	dalet_buffer::cl.Buffer{Float32}
	het_buffer::cl.Buffer{Float32}
	phi_buffer::cl.Buffer{Float32}
	xi_buffer::cl.Buffer{Float32}

	function gpuCTPF(corp::Corpus, K::Integer)
		check_corp(corp)
		K > 0 || throw(ArgumentError("Number of topics must be a positive integer."))

		M, V, U = size(corp)
		N = [length(doc) for doc in corp]
		C = [size(doc) for doc in corp]
		R = [length(doc.readers) for doc in corp]

		topics = [collect(1:V) for _ in 1:K]
		scores = zeros(M, U)

		libs = [Int[] for _ in 1:U]
		for d in 1:M, u in corp[d].readers
			push!(libs[u], d)
		end

		urecs = VectorList{Int}(undef, U)
		for u in 1:U
			ur = trues(M)
			ur[libs[u]] .= false
			urecs[u] = findall(ur)
		end

		drecs = VectorList{Int}(undef, M)
		for d in 1:M
			nr = trues(U)
			nr[corp[d].readers] .= false
			drecs[d] = findall(nr)
		end

		a, b, c, d, e, f, g, h = fill(0.1f0, 8)

		alef = exp.(rand(Dirichlet(V, 1.0f0), K)' .- 0.5f0)
		he = ones(Float32, K, U)
		bet = ones(Float32, K)
		vav = ones(Float32, K)
		gimel = [ones(Float32, K) for _ in 1:M]
		gimel_old = deepcopy(gimel)
		zayin = [ones(Float32, K) for _ in 1:M]
		dalet = ones(Float32, K)
		het = ones(Float32, K)
		phi = [fill(Float32(1/K), K, N[d]) for d in 1:M]
		xi = [fill(Float32(1/2K), 2K, R[d]) for d in 1:M]
		elbo = 0f0

		device, context, queue = cl.create_compute_context()

		alef_program = cl.Program(context, source=CTPF_ALEF_c) |> cl.build!
		he_program = cl.Program(context, source=CTPF_HE_c) |> cl.build!
		bet_program = cl.Program(context, source=CTPF_BET_c) |> cl.build!
		vav_program = cl.Program(context, source=CTPF_VAV_c) |> cl.build!
		gimel_program = cl.Program(context, source=CTPF_GIMEL_c) |> cl.build!
		zayin_program = cl.Program(context, source=CTPF_ZAYIN_c) |> cl.build!
		dalet_program = cl.Program(context, source=CTPF_DALET_c) |> cl.build!
		het_program = cl.Program(context, source=CTPF_HET_c) |> cl.build!
		phi_program = cl.Program(context, source=CTPF_PHI_c) |> cl.build!
		phi_norm_program = cl.Program(context, source=CTPF_PHI_NORM_c) |> cl.build!
		xi_program = cl.Program(context, source=CTPF_XI_c) |> cl.build!
		xi_norm_program = cl.Program(context, source=CTPF_XI_NORM_c) |> cl.build!

		alef_kernel = cl.Kernel(alef_program, "update_alef")
		he_kernel = cl.Kernel(he_program, "update_he")
		bet_kernel = cl.Kernel(bet_program, "update_bet")
		vav_kernel = cl.Kernel(vav_program, "update_vav")
		gimel_kernel = cl.Kernel(gimel_program, "update_gimel")
		zayin_kernel = cl.Kernel(zayin_program, "update_zayin")
		dalet_kernel = cl.Kernel(dalet_program, "update_dalet")
		het_kernel = cl.Kernel(het_program, "update_het")
		phi_kernel = cl.Kernel(phi_program, "update_phi")
		phi_norm_kernel = cl.Kernel(phi_norm_program, "normalize_phi")
		xi_kernel = cl.Kernel(xi_program, "update_xi")
		xi_norm_kernel = cl.Kernel(xi_norm_program, "normalize_xi")

		model = new(K, M, V, U, N, C, R, copy(corp), topics, scores, libs, drecs, urecs, a, b, c, d, e, f, g, h, alef, he, bet, vav, gimel, gimel_old, zayin, dalet, het, phi, xi, elbo, device, context, queue, alef_kernel, he_kernel, bet_kernel, vav_kernel, gimel_kernel, zayin_kernel, dalet_kernel, het_kernel, phi_kernel, phi_norm_kernel, xi_kernel, xi_norm_kernel)
		return model
	end
end

function Elogpya(model::gpuCTPF, d::Int)
	"Compute E_q[log(P(ya))]."

	x = -dot(model.gimel[d] ./ (model.dalet .* model.vav), sum(model.he, dims=2))
	readers, ratings = model.corp[d].readers, model.corp[d].ratings
	for (u, (re, ra)) in enumerate(zip(readers, ratings)), i in 1:model.K
		binom = Binomial(ra, model.xi[d][i,u])
		x += (ra * model.xi[d][i,u] * (digamma(model.gimel[d][i]) - log(model.dalet[i]) + digamma(model.he[i,re]) - log(model.vav[i])) - sum([pdf(binom, y) * loggamma(y + 1) for y in 0:ra]))
	end
	return x
end

function Elogpyb(model::gpuCTPF, d::Int)
	"Compute E_q[log(P(yb))]."

	x = -dot(model.zayin[d] ./ (model.het .* model.vav), sum(model.he, dims=2))
	readers, ratings = model.corp[d].readers, model.corp[d].ratings
	for (u, (re, ra)) in enumerate(zip(readers, ratings)), i in 1:model.K
		binom = Binomial(ra, model.xi[d][model.K+i,u])
		x += (ra * model.xi[d][model.K+i,u] * (digamma(model.zayin[d][i]) - log(model.het[i]) + digamma(model.he[i,re]) - log(model.vav[i])) - sum([pdf(binom, y) * loggamma(y + 1) for y in 0:ra]))
	end
	return x
end

function Elogpz(model::gpuCTPF, d::Int)
	"Compute E_q[log(P(z))]."

	x = -dot(model.gimel[d] ./ (model.dalet .* model.bet), sum(model.alef, dims=2))
	terms, counts = model.corp[d].terms, model.corp[d].counts
	for (n, (j, c)) in enumerate(zip(terms, counts)), i in 1:model.K
		binom = Binomial(c, model.phi[d][i,n])
		x += (c * model.phi[d][i,n] * (digamma(model.gimel[d][i]) - log(model.dalet[i]) + digamma(model.alef[i,j]) - log(model.bet[i])) - sum([pdf(binom, z) * loggamma(z + 1) for z in 0:c]))
	end
	return x
end

function Elogpbeta(model::gpuCTPF)
	"Compute E_q[log(P(beta))]."

	x = model.V * model.K * (model.a * log(model.b) - loggamma(model.a))
	for j in 1:model.V, i in 1:model.K
		x += (model.a - 1) * (digamma(model.alef[i,j]) - log(model.bet[i])) - model.b * model.alef[i,j] / model.bet[i]
	end
	return x
end

function Elogptheta(model::gpuCTPF, d::Int)
	"Compute E_q[log(P(theta))]."

	x = model.K * (model.c * log(model.d) - loggamma(model.c))
	for i in 1:model.K
		x += (model.c - 1) * (digamma(model.gimel[d][i]) - log(model.dalet[i])) - model.d * model.gimel[d][i] / model.dalet[i]
	end
	return x
end

function Elogpeta(model::gpuCTPF)
	"Compute E_q[log(P(eta))]."

	x = model.U * model.K * (model.e * log(model.f) - loggamma(model.e))
	for u in 1:model.U, i in 1:model.K
		x += (model.e - 1) * (digamma(model.he[i,u]) - log(model.vav[i])) - model.f * model.he[i,u] / model.vav[i]
	end
	return x
end

function Elogpepsilon(model::gpuCTPF, d::Int)
	"Compute E_q[log(P(epsilon))]."

	x = model.K * (model.g * log(model.h) - loggamma(model.g))
	for i in 1:model.K
		x += (model.g - 1) * (digamma(model.zayin[d][i]) - log(model.het[i])) - model.h * model.zayin[d][i] / model.het[i]
	end
	return x
end

function Elogqy(model::gpuCTPF, d::Int)
	"Compute E_q[log(q(y))]."

	x = 0
	for (u, ra) in enumerate(model.corp[d].ratings)
		x -= entropy(Multinomial(ra, model.xi[d][:,u]))
	end
	return x
end

function Elogqz(model::gpuCTPF, d::Int)
	"Compute E_q[log(q(z))]."

	x = 0
	for (n, c) in enumerate(model.corp[d].counts)
		x -= entropy(Multinomial(c, model.phi[d][:,n]))
	end
	return x
end

function Elogqbeta(model::gpuCTPF)
	"Compute E_q[log(q(beta))]."

	x = 0
	for j in 1:model.V, i in 1:model.K
		x -= entropy(Gamma(model.alef[i,j], 1 / model.bet[i]))
	end
	return x
end

function Elogqtheta(model::gpuCTPF, d::Int)
	"Compute E_q[log(q(theta))]."

	x = 0
	for i in 1:model.K
		x -= entropy(Gamma(model.gimel[d][i], 1 / model.dalet[i]))
	end
	return x
end

function Elogqeta(model::gpuCTPF)
	"Compute E_q[log(q(eta))]."

	x = 0
	for u in 1:model.U, i in 1:model.K
		x -= entropy(Gamma(model.he[i,u], 1 / model.vav[i]))
	end
	return x
end

function Elogqepsilon(model::gpuCTPF, d::Int)
	"Compute E_q[log(q(epsilon))]."

	x = 0
	for i in 1:model.K
		x -= entropy(Gamma(model.zayin[d][i], 1 / model.het[i]))
	end
	return x
end

function update_elbo!(model::gpuCTPF)
	"Update the evidence lower bound."

	model.elbo = Elogpbeta(model) + Elogpeta(model) - Elogqbeta(model) - Elogqeta(model)
	for d in 1:model.M
		model.elbo += Elogpya(model, d) + Elogpyb(model, d) + Elogpz(model, d) + Elogptheta(model, d) + Elogpepsilon(model, d) - Elogqy(model, d) - Elogqz(model, d) - Elogqtheta(model, d) - Elogqepsilon(model, d)
	end

	return model.elbo
end

const CTPF_ALEF_c =
"""
kernel void
update_alef(long K,
			float a,
			const global long *J_cumsum,
			const global long *counts,
			const global long *terms_sortperm,
			const global float *phi,
			global float *alef)

			{
			long i = get_global_id(0);
			long j = get_global_id(1);

			float acc = 0.0f;

			for (long w=J_cumsum[j]; w<J_cumsum[j+1]; w++)
				acc += counts[terms_sortperm[w]] * phi[K * terms_sortperm[w] + i];

			alef[K * j + i] = a + acc;
			}
			"""

function update_alef!(model::gpuCTPF)
	"Update alef."
	"Analytic."

	model.queue(model.alef_kernel, (model.K, model.V), nothing, model.K, model.a, model.J_cumsum_buffer, model.counts_buffer, model.terms_sortperm_buffer, model.phi_buffer, model.alef_buffer)
end

const CTPF_BET_c =
"""
kernel void
update_bet(	long K,
			long M,
			float b,
			const global float *alef,
			const global float *gimel,
			const global float *dalet,
			global float *bet)

			{
			long i = get_global_id(0);

			float acc = 0.0f;

			for (long d=0; d<M; d++)
				acc += gimel[K * d + i];

			bet[i] = b + acc / dalet[i];
			}
			"""

function update_bet!(model::gpuCTPF)
	"Update bet."
	"Analytic."

	model.queue(model.bet_kernel, model.K, nothing, model.K, model.M, model.b, model.alef_buffer, model.gimel_buffer, model.dalet_buffer, model.bet_buffer)
end

const CTPF_GIMEL_c =
"""
kernel void
update_gimel(	long K,
				float c,
				const global long *N_cumsum,
				const global long *R_cumsum,
				const global long *counts,
				const global long *ratings,
				const global float *phi,
				const global float *xi,
				global float *gimel)

				{
				long i = get_global_id(0);
				long d = get_global_id(1);

				float acc_phi = 0.0f;
				float acc_xi = 0.0f;

				for (long n=N_cumsum[d]; n<N_cumsum[d+1]; n++)
					acc_phi += phi[K * n + i] * counts[n];

				for (long r=R_cumsum[d]; r<R_cumsum[d+1]; r++)
					acc_xi += xi[2 * K * r + i] * ratings[r];

				gimel[K * d + i] = c + acc_phi + acc_xi;
				}
				"""

function update_gimel!(model::gpuCTPF)
	"Update gimel."
	"Analytic."

	model.gimel_old = model.gimel

	model.queue(model.gimel_kernel, (model.K, model.M), nothing, model.K, model.c, model.N_cumsum_buffer, model.R_cumsum_buffer, model.counts_buffer, model.ratings_buffer, model.phi_buffer, model.xi_buffer, model.gimel_buffer)
	@host model.gimel_buffer
end

const CTPF_DALET_c =
"""
kernel void
update_dalet(	long K,
				long V,
				long U,
				float d,
				const global float *alef,
				const global float *bet,
				const global float *he,
				const global float *vav,
				global float *dalet)

				{
				long i = get_global_id(0);

				float acc_alef = 0.0f;
				float acc_he = 0.0f;

				for (long j=0; j<V; j++)
					acc_alef += alef[K * j + i];

				for (long u=0; u<U; u++)
					acc_he += he[K * u + i];

				dalet[i] = d + acc_alef / bet[i] + acc_he / vav[i];
				}
				"""

function update_dalet!(model::gpuCTPF)
	"Update dalet."
	"Analytic."

	model.queue(model.dalet_kernel, model.K, nothing, model.K, model.V, model.U, model.d, model.alef_buffer, model.bet_buffer, model.he_buffer, model.vav_buffer, model.dalet_buffer)
end

const CTPF_HE_c =
"""
kernel void
update_he(	long K,
			float e,
			const global long *Y_cumsum,
			const global long *ratings,
			const global long *readers_sortperm,
			const global float *xi,
			global float *he)

			{
			long i = get_global_id(0);
			long u = get_global_id(1);

			float acc = 0.0f;

			for (long r=Y_cumsum[u]; r<Y_cumsum[u+1]; r++)
				acc += ratings[readers_sortperm[r]] * (xi[2 * K * readers_sortperm[r] + i] + xi[K * (2 * readers_sortperm[r] + 1) + i]);

			he[K * u + i] = e + acc;
			}
			"""

function update_he!(model::gpuCTPF)
	"Update he."
	"Analytic."

	(model.U > 0) && model.queue(model.he_kernel, (model.K, model.U), nothing, model.K, model.e, model.Y_cumsum_buffer, model.ratings_buffer, model.readers_sortperm_buffer, model.xi_buffer, model.he_buffer)
end

const CTPF_VAV_c =
"""
kernel void
update_vav(	long K,
			long M,
			float f,
			const global float *gimel,
			const global float *dalet,
			const global float *zayin,
			const global float *het,
			global float *vav)

			{
			long i = get_global_id(0);

			float acc_gimel = 0.0f;
			float acc_zayin = 0.0f;

			for (long d=0; d<M; d++)
			{
				acc_gimel += gimel[K * d + i];
				acc_zayin += zayin[K * d + i];
			}

			vav[i] = f + acc_gimel / dalet[i] + acc_zayin / het[i];
			}
			"""

function update_vav!(model::gpuCTPF)
	"Update vav."
	"Analytic."

	model.queue(model.vav_kernel, model.K, nothing, model.K, model.M, model.f, model.gimel_buffer, model.dalet_buffer, model.zayin_buffer, model.het_buffer, model.vav_buffer)
end

const CTPF_ZAYIN_c =
"""
kernel void
update_zayin(	long K,
				float g,
				const global long *R_cumsum,
				const global long *ratings,
				const global float *xi,
				global float *zayin)

				{
				long i = get_global_id(0);
				long d = get_global_id(1);

				float acc = 0.0f;

				for (long r=R_cumsum[d]; r<R_cumsum[d+1]; r++)
					acc += xi[K * (2 * r + 1) + i] * ratings[r];

				zayin[K * d + i] = g + acc;
				}
				"""

function update_zayin!(model::gpuCTPF)
	"Update zayin."
	"Analytic."

	model.queue(model.zayin_kernel, (model.K, model.M), nothing, model.K, model.g, model.R_cumsum_buffer, model.ratings_buffer, model.xi_buffer, model.zayin_buffer)
end

const CTPF_HET_c =
"""
kernel void
update_het(	long K,
			long U,
			float h,
			const global float *he,
			const global float *vav,
			global float *het)

			{
			long i = get_global_id(0);

			float acc = 0.0f;

			for (long u=0; u<U; u++)
				acc += he[K * u + i];

			het[i] = h + acc / vav[i];
			}
			"""

function update_het!(model::gpuCTPF)
	"Update het."
	"Analytic."

	model.queue(model.het_kernel, model.K, nothing, model.K, model.U, model.h, model.he_buffer, model.vav_buffer, model.het_buffer)
end

const CTPF_PHI_c =
"""
$(DIGAMMA_c)

kernel void
update_phi(	long K,
			const global long *N_cumsum,
			const global long *terms,
			const global float *alef,
			const global float *bet,
			const global float *gimel,
			const global float *dalet,
			global float *phi)

			{

			long i = get_global_id(0);
			long d = get_global_id(1);

			float gdb = digamma(gimel[K * d + i]) - log(dalet[i]) - log(bet[i]);

			for (long n=N_cumsum[d]; n<N_cumsum[d+1]; n++)
				phi[K * n + i] = gdb + digamma(alef[K * terms[n] + i]);
			}
			"""

const CTPF_PHI_NORM_c =
"""
kernel void
normalize_phi(	long K,
				global float *phi)

				{
				long dn = get_global_id(0);

				float maxval = 0.0f;

				for (long i=0; i<K; i++)
					if (phi[K * dn + i] > maxval)
						maxval = phi[K * dn + i];

				float normalizer = 0.0f;

				for (long i=0; i<K; i++)
				{
					phi[K * dn + i] -= maxval;
					normalizer += exp(phi[K * dn + i]);
				}

				for (long i=0; i<K; i++)
					phi[K * dn + i] = exp(phi[K * dn + i]) / normalizer;
				}
				"""

function update_phi!(model::gpuCTPF)
	"Update phi."
	"Analytic."

	model.queue(model.phi_kernel, (model.K, model.M), nothing, model.K, model.N_cumsum_buffer, model.terms_buffer, model.alef_buffer, model.bet_buffer, model.gimel_buffer, model.dalet_buffer, model.phi_buffer)
	model.queue(model.phi_norm_kernel, sum(model.N), nothing, model.K, model.phi_buffer)
end

const CTPF_XI_c =
"""
$(DIGAMMA_c)

kernel void
update_xi(	long K,
			const global long *R_cumsum,
			const global long *readers,
			const global float *bet,
			const global float *gimel,
			const global float *dalet,
			const global float *he,
			const global float *vav,
			const global float *zayin,
			const global float *het,
			global float *xi)

			{
			long i = get_global_id(0);
			long d = get_global_id(1);

			float gdv = digamma(gimel[K * d + i]) - log(dalet[i]) - log(bet[i]);
			float zhv = digamma(zayin[K * d + i]) - log(het[i]) - log(vav[i]);

			for (long r=R_cumsum[d]; r<R_cumsum[d+1]; r++)
			{
				xi[2 * K * r + i] = gdv + digamma(he[K * readers[r] + i]);
				xi[K * (2 * r + 1) + i] = zhv + digamma(he[K * readers[r] + i]);
			}
			}
			"""

const CTPF_XI_NORM_c =
"""
kernel void
normalize_xi(	long K,
				global float *xi)

				{
				long dr = get_global_id(0);

				float maxval = 0.0f;

				for (long i=0; i<2*K; i++)
					if (xi[2 * K * dr + i] > maxval)
						maxval = xi[2 * K * dr + i];

				float normalizer = 0.0f;

				for (long i=0; i<2*K; i++)
				{
					xi[2 * K * dr + i] -= maxval;
					normalizer += exp(xi[2 * K * dr + i]);
				}

				for (long i=0; i<2*K; i++)
					xi[2 * K * dr + i] = exp(xi[2 * K * dr + i]) / normalizer;
				}
				"""

function update_xi!(model::gpuCTPF)
	"Update xi."
	"Analytic."

	if sum(model.R) > 0
		model.queue(model.xi_kernel, (model.K, model.M), nothing, model.K, model.R_cumsum_buffer, model.readers_buffer, model.bet_buffer, model.gimel_buffer, model.dalet_buffer, model.he_buffer, model.vav_buffer, model.zayin_buffer, model.het_buffer, model.xi_buffer)
		model.queue(model.xi_norm_kernel, sum(model.R), nothing, model.K, model.xi_buffer)
	end
end

function train!(model::gpuCTPF; iter::Integer=150, tol::Real=1.0, viter::Integer=10, vtol::Real=1/model.K^2, checkelbo::Real=1, printelbo::Bool=true)
	"Coordinate ascent optimization procedure for GPU accelerated collaborative topic Poisson factorization variational Bayes algorithm."

	check_model(model)
	all([tol, vtol] .>= 0)												|| throw(ArgumentError("Tolerance parameters must be nonnegative."))
	all([iter, viter] .>= 0)											|| throw(ArgumentError("Iteration parameters must be nonnegative."))
	(isa(checkelbo, Integer) & (checkelbo > 0)) | (checkelbo == Inf)	|| throw(ArgumentError("checkelbo parameter must be a positive integer or Inf."))
	all([isempty(doc) for doc in model.corp]) ? (iter = 0) : update_buffer!(model)
	(checkelbo <= iter) && update_elbo!(model)

	for k in 1:iter
		for _ in 1:viter
			update_xi!(model)
			update_phi!(model)
			update_zayin!(model)
			update_gimel!(model)

			if sum([norm(model.gimel[d] - model.gimel_old[d]) for d in 1:model.M]) < model.M * vtol
				break
			end
		end
		update_he!(model)
		update_alef!(model)
		update_dalet!(model)
		update_het!(model)
		update_bet!(model)
		update_vav!(model)

		if check_elbo!(model, checkelbo, printelbo, k, tol)
			break
		end
	end

	(iter > 0) && update_host!(model)
	Ebeta = model.alef ./ model.bet
	model.topics = [reverse(sortperm(vec(Ebeta[i,:]))) for i in 1:model.K]

	Eeta = model.he ./ model.vav
	for d in 1:model.M
		Etheta = model.gimel[d] ./ model.dalet
		Eepsilon = model.zayin[d] ./ model.het
		model.scores[d,:] = sum(Eeta .* (Etheta + Eepsilon), dims=1)
	end

	model.urecs = VectorList{Int}(undef, model.U)
	for u in 1:model.U
		ur = trues(model.M)
    	ur[model.libs[u]] .= false
		model.urecs[u] = findall(ur)[reverse(sortperm(model.scores[ur,u]))]
	end

	model.drecs = VectorList{Int}(undef, model.M)
	for d in 1:model.M
    	nr = trues(model.U)
    	nr[model.corp[d].readers] .= false
    	model.drecs[d] = findall(nr)[reverse(sortperm(model.scores[d,nr]))]
	end
	nothing
end


@testset "testing copper plate network construction" begin
    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PSI.CopperPlatePowerModel, sys5b, time_range);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PSI.CopperPlatePowerModel, sys5b, time_range);
    PSI.construct_network!(ps_model, PSI.CopperPlatePowerModel, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 120
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 24

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)
    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL

    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods; parameters = false)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PSI.CopperPlatePowerModel, sys5b, time_range; parameters = false);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PSI.CopperPlatePowerModel, sys5b, time_range; parameters = false);
    PSI.construct_network!(ps_model, PSI.CopperPlatePowerModel, sys5b, time_range; parameters = false);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 120
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 24

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)
    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL
end

@testset "testing DC-PF with PTDF formulation" begin
    PTDF, A = PowerSystems.buildptdf(branches5, nodes5)
    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PSI.StandardPTDFForm, sys5b, time_range);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PSI.StandardPTDFForm, sys5b, time_range);
    PSI.construct_network!(ps_model, PSI.StandardPTDFForm, sys5b, time_range; PTDF = PTDF)
    PSI.construct_device!(ps_model, PSY.Branch, PSI.SeriesLine, PSI.StandardPTDFForm, sys5b, time_range)
    @test JuMP.num_variables(ps_model.JuMPmodel) == 264
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 264
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 264

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL

    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods; parameters = false)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PSI.StandardPTDFForm, sys5b, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PSI.StandardPTDFForm, sys5b, time_range; parameters = false)
    PSI.construct_network!(ps_model, PSI.StandardPTDFForm, sys5b, time_range; PTDF = PTDF, parameters = false)
    PSI.construct_device!(ps_model, PSY.Branch, PSI.SeriesLine, PSI.StandardPTDFForm, sys5b, time_range; parameters = false)
    @test JuMP.num_variables(ps_model.JuMPmodel) == 264
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.Interval{Float64}) == 264
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 0
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 264

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL
end

 @testset "PTDF ArgumentError" begin
    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PSI.StandardPTDFForm, sys5b, time_range);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PSI.StandardPTDFForm, sys5b, time_range);
    @test_throws ArgumentError PSI.construct_network!(ps_model, PSI.StandardPTDFForm, sys5b, time_range)
end

@testset "testing DC-PF network construction" begin
    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PM.DCPlosslessForm, sys5b, time_range);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PM.DCPlosslessForm, sys5b, time_range);
    PSI.construct_network!(ps_model, PM.DCPlosslessForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 384
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 288

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL

    ps_model = PSI._canonical_model_init(bus_numbers, GLPK_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods; parameters = false)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PM.DCPlosslessForm, sys5b, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PM.DCPlosslessForm, sys5b, time_range; parameters = false)
    PSI.construct_network!(ps_model, PM.DCPlosslessForm, sys5b, time_range; parameters = false)
    @test JuMP.num_variables(ps_model.JuMPmodel) == 384
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 288

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) == MOI.OPTIMAL
end

@testset  "testing AC-PF network construction" begin
    ps_model = PSI._canonical_model_init(bus_numbers, ipopt_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PM.StandardACPForm, sys5b, time_range);
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PM.StandardACPForm, sys5b, time_range);
    PSI.construct_network!(ps_model, PM.StandardACPForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 1056
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 264

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED]

    ps_model = PSI._canonical_model_init(bus_numbers, ipopt_optimizer, PM.AbstractPowerFormulation, 1:sys5b.time_periods; parameters = false)
    PSI.construct_device!(ps_model, PSY.ThermalGen, PSI.ThermalDispatch, PM.StandardACPForm, sys5b, time_range; parameters = false)
    PSI.construct_device!(ps_model, PSY.PowerLoad, PSI.StaticPowerLoad, PM.StandardACPForm, sys5b, time_range; parameters = false)
    PSI.construct_network!(ps_model, PM.StandardACPForm, sys5b, time_range);
    @test JuMP.num_variables(ps_model.JuMPmodel) == 1056
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.LessThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.GreaterThan{Float64}) == 144
    @test JuMP.num_constraints(ps_model.JuMPmodel,GenericAffExpr{Float64,VariableRef},MOI.EqualTo{Float64}) == 264

    JuMP.@objective(ps_model.JuMPmodel, Min, AffExpr(0))
    JuMP.optimize!(ps_model.JuMPmodel)

    @test termination_status(ps_model.JuMPmodel) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED]
end


using Visualize, GeometryTypes, ModernGL
using Visualize: GLRasterization, TextUniforms, get_texture_atlas, Sprite2, STDCanvas, getposition

resolution = (1024, 1024)

proj = orthographicprojection(SimpleRectangle(0, 0, resolution...), -10_000f0, 10_000f0)

canvas = STDCanvas(
    proj,
    eye(Mat4f0),
    proj,
    resolution
)

atlas = get_texture_atlas();

text = Visualize.Text(
    Sprite2[],
    Char[],
    atlas,
    [0],
    Visualize.defaultfont(),
    45f0,
    0f0,
    Vec4f0(0f0, 0f0, 0f0, 1f0),
    Point2f0(0),
    1.5
);

print(text, 'c')
print(text, " nice test bro!")
text.color = Vec4f0(1f0, 0f0, 0f0, 1f0)
print(text, " Muashahahaha@")

c = CairoRGBSurface(resolution...);
cr = CairoContext(c);
save(cr);
set_source_rgb(cr, 1.0, 1.0, 1.0);    # light gray
rectangle(cr, 0.0, 0.0, resolution...); # background
fill(cr);
restore(cr);

save(cr);

function draw_text(cr, text, canvas, uniforms)
    #set_font_face(cr, text.font)
    for (c, sprite) in zip(text.text, text.data)
        pos = getposition(sprite)
        pos = canvas.projectionview * #=uniforms.model * =# Visualize.to_vec4(Vec2f0(pos))
        pos2d = clip2pixel_space(pos, canvas.resolution)
        pos2d = Vec2f0(pos2d[1], resolution[2] - pos2d[2])
        scale = sprite.scale
        move_to(cr, pos2d...)
        set_source_rgba(cr, sprite.color...)
        set_font_size(cr, scale[2])
        show_text(cr, string(c))
    end
end
draw_text(cr, text, canvas, uniforms)
write_to_png(c, "sample_dash.png")


module SunbergTypes

using Random

using CPUTime
using POMDPs
using POMDPPolicies
using DiscreteValueIteration
using ParticleFilters
using POMDPModelTools
using QMDP
using MCTS
using BasicPOMCP
using LaserTag
using LinearAlgebra

using Parameters

import POMDPs: initialstate, generate_s, generate_o, generate_sor, support, discount, isterminal, generate_sr
import POMDPs: actions, n_actions, actionindex, action, dimensions, isterminal_act
import POMDPs: states, n_states, stateindex, transition
import POMDPs: observations, observation, n_observations, obsindex, isterminal_obs
import POMDPs: initialstate, initialstate_distribution
import POMDPs: updater, update
import POMDPs: reward
import POMDPs: convert_s, convert_a, convert_o
import POMDPs: solve

import POMDPs: max_possible_weight, new_particle, reset_distribution

export
    ObsAdaptiveParticleFilter,
    GBMDPSolver,
    MeanRewardBeliefMDP,

    PDPWSolver,

    reset_distribution,
    max_possible_weight,
    new_particle

include("heuristics.jl")
include("mr_belief_mdp.jl")
include("obs_adaptive_pf.jl")
include("gbmdp_solver.jl")
include("rollout.jl")

end # module


module GeoTime

using Dates, Printf

import Dates: UTInstant, Millisecond
import Dates: year,  month,  day, hour, minute, second, millisecond
import Dates: daysinmonth, daysinyear, yearmonthday, yearmonth
import Dates: monthday, len

import Base: +, -, isless, string, show, convert, reinterpret

export daysinmonth, daysinyear, yearmonthday, yearmonth, monthday

export timeencode, timedecode, datetuple

export DateTimeStandard, DateTimeJulian, DateTimeProlepticGregorian,
       DateTimeAllLeap, DateTimeNoLeap, DateTime360Day, AbstractCFDateTime

include("time.jl")

end


module Optim

"""
`argmax(v, ℓ)`

Find `ω` such that `v(ω) != ⊥` and ℓ(ω) is maximised
"""
function argmax end

"""
`solution(v)`

Find `ω` that satisfies constraints of `v` (`v(ω) != ⊥`)
"""
function solution end

end


module SummaryWrapper

using JavaCall

const AbstractStorelessUnivariateStatistic = @jimport org.hipparchus.stat.descriptive.AbstractStorelessUnivariateStatistic
const AggregatableStatistic = @jimport org.hipparchus.stat.descriptive.AggregatableStatistic
const Class = @jimport java.lang.Class
const DoubleConsumer = @jimport java.util.function.DoubleConsumer
const Iterable = @jimport java.lang.Iterable
const JString = @jimport java.lang.String
const Object = @jimport java.lang.Object
const Product = @jimport org.hipparchus.stat.descriptive.summary.Product
const StorelessUnivariateStatistic = @jimport org.hipparchus.stat.descriptive.StorelessUnivariateStatistic
const Sum = @jimport org.hipparchus.stat.descriptive.summary.Sum
const SumOfLogs = @jimport org.hipparchus.stat.descriptive.summary.SumOfLogs
const SumOfSquares = @jimport org.hipparchus.stat.descriptive.summary.SumOfSquares
const UnivariateStatistic = @jimport org.hipparchus.stat.descriptive.UnivariateStatistic
const WeightedEvaluation = @jimport org.hipparchus.stat.descriptive.WeightedEvaluation

export Product
export Sum
export SumOfLogs
export SumOfSquares
export accept
export aggregate
export and_then
export clear
export copy
export equals
export evaluate
export get_class
export get_n
export get_result
export hash_code
export increment
export increment_all
export notify
export notify_all
export to_string
export wait

include("product.jl")
include("sum.jl")
include("sum_of_logs.jl")
include("sum_of_squares.jl")

end


"""
    add!!(x, y)

Returns `x+y`, potentially mutating `x` in-place to hold this value.
This avoids allocations when `x` can be mutated in this way.

See also: [`InplaceableThunk`](@ref).
"""
add!!(x, y) = x + y

add!!(x, t::InplaceableThunk) = t.add!(x)

function add!!(x::Array{<:Any, N}, y::AbstractArray{<:Any, N}) where N
    return x .+= y
end


## -------------------------------------------------------------------
# model vs exp comparison
let
    METHODS = [
        :FBA_Z_FIX_MAX_VG_MIN_COST,
        :FBA_Z_FIX_MAX_VG_MAX_COST,
        :FBA_Z_FIX_MIN_VG_MIN_COST,
        # :FBA_Z_FIX_MIN_VG_MAX_COST,
        # :FBA_Z_FIX_ADJ_MAX_VG_MIN_COST,
        # :FBA_Z_FIX_ADJ_MAX_VG_MAX_COST,
        # :FBA_Z_FIX_ADJ_MIN_VG_MIN_COST,
        # :FBA_Z_FIX_ADJ_MIN_VG_MAX_COST,
        # :FBA_Z_FIX_MAX_ATP_MIN_COST,
        :FBA_Z_FIX_MAX_ATP_MAX_COST,
        :ME_MAX_POL,
        # :ME_MAX_POL_B0
    ]

    colors = Dict(
        :exp => :black,
        :FBA_Z_FIX_MAX_VG_MIN_COST => :blue,
        :FBA_Z_FIX_MAX_VG_MAX_COST => :red,
        :FBA_Z_FIX_MIN_VG_MIN_COST => :black,
        :FBA_Z_FIX_MIN_VG_MAX_COST => :red,
        :FBA_Z_FIX_MAX_ATP_MIN_COST => :blue,
        :FBA_Z_FIX_MAX_ATP_MAX_COST => :black,
        :ME_MAX_POL => :yellow,
        :ME_MAX_POL_B0 => :purple,
    )

    ms = 8

    # [:none, :auto, :circle, :rect, :star5, :diamond, :hexagon, :cross, :xcross, :utriangle,
    # :dtriangle, :rtriangle, :ltriangle, :pentagon, :heptagon, :octagon, :star4, :star6,
    # :star7, :star8, :vline, :hline, :+, :x].
    shapes = Dict(
        :exp => :star,
        :FBA_Z_FIX_MAX_VG_MIN_COST => :utriangle,
        :FBA_Z_FIX_MAX_VG_MAX_COST => :utriangle,
        :FBA_Z_FIX_MIN_VG_MIN_COST => :square,
        :FBA_Z_FIX_MIN_VG_MAX_COST => :square,
        :FBA_Z_FIX_MAX_ATP_MIN_COST => :hexagon,
        :FBA_Z_FIX_MAX_ATP_MAX_COST => :hexagon,
        :ME_MAX_POL => :circle,
        :ME_MAX_POL_B0 => :square,
    )

    iJR = ChN.iJR904
    Data = ChN.NanchenData

    Nd_rxns_map = iJR.load_rxns_map2() # inner reacts
    iJR_ider_subs = iJR.load_inner_rxns_subs()

    sort_iders(iders) = sort!(deepcopy(iders),
        by = (ider) -> ider == "GLC" ? "AAA" : ider
    )

    # Collect
    src = nameof(Data)

    DAT = ChE.load_DAT(src)

    EXCH_FLX_IDERS = sort_iders(DAT[:EXCH_FLX_IDERS])
    # EXPS = DAT[:EXPS]

    # experiments with approx D
    Dintervals = Dict(
        0.05 => 1:3,
        0.1 => 4:9,
        0.15 => 4:11,
        0.2 => 10:11,
        0.25 => 10:15,
        0.3 => 12:15,
        0.35 => 12:17,
        0.4 => 16:17,
        :tot => 1:17
    )

    @info("Doing", src)

    for (avD, exps) in Dintervals
        dat_pool = Dict()
        # Experiments
        edat = get!(dat_pool, :exp, Dict())
        for exp in exps
            !haskey(DAT, :exp, :flx, "GLC", exp) && continue
            glc_exp_val = DAT[:exp, :flx, "GLC", exp]

            for (sub, iJR_iders) in iJR_ider_subs
                for iJR_ider in iJR_iders
                    Nd_ider = Nd_rxns_map[iJR_ider]
                    exp_val = abs(DAT[:exp, :flx, Nd_ider, exp] / glc_exp_val)

                    # @info(:exp, exp, iJR_ider, exp_val); println()

                    eidat = get!(edat, iJR_ider, [])
                    push!(eidat, exp_val)
                end
            end
        end

        # methods
        for method in METHODS
            mdat = get!(dat_pool, method, Dict())

            for exp in exps
                !haskey(DAT, :exp, :flx, "GLC", exp) && continue
                glc_exp_val = DAT[:exp, :flx, "GLC", exp]

                @info(method, exp); println()

                for (sub, iJR_iders) in iJR_ider_subs
                    (sub in ["others"]) && continue
                    for iJR_ider in iJR_iders
                        !haskey(DAT, method, :flx, iJR_ider, exp) && continue
                        model_val = abs(DAT[method, :flx, iJR_ider, exp] / glc_exp_val)

                        @info(method, exp, iJR_ider, model_val); println()

                        midat = get!(mdat, iJR_ider, [])
                        push!(midat, model_val)
                    end
                end
            end
        end

        # Plots
        xticks_pos = []
        xticks_iders = []
        fontsize = 13
        p = plot(;xlabel = "Reactions",
            ylabel = "flux / glc exch.", xgrid = false,
            # dpi = 1000,
            thickness_scaling = 1.3,
            xguidefontsize = fontsize, yguidefontsize = fontsize
        )
        sep1 = 5
        sep2 = 3 * sep1

        # backgound
        gc = 1
        last_gc = gc
        ci = 1
        for sub in NiJR.subs()
            (sub in ["others"]) && continue
            iJR_iders = iJR_ider_subs[sub]

            for iJR_ider in iJR_iders
                vline!(p, [gc - sep1];
                    label = "", color = :darkgray, alpha = 0.8, ls = :dash
                )
                gc += sep1
                for method in METHODS
                    gc += sep1
                end
                gc += sep2
            end
            fillalpha = isodd(ci) ? 0.6 : 0.3
            vspan!(p, [last_gc - sep1, gc - sep1]; label = "",
                linecolor = :darkgray, fillcolor = :darkgray,
                fillalpha, linealpha = 0.6
            )
            last_gc = gc
            ci += 1
        end

        # fluxes
        gc = 1
        # f(v) = iszero(v) ? -6.0 : log(v)
        f(v) = v
        for sub in NiJR.subs()
            (sub in ["others"]) && continue

            iJR_iders = iJR_ider_subs[sub]
            for iJR_ider in iJR_iders

                # Experimental
                edat = dat_pool[:exp]
                !haskey(edat, iJR_ider) && continue
                m = mean(abs.(edat[iJR_ider]))
                e = std(abs.(edat[iJR_ider]))

                @info(iJR_ider, m, e); println()

                scatter!(p, [gc + sep1], [f(m)];
                    yerr = [e],
                    color = colors[:exp], label = "",
                    m = (ms, shapes[:exp]),
                    alpha = 0.8
                )
                push!(xticks_pos, gc + 2 * sep1)
                push!(xticks_iders, iJR_ider)
                gc += sep1

                # modeled
                for method in METHODS
                    mdat = dat_pool[method]
                    color = colors[method]
                    μ = mean(abs.(mdat[iJR_ider]))
                    e = std(abs.(mdat[iJR_ider]))
                    m = (ms, get(shapes, method, :square))
                    scatter!(p, [gc + sep1], [f(μ)];
                        yerr = [e],
                        color, label = "", m, alpha = 0.8
                    )
                    gc += sep1
                end
                gc += sep2
            end
        end

        xticks = (xticks_pos, xticks_iders)
        xrotation = 35
        size = (1100, 500)
        thickness_scaling = 1.3
        plot!(p; xticks, xrotation, size, thickness_scaling)
        mysavefig(p, "inner_flx_comparizon"; avD)
        PS[:inner_flx_comparizon, :all_subs, avD] = deepcopy(p)
    end
end


using PackageTest
using Test

@testset "PackageTest.jl" begin
    # 2x + y
    @test my_f(2,1) == 5
end


module Actors

using ..WalksBase: get_nsteps, get_time, get_position
using ..Points: get_x, norm_squared
using ..LatticeVars: get_num_sites_visited
import ..LatticeVars: init!
using EmpiricalCDFs
import EmpiricalCDFs: get_data
import Statistics
export AbstractActor, act!, ActorSet, NullActor, StepLimitActor, ErrorOnExitActor, FirstReturnActor,
    get_actor_value

export ActorExitException

export StoredValues, StoringActor, init!, storing_position_actor,
    storing_num_sites_visited_actor, get_times, get_ecdf_times, get_values,
    get_stored_values, storing_nsteps_actor, storing_nsteps_position_actor,
    storing_nsteps_num_sites_visited_actor, storing_msd_actor

export CountActor, get_count, CountLimitException, check_counter
export ECDFsActor, get_cdfs, ECDFActor, ECDFValueActor, get_cdf, unpack, prune
export SampleLoopActor, get_actor, ProgressIter

###
### AbstractActor
###

abstract type AbstractActor end

init!(_::AbstractActor) = nothing
finalize!(_::AbstractActor) = nothing
condition_satisfied(_::AbstractActor) = true
# Following makes detecting bugs harder. Leave here to avoid temptation.
# act!(_::AbstractActor, args...) = true

###
### NullActor
###

struct NullActor <: AbstractActor
end
act!(_::NullActor, args...) = true

###
### StepLimitActor
###

mutable struct StepLimitActor <: AbstractActor
    max_step_limit::Int
    hit_max_step_limit::Bool
    StepLimitActor(n=10) = new(n, false)
end

function act!(actor::StepLimitActor, latwalk)
    nsteps = get_nsteps(latwalk)
    if nsteps >= actor.max_step_limit
        actor.hit_max_step_limit = true
        return false
    end
    return true
end

init!(actor::StepLimitActor) = (actor.hit_max_step_limit = false; nothing)
condition_satisfied(actor::StepLimitActor) = actor.hit_max_step_limit

function Base.show(io::IO, actor::StepLimitActor)
    print(io, "StepLimitActor(n=", actor.max_step_limit,
          ", hit=", actor.hit_max_step_limit, ")")
end

###
### ErrorOnExitActor
###

struct ActorExitException{T} <: Exception
    actor::T
    msg::String
end

ActorExitException(actor::T) where {T} = ActorExitException(actor, "actor of type $T exited")
Base.showerror(io::IO, e::ActorExitException) = print(io, e.msg)

struct ErrorOnExitActor{T <: AbstractActor} <: AbstractActor
    child_actor::T
end

for f  in (:init!, :condition_satisfied)
    @eval ($f)(actor::ErrorOnExitActor, args...) = ($f)(actor.child_actor, args...)
end

function act!(actor::ErrorOnExitActor, arg)
    if ! act!(actor.child_actor, arg)
        throw(ActorExitException(actor.child_actor))
    end
    return true
end

###
### CountActor
###

mutable struct Counter
    c::Int
end
Counter() = Counter(0)
init!(counter::Counter) = counter.c = 0
increment(counter::Counter) = counter.c += 1
get_count(counter::Counter) = counter.c

struct CountActor{F} <: AbstractActor
    counter::Counter
    counting_func::F
end

function CountActor(child_actor::AbstractActor)
    CountActor(Counter(), () -> condition_satisfied(child_actor))
end

for f in (:increment, :get_count, :init!)
    @eval ($f)(actor::CountActor, args...) = ($f)(actor.counter, args...)
end
get_actor_value(actor::CountActor) = get_count(actor.counter)

function act!(actor::CountActor, _system)
    if actor.counting_func()
        increment(actor.counter)
    end
    return true
end

###
### CountLimitException
###

struct CountLimitException <: Exception
    msg::String
end

Base.showerror(io::IO, e::CountLimitException) = print(io, e.msg)

function check_counter(actor::CountActor, msg=""; max_count = 0)
    if get_actor_value(actor) > max_count
        throw(CountLimitException(string(msg, " CountActor made ", get_actor_value(actor), " counts. ",
                                         "Maximum allowed $max_count")))
    end
    return true
end

###
### FirstReturnActor
###

"""
    FirstReturnActor <: AbstractActor

Records and returns `true`, and the step number if the position is the origin.
"""
mutable struct FirstReturnActor <: AbstractActor
    return_step_num::Int
    has_walker_returned::Bool
end

FirstReturnActor() = FirstReturnActor(0, false)

init!(actor::FirstReturnActor) = (actor.has_walker_returned = false; actor.return_step_num = 0; nothing)

function act!(actor::FirstReturnActor, system)
    iszero(get_position(system)) || return true
    actor.has_walker_returned = true
    actor.return_step_num = get_nsteps(system)
    return false
end

condition_satisfied(actor::FirstReturnActor) = actor.has_walker_returned
get_actor_value(actor::FirstReturnActor) = actor.return_step_num

###
### ActorSet
###

"""
    struct ActorSet

A set of several `Actors` to be applied sequentially.
If one of the set returns `false`, then `ActorSet` immediately returns false.
"""
struct ActorSet{T <: Tuple} <: AbstractActor
    actors::T
end

ActorSet(child_actors::AbstractActor...) = ActorSet((child_actors...,))
ActorSet(child_actor::AbstractActor) = ActorSet((child_actor,))
init!(actor_set::ActorSet) = foreach(actor -> init!(actor), actor_set.actors)

function act!(actor_set::ActorSet, args...)
    for actor in actor_set.actors
        act!(actor, args...)::Bool || return false
    end
    return true
end

###
### StoredValues
###

"""
    mutable struct StoredValues{T, X}

Stores the data for `StoringActor`
"""
mutable struct StoredValues{T, X}
    times::T
    values::X
    current_index::Int
end

Base.getindex(sv::StoredValues, ind1, ind2) = sv.values[ind2][ind1]

# FIXME: used to work
# Base.show(io::IO, sv::StoredValues) = Base.show(io, MIME("text/plain"), sv)
# function Base.show(io::IO, m::typeof(MIME("text/plain")), sv::StoredValues{T}) where T
#                    println(io, "StoredValues{times_type=$T}(current_index=", sv.current_index, ")")
#                    maxind = length(sv.values[1])
#     show(io, m, [sv.times[1:maxind] (x[1:maxind] for x in sv.values)... ])
# end

get_target_time(sv::StoredValues) = sv.times[sv.current_index]
increment_current_index(sv::StoredValues) = sv.current_index += 1
Base.lastindex(sv::StoredValues) = lastindex(sv.times)
get_current_index(sv::StoredValues) = sv.current_index
reset_current_index!(sv::StoredValues) = sv.current_index = 1
get_values(sv::StoredValues) = sv.values
get_times(sv::StoredValues) = sv.times

Base.empty!(sv::StoredValues) = empty!(sv.values)

function Base.empty!(sv::StoredValues{<:Any, <:Tuple})
    for v in sv.values
        empty!(v)
    end
end

StoredValues(times; value_types = Float64) = _StoredValues(times, value_types)

function _StoredValues(times::T, value_types) where T
    initial_index = 1
    return StoredValues{T, Vector{value_types}}(times, zeros(value_types,0), initial_index)
end

function _StoredValues(times::T, value_types::Tuple) where T
    initial_index = 1
    tup = ((zeros(t,0) for t in value_types)...,)
    return StoredValues{T, typeof(tup)}(times, tup, initial_index)
end

###
### StoringActor
###

struct StoringActor{T, X, F} <: AbstractActor
    stored_values::StoredValues{T, X}
    storing_func::F
end

StoringActor(times, storing_func; value_types=Float64) = StoringActor(times, storing_func, value_types)
StoringActor(times, storing_func, value_types) = StoringActor(StoredValues(times; value_types=value_types), storing_func)
StoringActor(times, storing_func::Tuple, value_types::Tuple) = StoringActor(StoredValues(times; value_types=value_types), storing_func)

function act!(actor::StoringActor, system)
    stored_values = actor.stored_values
    get_current_index(stored_values) > lastindex(stored_values) && return false # terminate walk
    while get_time(system) >= get_target_time(stored_values)
        push!(stored_values.values, actor.storing_func(system))
        increment_current_index(stored_values)
        get_current_index(stored_values) > lastindex(stored_values) && return false # terminate walk
    end
    return true
end

function act!(actor::StoringActor{T, <:Tuple, <:Tuple}, system) where T
    get_current_index(actor.stored_values) > lastindex(actor.stored_values) && return false # terminate walk
    while get_time(system) >= get_target_time(actor.stored_values)
        for (array, func) in zip(actor.stored_values.values, actor.storing_func)
            push!(array, func(system))
        end
        increment_current_index(actor.stored_values)
        get_current_index(actor.stored_values) > lastindex(actor.stored_values) && return false # terminate walk
    end
    return true
end

function init!(actor::StoringActor)
    reset_current_index!(actor.stored_values)
    empty!(actor.stored_values)
    return nothing
end

for f in (:get_values, :get_times)
    @eval ($f)(sa::StoringActor) = ($f)(sa.stored_values)
end
get_stored_values(sa::StoringActor) = sa.stored_values

Base.getindex(sa::StoringActor, ind1::Integer, inds...) = _getindex(sa, Val(ind1), inds...)
Base.getindex(sa::StoringActor, ind1::Integer) = _getindex(sa, Val(ind1))
_getindex(sa::StoringActor, ::Val{1}, inds...) = get_times(sa)[inds...]
_getindex(sa::StoringActor, ::Val{n}, inds...) where {n} = get_values(sa)[n-1][inds...]
_getindex(sa::StoringActor, ::Val{1}) = get_times(sa)
_getindex(sa::StoringActor, ::Val{n}) where {n} = get_values(sa)[n-1]
Base.length(sa::StoringActor) = length(get_values(sa)) + 1
Base.size(sa::StoringActor) = (length(sa), ((length(sa[i]) for i in 1:length(sa))...,))
Base.show(io::IO, actor::StoringActor) = print(io, "StoringActor(", actor.stored_values, ")")

###
### Some instances of StoringActor
###

# FIXME: allowing single arrays rather than tuple of arrays (possibly of length 1) increases complexity
# and amount of code that must be maintained: Must support only tuples.
# hmmm. maybe convert single array and single type to tuples upon construction
storing_position_actor(times) = StoringActor(times, (system) -> get_x(get_position(system)))

function storing_num_sites_visited_actor(times)
    funcs = ((system) -> get_num_sites_visited(system),)
    value_types = (Int,)
    return StoringActor(times, funcs; value_types=value_types)
end

function storing_nsteps_actor(times)
    funcs = ((system) -> get_nsteps(system), )
    value_types = (Int, )
    return StoringActor(times, funcs; value_types=value_types)
end

function storing_nsteps_position_actor(times)
    funcs = ((system) -> get_nsteps(system), (system) -> get_x(get_position(system)))
    return StoringActor(times, funcs; value_types=(Int, Float64))
end

function storing_nsteps_num_sites_visited_actor(times)
    funcs = ((system) -> get_nsteps(system), (system) -> get_num_sites_visited(system))
    value_types = (Int, Int)
    return StoringActor(times, funcs; value_types=value_types)
end

function storing_msd_actor(times)
    funcs = ((system) -> norm_squared(get_position(system)), )
    return StoringActor(times, funcs; value_types = (Float64,))
end

###
### ECDFActor
###

struct ECDFActor{T <: AbstractEmpiricalCDF, F, FC} <: AbstractActor
    action_func::F
    ecdf::T
    condition_func::FC
end

ECDFActor(action_func, ecdf = EmpiricalCDF()) = ECDFActor(action_func, ecdf, _ -> true)
init!(actor::ECDFActor) = (empty!(actor.ecdf); nothing)
finalize!(actor::ECDFActor) = (sort!(actor.ecdf); nothing)

function act!(actor::ECDFActor, system)
    if actor.condition_func(system)
        push!(actor.ecdf, actor.action_func(system))
    end
    return true
end

get_cdf(ea::ECDFActor) = ea.ecdf

"""
    ECDFValueActor(child_actor::AbstractActor)

Builds a CDF for any `child_actor` that implements `condition_satisfied` and `get_actor_value`.
"""
function ECDFValueActor(child_actor::AbstractActor)
    ecdf = EmpiricalCDF{typeof(get_actor_value(child_actor))}()
    condition_func = _ -> condition_satisfied(child_actor)
    action_func = _ -> get_actor_value(child_actor)
    return ECDFActor(action_func, ecdf, condition_func)
end

###
### ECDFsActor
###

# In some cases, we may not need, or even have, a child_actor
struct ECDFsActor{CDFT, F} <: AbstractActor
    ecdfs_times::CDFT
    storing_func::F
end

get_ecdf_times(ea::ECDFsActor) = ea.ecdfs_times

struct ECDFsTimes{TimeT, CDFT}
    times::TimeT
    ecdfs::CDFT
end
get_times(et::ECDFsTimes) = et.times
get_cdfs(et::ECDFsTimes) = et.ecdfs
unpack(et::ECDFsTimes) = [get_times(et) get_cdfs(et)]

function prune(et::ECDFsTimes; f = x -> !isempty(x))
    good_inds = unique(getindex.(findall(f, et.ecdfs),1))
    new_times = et.times[good_inds]
    new_ecdfs = et.ecdfs[good_inds,:]
    return ECDFsTimes(new_times, new_ecdfs)
end

function Base.show(io::IO, actor::ECDFsActor)
    print(io, "ECDFsActor{", typeof(get_ecdf_times(actor)), ", Func, ")
    print(io, "}")
end

act!(actor::ECDFsActor, system) = (actor.storing_func(system); true)
get_cdfs(ea::ECDFsActor) = get_cdfs(get_ecdf_times(ea))

function init!(actor::ECDFsActor)
    empty!(actor)
    nothing
end

finalize!(actor::ECDFsActor) = (sort!(actor); nothing)

# TODO: Fix indexing. Time should not be in index value 1
function ECDFsActor(storing_actor::StoringActor)
    times = get_times(storing_actor)
    ecdfs = [EmpiricalCDF{eltype(storing_actor[j])}() for i in 1:length(times), j in 2:length(storing_actor)]
    ecdfs_times = ECDFsTimes(times, ecdfs)
    storing_func = function(_...)
        for j in 2:length(storing_actor)
            for i in 1:length(storing_actor[j])
                push!(ecdfs[i, j-1], storing_actor[j][i])
            end
        end
        return true
    end
    return ECDFsActor(ecdfs_times, storing_func)
end

get_times(a::ECDFsActor) = get_times(get_ecdf_times(a))
unpack(a::ECDFsActor) = unpack(get_ecdf_times(a))

for f in (:sort!, :empty!)
    @eval function Base.$(f)(actor::ECDFsActor)
        ecdfs = get_cdfs(get_ecdf_times(actor))
        for cdf in ecdfs
           ($f)(cdf)
        end
        return actor
    end
end

get_data(cdf::ECDFActor) = get_data(cdf.ecdf)

for f in (:length, :size, :minimum, :maximum, :extrema, :issorted, :iterate, :getindex,
          :lastindex, :firstindex, :eltype, :view)
    @eval begin
        Base.$(f)(cdf::ECDFActor, args...) = $(f)(cdf.ecdf, args...)
#        Base.$(f)(cdf::ECDFsActor, ind, args...) = $(f)(cdf.ecdfs[ind], args...)
    end
end

for f in (:mean, :median, :middle, :std, :stdm, :var, :varm, :quantile)
    @eval begin
        Statistics.$(f)(cdf::ECDFActor, args...; kws...) = Statistics.$(f)(cdf.ecdf, args...; kws...)
        Statistics.$(f)(cdf::ECDFsActor, ind, args...; kws...) = Statistics.$(f)(cdf.ecdfs[ind], args...; kws...)
    end
end

for f in (:sort!, :empty!)
    @eval begin
        Base.$(f)(cdf::ECDFActor, args...) = ($(f)(cdf.ecdf,args...); cdf)
    end
end

###
### SampleLoopActor
###

# abstract type AbstractSampleLoopActor <: AbstractActor
# end

struct ProgressIter{IterT}
    iter::IterT
end
ProgressIter(n::Integer) = ProgressIter(1:n)

struct SampleLoopActor{Iter, ActorT} <: AbstractActor
    iter::Iter
    actor::ActorT
end

"""
    SampleLoopActor(iter, actor::AbstractActor = NullActor())

Actor for use in `trial!`. The sample iterator is `iter`.
"""
SampleLoopActor(iter) = SampleLoopActor(iter, NullActor())

"""
    SampleLoopActor(n::Integer,  actor = NullActor())

Actor for use in `trial!` that performs `n` samples.
"""
SampleLoopActor(n::Integer, actor = NullActor()) = SampleLoopActor(1:n, actor)

get_actor(s::SampleLoopActor) = s.actor

end # module Actors


module Helpers

using CSV
using DataFrames
using Formatting
using StatsBase
using StatsModels

export get_coefs, allequal, featurestrings
export ys1d, roundclip, f1, log2file, xdecim

get_coefs(models::Vector{<:RegressionModel})::Vector{Vector{Float64}} = coef.(models)
function allequal(x::Vector)
    length(x) < 2 && return true
    e1 = x[1]
    @inbounds for i in 2:length(x)
        x[i] == e1 || return false
    end
    return true
end
function featurestrings(models::Vector{<:RegressionModel})::Vector{String}
    coefs = get_coefs(models)
    selected = [c .!= 0 for c in coefs]
    if allequal(selected)
        return [join([b ? "1" : "0" for b in selected[1]])]
    else
        return [join([b ? "1" : "0" for b in s]) for s in selected]
    end
end

ys1d(ymat::VecOrMat{Int})::Vector{Int} = ymat
function ys1d(ymat::Matrix{Float64})::Vector{Int}
    if size(ymat, 2) > 1
        [argmax(row) for row in eachrow(ymat)]
    else
        vec(1 * (ymat .>= .5))
    end
end
roundclip(vec::Vector{Float64}, range::UnitRange{Int})::Vector{Int} = max.(first(range), min.(last(range), round.(Int, vec)))

xdecim(num::Real, dec::Int) = sprintf1("%.$(dec)f", num)

function log2file(filename::String, x::String)
    path = "log/$filename.txt"
    dir, _ = splitdir(path)
    mkpath(dir)
    open(path, "a") do io
        write(io, "$x\n")
    end
end

function log2file(filename::String, df::DataFrame, header::Bool = false)
    CSV.write("log/$filename.txt", df, append = true, writeheader = header)
end
log2file(filename::String, mat::Matrix, names::Vector, header::Bool = false) = log2file(filename, DataFrame(mat, names), header)

end

using .Helpers


export MNIST

"""
The MNIST database of handwritten digits

- Authors: Yann LeCun, Corinna Cortes, Christopher J.C. Burges
- Website: http://yann.lecun.com/exdb/mnist/

MNIST is a classic image-classification dataset that is often
used in small-scale machine learning experiments. It contains
70,000 images of handwritten digits. Each observation is a 28x28
pixel gray-scale image that depicts a handwritten version of 1 of
the 10 possible digits (0-9).

# Interface

- [`MNIST.traintensor`](@ref), [`MNIST.trainlabels`](@ref), [`MNIST.traindata`](@ref)
- [`MNIST.testtensor`](@ref), [`MNIST.testlabels`](@ref), [`MNIST.testdata`](@ref)

# Utilities

- [`MNIST.download`](@ref)
- [`MNIST.convert2image`](@ref)
"""
module MNIST
    using DataDeps
    using ColorTypes
    using FixedPointNumbers
    using ..MLDatasets: bytes_to_type, datafile, download_dep, download_docstring,
                        _colorview

    export

        traintensor,
        testtensor,

        trainlabels,
        testlabels,

        traindata,
        testdata,

        convert2image,

        download

    @deprecate convert2features reshape

    const DEPNAME = "MNIST"
    const TRAINIMAGES = "train-images-idx3-ubyte.gz"
    const TRAINLABELS = "train-labels-idx1-ubyte.gz"
    const TESTIMAGES  = "t10k-images-idx3-ubyte.gz"
    const TESTLABELS  = "t10k-labels-idx1-ubyte.gz"

    """
        download([dir]; [i_accept_the_terms_of_use])

    Trigger the (interactive) download of the full dataset into
    "`dir`". If no `dir` is provided the dataset will be
    downloaded into "~/.julia/datadeps/$DEPNAME".

    This function will display an interactive dialog unless
    either the keyword parameter `i_accept_the_terms_of_use` or
    the environment variable `DATADEPS_ALWAYS_ACCEPT` is set to
    `true`. Note that using the data responsibly and respecting
    copyright/terms-of-use remains your responsibility.
    """
    download(args...; kw...) = download_dep(DEPNAME, args...; kw...)

    include(joinpath("Reader","Reader.jl"))
    include("interface.jl")
    include("utils.jl")

    function __init__()
        register(DataDep(
            DEPNAME,
            """
            Dataset: THE MNIST DATABASE of handwritten digits
            Authors: Yann LeCun, Corinna Cortes, Christopher J.C. Burges
            Website: http://yann.lecun.com/exdb/mnist/

            [LeCun et al., 1998a]
                Y. LeCun, L. Bottou, Y. Bengio, and P. Haffner.
                "Gradient-based learning applied to document recognition."
                Proceedings of the IEEE, 86(11):2278-2324, November 1998

            The files are available for download at the offical
            website linked above. Note that using the data
            responsibly and respecting copyright remains your
            responsibility. The authors of MNIST aren't really
            explicit about any terms of use, so please read the
            website to make sure you want to download the
            dataset.
            """,
            "https://ossci-datasets.s3.amazonaws.com/mnist/" .* [TRAINIMAGES, TRAINLABELS, TESTIMAGES, TESTLABELS],
            "0bb1d5775d852fc5bb32c76ca15a7eb4e9a3b1514a2493f7edfcf49b639d7975",
        ))
    end
end


module VPTrees

include("vantage_point_tree.jl")

export VPTree, find, find_nearest

end # module


# # A vector of SVectors, denoting the edge ID each face is connected to.
# function face_edge_connectivity(mesh::Quadrilateral8Mesh_2D)
#     if length(mesh.edges) === 0
#         @error "Mesh does not have edges!"
#     end
#     # A vector of MVectors of zeros for each face
#     # Each MVector is the length of the number of edges
#     face_edge = [MVector{4, UInt32}(0, 0, 0, 0) for _ in eachindex(mesh.faces)]
#     # For each face in the mesh, generate the edges.
#     # Search for the index of the edge in the mesh.edges vector
#     # Insert the index of the edge into the face_edge connectivity vector
#     for i in eachindex(mesh.faces)
#         for (j, edge) in enumerate(edges(mesh.faces[i]))
#             face_edge[i][j] = searchsortedfirst(mesh.edges, SVector(edge.data))
#         end
#         if any(x->x === 0x00000000, face_edge[i])
#             @error "Could not determine the face/edge connectivity of face $i"
#         end
#     end
#     return [SVector(sort(conn).data) for conn in face_edge]
# end
#
# # A vector of SVectors, denoting the edge ID each face is connected to.
# function face_edge_connectivity(mesh::Triangle6Mesh_2D)
#     if length(mesh.edges) === 0
#         @error "Mesh does not have edges!"
#     end
#     # A vector of MVectors of zeros for each face
#     # Each MVector is the length of the number of edges
#     face_edge = [MVector{3, UInt32}(0, 0, 0) for _ in eachindex(mesh.faces)]
#     # For each face in the mesh, generate the edges.
#     # Search for the index of the edge in the mesh.edges vector
#     # Insert the index of the edge into the face_edge connectivity vector
#     for i in eachindex(mesh.faces)
#         for (j, edge) in enumerate(edges(mesh.faces[i]))
#             face_edge[i][j] = searchsortedfirst(mesh.edges, SVector(edge.data))
#         end
#         if any(x->x === 0x00000000, face_edge[i])
#             @error "Could not determine the face/edge connectivity of face $i"
#         end
#     end
#     return [SVector(sort(conn).data) for conn in face_edge]
# end


Random.seed!(209)

@testset "Deprecated spaces" begin
    # GraphSpace
    @test_deprecated (@test_throws TypeError Space(Agents.Graph(1)))
    # GridSpace
    @test_deprecated (@test_throws TypeError Space((5, 1)))
    # ContinuousSpace
    @test_deprecated Space(5, (a, m) -> nothing)
end

@testset "0D grids" begin
    @test GridSpace((1,)).graph == Agents.Graph(1)
end

@testset "1D grids" begin
    a = GridSpace((5, 1))
    ae = collect(Agents.LightGraphs.edges(a.graph))

    b = GridSpace((5, 1), periodic = true)
    be = collect(Agents.LightGraphs.edges(b.graph))

    @test ae == [
        Agents.LightGraphs.Edge(1, 2),
        Agents.LightGraphs.Edge(2, 3),
        Agents.LightGraphs.Edge(3, 4),
        Agents.LightGraphs.Edge(4, 5),
    ]
    @test be == [
        Agents.LightGraphs.Edge(1, 2),
        Agents.LightGraphs.Edge(1, 5),
        Agents.LightGraphs.Edge(2, 3),
        Agents.LightGraphs.Edge(3, 4),
        Agents.LightGraphs.Edge(4, 5),
    ]
end

@testset "2D grids" begin
    @test GridSpace((2, 1)).graph == Agents.Graph(2, 1)

    a = GridSpace((2, 3))
    b = GridSpace((2, 3), periodic = true) # 2D grid

    @test a.dimensions == (2, 3)
    @test b.dimensions == (2, 3)
    @test length(a.agent_positions) == 6
    @test length(b.agent_positions) == 6

    @test nv(a) == 6
    @test ne(a) == 7
    @test nv(b) == 6
    @test ne(b) == 9

    ae = collect(Agents.LightGraphs.edges(a.graph))
    be = collect(Agents.LightGraphs.edges(b.graph))

    @test ae == [
        Agents.LightGraphs.Edge(1, 2),
        Agents.LightGraphs.Edge(1, 3),
        Agents.LightGraphs.Edge(2, 4),
        Agents.LightGraphs.Edge(3, 4),
        Agents.LightGraphs.Edge(3, 5),
        Agents.LightGraphs.Edge(4, 6),
        Agents.LightGraphs.Edge(5, 6),
    ]
    @test be == [
        Agents.LightGraphs.Edge(1, 2),
        Agents.LightGraphs.Edge(1, 3),
        Agents.LightGraphs.Edge(1, 5),
        Agents.LightGraphs.Edge(2, 4),
        Agents.LightGraphs.Edge(2, 6),
        Agents.LightGraphs.Edge(3, 4),
        Agents.LightGraphs.Edge(3, 5),
        Agents.LightGraphs.Edge(4, 6),
        Agents.LightGraphs.Edge(5, 6),
    ]
end

@testset "2D Moore" begin
    a = GridSpace((3, 3), moore = true)
    @test Agents.nv(a) == 9
    @test Agents.ne(a) == 20

    a = GridSpace((3, 4), moore = true)
    @test Agents.nv(a) == 12
    @test Agents.ne(a) == 29

    b = GridSpace((3, 2), moore = true, periodic = true)
    @test Agents.nv(b) == 6
    @test Agents.ne(b) == 15

    b = GridSpace((3, 3), moore = true, periodic = true)
    @test Agents.nv(b) == 9
    @test Agents.ne(b) == 36
end

@testset "3D grid" begin
    g1 = GridSpace((2, 3, 2))
    g2 = GridSpace((2, 3, 3))
    g3 = GridSpace((2, 3, 2), periodic = true)
    g4 = GridSpace((2, 3, 3), periodic = true)
    @test Agents.ne(g1) == 20
    @test Agents.ne(g2) == 33
    @test Agents.ne(g3) == 24
    @test Agents.ne(g4) == 39
end

@testset "grid coord/vertex conversions" begin
    @test Agents.coord2vertex((2, 2, 1), (3, 4, 1)) == 5
    @test Agents.coord2vertex((2, 2), (3, 4)) == 5
    @test Agents.coord2vertex((2, 2, 1), (3, 4, 1)) == 5
    @test Agents.coord2vertex((2, 2), (3, 4)) == 5
    @test Agents.vertex2coord(5, (3, 4, 1)) == (2, 2, 1)
    @test Agents.vertex2coord(5, (3, 4)) == (2, 2)

    @test Agents.coord2vertex((2, 2, 1), (2, 3, 3)) == 4
    @test Agents.coord2vertex((2, 2, 2), (2, 3, 3)) == 10
    @test Agents.coord2vertex((2, 2, 3), (2, 3, 3)) == 16
    @test Agents.coord2vertex(((1, 3, 1)), (2, 3, 3)) == 5
    @test Agents.coord2vertex((1, 3, 2), (2, 3, 3)) == 11
    @test Agents.coord2vertex((1, 3, 3), (2, 3, 3)) == 17
    @test Agents.coord2vertex((2,), (2, 3, 3)) == 2
    @test Agents.coord2vertex(2, (2, 3, 3)) == 2

    @test Agents.vertex2coord(5, (2, 3, 3)) == (1, 3, 1)
    @test Agents.vertex2coord(7, (2, 3, 3)) == (1, 1, 2)
    @test Agents.vertex2coord(13, (2, 3, 3)) == (1, 1, 3)
    @test Agents.vertex2coord(13, (2, 3, 3)) == (1, 1, 3)
    @test Agents.vertex2coord(15, (2, 3, 3)) == (1, 2, 3)
    @test Agents.vertex2coord(18, (2, 3, 3)) == (2, 3, 3)
    @test Agents.vertex2coord(18, (2, 3, 3)) == (2, 3, 3)

    Random.seed!(648)
    model = ABM(Agent3, GridSpace((5,5)))
    agent = Agent3(1, (1,1), 5.5)
    add_agent!(agent, model)
    @test Agents.coord2vertex(agent, model) == 20
    @test Agents.coord2vertex((1, 3), model) == 11
    @test Agents.coord2vertex(15, model) == 15
    @test Agents.vertex2coord((2,3), model) == (2,3)
    @test_throws MethodError Agents.coord2vertex((1, 3, 7), model) == 11
    @test_throws ErrorException Agents.vertex2coord(3, GraphSpace(complete_digraph(5)))
end

@testset "nodes" begin
    Random.seed!(782)
    space = GridSpace((3, 3))
    model = ABM(Agent1, space)
    @test has_empty_nodes(model)
    for node in nodes(model)
        if rand() > 0.7
            add_agent!(node, model)
        end
    end
    @test pick_empty(model) == 4
    @test nodes(model) == 1:9
    @test nodes(model, by = :random) == [9, 1, 8, 6, 3, 5, 7, 2, 4]
    @test nodes(model, by = :population) == [1, 5, 6, 9, 2, 3, 4, 7, 8]
    @test length(get_node_contents(5, model)) > length(get_node_contents(7, model))
    @test_throws ErrorException nodes(model, by = :notreal)

    iter = NodeIterator(model)
    @test length(iter) == 9
    @test first(NodeIterator(model))[1] == model[1].pos
end

@testset "Neighbors" begin
    undirected = ABM(Agent5, GraphSpace(path_graph(5)))
    @test node_neighbors(3, undirected) == [2, 4]
    @test node_neighbors(1, undirected) == [2]
    @test node_neighbors(5, undirected) == [4]
    @test node_neighbors(3, undirected; neighbor_type = :out) ==
    node_neighbors(3, undirected; neighbor_type = :in) ==
    node_neighbors(3, undirected; neighbor_type = :all) ==
    node_neighbors(3, undirected; neighbor_type = :default)
    add_agent!(1, undirected, rand())
    add_agent!(2, undirected, rand())
    add_agent!(3, undirected, rand())
    # We expect id 2 to be included for a grid based search
    @test sort!(space_neighbors(2, undirected)) == [1, 2, 3]
    # But to be excluded if we are looking around it.
    @test sort!(space_neighbors(undirected[2], undirected)) == [1, 3]

    directed = ABM(Agent5, GraphSpace(path_digraph(5)))
    @test node_neighbors(3, directed) == [4]
    @test node_neighbors(1, directed) == [2]
    @test node_neighbors(5, directed) == []
    @test node_neighbors(3, directed; neighbor_type = :default) ==
          node_neighbors(3, directed)
    @test node_neighbors(3, directed; neighbor_type = :in) == [2]
    @test node_neighbors(3, directed; neighbor_type = :out) == [4]
    @test sort!(node_neighbors(3, directed; neighbor_type = :all)) == [2, 4]
    add_agent!(1, directed, rand())
    add_agent!(2, directed, rand())
    add_agent!(3, directed, rand())
    @test sort!(space_neighbors(2, directed)) == [2, 3]
    @test sort!(space_neighbors(2, directed; neighbor_type=:in)) == [1, 2]
    @test sort!(space_neighbors(2, directed; neighbor_type=:all)) == [1, 2, 3]
    @test space_neighbors(directed[2], directed) == [3]
    @test space_neighbors(directed[2], directed; neighbor_type=:in) == [1]
    @test sort!(space_neighbors(directed[2], directed; neighbor_type=:all)) == [1, 3]

    gridspace = ABM(Agent3, GridSpace((3, 3)))
    @test node_neighbors((2, 2), gridspace) == [(2, 1), (1, 2), (3, 2), (2, 3)]
    @test node_neighbors((1, 1), gridspace) == [(2, 1), (1, 2)]
    a = add_agent!((2, 2), gridspace, rand())
    add_agent!((3, 2), gridspace, rand())
    @test space_neighbors((1, 2), gridspace) == [1]
    @test sort!(space_neighbors((1, 2), gridspace, 2)) == [1, 2]
    @test_throws MethodError space_neighbors((1, 2), gridspace, 1.5)
    @test sort!(space_neighbors((2, 2), gridspace)) == [1, 2]
    @test space_neighbors(a, gridspace) == [2]

    continuousspace = ABM(Agent6, ContinuousSpace(2; extend = (1, 1)))
    a = add_agent!((0.5, 0.5), continuousspace, (0.2, 0.1), 0.01)
    b = add_agent!((0.6, 0.5), continuousspace, (0.1, -0.1), 0.01)
    @test_throws MethodError node_neighbors(1, continuousspace)
    @test space_neighbors(a, continuousspace, 0.05) == []
    @test space_neighbors(a, continuousspace, 0.1) == [2]
    @test sort!(space_neighbors((0.55, 0.5), continuousspace, 0.05)) == [1, 2]
    move_agent!(a, continuousspace)
    move_agent!(b, continuousspace)
    @test space_neighbors(a, continuousspace, 0.1) == []
    # Checks for type instability #208
    @test typeof(space_neighbors(a, continuousspace, 0.1)) <: Vector{Int}
    @test typeof(space_neighbors((0.55, 0.5), continuousspace, 0.05)) <: Vector{Int}
end


module NWNTools

    include("objects.jl")
    include("intersections.jl")
    include("graph_tools.jl")
    include("circuit_tools.jl")
    include("randomization.jl")
    include("stat_tools.jl")
    include("plotting.jl")

end


"""
    read_cuboid(cfgpath :: String)

Read 3D array from a disk. The data on disk consists of two files:
JSON configuration file (which is passed to this function) and a raw
binary array data.

Scheme of the configuration file is as follows, where `x`, `y` and `z`
are dimensions of the array:

~~~~{.json}
{
    "dimensions": [x, y, z],
    "datapath": "file-with-data"
}
~~~~

The file with binary data, whose name is specified in `datapath`
field, is searched relatively to the directory with the JSON
configuration file. Its size must be exactly `x`⋅`y`⋅`z` bytes, each
byte containing an element of the resulting array.
"""
function read_cuboid(cfgpath :: String)
    dim, datapath = open(cfgpath) do input
        json = JSON.parse(input)
        Vector{Int64}(json["dimensions"]), json["datapath"]
    end

    datapath = joinpath(dirname(cfgpath), datapath)
    s = stat(datapath)
    if s.size != prod(dim)
        error("File size mismatch. Got $(s.size), expected $(prod(dim))")
    end

    data = Array{Int8, length(dim)}(undef, dim...)
    open(datapath) do input
        read!(input, data)
    end

    return data
end

@doc raw"""
    read_cuboid(datapath :: String, side, dim)

Read 3D array from a disk. The data on the disk must be in binary
format, one octet per sample. Totally, there must be $side^{dim}$
octets which are read into $side \times side \times \dots \times side$
array.
"""
function read_cuboid(datapath :: String,
                     side     :: Integer,
                     dim      :: Integer)
    s = stat(datapath)
    if s.size != prod(side^dim)
        error("File size mismatch. Got $(s.size), expected $(side^dim)")
    end

    dimensions = fill(side, dim) |> Tuple
    data = Array{Int8, dim}(undef, dimensions...)

    open(datapath) do input
        read!(input, data)
    end

    return data
end

######################
# Component labeling #
######################

# Two components are considered connected if they have manhattan_dist == 1
manhattan_dist(x :: NTuple{N, Int}, y :: NTuple{N, Int}) where N =
    sum(abs.(x .- y))
manhattan_dist(x :: CartesianIndex{N}, y :: CartesianIndex{N}) where N =
    manhattan_dist(x |> Tuple, y |> Tuple)

abstract type Topology end
struct Plane <: Topology end # For non-periodic c2
struct Torus <: Topology end # For periodic c2

function wrapidx(idx :: CartesianIndex{N}, array :: AbstractArray{T, N}) where {T, N}
    tuple = mod.(Tuple(idx), axes(array))
    return CartesianIndex(tuple)
end

# Pregenerate an array of adjacent elements
macro gen_adjacent(N :: Int)
    name = Symbol("adj", N, "d")
    center = zeros(Int, N)
    diffs = Tuple(-1:1 for n in 1:N)
    quote
        const $(esc(name)) = [
            x
            for x in CartesianIndices($diffs)
            if manhattan_dist(x, CartesianIndex($(center...))) == 1
        ]
    end
end

@gen_adjacent 1
@gen_adjacent 2
@gen_adjacent 3

function adjacent_elements(N :: Int)
    if N == 3 return adj3d
    elseif N == 2 return adj2d
    elseif N == 1 return adj1d
    else error("Unsupported number of dimensions")
    end
end

# Iterate over adjacent element in a "wrapped" torus space
function iterate_adjacent(index :: CartesianIndex{N},
                          array :: AbstractArray{T, N}) where{T, N}
    return (wrapidx(index + adj, array)
            for adj in adjacent_elements(N)
            if manhattan_dist(index, index + adj) == 1)
end

function Images.label_components(input :: AbstractArray{T, N},
                                 _     :: Torus) where {T <: Integer, N}
    # -1 means an absence of label
    output = fill(-1, size(input))
    # Current label
    label :: Int = 1
    # Queue of unlabeled neighbors
    queue = Vector{CartesianIndex{N}}(undef, 0)

    push_in_queue!(index :: CartesianIndex) =
        if input[index] == 0
            # If an element is a background element, label it as so
            output[index] = 0
        elseif output[index] == -1
            # If an element does not have a label, assign current label
            output[index] = label
            push!(queue, index)
        else
            # The item is labeled, do not insert it in the queue
        end
    pop_from_queue!() = pop!(queue)

    for index in CartesianIndices(input)
        push_in_queue!(index)

        delta = (length(queue) != 0) ? 1 : 0
        while length(queue) > 0
            idx = pop_from_queue!()

            for aidx in iterate_adjacent(idx, input)
                push_in_queue!(aidx)
            end
        end
        label = label + delta
    end

    return output
end

Images.label_components(input :: AbstractArray, :: Plane) = Images.label_components(input)

################################
# Euclidean distance transform #
################################

# This code can be rewritten into more beautiful implementation.
# The bad thing is that Julia is against any beauty.

function edt(array    :: AbstractArray{Bool},
             topology :: Topology)
    len = array |> length |> Float64
    binary = map(x -> len^2*(1-x), array)
    return edt!(binary, topology) .|> sqrt
end

function lower_envelope(array :: AbstractVector{Float64})
    # This code is Fortran-like.
    # Proper data structures like lists are required to make it more understandable.
    # Unfortunately, Julia does not have them, so prepare to suffer.
    len = length(array)
    v = zeros(Int, len)
    z = zeros(Float64, len+1)

    # Compute the lower envelope of family of parabolas
    k = 1
    v[1] = 1
    z[1] = -Inf
    z[2] = Inf

    for i in 2:len
        s = 0
        while true
            j = v[k]
            s = ((array[i] + i^2) - (array[j] + j^2))/(2i - 2j)
            if s > z[k]
                break
            end
            k = k - 1
        end
        k = k + 1
        v[k] = i
        z[k] = s
        z[k+1] = Inf
    end

    return v, z
end

function edt!(array :: AbstractVector{Float64}, :: Plane)
    v, z = lower_envelope(array)
    len = length(array)
    dist = similar(array)

    # Compute EDT by selecting a parabola which belongs to a lower
    # envelope for this line segment
    k = 1
    for i = 1:len
        while z[k+1] < i
            k = k + 1
        end
        j = v[k]
        dist[i] = array[j] + (i - j)^2
    end

    array .= dist
    return array
end

function edt!(array :: AbstractVector{Float64}, :: Torus)
    # Just do three times more work than for ::Plane version.
    # This can be optimized in the future.
    len = length(array)
    a2 = vcat(array, array, array)
    edt!(a2, Plane())
    array .= a2[len+1:2len]
    return array
end

function edt!(array :: AbstractMatrix{Float64}, topology :: Topology)
    x, y = size(array)
    local edtfn!(x) = edt!(x, topology)

    columns = (view(array, :, i) for i in 1:y)
    foreach(edtfn!, columns)

    rows = (view(array, i, :) for i in 1:x)
    foreach(edtfn!, rows)

    return array
end

function edt!(array :: AbstractArray{Float64, 3}, topology :: Topology)
    x, y, z = size(array)
    local edtfn!(x) = edt!(x, topology)

    planes = (view(array, :, :, i) for i in 1:z)
    foreach(edtfn!, planes)

    sides = (view(array, i, j, :) for i in 1:x for j in 1:y)
    foreach(edtfn!, sides)

    return array
end

# Use faster implementation from ImageMorphology with Plane topology.
Images.distance_transform(array :: AbstractArray{Bool}, :: Plane) =
    array |> Images.feature_transform |> Images.distance_transform

Images.distance_transform(array :: AbstractArray{Bool}, :: Torus) =
    edt(array, Torus())


# cache strategy -- the interface
struct LocalWindowCache{N} <: AbstractCacheStrategy
    window_size::NTuple{N, Int}
end
LocalWindowCache(window_size::Tuple) = LocalWindowCache{length(window_size)}(map(i->convert(Int, i), window_size))
make_cache(T, S::LocalWindowCache{N}, ax::Tuple) where N = LocalWindowCacheArray{T}(ax..., S.window_size)

# cache implementation
struct LocalWindowCacheArray{T, N, NA, AT<:AbstractArray{T, N}, IA, IB} <: CachedView{T, N}
    cache::AT
    axesA::IA
    axesB::IB
end

function LocalWindowCacheArray{T}(axesA::NTuple{NA}, axesB::NTuple{NA}, window_size::NTuple{NA, Integer}) where {T, NA}
    window_ax = map(window_size) do w
        r = w ÷ 2
        -r:r
    end
    cache_ax = (axesA..., window_ax...)
    N = length(axesA)+length(axesB)

    FT = Union{Missing, T}
    cache = OffsetArray{FT, N}(missing, cache_ax)
    LocalWindowCacheArray{FT, N, length(axesA), typeof(cache), typeof(axesA), typeof(axesB)}(cache, axesA, axesB)
end

Base.axes(A::LocalWindowCacheArray) = (A.axesA..., A.axesB...)
Base.size(A::LocalWindowCacheArray) = map(length, axes(A))