# This type keeps track of parents/siblings using integer indices
abstract type IntNode{T,O} <: AbstractNode{T,O} end

struct IntTree{T,O,UI<:Unsigned} <: AbstractTree{T,O}
    nodes::Vector{IntNode{T,O}}
end
IntTree{T,O,UI}() where {T,O,UI} = IntTree{T,O,UI}(Vector{IntNode{T,O}}())
IntTree(::Type{T},::Type{O},::Type{UI}=UInt) where {T,O,UI} = IntTree{T,O,UI}()
IntTree{T,O}() where {T,O} = IntTree(T,O)

Base.getindex(tree::IntTree, node::Int) = tree.nodes[node]

nodetype(::IntTree{T,O}) where {T,O} = IntNode{T,O}

parent(n::IntNode, t::IntTree) = t.nodes[n.parent]
sibling(n::IntNode, t::IntTree) = t.nodes[n.sibling]

mutable struct BinaryIntNode{T,O} <: IntNode{T,O}
    left::Int
    right::Int
    parent::Int
    sibling::Int

    bbox::AABB{T}
end

function Base.intersect(ray::Ray{T}, n::BinaryIntNode{T}, tree::IntTree{T}) where T
    left = intersect(ray, tree.nodes[n.left].bbox)
    right = intersect(ray, tree.nodes[n.right].bbox)
    (left || right) && (left && intersect(ray, tree.nodes[n.left], tree) ||
                      right && intersect(ray, tree.nodes[n.right], tree))
end

function Base.intersect!(intersection::Intersection{T,O}, n::BinaryIntNode{T,O}, tree::IntTree{T,O}) where {T,O}
    intersect(intersection.ray, n.bbox) || return
    intersect!(intersection, tree.nodes[n.left], tree)
    intersect!(intersection, tree.nodes[n.right], tree)
end

function add_binary_node!(tree::IntTree{T,O}, left::Int, right::Int) where {T,O}
    lnode = tree.nodes[left]
    rnode = tree.nodes[right]
    bbox = AABB(aabb(lnode), aabb(rnode))
    node = BinaryIntNode{T,O}(left, right, 0, 0, bbox)
    push!(tree.nodes, node)
    lnode.sibling = right
    rnode.sibling = left
    lnode.parent = rnode.parent = length(tree.nodes)
end

mutable struct LeafIntNode{T,O} <: IntNode{T,O}
    objs::Vector{O}
    parent::Int
    sibling::Int

    bbox::AABB{T}
end

Base.intersect(ray::Ray{T}, n::LeafIntNode{T,O}, tree::IntTree{T,O}) where {T,O} =
    any(o -> intersect(ray, o), n.objs)

function Base.intersect!(intersection::Intersection{T,O}, n::LeafIntNode{T,O}) where {T,O}
    foreach(o -> intersect!(intersection, o), n.objs)
    is_hit(intersection)
end
Base.intersect!(intersection::Intersection{T,O}, n::LeafIntNode{T,O}, ::IntTree{T,O}) where {T,O} =
    intersect!(intersection, n)

function add_leaf_node!(tree::IntTree{T,O},
                        objs::AbstractVector{O},
                        bbox::AABB{T}) where {T,O<:Intersectable{T}}
    node = LeafIntNode(objs[:], 0, 0, bbox)
    push!(tree.nodes, node)
    length(tree.nodes)
end

is_inner(::BinaryIntNode) = true
is_leaf(::BinaryIntNode) = false
is_inner(::LeafIntNode) = false
is_leaf(::LeafIntNode) = true

objects(n::LeafIntNode) = n.objs

export IntTree


struct RuntimeArrays{T}
    # Iterparticle Arrays
    indij       :: Vector{SVector{2, Int}}
    rij         :: Vector{SVector{3, <:T}}
    inside      :: Vector{<:T}
    wj          :: Vector{<:T}
    rcutij      :: Vector{<:T}
    element     :: Vector{Int}

    # U Arrays: Real, Imaginary Parts
    ulisttot_r  :: Vector{<:T}
    ulisttot_i  :: Vector{<:T}
    dulist_r    :: Matrix{<:T}
    dulist_i    :: Matrix{<:T}

    ulist_r_ij  :: Matrix{<:T}
    ulist_i_ij  :: Matrix{<:T}

    # Z Arrays: Real, Imaginary Parts
    zlist_r     :: Vector{<:T}
    zlist_i     :: Vector{<:T}

    # Y Arrays: Real, Imaginary Parts
    ylist_r     :: Vector{<:T}
    ylist_i     :: Vector{<:T}

    # Bispectrum Arrays
    blist       :: Vector{<:T}
    dblist      :: Matrix{<:T}
end

function initialize_runtime_arrays(i::Int, nnlist::NeighborList, A::AbstractSystem, snap :: SNAPParams)
    # Initialize Arrays
    ind_ij  = SVector{2, Int}[]  # Contains (i,j) pairs
    rcut_ij = AbstractFloat[]   # Contains rcut between (i,j)
    inside  = AbstractFloat[]   # Unused
    wj      = AbstractFloat[]   # Weight of particle (j) w.r.t particle (i)
    rij     = SVector{3, AbstractFloat}[] # vector from particle j to particle i
    element = Int[]            # Element of particle (j)
    elements = snap.elements

    i_list = nnlist.i[i]
    j = nnlist.j[i]
    r = nnlist.r[i]
    i_element = findall(x->x==Symbol(A[i].atomic_symbol), elements)[1]
    for (jj, rj) in zip(j, r)
        push!(ind_ij, SVector{2}([i, jj]))

        j_element = findall(x->x==Symbol(A[jj].atomic_symbol), elements)[1]
        push!(element, j_element)
        push!(rcut_ij, (snap.radii[i_element] + snap.radii[j_element])*snap.rcutfac)
        push!(wj, snap.weight[j_element])
        push!(inside, 1)
        push!(rij, rj)
    end

    n_interactions = length(rij)
    num_elements = snap.prebuilt_arrays.num_elements
    n_doubles = num_elements * num_elements
    n_triples = n_doubles * num_elements
    return RuntimeArrays{AbstractFloat}(
        ind_ij,                                                 # indij
        rij,                                                    # rij
        inside,                                                 # inside
        wj,                                                     # wj
        rcut_ij,                                                 # rcutij
        element,                                                # element
        zeros(snap.prebuilt_arrays.idxu_max*num_elements),   # ulisttot_r
        zeros(snap.prebuilt_arrays.idxu_max*num_elements),   # ulisttot_i
        zeros(snap.prebuilt_arrays.idxu_max, 3),                # dulist_r
        zeros(snap.prebuilt_arrays.idxu_max, 3),                # dulist_i
        zeros(n_interactions+1, snap.prebuilt_arrays.idxu_max),                       # ulist_r_ij
        zeros(n_interactions+1, snap.prebuilt_arrays.idxu_max),                       # ulist_i_ij
        zeros(snap.prebuilt_arrays.idxz_max*n_doubles),         # zlist_r
        zeros(snap.prebuilt_arrays.idxz_max*n_doubles),         # zlist_i
        zeros(snap.prebuilt_arrays.idxu_max*num_elements),   # ylist_r
        zeros(snap.prebuilt_arrays.idxu_max*num_elements),   # ylist_i
        zeros(snap.prebuilt_arrays.idxb_max*n_triples),         # blist
        zeros(snap.prebuilt_arrays.idxb_max*n_triples, 3)       # dblist
    )
end


"""
    Atlas <: Environment

    humanoid robot designed by Boston Dynamics (v5)
"""
struct Atlas end

function atlas(;
    representation=:minimal,
    timestep=0.01,
    gravity=[0.0; 0.0; -9.81],
    friction_coefficient=0.8,
    damper=0.0,
    spring=0.0,
    parse_damper=true,
    info=nothing,
    model_type=:simple,
    seed=1,
    contact_feet=true,
    contact_body=false,
    vis=Visualizer(),
    name=:robot,
    infeasible_control=false,
    opts_step=SolverOptions(rtol=3.0e-4, btol=3.0e-4, undercut=1.5),
    opts_grad=SolverOptions(rtol=3.0e-4, btol=3.0e-4, undercut=1.5),
    T=Float64)

    mechanism = get_mechanism(:atlas;
        timestep,
        gravity,
        friction_coefficient,
        damper,
        spring,
        parse_damper,
        contact_feet,
        contact_body,
        model_type)

    initialize!(mechanism, :atlas)

    if representation == :minimal
        nx = minimal_dimension(mechanism)
    elseif representation == :maximal
        nx = maximal_dimension(mechanism)
    end
    nu_inf = input_dimension(mechanism)
    nu = infeasible_control ? nu_inf : nu_inf - 6 # remove first 6 controls
    no = nx

    aspace = BoxSpace(nu,
        low=(-1.0e8 * ones(nu)),
        high=(1.0e8 * ones(nu)))
    ospace = BoxSpace(no,
        low=(-Inf * ones(no)),
        high=(Inf * ones(no)))

    rng = MersenneTwister(seed)
    z = get_maximal_state(mechanism)
    x = representation == :minimal ? maximal_to_minimal(mechanism, z) : z
    fx = zeros(nx, nx)
    fu = zeros(nx, nu)

    u_prev = zeros(nu)
    control_mask = infeasible_control ? I(nu) : [zeros(nu, 6) I(nu)]
    control_scaling = Diagonal(ones(nu))

    build_robot(mechanism, vis=vis, name=name)

    TYPES = [Atlas, T, typeof(mechanism), typeof(aspace), typeof(ospace), typeof(info)]
    Environment{TYPES...}(mechanism, representation, aspace, ospace,
        x, fx, fu,
        u_prev, control_mask' * control_scaling,
        nx, nu, no,
        info,
        [rng], vis,
        opts_step, opts_grad)
