# functions for gathering MeshEntity*s
"""
  This function calculates the numNodesPerType and typeOffsetsPerElement arrays
  and returns them

  Inputs:
    fshape: a FieldShape*
    dim: the dimensionality of the mesh (2 or 3)
    numTypePerElement: number of each dimension entity per element (see
                       interfaces.md)

    Outputs:
      numNodesPerType: array of length 3 or 4 (2d or 3d), containing the number
                       of nodes on each vert, edge, face (or region).

      typeOffsetsPerElement: array of length dim + 2 containing the index of
                             the first node of each type.  The last element
                             is one more than the number of nodes

"""
function getNodeInfo(fshape::Ptr{Void}, dim::Integer, numTypePerElement::Array{I, 1}) where I <: Integer

  num_nodes_v = apf.countNodesOn(fshape, 0)  # number of nodes on a vertex
  num_nodes_e = apf.countNodesOn(fshape, 1) # on edge
  num_nodes_f = apf.countNodesOn(fshape, 2) # on face

  if dim == 3
    num_nodes_r = apf.countNodesOn(fshape, apf.TET)  # on region
    numNodesPerType = [num_nodes_v, num_nodes_e, num_nodes_f, num_nodes_r]
  else
    numNodesPerType = [num_nodes_v, num_nodes_e, num_nodes_f]
  end
  # count numbers of different things per other thing
  # use for bookkeeping
  typeOffsetsPerElement = zeros(Int, dim+2)
  pos = 1
  typeOffsetsPerElement[1] = pos
  for i=2:(dim + 2)
    pos += numTypePerElement[i-1]*numNodesPerType[i-1]
    typeOffsetsPerElement[i] = pos
  end

  return numNodesPerType, typeOffsetsPerElement
end

# can be generalized trivially
function getBoundaryElements(mesh::PumiMeshDG2, bndry_edges::AbstractArray{Int, 1})
# get the faces corresponding to the boundary edges

bndry_faces = zeros(bndry_edges)
faces = Array{Ptr{Void}}(400)  # equivilent to apf::Up
numbering = mesh.entity_Nptrs[mesh.dim + 1]
for i=1:bndry_edges
  edgenum_i = bndry_edges[i]
  face_i = mesh.faces[edgenum_i]
  numFace = apf.countAdjacent(mesh.m_ptr, face_i, mesh.dim)  # should be count upward
  apf.getAdjacent(faces)
  facenum = apf.getNumberJ(numbering, face_i, 0, 0) + 1
#  facenum = getFaceNumber2(faces[1]) + 1

  bndry_faces[i] = facenum
end

return bndry_faces

end

# add if statements to generalized this to 3D
function getEntityPointers(mesh::PumiMesh)
# get the pointers to all the apf::MeshEntities and put them in arrays
# uses the Numberings to determine what the index in the array of each
# entity

  verts = Array{Ptr{Void}}(mesh.numVert)
  edges = Array{Ptr{Void}}(mesh.numEdge)
  elements = Array{Ptr{Void}}(mesh.numEl)
  # get pointers to all MeshEntities
  # also initilize the field to zero
  for entity in apf.MeshIterator(mesh.m_ptr, 0)
    idx = apf.getNumberJ(mesh.vert_Nptr, entity, 0, 0) + 1
    verts[idx] = entity
  end

  for entity in apf.MeshIterator(mesh.m_ptr, 1)
    idx = apf.getNumberJ(mesh.edge_Nptr, entity, 0, 0) + 1
    edges[idx] = entity
  end

  if mesh.dim == 3
    faces = Array{Ptr{Void}}(mesh.numFace)
    for entity in apf.MeshIterator(mesh.m_ptr, 2)
      idx = apf.getNumberJ(mesh.face_Nptr, entity, 0, 0) + 1
      faces[idx] = entity
    end

    for entity in apf.MeshIterator(mesh.m_ptr, 3)
      idx = apf.getNumberJ(mesh.el_Nptr, entity, 0, 0) + 1
      elements[idx] = entity
    end

  else
    faces = edges
    for entity in apf.MeshIterator(mesh.m_ptr, 2)
      idx = apf.getNumberJ(mesh.el_Nptr, entity, 0, 0) + 1
      elements[idx] = entity
    end
  end

  return verts, edges, faces, elements

end  # end getEntityPointers

function getNodeIdx(e_type::Integer, e_idx::Integer, node_idx::Integer, typeOffsetsPerElement, numNodesPerType )
# calculate the local node number (in the list of all nodes on the element)
# from the entity the node is classified on and its index on the entity

# e_type is the type of the entity: 1 = vert, 2 = edge, 3 = face
# node_idx is the index of the node on the entity it is classified on
# e_idx is the index of the entity the node is classified on (ie 1st, 2nd or
# 3rd edge)

  type_offset = typeOffsetsPerElement[e_type]
  nodes_on_type = numNodesPerType[e_type]
  return type_offset -1 + (e_idx - 1)*nodes_on_type + node_idx
end

#------------------------------------------------------------------------------
# misc. helper functions
function getGlobalNodeNumbers(mesh::PumiMesh, elnum::Integer; getdofs=true)

  if getdofs
    numDofPerNode = mesh.numDofPerNode
  else
    numDofPerNode = 1
  end
  dofnums = zeros(Int32, numDofPerNode, mesh.numNodesPerElement)
  getGlobalNodeNumbers(mesh, elnum, dofnums)

  return dofnums
end

# should be generalizable with entity counts
function getGlobalNodeNumbers(mesh::PumiMesh, elnum::Integer, dofnums::AbstractArray{Int32}; getdofs=true)
# gets global node numbers of all dof on all nodes of the element
# output formap is array [numdofpernode, nnodes]  (each column contains dof numbers for a node)
# if getdofs=false, then dofnums need only have 1 column, or can be a vector
# dofnums must be passable to C
# 2D only
# getdofs specifies whether to get dof numbers or node numbers
#

  el_i = mesh.elements[elnum]
  type_i = apf.getType(mesh.m_ptr, el_i)  # what is this used for?

  #println("elnum = ", elnum)
  # calculate total number of nodes
  #nnodes = 3 + 3*mesh.numNodesPerType[2] + mesh.numNodesPerType[3]
  nnodes = mesh.numNodesPerElement

  if getdofs
    numdof = nnodes*mesh.numDofPerNode  # what is this used for?
    numbering_ptr = mesh.dofnums_Nptr
  else
    numdof = nnodes
    numbering_ptr = mesh.nodenums_Nptr
  end

  # get entities in the Pumi order
  node_entities = apf.getNodeEntities(mesh.m_ptr, mesh.mshape_ptr, el_i)

  # get node offsets in the SBP order
  node_offsets = sview(mesh.elementNodeOffsets, :, elnum)
  #node_offsets = sview(mesh.elementNodeOffsets[:, elnum])

  PumiInterface.apf.getDofNumbers(numbering_ptr, node_entities, node_offsets, mesh.nodemapPumiToSbp, el_i, dofnums)  # C implimentation

  return nothing
end

# is this really needed?
function getAdjacentEntityNums(mesh::PumiMesh, entity_index::Integer, input_dimension::Integer, output_dimension::Integer)
# gets the numbers of the adjacent entities (upward or downward) of the given entity
# entity_index specifies the index of the input entity
# input_dimension specifies the dimension of the input entity (0 =vert, 1 = edge ...)
# output_dimension is the dimension of the adjacent  entities to be retrieved
# returns an array containing the indicies of the adjacent entities, and the number of them
# the length of the array might be greater than the number of adjacencies, so use the second returned value for the number of adjacencies

if (input_dimension == output_dimension)
  println("cannot get same level adjacencies")
end

if typeof(mesh) <: Union{PumiMeshDG2, PumiMesh2}

  array1 = [mesh.vert_Nptr, mesh.edge_Nptr, mesh.el_Nptr]
  #array2 = [mesh.verts; mesh.edges; mesh.elements]  # array of arrays
  array2 = Array{Array{Ptr{Void},1}}(3)
  array2[1] = mesh.verts
  array2[2] = mesh.edges
  array2[3] = mesh.elements
else # 3d mesh
   array1 = [mesh.vert_Nptr, mesh.edge_Nptr, mesh.face_Nptr,  mesh.el_Nptr]
  #array2 = [mesh.verts; mesh.edges; mesh.elements]  # array of arrays
  array2 = Array{Array{Ptr{Void},1}}(4)
  array2[1] = mesh.verts
  array2[2] = mesh.edges
  array2[3] = mesh.faces
  array2[4] = mesh.elements
end

# choose which numbering to use
numbering_ptr = array1[output_dimension + 1]
#println("numbering_ptr = ", numbering_ptr)

# get the the entity
#println("array2 = ", array2)
entity_array = array2[input_dimension + 1]
entity = entity_array[entity_index]
#entity = array2[input_dimension+1][entity_index]
#println("entity = ", entity)

#=
entity_type = apf.getType(mesh.m_ptr, entity)
println("entity_type = ", entity_type)
entity_num = apf.getNumberJ(mesh.edge_Nptr, entity, 0, 0)
println("edge number = ", entity_num)
=#

if input_dimension > output_dimension # downward adjacencies
#  println("getting downward adjacencies")
  adjacent_entities, num_adjacent = apf.getDownward(mesh.m_ptr, entity, output_dimension)

else  # upward adjacencies
#  println("getting upward adjacencies")
  num_adjacent = apf.countAdjacent(mesh.m_ptr, entity, output_dimension)
  adjacent_entities = apf.getAdjacent(num_adjacent)
end

# get their numbers
adjacent_nums = zeros(Int, num_adjacent)
for i=1:num_adjacent # loop over adjacent entities

  # get their numbers here
  adjacent_nums[i] = apf.getNumberJ(numbering_ptr, adjacent_entities[i], 0, 0) + 1
end

return adjacent_nums, num_adjacent

end

# this can be generalized with mesh.dim
function getBoundaryFaceLocalNum(mesh::PumiMesh, edge_num::Integer)
# gets the local edge number of a specified edge that is on the boundary
# of the mesh
# edge_num is an edge num from the output of getBoundaryEdgeNums() (ie. the global edge number)
# the local edge number is the edge number within the element (1st,s 2nd, 3rd...)
#TODO: usage of this function might be a problem because it returns the local face number
#      of the *Pumi* reference topology, not the SBP topology.
  face_i = mesh.faces[edge_num]

  # get mesh face associated with edge
  apf.countAdjacent(mesh.m_ptr, face_i, mesh.dim)
  el = apf.getAdjacent(1)[1]  # get the single face (not an array)

#  elnum_i = apf.getNumberJ(mesh.el_Nptr, face, 0, 0) + 1
#  facenum_i = getFaceNumber2(face) + 1  # convert to 1 based indexing
  down_faces = Array{Ptr{Void}}(12)
  numedges = apf.getDownward(mesh.m_ptr, el, mesh.dim-1, down_faces)
  facenum_local = 0
  for j = 1:mesh.numFacesPerElement  # find which local edge is edge_i
    if down_faces[j] == face_i
      facenum_local = j
    end
  end

  return facenum_local

end

# this can be generalized with some topology and dimension information
function getFaceLocalNum(mesh::PumiMesh, edge_num::Integer, element_num::Integer)
# find the local edge number of a specified edge on a specified element

  edge = mesh.faces[edge_num]
  element = mesh.elements[element_num]

  down_edges = Array{Ptr{Void}}(12)
   numedges = apf.getDownward(mesh.m_ptr, element, mesh.dim-1, down_edges)

  edgenum_local = 0
   for j = 1:mesh.numFacesPerElement  # find which local edge is edge_i
    if down_edges[j] == edge
      edgenum_local = j
    end
  end

  return edgenum_local

end

function getElementVertMap(mesh::PumiMesh)
# get array that maps from elements to their vertex numbers
# the array is numVertPerElement x numEl
  numVertPerElement = mesh.numTypePerElement[1]

  elvertmap = zeros(Int32, numVertPerElement, mesh.numEl)
  verts = Array{Ptr{Void}}(12)  # apf::Downward
  vert_dim = 0

  for i=1:mesh.numEl
    el_i = mesh.elements[i]
    apf.getDownward(mesh.m_ptr, el_i,  vert_dim, verts)

    for j=1:numVertPerElement
      vert_j = verts[j]
      elvertmap[j, i] = apf.getNumberJ(mesh.vert_Nptr, vert_j, 0, 0) + 1
    end
  end

  return elvertmap
end


using Downloads
using ZipFile
url = "https://m-selig.ae.illinois.edu/ads/archives/coord_seligFmt.zip"
archive = joinpath(@__DIR__, "coord_seligFmt.zip")
reader = ZipFile.Reader(archive)
output = open(joinpath(@__DIR__, "../src/database.jl"), "w")
println(output, "#################### AUTO GENERATED! DO NOT MODIFY ####################")
println(output, "const database = AirfoilData[")
index = []
asciionly(s) =  join(map(m->getfield(m, :match), eachmatch(r"[0-9a-z ]", s)))

