# Example of GOKU-net model on friction-less pendulum data created with Luxor

using LatentDiffEq
using FileIO
using Parameters: @with_kw
using ProgressMeter: Progress, next!
using Random
using Statistics
using MLDataUtils
using BSON: @save
using Flux.Data: DataLoader
using Flux
using OrdinaryDiffEq
using StochasticDiffEq
using ModelingToolkit
using DiffEqSensitivity
using Images
using Plots
import GR
using CUDA
CUDA.allowscalar(false)

include("pendulum.jl")
include("create_data.jl")

################################################################################
## Arguments for the train function
@with_kw mutable struct Args
    ## Global model
    model_type = GOKU()

    ## Latent Differential Equations
    diffeq = Pendulum()
    # diffeq = SPendulum()

    ## Training params
    η = 1e-3                        # learning rate
    decay = 0.001f0                 # decay applied to weights during optimisation
    batch_size = 64                 # minibatch size
    seq_len = 50                    # sequence length for training samples
    epochs = 1500                   # number of epochs for training
    seed = 333                      # random seed
    cuda = true                     # GPU usage (not working well yet)
    dt = 0.05                       # timestep for ode solve
    variational = true              # variational or deterministic training

    ## Annealing schedule
    start_β = 0f0                   # start value
    end_β = 1f0                     # end value
    n_cycle = 4                     # number of annealing cycles
    ratio = 0.9                     # proportion used to increase β (and 1-ratio used to fix β)

    ## Progressive observation training
    progressive_training = false    # progressive training usage
    prog_training_duration = 200    # number of epochs to reach the final seq_len
    start_seq_len = 10              # training sequence length at first step

    ## Visualization
    vis_len = 60                    # number of test frames to visualize after each epoch
    save_figure = true              # true: save visualization figure in save_path folder
                                    # false: display image instead of saving it
end

################################################################################
################################################################################
## Training done manualy

function train(; kws...)
    ## Load hyperparameters and GPU config
    args = Args(; kws...)
    @unpack_Args args

    if cuda && has_cuda_gpu()
        device = gpu
        @info "Training on GPU"
    else
        device = cpu
        @info "Training on CPU"
    end

    ############################################################################
    ## Prepare training data
    root_dir = @__DIR__
    data_path = "$root_dir/data/data.bson"

    if ~isfile(data_path)
        @info "Generating data"
        latent_data, u0s, ps, high_dim_data = generate_dataset(diffeq = diffeq)
        data = (latent_data, u0s, ps, high_dim_data)
        mkpath("$root_dir/data")
        @save data_path data
    end

    seed > 0 && Random.seed!(seed)

    data_loaded = load(data_path, :data)
    train_data = data_loaded[4]
    latent_data = data_loaded[1]
    params_data = data_loaded[3]

    # Stack time for each sample
    train_data = Flux.stack.(train_data, 3)

    # Stack all samples
    train_data = Flux.stack(train_data, 4) # 28x28x400x450
    h, w, full_seq_len, observations = size(train_data)
    latent_data = Flux.stack(latent_data, 3)
    params_data = Flux.stack(params_data, 3)

    # Vectorize frames
    train_data = reshape(train_data, :, full_seq_len, observations) # input_dim, time_size, samples
    train_data = Float32.(train_data)

    # Split into train and validation sets
    train_set, val_set = Array.(splitobs(train_data, 0.9))
    train_set_latent, val_set_latent = Array.(splitobs(latent_data, 0.9))
    train_set_params, val_set_params = Array.(splitobs(params_data, 0.9))

    # Prepare data loader
    loader_train = DataLoader(train_set, batchsize=batch_size, shuffle=true, partial=false)

    val_set = permutedims(val_set, [1,3,2])
    t_val = range(0.f0, step=dt, length = size(val_set, 3))
    input_dim = size(train_set, 1)

    ############################################################################
    # Create model
    encoder_layers, decoder_layers = default_layers(model_type, input_dim, diffeq, device = device)
    model = LatentDiffEqModel(model_type, encoder_layers, decoder_layers)

    # Get parameters
    ps = Flux.params(model)

    ############################################################################
    ## Define optimizer
    #opt = ADAM(η)
    # opt = AdaBelief(η)
    opt = ADAMW(η, (0.9,0.999), decay)

    ############################################################################
    ## Various definitions
    if progressive_training
        prog_seq_lengths = range(start_seq_len, seq_len, step=(seq_len-start_seq_len)/(prog_training_duration-1))
        prog_seq_lengths = Int.(round.(prog_seq_lengths))
    else
        prog_training_duration = 0
    end

    # KL annealing scheduling
    annealing_schedule = frange_cycle_linear(epochs, start_β, end_β, n_cycle, ratio)

    # Preparation for saving best models weights
    mkpath("$root_dir/output")
    best_val_loss = Inf32
    val_loss = 0f0

    # mkpath("$root_dir/output")
    # args = struct2dict(args)
    # @save "$root_dir/output/args.bson" args

    ## Visualization options
    if save_figure
        vis_dir = "$root_dir/output/visualization"
        mkpath(vis_dir)
        @info "Visualizations at $vis_dir"
        GR.inline("pdf")
    end

    ############################################################################
    ## Main train loop
    @info "Start Training of $(typeof(model_type))-net, total $epochs epochs"
    for epoch = 1:epochs

        # Set annealing factor
        β = annealing_schedule[epoch]

        # Set a sequence length for training samples
        seq_len = epoch ≤ prog_training_duration ? prog_seq_lengths[epoch] : seq_len

        # Model evaluation length
        t = range(0.f0, step=dt, length=seq_len)

        @info "Epoch $epoch .. (Sequence training length $seq_len)"
        progress = Progress(length(loader_train))

        for x in loader_train

            # Permute dimesions for having (pixels, batch_size, time)
            x = PermutedDimsArray(x, [1,3,2])

            # Use only random sequences of length seq_len for the current minibatch
            x = time_loader(x, full_seq_len, seq_len)

            # Run the model with the current parameters and compute the loss
            loss, back = Flux.pullback(ps) do
                loss_batch(model, x |> device, t, β, variational)
            end

            # Backpropagate and update
            grad = back(1f0)
            Flux.Optimise.update!(opt, ps, grad)

            # Use validation set to get loss and visualisation
            val_loss = loss_batch(model, val_set |> device, t_val, β, false)

            # Progress meter
            next!(progress; showvalues=[(:loss, loss), (:val_loss, val_loss)])
        end

        visualize_val_image(model, val_set |> device, val_set_latent, val_set_params, vis_len, dt, h, w, device, save_figure, epoch)

        if val_loss < best_val_loss
            best_val_loss = deepcopy(val_loss)
            weights = Flux.params(model)
            @save "$root_dir/output/best_model_weights.bson" weights
            @info "Model saved"
        end
    end
end

################################################################################
## Loss definition

function loss_batch(model, x, t, β, variational)

    # Make prediction
    X̂, μ, logσ² = model(x, t, variational)
    x̂, ẑ, l̂ = X̂

    # Compute reconstruction loss
    reconstruction_loss = sum(mean((x .- x̂).^2, dims=(2,3)))

    # Compute KL losses for parameters and initial values
    kl_loss = vector_kl(μ, logσ²)

    return reconstruction_loss + β * kl_loss
end

################################################################################
## Visualization function

function visualize_val_image(model, val_set, val_set_latent, val_set_params, vis_len, dt, h, w, device, save_figure, epoch)

    # randomly pick a sample from val_set and a random time interval of length vis_len
    j = rand(1:size(val_set, 2))
    idxs = rand_time(size(val_set, 3), vis_len)
    x = val_set[:, j:j, idxs] |> device
    true_latent = val_set_latent[:, idxs, j]
    true_params = val_set_params[j]

    # create time range for the model diffeq solving
    t_val = range(0.f0, step=dt, length=vis_len)

    # run model with current parameters on the picked sample
    X̂, μ, logσ² = model(x, t_val)
    x̂, ẑ, l̂ = X̂
    ẑ₀, θ̂ = l̂

    θ̂ = cpu(θ̂)
    ẑ = cpu(ẑ)
    x̂ = cpu(x̂)
    x = cpu(x)
    θ̂ = θ̂[1]

    # plot actual and inferred angles
    plt1 = plot(ẑ[1,1,:], legend=false, ylabel="inferred angle", box = :on, color=:indigo, yforeground_color_axis=:indigo, yforeground_color_text=:indigo, yguidefontcolor=:indigo, rightmargin = 2.0Plots.cm)
    xlabel!("time")
    plt1 = plot!(twinx(), true_latent[1,:], color=:darkorange1, box = :on, xticks=:none, legend=false, ylabel="true angle", yforeground_color_axis=:darkorange1, yforeground_color_text=:darkorange1, yguidefontcolor=:darkorange1)
    title!("Sample from validation set")

    # downsample
    x = @view x[:, :, 1:6:end]
    x̂ = @view x̂[:, :, 1:6:end]

    # build frames vectors
    to_image(x) = Gray{N0f8}.(reshape(x, h, w))
    frames_val = [to_image(xₜ) for xₜ in eachslice(x, dims = 3)]
    frames_pred = [to_image(x̂ₜ) for x̂ₜ in eachslice(x̂, dims = 3)]

    # plot a mosaic view of the frames
    plt2 = mosaicview(frames_val..., frames_pred..., nrow=2, rowmajor=true)
    plt2 = plot(plt2, leg = false, ticks = nothing, border = :none)
    annotate!((208, -21, ("True Pendulum Length = $(round(true_params, digits = 2))", 9, :gray, :right)))
    annotate!((208, -11, ("Inferred Pendulum Length = $(round(θ̂, digits = 2))", 9, :gray, :right)))
    plt = plot(plt1, plt2, layout = @layout([a; b]))
    save_figure ? savefig(plt, "output/visualization/fig_$epoch.pdf") : display(plt)
    return nothing
end

train()


module BALUtils

using StaticArrays
using SparseArrays
using LinearAlgebra
using Rotations

export Camera, pose, axisangle, BA, readbal, visibility_graph, restrict, writebal, center, AngleAxis, num_cameras, num_points, num_observations

function rodrigues_rotate(angle_axis :: AbstractArray, point :: AbstractArray)
    theta = norm(angle_axis)
    axis = normalize(angle_axis)
    point * cos(theta) + cross(axis, point) * sin(theta) + axis * dot(axis, point) * (1 - cos(theta))
end

"""
Rotation represented as a 3 vector who's norm is the angle to rotate around the
normalized vector.
"""
struct AngleAxis{T <: Number}
    angleaxis :: SVector{3, T}
end

function AngleAxis(mat :: Array{<: Number, 2})
    AngleAxis(axisangle(mat))
end

function Base.:*(v :: AngleAxis, x :: AbstractArray)
    rodrigues_rotate(v.angleaxis, x)
end

