@testset "cat" begin
    a = [10 20; 31 40]
    nda = NamedDimsArray(a, (:x, :y))

    @testset "basic functionality" begin
        # check cat when NDAs are involved gives the same result as cat on arrays
        for d in 1:3
            @test cat(a; dims=d) ==
                  parent(cat(nda; dims=d))
            @test cat(a, a; dims=d) ==
                  parent(cat(nda, a; dims=d)) ==
                  parent(cat(a, nda; dims=d)) ==
                  parent(cat(nda, nda; dims=d))
            @test cat(a, a, a; dims=d) ==
                  parent(cat(a, nda, nda; dims=d)) ==
                  parent(cat(nda, a, nda; dims=d)) ==
                  parent(cat(nda, nda, nda; dims=d))
        end
    end

    @testset "dimensions requirements" begin
        # check that conflicting dimensions are flagged as errors
        for d in 1:3
            @test_throws DimensionMismatch cat(nda, nda'; dims=d)
            @test_throws DimensionMismatch cat(nda, nda, nda'; dims=d)
            @test_throws DimensionMismatch cat(a, nda, nda'; dims=d)
            @test_skip @test_throws DimensionMismatch cat(a, a, nda, nda'; dims=d)
        end

        # check that dimnames work as expected (for one, two, three args)
        for d in 1:2
            @test dimnames(cat(nda; dims=d)) == dimnames(nda)

            @test dimnames(cat(nda, nda; dims=d)) == dimnames(nda)
            @test dimnames(cat(nda, a; dims=d)) == dimnames(nda)
            @test dimnames(cat(a, nda; dims=d)) == dimnames(nda)

            @test dimnames(cat(nda, nda, nda; dims=d)) == dimnames(nda)
            @test dimnames(cat(a, nda, nda; dims=d)) == dimnames(nda)
            @test dimnames(cat(nda, a, nda; dims=d)) == dimnames(nda)
        end

        # and the same thing for dims=3
        @test dimnames(cat(nda; dims=3)) == (dimnames(nda)..., :_)

        @test dimnames(cat(nda, nda; dims=3)) == (dimnames(nda)..., :_)
        @test dimnames(cat(nda, a; dims=3)) == (dimnames(nda)..., :_)
        @test dimnames(cat(a, nda; dims=3)) == (dimnames(nda)..., :_)

        @test dimnames(cat(nda, nda, nda; dims=3)) == (dimnames(nda)..., :_)
        @test dimnames(cat(a, nda, nda; dims=3)) == (dimnames(nda)..., :_)
        @test dimnames(cat(nda, a, nda; dims=3)) == (dimnames(nda)..., :_)
    end

    # check dims::Symbol cases
    @testset "dims argument is named" begin
        @test cat(nda, nda; dims=1) == cat(nda, nda; dims=:x)
        @test cat(nda, nda; dims=2) == cat(nda, nda; dims=:y)

        @test dimnames(cat(nda, nda; dims=:z)) == (:x, :y, :z)
    end
end

for (f, d) in zip((vcat, hcat), (1, 2))
    a = [10 20; 31 40]
    nda = NamedDimsArray(a, (:x, :y))
    v = [1, 2, 3, 4]
    ndv = NamedDimsArray(v, (:x, ))

    @testset "$f" begin
        @testset "basic functionality" begin
            @test f(nda) == cat(nda; dims=d)
            @test f(ndv) == cat(ndv; dims=d)

            @test f(nda, nda) == cat(nda, nda; dims=d)
            @test f(a, nda) == cat(a, nda; dims=d)
            @test f(nda, a) == cat(nda, a; dims=d)

            @test f(nda, nda, nda) == cat(nda, nda, nda; dims=d)
            @test f(a, nda, nda) == cat(a, nda, nda; dims=d)
            @test f(nda, a, nda) == cat(nda, a, nda; dims=d)
            @test f(nda, nda, a) == cat(nda, nda, a; dims=d)
        end

        @testset "dimension requirements" begin
            @test_throws DimensionMismatch f(nda, nda')
            @test_throws DimensionMismatch f(nda, nda, nda')

            @test dimnames(f(nda)) == dimnames(cat(nda; dims=d))
            @test dimnames(f(ndv)) == dimnames(cat(ndv; dims=d))

            @test dimnames(f(nda, nda)) == dimnames(cat(nda, nda; dims=d))
            @test dimnames(f(nda, a)) == dimnames(cat(nda, a; dims=d))
            @test dimnames(f(a, nda)) == dimnames(cat(a, nda; dims=d))
        end
    end
end


using BusinessDays
import BusinessDays.isholiday

type CustomCalendar <: HolidayCalendar end

isholiday(::CustomCalendar, dt::Date) = dt == Date(2015,8,27)

cc = CustomCalendar()
println("$(isholiday(cc, Date(2015,8,26)))")
println("$(isholiday(cc, Date(2015,8,27)))")


using st16SseqJuliaTools
if abspath(PROGRAM_FILE) == @__FILE__
    map = read_jc_clusters(ARGS[1])
    sizes = Array{Tuple{String,Int64},1}()
    for k in keys(map)
        push!(sizes,(k,length(map[k])))
    end
    sort!(sizes,by=x->x[2],rev=true)
    for p in map[sizes[1][1]]
        println(p)
    end
end


function tuning_display(p)
    lines=show(p.output, p.tuner, progress=true)
    println();
    return lines
end

function convergence_display(p)
    try
        ciplot=lineplot([p.counter-(length(p.convergence_history)-1):p.counter...], p.convergence_history, title="Convergence Interval Recent History", xlabel="Iterate",ylabel="CI", color=:yellow)
        lines=nrows(ciplot.graphics)+5
        show(p.output, ciplot); println()
        return lines
    catch
        printstyled(p.output, "CONVERGENCE PLOT UNAVAILABLE. STANDBY\n", bold=true, color=:yellow); println()
        return 2
    end
end

function evidence_display(p)
    try
        log_Zis=p.e.log_Zi[2:end]
        posidx=findfirst(i->i > -Inf, log_Zis)
        log_Zis[1:posidx-1].=log_Zis[posidx]
        evplot=lineplot(log_Zis, title="Evidence History", xlabel="Iterate", color=:red, name="Ensemble logZ")
        lines=nrows(evplot.graphics)+5
        show(p.output, evplot); println()
        return lines
    catch
        printstyled(p.output, "EVIDENCE PLOT UNAVAILABLE. STANDBY\n", bold=true, color=:red); println()
        return 2
    end
end

function info_display(p)
    try
        infoplot=lineplot(p.e.Hi[2:end], title="Information History", xlabel="Iterate", color=:green, name="Ensemble H")
        lines=nrows(infoplot.graphics)+5
        show(p.output, infoplot); println()
        return lines
    catch
        printstyled(p.output, "INFORMATION PLOT UNAVAILABLE. STANDBY\n", bold=true, color=:green); println()
        return 2
    end
end

function lh_display(p)
    try
        lhplot=lineplot(p.e.log_Li[2:end], title="Contour History", xlabel="Iterate", color=:magenta, name="Ensemble logLH")
        lines=nrows(lhplot.graphics)+5
        show(p.output, lhplot); println()
        return lines
    catch
        printstyled(p.output, "CONTOUR HISTORY UNAVAILABLE. STANDBY\n", bold=true, color=:magenta); println()
        return 2
    end
end

function liwi_display(p)
    try
        liwiplot=lineplot([max(2,p.counter-(CONVERGENCE_MEMORY-1)):p.counter...],p.e.log_Liwi[max(2,end-(CONVERGENCE_MEMORY-1)):end], title="Recent iterate evidentiary weight", xlabel="Iterate", name="Ensemble log Liwi", color=:cyan)
        lines=nrows(liwiplot.graphics)+5
        show(p.output, liwiplot); println()
        return lines
    catch
        printstyled(p.output, "EVIDENTIARY HISTORY UNAVAILABLE. STANDBY\n", bold=true, color=:cyan); println()
        return 2
    end
end

function ensemble_display(p)
    return lines=show(p.output, p.e, progress=true)
end

function model_display(p)
    try
        println("Current MAP model:")
        return lines=show(p.output, p.top_m, progress=true)
    catch
        printstyled(p.output, "MODEL DISPLAY UNAVAILABLE\n", bold=true, color=:blue); println()
        return 3
    end
end

function model_obs_display(p)
    try
        println("Current MAP model")
        return lines=show(p.output, p.top_m, p.e, progress=true)
    catch
        printstyled(p.output, "MODEL DISPLAY UNAVAILABLE\n", bold=true, color=:blue); println()
        return 3
    end
end


# =============================
# Successor for discrete system
# =============================

"""
    successor(system::DiscreteIdentitySystem, x::AbstractVector)

Return the successor state of a `DiscreteIdentitySystem`.

### Input

- `system` -- `DiscreteIdentitySystem`
- `x`      -- state (it should be any vector type)

### Output

The same state `x`.
"""
function successor(system::DiscreteIdentitySystem, x::AbstractVector)
    !_is_conformable_state(system, x) && _argument_error(:x)
    return x
end

"""
    successor(system::ConstrainedDiscreteIdentitySystem, x::AbstractVector;
              [check_constraints]=true)

Return the successor state of a `ConstrainedDiscreteIdentitySystem`.

### Input

- `system`            -- `ConstrainedDiscreteIdentitySystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The same state `x`.
"""
function successor(system::ConstrainedDiscreteIdentitySystem, x::AbstractVector;
                   check_constraints::Bool=true)
    !_is_conformable_state(system, x) && _argument_error(:x)
    if check_constraints
        !_in_stateset(system, x) && _argument_error(:x,:X)
    end
    return x
end

"""
    successor(system::AbstractDiscreteSystem, x::AbstractVector;
              [check_constraints]=true)

Return the successor state of an `AbstractDiscreteSystem`.

### Input

- `system`            -- `AbstractDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to state `x`.
"""
successor(system::AbstractDiscreteSystem, x::AbstractVector; kwargs...) =
    _instantiate(system, x; kwargs...)

"""
    successor(system::AbstractDiscreteSystem, x::AbstractVector, u::AbstractVector;
              [check_constraints]=true)

Return the successor state of an `AbstractDiscreteSystem`.

### Input

- `system`            -- `AbstractDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type) or noise, if `system`
                         is not controlled
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to state `x` and input `u`.

### Notes

If the system is not controlled but noisy, the input `u` is interpreted as noise.
    """
successor(system::AbstractDiscreteSystem, x::AbstractVector, u::AbstractVector; kwargs...) =
    _instantiate(system, x, u; kwargs...)

"""
    successor(system::AbstractDiscreteSystem,
              x::AbstractVector, u::AbstractVector, w::AbstractVector; [check_constraints]=true)

Return the successor state of an `AbstractDiscreteSystem`.

### Input

- `system`            -- `AbstractDiscreteSystem`
- `x`                 -- state (it should be any vector type)
- `u`                 -- input (it should be any vector type)
- `w`                 -- noise (it should be any vector type)
- `check_constraints` -- (optional, default: `true`) check if the state belongs to
                         the state set

### Output

The result of applying the system to state `x`, input `u` and noise `w`.
"""
successor(system::AbstractDiscreteSystem, x::AbstractVector, u::AbstractVector, w::AbstractVector; kwargs...) =
    _instantiate(system, x, u, w; kwargs...)


# The Numerical stability checks code in this file is based on the code from the SDDP.jl package,
# from the below mentioned commit and file.
# commit :8cd305188caffc50a1734913053fc81bba613778
# link to file :https://github.com/odow/SDDP.jl/blob/d353fe5a2903421e7fed6d609eb9377c35d715a1/src/print.jl#L190

########### JuMP model utils #########

mutable struct NumericalBounds
    min::Float64
    max::Float64
    min_index::Any
    max_index::Any
end

NumericalBounds() = NumericalBounds(Inf, -Inf, nothing, nothing)

set_min!(v::NumericalBounds, value::Real) = v.min = value
set_max!(v::NumericalBounds, value::Real) = v.max = value
set_min_index!(v::NumericalBounds, idx) = v.min_index = idx
set_max_index!(v::NumericalBounds, idx) = v.max_index = idx

mutable struct ConstraintBounds
    coefficient::NumericalBounds
    rhs::NumericalBounds
    function ConstraintBounds()
        return new(NumericalBounds(), NumericalBounds())
    end
end

function _update_coefficient_bounds(
    v::ConstraintBounds,
    constraint::JuMP.ScalarConstraint,
    idx,
)
    _update_numerical_bounds(v.coefficient, constraint.func, idx)
    return
end

function _update_rhs_bounds(v::ConstraintBounds, constraint::JuMP.ScalarConstraint, idx)
    _update_numerical_bounds(v.rhs, constraint.set, idx)
end

mutable struct VariableBounds
    bounds::NumericalBounds
    function VariableBounds()
        return new(NumericalBounds())
    end
end

function _update_variable_bounds(v::VariableBounds, variable::JuMP.VariableRef, idx)
    if JuMP.is_binary(variable)
        set_min!(v.bounds, 0.0)
        _update_numerical_bounds(v.bounds, 1.0, idx)
    else
        if JuMP.has_lower_bound(variable)
            _update_numerical_bounds(v.bounds, JuMP.lower_bound(variable), idx)
        end
        if JuMP.has_upper_bound(variable)
            _update_numerical_bounds(v.bounds, JuMP.upper_bound(variable), idx)
        end
    end
    return
end

function _update_numerical_bounds(v::NumericalBounds, value::Real, idx)
    if !isapprox(value, 0.0)
        if v.min > abs(value)
            set_min!(v, value)
            set_min_index!(v, idx)
        elseif v.max < abs(value)
            set_max!(v, value)
            set_max_index!(v, idx)
        end
    end
    return
end

function _update_numerical_bounds(bonuds::NumericalBounds, func::JuMP.GenericAffExpr, idx)
    for coefficient in values(func.terms)
        _update_numerical_bounds(bonuds, coefficient, idx)
    end
    return
end

function _update_numerical_bounds(bonuds::NumericalBounds, func::MOI.LessThan, idx)
    return _update_numerical_bounds(bonuds, func.upper, idx)
end

function _update_numerical_bounds(bonuds::NumericalBounds, func::MOI.GreaterThan, idx)
    return _update_numerical_bounds(bonuds, func.lower, idx)
end

function _update_numerical_bounds(bonuds::NumericalBounds, func::MOI.EqualTo, idx)
    return _update_numerical_bounds(bonuds, func.value, idx)
end

function _update_numerical_bounds(bonuds::NumericalBounds, func::MOI.Interval, idx)
    _update_numerical_bounds(bonuds, func.upper, idx)
    return _update_numerical_bounds(bonuds, func.lower, idx)
end

# Default fallback for unsupported constraints.
_update_numerical_bounds(range::NumericalBounds, func, idx) = nothing

function get_constraint_numerical_bounds(model::OperationsProblem; verbose = false)
    if verbose
        constraint_bounds = Dict()
        for (const_key, constriant_array) in
            get_constraints(get_optimization_container(model))
            bounds = ConstraintBounds()
            for idx in Iterators.product(constriant_array.axes...)
                con_obj = JuMP.constraint_object(constriant_array[idx...])
                _update_coefficient_bounds(bounds, con_obj, idx)
                _update_rhs_bounds(bounds, con_obj, idx)
            end
            constraint_bounds[const_key] = bounds
        end
        return constraint_bounds
    else
        bounds = ConstraintBounds()
        for (const_key, constriant_array) in
            get_constraints(get_optimization_container(model))
            for idx in Iterators.product(constriant_array.axes...)
                con_obj = JuMP.constraint_object(constriant_array[idx...])
                _update_coefficient_bounds(bounds, con_obj, (const_key, idx))
                _update_rhs_bounds(bounds, con_obj, (const_key, idx))
            end
        end
        return bounds
    end
end

function get_variable_numerical_bounds(model::OperationsProblem; verbose = false)
    if verbose
        variable_bounds = Dict()
        for (variable_key, variable_array) in
            get_variables(get_optimization_container(model))
            bounds = VariableBounds()
            for idx in Iterators.product(variable_array.axes...)
                var = variable_array[idx...]
                _update_variable_bounds(bounds, var, idx)
            end
            variable_bounds[variable_key] = bounds
        end
        return variable_bounds
    else
        bounds = VariableBounds()
        for (variable_key, variable_array) in
            get_variables(get_optimization_container(model))
            for idx in Iterators.product(variable_array.axes...)
                var = variable_array[idx...]
                _update_variable_bounds(bounds, var, (variable_key, idx))
            end
        end
        return bounds
    end
end


# This file was generated, do not modify it. # hide
using MLJ, RDatasets, ScientificTypes, PrettyPrinting

@load LinearRegressor pkg=MLJLinearModels
@load RidgeRegressor pkg=MLJLinearModels
@load LassoRegressor pkg=MLJLinearModels

hitters = dataset("ISLR", "Hitters")
@show size(hitters)
names(hitters) |> pprint


module MLLabelUtils

using StatsBase
using LearnBase
using MappedArrays
import Base.Broadcast: broadcastable

export

    ind2label,
    label2ind,

    labeltype,

    label,
    nlabel,

    poslabel,
    neglabel,
    isposlabel,
    isneglabel,

    ObsDim,
    classify,
    classify!,

    convertlabel,
#    convertlabel!,

    labelmap,
    labelmap!,
    labelfreq,
    labelfreq!,
    labelmap2vec,

    LabelEnc,
    labelenc,
    islabelenc,

    convertlabelview

include("learnbase.jl")
include("labelencoding.jl")
include("classify.jl")
include("convertlabel.jl")
include("labelmap.jl")

end # module


# Utilities for `TimeArray`.

# Import modules.

using Dates, TimeSeries

import Base.map

"""
Align two timeseries according to their times, padding values from earlier times.

# Arguments
- `x: TimeArray{X,1,T,Array{X,1}}`: the first timeseries
- `y: TimeArray{Y,1,T,Array{Y,1}}`: the first timeseries

# Example
```
x = TimeArray(
    [Time(0), Time(1), Time(3), Time(5)],
    [    0.1,     1.1,     3.1,     5.1]
)
y = TimeArray(
    [Time(0), Time(1), Time(2), Time(5), Time(6)],
    [    0.1,     1.1,     2.1,     5.1,     6.1]
)
z = align(x, y)
```
"""
function aligntimes(x :: TimeArray{X,1,T,Array{X,1}}, y :: TimeArray{Y,1,T,Array{Y,1}}) :: TimeArray{Tuple{X,Y},1,T,Array{Tuple{X,Y},1}} where X where Y where T <: TimeType
    nx = length(x)
    ny = length(y)
    ix = 1
    iy = 1
    xt = timestamp(x)
    yt = timestamp(y)
    zt = Array{T,1}()
    xv = values(x)
    yv = values(y)
    zv = Array{Tuple{X,Y},1}()
    if nx == 0 || ny == 0
        throw(ArgumentError("align: TimeArrays must not be empty."))
    elseif xt[1] != yt[1]
        throw(ArgumentError("align: TimeArrays must start at the same timestamp."))
    end
    while ix <= nx || iy <= ny
        if ix <= nx && (iy > ny || xt[ix] < yt[iy])
            push!(zt, xt[ix])
            push!(zv, (xv[ix], yv[iy-1]))
            ix = ix + 1
        elseif iy <= ny && (ix > nx || xt[ix] > yt[iy])
            push!(zt, yt[iy])
            push!(zv, (xv[ix-1], yv[iy]))
            iy = iy + 1
        else
            push!(zt, xt[ix])
            push!(zv, (xv[ix], yv[iy]))
            ix = ix + 1
            iy = iy + 1
        end
    end
    TimeArray(zt, zv)
end

"""
Compute the time difference corresponding to a fractional number of hours.

# Arguments
- `delta` : the fractional number of hours
"""
function fractionalhours(delta) :: Nanosecond
    onehour = Time(1) - Time(0)
    if false # Old implementation.
        resolution = 60 * 60 * 1000 # Resolve to the nearest millisecond.
        onehour * trunc(Int64, resolution * delta) / resolution
    else # New implementation.
        typeof(onehour)(trunc(Int64, delta * onehour.value))
    end
end

"""
Add a fractional number of hours to a time.

# Arguments
- `t :: Time`: the time
- `delta`    : the number of hours to add
"""
function addhours(t :: Time, delta) :: Time
    onehour = Time(1) - Time(0)
    t1 = t.instant / onehour + delta
    if t1 > 24
        Time(23, 59, 59, 999, 999, 999)
    else
        t + fractionalhours(delta)
    end
end

"""
Add a fractional number of hours to a time.

# Arguments
- `t :: Time`: the time
- `delta`    : the number of hours to add
"""
function addhours(t :: TimeType, delta) :: TimeType
    onehour = Time(1) - Time(0)
    t + delta * onehour
end

"""
Map the values in a time array.

# Arguments
- `f`                                : the function to apply to the values
- `x :: TimeArray{X,1,T,Array{x,1}}` : the time array
"""
function map(f, x :: TimeArray{X,1,T,Array{X,1}}) where X where T <: TimeType
    xt = timestamp(x)
    xv = values(x)
    TimeArray(xt, map(f, xv))
end


module TestInsert
using Test
using StaticArrays
using Accessors
using Accessors: insert

@testset "test insert" begin
    @testset "function" begin
        @test @inferred(insert( (b=2, c=3), @optic(_.a), 1 )) == (b=2, c=3, a=1)
        let A = [1, 2]
            @test insert(A, @optic(_[2]), 3) == [1, 3, 2]
            @test_throws BoundsError insert(A, @optic(_[4]), 3)
            @test_throws Exception insert(A, @optic(_[1, 3]), 3)
            @test insert(A, first, 3) == [3, 1, 2]
            @test A == [1, 2]  # not changed
        end
        @test insert((1,2), last, 3) == (1, 2, 3)
        Base.thisminor(VERSION) == v"1.6" && @inferred(insert((1,2), last, 3))
        @test @inferred(insert(SVector(1,2), @optic(_[1]), 3)) == SVector(3, 1, 2)
        @test @inferred(insert(SVector(1,2), last, 3)) == SVector(1, 2, 3)
        let D = Dict(:a => 1)
            @test insert(D, @optic(_[:b]), 2) == Dict(:a => 1, :b => 2)
            @test D == Dict(:a => 1)  # not changed
        end
        @test insert((a=1, b=(2, 3)), @optic(_.b[2]), "xxx") === (a=1, b=(2, "xxx", 3))
        @test_broken begin
            @inferred insert((a=1, b=(2, 3)), @optic(_.b[2]), "xxx")
            true
        end
    end

    @testset "macro" begin
        x = (b=2, c=3)
        @test @insert(x.a = 1) == (b=2, c=3, a=1)
        x = [1, 2]
        @test @insert(x[3] = 3) == [1, 2, 3]
        x = (a=(b=(1, 2),), c=1)
        @test @insert(x.a.b[1] = 0) == (a=(b=(0, 1, 2),), c=1)
    end
end

end


#
# export JULIA_DEPOT_PATH=`pwd`/.julia
#
using Pkg
Pkg.instantiate()

using ClimateMachine


using .Turing: Turing, DynamicPPL, MCMCChains
using Accessors: Accessors

# utilities for working with Turing model parameter names using only the DynamicPPL API

"""
    flattened_varnames_list(model::DynamicPPL.Model) -> Vector{Symbol}

Get a vector of varnames as `Symbol`s with one-to-one correspondence to the
flattened parameter vector.

```julia
julia> @model function demo()
           s ~ Dirac(1)
           x = Matrix{Float64}(undef, 2, 4)
           x[1, 1] ~ Dirac(2)
           x[2, 1] ~ Dirac(3)
           x[3] ~ Dirac(4)
           y ~ Dirac(5)
           x[4] ~ Dirac(6)
           x[:, 3] ~ arraydist([Dirac(7), Dirac(8)])
           x[[2, 1], 4] ~ arraydist([Dirac(9), Dirac(10)])
           return s, x, y
       end
demo (generic function with 2 methods)

julia> flattened_varnames_list(demo())
10-element Vector{Symbol}:
 :s
 Symbol("x[1,1]")
 Symbol("x[2,1]")
 Symbol("x[3]")
 Symbol("x[4]")
 Symbol("x[:,3][1]")
 Symbol("x[:,3][2]")
 Symbol("x[[2, 1],4][1]")
 Symbol("x[[2, 1],4][2]")
 :y
```
"""
function flattened_varnames_list(model::DynamicPPL.Model)
    varnames_ranges = varnames_to_ranges(model)
    nsyms = maximum(maximum, values(varnames_ranges))
    syms = Vector{Symbol}(undef, nsyms)
    for (var_name, range) in varnames_to_ranges(model)
        sym = Symbol(var_name)
        if length(range) == 1
            syms[range[begin]] = sym
            continue
        end
        for i in eachindex(range)
            syms[range[i]] = Symbol("$sym[$i]")
        end
    end
    return syms
end

# code snippet shared by @torfjelde
"""
    varnames_to_ranges(model::DynamicPPL.Model)
    varnames_to_ranges(model::DynamicPPL.VarInfo)
    varnames_to_ranges(model::DynamicPPL.Metadata)

Get `Dict` mapping variable names in model to their ranges in a corresponding parameter vector.

# Examples

```julia
julia> @model function demo()
           s ~ Dirac(1)
           x = Matrix{Float64}(undef, 2, 4)
           x[1, 1] ~ Dirac(2)
           x[2, 1] ~ Dirac(3)
           x[3] ~ Dirac(4)
           y ~ Dirac(5)
           x[4] ~ Dirac(6)
           x[:, 3] ~ arraydist([Dirac(7), Dirac(8)])
           x[[2, 1], 4] ~ arraydist([Dirac(9), Dirac(10)])
           return s, x, y
       end
demo (generic function with 2 methods)

julia> demo()()
(1, Any[2.0 4.0 7 10; 3.0 6.0 8 9], 5)

julia> varnames_to_ranges(demo())
Dict{AbstractPPL.VarName, UnitRange{Int64}} with 8 entries:
  s           => 1:1
  x[4]        => 5:5
  x[:,3]      => 6:7
  x[1,1]      => 2:2
  x[2,1]      => 3:3
  x[[2, 1],4] => 8:9
  x[3]        => 4:4
  y           => 10:10
```
"""
function varnames_to_ranges end

varnames_to_ranges(model::DynamicPPL.Model) = varnames_to_ranges(DynamicPPL.VarInfo(model))
function varnames_to_ranges(varinfo::DynamicPPL.UntypedVarInfo)
    return varnames_to_ranges(varinfo.metadata)
end
function varnames_to_ranges(varinfo::DynamicPPL.TypedVarInfo)
    offset = 0
    dicts = map(varinfo.metadata) do md
        vns2ranges = varnames_to_ranges(md)
        vals = collect(values(vns2ranges))
        vals_offset = map(r -> offset .+ r, vals)
        offset += reduce((curr, r) -> max(curr, r[end]), vals; init=0)
        Dict(zip(keys(vns2ranges), vals_offset))
    end

    return reduce(merge, dicts)
end
function varnames_to_ranges(metadata::DynamicPPL.Metadata)
    idcs = map(Base.Fix1(getindex, metadata.idcs), metadata.vns)
    ranges = metadata.ranges[idcs]
    return Dict(zip(metadata.vns, ranges))
end

function pathfinder(
    model::DynamicPPL.Model;
    rng=Random.GLOBAL_RNG,
    init_scale=2,
    init_sampler=UniformSampler(init_scale),
    init=nothing,
    kwargs...,
)
    var_names = flattened_varnames_list(model)
    prob = Turing.optim_problem(model, Turing.MAP(); constrained=false, init_theta=init)
    init_sampler(rng, prob.prob.u0)
    result = pathfinder(prob.prob; rng, input=model, kwargs...)
    draws = reduce(vcat, transpose.(prob.transform.(eachcol(result.draws))))
    chns = MCMCChains.Chains(draws, var_names; info=(; pathfinder_result=result))
    result_new = Accessors.@set result.draws_transformed = chns
    return result_new
end

function multipathfinder(
    model::DynamicPPL.Model,
    ndraws::Int;
    rng=Random.GLOBAL_RNG,
    init_scale=2,
    init_sampler=UniformSampler(init_scale),
    nruns::Int,
    kwargs...,
)
    var_names = flattened_varnames_list(model)
    fun = Turing.optim_function(model, Turing.MAP(); constrained=false)
    init1 = fun.init()
    init = [init_sampler(rng, init1)]
    for _ in 2:nruns
        push!(init, init_sampler(rng, deepcopy(init1)))
    end
    result = multipathfinder(fun.func, ndraws; rng, input=model, init, kwargs...)
    draws = reduce(vcat, transpose.(fun.transform.(eachcol(result.draws))))
    chns = MCMCChains.Chains(draws, var_names; info=(; pathfinder_result=result))
    result_new = Accessors.@set result.draws_transformed = chns
    return result_new
end


module LazyWAVFiles
    using WAV, LazyArrays
    export LazyWAVFile, DistributedWAVFile
    struct LazyWAVFile{T,N,S<:Tuple} <: AbstractArray{T,N}
        path::String
        size::S
    end
    function LazyWAVFile(path)
        r = wavread(path, format="native", subrange=1)[1]
        s = wavread(path, format="size")
        T = eltype(r)
        N = length(s[2])
        if N == 1
            s = (s[1],)
        end
        LazyWAVFile{T,N,typeof(s)}(path, s)
    end
    Base.size(f::LazyWAVFile) = f.size
    Base.size(f::LazyWAVFile{T,N},i) where {T,N} = i > N ? 1 : f.size[i]
    Base.length(f::LazyWAVFile) = prod(f.size)

    Base.getindex(f::LazyWAVFile{T,N}, i::Number) where {T,N} = wavread(f.path, format="native", subrange=i:i)[1][1]::T
    Base.getindex(f::LazyWAVFile{T,1}, i::AbstractRange) where {T} = vec(wavread(f.path, format="native", subrange=i)[1])::Array{T,1}
    Base.getindex(f::LazyWAVFile{T,2}, i::AbstractRange) where {T} = wavread(f.path, format="native", subrange=i)[1]::Array{T,2}

    Base.getindex(f::LazyWAVFile{T,1}, ::Colon) where {T} = vec(wavread(f.path, format="native")[1])::Array{T,1}
    Base.getindex(f::LazyWAVFile{T,2}, ::Colon) where {T} = wavread(f.path, format="native")[1]::Array{T,2}

    Base.eltype(f::LazyWAVFile{T}) where T = T
    Base.ndims(f::LazyWAVFile{T,N}) where {T,N} = N
    Base.show(io::IO, f::LazyWAVFile{T,N,S}) where {T,N,S} = println(io, "LazyWAV{$T, $N, $(f.size)}: ", f.path)

    struct DistributedWAVFile{T,N,L} <: AbstractArray{T,N}
        files::Vector{LazyWAVFile{T,N}}
        lazyarray::L
    end
    function DistributedWAVFile(folder::String)
        files = filter(readdir(folder)) do file
            file[end-2:end] == "wav"
        end
        files = sort(files)
        files = LazyWAVFile.(joinpath.(Ref(folder), files))
        DistributedWAVFile(files)
    end
    function DistributedWAVFile(files)
        lazyarray = Vcat(files...)
        DistributedWAVFile{eltype(files[1]), ndims(files[1]), typeof(lazyarray)}(files, lazyarray)
    end
    Base.length(f::DistributedWAVFile) = sum(length, f.files)
    Base.size(f::DistributedWAVFile{T,N}) where {T,N} = ntuple(i->sum(x->size(x,i), f.files), N)

    Base.show(io::IO, f::DistributedWAVFile{T,N}) where {T,N} = println(io, "DistributedWAVFile{$T, $N} with $(length(f.files)) files")

    Base.getindex(df::DistributedWAVFile, i...) = getindex(df.lazyarray, i...)

    Base.vcat(dfs::DistributedWAVFile...) = DistributedWAVFile(reduce(vcat, getfield.(dfs, :files)))

end


using Flexpart
using Test
using Dates

# Flexpart.NCF_OUTPUT
# @show Flexpart.option2dict("outgrid")
# Flexpart.find_ncf()
# # Flexpart.outclear()
# # Flexpart.outinfo()
# Flexpart.outgrid()
# @show Flexpart.conc(9)
# Flexpart.fields(9)
# outgrid = Flexpart.Outgrid(5.009, 50.353, 1111, 593, 0.001, 0.001, [100.0])

# @testset "Flexpart.jl" begin

    ###################################
    ############ UTILS ################
    ###################################$
    area = [60.1875, -5.0625, 34.875, 20.25]
    area = [60, -5, 35, 20]
    grid1 = 0.22
    grid1 = 0.5
    alons = -180.0:grid1:180.0 |> collect

    outb = Flexpart.outer_vals(alons, (area[2], area[4]))
    inb = Flexpart.inner_vals(alons, (area[2], area[4]))

    outa = Flexpart.outer_area(area, grid1)
    ina = Flexpart.inner_area(area, grid1)

    ###################################
    ###### TEST FLEXPART DIR ##########
    ###################################
    dirpath = "test/fp_dir_test"
    newdir = Flexpart.create(dirpath)
    fpdir = FlexpartDir(dirpath)
    # pathnam = Flexpart.pathnames(newdir)
    fpdir[:output]
    fpdir[:input] = "/home/tcarion/.julia/dev/Flexpart/test/fe_template/fedir/input"
    fpdir[:input]
    write(fpdir)

    ###################################
    ###### TEST FLEXPART INPUT ########
    ###################################
    inputdir = "test/fp_dir_test/input"
    # av = Flexpart.Available(fpdir)
    # new_av = Flexpart.update(av, "/home/tcarion/.julia/dev/Flexpart/test/fe_template/fedir/input")
    # new_av2 = Flexpart.update(av, DateTime(2021, 8, 8):Dates.Hour(1):DateTime(2021, 8, 9)|>collect, "PREF")
    newav = Flexpart.updated_available(fpdir)
    Flexpart.write(fpdir, newav)

    ###################################
    ###### TEST FLEXPART OPTIONS ######
    ###################################
    fpoptions = FlexpartOptions(dirpath)
    fpoptions["COMMAND"][:command][:ldirect] = 9
    area = [50, 4, 52, 6]
    newv = area2outgrid(area)
    set!(fpoptions["OUTGRID"][:outgrid], newv)
    write(fpoptions, pwd())

    fopt = Flexpart.getnamelists(Flexpart.getdir(fpdir, :options))

    comp = Flexpart.compare(
        "/home/tcarion/.julia/dev/Flexpart/test/fp_dir_test/output/COMMAND.namelist",
        "/home/tcarion/.julia/dev/Flexpart/test/fp_dir_test/output/COMMAND.namelist2")

    # dirs = files[isdir.(files)]
    ###################################
    ###### TEST FLEXPART OUTPUTS ######
    ###################################
    output_files = ncf_files(fpdir)
    fpoutput = FlexpartOutput(output_files[1])

    var2d = Flexpart.variables2d(fpoutput)
    vars = Flexpart.variables(fpoutput)
    dims = Flexpart.remdim(fpoutput, "spec001_mr")
    globattr = Flexpart.attrib(fpoutput)
    specattrib = Flexpart.attrib(fpoutput, "spec001_mr")
    adims = Flexpart.alldims(fpoutput, "spec001_mr")

    spec001 = Flexpart.select(fpoutput, "spec001_mr");
    oro = Flexpart.select(fpoutput, "ORO")

    spec001_alltimes = Flexpart.select(fpoutput, "spec001_mr", (time=:, height=1, pointspec=1, nageclass=1))
    spec001_2d_1 = Flexpart.select(fpoutput, "spec001_mr",
        Dict(:time=>adims[:time][4], :height=>adims[:height][2], :pointspec=>1, :nageclass=>1))

    spec001_2d_2 = Flexpart.select(fpoutput, "spec001_mr",
        Dict(:time=>adims[:time][7], :height=>adims[:height][2], :pointspec=>1, :nageclass=>1))

    spec001_2d_3 = Flexpart.select(fpoutput, "spec001_mr",
        Dict(:time=>adims[:time][7], :height=>:, :pointspec=>1, :nageclass=>1))

    seltime = Flexpart.selected(spec001_alltimes)
    sel = Flexpart.selected(spec001_2d)

    dayav_error = Flexpart.daily_average(spec001)
    dayav = Flexpart.daily_average(spec001_alltimes)

    compdim = Flexpart.completedim(spec001_alltimes)

    Flexpart.addable(spec001_2d_1, spec001_2d_2)
    Flexpart.addable(spec001_2d_1, spec001_alltimes)

    added = spec001_2d_1 + spec001_2d_2
    diff = spec001_2d_1 - spec001_2d_2

    Flexpart.write_daily_average(spec001, copy=true)

    ###################################
    ###### TEST FLEXEXTRACT ###########
    ###################################
    installpath = "/home/tcarion/flexpart/flex_extract_v7.1.2"
    defaultcontrol = "./test/fe_template/CONTROL_OD.OPER.FC.eta.highres.app"
    fepath = "test/fe_template/fedir"
    pythonpath = "/opt/anaconda3/bin/python3"

    fcontrol = FlexControl(defaultcontrol)
    area = [52.2, 4, 49, 6]
    fcontrol[:REQUEST] = 1
    # set!(fcontrol, Dict(:CLASS => "foo", :ETA => 2))
    set_area!(fcontrol, area)
    set_steps!(fcontrol, DateTime("2021-09-05T00:00:00"), DateTime("2021-09-07T00:00:00"), 1)

    fedir = FlexextractDir(fepath, fcontrol)
    fesource = FeSource(installpath, pythonpath)

    cmd = Flexpart.getcmd(fedir, fesource)

    # pip = pipeline(cmd, `sleep 3`, `echo COUCOU`)
    # logf = open("log.log", "w")
    # open(pip) do io
    #     lines = readlines(io, keep=true)
    #     for line in lines
    #         Base.write(logf, line)
    #         flush(logf)
    #     end
    # end
    # close(logf)

    # write(fcontrol, "test/fe_template/fe_output")

    ###################################
    ######## TEST MARSREQUESTS ########
    ###################################
    destmars = "test/fe_template/"
    csvpath = "test/fe_template/²fedir/input/mars_requests.csv"
    tmpdir = "test/tmp"
    requests = MarsRequest(csvpath)
    req = requests[1]
    push!(req.dict, :dqsdqsdq => "dqsdqsdq")
    Flexpart.retrieve(fesource, [req])
    # cmd = Flexpart.retrievecmd(fesource, req, tmpdir)

    open("log.log", "w") do logf
        Flexpart.retrieve(fesource, [req]) do stream
            data = readline(stream, keep=true)
            Base.write(logf, data)
            flush(logf)
        end
    end
    write(destmars, [requests[1]])

    ###################################
    ###### TEST PREPARE FLEXPART ######
    ###################################
    fedir = FlexextractDir(fepath)
    cmd = Flexpart.preparecmd(fedir, fesource)
    run(cmd)

    cmd = Flexpart.preparecmd(FlexextractDir(), fesource)
    Flexpart.prepare(fedir, fesource) do stream
        data = readline(stream, keep=true)
        Base.write(logf, data)
        flush(logf)
    end
    ###### TEST FLEXPART OLD ######
    close(logf)
    Flexpart.relloc(filename)
    Flexpart.start_dt(filename)
    Flexpart.times_dt(filename)
    output1 = Flexpart.FlexpartOutput(filename);
    Flexpart.select!(output1, "spec001_mr");
    output2 = Flexpart.FlexpartOutput(filename);
    @test Flexpart.hasselection(output1) == true
    @test Flexpart.hasselection(output2) == false
    @test Flexpart.hasfield2d(output1) == true

    Flexpart.isspatial(output1)
    Flexpart.isspatial(output2)
    Flexpart.remdim(output1)
    Flexpart.remdim(output2)

    dims = (time=:, height=1, pointspec=1, nageclass=1)
    dims = Dict(:height=>15., :time=>"20200203T080000", :pointspec=>1, :nageclass=>1)
    Flexpart.select!(output1, dims);
    time_av = Flexpart.faverage(output1);
    daily_av, days = Flexpart.daily_average(output1);
    Flexpart.attrib(output1)

    out_daily = Flexpart.write_daily_average(output1)
    Flexpart.select!(out_daily, "daily_av")
    Flexpart.select!(out_daily, (day=:,))
    out_worst = Flexpart.write_worst_day(out_daily)
    Flexpart.alldims(output2)
    Flexpart.close(output1)
    Flexpart.close(output2)

# end


# Read configration files

###################################
"""
    params = geodata("simlation/path/_output"::AbstractString)
    params = geodata("simlation/path/_output/geoclaw.data"::AbstractString)

Function: geoclaw.data reader
"""
function geodata(outdir::AbstractString)
    ## set filename
    fname = occursin("geoclaw.data", basename(outdir)) ? outdir : joinpath(outdir, "geoclaw.data")

    ## check whether it exists
    if !isfile(fname); error("File $fname is not found."); end
    ## read all lines
    open(fname,"r") do f
        global txt = readlines(f)
    end
    ## parse parameters
    # parameters (mandatory?)
    cs = parse(Float64,split(txt[occursin.("coordinate",txt)][1],r"\s+")[1])
    p0 = parse(Float64,split(txt[occursin.("ambient_pressure",txt)][1],r"\s+")[1])
    R = parse(Float64,split(txt[occursin.("earth_radius",txt)][1],r"\s+")[1])
    eta0 = parse(Float64,split(txt[occursin.("sea_level",txt)][1],r"\s+")[1])
    dmin = parse(Float64,split(txt[occursin.("dry_tolerance",txt)][1],r"\s+")[1])
    # parameters (optional?)
    if any(occursin.("manning_coefficient",txt))
        n = parse(Float64,split(txt[occursin.("manning_coefficient",txt)][1],r"\s+")[1])
    else
        n = 0.0
    end
    ## instance
    params = VisClaw.GeoParam(cs,p0,R,eta0,n,dmin)
    ## return values
    return params
end
###################################

###################################
"""
    surgeparams = surgedata("simlation/path/_output"::AbstractString)
    surgeparams = surgedata("simlation/path/_output/surge.data"::AbstractString)

Function: surge.data reader
"""
function surgedata(outdir::AbstractString)
    ## set filename
    fname = occursin("surge.data", basename(outdir)) ? outdir : joinpath(outdir, "surge.data")

    ## check whether it exists
    if !isfile(fname); error("File $fname is not found."); end
    ## read all lines
    open(fname,"r") do f
        global txt = readlines(f)
    end
    ## parse parameters
    windindex = parse(Int64,split(txt[occursin.("wind_index",txt)][1],r"\s+")[1])
    slpindex = parse(Int64,split(txt[occursin.("pressure_index",txt)][1],r"\s+")[1])
    stormtype = parse(Int64,split(txt[occursin.("storm_specification_type",txt)][1],r"\s+")[1])
    ## instance
    surgeparams = VisClaw.SurgeParam(windindex,slpindex,stormtype)
    ## return values
    return surgeparams
end
###################################

###################################
"""
    gaugeinfo = gaugedata("simlation/path/_output"::AbstractString)
    gaugeinfo = gaugedata("simlation/path/_output/gauges.data"::AbstractString)

Function: gauges.data reader
"""
function gaugedata(outdir::AbstractString)
    ## set filename
    fname = occursin("gauges.data", basename(outdir)) ? outdir : joinpath(outdir,"gauges.data")

    ## check whether it exists
    if !isfile(fname); error("File $fname is not found."); end
    ## read all lines
    open(fname,"r") do f
        global txt = readlines(f)
    end
    ## parse parameters
    ngauges = parse(Int64, split(txt[occursin.("ngauges",txt)][1],r"\s+")[1])
    ## preallocate
    gaugeinfo = Vector{VisClaw.Gauge}(undef,ngauges)

    ## read gauge info
    baseline = findfirst(x->occursin("ngauges", x), txt)
    for i = 1:ngauges
        txtline = split(strip(txt[baseline+i]), r"\s+", keepempty=false)
        label = txtline[1]
        id = parse(Int64,txtline[1])
        loc = [parse(Float64,txtline[2]), parse(Float64,txtline[3])]
        time = [parse(Float64,txtline[4]), parse(Float64,txtline[5])]
        # instance
        gaugeinfo[i] = VisClaw.Gauge(label,id,0,loc,[],time,[])
    end

    ## return values
    return gaugeinfo
end
###################################

###################################
"""
    amrparam = amrdata("simlation/path/_output"::AbstractString)
    amrparam = amrdata("simlation/path/_output/amr.data"::AbstractString)

Function: amr.data reader
"""
function amrdata(outdir::AbstractString)
    ## set filename
    fname = occursin("amr.data", basename(outdir)) ? outdir : joinpath(outdir, "amr.data")

    ## check whether it exists
    if !isfile(fname); error("File $fname is not found."); end
    ## read all lines
    open(fname,"r") do f
        global txt = readlines(f)
    end

    ## parse parameters
    maxlevel = parse(Int64,split(txt[occursin.("amr_levels_max",txt)][1],r"\s+")[1])
    ## instance
    amrparam = VisClaw.AMRParam(maxlevel)
    ## return values
    return amrparam
end
###################################

###################################
"""
    regions = regiondata("simlation/path/_output"::AbstractString)
    regions = regiondata("simlation/path/_output/region.data"::AbstractString)

Function: region.data reader
"""
function regiondata(outdir::AbstractString)
    ## set filename
    fname = occursin("regions.data", basename(outdir)) ? outdir : joinpath(outdir, "regions.data")

    ## check whether it exists
    if !isfile(fname); error("File $fname is not found."); end
    ## read all lines
    open(fname,"r") do f
        global txt = readlines(f)
    end

    ## parse parameters
    nregion = parse(Int64, split(txt[occursin.("num_regions",txt)][1],r"\s+")[1])
    ## preallocate
    regions = Vector{VisClaw.Region}(undef,nregion)

    ## read gauge info
    baseline = findfirst(x->occursin("num_regions", x), txt)
    for i = 1:nregion
        txtline = split(strip(txt[baseline+i]), r"\s+", keepempty=false)
        minlevel = parse(Int64,txtline[1])
        maxlevel = parse(Int64,txtline[2])
        tl = (parse(Float64,txtline[3]), parse(Float64,txtline[4]))
        xl = (parse(Float64,txtline[5]), parse(Float64,txtline[6]))
        yl = (parse(Float64,txtline[7]), parse(Float64,txtline[8]))
        ## instance
        regions[i] = VisClaw.Region(minlevel:maxlevel, tl, xl, yl)
    end

    return regions
end
###################################


# Define phenotypic evolvability where a phenotype is a goal or chromosome output.
# Note that genotypic evolvability is computed by calling mutate_all( c, funcs, robust_only=true )[2]
# Use Wagner's (2008) method of evolving nchromess chromsomes whose output is the goal (where Wagner uses nchromes=100).

using DataFrames
using Dates
using CSV
using Distributed
using HypothesisTests
using Statistics
using Printf

export evolvability, run_evolvability, evo_result, test_evo, evo_result_type, run_evolve_g_pairs
export run_geno_robustness, geno_robustness, evo_robust, random_neutral_walk, run_random_neutral_walk
export run_geno_complexity, geno_complexity, pop_evolvability_robustness
export parent_child_complexity, rand_norm, scatter_plot
#=  Moved to aliases.jl so that this file can be included.
mutable struct evo_result_type
  goal::Goal
  nchromes::Int64
  numinputs::Int64
  numoutputs::Int64
  numints::Int64
  levelsback::Int64
  maxsteps::Int64
  robustness::Int64
  evolvability::Int64
end
=#

function evo_result( g::Goal, nchromes, max_steps::Int64, p::Parameters, nrepeats::Int64, evolvable_count::Int64, all_count::Int64,
      evo_diff_count::Int64 )
  evo_result_type(
    g,
    nchromes,
    p.numinputs,
    p.numoutputs,
    p.numinteriors,
    p.numlevelsback,
    nrepeats,
    max_steps,
    all_count,
    evolvable_count,
    evo_diff_count
  )
end

function evo_result( g::Goal, nchromes, numinputs::Int64, numoutputs::Int64, numinteriors::Int64, numlevelsback::Int64,
      max_steps::Int64, nrepeats::Int64, all_count::Int64, evolvable_count::Int64, evo_diff_count::Int64 )
  evo_result_type(
    g,
    nchromes,
    numinputs,
    numoutputs,
    numinteriors,
    numlevelsback,
    nrepeats,
    max_steps,
    all_count,
    evolvable_count,
    evo_diff_count
  )
end

function evo_result_to_tuple( er::evo_result_type )
  (
    er.goal,
    er.nchromes,
    er.numinputs,
    er.numints,
    er.levelsback,
    er.maxsteps,
    er.nrepeats,
    er.all_count,
    er.evolvable_count,
    er.evo_diff_count,
    er.evolvable_count/er.all_count   # the Q field
  )
end

# return the pair (pheno_evolvability, pheno_robustness)
function evo_robust( g::Goal, p::Parameters, funcs::Vector{Func}, nchromes::Int64, maxsteps::Int64, n_repeats::Int64  )
  all_count = 0
  robust_count = 0
  goal_list = Goal[]
  for i = 1:nchromes
    j = 1
    while j < n_repeats
      res =  mut_evolve_repeat(n_repeats, p, [g], funcs, maxsteps)
      if res == nothing
        j += 1
        continue
      end
      (c,step,worse,same,better,output,matched_goals,matched_goals_list) = res
      @assert output_values(c) == g
      #print("i: ",i,"  found c: ")
      #print_build_chromosome(c)
      outputs = mutate_all( c, funcs, output_outputs=true )
      #println("outputs: ",outputs)
      #println("i: ",i,"  unique outputs: ",sort(unique(outputs)))
      all_count += length(outputs)
      robust_count += length(filter( x->x == g, outputs ))
      #println("len unique outputs: ", length(unique(outputs)))
      #println("robust outputs: ",length(filter( x->x == g, outputs )))
      goal_list = unique(vcat( goal_list, unique(outputs) ))
      #println("goal_list: ",sort(goal_list))
      println((all_count, robust_count, length(goal_list )))
      j = n_repeats
    end
  end
  println((all_count, robust_count, length(goal_list )))
  ( length(goal_list)/all_count, robust_count/all_count )
end

function evolvability( g::Goal, funcs::Vector{Func}, nchromes::Int64, maxsteps::Int64,
      numinputs::Int64, numinteriors::Int64, numlevelsback::Int64 )
  p = Parameters( numinputs=numinputs, numoutputs=length(g), numinteriors=numinteriors, numlevelsback=numlevelsback )
  evolvability( g, funcs, nchromes,  maxsteps, p )
end

# Update er with results of one run of computation of evolvability
# The following is done nchromes times:
#   Start with a random chromosome and evolve the goal.
#   When the goal is found, generate the output of the chromosome.
# Evolvability is the count of the unique goals over all of the nchromes chromosome outputs.
# TODO:  convert this to a relative evolvability.
# If intermediate_gens is a non-empty list, save intermediate results for those number of chromosomes
function evolvability( er::evo_result_type, funcs::Vector{Func}, max_repeats::Int64; intermediate_gens::Vector{Int64}=Int64[],
    increase_numints::Bool=true )
  #println("er: ",er)
  succeed = true    # Whether the evoluton succeeded.  Reset below
  p = Parameters( numinputs=er.numinputs, numoutputs=er.numoutputs, numinteriors=er.numints, numlevelsback=er.levelsback )
  all_sum = 0      # Accumulation of count of all neutral neighbors
  unique_result = Goal[]  # Accumulation of unique neutral neighbors
  goal_diff_counts = Int64[]  # save for intermediate generations
  if length(intermediate_gens) > 0
    @assert maximum(intermediate_gens) <= er.nchromes
    all_sums = Int64[]  # save all_sum for intermediate generations
    unique_intermeds = Vector{Goal}[]   # results saved for steps i in intermediate_gens
    intermediate_count = 1
    prev_goal_count = 0
    #println("len inter > 0")
  end
  er.nrepeats = 0    # Establish scope for variable nrepeats
  @assert p.numoutputs==length(er.goal)
  for i = 1:er.nchromes
    if increase_numints
      numinteriors_repeat = er.numints
      numlevelsback_repeat = er.levelsback
    end
    c = random_chromosome( p, funcs )
    (c,steps,output,mached_goals,matched_goals_list) = mut_evolve( c, [er.goal], funcs, er.maxsteps )
    nrepeats = 0
    while nrepeats < max_repeats && steps == er.maxsteps
      if increase_numints
        numinteriors_repeat += 1
        numlevelsback_repeat += 1
        p_repeat = Parameters( numinputs=er.numinputs, numoutputs=length(er.goal), numinteriors=numinteriors_repeat,
            numlevelsback=numlevelsback_repeat )
        println("repeating function mut_evolve with numints: ",numinteriors_repeat,"  and with levsback: ",numlevelsback_repeat)
      else
        p_repeat = p
        println("repeating function mut_evolve" )
      end
      c = random_chromosome( p_repeat, funcs )
      (c,steps,output,mached_goals,matched_goals_list) = mut_evolve( c, [er.goal], funcs, er.maxsteps )
      nrepeats += 1
    end
    succeed = (steps < er.maxsteps)
    if !succeed
      println("nrepeats: ",nrepeats)
      println("evolution failed for goal: ",er.goal)
      er.nrepeats += 100000
    end
    #print_build_chromosome( c )
    goal_list = succeed ? mutate_all( c, funcs, output_outputs=true ) : Vector{MyInt}[] # goals for all possible mutations
    all_sum += length(goal_list)
    #println("goal_list: ",goal_list)
    unique_goals = unique(goal_list)
    #print("len unique_goal_list)): ",length(unique_goals))
    unique_result = unique(vcat(unique_result,unique_goals))
    current_goal_count = length(unique_result)
    if length(intermediate_gens) > 0 && i == intermediate_gens[intermediate_count]
      diff_count = current_goal_count - prev_goal_count
      #println("  diff_count: ",diff_count)
      push!(goal_diff_counts,diff_count)
      prev_goal_count = length(unique_result)
      unique_intermed = deepcopy(unique_result)
      ##println("i: ",i,"  int_count: ",intermediate_count,"  length(intermediate): ",length(intermediate))
      push!(all_sums,all_sum)
      push!(unique_intermeds,unique_intermed)
      intermediate_count += 1
    end
    er.nrepeats += nrepeats
  end
  if length(intermediate_gens) == 0
    #println("len(unique_result): ",length(unique_result))
    er.all_count = all_sum
    er.evolvable_count = length(unique_result)
    er.evo_diff_count = 0
    return er
  else
    er_list = evo_result_type[]
    int_count = 1
    for int in intermediate_gens
      er_i = deepcopy(er)
      er_i.nchromes = int
      er_i.all_count = all_sums[int_count]
      er_i.evolvable_count = length(unique_intermeds[int_count])
      er_i.evo_diff_count = goal_diff_counts[int_count]
      #println("int: ",int,"  er_i: ",er_i)
      push!(er_list,er_i)
      int_count += 1
    end
    return er_list
  end
end

function run_evolvability( nreps::Int64, gl::GoalList, funcs::Vector{Func}, nchromes::IntRange,  maxsteps::IntRange,
      numinputs::Int64, numinteriors::IntRange, numlevelsback::IntRange, max_repeats::Int64;
      increase_numints::Bool=false, intermediate_gens::Vector{Int64}=Int64[] )
  #repeats=3
  if length(intermediate_gens) == 0
    evo_result_list = evo_result_type[]
  else
    evo_result_list = Vector{evo_result_type}[]
  end
  df = DataFrame()
  df.goal=Vector{MyInt}[]
  df.nchromes=Int64[]
  df.numinputs=Int64[]
  df.numints=Int64[]
  df.levsback=Int64[]
  df.maxsteps=Int64[]
  df.nrepeats=Int64[]
  df.all_count=Int64[]
  df.evolvable_count=Int64[]
  df.evo_diff_count=Int64[]
  df.Q = Float64[]
  for g in gl
    for nch = nchromes
      for nints = numinteriors
        for levsback = numlevelsback
          for mxsteps = maxsteps
            for rep = 1:nreps
              #er = evo_result( g, nch, mxsteps, p, 0 )
              er = evo_result( g, nch, numinputs, length(g), nints, levsback, mxsteps, 0, 0, 0, 0 )
              #er = evolvability( er, funcs )
              #push!(df,(g,nch,numinputs,nints,levsback,mxsteps,evob))
              if length(intermediate_gens)==0
                push!(evo_result_list, er )
              else
                push!(evo_result_list, [er] )
              end
            end
          end
        end
      end
    end
  end
  #println("evo_result_list: ",evo_result_list)
  if length(intermediate_gens) == 0
    #new_evo_result_list = pmap( x->evolvability(x,funcs,max_repeats,increase_numints=increase_numints,intermediate_gens=intermediate_gens), evo_result_list )
    new_evo_result_list = map( x->evolvability(x,funcs,max_repeats,increase_numints=increase_numints,intermediate_gens=intermediate_gens), evo_result_list )
  else
    #new_evo_result_list = pmap( x->evolvability(x[1],funcs,max_repeats,increase_numints=increase_numints,intermediate_gens=intermediate_gens), evo_result_list )
    new_evo_result_list = map( x->evolvability(x[1],funcs,max_repeats,increase_numints=increase_numints,intermediate_gens=intermediate_gens), evo_result_list )
  end
  #println("evo_result_list[1]: ",evo_result_list[1])
  #new_evo_result_list = [evolvability(evo_result_list[1][1],funcs,intermediate_gens=intermediate_gens)]
  for er in new_evo_result_list
    if length(intermediate_gens) == 0
      new_row = evo_result_to_tuple(er)
      push!(df, new_row)
    else
      for e in er
        new_row = evo_result_to_tuple(e)
        push!(df, new_row)
      end
    end
  end
  df
end

function run_evolvability( nreps::Int64, gl::GoalList, funcs::Vector{Func}, nchromes::IntRange, maxsteps::IntRange,
      numinputs::Int64, numinteriors::IntRange, numlevelsback::IntRange, max_repeats::Int64, csvfile::String;
      increase_numints::Bool=false, intermediate_gens::Vector{Int64}=Int64[] )
  #result = @timed run_evolvability( nreps, gl, funcs, nchromes,  maxsteps, numinputs, numinteriors, numlevelsback )
  (df,ttime) = @timed run_evolvability( nreps, gl, funcs, nchromes,  maxsteps, numinputs, numinteriors, numlevelsback,
      max_repeats, intermediate_gens=intermediate_gens )
  hostname = chomp(open("/etc/hostname") do f read(f,String) end)
  println("size(df): ",size(df))
  println("# host: ",hostname," with ",nprocs()-1,"  processes: " )
  println("# date and time: ",Dates.now())
  println("# run time in minutes: ",ttime/60)
  println("# max_repeats: ",max_repeats)
  println("# funcs: ", Main.CGP.default_funcs(numinputs[end]))
  open( csvfile, "w" ) do f
    println(f,"# date and time: ",Dates.now())
    println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
    println(f,"# run time in minutes: ",ttime/60)
    println(f,"# max_repeats: ",max_repeats)
    println(f,"# funcs: ", Main.CGP.default_funcs(numinputs[end]))
    CSV.write( f, df, append=true, writeheader=true )
  end
  df
end

function evolve_g_pairs( df::DataFrame, g_pair::Tuple{String,String}, p::Parameters,  maxsteps::Int64 )
  (src_g,dst_g) = g_pair
  funcs = default_funcs(p.numinputs)
  source_g = parse(MyInt,src_g)
  dest_g = parse(MyInt,dst_g)
  (c,step,worse,same,better,output,matched_goals,matched_goals_list,new_numints,new_levsback) =
      mut_evolve_increase_numints( random_chromosome( p, funcs ), [[source_g]], funcs, maxsteps )
  if step == maxsteps
    println("failed to find chromosome for source goal: ",src_g)
    return nothing
  else
    println("found source goal: ",src_g)
  end
  (c,step,worse,same,better,output,matched_goals,matched_goals_list,new_numints,new_levsback) =
      mut_evolve_increase_numints(c, [[dest_g]], funcs, maxsteps )
  if step == maxsteps
    println("failed to find chromosome for dest goal: ",dst_g)
    return nothing
  else
    println("found dest goal: ",dst_g)
  end
  total_steps = (new_levsback - p.numlevelsback)*maxsteps + step
  s_df = df[df.goal.==src_g,[:counts10ints,:complex,:mutrobust,:evolvability]]
  d_df = df[df.goal.==dst_g,[:counts10ints,:complex,:mutrobust,:evolvability]]
  hdist = hamming_distance(source_g,dest_g,p.numinputs)
  # changed order on 9/15/20
  row=(src_g,s_df[1,:counts10ints],s_df[1,:complex],s_df[1,:mutrobust],s_df[1,:evolvability],
        dst_g,d_df[1,:counts10ints],d_df[1,:complex],d_df[1,:mutrobust],d_df[1,:evolvability],hdist,
        p.numinputs,p.numoutputs,new_numints,new_levsback,maxsteps,total_steps)
  row
end

# Test Wagner's hypothesis that "An evolutionary search’s ability to find a target genotype is only weakly
#    correlated with the evolvability of the source genotype".
# Choose a random sample of sample_genotypes for source and destination genotypes.
# For nreps pairs (source_g, dest_g) from this sample, calulate the genotypic evolvability if not already done.
# Then for each pair run mut_evolve from a genotype that corresponds to the source_g with goal dest_g nruns times,
#   and count the number of steps for each run.  Use a large maxsteps and a large numints so that
#   reruns due to failures are rare.
# Returns a DataFrame whose fields are given below.
# Objective: determine correlation of steps with src_count, src_complex, dst_count, dst_complex
# Wagner (2008) claims that there is little correlation of difficulty with src for his RNA data
# As of 9/15/20, the dataframe df can be obtained by
# df = read_dataframe("../data/consolidate/geno_pheno_raman_df_all_9_13.csv");
function run_evolve_g_pairs( df::DataFrame, sample_size::Int64, nreps::Int64, nruns::Int64, numints::Int64, maxsteps::Int64 )
  println("run_evolve_g_pairs")
  p = Parameters( numinputs=df.numinputs[1], numoutputs=df.numoutputs[1], numinteriors=numints, numlevelsback=df.levsback[1] )
  funcs = default_funcs(p.numinputs)
  ndf = DataFrame()
  ndf.source_g = String[]
  ndf.src_count=Int64[]
  ndf.src_cmplx=Float64[]
  ndf.src_mrobust=Float64[]
  ndf.src_evolble=Float64[]
  ndf.dest_g = String[]
  ndf.dst_count=Int64[]
  ndf.dst_cmplx=Float64[]
  ndf.dst_mrobust=Float64[]
  ndf.dst_evolble=Float64[]
  ndf.hamming_dist = Float64[]
  ndf.numinputs = Int64[]
  ndf.numoutputs = Int64[]
  ndf.numints = Int64[]
  ndf.levelsback = Int64[]
  ndf.maxsteps = Int64[]
  ndf.steps = Int64[]
  samples = rand( 1:size(df)[1], sample_size )
  row_list = Tuple[]
  g_pair_list = Tuple{String,String}[]
  for i = 1:nreps
    src_g = df.goal[rand(samples)]
    dst_g = df.goal[rand(samples)]
    for j = 1:nruns
      push!(g_pair_list,(src_g,dst_g))
    end
  end
  row_list = pmap( pair->evolve_g_pairs( df, pair, p, maxsteps), g_pair_list )
  #row_list = map( pair->evolve_g_pairs( df, pair, p, maxsteps), g_pair_list )
  for row in row_list
    if row != nothing
      push!(ndf,row)
    else
      println("evolve_g_pairs() failed!")
    end
  end
  ndf
end

# Calls the above version of run_evolve_g_pairs() with a csvfile argument.
# Outputs the resulting dataframe to the csvfile.
# For example runs, see data/9_15, 9_18, 10_8, 10_29
function run_evolve_g_pairs( df::DataFrame, sample_size::Int64, nreps::Int64, nruns::Int64,
      numints::Int64, maxsteps::Int64, csvfile::String)
  println("run_evolve_g_pairs: csvfile: ",csvfile)
  p = Parameters( numinputs=df.numinputs[1], numoutputs=df.numoutputs[1], numinteriors=numints, numlevelsback=df.levsback[1] )
  (ndf,ttime) = @timed run_evolve_g_pairs( df, sample_size, nreps, nruns, numints, maxsteps )
  hostname = chomp(open("/etc/hostname") do f read(f,String) end)
  println("size(df): ",size(df))
  println("# host: ",hostname," with ",nprocs()-1,"  processes: " )
  println("# date and time: ",Dates.now())
  println("# run time in minutes: ",ttime/60)
  println("# funcs: ", Main.CGP.default_funcs(p.numinputs))
  open( csvfile, "w" ) do f
    println(f,"# date and time: ",Dates.now())
    println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
    println(f,"# run time in minutes: ",ttime/60)
    println(f,"# funcs: ", Main.CGP.default_funcs(p.numinputs))
    CSV.write( f, ndf, append=true, writeheader=true )
  end
  ndf
end

# Test hypotheses that evolution starting with complex goals takes fewer cases than evolution starting with simple goals.
# There are two cases:  evolution of simple goals and evolution of complex goals.
# df is the dataframe that results from running 10 (in the 4x1) case evolutions to find each goal.
# df = read_dataframe("../data/consolidate/geno_pheno_raman_df_all_9_13.csv");
# df = read_dataframe("../../../complexity/data/consolidate/geno_pheno_raman_df_all_9_13.csv");
# sample_size is the size of the sample used in this run.  See the comments on sample_g_pairs()
# order_by is the column of the dataframe df used to measure complexity.  Default is :complex
# Defaults for the 4x1 case might be numinteriors=10, numlevelsback=5, maxsteps=150000
function run_sample_g_pairs( df::DataFrame, nreps::Int64, sample_size::Int64, order_by::Symbol,
    p::Parameters, maxsteps::Int64; hamming::Bool=false )
  result = pmap( x-> sample_g_pairs( df, nreps, sample_size, order_by, p, maxsteps, x, hamming=hamming ), [1,2,3,4] )
  #result = map( x-> sample_g_pairs( df, nreps, sample_size, order_by, p, maxsteps, x, hamming=hamming ), [1,2,3,4] )
  pvalues =
      (pvalue(EqualVarianceTTest(result[1][4],result[3][4])),
       pvalue(EqualVarianceTTest(result[2][4],result[4][4])))
  (pvalues,result)
end

# Choose a random sample of genotypes of size 4*sample size.
# Sort this sample according to field order_by of DataFrame df.  Then return the smallest eighth
#  and the largest eigth (assuming that sample_size_multiplier==8 as set below).
function sample_g_pairs( df::DataFrame, nreps::Int64, sample_size::Int64, order_by::Symbol,
    p::Parameters, maxsteps::Int64, option::Int64; hamming::Bool=false)
  sample_size_multiplier = 8
  sdf = DataFrame()
  sdf.goals = rand(MyInt(0):MyInt(size(df)[1]-1),sample_size_multiplier*sample_size)
  #sdf.goals = map(x->@sprintf("0x%x",x), rand(1:size(df)[1],sample_size_multiplier*sample_size))
  #println("sdf.goals: ",sdf.goals)
  #sdf[!,order_by] = [ df[ gg, order_by ] for gg in randgoallist(sample_size_multiplier*sample_size,p.numinputs,p.numoutputs) ]
  sdf[!,order_by] = [df[ df.goal.==@sprintf("0x%x",gg),order_by] for gg in sdf.goals ]
  nsdf = sort(sdf,order(order_by))
  simple_goals = nsdf.goals[1:sample_size]
  complex_goals = nsdf.goals[((sample_size_multiplier-1)*sample_size+1):(sample_size_multiplier*sample_size)]
  steps_list = Int64[]
  steps_count = 0
  steps_squared_count = 0
  for i = 1:nreps
    if option == 1
      (src_g,dst_g) = choose_g_pair(simple_goals,simple_goals, sample_size, p, hamming=hamming)
    elseif option == 2
      (src_g,dst_g) = choose_g_pair(simple_goals,complex_goals, sample_size, p, hamming=hamming)
    elseif option == 3
      (src_g,dst_g) = choose_g_pair(complex_goals,simple_goals, sample_size, p, hamming=hamming)
    elseif option == 4
      (src_g,dst_g) = choose_g_pair(complex_goals,complex_goals, sample_size, p, hamming=hamming)
    end
    steps = evolve_g_pair( [src_g], [dst_g], p, maxsteps)
    #println("steps: ",steps,"  steps_count: ",steps_count)
    push!(steps_list,steps)
    steps_count += steps
    steps_squared_count += steps^2
  end
  variance = (steps_squared_count - steps_count^2/nreps)/(nreps-1)
  (option, steps_count/nreps, variance, steps_list )
end

function choose_g_pair( goals1::Vector{MyInt}, goals2::Vector{MyInt}, sample_size::Int64, p::Parameters; hamming::Bool=false )
  if !hamming
    return (rand(goals1), rand(goals1))
  else
    if rand() < 0.5
      choice1 = rand(goals1)
      cdf= DataFrame()
      cdf.goals2 = goals2
      cdf.chooseby = [ hamming_distance( choice1, gg, p.numinputs ) for gg in goals2 ]
      sort!(cdf, [:chooseby])
      choice2 = rand(cdf.goals2[1:Int(ceil(sample_size/4))])
    else
      choice2 = rand(goals2)
      cdf= DataFrame()
      cdf.goals1 = goals1
      cdf.chooseby = [ hamming_distance( choice2, gg, p.numinputs ) for gg in goals1 ]
      sort!(cdf, [:chooseby])
      choice1 = rand(cdf.goals1[1:Int(ceil(sample_size/4))])
    end
    return (choice1,choice2)
  end
end

# First, evolve a circuit that outputs goal s_g.  Then starting with this circuit,
#   evolve another circuit that outputs d_g.
function evolve_g_pair( s_g::Goal, d_g::Goal, p::Parameters, maxsteps::Int64 )
  funcs = default_funcs(p.numinputs)
  c = random_chromosome( p, funcs)
  (c,step,worse,same,better,output,matched_goals,matched_goals_list,new_numints,new_levsback) =
      mut_evolve_increase_numints(c, [s_g], funcs, maxsteps )
  println("src chromsome evolved in ",step," steps.")
  (c,step,worse,same,better,output,matched_goals,matched_goals_list,new_numints,new_levsback) =
      mut_evolve_increase_numints(c, [d_g], funcs, maxsteps )
  println("dst chromsome evolved in ",step," steps.")
  step
end

# Test the Wagner (2008) that genotypic robustness is negatively associated with genotypic evolvability.
# Strongly confirmed by:
# julia> tge=test_g_evolvability_g_robustness( 100000, 4, 1, 9, 5 );
# julia> corspearman( tge[1], tge[2])
#  -0.8104812361089767
#=
function test_g_evolvability_g_robustness( nreps::Int64, numinputs::Int64, numoutputs::Int64, numinteriors::Int64, numlevelsback::Int64 )
  p = Parameters( numinputs = numinputs, numoutputs = numoutputs, numinteriors = numinteriors, numlevelsback = numlevelsback )
  funcs = default_funcs(numinputs)
  robust_vec = Float64[]
  all_outputs = Goal[]
  for i = 1:nreps
    c = random_chromosome(p,funcs)
    c_output = output_values(c)
    #println("c_output: ",c_output)
    outputs = mutate_all( c, funcs, output_outputs=true )
    #println("outputs: ",outputs)
    robust_outputs = filter( x->x==c_output, outputs )
    #println("robust_outputs: ",robust_outputs)
    evolvable_outputs = unique(outputs)
    all_outputs = unique( vcat( all_outputs, evolvable_outputs )
    #println("evolvable_outputs: ",evolvable_outputs)
    #println("push: ",length(robust_outputs)/length(outputs),"  ",length(evolvable_outputs)/length(outputs))
    push!( robust_vec, length(robust_outputs)/length(outputs))
    push!( evolvable_vec, length(evolvable_outputs)/length(outputs))
  end
  ( robust_vec, evolvable_vec )
end
=#

# Compute average genotypic robustness, average genotypic evolvability, and phenotypic evolvability for each of ngoals random goals.
# maxreps is attempted number of repetitions.  max_steps the the upper bound on the number of attempts including failures of mut_evolve().
function run_geno_robustness( ngoals::Int64, maxreps::Int64, numinputs::Int64, numoutputs::Int64,
    numinteriors::IntRange, numlevelsback::Int64, max_steps::Int64, max_tries::Int64; csvfile = "" )
  if max_tries < maxreps
    error("max_tries should be greater than maxreps in geno_complexity.")
  end
  p = Parameters( numinputs=numinputs, numoutputs=numoutputs, numinteriors=10, numlevelsback=numlevelsback ) # Establish scope for p
  robust_vec = Float64[]
  evolvable_vec = Float64[]
  list_goals_params = Tuple[]
  for j = 1:ngoals
    goal = randgoal( numinputs, numoutputs)
    for numints in numinteriors
      p = Parameters( numinputs=numinputs, numoutputs=numoutputs, numinteriors=numints, numlevelsback=numlevelsback )
      println("goal: ",goal)
      push!(list_goals_params,(goal,p) )
    end
  end
  println("list goals_params: ",map(x->x[1],list_goals_params))
  result = pmap(x->geno_robustness( x[1], maxreps, p, max_steps, max_tries ), list_goals_params )
  goal_vec = [ rb[1] for rb in result ]
  robust_vec = [ rb[2] for rb in result ]
  geno_evolvable_vec = [ rb[3] for rb in result ]
  pheno_evolvable_vec = [ rb[4] for rb in result ]
  numints_vec = [ rb[5] for rb in result ]
  ntries_vec = [ rb[6] for rb in result ]
  nrepeats_vec = [ rb[7] for rb in result ]
  #println("(robust_vec, evolvable_vec): ",(robust_vec, evolvable_vec))
  robust_evo_df = DataFrame()
  robust_evo_df.goal = goal_vec
  robust_evo_df.robust = robust_vec
  robust_evo_df.geno_evolvable = geno_evolvable_vec
  robust_evo_df.pheno_evolvable = pheno_evolvable_vec
  robust_evo_df.numints = numints_vec
  robust_evo_df.ntries = ntries_vec
  robust_evo_df.nrepeats = nrepeats_vec
  if length(csvfile) > 0
    hostname = chomp(open("/etc/hostname") do f read(f,String) end)
    open( csvfile, "w" ) do f
      println(f,"# date and time: ",Dates.now())
      println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
      #println(f,"# run time in minutes: ",ttime/60)
      println(f,"# funcs: ", Main.CGP.default_funcs(numinputs))
      println(f,"# ngoals: ",ngoals)
      println(f,"# maxreps: ",maxreps)
      println(f,"# max_tries: ",max_tries)
      println(f,"# numinputs: ",numinputs)
      println(f,"# numoutputs: ",numoutputs)
      println(f,"# numlevelsback: ",numlevelsback)
      println(f,"# max_steps: ",max_steps)
      CSV.write(f, robust_evo_df, append=true, writeheader=true )
    end
  end
  return robust_evo_df
end

# Compute average genotypic robustness, average genotypic evolvability, and phenotypic evolvability for goal.
# Return a row of the dataframe constructed in run_geno_robustness()
# maxreps is attempted number of repetitions.  max_steps the the upper bound on the number of attempts including failures of mut_evolve().
function geno_robustness( goal::Goal, maxreps::Int64, p::Parameters, max_steps::Int64, max_tries::Int64 )
  #p = Parameters( numinputs = numinputs, numoutputs = numoutputs, numinteriors = numinteriors, numlevelsback = numlevelsback )
  funcs = default_funcs(p.numinputs)
  nrepeats = 0
  all_outputs_sum = 0
  robust_sum = 0
  g_evolvable_sum = 0
  all_unique_outputs = Goal[]
  i = 0
  while i < max_tries && nrepeats < maxreps
    i += 1
    #println("i: ",i,"  nrepeats: ",nrepeats)
    c = random_chromosome(p,funcs)
    c_output = output_values(c)
    (c,step,worse,same,better,output,matched_goals,matched_goals_list) =
      mut_evolve( c, [goal], funcs, max_steps )
    if step == max_steps
      println("mut evolve failed for goal: ",goal)
      continue
    end
    c_output = output_values(c)
    @assert c_output == goal
    #println("output values: ",c_output)
    outputs = mutate_all( c, funcs, output_outputs=true )
    all_outputs_sum += length(outputs)
    robust_outputs = filter( x->x==c_output, outputs )
    robust_sum += length(robust_outputs)
    p_evolvable_outputs = unique(outputs)
    g_evolvable_sum += length(p_evolvable_outputs)
    all_unique_outputs = unique(vcat(all_unique_outputs,p_evolvable_outputs))
    nrepeats += 1
    #println((all_outputs_sum,robust_sum,length(all_unique_outputs)))
  end
  ntries = i
  if nrepeats > 0

  else
    return ( goal, 0.0, 0.0, 0.0, p.numinteriors, ntries, nrepeats )
  end
end

# For each of the goals in goallist, evolves maxreps chromosomes whose output is that goal.
# Computes properties of these goals and Chromosomes/LinCircuits..
# Arguments:
#   maxreps:  The number of evolutions attempted for each goal
#   iter_maxreps:  The number of evolutions attemped in each pmap-parallel iteration
#   max_steps:  The maximum number of steps for each run of neutral_evolution.
#   max_tries:  The maximum number of evolutions tried in each pmap-parallel iteration
#   consolidate:  if true, consolidate multiple rows with the same goal
# Some goals are much more difficult to evolve than others.  To spread the evolutions of difficult
#   goals over more pmap-parallel iterations, set iter_maxreps to be small relative to maxreps
# The max_tries parameter determines how many attempts are made to evolve difficult goals.
# Returns a dataframe with num_iterations=Int(ceil(maxreps/iter_maxreps)) rows per goal.
function run_geno_complexity( goallist::GoalList, maxreps::Int64, iter_maxreps::Int64, p::Parameters,
      max_steps::Int64, max_tries::Int64, maxsteps_recover::Int64=0, maxtrials_recover::Int64=0, maxtries_recover::Int64=0;
      use_lincircuit::Bool=false, consolidate::Bool=true, csvfile::String = "" )
  #p = Parameters( numinputs=numinputs, numoutputs=numoutputs, numinteriors=10, numlevelsback=numlevelsback ) # Establish scope for p
  geno_complexity_df = DataFrame()
  geno_complexity_df.goal = Vector{MyInt}[]
  geno_complexity_df.numinputs = Int64[]
  geno_complexity_df.numoutputs = Int64[]
  if use_lincircuit
    geno_complexity_df.numinstructs = Int64[]
    geno_complexity_df.numregisters = Int64[]
  else
    geno_complexity_df.numints = Int64[]
    geno_complexity_df.numlevsback = Int64[]
  end
  geno_complexity_df.maxsteps = Int64[]
  geno_complexity_df.ntries = Int64[]
  geno_complexity_df.nsuccesses = Float64[]
  geno_complexity_df.avg_steps = Float64[]
  geno_complexity_df.log_avg_steps = Float64[]
  geno_complexity_df.robustness = Float64[]
  geno_complexity_df.evo_count = Int64[]
  geno_complexity_df.unique_goals = GoalList[]
  geno_complexity_df.nactive = Float64[]
  geno_complexity_df.complexity = Float64[]
  geno_complexity_df.degeneracy = Float64[]
  geno_complexity_df.sumsteps = Float64[]
  geno_complexity_df.sumtries = Float64[]
  #geno_complexity_df.complexQ95 = Float64[]
  #geno_complexity_df.complexQ99 = Float64[]
  #geno_complexity_df.epi2= Float64[]
  #geno_complexity_df.epi3= Float64[]
  #geno_complexity_df.epi4= Float64[]
  #geno_complexity_df.epi_total = Float64[]
  #geno_complexity_df.f_mutrobust= Float64[]
  list_goals = Goal[]
  num_iterations = Int(ceil(maxreps/iter_maxreps))  # The number of iterations for each goal
  iter_maxtries = Int(ceil(max_tries/num_iterations))
  for g in goallist
    for i = 1:num_iterations
      #println("i: ",i,"  goal: ",g)
      push!(list_goals,g)
    end
  end
  funcs = default_funcs(p.numinputs)
  c = random_chromosome(p,funcs)
  sample_size = length(mutate_all(c, funcs, output_outputs=true))*iter_maxreps
  num_goals = 2^2^p.numinputs
  #println("sample size: ",sample_size,"  num_goals: ",num_goals)
  #println("list_goals: ",list_goals)
  result = pmap(g->geno_complexity( g, iter_maxreps, p, max_steps, iter_maxtries,
      maxsteps_recover, maxtrials_recover, iter_maxtries, use_lincircuit=use_lincircuit ), list_goals)
  #result = map(g->geno_complexity( g, iter_maxreps, p, max_steps, iter_maxtries,
  #    maxsteps_recover, maxtrials_recover, maxtries_recover, use_lincircuit=use_lincircuit ), list_goals)
  #println("after pmap:  size(geno_complexity_df): ",size(geno_complexity_df))
  for res in result
    #println("length(res): ",length(res))
    push!(geno_complexity_df,res)
  end
  geno_complexity_df.evo_count = zeros(Int64,size(geno_complexity_df)[1] )
  #geno_complexity_df.ratio = zeros(Float64,size(geno_complexity_df)[1] )
  #geno_complexity_df.estimate = zeros(Float64,size(geno_complexity_df)[1] )
  j = 1
  for g in goallist
    println("goal ",g)
    all_unique_goals = Goal[]
    prev_evo_count = 0
    for i = 1:num_iterations
      all_unique_goals = unique( vcat( all_unique_goals, geno_complexity_df[j,:unique_goals] ))
      evo_count = length(all_unique_goals)
      #println("goal: ",geno_complexity_df[j,:goal],"  j: ",j,"  i: ",i,"  length(all_unique_goals): ",length(all_unique_goals))
      new_count = evo_count - prev_evo_count
      geno_complexity_df[j,:evo_count] = evo_count
      prev_evo_count = evo_count
      j += 1
    end
  end
  select!(geno_complexity_df,DataFrames.Not(:unique_goals))    # Remove :unique_goals from gcdf
  if consolidate   # Consolidate multiple rows with the same goal.  See Analyze.jl for code.
    geno_complexity_df = consolidate_dataframe( geno_complexity_df )
  end
  if length(csvfile) > 0
    hostname = chomp(open("/etc/hostname") do f read(f,String) end)
    open( csvfile, "w" ) do f
      println(f,"# date and time: ",Dates.now())
      println(f,"# host: ",hostname," with ",nprocs()-1,"  processes: " )
      #println(f,"# run time in minutes: ",ttime/60)
      println(f,"# funcs: ", Main.CGP.default_funcs(p.numinputs))
      println(f,"# use_lincircuit: ",use_lincircuit)
      println(f,"# maxreps: ",maxreps)
      println(f,"# iter_maxreps: ",iter_maxreps)
      println(f,"# numinputs: ",p.numinputs)
      println(f,"# numoutputs: ",p.numoutputs)
      if use_lincircuit
        println(f,"# numinstructions: ",p.numinteriors)
        println(f,"# numregisters: ",p.numlevelsback)
      else
        println(f,"# numinteriors: ",p.numinteriors)
        println(f,"# numlevelsback: ",p.numlevelsback)
      end
      println(f,"# max_steps: ",max_steps)
      CSV.write(f, geno_complexity_df, append=true, writeheader=true )
    end
  end
  return geno_complexity_df
end

# For the given goal, evolves iter_maxreps chromosomes that output that goal.
# Returns a tuple which is pushed as a row onto the dataframe constructed in run_geno_complexity().
# This can be run in parallel for one goal because each run is evolving iter_maxreps circuits that compute goal.
# all_unique_outputs is returned as one of the fields in the returned dataframe.
# Then run_geno_complexity combines the all_unique_outputs from the parallell runs
function geno_complexity( goal::Goal, iter_maxreps::Int64, p::Parameters,  maxsteps::Int64, max_tries::Int64,
      maxsteps_recover::Int64, maxtrials_recover::Int64, maxtries_recover::Int64; use_lincircuit::Bool=false )
  #println("geno_complexity: goal: ",goal)
  if max_tries < iter_maxreps
    println("max_tries: ",max_tries,"  iter_maxreps: ",iter_maxreps)
    error("max_tries should be greater than iter_maxreps in geno_complexity.")
  end
  funcs = default_funcs(p.numinputs)
  #W = Walsh(2^p.numinputs)
  all_outputs_sum = 0
  robust_sum = 0
  all_unique_outputs = Goal[]
  nactive_list = Int64[]
  complexity_list = Float64[]
  degeneracy_list = Float64[]
  sumsteps_list = Int64[]
  sumtries_list = Int64[]
  #frenken_mi_list = Float64[]
  if p.numoutputs == 0
    epi2 = k_bit_epistasis(W,2,goal[1])
    epi3 = k_bit_epistasis(W,3,goal[1])
    epi4 = k_bit_epistasis(W,4,goal[1])
    epi_total = total_epistasis(W,goal[1])
  end
  #println("After epi")
  sum_steps = 0.0
  sum_steps_per_iteration = 0  # for one iteration of the while loop
  numsuccesses = 0  # The number of successful evolutions
  i = 0
  # Do up to max_tries evolutions attempting to do iter_maxreps successful evolutions
  while i < max_tries && numsuccesses < iter_maxreps
    i += 1
    c = use_lincircuit ? rand_lcircuit(p,funcs) : random_chromosome(p,funcs)
    c_output = output_values(c)
    #(c,steps,worse,same,better,output,matched_goals,matched_goals_list) = mut_evolve( c, [goal], funcs, maxsteps )
    (c,steps) = neutral_evolution( c, goal, maxsteps )
    if steps == maxsteps
      #println("mut evolve failed for goal: ",goal)
      #println("neutral evolution failed for goal: ",goal)
      sum_steps_per_iteration += steps
      continue
    end
    c_output = output_values(c)
    #println("goal: ",goal,"  i: ",i,"  numsuccesses: ",numsuccesses,"  c_output: ",c_output)
    @assert sort(c_output) == sort(goal)
    #println("output values: ",c_output,"  complexity5(c): ",complexity5(c))
    outputs = mutate_all( c, funcs, output_outputs=true )
    all_outputs_sum += length(outputs)
    robust_outputs = filter( x->x==c_output, outputs )
    robust_sum += length(robust_outputs)
    evolvable_outputs = unique(outputs)
    all_unique_outputs = unique(vcat(all_unique_outputs,evolvable_outputs))
    push!( nactive_list, number_active( c ))
    complexity = use_lincircuit ? lincomplexity( c, funcs ) : complexity5(c)
    push!( complexity_list, complexity )
    push!( degeneracy_list, degeneracy( c ))
    (sumsteps,sumtries) = recover_phenotype( c, maxsteps_recover, maxtrials_recover, maxtries_recover )
    push!( sumsteps_list, sumsteps )
    push!( sumtries_list, sumtries )
    #push!( frenken_mi_list, fmi_chrome( c ))
    sum_steps_per_iteration += steps
    sum_steps += sum_steps_per_iteration
    sum_steps_per_iteration = 0
    numsuccesses += 1
  end
  ntries = i
  #println("ntries: ",ntries,"  numsuccesses: ",numsuccesses,"  all_outputs_sum: ",all_outputs_sum,"  len all_unique_outputs: ",length(all_unique_outputs))
  println("num_successes: ",numsuccesses)
  # Return evolvability count for testing  10/13
  #length(all_unique_outputs)
  # Temporarily comment out for testing evolvability  10/13
  if numsuccesses > 0
    return (
      goal,
      p.numinputs,
      p.numoutputs,
      p.numinteriors,
      p.numlevelsback,
      maxsteps,
      ntries,
      numsuccesses,
      sum_steps/numsuccesses,   # Note:  Only counts steps for successful runs.
      sum_steps != 0.0 ? log10(sum_steps/numsuccesses) : 0.0,
      robust_sum/all_outputs_sum,
      0,   # evo_count, value filled in later
      #0.0,  # ratio, value filled in later
      #0.0,  # estimate, value filled in later
      all_unique_outputs,
      sum( nactive_list )/iter_maxreps,
      sum( complexity_list )/iter_maxreps,
      sum( degeneracy_list )/iter_maxreps,
      sum( sumsteps_list )/iter_maxreps,
      sum( sumtries_list )/iter_maxreps,
      #quantile(complexity_list,0.95),
      #quantile(complexity_list,0.99),
      #p.numoutputs==0 ? epi2 : 0.0,
      #p.numoutputs==0 ? epi3 : 0.0,
      #p.numoutputs==0 ? epi4 : 0.0,
      #p.numoutputs==0 ? epi_total : 0.0,
      #sum(frenken_mi_list)/iter_maxreps
    )
  else  # Evolution always failed
    return (
      goal,
      p.numinputs,
      p.numoutputs,
      p.numinteriors,
      p.numlevelsback,
      maxsteps,
      ntries,
      0,   # numsuccesses
      0,   # sum_steps
      0,   # log_sum_steps
      0.0, # robust sum
      0,   # evo_count
      #0.0, # ratio
      #0.0, # estimate
      Goal[],
      sum( nactive_list )/iter_maxreps,
      sum( complexity_list )/iter_maxreps,
      0.0,  # degeneracy
      0.0,  #sumsteps_list
      0.0,  #sumtries_list
      #0.0,  # quantile(complexity_list,0.95)
      #0.0,
      #0.0,
      #0.0,
      #0.0,
      #0.0
    )
  end
end

function add_frequencies_to_dataframe( gdf:: DataFrame, counts_field::Symbol, count_csv_file::String="../data/counts/count_out_4x1_all_ints_10_10.csv" )
  cdf = read_dataframe(count_csv_file)
  # The following is what works when the :goal field of gdf is of the format "UInt16[0x4fd1]"  where UInt16 is MyInt
  #counts = [cdf[cdf.goals.==@sprintf("0x%x",eval(Meta.parse(gdf.goal[i]))[1]),counts_field][1] for i = 1:size(gdf)[1]]
  # The following is what works when the :goal field of gdf is of the format [0x4fd1] where UInt16 is MyInt
  counts = [cdf[cdf.goals.==@sprintf("0x%x",gdf.goal[i][1]),counts_field][1] for i = 1:size(gdf)[1]]
  gdf.counts = counts
  gdf
end

# Create and save a scatter plot using Plots package
# For examples, see notes/diary10_11.txt
function scatter_plot( gcdf::DataFrame, y_var::Symbol, x_var::Symbol, circuit_type::String, numints::Int64, numlevsback::Int64 )
  Plots.scatter( gcdf[!,x_var], gcdf[!,y_var], title="$y_var vs $x_var $circuit_type $numints ints $numlevsback levsback", ylabel=y_var, xlabel=x_var, label="")
  fname = "$y_var@vs@$x_var@$circuit_type@$numints@ints@$numlevsback@levsback"  # "@" will be changed to "_"
  Plots.savefig( replace(fname, "@"=>"_") )
  # Redo plot so that it shows up interactively
  Plots.scatter( gcdf[!,x_var], gcdf[!,y_var], title="$y_var vs $x_var $circuit_type $numints ints $numlevsback levsback", ylabel=y_var, xlabel=x_var, label="")
end

# See diary9_26.txt for motivation and sample results
# TODO:  filter parent chromosomes for high or low complexity
function parent_child_complexity( p::Parameters, nreps::Int64 )
  nrepeats = 3
  funcs = default_funcs(p.numinputs)
  complexity_pair_list = Tuple{Float64,Float64}[]
  for i = 1:nreps
    pc = random_chromosome(p,funcs)
    p_complex = complexity5( pc )
    cc = mutate_chromosome!( deepcopy(pc), funcs )[1]
    c_complex = complexity5( cc )
    #println("pair: ",(p_complex, c_complex ))
    push!( complexity_pair_list, (p_complex, c_complex ) )
  end
  complexity_pair_list
end

rand_norm( mean::Float64, std::Float64, nreps::Int64 ) = std.*randn(nreps).+mean
rand_norm(nreps::Int64) = rand_norm( 3.12727, 1.164145, nreps )

# population is a vector of chromsomes
# Returns the pair (evolvability,robustness)
# The evolvability of the population is the length of the set of unique phenotypes in the union
#  of mutational neighborhoods of the genotypes of the population
#
function pop_evolvability_robustness( population::Vector{Chromosome} )
  p = population[1].params
  funcs = default_funcs(p.numinputs)
  robust_sum = 0.0
  count_neighbors = 0
  ph_set = Set(Vector{Vector{MyInt}}())
  for ch in population
    neighbors = mutate_all( ch, funcs, output_outputs=true )
    count_neighbors += length(neighbors)
    robustness = length(filter(x->x==output_values(ch),neighbors))/length(neighbors)
    robust_sum += robustness
    for neighbor in neighbors
      push!( ph_set, neighbor )
    end
  end
  (length(ph_set)/count_neighbors,robust_sum/length(population))
end


module BoundingBoxes

export @boundingbox

macro boundingbox(ex)
    bound_name = ex.args[1]
    axes = ex.args[2:end]
    # construct Bounding Box type
    types = [Expr(:(::),Symbol(axe*"_max"),:T) for axe in axes]
    append!(types,[Expr(:(::),Symbol(axe*"_min"),:T) for axe in axes])
    curly = Expr(:curly, bound_name, Expr(:(<:), :T, :Number))
    bound = Expr(:type, true, curly, Expr(:block, types...))

    # construct equality
    eqs = [Expr(:call, :(==),
                 dots("a", "_max", axe),
                 dots("b", "_max", axe)) for axe in axes]
    append!(eqs, [Expr(:call, :(==),
                 dots("a", "_min", axe),
                 dots("b", "_min", axe)) for axe in axes])
    eq_return = Expr(:return, andlist(eqs))
    eq_block = Expr(:block, eq_return)
    eq_args = :(==(a::$(bound_name), b::$(bound_name)))
    equality = Expr(:function, eq_args, eq_block)

    # construct empty bounds
    empty_max = [Expr(:call, :typemin, :T) for i = 1:length(axes)]
    empty_min = [Expr(:call, :typemax, :T) for i = 1:length(axes)]
    empty_bounds = :($(bound_name)(T) = $(bound_name){T}($(empty_max...), $(empty_min...)))

    # promotion handling function
    promote_bounds = :($(bound_name)(x...) = $(bound_name)(promote(x...)...))

    # construct bounds updating
    update_args = :(update!(a::$(bound_name), b::AbstractArray))
    update_body = [Expr(:(=), dots("a", "_max", axes[i]),
                        Expr(:call,
                            :(max), dots("a", "_max", axes[i]),
                            Expr(:ref, :b, i))
                        ) for i = 1:length(axes)]
    append!(update_body, [Expr(:(=), dots("a", "_min", axes[i]),
                            Expr(:call,
                                    :(min), dots("a", "_min", axes[i]),
                                    Expr(:ref, :b, i))
                            ) for i = 1:length(axes)])
    update_block = Expr(:block, update_body..., :(return nothing))
    update_bounds = Expr(:function, update_args, update_block)

    # construct is (a in b)
    in_eqs = [Expr(:call, :(>),
                 dots("b", "_max", axe),
                 dots("a", "_max", axe)) for axe in axes]
    append!(in_eqs, [Expr(:call, :(<),
                 dots("b", "_min", axe),
                 dots("a", "_min", axe)) for axe in axes])
    in_return = Expr(:return, andlist(in_eqs))
    in_block = Expr(:block, in_return)
    in_args = :(Base.in(a::$(bound_name), b::$(bound_name)))
    in_bounds = Expr(:function, in_args, in_block)

    quote
        $(esc(bound)) # create bounding box type
        $(esc(equality)) # create equality method (==)
        $(esc(promote_bounds)) # create arg promoting method
        $(esc(update_bounds)) # create method for updating against AbstractArray
        $(esc(in_bounds)) # create in method
        $(esc(empty_bounds)) # create empty bounds (Bounds(T))
    end
end

# member access
function dots(parent, elt, axe)
    return Expr(:(.), Symbol(parent), QuoteNode(Symbol(axe*elt)))
end

# create a list of chained "&&" Exprs
function andlist(exprs)
    last_expr = exprs[1]
    expr = Expr(:block)
    for i = 2:length(exprs)
        expr = Expr(:(&&), exprs[i], last_expr)
        last_expr = expr
    end
    return expr
end

end # module


# Test print_tree()

struct Num{I} end
Num(I::Int) = Num{I}()
Base.show(io::IO, ::Num{I}) where {I} = print(io, I)
AbstractTrees.children(x::Num{I}) where {I} = (Num(I+1), Num(I+1))

struct SingleChildInfiniteDepth end
AbstractTrees.children(::SingleChildInfiniteDepth) = (SingleChildInfiniteDepth(),)

@testset "Truncation" begin

    # test that `print_tree(headnode, maxdepth)` truncates the output at right depth
    # julia > print_tree(Num(0), 3)
    # 0
    # ├─ 1
    # │  ├─ 2
    # │  │  ├─ 3
    # │  │  └─ 3
    # │  └─ 2
    # │     ├─ 3
    # │     └─ 3
    # └─ 1
    #    ├─ 2
    #    │  ├─ 3
    #    │  └─ 3
    #    └─ 2
    #       ├─ 3
    #       └─ 3
    #

    for maxdepth in [3,5,8]
        ptxt = repr_tree(Num(0), maxdepth)

        n1  = sum([1 for c in ptxt if c=="$(maxdepth-1)"[1]])
        n2  = sum([1 for c in ptxt if c=="$maxdepth"[1]])
        n3  = sum([1 for c in ptxt if c=="$(maxdepth+1)"[1]])

        @test n1==2^(maxdepth-1)
        @test n2==2^maxdepth
        @test n3==0
    end

    # test that `print_tree(headnode)` prints truncation characters under each
    # node at the default maxdepth level = 5
    truncation_char = AbstractTrees.TreeCharSet().trunc
    ptxt = repr_tree(Num(0))

    n1  = sum([1 for c in ptxt if c=='5'])
    n2  = sum([1 for c in ptxt if c=='6'])
    n3  = sum([1 for c in ptxt if c==truncation_char])
    @test n1==2^5
    @test n2==0
    @test n3==2^5

    lines = split(ptxt, '\n')
    for i in 1:length(lines)
        if ~isempty(lines[i]) && lines[i][end] == '5'
            @test lines[i+1][end] == truncation_char
        end
    end

    # test correct number of lines printed 1
    ptxt = repr_tree(SingleChildInfiniteDepth())
    numlines = sum([1 for c in split(ptxt, '\n') if ~isempty(strip(c))])
    @test numlines == 7 # 1 (head node) + 5 (default depth) + 1 (truncation char)

    # test correct number of lines printed 2
    ptxt = repr_tree(SingleChildInfiniteDepth(), 3)
    numlines = sum([1 for c in split(ptxt, '\n') if ~isempty(strip(c))])
    @test numlines == 5 # 1 (head node) + 3 (depth) + 1 (truncation char)

    # test correct number of lines printed 3
    ptxt = repr_tree(SingleChildInfiniteDepth(), 3, indicate_truncation=false)
    numlines = sum([1 for c in split(ptxt, '\n') if ~isempty(strip(c))])
    @test numlines == 4 # 1 (head node) + 3 (depth)
end