end


"""
Abstract problem.
"""
abstract type AbstractProblem{T} end

"""
    GeneralBTPDE(;
        model,
        matrices,
        abstol = 1e-6,
        reltol = 1e-4,
        odesolver = QNDF(autodiff = false),
    )

General BTPDE problem.
"""
Base.@kwdef struct GeneralBTPDE{T,S} <: AbstractProblem{T}
    model::Model{T}
    matrices::NamedTuple
    abstol::T = 1e-6
    reltol::T = 1e-4
    odesolver::S = QNDF(autodiff = false)
end

"""
    IntervalConstantBTPDE(; model, matrices, θ = 0.5, timestep)

BTPDE problem specialized on intervalwise constant `ScalarGradient`s, e.g [`PGSE`](@ref),
[`DoublePGSE`](@ref).
"""
Base.@kwdef struct IntervalConstantBTPDE{T} <: AbstractProblem{T}
    model::Model{T}
    matrices::NamedTuple
    θ::T = 0.5
    timestep::T
end

"""
    HADC(;
        model,
        matrices,
        abstol = 1e-6,
        reltol = 1e-4,
        odesolver = QNDF(autodiff = false),
    )

HADC problem.
"""
Base.@kwdef struct HADC{T,S} <: AbstractProblem{T}
    model::Model{T}
    matrices::NamedTuple
    abstol::T = 1e-6
    reltol::T = 1e-4
    odesolver::S = QNDF(autodiff = false)
end

"""
    Karger(; model, difftensors, odesolver, timestep)

Karger problem.
"""
Base.@kwdef struct Karger{T,S} <: AbstractProblem{T}
    model::Model{T}
    difftensors::Vector{SMatrix{3,3,T,9}}
    odesolver::S = MagnusGL6()
    timestep::T
end

"""
    Laplace(; model, matrices, neig_max)

Laplace eigenvalue problem.
"""
Base.@kwdef struct Laplace{T} <: AbstractProblem{T}
    model::Model{T}
    matrices::NamedTuple
    neig_max::Int
end

"""
    MatrixFormalism(; model, matrices, lap_eig, ninterval)

Matrix formalism problem. Given a Laplace eigendecomposition `lap_eig`, this problem
consists of computing the MF magnetization.
"""
Base.@kwdef struct MatrixFormalism{T} <: AbstractProblem{T}
    model::Model{T}
    matrices::NamedTuple
    lap_eig::NamedTuple
    ninterval::Int
end

"""
    AnalyticalLaplace(; ρ, r, D, W, T₂, γ, dim, eiglim, eigstep)

Analytical radial Laplace eigenvalue problem.
"""
Base.@kwdef struct AnalyticalLaplace{T} <: AbstractProblem{T}
    ρ::Vector{Complex{T}}
    r::Vector{T}
    D::Vector{T}
    W::Vector{T}
    T₂::Vector{T}
    γ::T
    dim::Int
    eiglim::T
    eigstep::T
end

"""
    AnalyticalMatrixFormalism(; analytical_laplace, lap_mat, volumes)

Analytical Matrix formalism problem. Given a radial Laplace eigendecomposition
`analytical_laplace`, this problem consists of computing the MF compartment signals.
"""
Base.@kwdef struct AnalyticalMatrixFormalism{T} <: AbstractProblem{T}
    analytical_laplace::AnalyticalLaplace{T}
    lap_mat::NamedTuple
    volumes::Vector{T}
end


module KnapsackPlates

module Args
	import ...Utilities
	import ...Utilities.Args: Arg
	#export accepted_arg_list, throw_if_incompatible_options
	# No extra flags for this model yet.
	Utilities.Args.accepted_arg_list(::Val{:KnapsackPlates}) = Vector{Arg}()
	function Utilities.Args.throw_if_incompatible_options(
		::Val{:KnapsackPlates}, p_args
	)
		nothing
	end
end

using JuMP

import ..build_model
"""
    build_model(::Val{:KnapsackPlates}, model, d, p, l, w, L, W, [options])

The KnapsackPlates models each plate as a 1D knapsack of pieces and smaller
subplates. Basically, a plate is allowed to be cut in just one orientation
(except the original plate, that has a hack to allow the first stage to
be any of the two orientations) and, this way, the plate is treated as a
1D knapsack problem using the size of pieces and plates in the considered
dimension.

NOTE: THIS MODEL IS BROKEN. The source has the skeleton of the implementation
but it fails compilation as it is.

```
TODO: add high-level description of the mathematical model here.
```

"""
function build_model(
	::Val{:KnapsackPlates}, model, d :: Vector{D}, p :: Vector{P},
	l :: Vector{S}, w :: Vector{S}, L :: S, W :: S,
	options :: Dict{String, Any} = Dict{String, Any}()
) where {D, S, P}
	@assert length(d) == length(l) && length(l) == length(w)
	num_fplate_types = convert(D, length(d))

	@variable(model, 0 <= fplate[i = 1:num_fplate_types] <= d[i], Int)
	@variable(model, 0 <= iplate[i = 1:num_iplate_types] <= qt_fit_op[i], Int)

	@objective(model, Max,
		sum(p[fpi] * fplate[fpi] for fpi = 1:num_fplate_types)
	)

	# The original plate issue (no way to cut allow cutting both horizonatlly and
	# vertically) may be solved by having a default cut orientation for the
	# original plate, and allowing to cut a same-size-but-opposite-orientation
	# plate. Such plate has length L and is contained in the list of plates to
	# be cut vertically.
	@constraint(model,
		sum(lfp .* fp) + sum(lip2cv .* ip2cv) <= L
	)
	# iip2ch: Index of Intermediary Plates To Cut Horizontally
	for iip2ch = 1:num_ip2ch_types
		fitting_fp = fp_fitting_iipch[iip2ch]
		fitting_ip2cv = iip2cv_fitting_iip2ch[iip2ch]
		isempty(fitting_fp) || isempty(fitting_ip2cv) && continue
		@constraint(model,
			sum(lfp[fitting_fp] .* fp[fitting_fp]) +
			sum(lip2cv[fitting_ip2cv] .* ip2cv[fitting_ip2cv]) <=
			lip2ch[iip2ch] * ip2ch[iip2ch]
		)
	end

	for iip2cv = 1:num_ip2cv_types
		fitting_fp = fp_fitting_iipcv[iip2cv]
		fitting_ip2ch = iip2ch_fitting_iip2cv[iip2cv]
		isempty(fitting_fp) || isempty(fitting_ip2ch) && continue
		@constraint(model,
			sum(wfp[fitting_fp] .* fp[fitting_fp]) +
			sum(wip2ch[fitting_ip2ch] .* ip2ch[fitting_ip2ch]) <=
			wip2cv[iip2cv] * ip2cv[iip2cv]
		)
	end

	model
end

end # module


export move_particle!, equilibrate_diagonal!

"""
    move_particle!(m::Model, e::Ensemble, c::Configuration) :: Tuple{Float64, Step}
Update that removes occupation in one kinkfree Orbital and adds it to another.
"""
function move_particle!(m::Model, e::Ensemble, c::Configuration) :: Tuple{Float64, Step}
    free_orbitals = filter(x -> isunaffected(c.kinks, x), c.occupations)
    if isempty(free_orbitals)
        return 1.0, Step()
    else
        x = rand( free_orbitals )
    end
    oe = filter(x -> isunaffected(c.kinks,x), setdiff!(sphere_with_same_spin(x, dk = ex_radius), c.occupations))

    #if there are no empty non interacting orbitals in neighbourhood make no change
    if isempty(oe)
        return 1.0, Step()
    end
    y = rand(oe)
    @assert x != y "same Configuration proposed."

    delta_di = ΔWdiag_element(m, e, c, y, x, ImgTime(0), ImgTime(1))
    @assert delta_di != Inf
    # weight change
    dw = exp(-(e.β*(energy(m,y)-energy(m,x)) + e.β*delta_di))

    # MC Step generated by this update

    Δ = Step((x,), (y,))
    apply_step!(c,Δ)

    # get orbitals for reverse update
    oe2 = filter(x -> isunaffected(c.kinks,x), setdiff!(sphere_with_same_spin(y, dk = ex_radius), c.occupations) )

    # quotient of proposal probabilities
    dv = length(oe)/length(oe2)
    @assert dv >= 0
    @assert dw >= 0 "dw=$(dw), x=$(x), y=$(y), delta_di=$(delta_di)"
    @assert !isinf(abs(dv*dw))
    return dv*dw, Δ
end

"""
    equilibrate_diagonal(m::Model, e::Ensemble, c::Configuration)
This function will propose 100 type-a-updates for each particle in c::Configuration.
As move_particle! can only be called on kinkfreee orbitals, calling this before starting the true equilibration
might reduce the error of the kinetic energy in runs with lots of kinks.
"""
function equilibrate_diagonal!(m::Model, e::Ensemble, c::Configuration)
    for _ in 1:e.N*1e2
        dv, Δ = move_particle!(m, e, c)
        if rand() > dv
            #deny step
            apply_back_step!(c, Δ)
        end
    end
end


#== # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
#
#   The NRLMSIS-00 empirical atmosphere model was developed by Mike
#   Picone, Alan Hedin, and Doug Drob based on the MSISE90 model.
#
#   The MSISE90 model describes the neutral temperature and densities in Earth's
#   atmosphere from ground to thermospheric heights. Below 72.5 km the model is
#   primarily based on the MAP Handbook (Labitzke et al., 1985) tabulation of
#   zonal average temperature and pressure by Barnett and Corney, which was also
#   used for the CIRA-86. Below 20 km these data were supplemented with averages
#   from the National Meteorological Center (NMC). In addition, pitot tube,
#   falling sphere, and grenade sounder rocket measurements from 1947 to 1972
#   were taken into consideration. Above 72.5 km MSISE-90 is essentially a
#   revised MSIS-86 model taking into account data derived from space shuttle
#   flights and newer incoherent scatter results. For someone interested only in
#   the thermosphere (above 120 km), the author recommends the MSIS-86 model.
#   MSISE is also not the model of preference for specialized tropospheric work.
#   It is rather for studies that reach across several atmospheric boundaries.
#   (quoted from http://nssdc.gsfc.nasa.gov/space/model/atmos/nrlmsise00.html)
#
#   This Julia version of NRLMSISE-00 was converted from the C version
#   implemented and maintained by Dominik Brodowski <devel@brodo.de> and
#   available at http://www.brodo.de/english/pub/nrlmsise/index.html .
#
#   The source code is available at the following git:
#
#       https://git.linta.de/?p=~brodo/nrlmsise-00.git;a=tree
#
#   The conversion also used information available at the FORTRAN source code
#   available at
#
#       https://ccmc.gsfc.nasa.gov/pub/modelweb/atmospheric/msis/nrlmsise00/
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# References
#
#   [1] https://www.brodo.de/space/nrlmsise/index.html
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ==#

export conf_nrlmsise00, gtd7, gtd7d, nrlmsise00

################################################################################
#                                  Constants
################################################################################

include("./nrlmsise00_coefs.jl")

################################################################################
#                                     API
################################################################################

"""
    nrlmsise00(JD::Number, alt::Number, g_lat::Number, g_long::Number [, f107A::Number, f107::Number, ap::Union{Number,AbstractVector}]; output_si::Bool = true, dversion::Bool = true)

**NRLMSISE-00**

Neutral Atmosphere Empirical Model from the surface to lower exosphere.

This routine computes the NRLMSISE-00 outputs (see `NRLMSISE00_Output`) using
the configurations in the structure `nrlmsise00` (see `NRLMSISE00_Structure`).

Notice that the NRLMSISE-00 will be run using the default flags (see
`NRLMSISE00_DEFAULT_FLAGS`). The user can only change the value of
`flags[:output_m_kg]` using the keyword `output_si` to select whether the output
must be converted to SI units. If more control is needed, then the user must
manually call the function `conf_nrlmsise00` and then call `gtd7` or `gtd7d`
with the desired flags.

If the space indices `f107A`, `f107`, and `ap` are missing, then they will be
obtained from the online databases (see `init_space_indices()`).

# Args

* `JD`: Julian Day [UTC].
* `alt`: Altitude [m].
* `g_lat`: Geodetic latitude [rad].
* `g_long`: Geodetic longitude [rad].
* `f107A`: 81 day average of F10.7 flux (centered on day of year `JD`).
* `f107`: Daily F10.7 flux for previous day.
* `ap`: Magnetic index (daily) if it is a number. If it is an array, then see
        **Remarks**.

# Keywords

* `output_si`: (OPTIONAL) If `true`, then the output units will be [m⁻³] for
               species number density and [kg/m⁻³] for the total density.
               Otherwise, the units will be [cm⁻³] and [g/cm⁻³], respectively.
* `dversion`: (OPTIONAL) If `true`, run `gtd7d`. Otherwise, run `gtd7` (see
              **Remarks**).

# Returns

An instance of the structure `NRLMSISE00_Output`. The result in variable
`den_Total` depends on the value of `dversion` (see **Remarks**, **Notes on
input variables**).

# Remarks

1. The densities of `O`, `H`, and `N` are set to `0` below `72.5 km`.
2. The exospheric temperature `T_exo` is set to global average for altitudes
   below `120 km`. The `120 km` gradient is left at global average value for
   altitudes below `72.5 km`.
3. Anomalous oxygen is defined as hot atomic oxygen or ionized oxygen that can
   become appreciable at high altitudes (`> 500 km`) for some ranges of inputs,
   thereby affection drag on satellites and debris. We group these species under
   the term **Anomalous Oxygen**, since their individual variations are not
   presently separable with the drag data used to define this model component.

## AP

If `ap` is a `Vector`, then it must be a vector with 7 dimensions as described
below:

| Index | Description                                                                   |
|-------|:------------------------------------------------------------------------------|
|     1 | Daily AP.                                                                     |
|     2 | 3 hour AP index for current time.                                             |
|     3 | 3 hour AP index for 3 hours before current time.                              |
|     4 | 3 hour AP index for 6 hours before current time.                              |
|     5 | 3 hour AP index for 9 hours before current time.                              |
|     6 | Average of eight 3 hour AP indices from 12 to 33 hours prior to current time. |
|     7 | Average of eight 3 hour AP indices from 36 to 57 hours prior to current time. |

## Notes on input variables

`f107` and `f107A` values used to generate the model correspond to the 10.7 cm
radio flux at the actual distance of the Earth from the Sun rather than the
radio flux at 1 AU. The following site provides both classes of values:

    ftp://ftp.ngdc.noaa.gov/STP/SOLAR_DATA/SOLAR_RADIO/FLUX/

`f107`, `f107A`, and `ap` effects are neither large nor well established below
80 km and these parameters should be set to 150, 150, and 4 respectively.

If `dversion` is `true`, then the total mass `den_Total` (see
`NRLMSISE00_Output`) is the sum of the mass densities of the species `He`, `O`,
`N₂`, `O₂`, `Ar`, `H`, and `N`, but **does not** include anomalous oxygen.

If `dversion` is `false`, then total mass `den_Total` (see `NRLMSISE00_Output`)
is the effective total mass density for drag and is the sum of the mass
densities of all species in this model **including** the anomalous oxygen.

"""
function nrlmsise00(JD::Number, alt::Number, g_lat::Number, g_long::Number;
                    output_si::Bool = true, dversion::Bool = true)

    # Get the space indices. If the altitude is lower than 80km, set to default
    # according to the instructions in NRLMSISE-00 source code.
    if alt < 80e3
        f107A = 150.0
        f107  = 150.0
        ap    = 4.0
    else
        # TODO: The online version of NRLMSISE-00 seems to use 90 days, whereas
        # the NRLMSISE-00 source code mentions 81 days.
        f107A = get_space_index(Val(:F10Madj), JD; window = 90)
        f107  = get_space_index(Val(:F10adj),  JD-1)
        ap    = get_space_index(Val(:Ap),      JD)
    end

    # Call the NRLMSISE-00 model.
    nrlmsise00(JD, alt, g_lat, g_long, f107A, f107, ap; output_si = output_si,
               dversion = dversion)
end

function nrlmsise00(JD::Number, alt::Number, g_lat::Number, g_long::Number,
                    f107A::Number, f107::Number,
                    ap::Union{Number,AbstractVector}; output_si::Bool = true,
                    dversion::Bool = true)

    # Constant
    dgtr = 1.74533e-2 # Convert degrees to radians.

    # Convert the Julian Day to Date.
    (Y, M, D, h, m, s) = jd_to_date(JD)

    # Get the number of days since the beginning of the year.
    doy = round(Int,date_to_jd(Y, M, D, 0, 0, 0) - date_to_jd(Y, 1, 1, 0, 0, 0)) + 1

    # Get the number of seconds since the beginning of the day.
    Δds = 3600h + 60m + s

    # Get the local apparent solar time [hours].
    #
    # TODO: To be very precise, I think this should also take into consideration
    # the Equation of Time. However, the online version of NRLMSISE-00 does not
    # use this.
    lst = Δds/3600 + g_long*12/π

    # Create the input structure for NRLMSISE-00 converting the arguments.
    #
    # Notice that, for this algorithm, the year **is not** used.
    nrlmsise00d = conf_nrlmsise00(Y, doy, Δds, alt/1000, g_lat/dgtr,
                                  g_long/dgtr, lst, f107A, f107, ap,
                                  NRLMSISE00_Flags(output_m_kg = output_si))

    # Call the NRLMSISE-00 model.
    nrlmsise00_out = (dversion) ? gtd7d(nrlmsise00d) : gtd7(nrlmsise00d)

    nrlmsise00_out
end

################################################################################
#                             Auxiliary Functions
################################################################################