function formatpoints(points)
    x0, y0 = parse.(Float64, split(points[1]))
    iox = IOBuffer()
    ioy = IOBuffer()
    print(iox, '[')
    print(ioy, '[')
    if x0>2 && y0>2 # Lednicer's format
        np1 = round(Int, x0)
        np2 = round(Int, y0)
        @assert np1+np2 == length(points)-1
        for i in np1:-1:2
            x, y = split(points[i+1])
            print(iox, x, ',')
            print(ioy, y, ',')
        end
        for i in 1:np2
            x, y = split(points[i+1+np1])
            print(iox, x, ',')
            print(ioy, y, ',')
        end
    else # original format
        for l in points
            x, y = split(l)
            print(iox, x, ',')
            print(ioy, y, ',')
        end
    end
    print(iox, "],")
    write(iox, take!(ioy))
    print(iox, ']')
    String(take!(iox))
end

for f in reader.files
    dir, fname = splitdir(f.name)
    name, ext = splitext(fname)
    name = uppercase(name)
    lines = readlines(f)
    desc = strip(lines[1])
    points = filter(l->!isempty(l), strip.(@view lines[2:end]))
    @info "    $name: $desc"
    println(output, "    AirfoilData(\"", name, "\",\"", desc, "\",", formatpoints(points), "),")
    push!(index, lowercase(name) * " " * asciionly(lowercase(desc)))
end

println(output, "]")
println(output)
println(output, "const index = String[")
for l in index
    println(output, "    \"", l, "\",")
end
println(output, "]")
println(output)
println(output, "const db_size = $(length(index))")
close(output)


module SimpleNeRF

using Random

using ArgCheck
using Distributions
using Flux
using Functors
using Optimisers
using Configurations
using ChainRulesCore
using ChainRulesCore: NoTangent
using MLUtils: stack, unsqueeze, unbatch
# using PyCall
# using PyCallChainRules.Jax: JaxFunctionWrapper

# const jax = PyNULL()

# function __init__()
#   copy!(jax, pyimport("jax"))
# end

# # From https://github.com/FluxML/Functors.jl/issues/35
# # and https://github.com/FluxML/Optimisers.jl/pull/57
# const INIT = Base._InitialValue();
# function total(f, x; init = INIT)
#     fmap(x; exclude = Optimisers.isnumeric, walk = (f, z) -> foreach(f, Optimisers._trainable(z))) do y
#       val = f(y)
#       init = init===INIT ? val : (init+val)
#     end
#     init
# end

# function fmapreduce(f, op, x; init = INIT, walk = (f, x) -> foreach(f, Functors.children(x)), kw...)
#     fmap(x; walk, kw...) do y
#       init = init===INIT ? f(y) : op(init, f(y))
#     end
#     init===INIT ? Base.mapreduce_empty(f, op) : init
# end

include("repeat.jl")
include("interpolations.jl")

include("data.jl")
include("model.jl")
include("render.jl")

include("train.jl")

end


#!/usr/bin/env julia

using Luxor

using Test

using Random
Random.seed!(42)

function test_rulers(x, y, rot, w)
    gsave()
    translate(x, y)
    rotate(rot)
    box(0, 0, w, w, :clip)
    rulers()
    clipreset()
    grestore()
end

function drawing_png_buffer()
    width, height = 2000, 2000
    Drawing(width, height, :png)
    origin(1200, 1200)
    background("ivory")
    pagetiles = Tiler(width, height, 5, 5, margin=10)
    for (pos, n) in pagetiles
      test_rulers(pos.x, pos.y, rand() * 2pi, pagetiles.tilewidth)
    end

    println("...finished rulers in png buffer test")
    @test finish() == true
end

function drawing_svg_buffer()
    width, height = 2000, 2000
    Drawing(width, height, :svg)
    origin(1200, 1200)
    background("ivory")
    pagetiles = Tiler(width, height, 5, 5, margin=10)
    for (pos, n) in pagetiles
      test_rulers(pos.x, pos.y, rand() * 2pi, pagetiles.tilewidth)
    end

    println("...finished rulers in svg buffer test")
    @test finish() == true
end

function drawing(fname)
    width, height = 2000, 2000
    Drawing(width, height, fname)
    origin(1200, 1200)
    background("ivory")
    pagetiles = Tiler(width, height, 5, 5, margin=10)
    for (pos, n) in pagetiles
      test_rulers(pos.x, pos.y, rand() * 2pi, pagetiles.tilewidth)
    end

    @test finish() == true
    println("...finished rulers test: output in $(fname)")
end

drawing_png_buffer()
drawing_svg_buffer()
drawing("rulers-test.pdf")


module Filtering

using LinearAlgebra,
    Statistics,
    Optim,
    ProgressMeter,
    StatsBase,
    DiffResults,
    Distributions,
    Logging,
    Random,
    ForwardDiff,
    StatsFuns

include("models.jl")
include("filter.jl")
include("smoother.jl")
include("em.jl")
include("resampling.jl")
include("particle.jl")
include("score_filter.jl")
#include("bare_particle.jl")
#include("gradient_particle.jl") # This may be outdated
#include("online_particle.jl")
#include("threads.jl")
include("optimizers.jl")

# TODO: These models should be moved to a FilteringModels package
include("models/ar1.jl")
include("models/smoothingspline.jl")
include("models/ss_tfi.jl")
include("models/tfi.jl")

end # module


#= License
Copyright 2019, 2020, 2021 (c) Yossi Bokor

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=#

__precompile__()

module Skyler

#### Requirements ####
using CSV
using DataFrames
using LinearAlgebra
using NearestNeighbors

#=
using Distributed
@everywhere using CSV
@everywhere using DataFrames
@everywhere using LinearAlgebra
@everywhere using NearestNeighbors
=#

using Distances
using Optim
using SimpleGraphs
using Random
using Distributions
using QuadGK
using Plots
using JLD
using SpecialFunctions
using SharedArrays
using Calculus
using NNlib
using FiniteDiff
using ProgressMeter

#### Exports ####

export 	skyler,
		unittest,
		Determine_Dimension,
		Plot_PPC,
		Plot_Graph,
		Load_Example,
		PartitionedPointCloud,
		StratifiedSpace,
		EmbeddedStratifiedSpace,
		Dimension_Split,
		Create_Partition_DataFrame

#### Structures ####

mutable struct PartitionedPointCloud
	Points::AbstractArray
	Strata::Dict{Int, Set}
	Dimensions::Dict{Int, Set}
	Boundaries::AbstractArray
end

mutable struct StratifiedSpace
	StrataDimensions::Dict{Int, Set}
	Boundaries::AbstractArray
end

mutable struct EmbeddedStratifiedSpace
	StratSpace::StratifiedSpace
	Equations::Dict{Int, Array}
end

#### Background Functions ####

function Spherical_Shell(ball_tree::BallTree, point::Array, inner::T, outer::U) where {T<: Number, U<: Number} #this obtains all the samples in the spherical shell around a chosen sample 'point'
	ball_1 = inrange(ball_tree, point, inner, true)
	ball_2 = inrange(ball_tree, point, outer, true)
	annulus = setdiff(ball_2, ball_1)
	return annulus
end

function Determine_Dimension(sample::A, ball_tree::BallTree, index::Int, sample_epsilon::P, radius::R) where {P<:Number,R<:Number, A<:Union{AbstractArray, LinearAlgebra.Adjoint}}# determine whether the `index'th point in the sample looks 0 or 1 dimensional given the coordinates of the other points, a pre-generated ball tree, and the correct algorithm parameters of 'angle_conditon', 'inner_radius', and 'outer_radius'
	point = sample[:,index]
	distance_matrix = pairwise(Euclidean(), sample, dims=2)
	node_neighbours = Spherical_Shell(ball_tree, point, 0., radius+sample_epsilon)
	node_neighbours = push!(node_neighbours, index)
	G_point = IntGraph()
	for i in node_neighbours
		add!(G_point,i)
	end
	for i in vlist(G_point)
		for j in vlist(G_point)
			if distance_matrix[i,j] <= 2*sample_epsilon
				add!(G_point, i, j)
			end
		end
	end
	n_p = size(sample,2) #number of samples
	nc_1 = num_components(G_point)
	if nc_1 != 1 #if the graph on the points within a ball of the chosen points is disconnected, then the point is not close to a vertex
		return 1
	else # if the graph on the points in a ball is connected, then we need to check if 0 or 1 dimensional structure
		node_neighbours_1 = Spherical_Shell(ball_tree, point, radius-1sample_epsilon, radius+sample_epsilon)
		G_point_1 = IntGraph() # construct graph on points in a spherical shell
		for i in node_neighbours_1
			add!(G_point_1, i)
		end
		for i in vlist(G_point_1)
			for j in vlist(G_point_1)
				if distance_matrix[i,j] <= 3*sample_epsilon
					add!(G_point_1, i, j)
				end
			end
		end
		nc_2 =num_components(G_point_1)
		if nc_2 == 2 #if the number of connected components is 2, we need to check if they are roughly opposite each other or not
			cc = collect(SimpleGraphs.components(G_point_1))
			mid_point_1 = []
			for l in 1:size(sample,1)
				mid_point_1_l = 0
				cc_1 = collect(cc[1])
				for p in cc_1
					mid_point_1_l += sample[l, p]
				end
			append!(mid_point_1, mid_point_1_l/(length(cc_1)))
			end
			mid_point_2= []
			for l in 1:size(sample,1)
				mid_point_2_l = 0
				cc_2 = collect(cc[2])
				for p in cc_2
					mid_point_2_l += sample[l,p]
				end
			append!(mid_point_2, mid_point_2_l/(length(cc_2)))
			end
			mid_point_1 = mid_point_1 .- sample[:,index]
			mid_point_2 = mid_point_2 .- sample[:,index]
			#cos_of_angle = dot(mid_point_1, mid_point_2)/(norm(mid_point_1)*norm(mid_point_2))
			if dot(mid_point_1, mid_point_2) <= -radius^2 +2*radius*sample_epsilon +7*sample_epsilon^2
				return 1
			else
				return 0
			end
		else #if the number of connected components is not 2, return dimension 0
			return 0
		end
	end
end

function Dimension_Split(sample::A, sample_epsilon::S, radius::U) where {S<: Number, U<:Number, A<:AbstractArray}# for each point in the sample, determine if it looks 0 or 1 dimensional
	#if nprocs() == 1
		n_p = size(sample,2)
		ball_tree = BallTree(sample)
		dimension_split = Dict{Int,Set}(0 => Set(), 1 =>Set())
		println("Calculating local structure for each sample: ")
		@showprogress for i in 1:n_p
			dim = Determine_Dimension(sample, ball_tree, i, sample_epsilon,radius)
			dimension_split[dim]=push!(dimension_split[dim], i)
		end
		return dimension_split
	#else
	#	n_p = size(sample,2)
	#	@everywhere ball_tree=BallTree(sample)
	#end
end

function Find_Groups(distance_matrix::A, dictionary_of_points::Dict{Int, Set}, dimension::T, connection_threshold::W) where {T <: Number, W<: Number, A<:AbstractArray} # obtain the clusters of points which look 'dimensional'  dimensional
	points = collect(dictionary_of_points[dimension]) #create a set of points
	G = SimpleGraph()
	for i in points
		add!(G, i)
	end
	for i in points
		for j in points
			if distance_matrix[i,j] <= connection_threshold #connect points below the connection threshold to cluster
				add!(G, i, j)
			end
		end
	end
	components = []
	vertices = vlist(G)
	n_v = length(vertices)
	checked = Set()
	discovered=[]
	function DFS(Graph, vertex)
		append!(discovered,vertex)
		for u in neighbors(Graph, vertex)
			if u in(discovered)
			else
				DFS(Graph, u)
			end
		end
	end
	for i in vertices
		if i in checked
		else
			push!(checked, i)
			discovered =[]
			DFS(G, i)
			append!(components, [discovered])
			for n in discovered
				push!(checked, n)
			end
		end
	end
	return components
end

function Generate_Strata_Assignments(distance_matrix::Array, dimension_split::Dict{Int, Set}, v_threshold::T, e_threshold::W) where {T <: Number, W<:Number} # generate the clusters of vertices and edges using the above functions
	v_comps = Find_Groups(distance_matrix, dimension_split, 0, v_threshold)
	number_of_vertices = length(v_comps)
	e_comps = Find_Groups(distance_matrix, dimension_split, 1, e_threshold)
	number_of_edges = length(e_comps)
	change = []
	for i in 1:number_of_edges
		if size(e_comps[i]) == 1
			append!(change, e_comps[i][1])
		end
	end
	if length(change) !=0
		d_s = copy(dimension_split)
		for i in 1:length(change)
			d_s[0] = push!(d_s[0], change[i])
			d_s[1] = setdiff(d_s[1], Set([change[i]]))
		end
		v_comps = Find_Groups(distance_matrix, d_s, 0, v_threshold)
		number_of_vertices = length(v_comps)
		e_comps = Find_Groups(distance_matrix, d_s, 1, e_threshold)
		number_of_edges = length(e_comps)
	end
	strata_assignments = Dict{Int, Set}(1=>Set())
	strata_dimensions = Dict{Int,Set}(0=> Set(), 1=>Set())
	#println("Detected ", number_of_vertices," vertices and ", number_of_edges, " edges.")
	for i in 1:number_of_vertices
		strata_assignments[i] =Set(v_comps[i])
		strata_dimensions[0] =push!(strata_dimensions[0],i)
	end
	for i in 1:number_of_edges
		strata_assignments[number_of_vertices+i] =Set(e_comps[i])
		strata_dimensions[1] =push!(strata_dimensions[1],number_of_vertices+i)
	end
	return strata_assignments, strata_dimensions