# from https://math.stackexchange.com/questions/382760/composition-of-two-axis-angle-rotations
function Base.:*(x :: AngleAxis, y :: AngleAxis)
    alpha = norm(x.angleaxis)
    beta = norm(y.angleaxis)
    ax = normalize(x.angleaxis)
    ay = normalize(y.angleaxis)
    gamma = 2 * acos(cos(alpha/2) * cos(beta/2) - sin(alpha/2) * sin(beta/2) * dot(ax, ay))
    axis = (sin(alpha/2) * cos(beta/2) * ax + cos(alpha/2) * sin(beta/2) * ay - sin(alpha/2) * sin(beta/2)  * cross(ax, ay)) / sin(gamma/2)
    AngleAxis(axis * gamma)
end

Base.transpose(v :: AngleAxis) = AngleAxis(-v.angleaxis)
Base.adjoint(v :: AngleAxis) = transpose(v)

struct Camera{T}
    pose :: SVector{3,T}
    rotation :: SVector{3,T}
    intrinsics :: SVector{3,T}
end

"""
Construct a camera from a rotation, translation, instrinsics vector.
"""
function Camera(x :: AbstractArray)
    Camera(SVector{3,Float64}(x[4:6]), SVector{3,Float64}(x[1], x[2], x[3]), SVector{3,Float64}(x[7:9]))
end