"""
    conf_nrlmsise00(year::Int, doy::Int, sec::Number, alt::Number, g_lat::Number, g_long::Number, lst::Number, f107A::Number, f107::Number, ap::[Number, AbstractVector], flags::NRLMSISE00_Flags = NRLMSISE00_Flags())

Create the structure with the proper configuration to call the NRLMSISE-00
model.

Notice that the input variables have the same units of the original model.

# Args

* `year`: Year (currently ignored).
* `doy`: Day of year.
* `sec`: Seconds in day [UT].
* `alt`: Altitude [km].
* `g_lat`: Geodetic latitude [deg].
* `g_long`: Geodetic longitude [deg].
* `lst`: Local apparent solar time (hours).
* `f107A`: 81 day average of F10.7 flux (centered on day of year `doy`).
* `f107`: Daily F10.7 flux for previous day.
* `ap`: Magnetic index (daily) if it is a number. If it is an array, then see
        **Remarks**.
* `flags`: (OPTIONAL) An instance of the structure `NRLMSISE00_Flags` with the
            configuration flags for NRLMSISE00. If omitted, then the default
            configurations will be used.

# Returns

An instance of the structure `NRLMSISE00_Structure`.

# Remarks

If `ap` is a `Vector`, then it must be a vector with 7 dimensions as described
below:

| Index | Description                                                                   |
|-------|:------------------------------------------------------------------------------|
|     1 | Daily AP.                                                                     |
|     2 | 3 hour AP index for current time.                                             |
|     3 | 3 hour AP index for 3 hours before current time.                              |
|     4 | 3 hour AP index for 6 hours before current time.                              |
|     5 | 3 hour AP index for 9 hours before current time.                              |
|     6 | Average of eight 3 hour AP indices from 12 to 33 hours prior to current time. |
|     7 | Average of eight 3 hour AP indices from 36 to 57 hours prior to current time. |

## Notes on input variables

UT, Local Time, and Longitude are used independently in the model and are not of
equal importance for every situation. For the most physically realistic
calculation these three variables should be consistent (`lst=sec/3600 +
g_long/15`). The Equation of Time departures from the above formula for
apparent local time can be included if available but are of minor importance.

`f107` and `f107A` values used to generate the model correspond to the 10.7 cm
radio flux at the actual distance of the Earth from the Sun rather than the
radio flux at 1 AU. The following site provides both classes of values:

    ftp://ftp.ngdc.noaa.gov/STP/SOLAR_DATA/SOLAR_RADIO/FLUX/

`f107`, `f107A`, and `ap` effects are neither large nor well established below
80 km and these parameters should be set to 150, 150, and 4 respectively.

"""
function conf_nrlmsise00(year::Int,
                         doy::Int,
                         sec::Number,
                         alt::Number,
                         g_lat::Number,
                         g_long::Number,
                         lst::Number,
                         f107A::Number,
                         f107::Number,
                         ap::Number,
                         flags::NRLMSISE00_Flags = NRLMSISE00_Flags())

    # Constants
    # =========

    dgtr = 1.74533e-2 # Convert degrees to radians.
    hr   = 0.2618     # Convert hour angle to radians.

    # Compute auxiliary variables
    # ===========================
    df  = f107  - f107A
    dfa = f107A - 150

    stloc, ctloc  = sincos(1hr*lst)
    s2tloc,c2tloc = sincos(2hr*lst)
    s3tloc,c3tloc = sincos(3hr*lst)

    # Compute Legendre polynomials.
    #
    # TODO: Not all coefficients are used. Hence, we could gain performance
    # here.
    plg = legendre(Val(:conv), pi/2-g_lat*dgtr, 8, 8, false)'

    # Latitude variation of gravity
    # =============================
    #
    # None for flags.time_independent = false.
    xlat      = (!flags.time_independent) ? 45.0 : Float64(g_lat)
    gsurf, re = _glatf(xlat)

    # Create and return the structure
    # ===============================

    # This is necessary to avoid type instability as reported here:
    #
    #   https://github.com/mauro3/Parameters.jl/issues/58
    #
    nrlmsise00d::NRLMSISE00_Structure{Float64} =
        NRLMSISE00_Structure{Float64}(year     = year,
                                      doy      = doy,
                                      sec      = sec,
                                      alt      = alt,
                                      g_lat    = g_lat,
                                      g_long   = g_long,
                                      lst      = lst,
                                      f107A    = f107A,
                                      f107     = f107,
                                      ap       = ap,
                                      flags    = flags,
                                      re       = re,
                                      gsurf    = gsurf,
                                      df       = df,
                                      dfa      = dfa,
                                      plg      = plg,
                                      stloc    = stloc,
                                      ctloc    = ctloc,
                                      s2tloc   = s2tloc,
                                      c2tloc   = c2tloc,
                                      s3tloc   = s3tloc,
                                      c3tloc   = c3tloc)

    nrlmsise00d
end

function conf_nrlmsise00(year::Int, doy::Int, sec::Number, alt::Number,
                         g_lat::Number, g_long::Number, lst::Number,
                         f107A::Number, f107::Number, ap::AbstractVector,
                         flags::NRLMSISE00_Flags = NRLMSISE00_Flags())

    # Constants
    # =========

    dgtr = 1.74533e-2 # Convert degrees to radians.
    hr   = 0.2618     # Convert hour angle to radians.

    # Compute the new flags
    # =====================
    flags.use_ap_array = true

    # Compute auxiliary variables
    # ===========================
    df     = f107  - f107A
    dfa    = f107A - 150

    stloc, ctloc  = sincos(1hr*lst)
    s2tloc,c2tloc = sincos(2hr*lst)
    s3tloc,c3tloc = sincos(3hr*lst)

    # Compute Legendre polynomials.
    #
    # TODO: Not all coefficients are used. Hence, we could gain performance
    # here.
    plg = legendre(Val(:conv), pi/2-g_lat*dgtr, 8, 8, false)'

    # Latitude variation of gravity
    # =============================
    #
    # None for flags.time_independent = false.
    xlat      = (!flags.time_independent) ? 45.0 : Float64(g_lat)
    gsurf, re = _glatf(xlat)

    # Create and return the structure
    # ===============================

    # This is necessary to avoid type instability as reported here:
    #
    #   https://github.com/mauro3/Parameters.jl/issues/58
    #
    nrlmsise00d::NRLMSISE00_Structure{Float64} =
        NRLMSISE00_Structure{Float64}(year     = year,
                                      doy      = doy,
                                      sec      = sec,
                                      alt      = alt,
                                      g_lat    = g_lat,
                                      g_long   = g_long,
                                      lst      = lst,
                                      f107A    = f107A,
                                      f107     = f107,
                                      ap_array = ap,
                                      flags    = flags,
                                      re       = re,
                                      gsurf    = gsurf,
                                      df       = df,
                                      dfa      = dfa,
                                      plg      = plg,
                                      stloc    = stloc,
                                      ctloc    = ctloc,
                                      s2tloc   = s2tloc,
                                      c2tloc   = c2tloc,
                                      s3tloc   = s3tloc,
                                      c3tloc   = c3tloc)

    nrlmsise00d
end

################################################################################
#                               Public Functions
################################################################################