end

function Generate_Abstract_Structure(points::A, dimension_split::Dict{Int, Set}, sample_epsilon::T, vertex_threshold::T, edge_threshold::U) where {U<:Number, T<:Number, A<:AbstractArray} # obtain the abstract structure of the graph
	dists = pairwise(Euclidean(), points, dims=2)
	corrected = false
	while corrected == false
		strata_assignments, strata_dimensions = Generate_Strata_Assignments(dists, dimension_split, vertex_threshold, edge_threshold)
		max_dim = maximum(keys(strata_dimensions))
		n_s = maximum(keys(strata_assignments))
		boundaries = zeros(n_s, n_s)
		@showprogress for i in 0:max_dim-1
			for j in strata_dimensions[i]
				s_j = points[:,collect(strata_assignments[j])]
				for k in strata_dimensions[i+1]
					s_k = points[:,collect(strata_assignments[k])]
					d_j_k = Distances.pairwise(Euclidean(), s_j, s_k, dims=2)
					if minimum(d_j_k) <= 3*sample_epsilon
						boundaries[j,k]=1
					end
				end
			end
		end
		println("\r The strata are ", strata_dimensions, " and the boundary matrix is  ", boundaries,".")
		change = []
		if isempty(strata_dimensions[1])
			corrected = true
			StratSpace = StratifiedSpace(strata_dimensions, boundaries)
			return StratSpace, strata_assignments
			break
		else
			for i in strata_dimensions[1]
				if sum(boundaries[:,i]) !=2
					append!(change, i)
				end
			end
		end
		if length(change) == 0
			corrected = true
			StratSpace = StratifiedSpace(strata_dimensions, boundaries)
			return StratSpace, strata_assignments
			break
		else
			println("Stray edge detected, addressing it.")
			for k in 1:length(change)
				dimension_split[0] = union(dimension_split[0], strata_assignments[change[k]])
				dimension_split[1] = setdiff(dimension_split[1], strata_assignments[change[k]])
			end
		end
	end
end

function Partition_Point_Cloud(points::A, sample_epsilon::P, radius::W, vertex_threshold::T, edge_threshold::U)::PartitionedPointCloud where {P<:Number, Q<:Number, R<:Number, W<:Number, T <: Number, U<:Number, A<:AbstractArray}
	dimension = size(points,1)
	distances = pairwise(Euclidean(), points, dims=2)
	dim_split = Dimension_Split(points, sample_epsilon, radius) #partition the sample into 0 or 1 dimensional using a dictionary
	#strata_assignments, strata_dimensions = Generate_Strata_Assignments(distances, dim_split, vertex_threshold, edge_threshold) #find the strata for the partioned point cloud
	StratSpace, strata_assignments = Generate_Abstract_Structure(points, dim_split, sample_epsilon, vertex_threshold, edge_threshold) #obtain the boundary relations
	change = []
	ppc = PartitionedPointCloud(points, strata_assignments, StratSpace.StrataDimensions, StratSpace.Boundaries)
	return ppc
end
#=
Deprecated this was used for modelling the embedding, unsure about future uses
function Generate_Partition(v_clusters, e_clusters)
	lengths = []
	partitions = []
	for i in 1:length(v_clusters)
		append!(lengths, length(v_clusters[i]))
		append!(partitions, v_clusters[i])
	end
	for i in 1:length(e_clusters)
		append!(lengths, length(e_clusters[i]))
		append!(partitions, e_clusters[i])
	end
	return partitions, lengths
end
=#