rot(c :: Camera) = RodriguesVec(c.rotation...)
pose(c :: Camera) = c.pose
center(c :: Camera) = -(rot(c)' * pose(c))
Base.vec(c :: Camera) = vcat(c.rotation, c.pose, c.intrinsics)

function axisangle(x :: Array{Float64, 2})
    u = [x[3,2]-x[2,3], x[3,1]-x[1,3], x[2,1] - x[1,2]]
    angle = acos((trace(x)-1)/2)
    SVector{3,Float64}((u / norm(u) * 2 * sin(angle))...)
end

struct BA
    cameras :: Vector{Camera}
    observations :: Vector{Vector{Tuple{Int64,Float64,Float64}}}
    points :: Vector{SVector{3,Float64}}
end

function restrict(ba :: BA, inds; ignore_points=false, vis_thresh=2)
    cams = ba.cameras[inds]
    obs = ba.observations[inds]
    obs, points = if !ignore_points
        # get all points still visible
        ps = vcat(map(x -> map(y -> y[1], x), obs)...)
        # remove points visible by only one camera (or threshold)
        ps = filter(x -> count(i -> i == x, ps) >= vis_thresh, unique(ps))
        # remap point ids
        d = Dict(zip(ps, 1:length(ps)))
        points = ba.points[ps]
        obs = map(obs) do ob
            vs = []
            for (i, x, y) in ob
                # drop points
                if i in keys(d)
                    push!(vs, (d[i], x, y))
                end
            end
            vs
        end
        obs, points
    else
        obs, ba.points
    end
    BA(cams, obs, points)
end

num_cameras(ba :: BA) = length(ba.cameras)
num_points(ba :: BA) = length(ba.points)
num_observations(ba :: BA) = sum(length.(ba.observations))

function Base.show(io :: IO, ba :: BA)
    print(io, "Bundle adjustment problem with $(length(ba.cameras)) cameras, $(length(ba.points)) points, $(sum(length.(ba.observations))) observations")
end

function readbal(filename)
    isfile(filename) || SystemError("$filename is not a file")
    if splitext(filename)[2] == ".bbal"
        open(filename) do f
            num_cameras = read(f, UInt64) |> ntoh
            num_points = read(f, UInt64) |> ntoh
            num_observations = read(f, UInt64) |> ntoh

            obs = map(1:num_cameras) do i
                nobs = read(f, UInt64) |> ntoh
                obs = Vector{Tuple{Int64,Float64,Float64}}()
                sizehint!(obs, nobs)
                for j in 1:nobs
                    p = read(f, UInt64) |> ntoh
                    u = read(f, Float64) |> ntoh
                    v = read(f, Float64) |> ntoh
                    push!(obs, (p+1, u, v))
                end
                obs
            end

            cameras = map(1:num_cameras) do i
                ary = Array{Float64}(undef, 9)
                read!(f, ary)
                Camera(ntoh.(ary))
            end

            points = map(1:num_points) do i
                ary = Array{Float64}(undef, 3)
                read!(f, ary)
                SVector{3,Float64}(ntoh.(ary))
            end

            BA(cameras, obs, points)
        end
    else
        f = open(filename)
        line = readline(f)
        (num_cameras, num_points, num_observations) = map(x->parse(Int,x), split(line))

        obs = [Vector{Tuple{Int64,Float64,Float64}}() for _ in  1:num_cameras]

        for i in 1:num_observations
            (c_ind_, p_ind_, x_, y_) = split(readline(f))
            c_ind = parse(Int, c_ind_)+1
            p_ind = parse(Int, p_ind_)+1
            x = parse(Float64, x_)
            y = parse(Float64, y_)
            push!(obs[c_ind], (p_ind, x, y))
        end

        cameras = map(1:num_cameras) do i
            t = map(1:9) do j
                parse(Float64, readline(f))
            end
            Camera(t)
        end

        points = map(1:num_points) do i
            t = map(1:3) do j
                parse(Float64, readline(f))
            end
            SVector{3,Float64}(t...)
        end

        BA(cameras, obs, points)
    end
end

function visibility_graph(ba :: BA)
    is = []
    sizehint!(is, num_observations(ba))
    js = []
    sizehint!(js, num_observations(ba))
    for (i, obs) in enumerate(ba.observations)
        for (j, _, _) in obs
            push!(is, i)
            push!(js, j)
        end
    end
    m = sparse(is, js, ones(Int, length(js)))
    m * m'
end

function writebal(filepath :: AbstractString, ba :: BA)
    open(filepath, "w") do f
        if splitext(filepath)[2] == ".bbal"
            write(f, UInt64(num_cameras(ba)) |> hton)
            write(f, UInt64(num_points(ba)) |> hton)
            write(f, UInt64(num_observations(ba)) |> hton)

            for (cam, obs) in enumerate(ba.observations)
                write(f, UInt64(length(obs)) |> hton)
                for (point, x, y) in obs
                    write(f, UInt64(point - 1) |> hton)
                    write(f, Float64(x) |> hton)
                    write(f, Float64(y) |> hton)
                end
            end

            for c in ba.cameras
                write(f, hton.(Float64.(vec(c))))
            end

            for p in ba.points
                write(f, hton.(Float64.(p)))
            end
        else
            write(f, "$(num_cameras(ba)) $(num_points(ba)) $(num_observations(ba))\n")
            for (cam, obs) in enumerate(ba.observations)
                for (point, x, y) in obs
                    # zero indexed
                    write(f, "$(cam - 1) $(point - 1) $x $y\n")
                end
            end
            for cam in ba.cameras
                for v in vec(cam)
                    write(f, "$v ")
                end
                write(f, "\n")
            end
            for point in ba.points
                write(f, "$(point[1]) $(point[2]) $(point[3])\n")
            end
        end
    end
end

end # module


module TestCategories
using Test

using Catlab.Theories
using Catlab.CategoricalAlgebra.Sets, Catlab.CategoricalAlgebra.Categories

# Categories from Julia types
#############################

C = TypeCat(FreeCategory.Ob, FreeCategory.Hom)
@test Ob(C) == TypeSet(FreeCategory.Ob)

x, y = Ob(FreeCategory, :x, :y)
f = Hom(:f, x, y)
F = id(C)
@test (dom(F), codom(F)) == (C, C)
@test F(x) == x
@test F(f) == f

α = id(F)
@test (dom(α), codom(α)) == (F, F)

end


using MLDatasets: MNIST
using Flux: onehotbatch
using ProtoGrad: Conv, Linear, Dropout, Compose, maxpool, relu, softmax
using ProtoGrad: SupervisedObjective, cross_entropy, class_error
using ProtoGrad: Adam, forever, within_training_mode
using ProgressMeter: @showprogress
using StatsBase: sample
using Serialization: serialize, deserialize

train_x, train_y = MNIST.traindata(Float32, dir=joinpath(".", "datasets", "MNIST"))
test_x, test_y = MNIST.testdata(Float32, dir=joinpath(".", "datasets", "MNIST"))

train_x_with_channel = reshape(train_x, size(train_x)[1:2]..., 1, size(train_x)[3])
train_y_onehot = onehotbatch(train_y, 0:9)

height, width, input_channels, dataset_size = size(train_x_with_channel)
output_channels = 16
num_classes = 10

out_conv_size = (div(height, 4) - 3, div(width, 4) - 3, 16)

m = Compose(
    Dropout(0.1),
    Conv(input_channels=>6, (5, 5)),
    x -> relu.(x),                       # can also use ReLU()
    x -> maxpool(x, (2, 2)),             # can also use MaxPool((2, 2))
    Dropout(0.1),
    Conv(6=>output_channels, (5, 5)),
    x -> relu.(x),
    x -> maxpool(x, (2, 2)),
    x -> reshape(x, :, size(x, 4)),      # can also use Reshape((...))
    Dropout(0.1),
    Linear(prod(out_conv_size)=>120),
    x -> relu.(x),
    Dropout(0.1),
    Linear(120=>84),
    x -> relu.(x),
    Dropout(0.1),
    Linear(84=>num_classes),
    softmax,
)

batch_size = 128

training_batches = forever() do
    idx = sample(1:size(train_x_with_channel)[end], batch_size, replace = false)
    return (train_x_with_channel[:, :, :, idx], train_y_onehot[:, idx])
end

f = SupervisedObjective(cross_entropy, training_batches)

optimizer = Adam()
iterations = Iterators.Stateful(optimizer(m, f))

num_epochs = 20
num_batches_per_epoch = 500

within_training_mode() do
    @showprogress for epoch in 1:num_epochs
        for output in Iterators.take(iterations, num_batches_per_epoch)
            global m = output.solution
        end
    end
end

model_filename = "./serialized_conv_model"
println("saving model to $(model_filename)")
serialize(model_filename, m)

m_deserialized = deserialize(model_filename)

test_x_with_channel = reshape(test_x, size(test_x)[1:2]..., 1, size(test_x)[3])
test_y_onehot = onehotbatch(test_y, 0:9)

err = class_error(m_deserialized(test_x_with_channel), test_y_onehot)

println("test error = $(err * 100)%")


module ComposeTests

using InplaceArrays.TensorValues
using InplaceArrays.Arrays
using InplaceArrays.Fields
using InplaceArrays.Fields: MockField
using InplaceArrays.Fields: Comp
using FillArrays

import InplaceArrays.Fields: ∇

np = 4
p = Point(1,2)
x = fill(p,np)

fun(x,y) = 2*x
∇fun(x,y) = VectorValue(2*one(x[1]),2*one(x[1]))
∇(::typeof(fun)) = ∇fun

v = 3.0
d = 2
f = MockField{d}(v)
fx = fill(v,np)

k = Comp(fun)
test_kernel(k,(v,v),2*v)

g = compose(fun,f,f)
gx = 2*fx
∇gx = fill(∇fun(v,v),np)
test_field(g,x,gx,grad=∇gx)

l = 10
af = Fill(f,l)
ax = fill(x,l)
ag = compose(fun,af,af)
agx = fill(gx,l)
a∇gx = fill(∇gx,l)
test_array_of_fields(ag,ax,agx,grad=a∇gx)

end # module


using Distributions

abstract type Bandit end
pull(b::Bandit, action_idx::Integer) = 0
regret(b::Bandit, action_idx::Integer) = 0

struct StaticBandit <: Bandit
  action_distributions::AbstractVector{Distribution}
  num_arms::Integer
  optimal_expected_return::Float64 #here the arms are static so the best arm is always the same
end

# TODO: weird typing issue here - have a sense it's a Julia bug or I'm misunderstanding something in v0.6
staticbandit(action_distributions) = StaticBandit(action_distributions, length(action_distributions), maximum([mean(distr) for distr in action_distributions]))

pull(sb::StaticBandit, action_idx::Integer) = rand(sb.action_distributions[action_idx])

regret(sb::StaticBandit, action_idx::Integer) = sb.optimal_expected_return - mean(sb.action_distributions[action_idx])


"""
Includes all useful functions for applying GPR to T and wT profiles from Oceananigans.jl simulations.
Uses ProfileData struct to store data and GP struct for performing GPR on the data in ProfileData object.
"""
module GaussianProcess

using LearnConvection.Data

include("kernels.jl")
export  Kernel,
        SquaredExponentialI,
        RationalQuadraticI,
        Matern12I,
        Matern32I,
        Matern52I,
        SpectralMixtureProductI,
        SpectralMixtureProductA,
        kernel_function

include("distances.jl")
export  euclidean_distance,
        derivative_distance,
        antiderivative_distance

include("gp.jl")
export  model_output,
        uncertainty,
        compute_kernel_matrix,
        mean_log_marginal_loss
export  model
        # predict

export get_kernel

"""
```get_kernel(kernel_id::Int64, logγ, logσ, distance; logα=0.0)```

# Description
Returns a Kernel object with the specified parameters.

# Arguments
- `kernel_id`: (Int64). Identification number for the kernel type (see kernel options)
- `logγ`: (Float64). Log(length scale) parameter.
- `logσ`: (Float64). Log(signal variance) parameter.
- `distance`: (Function). Distance function to use in the kernel.

# Keyword Arguments
- `logα`: (Float64). Log(α) parameter if kernel_id = 5.

# kernel options
 1   =>   "Squared exponential kernel:        k(x,x') = σ * exp( - d(x,x')² / 2γ² )",
 2   =>   "Matérn with ʋ=1/2:                 k(x,x') = σ * exp( - d(x,x') / γ )",
 3   =>   "Matérn with ʋ=3/2:                 k(x,x') = σ * (1+c) * exp(-√(3)*d(x,x'))/γ)",
 4   =>   "Matérn with ʋ=5/2:                 k(x,x') = σ * ( 1 + √(5)*d(x,x'))/γ + 5*d(x,x')²/(3*γ^2) ) * exp(-√(5)*d(x,x'))/γ)",
 5   =>   "Rational quadratic kernel:         k(x,x') = σ * (1+(x-x')'*(x-x')/(2*α*(γ²))^(-α)",
"""
# function get_kernel(kernel_id::Int64, logγ, logσ, distance; logα=0.0)
#         # convert from log10 scale
#         γ = 10^logγ
#         σ = 10^logσ
#         α = 10^logα
#         if kernel_id==1; return SquaredExponentialI(γ, σ, distance) end
#         if kernel_id==2; return Matern12I(γ, σ, distance) end
#         if kernel_id==3; return Matern32I(γ, σ, distance) end
#         if kernel_id==4; return Matern52I(γ, σ, distance) end
#         if kernel_id==5; return RationalQuadraticI(γ, σ, α, distance)
#         else; throw(error()) end
# end

function get_kernel(kernel_id::Int64, args...)
        # args = f.(args) # convert from log10 scale

        function g(x)
                if typeof(x) <: Number
                        return 10^x end
                return x
        end

        args = g.(args)

        if kernel_id==1; return SquaredExponentialI(args...) end
        if kernel_id==2; return Matern12I(args...) end
        if kernel_id==3; return Matern32I(args...) end
        if kernel_id==4; return Matern52I(args...) end
        if kernel_id==5; return RationalQuadraticI(args...) end
        if kernel_id==6; return SpectralMixtureProductI(args...)
        else; throw(error()) end
end

end


@testset "Block Tridiagonal" begin
    solve = AirfoilMeshers.solve_block_tridiag!
    DIM = 5
    A = [rand(2, 2) for i in 1:DIM]
    B = [I + rand(2, 2) for i in 1:DIM]
    C = [rand(2, 2) for i in 1:DIM]
    D = [rand(2) for i in 1:DIM]
    MM = zeros(2DIM, 2DIM)
    blk(MM, i, j) = @view MM[(2i - 1):(2i), (2j - 1):(2j)]
    for i in 1:DIM
        blk(MM, i, i) .= B[i]
        if i > 1
            blk(MM, i, i - 1) .= A[i]
        end
        if i < DIM
            blk(MM, i, i + 1) .= C[i]
        end
    end
    DD = vcat(D...)
    X0 = MM \ DD
    solve(A,B,C,D)
    X1 = vcat(D...)
    @test X0 ≈ X1
end


using Plots

include("functions.jl")

assets_path = "$(@__DIR__)/../assets"

plot_fibo(n) = begin
    (0:(n - 1), fibonacci(n)) |> plot
    title!("Fibonacci Sequence")
    xlabel!("N")
    ylabel!("Fibonacci(N)")
    savefig("$assets_path/fibonacci($n).png")
end

factor_data(n, base, filt=nothing) = begin
    0:(n - 1) |>
    (x -> (collect(x), [factor(i, base) for i ∈ fibonacci(n)])) |>
    zip |>
    collect |>
    (x -> filt === nothing ? x : filter(a -> filt(a[2]), x)) |>
    unzip
end

plot_factor(n, base) = begin
    factor_data(n, base) |> plot
    title!("Factor $base in Fibonacci Numbers")
    xlabel!("N")
    ylabel!("Factor $base in Fibonacci(N)")
    savefig("$assets_path/factor($n,$base).png")
end

plot_factor_except(n, base, except) = begin
    factor_data(n, base, x -> x % except ≠ 0) |> plot
    title!("Factor $base in Fibonacci Numbers except exponent $except")
    xlabel!("N")
    ylabel!("Factor $base in Fibonacci(N) except exponent $except")
    savefig("$assets_path/factor($n,$base)except($except).png")
end

plot_factor_only(n, base, only) = begin
    factor_data(n, base, x -> x % only == 0) |> plot
    title!("Factor $base in Fibonacci Numbers only for exponent $only")
    xlabel!("N")
    ylabel!("Factor $base in Fibonacci(N) only for exponent $only")
    savefig("$assets_path/factor($n,$base)only($only).png")
end

plot_factor(n, base::Vector) = begin
    p = factor_data(n, base[begin]) |> (a, b) -> plot(a, b, label="Base $(base[begin])")
    for i ∈ base[begin + 1:end - 1]
        data = factor_data(n, i)
        plot!(p, data..., label="Base $i")
    end
    factor_data(n, base[end]) |> (a, b) -> plot!(a, b, label="Base $(base[end])")
    title!("Factor $base in Fibonacci Numbers")
    xlabel!("N")
    ylabel!("Multiple factors in Fibonacci(N)")
    bases = map(x -> "$x", base) |> x -> join(x, ",")
    savefig("$assets_path/factor($n,[$bases]).png")
end

function plot_distance(n, base1, base2, e, op)
    (x1, _) = factor_data(n, base1) |>
        zip |>
        collect |>
        k -> filter(d -> d[2] == e, k) |>
        unzip
    (x2, _) = factor_data(n, base2) |>
        zip |>
        collect |>
        k -> filter(d -> d[2] == e, k) |>
        unzip

    (length(x1), length(x2)) |>
    min |>
    (l -> (x1[begin:l], x2[begin:l])) |>
    op |>
    enumerate |>
    unzip |>
    plot

    optitle = if (op == -) "Difference" else "Division" end
    title!("$optitle of factors $base1 and $base2 in Fibonacci(N) for exponent $e")
    xlabel!("N")
    ylabel!("$optitle(N)")
    opname = if (op == -) "minus" else "div" end
    savefig("$assets_path/distance($n,$base1,$base2,$e,$opname).png")
end


#=
regularized matrix regression with row-sparsity

min ‖Y - X A‖ + λₙ ‖A‖⋆ :
Aᵢⱼ ∈ [-1,1], ∀i,j
ℓ₀(‖A₁‖₁, …, ‖Aₙ‖₁) ≤ k
where:
- design matrix X is n x p
- response matrix Y is n x m
- coefficient matrix variable A is p x m
- Aᵢ is the ith row of A
=#

struct MatrixRegression <: ExampleInstance
    n::Int # number of samples
    p::Int # number of predictors
    m::Int # number of responses
    use_nat::Bool # use nuclear norm cone, else PSD cone formulation
end

function build(inst::MatrixRegression)
    (n, m, p) = (inst.n, inst.m, inst.p)
    @assert n >= p >= m > 1
    k = ceil(Int, p * 2 / 3)

    # generate noisy data
    A0 = 2 * rand(p, m) .- 1
    A0[(k + 1):end, :] .= 0
    X = randn(n, p)
    Y = X * A0 + 0.1 * randn(n, m)

    # use dimension reduction via QR
    F = qr(X, ColumnNorm())
    Y = (F.Q' * Y)[1:p, :]
    X = F.R[1:p, 1:p] * F.P'
    λ = 0.2 # TODO?

    # build model
    model = JuMP.Model()
    JuMP.@variable(model, loss)
    JuMP.@variable(model, nuc)
    JuMP.@variable(model, A[1:p, 1:m])
    JuMP.@variable(model, row_on[1:p], Bin)

    JuMP.@objective(model, Min, loss + λ * nuc)

    JuMP.@constraints(
        model,
        begin
            vcat(loss, vec(Y - X * A)) in
            Hypatia.EpiNormEuclCone{Float64}(1 + length(vec(Y)))
            [i = 1:p], A[i, :] .<= row_on[i]
            [i = 1:p], A[i, :] .>= -row_on[i]
            sum(row_on) <= k
        end
    )

    add_nonsymm_specnuc(inst.use_nat, true, nuc, A', model)

    # save for use in tests
    model.ext[:λ] = λ
    model.ext[:X] = X
    model.ext[:Y] = Y
    model.ext[:A] = A
    model.ext[:row_on] = row_on

    return model
end

function test_extra(inst::MatrixRegression, model::JuMP.Model)
    stat = JuMP.termination_status(model)
    @test stat in OPT_OR_LIMIT
    (stat == MOI.OPTIMAL) || return

    tol = eps()^0.2
    A = JuMP.value.(model.ext[:A])
    (Y, X) = (model.ext[:Y], model.ext[:X])
    # check objective
    loss = norm(Y - X * A)
    nuc = sum(svdvals(A))
    obj_result = loss + model.ext[:λ] * nuc
    @test JuMP.objective_value(model) ≈ obj_result atol = tol rtol = tol
    # check integer feasibility
    row_on = JuMP.value.(model.ext[:row_on])
    @test row_on ≈ round.(Int, row_on) atol = tol rtol = tol
    @test all(-tol .<= row_on .<= 1 + tol)
    for i in 1:size(A, 1)
        if abs(row_on[i]) < tol
            @assert norm(A[i, :]) < tol
        end
    end
    return
end


using BayesianDataFusion
using Base.Test

using DataFrames

A = randn(15,2);
B = randn(20,2);
C = randn(2,2);

X = Float64[ sum(A[i,:].*B[j,:].*C[k,:]) for i in 1:size(A,1), j in 1:size(B,1), k in 1:size(C,1)]

df = DataFrame(A=Int64[], B=Int64[], C=Int64[], v=Float64[])
for i=1:size(A,1), j=1:size(B,1), k=1:size(C,1)
  push!(df, Any[i, j, k, X[i,j,k]])
end

rd = RelationData(df)
@test rd.entities[1].name == "A"
@test rd.entities[2].name == "B"
@test rd.entities[3].name == "C"

assignToTest!(rd.relations[1], 10)
result = macau(rd, burnin=50, psamples=10, num_latent=2, verbose=false)

## checking prediction for whole matrix for tensors
Yhat = pred_all(rd.relations[1])
@test size(Yhat) == size(X)
yprod   = rd.entities[1].model.sample[:,4]
yprod .*= rd.entities[2].model.sample[:,2]
yprod .*= rd.entities[3].model.sample[:,1]
@test_approx_eq Yhat[4,2,1] sum(yprod)+rd.relations[1].model.mean_value


include("utils.jl")

@testset "Stiefel" begin
    @testset "Real" begin
        M = Stiefel(3, 2)
        @testset "Basics" begin
            @test repr(M) == "Stiefel(3, 2, ℝ)"
            x = [1.0 0.0; 0.0 1.0; 0.0 0.0]
            @test representation_size(M) == (3, 2)
            @test manifold_dimension(M) == 3
            base_manifold(M) === M
            @test_throws DomainError is_manifold_point(M, [1.0, 0.0, 0.0, 0.0], true)
            @test_throws DomainError is_manifold_point(
                M,
                1im * [1.0 0.0; 0.0 1.0; 0.0 0.0],
                true,
            )
            @test !is_tangent_vector(M, x, [0.0, 0.0, 1.0, 0.0])
            @test_throws DomainError is_tangent_vector(
                M,
                x,
                1 * im * zero_tangent_vector(M, x),
                true,
            )
        end
        @testset "Embedding and Projection" begin
            x = [1.0 0.0; 0.0 1.0; 0.0 0.0]
            y = similar(x)
            z = embed(M, x)
            @test z == x
            embed!(M, y, x)
            @test y == z
            a = [1.0 0.0; 0.0 2.0; 0.0 0.0]
            @test !is_manifold_point(M, a)
            b = similar(a)
            c = project(M, a)
            @test c == x
            project!(M, b, a)
            @test b == x
            X = [0.0 0.0; 0.0 0.0; -1.0 1.0]
            Y = similar(X)
            Z = embed(M, x, X)
            embed!(M, Y, x, X)
            @test Y == X
            @test Z == X
        end

        types = [Matrix{Float64}]
        TEST_FLOAT32 && push!(types, Matrix{Float32})
        TEST_STATIC_SIZED && push!(types, MMatrix{3,2,Float64,6})

        @testset "Stiefel(2, 1) special case" begin
            M21 = Stiefel(2, 1)
            w = inverse_retract(
                M21,
                SMatrix{2,1}([0.0, 1.0]),
                SMatrix{2,1}([sqrt(2), sqrt(2)]),
                QRInverseRetraction(),
            )
            @test isapprox(M21, w, SMatrix{2,1}([1.0, 0.0]))
        end

        @testset "inverse QR retraction cases" begin
            M43 = Stiefel(4, 3)
            p = [1.0 0.0 0.0; 0.0 1.0 0.0; 0.0 0.0 1.0; 0.0 0.0 0.0]
            Xinit = [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0; 1.0 1.0 1.0]
            q = retract(M43, p, Xinit, QRRetraction())
            X1 = inverse_retract(
                M43,
                SMatrix{4,3}(p),
                SMatrix{4,3}(q),
                QRInverseRetraction(),
            )
            X2 = inverse_retract(M43, p, q, QRInverseRetraction())
            @test isapprox(M43, p, X1, X2)
            @test isapprox(M43, p, X1, Xinit)

            p2 = [1.0 0.0; 0.0 1.0; 0.0 0.0]
            q2 = exp(M, p2, [0.0 0.0; 0.0 0.0; 1.0 1.0])

            X1 = inverse_retract(
                M,
                SMatrix{3,2}(p2),
                SMatrix{3,2}(q2),
                QRInverseRetraction(),
            )
            X2 = inverse_retract(M, p2, q2, QRInverseRetraction())
            @test isapprox(M43, p2, X1, X2)
        end

        @testset "Type $T" for T in types
            x = [1.0 0.0; 0.0 1.0; 0.0 0.0]
            y = exp(M, x, [0.0 0.0; 0.0 0.0; 1.0 1.0])
            z = exp(M, x, [0.0 0.0; 0.0 0.0; -1.0 1.0])
            @test isapprox(
                M,
                retract(
                    M,
                    SMatrix{3,2}(x),
                    SA[0.0 0.0; 0.0 0.0; -1.0 1.0],
                    PolarRetraction(),
                ),
                retract(M, x, [0.0 0.0; 0.0 0.0; -1.0 1.0], PolarRetraction()),
                atol=1e-15,
            )
            pts = convert.(T, [x, y, z])
            v = inverse_retract(M, x, y, PolarInverseRetraction())
            @test !is_manifold_point(M, 2 * x)
            @test_throws DomainError !is_manifold_point(M, 2 * x, true)
            @test !is_tangent_vector(M, 2 * x, v)
            @test_throws DomainError !is_tangent_vector(M, 2 * x, v, true)
            @test !is_tangent_vector(M, x, y)
            @test_throws DomainError is_tangent_vector(M, x, y, true)
            test_manifold(
                M,
                pts,
                basis_types_to_from=(DefaultOrthonormalBasis(),),
                basis_types_vecs=(DefaultOrthonormalBasis(),),
                test_exp_log=false,
                default_inverse_retraction_method=PolarInverseRetraction(),
                test_injectivity_radius=false,
                test_is_tangent=true,
                test_project_tangent=true,
                test_default_vector_transport=false,
                point_distributions=[Manifolds.uniform_distribution(M, pts[1])],
                test_forward_diff=false,
                test_reverse_diff=false,
                test_vee_hat=false,
                projection_atol_multiplier=15.0,
                retraction_atol_multiplier=10.0,
                is_tangent_atol_multiplier=4 * 10.0^2,
                retraction_methods=[
                    PolarRetraction(),
                    QRRetraction(),
                    CayleyRetraction(),
                    PadeRetraction(2),
                ],
                inverse_retraction_methods=[
                    PolarInverseRetraction(),
                    QRInverseRetraction(),
                ],
                vector_transport_methods=[
                    DifferentiatedRetractionVectorTransport{PolarRetraction}(),
                    DifferentiatedRetractionVectorTransport{QRRetraction}(),
                    ProjectionTransport(),
                ],
                vector_transport_retractions=[
                    PolarRetraction(),
                    QRRetraction(),
                    PolarRetraction(),
                ],
                vector_transport_inverse_retractions=[
                    PolarInverseRetraction(),
                    QRInverseRetraction(),
                    PolarInverseRetraction(),
                ],
                test_vector_transport_direction=[true, true, false],
                mid_point12=nothing,
            )

            @testset "inner/norm" begin
                v1 = inverse_retract(M, pts[1], pts[2], PolarInverseRetraction())
                v2 = inverse_retract(M, pts[1], pts[3], PolarInverseRetraction())

                @test real(inner(M, pts[1], v1, v2)) ≈ real(inner(M, pts[1], v2, v1))
                @test imag(inner(M, pts[1], v1, v2)) ≈ -imag(inner(M, pts[1], v2, v1))
                @test imag(inner(M, pts[1], v1, v1)) ≈ 0

                @test norm(M, pts[1], v1) isa Real
                @test norm(M, pts[1], v1) ≈ sqrt(inner(M, pts[1], v1, v1))
            end
        end

        @testset "Distribution tests" begin
            usd_mmatrix = Manifolds.uniform_distribution(M, @MMatrix [
                1.0 0.0
                0.0 1.0
                0.0 0.0
            ])
            @test isa(rand(usd_mmatrix), MMatrix)
        end
    end

    @testset "Complex" begin
        M = Stiefel(3, 2, ℂ)
        @testset "Basics" begin
            @test repr(M) == "Stiefel(3, 2, ℂ)"
            @test representation_size(M) == (3, 2)
            @test manifold_dimension(M) == 8
            @test Manifolds.allocation_promotion_function(M, exp!, (1,)) == complex
            @test !is_manifold_point(M, [1.0, 0.0, 0.0, 0.0])
            @test !is_tangent_vector(M, [1.0 0.0; 0.0 1.0; 0.0 0.0], [0.0, 0.0, 1.0, 0.0])
            x = [1.0 0.0; 0.0 1.0; 0.0 0.0]
        end
        types = [Matrix{ComplexF64}]
        @testset "Type $T" for T in types
            x = [0.5+0.5im 0.5+0.5im; 0.5+0.5im -0.5-0.5im; 0.0 0.0]
            y = exp(M, x, [0.0 0.0; 0.0 0.0; 1.0 1.0])
            z = exp(M, x, [0.0 0.0; 0.0 0.0; -1.0 1.0])
            pts = convert.(T, [x, y, z])
            v = inverse_retract(M, x, y, PolarInverseRetraction())
            @test !is_manifold_point(M, 2 * x)
            @test_throws DomainError !is_manifold_point(M, 2 * x, true)
            @test !is_tangent_vector(M, 2 * x, v)
            @test_throws DomainError !is_tangent_vector(M, 2 * x, v, true)
            @test !is_tangent_vector(M, x, y)
            @test_throws DomainError is_tangent_vector(M, x, y, true)
            test_manifold(
                M,
                pts,
                test_exp_log=false,
                default_inverse_retraction_method=PolarInverseRetraction(),
                test_injectivity_radius=false,
                test_is_tangent=true,
                test_project_tangent=true,
                test_default_vector_transport=false,
                test_forward_diff=false,
                test_reverse_diff=false,
                test_vee_hat=false,
                projection_atol_multiplier=15.0,
                retraction_atol_multiplier=10.0,
                is_tangent_atol_multiplier=4 * 10.0^2,
                retraction_methods=[PolarRetraction(), QRRetraction()],
                inverse_retraction_methods=[
                    PolarInverseRetraction(),
                    QRInverseRetraction(),
                ],
                vector_transport_methods=[
                    DifferentiatedRetractionVectorTransport{PolarRetraction}(),
                    DifferentiatedRetractionVectorTransport{QRRetraction}(),
                    ProjectionTransport(),
                ],
                vector_transport_retractions=[
                    PolarRetraction(),
                    QRRetraction(),
                    PolarRetraction(),
                ],
                vector_transport_inverse_retractions=[
                    PolarInverseRetraction(),
                    QRInverseRetraction(),
                    PolarInverseRetraction(),
                ],
                test_vector_transport_direction=[true, true, false],
                mid_point12=nothing,
            )

            @testset "inner/norm" begin
                v1 = inverse_retract(M, pts[1], pts[2], PolarInverseRetraction())
                v2 = inverse_retract(M, pts[1], pts[3], PolarInverseRetraction())

                @test real(inner(M, pts[1], v1, v2)) ≈ real(inner(M, pts[1], v2, v1))
                @test imag(inner(M, pts[1], v1, v2)) ≈ -imag(inner(M, pts[1], v2, v1))
                @test imag(inner(M, pts[1], v1, v1)) ≈ 0

                @test norm(M, pts[1], v1) isa Real
                @test norm(M, pts[1], v1) ≈ sqrt(inner(M, pts[1], v1, v1))
            end
        end
    end

    @testset "Quaternion" begin
        M = Stiefel(3, 2, ℍ)
        @testset "Basics" begin
            @test representation_size(M) == (3, 2)
            @test manifold_dimension(M) == 18
        end
    end

    @testset "Padé & Caley retractions and Caley based transport" begin
        M = Stiefel(3, 2)
        p = [1.0 0.0; 0.0 1.0; 0.0 0.0]
        X = [0.0 0.0; 0.0 0.0; 1.0 1.0]
        r1 = CayleyRetraction()
        @test r1 == PadeRetraction(1)
        @test repr(r1) == "CayleyRetraction()"
        q1 = retract(M, p, X, r1)
        @test is_manifold_point(M, q1)
        Y = vector_transport_direction(
            M,
            p,
            X,
            X,
            DifferentiatedRetractionVectorTransport(CayleyRetraction()),
        )
        @test is_tangent_vector(M, q1, Y; atol=10^-15)
        Y2 = vector_transport_direction(
            M,
            p,
            X,
            X,
            DifferentiatedRetractionVectorTransport{CayleyRetraction}(),
        )
        @test is_tangent_vector(M, q1, Y2; atol=10^-15)
        r2 = PadeRetraction(2)
        @test repr(r2) == "PadeRetraction(2)"
        q2 = retract(M, p, X, r2)
        @test is_manifold_point(M, q2)
    end
end


using FGES
using CSV, DataFrames
using Combinatorics
using Statistics

#Generate a dataset
numFeatures = 100
numObservations = 10000
data = zeros(numObservations, numFeatures)

#number of previous features to use in caluclating the next feature
n=5

for i in 1:numFeatures
    if i ≤ n
        data[:,i] = randn(numObservations)
    else
        data[:,i] = sum(rand()*data[:,i-j] for j∈1:n) + randn(numObservations)
        #Stop the data from exploding
        data[:,i] ./= mean(data[:,i])
    end
end

#Run the julia version of FGES and time it
gJulia = @time fges(data)

#Save the dataset to run for java
df = DataFrame(data, :auto)
CSV.write("test/testDatasets/features$(numFeatures)_observations$(numObservations).txt", df ,delim=" ")

#Read and parse the java results
gJava = PDAG(numFeatures)
javaFilepath = "test/testDatasets/features$(numFeatures)_observations$(numObservations)_java.txt"

javaResult = filter(line -> occursin(r"x\d+ \D{3} x\d+",line),readlines(open(javaFilepath)))
javaResult = split.(javaResult," ")

javaEdges = [parse.(Int, filter.(isdigit, line[[2,4]])) for line in javaResult]

for (i,edge) in enumerate(javaEdges)
    addedge!(gJava,edge...,directed=javaResult[i][3]=="-->" ? true : false) #not worried about orientation for now
end

#Generate the true graph
gTrue = PDAG(numFeatures)

for i=n+1:numFeatures
    for j=1:n
        addedge!(gTrue,i-j, i)
    end
end

#Calculate precision and recall
function contingencyTable(g,gTrue)

    continTable = zeros(Int,2,2)

    for (x,y) in combinations(vertices(gTrue),2)
        trueEdge = isadjacent(gTrue,x,y)
        estEdge = isadjacent(g,x,y)

        if trueEdge & estEdge #true positive
            continTable[1,1] += 1
        elseif !trueEdge & !estEdge #true negative
            continTable[2,2] += 1
        elseif !trueEdge & estEdge #false positive
            continTable[2,1] += 1
        else #false negative
            continTable[1,2] += 1
        end
    end

    return continTable
end

modelPrecision(continTable) = continTable[1,1] / sum(continTable[:,1])
modelRecall(continTable) = continTable[1,1] / sum(continTable[1,:])

continTableJulia = contingencyTable(gJulia,gTrue)
modelPrecision(continTableJulia)
modelRecall(continTableJulia)

continTableJava = contingencyTable(gJava,gTrue)
modelPrecision(continTableJava)
modelRecall(continTableJava)

#Calculate precision and recall
function contingencyTableOrient(g,gTrue)

    continTable = zeros(Int,2,2)

    for (x,y) in permutations(vertices(gTrue),2)
        # trueAdj = isadjacent(gTrue,x,y)
        # estAdj = isadjacent(g,x,y)

        # trueDir = isparent(gTrue,x,y)
        # estDir = isparent(g,x,y)
        trueDir = Edge(x,y,true) ∈ edges(gTrue)
        estDir = Edge(x,y,true) ∈ edges(g)

        # if (trueAdj & estAdj) & (!trueUndir & !estUndir) #true positive
        #     continTable[1,1] += 1
        # elseif (!trueAdj & !estAdj) | (trueUndir & estUndir) #true negative
        #     continTable[2,2] += 1
        # elseif (!trueAdj | trueUndir) & (estAdj & !estUndir) #false positive
        #     continTable[2,1] += 1
        # else #false negative
        #     continTable[1,2] += 1
        # end

        if trueDir & estDir
            continTable[1,1] += 1
        elseif !trueDir & !estDir
            continTable[2,2] += 1
        elseif !trueDir & estDir
            continTable[2,1] += 1
        else
            continTable[1,2] += 1
        end
    end

    return continTable
end

continTableJulia = contingencyTableOrient(gJulia,gTrue)
modelPrecision(continTableJulia)
modelRecall(continTableJulia)

continTableJava = contingencyTableOrient(gJava,gTrue)
modelPrecision(continTableJava)
modelRecall(continTableJava)


using Test
using PolynomialBasis
using ImplicitDomainQuadrature
# using Revise
using CutCellDG
include("../useful_routines.jl")

function test_incoherent_interface_simple_tension()
    L = 1.0
    W = 1.0
    lambda1, mu1 = 1.0, 2.0
    lambda2, mu2 = 3.0, 4.0
    penaltyfactor = 1e2
    nelmts = 1
    dx = L / nelmts
    dy = W / nelmts
    penalty = penaltyfactor / dx * (lambda1 + mu1 + lambda2 + mu2) * 0.5
    eta = 1

    applydisplacement = 0.01

    stiffness = CutCellDG.HookeStiffness(lambda1, mu1, lambda2, mu2)
    polyorder = 1
    numqp = 2

    elasticitybasis = LagrangeTensorProductBasis(2, polyorder)
    levelsetbasis = HermiteTensorProductBasis(2)
    dim, nf = size(interpolation_points(levelsetbasis))
    refpoints = interpolation_points(elasticitybasis)

    cgmesh = CutCellDG.CGMesh([0.0, 0.0], [L, W], [nelmts, nelmts], nf)
    mesh = CutCellDG.DGMesh([0.0, 0.0], [L, W], [nelmts, nelmts], refpoints)

    x0 = [0.5, 0.0]
    normal = [1.0, 0.0]
    levelset = CutCellDG.LevelSet(
        x -> plane_distance_function(x, normal, x0),
        cgmesh,
        levelsetbasis,
    )

    cutmesh = CutCellDG.CutMesh(mesh, levelset)
    cellquads = CutCellDG.CellQuadratures(cutmesh, levelset, numqp)
    interfacequads = CutCellDG.InterfaceQuadratures(cutmesh, levelset, numqp)
    facequads = CutCellDG.FaceQuadratures(cutmesh, levelset, numqp)

    mergedmesh = cutmesh

    onbottomboundary(x) = x[2] ≈ 0.0 ? true : false
    onrightboundary(x) = x[1] ≈ L ? true : false
    ontopboundary(x) = x[2] ≈ W ? true : false
    onleftboundary(x) = x[1] ≈ 0.0 ? true : false

    sysmatrix = CutCellDG.SystemMatrix()
    sysrhs = CutCellDG.SystemRHS()

    CutCellDG.assemble_displacement_bilinear_forms!(
        sysmatrix,
        elasticitybasis,
        cellquads,
        stiffness,
        mergedmesh,
    )
    CutCellDG.assemble_incoherent_interface_condition!(
        sysmatrix,
        elasticitybasis,
        interfacequads,
        stiffness,
        mergedmesh,
        penalty,
        eta,
    )
    CutCellDG.assemble_penalty_displacement_component_bc!(
        sysmatrix,
        sysrhs,
        x -> 0.0,
        elasticitybasis,
        facequads,
        stiffness,
        mergedmesh,
        x -> onleftboundary(x),
        [1.0, 0.0],
        penalty,
    )
    CutCellDG.assemble_penalty_displacement_component_bc!(
        sysmatrix,
        sysrhs,
        x -> 0.0,
        elasticitybasis,
        facequads,
        stiffness,
        mergedmesh,
        x -> onbottomboundary(x),
        [0.0, 1.0],
        penalty,
    )
    CutCellDG.assemble_penalty_displacement_component_bc!(
        sysmatrix,
        sysrhs,
        x -> applydisplacement,
        elasticitybasis,
        facequads,
        stiffness,
        mergedmesh,
        x -> onrightboundary(x),
        [1.0, 0.0],
        penalty,
    )

    matrix = CutCellDG.sparse_displacement_operator(sysmatrix, mergedmesh)
    rhs = CutCellDG.displacement_rhs_vector(sysrhs, mergedmesh)

    solution = matrix \ rhs
    nodaldisplacement = reshape(solution, 2, :)

    E1 = mu1 * (lambda1 + mu1) / (lambda1 + 2mu1)
    E2 = mu2 * (lambda2 + mu2) / (lambda2 + 2mu2)

    uI = E1 / (E1 + E2) * applydisplacement

    u10 = uI + (applydisplacement - uI) / 0.5 * (-0.5)
    Δx = applydisplacement

    u12 = -lambda1 / (lambda1 + 2mu1) * 2 * (Δx - uI)
    u22 = -lambda2 / (lambda2 + 2mu2) * 2 * uI

    testdisplacement = [
        u10 u10 Δx Δx 0.0 0.0 2uI 2uI
        0.0 u12 0.0 u12 0.0 u22 0.0 u22
    ]

    @test allapprox(nodaldisplacement, testdisplacement, 1e2eps())
end

test_incoherent_interface_simple_tension()


julia> n = 20
20

julia> [(x, y, z) for x = 1:n for y = x:n for z = y:n if x^2 + y^2 == z^2]
6-element Array{Tuple{Int64,Int64,Int64},1}:
 (3,4,5)
 (5,12,13)
 (6,8,10)
 (8,15,17)
 (9,12,15)
 (12,16,20)


function coef_G(::Type{T}, rn::Integer) where T <: AbstractFloat
    G = ones(T, rn, rn)
    for j = 1:rn
        for i = j+1:2:rn
            G[i,j] = -G[i,j]
        end
    end
    return G
end

coef_G(rn) = coef_G(Float64, rn)

function coef_K(::Type{T}, rn::Integer, rnm1::Integer) where T <:AbstractFloat
    K = ones(T, rn, rnm1)
    for j = 1:rnm1
        for i = 2:2:rn
            K[i,j] = -K[i,j]
        end
    end
    return K
end

coef_K(rn, rnm1) = coef_K(Float64, rn, rnm1)

function coef_Hn!(r::Integer, n::Integer, ℓbar_hi::Integer, t::OffsetVector{T},
                 M::Integer, store::Store{T},
                 version=2) where T <: AbstractFloat
    Hn = OffsetVector{Matrix{T}}(undef, 0:ℓbar_hi)
    for ℓbar = 0:ℓbar_hi
        Hn[ℓbar] = Array{T}(undef, r, r)
    end
    coef_Hn!(Hn, n, ℓbar_hi, t, M, store, version)
    return Hn
end

function coef_Hn!(Hn::OffsetVector{Matrix{T}}, n::Integer, ℓbar_hi::Integer,
                 t::OffsetVector{T}, M::Integer,
                 store::Store{T}, version=2) where T <: AbstractFloat
    N = length(t) - 1
    @argcheck 1 ≤ n ≤ N
    @argcheck 0 ≤ ℓbar_hi ≤ n-1
    @argcheck length(Hn) ≥ ℓbar_hi + 1
    rn = size(Hn[0], 1)
    @argcheck size(Hn[0], 2) == rn
    @argcheck rn ≤ store.rmax
    coef_H0_uniform!(Hn[0], store)
    kn = t[n] - t[n-1]
    α = store.α
    for j = 1:rn, i = 1:rn
        Hn[0][i,j] *= kn^α
    end
    if ℓbar_hi ≥ 1
        coef_Hn1!(Hn[1], n, t, M, store)
    end
    if ℓbar_hi ≥ 2
        if version == 1
            coef_Hn_ver1!(Hn, n, 2:ℓbar_hi, t, M, store)
        elseif version == 2
            coef_Hn_ver2!(Hn, n, 2:ℓbar_hi, t, M, store)
        else
            throw(ArgumentError("version must be 1 or 2"))
        end
    end
end

function coef_Hn1!(rn::Integer, rnm1::Integer, n::Integer, t::OffsetVector{T},
                   M::Integer, store::Store{T}) where T <: AbstractFloat
    Hn1 = Array{T}(undef, rn, rnm1)
    coef_Hn1!(Hn1, n, t, M, store)
    return Hn1
end

function coef_Hn1!(Hn1::Matrix{T}, n::Integer, t::OffsetVector{T}, M::Integer,
                   store::Store{T}) where T <: AbstractFloat
    rn, rnm1 = size(Hn1)
    @argcheck rnm1 ≤ store.rmax
    A = view(store.A, 1:rnm1)
    B = view(store.B, 1:rnm1)
    C = view(store.C, 1:rn, 1:rnm1)
    α = store.α
    kn = t[n] - t[n-1]
    knm1 = t[n-1] - t[n-2]
    ρn = kn / knm1

    σ, wσ = rule(store.legendre[M])
    Ψ = view(store.Ψ, 1:rnm1, 1:M)
    legendre_polys!(Ψ, σ)
    c = (1+ρn)^(1-α)
    for j = 1:rnm1
        s = zero(T)
        for m = 1:M
            s += wσ[m] * ( 2ρn + 1 - σ[m] )^(α-1) * Ψ[j,m]
        end
        A[j] = c * s
    end

    Mσ = ceil(Integer, rnm1/2)
    σ, wσ = rule(store.jacobi1[Mσ])
    Ψ = view(store.Ψ, 1:rnm1, 1:Mσ)
    legendre_polys!(Ψ, σ)
    for j = 1:rnm1
        s = zero(T)
        for m = 1:Mσ
            s+= wσ[m] * Ψ[j,m]
        end
        B[j] = c * s
    end

    Mτ = ceil(Integer, rn-1)
    Mσ = Mτ
    Mz = M
    z, wz = rule(store.unitlegendre[Mz])
    σ, wσ = rule(store.jacobi3[Mσ])
    τ, wτ = rule(store.jacobi4[Mτ])
    dΨ = view(store.dΨ, 1:rn, 1:Mτ)
    deriv_legendre_polys!(dΨ, τ)
    for j = 1:rnm1
        Ψ = view(store.Ψ, 1:j)
        for i = 1:rn
            outer = zero(T)
            for mτ = 1:Mτ
                τ_ = τ[mτ]
                inner = zero(T)
                for mz = 1:Mz
                    z_ = z[mz]
                    legendre_polys!(Ψ, 1 - z_*(1+τ_))
                    inner += wz[mz] * ( ρn + z_ )^(α-1) * Ψ[j]
                end
                outer += wτ[mτ] * dΨ[i,mτ] * inner
            end
            C[i,j] = c * outer
        end
    end
    Ψ = view(store.Ψ, 1:rnm1, 1:Mσ)
    legendre_polys!(Ψ, σ)
    for j = 1:rnm1
        for i = 1:rn
            dΨ = view(store.dΨ, 1:i)
            outer = zero(T)
            for mσ = 1:Mσ
                σ_ = σ[mσ]
                inner = zero(T)
                for mz = 1:Mz
                    z_ = z[mz]
                    deriv_legendre_polys!(dΨ, z_*(1-σ_)-1)
                    inner += wz[mz] * ( ρn*z_ + 1 )^(α-1) * dΨ[i]
                end
                outer += wσ[mσ] * Ψ[j,mσ] * inner
            end
            C[i,j] += c * outer
        end
    end

    D = ( kn + knm1 ) / 2
    c = D^(α-1) * knm1 / ( 2Γ(α) )
    for j = 1:rnm1
        pow = one(T)
        for i = 1:rn
            pow = -pow
            Hn1[i,j] = c * ( A[j] + pow * B[j] - C[i,j] )
        end
    end
end

function coef_Hn_ver1!(Hn::OffsetVector{Matrix{T}}, n::Integer, rng::UnitRange,
                       t::OffsetVector{T}, M::Integer,
                       store::Store{T}) where T <: AbstractFloat
    @argcheck rng.start ≥ 2
    rn = size(Hn[rng.start], 1)
    kn = t[n] - t[n-1]

    σ, wσ = rule(store.legendre[M])
    τ, wτ = σ, wσ
    Ψ  = view(store.Ψ,  1:store.rmax, 1:M)
    dΨ = view(store.dΨ, 1:store.rmax, 1:M)
    legendre_polys!(Ψ, σ)
    deriv_legendre_polys!(dΨ, τ)
    α = store.α
    c = 1 / ( 2Γ(α) )
    for ℓbar in rng
        rℓ = size(Hn[ℓbar], 2)
        @argcheck rℓ ≤ store.rmax
        ℓ = n - ℓbar
        kℓ = t[ℓ] - t[ℓ-1]
        two_D = (t[n]+t[n-1]) - (t[ℓ]+t[ℓ-1])

        A = view(store.A, 1:rℓ)
        B = view(store.B, 1:rℓ)
        C = view(store.C, 1:rn, 1:rℓ)

        for j = 1:rℓ
            s = zero(T)
            for m = 1:M
                Δ = ( kn - σ[m]*kℓ ) / two_D
                s += wσ[m] * ( 1 + Δ )^(α-1) * Ψ[j,m]
            end
            A[j] = s
        end

        for j = 1:rℓ
            s = zero(T)
            for m = 1:M
                Δ = ( -kn - σ[m]*kℓ ) / two_D
                s += wσ[m] * ( 1 + Δ )^(α-1) * Ψ[j,m]
            end
            B[j] = s
        end

        for j = 1:rℓ, i = 1:rn
            outer = zero(T)
            for mτ = 1:M
                inner = zero(T)
                for mσ = 1:M
                    Δ = ( τ[mτ]*kn - σ[mσ]*kℓ ) / two_D
                    inner += wσ[mσ] * (1+Δ)^(α-1) * Ψ[j,mσ]
                end
                outer += wτ[mτ] * dΨ[i,mτ] * inner
            end
            C[i,j] = outer
        end

        D = two_D / 2
        for j = 1:rℓ
            pow = one(T)
            for i = 1:rn
                pow = -pow
                Hn[ℓbar][i,j] = c * D^(α-1) * kℓ * (
                                A[j] + pow * B[j] - C[i,j] )
            end
        end
    end
end

function coef_Hn_ver2!(Hn::OffsetVector{Matrix{T}}, n::Integer, rng::UnitRange,
                       t::OffsetVector{T}, M::Integer,
                       store::Store{T}) where T <: AbstractFloat
    @argcheck rng.start ≥ 2
    rn = size(Hn[rng.start], 1)
    kn = t[n] - t[n-1]

    σ, wσ = rule(store.legendre[M])
    τ, wτ = σ, wσ
    Ψ  = view(store.Ψ,  1:store.rmax, 1:M)
    legendre_polys!(Ψ, σ)
    α = store.α
    c = - (1-α) / ( 4Γ(α) )
    for ℓbar in rng
        rℓ = size(Hn[ℓbar], 2)
        @argcheck rℓ ≤ store.rmax
        ℓ = n - ℓbar
        kℓ = t[ℓ] - t[ℓ-1]
        two_D = (t[n]+t[n-1]) - (t[ℓ]+t[ℓ-1])
        D = two_D / 2

        for j = 1:rℓ, i = 1:rn
            outer = zero(T)
            for mτ = 1:M
                inner = zero(T)
                for mσ = 1:M
                    Δ = ( τ[mτ]*kn - σ[mσ]*kℓ ) / two_D
                    inner += wσ[mσ] * (1+Δ)^(α-2) * Ψ[j,mσ]
                end
                outer += wτ[mτ] * Ψ[i,mτ] * inner
            end
            Hn[ℓbar][i,j] = c * kn * kℓ * D^(α-2) * outer
        end
    end
end


using LightGraphs
using LightGraphs.SimpleGraphs
using LinearAlgebra

function normalized_laplacian(g::SimpleGraph)
    L = float(laplacian_matrix(g))
    for i in vertices(g)
        if L[i,i] != 0
            L[i,:] /= L[i,i]
        end
    end
    return L
end

function normalized_laplacian(g::SimpleDiGraph)
    L = float(laplacian_matrix(g))
    for i in vertices(g)
        if L[i,i] != 0
            L[i,:] /= L[i,i]
        end
    end
    return L
end


@testset "linear problems" begin
    @testset "lp1_test" begin
        #=
        primal
            min -4x_2 - 1
        s.t.
            x_1 >= 3         :y_2
            x_1 + 2x_2 <= 3  :y_3
        dual
            max 3y_2 + 3y_3 - 1
        s.t.
            y_2 >= 0
            y_3 <= 0
            y_2 + y_3 == 0    :x_1
            2y_3 == -4        :x_2
        =#
        primal_model = lp1_test()
        dual_model, primal_dual_map = dual_model_and_map(primal_model)

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 2
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (VI, MOI.GreaterThan{Float64})
                (VI, MOI.LessThan{Float64})
                (SAF{Float64}, MOI.EqualTo{Float64})
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.LessThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 2
        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test MOI.coefficient.(obj.terms) == [3.0; 3.0]
    end

    @testset "lp7_test compact" begin
        #=
        primal
            min -4x1 -3x2 -1
        s.t.
            2x1 + x2 - 3 <= 0  :y_3
            x1 + 2x2 - 3 <= 0  :y_4
            x1 >= 1            :y_1 (behaves like affine since rhs!=0)
            x2 >= 0            :y_2 (does not appear in compact)
        standard dual
            max 3y_4 + 3y_3 + y_1 - 1
        s.a.
            y_1 + 2y_3 + y_4 = -4 :x_1
            y_2 + y_3 + 2y_4 = -3 :x_2
            y_1 >= 0
            y_2 >= 0
            y_3 <= 0
            y_4 <= 0
        compact dual
            max 3y_4 + 3y_3 + y_1 - 1
        s.a.
            y_1 + 2y_3 + y_4 = -4 :x_1
            - y_3 - 2y_4 >= 3 :x_2
            y_1 >= 0
            y_3 <= 0
            y_4 <= 0
        =#
        primal_model = lp7_test()
        dual_model, primal_dual_map = dual_model_and_map(primal_model)

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 3
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (VI, MOI.GreaterThan{Float64})
                (SAF{Float64}, MOI.EqualTo{Float64})
                (SAF{Float64}, MOI.GreaterThan{Float64})
                (VVF, MOI.Nonpositives)
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VVF,MOI.Nonpositives}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.GreaterThan{Float64}}(),
        ) == 1
        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([3.0; 1.0; 3.0])

        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([1.0; 2.0; 1.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(-4.0)
        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.GreaterThan{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([-1.0; -2.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
              MOI.GreaterThan(3.0)
    end

    @testset "lp7_test standard" begin
        #=
        primal
            min -4x1 -3x2 -1
        s.t.
            2x1 + x2 - 3 <= 0  :y_3 (nopos1)
            x1 + 2x2 - 3 <= 0  :y_4 (nopos1)
            x1 >= 1            :y_1 (behaves like affine since rhs!=0)
            x2 >= 0            :y_2 (does not appear in compact)
        standard dual
            max 3y_4 + 3y_3 + y_1 - 1
        s.a.
            y_1 + 2y_3 + y_4 = -4 :x_1
            y_2 + y_3 + 2y_4 = -3 :x_2
            y_1 >= 0
            y_2 >= 0
            y_3 <= 0 (nopos1)
            y_4 <= 0 (nopos1)
        compact dual
            max 3y_4 + 3y_3 + y_1 - 1
        s.a.
            y_1 + 2y_3 + y_4 = -4 :x_1
            - y_3 - 2y_4 >= 3 :x_2
            y_1 >= 0
            y_3 <= 0
            y_4 <= 0
        =#
        primal_model = lp7_test()
        dual = dualize(primal_model, consider_constrained_variables = false)
        dual_model, primal_dual_map = dual.dual_model, dual.primal_dual_map
        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 4
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (SAF{Float64}, MOI.EqualTo{Float64})
                (VI, MOI.GreaterThan{Float64})
                (VVF, MOI.Nonpositives)
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VVF,MOI.Nonpositives}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 2
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 2
        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([3.0; 1.0; 3.0])

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{VI,MOI.GreaterThan{Float64}}(),
        )
        for ci in cis
            @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
                  MOI.GreaterThan(0.0)
        end

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}(),
        )
        @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) ==
              Set([MOI.EqualTo(-4.0), MOI.EqualTo(-3.0)])
    end

    @testset "lp14_test_min" begin
        #=
            min -1 +2x1 -3x2 +4x3
        s.t.
            +5x1  -6x2  +7x3 <= 8   (y1)
            -9x1 +10x2 -11x3 >= -12 (y2)
            13x1 -14x2 +15x3 == 16  (y3)
            x1 >= 0 (y4)
            x2 <= 0 (y5)
            x3 free (y6)
        standard dual
            ...
        compact dual
            max 8y1 -12y2 +16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 +2 >= 0 (x1)
            +6y1 -10y2 +14y3 -3 <= 0 (x2)
            -7y1 +11y2 -15y3 +4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
        =#
        primal_model = lp14_test(MOI.MIN_SENSE)
        dual_model, primal_dual_map = dual_model_and_map(primal_model)

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 3
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (VI, MOI.GreaterThan{Float64})
                (VI, MOI.LessThan{Float64})
                (SAF{Float64}, MOI.EqualTo{Float64})
                (SAF{Float64}, MOI.GreaterThan{Float64})
                (SAF{Float64}, MOI.LessThan{Float64})
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.LessThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.LessThan{Float64}}(),
        ) == 1

        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([8.0, -12.0, 16])

        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.GreaterThan{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([-5.0; 9.0; -13.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
              MOI.GreaterThan(-2.0)

        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.LessThan{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([6.0; -10.0; +14.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.LessThan(3.0)

        # TODO flip these
        # ci = MOI.get(dual_model, MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}())[]
        # f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        # @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0])
        # @test MOI.constant(f) == 0.0
        # @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(-4.0)

    end
    @testset "lp14_test_max compact" begin
        #=
            max -1 +2x1 -3x2 +4x3
        s.t.
            +5x1  -6x2  +7x3 <= 8   (y1)
            -9x1 +10x2 -11x3 >= -12 (y2)
            13x1 -14x2 +15x3 == 16  (y3)
            x1 >= 0 (y4)
            x2 <= 0 (y5)
            x3 free (y6)
        standard dual
            min -8y1 +12y2 -16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 -y4 -2 == 0 (x1)
            +6y1 -10y2 +14y3 -y5 +3 == 0 (x2)
            -7y1 +11y2 -15y3 -y6 -4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
            y4 >= 0
            y5 <= 0
            y6 == 0
        compact dual
            min -8y1 +12y2 -16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 -2 >= 0 (x1)
            +6y1 -10y2 +14y3 +3 <= 0 (x2)
            -7y1 +11y2 -15y3 -4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
        =#
        primal_model = lp14_test(MOI.MAX_SENSE)
        dual_model, primal_dual_map = dual_model_and_map(primal_model)

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 3
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (VI, MOI.GreaterThan{Float64})
                (VI, MOI.LessThan{Float64})
                (SAF{Float64}, MOI.EqualTo{Float64})
                (SAF{Float64}, MOI.GreaterThan{Float64})
                (SAF{Float64}, MOI.LessThan{Float64})
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.LessThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.GreaterThan{Float64}}(),
        ) == 1
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.LessThan{Float64}}(),
        ) == 1

        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([-8.0, 12.0, -16])

        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.GreaterThan{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([-5.0; 9.0; -13.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
              MOI.GreaterThan(2.0)

        ci = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.LessThan{Float64}}(),
        )[]
        f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        @test Set(MOI.coefficient.(f.terms)) == Set([6.0; -10.0; +14.0])
        @test MOI.constant(f) == 0.0
        @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.LessThan(-3.0)

        # TODO flip these
        # ci = MOI.get(dual_model, MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}())[]
        # f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
        # @test Set(MOI.coefficient.(f.terms)) == Set([-7.0; +11.0; -15.0])
        # @test MOI.constant(f) == 0.0
        # @test MOI.get(dual_model, MOI.ConstraintSet(), ci) == MOI.EqualTo(4.0)

    end
    @testset "lp14_test_min standard" begin
        #=
            min -1 +2x1 -3x2 +4x3
        s.t.
            +5x1  -6x2  +7x3 <= 8   (y1)
            -9x1 +10x2 -11x3 >= -12 (y2)
            13x1 -14x2 +15x3 == 16  (y3)
            x1 >= 0 (y4)
            x2 <= 0 (y5)
            x3 free
        standard dual
            min 8y1 -12y2 +16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 -y4 +2 == 0 (x1)
            +6y1 -10y2 +14y3 -y5 -3 == 0 (x2)
            -7y1 +11y2 -15y3     +4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
            y4 >= 0
            y5 <= 0
        compact dual
            max 8y1 -12y2 +16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 +2 >= 0 (x1)
            +6y1 -10y2 +14y3 -3 <= 0 (x2)
            -7y1 +11y2 -15y3 +4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
        =#
        primal_model = lp14_test(MOI.MIN_SENSE)
        dual = dualize(primal_model, consider_constrained_variables = false)
        dual_model, primal_dual_map = dual.dual_model, dual.primal_dual_map

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 5
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (SAF{Float64}, MOI.EqualTo{Float64})
                (VI, MOI.GreaterThan{Float64})
                (VI, MOI.LessThan{Float64})
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 3
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 2
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.LessThan{Float64}}(),
        ) == 2

        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([8.0, -12.0, 16])

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{VI,MOI.GreaterThan{Float64}}(),
        )
        for ci in cis
            @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
                  MOI.GreaterThan(0.0)
        end
        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{VI,MOI.LessThan{Float64}}(),
        )
        for ci in cis
            @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
                  MOI.LessThan(0.0)
        end

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}(),
        )
        # TODO: review signs here
        @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) ==
              Set([MOI.EqualTo(2.0), MOI.EqualTo(-3.0), MOI.EqualTo(4.0)])
        for ci in cis
            set = MOI.get(dual_model, MOI.ConstraintSet(), ci)
            f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
            if set == MOI.EqualTo(2.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[-5.0; 9.0; -13.0; -1])
                @test MOI.constant(f) == 0.0
            elseif set == MOI.EqualTo(-3.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[6.0; -10.0; +14.0; -1])
                @test MOI.constant(f) == 0.0
            elseif set == MOI.EqualTo(4.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[-7.0; +11.0; -15.0])
                @test MOI.constant(f) == 0.0
            else
                error("wrong ci")
            end
        end
    end
    @testset "lp14_test_max standard" begin
        #=
            max -1 +2x1 -3x2 +4x3
        s.t.
            +5x1  -6x2  +7x3 <= 8   (y1)
            -9x1 +10x2 -11x3 >= -12 (y2)
            13x1 -14x2 +15x3 == 16  (y3)
            x1 >= 0 (y4)
            x2 <= 0 (y5)
            x3 free
        standard dual
            min -8y1 +12y2 -16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 -y4 -2 == 0 (x1)
            +6y1 -10y2 +14y3 -y5 +3 == 0 (x2)
            -7y1 +11y2 -15y3     -4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
            y4 >= 0
            y5 <= 0
        compact dual
            min -8y1 +12y2 -16y3 - 1
        s.a.
            -5y1  +9y2 -13y3 -2 >= 0 (x1)
            +6y1 -10y2 +14y3 +3 <= 0 (x2)
            -7y1 +11y2 -15y3 -4 == 0 (x3)
            y1 <= 0
            y2 >= 0
            y3 free
        =#
        primal_model = lp14_test(MOI.MAX_SENSE)
        dual = dualize(primal_model, consider_constrained_variables = false)
        dual_model, primal_dual_map = dual.dual_model, dual.primal_dual_map

        @test MOI.get(dual_model, MOI.NumberOfVariables()) == 5
        list_of_cons = MOI.get(dual_model, MOI.ListOfConstraintTypesPresent())
        @test Set(list_of_cons) == Set(
            [
                (SAF{Float64}, MOI.EqualTo{Float64})
                (VI, MOI.GreaterThan{Float64})
                (VI, MOI.LessThan{Float64})
            ],
        )
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{SAF{Float64},MOI.EqualTo{Float64}}(),
        ) == 3
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.GreaterThan{Float64}}(),
        ) == 2
        @test MOI.get(
            dual_model,
            MOI.NumberOfConstraints{VI,MOI.LessThan{Float64}}(),
        ) == 2

        obj_type = MOI.get(dual_model, MOI.ObjectiveFunctionType())
        @test obj_type == SAF{Float64}
        obj = MOI.get(dual_model, MOI.ObjectiveFunction{obj_type}())
        @test MOI.constant(obj) == -1.0
        @test Set(MOI.coefficient.(obj.terms)) == Set([-8.0, 12.0, -16])

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{VI,MOI.GreaterThan{Float64}}(),
        )
        for ci in cis
            @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
                  MOI.GreaterThan(0.0)
        end
        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{VI,MOI.LessThan{Float64}}(),
        )
        for ci in cis
            @test MOI.get(dual_model, MOI.ConstraintSet(), ci) ==
                  MOI.LessThan(0.0)
        end

        cis = MOI.get(
            dual_model,
            MOI.ListOfConstraintIndices{SAF{Float64},MOI.EqualTo{Float64}}(),
        )
        # TODO: review signs here
        @test Set(MOI.get.(dual_model, MOI.ConstraintSet(), cis)) ==
              Set([MOI.EqualTo(-2.0), MOI.EqualTo(3.0), MOI.EqualTo(-4.0)])
        for ci in cis
            set = MOI.get(dual_model, MOI.ConstraintSet(), ci)
            f = MOI.get(dual_model, MOI.ConstraintFunction(), ci)
            if set == MOI.EqualTo(-2.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[-5.0; 9.0; -13.0; -1])
                @test MOI.constant(f) == 0.0
            elseif set == MOI.EqualTo(3.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[6.0; -10.0; +14.0; -1])
                @test MOI.constant(f) == 0.0
            elseif set == MOI.EqualTo(-4.0)
                @test Set(MOI.coefficient.(f.terms)) ==
                      Set(-[-7.0; +11.0; -15.0])
                @test MOI.constant(f) == 0.0
            else
                error("wrong ci")
            end
        end
    end