"""
    gtd7(nrlmsise00d::NRLMSISE00_Structure{T}) where T<:Number

**NRLMSISE-00**

Neutral Atmosphere Empirical Model from the surface to lower exosphere.

This routine computes the NRLMSISE-00 outputs (see `NRLMSISE00_Output`) using
the configurations in the structure `nrlmsise00` (see `NRLMSISE00_Structure`).

# Args

* `nrlmsise00d`: An instance of `NRLMSISE00_Structure`.

# Returns

An instance of structure `NRLMSISE00_Output` with the outputs.

In this case, the total mass `den_Total` (see `NRLMSISE00_Output`) is the sum of
the mass densities of the species `He`, `O`, `N₂`, `O₂`, `Ar`, `H`, and `N`, but
**does not** include anomalous oxygen.

# Remarks

1. The densities of `O`, `H`, and `N` are set to `0` below `72.5 km`.
2. The exospheric temperature `T_exo` is set to global average for altitudes
   below `120 km`. The `120 km` gradient is left at global average value for
   altitudes below `72.5 km`.
3. Anomalous oxygen is defined as hot atomic oxygen or ionized oxygen that can
   become appreciable at high altitudes (`> 500 km`) for some ranges of inputs,
   thereby affection drag on satellites and debris. We group these species under
   the term **Anomalous Oxygen**, since their individual variations are not
   presently separable with the drag data used to define this model component.

"""
function gtd7(nrlmsise00d::NRLMSISE00_Structure{T}) where T<:Number

    @unpack_NRLMSISE00_Structure nrlmsise00d

    # Constants
    # =========

    mn2  = 4
    zn2  = SVector{4,T}([72.5,55.0,45.0,32.5])
    mn3  = 5
    zn3  = SVector{5,T}([32.5,20.0,15.0,10.0,0.0])
    zmix = T(62.5)

    # Initialization of variables
    # ===========================
    meso_tn2  = MVector{4,T}(undef)
    meso_tn3  = MVector{5,T}(undef)
    meso_tgn2 = MVector{2,T}(undef)
    meso_tgn3 = MVector{2,T}(undef)

    @inbounds begin
        # Latitude variation of gravity
        # =============================

        xmm = pdm[3,5]

        # Thermosphere / Mesosphere (above zn2[1])
        # ========================================

        (alt < zn2[1]) && (nrlmsise00d.alt = zn2[1])

        out_thermo = gts7(nrlmsise00d)
        nrlmsise00d.alt = alt

        # Unpack the values again because `gts7` may have modified `nrlmsise00d`.
        @unpack_NRLMSISE00_Structure nrlmsise00d

        # If we are above `zn2[1]`, then we do not need to compute anything else.
        (alt >= zn2[1]) && return out_thermo

        # Unpack the output values from the thermospheric portion.
        @unpack_NRLMSISE00_Output out_thermo

        # Check if we must convert to SI.
        dm28m = (flags.output_m_kg) ?  dm28*1e6 : dm28

        # Lower Mesosphere / Upper Stratosphere (between `zn3[1]` and `zn2[1]`)
        # =====================================================================

        meso_tgn2[1] = meso_tgn1_2

        meso_tn2[1]  = meso_tn1_5

        meso_tn2[2]  =  pma[1,1]*pavgm[1]/
                        ( 1 - flags.all_tn2_var*_glob7s(pma[1,:], nrlmsise00d) )
        meso_tn2[3]  =  pma[2,1]*pavgm[2]/
                        ( 1 - flags.all_tn2_var*_glob7s(pma[2,:], nrlmsise00d) )
        meso_tn2[4]  =  pma[3,1]*pavgm[3]/
                        ( 1 - flags.all_tn2_var*flags.all_tn3_var*_glob7s(pma[3,:], nrlmsise00d) )
        meso_tn3[1]  = meso_tn2[4]

        meso_tgn2[2] = pavgm[9]*pma[10,1]*
                        ( 1 + flags.all_tn2_var*flags.all_tn3_var*_glob7s(pma[10,:], nrlmsise00d))*
                        meso_tn2[4]^2/(pma[3,1]*pavgm[3])^2

        # Lower Stratosphere and Troposphere (below `zn3[1]`)
        # ===================================================

        if alt < zn3[1]
            meso_tgn3[1] = meso_tgn2[2]
	    	meso_tn3[2]  = pma[4,1]*pavgm[4]/
                           ( 1 - flags.all_tn3_var*_glob7s(pma[4,:], nrlmsise00d) )
	    	meso_tn3[3]  = pma[5,1]*pavgm[5]/
                           ( 1 - flags.all_tn3_var*_glob7s(pma[5,:], nrlmsise00d) )
	    	meso_tn3[4]  = pma[6,1]*pavgm[6]/
                           ( 1 - flags.all_tn3_var*_glob7s(pma[6,:], nrlmsise00d) )
	    	meso_tn3[5]  = pma[7,1]*pavgm[7]/
                           ( 1 - flags.all_tn3_var*_glob7s(pma[7,:], nrlmsise00d) )
	    	meso_tgn3[2] = pma[8,1]*pavgm[8]*
                           ( 1 + flags.all_tn3_var*_glob7s(pma[8,:], nrlmsise00d))*
                           meso_tn3[5]*meso_tn3[5]/(pma[7,1]*pavgm[7])^2
        end

        # Linear Transition to Full Mixing Below `zn2[1]`
        # ===============================================

        dmc  = (alt > zmix) ? 1 - (zn2[1]-alt)/(zn2[1] - zmix) : T(0)
        dz28 = den_N2

        # N2 Density
        # ==========

        dmr = den_N2 / dm28m - 1

        den_N2, tz = _densm(re, gsurf, alt, dm28m, xmm, T(0), zn3, meso_tn3,
                            meso_tgn3, zn2, meso_tn2, meso_tgn2)

        den_N2 *= 1 + dmr*dmc

        # He Density
        # ==========

        dmr    = den_He / (dz28 * pdm[1,2]) - 1
        den_He = den_N2 * pdm[1,2] * ( 1 + dmr*dmc )

        # O Density
        # =========

        den_O  = T(0)
        den_aO = T(0)

        # O2 Density
        # ==========

        dmr    = den_O2 / (dz28 * pdm[4,2]) - 1
        den_O2 = den_N2 * pdm[4,2] * ( 1 + dmr*dmc )

        # Ar Density
        # ==========

        dmr    = den_Ar / (dz28 * pdm[5,2]) - 1
        den_Ar = den_N2 * pdm[5,2] * ( 1 + dmr*dmc )

        # H Density
        # =========

        den_H = T(0)

        # N Density
        # =========

        den_N = T(0)

        # Total Mass Density
        # ==================

        den_Total = 1.66e-24( 4den_He +
                             16den_O  +
                             28den_N2 +
                             32den_O2 +
                             40den_Ar +
                               den_H  +
                             14den_N)

        # Check if we must convert the units to SI.
        (flags.output_m_kg) && (den_Total /= 1000)

        # Temperature at Selected Altitude
        # ================================

        dd, T_alt = _densm(re, gsurf, alt, T(1), T(0), tz, zn3, meso_tn3,
                           meso_tgn3, zn2, meso_tn2, meso_tgn2)

        # Create output structure and return.
        #
        # This is necessary to avoid type instability as reported here:
        #
        #   https://github.com/mauro3/Parameters.jl/issues/58
        #

        nrlmsise00_out::NRLMSISE00_Output{T} =
            NRLMSISE00_Output{T}(den_N     = den_N,
                                 den_N2    = den_N2,
                                 den_O     = den_O,
                                 den_aO    = den_aO,
                                 den_O2    = den_O2,
                                 den_H     = den_H,
                                 den_He    = den_He,
                                 den_Ar    = den_Ar,
                                 den_Total = den_Total,
                                 T_exo     = T_exo,
                                 T_alt     = T_alt,
                                 flags     = flags)
    end

    nrlmsise00_out
end

"""
    gtd7d(nrlmsise00d::NRLMSISE00_Structure{T}) where T<:Number

**NRLMSISE-00**

Neutral Atmosphere Empirical Model from the surface to lower exosphere.

This routine computes the NRLMSISE-00 outputs (see `NRLMSISE00_Output`) using
the configurations in the structure `nrlmsise00` (see `NRLMSISE00_Structure`).

# Args

* `nrlmsise00d`: An instance of `NRLMSISE00_Structure`.

# Returns

An instance of structure `NRLMSISE00_Output` with the outputs.

In this case, the total mass `den_Total` (see `NRLMSISE00_Output`) is the
effective total mass density for drag and is the sum of the mass densities of
all species in this model **including** the anomalous oxygen.

# Remarks

1. The densities of `O`, `H`, and `N` are set to `0` below `72.5 km`.
2. The exospheric temperature `T_exo` is set to global average for altitudes
   below `120 km`. The `120 km` gradient is left at global average value for
   altitudes below `72.5 km`.
3. Anomalous oxygen is defined as hot atomic oxygen or ionized oxygen that can
   become appreciable at high altitudes (`> 500 km`) for some ranges of inputs,
   thereby affection drag on satellites and debris. We group these species under
   the term **Anomalous Oxygen**, since their individual variations are not
   presently separable with the drag data used to define this model component.

"""
function gtd7d(nrlmsise00d::NRLMSISE00_Structure{T}) where T<:Number
    # Call `gt7d` to compute the NRLMSISE-00 outputs.
    out = gtd7(nrlmsise00d)

    # Update the computation of the total mass density.
    den_Total = 1.66e-24( 4out.den_He +
                         16out.den_O  +
                         28out.den_N2 +
                         32out.den_O2 +
                         40out.den_Ar +
                           out.den_H  +
                         14out.den_N  +
                         16out.den_aO)

    # Check if we must convert the units to SI.
    (nrlmsise00d.flags.output_m_kg) && (den_Total /= 1000)

    # Create the new output and return.
    #
    # This is necessary to avoid type instability as reported here:
    #
    #   https://github.com/mauro3/Parameters.jl/issues/58
    #

    # Densities.
    #
    # Originally, we were using `reconstruct` to copy all values except one:
    #
    #   NRLMSISE00_Output(out; den_Total = den_Total)
    #
    # However, it turns out to be a very slow method. Hence, it was change to
    # this version.
    nrlmsise00_out::NRLMSISE00_Output{T} =
        NRLMSISE00_Output(den_N     = out.den_N,
                          den_N2    = out.den_N2,
                          den_O     = out.den_O,
                          den_aO    = out.den_aO,
                          den_O2    = out.den_O2,
                          den_H     = out.den_H,
                          den_He    = out.den_He,
                          den_Ar    = out.den_Ar,
                          den_Total = den_Total,
                          T_exo     = out.T_exo,
                          T_alt     = out.T_alt,
                          flags     = out.flags)

    nrlmsise00_out
end

################################################################################
#                              Private Functions
################################################################################

include("./nrlmsise00_priv.jl")


module MeshAlgorithms

using GeometryTypes
using StaticArrays
using LinearAlgebra

export
    Mesh,
    convert_from_homogenous,
    distance,
    fundamental_error_quadric

include("simplification.jl")

end # module


module TestTeeRF

include("preamble.jl")

TeeRF_by_ProductRF(fs...) = TeeRF_by_ProductRF(fs)
TeeRF_by_ProductRF(fs::Tuple) =
    reducingfunction(Map(x -> ntuple(_ -> x, length(fs))), ProductRF(fs))

@testset "extrema" begin
    @testset "$TeeRF" for TeeRF in [TeeRF, TeeRF_by_ProductRF]
        @testset "$fold" for fold in [foldl, simple_reduce, foldxt_bs1, foldxt]
            foldl = nothing
            custom_extrema(xs, xf = Map(identity)) = fold(TeeRF(min, max), xf, xs)
            @test custom_extrema([5, 2, 6, 8, 3]) == (2, 8)
            @test custom_extrema([5, 2, 6, 8, 3], Filter(isodd)) == (3, 5)
            @test_throws EmptyResultError custom_extrema(1:0)
            @test_throws EmptyResultError custom_extrema(1:10, Filter(_ -> false))
        end
    end
end

@testset "partially started" begin
    @testset "$TeeRF" for TeeRF in [TeeRF, TeeRF_by_ProductRF]
        @testset "$fold" for fold in [foldl, simple_reduce, foldxt_bs1, foldxt]
            foldl = nothing
            f(xs) =
                fold(TeeRF(min, reducingfunction(Filter(isodd), max)), Map(identity), xs)
            @test f([5, 2, 6, 8, 3]) == (2, 5)
            @test_throws EmptyResultError f(2:2:8)
        end
    end
end

@testset "nested" begin
    @testset "$TeeRF" for TeeRF in [TeeRF, TeeRF_by_ProductRF]
        @testset "$fold" for fold in [foldl, simple_reduce, foldxt_bs1, foldxt]
            foldl = nothing
            @test fold(
                TeeRF(TeeRF(min, max), +, TeeRF(reducingfunction(Map(isodd), |), *)),
                Map(identity),
                [5, 2, 6, 8, 3],
            ) === ((2, 8), 24, (true, 1440))
            @test fold(TeeRF(TeeRF(TeeRF(min))), Map(identity), [5, 2, 6, 8, 3]) ===
                  (((2,),),)
        end
    end
