module Algorithms

using DifferentialForms
using StaticArrays

export barycentre
barycentre(xs::SVector{N,<:Form{D,1,T}}) where {N,D,T} = sum(xs) / length(xs)
barycentre(xs::SVector{N,SVector{D,T}}) where {N,D,T} = sum(xs) / length(xs)

# function circumcentre1(xs::SVector{R, <:Chain{V, 1, T}}) where {R, V, T}
#     # G. Westendorp, A formula for the N-circumsphere of an N-simplex,
#     # <https://westy31.home.xs4all.nl/Circumsphere/ncircumsphere.htm>,
#     # April 2013.
#     @assert iseuclidean(V)
#     D = ndims(V)
#     @assert R == D + 1
#
#     # Convert Euclidean to conformal basis
#     cxs = conformal.(xs)
#     # Circumsphere (this formula is why we are using conformal GA)
#     X = ∧(cxs)
#     # Hodge dual
#     sX = ⋆X
#     # Euclidean part is centre
#     cc = euclidean(sX)
#
#     # Calculate radius
#     # TODO: Move this into a test case
#     r2 = scalar(abs2(cc)).v - 2 * sX.v[1]
#     # Check radii
#     for i ∈ 1:R
#         ri2 = scalar(abs2(xs[i] - cc)).v
#         @assert abs(ri2 - r2) ≤ T(1.0e-12) * r2
#     end
#
#     cc::Chain{V, 1, T}
# end

export circumcentre
function circumcentre(xs::SVector{N,<:Form{D,1,T}}) where {N,D,T}
    D::Int
    @assert D ≥ 0
    N::Int
    @assert N ≥ 1
    @assert N ≤ D + 1
    # See arXiv:1103.3076v2 [cs.RA], section 10.1
    A = SMatrix{N + 1,N + 1}(i ≤ N && j ≤ N ? 2 * (xs[i] ⋅ xs[j])[] :
                             i == j ? zero(T) : one(T)
                             for i in 1:(N + 1), j in 1:(N + 1))
    b = SVector{N + 1}(i ≤ N ? (xs[i] ⋅ xs[i])[] : one(T) for i in 1:(N + 1))
    c = A \ b
    cc = sum(c[i] * xs[i] for i in 1:N)
    return cc::Form{D,1,T}
end

function circumcentre(xs::SVector{N,<:Form{D,1,T}},
                      ws::SVector{N,<:Form{D,0,T′}}) where {N,D,T,T′}
    # See arXiv:math/0508188, section 3.2:
    # See arXiv:1103.3076v2 [cs.RA], section 10.1:
    #
    # C = Ci xi + Cj xj + Ck xk
    # (C - xi) (C - xi) = r^2 + wi
    # (C - xj) (C - xj) = r^2 + wj
    # (C - xk) (C - xk) = r^2 + wk
    # Ci + Cj + Ck = 1
    #
    # (Ci xi + Cj xj + Ck xk - xi) (Ci xi + Cj xj + Ck xk - xi) = r^2 + wi
    # (Ci xi + Cj xj + Ck xk - xj) (Ci xi + Cj xj + Ck xk - xj) = r^2 + wj
    # (Ci xi + Cj xj + Ck xk - xk) (Ci xi + Cj xj + Ck xk - xk) = r^2 + wk
    # Ci + Cj + Ck = 1
    #
    # C^2 - 2 xi (Ci xi + Cj xj + Ck xk) + xi^2 = r^2 + wi
    # C^2 - 2 xj (Ci xi + Cj xj + Ck xk) + xj^2 = r^2 + wj
    # C^2 - 2 xk (Ci xi + Cj xj + Ck xk) + xk^2 = r^2 + wk
    # Ci + Cj + Ck = 1
    #
    # 2 xi (Ci xi + Cj xj + Ck xk) + (r^2 - C^2) = xi^2 - wi
    # 2 xj (Ci xi + Cj xj + Ck xk) + (r^2 - C^2) = xj^2 - wj
    # 2 xk (Ci xi + Cj xj + Ck xk) + (r^2 - C^2) = xk^2 - wk
    # Ci + Cj + Ck = 1

    D::Int
    @assert D ≥ 0
    N::Int
    @assert N ≥ 1
    @assert N ≤ D + 1
    # See arXiv:1103.3076v2 [cs.RA], section 10.1
    A = SMatrix{N + 1,N + 1}(i ≤ N && j ≤ N ? 2 * (xs[i] ⋅ xs[j])[] :
                             i == j ? zero(T) : one(T)
                             for i in 1:(N + 1), j in 1:(N + 1))
    b = SVector{N + 1}(i ≤ N ? (xs[i] ⋅ xs[i] - ws[i])[] : one(T)
                       for i in 1:(N + 1))
    c = A \ b
    cc = sum(c[i] * xs[i] for i in 1:N)
    return cc::Form{D,1}
end

export volume
"""
Unsigned volume
"""
function volume(xs::SVector{N,<:Form{D,1,T}}) where {N,D,T}
    D::Int
    @assert D ≥ 0
    N::Int
    @assert N ≥ 1
    @assert N ≤ D + 1
    ys = map(x -> x - xs[1], deleteat(xs, 1))
    vol0 = norm(∧(ys))
    if T <: Rational
        vol = rationalize(typeof(zero(T).den), vol0; tol=sqrt(eps(vol0)))
    else
        vol = vol0
    end
    vol::T
    vol /= factorial(N - 1)
    return vol::T
end
function volume(xs::SVector{N,SVector{D,T}}) where {N,D,T}
    return volume(SVector{N}(Form{D,1,T}(x) for x in xs))
end

export signed_volume
"""
Signed volume
"""
function signed_volume(xs::SVector{N,<:Form{D,1,T}},
                       signature::Int=1) where {N,D,T}
    D::Int
    @assert D ≥ 0
    N::Int
    R = N - 1
    @assert 0 ≤ R ≤ D
    # TODO: add metric signatures to DifferentialForms.jl
    # TODO: add outermorphisms to DifferentialForms.jl
    # TODO: move Plotting into its own module; just provide functions
    #       to obtain vertices, connectivity, etc.
    ys = map(x -> x - xs[1], deleteat(xs, 1))
    η = SVector{D,T}(a == D ? signature : 1 for a in 1:D)
    ys′ = map(y -> Form{D,1,T}(η .* y.elts), ys)
    y = ∧(ys)
    y′ = ∧(ys′)
    v2 = (y′ ⋅ y)[]::T
    sv2 = sign(v2)::T
    v = (sv2 * sqrt(abs(v2)))::T
    return v / factorial(R)
end

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

# See also <https://github.com/JaneliaSciComp/Morton.jl>
export morton
function morton(xs::SVector{D,T}, xmin::SVector{D,T},
                xmax::SVector{D,T}) where {U<:Unsigned,D,T}
    return morton(UInt64, xs, xmin, xmax)
end
function morton(::Type{U}, xs::SVector{D,T}, xmin::SVector{D,T},
                xmax::SVector{D,T}) where {U<:Unsigned,D,T}
    D == 0 && return morton(SVector{D,U}())
    y = (xs - xmin) ./ (xmax - xmin)
    nbits = min(sizeof(U) - 1, 8 * sizeof(U) ÷ D)
    nbins = U(1) << nbits
    i = clamp.(floor.(U, nbins .* y), 0, nbins - 1)
    return morton(i)
end
function morton(i::SVector{D,U}) where {D,U<:Unsigned}
    D == 0 && return U(0)
    return reduce(|, SVector{D,U}(spread(Val(D), i[d]) << (d - 1) for d in 1:D))
end

spread(::Val{1}, i::Unsigned) = i
function spread(::Val{2}, i::UInt64)
    # can use 64 ÷ 2 = 32 bits
    @assert i ==
            i &
            0b0000000000000000000000000000000011111111111111111111111111111111
    i = i & 0b0000000000000000000000000000000011111111111111111111111111111111
    i = ((i << 16) | i) &
        0b0000000000000000111111111111111100000000000000001111111111111111
    i = ((i << 08) | i) &
        0b0000000011111111000000001111111100000000111111110000000011111111
    i = ((i << 04) | i) &
        0b0000111100001111000011110000111100001111000011110000111100001111
    i = ((i << 02) | i) &
        0b0011001100110011001100110011001100110011001100110011001100110011
    i = ((i << 01) | i) &
        0b0101010101010101010101010101010101010101010101010101010101010101
    return i
end
function spread(::Val{3}, i::UInt64)
    # can use 64 ÷ 3 = 21 bits
    @assert i ==
            i &
            0b000000000000000000000000000000000000000000111111111111111111111
    i = i & 0b000000000000000000000000000000000000000000111111111111111111111
    i = ((i << 32) | i) &
        0b000000000011111000000000000000000000000000000001111111111111111
    i = ((i << 16) | i) &
        0b000000000011111000000000000000011111111000000000000000011111111
    i = ((i << 08) | i) &
        0b001000000001111000000001111000000001111000000001111000000001111
    i = ((i << 04) | i) &
        0b001000011000011000011000011000011000011000011000011000011000011
    i = ((i << 02) | i) &
        0b001001001001001001001001001001001001001001001001001001001001001
    return i
end
function spread(::Val{4}, i::UInt64)
    # can use 64 ÷ 4 = 16 bits
    @assert i ==
            i &
            0b0000000000000000000000000000000000000000000000001111111111111111
    i = i & 0b0000000000000000000000000000000000000000000000001111111111111111
    i = ((i << 24) | i) &
        0b0000000000000000000000001111111100000000000000000000000011111111
    i = ((i << 12) | i) &
        0b0000000000001111000000000000111100000000000011110000000000001111
    i = ((i << 06) | i) &
        0b0000001100000011000000110000001100000011000000110000001100000011
    i = ((i << 03) | i) &
        0b0001000100010001000100010001000100010001000100010001000100010001
    return i
end
function spread(::Val{5}, i::UInt64)
    # can use 64 ÷ 5 = 12 bits
    @assert i ==
            i & 0b000000000000000000000000000000000000000000000000111111111111
    i = i & 0b000000000000000000000000000000000000000000000000111111111111
    i = ((i << 32) | i) &
        0b000000000000000011110000000000000000000000000000000011111111
    i = ((i << 16) | i) &
        0b000000000000000011110000000000000000111100000000000000001111
    i = ((i << 08) | i) &
        0b000000001100000000110000000011000000001100000000110000000011
    i = ((i << 04) | i) &
        0b000010000100001000010000100001000010000100001000010000100001
    return i
end
function spread(::Val{6}, i::UInt64)
    # can use 64 ÷ 6 = 10 bits
    @assert i ==
            i & 0b000000000000000000000000000000000000000000000000001111111111
    i = i & 0b000000000000000000000000000000000000000000000000001111111111
    i = ((i << 40) | i) &
        0b000000000011000000000000000000000000000000000000000011111111
    i = ((i << 20) | i) &
        0b000000000011000000000000000000001111000000000000000000001111
    i = ((i << 10) | i) &
        0b000000000011000000000011000000000011000000000011000000000011
    i = ((i << 05) | i) &
        0b000001000001000001000001000001000001000001000001000001000001
    return i
end

end


## TYPES
"""
Compact structure for 2D point.
"""
struct Point
    x::Float64
    y::Float64
end
Point() = Point(0.0,0.0)
Point(v::V) where {V <: AbstractVector} = Point(v[1], v[2])
Point(t::Tuple{Float64,Float64}) = Point(t[1], t[2])

function point_dist(p1::Point, p2::Point, l::Real=2)
    s = SVector{2,Float64}(p1.x-p2.x, p1.y-p2.y)
    return norm(s,l)
end

function point_norm(p::Point, l::Real=2)
    s = SVector{2,Float64}(p.x,p.y)
    return norm(s,l)
end

function equal(p1::Point, p2::Point)
    return (isapprox(p1.x,p2.x) && isapprox(p1.y,p2.y))
end

function interpolate(p1::Point,p2::Point,frac::Float64)

    xval = p1.x + frac*(p2.x-p1.x)
    yval = p1.y + frac*(p2.y-p1.y)

    return Point(xval,yval)

end


using SchumakerSpline
using Test

xvals = [1,2,3,4,5,6]
shape_map(x,upper,lower) = min(max(x,lower), upper)

# Increasing
yvals = [1.0, 2.0, 2.9, 3.9, 2.3, 1.4]
vals2 = reshape_values(xvals, yvals; increasing = true, concave = true, shape_map = shape_map)
all(abs.([1.0, 2.0, 2.9, 3.8, 3.8, 3.8] .- vals2) .< 10*eps()) # Changes

# Increasing and concave
yvals = [1.0, 2.0, 2.9, 3.9, 4.3, 4.4]
vals2 = reshape_values(xvals, yvals; increasing = true, concave = true, shape_map = shape_map)
all(abs.([1.0, 2.0, 2.9, 3.8, 4.3, 4.4] .- vals2) .< 10*eps()) # Changes
yvals = sqrt.(xvals)
vals2 = reshape_values(xvals, yvals; increasing = true, concave = true, shape_map = shape_map)
all(abs.(vals2 .- yvals) .< 10*eps()) # No changes as input already increasing concave

# Increasing and convex
yvals = [1.0, 2.0, 3.1, 3.9, 4.3, 4.4]
vals2 = reshape_values(xvals, yvals; increasing = true, concave = false, shape_map = shape_map)
all(abs.([1.0, 2.0, 3.1, 4.2, 5.3, 6.4] .- vals2) .< 10*eps()) #changes
yvals = (xvals) .^ 2
vals2 = reshape_values(xvals, yvals; increasing = true, concave = false, shape_map = shape_map)
all(abs.(vals2 .- yvals) .< 10*eps()) # No changes as input already increasing concave

# Decreasing
yvals = -[1.0, 2.0, 2.9, 3.9, 2.3, 1.4]
vals2 = reshape_values(xvals, yvals; increasing = false, concave = false, shape_map = shape_map)
all(abs.([-1.0, -2.0, -2.9, -3.8, -3.8, -3.8] .- vals2) .< 10*eps()) # Changes

# Decreasing and convex
yvals = -[1.0, 2.0, 2.9, 3.9, 4.3, 4.4]
vals2 = reshape_values(xvals, yvals; increasing = false, concave = false, shape_map = shape_map)
all(abs.(-[1.0, 2.0, 2.9, 3.8, 4.3, 4.4] .- vals2) .< 10*eps()) # Changes
yvals = -sqrt.(xvals)
vals2 = reshape_values(xvals, yvals; increasing = false, concave = false, shape_map = shape_map)
all(abs.(vals2 .- yvals) .< 10*eps()) # No changes as input already increasing concave

# Decreasing and concave
yvals = -[1.0, 2.0, 3.1, 3.9, 4.3, 4.4]
vals2 = reshape_values(xvals, yvals; increasing = false, concave = true, shape_map = shape_map)
all(abs.(-[1.0, 2.0, 3.1, 4.2, 5.3, 6.4] .- vals2) .< 10*eps()) #changes
yvals = -(xvals) .^ 2
vals2 = reshape_values(xvals, yvals; increasing = false, concave = true, shape_map = shape_map)
all(abs.(vals2 .- yvals) .< 10*eps()) # No changes as input already increasing concave