end


import Roots.fzero

# test robustness of derivative free algorithms

## tests:http://people.sc.fsu.edu/~jburkardt/cpp_src/test_zero/test_zero.html
function newton_baffler(x)
    if ( x - 0.0 ) < -0.25
        0.75 * ( x - 0 ) - 0.3125
    elseif  ( x - 0 ) < 0.25
        2.0 * ( x - 0 )
    else
        0.75 * ( x - 0 ) + 0.3125
    end
end

mutable struct CallableFunction
f
n
end
(F::CallableFunction)(x) = (F.n+=1; F.f(x))

pathological = [
                (x ->sin( x ) - x / 2, .1),
                (x-> 2 * x - exp( - x ), 1),
                (x -> x * exp( - x ), 1),
                (x -> exp( x ) - 1 / ( 10 * x )^2, 1),
                (x -> ( x + 3 ) * ( x - 1 )^2, 1),

                (x -> exp( x ) - 2 - 1 / ( 10 * x )^2 + 2 / ( 100 * x )^3, 1),
                (x -> cbrt(x), 1),
                (x -> cos( x ) - x, 1),
                (x-> newton_baffler(x), 8),
                (x -> 20.0 * x / ( 100.0 * x^2 + 1.0), 0.095),

                (x ->  ( 4.0 + x^2) * ( 2.0 + x ) * ( 2.0 - x )  / ( 16.0 * x * x * x * x + 0.00001 ), 1),
                (x -> (x == 1.0) ? float(0) : sign(x-1.0) * exp(log(1e4) + log(abs(x - 1.0)) - 1.0/(x-1.0)^2), 1),
                (x -> 0.00000000001 * (x - 100.0), 1),
                (x -> 1.0 / ( ( x - 0.3 ) * ( x - 0.3 ) + 0.01 ) + 1.0 / ( ( x - 0.9 ) * ( x - 0.9 ) + 0.04 ) + 2.0 * x - 5.2, -1),
                (x -> ( 1 - 6x^2) * cbrt(x) * exp(-x^2) / (3*x), -0.25),

                (x -> ( pi * ( x - 5.0 ) / 180.0 ) - 0.8 * sin( pi * x / 180.0 ), 1),
                (x -> x^3 - 2*x - 5, 2),
                (x -> 1e6 * (x^7 -7x^6 +21x^5 -35x^4 +35x^3-21x^2+7x-1),  0.990),
                (x -> cos(100*x)-4*erf(30*x-10), 0.0)
                ]