end

@testset "trivial" begin
    @testset "$TeeRF" for TeeRF in [TeeRF, TeeRF_by_ProductRF]
        @testset "$fold" for fold in [foldl, simple_reduce, foldxt_bs1, foldxt]
            @test fold(TeeRF(()), Map(identity), [5, 2, 6, 8, 3]) === ()
        end
    end
end

@testset "init" begin
    @testset "$TeeRF" for TeeRF in [TeeRF, TeeRF_by_ProductRF]
        @testset "$fold" for fold in [foldl, simple_reduce, foldxt_bs1, foldxt]
            foldl = nothing
            @test fold(TeeRF(+, +), Map(identity), 1:9) === (45, 45)
            @test fold(TeeRF(+, +), Map(identity), 1:9, init = 0.0) === (45.0, 45.0)
        end
        @test foldl(TeeRF(+, +), Map(identity), 1:2; init = 1) === (4, 4)
        @test foldl(TeeRF(+, +), Map(identity), 1:2; init = (0, 1)) === (3, 4)
    end
end

@testset "error handling" begin
    err = @test_error foldl(TeeRF(min, max), [5, 2, 6, 8, 3])
    msg = sprint(showerror, err)
    @test occursin("incompatible accumulator", msg)
end

end  # module


module Types

    using RandomExtensions
    import Dates

    export Entity, EntityData, GAmodel, targetParams, GAParams

    abstract type Entity end
    # -------

    mutable struct EntityData
        entity
        generation::Int
        scores :: Array

        EntityData(entity, generation::Int) = new(entity, generation)
        EntityData(entity, model) = new(entity, model.params.currentGeneration)
    end

    mutable struct GAParams
        populationSize ::  Int
        generations ::  Int
        genomeSize ::  Int
        maxGenomeSize :: Int
        crossoverRate :: Float64
        mutationRate :: Float64
        elitism  :: Bool # Keep previous generation's fittest individual in place of worst in current
        historyPath :: IOStream  # Path to save log of fitness history at each generation. Can be used to plot on Excel chart etc.

        totalFitness :: Float64
        targetFitness :: Float64
        targetFitnessCount ::  Int
        currentGeneration ::  Int

        #thisGeneration :: Array
        #nextGeneration :: Array
        #fitnessTable :: Array{Float64,1}
    end

    # -------

    mutable struct GAmodel
        initial_pop_size::Int
        gen_num::Int

        population::Array
        #pop_data::Array{EntityData}
        #freezer::Array{EntityData}

        rng::AbstractRNG

        ga
        specific_fitness

        scores :: Array
        params :: GAParams

        instructionsSet #:: AbstractDict

        GAmodel() = new(0, 1, Any[], MersenneTwister(time_ns()), nothing, nothing, [],
                GAParams(136, 1000 , 150, 150, 0.7, 0.01, true, open("output.txt", "w") ,  0.0 , 0, 0.0 , 0 ),
                Dict() )
        GAmodel(params :: GAParams) = new(0, 1, Any[], MersenneTwister(time_ns()),
                nothing, nothing, [], params, Dict())
    end

    struct Function
        ## <summary>
        ## Controls how functions read input (,) from parent memory: either at the current memory data pointer or from the start of memory.
        ## If true, input will be read from position 0 from the parent. Meaning, the first input value that the parent read will be the first input value the function gets, regardless of the parent's current memory data position. This may make it easier for the GA to run the function, since it does not require an exact memory position before calling the function.
        ## If false (default), input will be read from the current memory data position of the parent. Meaning, if the parent has shifted the memory pointer up 3 slots, the function will begin reading from memory at position 3.
        ## </summary>
        ReadInputAtMemoryStart :: Bool
        ## <summary>
        ## Custom max iteration counts for functions.
        ## </summary>
        MaxIterationCount :: Int
    end

    struct FunctionInst #:: Function
        ## <summary>
        ## Starting instruction index for this function, within the program code.
        ## </summary>
         InstructionPointer
    end

    struct GAStatus
        ## <summary>
        ## Best fitness so far.
        ## </summary>
        Fitness :: Float64
        ## <summary>
        ## Best true fitness so far, used to determine when a solution is found.
        ## </summary>
        TrueFitness :: Float64
        ## <summary>
        ## Best program so far.
        ## </summary>
        Program :: String
        ## <summary>
        ## Best program output so far.
        ## </summary>
        Output :: String
        ## <summary>
        ## Current iteration (generation) count.
        ## </summary>
        Iteration :: Int
        ## <summary>
        ## Count of status prompts.
        ## </summary>
        StatusCount :: Int
        ## <summary>
        ## Number of instructions executed by the best program.
        ## </summary>
        Ticks :: Int
        ## <summary>
        ## Number of instructions executed by the best program, including function calls.
        ## </summary>
        TotalTicks :: Int
        ## <summary>
        ## Time of last improved evolution.
        ## </summary>
        LastChangeDate :: Dates.DateTime

        GAStatus() = new(0.0, 0.0, "", "", 0, 0, 0, 0, Dates.now())
    end

    #=struct targetParams
        targetString :: String
        targetFitness :: String
    end =#

end  # module Types


module CutPursuit

using CxxWrap

const depsjl_path = joinpath(@__DIR__, "..", "deps", "deps.jl")
if !isfile(depsjl_path)
    error("LibCutPursuit not installed properly, run Pkg.build(\"CutPursuit\"), restart Julia and try again")
end
include(depsjl_path)
check_deps()

function __init__()
    @initcxx
end

# Wrap cut pursuit c++ code

@wrapmodule libcpjl

export cut_pursuit

function test()
    # obs =
    # source =
    # target =
    # edge_weight =

    cut_pursuit(rand(100,2), Int32.(rand(1:100, 100)), Int32.(rand(1:100,100)), ones(Float64,100))
end

end # module


#####
##### Broadcasting utilities
#####

using Base.Broadcast: DefaultArrayStyle
using Base.Broadcast: Broadcasted
using CUDA

using Oceananigans.Architectures: device_event

struct FieldBroadcastStyle <: Broadcast.AbstractArrayStyle{3} end

Base.Broadcast.BroadcastStyle(::Type{<:AbstractField}) = FieldBroadcastStyle()

# Precedence rule
Base.Broadcast.BroadcastStyle(::FieldBroadcastStyle, ::DefaultArrayStyle{N}) where N = FieldBroadcastStyle()
Base.Broadcast.BroadcastStyle(::FieldBroadcastStyle, ::CUDA.CuArrayStyle{N}) where N = FieldBroadcastStyle()

# For use in Base.copy when broadcasting with numbers and arrays (useful for comparisons like f::AbstractField .== 0)
Base.similar(bc::Broadcasted{FieldBroadcastStyle}, ::Type{ElType}) where ElType = similar(Array{ElType}, axes(bc))

# Bypass style combining for in-place broadcasting with arrays / scalars to use built-in broadcasting machinery
const BroadcastedArrayOrCuArray = Union{Broadcasted{<:DefaultArrayStyle},
                                        Broadcasted{<:CUDA.CuArrayStyle}}

@inline Base.Broadcast.materialize!(dest::AbstractField, bc::BroadcastedArrayOrCuArray) =
    Base.Broadcast.materialize!(interior(dest), bc)

#####
##### Kernels
#####

@kernel function broadcast_kernel!(dest, bc)
    i, j, k = @index(Global, NTuple)
    @inbounds dest[i, j, k] = bc[i, j, k]
end

broadcasted_to_abstract_operation(loc, grid, a) = a

# Broadcasting with interpolation breaks Base's default rules for AbstractOperations
@inline Base.Broadcast.materialize!(::Base.Broadcast.BroadcastStyle,
                                    dest::AbstractField,
                                    bc::Broadcasted{<:FieldBroadcastStyle}) = copyto!(dest, convert(Broadcasted{Nothing}, bc))

@inline function Base.copyto!(dest::Field, bc::Broadcasted{Nothing})

    grid = dest.grid
    arch = architecture(dest)

    bc′ = broadcasted_to_abstract_operation(location(dest), grid, bc)

    event = launch!(arch, grid, :xyz, broadcast_kernel!, dest, bc′,
                    include_right_boundaries = true,
                    dependencies = device_event(arch),
                    reduced_dimensions = reduced_dimensions(dest),
                    location = location(dest))

    wait(device(arch), event)

    return dest
end


const RealOrComplex{T} = Union{T, Complex{T}} where T <: FFTReal
const PlanArrayPair{P,A} = Pair{P,A} where {P <: PencilPlan1D, A <: PencilArray}

# Types of array over which a PencilFFTPlan can operate.
# PencilArray and ManyPencilArray are respectively for out-of-place and in-place
# transforms.
const FFTArray{T,N} = Union{PencilArray{T,N}, ManyPencilArray{T,N}} where {T,N}

# Collections of FFTArray (e.g. for vector components), for broadcasting plans
# to each array. These types are basically those returned by `allocate_input`
# and `allocate_output` when optional arguments are passed.
const FFTArrayCollection =
    Union{Tuple{Vararg{A}}, AbstractArray{A}} where {A <: FFTArray}

# This allows to treat plans as scalars when broadcasting.
# This means that, if u = (u1, u2, u3) is a tuple of PencilArrays
# compatible with p, then p .* u does what one would expect, that is, it
# transforms the three components and returns a tuple.
Broadcast.broadcastable(p::PencilFFTPlan) = Ref(p)