### A Pluto.jl notebook ###
# v0.12.11

using Markdown
using InteractiveUtils

# ╔═╡ be75fc5e-3247-11eb-1c9f-6f30ea72331c
begin
	import Pkg; Pkg.add("ForwardDiff")
	Pkg.add("Plots")
	using Plots
end

# ╔═╡ 5f085b68-323f-11eb-3ee9-13b634011eac
md"""
# Julia Essentials
### Solutions to the Exercise
"""

# ╔═╡ e6599ddc-323f-11eb-3f27-a7f05eb26ae5
md"""
##### `Exercise 1 - 4 are quite naive and will only help you to get familiarity with the platform, you are advised to permorn on your own` """

# ╔═╡ e57c7f4c-323f-11eb-26dd-5b88be11bed5
html"""

"""

# ╔═╡ c994bf8e-3245-11eb-259f-a791ae0734dc
md"""
##### Exercise 5
The Julia libraries include functions for interpolation and approximation.

Nevertheless, let’s write our own function approximation routine as an exercise.

In particular, write a function linapprox that takes as arguments

A function f mapping some interval [a,b] into ℝ.
two scalars a and b providing the limits of this interval.
An integer n determining the number of grid points.
A number x satisfying a ≤ x ≤ b.
and returns the piecewise linear interpolation of f at x, based on n evenly spaced grid points a = point[1] < point[2] < ... < point[n] = b.

Aim for clarity, not efficiency.

Hint: use the function range to linearly space numbers."""

# ╔═╡ 20cd089a-3246-11eb-368f-fdbc5129f16c
function linapprox(f,a,b,n,x)
	Δ = (b-a)/(n-1)
	k = (x-a)÷Δ
	u,v = k*Δ +a,(k+1)*Δ +a
	δ= (f(v)-f(u))/(Δ)
	f(u) + (x-u)*δ
end

# ╔═╡ 4a812cde-3249-11eb-3d46-c1937d75ef1e
html"""

"""

# ╔═╡ 42fd5d3c-3248-11eb-0dac-43b98063620e
md"""
##### Exercise 6
`should be easy, try to do it yourself`
"""

# ╔═╡ 58b6b208-3249-11eb-110e-1f2d5f82756c
html"""

"""

# ╔═╡ 46a8a40c-3249-11eb-345a-8143e3ac48cc
md"""
##### Exercise 7
Redo Exercise 5 except

Pass in a range instead of the a, b, and n. Test with a range such as nodes = -1.0:0.5:1.0.
Instead of the while used in the solution to Exercise 5, find a better way to efficiently bracket the x in the nodes.
Hints:

Rather than the signature as function linapprox(f, a, b, n, x), it should be called as function linapprox(f, nodes, x).
step(nodes), length(nodes), nodes[1], and nodes[end] may be useful.
Type ?÷ into jupyter to explore quotients from Euclidean division for more efficient bracketing."""

# ╔═╡ 78f845ea-3249-11eb-3ac1-fb51649acd04
function linapprox(f,nodes,x)
	a,b = nodes[1],nodes[end]
	n = length(nodes)
	linapprox(f,a,b,n,x)
end

# ╔═╡ 9dbef2fe-3247-11eb-2827-1b0878e2cd0c
let
	f_ex5(x) = x^2
	g_ex5(x) = linapprox(f_ex5, -1, 1, 3, x)
	x_grid = range(-1.0, 1.0, length = 100)
	y_vals = f_ex5.(x_grid)
	y = g_ex5.(x_grid)
	plot(x_grid, y_vals, label = "true")
	plot!(x_grid, y, label = "approximation")
end

# ╔═╡ 9e334f98-324a-11eb-024f-93e55e491e58
let
	f_ex5(x) = x^2
	g_ex5(x) = linapprox(f_ex5, -1:5, x)
	x_grid = range(-1.0, 5.0, length = 100)
	y_vals = f_ex5.(x_grid)
	y = g_ex5.(x_grid)
	plot(x_grid, y_vals, label = "true")
	plot!(x_grid, y, label = "approximation")
end

# ╔═╡ Cell order:
# ╟─5f085b68-323f-11eb-3ee9-13b634011eac
# ╟─e6599ddc-323f-11eb-3f27-a7f05eb26ae5
# ╟─e57c7f4c-323f-11eb-26dd-5b88be11bed5
# ╟─c994bf8e-3245-11eb-259f-a791ae0734dc
# ╠═20cd089a-3246-11eb-368f-fdbc5129f16c
# ╟─9dbef2fe-3247-11eb-2827-1b0878e2cd0c
# ╟─4a812cde-3249-11eb-3d46-c1937d75ef1e
# ╟─42fd5d3c-3248-11eb-0dac-43b98063620e
# ╟─58b6b208-3249-11eb-110e-1f2d5f82756c
# ╟─46a8a40c-3249-11eb-345a-8143e3ac48cc
# ╠═78f845ea-3249-11eb-3ac1-fb51649acd04
# ╟─9e334f98-324a-11eb-024f-93e55e491e58
# ╟─be75fc5e-3247-11eb-1c9f-6f30ea72331c

getting started with julia/Julia Esswntials.jl


export LMatrixOp

"""
`LMatrixOp(domainType=Float64::Type, dim_in::Tuple, b::Union{AbstractVector,AbstractMatrix})`

`LMatrixOp(b::AbstractVector, number_of_rows::Int)`

Creates a `LinearOperator` which, when multiplied with a matrix `X::AbstractMatrix`, returns the product `X*b`.

```julia
julia> op = LMatrixOp(Float64,(3,4),ones(4))
(⋅)b  ℝ^(3, 4) -> ℝ^3

julia> op = LMatrixOp(ones(4),3)
(⋅)b  ℝ^(3, 4) -> ℝ^3

julia> op*ones(3,4)
3-element Array{Float64,1}:
 4.0
 4.0
 4.0

```

"""
struct LMatrixOp{T, A <: Union{AbstractVector,AbstractMatrix},
		 B <: AbstractMatrix} <: LinearOperator
	b::A
	bt::B
	n_row_in::Integer
end

##TODO decide what to do when domainType is given, with conversion one loses pointer to data...
# Constructors
function LMatrixOp(DomainType::Type,
		   DomainDim::Tuple{Int,Int}, b::A) where {A <: Union{AbstractVector,AbstractMatrix}}
	bt = b'
	LMatrixOp{DomainType, A, typeof(bt)}(b,bt,DomainDim[1])
end

LMatrixOp(b::A, n_row_in::Int) where {T,A<:Union{AbstractVector{T},AbstractMatrix{T}}} =
LMatrixOp(T,(n_row_in,size(b,1)),b)

# Mappings
mul!(y::C, L::LMatrixOp{T,A,B}, X::AbstractMatrix{T} ) where {T,A,B,C<:Union{AbstractVector,AbstractMatrix}} =
mul!(y,X,L.b)

function mul!(y::AbstractMatrix{T}, L::AdjointOperator{LMatrixOp{T,A,B}}, Y::AbstractVector{T} ) where {T,A,B}
	y .= L.A.bt.*Y
end