function howgood(f1, xstar)
    if isnan(xstar)
        "x"
    elseif iszero(f1(xstar))       # perfect
        "."
    elseif f1(prevfloat(xstar)) * f1(nextfloat(xstar)) < 0 # perfect
        "."
    elseif abs(f1(xstar)) < 8 * eps(xstar) # nearly perfect
        "."
    elseif abs(f1(xstar)) <= 10eps(xstar)^(1/3)
        "~"
    elseif abs(xstar) > 5   # ran away yet converged
        "!"
    else                    # not close?
        "*"
    end
end

## make a graphic comparing values
function run_robustness_test(bigfloats=false)
    orders = [Order0(),Order1(),Order2(),Order5(),Order8(),Order16()]
    N = length(orders) + 1
    k,n = length(pathological), 50
    m = ["" for i=1:(N + 2)*k, j=1:n]
    ## how to mark values. If error we also use "x"

    marks = ["HP", "0 ", "1 ", "2 ", "5 ", "8 ", "16"]
    io = IOBuffer()

    for i in 1:k

        println("")
        println("-- $i ---")
        println("")

        f1,x = pathological[i]
        if bigfloats
            xs = -big(5):10/50:5
        else
            xs = linspace(-5, 5, n)
        end

        calls = Int[0]
        hits = 0
        j = 1
        print(io, marks[j], " ")
        for k in 1:n
            F = CallableFunction(f1, 0)
            xstar = Roots.dfree(F, xs[k])
            push!(calls, F.n)
            val = howgood(f1, xstar)
            val in [".", "~"] && (hits += 1)
            print(io, val)
        end
        print(String(take!(io)))
        print(" h=$hits n = $(sum(calls)) max=$(maximum(calls))")
        println("")

        for j in 2:N
            calls = Int[0]
            hits = 0
            print(io, marks[j], " ")
            for k in 1:n
                M = orders[j-1]
                F = CallableFunction(f1, 0)
                state = Roots.init_state(M, F, xs[k])
                options = Roots.init_options(M, state)
                tracks = Roots.NullTracks()
                xstar = find_zero(M, F, options, state, tracks)
                push!(calls, F.n)
                val = howgood(f1, xstar)
                val in [".", "~"] && (hits += 1)
                print(io, val)
            end
            print(String(take!(io)))
            print(" h=$hits n = $(sum(calls)) max=$(maximum(calls))")
            println("")
        end
    end