# Forward transforms
function LinearAlgebra.mul!(
        dst::FFTArray{To,N}, p::PencilFFTPlan{T,N}, src::FFTArray{T,N},
    ) where {T, N, To <: RealOrComplex}
    @timeit_debug p.timer "PencilFFTs mul!" begin
        _check_arrays(p, src, dst)
        _apply_plans!(Val(FFTW.FORWARD), p, dst, src)
    end
end

# Backward transforms
function LinearAlgebra.ldiv!(
        dst::FFTArray{T,N}, p::PencilFFTPlan{T,N}, src::FFTArray{Ti,N},
    ) where {T, N, Ti <: RealOrComplex}
    @timeit_debug p.timer "PencilFFTs ldiv!" begin
        _check_arrays(p, dst, src)
        _apply_plans!(Val(FFTW.BACKWARD), p, dst, src)
    end
end

function Base.:*(p::PencilFFTPlan, src::FFTArray)
    dst = _maybe_allocate(allocate_output, p, src)
    mul!(dst, p, src)
end

function Base.:\(p::PencilFFTPlan, src::FFTArray)
    dst = _maybe_allocate(allocate_input, p, src)
    ldiv!(dst, p, src)
end

# Out-of-place version
_maybe_allocate(allocator::Function, p::PencilFFTPlan{T,N,false} where {T,N},
                ::PencilArray) = allocator(p)

# In-place version
_maybe_allocate(::Function, ::PencilFFTPlan{T,N,true} where {T,N},
                src::ManyPencilArray) = src

# Fallback case.
function _maybe_allocate(::Function, p::PencilFFTPlan, src::A) where {A}
    s = is_inplace(p) ? "in-place" : "out-of-place"
    throw(ArgumentError(
        "input array type $A incompatible with $s plans"))
end

function _check_arrays(
        p::PencilFFTPlan{T,N,false} where {T,N},
        Ain::PencilArray, Aout::PencilArray,
    )
    if Base.mightalias(Ain, Aout)
        throw(ArgumentError("out-of-place plan applied to aliased data"))
    end
    _check_pencils(p, Ain, Aout)
    nothing
end

function _check_arrays(
        p::PencilFFTPlan{T,N,true} where {T,N},
        Ain::ManyPencilArray, Aout::ManyPencilArray,
    )
    if Ain !== Aout
        throw(ArgumentError(
            "input and output arrays for in-place plan must be the same"))
    end
    _check_pencils(p, first(Ain), last(Ain))
    nothing
end

# Fallback case: plan type is incompatible with array types.
# For instance, plan is in-place, and at least one of the arrays is a regular
# PencilArray (instead of a ManyPencilArray).
function _check_arrays(p::PencilFFTPlan, ::Ai, ::Ao) where {Ai, Ao}
    s = is_inplace(p) ? "in-place" : "out-of-place"
    throw(ArgumentError(
        "array types ($Ai, $Ao) incompatible with $s plans"))
end

function _check_pencils(p::PencilFFTPlan, Ain::PencilArray, Aout::PencilArray)
    if first(p.plans).pencil_in !== pencil(Ain)
        throw(ArgumentError("unexpected dimensions of input data"))
    end
    if last(p.plans).pencil_out !== pencil(Aout)
        throw(ArgumentError("unexpected dimensions of output data"))
    end
    nothing
end

# Operations for collections.
function check_compatible(a::FFTArrayCollection, b::FFTArrayCollection)
    Na = length(a)
    Nb = length(b)
    if Na != Nb
        throw(ArgumentError("collections have different lengths: $Na ≠ $Nb"))
    end
    nothing
end

for f in (:mul!, :ldiv!)
    @eval LinearAlgebra.$f(dst::FFTArrayCollection, p::PencilFFTPlan,
                           src::FFTArrayCollection) =
        (check_compatible(dst, src); $f.(dst, p, src))
end

for f in (:*, :\)
    @eval Base.$f(p::PencilFFTPlan, src::FFTArrayCollection) =
        $f.(p, src)
end

@inline transform_info(::Val{FFTW.FORWARD}, p::PencilPlan1D{Ti,To}) where {Ti,To} =
    (Ti = Ti, To = To, Pi = p.pencil_in, Po = p.pencil_out, fftw_plan = p.fft_plan)

@inline transform_info(::Val{FFTW.BACKWARD}, p::PencilPlan1D{Ti,To}) where {Ti,To} =
    (Ti = To, To = Ti, Pi = p.pencil_out, Po = p.pencil_in, fftw_plan = p.bfft_plan)

# Out-of-place version
function _apply_plans!(
        dir::Val, full_plan::PencilFFTPlan{T,N,false} where {T,N},
        y::PencilArray, x::PencilArray)
    plans = let p = full_plan.plans
        # Backward transforms are applied in reverse order.
        dir === Val(FFTW.BACKWARD) ? reverse(p) : p
    end

    _apply_plans_out_of_place!(dir, full_plan, y, x, plans...)

    if dir === Val(FFTW.BACKWARD)
        # Scale transform.
        ldiv!(scale_factor(full_plan), y)
    end

    y
end

# In-place version
function _apply_plans!(
        dir::Val, full_plan::PencilFFTPlan{T,N,true} where {T,N},
        A::ManyPencilArray, A_again::ManyPencilArray)
    @assert A === A_again
    pairs = _make_pairs(full_plan.plans, A.arrays)

    # Backward transforms are applied in reverse order.
    pp = dir === Val(FFTW.BACKWARD) ? reverse(pairs) : pairs

    _apply_plans_in_place!(dir, full_plan, nothing, pp...)

    if dir === Val(FFTW.BACKWARD)
        # Scale transform.
        ldiv!(scale_factor(full_plan), first(A))
    end

    A
end

function _apply_plans_out_of_place!(
        dir::Val, full_plan::PencilFFTPlan, y::PencilArray, x::PencilArray,
        plan::PencilPlan1D, next_plans::Vararg{PencilPlan1D})
    @assert !is_inplace(full_plan) && !is_inplace(plan)
    r = transform_info(dir, plan)

    # Transpose data if required.
    u, t = if pencil(x) === r.Pi
        x, nothing
    else
        u = _temporary_pencil_array(r.Ti, r.Pi, full_plan.ibuf,
                                    full_plan.extra_dims)
        t = Transpositions.Transposition(u, x, method=full_plan.transpose_method)
        u, transpose!(t, waitall=false)
    end

    v = if pencil(y) === r.Po
        y
    else
        _temporary_pencil_array(r.To, r.Po, full_plan.obuf,
                                full_plan.extra_dims)
    end

    @timeit_debug full_plan.timer "FFT" mul!(parent(v), r.fftw_plan, parent(u))

    _wait_mpi_operations!(t, full_plan.timer)
    _apply_plans_out_of_place!(dir, full_plan, y, v, next_plans...)
end

_apply_plans_out_of_place!(dir::Val, ::PencilFFTPlan, y::PencilArray,
                           x::PencilArray) = y

# Wait for send operations to complete (only has an effect for specific
# transposition methods).
_wait_mpi_operations!(t, to) = @timeit_debug to "MPI.Waitall!" MPI.Waitall!(t)
_wait_mpi_operations!(::Nothing, to) = nothing

function _apply_plans_in_place!(
        dir::Val, full_plan::PencilFFTPlan, u_prev::Union{Nothing, PencilArray},
        pair::PlanArrayPair, next_pairs...)
    plan = pair.first
    u = pair.second
    r = transform_info(dir, plan)

    @assert is_inplace(full_plan) && is_inplace(plan)
    @assert pencil(u) === r.Pi === r.Po

    # Buffers should take no memory for in-place transforms.
    @assert length(full_plan.ibuf) == length(full_plan.obuf) == 0

    t = if u_prev === nothing
        nothing
    else
        # Transpose data from previous configuration.
        @assert Base.mightalias(u_prev, u)  # they're aliased!
        t = Transpositions.Transposition(u, u_prev,
                                         method=full_plan.transpose_method)
        transpose!(t, waitall=false)
    end

    # Perform in-place FFT
    @timeit_debug full_plan.timer "FFT!" r.fftw_plan * parent(u)

    _wait_mpi_operations!(t, full_plan.timer)
    _apply_plans_in_place!(dir, full_plan, u, next_pairs...)
end

_apply_plans_in_place!(::Val, ::PencilFFTPlan, u_prev::PencilArray) = u_prev

_split_first(a, b...) = (a, b)  # (x, y, z, w) -> (x, (y, z, w))

function _make_pairs(plans::Tuple{Vararg{PencilPlan1D,N}},
                     arrays::Tuple{Vararg{PencilArray,N}}) where {N}
    p, p_next = _split_first(plans...)
    a, a_next = _split_first(arrays...)
    (p => a, _make_pairs(p_next, a_next)...)
end

_make_pairs(::Tuple{}, ::Tuple{}) = ()

@inline function _temporary_pencil_array(
        ::Type{T}, p::Pencil, buf::Vector{UInt8}, extra_dims::Dims) where {T}
    # Create "unsafe" pencil array wrapping buffer data.
    dims = (size_local(p, MemoryOrder())..., extra_dims...)
    nb = prod(dims) * sizeof(T)
    resize!(buf, nb)
    x = Transpositions.unsafe_as_array(T, buf, dims)
    PencilArray(p, x)
end

_temporary_pencil_array(::Type, ::Nothing, etc...) = nothing


# * Scene

mutable struct Scene{O<:Intersectable{<:AbstractFloat}}
    objs::Vector{O}