function mul!(y::AbstractMatrix{T}, L::AdjointOperator{LMatrixOp{T,A,B}}, Y::AbstractMatrix{T} ) where {T,A,B}
	mul!(y,Y,L.A.b')
end

# Properties
domainType(L::LMatrixOp{T, A}) where {T, A} = T
codomainType(L::LMatrixOp{T, A}) where {T, A} = T

fun_name(L::LMatrixOp) = "(⋅)b"

size(L::LMatrixOp{T,A,B}) where {T,A <: AbstractVector,B <: Adjoint} = (L.n_row_in,),(L.n_row_in, length(L.b))
size(L::LMatrixOp{T,A,B}) where {T,A <: AbstractMatrix,B <: AbstractMatrix} = (L.n_row_in,size(L.b,2)),(L.n_row_in, size(L.b,1))

#TODO

#is_full_row_rank(L::LMatrixOp) =
#is_full_column_rank(L::MatrixOp) =


### 3.1 - Loops

n = 0
m = 1

#=
# Major problems. n is not recognized inside the while loop. This makes the loop unusable
while n < 10
    n += 1
    println(m)
end
=#

for i ∈ 0:10
    println(i)
    println(n)
end

# Q1

for n in 1:100
    print(n^2, ", ")
end

# Q2

sq_dict = Dict{Int64, Int64}()

for n in 1:100
    print(n^2, ", ")
    sq_dict[n] = n^2
end

# Q3

squares_arr = [i^2 for i in 1:100]

@assert length(squares_arr) == 100
@assert sum(squares_arr) == 338350

### 3.2 - Conditionals

# Q1

x = 4

if (x % 2) == 0
    println(x)
else
    println("odd")
end

# Q2

(x % 2) == 1 ? println(x) : println("odd")

### 3.3 - Functions

# Q1

add_one = x -> x+1

# Q2 & Q3

M = [i+j for i in 1:3, j in 1:3]
add_one.(M)


using Dates, ReachabilityBenchmarks

# print current time
now()

# ISS benchmark
include("SpaceStation/iss_benchmark.jl")

# Spacecraft Rendezvous benchmark
include("Rendezvous/SpacecraftRendezvous_benchmark.jl")

# Powertrain benchmark
# N/A

# Building benchmark
include("Building/building_benchmark.jl")

# Platooning benchmark
include("Platooning/Platooning_benchmark.jl")

# Gearbox benchmark
# include("Gearbox/Gearbox.jl")
# system, options = gearbox()
# res = run_gearbox(system, options)
# @assert res.satisfied
# @time run_gearbox(system, options)

nothing


@testset "inverse.jl" begin
    @testset "square" begin
        x = randn()
        @test GPLikelihoods.square(x) == x^2
    end

    @testset "inverse" begin
        x = rand()
        for f in (
            exp,
            log,
            inv,
            log1pexp,
            logexpm1,
            sqrt,
            GPLikelihoods.square,
            logit,
            logistic,
            normcdf,
            norminvcdf,
        )
            g = GPLikelihoods.inverse(f)

            # check that definitions are correct
            @test g(f(x)) ≈ x
            @test f(g(x)) ≈ x

            # check that definitions are complete
            @test GPLikelihoods.inverse(g) === f
        end
    end
end


function _make_test_grid(args...)
    return reshape(Iterators.flatten(
                            reduce(vcat, Iterators.product(args...))
                            ) |> collect,
                    length(args), *(length.(args)...)) |> permutedims
end

function _make_test_df(args...; vars)
    DataFrame(_make_test_grid(args...), vars)
end

function _make_test_rows(args...)
    RowVecs(_make_test_grid(args...))
end

function getrank(v; flattenzeros=true)
    r = invperm(sortperm(v))
    if flattenzeros
        z = findall(iszero, v)
        r[z] .= 1
    end
    return r
end

function invnormaltransform(v; μ=0, σ=1, c=3/8, flattenzeros=true)
    rank = getrank(v; flattenzeros)
    return [norminvcdf(μ, σ, (x - c) / (length(v) - 2c + 1)) for x in rank]
end

function _df_output(df, args)
    if !isnothing(args["output"])
        @info "Writing to $(args["output"])"
        out = args["output"]
        delim = endswith(out, "csv") ? ',' :
              endswith(out, "tsv") ? '\t' : error("--output arg must be '.tsv' or '.csv'")
        CSV.write(expanduser(args["output"]), df; delim)
    else
        @show df
    end
end


@testset "utils.jl" begin
    @testset "writing and reading" begin
        S = [rand(ComplexF64, 3) for i = 1:2]
        write_solutions("tmp_sols.txt", S)
        @test read_solutions("tmp_sols.txt") == S

        p = rand(ComplexF64, 14)
        write_parameters("tmp_params.txt", p)
        @test read_parameters("tmp_params.txt") == p
    end

    Q = HC.rand_unitary_matrix(8, ComplexF64)
    @test norm(LinearAlgebra.I - Q' * Q, Inf) < 1e-14

    @test HC.fast_abs(-2.0) ≈ abs(-2.0)
    @test HC.fast_abs(2.0 + 3im) ≈ abs(2.0 + 3im)

    @test HC.nthroot(42.2, 0) == 1.0
    @test HC.nthroot(42.2, 1) == 42.2
    @test HC.nthroot(42.1, 2) == sqrt(42.1)
    @test HC.nthroot(42.1, 3) == cbrt(42.1)
    @test HC.nthroot(42.1, 4) == sqrt(sqrt(42.1))
    @test HC.nthroot(42.1, 5) == 42.1^(1 / 5)

    @testset "SegmentStepper" begin
        seg = HC.SegmentStepper(0, 1)
        @test seg.start == 0
        @test seg.target == 1
        @test seg.abs_Δ == 1.0
        @test seg.forward
        @test !isempty(sprint(show, seg))

        HC.propose_step!(seg, exp2(-60))
        @test seg.Δs == exp2(-60)
        @test seg.s′ == exp2(-60)
        @test seg.t′ == exp2(-60)
        @test seg.t == 0
        @test seg.Δt == exp2(-60)
        HC.step_success!(seg)
        @test seg.s == exp2(-60)
        @test seg.t == exp2(-60)
        @test seg.t′ == exp2(-60)
        HC.propose_step!(seg, exp2(-50))
        @test seg.t == exp2(-60)
        @test seg.s′ == exp2(-60) + exp2(-50)
        @test seg.t′ == exp2(-60) + exp2(-50)

        HC.propose_step!(seg, 4)
        @test seg.s′ == 1.0
        @test seg.t′ == 1.0
        @test !HC.is_done(seg)
        HC.step_success!(seg)
        @test HC.is_done(seg)

        seg = HC.SegmentStepper(im, 0)
        @test seg.start == im
        @test seg.target == 0
        @test seg.abs_Δ == 1.0
        @test !seg.forward

        HC.propose_step!(seg, exp2(-20))
        @test seg.Δs == exp2(-20)
        @test seg.s′ == 1 - exp2(-20)
        @test seg.t′ == im * (1 - exp2(-20))
        @test seg.t == im
        @test seg.Δt == -im * exp2(-20)
        HC.step_success!(seg)
        @test seg.s == 1 - exp2(-20)
        @test seg.t == im * (1 - exp2(-20))
        @test seg.t′ == im * (1 - exp2(-20))
        HC.propose_step!(seg, exp2(-10))
        @test seg.t == im * (1 - exp2(-20))
        @test seg.s′ == 1 - (exp2(-20) + exp2(-10))
        @test seg.t′ == im * (1 - (exp2(-20) + exp2(-10)))

        HC.propose_step!(seg, 2)
        @test seg.s′ == 0.0
        @test seg.t′ == 0.0
        @test !HC.is_done(seg)
        HC.step_success!(seg)
        @test HC.is_done(seg)
    end

    struct Foo_print_fieldnames
        a::Int
        b::Float64
    end

    @test sprint(HC.print_fieldnames, Foo_print_fieldnames(2, 2.2)) ==
          "Foo_print_fieldnames:\n • a → 2\n • b → 2.2\n"
end


# Direct Search with Probabilistic Descent as described in Gratton2014:
#
#  S. Gratton, C. W. Royer, L. N. Vicente, and Z. Zhang, Direct search
#  based on probabilistic descent, preprint 14-11, Dept. Mathematics, Univ. Coimbra.
#  http://www.mat.uc.pt/~lnv/papers/ds-random.pdf
#

"""
Generate `num` random vectors on the `n`-dimensional, unit (hyper)sphere.
This is the Muller-Marsaglia method as described [here](http://mathworld.wolfram.com/HyperspherePointPicking.html).
"""
function sample_unit_hypersphere(n, num = 1)
    X = randn(n, num)
    sqrootsums = 1 ./ sqrt.(sum(abs2, X, dims=1))
    sqrootsums .* X
end

struct RandomDirectionGen <: DirectionGenerator
    numDimensions::Int
    numDirections::Int
end

directions_for_k(rdg::RandomDirectionGen, k) =
    sample_unit_hypersphere(rdg.numDimensions, rdg.numDirections)

"""
Generate half of the directions randomly and then
mirrors by negating them.
"""
struct MirroredRandomDirectionGen <: DirectionGenerator
    numDimensions::Int
    numDirections::Int

    function MirroredRandomDirectionGen(numDims, numDirections)
        iseven(numDirections) ||
            throw(ArgumentError("the number of directions must be even"))
        new(numDims, numDirections)
    end
end

function directions_for_k(rdg::MirroredRandomDirectionGen, k)
    r = sample_unit_hypersphere(rdg.numDimensions, rdg.numDirections÷2)
    [r -r]
end

const DirectSearchProbabilisticDescentDefaultParameters = ParamsDict(
    :NumDirections => 2, # This should be a function of Gamma and Phi for the GSS but 2 is often enough
)

function direct_search_probabilistic_descent(problem::OptimizationProblem, parameters::Parameters)
    params = chain(DirectSearchProbabilisticDescentDefaultParameters, parameters)
    params[:DirectionGenerator] = MirroredRandomDirectionGen(numdims(problem), params[:NumDirections])
    GeneratingSetSearcher(problem, params)
end


#=
This is an experiment to see if one player starts out holding just four aces
and the other player has the rest of the deck, how do they fare?
=#

using PlayingCards52, War, ProgressMeter

"""
    ace_race(reps::Int = 10_000)

returns the frequency that four aces vs the rest of the deck wins at war.
Optionally, `ace_race(reps, skip_rank::Int)` to skip a different rank, e.g.,
`ace_race(skip_rank=13)` for kings.
"""
function ace_race(reps::Int = 10_000; skip_rank = 1)

    d = deck(false)
    d2 = [c for c in d if rank(c) != skip_rank]
    d1 = [Card(s, skip_rank) for s in PlayingCards52.suit_list]

    a_wins = 0
    PM = Progress(reps)

    for k = 1:reps
        shuffle!(d2)
        r = play_war(d1, d2)
        if r[end] > 0
            a_wins += 1
        end
        next!(PM)
    end

    a_pct = a_wins / reps
    return a_pct

    # a_len = a_steps / reps
    # b_len = b_steps / reps

    # println("Four aces wins $a_pct% of the time")
    # println("Average length of a winning game: $a_len")
    # println("Average length of a losing game:  $b_len")

end


export post_process

function post_process(data::SyntheticData, model::Model, J::Int64, statehist::Array{Array{Float64,2},1})
    Ne = size(statehist[1],2)
    @show Ne
    # Post_process compute the statistics (mean, median, and
    # standard deviation) of the RMSE, spread and coverage
    # probability over (J-T_BurnIn) assimilation steps.

    # enshist contains the initial condition, so one more element
    idx_xt = model.Tstep+1:model.Tspinup+model.Tstep
    idx_ens = model.Tstep-model.Tburn+2:model.Tstep+1
    # Compute root mean square error statistics
    Rmse, Rmse_med, Rmse_mean, Rmse_std = metric_hist(rmse, data.xt[:,idx_xt], statehist[idx_ens])
    # Compute ensemble spread statistics
    Spread, Spread_med, Spread_mean, Spread_std = metric_hist(spread, statehist[idx_ens])
    # Compute quantile information
    qinf, qsup = quant(statehist[idx_ens])

    # Compute coverage probability statistics

    Covprob = zeros(length(idx_xt))
    b = zeros(Bool, model.Nx)
    for (i,idx) in enumerate(idx_xt)
        for j=1:model.Nx
        b[j] = (qsup[j,i] >= data.xt[j,idx] >= qinf[j,i])
        end
        Covprob[i] = deepcopy(mean(b))
    end

    Covprob_med  = median(Covprob)
    Covprob_mean = mean(Covprob)
    Covprob_std  = std(Covprob)

    Metric = Metrics(Ne, Rmse, Rmse_med, Rmse_mean, Rmse_std, Spread, Spread_med,
                    Spread_mean, Spread_std, Covprob, Covprob_med,
                    Covprob_mean, Covprob_std)
    return Metric
end


using Test, QBase

@testset "./src/Nonlocality/optimize_measurements.jl" begin

using BellScenario

@testset "Nonlocality.optimize_measurement(LocalSignaling)" begin
    @testset "non State types" begin
        scenario = LocalSignaling(3,3,2)
        game = BellGame([1 0 0;0 1 0;0 0 1], 2)
        ρ_states = [[1 0;0 0],[0 0;0 1],[0 0;0 1]]

        dict = Nonlocality.optimize_measurement(scenario, game, ρ_states)

        @test isapprox(dict["violation"], 0.0, atol=1e-6)
        @test dict["povm"] ≈ [[1 0;0 0],[0 0;0 0.5],[0 0;0 0.5]]
    end

    @testset "trine states" begin
        scenario = LocalSignaling(3,3,2)
        game = BellGame([1 0 0;0 1 0;0 0 1], 2)
        ρ_states = trine_qubit_states()

        dict = Nonlocality.optimize_measurement(scenario, game, ρ_states)

        @test isapprox(dict["violation"], 0.0, atol=1e-6)
        @test all(isapprox.(dict["povm"][1], 2/3*ρ_states[1], atol=1e-6))
        @test all(isapprox.(dict["povm"][2], 2/3*ρ_states[2], atol=1e-6))
        @test all(isapprox.(dict["povm"][3], 2/3*ρ_states[3], atol=1e-6))

        @test dict["states"] == ρ_states
        @test dict["game"] == game
        @test dict["scenario"] == scenario
    end

    @testset "bb84 states" begin
        scenario = LocalSignaling(4,4,2)
        game = BellGame([1 0 0 0;0 1 0 0;0 0 1 0;0 0 0 1], 2)
        ρ_states = bb84_qubit_states()

        dict = Nonlocality.optimize_measurement(scenario, game, ρ_states)

        @test isapprox(dict["violation"], 0.0, atol=1e-6)
        @test all(isapprox.(dict["povm"][1], 1/2*ρ_states[1], atol=1e-3))
        @test all(isapprox.(dict["povm"][2], 1/2*ρ_states[2], atol=1e-3))
        @test all(isapprox.(dict["povm"][3], 1/2*ρ_states[3], atol=1e-3))
        @test all(isapprox.(dict["povm"][4], 1/2*ρ_states[4], atol=1e-3))
    end

    @testset "sic qubit states" begin
        scenario = LocalSignaling(4,4,2)
        game = BellGame([1 0 0 0;0 1 0 0;0 0 1 0;0 0 0 1], 2)
        ρ_states = sic_qubit_states()

        dict = Nonlocality.optimize_measurement(scenario, game, ρ_states)

        @test isapprox(dict["violation"], 0.0, atol=1e-5)
        @test all(isapprox.(dict["povm"][1], 1/2*ρ_states[1], atol=1e-5))
        @test all(isapprox.(dict["povm"][2], 1/2*ρ_states[2], atol=1e-5))
        @test all(isapprox.(dict["povm"][3], 1/2*ρ_states[3], atol=1e-5))
        @test all(isapprox.(dict["povm"][4], 1/2*ρ_states[4], atol=1e-5))
    end

    @testset "Errors" begin
        scenario = LocalSignaling(3,3,2)
        states = bb84_qubit_states()
        game = BellGame([1 0 0 0;0 1 0 0;0 0 1 0;0 0 0 1], 2)

        @test_throws DomainError Nonlocality.optimize_measurement(scenario, game, states)

        scenario = LocalSignaling(4,4,3)
        @test_throws DomainError  Nonlocality.optimize_measurement(scenario, game, states)

        scenario = LocalSignaling(3,3,2)
        game = BellGame([1 0 0;0 1 0;0 0 1],2)
        states = [[1 0;0 1],[0 0;0 1],[0.5 0;0 0.5]]
        @test_throws DomainError Nonlocality.optimize_measurement(scenario, game, states)
    end
end

@testset "Nonlocality.optimize_measurement(::BipartiteNonSignaling)" begin
    @testset "matrix states and povms" begin
        scenario = BipartiteNonSignaling(2,2,2,2)
        game = BellGame([0 1 1 0;0 0 0 1;0 0 0 1;1 0 0 1],2)
        ρ_AB = [1 0 0 1;0 0 0 0;0 0 0 0;1 0 0 1]/2
        POVMs = [
            [[1 0;0 0],[0 0;0 1]],
            [[0.5 0.5;0.5 0.5],[0.5 -0.5;-0.5 0.5]]
        ]

        opt_dictA = Nonlocality.optimize_measurement(scenario, game, ρ_AB, A_POVMs=POVMs)
        opt_dictB = Nonlocality.optimize_measurement(scenario, game, ρ_AB, B_POVMs=POVMs)

        @test opt_dictA["score"] ≈ 2.20710279
        @test opt_dictA["violation"] ≈ 0.207102796
        @test opt_dictA["A_POVMs"] isa Vector{<:POVM}
        @test opt_dictA["state"] isa State

        @test opt_dictB["score"] ≈ 2.20710279
        @test opt_dictB["violation"] ≈ 0.207102796
        @test opt_dictB["B_POVMs"] isa Vector{<:POVM}
        @test opt_dictB["state"] isa State
    end

    @testset "CHSH inequality" begin
        scenario = BipartiteNonSignaling(2,2,2,2)
        game = BellGame([0 1 1 0;0 0 0 1;0 0 0 1;1 0 0 1],2)
        ρ_AB = bell_states()[1]
        POVMs = [
            POVM([[1 0;0 0],[0 0;0 1]]),
            POVM([[0.5 0.5;0.5 0.5],[0.5 -0.5;-0.5 0.5]])
        ]

        @test game.β == 2

        opt_dictA = Nonlocality.optimize_measurement(scenario, game, ρ_AB, A_POVMs=POVMs)
        opt_dictB = Nonlocality.optimize_measurement(scenario, game, ρ_AB, B_POVMs=POVMs)

        @test length(keys(opt_dictA)) == 7
        @test opt_dictA["scenario"] == scenario
        @test opt_dictA["game"] == game
        @test opt_dictA["game"].β == 2
        @test opt_dictA["score"] ≈ 2.20710279
        @test opt_dictA["violation"] ≈ 0.207102796
        @test opt_dictA["state"] == ρ_AB
        @test opt_dictA["A_POVMs"] == POVMs
        @test isapprox(
            opt_dictA["B_POVMs"],
            [
                [bloch_qubit_state(π/4,0), bloch_qubit_state(5π/4,0)],
                [bloch_qubit_state(7π/4,0), bloch_qubit_state(3π/4,0)]
            ],
            atol=1e-5
        )

        @test length(keys(opt_dictB)) == 7
        @test opt_dictB["scenario"] == scenario
        @test opt_dictB["game"] == game
        @test opt_dictB["game"].β == 2
        @test opt_dictB["score"] ≈ 2.2071028
        @test opt_dictB["violation"] ≈ 0.207102796
        @test opt_dictB["state"] == ρ_AB
        @test opt_dictB["B_POVMs"] == POVMs
        @test isapprox(
            opt_dictB["A_POVMs"],
            [
                [bloch_qubit_state(π/4,0), bloch_qubit_state(5π/4,0)],
                [bloch_qubit_state(7π/4,0),bloch_qubit_state(3π/4,0)]
            ],
            atol=1e-5
        )
    end

    @testset "Domain Errors" begin
        scenario = BipartiteNonSignaling(2,2,2,2)
        scenarioA = BipartiteNonSignaling(3,2,3,2)
        scenarioB = BipartiteNonSignaling(2,3,2,3)
        game = BellGame([0 1 1 0;0 0 0 1;0 0 0 1;1 0 0 1],2)
        ρ_AB = bell_states()[1]
        POVMs = [
            POVM([[1 0;0 0],[0 0;0 1]]),
            POVM([[0.5 0.5;0.5 0.5],[0.5 -0.5;-0.5 0.5]])
        ]

        @test_throws DomainError Nonlocality.optimize_measurement(scenario, game, ρ_AB)

        @test_throws DomainError Nonlocality.optimize_measurement(scenarioA, game, ρ_AB, A_POVMs=POVMs)
        @test_throws DomainError Nonlocality.optimize_measurement(scenarioB, game, ρ_AB, B_POVMs=POVMs)

        @test_throws DomainError Nonlocality.optimize_measurement(
            scenario, game, [1 0 0 1;0 0 0 0;0 0 0 0;1 0 0 1],A_POVMs=POVMs
        )
        @test_throws DomainError Nonlocality.optimize_measurement(
            scenario, game, ρ_AB, A_POVMs = [[[1 0;0 1],[1 0;0 1]],[[1 0;0 0],[1 0;0 0]]]
        )
        @test_throws DomainError Nonlocality.optimize_measurement(
            scenario, game, ρ_AB, B_POVMs = [[[1 0;0 1],[1 0;0 1]],[[1 0;0 0],[1 0;0 0]]]
        )
    end
end

end


#!/usr/bin/env julia

# HOW TO RUN THIS SCRIPT:
# 1: check that you have a recent(ish, >1.0) version of Julia installed
# 2: within Julia, make sure you have the required packages (below, in the "Using..." statement) installed (I think they all come by default though)
# 3: make sure you have FFmpeg installed
# 4: setup an empty sub-directory called "tmpPlots"
# 5: go to line 693 and uncomment the `makeAnim()` function call
# 6: save this file
# 7: run this file (double click it and tell it to run with Julia, open a terminal and type `julia threeBodyProb.jl`, or start a julia session and type `include("threeBodyProb.jl")` )
# 8: profit

# following the above steps should generate a random three-body problem video in 720p without music
# to add music, specify initial conditions, or otherwise tinker...read the code/README and the comments in the code!
# this has only been tested on Linux (Mint) and Windows 10, so you might run into small additional issues on macOS?

using Plots, Random, Printf, Plots.Measures, Dates, CSV, DataFrames, DataFramesMeta

function initCondGen() #get random initial conditions for mass/radius, position, and velocity
    function getMass(nBodies) #generate random masses that better reflect actual stellar populations, although not currently using because it's boring
        mList=zeros(nBodies)
        N=(0.5^(-1.3)-150^(-1.3))/1.3 #crude approximation of IMF integral assuming alpha = 2.3, stellar mass range of 0.5:150 solar masses
        rescale=1e6
        max=floor(Int,N*rescale)
        for i=1:nBodies
            intTarget=rand(0:max,1)[1]/rescale
            m=(0.5^(-1.3)-intTarget*1.3)^(-1/1.3) #just algebra from above
            mList[i]=round(m,digits=2)
        end
        return mList
    end
    m=rand(1:1500,3)./10 #3 random masses between 0.1 and 150 solar masses, uniform distribution
    #m=getMass(3) #get mass from IMF -- this way is kind of boring...so not using it, but left here in case I change my mind?
    rad=m.^0.8 #3 radii based on masses in solar units
    m=m.*2e30 #convert to SI kg
    rad=rad.*7e8 #convert to SI m
    pos1=rand(-25:25,2) #random initial coordinates x & y for first body, AU
    function genPos2(pos1)
        accept2=false
        while accept2==false
            pos2=rand(-25:25,2) #random initial coordinates for second body, AU
            dist21=sqrt((pos1[1]-pos2[1])^2+(pos1[2]-pos2[2])^2)
            if (dist21*1.5e11)>(rad[1]+rad[2]) #they aren't touching
                accept2=true
                return pos2
            end
        end
    end
    pos2=genPos2(pos1)
    function genPos3(pos1,pos2)
        accept3=false
        while accept3==false
            pos3=rand(-25:25,2) #random initial coordinates for third body, AU
            dist31=sqrt((pos1[1]-pos3[1])^2+(pos1[2]-pos3[2])^2)
            dist32=sqrt((pos2[1]-pos3[1])^2+(pos2[2]-pos3[2])^2)
            if (dist31*1.5e11)>(rad[1]+rad[3]) && (dist32*1.5e11)>(rad[2]+rad[3]) #3rd isn't touching either
                accept3=true
                return pos3
            end
        end
    end
    pos3=genPos3(pos1,pos2)
    pos=[pos1[1],pos1[2],pos2[1],pos2[2],pos3[1],pos3[2]].*1.5e11 #convert accepted positions to SI, m
    v=rand(-7e3:7e3,6) #random x & y velocities with mag between -10 & 10 km/s, totally arbitrary...
    #r=[x1,y1,x2,y2,x3,y3,v1x,v1y,v2x,v2y,v3x,v3y] -- format if you want to specify your own initial conditions
    r=[pos[1],pos[2],pos[3],pos[4],pos[5],pos[6],v[1],v[2],v[3],v[4],v[5],v[6]]
    open("initCond.txt","w") do f #save initial conditions to file in folder where script is run
        write(f,"m1=$(@sprintf("%.1f",(m[1]/2e30))) m2=$(@sprintf("%.1f",(m[2]/2e30))) m3=$(@sprintf("%.1f",(m[3]/2e30))) (solar masses)\nv1x=$(v[1]/1e3) v1y=$(v[2]/1e3) v2x=$(v[3]/1e3) v2y=$(v[4]/1e3) v3x=$(v[5]/1e3) v3y=$(v[6]/1e3) (km/s)\nx1=$(pos1[1]) y1=$(pos1[2]) x2=$(pos2[1]) y2=$(pos2[2]) x3=$(pos3[1]) y3=$(pos3[2]) (AU from center)")
    end
    return r, rad, m
end

function dR(r,m;energyBool=0) #function we will use RK4 on to approximate solution
    G=6.67408313131313e-11# Nm^2/kg^2
    M1,M2,M3=m[1],m[2],m[3] #kg
    x1,x2,x3=r[1],r[3],r[5] #m
    y1,y2,y3=r[2],r[4],r[6] #m

    c1,c2,c3=G*M1,G*M2,G*M3 #Nm^2/kg
    r1_2=sqrt((x1-x2)^2+(y1-y2)^2) #distance from 1->2, m
    r1_3=sqrt((x1-x3)^2+(y1-y3)^2) #distance from 1->3, m
    r2_3=sqrt((x2-x3)^2+(y2-y3)^2) #distance from 2->3, m

    v1X,v2X,v3X=r[7],r[9],r[11] #these are our change in position after dt (dr/dt*dt=dr)
    v1Y,v2Y,v3Y=r[8],r[10],r[12] #m after * dt

    #get change in velocity from accelerations (d^2r/dt^2*dt=dv/dt*dt=dv)
    aX1=-(c2*(x1-x2)/(r1_2^3))-(c3*(x1-x3)/(r1_3^3)) #d^2x/dt^2 for 1, m/s after * dt
    aX2=-(c1*(x2-x1)/(r1_2^3))-(c3*(x2-x3)/(r2_3^3)) #d^2x/dt^2 for 2, m/s
    aX3=-(c1*(x3-x1)/(r1_3^3))-(c2*(x3-x2)/(r2_3^3)) #d^2x/dt^2 for 3, m/s
    aY1=-(c2*(y1-y2)/(r1_2^3))-(c3*(y1-y3)/(r1_3^3)) #d^2y/dt^2 for 1, m/s
    aY2=-(c1*(y2-y1)/(r1_2^3))-(c3*(y2-y3)/(r2_3^3)) #d^2y/dt^2 for 2, m/s
    aY3=-(c1*(y3-y1)/(r1_3^3))-(c2*(y3-y2)/(r2_3^3)) #d^2y/dt^2 for 3, m/s

    global energy #keep track of energy loss from RK4 error
    U=-G*M1*M2/r1_2-G*M2*M3/r2_3-G*M1*M3/r1_3 #grav potential
    K=0.5*M1*(v1X^2+v1Y^2)+0.5*M2*(v2X^2+v2Y^2)+0.5*M3*(v3X^2+v3Y^2) #kinetic
    if energyBool==1
        push!(energy,K+U) #total system energy
    end

    return [v1X,v1Y,v2X,v2Y,v3X,v3Y,aX1,aY1,aX2,aY2,aX3,aY3]
end

function gen3Body(stopCond=[10,100],numSteps=10000,initCond=nothing) #default stop conditions of 10 yrs and 100 AU sep
    tStop=stopCond[1]*365*24*3600 #convert to SI s
    sepStop=stopCond[2]*1.5e11 #convert to SI m
    stop=false
    currentT=0
    t=range(0,stop=tStop,length=(numSteps+1)) #+1 because I don't want 0 to count
    stepSize=tStop/numSteps
    x1=zeros(length(t))
    y1=zeros(length(t))
    x2=zeros(length(t))
    y2=zeros(length(t))
    x3=zeros(length(t))
    y3=zeros(length(t))
    if(initCond !== nothing)
        r,m,rad=initCond

        # println("r: $r")
        # println("n: $m")
        # println("rad: $rad")

        # open("initCond.txt","w") do f #save initial conditions to file in folder where script is run
        #     write(f,"preset: m1=$(@sprintf("%.1f",(m[1]/2e30))) m2=$(@sprintf("%.1f",(m[2]/2e30))) m3=$(@sprintf("%.1f",(m[3]/2e30))) (solar masses)\nv1x=$(r[7]/1e3) v1y=$(r[8]/1e3) v2x=$(r[9]/1e3) v2y=$(r[10]/1e3) v3x=$(r[11]/1e3) v3y=$(r[12]/1e3) (km/s)\nx1=$(r[1]/1.5e11) y1=$(r[2]/1.5e11) x2=$(r[3]/1.5e11) y2=$(r[4]/1.5e11) x3=$(r[5]/1.5e11) y3=$(r[6]/1.5e11) (AU from center)")
        # end
    else
        r,rad,m=initCondGen()

        # println("r: $r")
        # println("n: $m")
        # println("rad: $rad")
    end
    initV=copy(r[7:end])
    min12=rad[1]+rad[2]
    min13=rad[1]+rad[3]
    min23=rad[2]+rad[3]
    i=1
    stopT=maximum(t)
    collisionBool=false; collisionInds=[0,0]
    #implement RK4 to model solutions to differential equations
    while stop==false
        if currentT==stopT || currentT>stopT #in case of rounding error or something
            stop=true
        elseif i>(numSteps+1) #inf loop failsafe
            stop=true
            println("error: shouldn't have gotten here")
        else
            x1[i]=r[1] #store current positions
            y1[i]=r[2]
            x2[i]=r[3]
            y2[i]=r[4]
            x3[i]=r[5]
            y3[i]=r[6]

            k1=stepSize*dR(r,m,energyBool=1)
            k2=stepSize*dR(r.+0.5.*k1,m)
            k3=stepSize*dR(r.+0.5.*k2,m)
            k4=stepSize*dR(r.+k3,m)
            r+=(k1.+2.0*k2.+2.0.*k3.+k4)./6 #RK4 update to positions, velocities

            #check separation after each dt step
            sep12=sqrt((x1[i]-x2[i])^2+(y1[i]-y2[i])^2)
            sep13=sqrt((x1[i]-x3[i])^2+(y1[i]-y3[i])^2)
            sep23=sqrt((x3[i]-x2[i])^2+(y3[i]-y2[i])^2)

            if sep12<min12 || sep13<min13 || sep23<min23 || sep12>sepStop || sep13>sepStop || sep23>sepStop
                if sep12<min12 || sep13<min13 || sep23<min23
                    collisionBool=true
                    if sep12<min12
                        collisionInds=[1,2]
                    elseif sep13<min13
                        collisionInds=[1,3]
                    elseif sep23<min23
                        collisionInds=[2,3]
                    end
                else
                    collisionBool=false
                end
                stop=true #stop if collision happens or body is ejected
                t=range(0,stop=currentT,length=i) #t should match pos vectors
                x1=x1[1:i] #don't want trailing zeros
                y1=y1[1:i]
                x2=x2[1:i]
                y2=y2[1:i]
                x3=x3[1:i]
                y3=y3[1:i]
            end
            i+=1
            currentT+=stepSize #next step
        end
    end
    return [x1,y1,x2,y2,x3,y3], t, m, rad, collisionBool, collisionInds, initV
end

function getInteresting3Body(minTime=0, initCond=nothing) #in years, defaults to 0
    #sometimes (most of the time) random conditions result in a really short animation where things
    #just crash into each other/fly away, so this function throws away those
    yearSec=365*24*3600
    interesting=false
    i=1
    while interesting==false
        global energy=[] #re-initialize empty energy array
        plotData,t,m,rad,collisionBool,collisionInds,initV=gen3Body([60,200],600000, initCond)
        if initCond !== nothing  #if we are setting the valuas manually, don't log
            println(maximum(t)/yearSec) #tell me how many years we are simulating
            return plotData,t,m,rad,collisionBool,collisionInds
            interesting=true
        elseif (maximum(t)/yearSec)>minTime #only return if simulation runs for longer than minTime
            println(maximum(t)/yearSec) #tell me how many years we are simulating
            open("cron_log.txt","a") do f #for cron logging, a flag = append
                write(f,"$(maximum(t)/yearSec)\n")
            end
            open("3BodyStats.txt","a") do f #for stats logging
                initPos=[plotData[1][1],plotData[2][1],plotData[3][1],plotData[4][1],plotData[5][1],plotData[6][1]]./1.5e11 #AU
                write(f,"$(today()),$(maximum(t)/yearSec),$(m[1]/2e30),$(m[2]/2e30),$(m[3]/2e30),$(rad[1]/7e8),$(rad[2]/7e8),$(rad[3]/7e8),$collisionBool,$(collisionInds[1]),$(collisionInds[2]),$(initPos[1]),$(initPos[2]),$(initPos[3]),$(initPos[4]),$(initPos[5]),$(initPos[6]),$(initV[1]/1e3),$(initV[2]/1e3),$(initV[3]/1e3),$(initV[4]/1e3),$(initV[5]/1e3),$(initV[6]/1e3),$i\n")
            end
            return plotData,t,m,rad,collisionBool,collisionInds
            interesting=true
        elseif i>1999 #computationally expensive so don't want to go forever
            interesting=true #render it anyways I guess because sometimes it's fun?
            println("did not find interesting solution in number of tries allotted, running anyways")
            println(maximum(t)/yearSec) #how many years simulation runs for
            open("cron_log.txt","a") do f #for cron logging
                write(f,"found a solution with t = $(maximum(t)/yearSec) in $i iterations\n")
            end
            open("3BodyStats.txt","a") do f #log stats, see "3BodyAnalysis.ipynb"
                initPos=[plotData[1][1],plotData[2][1],plotData[3][1],plotData[4][1],plotData[5][1],plotData[6][1]]./1.5e11 #AU
                write(f,"$(today()),$(maximum(t)/yearSec),$(m[1]/2e30),$(m[2]/2e30),$(m[3]/2e30),$(rad[1]/7e8),$(rad[2]/7e8),$(rad[3]/7e8),$collisionBool,$(collisionInds[1]),$(collisionInds[2]),$(initPos[1]),$(initPos[2]),$(initPos[3]),$(initPos[4]),$(initPos[5]),$(initPos[6]),$(initV[1]/1e3),$(initV[2]/1e3),$(initV[3]/1e3),$(initV[4]/1e3),$(initV[5]/1e3),$(initV[6]/1e3),$i\n")
            end
            return plotData,t,m,rad,collisionBool,collisionInds
        end
        i+=1
    end
end

function detectOrbiting(d1_2,d1_3,d2_3,m,x,y) #determines if 2 bodies are orbiting, so we should use their center of mass for frame calculation
    if d1_2/d2_3 > 2 && d1_3/d2_3 > 2 #objects 2 and 3 are orbiting?
        orbiting=23
        cmX=(m[2]*x[2]+m[3]*x[3])/(m[2]+m[3]) #get centers of mass to use in limit calculations to prevent oscillations
        cmY=(m[2]*y[2]+m[3]*y[3])/(m[2]+m[3])
        xNew=[x[1],cmX]
        yNew=[y[1],cmY]
        return orbiting,xNew,yNew
    elseif d2_3/d1_2 > 2 && d1_3/d1_2 > 2 #objects 2 and 1 are orbiting?
        orbiting=21
        cmX=(m[2]*x[2]+m[1]*x[1])/(m[2]+m[1]) #get centers of mass
        cmY=(m[2]*y[2]+m[1]*y[1])/(m[2]+m[1])
        xNew=[x[3],cmX]
        yNew=[y[3],cmY]
        return orbiting,xNew,yNew
    elseif d1_2/d1_3 > 2 && d2_3/d1_3 > 2 #objects 1 and 3 are orbiting?
        orbiting=13
        cmX=(m[1]*x[1]+m[3]*x[3])/(m[1]+m[3]) #get centers of mass
        cmY=(m[1]*y[1]+m[3]*y[3])/(m[1]+m[3])
        xNew=[x[2],cmX]
        yNew=[y[2],cmY]
        return orbiting,xNew,yNew
    else #no pairs orbiting
        return 0,x,y
    end
end

function getLims(xNew,yNew,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #computes limits given padding and offsets
    cX,cY=sum(xNew)/length(xNew),sum(yNew)/length(yNew) #next thing to change..?
    dx=maximum(xNew)-minimum(xNew); dy=maximum(yNew)-minimum(yNew)
    dF = dx<dy ? dy : dx #do we use dx or dy for the frame?
    xlims=[(cX+ΔCx)-padding-dF/2+ΔL,(cX+ΔCx)+padding+dF/2+ΔR]
    ylims=[(cY+ΔCy)-padding-dF/2+ΔD,(cY+ΔCy)+padding+dF/2+ΔU]
    return xlims,ylims
end

function getΔC(target,start,pos,extraDx,extraDy,x,y,padding,tol=0.0001,maxIter=100000) #find center shifts brute force
    targCx,targCy,targxlims,targylims = target #these are the "old" limits we want offset to
    cx,cy = start #from new limits
    ΔCx,ΔCy = cx-targCx,cy-targCy #initial "guess"
    diffxList = [0.,0.,0.]; diffyList = [0.,0.,0.]
    xtargList = [0.,0.,0.]; ytargList = [0.,0.,0.]
    ΔL,ΔR = extraDx; ΔU,ΔD = extraDy
    xlims,ylims = getLims(x,y,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD)
    diff(r,rTarg)=abs(r-rTarg)
    for i = 1:length(pos)
        Bx,By = pos[i]
        rx,ry = relative(xlims,ylims,Bx,By) #where is it in terms of the frame width?
        rxTarg,ryTarg = relative(targxlims,targylims,Bx,By) #where was it? (where it should be)
        xtargList[i] = rxTarg; ytargList[i] = ryTarg
        diffxList[i] = diff(rx,rxTarg); diffyList[i] = diff(ry,ryTarg)
    end
    diffx,xInd = findmax(diffxList); diffy,yInd = findmax(diffyList) #find the one that shifted the most
    Bx = pos[xInd][1]; By = pos[yInd][2]
    rxTarg = xtargList[xInd]; ryTarg = ytargList[yInd]
    if diffx<tol && diffy<tol #not that big a difference, just return the initial guess
        return xlims,ylims,ΔCx,ΔCy
    else
        dx = targxlims[2]-targxlims[1]; dy = targylims[2]-targylims[1]
        function getDir(targxlims,targylims,ΔCx,ΔCy,tol,dx,dy) #figure out which way we need to shift
            acceptXDir = false; acceptYDir = false
            signdx = diffx<tol ? 0 : -1; signdy = diffy<tol ? 0 : -1
            counter = 1
            while acceptXDir == false || acceptYDir == false
                if acceptXDir == false
                    signdx=signdx^counter #flip the direction if what we tried before didn't work
                end
                if acceptYDir == false
                    signdy=signdy^counter
                end
                guessX = ΔCx+signdx*dx*tol/10; guessY = ΔCy+signdy*dy*tol/10
                xlims,ylims = getLims(x,y,padding,guessX,guessY,ΔL,ΔR,ΔU,ΔD)
                rx,ry = relative(xlims,ylims,Bx,By)
                newDiffx=diff(rx,rxTarg); newDiffy=diff(ry,ryTarg)
                acceptXDir = newDiffx<=diffx; acceptYDir = newDiffy<=diffy #are we going in the right direction?
                if counter>2
                    println("PROBLEM: changing center sign has no effect")
                    break
                end
                counter+=1
            end
            return signdx,signdy,guessX,guessY
        end
        signdx,signdy,guessX,guessY = getDir(targxlims,targylims,ΔCx,ΔCy,tol,dx,dy)
        counter = 2
        stopX = false; stopY = false
        while diffx>tol || diffy>tol #keep guessing in the right direction until we're within the tolerance
            #this could actaully probably just be calculated? whatever "if it ain't broke don't fix it" and
            #this is not high performance code
            if stopX == false
                guessX = ΔCx+signdx*dx*tol/10*counter
            end
            if stopY == false
                guessY = ΔCy+signdy*dy*tol/10*counter
            end
            xlims,ylims = getLims(x,y,padding,guessX,guessY,ΔL,ΔR,ΔU,ΔD)
            rx,ry = relative(xlims,ylims,Bx,By)
            diffx=diff(rx,rxTarg); diffy=diff(ry,ryTarg)
            stopX = diffx<tol; stopY = diffy<tol #compare new guesses and see if we should stop guessing
            counter+=1
            if counter == maxIter
                println("PROBLEM: did not converge in $maxIter iterations")
                break
            end
        end
        return xlims,ylims,guessX,guessY
    end
end

relative(xlims,ylims,x,y)=(x-xlims[1])/(xlims[2]-xlims[1]),(y-ylims[1])/(ylims[2]-ylims[1]) #returns position in terms of frame widths
center(xy) = sum(xy)/length(xy)

function comparePos(orbitOld,orbiting,m,x,y,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #try to prevent jumps when switching modes
    orbitStr = orbiting!=0 ? string(orbiting) : string(orbitOld) #was the old one orbiting or is this one orbiting?
    i1,i2=parse(Int64,string(orbitStr[1])),parse(Int64,string(orbitStr[2])) #indices of two orbiting bodies
    inds = [1,2,3]; otherInd = 0 #trying to make generalization to n-bodies easier
    for i = 1:length(inds)
        if inds[i] != i1 && inds[i] != i2
            otherInd = i
        end
    end
    cmX=(m[i1]*x[i1]+m[i2]*x[i2])/(m[i1]+m[i2]) #get centers of mass
    cmY=(m[i1]*y[i1]+m[i2]*y[i2])/(m[i1]+m[i2])
    xNew=[x[otherInd],cmX] #new is a bit of a misnomer because sometimes it's old
    yNew=[y[otherInd],cmY] #point is we use this one for orbiting, the other for not
    xlimsOrbit,ylimsOrbit=getLims(xNew,yNew,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD)
    xlimsNorm,ylimsNorm=getLims(x,y,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD)
    function getOld(orbitOld,xlimsOrbit,ylimsOrbit,xlimsNorm,ylimsNorm,x,y,xNew,yNew) #which one was the old one?
        if orbitOld != 0 #thing was orbiting
            oldxlims,oldylims = xlimsOrbit,ylimsOrbit
            oldCx,oldCy = center(xNew),center(yNew)
            return oldxlims,oldylims,oldCx,oldCy
        else
            oldxlims,oldylims = xlimsNorm,ylimsNorm
            oldCx,oldCy = center(x),center(y)
            return oldxlims,oldylims,oldCx,oldCy
        end
    end
    oldxlims,oldylims,oldCx,oldCy = getOld(orbitOld,xlimsOrbit,ylimsOrbit,xlimsNorm,ylimsNorm,x,y,xNew,yNew)
    if orbiting != 0 #transitioning to orbiting, frame instantaneously wants to shrink
        cx = center(xNew); cy = center(yNew)
        ΔL = oldxlims[1]-xlimsOrbit[1]; ΔR = oldxlims[2]-xlimsOrbit[2]
        ΔU = oldylims[2]-ylimsOrbit[2]; ΔD = oldylims[1]-ylimsOrbit[1]
        extraDx = [ΔL,ΔR]; extraDy = [ΔU,ΔD]
        xlims,ylims,ΔCx,ΔCy = getΔC([oldCx,oldCy,oldxlims,oldylims],[cx,cy],[[x[1],y[1]],[x[2],y[2]],[x[3],y[3]]],extraDx,extraDy,xNew,yNew,padding)
        return xlims,ylims,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD
    else #transitioning from orbiting, frame instantaneously wants to expand
        cx = center(x); cy = center(y)
        ΔL = oldxlims[1]-xlimsNorm[1]; ΔR = oldxlims[2]-xlimsNorm[2]
        ΔU = oldylims[2]-ylimsNorm[2]; ΔD = oldylims[1]-ylimsNorm[1]
        extraDx = [ΔL,ΔR]; extraDy = [ΔU,ΔD]
        xlims,ylims,ΔCx,ΔCy = getΔC([oldCx,oldCy,oldxlims,oldylims],[cx,cy],[[x[1],y[1]],[x[2],y[2]],[x[3],y[3]]],extraDx,extraDy,x,y,padding)
        return xlims,ylims,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD
    end
end

function computeLimits(pos,posFuture,padding,m,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #determines plot limits at each frame, padding in units of pos
    x=[pos[1],pos[3],pos[5]]
    y=[pos[2],pos[4],pos[6]]
    d1_2=sqrt((x[1]-x[2])^2 + (y[1]-y[2])^2)
    d1_3=sqrt((x[1]-x[3])^2 + (y[1]-y[3])^2)
    d2_3=sqrt((x[2]-x[3])^2 + (y[2]-y[3])^2)
    orbiting,xNew,yNew = detectOrbiting(d1_2,d1_3,d2_3,m,x,y) #are they orbiting?
    if orbiting != orbitOld #are we switching modes?
        xlims,ylims,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD = comparePos(orbitOld,orbiting,m,x,y,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD)
    else #"slowly" make the offsets go to zero, produces a nice smooth camera motion as we adjust
        relax = 0.95
        ΔCx*=relax;ΔCy*=relax;ΔL*=relax;ΔR*=relax;ΔU*=relax;ΔD*=relax
        xlims,ylims = getLims(xNew,yNew,padding,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD)
    end
    cNew = [(xlims[2]-xlims[1])/2+xlims[1],(ylims[2]-ylims[1])/2+ylims[1]] #dx+min(x); dy+min(y)
    return xlims,ylims,cNew,orbiting,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD
end

function getColors(m,c) #places colors of objects according to mass/size
    #c=[:biggest,:medium,:smallest] (order of input colors)
    maxM=maximum(m)
    minM=minimum(m)
    colors=[:blue,:blue,:blue] #testing
    if m[1]==maxM
        colors[1]=c[1]
        if m[2]==minM
            colors[2]=c[3]
            colors[3]=c[2]
        else
            colors[3]=c[3]
            colors[2]=c[2]
        end
    elseif m[2]==maxM
        colors[2]=c[1]
        if m[1]==minM
            colors[1]=c[3]
            colors[3]=c[2]
        else
            colors[3]=c[3]
            colors[1]=c[2]
        end
    else
        colors[3]=c[1]
        if m[1]==minM
            colors[1]=c[3]
            colors[2]=c[2]
        else
            colors[2]=c[3]
            colors[1]=c[2]
        end
    end
    return colors
end

function makeCircleVals(r,center=[0,0]) #makes circle values for the stars to plot
    xOffset=center[1]
    yOffset=center[2]
    xVals=[r*cos(i)+xOffset for i=0:(pi/64):(2*pi)]
    yVals=[r*sin(i)+yOffset for i=0:(pi/64):(2*pi)]
    return xVals,yVals
end

function saveFrames(plotData,t,m,rad,collisionBool,collisionInds)
    c=[:DodgerBlue,:Gold,:Tomato] #most massive to least massive, also roughly corresponds to temp
    colors=getColors(m,c)
    #adding fake stars
    numStars=2500
    starsX=zeros(numStars)
    starsY=zeros(numStars)
    for i=1:numStars
        num=rand(-200:200,2) #box size is 70 AU but we need some extra padding for movement
        starsX[i]=num[1]
        starsY[i]=num[2]
    end

    function getRatioRight(ratio,dx,dy) #makes sure the frame matches the ratio we want (for Twitter, square)
        if (dx/dy)!=ratio
            if dx>(ratio*dy)
                dy=dx/ratio
            else
                dx=dy*ratio
            end
        end
        return dx,dy
    end

    function relative(p::Plots.Subplot, rx, ry) #so I can plot in relative to parent
       xlims=Plots.xlims(p)
       ylims=Plots.ylims(p)
       return xlims[1]+rx*(xlims[2]-xlims[1]), ylims[1]+ry*(ylims[2]-ylims[1])
    end

    frameNum=1 #initialize frame counter
    stop=length(t) #what index the end is
    if collisionBool==true
        stop=length(t)-600 #-600 because we want to do the last 600 frames in slo-mo
    end
    #initialize a bunch of other things we'll need
    listInd=0
    limList=[]
    ratio=1
    offsetX = [0.,0.]; offsetY = [0.,0.]
    orbitOld = 0
    center = [0.,0.]; vel = [0.,0.]
    ΔCx = 0.;ΔCy = 0.;ΔL = 0.;ΔR = 0.;ΔU = 0.;ΔD = 0.
    println("energy loss = $((energy[end]-energy[1])/energy[1]*100) %") #anecdotally this is usually very small, but occasionally gets as high as ~1%
    for i=1:333:stop #this makes animation scale ~1 sec/year with other conditions
        GR.inline("png") #added to eneable cron/jobber compatibility, also this makes frames generate WAY faster? Prior to adding this when run from cron/jobber frames would stop generating at 408 for some reason.
        gr(legendfontcolor = plot_color(:white)) #legendfontcolor=:white plot arg broken right now (at least in this backend)
        print("$(@sprintf("%.2f",i/length(t)*100)) % complete\r") #output percent tracker
        pos=[plotData[1][i],plotData[2][i],plotData[3][i],plotData[4][i],plotData[5][i],plotData[6][i]] #current pos
        future = i+500<stop ? i+500 : i #make sure we don't go past end of data
        posFuture=[plotData[1][future],plotData[2][future],plotData[3][future],plotData[4][future],plotData[5][future],plotData[6][future]] #future pos
        limx,limy,center,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD=computeLimits(pos./1.5e11,posFuture./1.5e11,15,m,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #compute limits in AU, 15 AU padding
        dx,dy=(limx[2]-limx[1]),(limy[2]-limy[1])
        dx,dy=getRatioRight(ratio,dx,dy) #check ratio
        if listInd>1
            oldLimx,oldLimy=limList[listInd][1],limList[listInd][2]
            oldDx,oldDy=oldLimx[2]-oldLimx[1],oldLimy[2]-oldLimy[1]
            maxContraction=0.98; maxExpansion=1.02
            if dx/oldDx<maxContraction #frame shrunk more than x%
                limx[1]=center[1]-oldDx*maxContraction/2
                limx[2]=center[1]+oldDx*maxContraction/2
                limy[1]=center[2]-oldDx*maxContraction/2
                limy[2]=center[2]+oldDx*maxContraction/2
            elseif dx/oldDx>maxExpansion #grew more than x%
                limx[1]=center[1]-oldDx*maxExpansion/2
                limx[2]=center[1]+oldDx*maxExpansion/2
                limy[1]=center[2]-oldDx*maxExpansion/2
                limy[2]=center[2]+oldDx*maxExpansion/2
            elseif dy/oldDy<maxContraction #shrunk more than y%
                limx[1]=center[1]-oldDy*maxContraction/2
                limx[2]=center[1]+oldDy*maxContraction/2
                limy[1]=center[2]-oldDy*maxContraction/2
                limy[2]=center[2]+oldDy*maxContraction/2
            elseif dy/oldDy>maxExpansion #grew more than y%
                limx[1]=center[1]-oldDy*maxExpansion/2
                limx[2]=center[1]+oldDy*maxExpansion/2
                limy[1]=center[2]-oldDy*maxExpansion/2
                limy[2]=center[2]+oldDy*maxExpansion/2
            end
        end
        listInd+=1
        dx,dy=(limx[2]-limx[1]),(limy[2]-limy[1])
        dx,dy=getRatioRight(ratio,dx,dy) #check again
        limx = [center[1]-dx/2,center[1]+dx/2]; limy = [center[2]-dy/2,center[2]+dy/2]
        push!(limList,[limx,limy]) #record limits for later use, push! is bad and we should just preallocate this but whatever
        p=plot(plotData[1][1:33:i]./1.5e11,plotData[2][1:33:i]./1.5e11,label="",linewidth=2,linecolor=colors[1],linealpha=max.((1:33:i) .+ 10000 .- i,2500)/10000) #plot orbits up to i
        p=plot!(plotData[3][1:33:i]./1.5e11,plotData[4][1:33:i]./1.5e11,label="",linewidth=2,linecolor=colors[2],linealpha=max.((1:33:i) .+ 10000 .- i,2500)/10000) #linealpha argument causes lines to decay
        p=plot!(plotData[5][1:33:i]./1.5e11,plotData[6][1:33:i]./1.5e11,label="",linewidth=2,linecolor=colors[3],linealpha=max.((1:33:i) .+ 10000 .- i,2500)/10000) #example: alpha=max.((1:i) .+ 100 .- i,0) causes only last 100 to be visible
        p=scatter!(starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
        star1=makeCircleVals(rad[1],[plotData[1][i],plotData[2][i]]) #generate circles with appropriate sizes for each star
        star2=makeCircleVals(rad[2],[plotData[3][i],plotData[4][i]]) #at current positions
        star3=makeCircleVals(rad[3],[plotData[5][i],plotData[6][i]])
        p=plot!(star1[1]./1.5e11,star1[2]./1.5e11,label="$(@sprintf("%.1f", m[1]./2e30))",color=colors[1],fill=true) #plot star circles with labels
        p=plot!(star2[1]./1.5e11,star2[2]./1.5e11,label="$(@sprintf("%.1f", m[2]./2e30))",color=colors[2],fill=true)
        p=plot!(star3[1]./1.5e11,star3[2]./1.5e11,label="$(@sprintf("%.1f", m[3]./2e30))",color=colors[3],fill=true)
        p=plot!(background_color=:black,background_color_legend=:transparent,foreground_color_legend=:transparent,
            background_color_outside=:white,aspect_ratio=:equal,legendtitlefontcolor=:white,legendfontfamily="Courier") #formatting for plot frame
        p=plot!(xlabel="x: AU",ylabel="y: AU",title="Random Three-Body Problem\nt:      years after start",
            legend=:best,xaxis=("x: AU",(limx[1],limx[2]),font(9,"Courier")),yaxis=("y: AU",(limy[1],limy[2]),font(9,"Courier")),tickfontcolor=:white,
            grid=false,titlefont=font(14,"Courier"),size=(720,721),legendfontsize=8,legendtitle="Mass (in solar masses)",legendtitlefontsize=8,legendtitlefont="Courier") #add in axes/title/legend with formatting

        tX,tY=relative(p[1],0.3,1.044)#static coords for time relative to parent
        p = annotate!(tX,tY,Plots.text((@sprintf("%0.2f",t[i]/365/24/3600)),"Courier",14,"black"))
        png(p,@sprintf("tmpPlots/frame_%06d.png",frameNum))
        frameNum+=1
        closeall() #close plots
    end

    if collisionBool==true #this condition makes 2 seconds of slo-mo right before the collision
        println("making collision cam")
        for i=1:10:600
            GR.inline("png") #added to eneable cron/jobber compatibility, also this makes frames generate WAY faster? Prior to adding this when run from cron/jobber frames would stop generating at 408 for some reason.
            gr(legendfontcolor = plot_color(:white)) #legendfontcolor=:white plot arg broken right now (at least in this backend)
            print("$(@sprintf("%.2f",i/600*100)) % complete\r") #output percent tracker
            pos=[plotData[1][end-(600-i)],plotData[2][end-(600-i)],plotData[3][end-(600-i)],plotData[4][end-(600-i)],plotData[5][end-(600-i)],plotData[6][end-(600-i)]] #current pos
            posFuture=pos #don't need future position at end
            limx,limy,center,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD=computeLimits(pos./1.5e11,posFuture./1.5e11,15,m,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #convert to AU, 10 AU padding
            p=plot(plotData[1][1:33:end-(600-i)]./1.5e11,plotData[2][1:33:end-(600-i)]./1.5e11,label="",linecolor=colors[1],linewidth=2,linealpha=max.((1:33:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000) #plot orbits up to i
            p=plot!(plotData[3][1:33:end-(600-i)]./1.5e11,plotData[4][1:33:end-(600-i)]./1.5e11,label="",linecolor=colors[2],linewidth=2,linealpha=max.((1:33:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000) #linealpha argument causes lines to decay
            p=plot!(plotData[5][1:33:end-(600-i)]./1.5e11,plotData[6][1:33:end-(600-i)]./1.5e11,label="",linecolor=colors[3],linewidth=2,linealpha=max.((1:33:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000) #example: alpha=max.((1:i) .+ 100 .- i,0) causes only last 100 to be visible
            p=scatter!(starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
            star1=makeCircleVals(rad[1],[plotData[1][end-(600-i)],plotData[2][end-(600-i)]]) #generate circles with appropriate sizes for each star
            star2=makeCircleVals(rad[2],[plotData[3][end-(600-i)],plotData[4][end-(600-i)]]) #at current positions
            star3=makeCircleVals(rad[3],[plotData[5][end-(600-i)],plotData[6][end-(600-i)]])
            p=plot!(star1[1]./1.5e11,star1[2]./1.5e11,label="$(@sprintf("%.1f", m[1]./2e30))",color=colors[1],fill=true) #plot star circles with labels
            p=plot!(star2[1]./1.5e11,star2[2]./1.5e11,label="$(@sprintf("%.1f", m[2]./2e30))",color=colors[2],fill=true)
            p=plot!(star3[1]./1.5e11,star3[2]./1.5e11,label="$(@sprintf("%.1f", m[3]./2e30))",color=colors[3],fill=true)
            p=plot!(background_color=:black,background_color_legend=:transparent,foreground_color_legend=:transparent,
                background_color_outside=:white,aspect_ratio=:equal,legendtitlefontcolor=:white,legendfontfamily="Courier") #formatting for plot frame
            p=plot!(xlabel="x: AU",ylabel="y: AU",title="Random Three-Body Problem\nt: $(@sprintf("%0.2f",t[end-(600-i)]/365/24/3600)) years after start",
                legend=:best,xaxis=("x: AU",(limx[1],limx[2]),font(9,"Courier")),yaxis=("y: AU",(limy[1],limy[2]),font(9,"Courier")),
                grid=false,titlefont=font(14,"Courier"),size=(720,721),legendfontsize=8,legendtitle="Mass (in solar masses)",legendtitlefontsize=8,legendtitlefont="Courier") #add in axes/title/legend with formatting
            #collision cam zoom in
            i1,i2=collisionInds #these are the ones that are colliding, we use them to set the frame limits
            X=[plotData[1][end-(600-i)],plotData[3][end-(600-i)],plotData[5][end-(600-i)]]./1.5e11; Y=[plotData[2][end-(600-i)],plotData[4][end-(600-i)],plotData[6][end-(600-i)]]./1.5e11
            minX=(min(X[i1],X[i2])-1); maxX=(max(X[i1],X[i2])); minY=(min(Y[i1],Y[i2])-1); maxY=(max(Y[i1],Y[i2]))
            dx=maxX-minX; dy=maxY-minY
            dF=dx<dy ? dy : dx #use dy for frame if dx smaller, else dx

            #draw zoom box
            cornersX=[minX,minX+dF+1]; cornersY=[minY,minY+dF+1]
            p=plot!([cornersX[1],cornersX[2]],[cornersY[1],cornersY[1]],c=:white,label="") #side 1
            p=plot!([cornersX[2],cornersX[2]],[cornersY[1],cornersY[2]],c=:white,label="") #side 2
            p=plot!([cornersX[1],cornersX[2]],[cornersY[2],cornersY[2]],c=:white,label="") #side 3
            p=plot!([cornersX[1],cornersX[1]],[cornersY[1],cornersY[2]],c=:white,label="") #side 4
            offset = 0.0145 #for some reason the x corners don't quite match...
            s1x,s1y = relative(p[1],1/8-offset,7/8-0.25); s2x,s2y = relative(p[1],1/8+0.25-offset,7/8)
            subCornersX=[s1x,s2x]; subCornersY=[s1y,s2y] #physical coordinates, box in top left
            p=plot!([subCornersX[1],cornersX[1]],[subCornersY[1],cornersY[1]],c=:white,label = "") #corner 1 -> corner 1
            p=plot!([subCornersX[2],cornersX[2]],[subCornersY[2],cornersY[2]],c=:white,label = "") #corner 2 -> corner 2
            p=plot!([subCornersX[1],cornersX[1]],[subCornersY[2],cornersY[2]],c=:white,label = "") #corner 3 -> corner 3
            p=plot!([subCornersX[2],cornersX[2]],[subCornersY[1],cornersY[1]],c=:white,label = "") #corner 4 -> corner 4
            #draw box before plot so plot labels are on top
            p=plot!(title="COLLISION CAM\n(slo-mo x 33)",titlefontcolor=:orange,inset=(1,bbox(1/8,1/8,0.25,0.25)),
                    xlims=(minX,minX+dF+1),ylims=(minY,minY+dF+1),legend=:false,left_margin=0mm,right_margin=0mm,top_margin=0mm,bottom_margin=0mm,
                    foreground_color_border=:white,foreground_color_axis=:white,foreground_color_text=:white,grid=:false,
                    aspect_ratio=:equal,fontfamily=:Courier,subplot=2,framestyle=:box,titlefontsize=10,tickfontsize=6)
            p=plot!(p[2],plotData[1][1:10:end-(600-i)]./1.5e11,plotData[2][1:10:end-(600-i)]./1.5e11,label="",linecolor=colors[1],linewidth=2,linealpha=max.((1:10:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000)
            p=plot!(p[2],plotData[3][1:10:end-(600-i)]./1.5e11,plotData[4][1:10:end-(600-i)]./1.5e11,label="",linecolor=colors[2],linewidth=2,linealpha=max.((1:10:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000)
            p=plot!(p[2],plotData[5][1:10:end-(600-i)]./1.5e11,plotData[6][1:10:end-(600-i)]./1.5e11,label="",linecolor=colors[3],linewidth=2,linealpha=max.((1:10:(i+length(t)-600)) .+ 10000 .- (i+length(t)-600),2500)/10000)
            p=plot!(p[2],star1[1]./1.5e11,star1[2]./1.5e11,color=colors[1],fill=true)
            p=plot!(p[2],star2[1]./1.5e11,star2[2]./1.5e11,color=colors[2],fill=true)
            p=plot!(p[2],star3[1]./1.5e11,star3[2]./1.5e11,color=colors[3],fill=true)
            p=scatter!(p[2],starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
            #save frame
            png(p,@sprintf("tmpPlots/frame_%06d.png",frameNum))
            frameNum+=1
            closeall() #close plots
        end
        println("making freeze frame ending")
        for i=1:15 #make 0.5 s freeze frame ending
            GR.inline("png") #added to eneable cron/jobber compatibility, also this makes frames generate WAY faster? Prior to adding this when run from cron/jobber frames would stop generating at 408 for some reason.
            gr(legendfontcolor = plot_color(:white)) #legendfontcolor=:white plot arg broken right now (at least in this backend)
            print("$(@sprintf("%.2f",i/15*100)) % complete\r") #output percent tracker
            pos=[plotData[1][end],plotData[2][end],plotData[3][end],plotData[4][end],plotData[5][end],plotData[6][end]] #current pos
            posFuture=pos #don't need future position at end
            limx,limy,center,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD=computeLimits(pos./1.5e11,posFuture./1.5e11,15,m,orbitOld,ΔCx,ΔCy,ΔL,ΔR,ΔU,ΔD) #convert to AU, 10 AU padding
            p=plot(plotData[1][1:33:end]./1.5e11,plotData[2][1:33:end]./1.5e11,label="",linecolor=colors[1],linewidth=2,linealpha=max.((1:33:(length(t))) .+ 10000 .- (length(t)),2500)/10000) #plot orbits up to i
            p=plot!(plotData[3][1:33:end]./1.5e11,plotData[4][1:33:end]./1.5e11,label="",linecolor=colors[2],linewidth=2,linealpha=max.((1:33:(length(t))) .+ 10000 .- (length(t)),2500)/10000) #linealpha argument causes lines to decay
            p=plot!(plotData[5][1:33:end]./1.5e11,plotData[6][1:33:end]./1.5e11,label="",linecolor=colors[3],linewidth=2,linealpha=max.((1:33:(length(t))) .+ 10000 .- (length(t)),2500)/10000) #example: alpha=max.((1:i) .+ 100 .- i,0) causes only last 100 to be visible
            p=scatter!(starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
            star1=makeCircleVals(rad[1],[plotData[1][end],plotData[2][end]]) #generate circles with appropriate sizes for each star
            star2=makeCircleVals(rad[2],[plotData[3][end],plotData[4][end]]) #at current positions
            star3=makeCircleVals(rad[3],[plotData[5][end],plotData[6][end]])
            p=plot!(star1[1]./1.5e11,star1[2]./1.5e11,label="$(@sprintf("%.1f", m[1]./2e30))",color=colors[1],fill=true) #plot star circles with labels
            p=plot!(star2[1]./1.5e11,star2[2]./1.5e11,label="$(@sprintf("%.1f", m[2]./2e30))",color=colors[2],fill=true)
            p=plot!(star3[1]./1.5e11,star3[2]./1.5e11,label="$(@sprintf("%.1f", m[3]./2e30))",color=colors[3],fill=true)
            p=plot!(background_color=:black,background_color_legend=:transparent,foreground_color_legend=:transparent,
                background_color_outside=:white,aspect_ratio=:equal,legendtitlefontcolor=:white,legendfontfamily="Courier") #formatting for plot frame
            p=plot!(xlabel="x: AU",ylabel="y: AU",title="Random Three-Body Problem\nt: $(@sprintf("%0.2f",t[end]/365/24/3600)) years after start",
                legend=:best,xaxis=("x: AU",(limx[1],limx[2]),font(9,"Courier")),yaxis=("y: AU",(limy[1],limy[2]),font(9,"Courier")),
                grid=false,titlefont=font(14,"Courier"),size=(720,721),legendfontsize=8,legendtitle="Mass (in solar masses)",legendtitlefontsize=8,legendtitlefont="Courier") #add in axes/title/legend with formatting
            #collision cam zoom in
            i1,i2=collisionInds #these are the ones that are colliding, we use them to set the frame limits
            X=[plotData[1][end],plotData[3][end],plotData[5][end]]./1.5e11; Y=[plotData[2][end],plotData[4][end],plotData[6][end]]./1.5e11
            minX=(min(X[i1],X[i2])-1); maxX=(max(X[i1],X[i2])); minY=(min(Y[i1],Y[i2])-1); maxY=(max(Y[i1],Y[i2]))
            dx=maxX-minX; dy=maxY-minY
            dF=dx<dy ? dy : dx #use dy for frame if dx smaller, else dx

            p=plot!(title="COLLISION CAM\n(slo-mo x 33)",titlefontcolor=:orange,inset=(1,bbox(1/8,1/8,0.25,0.25)),
                    xlims=(minX,minX+dF+1),ylims=(minY,minY+dF+1),legend=:false,left_margin=0mm,right_margin=0mm,top_margin=0mm,bottom_margin=0mm,
                    foreground_color_border=:white,foreground_color_axis=:white,foreground_color_text=:white,grid=:false,
                    aspect_ratio=:equal,fontfamily=:Courier,subplot=2,framestyle=:box,titlefontsize=10,tickfontsize=6)
            p=plot!(p[2],plotData[1][1:10:end]./1.5e11,plotData[2][1:10:end]./1.5e11,label="",linecolor=colors[1],linewidth=2,linealpha=max.((1:10:(length(t))) .+ 10000 .- (length(t)),2500)/10000)
            p=plot!(p[2],plotData[3][1:10:end]./1.5e11,plotData[4][1:10:end]./1.5e11,label="",linecolor=colors[2],linewidth=2,linealpha=max.((1:10:(length(t))) .+ 10000 .- (length(t)),2500)/10000)
            p=plot!(p[2],plotData[5][1:10:end]./1.5e11,plotData[6][1:10:end]./1.5e11,label="",linecolor=colors[3],linewidth=2,linealpha=max.((1:10:(length(t))) .+ 10000 .- (length(t)),2500)/10000)
            p=plot!(p[2],star1[1]./1.5e11,star1[2]./1.5e11,color=colors[1],fill=true)
            p=plot!(p[2],star2[1]./1.5e11,star2[2]./1.5e11,color=colors[2],fill=true)
            p=plot!(p[2],star3[1]./1.5e11,star3[2]./1.5e11,color=colors[3],fill=true)
            p=scatter!(p[2],starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
            #draw zoom box
            cornersX=[minX,minX+dF+1]; cornersY=[minY,minY+dF+1]
            p=plot!([cornersX[1],cornersX[2]],[cornersY[1],cornersY[1]],c=:white,label="") #side 1
            p=plot!([cornersX[2],cornersX[2]],[cornersY[1],cornersY[2]],c=:white,label="") #side 2
            p=plot!([cornersX[1],cornersX[2]],[cornersY[2],cornersY[2]],c=:white,label="") #side 3
            p=plot!([cornersX[1],cornersX[1]],[cornersY[1],cornersY[2]],c=:white,label="") #side 4
            offset = 0.0145 #for some reason the x corners don't quite match...
            s1x,s1y = relative(p[1],1/8-offset,7/8-0.25); s2x,s2y = relative(p[1],1/8+0.25-offset,7/8)
            subCornersX=[s1x,s2x]; subCornersY=[s1y,s2y] #physical coordinates, box in top left
            p=plot!([subCornersX[1],cornersX[1]],[subCornersY[1],cornersY[1]],c=:white,label = "") #corner 1 -> corner 1
            p=plot!([subCornersX[2],cornersX[2]],[subCornersY[2],cornersY[2]],c=:white,label = "") #corner 2 -> corner 2
            p=plot!([subCornersX[1],cornersX[1]],[subCornersY[2],cornersY[2]],c=:white,label = "") #corner 3 -> corner 3
            p=plot!([subCornersX[2],cornersX[2]],[subCornersY[1],cornersY[1]],c=:white,label = "") #corner 4 -> corner 4
            #save frame
            png(p,@sprintf("tmpPlots/frame_%06d.png",frameNum))
            frameNum+=1
            closeall() #close plots
        end
    end
end

function doOne()
    println("sit tight -- finding an interesting solution")

    #v1x=-1.673 v1y=-4.038 v2x=-4.276 v2y=6.851 v3x=2.399 v3y=0.942 (km/s)
    #x1=-7 y1=-11 x2=8 y2=-13 x3=19 y3=4 (AU from center)
    r=[-7,-11,8,-13,19,4,-1.673 * 1e3,-4.038 * 1e3,-4.276 * 1e3,6.851 * 1e3,2.399 * 1e3, 0.942 * 1e3]
    #m1=84.2 m2=26.9 m3=58.0 (solar masses)
    m=[84.2, 26.9, 58.0]
    rad=m.^0.8 #3 radii based on masses in solar units
    m=m.*2e30 #convert to SI kg
    rad=rad.*7e8 #convert to SI m

    initCond = [r,m,rad]
    initCond = nothing

    plotData,t,m,rad,collisionBool,collisionInds=getInteresting3Body(15, initCond) #find an interesting solution at least 15 years
    if collisionBool == true
        println("collision! inds = $collisionInds")
    else
        println("no collision")
    end

    #saveFrames(plotData,t,m,rad,collisionBool,collisionInds)

end

function mapCollisions()
    yearSec=365*24*3600

    xres = 800
    yres = 800
    xmin = -80
    xmax = 80
    ymin = -80
    ymax = 80

    data = rand(xres,yres)

    dfdata = DataFrame(xPos = Number[], yPos = Number[], timeToCrash = Number[])

    #Optional, preload dataframe from where we last left off
    #old_dfdata = DataFrame(CSV.File("tmpPlots/tmpSeparatedData_300y.csv"))
	old_dfdata = DataFrame(xPos = [], yPos= [])

    xvals = collect(1:xres) /  xres * (xmax - xmin) .+ xmin
    yvals = collect(1:yres) / yres * (ymax - ymin) .+ ymin

    for i = 1:xres
        x = xvals[i]
        for j = 1:yres
            y = yvals[j]
            #x1=-7 y1=-11 x2=8 y2=-13 x3=19 y3=4 (AU from center) #v1x=-1.673 v1y=-4.038 v2x=-4.276 v2y=6.851 v3x=2.399 v3y=0.942 (km/s)
            r=[-7*1.5e11,-11*1.5e11,8*1.5e11,-13*1.5e11,19*1.5e11,4*1.5e11,
                -1.673 * 1e3,-4.038 * 1e3,-4.276 * 1e3,6.851 * 1e3,2.399 * 1e3, 0.942 * 1e3]
            #m1=84.2 m2=26.9 m3=58.0 (solar masses)
            m=[84.2, 26.9, 58.0]

            rad=m.^0.8 #3 radii based on masses in solar units
            m=m.*2e30 #convert to SI kg
            rad=rad.*7e8 #convert to SI m

            #r[1] = (i / xres * (xmax - xmin) + xmin) * 1.5e11
            #r[2] = (j / yres * (ymax - ymin) + ymin) * 1.5e11

            r[1] = x * 1.5e11
            r[2] = y * 1.5e11

            println("starting coordinates: $x, $y")
            #println(r[1])
            #println(r[2])

            #Check if data for this point already exists (loading previous progress)
            alreadyexisting = @subset(old_dfdata, :xPos .== x, :yPos .== y)
            if nrow(alreadyexisting) > 0
                println("already existing data loaded")
                time = alreadyexisting[1, :timeToCrash]
                if time == -30
                    collisionBool = false
                else
                    collisionBool = true
                end
            else
                initCond = [r,m,rad]
                plotData,t,m,rad,collisionBool,collisionInds=getInteresting3Body(15, initCond) #find an interesting solution at least 15 years
                time = maximum(t)/yearSec
            end

            if collisionBool == true
                println("***********************")
                println("collision!")
                println("***********************")
                push!(dfdata, (x, y, time))
                data[i, j] = time
            else
                println("no collision, time $time")
                push!(dfdata, (x, y, -30))
                data[i, j] = -30
            end

        end
        CSV.write("tmpPlots/tmpSeparatedData.csv", dfdata)
    end

    CSV.write("tmpPlots/separatedData.csv", dfdata)

    df = DataFrame(data, :auto)
    open("tmpPlots/row-col-vals.txt","w") do f #save initial conditions to file in folder where script is run
        write(f,"rownames=$yvals \ncolvals=$xvals")
    end
    CSV.write("tmpPlots/heatmapdata.csv", df)

    gr()
    p=heatmap(xvals,
        yvals, data,
        c=cgrad([:blue, :white,:red, :yellow]),
        xlabel="x values", ylabel="y values",
        title="My title")

    png(p,@sprintf("tmpPlots/heatmap.png"))

end

function main() #pulls everything together, speeds things up to put everything in a function + gets rid of bad global syntax
    #doOne()

    mapCollisions()

end

#this is a function that will generate the animation for you without having to use the command line, works on Linux and Windows (run as administrator), untested on macOS
function makeAnim(clean=true)
    run(`ffmpeg -framerate 30 -i "tmpPlots/frame_%06d.png" -c:v libx264 -preset slow -coder 1 -movflags +faststart -g 15 -crf 18 -pix_fmt yuv420p -profile:v high -y -bf 2 -vf "scale=720:720,setdar=1/1" "threeBody.mp4"`)
    if clean==true
        println("cleaning up png files")
        foreach(rm,[string("tmpPlots/",x) for x in filter(endswith(".png"),readdir("tmpPlots"))])
    end
end

#generate frames!
main()

#generate the animation!
#makeAnim() #commented out because I compile the frames in the shell script (see 3BodyShell.sh)

#you'll want to uncomment the line above to tell the script to just make the animation
#or you can load the script into a julia terminal with include("threeBodyProb.jl")
#and then call main() and makeAnim() to your heart's content

#OLD STUFF....

#threeBodyFile="3Body_fps30.mp4"

#crf is compression value (17 or 18 "visually lossless"), pix_fmt is for twitter specific vid req, -b:v specifies target bitrate, -vcodec specifies codec (h264 in this case) -y says overwrite existing file
#run( `ffmpeg -framerate 30 -i $plotLoadPath"%06d.png" -vcodec libx264 -pix_fmt yuv420p -profile:v high -b:v 2048K -y -vf "scale=720:720,setdar=1/1" $threeBodyFile` ) #-vf scale=720:72 -crf 25
#run( `ffmpeg -framerate 30 -i $plotLoadPath"%06d.png" -c:v libx264 -preset slow -coder 1 -movflags +faststart -g 15 -crf 18 -pix_fmt yuv420p -profile:v high -y -bf 2 -vf "scale=720:720,setdar=1/1" $threeBodyFile` ) #all this bullshit to hopefully satisfy twitter requirements

#NOTE: moved ffmpeg commands to shell script

#old (simpler) way of generating animation
#uncomment and use this way if you just want a simple animation saved and don't
#care about performance/specific formatting of video.

# threeBodyAnim=@animate for i=1:length(t)
#     gr(legendfontcolor = plot_color(:white)) #plot arg broken right now in Julia
#     print("$(@sprintf("%.2f",i/length(t)*100)) % complete\r") #output percent tracker
#     pos=[plotData[1][i],plotData[2][i],plotData[3][i],plotData[4][i],plotData[5][i],plotData[6][i]] #current pos
#     limx,limy,center=getLims(pos./1.5e11,5) #convert to AU, 5 AU padding
#     plot(plotData[1][1:i]./1.5e11,plotData[2][1:i]./1.5e11,label="",linecolor=colors[1])
#     plot!(plotData[3][1:i]./1.5e11,plotData[4][1:i]./1.5e11,label="",linecolor=colors[2])
#     plot!(plotData[5][1:i]./1.5e11,plotData[6][1:i]./1.5e11,label="",linecolor=colors[3])
#     scatter!(starsX,starsY,markercolor=:white,markersize=:1,label="") #fake background stars
#     star1=makeCircleVals(rad[1],[plotData[1][i],plotData[2][i]])
#     star2=makeCircleVals(rad[2],[plotData[3][i],plotData[4][i]])
#     star3=makeCircleVals(rad[3],[plotData[5][i],plotData[6][i]])
#     plot!(star1[1]./1.5e11,star1[2]./1.5e11,label="$(@sprintf("%.1f", m[1]./2e30))",color=colors[1],fill=true)
#     plot!(star2[1]./1.5e11,star2[2]./1.5e11,label="$(@sprintf("%.1f", m[2]./2e30))",color=colors[2],fill=true)
#     plot!(star3[1]./1.5e11,star3[2]./1.5e11,label="$(@sprintf("%.1f", m[3]./2e30))",color=colors[3],fill=true)
#     plot!(background_color=:black,background_color_legend=:transparent,background_color_outside=:white,aspect_ratio=:equal,legendtitlefontcolor=:white) #legendfontcolor=:white
#     plot!(xlabel="x: AU",ylabel="y: AU",title="Random Three Body Problem\nt: $(@sprintf("%0.2f",t[i]/365/24/3600)) yrs after start",
#         legend=:best,xaxis=("x: AU",(limx[1],limx[2]),font(12,"Courier")),yaxis=("y: AU",(limy[1],limy[2]),font(12,"Courier")),
#         grid=false,titlefont=font(24,"Courier"),size=(720,720),legendfontsize=12,legendtitle="Mass (in solar masses)",legendtitlefontsize=14)
#     end every 25

#mp4(threeBodyAnim,"3Body_fps30.mp4",fps=30)
#OR
#gif(threeBodyAnim,"3Body_fps30.gif",fps=30)


# Portions translated from SLICOT-Reference distribution
# Copyright (c) 2002-2020 NICONET e.V.
function run_mb03rd(datfile, io=stdout)
    NIN = 5
    NOUT = 6
    NMAX = 10
    LDA = NMAX
    LDX = NMAX
    LDWORK = 3*NMAX
    A = Array{Float64,2}(undef, LDA,NMAX)
    X = Array{Float64,2}(undef, LDX,NMAX)
    BLSIZE = Array{BlasInt,1}(undef, NMAX)
    WR = Array{Float64,1}(undef, NMAX)
    WI = Array{Float64,1}(undef, NMAX)
    DWORK = Array{Float64,1}(undef, LDWORK)
    BWORK = Array{Bool,1}(undef, NMAX)
    f = open(datfile,"r")
    readline(f)
    vs = split(readline(f))
    N = parse(BlasInt, vs[1])
    PMAX = parse(Float64, replace(vs[2],'D'=>'E'))
    TOL = parse(Float64, replace(vs[3],'D'=>'E'))
    JOBX =  vs[4][1]
    SORT =  vs[5][1]
    if ( N<0 || N>NMAX )
        @error "Invalid N=$N"
    end

    vs = String[]
    _isz,_jsz = (N,N)
    while length(vs) < _isz*_jsz
        append!(vs, replace.(split(readline(f)),'D'=>'E'))
    end
    for i in 1:_isz
       _i0 = (i-1)*_jsz
       A[i,1:_jsz] .= parsex.(Float64, vs[_i0+1:_i0+_jsz])
    end
# interp call 1

    #FOREIGN.dgees!( 'Vectors', 'Not sorted', SELECT, N, A, LDA, SDIM, WR, WI, X, LDX, DWORK, LDWORK, BWORK, INFO )
    Aschur = schur(A[1:N,1:N])
    A[1:N,1:N] .= Aschur.T
    X[1:N,1:N] .= Aschur.Z

# interp call 2

    NBLCKS, INFO = SLICOT.mb03rd!(JOBX, SORT, N, PMAX, A, X, BLSIZE, WR, WI, TOL)
    println(io, "NBLCKS = $NBLCKS")
    @test INFO == 0
    INFO == 0 || return

    if ( INFO!=0 )
        @warn "mb03rd returns INFO=$INFO"
    end
# interp output 1
    println(io, "BLSIZE:")
    _nr = NBLCKS
    show(io, "text/plain", BLSIZE[1:_nr])
    println(io)

# interp output 2
    println(io, "A:")
    _nc = N
    _nr = N
    show(io, "text/plain", A[1:_nr,1:_nc])
    println(io)

# interp output 3
    println(io, "X:")
    _nc = N
    _nr = N
    show(io, "text/plain", X[1:_nr,1:_nc])
    println(io)

    close(f)
end # run_mb03rd()


using Test, Distributions, ForwardDiff, ReverseDiff, KernelGoodnessOfFit

# setup the problem
rbf_ = GaussianRBF(1.0)

μ = 0.0
σ² = 1.0
p_ = MultivariateNormal([μ, 0], [σ² 0; 0 σ²])  # true
q_ = MultivariateNormal([μ, 5.0], [σ² 0; 0 σ²])  # false

vs_ = [0.0 ; 0.0]
vs_ = reshape(vs_, 2, 1)

n_ = 400
xs_ = rand(p_, n_)

# new vs old: verify that new implementation is correct
@testset "fssd vs fssd_old: eq" begin
    @test KernelGoodnessOfFit.fssd(rbf_, q_, xs_, vs_) == KernelGoodnessOfFit.fssd_old(rbf_, q_, xs_, vs_)
    @test KernelGoodnessOfFit.fssd(rbf_, p_, xs_, vs_) == KernelGoodnessOfFit.fssd_old(rbf_, p_, xs_, vs_)
end

# ensure new method is faster :)
@testset "fssd vs fssd_old: perf" begin
    t1 = @elapsed KernelGoodnessOfFit.fssd(rbf_, q_, xs_, vs_)
    t2 = @elapsed KernelGoodnessOfFit.fssd_old(rbf_, q_, xs_, vs_)

    @test t1 < t2
end

# sizes: testing of sizes
@testset "shapes" begin
    # setup for tests
    Ξ_ = KernelGoodnessOfFit.compute_Ξ(rbf_, q_, xs_, vs_)

    J, = size(Ξ_)
    d, n = size(Ξ_[1])

    @test (d, J) == size(vs_)
    @test (d, n) == size(xs_)

    τ = KernelGoodnessOfFit.τ_from_Ξ(Ξ_)
    @test size(τ) == (d * J, n)

    μ, Σ = KernelGoodnessOfFit.Σₚ(τ)
    @test size(μ) == (d * J, 1)
    @test size(Σ) == (d * J, d * J)

    σ = KernelGoodnessOfFit.σ²_H₁(μ, Σ)
    @test size(σ) == ()
end

# gradients: verify gradients
@testset "gradient shapes" begin
    Δv = ForwardDiff.gradient(v -> KernelGoodnessOfFit.fssd_H₁_opt_factor(rbf_, q_, xs_, v), vs_)
    @test size(Δv) == size(vs_)

    Δv = ReverseDiff.gradient(v -> KernelGoodnessOfFit.fssd_H₁_opt_factor(rbf_, q_, xs_, v), vs_)
    @test size(Δv) == size(vs_)

    # fails for some reason; but does not fail when I actually run the test...interesting, eh?
    # Δσₖ = ForwardDiff.derivative(σₖ -> fssd_H₁(GaussianRBF(σₖ), q_, xs_, vs_), 1.0)
end

# values: testing specific values
@testset "specific values" begin
    V = reshape([0; 0], (2, 1))

    X = [1.0 2.0 3.0; 2.0 3.0 4.0]

    Ξ_1_true = [
        -0.116086 -0.00425237 -1.58109e-5;
        0.0580429 -0.00106309 -7.90543e-6
    ]
    Ξ_ = KernelGoodnessOfFit.compute_Ξ(rbf_, q_, X, V)

    @test Ξ_[1] ≈ Ξ_1_true atol=0.01

    fssd_true = 0.0004333865124975432

    @test KernelGoodnessOfFit.fssd(Ξ_) * size(X)[2] ≈ fssd_true

    σ₁² = 2.8732542480282987e-5

    τ_ = KernelGoodnessOfFit.τ_from_Ξ(Ξ_)
    μ_, Σ_ = KernelGoodnessOfFit.Σₚ(τ_)

    @test KernelGoodnessOfFit.σ²_H₁(μ_, Σ_) ≈ σ₁²
end

# optimizing the power
@testset "optimize_power" begin
    # because of compilation and stuff, running them in sequence is heavily biased towards
    # the ones being run last
    KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, q_; diff = :difference)
    KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, q_; diff = :backward)
    KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, q_; diff = :forward)

    μs = 1:10

    t_difference = mean([@elapsed KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, MultivariateNormal([μ, 5.0], [σ² 0; 0 σ²]); diff = :difference) for μ ∈ μs])
    t_forward = mean([@elapsed KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, MultivariateNormal([μ, 5.0], [σ² 0; 0 σ²]); diff = :forward) for μ ∈ μs])
    t_backward = mean([@elapsed KernelGoodnessOfFit.optimize_power(rbf_, vs_, xs_, MultivariateNormal([μ, 5.0], [σ² 0; 0 σ²]); diff = :backward) for μ ∈ μs])

    @test t_backward ≥ 0.0
    @test t_forward ≥ 0.0
    @test t_forward < t_difference
end


using DataFrames, CSV, McCormick, EAGO, JuMP

include("nitrification_surrogate.jl")

# alternatively McCormick can be used instead EAGO in this example
include("nitrification_surrogate.jl")

function Ammonia_oxidation_rate(x)
    V = 1000.0
    m_in = V / 240 # L/s
    m_out = V / 240 # L/s
    X_AO = 0.505 # g VSS/L
    M_N = 14.0067 #
    c_NH = x[1]
    c_O = x[2]
    r_AOmax = 0.67 # mg N-NH+ /g VSS_AO min
    K_OAO = 0.3 # mg O2/L
    K_SAO = 0.34 # mg N-NH4+/L
    K_IAO = 6200 # mg N-NH4+/L
    r_AO = r_AOmax*(c_NH/(K_SAO + c_NH + c_NH^2/K_IAO))*(c_O/(K_OAO + c_O)); # mg N-NH+ /g VSS_AO min
    return r_AO
end

## Kinetics Model
function DataModel(t, y, p, u)

    ## Parameters
    K_SAO = 0.24 # mg N-NH/L
    K_IAO = 6200.0 # mg N-NH/L
    K_OAO = 0.3 # mg/L
    K_SNO = 1.6 # mg N-NI/L
    K_INO = 13000.0 # mg N-NI/L
    K_ONO = 1.5 # mg/L
    X_AO = 0.505 # g VSS/L
    X_NO = 0.151 # g VSS/L
    rNO_MAX = 1.07/60 # mg N-NI/(g-VSS_NO*s)

    # Volumetric flow
    V = 1000.0 # L
    m_in = V/240 # L/s
    m_out = V/240 # L/s
    Cs = 31.0 # mg N-NH/L

    # Oxygen flow
    SOTE = 0.1
    cO_s = 9.1 # mg/L
    Phi_OAO = 2.5 # mg O2/ mg N-NH
    Phi_ONO = 0.32 # mg O2/ mg N-NI

    cNH, cNI, cNA, cO = y

    if t >= 3080.0 && t <= 3100.0
        Cin = p[1]
    else
        Cin = Cs
    end

    # ANN
    #@show cNH, cO
    rAO = ANN1([cNH, cO])/60.0 # mg N-NH/(g-VSS_AO*s)
    #rAO = Ammonia_oxidation_rate([cNH, cO])
    t1 =(cNI/positive(K_SNO + cNI + cNI^2 /K_INO))
    t2 =(cO/positive(K_ONO + cO))
    rNO = rNO_MAX*t1*t2

    # Reaction Rate
    R_NH = -rAO*X_AO  # mg N-NH/(L*s)
    R_NI = rAO*X_AO - rNO*X_NO  # mg N-NI/(L*s)
    R_NA = rNO*X_NO  # mg N-NA/(L*s)

    W = 0.2967*u[1] # mg/s
    SOTR = SOTE*W # mg/s
    kla = SOTR/cO_s/V # s^(-1)
    R_O = kla*(cO_s - cO) - rAO*Phi_OAO*X_AO - rNO*Phi_ONO*X_NO # mg O2/(L*s)

    # Mass Balacne
    h1 = 1 /V*(m_in*Cin - m_out*cNH) + R_NH
    h2 = R_NI
    h3 = R_NA
    h4 = R_O

    return h1, h2, h3, h4
end

t0 = 0.0
h = 20.0
tspan_0 = 3080.0
n_out_0 = convert(Int, round(tspan_0 / h))

function EE_Form_Initial(u0)
    cNH0 = 30.0
    cNI0 = 0.0
    cNA0 = 0.0
    cO0 = 0.0
    y0 = cNH0, cNI0, cNA0, cO0
    p0 = 40.0
    y = y0
    y1, y2, y3, y4 = y0
    t = t0
    for j = 1:n_out_0
        t = t + h
        dy1, dy2, dy3, dy4 = DataModel(t, y, p0, u0)    # Perform Step
        #y1 = bnd(y1 + h*dy1, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
        #y2 = bnd(y2 + h*dy2, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
        #y3 = bnd(y3 + h*dy3, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
        #y4 = bnd(y4 + h*dy4, 0.0, 9.1)
        y1 = y1 + h*dy1
        y2 = y2 + h*dy2
        y3 = y3 + h*dy3
        y4 = y4 + h*dy4
        y = y1, y2, y3, y4              # Repackage as tuple
    end
    return t, y
end

u0 = 440.0
tc, yc = EE_Form_Initial(u0)
# 500, 50 bad....
#h = 0.1
tspan = 500.0
n_out = convert(Int, round(tspan / h))
function EE_Form(p, u)
    y1, y2, y3, y4 = yc
    y = y1, y2, y3, y4
    t = tc
    for j = 1:n_out
            t = t + h
            #@show j
            dy1, dy2, dy3, dy4 = DataModel(t, y, p, u)
            #y1 = bnd(y1 + h*dy1, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
            #y2 = bnd(y2 + h*dy2, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
            #y3 = bnd(y3 + h*dy3, 0.0, 40.0)  # Enforce physical bounds on quantity at each step
            #y4 = bnd(y4 + h*dy4, 0.0, 9.1)
            #y1 = positive(y1 + h*dy1)
            #y2 = positive(y2 + h*dy2)
            #y3 = positive(y3 + h*dy3)
            #y4 = positive(y4 + h*dy4)
            y1 = y1 + h*dy1
            y2 = y2 + h*dy2
            y3 = y3 + h*dy3
            y4 = positive(y4 + h*dy4)
            y = y1, y2, y3, y4              # Repackage as tuple
            #@show y
    end
    return t, y
end

#=
function gsip(p, u)
    @show p, u
    t, y = EE_Form(p, u)
    cNH_f, cNI_f, cNA_f, cO_f = y
    @show cNH_f - SP
    return cNH_f - SP
end
=#
function gsip(u,p)
    SP = 30.0
    y1, y2, y3, y4 = yc
    y = y1, y2, y3, y4
    t = tc
    for j = 1:n_out
        t = t + h
        dy1, dy2, dy3, dy4 = DataModel(t, y, p, u)
        y1 = y1 + h*dy1
        y2 = y2 + h*dy2
        y3 = y3 + h*dy3
        y4 = positive(y4 + h*dy4)
        y = y1, y2, y3, y4              # Repackage as tuple
    end
    cNH_f, cNI_f, cNA_f, cO_f = y
    return cNH_f - SP
end

f(u) = tspan * u[1]

function solve_nitrification_sip()

    sip_x_lo = Float64[440.0]
    sip_x_hi = Float64[2000.0]
    sip_p_lo = Float64[31.0]
    sip_p_hi = Float64[40.0]

    opt = EAGO.Optimizer
    sip_result = explicit_sip_solve(
        sip_x_lo,
        sip_x_hi,
        sip_p_lo,
        sip_p_hi,
        f,
        [gsip],
        sip_absolute_tolerance = 3000.00, # a little less than 1E-2 rel tolerance (but not significant)
        sip_optimizer = opt,
        upper_bounding_depth = 12,
        verbosity = 0
    )
    return sip_result
end

polymer_sip_result = solve_nitrification_sip()


function SeisSmoothStructure(m::AbstractString,d::AbstractString,param)

	adj = get(param,"adj",false)
	param["f"] = [smooth_structure]
	param["group"] = "gather"
	param["key"] = ["iaz","iang"]
	tmp1 = join(["tmp_SmoothStructure1_",string(int(rand()*100000))])
	tmp2 = join(["tmp_SmoothStructure2_",string(int(rand()*100000))])
	if (adj==false)
		SeisSort(m,tmp1,key=["iaz","iang","imy","imx"]);
		SeisProcess(tmp1,tmp2,param)
		SeisSort(tmp2,d,key=["imx","imy","iang","iaz"]);
	else
		SeisSort(d,tmp1,key=["iaz","iang","imy","imx"]);
		SeisProcess(tmp1,tmp2,param)
		SeisSort(tmp2,m,key=["imx","imy","iang","iaz"]);
	end
	SeisRemove(tmp1)
	SeisRemove(tmp2)

end

function smooth_structure(d,h,param)

	Nsmooth2 = get(param,"Nsmooth2",3)
	dip_field = get(param,"dip","dip")
	dip,h_dip = SeisRead(dip_field)
	#dip2 = atan(abs(dip))
	dip2 = atan(dip)

	nx = size(d,2)
	nz = size(d,1)
	WL = int(Nsmooth2/2)
	d2 = copy(d)
	smootharray!(d,d2,dip,dip2,nx,nz,Nsmooth2,WL)

	return d2,h;
end

function smootharray!(d::Array{Float32,2},d2::Array{Float32,2},dip::Array{Float32,2},dip2::Array{Float32,2},nx::Int64,nz::Int64,Nsmooth2::Int64,WL::Int64)

	for ix = WL : size(d,2) - WL
		for iz = WL : size(d,1) - WL
			@inbounds d2[iz,ix] = 0.
			@inbounds d2[iz,ix] = smooth1pixel(d,dip[iz,ix],cos(dip2[iz,ix]),sin(dip2[iz,ix]),ix - int(WL*cos(abs(dip2[iz,ix]))),iz - int(WL*sin(dip2[iz,ix])),nx,nz,Nsmooth2)
		end
	end

end

function smooth1pixel(m::Array{Float32,2},dip::Float32,xval::Float32,zval::Float32,ixo::Int64,izo::Int64,nx::Int64,nz::Int64,nw::Int64)

	val = 0.
	for iw = 1 : nw
		@inbounds val += m[izo + int(iw*zval),ixo + int(iw*xval)]
	end

	return val/nw

end