end


@testset ExtendedTestSet "checkerboard" begin
    @testset ExtendedTestSet "checkerboard" begin
        answer = [
            0.0 0.0 0.0 0.0 0.0
            0.0 0.3454915028125263 0.5590169943749475 0.5590169943749475 0.3454915028125263
            0.0 0.5590169943749475 0.9045084971874736 0.9045084971874736 0.5590169943749475
            0.0 0.5590169943749475 0.9045084971874736 0.9045084971874736 0.5590169943749475
            0.0 0.3454915028125263 0.5590169943749475 0.5590169943749475 0.3454915028125263
        ]
        test = checkerboard((5, 5))
        @test test ≈ answer
    end
end

@testset ExtendedTestSet "disk" begin
    @testset ExtendedTestSet "disk" begin
        answer = [
            0.2928932188134524 0.5 0.2928932188134524 -0.1180339887498949 -0.5811388300841898
            0.5 1.0 0.5 0.0 -0.5
            0.2928932188134524 0.5 0.2928932188134524 -0.1180339887498949 -0.5811388300841898
            -0.1180339887498949 0.0 -0.1180339887498949 -0.41421356237309515 -0.8027756377319946
            -0.5811388300841898 -0.5 -0.5811388300841898 -0.8027756377319946 -1.1213203435596424
        ]
        test = disk((5, 5); factor=1)
        @test test ≈ answer
    end

    @testset ExtendedTestSet "disk" begin
        answer = [
            -0.41421356237309515 0.0 -0.41421356237309515 -1.2360679774997898 -2.1622776601683795
            0.0 1.0 0.0 -1.0 -2.0
            -0.41421356237309515 0.0 -0.41421356237309515 -1.2360679774997898 -2.1622776601683795
            -1.2360679774997898 -1.0 -1.2360679774997898 -1.8284271247461903 -2.605551275463989
            -2.1622776601683795 -2.0 -2.1622776601683795 -2.605551275463989 -3.2426406871192848
        ]
        test = disk((5, 5); factor=2)
        @test test ≈ answer
    end