end
# Scene(objs::Vector{O}) where {T,O<:Intersectable{T}} = Scene{T,O}(objs)
Scene{O}() where {O} = Scene(Vector{O}())

Base.eltype(::Scene{O}) where O = O

Base.show(io::IO, scene::S) where {O,S<:Scene{O}} =
    write(io, "$S with $(length(scene.objs)) $(O)s")

function Scene(::Type{O}, filename::String) where {T,O<:Intersectable{T}}
    scene = Scene{O}()
    append!(scene, FileIO.load(filename))
    scene
end

function Base.append!(scene::Scene{Triangle{T}}, mesh::HomogenousMesh) where T
    objs = map(mesh.faces) do face
        Triangle(SVector.(mesh.vertices[face]), SVector.(mesh.normals[face]))
    end
    append!(scene.objs, objs)
    scene
end

function Base.append!(scene::Scene{O}, objs::Vector{O}) where {T,O<:Intersectable{T}}
    append!(scene.objs, objs)
    scene
end

function extents(scene::Scene)
    e = extents.(scene.objs)
    join_extents(a,b) = [(min(a[i][1],b[i][1]),max(a[i][2],b[i][2])) for i=1:3]
    reduce(join_extents, e, init=e[1])
end

Images.center(scene::Scene) = mean.(extents(scene))

# ** Random scenes
rand_point(::Type{T}) where T = SVector{3}(2rand(T)-1,2rand(T)-1,2rand(T)-1)

Random.rand(::Type{Triangle{T}}) where T =
    Triangle((rand_point(T), rand_point(T), rand_point(T)))

Random.rand(::Type{Sphere{T}}) where T =
    Sphere(rand_point(T), T(0.2)*rand(T))

Random.rand(::Type{Intersectable{T}}) where T =
    rand(rand([Triangle{T}, Sphere{T}]))

Random.rand(::Type{O}, n::Integer) where {T,O<:Intersectable{T}} =
    [rand(O) for i=1:n]

Random.rand(::Type{S}, n::Integer) where {T,O<:Intersectable{T},S<:Scene{O}} =
    Scene(rand(O, n))

# ** Scene file format

Base.write(io::IOStream, v::SVector) = write(io, "(", join(string.(v), ","), ")")

function Base.write(io::IOStream, s::Sphere)
    write(io, "Sphere ", string(eltype(s)), " ")
    write(io, s.pos, " ")
    write(io, string(s.radius), "\n")
end

function Base.write(io::IOStream, t::Triangle)
    write(io, "Triangle ", string(eltype(t)), " ")
    for v in [t.o, t.e₁, t.e₂, t.na, t.nb, t.nc]
        write(io, v, " ")
    end
    write(io, "\n")
end

function FileIO.save(f::File{format"RSC"},
                     objs::AbstractVector{O}) where {T,O<:Intersectable{T}}
    open(f, "w") do file
        write(file, magic(format"RSC"))
        write(file, '\n')
        for o in objs
            write(file, o)
        end
    end
end

FileIO.save(f::File{format"RSC"}, scene::Scene) = save(f, scene.objs)

function Base.parse(V::Type{SVector{N,T}}, s::AbstractString) where {N,T<:Number}
    s[1] == '(' && s[end] == ')' || error("Malformed $(V), $(s)")
    s = s[2:end-1]
    V([parse(T,c) for c in split(s, ",")]...)
end

read_primitive(P::Type{Sphere{T}}, pos, radius) where {T<:Number} =
    P(parse(SVector{3,T}, pos), parse(T, radius))

read_primitive(P::Type{Triangle{T}}, o, e₁, e₂, na, nb, nc) where {T<:Number} =
    P(parse(SVector{3,T}, o),
      parse(SVector{3,T}, e₁),
      parse(SVector{3,T}, e₂),
      parse(SVector{3,T}, na),
      parse(SVector{3,T}, nb),
      parse(SVector{3,T}, nc))

function FileIO.load(f::File{format"RSC"})
    open(f) do file
        skipmagic(file)
        readline(stream(file))
        FileIO.load(file)
    end
end

function FileIO.load(s::Stream{format"RSC"})
    objs = Vector{Intersectable}()
    for line in eachline(stream(s))
        d = split(strip(line))
        P = eval(Symbol(d[1]))
        T = eval(Symbol(d[2]))
        push!(objs, read_primitive(P{T}, d[3:end]...))
    end
    O = promote_type(unique(typeof.(objs))...)
    Scene{O}(objs)
end

export Scene, extents, save, load


##################################################
############### LOAD DATA ########################
##################################################

include(joinpath(dirname(@__FILE__), "data_tests/test01.jl"))
omib_sys_file = System(PowerModelsData(omib_file_dir), runchecks = false)

##################################################
############### SOLVE PROBLEM ####################
##################################################
#Define Fault: Change of YBus
Ybus_change = NetworkSwitch(
    1.0, #change at t = 1.0
    Ybus_fault,
) #New YBus

@testset "Make Simulation" begin
    path1 = (joinpath(pwd(), "test-Base-1"))
    !isdir(path1) && mkdir(path1)
    try
        #Define Simulation Problem
        sim = Simulation(
            ImplicitModel,
            omib_sys, #system
            path1,
            (0.0, 30.0), #time span
            Ybus_change;
            system_to_file = true,
        )
        @test sim.status == PSID.BUILT
        o_system = System(joinpath(path1, "input_system.json"))
        for b in get_components(Bus, o_system)
            b_sys = get_component(Bus, omib_sys, get_name(b))
            b_file = get_component(Bus, omib_sys_file, get_name(b))
            @test get_angle(b) == get_angle(b_sys)
            @test get_angle(b) == get_angle(b_file)
        end

    finally
        @info("removing test files")
        rm(path1, force = true, recursive = true)
    end

    path2 = (joinpath(pwd(), "test-Base-2"))
    !isdir(path2) && mkdir(path2)
    try
        #Define Simulation Problem
        sim = Simulation!(
            ImplicitModel,
            omib_sys, #system
            path2,
            (0.0, 30.0), #time span
            Ybus_change;
            system_to_file = true,
        )
        @test sim.status == PSID.BUILT
        m_system = System(joinpath(path2, "initialized_system.json"))
        for b in get_components(Bus, m_system)
            b_sys = get_component(Bus, omib_sys, get_name(b))
            b_file = get_component(Bus, omib_sys_file, get_name(b))
            @test get_angle(b) == get_angle(b_sys)
            if get_bustype(b) == PSY.BusTypes.REF
                @test get_angle(b) == get_angle(b_file)
            else
                @test get_angle(b) != get_angle(b_file)
            end
        end

    finally
        @info("removing test files")
        rm(path2, force = true, recursive = true)
    end
end

@testset "Hybrid Line Indexing" begin
    ## Create threebus system with more dyn lines ##
    three_bus_file_dir = joinpath(dirname(@__FILE__), "data_tests/ThreeBusInverter.raw")
    threebus_sys_dyns = System(three_bus_file_dir, runchecks = false)
    add_source_to_ref(threebus_sys_dyns)

    dyn_branch12 =
        DynamicBranch(get_component(Branch, threebus_sys_dyns, "BUS 1-BUS 2-i_2"))
    dyn_branch23 =
        DynamicBranch(get_component(Branch, threebus_sys_dyns, "BUS 2-BUS 3-i_3"))

    add_component!(threebus_sys_dyns, dyn_branch12)
    add_component!(threebus_sys_dyns, dyn_branch23)

    # Attach dyn devices
    for g in get_components(Generator, threebus_sys_dyns)
        if get_number(get_bus(g)) == 102
            case_gen = dyn_gen_second_order(g)
            add_component!(threebus_sys_dyns, case_gen, g)
        elseif get_number(get_bus(g)) == 103
            case_inv = inv_case78(g)
            add_component!(threebus_sys_dyns, case_inv, g)
        end
    end

    # Tests for all Dynamic Lines
    sim = Simulation(ImplicitModel, threebus_sys_dyns, pwd(), (0.0, 10.0))
    @test sim.status == PSID.BUILT
    sim_inputs = sim.simulation_inputs
    dae_vector = PSID.get_DAE_vector(sim_inputs)
    @test all(dae_vector)
    total_shunts = PSID.get_total_shunts(sim_inputs)
    for v in LinearAlgebra.diag(total_shunts)
        @test imag(v) > 0
    end

    for entry in LinearAlgebra.diag(sim_inputs.mass_matrix)
        @test entry > 0
    end
    voltage_buses_ix = PSID.get_voltage_buses_ix(sim_inputs)
    @test length(voltage_buses_ix) == 3

    # Tests for dynamic lines with b = 0
    set_b!(dyn_branch12, (from = 0.0, to = 0.0))
    set_b!(dyn_branch23, (from = 0.0, to = 0.0))
    sim = Simulation(ImplicitModel, threebus_sys_dyns, pwd(), (0.0, 10.0))
    @test sim.status == PSID.BUILT
    sim_inputs = sim.simulation_inputs
    dae_vector = PSID.get_DAE_vector(sim_inputs)
    @test sum(.!dae_vector) == 6
    for (ix, entry) in enumerate(dae_vector)
        if !entry
            @test LinearAlgebra.diag(sim_inputs.mass_matrix)[ix] == 0
        elseif entry
            @test LinearAlgebra.diag(sim_inputs.mass_matrix)[ix] == 1
        else
            @test false
        end
    end

    total_shunts = PSID.get_total_shunts(sim_inputs)
    @test sum(LinearAlgebra.diag(total_shunts)) == 0.0
    voltage_buses_ix = PSID.get_voltage_buses_ix(sim_inputs)
    @test isempty(voltage_buses_ix)
end