function Create_Partition_DataFrame(ppc::PartitionedPointCloud) #create a dataframe we can save the partitioned point cloud in
	dimension = size(ppc.Points,1)
	df = DataFrame(ppc.Points[:,1]')
	insert!(df, 1, [1], :index)
	insert!(df, 2, [1], :dimension)
	deleterows!(df, 1)
	for i in 0:maximum(keys(ppc.Dimensions))
		for j in ppc.Dimensions[i]
			for k in ppc.Strata[j]
				l = [j i]
				for n in 1:dimension
					l=hcat(l, ppc.Points[n,k])
				end
				push!(df, l)
			end
		end
	end
	return sort(df)
end

function Plot_PPC(ppc::PartitionedPointCloud) #plot the partiton with nice colours
	Plots.pyplot()
	if size(ppc.Points,1) == 2
		df = DataFrame(index=Int[], dimension=Int[], c1=[], c2=[])
		for i in 0:maximum(keys(ppc.Dimensions))
			for j in ppc.Dimensions[i]
				for k in ppc.Strata[j]
					push!(df, [j,i,ppc.Points[1,k], ppc.Points[2,k]])
				end
			end
		end
		Plots.scatter( df[!,:c1],df[!,:c2], group=df[!,:index])
	else
		println("Points live in 3 or more dimensions, projecting onto the first 3.")
		df = DataFrame(index=Int[], dimension=Int[], c1=[], c2=[], c3=[])
		for i in 0:maximum(keys(ppc.Dimensions))
			for j in ppc.Dimensions[i]
				for k in ppc.Strata[j]
					push!(df, [j,i,ppc.Points[1,k], ppc.Points[2,k], ppc.Points[3,k]])
				end
			end
		end
		Plots.scatter3d( df[!,:c1],df[!,:c2], df[!,:c3], group=df[!,:index])
	end
end

function Plot_PPC(partition::DataFrame)
	Plots.pyplot()
	dimension = size(partition[!,:point][1],1)
	if dimension == 2
		points = Array{Float64}(undef, 0,2)
		for i in 1:size(partition,2)
			points = hcat(points, partition[i,:point])
		end
		Plots.scatter( points[:,1], points[:,2], group=partition[!,:index])
	else
		println("Points live in 3 or more dimensions, projecting onto the first 3.")
		points = Array{Float64}(undef, 0,3)
		for i in 1:size(partition,1)
			points = vcat(points, partition[i,:point][1:3]')
		end
		Plots.scatter3d( points[:,1], points[:,2], points[:,3], group=partition[!,:index])
	end
end

function Plot_PPC(partition::DataFrame, c_1::Int, c_2::Int)
	Plots.pyplot()
	println("Projecting on to the following coordinates: $c_1 and $c_2.")
	points = Array{Float64}(undef, 0,2)
	for i in 1:size(partition,1)
		points = vcat(points, [partition[i,:point][c_1] partition[i,:point][c_2]])
	end
	Plots.scatter( points[:,1], points[:,2], group=partition[!,:index])
end

function Plot_PPC(partition::DataFrame, c_1::Int, c_2::Int, c_3::Int)
	Plots.pyplot()
	println("Projecting on to the following coordinates: $c_1, $c_2 and $c_3.")
	points = Array{Float64}(undef, 0,3)
	for i in 1:size(partition,1)
		points = vcat(points, [partition[i,:point][c_1] partition[i,:point][c_2] partition[i,:point][c_3]])
	end
	Plots.scatter3d( points[:,1], points[:,2], points[:,3], group=partition[!,:index])
end

function Plot_Graph(vertex_locations, edges, sample) #plot a graph (either exact or modelled)
	Plots.pyplot()
	if length(sample) == 2
		scatter(vertex_locations[sample[1], :], vertex_locations[sample[2], :], s=1)
		for i in 1:(size(edges,1)-1)
			plot!([vertex_locations[sample[1], edges[i,1]], vertex_locations[sample[1],edges[i,2]]], [vertex_locations[sample[2], edges[i,1]], vertex_locations[sample[2], edges[i,2]]], linestyle = :solid)
		end
		i=size(edges,1)
		plot!([vertex_locations[sample[1], edges[i,1]], vertex_locations[sample[1],edges[i,2]]], [vertex_locations[sample[2], edges[i,1]], vertex_locations[sample[2], edges[i,2]]], linestyle = :solid)
	elseif length(sample) == 3
		scatter3d(vertex_locations[sample[1], :], vertex_locations[sample[2], :], vertex_locations[sample[3], :],s=1)
		for i in 1:(size(edges,1)-1)
			plot!([vertex_locations[sample[1], edges[i,1]], vertex_locations[sample[1], edges[i,2]]], [vertex_locations[sample[2], edges[i,1]], vertex_locations[sample[2], edges[i,2]]], [vertex_locations[sample[3], edges[i,1]], vertex_locations[sample[3], edges[i,2]]],  linestyle =:solid)
		end
		i=size(edges,1)
		plot!([vertex_locations[sample[1], edges[i,1]], vertex_locations[sample[1], edges[i,2]]], [vertex_locations[sample[2], edges[i,1]], vertex_locations[sample[2], edges[i,2]]], [vertex_locations[sample[3], edges[i,1]], vertex_locations[sample[3], edges[i,2]]],  linestyle =:solid)
	else
		println("Please specify 2 or 3 sample to project onto.")
		return []
	end
end

function Plot_Graph(vertex_locations, edges) #plot a graph (either exact or modelled)
	Plots.pyplot()
	if size(vertex_locations,1) == 2
		scatter(vertex_locations[sample[1], :], vertex_locations[sample[2], :], s=1)
		for i in 1:(size(edges,1)-1)
			plot!([vertex_locations[1, edges[i,1]], vertex_locations[1,edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], linestyle = :solid)
		end
		i=size(edges,1)
		plot!([vertex_locations[1, edges[i,1]], vertex_locations[1,edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], linestyle = :solid)
	elseif size(vertex_locations,1) == 3
		scatter3d(vertex_locations[1, :], vertex_locations[2, :], vertex_locations[3, :],s=1)
		for i in 1:(size(edges,1)-1)
			plot!([vertex_locations[1, edges[i,1]], vertex_locations[1, edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], [vertex_locations[3, edges[i,1]], vertex_locations[3, edges[i,2]]],  linestyle =:solid,  c=:red)
		end
		i=size(edges,1)
		plot!([vertex_locations[1, edges[i,1]], vertex_locations[1, edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], [vertex_locations[3, edges[i,1]], vertex_locations[3, edges[i,2]]],  linestyle =:solid,  c=:red)
	else
		println("Projecting onto the first 3 coordinates.")
		scatter3d(vertex_locations[1, :], vertex_locations[2, :], vertex_locations[3, :],s=1)
		for i in 1:(size(edges,1)-1)
			plot!([vertex_locations[1, edges[i,1]], vertex_locations[1, edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], [vertex_locations[3, edges[i,1]], vertex_locations[3, edges[i,2]]],  linestyle =:solid)
		end
		i=size(edges,1)
		plot!([vertex_locations[1, edges[i,1]], vertex_locations[1, edges[i,2]]], [vertex_locations[2, edges[i,1]], vertex_locations[2, edges[i,2]]], [vertex_locations[3, edges[i,1]], vertex_locations[3, edges[i,2]]],  linestyle =:solid)
	end
end

function model_fit(data, muT, sigma, E, S; EM_it = 3)

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

	#### Functions for modelling the underlying structure ####

	#=
	To update:
	-keeps sigma fixed at the moment, this can be made optional
	=#

	#=
	MAKE SURE S AND MUT ARE IN THE SAME ORDER
	=#

	#=
	notns:
	n		---	Number of data points
	d		---	Dimension of data
	N		---	Number of strata pieces
	N_0		---	Number of 0-dim strata pieces
	N_1		---	Number of 1-dim strata pieces
	data	---	n dim array of d arrays		---	Sample from embedded graph
	mu		---	N_0 by d dim array	---	Iniitial vertex sample locations
	sigma	---	N dim array			---	Initial error on each strata piece
	E		---	n by N array		---	Initial assignment for each data point
	S		---	N dim str array		---	Abstract graph vertex/edge names in that order
	=#

	# data is a n dim array of d dimensional vectors

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

	####pre-processing

	#get size of different dimensional strata

	#println("S starts as $S")

	#println("mu starts as $muT")

	N_0 	= size(muT,1)

	mu 		= [muT[i,:] for i in 1:N_0]

	N_1 	= size(S ,1) - N_0

	N 		= N_0 + N_1

	n  		= size(data,1)

	d 		= size(data[1],1)

	#load parameters

	E = [E[i,:] for i = 1:n]

	Pi      = 	sum(E)./n

	#println("sigma in EM is", sigma)

	MU		=	Dict{Any,Array}(S[i] => mu[i] for i = 1:N_0)

	SIGMA	=	Dict{Any,Float64}(S[i] => sigma[i] for i = 1:N)

	PI		=	Dict{Any,Float64}(S[i] => Pi[i] for i =1:N)

	#println("MU: ", MU)

	#println("SIGMA: ", SIGMA)

	#println("PI: ", PI)

	function reload_param(mu_t,sigma_t,Pi_t; S_t = S, N_0_t = N_0, N_t = N)

		#updates dictionary values for:
		#mu_t 		N_0 by d float array
		#SIGMA_t 	N float array
		#Pi 		N_float array

		MU_t	=  	Dict{Any,Array}(S_t[i] => mu_t[i] for i = 1:N_0_t)

		SIGMA_t	=  	Dict{Any,Float64}(S_t[i] => sigma_t[i] for i = 1:N_t)

		PI_t	=  	Dict{Any,Float64}(S_t[i] => Pi_t[i] for i = 1:N_t)

		return MU_t, SIGMA_t, PI_t

	end

	####density functions

	#0-dim strata

	function gaussian(x,v,s)

		linear = x - v

		return exp(-dot(linear, linear)/(2*s))

	end

	function p_e(x,v,s)

		d = size(x)[1]

		den = gaussian(x,v,s)/((2*pi*s)^(d/2))

		return den

	end

	function log_p_e(x,v,s)

		d = size(x)[1]

		linear = x - v

		den= -(d/2)*log(2*pi*s^2) - dot(linear,linear)/(2*s^2)

		return den

	end

	#1-dim strata

	#with clipping

	function p_l(xj, v1, v2, s)

		d = size(xj,1)

		xj = - xj

		den = (exp((4*dot(v1 - v2, (v1 + v2)/2 + xj)^2 - 4*norm(v1 - v2)^2*norm((v1 + v2)/2 + xj)^2)/(8*s^2*norm(v1 - v2)^2))*pi^(1/2 - d/2)*(-erf((2*dot(v1 - v2, (v1 + v2)/2 + xj) - norm(v1 - v2)^2)/(2*sqrt(2)*s*norm(v1 - v2))) + erf((2*dot(v1 - v2, (v1 + v2)/2 + xj) + norm(v1 - v2)^2)/(2*sqrt(2)*s*norm(v1 - v2)))))/norm(v1 - v2)

		if den <= -6

			den = -6

		end

  		return den

  	end

  	function log_p_l(Xj, V1, V2, s)

		d = size(Xj,1)

		Xj = - Xj

		den = log(2^((-1 - d)/2)*pi^(1/2 - d/2)*s^(1 - d)) +
				log(-erf((2*dot(V1 - V2, (V1 + V2)/2 + Xj) - norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2))) +
			   	erf((2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2)))) -
			 	log(norm(V1 - V2)) + (4*dot(V1 - V2, (V1 + V2)/2 + Xj)^2 - 4*norm(V1 - V2)^2*norm((V1 + V2)/2 + Xj)^2)/
			  	(8*s^2*norm(V1 - V2)^2)

		if den <= -6

			den = -6

		end

  		return den

  	end

  	#density evaluation matrix

	function densities(MU_t,SIGMA_t; S_t = S, N_0 = N_0 , N_1 = N_1, N = N)

		zero_dim 	= [x -> p_e(x,MU_t[strat],SIGMA_t[strat]) for strat = S_t[1:N_0]]

		one_dim  	= [x -> p_l(x,MU_t[strat[2]],MU_t[strat[1]],SIGMA_t[strat]) for strat = S_t[N_0+1:N]]

		return one_dim # vcat(zero_dim,one_dim)

	end

	function log_densities(mu_t,SIGMA_t; S_t = S, N_0 = N_0 , N_1 = N_1, N = N)

		MU_t    	=   Dict{Any,Array}(S_t[i] => mu_t[i] for i = 1:N_0)

		zero_dim 	=   [x -> log_p_e(x,MU_t[strat],SIGMA_t[strat]) for strat = S_t[1:N_0]]

		one_dim  	=   [x -> log_p_l(x,MU_t[strat[1]],MU_t[strat[2]],SIGMA_t[strat]) for strat = S_t[N_0+1:N]]

		return  vcat(zero_dim,one_dim)

	end

	function density_matrix(data,density_functions)

		dm 			= 	[ (broadcast(rho, data))  for rho in density_functions ]

		dm 			=  	hcat(dm...)

		dm 			= 	[dm[i,:] for i = 1:n]

		return dm

	end

	####cost construction

	#expectation matrix for current parameters

	function expectation(data,MU_t,SIGMA_t,Pi_t)

		DM = density_matrix(data,log_densities(MU_t,SIGMA_t))

		logpi = log.(Pi_t)

		E = broadcast(t -> logpi .+ t, DM)

		E = broadcast(t -> softmax(t), E)

		return E

	end

	#cost for current vertex values

	function Cost(data,mu0,SIGMA_t, Pi, E)

		mu_t = [mu0[i,:] for i in 1:N_0]

		log_DM = density_matrix(data,log_densities(mu_t,SIGMA_t))

		#not needed in optimisation but can be checked for EM
		#log_pi = log.(Pi)
		#log_sum = broadcast(t -> t + log_pi, log_DM)

		w_sum = broadcast((e,t) -> sum(e.*t), E, log_DM)

		return sum(w_sum)/n

	end

	####analytic gradient construction

	#gradient values for cost function

	function grad_log_p_l(k, Xj, V1, V2, s)

		xj = Xj[k]

		v1 = V1[k]

		v2 = V2[k]

		if dot(V2 - V1, Xj) > dot(V1, V2 - V1)

			xj = xj - ((dot(V2 - V1, Xj) - dot(V1, V2 - V1) )/(norm(V2 - V1)^2))*(v2 - v1)

			Xj = Xj - ((dot(V2 - V1, Xj) - dot(V1, V2 - V1) )/(norm(V2 - V1)^2)).*(V2 - V1)

		elseif dot(V2 - V1, Xj) < dot(V2, V1 - V2)

			xj = xj - ((dot(V2 - V1, Xj) -  dot(V2, V1 - V2) )/(norm(V2 - V1)^2))*(v2 - v1)

			Xj = Xj - ((dot(V2 - V1, Xj) -  dot(V2, V1 - V2) )/(norm(V2 - V1)^2)).*(V2 - V1)

		end

		g_v1 = -(v1 + v2 + 2*xj)/(4*s^2) + (2*(-1 + exp(dot(V1 - V2, (V1 + V2)/2 + Xj)/s^2))*(v1 - v2)*
			dot(V1 - V2, (V1 + V2)/2 + Xj) + (3*v1 - v2 + 2*xj - exp(dot(V1 - V2, (V1 + V2)/2 + Xj)/s^2)*
		  	(v1 + v2 + 2*xj))*norm(V1 - V2)^2)/
			(exp((2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)^2/(8*s^2*norm(V1 - V2)^2))*sqrt(2*pi)*s*
			(erf((-2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2))) +
			erf((2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2))))*
			norm(V1 - V2)^3) + ((-v1 + v2)*dot(V1 - V2, (V1 + V2)/2 + Xj)^2 +
			(s^2*(-v1 + v2) + (v1 + xj)*dot(V1 - V2, (V1 + V2)/2 + Xj))*norm(V1 - V2)^2)/(s^2*norm(V1 - V2)^4)

		g_v2 = -(v1 + v2 + 2*xj)/(4*s^2) + (-2*(-1 + exp(dot(V1 - V2, (V1 + V2)/2 + Xj)/s^2))*(v1 - v2)*
			dot(V1 - V2, (V1 + V2)/2 + Xj) + (-v1 - v2 - 2*xj + exp(dot(V1 - V2, (V1 + V2)/2 + Xj)/s^2)*
	 		(-v1 + 3*v2 + 2*xj))*norm(V1 - V2)^2)/
			(exp((2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)^2/(8*s^2*norm(V1 - V2)^2))*sqrt(2*pi)*s*
			(erf((-2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2))) +
			erf((2*dot(V1 - V2, (V1 + V2)/2 + Xj) + norm(V1 - V2)^2)/(2*sqrt(2)*s*norm(V1 - V2))))*
			norm(V1 - V2)^3) + ((v1 - v2)*dot(V1 - V2, (V1 + V2)/2 + Xj)^2 -
			(s^2*(-v1 + v2) + (v2 + xj)*dot(V1 - V2, (V1 + V2)/2 + Xj))*norm(V1 - V2)^2)/(s^2*norm(V1 - V2)^4)

	   	return g_v1, g_v2

	end

 	function grad_log_p_l_n(k, Xj, V1, V2, s)

		xj = Xj[k]

		v1 = V1[k]

		v2 = V2[k]

                if dot(V2 - V1, Xj) > dot(V1, V2 - V1)

                        xj = xj - ((dot(V2 - V1, Xj) - dot(V1, V2 - V1) )/(norm(V2 - V1)^2))*(v2 - v1)

                        Xj = Xj - ((dot(V2 - V1, Xj) - dot(V1, V2 - V1) )/(norm(V2 - V1)^2)).*(V2 - V1)

                elseif dot(V2 - V1, Xj) < dot(V2, V1 - V2)

                        xj = xj - ((dot(V2 - V1, Xj) -  dot(V2, V1 - V2) )/(norm(V2 - V1)^2))*(v2 - v1)

                        Xj = Xj - ((dot(V2 - V1, Xj) -  dot(V2, V1 - V2) )/(norm(V2 - V1)^2)).*(V2 - V1)

                end

        function V_r(v_r, V)

        	V[k] = v_r

        	return V

        end

		log_p_l_proj = v_r -> log_p_l(Xj, V_r(v_r[1], V1), V_r(v_r[2], V2), s)

		g = Calculus.gradient(log_p_l_proj)

		return g([v1, v2])

	end

	function grad_log_cost!(Grad, data, mu_t, S_t, E_list, SIGMA_t)

		n  = size(data,1)

		d  = size(data[1],1)

		grad = Dict{Any,Array}(S_t[i] => zeros(Float64, 1 , d) for i = 1:N_0)

		Mu_t = Dict{Any,Array}(S_t[i] => mu_t[i,:] for i = 1:N_0)

		E_array = vcat(transpose(E_list)...)

		E_t = Dict{Any,Array}(S_t[i] => E_array[:,i] for i = 1:N)

		for s in S_t

			if s isa Integer

				grad[s] += transpose(sum(E_t[s].*broadcast( x -> ((MU[s] - x)./SIGMA[s]), data))/n)

			elseif s isa Tuple

				for k in 1:d

						for j in 1:n

							g_v1, g_v2 = grad_log_p_l_n(k, data[j], Mu_t[s[1]], Mu_t[s[2]], SIGMA_t[s])

							grad[s[1]][k] += E_t[s[1]][j]*g_v1/n

							grad[s[2]][k] += E_t[s[2]][j]*g_v2/n

							#println(grad[s[1]][k],grad[s[2]][k])
						end

				end

			end

		end

		for i in 1:N_0

			Grad[i,:] = - grad[S_t[i]]

		end

	end

	####EM algorithm

	mu00 = vcat([m for m in mu]'...)

	for it = 1:EM_it
		#E step

		E = expectation(data,MU,SIGMA,Pi)

		Pi      = 	sum(E)./n

		#M step

		mu0 = vcat([m for m in mu]'...)

		C = t -> -Cost(data,t,SIGMA, Pi ,E)

		#swap if don't want gradient clipping or set tol = Inf
		#G! = (g,t) -> FiniteDiff.finite_difference_gradient!(g,C,t)

		function G!(storage,t; tol = 0.1)

			FiniteDiff.finite_difference_gradient!(storage,C,t)

			storage[abs.(storage) .> tol] = sign.(storage[abs.(storage) .> tol])*tol

			return

		end

		results = Optim.optimize(C,G!,mu0,GradientDescent(),Optim.Options(g_tol = 1e-2))

		mu1 = Optim.minimizer(results)

		mu = [mu1[i,:] for i in 1:N_0]

		#println("Current Pi:")
		#println(Pi)

		#println("Current Sigma:")
		#println(sigma)

		#println("Current vertices:")
		#println(mu)

		print("For iteration: ", it," cost is: ", -C(mu1) + sum(log.(Pi)) )

		#Update parameters

		MU, SIGMA, PI = reload_param(mu, sigma, Pi ; S_t = S, N_0_t = N_0, N_t = N)

		#println(MU)

	end

	return MU, SIGMA, PI, E

end

function Save_PPC(ppc::PartitionedPointCloud, path::String)
	jldopen(path, "w") do file
		addrequire(file, Skyler)
		addrequire(file, LinearAlgebra)
		write(file, "PartitionedPointCloud", ppc)
	end
end # Save_PPC

#### Main functions and wrappers ####
function skyler(points::A, sample_epsilon::P, radius::R; out = "model", EM_it = 3, sig = sample_epsilon/2) where {P<:Number, Q<:Number, R<:Number, A<:Union{LinearAlgebra.Adjoint,AbstractArray}} #wrapper for obtaining the partition

	dimension = size(points,1)
	ppc = Partition_Point_Cloud(points, sample_epsilon, radius, (3*radius)/2 + 2*sample_epsilon, 3*sample_epsilon)
	if out == "PPC"
		return ppc
	elseif out == "DF"
			return Create_Partition_DataFrame(ppc)
	elseif out == "model"
		vertices = size(collect(ppc.Dimensions[0]),1)
		vertex_guess = Array{Float64}(undef, dimension,0)
		for i in 1:vertices
			v_i = zeros(dimension, 1)
			for j in collect(ppc.Strata[i])
				v_i .+= points[:, j]
			end
			v_i = v_i/(length(collect(ppc.Strata[i])))
			vertex_guess = hcat(vertex_guess, v_i)
		end
		#println("Vertex guess is: ",vertex_guess)
		E = zeros(size(points,2), size(ppc.Boundaries,1))

		for i in 1:vertices
			for j in collect(ppc.Strata[i])
				E[j,i] =1
			end
		end
		for i in collect(ppc.Dimensions[1])
			for j in collect(ppc.Strata[i])
				E[j,i] =1
			end
		end

		sigma = [sig for i in 1:size(ppc.Boundaries,1)]

		S = []
		for i in collect(ppc.Dimensions[0])
			push!(S, i)
		end
		for k in 1:maximum(keys(ppc.Dimensions))
			for i in collect(ppc.Dimensions[k])
				t =tuple(findall(x->x==1, ppc.Boundaries[:,i])...,)
				push!(S, t)
			end
		end

		data = vcat([points[:,i] for i in 1:size(points,2)])

		v_guess = Array{Float64}(undef, size(vertex_guess,2), size(vertex_guess,1))

		for i in 1:size(vertex_guess,2)
			for j in 1:size(vertex_guess,1)
				v_guess[i,j] = vertex_guess[j,i]
			end
		end
		#println("S is $S")
		MU, SIGMA, PI, E = model_fit(data, v_guess, sigma, E, S, EM_it= EM_it)
		mu_ans = collect(values(MU))
		mu_ans = vcat([datum' for datum in mu_ans]...)
		return ppc, mu_ans', vertex_guess
	else
		println("Error: output option 'out' selected is not valid, please choose from 'model' to obtain a model of the underlying structure, or 'struct' to receive a DataFrame indicating which strata a point as been associated with and the dimension of this strata, and the boundary relations.")
		return []
	end
end

function skyler(ppc::PartitionedPointCloud, sample_epsilon::P; EM_it=3, sig=sample_epsilon/2) where {P<:Number} #wrapper for modelling a point cloud
	points=ppc.Points
	vertices = size(collect(ppc.Dimensions[0]),1)
	vertex_guess = Array{Float64}(undef, dimension,0)
	for i in 1:vertices
		for j in collect(ppc.Strata[i])
			v_i .+= points[:, j]
		end
		v_i = v_i/(length(collect(ppc.Strata[i])))
		vertex_guess = hcat(vertex_guess, v_i)
	end
	println("Vertex guess is: ",vertex_guess)
	E = zeros(size(points,2), size(ppc.Boundaries,1))

	for i in 1:vertices
		for j in collect(ppc.Strata[i])
			E[j,i] =1
		end
	end
	for i in collect(ppc.Dimensions[1])
		for j in collect(ppc.Strata[i])
			E[j,i] =1
		end
	end

	sigma = [sig for i in 1:size(ppc.Boundaries,1)]

	S = []
	for i in collect(ppc.Dimensions[0])
		push!(S, i)
	end
	for k in 1:maximum(keys(ppc.Dimensions))
		for i in collect(ppc.Dimensions[k])
			t =tuple(findall(x->x==1, ppc.Boundaries[:,i])...,)
			push!(S, t)
		end
	end

	data = vcat([points[:,i] for i in 1:size(points,2)])

	v_guess = Array{Float64}(undef, size(vertex_guess,2),size(vertex_guess,1))

	for i in 1:size(vertex_guess,2)
		for j in 1:size(vertex_guess,1)
			v_guess[i,j] = vertex_guess[j,i]
		end
	end
	MU, SIGMA, PI, E = model_fit(data, v_guess, sigma, E, S, EM_it)
	mu_ans = collect(values(MU))
	mu_ans = vcat([datum' for datum in mu_ans]...)
	return mu_ans', vertex_guess
end #skyler for modelling a partioned point cloud

#### Functions for examples ####
function Load_Example(i)
	dir = pwd()
	cd(@__DIR__)
	cd("..")
	cd("Examples")
	points = CSV.read("Sample-$i.csv", header=false)
	cd(dir)
	return convert(Array,points)
end

#### unittest ####

function test_1()
dir = pwd()
	cd(@__DIR__)
	cd("..")
	cd("Examples")

	points= convert(Array,CSV.read("Sample-1.csv", header=false))
	ppc = skyler(points, 0.01, 0.12, out="PPC")
	par = Create_Partition_DataFrame(ppc)
	saved_partition = CSV.read("Partition-1.csv")

	if par == saved_partition
		return []
	else
		println("Error: test_1 not passed.")
		return par
	end
end

function test_2()
	dir = pwd()
	cd(@__DIR__)
	cd("..")
	cd("Examples")

	points= convert(Array,CSV.read("Sample-2.csv",header=false))'
	ppc = skyler(points, 0.1, 1.2, out="PPC")
	par = Create_Partition_DataFrame(ppc)
	saved_partition = CSV.read("Partition-2.csv")

	if par == saved_partition
		return []
	else
		println("Error: test_2 not passed.")
		return par
	end
end

function unittest()
	x = Array{Any}(undef, 2)

	x[1] = test_1() #expected answer empty
	x[2] = test_2() #expected answer empty

	for p 	= 	1:length(x)
		if !isempty(x[p])
			println(p)
			return x
		end
	end
	return []
end

end #module


###############################################################################
#
# CanopyLayer parameters container
#
###############################################################################
"""
    struct CanopyLayer{FT}

Struct to store leaf information (multi-dimensional).

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef mutable struct CanopyLayer{FT<:AbstractFloat}
    # CanopyLayer photosynthesis system
    "leaf photosynthesis system"
    ps  ::Leaf{FT} = Leaf{FT}()
    "Memory leaf photosynthesis system"
    ps_m::Leaf{FT} = Leaf{FT}()
    "Total leaf area `[m²]`"
    LA    ::FT = 150
    "Leaf area index in the layer"
    LAI   ::FT = 3
    "Total leaf area index in the layer"
    tLAI  ::FT = 3
    "Memory APAR `[μmol m⁻² s⁻¹]`"
    APAR_m::FT = 500
    "Memory environment"
    envir_m::AirLayer{FT} = AirLayer{FT}()

    # Number of leaves per canopy layer
    n_leaf::Int = 325

    # Temperature related, different for each leaf
    "Sensible Heat Flux `[W m⁻²]`"
    H ::Array{FT,1} = zeros(FT, n_leaf)
    "Latent Heat Flux `[W m⁻²]`"
    LE::Array{FT,1} = zeros(FT, n_leaf)
    "Net Radiation Balance `[W m⁻²]`"
    Rn::Array{FT,1} = zeros(FT, n_leaf)

    # Tempearture related, same for all leaves
    "Latent Heat of evaporation `[J mol⁻¹]`"
    LV   ::FT = latent_heat_vapor(T_25(FT)) * M_H₂O(FT)
    "Temperature `[K]`"
    T    ::FT = T_25(FT)
    "Old temperature `[K]`"
    T_old::FT = FT(0)
    "Leaf width `[m]`"
    width::FT = FT(0.05)

    # Diffusive conductances, same for all leaves
    "Boundary layer conductance to CO₂ `[mol m⁻² s⁻¹]`"
    g_bc::Array{FT,1} = zeros(FT, n_leaf) .+ FT(3/1.35)
    "Boundary layer conductance to heat `[mol m⁻² s⁻¹]`"
    g_bh::Array{FT,1} = zeros(FT, n_leaf) .+ FT(3)
    "Boundary layer conductance to H₂O `[mol m⁻² s⁻¹]`"
    g_bw::Array{FT,1} = zeros(FT, n_leaf) .+ FT(3)
    "Leaf diffusive conductance to water CO₂ `[mol m⁻² s⁻¹]`"
    g_lc::Array{FT,1} = zeros(FT, n_leaf) .+ FT(0.08032)
    "Leaf diffusive conductance to water H₂O `[mol m⁻² s⁻¹]`"
    g_lw::Array{FT,1} = zeros(FT, n_leaf) .+ FT(0.1519)
    "Mesophyll conductance for CO₂ `[mol m⁻² s⁻¹]`"
    g_m ::Array{FT,1} = zeros(FT, n_leaf) .+ FT(0.5)
    "Stomatal conductance to water CO₂ `[mol m⁻² s⁻¹]`"
    g_sc::Array{FT,1} = zeros(FT, n_leaf) .+ FT(0.1)
    "Stomatal conductance to water H₂O `[mol m⁻² s⁻¹]`"
    g_sw::Array{FT,1} = zeros(FT, n_leaf) .+ FT(0.16)

    # Diffusive conductances, same for all leaves
    "Gias correction constant"
    g_ias_c::FT = FT(0)
    "Gias correction exponent"
    g_ias_e::FT = FT(0.3)
    "Maximal leaf diffusive conductance `[mol m⁻² s⁻¹]`"
    g_max  ::FT = FT(0.8)
    "Maximal leaf diffusive conductance at 298.15 K `[mol m⁻² s⁻¹]`"
    g_max25::FT = FT(0.8)
    "Minimal leaf diffusive conductance `[mol m⁻² s⁻¹]`"
    g_min  ::FT = FT(0.025)
    "Minimal leaf diffusive conductance at 298.15 K `[mol m⁻² s⁻¹]`"
    g_min25::FT = FT(0.025)

    # CO₂ and H₂O pressures, different for each leaf
    "Leaf internal CO₂ partial pressure `[Pa]`"
    p_i::Array{FT,1} = zeros(FT, n_leaf) .+ FT(10)
    "Leaf surface CO₂ partial pressure `[Pa]`"
    p_s::Array{FT,1} = zeros(FT, n_leaf) .+ FT(40)

    # CO₂ and H₂O pressures, same for all leaves
    "Leaf saturation vapor pressure `[Pa]`"
    p_sat::FT = saturation_vapor_pressure(T)

    # Photosynthesis related, different for each leaf
    "RubisCO limited photosynthetic rate `[μmol m⁻² s⁻¹]`"
    Ac   ::Array{FT,1} = zeros(FT, n_leaf)
    "Light limited photosynthetic rate `[μmol m⁻² s⁻¹]`"
    Aj   ::Array{FT,1} = zeros(FT, n_leaf)
    "Gross photosynthetic rate `[μmol m⁻² s⁻¹]`"
    Ag   ::Array{FT,1} = zeros(FT, n_leaf)
    "Net photosynthetic rate `[μmol m⁻² s⁻¹]`"
    An   ::Array{FT,1} = zeros(FT, n_leaf)
    "Product limited photosynthetic rate `[μmol m⁻² s⁻¹]`"
    Ap   ::Array{FT,1} = zeros(FT, n_leaf)
    "Electron transport `[μmol m⁻² s⁻¹]`"
    J    ::Array{FT,1} = zeros(FT, n_leaf)
    "Potential Electron Transport Rate `[μmol m⁻² s⁻¹]`"
    J_pot::Array{FT,1} = zeros(FT, n_leaf)

    # Fluorescence related, different for each leaf
    "Total efficiency, incl. photorespiration `[mol CO₂ mol⁻¹ e-]`"
    e2c::Array{FT,1} = zeros(FT, n_leaf) .+ FT(1/6)
    "light adapted yield (`Kp=0`)"
    Fm′::Array{FT,1} = zeros(FT, n_leaf)
    "light-adapted fluorescence yield in the dark (`Kp=max`)"
    Fo′::Array{FT,1} = zeros(FT, n_leaf)
    "Actual electron transport rate `[μmol m⁻² s⁻¹]`"
    Ja ::Array{FT,1} = zeros(FT, n_leaf)
    "Non-Photochemical quenching "
    NPQ::Array{FT,1} = zeros(FT, n_leaf)
    "Photochemical quenching"
    qQ ::Array{FT,1} = zeros(FT, n_leaf)
    "energy quenching"
    qE ::Array{FT,1} = zeros(FT, n_leaf)
    "PSII yield"
    φ  ::Array{FT,1} = zeros(FT, n_leaf)
    "Steady-state (light-adapted) yield (aka Fs)"
    φs ::Array{FT,1} = zeros(FT, n_leaf)

    # Fluorescence related, same for all leaves
    "dark adapted yield (`Kp=0`)"
    Fm::FT = FT(0)
    "dark-adapted fluorescence yield (`Kp=max`)"
    Fo::FT = FT(0)

    # Environment related, different for each leaf
    "Absorbed photosynthetic active radiation `[μmol m⁻² s⁻¹]`"
    APAR::Array{FT,1} = zeros(FT, n_leaf) .+ 100
    "Leaf area fractions"
    LAIx::Array{FT,1} = ones(FT, n_leaf) ./ n_leaf;

    # Stomtal optimization related, different for each leaf
    "Maximal photosynthetic rate `[μmol m⁻² s⁻¹]`"
    a_max::Array{FT,1} = zeros(FT, n_leaf)
    "Flow rate `[mol m⁻² s⁻¹]`"
    e    ::Array{FT,1} = zeros(FT, n_leaf)

    # Stomtal optimization related, same for all leaves
    "Critical flow rate `[mol m⁻² s⁻¹]`"
    ec    ::FT = FT(2e-9)
    "Maximal hydraulic conductance ratio"
    kr_max::FT = FT(1)
    "Base xylem pressre `[MPa]`"
    p_ups ::FT = FT(0)
    "Base xylem pressre memory `[MPa]`"
    p_old ::FT = FT(1)
    "Fitness factor for nighttime stomtal conductance"
    ff    ::FT = FT(0.15)

    # time constant related to prognostic stomatal conductance
    "τ for empirical stomatal models `[-]`, Δg/Δt = (g_ss - gsw) / τ"
    τ_esm::FT = 600
    "τ for optimal stomatal models `[μmol⁻¹]`, Δg/Δt = (∂A/∂E - ∂Θ/∂E) * τ"
    τ_osm::FT = FT(1e-6)
    "τ for nighttime optimal stomatal models `[μmol⁻¹]`"
    τ_noc::FT = FT(2e-6)
end


@testset "4.6.3.1 (a+b csc)^m (d csc)^n (A+B csc)" begin
    (A, a, c, d, x, ) = @variables A a c d x

    #= ::Package:: =#

    #= ::Title:: =#
    #=Integrands*of*the*form*(a+b*csc(e+f*x))^m*(d*csc(e+f*x))^n*(A+B*csc(e+f*x))=#

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*(a+a*csc(e+f*x))^m*(d*csc(e+f*x))^n*(A+B*csc(e+f*x))=#

    #= ::Subsection:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A+B*csc(c+d*x))*(a+a*csc(c+d*x))^n=#

    #= ::Subsection:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A+B*csc(c+d*x))*(a+a*csc(c+d*x))^(n/2)=#

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A+A*csc(c+d*x))*(a+a*csc(c+d*x))^n=#

    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^3, x, 7, -((7*a*A*atanh(cos(c + d*x)))/(8*d)) - (2*a*A*cot(c + d*x))/d - (2*a*A*cot(c + d*x)^3)/(3*d) - (7*a*A*cot(c + d*x)*csc(c + d*x))/(8*d) - (a*A*cot(c + d*x)*csc(c + d*x)^3)/(4*d)]
    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^2, x, 7, -((a*A*atanh(cos(c + d*x)))/d) - (5*a*A*cot(c + d*x))/(3*d) - (a*A*cot(c + d*x)*csc(c + d*x))/d - (a*A*cot(c + d*x)*csc(c + d*x)^2)/(3*d)]
    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^1, x, 6, -((3*a*A*atanh(cos(c + d*x)))/(2*d)) - (2*a*A*cot(c + d*x))/d - (a*A*cot(c + d*x)*csc(c + d*x))/(2*d)]
    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*sin(c + d*x)^1, x, 5, 2*a*A*x - (a*A*atanh(cos(c + d*x)))/d - (a*A*cos(c + d*x))/d]
    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*sin(c + d*x)^2, x, 5, (3*a*A*x)/2 - (2*a*A*cos(c + d*x))/d - (a*A*cos(c + d*x)*sin(c + d*x))/(2*d)]
    @test_int [(A + A*csc(c + d*x))*(a + a*csc(c + d*x))^1*sin(c + d*x)^3, x, 7, a*A*x - (2*a*A*cos(c + d*x))/d + (a*A*cos(c + d*x)^3)/(3*d) - (a*A*cos(c + d*x)*sin(c + d*x))/d]

    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^3, x, 4, -((a*A*atanh(cos(c + d*x)))/(8*d)) - (a*A*cot(c + d*x)*csc(c + d*x))/(8*d) + (a*A*cot(c + d*x)*csc(c + d*x)^3)/(4*d)]
    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^2, x, 3, (a*A*cot(c + d*x)^3)/(3*d)]
    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^1, x, 3, -((a*A*atanh(cos(c + d*x)))/(2*d)) + (a*A*cot(c + d*x)*csc(c + d*x))/(2*d)]
    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*sin(c + d*x)^1, x, 4, (a*A*atanh(cos(c + d*x)))/d - (a*A*cos(c + d*x))/d]
    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*sin(c + d*x)^2, x, 3, (-(1/2))*a*A*x - (a*A*cos(c + d*x)*sin(c + d*x))/(2*d)]
    @test_int [(A + A*csc(c + d*x))*(a - a*csc(c + d*x))^1*sin(c + d*x)^3, x, 3, (a*A*cos(c + d*x)^3)/(3*d)]

    #= ::Subsection::Closed:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A-A*csc(c+d*x))*(a+a*csc(c+d*x))^n=#

    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^3, x, 4, -((a*A*atanh(cos(c + d*x)))/(8*d)) - (a*A*cot(c + d*x)*csc(c + d*x))/(8*d) + (a*A*cot(c + d*x)*csc(c + d*x)^3)/(4*d)]
    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^2, x, 3, (a*A*cot(c + d*x)^3)/(3*d)]
    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1*csc(c + d*x)^1, x, 3, -((a*A*atanh(cos(c + d*x)))/(2*d)) + (a*A*cot(c + d*x)*csc(c + d*x))/(2*d)]
    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1/csc(c + d*x)^1, x, 4, (a*A*atanh(cos(c + d*x)))/d - (a*A*cos(c + d*x))/d]
    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1/csc(c + d*x)^2, x, 3, (-(1/2))*a*A*x - (a*A*cos(c + d*x)*sin(c + d*x))/(2*d)]
    @test_int [(A - A*csc(c + d*x))*(a + a*csc(c + d*x))^1/csc(c + d*x)^3, x, 3, (a*A*cos(c + d*x)^3)/(3*d)]

    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^3, x, 7, -((7*a*A*atanh(cos(c + d*x)))/(8*d)) + (2*a*A*cot(c + d*x))/d + (2*a*A*cot(c + d*x)^3)/(3*d) - (7*a*A*cot(c + d*x)*csc(c + d*x))/(8*d) - (a*A*cot(c + d*x)*csc(c + d*x)^3)/(4*d)]
    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^2, x, 7, (a*A*atanh(cos(c + d*x)))/d - (5*a*A*cot(c + d*x))/(3*d) + (a*A*cot(c + d*x)*csc(c + d*x))/d - (a*A*cot(c + d*x)*csc(c + d*x)^2)/(3*d)]
    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1*csc(c + d*x)^1, x, 6, -((3*a*A*atanh(cos(c + d*x)))/(2*d)) + (2*a*A*cot(c + d*x))/d - (a*A*cot(c + d*x)*csc(c + d*x))/(2*d)]
    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1/csc(c + d*x)^1, x, 5, -2*a*A*x - (a*A*atanh(cos(c + d*x)))/d - (a*A*cos(c + d*x))/d]
    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1/csc(c + d*x)^2, x, 5, (3*a*A*x)/2 + (2*a*A*cos(c + d*x))/d - (a*A*cos(c + d*x)*sin(c + d*x))/(2*d)]
    @test_int [(A - A*csc(c + d*x))*(a - a*csc(c + d*x))^1/csc(c + d*x)^3, x, 7, (-a)*A*x - (2*a*A*cos(c + d*x))/d + (a*A*cos(c + d*x)^3)/(3*d) + (a*A*cos(c + d*x)*sin(c + d*x))/d]

    #= ::Section:: =#
    #=Integrands*of*the*form*(a+a*csc(e+f*x))^m*(d*sin(e+f*x))^n*(A+B*csc(e+f*x))=#

    #= ::Section::Closed:: =#
    #=Integrands*of*the*form*(a+b*csc(e+f*x))^m*(d*csc(e+f*x))^n*(A+B*csc(e+f*x))=#

    #= ::Subsection:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A+B*csc(c+d*x))*(a+b*csc(c+d*x))^n=#

    #= ::Subsection:: =#
    #=Integrands*of*the*form*csc(c+d*x)^m*(A+B*csc(c+d*x))*(a+b*csc(c+d*x))^(n/2)=#

    #= ::Section:: =#
    #=Integrands*of*the*form*(a+b*csc(e+f*x))^m*(d*sin(e+f*x))^n*(A+B*csc(e+f*x))=#
    end

test/4 Trig functions/4.6 Cosecant/4.6.3.1 (a+b csc)^m (d csc)^n (A+B csc).jl


#function to obtain positional likelihoods for a sequence under a given BHMM.
function get_BGHMM_symbol_lh(seq::AbstractMatrix, hmm::BHMM)
    @assert size(seq)[1] == 1
    (seq=Array(transpose(seq))) # one sequence at a time only
    symbol_lhs = zeros(length(seq))
    length_mask = [length(seq)-1]

    lls = bw_llhs(hmm, seq) #obtain log likelihoods for sequences and states
    log_α = messages_forwards_log(hmm.a, hmm.A, lls, length_mask) #get forward messages
    log_β = messages_backwards_log(hmm.A, lls, length_mask) # get backwards messages

    #calculate observation probability and γ weights
    K,Tmaxplus1,Strand = size(lls) #the last T value is the 0 end marker of the longest T

    #transforms to cut down log_ξ, log_γ assignment times
    lls = permutedims(lls, [2,1,3]) # from (K,T) to (T,K)
    log_α = permutedims(log_α, [2,1,3])
    log_β = permutedims(log_β, [2,1,3])

    log_γ = fill(-Inf, Tmaxplus1,K)
    log_pobs = logsumexp(lps.(log_α[1,:], log_β[1,:]))

    @inbounds for i = 1:K, t = 1:length_mask[1]
            log_γ[t,i] = lps(log_α[t,i],log_β[t,i],-log_pobs)
    end

    for t in 1:length_mask[1]
        symbol_lh::AbstractFloat = -Inf #ie log(p=0)
        for k = 1:K #iterate over states
                state_symbol_lh::AbstractFloat = lps(log_γ[t,k], log(hmm.B[k].p[seq[t]])) #state symbol likelihood is the γ weight * the state symbol probability (log implementation)
                symbol_lh = logaddexp(symbol_lh, state_symbol_lh) #sum the probabilities over states
        end
        symbol_lhs[t] = symbol_lh
    end

    return symbol_lhs[1:end-1] #remove trailing index position
end

#function to calculate BGHMM from an observation set and a dict of BGHMMs
function BGHMM_likelihood_calc(observations::DataFrame, BGHMM_dict::Dict, code_partition_dict = get_partition_code_dict(false); symbol=:PadSeq)
    lh_matrix_size = ((findmax(length.(collect(values(observations[!, symbol]))))[1]), length(observations[!, symbol]))
    BGHMM_lh_matrix = zeros(lh_matrix_size) #T, Strand, O

    BGHMM_fragments = fragment_observations_by_BGHMM(observations[!, symbol], observations.MaskMatrix)

    #@showprogress 1 "Writing frags to matrix.."
    Threads.@threads for (jobid, frag) in BGHMM_fragments
        (frag_start, o, partition, strand) = jobid

        partition_BGHMM::BHMM = BGHMM_dict[code_partition_dict[partition]]
        no_symbols = length(partition_BGHMM.B[1].p)
        order = Int(log(4,no_symbols) - 1)

        order_seq = get_order_n_seqs([frag], order)
        coded_seq = code_seqs(order_seq)

        subseq_symbol_lh = get_BGHMM_symbol_lh(coded_seq, partition_BGHMM)

        if strand == -1
            subseq_symbol_lh = reverse(subseq_symbol_lh)
        end #positive, unstranded frags  are inserted as-is
        BGHMM_lh_matrix[frag_start:frag_start+length(frag)-1,o] = subseq_symbol_lh
    end

    return BGHMM_lh_matrix
end

function fragment_observations_by_BGHMM(seqs::AbstractVector, masks::AbstractVector)
    likelihood_jobs = Vector{Tuple{Tuple,LongSequence{DNAAlphabet{2}}}}()
    @showprogress 1 "Fragmenting observations by partition..." for (o, obs_seq) in enumerate(seqs)
        mask = masks[o]
        frags = Vector{LongSequence{DNAAlphabet{2}}}() #container for all subsequences in observation
        frag = LongSequence{DNAAlphabet{2}}()

        frag_end=0
        frag_start = 1

        while frag_start < length(obs_seq) # while we're not at the sequence end
            curr_partition = mask[frag_start,1] #get the partition code of the frag start
            curr_strand = mask[frag_start,2] #get the strand of the frag start

            #JOBID COMPOSED HERE
            jobid = (frag_start, o, curr_partition, curr_strand) #compose an identifying index for this frag

            findnext(!isequal(curr_partition),mask[:,1],frag_start) != nothing ? frag_end = findnext(!isequal(curr_partition),mask[:,1],frag_start) -1 : frag_end = length(obs_seq) #find the next position in the frag that has a different partition mask value from hte current one and set that position-1 to frag end, alternately frag end is end of the overall  sequence
            frag = obs_seq[frag_start:frag_end] #get the frag bases
            if curr_strand == -1 #if the fragment is reverse stranded
                reverse_complement!(frag) #use the reverse complement sequence
            end

            push!(likelihood_jobs,(jobid, frag)) #put the frag in the jobs vec
            frag_start = frag_end + 1 #move on
        end
    end
    return likelihood_jobs
end


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
"""
    add_pv_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")

Adds the PV results to the dictionary passed back from `run_reopt` using the solved model `m` and the `REoptInputs` for node `_n`.
Note: the node number is an empty string if evaluating a single `Site`.

!!! warn
    The key(s) used to access PV outputs in the results dictionary is determined by the `PV.name` value to allow for modeling multiple PV options. (The default `PV.name` is "PV".)

PV results:
- `size_kw` Optimal PV capacity
- `lifecycle_om_cost` Lifecycle operations and maintenance cost in present value, after tax
- `year_one_energy_produced_kwh` Energy produced over the first year
- `average_annual_energy_produced_kwh` Average annual energy produced when accounting for degradation
- `lcoe_per_kwh` Levelized Cost of Energy produced by the PV system
- `year_one_to_load_series_kw` Vector of power used to meet load over the first year
- `year_one_to_battery_series_kw` Vector of power used to charge the battery over the first year
- `year_one_to_grid_series_kw` Vector of power exported to the grid over the first year
- `year_one_curtailed_production_series_kw` Vector of power curtailed over the first year
- `average_annual_energy_exported_kwh` Average annual energy exported to the grid
"""
function add_pv_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
    for t in p.techs.pv
        r = Dict{String, Any}()
		r["size_kw"] = round(value(m[Symbol("dvSize"*_n)][t]), digits=4)

		# NOTE: must use anonymous expressions in this loop to overwrite values for cases with multiple PV
		if !isempty(p.s.storage.types)
			PVtoBatt = (sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types) for ts in p.time_steps)
		else
			PVtoBatt = repeat([0], length(p.time_steps))
		end
		r["year_one_to_battery_series_kw"] = round.(value.(PVtoBatt), digits=3)

        r["year_one_to_grid_series_kw"] = zeros(size(r["year_one_to_battery_series_kw"]))
        r["average_annual_energy_exported_kwh"] = 0.0
        if !isempty(p.s.electric_tariff.export_bins)
            PVtoGrid = @expression(m, [ts in p.time_steps],
                    sum(m[:dvProductionToGrid][t, u, ts] for u in p.export_bins_by_tech[t]))
            r["year_one_to_grid_series_kw"] = round.(value.(PVtoGrid), digits=3).data

            r["average_annual_energy_exported_kwh"] = round(
                sum(r["year_one_to_grid_series_kw"]) * p.hours_per_timestep, digits=0)
        end

		PVtoCUR = (m[Symbol("dvCurtail"*_n)][t, ts] for ts in p.time_steps)
		r["year_one_curtailed_production_series_kw"] = round.(value.(PVtoCUR), digits=3)
		PVtoLoad = (m[Symbol("dvRatedProduction"*_n)][t, ts] * p.production_factor[t, ts] * p.levelization_factor[t]
					- r["year_one_curtailed_production_series_kw"][ts]
					- r["year_one_to_grid_series_kw"][ts]
					- r["year_one_to_battery_series_kw"][ts] for ts in p.time_steps
		)
		r["year_one_to_load_series_kw"] = round.(value.(PVtoLoad), digits=3)
		Year1PvProd = (sum(m[Symbol("dvRatedProduction"*_n)][t,ts] * p.production_factor[t, ts] for ts in p.time_steps) * p.hours_per_timestep)
		r["year_one_energy_produced_kwh"] = round(value(Year1PvProd), digits=0)
        r["average_annual_energy_produced_kwh"] = round(r["year_one_energy_produced_kwh"] * p.levelization_factor[t], digits=2)
		PVPerUnitSizeOMCosts = p.om_cost_per_kw[t] * p.pwf_om * m[Symbol("dvSize"*_n)][t]
		r["lifecycle_om_cost"] = round(value(PVPerUnitSizeOMCosts) * (1 - p.s.financial.owner_tax_pct), digits=0)
        r["lcoe_per_kwh"] = calculate_lcoe(p, r, get_pv_by_name(t, p.s.pvs))
        d[t] = r
	end
    nothing
end

function add_pv_results(m::JuMP.AbstractModel, p::MPCInputs, d::Dict; _n="")
    for t in p.techs.pv
        r = Dict{String, Any}()

		# NOTE: must use anonymous expressions in this loop to overwrite values for cases with multiple PV
		if p.s.storage.size_kw[:elec] > 0  # TODO handle multiple storage types
			PVtoBatt = (sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types) for ts in p.time_steps)
            PVtoBatt = round.(value.(PVtoBatt), digits=3)
            r["to_battery_series_kw"] = PVtoBatt
		else
			PVtoBatt = repeat([0], length(p.time_steps))
		end

        r["to_grid_series_kw"] = zeros(length(p.time_steps))
        if !isempty(p.s.electric_tariff.export_bins)
            PVtoGrid = @expression(m, [ts in p.time_steps],
                    sum(m[:dvProductionToGrid][t, u, ts] for u in p.export_bins_by_tech[t]))
            r["to_grid_series_kw"] = round.(value.(PVtoGrid), digits=3).data
        end

		PVtoCUR = (m[Symbol("dvCurtail"*_n)][t, ts] for ts in p.time_steps)
		r["curtailed_production_series_kw"] = round.(value.(PVtoCUR), digits=3)
		PVtoLoad = (m[Symbol("dvRatedProduction"*_n)][t, ts] * p.production_factor[t, ts] * p.levelization_factor[t]
					- r["curtailed_production_series_kw"][ts]
					- r["to_grid_series_kw"][ts]
					- PVtoBatt[ts] for ts in p.time_steps
		)
		r["to_load_series_kw"] = round.(value.(PVtoLoad), digits=3)
		Year1PvProd = (sum(m[Symbol("dvRatedProduction"*_n)][t,ts] * p.production_factor[t, ts] for ts in p.time_steps) * p.hours_per_timestep)
		r["energy_produced_kwh"] = round(value(Year1PvProd), digits=0)
        d[t] = r
	end
    nothing
end

"""
    organize_multiple_pv_results(p::REoptInputs, d::Dict)

The last step in results processing: if more than one PV was modeled then move their results from the top
level keys (that use each PV.name) to an array of results with "PV" as the top key in the results dict `d`.
"""
function organize_multiple_pv_results(p::REoptInputs, d::Dict)
    if length(p.techs.pv) == 1 && p.techs.pv[1] == "PV"
        return nothing
    end
    pvs = Dict[]
    for pvname in p.techs.pv
        d[pvname]["name"] = pvname  # add name to results dict to distinguish each PV
        push!(pvs, d[pvname])
        delete!(d, pvname)
    end
    d["PV"] = pvs
    nothing
end


import ..TimePetriNets
using SparseArrays

function TimePetriNets.TPN(n::Neuron{H}, name=:neuron; M=Int) where {H}
    # collect all places and transitions to glue dendrite segments + synapses together
    P = Symbol[]
    T = Symbol[]
    m₀ = M[]
    eft = H[]
    lft = H[]
    trans_probs = Float64[]

    R = Tuple{Int,Int,M}[]
    ΔF = Tuple{Int,Int,M}[]

    function add_place!(name, initial)
        push!(P, Symbol(name))
        push!(m₀, M(initial))
        length(P)
    end

    function add_transition!(name, e, l=e, p=1.0)
        push!(T, Symbol(name))
        push!(eft, H(e))
        push!(lft, H(l))
        push!(trans_probs, p)
        length(T)
    end

    function copy_transitions!(other, names...)
        idxs = indexin(Symbol.(names), other.T)
        return add_transition!.(
            names,
            other.eft[idxs],
            other.lft[idxs],
            other.trans_probs[idxs]
        )
    end

    function copy_places!(other, names...)
        idxs = indexin(Symbol.(names), other.P)
        return add_place!.(
            names,
            other.x₀.m[idxs]
        )
    end

    sub_tpns = TPN{M,H}[]

    # go through dendritic tree
    queue = Tuple{SomaOrDendrite,Union{Nothing,Int},Union{Nothing,Int}}[(n.dendrite, nothing, nothing)]
    while !isempty(queue)
        (seg, parent_counter, parent_trans) = pop!(queue)
        has_reset = !isempty(seg.inh_inputs)

        # the dendrite is mainly a monoflop
        dendrite_tpn = TimePetriNets.Monoflop(Symbol("$(name)_$(seg.name)"),
            seg==n.dendrite ? seg.parameters.spike_duration+seg.parameters.refractory_period : seg.parameters.plateau_duration; add_reset=has_reset)
        push!(sub_tpns, dendrite_tpn)

        # need to access start, stop transition as well as the on place --> copy them
        t_names = ("$(name)_$(seg.name)_start", "$(name)_$(seg.name)_stop")
        seg_start, seg_stop = copy_transitions!(dendrite_tpn, t_names...)
        seg_reset = has_reset ? copy_transitions!(dendrite_tpn, "$(name)_$(seg.name)_reset")[1] : nothing
        (seg_on,) = copy_places!(dendrite_tpn, "$(name)_$(seg.name)_on")

        # depending on excitatory #inputs, add synaptic counter
        syn_counter_exc = length(seg.exc_inputs) == 1 ? nothing :
                          add_place!("$(name)_$(seg.name)_syn_counter_exc", 0)

        # depending on inhibitory #inputs, add synaptic counter and inverted counter
        syn_counter_inh = length(seg.inh_inputs) == 1 ? nothing :
                          add_place!("$(name)_$(seg.name)_syn_counter_inh", 0)
        syn_counter_inh_off = add_place!("$(name)_$(seg.name)_syn_counter_inh_off", length(seg.inh_inputs))

        # depending on #children, add dendritic counter
        den_counter = length(seg.children) == 1 ? nothing :
                      add_place!("$(name)_$(seg.name)_den_counter", 0)

        # if there is a parent counter, update it, too
        if !isnothing(parent_counter)
            push!(ΔF, (parent_counter, seg_start, 1), (parent_counter, seg_stop, -1))
        end

        # if there is a parent transaction, make it sensitive to this plateau
        if !isnothing(parent_trans)
            push!(R, (seg_on, parent_trans, 1))
        end

        ## Add all inhibitory synapses
        for inp in seg.inh_inputs
            # the inhibitory synapse is mainly a monoflop
            synapse_tpn = TimePetriNets.Monoflop(Symbol("$(name)_$(seg.name)_$(inp)"), seg.parameters.ipsp_duration; add_reset=false)
            push!(sub_tpns, synapse_tpn)
            # we need to access the start and stop transition and on place --> copy them
            (t_start, t_stop) = copy_transitions!(synapse_tpn, "$(name)_$(seg.name)_$(inp)_start", "$(name)_$(seg.name)_$(inp)_stop")
            (p_on,) = copy_places!(synapse_tpn, "$(name)_$(seg.name)_$(inp)_on")

            # Add trigger place
            t_trigger = add_place!("$(name)_$(seg.name)_$(inp)_trigger", 0)
            push!(ΔF, (t_trigger, t_start, -1))

            # if there is a synapse counter, update it, too
            if !isnothing(syn_counter_inh)
                push!(ΔF, (syn_counter_inh, t_start, 1), (syn_counter_inh, t_stop, -1))
            else
                syn_counter_inh = p_on
            end
            # off-counter works in opposite direction
            push!(ΔF, (syn_counter_inh_off, t_start, -1), (syn_counter_inh_off, t_stop, 1))
        end

        if has_reset
            if !isnothing(parent_counter)
                push!(ΔF, (parent_counter, seg_reset, -1))
            end

            # enable reset when at least one inhibitory synapse is on
            # enable segment if all inhibitory synapses are off
            push!(R,
                (syn_counter_inh, seg_reset, 1),
                (syn_counter_inh_off, seg_start, length(seg.inh_inputs))
            )
        end

        ## Add all excitatory synapses
        for inp in seg.exc_inputs
            # the excitatory synapse is mainly a monoflop
            synapse_tpn = TimePetriNets.Monoflop(Symbol("$(name)_$(seg.name)_$(inp)"), seg.parameters.epsp_duration; add_reset=false)
            push!(sub_tpns, synapse_tpn)
            # we need to access the start and stop transition and on place --> copy them
            (t_start, t_stop) = copy_transitions!(synapse_tpn, "$(name)_$(seg.name)_$(inp)_start", "$(name)_$(seg.name)_$(inp)_stop")
            (p_on,) = copy_places!(synapse_tpn, "$(name)_$(seg.name)_$(inp)_on")

            # Add trigger place
            t_trigger = add_place!("$(name)_$(seg.name)_$(inp)_trigger", 0)
            push!(ΔF, (t_trigger, t_start, -1))

            # if there is a synapse counter, update it, too
            if !isnothing(syn_counter_exc)
                push!(ΔF, (syn_counter_exc, t_start, 1), (syn_counter_exc, t_stop, -1))
            else
                syn_counter_exc = p_on
            end
        end

        # Make start transition sensitive to excitatory synaptic input
        push!(R, (syn_counter_exc, seg_start, seg.parameters.synaptic_threshold))

        if !isnothing(den_counter)
            # Make start transition sensitive to dendritic input
            push!(R, (den_counter, seg_start, min(length(seg.children), seg.parameters.dendritic_threshold)))

            # Go through child branches with shared dendrite counter
            for child in seg.children
                push!(queue, (child, den_counter, nothing))
            end
        else
            # Add single child branch without extra counter -> directly connect to seg_start
            push!(queue, (seg.children[1], nothing, seg_start))
        end
    end

    m = length(P)
    n = length(T)

    R = isempty(R) ? spzeros(M,m,n) : sparse(collect.(zip(R...))..., m, n)
    ΔF = isempty(ΔF) ? spzeros(M,m,n) : sparse(collect.(zip(ΔF...))..., m, n)

    glue = TimePetriNets.TPN(P, T, R, ΔF, eft, lft, trans_probs, m₀)
    |(glue, sub_tpns...)
end

function TimePetriNets.TPN(net::NeuralNetwork, args...;kwargs...)
    # construct all neurons' tpns
    neuron_tpns = [TimePetriNets.TPN(neuron,name,args...;kwargs...) for (name,neuron) in pairs(net.neurons)]
    (H,M) = eltype(neuron_tpns).parameters

    input_trans = Monoflop.(Symbol.(Ref("input_").*String.(net.inputs)), 0)

    # combine synapse and neuron tpns into one tpn
    net_tpn = |(input_trans..., neuron_tpns...)

    # set lft of all inputs to infinity
    for input in net.inputs
        idx = net_tpn.T_index[Symbol("input_$(input)_start")]
        net_tpn.eft[idx] = zero(H)
        net_tpn.lft[idx] = typemax(H)
    end

    function add_axon!(src_idx, axons)
        for axon in axons
            if isa(axon.target,Symbol)
                continue
            end

            (neuron,input) = axon.target
            neuron_name = neuron[].name[]

            for dendrite in neuron[].ports[input]
                tgt_idx = net_tpn.P_index[Symbol("$(neuron_name)_$(dendrite.name)_$(input)_trigger")]
                # add arc from transition to trigger place
                net_tpn.ΔF[tgt_idx, src_idx] += 1

                t_idx = get(net_tpn.T_index, Symbol("$(neuron_name)_$(dendrite.name)_$(input)_start"), nothing)
                if !isnothing(t_idx)
                    net_tpn.eft[t_idx] = axon.parameters.axon_delay
                    net_tpn.lft[t_idx] = axon.parameters.axon_delay
                end
                t_idx = get(net_tpn.T_index, Symbol("$(neuron_name)_$(dendrite.name)_$(input)_fail"), nothing)
                if !isnothing(t_idx)
                    net_tpn.eft[t_idx] = axon.parameters.axon_delay
                    net_tpn.lft[t_idx] = axon.parameters.axon_delay
                end
            end
        end
        nothing
    end

    # add synapses between the neurons
    for (source_name, axons) in pairs(net.axons)
        # find the spike-start transition of the source neuron or input
        source_name = if source_name ∈ net.inputs
            Symbol("input_$(source_name)")
        else
            Symbol("$(source_name)_$(net.neurons[source_name].dendrite.name)")
        end
        src_name = Symbol("$(source_name)_start")
        src_idx = net_tpn.T_index[src_name]
        add_axon!(src_idx, axons)
    end
    # update TPN
    TimePetriNets.update!(net_tpn)
    return net_tpn
end


module KhepriBase
using ColorTypes
using LinearAlgebra
using StaticArrays
using Dierckx
using Dates
using Sockets
using Base.Iterators
using Base: @kwdef

#=
Dierckx does not yet support derivatives. Until they update it,
we extend it here to have that support.
=#
export evaluate_derivative
function evaluate_derivative(spline::Spline2D, x::AbstractVector, y::AbstractVector, dx::Int, dy::Int)
    m = length(x)
    @assert length(y) == m
    xin = convert(Vector{Float64}, x)
    yin = convert(Vector{Float64}, y)
    zout = Vector{Float64}(undef, m)
    nx = length(spline.tx)
    ny = length(spline.ty)
    lwrk = (nx*ny)+(spline.kx+1)*m+(spline.ky+1)*m
    wrk = Vector{Float64}(undef, lwrk)
    ier = Ref{Int32}()
    kwrk = 2*m
    iwrk = Vector{Int32}(undef, kwrk)
    ccall((:pardeu_, Dierckx.libddierckx), Nothing,
          (Ref{Float64}, Ref{Int32},  # ty, ny
           Ref{Float64}, Ref{Int32},  # tx, nx
           Ref{Float64},              # c
           Ref{Int32}, Ref{Int32},    # ky, kx
           Ref{Int32}, Ref{Int32},    # nuy, nux
           Ref{Float64},              # y
           Ref{Float64},              # x
           Ref{Float64},              # z
           Ref{Int32},                # m
           Ref{Float64}, Ref{Int32},  # wrk, lwrk
           Ref{Int32}, Ref{Int32},    # iwrk, kwrk
           Ref{Int32}),               # ier
          spline.ty, ny,
          spline.tx, nx,
          spline.c,
          spline.ky, spline.kx,
          dy, dx,
          yin, xin, zout, m,
          wrk, lwrk,
          iwrk, kwrk,
          ier)
    ier[] == 0 || error(Dierckx._eval2d_message)
    return zout
end
function evaluate_derivative(spline::Spline2D, x::Number, y::Number, dx::Int, dy::Int)
  if dx >= spline.kx
    ϵ = 1e-9
    (evaluate(spline, x+ϵ, y) - evaluate(spline, x-ϵ, y))/2/ϵ
  elseif dy >= spline.kx
    ϵ = 1e-9
    (evaluate(spline, x, y+ϵ) - evaluate(spline, x, y-ϵ))/2/ϵ
  else
    evaluate_derivative(spline, [x], [y], dx, dy)[1]
  end
end

import Base:
    +, -, *, /, length,
    show, showerror,
    zero, iterate, convert,
    getindex, firstindex, lastindex, broadcastable,
    union, fill
import LinearAlgebra:
    cross, dot, norm

include("Parameters.jl")
include("Utils.jl")
include("Coords.jl")
include("Regions.jl")
include("Paths.jl")
include("Geometry.jl")
include("Backends.jl")
include("Frontend.jl")
include("Shapes.jl")
include("Primitives.jl")
include("BIM.jl")
include("Camera.jl")

export khepribase_interface_file
khepribase_interface_file() = joinpath(@__DIR__, "Interface.jl")

# From ColorTypes
export RGB, RGBA, rgb, rgba

export and_mark_deleted,
       @remote_functions,
       parse_signature,
       encode,
       decode,
       decode_or_error,
       @encode_decode_as,
       SocketBackend,
       create_backend_connection,
       current_backends,
       @remote,
       @get_remote,
       connection,
       reset_backend,
       IOBufferBackend,
       backend_frame_at,
       acad_revolution,
       backend_bounding_box,
       b_delete_shapes,
       backend_fill_curves,
       backend_loft_curve_point,
       backend_loft_curves,
       backend_loft_surface_point,
       backend_loft_surfaces,
       backend_map_division,
       backend_node_displacement_function,
       backend_panel,
       backend_revolve_curve,
       backend_revolve_point,
       backend_revolve_surface,
       backend_surface_boundary,
       backend_surface_domain,
       captured_shape,
       captured_shapes,
       create_block,
       dimension,
       generate_captured_shape,
       generate_captured_shapes,
       pre_selected_shapes_from_se,
       ref,
       save_file,
       shape_from_ref,
       show_truss_deformation,
       slice_ref

# Ports for socket-based backends are defined here to avoid conflicts
export autocad_port, revit_port, rhino_port, unity_port, unreal_port, blender_port
const autocad_port = 11000
const revit_port = 11001
const rhino_port = 12000
const unity_port = 11002
const unreal_port = 11010
const blender_port = 11003

end


#=
  Package: FishEBM
  File: types.jl
  Justin Angevaare, Devin Rose
  Type definitions for the structured stock-level and agent-level model components
  Created: May 2015
=#

"""
  Description: Assumptions regarding mortality, maturity, broodsize, and
    catchability of age specific adult agents.

  naturalmortality = Age specific survivorship (survivorship at carrying
    capacity if density depedence occurs).
  halfmature = Age at 50% mature (Binomial cdf assumed).
  broodsize = Age specific fecundity (i.e. mean quantity of eggs each spawning
    female will produce).
  fecunditycompensation = compensatory strength for changes in fecundity.
    Compensatory strength is a divisor of K which will result in a 68% change in
    fecundity - smaller values indicate lower compensation strength.
    Compensation function based on Normal CDF. Use NaN if compensation is
    assumed to not occur.
  maturitycompensation = compensatory strength for changes in age of sexual
    maturity. Compensatory strength is a divisor of K which will result in a 68%
    change in age of sexual maturity - smaller values indicate lower
    compensation strength. Compensation function based on Normal CDF. Use NaN if
    compensation is assumed to not occur.
  mortalitycompensation = compensatory strength for changes in age of sexual
    maturity. Compensatory strength is a divisor of K which will result in a 68%
    change in natural mortality - smaller values indicate lower compensation
    strength. Compensation function based on Normal CDF. Use NaN if compensation
    is assumed to not occur.

    Last Update: September 2016
"""
type AdultAssumptions
  naturalmortality::Vector{Float64}
  halfmature::Float64
  broodsize::Vector{Int64}
  fecunditycompensation::Float64
  maturitycompensation::Float64
  mortalitycompensation::Float64
  catchability::Vector{Float64}

  AdultAssumptions() = new()
  AdultAssumptions(naturalmortality, halfmature, broodsize, fecunditycompensation, maturitycompensation, mortalitycompensation, catchability) =
    new(naturalmortality, halfmature, broodsize, fecunditycompensation, maturitycompensation, mortalitycompensation, catchability)
end

"""
  Description: Assumptions regarding mortality, movement, and growth of
    autonomous agents.

    naturalmortality = Stage specific survivorship of each habitat type and stage
      of agents.
    extramortality = Stage specific survivorship of agents (each independent life
      stage) in an environment location influenced by anthropogenic effects.
    growth = Growth rate (in weeks) from one life stage to the next.
    movement = Movement weight matrices for each life stage.
    autonomy = Movement autonomy.

  Last Update: October 2016
"""
type AgentAssumptions
  naturalmortality::Array
  extramortality::Vector{Float64}
  growth::Vector{Int64}
  movement::Array
  autonomy::Vector{Float64}

  AgentAssumptions() = new()
  AgentAssumptions(naturalmortality, extramortality, growth, movement, autonomy) =
    new(naturalmortality, extramortality, growth, movement, autonomy)
end

"""
  Description: This "Environment agent" is used to track fish population
    dynamics during a simulation.

  Last Update: September 2016
"""
type EnviroAgent
   # locationID corresponds to the ID in the habitat array
  locationID::Int64

  # Number of alive agents alive in each cohort
  alive::Vector{Int64}

  # Independent mortality tracking by location
  killedNatural::Vector{Int64}
  killedExtra::Vector{Int64}
  harvest::Int64

  # The spawning week of each cohort during the simulation
  weekNum::Vector{Int64}

  EnviroAgent(locationID) = new(locationID, [0], [0,0,0,0], [0,0,0,0], 0, [0])
end

"""
  Description: A specialized type which contains layers of information to
    indicate spawning area, habitat type, and additional risks. Abstract matches areas of interest
    to habitat index number, and hashing matches index numbers to agent numbers. Hash contains agent
    number during run-time.

  Precondition: Location id should be specified as NaN when a valid location
    does not exist.

  Last Update: October 2016
"""
type EnvironmentAssumptions
  # Spawning map and a hashed list of areas
  spawning::Vector{Int64}
  spawningHash::Vector{Int64}

  # Habitat type array
  habitat::Array{Int64}

  # risk map and a hashed list of areas
  risk::Vector{Int64}
  riskHash::Vector{Int64}

  # Harvest map, hashed list and harvest zones
  harvest::Array{Int64}
  abstractHarvest::Vector{Int64}
  harvestZoneNum::Vector{Int64}
  harvestHash::Vector{Int64}

  # Constructors
  EnvironmentAssumptions() = new()
  EnvironmentAssumptions(spawning, spawningHash, habitat, risk, riskHash, harvest, abstractHarvest, harvestZoneNum, harvestHash) =
  new(spawning, spawningHash, habitat, risk, riskHash, harvest, abstractHarvest, harvestZoneNum, harvestHash)
end