end


"""
The syntax for a while is

    while *condition*
        *loop body*
    end
"""

n = 1
while n <= 10
    n += n
    println(n)
end

# My only friends are numbers 😢
friends = [1, 2, 3, 4, 5, 6]

i = 1 # This language better have an iterator/higher-order functions STG
while i <= length(friends)
    friend = friends[i]
    println("Hi $friend, it's nice to see that you're still hanging around these parts.")
    i += 1
end

# Praise
for friend in friends
    println(friend)
end

# Julia has "ranges"
example_range = 1:100 # UnitRange{Int64}
another_range = 'a':'z' # StepRange{Char, Int64}

for letter in 'a':'z'
    print(letter)
end
println()

# Julia also has unicode overloading
for letter = 'a':'z'
    print(letter)
end
println()

# That's a Unicode U+2208 "Element of" symbol
for letter ∈ 'a':'z'
    print(letter)
end
println()

# Make a 5x5 addition table
m, n = 5, 5
table = zeros(m, n)
println(typeof(table)) # Matrix{Float64}, NOT a 2D array
for i in 1:m
    for j in 1:n
        table[i, j] = i + j
    end
end
table

# Same thing but condensed
table = zeros(m, n)
for i in 1:m, j in 1:n
    table[i, j] = i+j
end
table

# More exploration of condensing loops.
combinations_2 = []
for f1 in friends, f2 in friends
    if f1 != f2
        push!(combinations_2, (f1, f2))
    end
end

combinations_3 = []
#for f1 in friends, f2 = friends, f3 ∈ friends
for f1 = friends, f2 = friends, f3 = friends
    if f1 != f2 && f2 != f3 && f1 != f3
        push!(combinations_3, (f1, f2, f3))
    end
end
combinations_3

# COMPREHENSIONS!!!
table = [i+j for i in 1:m, j in 1:n]

for n in 1:10
    powtable = [i^j for i = 1:n, j in 1:n]
    display(powtable)
end